File size: 4,817 Bytes
7bbbbf6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
"""Load the frozen firepredict model and compute a chance for one commune-day.

Run from this folder:
    uv run --with scikit-learn==1.9.1 --with joblib --with pandas python example.py

The model answers: what is the chance that at least one fire of 0.1 ha or more
starts in this Var commune on the target day? The forecast is issued on the
morning of the day before the target day and uses weather up to two days
before the target day.

The five inputs (one row per commune and target day):

  log_usual_rate  log(usual_rate + 0.01). usual_rate is the commune's average
                  number of days per year with at least one fire start of
                  0.1 ha or more (BDIFF, 2006-2025 without 2024). Take it from
                  usual_rates.csv by INSEE code.
  log_local_fwi   log(1 + FWI). FWI is the Fire Weather Index (no unit) of the
                  day two days before the target day, computed from the
                  commune's nearest Météo-France stations with daily values
                  (maximum temperature, minimum humidity, mean wind, daily
                  rain). It is an approximation, not the official FWI product.
  local_wb_30d    Rain minus potential evaporation (Météo-France ETP), in mm,
                  summed over the 30 days that end two days before the target
                  day, at the commune's nearest stations. Negative = dry.
  orange_period   1 if the target day is in February or March (brush burning
                  allowed under conditions, Var prefectoral orders), else 0.
  red_period      1 if the target day is in June to September (fire use
                  banned), else 0.

A missing input is replaced with the training median stored in
metadata.json ("fill_values"), as the frozen pipeline does.

To build these inputs from raw data (weather download, station matching,
FWI), use the code at https://github.com/guillaumegay13/firepredict
(firepredict.model_communes.build_panel and firepredict.freeze.predict).

This is a research preview, not an official warning tool. Fire danger
warnings in France come from Météo-France and the prefectures.
"""

import hashlib
import json
from pathlib import Path

import joblib
import numpy as np
import pandas as pd
import sklearn

HERE = Path(__file__).parent


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


# The pipeline was saved with scikit-learn 1.9.1; older versions fail to load it.
if sklearn.__version__ != "1.9.1":
    print(f"warning: scikit-learn {sklearn.__version__} found, the model was saved with 1.9.1")

metadata = json.loads((HERE / "metadata.json").read_text())

# joblib files are pickles: only load files whose fingerprint you trust.
assert sha256(HERE / "model.joblib") == metadata["model_sha256"], "model.joblib does not match metadata.json"
assert sha256(HERE / "forecasts_2026.csv") == metadata["forecasts_2026"]["sha256"], "forecasts_2026.csv changed"

model = joblib.load(HERE / "model.joblib")
rates = pd.read_csv(HERE / "usual_rates.csv", dtype={"insee": str}).set_index("insee").usual_rate


def inputs(insee: str, fwi: float, wb_30d_mm: float, target_month: int) -> pd.DataFrame:
    """One row of the five model inputs, in the order the model expects."""
    row = {
        "log_usual_rate": np.log(rates.get(insee, np.nan) + 0.01),
        "log_local_fwi": np.log1p(fwi),
        "local_wb_30d": wb_30d_mm,
        "orange_period": int(target_month in (2, 3)),
        "red_period": int(target_month in (6, 7, 8, 9)),
    }
    X = pd.DataFrame([row])[metadata["inputs"]]
    return X.fillna(pd.Series(metadata["fill_values"]))


examples = [
    # (description, INSEE code, FWI two days before, 30-day rain - evaporation in mm, month of target day)
    ("Toulon, dry July day", "83137", 35.0, -140.0, 7),
    ("Toulon, February day", "83137", 8.0, -20.0, 2),
    ("Toulon, wet November day", "83137", 2.0, 60.0, 11),
]

print(f"model {metadata['version']}, frozen {metadata['frozen_at']}, inputs {metadata['inputs']}")
print(f"usual rate of Toulon (83137): {rates['83137']:.3f} days with a fire start per year\n")
for label, insee, fwi, wb, month in examples:
    X = inputs(insee, fwi, wb, month)
    chance = model.predict_proba(X)[0, 1]
    # Same result by hand: standardise, weighted sum, logistic function.
    scaler, logit = model[0], model[-1]
    z = ((X.to_numpy()[0] - scaler.mean_) / scaler.scale_) @ logit.coef_[0] + logit.intercept_[0]
    assert np.isclose(chance, 1 / (1 + np.exp(-z)))
    print(f"{label:28s} {X.round(3).to_dict('records')[0]}")
    print(f"{'':28s} chance of a fire start: {chance:.4%}\n")

forecasts = pd.read_csv(HERE / "forecasts_2026.csv", dtype={"insee": str})
print(f"forecasts_2026.csv: {len(forecasts)} rows, {forecasts.target_day.min()} to {forecasts.target_day.max()}")