summaryrefslogtreecommitdiffstats
path: root/code/evaluate.py
blob: 2a5fb2c373976752d2375ac8da4c08d89c591adb (plain) (blame)
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""How good are the models? Everything is scored on data the model did not see.

1. Risk model on the full grid of each held-out AOI (real flood share, not the balanced training
   sample): ROC AUC, average precision, and precision / recall when flagging the top 5 % / 10 % of
   the area. Compared with the EFAS RP100 map and with the CEMS-river variant.
2. Building damage: do depth (EFAS + DEM check) and terrain risk separate damaged from undamaged
   buildings in the CEMS grading maps (builtUpP damage_gra)?
3. Collects the earlier checks (roads, DEM depth check, in-between model) into one table.
Writes outputs/eval/summary.csv and per-part CSVs.
"""
import argparse
import geopandas as gpd, numpy as np, pandas as pd, shapely
from sklearn.metrics import average_precision_score, roc_auc_score

import risk_model
from main import DATA
from paths import CACHE, OUTPUTS
from risk_model import FEATURES, Area, new_model
from road_model import sample_raster

OUT = OUTPUTS / "eval"
DAMAGED = {"Damaged", "Destroyed", "Possibly damaged"}


def top_share(score, truth, share):
    """Precision and recall when the highest-scoring `share` of cells is flagged."""
    k = max(1, int(len(score) * share))
    flag = np.zeros(len(score), bool)
    flag[np.argsort(-score)[:k]] = True
    return (truth & flag).sum() / k, (truth & flag).sum() / max(truth.sum(), 1)


def grid_scores(name, score, truth):
    row = dict(area=name, flooded_pct=100 * truth.mean(), auc=roc_auc_score(truth, score),
               avg_precision=average_precision_score(truth, score), baseline_precision=truth.mean())
    for s in (0.05, 0.10):
        row[f"precision_top{int(s * 100)}"], row[f"recall_top{int(s * 100)}"] = top_share(score, truth, s)
    return row


def risk_eval(rivers, features=FEATURES):
    df, grids = pd.read_pickle(CACHE / f"risk_features_{rivers}.pkl")
    rows = []
    for name, g in grids.items():
        truth = np.asarray(g["flooded"], bool)
        if truth.sum() < 10 or (~truth).sum() < 10:
            continue
        model = new_model().fit(df.loc[df.area != name, features], df.loc[df.area != name, "flooded"])
        rows.append(dict(model=f"risk ({rivers} rivers)", **grid_scores(name, model.predict_proba(g["feats"][features])[:, 1], truth)))
        rows.append(dict(model="EFAS RP100 depth", **grid_scores(name, g["feats"]["rp100"].to_numpy(), truth)))
    return pd.DataFrame(rows)


def damage_eval():
    """CEMS builtUpP only lists damaged buildings, so two tests:
    (a) hit: graded buildings vs GHS-OBAT buildings within 1 km of a graded one but > 20 m from any;
    (b) severity: damaged / destroyed vs possibly damaged, among graded buildings."""
    from access_model import buildings_in
    from scipy.spatial import cKDTree
    rows = []
    for folder in sorted(DATA.glob("ems_*/*_GRA_PRODUCT_*_vector")):
        path = next(iter(folder.glob("*_builtUpP_*.shp")), None)
        if path is None:
            continue
        g = gpd.read_file(path).to_crs(3035)
        g = g[g.damage_gra.isin(DAMAGED)]
        area = Area(folder)
        gxy = shapely.get_coordinates(g.geometry.representative_point().values)
        _, oxy = buildings_in(area.aoi)
        d = cKDTree(gxy).query(oxy)[0]
        nxy = oxy[(d > 20) & (d <= 1000)]
        tif = risk_model.OUT / f"risk_{area.name}.tif"

        def scores(xy):
            out = {"EFAS RP100 depth (DEM-checked)": area.depth("rp100", xy),
                   "EFAS RP100 depth (raw)": area.depth("rp100", xy, dem_check=False),
                   "inside observed 2021 extent": shapely.contains_xy(area.flood, xy[:, 0], xy[:, 1]).astype(float)}
            if tif.exists():
                out["terrain risk (held-out map)"] = np.nan_to_num(sample_raster(tif, xy))
            return out

        tests = {"hit": (np.vstack([gxy, nxy]), np.r_[np.ones(len(gxy)), np.zeros(len(nxy))].astype(bool)),
                 "severity": (gxy, g.damage_gra.isin({"Damaged", "Destroyed"}).to_numpy())}
        for test, (xy, y) in tests.items():
            if y.sum() < 5 or (~y).sum() < 5:
                continue
            for k, sc in scores(xy).items():
                rows.append(dict(test=test, area=area.name, score=k, n=len(y), positive_pct=100 * y.mean(), auc=roc_auc_score(y, sc)))
        print(f"{area.name}: {len(gxy)} graded, {len(nxy)} ungraded nearby", flush=True)
        del area
    return pd.DataFrame(rows)


if __name__ == "__main__":
    p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    p.parse_args()
    OUT.mkdir(parents=True, exist_ok=True)
    pd.set_option("display.width", 200)

    risk = pd.concat([risk_eval(r) for r in ("osm", "cems") if (CACHE / f"risk_features_{r}.pkl").exists()])
    risk = risk.drop_duplicates(["model", "area"])
    risk.round(4).to_csv(OUT / "risk_grid.csv", index=False)
    med = risk.groupby("model")[["auc", "avg_precision", "baseline_precision", "precision_top5", "recall_top5",
                                 "precision_top10", "recall_top10"]].median()
    print("\n1. Flood extent on the full held-out AOI grid (medians over AOIs)\n" + med.round(3).to_string())

    dmg = damage_eval()
    dmg.round(3).to_csv(OUT / "building_damage.csv", index=False)
    print("\n2. Building damage, ROC AUC (hit: graded vs ungraded nearby; severity: damaged/destroyed vs possibly)\n"
          + dmg.pivot_table(index=["test", "area"], columns="score", values="auc").round(3).to_string())

    summary = [dict(model=m, check="flood extent, held-out AOI grid", metric="median ROC AUC / avg precision",
                    value=f"{r.auc:.3f} / {r.avg_precision:.3f} (base rate {r.baseline_precision:.3f})") for m, r in med.iterrows()]
    summary += [dict(model=sc, check=f"building damage, {t} (CEMS grades)", metric="median ROC AUC", value=f"{v:.3f}")
                for (t, sc), v in dmg.groupby(["test", "score"]).auc.median().items()]
    for f, check in [("roads/damage_validation.csv", "road damage (CEMS grades)"), ("eval/depth_check.csv", "DEM depth check")]:
        if (OUTPUTS / f).exists():
            summary.append(dict(model=f.split("/")[0], check=check, metric="see file", value=str(OUTPUTS / f)))
    summary.append(dict(model="interp_model", check="predict AOI15 18/07 from 16 + 20/07", metric="IoU",
                        value="0.14 (shape morphing 0.007, nearest image 0.02)"))
    pd.DataFrame(summary).to_csv(OUT / "summary.csv", index=False)
    print(f"\nWrote {OUT}/summary.csv, risk_grid.csv, building_damage.csv")