ds-raster-stats · legacy method (upsample to 0.05° + per-slice rasterized masks) vs PR #49 (exactextract coverage-weight matrix, all slices at once) · real prod SEAS5 COGs + COD boundaries · measured on Databricks job compute (the environment the pipeline actually runs in) and on a laptop · 2026-08-26
100 SEAS5 issuances × 7 leadtimes (700 COG slices), admin levels 0–2, full pipeline stats path incl. weighted median and row validation. Seconds of compute per country on Databricks job compute — excludes the ~62 s/country remote data fetch, which is identical in both methods (see notes). Laptop figures are in the table view.
| Country | ADM2 units | Rows | DBX legacy (s) | DBX PR #49 (s) | DBX speedup | DBX end-to-end | Laptop speedup |
|---|---|---|---|---|---|---|---|
| Nigeria | 774 | 568,400 | 677.4 | 19.9 | 34.1× | 742 s → 83 s (8.9×) | 7.3× |
| Chad | 70 | 65,800 | 94.1 | 3.5 | 26.5× | 159 s → 64 s (2.5×) | 7.0× |
| Burundi | 119 | 96,600 | 111.6 | 3.8 | 29.4× | 174 s → 65 s (2.7×) | 4.4× |
Core zonal kernel on a Nigeria-scale grid (25×30 native pixels at 0.4°, 8× upsample equivalent), 100 slices, synthetic contiguous admin regions; best of 3 runs after warm-up, on Databricks job compute. Both axes log — the legacy cost climbs linearly with admin count while the weight-matrix cost stays well below it at every size. This is what makes adding many more (smaller) admin units cheap. Laptop figures are in the table view; the ratio narrows at large admin counts on the cluster node (11× at 3,000 vs 23× on the laptop) because the sparse-matrix path is the more memory-bandwidth-sensitive of the two.
| Admin units | DBX legacy (s) | DBX PR #49 (s) | DBX speedup | Laptop legacy (s) | Laptop PR #49 (s) |
|---|---|---|---|---|---|
| 10 | 0.528 | 0.013 | 42× | 0.179 | 0.011 |
| 100 | 1.537 | 0.046 | 33× | 0.438 | 0.010 |
| 774 | 3.906 | 0.305 | 13× | 1.478 | 0.051 |
| 3,000 | 12.280 | 1.112 | 11× | 4.412 | 0.193 |
The two sweeps above each vary one axis. This walks the full grid, so the interaction is visible. Cell colour and label are the speedup of PR #49 over the legacy method, on Databricks job compute, running the real pipeline runners (weighted median and row validation included) over synthetic non-pixel-aligned polygons on a Nigeria-sized SEAS5 grid.
| Admin units | Dates | Legacy (s) | PR #49 (s) | Speedup |
|---|
The two axes behave completely differently. Legacy cost is a clean product of both — 3,000 admins over 200 dates costs it 749 s. The new method is close to flat in dates: at 3,000 admins, going from 1 date to 200 moves it only 0.61 s → 0.97 s, because the sparse matrix product over 200 slices costs barely more than over one. So the multiplier comes from the date axis, while the admin axis mostly sets the floor (5–8× at a single date warm, and see the cold table below for the worst case where the weight build is not yet amortised). The practical reading: daily update runs sit in the cheap corner either way, and adding admin units is close to free — 800 → 3,000 admins costs the new method 0.25 s → 0.97 s at 200 dates, where it costs the legacy method 199 s → 749 s.
One asymmetry in the grid above, and the worst case it hides. The legacy timings rebuild their rasterized mask on every repetition, because that is what the legacy code does. The new-method timings load the weight matrix from its disk cache, because that is what the pipeline does after the first chunk touches a country. That is a fair model of a production run, but it amortises the weight build to zero — so the table below re-measures the same grid with the build inside the timed call (cold), which is the honest worst case for a single isolated run.
| Admin units | Dates | Legacy | PR #49 warm | PR #49 cold | Speedup warm | Speedup cold |
|---|---|---|---|---|---|---|
| 10 | 1 | 0.029 | 0.006 | 0.028 | 4.5× | 1.04× |
| 200 | 1 | 0.244 | 0.039 | 0.133 | 6.2× | 1.83× |
| 3,000 | 1 | 3.372 | 0.516 | 1.175 | 6.5× | 2.87× |
| 10 | 10 | 0.147 | 0.007 | 0.025 | 20.9× | 5.9× |
| 200 | 10 | 2.177 | 0.042 | 0.113 | 51.8× | 19.2× |
| 3,000 | 10 | 31.418 | 0.550 | 1.197 | 57.1× | 26.2× |
| 10 | 50 | 0.691 | 0.009 | 0.026 | 80.8× | 26.8× |
| 200 | 50 | 10.800 | 0.048 | 0.125 | 226.9× | 86.5× |
| 3,000 | 50 | 157.900 | 0.575 | 1.223 | 274.6× | 129.1× |
Two things follow. First, the weight build is a fixed cost per country and admin level, not per date — cold minus warm is 0.021 / 0.018 / 0.017 s at 10 admins across 1, 10 and 50 dates, and 0.659 / 0.647 / 0.648 s at 3,000. It scales with admin units (it is geometry work) and is then cached across every subsequent chunk, country-worker and run. Second, even paying it in full the new method is never slower: the true worst case in this grid — a single date over only 10 admin units, where there is nothing whatsoever to amortise against — is 1.04×, essentially break-even, rising to 2.9× at 3,000 units and past 100× once there are 50 dates to spread it over. Separate run from the grid above, so its warm figures differ slightly (different cluster instance); the cold/warm comparison within it is self-consistent.
One SEAS5 issuance (7 leadtimes), admin levels 0–2, warm weight cache, on Databricks job compute. Even at this small scale the method change is worth 9–13× per country. The rest of a production update run is boundary downloads, DB writes, and serial country processing, which PR #49 addresses separately (per-run boundary cache, configurable workers, country-splitting for single-chunk runs).
| Country | ADM2 units | Legacy (s) | PR #49 warm (s) | PR #49 cold (s) | Speedup |
|---|---|---|---|---|---|
| Nigeria | 774 | 7.73 | 0.60 | 1.63 | 12.9× |
| Burundi | 119 | 2.64 | 0.21 | 0.63 | 12.6× |
| Ethiopia | 92 | 2.06 | 0.19 | 1.06 | 10.7× |
| South Sudan | 79 | 1.64 | 0.16 | 0.79 | 9.9× |
| Chad | 70 | 1.68 | 0.18 | 0.43 | 9.3× |
The team's 2024 methodology work (retired Confluence, "Raster Statistics
→ Methodology") already made one big jump: rasterizing admin boundaries
once per country/level and reusing the mask across dates, instead
of re-rasterizing per date as baseline python-rasterstats
does. Their benchmarks: Ethiopia at 125 dates went from 6.6 s to
0.75 s (~9×), the Philippines from ~35 s to ~0.2 s — but
cost still grew linearly with the number of polygons (measured to ~90).
PR #49 is the next step in the same direction: precompute not just
the mask but the full pixel-coverage weight matrix, compute all slices at
once, and cache the matrix on disk.
On accuracy, the 2024 page chose 0.05° nearest-neighbour upsampling
explicitly as "a simplified approach for weighting the input pixels by
the amount of area covered", and measured that its error against
exactextract ground truth grows with the upsampled resolution (RMSE
≈ 0.07 at 0.05° on the Philippines ADM2 case), with finer resolutions
rejected for memory/performance. exactextract itself was
rejected as "much slower to run repeated calculations" — true when
invoked per date. Used once per country to build weights, its cost is a
one-off ≤ 0.4 s, and the per-date math is faster than the whole-pixel
path — so the 2024 accuracy/performance trade-off no longer exists. In a
comparison against prod values (BDI/NGA/TCD, one issuance, 6,475 rows)
the median mean-difference vs the legacy method was 0.25%, with larger
differences confined to the smallest polygons — the same distribution the
2024 Philippines histogram documented as the legacy method's
approximation error.
The 2024 methodology work benchmarked baseline
compute_zonal_statistics (python-rasterstats per date) against
fast_compute_zonal_statistics (rasterize-once + numpy — today's
legacy method). This reruns the original functions from the
2024 exact-extract branch, same setup as far as it is
recoverable — ERA5 monthly at native 0.25°, ETH and PHL ADM2 CODs,
sweeps over N dates — with the PR #49 kernel as a third line.
Outputs of baseline and fast were verified identical before timing.
Log y-axis; seconds per run (best of 10 iterations; 3 for the baseline).
| Country · N dates | Baseline (s) | Fast (s) | PR #49 (s) | 2024 chart: baseline / fast (s) |
|---|---|---|---|---|
| ETH · 5 | 0.50 | 0.006 | 0.005 | ~0.5 / ~0.05 |
| ETH · 125 | 10.93 | 0.155 | 0.027 | ~6.6 / ~0.75 |
| PHL · 5 | 10.39 | 0.006 | 0.005 | ~1.4 / ~0.02 |
| PHL · 125 | 60.12 | 0.133 | 0.014 | ~35.3 / ~0.2 |
| Country · N polygons | Baseline (s) | Fast (s) | PR #49 (s) |
|---|---|---|---|
| ETH · 1 | 0.046 | 0.006 | 0.0001 |
| ETH · 25 | 0.310 | 0.008 | 0.0016 |
| ETH · 92 | 0.963 | 0.013 | 0.0057 |
| PHL · 1 | 0.061 | 0.006 | 0.0002 |
| PHL · 25 | 5.471 | 0.008 | 0.0015 |
| PHL · 88 | 12.919 | 0.013 | 0.0051 |
What reproduces: every 2024 shape and ordering — all methods linear in dates, baseline linear in polygons and fast near-flat (ETH), and PHL's baseline far slower than ETH's despite a smaller grid (its coastline multipolygons dominate rasterstats' per-polygon cost). Absolute times differ from the 2024 charts (different machine and library versions): this run's baseline is slower and its fast is faster, so the baseline→fast gap measures 59–428× here vs ~9–175× in 2024. One shape did not reproduce: the 2024 PHL polygons chart was nearly flat (1.42→1.65 s over 1→88 polygons) where this run grows linearly (0.07→12.0 s) — consistent with their PHL polygon sweep having run on the upsampled 0.05° grid, where a large fixed per-date cost swamps the per-polygon term; the sweep configuration was never committed, so this is a hypothesis. At the largest size measured here the 2024-fast → PR #49 gap is 5.7× (ETH) and 9.8× (PHL) — and note that both kernels are running on the native grid, faithful to the 2024 harness, whereas the production legacy path first upsamples to 0.05° (25× the pixels for ERA5). Rerunning the same ETH/PHL data with the legacy method configured as production actually runs it widens the gap to 16–17× (ETH 0.7 s vs 0.043 s; PHL 0.5 s vs 0.030 s at 125 dates), and it grows further with slices and admin count — see the pipeline-scale and 2-D sections above.
Correction (2026-08-27). The first version of this figure timed with the mean of repeated runs, which absorbs contention from anything else on the machine rather than rejecting it. Other benchmarks were running concurrently, and because the largest point in each sweep is measured last it took the worst of it: the PR #49 figure at 125 dates was published as 0.076 s (PHL) and 0.042 s (ETH) against true values of 0.014 s and 0.027 s — inflating PHL roughly 6×, and making the gap look like 2–4× when it is 5.7–9.8×. Re-measuring the kernel in isolation on a quiet machine confirmed there is no cliff: per-slice cost is flat at ~0.10 ms from 75 through 200 slices. All timings on this page now use the minimum of repeated runs, which estimates the uncontended cost. The Databricks figures elsewhere on this page were unaffected — a dedicated cluster with nothing else on it, already min-of-N.
open_rasterio(…,
chunks="auto") only reads metadata, so the bulk transfer
happens later, per country, in the clip step. The PR reverted the
threading rather than ship unmeasured concurrency.Standard_DS4_v2, 8 cores, Linux, Python 3.10) — the
environment the pipeline actually runs in, and the headline numbers
here — and an Apple Silicon laptop (10 cores, macOS 14.5,
Python 3.12) for comparison. Both single-process. The cluster
node is ~3–20× slower in absolute terms and the legacy method degrades
furthest on it, which is why the same change is worth more in
production than on a laptop.affine ≥2.4 puts a cached_property on a
__slots__ namedtuple, which raises on Python 3.10
(the Databricks 14.3 runtime) whenever a rasterio transform is indexed
— harmless on 3.12. The PR pins affine==2.3.1. The same
run also caught a relative .cache/ default that the
Databricks workspace filesystem rejects; caches now default under the
system temp dir.