πŸ›°οΈ Multiband Zonal Stats

Fast, tidy zonal statistics for multiband rasters & polygon layers

Version 0.5.0 Β· QGIS 3.34+

⚑What it does

This plugin summarises a multiband raster over the polygons of a vector layer and writes a clean CSV. Instead of re-reading the raster once per polygon (the slow, classic pattern), it reads the raster in bounded tiles and aggregates every zone and band together with NumPy β€” so the work scales with the raster area touched, not with polygons Γ— bands.

Overlapping zones handled Multipart polygons Per-band nodata Bounded memory Atomic output

πŸš€Quick start

  1. Pick your layers β€” a file-based (GDAL) raster and a polygon layer.
  2. Choose a tab β€” Continuous for measured values (elevation, NDVI) or Categorical for class codes (land cover, soils).
  3. Select statistics and a layout in that tab.
  4. Set the output CSV (and optionally decimal places), then Run analysis.

Available from the Raster β†’ Multiband Zonal Stats menu, the toolbar, and the Processing Toolbox (two algorithms). Runs in the background with progress and a safe Cancel.

πŸŽ›οΈTwo modes β€” pick the one that matches your raster

πŸ“ˆ Continuous

Measured quantities where averaging makes sense.

Examples: elevation, temperature, rainfall, reflectance, NDVI.

Statistics: countnodata minmax summean stddev

Layouts: Long or Wide.

πŸ—ΊοΈ Categorical

Discrete class codes where averaging is meaningless.

Examples: land cover, soil type, zoning, CORINE classes.

Statistics: majorityminority varietycount nodata

Layouts: Summary, Class breakdown, or Class counts.

⚠️ Why the split matters The mean of land-cover codes {1=water, 2=forest, 5=urban} is a meaningless number. Categorical mode summarises the class composition instead β€” and rejects a raster with more than 65,536 distinct values as "actually continuous".

πŸ“Statistics reference

Continuous πŸ“ˆ

NameMeaning
countNumber of valid (non-nodata) pixels in the zone
nodataNumber of nodata pixels covered by the zone
min / maxSmallest / largest valid value
sumSum of valid values
meanAverage of valid values
stddevPopulation standard deviation (Γ· n), like QGIS's own tool

Categorical πŸ—ΊοΈ

NameMeaning
majorityMost common class (ties β†’ lowest class code)
minorityLeast common class (ties β†’ lowest class code)
varietyNumber of distinct classes present
count / nodataValid / nodata pixel counts

πŸ“‹Output layouts & examples

Every row of every layout begins with a unique fid.

Continuous Β· Long one row per polygon & band

fid,polygon_id,polygon_name,band_index,band_name,count,mean,stddev
1,101,North field,1,ndvi,4210,0.62,0.11
2,101,North field,2,ndvi_may,4210,0.58,0.09
3,102,South field,1,ndvi,3980,0.71,0.08

Continuous Β· Wide one row per polygon, bands across columns

fid,polygon_id,polygon_name,ndvi_mean,ndvi_stddev,ndvi_may_mean,ndvi_may_stddev
1,101,North field,0.62,0.11,0.58,0.09
2,102,South field,0.71,0.08,0.66,0.07

Categorical Β· Summary one row per polygon & band

fid,polygon_id,polygon_name,band_index,band_name,majority,minority,variety,count
1,101,North field,1,landcover,3,4,4,16
2,102,South field,1,landcover,2,1,3,16

Categorical Β· Class breakdown one row per polygon, band & class

fid,polygon_id,polygon_name,band_index,band_name,class,pixel_count,fraction
1,101,North field,1,landcover,1,4,0.250
2,101,North field,1,landcover,2,4,0.250
3,101,North field,1,landcover,3,5,0.312
4,101,North field,1,landcover,4,3,0.188

Categorical Β· Class counts (wide) one column per class

fid,polygon_id,polygon_name,band_index,band_name,class_1,class_2,class_3,class_4
1,101,North field,1,landcover,4,4,5,3
2,102,South field,1,landcover,7,4,1,4

Columns are the union of classes across all zones, so every row shares the same columns β€” a class absent from a zone is simply 0.

🏷️Band names & prefix / suffix

Output columns keep the raster's own band names (falling back to band_001, band_002… when unnamed). Add an optional common prefix and suffix to label a run of files:

Band namePrefixSuffixWide column (sanitised)
NDVI2020_β€”2020_ndvi_mean
Surface Tempβ€”β€”surface_temp_mean
band 1β€”_dryband_1_dry_mean

Wide headers are lowercased with spaces/punctuation turned into underscores; the raster's bands are never renamed.

πŸ”‘The fid column & decimal places

πŸ”‘ Unique fid

Every row starts with a unique 1-based fid β€” a stable row identity. Handy because polygon_id repeats across bands and classes, so it isn't a row key on its own. Use fid for joins, database imports, or referencing a row.

πŸ”’ Decimal places

Control the decimals of numeric cells. Full precision (the default) keeps every digit; set 0–15 to fix the decimals β€” only floating-point cells (stats, fraction) change; integers stay clean.

πŸ“ŠHistogram preview

The Histogram… button (bottom-left of the dialog) opens a quick preview of a band's distribution β€” handy before you commit to an analysis.

  • Pick any band, and toggle Continuous (binned values, with an adjustable bin count) or Categorical (one bar per class). It opens on the mode of your active tab.
  • It describes the whole raster (it ignores the polygons) and reads the band decimated for speed, so even large rasters preview promptly.
  • Summary stats sit beneath the chart: min / max / mean / sd for continuous, or the class count and majority for categorical.

Drawn with Qt directly β€” no extra Python packages needed.

🧭Coordinate systems

The raster's CRS is the analysis CRS, and the raster is never resampled (that would fabricate pixel values). Zones are transformed into the raster CRS automatically when they differ.

If a layer's CRS is missing or wrong, set a Raster CRS or Zones CRS override. An override assigns a CRS (reinterprets coordinates) β€” it does not reproject the data.

πŸ’‘Tips & troubleshooting

πŸ“Š Sums look like text in Excel (leading apostrophe)? That is a locale mismatch: the CSV uses a . decimal, but your Excel expects a ,. Use Data β†’ From Text/CSV and set the decimal separator, or reduce the output to a few decimal places for cleaner numbers.
🚫 "More than 65,536 distinct values" error You ran Categorical mode on a continuous raster. Switch to Continuous, or reclassify the raster into discrete classes first.
πŸ—‚οΈ "Needs a file-based raster" error The engine reads the raster directly with GDAL. Export WMS/tiled/provider-only layers to GeoTIFF first, then run the analysis on the export.
🚫 Excluding values (extra nodata) Type values into Exclude values to drop them from the calculation, just like nodata β€” comma-separated, e.g. 0, 255. Handy for categorical rasters where 0 marks out-of-boundary pixels. Open the Histogram first to spot which value to exclude.
🧩 Overlapping polygons Shared pixels are counted for every overlapping zone β€” overlaps are rasterized in separate passes so no zone loses its pixels.

Advanced performance settings

SettingWhat it does
Tile sizeRaster window read at a time. Larger = fewer reads, more memory.
Band chunkBands held in memory per pass. Lower it for many-band rasters on tight memory.
All touchedInclude every pixel a polygon touches, not just pixel centres (slower).