{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "kernelspec": { "name": "ir", "display_name": "R", "language": "R" }, "language_info": { "name": "R" }, "colab": { "provenance": [], "toc_visible": true } }, "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Session 2 · Soil data preparation with the KSSL dataset (part 1)\n", "**Module 1 · Introduction to R, spatial data and soil data preparation** · SoilFER Training\n", "\n", "[Course page](https://training.yigini.net/modules/01-r-soil-data/) · [Manual chapter](https://training.yigini.net/manual/introduction-to-soil-data-preparation-spatial-data.html) · [Original script](https://github.com/SoilFER/SoilFER-Training-Resources/blob/main/02_scripts/module1/Session2_Soil_Data_Preparation_Part1.R)\n", "\n", "**Before you start**\n", "1. Check that the runtime is **R**: *Runtime → Change runtime type → R* (this notebook should open in R automatically).\n", "2. Run the **Setup** cell below once per session (≈1–3 minutes). It downloads the training project, installs the R packages and downloads the course rasters and MIR data (≈1.2 GB) and sets the working folder.\n", "3. Then run the cells in order with **Shift + Enter**.\n", "\n", "> Colab resets when you close it or after ~90 minutes without activity. Save results you want to keep with *Files → Download* (left sidebar), or re-run the Setup cell after a reset.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# ==== SoilFER · Colab setup (run first, once per session) ==================\n", "options(repos = c(CRAN = \"https://cloud.r-project.org\"), timeout = 3600)\n", "# Works in Google Colab and in any other Jupyter (JupyterHub, JupyterLab on your computer)\n", "on_colab <- nzchar(Sys.getenv(\"COLAB_RELEASE_TAG\")) || dir.exists(\"/content/sample_data\")\n", "root <- if (on_colab) \"/content/SoilFER-Training-Resources\" else path.expand(\"~/SoilFER-Training-Resources\")\n", "Sys.setenv(SOILFER_ROOT = root)\n", "\n", "# 1. Training project (scripts, small data, outputs, assignments)\n", "if (!dir.exists(root))\n", " system(paste(\"git clone --depth 1 https://github.com/SoilFER/SoilFER-Training-Resources\", root))\n", "\n", "# 2. R packages (in Colab the R runtime installs ready-made binaries, so this is fast)\n", "pkgs <- c(\"dplyr\", \"knitr\", \"readxl\", \"sf\", \"terra\", \"tidyverse\", \"writexl\")\n", "need <- setdiff(pkgs, rownames(installed.packages()))\n", "if (length(need)) install.packages(need)\n", "\n", "# 3. Course rasters + MIR spectra (Google Drive folder of the SoilFER training, ≈1.2 GB)\n", "td <- file.path(root, \"01_data/module1/training_data\")\n", "if (!file.exists(file.path(td, \"MIR_KANSAS_data.xlsx\"))) {\n", " system(\"python3 -m pip -q install gdown\")\n", " drv <- file.path(dirname(root), \"soilfer_drive\")\n", " system(paste(\"python3 -m gdown --folder --quiet https://drive.google.com/drive/folders/1K7tq9zX5HsqbqWcNoT27WtfPtehcKBCu -O\", shQuote(drv)))\n", " f <- list.files(drv, recursive = TRUE, full.names = TRUE)\n", " file.copy(f, td, overwrite = FALSE)\n", "}\n", "\n", "setwd(root)\n", "cat(\"Ready. Working folder:\", getwd(), \"\\n\")\n", "missing <- setdiff(pkgs, rownames(installed.packages()))\n", "if (length(missing)) message(\"Not installed: \", paste(missing, collapse = \", \"))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### SoilFER Online Training Programme — Module 1" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "###############################################################################\n", "# SoilFER Online Training Programme — Module 1\n", "# SESSION 2: Data Preparation with the KSSL Dataset — Part 1 (1.5 hours)\n", "# Section: Working with Soil Data, Loading, Site data, Coordinates, Depths\n", "###############################################################################\n", "#\n", "# LEARNING OBJECTIVES\n", "# -------------------\n", "# By the end of this session, participants will be able to:\n", "# 1. Set the system and organize project files\n", "# 2. Load R packages for soil data analysis\n", "# 3. Import soil data from CSV and Excel files\n", "# 4. Explore and understand the structure of a real soil database\n", "# 5. Format sampling dates for data standardization\n", "# 6. Add unique row identifiers for data traceability\n", "# 7. Create unique profile identifiers from coordinates and sampling date\n", "# 8. Extract, standardize, and rename site columns\n", "# 9. Validate and filter geographic coordinates\n", "# 10. Validate and correct soil depth boundaries\n", "#\n", "# DATASET\n", "# -------\n", "# KSSL (Kellogg Soil Survey Laboratory) Kansas dataset\n", "# - File: 01_data/module1/kssl/KSSL_data.xlsx (or KSSL_data.csv)\n", "# - Soil horizon measurements from multiple soil profiles\n", "# - Columns: site info (location, depth), soil properties, spectral IDs\n", "#\n", "# TIMING GUIDE (approximate)\n", "# ---------------------------\n", "# 0:00 – 0:20 Working directory, packages, importing data\n", "# 0:20 – 0:45 Exploring the dataset; date; rowID and ProfID\n", "# 0:45 – 1:05 Site extraction and coordinate validation\n", "# 1:05 – 1:30 Depth validation (missing, negative, zero-thickness,\n", "# invalid logic, profiles without surface horizon)\n", "###############################################################################\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### PART 1 — SETTING UP THE ENVIRONMENT" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "# PART 1 — SETTING UP THE ENVIRONMENT\n", "# =============================================================================\n", "\n", "# -----------------------------------------------------------------------------\n", "# 1.1 Setting the Working Directory\n", "# -----------------------------------------------------------------------------\n", "# The working directory is where R looks for data files and saves outputs.\n", "# Use relative paths (e.g., \"01_data/module1/kssl/KSSL_data.csv\") whenever possible.\n", "\n", "# Check current working directory\n", "# getwd()\n", "\n", "# Set new working directory\n", "# setwd(\"C:/Users/YourName/Documents/YourSoilProject\")\n", "\n", "# On Mac/Linux\n", "# setwd(\"/Users/YourName/Documents/YourSoilProject\")\n", "\n", "# Set working directory to script location\n", "# setwd(file.path(Sys.getenv(\"SOILFER_ROOT\"), \"02_scripts/module1\")) # folder of this script (was rstudioapi::getActiveDocumentContext, RStudio only)\n", "\n", "# -----------------------------------------------------------------------------\n", "# 1.2 Load Packages\n", "# -----------------------------------------------------------------------------\n", "\n", "# library(readxl) # Read Excel files\n", "# library(tidyverse) # Data manipulation and visualization\n", "\n", "# -----------------------------------------------------------------------------\n", "# 1.3 Importing Data from Files\n", "# -----------------------------------------------------------------------------\n", "\n", "# Read a CSV file (generic syntax)\n", "# soil_data <- read.csv(\"path/to/soil_data.csv\")\n", "\n", "# If CSV uses different separator (semicolon, tab) (generic syntax)\n", "# soil_data <- read.csv(\"path/to/file.csv\", sep = \";\")\n", "# soil_data <- read.delim(\"path/to/file.txt\", sep = \"\\t\")\n", "\n", "# Read a CSV file using the `readr::read_csv` in tidyverse (generic syntax)\n", "# soil_data <- read_csv(\"path/to/file.txt\")\n", "\n", "# Read the KSSL data from a CSV file\n", "# soil_data <- read_csv(\"01_data/module1/kssl/KSSL_data.csv\")\n", "\n", "# View structure\n", "# str(soil_data)\n", "\n", "# View first rows\n", "# head(soil_data)\n", "\n", "# Read Excel file\n", "# soil_data <- read_excel(\"01_data/module1/kssl/KSSL_data.xlsx\", sheet = 1)\n", "\n", "# Or read a specific sheet by name\n", "# soil_data <- read_excel(\"01_data/module1/kssl/KSSL_data.xlsx\", sheet = \"SoilData\")\n", "\n", "# -----------------------------------------------------------------------------\n", "# 1.4 Exploring the Imported Data\n", "# -----------------------------------------------------------------------------\n", "\n", "# str(soil_data) # Structure + column types\n", "# summary(soil_data) # Quick summaries for each column\n", "# names(soil_data) # Column names\n", "#\n", "# head(soil_data) # First rows\n", "# tail(soil_data) # Last rows\n", "#\n", "# View(soil_data) # Opens a spreadsheet-style viewer in RStudio\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### PART 2 — LOADING THE KSSL DATASET" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "# PART 2 — LOADING THE KSSL DATASET\n", "# The KSSL dataset is the primary dataset used throughout the sessions.\n", "# All code below assumes the working directory is set to the project root.\n", "# Before cleaning, always understand what you're working with.\n", "#\n", "# WHAT TO DO:\n", "# - Define the file path to the actual data location\n", "# - Check column names and data types\n", "# - Inspect the first few rows\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.1 Basic Data Loading and Structure" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "\n", "# -----------------------------------------------------------------------------\n", "# 2.1 Basic Data Loading and Structure\n", "# -----------------------------------------------------------------------------\n", "\n", "# Load libraries\n", "library(tidyverse) # Data manipulation and visualization\n", "library(readxl) # Read Excel files\n", "library(writexl) # Write Excel files\n", "library(knitr) # For formatted tables\n", "library(sf) # Manipulation of Vector Data\n", "library(terra) # Manipulation of Raster Data\n", "\n", "# Define the folder to store the results of the exercise\n", "output_dir <- \"03_outputs/module1/\"\n", "\n", "# Create the output directory if not existing\n", "if (!file.exists(output_dir)) {\n", " dir.create(output_dir, recursive = TRUE)\n", "}\n", "\n", "# Read Excel file containing raw soil data\n", "raw_data <- read_excel(\"01_data/module1/kssl/KSSL_data.xlsx\", sheet = 1)\n", "\n", "str(raw_data) # Examine the structure of the data\n", "head(raw_data, 10) # Show the first 10 rows\n", "summary(raw_data) # Summarize the data\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### PART 3 — PREPARING SITE DATA" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "# PART 3 — PREPARING SITE DATA\n", "# - Start understanding how many valid points you have in the dataset\n", "# - Standardized column names make cleaning easier and less error-prone\n", "#\n", "# The raw data includes information of:\n", "# - Position and depth (SITE)\n", "# - Analytical values of soil parameters (LAB)\n", "# - The Dry Chemistry Data (SPECTRAL) has been stored in a separate file due to its size\n", "#\n", "# WHAT TO DO:\n", "# - Format the sampling date\n", "# - Add identifiers once, before filtering or restructuring the data\n", "# - Extract only the columns needed for site identification\n", "# - Rename to standard names: lon, lat, top, bottom, etc.\n", "# - Preserve repeated horizon records until laboratory data are validated\n", "#\n", "# We define a soil profile from the combination of geographic location and\n", "# sampling date. Horizons sharing the same longitude, latitude, and date are\n", "# initially assigned the same ProfID.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3.1 Format Sampling Date to ISO 8601 (YYYY-MM-DD)" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "\n", "# -----------------------------------------------------------------------------\n", "# 3.1 Format Sampling Date to ISO 8601 (YYYY-MM-DD)\n", "# -----------------------------------------------------------------------------\n", "# Sampling dates are required for soil-data standardization.\n", "# In this dataset, only the fiscal year is available. January 1 is therefore\n", "# used as an approximate reference date for the corresponding year.\n", "#\n", "# IMPORTANT:\n", "# - Use the actual sampling date whenever it is available.\n", "# - If a valid `date` column already exists, this step should be skipped.\n", "# -----------------------------------------------------------------------------\n", "\n", "raw_data <- raw_data %>%\n", " mutate(date = as.Date(paste0(fiscal_year, \"-01-01\")), .before = 4)\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 3.2 Adding Unique Row and Profile Identifiers\n", "# -----------------------------------------------------------------------------\n", "# Add rowID BEFORE filtering or changing the number/order of records.\n", "# WHY: rowID preserves the link between every processed record and its source.\n", "#\n", "# Add ProfID once at this stage and preserve it through the workflow.\n", "# WHY: The same coordinates can represent observations from different dates.\n", "# HOW: Group by longitude + latitude + sampling date, assign a sequential ID,\n", "# and format it as \"PROF0001\".\n", "# -----------------------------------------------------------------------------\n", "\n", "raw_data <- raw_data %>%\n", " # Assign a unique sequential identifier to each original record\n", " mutate(rowID = row_number(), .before = 1) %>%\n", " # Group all horizons belonging to the same initial profile\n", " group_by(Long_Site, Lat_Site, date) %>%\n", " # Assign a sequential identifier to each unique profile\n", " mutate(ProfID = cur_group_id(), .before = 2) %>%\n", " ungroup() %>%\n", " # Format as standardized IDs: PROF0001, PROF0002, etc.\n", " mutate(ProfID = sprintf(\"PROF%04d\", ProfID))\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 3.3 Extracting and Standardizing Column Names for Sites\n", "# -----------------------------------------------------------------------------\n", "# The `site` object stores the location, date, depth, and identifiers of the\n", "# soil observations. It becomes the authoritative object for structural\n", "# cleaning of profiles and horizons.\n", "#\n", "# Select relevant columns and rename to a consistent naming convention.\n", "# -----------------------------------------------------------------------------\n", "\n", "site <- raw_data %>%\n", " select(\n", " rowID,\n", " ProfID,\n", " date,\n", " Long_Site, # Raw column name for longitude\n", " Lat_Site, # Raw column name for latitude\n", " smp_id, # Sample/horizon identifier\n", " Top_depth_cm, # Top depth in centimeters\n", " Bottom_depth_cm # Bottom depth in centimeters\n", " )\n", "\n", "# Rename columns to standard, consistent names\n", "site <- site %>%\n", " rename(\n", " lon = Long_Site,\n", " lat = Lat_Site,\n", " HorID = smp_id,\n", " top = Top_depth_cm,\n", " bottom = Bottom_depth_cm\n", " )\n", "\n", "# Reorder columns for clarity\n", "site <- site %>%\n", " select(rowID, ProfID, HorID, date, lon, lat, top, bottom)\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 3.4 Retain Repeated Horizon Records Until Laboratory Validation\n", "# -----------------------------------------------------------------------------\n", "# Multiple records can have the same location and depth because they may\n", "# represent repeated analytical or spectroscopic measurements.\n", "#\n", "# OLD APPROACH:\n", "# Exact site duplicates were removed here.\n", "#\n", "# NEW APPROACH:\n", "# Keep them at this stage. Some repeated rows contain complementary analytical\n", "# information. Duplicate horizons are assessed and resolved only after the\n", "# laboratory variables have been attached and validated in Session 3.\n", "# -----------------------------------------------------------------------------\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### PART 4 — COORDINATE VALIDATION AND CORRECTION" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "# PART 4 — COORDINATE VALIDATION AND CORRECTION\n", "# WHY: Geographic coordinates define location. Bad coordinates = bad maps\n", "#\n", "# VALIDATION CHECKS:\n", "# 1. Check for missing coordinates (NA values)\n", "# 2. Check for out-of-bounds values\n", "# - Valid longitude: -180 to +180 degrees\n", "# - Valid latitude: -90 to +90 degrees\n", "#\n", "# COMMON ERRORS:\n", "# - Missing coordinates (NA)\n", "# - Swapped lon/lat\n", "# - lon/lat too large (e.g., 45000º instead of 45º)\n", "# - Projected coordinates instead of geographic\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4.1 Check 1: Missing Coordinates" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "\n", "# -----------------------------------------------------------------------------\n", "# 4.1 Check 1: Missing Coordinates\n", "# -----------------------------------------------------------------------------\n", "\n", "# Remove records with missing coordinates\n", "site <- site %>%\n", " dplyr::filter(!is.na(lon) & !is.na(lat))\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 4.2 Check 2: Valid Coordinate Ranges\n", "# -----------------------------------------------------------------------------\n", "# For geographic coordinates (decimal degrees):\n", "# Longitude: -180° to +180°\n", "# Latitude: -90° to +90°\n", "\n", "# Keep only rows with valid lon/lat geographic coordinates inside valid ranges\n", "site <- site %>%\n", " dplyr::filter(\n", " lon >= -180, lon <= 180,\n", " lat >= -90, lat <= 90\n", " )\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### PART 5 — SOIL DEPTH VALIDATION AND CORRECTION" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "# PART 5 — SOIL DEPTH VALIDATION AND CORRECTION\n", "# =============================================================================\n", "# Depth intervals define the soil layer each observation represents.\n", "#\n", "# VALIDATION CHECKS:\n", "# Five sequential quality-control checks are applied:\n", "# Check 1: Missing depth boundaries\n", "# Check 2: Negative depth values\n", "# Check 3: Zero-thickness intervals\n", "# Check 4: Invalid depth logic (bottom <= top)\n", "# Check 5: Profiles without a surface horizon (top > 0)\n", "#\n", "# NOTE:\n", "# Do NOT resolve repeated horizons or competing depth sequences at this stage.\n", "# These are resolved after laboratory data have been attached and validated\n", "# in Session 3.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5.1 Check 1: Missing Depth Boundaries" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "\n", "# -----------------------------------------------------------------------------\n", "# 5.1 Check 1: Missing Depth Boundaries\n", "# -----------------------------------------------------------------------------\n", "\n", "# Keep records where both `top` and `bottom` are available\n", "site <- site %>%\n", " dplyr::filter(!is.na(top) & !is.na(bottom))\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 5.2 Check 2: Negative Depth Values\n", "# -----------------------------------------------------------------------------\n", "\n", "# Keep records with non-negative top and bottom depths\n", "site <- site %>%\n", " filter(!(top < 0 | bottom < 0))\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 5.3 Check 3: Zero-Thickness Intervals\n", "# -----------------------------------------------------------------------------\n", "\n", "# Remove zero-thickness horizons\n", "site <- site %>%\n", " filter(!(bottom - top == 0))\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 5.4 Check 4: Invalid Depth Logic\n", "# -----------------------------------------------------------------------------\n", "\n", "# Keep only horizons where bottom depth is greater than top depth\n", "site <- site %>%\n", " filter(bottom > top)\n", "\n", "\n", "# -----------------------------------------------------------------------------\n", "# 5.5 Check 5: Profiles Without a Surface Horizon (top > 0)\n", "# WHY: Each profile should represent the soil column starting at the surface.\n", "# DSM applications require profiles that start at top = 0 cm.\n", "# -----------------------------------------------------------------------------\n", "\n", "# Keep only initial profiles that contain a surface horizon\n", "site <- site %>%\n", " dplyr::group_by(ProfID) %>%\n", " dplyr::filter(!is.na(top) & min(top, na.rm = TRUE) == 0) %>%\n", " dplyr::ungroup()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### PART 6 — SAVE SITE TIBBLE TO DISK" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "# PART 6 — SAVE SITE TIBBLE TO DISK\n", "# =============================================================================\n", "# Save the structurally cleaned site results to 'site_KSSL.csv'.\n", "#\n", "# IMPORTANT:\n", "# Repeated horizon records are intentionally retained here. The `site` object\n", "# will be combined with laboratory measurements using rowID in Session 3.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### output <- paste0(output_dir, \"site_KSSL.csv\")" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# =============================================================================\n", "\n", "output <- paste0(output_dir, \"site_KSSL.csv\")\n", "write.csv(site, output, row.names = FALSE)\n", "\n", "\n", "###############################################################################\n", "# END OF SESSION 2\n", "# Summary of what was accomplished:\n", "# - KSSL data loaded and explored\n", "# - Row IDs added for traceability\n", "# - Site columns extracted, renamed, and standardized\n", "# - Profile IDs created from geographic coordinates\n", "# - Exact duplicate records removed\n", "# - Coordinates validated: missing and out-of-range records removed\n", "# - Depth boundaries validated: 5 checks applied sequentially\n", "#\n", "# The 'site' object is now ready for integration with lab data in Session 3.\n", "#\n", "# Next session: Session 3 — Soil Data Preparation with KSSL Dataset (Part 2)\n", "###############################################################################\n" ] } ] }