Precipitation Assessment API Reference¶
For land-system classification, see the Classification API Reference.
savana.rainfall.pipeline
¶
RainfallAssessment: the chainable orchestrator tying every
savana.rainfall stage together, mirroring
:class:savana.pipeline.SavanaClassifier's builder pattern.
Every constructor argument is a default that can be overridden — a different product catalogue, a different gauge network, a different zone scheme, or different application weights all work the same way they do calling the individual stage functions directly. This class is a convenience, not a new capability.
RainfallAssessment
¶
Chainable orchestrator for a precipitation product assessment.
Example (defaults — reproduces the WA study)::
ra = (
RainfallAssessment()
.get_observations(source="ee_asset")
.ingest(start="2001-01-01", end="2020-12-31")
.extract()
.assign_zones()
.validate()
.score()
)
print(ra.summarize())
ra.export_workbook("decision_tool.xlsx")
Example (a different station network, subset of products)::
ra = (
RainfallAssessment(
stations=[(-1.5, 12.4), (2.1, 6.5)], # or a DataFrame, a
# .geojson/.csv path,
# or a single (lon, lat)
products={"CHIRPS": config.DEFAULT_PRODUCTS["CHIRPS"],
"GPM_IMERG": config.DEFAULT_PRODUCTS["GPM_IMERG"]},
)
.get_observations(source="download")
.ingest(start="2015-01-01", end="2023-12-31")
.extract()
.validate() # no assign_zones() call -> pooled validation
.score()
)
Source code in savana/rainfall/pipeline.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 | |
compare_table()
¶
Obs vs. every product's simulated value, side by side — one
row per (station, year, month), GPCC in its own column, one
column per product. The plain "just let me look at the numbers"
table, underlying every bias/KGE/etc. computed later. Requires
.merge() (or .validate(), which calls it) to have run.
Source code in savana/rainfall/pipeline.py
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
preview_comparison(station_id=None, product=None)
¶
Quick obs-vs-sim scatter, before running formal validation
metrics. Requires .merge() (or .validate(), which calls
it) to have run.
Source code in savana/rainfall/pipeline.py
237 238 239 240 241 242 243 244 245 246 247 248 249 | |
preview_map(product, kind='daily', reference=None, show_gpcc=False, region=None, m=None)
¶
Interactive map of one product's mean rainfall (kind=
"daily" or "annual"), or its bias against ANOTHER PRODUCT
if reference is given — a gridded-vs-gridded comparison,
never a GPCC comparison (GPCC has no gridded form here).
Set show_gpcc=True to overlay real GPCC station values (not
a rasterized surface — the true point observations, colored on
the same scale as the raster) on top of the mean map. Requires
.get_observations() to have already run. Ignored when
reference is also given (the overlay only applies to the
single-product mean map).
Requires .ingest() to have run.
Source code in savana/rainfall/pipeline.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | |
preview_observations(station_id=None)
¶
Quick time-series plot of raw GPCC observations. Requires
.get_observations() to have run — no product data needed.
Source code in savana/rainfall/pipeline.py
226 227 228 229 230 231 232 233 234 235 | |
preview_station_bias(product, m=None, zoom=5)
¶
Interactive map of per-station bias against REAL GPCC
observations for one product — the actual "does this agree with
ground truth, and where" spatial check. Requires .merge()
(or .validate(), which calls it) to have run.
Source code in savana/rainfall/pipeline.py
343 344 345 346 347 348 349 350 351 352 353 | |
preview_stations(m=None, zoom=5)
¶
Interactive map of station locations. Works as soon as
stations are set (before .get_observations() even) — the
first sanity check: are these actually where you think they are?
Source code in savana/rainfall/pipeline.py
211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
run(start, end, obs_source='download', **obs_kwargs)
¶
Run every stage end-to-end with sensible defaults.
Source code in savana/rainfall/pipeline.py
399 400 401 402 403 404 405 406 407 408 409 410 | |
validate_against_gpcc(stations=None, products=None, start_year=2001, end_year=2020, obs_source=None, obs_csv=None, cache_dir='savana_rainfall_data', zones_gdf=None, zones_fc=None, rain_threshold=None, ee_project=None)
¶
Validate one or more precipitation products against GPCC gauge observations at one or more stations, over a chosen year range — the one-call version of the whole assessment, matching the original paper's exact logic (16 WA stations, 6 products, 2001-2020) as the default, everything else overridable by simple parameters.
This is the function to reach for first. It does exactly what the
original per-station CSV workflow did — extract each requested
product at each requested station, save/reuse a per-product CSV
(cache_dir/precip_extraction_<PRODUCT>.csv, same as before),
merge against GPCC observations
(cache_dir/gpcc_obs_<start>_<end>.csv), and write the same
result CSVs the original scripts did (validation_by_zone.csv,
validation_overall.csv, product_ranking.csv,
threshold_sensitivity.csv) — just wrapped in one call instead of
six separate scripts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations
|
where to validate. Any of:
- |
None
|
|
products
|
list[str] | None
|
which products to check, by name (e.g.
|
None
|
start_year, end_year
|
inclusive year range (plain ints — the paper used 2001-2020; pick whatever you need). |
required | |
obs_source
|
str | None
|
where GPCC observations come from —
|
None
|
obs_csv
|
required if |
None
|
|
cache_dir
|
where per-product extraction CSVs, the GPCC obs CSV,
and the result CSVs are read from / written to. Sits right
next to your notebook by default; set to |
'savana_rainfall_data'
|
|
zones_gdf, zones_fc
|
optional zone geometry (see
:mod: |
required | |
rain_threshold
|
float | None
|
mm/day wet/dry threshold for categorical metrics. Defaults to the WMO standard (1.0 mm/day). |
None
|
ee_project
|
str | None
|
Google Cloud project registered for Earth Engine use
(only needed for |
None
|
Returns:
| Type | Description |
|---|---|
|
A fully populated :class: |
|
|
|
|
|
directly, or call |
|
|
|
|
|
one by hand. |
Example::
from savana.rainfall import validate_against_gpcc
# Reproduce the paper exactly:
result = validate_against_gpcc()
# One station, two products, a shorter recent period:
result = validate_against_gpcc(
stations=(-1.5, 12.4),
products=["CHIRPS", "GPM_IMERG"],
start_year=2018, end_year=2023,
)
print(result.validation_overall_df)
Source code in savana/rainfall/pipeline.py
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 | |
savana.rainfall.config
¶
Default configuration for global precipitation product assessment.
Everything here is a default, every public function in
savana.rainfall accepts overrides, so a user assessing a different
region, a different subset of products, their own gauge network, or
their own application weights is not locked into the West Africa study
configuration. The WA study (16 GPCC FDD v2022 stations, 5 ecological
zones, 6 global precipitation products, 7 conservation/water-management
applications) ships as the default so savana.rainfall is useful out
of the box and reproduces the original manuscript, but nothing here is
required to use the package on a different AOI.
Nothing in this module touches Earth Engine or hits the network, it is
pure data, safe to import eagerly. EE objects (ee.FeatureCollection,
ee.Geometry) are built lazily, inside functions in :mod:.stations
and :mod:.ingestion, exactly where the JS/EE equivalents built them.
default_stations_wa()
¶
The 16 West Africa GPCC gauge stations, as a pandas.DataFrame.
This is the manuscript's real validation network, not a synthetic
placeholder, the default stations_df used throughout
savana.rainfall when no stations_df is supplied. Any function
accepting stations_df= accepts a DataFrame with this same shape
(station_id, station_name, lon, lat, elevation_m, source) for a
different gauge network.
Source code in savana/rainfall/config.py
410 411 412 413 414 415 416 417 418 419 420 421 422 | |
savana.rainfall.stations
¶
Gauge station metadata and precipitation observation loading.
Every function here works on an arbitrary stations_df — a
pandas.DataFrame with at minimum station_id, lon, lat columns
(station_name, elevation_m, source are recommended but not
required). savana.rainfall.config.default_stations_wa() supplies the
16-station West Africa GPCC network as a convenient default so the
package works out of the box, but nothing here assumes those specific
stations. Point this module at your own gauge network by building a
DataFrame in that shape and passing it as stations_df= throughout.
Four ways to get observations, in increasing order of "how much can this handle a station set that isn't the WA 16":
- :func:
load_stations_from_csv— you already have your own station metadata + observation CSVs. Fully general. - :func:
download_gpcc— downloads the public GPCC Full Data Daily v2022 archive and extracts at whatever station coordinates you give it. Fully general, works for any station anywhere GPCC has coverage, but downloads ~440 MB and is slow the first time. - :func:
load_gpcc_obs_from_asset— fast, but only returns rows for station_ids that exist in the given EE table asset. The packaged default asset (:data:config.DEFAULT_GPCC_ASSET_WA) covers only the 16 WA stations; pointasset_idat your own pre-extracted table for a different network, or use option 1/2 instead.
All three return real gauge observations. There is deliberately no synthetic/simulated observation option: GPCC gauge data is the ground truth this package validates against, so fabricating it would make every resulting metric meaningless.
download_gpcc(stations_df=None, start_year=2001, end_year=2020, data_dir=None, keep_raw=False)
¶
Download the public GPCC archive and extract at any station set.
Works for any stations_df (defaults to the WA 16), anywhere the
GPCC 1.0-degree grid has coverage — this is the fully general path,
unlike :func:load_gpcc_obs_from_asset which only covers whatever
stations happen to already be in an EE asset.
Downloads are cached: files already present in data_dir are
skipped, so re-running after a partial failure only fetches what's
missing. Requires requests, xarray, netCDF4 (installed
with pip install "savana[rainfall]").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations_df
|
defaults to :func: |
None
|
|
start_year, end_year
|
inclusive year range. |
required | |
data_dir
|
str | Path | None
|
local cache directory. Defaults to
|
None
|
keep_raw
|
bool
|
if False (default), deletes the raw yearly NetCDF files after extraction to save disk space (~20 MB/year). |
False
|
Returns:
| Type | Description |
|---|---|
|
pandas.DataFrame with columns station_id, year, month, obs_mm_day. |
Source code in savana/rainfall/stations.py
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 | |
get_observations(stations_df=None, source='download', **kwargs)
¶
Single entry point for getting gauge observations, any station set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations_df
|
defaults to :func: |
None
|
|
source
|
str
|
one of
- |
'download'
|
**kwargs
|
forwarded to the selected loader. |
{}
|
Returns:
| Type | Description |
|---|---|
|
pandas.DataFrame with columns station_id, year, month, obs_mm_day. |
Source code in savana/rainfall/stations.py
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 | |
load_gpcc_obs_from_asset(stations_df=None, asset_id=None)
¶
Load pre-extracted GPCC observations from an Earth Engine table asset.
Fast (no download, no NetCDF processing) but only returns rows for
station_id values that already exist in the asset. Any station in
stations_df not found in the asset is reported via a printed
warning, not silently dropped without explanation — use
:func:download_gpcc for those instead, or build your own asset with
station_id, year, month, obs_mm_day columns and pass its ID here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations_df
|
defaults to :func: |
None
|
|
asset_id
|
str | None
|
EE table asset ID. Defaults to
:data: |
None
|
Returns:
| Type | Description |
|---|---|
|
pandas.DataFrame with columns station_id, year, month, obs_mm_day. |
Source code in savana/rainfall/stations.py
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
load_stations_any(stations=None)
¶
Turn almost anything describing station locations into a proper
stations_df — the single entry point every high-level function
(:func:savana.rainfall.pipeline.validate_against_gpcc) uses so a
user never has to hand-build a DataFrame just to try one station.
Accepts
None-> :func:savana.rainfall.config.default_stations_wa(the 16 WA GPCC stations).- an existing
stations_df(DataFrame withstation_id, lon, lat) -> validated and returned as-is. - a path to a
.geojson/.jsonfile of Point features -> one station per feature;station_id/station_nameare read from feature properties if present, else auto-generated. - a path to a
.csvfile -> loaded via :func:load_stations_from_csv's station-table shape. - a list of
(lon, lat)or(station_id, lon, lat)tuples, or a list of dicts with at leastlon/latkeys. - a single station as
(lon, lat)or[lon, lat]— both a tuple and a plain 2-element list work.
Returns:
| Type | Description |
|---|---|
|
A validated |
Source code in savana/rainfall/stations.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
load_stations_from_csv(stations_csv, obs_csv=None)
¶
Load your own station metadata (and optionally observations) from CSV.
stations_csv must have columns
station_id, station_name, lon, lat, elevation_m, source
(matching :data:config.DEFAULT_STATION_COLUMNS).
obs_csv, if given, must have columns
station_id, year, month, obs_mm_day.
This is the fully general entry point for a station network that isn't West Africa's 16 GPCC stations at all — bring your own gauge metadata and (optionally) your own already-extracted observations.
Source code in savana/rainfall/stations.py
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 | |
preview_map(stations_df=None, m=None, zoom=5)
¶
A quick interactive map of station locations — the first thing to check before extracting or validating anything: "are these actually where I think they are?"
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations_df
|
defaults to :func: |
None
|
|
m
|
an existing |
None
|
|
zoom
|
int
|
zoom level when centering on the stations. |
5
|
Returns:
| Type | Description |
|---|---|
|
A |
|
|
Click a point on the map to see its properties |
|
|
( |
|
|
geemap's built-in inspector panel. |
Source code in savana/rainfall/stations.py
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
stations_to_ee_fc(stations_df)
¶
Convert any stations DataFrame to an ee.FeatureCollection of points.
Works for any stations_df meeting :data:REQUIRED_STATION_COLUMNS
— not specific to the WA network. Extra columns are copied through
as feature properties.
Source code in savana/rainfall/stations.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | |
savana.rainfall.zones
¶
Ecological/climatic zone construction and station assignment.
No packaged shapefile, zones are built, from whatever base regions
and split logic you give :func:build_zones_from_bands. This is a
direct, generalised port of the author's GEE zone-delineation script:
3 named base climatic regions, each optionally split by a latitude band
into one or more final ecological zones. The West Africa 5-zone scheme
(:data:config.DEFAULT_ZONE_DEFS_WA + :data:config.DEFAULT_ZONE_BASE_ASSETS_WA)
is just one configuration of this generic builder, not special-cased
logic, a different region, a different number of base regions, or no
latitude splitting at all, all use the same function.
Three ways to get zone geometry, in order of generality:
- :func:
build_zones_from_bands, build zones yourself from any named base regions (EE assets, oree.FeatureCollection/ee.Geometryobjects you already have) plus your own split rules. Fully general, works for any region, any number of zones, any split logic (or none). - :func:
load_zones_from_file, you already have a zone boundary file (shapefile, GeoJSON, GeoPackage, ...) from QGIS or elsewhere. Fully general, no Earth Engine involved at all. - :func:
single_region_zone, you don't want zone stratification at all, just one study-area boundary. Wraps any boundary (a file path, a geojson dict, anee.Geometry, or a bounding box) into a one-zone table so the rest of the package (which only ever asks "is this station's zone-name X") doesn't need a special no-zone code path.
:func:assign_zones then attaches a zone label to a stations DataFrame
from any of the above, or falls back to a documented latitude-band
heuristic if no zone geometry is available at all.
assign_zones(stations_df, zones_gdf=None, zones_fc=None, name_field=None, use_default_if_none=False)
¶
Add a zone column to stations_df.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations_df
|
any DataFrame with |
required | |
zones_gdf
|
a local |
None
|
|
zones_fc
|
a live |
None
|
|
name_field
|
str | None
|
property/column holding the zone name. Defaults to
:data: |
None
|
use_default_if_none
|
bool
|
if True and neither |
False
|
Returns:
| Type | Description |
|---|---|
|
Copy of |
|
|
that can't be matched to a real zone polygon falls back to the |
|
|
latitude-band heuristic, with a printed warning, this always |
|
|
returns a usable |
Source code in savana/rainfall/zones.py
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | |
build_zones_from_bands(base_zones, zone_defs, bounds=None)
¶
Build a final zones ee.FeatureCollection from named base
regions, each optionally split by a latitude band.
This is the generic version of the GEE script's whole zone-building pipeline (its Sections 1, 3, 4), nothing here is specific to West Africa or to exactly 3 base regions / 5 output zones.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_zones
|
dict
|
|
required |
zone_defs
|
list[dict]
|
list of dicts, each describing one output zone:
- |
required |
bounds
|
tuple[float, float, float, float] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
|
|
|
|
feature per |
|
|
as properties. |
Source code in savana/rainfall/zones.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | |
default_wa_zones(bounds=None)
¶
Build the West Africa 5-zone scheme from the author's own base
EE assets (:data:config.DEFAULT_ZONE_BASE_ASSETS_WA) using
:data:config.DEFAULT_ZONE_DEFS_WA.
This is just the WA study's configuration of
:func:build_zones_from_bands, call that function directly with
your own base_zones/zone_defs for a different region.
Requires the 3 base assets to actually exist and be readable by the
caller's EE account, they're the author's own uploaded shapefiles,
not a public dataset. If you're not the author, either ask for read
access, upload your own copies and pass your own
base_zones dict, or use a completely different region's data.
Source code in savana/rainfall/zones.py
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
export_zones(zones_fc, asset_id=None, drive_folder=None, drive_description='ecological_zones', file_format='GeoJSON')
¶
Export a built zones FeatureCollection, port of the GEE script's
three export buttons (asset / GeoJSON / Shapefile), as background
ee.batch tasks rather than a UI panel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
zones_fc
|
from :func: |
required | |
asset_id
|
str | None
|
if given, submits an |
None
|
drive_folder, drive_description, file_format
|
if
|
required |
Returns:
| Type | Description |
|---|---|
|
list of submitted |
|
|
check |
|
|
batch export). |
Source code in savana/rainfall/zones.py
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
load_zones_from_file(path)
¶
Load your own zone boundaries from any vector file geopandas can
read (shapefile, GeoJSON, GeoPackage, ...). Fully general, for a
user who already has zone geometry from QGIS or elsewhere and
doesn't need :func:build_zones_from_bands at all.
Source code in savana/rainfall/zones.py
300 301 302 303 304 305 306 307 308 | |
single_region_zone(boundary, zone_name='Study Area')
¶
Wrap one boundary as a one-row zones table, for a user who wants a specific area of interest but no zone stratification.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
boundary
|
any of, a local vector file path (shapefile,
GeoJSON, ...), a GeoJSON-like dict, an |
required | |
zone_name
|
str
|
the single zone label everything in |
'Study Area'
|
Returns:
| Type | Description |
|---|---|
|
A |
|
|
func: |
|
|
an |
|
|
returned instead (usable with |
Source code in savana/rainfall/zones.py
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | |
zone_areas_km2(zones_fc, name_field=None)
¶
Add an area_km2 property to every feature, port of the GEE
script's area-reporting section. Returns the FeatureCollection with
the extra property; call .getInfo() or use
:func:zones_fc_to_gdf to inspect it locally.
Source code in savana/rainfall/zones.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
zones_fc_to_gdf(zones_fc, name_field=None)
¶
Pull a (typically small, a handful of zone polygons) EE
FeatureCollection down to a local geopandas.GeoDataFrame, for
use with :func:assign_zones's local-join path, or for saving to a
file yourself.
Source code in savana/rainfall/zones.py
311 312 313 314 315 316 317 318 319 320 321 322 323 | |
savana.rainfall.ingestion
¶
Precipitation product ingestion, harmonise any product catalogue to a common monthly mean mm/day ImageCollection.
Every function accepts a products dict (see
:data:savana.rainfall.config.DEFAULT_PRODUCTS for the required shape)
and a roi, so this works for a different product catalogue or a
different region, not just the WA six-product/study-area default.
Nothing here calls ee.Initialize(), that's the caller's
responsibility (see :mod:savana.ee_init, reused as-is), consistent
with the rest of savana never initialising EE as a side effect of
import.
build_roi(stations_df=None, bounds=None, buffer_deg=2.0)
¶
Build an ee.Geometry region of interest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stations_df
|
if given (and |
None
|
|
bounds
|
explicit |
None
|
|
buffer_deg
|
float
|
degrees of padding added around the station bbox. |
2.0
|
If neither is given, falls back to the West Africa study bounds used in the manuscript (5-25N, 20W-15E).
Source code in savana/rainfall/ingestion.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
export_merra2_yearly_assets(years, roi, asset_folder, products=None)
¶
Export one daily-aggregated MERRA-2 asset per year to
asset_folder, as a background EE batch task per year.
Splitting the export by year keeps each task well under GEE's
per-request compute/timeout limits. Returns the list of submitted
ee.batch.Task objects, check task.status() for progress;
this can take hours for a full 20-year run and is meant to be
fire-and-forget, not awaited synchronously.
Source code in savana/rainfall/ingestion.py
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
load_all_products(start, end, roi=None, products=None, stations_df=None)
¶
Load every product in products (default: all 6) as monthly
mm/day ImageCollections.
Returns {product_name: ee.ImageCollection}. roi defaults to
:func:build_roi from stations_df if not given.
Source code in savana/rainfall/ingestion.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
load_merra2_from_assets(asset_folder, start, end, roi)
¶
Rebuild the monthly mm/day MERRA-2 ImageCollection from
pre-exported yearly daily-aggregate assets (see
:func:export_merra2_yearly_assets).
Source code in savana/rainfall/ingestion.py
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
load_product(name, start, end, roi, products=None)
¶
Load one precipitation product as a monthly mean mm/day
ee.ImageCollection, dispatched by its conversion type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
key into |
required |
start, end
|
'YYYY-MM-DD', clipped to the product's real
availability window (see
:data: |
required | |
roi
|
|
required | |
products
|
dict | None
|
catalogue dict. Bring your own for a different
product set, must have the shape of
:data: |
None
|
Source code in savana/rainfall/ingestion.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
savana.rainfall.extraction
¶
Point-sample precipitation products at gauge stations, and merge with
observations into the long-format table :mod:.validation consumes.
Works for any stations_df, not just the WA 16, extraction is
purely a function of whatever station coordinates you give it.
extract_all_products(products_ic, stations_df, cache_dir=None)
¶
Extract every product in products_ic (from
:func:savana.rainfall.ingestion.load_all_products) at every
station, and stack into one long-format DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
products_ic
|
dict
|
|
required |
stations_df
|
any stations DataFrame. |
required | |
cache_dir
|
if given, each product's extraction is cached to
|
None
|
Returns:
| Type | Description |
|---|---|
|
Long-format DataFrame: ``station_id, year, month, product, |
|
|
sim_mm_day``. |
Source code in savana/rainfall/extraction.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
extract_product_at_stations(product_ic, stations_df, product_name)
¶
Point-sample one monthly mm/day ee.ImageCollection at every
station in stations_df.
Returns a long-format pandas.DataFrame:
station_id, year, month, product, sim_mm_day.
Source code in savana/rainfall/extraction.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
merge_with_observations(sim_long_df, obs_df, stations_df=None)
¶
Merge long-format simulated values with observations, optionally
attaching station metadata (including zone if already assigned).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sim_long_df
|
from :func: |
required | |
obs_df
|
from :mod: |
required | |
stations_df
|
optional, to bring along |
None
|
Returns:
| Type | Description |
|---|---|
|
Long-format DataFrame ready for :mod: |
|
|
|
|
|
(+ any joined station columns). |
Source code in savana/rainfall/extraction.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
savana.rainfall.validation
¶
Continuous and categorical validation metrics.
Two metric classes, matching the manuscript's dual-class framework:
- Continuous (:func:
compute_continuous): bias, pbias, mae, rmse, r, r2, nse, kge, how well magnitude and pattern agree. - Categorical (:func:
compute_categorical): pod, far, csi, ets, freq_bias, how well wet/dry events are detected above a threshold.
Both take plain obs/sim array-likes, so they work regardless of
which stations, products, or zones produced them. The four levels of
spatial/temporal aggregation used in the manuscript (per-station,
per-zone, per-season, pooled) are all just different group_cols to
the single :func:validate_grouped function, there's no separate
per-station/per-zone/per-season implementation to keep in sync.
add_season_column(merged_df, month_col='month')
¶
Add a season column (DJF/MAM/JJA/SON) derived from month_col.
Source code in savana/rainfall/validation.py
224 225 226 227 228 | |
compute_all_metrics(obs, sim, threshold=None)
¶
Continuous + categorical metrics for one obs/sim pair, merged.
Source code in savana/rainfall/validation.py
163 164 165 166 167 | |
compute_categorical(obs, sim, threshold=None)
¶
Categorical wet/dry detection metrics from a 2x2 contingency table.
A record is "wet" if the value >= threshold (default
:data:config.DEFAULT_RAIN_THRESHOLD_MM_DAY, the WMO standard of
1.0 mm/day), else "dry".
Source code in savana/rainfall/validation.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
compute_continuous(obs, sim)
¶
Continuous performance metrics for one obs/sim pair.
Rows where either obs or sim is missing are dropped before
computation, matching the manuscript's paired-record approach.
Returns {"n": 0, ...all-NaN} if fewer than 2 valid pairs remain
(metrics like r/NSE/KGE are undefined below that).
Source code in savana/rainfall/validation.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
rank_products(validation_df, metric='kge', group_cols=None)
¶
Rank products within each group by a single metric (default KGE, the manuscript's primary ranking metric, see methods 2.4.1).
group_cols defaults to every column in validation_df except
"product" and the metric columns, i.e. whatever grouping level
the input DataFrame already represents (zone, station, season...).
An empty result (e.g. pooled/overall validation with no zone column)
ranks across the whole table as a single group, rather than failing.
Source code in savana/rainfall/validation.py
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | |
validate_by_season(merged_df, threshold=None)
¶
Metrics per (zone, season, product).
Source code in savana/rainfall/validation.py
250 251 252 253 254 255 256 | |
validate_by_station(merged_df, threshold=None)
¶
Metrics per (station_id, product), manuscript's finest level.
Source code in savana/rainfall/validation.py
231 232 233 | |
validate_by_zone(merged_df, threshold=None)
¶
Metrics per (zone, product), the manuscript's primary analytical lens.
merged_df must already have a zone column
(see :func:savana.rainfall.zones.assign_zones).
Source code in savana/rainfall/validation.py
236 237 238 239 240 241 242 243 244 245 246 247 | |
validate_grouped(merged_df, group_cols, obs_col='obs_mm_day', sim_col='sim_mm_day', threshold=None)
¶
Compute continuous + categorical metrics for each group in merged_df.
merged_df must be long-format with one row per
(station, year, month, product) and columns obs_col/sim_col
(see :func:savana.rainfall.extraction.merge_with_observations).
This single function implements all four aggregation levels used in
the manuscript, pass the group_cols that define the level:
- per-station:
["station_id", "product"] - per-zone:
["zone", "product"] - per-season:
["zone", "season", "product"] - pooled/overall:
["product"]
Returns a DataFrame with one row per group, group_cols + all metrics.
Source code in savana/rainfall/validation.py
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
validate_overall(merged_df, threshold=None)
¶
Metrics per product, pooled across all stations/zones.
Source code in savana/rainfall/validation.py
259 260 261 | |
savana.rainfall.thresholds
¶
Rain-detection threshold sensitivity analysis.
Categorical metrics (POD, FAR, CSI, ETS) depend on the wet/dry
threshold used to classify a record, this matters most in dryland
zones where near-zero rainfall makes categorical detection structurally
unstable (see manuscript sections 2.5 and the Saharian zone note in
:data:savana.rainfall.config.DEFAULT_ZONE_NOTES). This module sweeps
:func:savana.rainfall.validation.compute_categorical across multiple
thresholds rather than duplicating its logic.
stability_summary(threshold_df, metric='csi', group_cols=None)
¶
Rank product/group robustness across the threshold sweep.
Returns one row per group with the metric's mean, std, and coefficient of variation across all swept thresholds, a low CV means the product's performance on that metric is stable regardless of exactly where the wet/dry line is drawn; a high CV flags threshold-sensitive rankings (per manuscript 2.5's structural instability finding in near-zero-rainfall zones).
Source code in savana/rainfall/thresholds.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
threshold_sensitivity(merged_df, thresholds=None, group_cols=None, obs_col='obs_mm_day', sim_col='sim_mm_day')
¶
Categorical metrics at each of several rain-detection thresholds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
merged_df
|
long-format obs/sim data (see
:func: |
required | |
thresholds
|
list[float] | None
|
mm/day values to sweep. Defaults to
:data: |
None
|
group_cols
|
list[str] | None
|
grouping level, e.g. |
None
|
Returns:
| Type | Description |
|---|---|
|
DataFrame with one row per (group, threshold) combination. |
Source code in savana/rainfall/thresholds.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
savana.rainfall.spatial
¶
Pixel-wise spatial diagnostics, the scripted counterpart to the interactive GEE app's on-demand map layers (bias/correlation/trend/ agreement), so those ~20 exploratory analyses produce real exportable outputs instead of only living as live map interaction.
Every function takes an explicit reference ImageCollection rather
than assuming GPCC gridded data is available, the manuscript's GPCC
reference is point-station only (see :mod:.stations), so by default
these functions compare products against each other (inter-product
agreement) or use whichever gridded product you designate as the
reference for a given call.
agreement_map(products_ic)
¶
Inter-product agreement: pixel-wise standard deviation across all products' mean-period image, normalised by the ensemble mean (coefficient of variation). Low CV = products agree spatially.
Source code in savana/rainfall/spatial.py
311 312 313 314 315 316 317 318 319 320 321 322 323 | |
bias_map(product_ic, reference_ic)
¶
Mean pixel-wise bias (product - reference) over the full period, in mm/day.
Source code in savana/rainfall/spatial.py
260 261 262 263 264 265 266 | |
correlation_map(product_ic, reference_ic)
¶
Pixel-wise Pearson correlation between two monthly ImageCollections
over the full period, via ee.Reducer.pearsonsCorrelation on a
time-matched image pair stack.
Source code in savana/rainfall/spatial.py
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | |
preview_bias_map(product_ic, reference_ic, product_name='', reference_name='', region=None, m=None)
¶
A quick INTER-PRODUCT bias map (product minus another gridded product), before running formal validation, "roughly where do these two products disagree spatially?" Direct port of the GEE app's Bias Map button.
IMPORTANT: this is product vs. product, never product vs. GPCC.
GPCC exists in this package only as point gauge observations (see
:mod:.stations), there's no gridded GPCC raster to difference a
product against pixel-by-pixel. For the actual "does this product
agree with real GPCC ground truth" spatial check, use
:func:preview_station_bias_map instead, which plots true bias at
each gauge location. This function is for a different, valid
question, "how much do CHIRPS and GPM-IMERG disagree with each
other spatially", not a validation check.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
product_ic, reference_ic
|
monthly mm/day ImageCollections (both gridded products, neither is GPCC). |
required | |
product_name, reference_name
|
labels for the map layers. |
required | |
region
|
clip to this |
None
|
|
m
|
an existing |
None
|
Returns:
| Type | Description |
|---|---|
|
The |
Source code in savana/rainfall/spatial.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
preview_mean_map(product_ic, product_name='', region=None, kind='daily', m=None, obs_df=None, stations_df=None)
¶
A quick look at one product's long-term mean rainfall on an interactive map, before running any validation, just "does this product's spatial pattern look sane over my area?"
Direct port of the GEE app's Annual Total / Mean Daily Rate map buttons.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
product_ic
|
a monthly mm/day |
required | |
product_name
|
str
|
label for the map layer. |
''
|
region
|
clip to this |
None
|
|
kind
|
str
|
|
'daily'
|
m
|
an existing |
None
|
|
obs_df, stations_df
|
if BOTH given, overlays real GPCC station values as colored markers on top of the raster, NOT a rasterized/interpolated GPCC surface (GPCC stays point data throughout this package), just each gauge's true mean observed value, plotted at its real location, colored on the same scale as the raster underneath it, so you can eyeball whether the raster's color at a station roughly matches that station's actual marker color. Click a marker (with geemap's Inspector tool active) to see both the exact GPCC value and the raster's pixel value at that same point side by side. |
required |
Returns:
| Type | Description |
|---|---|
|
The |
|
|
overlay, if requested). |
Source code in savana/rainfall/spatial.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
preview_station_bias_map(merged_df, product, m=None, zoom=5)
¶
Per-station mean bias against REAL GPCC observations, plotted as
colored markers, the spatial check that's actually anchored to
ground truth, unlike :func:preview_bias_map (which can only ever
compare two gridded products against each other, since GPCC has no
gridded form in this package).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
merged_df
|
long-format obs/sim table with station coordinates
already joined in, i.e. from
:func: |
required | |
product
|
str
|
which product's bias to show. |
required |
m
|
an existing |
None
|
Returns:
| Type | Description |
|---|---|
|
A |
|
|
product overestimates GPCC there, red if it underestimates. |
|
|
Click a marker to see the exact bias value. |
Source code in savana/rainfall/spatial.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | |
threshold_sensitivity_map(product_ic, reference_ic, thresholds=None)
¶
Pixel-wise CSI at each of several thresholds, spatial counterpart
to :func:savana.rainfall.thresholds.threshold_sensitivity.
Returns {threshold: ee.Image} of pixel-wise CSI.
Source code in savana/rainfall/spatial.py
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | |
trend_map(product_ic, unit='mm/day/year')
¶
Pixel-wise linear trend over time via
ee.Reducer.linearFit (time in years since first image).
Source code in savana/rainfall/spatial.py
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | |
zonal_rank_table(products_ic, zones_gdf, reference_ic=None, name_field=None)
¶
Zone-mean bias/trend per product, as a flat table, the scripted counterpart to the GEE app's zonal-ranking map layer.
Requires zones_gdf (see
:func:savana.rainfall.zones.default_zones_wa). If reference_ic
is given, also includes zone-mean bias against it.
Source code in savana/rainfall/spatial.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | |
savana.rainfall.decision
¶
Application-weighted product scoring and the interactive decision support workbook.
:func:score_products implements two normalisation modes:
"fixed"(default): each metric normalised against a fixed plausible range (KGE against -1..1, NSE against -5..1, |PBIAS| against 0..60, etc — see :data:savana.rainfall.config.DEFAULT_NORMALIZATION_BOUNDS). This is what the shipped decision workbook and reported figures actually use — verified by reproducingWA_Precipitation_Decision_Tool_v2.xlsx's SELECTOR-sheet scores exactly (Fire-risk x Saharian x CHIRPS = 0.7551)."zone_relative": per-zone min-max across whatever products are being compared, matching the manuscript's written formula (section 2.6). Scale-invariant — recommended if you add/remove products from the default six, since fixed bounds were tuned for the original product set's plausible range.
:func:build_workbook writes a live spreadsheet tool (data-validation
dropdowns + INDEX/MATCH formulas that recompute instantly), not a
static report — the same design as the current
WA_Precipitation_Decision_Tool_v2.xlsx, generalised to any
products/zones/apps rather than hardcoded to the WA six.
best_product(scores_df, app, zone=None)
¶
The top-scoring product for a given application (and optionally
zone). Returns (product, score) or (None, None) if no match.
Source code in savana/rainfall/decision.py
136 137 138 139 140 141 142 143 144 145 146 | |
build_workbook(out_path, validation_by_zone_df, validation_overall_df=None, ranking_df=None, threshold_df=None, scores_df=None, app_weights=None, zone_notes=None)
¶
Write the interactive decision-support workbook.
Sheet layout matches the current (v2) design: flat DATA_* sheets
holding the real numbers, APP_WEIGHTS as a visible reference
table, SCORES (flat app/zone/product/score — restores
compatibility with fig_application_rankings_v4.py, which reads
this exact sheet name/shape), and two live sheets driven by
data-validation dropdowns + INDEX/MATCH formulas:
SELECTOR (pick an application + zone, see every product ranked)
and SCORECARD (pick a zone + product, see its raw metrics).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
out_path
|
destination .xlsx path. |
required | |
validation_by_zone_df
|
from
:func: |
required | |
validation_overall_df, ranking_df, threshold_df
|
optional companion tables (validate_overall, rank_products, threshold_sensitivity outputs) — written as-is if given. |
required | |
scores_df
|
from :func: |
None
|
|
app_weights, zone_notes
|
default to
:data: |
required |
Source code in savana/rainfall/decision.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
score_products(validation_df, weights=None, normalization='fixed', bounds=None, group_cols=None)
¶
Application-weighted composite score for every (zone, product)
combination, for every application in weights.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
validation_df
|
per-zone (or per-station/pooled) metrics table,
e.g. from :func: |
required | |
weights
|
dict | None
|
|
None
|
normalization
|
str
|
|
'fixed'
|
bounds
|
dict | None
|
only used when |
None
|
group_cols
|
list[str] | None
|
columns identifying each row's context (default:
|
None
|
Returns:
| Type | Description |
|---|---|
|
Long-format DataFrame: |
Source code in savana/rainfall/decision.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
savana.rainfall.insights
¶
Grounded facts, summary, and Q&A for a rainfall assessment.
Mirrors :mod:savana.insights's pattern exactly: :func:compute_facts
computes everything once, with every section independently wrapped so
one failure (e.g. no threshold data was run) doesn't take down the
rest; :func:summarize turns facts into readable prose;
:func:answer does grounded keyword-based retrieval against the facts
dict. Every number in the output traces back to something actually
computed, never fabricated, the same rule as savana.insights.
answer(facts, question)
¶
Grounded keyword-based answer to a question about the assessment.
Matches application names and zone names appearing (case-insensitive,
substring) in question against facts, and reports only what
was actually computed. Falls back to :func:summarize if nothing
specific matches.
Source code in savana/rainfall/insights.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
compute_facts(scores_df, validation_df, ranking_df=None, threshold_df=None, zone_notes=None)
¶
Compute every grounded fact available from a rainfall assessment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores_df
|
from :func: |
required | |
validation_df
|
from
:func: |
required | |
ranking_df
|
optional, from
:func: |
None
|
|
threshold_df
|
optional, from
:func: |
None
|
|
zone_notes
|
dict | None
|
defaults to :data: |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
dict with keys: ``n_products, n_zones, apps, zones, products, |
dict
|
best_by_app_zone, best_by_app_pooled, top_kge_by_zone, |
dict
|
threshold_stability, zone_notes, warnings``. |
Source code in savana/rainfall/insights.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
summarize(facts)
¶
Turn compute_facts() output into a readable text summary.
Source code in savana/rainfall/insights.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
savana.rainfall.viz
¶
Static matplotlib figures for a rainfall assessment.
Reads directly from the DataFrames produced by :mod:.validation and
:mod:.decision, no Excel round-trip required, though
:func:recommendation_heatmap also happily reads a SCORES sheet
exported by :func:savana.rainfall.decision.build_workbook if that's
more convenient (same shape either way: app, zone, product, score).
application_ranking_bars(scores_df, app)
¶
Bar chart of every product's score for one application, one bar group per zone.
Source code in savana/rainfall/viz.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
metric_heatmap(validation_df, metric='kge', group_col='zone')
¶
Zone x product heatmap of one metric.
Source code in savana/rainfall/viz.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
preview_comparison(merged_df, station_id=None, product=None)
¶
A quick "does this look right?" comparison of observed vs simulated values, before computing formal validation metrics. Scatter with a 1:1 reference line, one color per product (or filtered to one product/station if given). Mirrors the GEE app's per-station validation scatter chart.
Source code in savana/rainfall/viz.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
preview_observations(obs_df, station_id=None)
¶
A quick time-series look at raw GPCC observations, before
extracting or validating any product, "does the reference data
itself look sane?" One line per station, or a single station if
station_id is given.
Source code in savana/rainfall/viz.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
recommendation_heatmap(scores_df)
¶
App x zone grid, each cell showing the single best product + its score, the "decision matrix" view, folding in fig_application_rankings_v4.py's recommendation-heatmap figure.
Source code in savana/rainfall/viz.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
taylor_diagram(validation_df, zone=None, ref_std=1.0)
¶
Simplified Taylor diagram (correlation vs normalised std dev) for every product, optionally filtered to one zone.
Source code in savana/rainfall/viz.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
zonal_boxplot(validation_df, metric='kge')
¶
Distribution of one metric across zones, one box per product.
Source code in savana/rainfall/viz.py
109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |