Dask — Scalable Raster Analytics
Goal: Use Dask with xarray/rioxarray to run chunked, lazy, and parallel raster analytics so that large rasters never need to be fully loaded into memory.
The scalability problem
- A single Sentinel-2 tile might be 10+ GB (all bands, full resolution).
- A time series of 100 dates exceeds RAM on most machines if loaded at once.
- Solution: Keep data in chunks, compute only the chunks that are needed, and run operations in parallel over chunks. Dask does exactly that.
Dask
Dask provides array-like types (dask.array) that look like NumPy but:
- Are lazy: operations build a task graph instead of executing immediately.
- Are chunked: the logical array is split into blocks; each block is a task.
- Execute in parallel when
.compute()is called (using threads, processes, or a cluster).
xarray integrates Dask: if a DataArray’s data is a dask.array, then xarray operations (slicing, reduction, arithmetic) stay lazy until .compute() or I/O (e.g. plot, write).
Chunking strategy
- Chunk size: Small enough to fit in memory (e.g. 512×512 or 1024×1024 pixels per chunk); large enough to amortize overhead.
- Alignment: Chunk boundaries should align with the raster’s internal blocks (e.g. COG tiles) when possible, so one read = one chunk.
- Dimensions: For a
(time, band, y, x)cube, chunkyandxand keeptimeandbandunchunked (or chunk time if there are many dates).
Typical pattern:
# When opening with rioxarray
data = rioxarray.open_rasterio(url, chunks={"x": 1024, "y": 1024})
# Or rechunk after open
data = data.chunk({"x": 1024, "y": 1024})
Lazy pipeline example
The lesson notebook opens the Lesson 3 GeoTIFFs with chunks= (no STAC). ERA5-Land is many days and few pixels; the DEM is one band and many pixels.
import rioxarray
prec = rioxarray.open_rasterio(
"data/prec_daily_2024_ERA5LAND_native.tif",
chunks={"band": 30, "x": 200, "y": 200},
)
dem = rioxarray.open_rasterio(
"data/copernicus_dem_30m.tif",
chunks={"x": 1024, "y": 1024},
).squeeze("band", drop=True)
# All of this is lazy
prec_ts = prec.mean(dim=("y", "x"))
dem_mean = dem.mean()
# Only this triggers the read
prec_ts.compute()
dem_mean.compute()
Dask will:
- Split each raster into chunks (time slabs on climate; 1024×1024 tiles on the DEM).
- Run arithmetic and reduction in parallel over chunks.
- Return a small result (a daily series, a scalar) without holding the full grid in memory.
Lesson 5 uses the same chunks= + reduce pattern on Sentinel-2 COGs from STAC.
When compute runs
- Explicit:
.compute()returns a concrete NumPy/xarray result. - Implicit:
.plot(),.to_netcdf(),.rio.to_raster()trigger computation for the data they need. - Persist:
.persist()computes and keeps the result in distributed memory (useful in a cluster) so later steps don’t recompute.
Local vs distributed
- Default (threaded):
dask.arrayuses a thread pool; good for I/O-bound COG reads. - Processes:
dask.distributed.Client(processes=True)for CPU-bound work. - Cluster: Point xarray/Dask to a
distributed.Client("scheduler-address")to run on multiple machines. The same code (open COG, chunk, reduce) scales up.
Best practices
- Chunk to align with COG tiles when possible (e.g. 512 or 1024).
- Avoid too many tiny chunks (overhead) and too few huge chunks (memory).
- Use reductions (
.mean(),.sum()) along dimensions to shrink data before.compute(). - Profile: Use
dask.distributed.dashboardor Dask’s profilers to see where time is spent.
Summary
| Concept | Detail |
|---|---|
| Dask | Lazy, chunked, parallel arrays; xarray can wrap Dask arrays |
| Chunking | Split (e.g. y, x) into blocks; align with COG tiles when possible |
| Lazy | Operations build a graph; .compute() or I/O triggers execution |
| Scale | Same code can run on one machine or a Dask cluster |
Download the notebook for this lesson: notebooks/04-dask-analytics.ipynb.