# Install packages if not already installed
install.packages("terra")
install.packages("ggplot2") # For visualization
install.packages("tidyterra") # For ggplot integration
install.packages("geodata") # For obtaining climate and vector dataGeospatial Analysis of Climate Data in R
Session 1 - Introduction to Geospatial Data in R
In this session, we will introduce spatial data analysis in R using the terra package. The goal is to understand how to handle geospatial data, extract climate information, and apply basic spatial operations.
We will use WorldClim climatology data for South Africa and explore land cover data from GLAD to mask non-crop areas. WorldClim version 2.1 provides historical climate data for 1970-2000.
Learning objectives
By the end of this session, you will:
Understand what raster data is and how it is structured.
Learn how to load and visualize raster datasets in R.
Perform basic raster operations: cropping, masking, and resampling.
Extract climate data for specific locations.
Use land cover data to focus on agricultural areas.
Understand the importance of coordinate reference systems (CRS) when working with spatial data.
Learn how to compute and compare global means for cropland vs. non-cropland areas.
Save processed raster and vector data for future use.
Raster and vector data in geospatial analysis
In geospatial analysis, we use two main types of data to represent information about the Earth’s surface: raster data and vector data. These formats help us store and work with different kinds of spatial information using GIS (Geographic Information Systems).
Understanding the difference between raster and vector data is essential for working with spatial data. Each format is suited to different types of analysis, and many geospatial projects use both together — for example, overlaying field boundaries (vector) on satellite images (raster) to monitor crop health.
Raster data
Raster data is made up of a grid of cells (also called pixels), much like a digital photo. Each cell holds a value that represents something about that location — such as temperature, elevation, or land cover type.
Think of a raster like a map made of tiny squares, where each square holds a number.
Raster data is great for representing continuous variables, such as rainfall, vegetation indices (e.g. NDVI), or satellite images.
The resolution of a raster tells you how big each cell is on the ground — for example, 30 metres by 30 metres.
The extent of a raster tells you the maximum dimensions (latitude and longitude) of the image.
The coordinate reference system tells you how the location descriptions (longitude, latitude) relate to the exact position on the Earth’s surface.
Examples of raster data:
Satellite imagery (e.g. from Sentinel-2, Landsat)
Climate data (e.g. WorldClim temperature or precipitation)
Elevation models (e.g. SRTM)
Vector data
Vector data represents the world using points, lines, and polygons.
A point can represent a specific location (e.g. a weather station).
A line can represent features like roads, rivers, or trails.
A polygon represents an area, such as a farm field, forest patch, or administrative boundary.
Vector data is best for representing discrete features that have clear boundaries.
Examples of vector data:
Locations of cities or sampling sites (points)
River networks or roads (lines)
District boundaries or land parcels (polygons)
| Feature | Raster Data | Vector Data |
|---|---|---|
| Structure | Grid of cells (pixels) | Points, lines, and polygons |
| Best for | Continuous data (e.g. elevation) | Discrete features (e.g. roads) |
| File examples | .tif, .img, .nc |
.shp, .geojson, .kml |
| Resolution | Cell size in metres or degrees | Depends on geometric precision |
Overview of spatial data packages in R
R has multiple packages for working with spatial data. Some of the most commonly used ones include:
terra: Optimized for raster data processing, replacingraster.sf: Designed for working with vector data, replacingsp.geodata: Provides easy access to climate, land cover, and administrative boundary data.tidyterra: Extendsggplot2to allow better visualization of raster data.
The terra package is particularly useful for handling large raster datasets efficiently. It provides faster processing and better memory management compared to older packages like raster.
Installing and loading required packages
# Load libraries
library(terra)
library(ggplot2)
library(tidyterra)
library(geodata)Downloading and Loading WorldClim Data
WorldClim provides climate data at different spatial resolutions. The functions worldclim_global() and worldclim_country() download monthly data, so we need to compute the annual mean using the app() function.
Keep the monthly data as we’ll look at those in the next session.
Initial download
# Set a directory to work in and store data
my_dir <- "~/Downloads" # change this for your own machine
setwd(my_dir)
# Obtain Worldclim monthly average temperature data globally at low resolution
temp <- worldclim_global(var = "tavg", res = 10, path = my_dir)
# Obtain WorldClim monthly average temperature for South Africa at high resolution
temp_mon_sa <- worldclim_country(country = "ZAF", var = "tavg", res = 0.5, path = my_dir)
# Obtain WorldClim monthly precipitation data for South Africa
# The code below is incomplete...
prec_mon_sa <- worldclim_country(...)
# Compute annual mean from monthly data
temp_sa <- app(temp_mon_sa, mean, na.rm = TRUE)
prec_sa <- # complete this code
# Get basic summary information on the data
temp
temp_sa
prec_saQuestion: Inspect the summary information. What does it tell you about the datasets?
Loading downloaded data
You don’t have to download the data each time you need it. Some of the datasets are very large!
If you run the worldclim_country() functions again, R will look in path first for the data, and use the downloaded version if it already exists there.
Later we’ll see how to save new datasets you have created during your work.
When working with spatial data from different sources, ensure that all layers have the same Coordinate Reference Systems (CRS). A CRS is a description of the spatial coordinates making up the raster or vector data, e.g. the units of distance (km, degrees) and the origin of the coordinates, i.e. where on Earth the (0,0) point is. R will often warn you if the CRS of two datasets don’t match.
If necessary, re-project the data to match CRS across datasests using:
# Check if the CRS for two objects are identical
crs(temp_sa) == crs(prec_sa)
# If not, project raster to match another raster or vector layer
temp_sa <- project(temp_sa, prec_sa)Obtaining South Africa boundary data
GADM provides Global ADMinistrative boundaries. We will use geodata::gadm() to obtain the administrative boundary of South Africa.
# Download South Africa's administrative boundaries
south_africa <- gadm(country = "ZAF", level = 0, path = my_dir, resolution = 2)Using land cover data to mask non-crop areas
There are many sources of gridded land cover and associated data (e.g. forests, agriculture, soils, elevation, population density etc.). We will use geodata::cropland() to obtain the GLAD cropland layer, and
# Obtain GLAD cropland layer (WARNING: large dataset)
# NB: Currently there is an error in the 2019 data
crops <- cropland(source = "GLAD", year = 2015, path = my_dir)
# Crop cropland layer to climate data
crops_sa <- crop(crops, temp_sa)
# Inspect summary information
crops_saVisualizing raster data using terra
The terra package has very good plotting functions, similar to those you’d find in a GIS software package.
# Basic plot of temperature data
plot(temp_sa, main="Mean Annual Temperature (°C)")
plot(south_africa, add=TRUE)Question: How would you change the colour scale on this map? How would you add a scale bar?
Masking non-crop areas
Let’s say we wanted to analyse only the climate in crop-growing areas. To do these, we’ll need to ‘mask’ the non-crop areas, meaning transform the values in those pixels to Not Available (NA).
# Define crop areas (assume > 0.05 coverage is agriculture)
crops_mask <- crops_sa > 0.05
temp_crops <- mask(temp_sa, crops_mask, maskvalues = c(NA, FALSE))
prec_crop <- mask(prec_sa, crop_mask)Extracting climate data for specific locations
If we’re interested in the climate for a specific location (point), set of locations (points), or region (polygon), we can obtain these using extract().
# Create example points (longitude, latitude)
points <- data.frame(lon = c(18.42, 30.98), lat = c(-33.92, -26.20))
# Convert to spatial points
points_sf <- vect(points, geom = c("lon", "lat"), crs = crs(temp_sa))
# Extract temperature values
extracted_temp <- extract(temp_sa, points_sf)
print(extracted_temp)Question: How would you plot the locations of those points on your map of temperature?
Computing and comparing mean temperatures
# Compute mean temperature for cropland and non-cropland areas
mean_temp_all <- global(temp_sa, fun = "mean", na.rm = TRUE)
mean_temp_cropland <- global(temp_crops, fun = "mean", na.rm = TRUE)
# Print results
print(paste("Mean temperature (all areas):", mean_temp_all))
print(paste("Mean temperature (cropland only):", mean_temp_cropland))Saving geospatial data
Saving raster data
Rasters can be saved in a range of different formats. GeoTIFF (.tif) is commonly used.
# Save processed rasters as GeoTIFF
writeRaster(temp_crops, "tavg_sa_cropland.tif", filetype="GTiff", overwrite=TRUE)Saving Vector Data
Shapefiles (.shp) save vectors in a set of linked files. When these are saved, keep them together in the same folder.
# Save vector data as shapefile
writeVector(south_africa, "south_africa_boundary.shp", overwrite=TRUE)Summary and next steps
In this session, we covered:
How to load and visualize raster climate data in R.
How to obtain South African climate and boundary data.
How to crop and mask data to focus on agricultural areas.
How to use land cover data to isolate agricultural areas.
How to extract climate data for specific locations.
The importance of coordinate reference systems (CRS).
How to compute mean temperatures for cropland and non-cropland areas.
How to save raster and vector data in various formats.
Session 2 - Analysing Monthly Climate Data
In this session, we will explore monthly climate data and compute Growing Degree Days (GDD), a key metric for agricultural applications. We will:
Work with WorldClim monthly climate data.
Extract seasonal temperature patterns.
Compute GDD for maize, using a base temperature of 10°C.
Use a defined extent to crop the dataset to a maize-growing region in northwest South Africa.
Compare WorldClim with CHELSA, ensuring both datasets are cropped to the same extent.
Loading monthly climate data
Both WorldClim and CHELSA provide data at 30 arc-second resolution (~1 km). We will manually download CHELSA data to match WorldClim.
The CHELSA data can be obtained from the CHELSA downloads site by clicking on Version 2.1 Download > GLOBAL > climatologies > 1981-2010 > tas and downloading each file, e.g. CHELSA_tas_01_1981-2010_V.2.1.tif. You can also use wget to download all the files at once, if you know how.
# Load the pre-downloaded WorldClim monthly temperature data
temp_mon_sa <- rast("path/to/worldclim_monthly_temperature.tif")
# Load CHELSA temperature data (after manual download)
chelsa_mon_sa <- rast("path/to/chelsa_monthly_temperature.tif")Inspect the data structure:
# Check the number of layers and structure
temp_mon_sa
chelsa_mon_saCropping data to a maize-growing region
We will define an extent for cropping both datasets to a maize-growing region in northwest South Africa.
# Define the extent (replace with correct coordinates)
maize_extent <- ext(28.9, 30.7, -27.2, -26.0)Let’s plot this area on a map of South Africa.
# Load the South Africa border from the previous session
sa <- vect("south_africa_boundary.shp")
# Plot South Africa and add the maize region outline
plot(sa)
plot(maize_extent, col = "red", add = TRUE)Crop the datasets to the required region:
temp_mon_sa_crop <- crop(temp_mon_sa, maize_extent)
chelsa_mon_sa_crop <- crop(chelsa_mon_sa, maize_extent)Let’s analyze temperature differences between WorldClim and CHELSA within the maize-growing region.
# Compute difference between WorldClim and CHELSA
temp_diff <- temp_mon_sa_crop - chelsa_mon_sa_cropQuestion: How would you plot the differences between WorldClim and CHELSA?
Understanding and computing growing degree days (GDD)
Growing Degree Days (GDD) measure heat accumulation, essential for predicting crop development stages. GDD is calculated as:
\[ GDD = \sum_{i=1}^{n} \left( \frac{T_{\text{max},i} + T_{\text{min},i}}{2} - T_{\text{base}} \right),\quad \text{for } T > T_{\text{base}} \]
Where:
\(T_{max}\) is the daily maximum temperature.
\(T_{min}\) is the daily minimum temperature.
\(T_{base}\) is the base temperature below which plant growth is minimal or stops.
If we know the daily mean temperature (\(T_{mean}\)) we can sum \(T_{mean} - T_{base}\) directly rather than estimating it from \(T_{max}\) and \(T_{min}\).
For maize, we use \(T_{base} = 10°C\)
When using monthly mean daily temperature, we approximate GDD by assuming each day of the month has the mean temperature, and each month is 30 days long.
# Assume each month has 30 days
days_per_month <- 30
# Define base temperature for maize
base_temp <- 10
# Compute monthly GDD (approximate daily excess * 30 days)
gdd_maize <- app(temp_mon_sa_crop, fun = function(x) {
pmax(x - base_temp, 0, na.rm = TRUE) * days_per_month
})
# Optional: sum across all months to get total GDD for the year
gdd_total <- sum(gdd_maize, na.rm = TRUE)Question: How would you sum the GDD for a growing season that ran from October to March?
Extracting and visualising seasonal data
Let’s take a look at average monthly temperature variation for a particular location. First, extract monthly temperatures for a particular grid cell containing the location of interest.
# Define sample location (longitude, latitude)
site_location <- vect(data.frame(lon = 29.8, lat = -27.2), geom = c("lon", "lat"), crs = crs(temp_sa_mon))
# Extract data
site_temp <- terra::extract(temp_mon_sa_crop, site_location, ID = FALSE)Plot seasonal trends using ggplot2
Now use ggplot2, a powerful plotting package, to view the data.
# Convert to dataframe and plot with ggplot
library(ggplot2)
library(tidyverse)
# Convert extracted data to data.frame
temp_df <- site_temp %>% # the %>% means pass the data along
t() %>% # transpose rows and columns
as.data.frame() %>% # convert to a data.frame
mutate(month = 1:12) %>% # add months by name
rename(tavg = V1) # rename the mean temperature variable
# Plot the data using ggplot2
ggplot(temp_df, aes(x = month, y = tavg)) +
geom_line(linewidth = 2, col = "dodgerblue") +
labs(title = "Seasonal Temperature Trends", x = "Month", y = "Temperature (°C)")Summary
In this session, we:
Worked with monthly climate data.
Used an extent to crop data to a maize-growing region.
Computed Growing Degree Days (GDD) for maize.
Compared WorldClim with CHELSA within the cropped region.
Session 3 - Climate Projections with CMIP6
In this session, we will explore how to work with future climate projections from the CMIP6 ensemble (Coupled Model Intercomparison Project Phase 6). Participants will learn how to obtain, process, and analyse future temperature and precipitation data under different climate scenarios.
We will:
Introduce the structure of CMIP6 data.
Select appropriate models and emission scenarios (e.g. SSP2-4.5, SSP5-8.5).
Compare future projections to historical climate.
Visualise projected changes in temperature and precipitation.
Overview of CMIP6 climate models
CMIP6 includes simulations from many global climate models (GCMs), run under various Shared Socioeconomic Pathways (SSPs). These describe future greenhouse gas trajectories:
SSP1-2.6: Low emissions / sustainability-focused.
SSP2-4.5: Intermediate scenario.
SSP3-7.0: High emissions scenario / fossil fuel reliance.
SSP5-8.5: Very high emissions / fossil-fuel intensive development.
Each model provides projected climate variables for different periods (e.g. 2021–2040, 2041–2060, etc.).
Downloading CMIP6 data
For this example, we’ll download data from the BCC-CSM2-MR model for SSP2-4.5 and the period 2041-2060.
Sometimes downloads of CMIP6 climate projections using geodata won’t work. In that case, you’ll have to download them directly from the Worldclim CMIP6 web page.
# Example using geodata package
library(geodata)
# Download future global average temperature under SSP2-4.5
temp_future <- cmip6_world(var = "tavg", ssp = "245", model = "BCC-CSM2-MR", time = "2041-2060", res = 5, path = my_dir) # change path as required
# OR load a downloaded version as a SpatRaster
temp_future <- rast("path/to/file.tif")
# Crop to South Africa extent
temp_future_sa <- crop(temp_future, south_africa)Note: Not all models have data for every SSP and variable. Choose a subset of models for comparative analysis.
Comparing historical and future climate
Use previously downloaded WorldClim data as the baseline (e.g. mean annual temperature). Ensure that the resolution is 5 arc minutes to match the future projections dataset. If the resolution isn’t 5 arc minutes, use the worldclim_global function to download it, or download it directly from the Worldclim website.
# Load current WorldClim monthly temperatures
temp_current <- rast("path/to/worldclim_current_temp.tif")
# OR download the data if you don't have it
temp_current <- worldclim_global(var = "tavg", res = 5, path = my_dir)
# Align rasters and compute difference
temp_diff <- temp_future - temp_current
# Plot the change for each month
plot(temp_diff, main = "Projected Change in Mean Annual Temperature (2041–2060)")Question: How would you plot the difference in mean annual temperature between current and future projected climate?
Visualising across scenarios
We can compare climate change projections across different SSPs. Remember, you can download the data manually from the Worldclim website if required.
# Load and compare multiple scenarios
ssp2_45 <- cmip6_world(var = "tavg", ssp = "245", model = "BCC-CSM2-MR", period = "2041-2060",
res = 5, path = my_dir)
ssp5_85 <- cmip6_world(var = "tavg", ssp = "585", model = "BCC-CSM2-MR", period = "2041-2060",
res = 5, path = my_dir)
# Crop and compute differences
diff_245 <- crop(ssp2_45, maize_extent) - crop(temp_current, maize_extent)
diff_585 <- crop(ssp5_85, maize_extent) - crop(temp_current, maize_extent)
# Calculate annual differences
diff_245_annual <- app(diff_245, fun = "mean")
diff_585_annual <- app(diff_585, fun = "mean")
# Stack the rasters
diff_annual <- c(diff_245_annual, diff_585_annual)
names(diff_annual) <- c("diff_245", "diff_585")
# Plot side by side and give bespoke titles
plot(diff_annual,
main = c("SSP2-4.5 (2041–2060)", "SSP5-8.5 (2041–2060)")Summary
In this session, we learned to:
Understand the purpose and structure of CMIP6 climate model projections.
Know how to download and work with CMIP6 raster data.
Compare future projections to current climatology.
Visualise changes across space and between scenarios.
Session 4 - Analysing hourly climate data with ERA5
In this session, we will explore ERA5 hourly reanalysis data to understand how to work with high-frequency climate data and examine diurnal variation (daily cycles) in temperature and other variables. We will also discuss how different temporal resolutions (hourly, daily, monthly, annual) are used in agricultural applications.
Learning Objectives
By the end of this session, you will:
Understand what ERA5 reanalysis data is and how it differs from satellite or modelled projections.
Learn how to download and process hourly climate data.
Aggregate hourly data to daily or monthly means.
Explore and visualise diurnal patterns in temperature.
Discuss applications of different temporal resolutions in agriculture.
Introduction to ERA5
ERA5 is the European Centre for Medium-Range Weather Forecasts (ECMWF) Reanalysis version 5. . It combines historical observations with modern forecasting models to estimate climate variables hourly from 1950 to present.
Available through the Copernicus Climate Data Store (CDS).
Variables include temperature, wind speed, radiation, precipitation, and many more.
Downloading ERA5 data
Users need to register for a Copernicus CDS account and install the cdsapi Python package for automated downloads. For this session, we will use pre-downloaded .grib (GRIB) files. Copernicus CDS will also supply data in NetCDF4 .nc format but this can have issues.
# Load raster from GRIB (e.g. hourly temperature data for a specific region)
library(terra) # Load library if not already loaded
# Load the data
era5_hourly <- rast("path/to/era5_hourly_temp.grib")
# Check the structure
era5_hourly
# Check the time stamps for each layer
time(era5_hourly)Aggregating hourly data
We can summarise hourly data to daily or monthly means to observe broader patterns.
The tapp function is like app, but instead of applying a function to every layer, it applies it to whatever set of layers you specify. In this case, we have hourly data with a clear time stamp (hour) for each layer. This means that using index = "days" gives us a summary mean value for each day (24 hour period) in the data set.
# Convert hourly to daily mean
era5_daily <- tapp(era5_hourly, index = "days", fun = "mean", na.rm = TRUE)Visualising diurnal temperature variation
We will select a representative location and extract the hourly temperature time series to explore the daily temperature cycle.
# Define a location (replace X and Y with your choice of lon and lat)
site <- vect(data.frame(lon = X, lat = Y), geom = c("lon", "lat"), crs = crs(era5_hourly))
# Extract hourly temperature at this site and reformat
# For this we'll need tidyverse functions to be efficient
library(tidyverse)
# Extract and reformat
temp_series <- terra::extract(era5_hourly, site, ID = FALSE) %>%
as.numeric() %>% # convert to a vector of values
tibble(temp_2m = .) %>% # convert to a tibble data.frame
mutate(temp_2m = temp_2m - 273, # Kelvin to Celsius
time = time(era5_hourly)) # add times
# Plot with ggplot (amend as required)
library(ggplot2)
ggplot(temp_series, aes(x = time, y = temp_2m)) +
geom_line() +
labs(title = "Hourly Temperature Variation",
subtitle = "January 2025",
x = "Time", y = "Temperature (°C)")Applications in agriculture
Different climate data resolutions are suited for different agricultural uses:
| Resolution | Example Uses |
|---|---|
| Hourly | Diurnal cycles, frost events, irrigation needs, epidemiological models |
| Daily | Rainfall thresholds, evapotranspiration |
| Monthly | Crop calendar planning, seasonal forecasts |
| Annual | Long-term climate trends, suitability zones |
Summary
In this session, we:
Worked with ERA5 hourly reanalysis data.
Aggregated data to daily and monthly resolutions.
Visualised diurnal variation in temperature.
Discussed the relevance of data resolution for agricultural decisions.
Here’s a summary paragraph you can use at the end of the geospatial sessions:
Session 5 - Introduction to Google Earth Engine (GEE)
What is Google Earth Engine?
Google Earth Engine (GEE) is a cloud-based platform for planetary-scale geospatial analysis. It allows users to analyse satellite imagery and other Earth observation data using a powerful cloud infrastructure, eliminating the need to download large datasets locally.
GEE supports processing of:
Satellite imagery (Landsat, Sentinel, MODIS, etc.)
Climate data
Land cover and land use products
Topographic and hydrologic data
It is free to use for research and educational purposes and is particularly well suited for time series analysis, image classification, and change detection.
Key Features
Access to petabytes of geospatial data in the cloud
Perform operations without downloading raw data
Built-in visualisation tools and interactive maps
Code Editor using JavaScript (also supports Python via API)
GEE and R
GEE is based on JavaScript, a language that is quite different from R:
It uses different syntax and naming conventions
Objects are immutable and often used in a functional style
The GEE JavaScript Code Editor runs in a browser (no R environment required)
If you are familiar with R, it will take a little practice to get comfortable with the new syntax.
Example: Mean NDVI for 2024 in the Nelspruit Region
In this example, we will:
Define a region of interest (ROI) around Nelspruit, South Africa
Access Sentinel-2 surface reflectance imagery
Compute the Normalized Difference Vegetation Index (NDVI)
Generate a mean annual NDVI composite for 2024
To follow along, visit https://code.earthengine.google.com and paste the following script into the Code Editor.
JavaScript Code (for GEE Code Editor)
// Define the region of interest (ROI) around Nelspruit
var roi = ee.Geometry.Rectangle([30.9, -25.6, 31.3, -25.2]);
// Load Sentinel-2 Surface Reflectance imagery
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(roi)
.filterDate('2024-01-01', '2024-12-31')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
.map(function(img) {
var ndvi = img.normalizedDifference(['B8', 'B4']).rename('NDVI');
return img.addBands(ndvi);
});
// Compute mean NDVI over the year
var meanNDVI = s2.select('NDVI').mean().clip(roi);
// Visualisation parameters
var ndviVis = {
min: 0.2,
max: 0.85,
palette: ['white', 'yellow', 'green']
};
// Add to map
Map.centerObject(roi, 11);
Map.addLayer(meanNDVI, ndviVis, 'Mean NDVI (2024)');
Map.addLayer(roi, {color: 'black'}, 'Region of Interest');What to Explore Next
Change the date range to examine seasonal NDVI
Export images or statistics to Google Drive
Use other indices (e.g. EVI, NDWI)
Compare different years or different sensors (e.g. Landsat)
Use
ee.Chartto analyse time series at specific points or polygons
Summary
In this session, we:
Learned what GEE is and how it works
Discussed how GEE differs from R
Created a JavaScript script to compute mean NDVI for 2024 around Nelspruit
Explore the Code Editor, try running and modifying the example, and begin to build familiarity with JavaScript-based geospatial workflows in GEE.
What’s next
Over the course of these sessions, you have learned how to work with geospatial climate data in R using the terra and geodata packages. You explored raster and vector data formats, downloaded and processed climate datasets from WorldClim, CHELSA, and CMIP6, and used land cover data to focus analyses on agricultural areas. You calculated growing degree days (GDD) for maize, analysed climate change projections using CMIP6 models, and examined diurnal temperature variation using ERA5 hourly data. These examples highlight the power of open-source geospatial tools for agricultural and environmental applications.
To build on this foundation, you are encouraged to practice working with other regions, variables (e.g. solar radiation, soil moisture), and crops. Explore more advanced spatial analysis tasks such as time series extraction, spatial aggregation, zonal statistics, and classification. Learning how to use tools like sf, stars, exactextractr, and Google Earth Engine will further expand your capabilities in remote sensing and spatial modelling. The skills you’ve developed here are valuable for research, policy, and practical decision-making in agriculture and environmental science.