Firefighters call it a holdover: a fire that looks done at dusk and is burning again at dawn. This build asks whether tonight's satellite picture of a fire, fused with tomorrow's weather forecast, can say which of today's active fires will still be there tomorrow. Across five Mediterranean countries, five fire seasons and a held-out year, the fusion beats the fire's own persistence and the weather alone, which is the claim: neither feed contains the signal by itself.
holdover_gbm, a calibrated gradient-boosted classifier over active fire cells
(0.1 degree, VIIRS S-NPP detections) in Portugal, Spain, France, Italy and
Greece, 2020 to now. The unit is an active cell-day; the label is that the same
cell has at least one detection the next day. Features are the fire's own state
today (detections, radiative power, 3x3 neighbourhood, streak length) and
tomorrow's weather at the cell (max temperature, min relative humidity, wind,
gusts, precipitation, reference evapotranspiration, trailing 30-day dryness).
Split by time, never shuffled: train 2020-2023, test 2024.
What this covers. Weather is backfilled for the cells reachable under the Open-Meteo free tier (Iberia-heavy: Portugal full, Spain two thirds, Italy and Greece about half, France sparse). The five-country backfill was quota-capped, not completed, so this is the model on the data we have. The training job re-runs nightly and gates itself either way.
| metric (holdout: 2024 season, 22.2k train / 2.2k test, base rate 0.129) | value |
|---|---|
| PR-AUC, fused | 0.557 |
| ROC-AUC, fused | 0.822 |
| PR-AUC, persistence only | 0.540 |
| PR-AUC, weather only | 0.200 |
| PR-AUC, shuffled-label control (mean of 25 seeds) | 0.147 |
| precision of the top 1% most confident flags | 0.91 |
| precision of the top 5% | 0.79 |
The ablation is the gate. Persistence knows the fire is big; weather knows the night will be dry and windy. FUSED beats both single-source models and beats a shuffled-label control (averaged over 25 seeds, because a single shuffle draw is noisy enough to coin-flip the gate on a test this size). If it did not, the training job would print DOG-HOUSE and the build would go to the dog house instead of this table.
The margin is thin and honest: weather adds 0.017 PR-AUC over persistence alone, and leave-one-country-out shows the lift is uneven. It helps in France (+0.049) and Portugal (+0.013), is flat in Italy and Greece, and on Spain the weather features slightly hurt cross-country transfer (-0.021). Tomorrow's weather carries real but modest signal on top of the fire simply being big.
The app does not just show forecasts. Every morning the previous day's issued probabilities are resolved against what VIIRS actually saw overnight, and the scoreboard (mean predicted vs realized, top-decile precision, the 15 most confident calls with their outcomes) renders on the page. The model gets no second chances on yesterday.
Read these before quoting the number anywhere.
- The label is "VIIRS sees the cell again", not "the fire is out". Clouds, smoke and overpass gaps cut both ways: a smouldering fire under cloud reads as out, a re-detected hotspot on a controlled burn reads as alive.
- A cell is not a fire. Large fires span several cells; the map is a cell-level risk field, not a fire-perimeter product.
- Training weather is ERA5 reanalysis; serving uses the real forecast. Live skill sits a little below the holdout number.
- Not an operational fire product. It is a fundamentals monitor built on open data; nobody should route a crew off this map.
- The weather for day D+1 is a legitimate feature: a forecast for it exists at issue time. Training uses the ERA5 value for D+1 (hindcast), serving uses the actual forecast.
- Fire-side features are strictly day-D or earlier: detections, FRP, neighbourhood activity, streak. Nothing from D+1 except the label.
- The split is temporal (2024 held out, never shuffled), the shuffled-label control trains at the base rate, and leave-one-country-out is reported so cross-country transfer is visible.
- Same-day rows with an unresolved label (the last observed day) are marked pending and excluded from training until the next day's pass resolves them.
An FTI (feature, training, inference) system on Hopsworks. Feature extraction
is one shared module (holdover_features.py) imported by every pipeline and
the app, so training and serving cannot skew.
flowchart LR
FA([FIRMS archives + NRT feed]):::ext --> FP[fires_pipeline] --> FGF[(holdover_fires)]:::hops
OM([Open-Meteo: ERA5 + forecast]):::ext --> WP[weather_pipeline] --> FGW[(holdover_weather)]:::hops
FGF --> FV{{holdover_fv}}:::hops
FGW --> FV
FV --> TR[train: GBM + ablation gate] --> MR[(Model Registry)]:::hops
MR --> INF[predict, daily]
FGF --> INF
FGW --> INF
INF --> FGX[(holdover_predictions)]:::hops
FGX --> APP[holdover app]
classDef hops fill:#10b98122,stroke:#34d399,color:#e5e7eb;
classDef ext fill:none,stroke:#6b7280,color:#9ca3af,stroke-dasharray:4 3;
The file-by-file map:
holdover_features.py shared MITs: cell binning, per-cell-day aggregation,
persistence features, label, date normalizer.
Imported everywhere.
collect/firms.py FIRMS country archives (keyless backfill) + global
7-day NRT CSV (keyless live), Med bbox filter
pipelines/fires_pipeline.py F -> holdover_fires (backfill 2020-2024 + daily NRT)
pipelines/weather_pipeline.py F -> holdover_weather (ERA5 activity windows,
quota-paced + resumable; daily forecast)
pipelines/build_fv.py T leak-free feature view (day-D fire x day-D+1 weather)
pipelines/train.py T temporal split, PERSIST/WEATHER/FUSED ablation,
shuffle control, LOCO, self-gate, calibrate,
register holdover_gbm + plots
pipelines/predict.py I daily: tonight's fires -> P(still burning) -> FG
app/server.py + static/ I FastAPI thin client: Med ember map, watchlist,
the daily self-judging scoreboard
Makefile the whole build, one target per stage
Everything keyless end to end: FIRMS for detections (archives and the NRT feed), Open-Meteo for ERA5 and the forecast. No account, no token, fully reproducible.
Clone into a Hopsworks project on the /hopsfs/... FUSE mount. Paths
self-derive.
make fires-job weather-job # F: backfill both FGs (weather is quota-paced,
# resumable: re-run until it reports 0 to fetch)
make fv train-job # T: leak-free FV, train + gate + register
make predict-job app # I: score tonight's fires + the live map
make schedule # daily cycle: fires 03:30, weather 04:00,
# predict 04:30, train 05:00 UTCThe app (app/) renders an isometric voxel terrain of the Mediterranean on
satellite imagery. Every active fire cell is a stacked-cube column whose height
and color follow the threat, risk times fire intensity, so the big fires likely
to still be burning tomorrow stand up as bright peaks. Past days play back as a
muted intensity terrain, today is the colored forecast, and the week animates as
a flow. Country and region labels orient the view; a watchlist and a "how honest
is this" panel carry the gate verdict, the holdout numbers, and each day's
predictions resolved against the next day's detections. Server-rendered, content
in the initial payload.
The cluster deployment (feature groups, model, jobs, live app) was torn down after the run to free shared resources. The build is fully reproducible from the pipelines below on any Hopsworks project; nothing here needs a key.


