This repo contains materials for my PyData Global 2025 talk on building scalable geospatial raster processing pipelines using Xarray, Dask.
The talk focuses on engineering principles, parallel processing, and memory-safe raster operations especially when working with very large rasters.
.
├── huge_rasters_using_xarraydask.py # Main processing pipeline
├── env_setup.sh # Environment setup script
├── PyDataGlobal.qmd # Quarto presentation
└── www/ # Slide images and diagrams
This script demonstrates a full, production-style geospatial raster processing workflow using chunked computation and distributed processing.
It covers the full pipeline:
- Creates a local Dask cluster
- Lets you tune workers/threads/memory for your machine
-
Rasters are opened lazily using
rioxarray.open_rasterio() -
Uses configurable chunk sizes (e.g., 1024×1024)
-
Avoids loading full rasters into memory
-
Immediately gives visibility on:
- Raster dimensions
- Internal Dask chunking
Large rasters often exceed available RAM. Here we use Rasterio’s WarpedVRT:
-
Reprojects on-the-fly
-
Lazily streams blocks instead of loading everything
-
Supports different resampling for different folders:
- SUM (downsampling for intensity-like rasters)
- NEAREST (upsampling for categorical rasters)
The script reprojects into a base grid defined by:
- CRS:
EPSG:4326 - Resolution:
(0.00083333333, -0.00083333333)(≈ ~ 100m)
Each folder in the project uses a different workflow:
| Folder | Operation | Description |
|---|---|---|
folder_1 |
Downsample | Weighted aggregation using Resampling.sum |
folder_2 |
Upsample | Nearest-neighbour upsampling |
folder_3 |
Clean nodata | No reprojection — nodata replaced with 0 |
The pipeline automatically chooses the correct approach.
- Loads an ADM3 shapefile once
- Ensures CRS matches raster CRS
- Assigns each polygon a
zone_id - Prepares shape tuples for rasterization
This is one of the hardest parts of large-scale zonal statistics.
The script performs chunk-by-chunk rasterization:
- Each Dask block determines its own spatial extent
- Only polygons intersecting the block are rasterized
- Produces a zone raster perfectly aligned with the reprojected/processed raster
- Avoids creating a massive in-memory label raster
This is essential for rasters with tens of thousands of chunks.
A custom zonal statistics engine runs with:
-
dask.delayed -
One task per (raster_chunk × zone_chunk)
-
Automatic merging of statistics across blocks
-
Handles nodata values correctly
-
Computes:
- Sum
- Count
- Min
- Max
- Mean
This avoids tools that require full in-memory rasters (e.g., rasterstats, geopandas overlay).
The pipeline:
-
Iterates through folders
-
Loads all
.tifand.tiffrasters -
Applies folder-specific preprocessing
-
Rasterizes zones
-
Computes zonal statistics
-
Merges results with ADM3 attributes
-
Saves both:
- A combined CSV for all folders
- A per-folder CSV
Outputs are written to:
.../zonalstats/zonal_stats_all.csv
.../zonalstats/zonal_stats_folder_1.csv
.../zonalstats/zonal_stats_folder_2.csv
.../zonalstats/zonal_stats_folder_3.csv
This approach is built for:
- 10–50GB+ rasters
- High-resolution climate/agriculture datasets
- SDG-level national statistics
- Raster stacks with hundreds of layers
It uses:
- Lazy loading
- Chunkwise reprojection
- Chunkwise rasterization
- Distributed compute graphs
- Memory-safe processing
Everything is built around avoiding:
Full raster reads Single-machine memory bottlenecks Inconsistent grids CRS mismatch issues
- Install dependencies
bash env_setup.sh
-
Edit paths in run_pipeline()
-
Download geospatial data from HDX(Population density maps), WorldPop harmonised covariates(https://hub.worldpop.org/project/categories?id=14), and Malaria atlas (https://data.malariaatlas.org/maps?layers=Accessibility:202001_Global_Motorized_Travel_Time_to_Healthcare,Malaria:202206_Global_Pf_Parasite_Rate&extent=-11815912.856289707,-6356003.33856192,28286163.259866484,14615055.359158086)
-
Run the huge_rasters_using_xarraydask.py