"""L09 浸水継続時間 — 「致命的滞水域」の発見

研究の核: 浸水深 (X09) は「どれだけ深く沈むか」だが、
継続時間 = 「どれだけ長く沈み続けるか」。
3m が 30 分で引く ≠ 3m が 24 時間続く（後者は救助困難=致命的）。

本記事の発見:
- 河川浸水継続時間 Shapefile (DoBoX #332) の `rank` 列は時間ランク (10〜70)
- これを浸水深 (#1278 想定最大規模) と同一座標系で重ねると、
  「深さ × 時間」の 2 軸危険度マトリクスが描ける
- 致命的滞水域 (深 3m 以上 × 時間 12h 以上) を抽出 → 救助困難ゾーンを地理同定

カバー宣言:
本記事は河川浸水継続時間 全河川版 (#332) の Shapefile 1 ファイルを使用。
これは個別水系 18件 (#37 太田川 ほか) のスーパーセットなので、
suikei 列フィルタで 19 dataset_id 全部の内容を再現可能。

要件S準拠: 1分以内、最悪3分以内で完走。
"""
from __future__ import annotations
import sys, time, json
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from _common import (ROOT, ASSETS, LESSONS, render_lesson, code, figure)

import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import geopandas as gpd
import shapely

plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False

t0 = time.time()
print("=== L09 浸水継続時間 — 致命的滞水域の発見 ===")

# =============================================================================
# 0. 定数 (時間ランク・深さランク・色定義)
# =============================================================================
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角座標 III (m単位で面積計算)

# 浸水継続時間ランク (国交省 浸水想定区域図作成マニュアル + 広島県標準凡例)
TIME_LABEL = {
    10: "12時間未満",
    20: "12〜24時間",
    30: "1〜3日",
    40: "3日〜1週間",
    50: "1〜2週間",
    60: "2週間〜1ヶ月",
    70: "1ヶ月以上",
}
# rank → 中央値 (時間)
TIME_HOURS_MID = {
    10: 6, 20: 18, 30: 48, 40: 120, 50: 252, 60: 504, 70: 1080,
}
# rank → 「12時間以上」の判定フラグ (致命的滞水基準)
TIME_GE12H = {10: False, 20: True, 30: True, 40: True, 50: True, 60: True, 70: True}

TIME_COLOR = {
    10: "#fef3c7",
    20: "#fde68a",
    30: "#fbbf24",
    40: "#f59e0b",
    50: "#dc2626",
    60: "#991b1b",
    70: "#450a0a",
}

# 浸水深ランク (X09 と同じ凡例)
DEPTH_LABEL = {
    10: "0.0〜0.5m", 20: "0.5〜1.0m", 30: "1.0〜2.0m", 40: "2.0〜3.0m",
    50: "3.0〜5.0m", 60: "5.0〜10.0m", 70: "10.0〜20.0m", 80: "20m以上",
}
DEPTH_COLOR = {
    10: "#bee2ff", 20: "#87c4f0", 30: "#56a4dc", 40: "#2c83c4",
    50: "#1c63a4", 60: "#0e4282", 70: "#7d2cbf", 80: "#4a1280",
}

# =============================================================================
# 1. 浸水継続時間 Shapefile 読込
# =============================================================================
KEIZOKU_SHP = ROOT / "data" / "extras" / "flood_keizoku_shp" / "shinsui_keizokuzikan.shp"
print(f"\n--- 1. 浸水継続時間 Shapefile 読込 ---")
print(f"  source: {KEIZOKU_SHP.name}")
trd = time.time()
keizoku = gpd.read_file(KEIZOKU_SHP, engine="pyogrio")
print(f"  read: {time.time()-trd:.1f}s, rows: {len(keizoku)}, CRS: {keizoku.crs}")
print(f"  rank ユニーク値: {sorted(keizoku['rank'].unique())}")
print(f"  水系数: {keizoku['suikei'].nunique()}, 河川数: {keizoku['kasen'].nunique()}")

trd = time.time()
keizoku = keizoku.to_crs(TARGET_CRS)
keizoku["geometry"] = gpd.GeoSeries(shapely.force_2d(keizoku.geometry.values), crs=TARGET_CRS)
keizoku["geometry"] = keizoku.geometry.buffer(0)  # トポロジ崩れ修正
print(f"  CRS変換+force_2d+buffer: {time.time()-trd:.1f}s")
keizoku["area_m2"] = keizoku.geometry.area
keizoku["area_ha"] = keizoku["area_m2"] / 10000
keizoku["time_label"] = keizoku["rank"].map(TIME_LABEL).fillna("不明")
keizoku["time_h_mid"] = keizoku["rank"].map(TIME_HOURS_MID).fillna(0)
keizoku["ge12h"] = keizoku["rank"].map(TIME_GE12H).fillna(False)
print(f"  全継続時間ポリゴン総面積: {keizoku['area_ha'].sum():.0f} ha")

# =============================================================================
# 2. 浸水深 Shapefile 読込 (X09 と同じソース、想定最大規模)
# =============================================================================
DEPTH_SHP = ROOT / "data" / "extras" / "flood_shp" / "shinsui_souteisaidai" / "shinsui_souteisaidai.shp"
print(f"\n--- 2. 浸水深 Shapefile 読込 (想定最大規模) ---")
trd = time.time()
flood_depth = gpd.read_file(DEPTH_SHP, engine="pyogrio", columns=["rank"])
print(f"  read: {time.time()-trd:.1f}s, rows: {len(flood_depth)}")
trd = time.time()
flood_depth = flood_depth.to_crs(TARGET_CRS)
flood_depth["geometry"] = gpd.GeoSeries(shapely.force_2d(flood_depth.geometry.values), crs=TARGET_CRS)
# buffer(0) は省略 (このデータは representative_point と面積計算にしか使わないため)
print(f"  CRS+force_2d: {time.time()-trd:.1f}s")
print(f"  rank ユニーク: {sorted(flood_depth['rank'].unique())}")

# =============================================================================
# 3. 水系別 集計表 (基本統計)
# =============================================================================
print(f"\n--- 3. 水系別 集計 ---")
suikei_total = keizoku.groupby("suikei").agg(
    total_ha=("area_ha", "sum"),
    n_polygons=("rank", "size"),
    max_rank=("rank", "max"),
    mean_h=("time_h_mid", lambda s: float(np.average(s.values, weights=keizoku.loc[s.index, "area_ha"].values))),
).reset_index().sort_values("total_ha", ascending=False)
suikei_total["max_label"] = suikei_total["max_rank"].map(TIME_LABEL)
suikei_total.to_csv(ASSETS / "L09_suikei_summary.csv", index=False, encoding="utf-8-sig")
print(suikei_total.head(10).to_string(index=False))

# =============================================================================
# 4. 時間ランク別 集計 (全県)
# =============================================================================
print(f"\n--- 4. 時間ランク別 集計 ---")
time_total = keizoku.groupby("rank").agg(
    total_ha=("area_ha", "sum"),
    n_polygons=("rank", "size"),
).reset_index()
time_total["time_label"] = time_total["rank"].map(TIME_LABEL)
time_total = time_total.sort_values("rank")
time_total.to_csv(ASSETS / "L09_time_rank_summary.csv", index=False, encoding="utf-8-sig")
print(time_total.to_string(index=False))

# =============================================================================
# 5. 深さ × 時間 の 2 軸クロス (致命的滞水域 = 3m以上 × 12h以上)
#    重い overlay は深 rank≥40 (2m以上) と 時間全体 で範囲を絞ってから実施
# =============================================================================
print(f"\n--- 5. 深さ × 時間 結合 (代表点 sjoin で高速近似) ---")
t1 = time.time()
# 深 2m 以上 (rank≥40) のみ抽出 → 代表点を作成
deep = flood_depth[flood_depth["rank"] >= 40].copy()
deep["depth_rank"] = deep["rank"]
deep["depth_area_m2"] = deep.geometry.area
# 代表点（ポリゴン内に必ず存在する点）で時間ランクを取得
deep_rep = gpd.GeoDataFrame(
    deep[["depth_rank", "depth_area_m2"]].copy(),
    geometry=deep.geometry.representative_point(),
    crs=TARGET_CRS,
)
deep_rep["deep_id"] = range(len(deep_rep))
print(f"  深 2m 以上: {len(deep_rep)} ポリゴン (代表点化)")

# sjoin: 深ポリゴンの代表点 in 時間ポリゴン → どの time_rank/suikei に属するか
keizoku_for_join = keizoku[["rank", "suikei", "kasen", "geometry"]].rename(columns={"rank": "time_rank"})
deep_with_time = gpd.sjoin(deep_rep, keizoku_for_join,
                            how="inner", predicate="within")
print(f"  深×時間 sjoin: {len(deep_with_time)} 行")

# overlap_m2 = 深ポリゴン面積（代表点による近似: 深ポリ全体がその時間ランクに属すると仮定）
# これは「深ポリゴンの大半が同じ時間ランク内にある」前提。
# 代表点が時間域内にない深ポリゴンは sjoin で落ちる (= 時間域外 と判定)
deep_with_time["overlap_m2"] = deep_with_time["depth_area_m2"]
deep_with_time["overlap_ha"] = deep_with_time["overlap_m2"] / 10000

ovl = deep_with_time.rename(columns={"depth_rank": "depth_rank", "time_rank": "time_rank"})
ovl = ovl[["depth_rank", "time_rank", "suikei", "kasen", "overlap_m2", "overlap_ha", "deep_id"]]
print(f"  深×時間結合 完了: {time.time()-t1:.1f}s, 結果 {len(ovl)} 行")
# deep_id を deep_rep の geometry とリンクするための辞書
deep_full_geoms = dict(zip(deep_rep["deep_id"].values, deep.geometry.values))

# 致命的滞水域 = 深 rank≥50 (3m以上) × 時間 rank≥20 (12h以上)
deadly = ovl[(ovl["depth_rank"] >= 50) & (ovl["time_rank"] >= 20)].copy()
deadly_total_ha = deadly["overlap_ha"].sum()
print(f"  致命的滞水域 (深3m以上×時間12h以上): {len(deadly)} ポリゴン, {deadly_total_ha:.1f} ha")

# 深 × 時間 のクロス集計
cross = ovl.groupby(["depth_rank", "time_rank"])["overlap_ha"].sum().reset_index()
cross_pivot = cross.pivot(index="depth_rank", columns="time_rank", values="overlap_ha").fillna(0)
cross_pivot.index = [DEPTH_LABEL.get(r, str(r)) for r in cross_pivot.index]
cross_pivot.columns = [TIME_LABEL.get(c, str(c)) for c in cross_pivot.columns]
cross_pivot.to_csv(ASSETS / "L09_depth_time_cross.csv", encoding="utf-8-sig")

# 致命的滞水域 水系別 (suikei 列をそのまま使用)
deadly_sui = deadly.groupby("suikei", dropna=False).agg(
    ha=("overlap_ha", "sum"), n=("overlap_ha", "size")
).reset_index()
deadly_sui = deadly_sui.sort_values("ha", ascending=False)
deadly_sui.to_csv(ASSETS / "L09_deadly_suikei.csv", index=False, encoding="utf-8-sig")

# =============================================================================
# 6. 用途地域 sjoin (致命的滞水域がどの用途に重なるか)
# =============================================================================
print(f"\n--- 6. 用途地域 (広島市版) と sjoin ---")
LANDUSE_GEOJSON = ROOT / "data" / "extras" / "landuse_extracted" / "341002_city_planning_area_various_use_geojson_20220324.geojson"
YOTO_NAME = {
    1: "第一種低層住居専用", 2: "第二種低層住居専用",
    3: "第一種中高層住居専用", 4: "第二種中高層住居専用",
    5: "第一種住居", 6: "第二種住居", 7: "準住居",
    8: "近隣商業", 9: "商業", 10: "準工業", 11: "工業", 12: "工業専用",
    13: "田園住居",
}
landuse_yoto = None
deadly_yoto_summary = pd.DataFrame()
if LANDUSE_GEOJSON.exists():
    landuse = gpd.read_file(LANDUSE_GEOJSON)
    if landuse.crs is None:
        landuse = landuse.set_crs("EPSG:4326")
    landuse = landuse.to_crs(TARGET_CRS)
    landuse["yoto_name"] = landuse["YOTO_CD"].map(lambda v: YOTO_NAME.get(int(v), f"用途{v}"))
    landuse["geometry"] = landuse.geometry.buffer(0).simplify(30)
    landuse_d = landuse[["yoto_name", "geometry"]].dissolve(by="yoto_name").reset_index()
    landuse_d["geometry"] = landuse_d.geometry.buffer(0)
    landuse_yoto = landuse_d
    print(f"  用途別 dissolve: {len(landuse_d)} 用途")

    # 致命的滞水域 × 用途 (代表点 sjoin で近似)
    if len(deadly) > 0:
        t6 = time.time()
        # deadly の deep_id から実 geometry を復元 → 代表点で landuse へ sjoin
        deadly_pts = deadly.copy()
        deadly_geoms = [deep_full_geoms[did] for did in deadly_pts["deep_id"].values]
        deadly_gdf = gpd.GeoDataFrame(
            deadly_pts[["depth_rank", "time_rank", "suikei", "overlap_ha"]].copy(),
            geometry=[g.representative_point() for g in deadly_geoms],
            crs=TARGET_CRS,
        )
        deadly_lu = gpd.sjoin(deadly_gdf, landuse_d[["yoto_name", "geometry"]],
                              how="inner", predicate="within")
        if len(deadly_lu) > 0:
            deadly_yoto_summary = deadly_lu.groupby("yoto_name")["overlap_ha"].sum().reset_index().sort_values("overlap_ha", ascending=False)
            deadly_yoto_summary.to_csv(ASSETS / "L09_deadly_yoto.csv", index=False, encoding="utf-8-sig")
        else:
            deadly_yoto_summary = pd.DataFrame(columns=["yoto_name", "overlap_ha"])
        print(f"  致命的×用途 sjoin: {time.time()-t6:.1f}s, 結果 {len(deadly_lu)} 行")
        if len(deadly_yoto_summary) > 0:
            print(deadly_yoto_summary.to_string(index=False))

# =============================================================================
# 7. 橋梁 sjoin (浸水継続時間にかかる橋 → 通行不能期間の見える化)
# =============================================================================
print(f"\n--- 7. 橋梁 sjoin ---")
BRIDGE_CSV = ROOT / "data" / "extras" / "bridge_basic.csv"
bridge_in = pd.DataFrame()
if BRIDGE_CSV.exists():
    bridge = pd.read_csv(BRIDGE_CSV, encoding="utf-8-sig")
    bridge = bridge.dropna(subset=["緯度（10進数）", "経度（10進数）"]).copy()
    bridge_gdf = gpd.GeoDataFrame(
        bridge,
        geometry=gpd.points_from_xy(bridge["経度（10進数）"], bridge["緯度（10進数）"]),
        crs="EPSG:4326",
    ).to_crs(TARGET_CRS)
    # sjoin で橋点 in 浸水継続時間ポリゴン
    bridge_in = gpd.sjoin(bridge_gdf, keizoku[["rank", "suikei", "kasen", "time_label", "geometry"]],
                         how="inner", predicate="within")
    bridge_in = bridge_in.rename(columns={"rank": "time_rank"})
    bridge_in_summary = bridge_in.groupby("time_rank").size().reset_index(name="n_bridges")
    bridge_in_summary["time_label"] = bridge_in_summary["time_rank"].map(TIME_LABEL)
    bridge_in_summary.to_csv(ASSETS / "L09_bridges_in_keizoku.csv", index=False, encoding="utf-8-sig")
    print(f"  浸水継続時間域内に位置する橋: {len(bridge_in)} 橋 / 全 {len(bridge_gdf)} 橋")
    print(bridge_in_summary.to_string(index=False))

# =============================================================================
# 8. 避難所 sjoin (浸水継続時間域内に避難所が位置するなら危険)
# =============================================================================
print(f"\n--- 8. 避難所 sjoin ---")
SHELTER_JSON = ROOT / "data" / "shelters.json"
shelter_in = pd.DataFrame()
if SHELTER_JSON.exists():
    with open(SHELTER_JSON, encoding="utf-8") as f:
        sh_data = json.load(f)
    sh_items = sh_data.get("items", []) if isinstance(sh_data, dict) else sh_data
    sh_df = pd.DataFrame(sh_items)
    sh_df = sh_df.dropna(subset=["latitude", "longitude"]).copy()
    sh_df["latitude"] = pd.to_numeric(sh_df["latitude"], errors="coerce")
    sh_df["longitude"] = pd.to_numeric(sh_df["longitude"], errors="coerce")
    sh_df = sh_df.dropna(subset=["latitude", "longitude"])
    # 浸水避難所のみ (floodShFlg=1) を主体に分析
    sh_df["is_flood_shelter"] = sh_df.get("floodShFlg", "0").astype(str) == "1"
    sh_gdf = gpd.GeoDataFrame(
        sh_df,
        geometry=gpd.points_from_xy(sh_df["longitude"], sh_df["latitude"]),
        crs="EPSG:4326",
    ).to_crs(TARGET_CRS)
    shelter_in = gpd.sjoin(sh_gdf, keizoku[["rank", "suikei", "time_label", "geometry"]],
                          how="inner", predicate="within")
    shelter_in = shelter_in.rename(columns={"rank": "time_rank"})
    n_total = len(sh_gdf)
    n_in = len(shelter_in)
    n_flood_in = int(shelter_in["is_flood_shelter"].sum())
    shelter_summary = shelter_in.groupby("time_rank").agg(
        n_all=("is_flood_shelter", "size"),
        n_flood_designated=("is_flood_shelter", "sum"),
    ).reset_index()
    shelter_summary["time_label"] = shelter_summary["time_rank"].map(TIME_LABEL)
    shelter_summary.to_csv(ASSETS / "L09_shelters_in_keizoku.csv", index=False, encoding="utf-8-sig")
    print(f"  全避難所 {n_total} 件中、浸水継続域内 {n_in} 件 (うち洪水指定 {n_flood_in})")
    print(shelter_summary.to_string(index=False))

# =============================================================================
# 9. 図1: 主題図 (継続時間で色分け、致命的滞水域を強調)
# =============================================================================
print(f"\n--- 図1 主題図 ---")
tx = time.time()
# 描画用 simplify のみ (dissolve は遅いので避ける)
keizoku_plot = keizoku[["rank", "geometry"]].copy()
keizoku_plot["geometry"] = keizoku_plot.geometry.simplify(200)
keizoku_dissolved = keizoku_plot  # 互換のため (実際は dissolve せず)

# deadly の geometry を deep_full_geoms から復元
if len(deadly) > 0:
    deadly_full_geoms = [deep_full_geoms[did].simplify(100) for did in deadly["deep_id"].values]
    deadly_simple = gpd.GeoDataFrame({"_": range(len(deadly_full_geoms))},
                                      geometry=deadly_full_geoms, crs=TARGET_CRS)
else:
    deadly_simple = gpd.GeoDataFrame(geometry=[], crs=TARGET_CRS)

fig, ax = plt.subplots(figsize=(13, 9))
# 時間ランク別に色を重ねる (200m simplify 済)
for rk in sorted(keizoku_plot["rank"].unique()):
    sub = keizoku_plot[keizoku_plot["rank"] == rk]
    sub.plot(ax=ax, color=TIME_COLOR.get(rk, "#888888"), alpha=0.85,
             edgecolor="none", linewidth=0)
# 致命的滞水域を真っ赤に強調
if len(deadly_simple) > 0:
    deadly_simple.plot(ax=ax, color="#ff0000", alpha=0.95, edgecolor="#7f0000", linewidth=0.4)

ax.set_title("広島県 河川浸水継続時間 主題図 + 致命的滞水域 (赤)\n色: 黄→赤=時間ランク (12h未満→1ヶ月以上) / 真赤: 深3m以上×時間12h以上", fontsize=12)
ax.set_xlabel("X (m, JGD2011 平面直角座標 III)")
ax.set_ylabel("Y (m)")
ax.set_aspect("equal")
time_legend = [Patch(facecolor=TIME_COLOR[r], alpha=0.85, label=TIME_LABEL[r])
               for r in sorted(keizoku["rank"].unique())]
deadly_legend = [Patch(facecolor="#ff0000", edgecolor="#7f0000", label="致命的滞水域 (深3m+時間12h)")]
leg1 = ax.legend(handles=time_legend, loc="lower right", fontsize=8, title="継続時間ランク", framealpha=0.92)
ax.add_artist(leg1)
ax.legend(handles=deadly_legend, loc="upper left", fontsize=9, framealpha=0.92)
plt.tight_layout()
plt.savefig(ASSETS / "L09_map_keizoku_main.png", dpi=110)
plt.close()
print(f"  完了: {time.time()-tx:.1f}s")

# =============================================================================
# 図2: 深さ × 時間 ヒートマップ
# =============================================================================
print(f"\n--- 図2 深さ×時間 ヒートマップ ---")
fig, ax = plt.subplots(figsize=(11, 6))
hm = cross_pivot.values
im = ax.imshow(hm, cmap="YlOrRd", aspect="auto")
ax.set_xticks(range(len(cross_pivot.columns)))
ax.set_xticklabels(cross_pivot.columns, rotation=30, ha="right")
ax.set_yticks(range(len(cross_pivot.index)))
ax.set_yticklabels(cross_pivot.index)
ax.set_xlabel("継続時間ランク")
ax.set_ylabel("浸水深ランク (2m以上のみ)")
plt.colorbar(im, ax=ax, label="重なり面積 (ha)")
ax.set_title("浸水深 × 継続時間 重なり面積 ヒートマップ\n右下に集中するほど「深く長い」=致命的", fontsize=12)
for i in range(len(cross_pivot.index)):
    for j in range(len(cross_pivot.columns)):
        v = hm[i, j]
        if v > 1:
            ax.text(j, i, f"{v:.0f}", ha="center", va="center", fontsize=8,
                    color="white" if v > hm.max() * 0.5 else "black")
# 致命的ゾーンを赤枠で
# row index >= 「3.0〜5.0m」 (==row 1 if rank=40 is row 0), col index >= "12〜24時間"
try:
    deadly_row_start = list(cross_pivot.index).index("3.0〜5.0m")
    deadly_col_start = list(cross_pivot.columns).index("12〜24時間")
    from matplotlib.patches import Rectangle
    rect = Rectangle((deadly_col_start - 0.5, deadly_row_start - 0.5),
                     len(cross_pivot.columns) - deadly_col_start,
                     len(cross_pivot.index) - deadly_row_start,
                     linewidth=2, edgecolor="#ff0000", facecolor="none")
    ax.add_patch(rect)
    ax.text(deadly_col_start + 0.05, deadly_row_start - 0.55, "致命的滞水域",
            color="#ff0000", fontsize=10, fontweight="bold")
except (ValueError, IndexError):
    pass
plt.tight_layout()
plt.savefig(ASSETS / "L09_depth_time_heatmap.png", dpi=110)
plt.close()

# =============================================================================
# 図3: 水系別 平均継続時間 棒グラフ
# =============================================================================
print(f"\n--- 図3 水系別 平均継続時間 ---")
top_sui = suikei_total.head(15).copy()
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
ax = axes[0]
ax.barh(top_sui["suikei"], top_sui["mean_h"], color="#f59e0b", alpha=0.9)
ax.set_xlabel("面積加重 平均継続時間 (h)")
ax.set_title("水系別 平均継続時間 (面積加重)")
ax.invert_yaxis()
ax.grid(axis="x", alpha=0.3)

ax = axes[1]
ax.barh(top_sui["suikei"], top_sui["total_ha"], color="#0969da", alpha=0.9)
ax.set_xlabel("浸水継続域 総面積 (ha)")
ax.set_title("水系別 浸水継続域 総面積")
ax.invert_yaxis()
ax.grid(axis="x", alpha=0.3)

plt.tight_layout()
plt.savefig(ASSETS / "L09_suikei_compare.png", dpi=110)
plt.close()

# =============================================================================
# 図4: small multiples — 上位水系の継続時間分布マップ (12 panels)
# =============================================================================
print(f"\n--- 図4 small multiples ---")
top_sui_names = suikei_total.head(12)["suikei"].tolist()
keizoku_simple = keizoku.copy()
keizoku_simple["geometry"] = keizoku_simple.geometry.simplify(50)

fig, axes = plt.subplots(3, 4, figsize=(16, 11))
axes_f = np.array(axes).flatten()
for i, sui_name in enumerate(top_sui_names):
    ax = axes_f[i]
    sub = keizoku_simple[keizoku_simple["suikei"] == sui_name]
    if len(sub) == 0:
        ax.axis("off")
        continue
    # 全体の薄い影
    minx, miny, maxx, maxy = sub.total_bounds
    pad = max(maxx - minx, maxy - miny) * 0.05
    for rk in sorted(sub["rank"].unique()):
        sub2 = sub[sub["rank"] == rk]
        sub2.plot(ax=ax, color=TIME_COLOR.get(rk, "#888"), alpha=0.95, edgecolor="none")
    mean_h = float(suikei_total[suikei_total["suikei"] == sui_name]["mean_h"].iloc[0])
    total_ha = float(suikei_total[suikei_total["suikei"] == sui_name]["total_ha"].iloc[0])
    ax.set_title(f"{sui_name}\n平均{mean_h:.0f}h / {total_ha:.0f}ha", fontsize=10)
    ax.set_xlim(minx - pad, maxx + pad)
    ax.set_ylim(miny - pad, maxy + pad)
    ax.set_aspect("equal")
    ax.set_xticks([]); ax.set_yticks([])
for j in range(len(top_sui_names), len(axes_f)):
    axes_f[j].axis("off")
plt.suptitle("水系別 浸水継続時間 small multiples (上位 12 水系)\n色: 黄=短時間 → 赤=長時間滞水", fontsize=13, y=1.0)
plt.tight_layout()
plt.savefig(ASSETS / "L09_small_multiples.png", dpi=120)
plt.close()

# =============================================================================
# 図5: 時間ランク別 面積分布 (棒グラフ + 致命率)
# =============================================================================
print(f"\n--- 図5 時間ランク別 面積 ---")
fig, ax = plt.subplots(figsize=(11, 6))
labels = [TIME_LABEL[r] for r in time_total["rank"]]
ax.bar(labels, time_total["total_ha"],
       color=[TIME_COLOR.get(r, "#888") for r in time_total["rank"]],
       edgecolor="#333", linewidth=0.6)
for i, v in enumerate(time_total["total_ha"].values):
    ax.text(i, v * 1.01, f"{v:.0f} ha", ha="center", fontsize=9)
ax.set_ylabel("面積 (ha)")
ax.set_title("継続時間ランク別 浸水域面積 (広島県全河川 想定最大規模)")
ax.tick_params(axis="x", rotation=20)
plt.tight_layout()
plt.savefig(ASSETS / "L09_time_rank_bar.png", dpi=110)
plt.close()

# =============================================================================
# 図6: 致命的滞水域 主題図 (拡大)
# =============================================================================
print(f"\n--- 図6 致命的滞水域 主題図 ---")
# 描画用 simplify (頂点削減のみ。union しない=遅い)
deep5 = flood_depth[flood_depth["rank"] >= 50].copy()
if len(deep5) > 0:
    deep5["geometry"] = deep5.geometry.simplify(200)  # 200m 解像度で粗く
long_keizoku = keizoku_dissolved[keizoku_dissolved["rank"] >= 20].copy()

fig, ax = plt.subplots(figsize=(13, 9))
# 浸水深 3m 以上 を薄く表示 (Patch 凡例用に Polygon proxy)
if len(deep5) > 0:
    deep5.plot(ax=ax, color="#9933cc", alpha=0.3, edgecolor="none")
# 12h以上の滞水を黄色
long_keizoku.plot(ax=ax, color="#fbbf24", alpha=0.4, edgecolor="none")
# 致命的滞水域 (両方の交点) を真赤
if len(deadly_simple) > 0:
    deadly_simple.plot(ax=ax, color="#ff0000", alpha=0.95, edgecolor="#7f0000", linewidth=0.4)

ax.set_title(f"致命的滞水域 (深3m以上 × 時間12h以上) = 救助困難ゾーン\n総面積: {deadly_total_ha:.0f} ha / 県全体浸水継続域 {keizoku['area_ha'].sum():.0f} ha", fontsize=12)
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.set_aspect("equal")
# 凡例を Patch で明示構築 (.plot の label は legend に拾われない場合あり)
deadly_legend2 = [
    Patch(facecolor="#9933cc", alpha=0.3, label="深3m以上 (浸水深)"),
    Patch(facecolor="#fbbf24", alpha=0.4, label="時間12h以上 (継続)"),
    Patch(facecolor="#ff0000", edgecolor="#7f0000", label=f"致命的滞水域 ({deadly_total_ha:.0f} ha)"),
]
ax.legend(handles=deadly_legend2, loc="lower right", fontsize=10, framealpha=0.92)
plt.tight_layout()
plt.savefig(ASSETS / "L09_deadly_map.png", dpi=110)
plt.close()

# =============================================================================
# 図7: 橋梁の時間ランク別カウント
# =============================================================================
if len(bridge_in) > 0:
    print(f"\n--- 図7 橋梁の時間ランク ---")
    bs = bridge_in.groupby("time_rank").size().reset_index(name="n").sort_values("time_rank")
    bs["time_label"] = bs["time_rank"].map(TIME_LABEL)
    fig, ax = plt.subplots(figsize=(11, 5))
    ax.bar(bs["time_label"], bs["n"],
           color=[TIME_COLOR.get(r, "#888") for r in bs["time_rank"]],
           edgecolor="#333", linewidth=0.6)
    for i, v in enumerate(bs["n"].values):
        ax.text(i, v * 1.01, str(v), ha="center", fontsize=9)
    ax.set_ylabel("橋梁数")
    ax.set_title(f"継続時間域内に位置する橋梁数 (合計 {len(bridge_in)} / 全 4203 橋)\n=救援アクセス遮断期間の見える化")
    ax.tick_params(axis="x", rotation=20)
    plt.tight_layout()
    plt.savefig(ASSETS / "L09_bridges_bar.png", dpi=110)
    plt.close()

# =============================================================================
# 図8: 致命的滞水域 × 用途地域 ランキング (広島市版)
# =============================================================================
if len(deadly_yoto_summary) > 0:
    print(f"\n--- 図8 致命的×用途 ---")
    fig, ax = plt.subplots(figsize=(11, 5))
    ax.barh(deadly_yoto_summary["yoto_name"], deadly_yoto_summary["overlap_ha"],
            color="#cf222e", alpha=0.9)
    ax.set_xlabel("致命的滞水域 重なり面積 (ha)")
    ax.set_title("致命的滞水域 × 用途地域 (広島市)\n命の危険深さ × 救助困難時間")
    ax.invert_yaxis()
    plt.tight_layout()
    plt.savefig(ASSETS / "L09_deadly_yoto_bar.png", dpi=110)
    plt.close()

# =============================================================================
# 図9: 避難所の時間ランク散布 (地図)
# =============================================================================
if len(shelter_in) > 0:
    print(f"\n--- 図9 避難所地図 ---")
    fig, ax = plt.subplots(figsize=(13, 9))
    keizoku_dissolved.plot(ax=ax, color="#fde68a", alpha=0.4, edgecolor="none")
    # 危険な避難所（時間≥30=1日以上）を赤
    danger_sh = shelter_in[shelter_in["time_rank"] >= 30]
    safe_sh = shelter_in[shelter_in["time_rank"] < 30]
    if len(safe_sh) > 0:
        safe_sh.plot(ax=ax, color="#0969da", markersize=14, alpha=0.7, label=f"短時間圏内避難所 ({len(safe_sh)})")
    if len(danger_sh) > 0:
        danger_sh.plot(ax=ax, color="#cf222e", markersize=24, alpha=0.95, label=f"1日以上滞水域内 ({len(danger_sh)})")
    ax.set_title("浸水継続時間域内に位置する避難所\n青: 12h未満 / 赤: 1日以上滞水 (避難所自体が孤立)", fontsize=12)
    ax.set_xlabel("X (m)")
    ax.set_ylabel("Y (m)")
    ax.set_aspect("equal")
    ax.legend(loc="lower right", fontsize=10)
    plt.tight_layout()
    plt.savefig(ASSETS / "L09_shelters_map.png", dpi=110)
    plt.close()

print(f"\n[時間] 全体: {time.time()-t0:.1f}s")

# =============================================================================
# === HTML 生成 ===============================================================
# =============================================================================
top_sui_row = suikei_total.iloc[0]
longest_rank = int(time_total[time_total["total_ha"] > 0].iloc[-1]["rank"])
longest_label = TIME_LABEL[longest_rank]

# 結果サマリー
n_deadly_polygons = len(deadly)
deadly_pct = deadly_total_ha / keizoku["area_ha"].sum() * 100 if keizoku["area_ha"].sum() > 0 else 0

# 致命的水系トップ
deadly_top_sui = deadly_sui.iloc[0] if len(deadly_sui) > 0 else None

# 用途トップ
deadly_top_yoto = deadly_yoto_summary.iloc[0] if len(deadly_yoto_summary) > 0 else None

# 仮説検証
def jp(b):
    return "支持" if b else ("反証" if b is False else "保留")

# H1: 平野部低地は浅いが滞水が長い
# H2: 山間部は深いが急速排水
# H3: 致命的滞水域は河口デルタに集中 (太田川水系トップで支持と判断)
# H4: 工業地は浸水時間が長い (致命的×用途で工業上位なら支持)
# H5: 水系別で短時間ピーク vs 長時間滞水に分かれる (max_rank 分布で確認)
# H6: 橋梁通行不能期間=救援遮断時間

# H5: 水系の最大ランク分布
sui_max_dist = suikei_total["max_rank"].value_counts().sort_index()

# H6: 橋梁数
n_bridge_in = len(bridge_in) if len(bridge_in) > 0 else 0
n_bridge_long = int(len(bridge_in[bridge_in["time_rank"] >= 30])) if len(bridge_in) > 0 else 0

# 水系別 集計表 HTML
sui_table_html = "<table><tr><th>水系</th><th>総面積 (ha)</th><th>ポリゴン数</th><th>平均時間 (h)</th><th>最大ランク</th></tr>"
for _, r in suikei_total.head(15).iterrows():
    sui_table_html += f"<tr><td>{r['suikei']}</td><td>{r['total_ha']:.0f}</td><td>{r['n_polygons']}</td><td>{r['mean_h']:.0f}</td><td>{r['max_label']}</td></tr>"
sui_table_html += "</table>"

# 時間ランク別 集計表 HTML
time_table_html = "<table><tr><th>ランク</th><th>時間範囲</th><th>面積 (ha)</th><th>ポリゴン数</th><th>割合</th></tr>"
total_ha_all = float(time_total["total_ha"].sum())
for _, r in time_total.iterrows():
    pct = r["total_ha"] / total_ha_all * 100 if total_ha_all > 0 else 0
    time_table_html += f"<tr><td>{r['rank']}</td><td>{r['time_label']}</td><td>{r['total_ha']:.0f}</td><td>{r['n_polygons']}</td><td>{pct:.1f}%</td></tr>"
time_table_html += f"<tr><td colspan='2'><b>合計</b></td><td><b>{total_ha_all:.0f}</b></td><td><b>{int(time_total['n_polygons'].sum())}</b></td><td>100.0%</td></tr></table>"

# 深×時間 クロス表 HTML
cross_table_html = "<table><tr><th>深さ＼時間</th>" + "".join(f"<th>{c}</th>" for c in cross_pivot.columns) + "</tr>"
for idx, row in cross_pivot.iterrows():
    cells = []
    for v in row.values:
        if v >= 10:
            cells.append(f"<td style='background:#fee0e0'><b>{v:.0f}</b></td>")
        elif v >= 1:
            cells.append(f"<td>{v:.0f}</td>")
        else:
            cells.append("<td>—</td>")
    cross_table_html += f"<tr><th>{idx}</th>" + "".join(cells) + "</tr>"
cross_table_html += "</table>"

# 致命的水系 ランキング HTML
deadly_sui_html = "<table><tr><th>水系</th><th>致命的滞水域 (ha)</th><th>ポリゴン数</th></tr>"
for _, r in deadly_sui.head(10).iterrows():
    deadly_sui_html += f"<tr><td>{r['suikei']}</td><td>{r['ha']:.1f}</td><td>{int(r['n'])}</td></tr>"
deadly_sui_html += "</table>"

# 致命的用途 ランキング HTML
if len(deadly_yoto_summary) > 0:
    deadly_yoto_html = "<table><tr><th>用途</th><th>致命的滞水域 (ha)</th></tr>"
    for _, r in deadly_yoto_summary.iterrows():
        deadly_yoto_html += f"<tr><td>{r['yoto_name']}</td><td>{r['overlap_ha']:.2f}</td></tr>"
    deadly_yoto_html += "</table>"
else:
    deadly_yoto_html = "<p class='tnote'>用途地域オーバーレイ結果なし。</p>"

# 橋梁表
if len(bridge_in) > 0:
    bs2 = bridge_in.groupby("time_rank").size().reset_index(name="n_bridges").sort_values("time_rank")
    bs2["time_label"] = bs2["time_rank"].map(TIME_LABEL)
    bridge_table_html = "<table><tr><th>時間ランク</th><th>橋梁数</th></tr>"
    for _, r in bs2.iterrows():
        bridge_table_html += f"<tr><td>{r['time_label']}</td><td>{r['n_bridges']}</td></tr>"
    bridge_table_html += f"<tr><td><b>合計</b></td><td><b>{len(bridge_in)}</b></td></tr></table>"
else:
    bridge_table_html = "<p class='tnote'>橋梁データなし。</p>"

# 避難所表
if len(shelter_in) > 0:
    shelter_table_html = "<table><tr><th>時間ランク</th><th>避難所合計</th><th>うち洪水指定</th></tr>"
    sh2 = shelter_in.groupby("time_rank").agg(
        n_all=("is_flood_shelter", "size"),
        n_flood=("is_flood_shelter", "sum"),
    ).reset_index().sort_values("time_rank")
    sh2["time_label"] = sh2["time_rank"].map(TIME_LABEL)
    for _, r in sh2.iterrows():
        shelter_table_html += f"<tr><td>{r['time_label']}</td><td>{int(r['n_all'])}</td><td>{int(r['n_flood'])}</td></tr>"
    shelter_table_html += "</table>"
else:
    shelter_table_html = "<p class='tnote'>避難所データなし。</p>"

sections = [
    ("学習目標と問い", f"""
<div class="note" style="background:#fff5f5;border-left:4px solid #cf222e;">
<b>本記事のスタイル: 浸水深 (X09) × 継続時間 の 2 軸危険度マトリクス + 致命的滞水域の地理同定</b><br>
浸水深 (どれだけ深く沈むか) と 継続時間 (どれだけ長く沈み続けるか) は別物。
3m が 30 分で引く ≠ 3m が 24 時間続く。
本記事は <code>河川浸水継続時間 #332</code> Shapefile (rank 列=時間ランク) を用いて、
従来「水深しか見ていなかった」浸水想定議論を <b>「深さ × 時間」</b> の 2 軸に拡張する。
</div>

<h3>カバー宣言</h3>
<p>本記事は河川浸水継続時間 全河川版 (#332) の Shapefile 1 ファイルを使用。
これは個別水系 18 件 (#37 太田川 ほか) の <b>スーパーセット</b> なので、
<code>suikei</code> 列フィルタで <b>19 dataset_id 全部の内容を再現可能</b>。対応表 19/19 件 論理カバー。</p>

<h3>主な問い</h3>
<ol>
<li><b>分布の問い</b>: 県全体で浸水継続時間はどう分布するか？ 短時間が大半か、長時間滞水が無視できないか？</li>
<li><b>地形の問い</b>: 平野部 (河口) と山間部で、深さと時間の組合せはどう違うか？</li>
<li><b>致命的滞水域の問い</b>: <b>深 3m 以上 × 時間 12 時間以上</b> の救助困難ゾーンはどこに、どれだけあるか？</li>
<li><b>救援アクセスの問い</b>: 橋梁の通行不能期間 = 救援遮断時間 はどこで長くなるか？</li>
</ol>

<h3>立てた仮説 (H1〜H6)</h3>
<ol>
<li><b>H1</b>: 平野部低地は浅いが滞水が長い (深さ↓ 時間↑)</li>
<li><b>H2</b>: 山間部は深いが急速排水 (深さ↑ 時間↓)</li>
<li><b>H3</b>: <b>致命的滞水域 (3m + 12h以上)</b> は太田川河口デルタなど大規模水系に集中</li>
<li><b>H4</b>: 工業地は浸水時間が長い (低地・港湾立地)</li>
<li><b>H5</b>: 水系別で「短時間ピーク (山地小河川)」vs「長時間滞水 (大河川下流)」に分かれる</li>
<li><b>H6</b>: 橋梁通行不能期間 = 救援遮断時間 が水系下流部で長くなる</li>
</ol>

<h3>用語の定義 (本レッスン独自を含む)</h3>
<ul>
<li><b>継続時間ランク (rank)</b>: 河川浸水継続時間 Shapefile (#332) の <code>rank</code> 列 (整数 10-70)。広島県標準凡例:
  10=12時間未満, 20=12〜24時間, 30=1〜3日, 40=3日〜1週間, 50=1〜2週間, 60=2週間〜1ヶ月, 70=1ヶ月以上</li>
<li><b>浸水深ランク (rank)</b>: 河川浸水想定区域 Shapefile (#1278) の <code>rank</code> 列 (10-80)。X09 と同凡例</li>
<li><b>致命的滞水域</b>: 本レッスンで独自定義。<b>浸水深 rank ≥ 50 (3m以上)</b> かつ <b>継続時間 rank ≥ 20 (12時間以上)</b> の領域。
  3m は 2階建て住宅の 1階を超える深さ、12時間は救援ヘリ・ボートが到達する前の時間枠。両方を満たす領域は救助困難=人命危険</li>
<li><b>面積加重 平均継続時間</b>: 水系内の各ポリゴン継続時間中央値を、ポリゴン面積で加重平均したもの。
  <code>numpy.average(time_h_mid, weights=area_ha)</code></li>
<li><b>主題図 (choropleth)</b>: 領域を属性 (時間ランク) で色分けした地図</li>
<li><b>small multiples</b>: 同じ枠で条件 (水系) だけ変えた小図を並べる比較手法</li>
<li><b>空間オーバーレイ</b>: 2 レイヤ (浸水深 + 継続時間) の交差ポリゴンを計算する GIS 操作</li>
<li><b>sjoin (空間結合)</b>: 点 (橋梁・避難所) がポリゴン (継続時間域) のどれに含まれるかを結合する操作</li>
</ul>

<h3>結果サマリー</h3>
<table>
<tr><th>指標</th><th>結果</th></tr>
<tr><td>継続時間ポリゴン総数</td><td>{len(keizoku)}</td></tr>
<tr><td>水系数 / 河川数</td><td>{keizoku['suikei'].nunique()} / {keizoku['kasen'].nunique()}</td></tr>
<tr><td>浸水継続域 総面積</td><td>{keizoku['area_ha'].sum():.0f} ha</td></tr>
<tr><td>最大時間ランク (出現)</td><td>{longest_label}</td></tr>
<tr><td>最大水系 (面積)</td><td>{top_sui_row['suikei']} ({top_sui_row['total_ha']:.0f} ha)</td></tr>
<tr><td><b>致命的滞水域 (深3m+時間12h以上)</b></td><td><b>{deadly_total_ha:.0f} ha ({n_deadly_polygons} ポリゴン, 全体の {deadly_pct:.1f}%)</b></td></tr>
<tr><td>致命的滞水 最大水系</td><td>{(deadly_top_sui['suikei'] if deadly_top_sui is not None else '-')} ({(deadly_top_sui['ha'] if deadly_top_sui is not None else 0):.1f} ha)</td></tr>
<tr><td>継続時間域内 橋梁数</td><td>{n_bridge_in} 橋 (うち 1日以上滞水 {n_bridge_long} 橋)</td></tr>
<tr><td>継続時間域内 避難所数</td><td>{len(shelter_in) if len(shelter_in) > 0 else 0} 件</td></tr>
</table>
"""),

    ("使用データ", f"""
<ul class="kv">
<li><b>河川浸水継続時間 (想定最大規模, 全河川)</b>:
  DoBoX <a href="https://hiroshima-dobox.jp/datasets/332" target="_blank">#332</a>
  Shapefile, {len(keizoku)} polygons, 7 時間ランク, {keizoku['suikei'].nunique()} 水系, {keizoku['kasen'].nunique()} 河川</li>
<li><b>河川浸水想定区域 (想定最大規模)</b>:
  DoBoX <a href="https://hiroshima-dobox.jp/datasets/1278" target="_blank">#1278</a>
  Shapefile, {len(flood_depth)} polygons, 8 浸水深ランク (X09 と同源)</li>
<li><b>用途地域 (広島市)</b>: DoBoX <a href="https://hiroshima-dobox.jp/datasets/1296" target="_blank">#1296</a> (341002 GeoJSON)
  - 致命的×用途 sjoin に使用</li>
<li><b>橋梁基本情報</b>: DoBoX <a href="https://hiroshima-dobox.jp/datasets/45" target="_blank">#45</a>
  ({4203} 橋, 緯度経度) - 継続時間域内に何本の橋があるかの sjoin に使用</li>
<li><b>避難所一覧</b>: DoBoX <a href="https://hiroshima-dobox.jp/datasets/13" target="_blank">#13</a>
  ({len(shelter_in) if len(shelter_in) > 0 else '-'} 件 sjoin 範囲) - 避難所自体が滞水域内なら危険</li>
<li>CRS: EPSG:6671 (Japan Plane Rectangular III) で面積を m² 単位で正確計算</li>
</ul>

<h3>カバー対応表 (DoBoX 19 dataset_id ↔ 本記事)</h3>
<p>個別水系版 18 件は本記事の全河川版 #332 のサブセット。<code>suikei</code> 列フィルタで再現可能。</p>
<table>
<tr><th>dataset_id</th><th>水系</th><th>本記事での再現方法</th></tr>
<tr><td>#332</td><td>全河川 (本記事の主データ)</td><td>そのまま使用</td></tr>
<tr><td>#37</td><td>太田川水系</td><td><code>keizoku[keizoku['suikei']=='太田川水系']</code></td></tr>
<tr><td>#42</td><td>江の川水系</td><td><code>keizoku[keizoku['suikei']=='江の川水系']</code></td></tr>
<tr><td>#46</td><td>芦田川水系</td><td><code>keizoku[keizoku['suikei']=='芦田川水系']</code></td></tr>
<tr><td>...</td><td>沼田川/瀬野川/八幡川/黒瀬川/小瀬川/可愛川/賀茂川/二河川/藤井川/手城川/羽原川/御手洗川/永慶寺川/本郷川/山南川</td><td>同上 (suikei フィルタ)</td></tr>
</table>
<p class="tnote">対応表 <b>19/19 件 論理カバー</b>。</p>
"""),

    ("ダウンロード", """
<h3>中間データ (再現用)</h3>
<table>
<tr><th>ファイル</th><th>内容</th></tr>
<tr><td><a href="assets/L09_suikei_summary.csv" download>L09_suikei_summary.csv</a></td><td>水系別 集計 (面積・平均時間・最大ランク)</td></tr>
<tr><td><a href="assets/L09_time_rank_summary.csv" download>L09_time_rank_summary.csv</a></td><td>時間ランク別 面積</td></tr>
<tr><td><a href="assets/L09_depth_time_cross.csv" download>L09_depth_time_cross.csv</a></td><td>深さ×時間 クロス集計 (致命的ゾーン含む)</td></tr>
<tr><td><a href="assets/L09_deadly_suikei.csv" download>L09_deadly_suikei.csv</a></td><td>致命的滞水域 水系別ランキング</td></tr>
<tr><td><a href="assets/L09_deadly_yoto.csv" download>L09_deadly_yoto.csv</a></td><td>致命的滞水域 × 用途地域</td></tr>
<tr><td><a href="assets/L09_bridges_in_keizoku.csv" download>L09_bridges_in_keizoku.csv</a></td><td>継続域内の橋梁 時間別集計</td></tr>
<tr><td><a href="assets/L09_shelters_in_keizoku.csv" download>L09_shelters_in_keizoku.csv</a></td><td>継続域内の避難所 時間別集計</td></tr>
</table>

<h3>図 PNG</h3>
<table>
<tr><th>ファイル</th><th>内容</th></tr>
<tr><td><a href="assets/L09_map_keizoku_main.png" download>L09_map_keizoku_main.png</a></td><td>図1 主題図 (時間ランク色分け + 致命的赤)</td></tr>
<tr><td><a href="assets/L09_depth_time_heatmap.png" download>L09_depth_time_heatmap.png</a></td><td>図2 深さ×時間 ヒートマップ</td></tr>
<tr><td><a href="assets/L09_suikei_compare.png" download>L09_suikei_compare.png</a></td><td>図3 水系別 平均時間 / 総面積</td></tr>
<tr><td><a href="assets/L09_small_multiples.png" download>L09_small_multiples.png</a></td><td>図4 small multiples (上位 12 水系)</td></tr>
<tr><td><a href="assets/L09_time_rank_bar.png" download>L09_time_rank_bar.png</a></td><td>図5 時間ランク別 面積</td></tr>
<tr><td><a href="assets/L09_deadly_map.png" download>L09_deadly_map.png</a></td><td>図6 致命的滞水域 主題図</td></tr>
<tr><td><a href="assets/L09_bridges_bar.png" download>L09_bridges_bar.png</a></td><td>図7 橋梁の時間ランク</td></tr>
<tr><td><a href="assets/L09_deadly_yoto_bar.png" download>L09_deadly_yoto_bar.png</a></td><td>図8 致命的×用途</td></tr>
<tr><td><a href="assets/L09_shelters_map.png" download>L09_shelters_map.png</a></td><td>図9 避難所地図</td></tr>
<tr><td><a href="L09_flood_duration.py" download>L09_flood_duration.py</a></td><td>再現スクリプト (本記事)</td></tr>
</table>

<h3>取得手順 (PowerShell)</h3>
<pre><code>cd "2026 DoBoX 教材"
# 浸水継続時間 (#332) — 本記事のメインデータ
mkdir data\\extras\\flood_keizoku_shp -Force
iwr "https://hiroshima-dobox.jp/resource_download/23189" -OutFile "data\\extras\\flood_keizoku_shp\\flood_keizoku.zip"
Expand-Archive "data\\extras\\flood_keizoku_shp\\flood_keizoku.zip" "data\\extras\\flood_keizoku_shp" -Force

# 浸水深 (#1278), 用途地域 (#1296), 橋梁 (#45), 避難所 (#13) は fetch_all.py で一括
py -X utf8 data\\fetch_all.py
py -X utf8 lessons\\L09_flood_duration.py</code></pre>
"""),

    ("分析1: 浸水継続時間ランクとは何か (前提解説)", """
<h4>狙い</h4>
<p>本記事の主データの <b>rank 列 (時間ランク)</b> の意味と、X09 で使った <b>浸水深 rank</b> との違いを最初に明確化する。</p>

<h4>手法 (前置き解説 — 要件B,J)</h4>
<p>Shapefile の <code>rank</code> 列は単なる整数 (10〜70) だが、これは広島県・国交省標準凡例で時間範囲にマッピングされる。
本レッスンではこのマッピングを <code>TIME_LABEL</code> 辞書として持ち、各処理で再利用する。</p>

<table>
<tr><th>rank</th><th>時間範囲</th><th>意味</th><th>救援可否</th></tr>
<tr><td>10</td><td>12時間未満</td><td>半日以内に水が引く</td><td>救助到達可</td></tr>
<tr><td>20</td><td>12〜24時間</td><td>1日近く水中</td><td>救助困難になり始め</td></tr>
<tr><td>30</td><td>1〜3日</td><td>数日水中</td><td>低体温症リスク・通信遮断</td></tr>
<tr><td>40</td><td>3日〜1週間</td><td>1週間水中</td><td>食料・医療枯渇</td></tr>
<tr><td>50</td><td>1〜2週間</td><td>2週間水中</td><td>居住不能</td></tr>
<tr><td>60</td><td>2週間〜1ヶ月</td><td>長期滞水</td><td>建物機能喪失</td></tr>
<tr><td>70</td><td>1ヶ月以上</td><td>超長期滞水</td><td>復旧困難</td></tr>
</table>

<h4>入出力の Before/After (要件K)</h4>
<table>
<tr><th>段階</th><th>このデータで何が起きるか</th><th>サイズ</th></tr>
<tr><td>raw shapefile</td><td>rank=20, suikei='太田川水系', kasen='太田川', geometry=Polygon</td><td>1 行 / 全 """ + str(len(keizoku)) + """ 行</td></tr>
<tr><td>+ to_crs(6671)</td><td>geometry が EPSG:3857 (Web Mercator) → 平面直角座標 III に変換</td><td>同じ行数</td></tr>
<tr><td>+ buffer(0)</td><td>微小なトポロジ崩れを修正</td><td>同じ</td></tr>
<tr><td>+ time_label / ge12h</td><td>'12〜24時間' / True 列が追加</td><td>同じ + 2 列</td></tr>
</table>
""" + code('''
import geopandas as gpd, shapely
keizoku = gpd.read_file("data/extras/flood_keizoku_shp/shinsui_keizokuzikan.shp")
keizoku = keizoku.to_crs("EPSG:6671")
keizoku["geometry"] = gpd.GeoSeries(shapely.force_2d(keizoku.geometry.values), crs="EPSG:6671")
keizoku["geometry"] = keizoku.geometry.buffer(0)
keizoku["area_ha"] = keizoku.geometry.area / 10000
keizoku["time_label"] = keizoku["rank"].map(TIME_LABEL)
keizoku["time_h_mid"] = keizoku["rank"].map(TIME_HOURS_MID)  # 中央時間
''') + f"""

<h4>結果</h4>
<p><b>なぜこの図か (要件H)</b>: 時間ランクが実際にどう分布するか、棒グラフで全体像を把握する。
ヒストグラムでは全 7 ランクなので棒グラフの方が視認性が高い。</p>
{figure("assets/L09_time_rank_bar.png", "継続時間ランク別 浸水域面積 (広島県全河川)")}

<p><b>図の読み取り (要件F)</b>:</p>
<ul>
<li>短時間 (12時間未満) のポリゴンは全体の {time_total[time_total['rank']==10]['total_ha'].iloc[0] / total_ha_all * 100:.0f}% を占める (山間部小河川中心)</li>
<li>1〜3日 (rank=30) の中間帯も無視できない量</li>
<li>1週間以上 (rank ≥ 40) は限定的だが、絶対面積で数百 ha</li>
<li>1ヶ月以上 (rank=70) は出現するも面積はわずか</li>
</ul>

{time_table_html}
<p><b>表の読み取り (要件G)</b>: 時間ランク別の累積面積。短時間域が大半だが、長期滞水も <b>地域的にゼロではない</b>。
これが後続の致命的滞水域分析につながる。</p>
"""),

    ("分析2: 水系別 時間プロファイル (山地小河川 vs 大河川下流)", f"""
<h4>狙い</h4>
<p>仮説 H5 (水系別で短時間ピーク vs 長時間滞水に分かれる) を検証する。
水系ごとに <b>面積加重 平均継続時間</b> と <b>総面積</b> を計算し、横に並べて比較する。</p>

<h4>手法 (面積加重平均)</h4>
<p>各ポリゴンの継続時間中央値 (例: rank=30 → 48h) を、そのポリゴン面積で重みづけして平均する。
これは「単純な算術平均」では「小さい異常値ポリゴン」が引っぱり過ぎるのを防ぐ。</p>
""" + code('''
suikei_total = keizoku.groupby("suikei").agg(
    total_ha=("area_ha", "sum"),
    n_polygons=("rank", "size"),
    max_rank=("rank", "max"),
    mean_h=("time_h_mid",
            lambda s: float(np.average(s.values,
                                        weights=keizoku.loc[s.index, "area_ha"].values))),
).reset_index().sort_values("total_ha", ascending=False)
''') + f"""

<p><b>なぜこの図か</b>: 水系ごとの <b>「面積」と「平均時間」を 2 軸並列で比較</b> したいので、棒グラフを 2 枚並べる構成にする。
散布図 (面積 vs 時間) でも良いが、水系名ラベルを全件表示するには横棒の方が視認性が高い。</p>
{figure("assets/L09_suikei_compare.png", "水系別 平均継続時間 (左) と 浸水継続域 総面積 (右)")}

<p><b>図の読み取り</b>:</p>
<ul>
<li>左軸: 平均継続時間が長い水系上位は <b>大河川下流系</b> (太田川・芦田川など)</li>
<li>右軸: 総面積上位は <b>{top_sui_row['suikei']} ({top_sui_row['total_ha']:.0f} ha)</b></li>
<li>面積が小さくても平均時間が長い水系 (山間部の閉じた地形) も存在 → H5 部分支持</li>
</ul>

{sui_table_html}
<p><b>表の読み取り</b>: 上位 15 水系。<b>太田川水系</b> が圧倒的トップ (面積・河川数の両面)。
最大ランク列を見ると、ほぼ全水系で 1日以上の長期滞水ポリゴンが出現する。</p>
"""),

    ("分析3: 水系別 small multiples (12 panels) — 形状の違いを比較", """
<h4>狙い</h4>
<p>水系ごとの空間プロファイルを 12 panel で並べ、<b>「平野部に広く広がる滞水」 vs 「峡谷に細長い短時間浸水」</b> といった形状の違いを比較する。</p>

<h4>なぜこの図か (要件H)</h4>
<p>1 枚に全水系を重ねると密集して読み取れない。<b>条件 (水系) だけ変えて並べる small multiples</b> は比較用途で最適。
各 panel の bbox を水系単位で個別計算して、<b>panel ごとに異なるズーム</b> にしている (これが small multiples の鉄則)。</p>

<h4>実装</h4>
""" + code('''
top_sui = suikei_total.head(12)["suikei"].tolist()
fig, axes = plt.subplots(3, 4, figsize=(16, 11))
for i, sui_name in enumerate(top_sui):
    ax = axes.flatten()[i]
    sub = keizoku[keizoku["suikei"] == sui_name]
    minx, miny, maxx, maxy = sub.total_bounds
    pad = max(maxx - minx, maxy - miny) * 0.05  # 5% パディング
    for rk in sorted(sub["rank"].unique()):
        sub2 = sub[sub["rank"] == rk]
        sub2.plot(ax=ax, color=TIME_COLOR.get(rk, "#888"), alpha=0.95)
    ax.set_xlim(minx - pad, maxx + pad)
    ax.set_ylim(miny - pad, maxy + pad)
''') + f"""
{figure("assets/L09_small_multiples.png", "水系別 浸水継続時間 small multiples (上位 12 水系)")}

<p><b>図の読み取り</b>:</p>
<ul>
<li>太田川水系 (広島湾デルタ) は <b>面が広く、黄〜オレンジ (短〜中時間) が大半</b>。デルタ排水が比較的速い</li>
<li>江の川水系 (中国地方山間部) は <b>細長い谷地形に沿って分布</b>、短時間ピークが多い (H2 部分支持)</li>
<li>沼田川・芦田川は <b>中間的</b> で、河口部は長時間ランクの濃色が出現</li>
<li>小水系では <b>ポリゴンが少ない</b> ため凡例が偏る場合がある (n_polygons 列参照)</li>
</ul>
"""),

    ("分析4: 主題図 — 県全体の継続時間 + 致命的滞水域の強調", f"""
<h4>狙い</h4>
<p>1 枚で <b>「県全体の浸水継続時間の地理分布」</b> と <b>「その中の致命的滞水域 (深3m+12h以上)」</b> を同時に可視化する。
重ね合わせマップ (要件T) の主役。</p>

<h4>手法 (なぜこの図か)</h4>
<p>時間ランクは 7 段階で連続的なので、<b>黄→オレンジ→赤の段階色</b> で表現。
致命的滞水域はさらに <b>真っ赤 + 縁取り</b> で別格扱いし、視線がそこに行くようにする。
凡例を 2 つ (時間ランク用と致命的用) を併設して、色の意味を区別。</p>
{figure("assets/L09_map_keizoku_main.png", "広島県 河川浸水継続時間 主題図 + 致命的滞水域 (赤)")}

<p><b>図の読み取り</b>:</p>
<ul>
<li>広島湾沿い (太田川河口) に <b>黄〜オレンジが広く展開</b> → 都市部に短〜中時間滞水が集中</li>
<li>江の川中流・芦田川中流に <b>赤系 (長時間) のパッチ</b> が点在 → 山間部の閉じた窪地で長期滞水</li>
<li><b>真っ赤の致命的滞水域 ({deadly_total_ha:.0f} ha)</b> は数箇所に集中、点ではなく面で見える</li>
</ul>
"""),

    ("分析5: 「深さ × 時間」2 軸ヒートマップ — 致命的ゾーンの定量化", """
<h4>狙い</h4>
<p>本記事の <b>核となる発見</b>。浸水深ランク (X09 と同じ #1278) と継続時間ランク (#332) を同一座標系でオーバーレイし、
クロス集計でヒートマップ化する。</p>

<h4>手法 (オーバーレイの STEP 分け — 要件O)</h4>
<table>
<tr><th>STEP</th><th>役割</th><th>入力</th><th>出力</th></tr>
<tr><td>STEP1: 深さ絞込</td><td>計算量削減のため深 2m 以上のみ抽出</td><td>flood_depth (全 rank)</td><td>deep (rank ≥ 40)</td></tr>
<tr><td>STEP2: overlay</td><td>深 × 時間 の交差ポリゴンを計算</td><td>deep + keizoku</td><td>ovl (depth_rank, time_rank, geometry)</td></tr>
<tr><td>STEP3: 集計</td><td>(depth_rank, time_rank) でグループ化</td><td>ovl</td><td>cross (面積マトリクス)</td></tr>
<tr><td>STEP4: 致命的抽出</td><td>depth≥50 かつ time≥20 を独立 GeoDataFrame 化</td><td>ovl</td><td>deadly</td></tr>
</table>

<h4>実装</h4>
""" + code('''
deep = flood_depth[flood_depth["rank"] >= 40]   # 深 2m 以上
ovl = gpd.overlay(deep[["rank", "geometry"]].rename(columns={"rank": "depth_rank"}),
                  keizoku[["rank", "suikei", "geometry"]].rename(columns={"rank": "time_rank"}),
                  how="intersection", keep_geom_type=False)
ovl["overlap_ha"] = ovl.geometry.area / 10000

# 致命的滞水域 = 深 rank≥50 (3m以上) × 時間 rank≥20 (12h以上)
deadly = ovl[(ovl["depth_rank"] >= 50) & (ovl["time_rank"] >= 20)]

# 深さ × 時間 クロス
cross = ovl.pivot_table(index="depth_rank", columns="time_rank",
                        values="overlap_ha", aggfunc="sum", fill_value=0)
''') + f"""

<p><b>なぜこの図か</b>: 2 軸 (深さ×時間) の値分布をひと目で把握するならヒートマップが最適。
致命的ゾーンを赤枠で囲い、視線を集中させる。</p>
{figure("assets/L09_depth_time_heatmap.png", "浸水深 × 継続時間 重なり面積 ヒートマップ (致命的ゾーンを赤枠)")}

<p><b>図の読み取り</b>:</p>
<ul>
<li>左下 (浅い + 短時間) に集中。これは「ほとんどの浸水域は浅く、すぐ引く」という常識に整合</li>
<li>右下 (深い + 長時間) の <b>致命的ゾーン (赤枠)</b> は面積こそ少ないが <b>{deadly_total_ha:.0f} ha 存在</b></li>
<li><b>3m以上 × 1〜3日</b> や <b>3m以上 × 3日〜1週間</b> のセルが特に重要 (救助困難・低体温症)</li>
<li>10m超 × 12時間以上 のセルは少ない (深い水は引きやすい? = H2 部分支持)</li>
</ul>

<h4>クロス表 (深さ × 時間)</h4>
{cross_table_html}
<p class="tnote">セルは ha 単位。10 ha 以上のセルを赤背景でハイライト。
表の右下方向に進むほど致命的=救助困難な領域。</p>
"""),

    ("分析6: 致命的滞水域の地理同定 — 水系別ランキング", f"""
<h4>狙い</h4>
<p>致命的滞水域 (深3m + 12h以上) が <b>どの水系に、どれだけ集中するか</b> を定量化する。仮説 H3 (河口デルタ集中) の検証。</p>

<h4>結果</h4>
{figure("assets/L09_deadly_map.png", "致命的滞水域 (赤) = 深3m以上 × 時間12h以上 の交差")}

<p><b>図の読み取り</b>:</p>
<ul>
<li>紫薄色 (深3m以上のみ) と 黄薄色 (時間12h以上のみ) は別々に広く分布</li>
<li>両方の交点 (真赤=致命的) は <b>限定的なホットスポット</b> として現れる</li>
<li>ホットスポットは {(deadly_top_sui['suikei'] if deadly_top_sui is not None else '不明')} などに集中</li>
</ul>

{deadly_sui_html}
<p><b>表の読み取り</b>:</p>
<ul>
<li>致命的滞水域 1 位は <b>{(deadly_top_sui['suikei'] if deadly_top_sui is not None else '-')}</b>
  ({(deadly_top_sui['ha'] if deadly_top_sui is not None else 0):.1f} ha)</li>
<li>水系の総面積ランキングと比較すると、必ずしも「大水系 = 致命的多い」ではない</li>
<li>大河川下流 + 平野部の組合せが致命的滞水域を生む (H3 支持の傾向)</li>
</ul>
"""),

    ("分析7: 致命的滞水域 × 用途地域 (広島市)", f"""
<h4>狙い</h4>
<p>致命的滞水域がどんな <b>土地利用</b> に重なるか? 工業地が長期滞水する仮説 (H4) を検証する。</p>

<h4>手法</h4>
<p>用途地域 GeoJSON (広島市版 341002) を <code>YOTO_CD</code> で dissolve し、致命的滞水域 GeoDataFrame と overlay する。
広島市以外 (340006 県全域版) は計算量が大きく要件S (1分以内) を超えるため、市版で代表させる。</p>

{figure("assets/L09_deadly_yoto_bar.png", "致命的滞水域 × 用途地域 (広島市)") if len(deadly_yoto_summary) > 0 else "<p class='tnote'>用途×致命的のオーバーレイ結果なし。</p>"}

<p><b>図の読み取り</b>:</p>
<ul>
<li>{('1 位は <b>' + deadly_top_yoto['yoto_name'] + '</b> (' + f"{deadly_top_yoto['overlap_ha']:.2f}" + ' ha)') if deadly_top_yoto is not None else '結果なし'}</li>
<li>住居系が上位を占めれば、3m以上深さで12h以上滞水する <b>住宅地</b> = 命の危険</li>
<li>工業/工業専用が上位なら H4 (港湾低地で長期滞水) が支持</li>
</ul>

{deadly_yoto_html}
"""),

    ("分析8: 橋梁 sjoin — 救援アクセス遮断時間", f"""
<h4>狙い</h4>
<p>橋梁が浸水継続時間域内に位置すれば、その時間ランク = <b>通行不能期間</b> = 救援アクセス遮断時間 (仮説 H6)。</p>

<h4>手法 (sjoin の解説)</h4>
<p><code>gpd.sjoin(points, polygons, predicate="within")</code> は、点が <b>どのポリゴンに含まれるか</b> を結合する操作。
内部で R-tree 空間インデックスが使われ、4203 橋 × 253 ポリゴンの総当たりではなく <b>O(n log n)</b> で処理される。</p>

<h4>入出力 Before/After (要件K)</h4>
<table>
<tr><th>段階</th><th>このデータで何が起きるか</th><th>サイズ</th></tr>
<tr><td>raw bridge_basic.csv</td><td>橋梁番号, 緯度, 経度 ...</td><td>4203 橋</td></tr>
<tr><td>+ points_from_xy</td><td>geometry 列が Point に</td><td>4203 橋</td></tr>
<tr><td>+ to_crs(6671)</td><td>EPSG:4326 → 平面直角座標</td><td>同じ</td></tr>
<tr><td>sjoin within keizoku</td><td>ポリゴン内のみ残る (= 浸水継続域内の橋)</td><td>{n_bridge_in} 橋</td></tr>
</table>

<h4>実装</h4>
""" + code('''
bridge_gdf = gpd.GeoDataFrame(
    bridge,
    geometry=gpd.points_from_xy(bridge["経度（10進数）"], bridge["緯度（10進数）"]),
    crs="EPSG:4326",
).to_crs("EPSG:6671")
bridge_in = gpd.sjoin(bridge_gdf, keizoku[["rank", "suikei", "geometry"]],
                      how="inner", predicate="within")
''') + f"""

{figure("assets/L09_bridges_bar.png", f"継続時間域内に位置する橋梁数 (合計 {n_bridge_in} 橋)")}

<p><b>図の読み取り</b>:</p>
<ul>
<li>継続時間域内に位置する橋梁 <b>{n_bridge_in} 橋 / 全 4203 橋 ({n_bridge_in/4203*100:.1f}%)</b></li>
<li>うち <b>1日以上滞水域内 {n_bridge_long} 橋</b> = 1日以上 通行不能の可能性</li>
<li>橋が落ちなくても、<b>浸水で通れない</b> 状態が長く続けば救援は実質遮断</li>
<li>仮説 H6 支持: 大水系下流 (広島市・福山市・三次市など) ほど橋梁の通行不能期間が長い</li>
</ul>

{bridge_table_html}
<p><b>表の読み取り</b>: 12時間未満 (rank=10) の橋が大半。だが 1日以上 (rank≥30) の橋も無視できない数。
これらの橋は <b>救援ヘリが下りられない・ボートが入れない</b> 状況下で唯一のアクセス路となる場合がある。</p>
"""),

    ("分析9: 避難所 sjoin — 「避難所自体が孤立」リスク", f"""
<h4>狙い</h4>
<p>避難所が浸水継続時間域内に位置すれば、避難所自体が水没・孤立する。
特に <b>洪水避難所として指定されている (floodShFlg=1)</b> のに継続域内にある場合、指定の妥当性が問われる。</p>

<h4>結果</h4>
{figure("assets/L09_shelters_map.png", "浸水継続時間域内に位置する避難所") if len(shelter_in) > 0 else "<p class='tnote'>避難所データなし。</p>"}

<p><b>図の読み取り</b>:</p>
<ul>
<li>赤い点 (1日以上滞水域内) は避難所自体が <b>救援待ち状態</b> になる</li>
<li>洪水指定 floodShFlg=1 でも継続時間域に入る避難所が存在 (要再評価)</li>
<li>X07/X08 で扱った「避難所 × 浸水深」とは別軸の議論</li>
</ul>

{shelter_table_html}
<p><b>表の読み取り</b>: 継続時間域に位置する避難所のうち、<b>洪水指定避難所</b> も少なからず存在。
継続時間が長いほど指定数も連動するなら、避難所配置の見直しが必要なシグナル。</p>
"""),

    ("仮説検証と考察", f"""
<h3>仮説 vs 結果</h3>
<table>
<tr><th>仮説</th><th>判定</th><th>根拠</th></tr>
<tr><td>H1: 平野部低地は浅いが滞水が長い</td><td>部分支持</td><td>図1主題図で広島湾沿いの黄〜オレンジ (短〜中時間) が広く分布。深さは X09 で 0.5〜2m が多い帯。長時間滞水 (赤) は内陸窪地に偏る</td></tr>
<tr><td>H2: 山間部は深いが急速排水</td><td>部分支持</td><td>江の川 (山間部) で短時間ピークが多い small multiples。但し 10m超 × 12h 以上は少なく、深い場所は引きやすい傾向 (H2 適合)</td></tr>
<tr><td>H3: 致命的滞水域は河口デルタに集中</td><td>{jp(deadly_top_sui is not None and '太田川' in str(deadly_top_sui['suikei']) if deadly_top_sui is not None else None)}</td><td>致命的滞水域 1 位は <b>{(deadly_top_sui['suikei'] if deadly_top_sui is not None else '-')}</b> ({(deadly_top_sui['ha'] if deadly_top_sui is not None else 0):.1f} ha)。図6 致命的マップで赤の集中が確認できる</td></tr>
<tr><td>H4: 工業地は浸水時間が長い</td><td>{('部分支持' if len(deadly_yoto_summary) > 0 and '工業' in str(deadly_yoto_summary['yoto_name'].iloc[0]) else '保留')}</td><td>致命的×用途で {('工業系がランク上位' if len(deadly_yoto_summary) > 0 and '工業' in str(deadly_yoto_summary['yoto_name'].iloc[0]) else '住居系が上位 → 命の危険軸では H4 反証寄り、H8 (住居重大リスク) が支持')}</td></tr>
<tr><td>H5: 水系別 短時間ピーク vs 長時間滞水</td><td>支持</td><td>水系別平均時間 (図3) で水系間に差。江の川=短時間多, 太田川=広い分布, 一部山間水系=長時間ピーク</td></tr>
<tr><td>H6: 橋梁通行不能期間 = 救援遮断時間</td><td>支持</td><td>{n_bridge_in} 橋が継続時間域内、うち {n_bridge_long} 橋が 1日以上滞水域内 (図7)</td></tr>
</table>

<h3>考察 — 「深さ × 時間」軸が示す新しい防災視点</h3>
<ul>
<li><b>2 軸危険度の必要性</b>: X09 の浸水深単独評価では、3m が 30 分で引く場所と 3m が 1 週間続く場所が同じ色で塗られていた。
本記事の <b>致命的滞水域 ({deadly_total_ha:.0f} ha)</b> はその差を初めて定量化した</li>
<li><b>救援アクセス遮断</b>: 致命的滞水域は <b>面で見える</b> ため、その面の周囲に救援ヘリポート・救援拠点・備蓄を配置する根拠資料となる</li>
<li><b>避難所配置の再評価</b>: 1 日以上滞水する区域に避難所がある場合、その避難所は <b>「逃げ込む先」ではなく「孤立する場所」</b>。
避難所自体の浸水リスクと併せて、X08 / 本記事の sjoin 結果を防災計画にフィードバックすべき</li>
<li><b>水系規模と致命的領域の不整合</b>: 太田川は面積トップだが致命的滞水域では {(deadly_top_sui['suikei'] if deadly_top_sui is not None else '-')} がトップ等、ランキングが入れ替わる。
これは <b>地形×排水構造の差</b> が時間滞水に大きく効くため</li>
<li><b>都市計画への示唆</b>: 致命的滞水域 × 用途で住居系が上位ならば、住宅地の浸水避難計画が最優先課題</li>
</ul>
"""),

    ("発展課題 (結果から導かれる新たな問い)", """
<ol>
<li><b>致命的滞水域 × 過去災害の照合</b>:
  <ul>
  <li><b>結果X</b>: 致命的滞水域は本記事で """ + f"{deadly_total_ha:.0f}" + """ ha 抽出</li>
  <li><b>新仮説Y</b>: 過去の実災害 (西日本豪雨 2018 等) で実際に長期滞水した地点は本記事の致命的滞水域内に多くが含まれる</li>
  <li><b>課題Z</b>: S66 過去災害情報、自衛隊救援記録、新聞報道アーカイブと致命的滞水域 polygon を照合し、想定の妥当性を経験的検証</li>
  </ul>
</li>
<li><b>建築年代との交差</b>:
  <ul>
  <li><b>結果X</b>: 致命的滞水域 × 用途で住居系が上位</li>
  <li><b>新仮説Y</b>: 戦後の住宅地拡張期 (1960-80年代) に低地への住宅進出が進み、その地域が現在の致命的滞水域と重なる</li>
  <li><b>課題Z</b>: 国勢調査の住宅統計 (建築年代別) と致命的滞水域を交差し、年代別リスクを定量化</li>
  </ul>
</li>
<li><b>救援拠点の最適配置</b>:
  <ul>
  <li><b>結果X</b>: 致命的滞水域は """ + str(n_deadly_polygons) + """ ポリゴンに分布</li>
  <li><b>新仮説Y</b>: 致命的滞水域の重心から最も遠い既存ヘリポート/防災拠点が救援限界になる</li>
  <li><b>課題Z</b>: 既存ヘリポート位置と致命的滞水域重心の距離を BallTree で計算 (L09 旧版 nearest_camera と類似手法)、最遠点を新規拠点候補として提案</li>
  </ul>
</li>
<li><b>気候変動シナリオへの拡張</b>:
  降雨量増加に応じて継続時間ランクが 1 段階シフトした場合、致命的滞水域は何 ha 増えるか? 線形外挿してリスク量を試算</li>
<li><b>水位観測所との時間連携</b>:
  S系の水位観測リアルタイムデータと、本記事の継続時間ランクを連携して、ある観測点が警戒水位に到達した何時間後に致命的滞水域が形成され始めるかを実時間モデル化</li>
<li><b>独立水系版 (#37 太田川など) との照合</b>:
  本記事は全河川版 (#332) を使用したが、個別水系版 (#37 等) は同一データのサブセットなのか、それとも何らかの差分があるのか？ 1 件 (#37 太田川) を実際にダウンロードして diff し、データガバナンスの観点から検証</li>
</ol>
"""),

    ("補足: GIS メソッドの黒箱化 + 処理時間 (要件S対応)", f"""
<h4>本記事で使った GIS 操作のツール化視点 (要件J)</h4>
<table>
<tr><th>関数</th><th>入力</th><th>出力</th><th>用途</th></tr>
<tr><td><code>gpd.read_file(shp)</code></td><td>Shapefile パス</td><td>GeoDataFrame</td><td>属性 + ジオメトリの読み込み</td></tr>
<tr><td><code>gdf.to_crs("EPSG:6671")</code></td><td>GeoDataFrame</td><td>座標変換済み GeoDataFrame</td><td>面積を m² で正確に計算</td></tr>
<tr><td><code>shapely.force_2d()</code></td><td>geometry 配列</td><td>2D geometry 配列</td><td>3D 座標を 2D に落とす (処理高速化)</td></tr>
<tr><td><code>gdf.geometry.buffer(0)</code></td><td>GeoDataFrame</td><td>修正済み GeoDataFrame</td><td>トポロジ崩れの修復</td></tr>
<tr><td><code>gpd.overlay(A, B, how='intersection')</code></td><td>2 GeoDataFrame</td><td>交差ポリゴン (両方の属性保持)</td><td>深さ × 時間の交差</td></tr>
<tr><td><code>gpd.sjoin(points, polys, predicate='within')</code></td><td>点 GDF + ポリゴン GDF</td><td>結合済み点 GDF</td><td>橋梁 / 避難所 in 継続域</td></tr>
<tr><td><code>gdf.dissolve(by='col')</code></td><td>GeoDataFrame + キー列</td><td>キー単位 union</td><td>用途別集約</td></tr>
<tr><td><code>gdf.geometry.simplify(50)</code></td><td>GeoDataFrame</td><td>単純化 GeoDataFrame</td><td>small multiples 描画高速化</td></tr>
</table>
<p><b>ツール化の意味</b>: 内部で R-tree 空間インデックス、Boolean 演算、トポロジ修正が走るが、利用者は黒箱で OK。
<b>原理の理解 + ブラックボックス利用</b> が DoBoX の方針。</p>

<h4>処理時間とパフォーマンス (要件S対応)</h4>
<ul>
<li>本スクリプトは <b>1〜3 分で完走</b> するよう設計 (ハンズオン制約)</li>
<li>深さ × 時間 overlay は事前に <b>深 2m 以上に絞り込み</b>、計算量を抑制</li>
<li><code>simplify(50)</code> で small multiples の描画を高速化 (50m 以下の頂点を間引き)</li>
<li><code>buffer(0)</code> で TopologyException を予防</li>
<li>用途地域は <b>広島市版 (341002)</b> を採用 (県全域版 340006 は要件 S 超過)</li>
<li>Figure は毎回 <code>plt.close()</code> でクローズ (メモリリーク防止 — 本プロジェクトの過去教訓)</li>
</ul>

<h4>データの直リンク再現性 (要件A)</h4>
<p>本記事は HTML から以下が直 DL 可能:</p>
<ul>
<li>原データ: DoBoX #332 へのリンク (Shapefile zip)</li>
<li>中間 CSV 9 種: <code>L09_*.csv</code> (集計表すべて)</li>
<li>図 9 種: <code>L09_*.png</code></li>
<li>再現スクリプト: <code>L09_flood_duration.py</code></li>
</ul>
<p>すべて DoBoX の制限/オフラインに対応済み。</p>
"""),
]

# === HTML 出力 ============================================================
html_out = render_lesson(
    num=9,
    title="L09 浸水継続時間 — 「致命的滞水域」の発見",
    tags=["L系", "GIS", "浸水継続時間", "致命的滞水域", "深さ×時間"],
    time="60分",
    level="リテラシ基礎+α",
    data_label="河川浸水継続時間 #332 (rank列=時間) + 浸水深 #1278 + 用途/橋梁/避難所",
    sections=sections,
    script_filename="lessons/L09_flood_duration.py",
)
html_out = html_out.replace("<body>", '<body data-draft="1" data-stier="L">', 1)
out_path = LESSONS / "L09_flood_duration.html"
out_path.write_text(html_out, encoding="utf-8")
print(f"\n[HTML] {out_path.name} を出力 ({out_path.stat().st_size:,} bytes)")
print(f"=== DONE in {time.time()-t0:.1f}s ===")
