summaryrefslogtreecommitdiffstats
path: root/code/preprocess.py
blob: 5d35450b3b0de21cf58895a2f1214208ee735fc6 (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
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
"""Clean the Copernicus EMS flood products down to AOIs that have both a PRODUCT and a MONIT01.

Output: one GeoPackage with one layer per EMS layer type (all AOIs stacked, tagged with
emsr/aoi/country/ptype/stage/version) plus two derived layers per AOI:
  - aoi_boundary: union of the PRODUCT and MONIT01 area of interest
  - flood_extent: union of the PRODUCT and MONIT01 observed riverine flood

Usage: python preprocess.py [--data working_data] [--out processed/ems_clean.gpkg]
"""
import argparse
import re
import warnings
from pathlib import Path

from paths import DATA, ROOT

import geopandas as gpd
import pandas as pd
from shapely.validation import make_valid

warnings.filterwarnings("ignore", category=RuntimeWarning)

COUNTRY = {"ems_de": "DE", "ems_be": "BE", "ems_nl": "NL"}
STAGES = ("PRODUCT", "MONIT01")
LAYERS = [
    "areaOfInterestA",
    "observedEventA",
    "builtUpA",
    "builtUpP",
    "transportationL",
    "hydrographyA",
    "hydrographyL",
    "naturalLandUseA",
]
DIR_RE = re.compile(r"(EMSR\d+)_(AOI\d+)_(DEL|GRA)_(PRODUCT|MONIT\d+)_r\d+_\w*?v(\d+)_vector$")
CRS = "EPSG:4326"


def find_products(data_dir):
    rows = []
    for country_dir, cc in COUNTRY.items():
        for d in sorted((data_dir / country_dir).glob("*_vector")):
            m = DIR_RE.match(d.name)
            if not m:
                print(f"  skip (unrecognised name): {d.name}")
                continue
            emsr, aoi, ptype, stage, ver = m.groups()
            rows.append(dict(country=cc, emsr=emsr, aoi=aoi, ptype=ptype, stage=stage, version=int(ver), path=d))
    df = pd.DataFrame(rows)
    # if a stage exists in several versions, keep the latest
    return df.sort_values("version").groupby(["emsr", "aoi", "ptype", "stage"]).tail(1)


def select_complete(products):
    """Keep only AOIs (per EMSR + product type) that have every stage in STAGES."""
    has = products.groupby(["emsr", "aoi", "ptype"]).stage.agg(set)
    complete = has[has.apply(lambda s: set(STAGES) <= s)].index
    dropped = has.index.difference(complete)
    for emsr, aoi, ptype in dropped:
        print(f"  drop {emsr} {aoi} {ptype}: stages {sorted(has[(emsr, aoi, ptype)])}")
    keep = products.set_index(["emsr", "aoi", "ptype"]).loc[complete].reset_index()
    return keep[keep.stage.isin(STAGES)]


def clean(gdf):
    gdf = gdf[gdf.geometry.notna() & ~gdf.geometry.is_empty].copy()
    bad = ~gdf.geometry.is_valid
    gdf.loc[bad, "geometry"] = gdf.loc[bad, "geometry"].apply(make_valid)
    # make_valid can turn polygons into collections; keep only the geometry family we started with
    if len(gdf):
        family = gdf.geometry.geom_type.mode()[0].replace("Multi", "")
        gdf = gdf.explode(index_parts=False)
        gdf = gdf[gdf.geometry.geom_type == family]
    gdf = gdf.replace({"Not Applicable": pd.NA, "Unknown": pd.NA})
    gdf = gdf[~gdf.geometry.duplicated()]
    return gdf.to_crs(CRS)


def read_layer(prod, layer):
    shp = list(prod.path.glob(f"*_{layer}_*.shp"))
    if not shp:
        return None
    gdf = clean(gpd.read_file(shp[0], engine="pyogrio"))
    if layer == "observedEventA":
        # only riverine flood (drop the few mudflow/landslide polygons)
        gdf = gdf[gdf.obj_desc == "Riverine flood"]
    for col in ("country", "emsr", "aoi", "ptype", "stage", "version"):
        gdf[col] = prod[col]
    return gdf


def dissolve_per_aoi(gdf, name):
    keys = ["country", "emsr", "aoi", "ptype"]
    out = gdf[keys + ["geometry"]].dissolve(by=keys).reset_index()
    out["area_km2"] = out.to_crs("EPSG:3035").area / 1e6
    out["layer"] = name
    return out


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--data", type=Path, default=DATA)
    ap.add_argument("--out", type=Path, default=ROOT / "processed" / "ems_clean.gpkg")
    args = ap.parse_args()

    print("Scanning EMS products...")
    products = select_complete(find_products(args.data))
    print(f"Keeping {products.groupby(['emsr', 'aoi', 'ptype']).ngroups} AOIs with {' + '.join(STAGES)}:")
    for (emsr, aoi, ptype), g in products.groupby(["emsr", "aoi", "ptype"]):
        print(f"  {emsr} {aoi} {ptype}  " + ", ".join(f"{s} v{v}" for s, v in zip(g.stage, g.version)))

    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.unlink(missing_ok=True)

    stacked = {}
    for layer in LAYERS:
        parts = [g for _, p in products.iterrows() if (g := read_layer(p, layer)) is not None and len(g)]
        if not parts:
            continue
        gdf = gpd.GeoDataFrame(pd.concat(parts, ignore_index=True), crs=CRS)
        stacked[layer] = gdf
        gdf.to_file(args.out, layer=layer, engine="pyogrio")

    boundary = dissolve_per_aoi(stacked["areaOfInterestA"], "aoi_boundary")
    flood = dissolve_per_aoi(stacked["observedEventA"], "flood_extent")
    boundary.to_file(args.out, layer="aoi_boundary", engine="pyogrio")
    flood.to_file(args.out, layer="flood_extent", engine="pyogrio")

    # summary per AOI and stage
    counts = pd.DataFrame({
        layer: gdf.groupby(["emsr", "aoi", "ptype", "stage"]).size() for layer, gdf in stacked.items()
    }).fillna(0).astype(int)
    summary = (
        boundary.set_index(["emsr", "aoi", "ptype"])[["country", "area_km2"]].rename(columns={"area_km2": "aoi_km2"})
        .join(flood.set_index(["emsr", "aoi", "ptype"])[["area_km2"]].rename(columns={"area_km2": "flood_km2"}))
        .round(2)
    )
    summary.to_csv(args.out.with_name("ems_summary.csv"))
    counts.to_csv(args.out.with_name("ems_layer_counts.csv"))

    pd.set_option("display.width", 200)
    print("\nPer AOI (union of both stages):\n", summary.to_string())
    print("\nFeatures per layer and stage:\n", counts.to_string())
    print(f"\nWrote {args.out}")


if __name__ == "__main__":
    main()