SIAGA hazard models: coupled flood and water stress, Pantura corridor
Two calibrated XGBoost classifiers that drive
SIAGA,
a Pusdalops decision board for coastal north Java. They answer two questions for
each of 324 kecamatan:
- Flood, 0 to 72 hours ahead.
- Water stress, next month.
The point of the pair is that both hazards draw on one water balance and one
fleet of trucks, pumps and crews. The probabilities feed a two-stage stochastic
CVaR program that pre-positions that shared fleet, so the two numbers have to be
comparable and honestly calibrated, not just individually accurate.
Built for RISTEK Fasilkom UI Datathon 2026.
Files
| File | What it is |
|---|
flood_model.json | XGBoost booster, flood in t+1..t+3 |
drought_model.json | XGBoost booster, water stress at month m+1 |
flood_calibrator.pkl | Isotonic calibrator, fitted out-of-fold |
drought_calibrator.pkl | Isotonic calibrator, fitted out-of-fold |
model_meta.json | Feature order for each model |
metrics.json | Full test metrics, feature importance, calibrator selection |
climatology.json | Per-district discharge threshold and SPI gamma parameters |
reliability.json, reliability.csv | Reliability curve on the held-out years |
climatology.json is needed to rebuild features from raw rainfall and
discharge, so it ships with the models rather than the data.
What the labels mean
Flood is river discharge exceeding that district's own 95th percentile at
some point in the next 72 hours, which is the return-period rule GloFAS uses for
its alerts. Discharge is independent of the rainfall features because it
integrates upstream routing.
Water stress is SPI-3 for the following month at or below -1.0, McKee's
moderate-drought threshold. SPI is the index BMKG uses operationally.
Neither label comes from disaster event records. BNPB's DIBI sits behind a
locked Superset instance, its open ArcGIS layers return HTTP 500, and the
Dartmouth Flood Observatory archive is decommissioned. Labelling from open
physical reanalysis is arguably more rigorous than a news-derived event list,
but it is a different thing and should be read as such: these models predict a
physical exceedance, not a declared disaster.
Performance
Trained on 2015 to 2022, tested on 2023 to 2024. No district or period appears
in both.
| Flood | Water stress |
|---|
| Test rows | 232,458 | 7,452 |
| Base rate | 5.2% | 18.6% |
| AUC | 0.931 | 0.959 |
| Average precision | 0.444 | 0.858 |
| Brier | 0.0364 | 0.0650 |
| Brier, uncalibrated | 0.0566 | 0.0693 |
| Reliability | 0.00051 | 0.00348 |
| Operating threshold | 0.227 | 0.500 |
| Precision at threshold | 0.353 | 0.770 |
| Recall at threshold | 0.686 | 0.792 |
Calibration matters more here than discrimination, because the downstream
optimizer multiplies these probabilities by exposed population and allocates on
the result. A miscalibrated 90% sends trucks to the wrong kecamatan. Calibrators
were selected on reliability rather than Brier score, since Brier rewards
sharpness and would have picked a better-separating but less honest model.
Read the flood precision before you trust an alert. At the operating
threshold, precision is 0.353. About two in three flood warnings are not
followed by a threshold exceedance. That is the cost of 0.686 recall on a 5.2%
base rate, and the product states it on the printed dispatch order rather than
hiding it.
The worst calibration gap above 0.5 is 0.079 for flood and 0.190 for water
stress. The water stress figure is the weaker one and is worth knowing before
acting on a high drought probability.
Inputs
Flood, per district-day: rain_1d, rain_3d, rain_7d, rain_30d,
disc_now, disc_7d_mean, disc_change_3d, month, coastal.
Water stress, per district-month: spi1, spi3, spi6, p1, dry_run,
month, coastal.
Order matters. Read it from model_meta.json rather than from this table.
Most of the flood signal is accumulated rainfall: rain_30d alone carries 0.358
of the importance, with rain_7d and rain_3d next. For water stress, spi1
carries 0.552.
Usage
1import json, pickle
2import numpy as np
3import xgboost as xgb
4from huggingface_hub import hf_hub_download
5
6REPO = "ethanchrstian/siaga-pantura-hazard-models"
7
8meta = json.load(open(hf_hub_download(REPO, "model_meta.json")))
9cols = meta["flood_feats"]
10
11booster = xgb.Booster()
12booster.load_model(hf_hub_download(REPO, "flood_model.json"))
13with open(hf_hub_download(REPO, "flood_calibrator.pkl"), "rb") as f:
14 cal = pickle.load(f)
15
16# Column order must match meta["flood_feats"].
17X = np.array([[12.0, 40.0, 88.0, 310.0, 220.0, 190.0, 35.0, 2, 1]])
18raw = booster.predict(xgb.DMatrix(X, feature_names=cols))
19
20if cal["type"] == "isotonic":
21 prob = np.clip(cal["model"].predict(raw), 1e-3, 1 - 1e-3)
22elif cal["type"] == "platt":
23 prob = cal["model"].predict_proba(raw.reshape(-1, 1))[:, 1]
24else:
25 prob = raw
26prob = np.clip(prob, 0.0, 1.0)
27# raw 0.714 -> calibrated 0.386
The calibration step is not optional, and the comment on the last line is why:
the booster's raw output on that row is 0.714, and the honest probability is
0.386. Skip the calibrator and every downstream allocation is computed from a
number roughly twice too large. metrics.json records brier_uncalibrated
alongside brier so the aggregate effect is visible too.
cal is a dict with keys type, model and op_threshold. Both models
currently use isotonic, but branch on type rather than assuming it. The
reference implementation is _apply_cal in backend/app/services/hazard.py in
the GitHub repo.
Training data
ethanchrstian/siaga-pantura-hazard-data,
which carries the feature tables, the labels, the boundaries, the population
raster extract and the precomputed probability history.
Sources: ERA5 rainfall and GloFAS v4 discharge, both via Open-Meteo; GADM v4.1
level 3 boundaries; WorldPop 2020 UN-adjusted population.
Limits
- Pantura only. 324 kecamatan across 18 kabupaten and kota on the north
coast of Java. The discharge thresholds and SPI parameters are fitted per
district and do not transfer.
- A probability is not a plan. These outputs are one input to an allocation
decision that also weighs exposure, depot capacity, travel time and shortfall
risk.
- Hindcast, not live service. The published probability history runs 2015 to
2024. Serving live would need a rainfall and discharge feed.
- Depots and fleet in the product are scenario data, not BNPB inventory,
which is not public. That affects the allocation demo, not these models.
Citation
1@misc{siaga2026,
2 title = {SIAGA: Sistem AI untuk Peringatan Dini Banjir-Kekeringan Terkopel
3 dan Optimasi Prapenempatan Sumber Daya Bencana},
4 year = {2026},
5 note = {RISTEK Fasilkom UI Datathon 2026},
6 url = {https://github.com/ethannchrstian/Siaga}
7}
License MIT. The underlying data keeps its own terms: ERA5 and GloFAS under
Copernicus, GADM non-commercial, WorldPop CC BY 4.0.