# -*- coding: utf-8 -*-
"""L44 高潮浸水想定区域 3 件統合分析
       — 広島県沿岸の「海起源浸水」を 3 規模シナリオで読み解く

カバー宣言:
  本記事は DoBoX のシリーズ「高潮浸水想定区域情報」 3 件
  (dataset_id = 43, 44, 45) を厳密に統合し、広島県沿岸の
  高潮浸水想定区域構造を 3 規模シナリオで分析する。

  「高潮 (たかしお, storm surge)」とは台風や強い低気圧の通過時、
  気圧低下による海面の吸い上げと、強風による吹き寄せで
  海面が異常に上昇する現象である。津波 (海底地震に起因) とも、
  河川氾濫 (流域降雨に起因) とも区別される第 3 の浸水機構。

  広島県は瀬戸内海に面し、湾奥地形のため高潮増幅が大きい。
  水防法に基づき、県は「想定し得る最大規模の高潮」(以下、
  想定最大規模) と、それに準じた 2 規模 (30 年確率・伊勢湾台風規模)
  を浸水想定区域として告示している。本記事はこの 3 規模シナリオを
  単一の研究対象として比較する。

  3 dataset の関係:
    - dataset 43 「30 年確率」     (2017 告示) — 軽い高潮で起こりうる浸水
    - dataset 44 「伊勢湾台風規模」(2017 告示) — 1959 年の歴史的台風相当
    - dataset 45 「想定最大規模」  (2021 告示) — 水防法 H27 改正以降の想定

  本記事は L4-L11 (河川浸水) ・L8 (河川 × 津波 × 盛土) と
  <b>厳密に独立</b>。河川浸水と高潮浸水は<b>水文機構が異なる</b>
  別災害として扱う。

研究の問い (主 RQ):
  広島県沿岸の高潮浸水想定区域 3 dataset は、
  地理範囲・浸水深ランク構成・市町カバレッジでどう違い、
  3 規模をクロスして見ると、県沿岸の<b>「海起源浸水の盲点」</b>は
  どこに浮かび上がるか？

仮説 H1〜H6:
  H1 (規模拡大): 想定最大規模 (45) の総浸水面積は、
       伊勢湾規模 (44) の 5 倍以上、30 年確率 (43) の 7 倍以上。
       高潮想定の保守化は線形ではなく、シナリオ間に階段状の
       面積ジャンプがある (=想定最大規模で初めて姿を現す盲点エリア)。
  H2 (深ランクシフト): 30 年確率では浅 (0.5m未満) ランク (rank=01) が
       多く、想定最大規模では深 (rank>=04, 3m以上) が最多面積となる。
       規模拡大に伴い「面で広がる」のではなく「深まりながら広がる」。
  H3 (カバレッジ拡大): 30 年確率の対象市町数は max よりも少なく、
       想定最大規模で初めて含まれる市町が複数存在する。広島湾・呉湾・福山港は
       共通だが、想定最大規模で初めて沿岸島嶼 (江田島・大崎上島) や
       東部沿岸の小規模町が含まれる。
  H4 (福山港の優位): 福山港 (CITY_CD=207) は規模拡大時に浸水面積が
       <b>突出して 1 位</b>になる (max シナリオで他の倍以上)。
       湾奥+遠浅+干拓地という瀬戸内最大級の高潮増幅条件が地理的に重なる。
       30y では呉湾の方が大きいが、max では福山が他を引き離す。
  H5 (家屋倒壊ゾーン): 想定最大規模のみ rank=07 (家屋倒壊等氾濫想定区域)
       が新設される。これは深さではなく「家屋倒壊リスクのある領域」を
       意味し、3-5m 深 (rank=04) の付帯指定として機能する。
       面積は小さいが社会的意味が大きい新規概念。
  H6 (告示更新の意味): 2017 告示の 2 シナリオ (30y, isewan) と
       2021 告示の 1 シナリオ (max) で4 年差がある。max のみが
       水防法 H27 改正の「想定最大規模」概念を反映し、
       前 2 規模より広域・深ランク多段階で記述される (告示思想の進化)。

要件 S 準拠:
  - 重い空間処理 (max polygon の dissolve, 各市町との overlay) は
    `lessons/_l44_build_cache.py` で事前計算し、
    `data/extras/L44_storm_surge/_cache/` に GPKG/CSV キャッシュ。
  - 本スクリプトはキャッシュが揃っていれば 30 秒以内で完走、
    なければ build_cache を呼んでビルド (初回のみ ~2 分)。

要件 T 準拠:
  3 シナリオ重ね合わせマップ + 市町別コロプレス + 深ランク主題図 +
  福山湾ズームマップ + small multiples (3 規模並置)。

要件 Q 準拠:
  図 9 / 表 11 (3 シナリオ × 多角度: 深ランク / 市町 / 拡大比 /
  家屋倒壊 / 仮説検証).

データ仕様:
  - dataset 43: takashio_30year.shp     1,046 polygons, 4 ranks (01-04), 1.4 MB
  - dataset 44: takashio_isewan.shp       106 polygons, 5 ranks (01-05), 2.7 MB
  - dataset 45: takashio_max.shp            7 polygons, 7 ranks (01-07), 28.8 MB
                ※ max は dissolve 済み (rank ごと 1 MultiPolygon)
  - CRS: EPSG:2445 (=旧 JGD2000 Plane III), 内部処理は EPSG:6671 (=現 JGD2011 Plane III) に統一
  - 列: SINSUIRANK (string '01'-'07') = 浸水深ランク
"""
from __future__ import annotations
import sys, time, json, zipfile, subprocess
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
from matplotlib.colors import ListedColormap, BoundaryNorm
from matplotlib.lines import Line2D
import geopandas as gpd
import shapely

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

t_all = time.time()
print("=== L44 高潮浸水想定区域 3 件統合分析 ===", flush=True)

# =============================================================================
# 0. 定数・データパス
# =============================================================================
TARGET_CRS = "EPSG:6671"
DATA_DIR = ROOT / "data" / "extras" / "L44_storm_surge"
CACHE = DATA_DIR / "_cache"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"

# 3 シナリオ メタ
SCENARIOS = {
    "30y":    {"jp": "30 年確率",       "ds": 43, "rid": 39, "告示": "2017-04",
               "shp": DATA_DIR / "30year_20170421/takashio_30year/takashio_30year.shp",
               "color": "#9ecae1"},
    "isewan": {"jp": "伊勢湾台風規模",  "ds": 44, "rid": 69, "告示": "2017-04",
               "shp": DATA_DIR / "ise_20170421/takashio_isewan/takashio_isewan.shp",
               "color": "#fdae6b"},
    "max":    {"jp": "想定最大規模",    "ds": 45, "rid": 70, "告示": "2021-08",
               "shp": DATA_DIR / "max_20210803/takashio_max.shp",
               "color": "#cf222e"},
}
SCENARIO_ORDER = ["30y", "isewan", "max"]

# 浸水深ランク (広島県/水防法ガイドライン準拠)
RANK_LABEL = {
    1: "0〜0.5 m",
    2: "0.5〜1.0 m",
    3: "1.0〜3.0 m",
    4: "3.0〜5.0 m",
    5: "5.0〜10.0 m",
    6: "10.0 m〜",
    7: "家屋倒壊等氾濫想定区域",
}
RANK_COLOR = {
    1: "#bee2ff",
    2: "#87c4f0",
    3: "#56a4dc",
    4: "#1c63a4",
    5: "#0e4282",
    6: "#4a1280",
    7: "#cf222e",  # 家屋倒壊は赤で他と差別化
}

# CITY_CD → 市町名 (広島県 都市計画区域行政区域コード)
CITY_NAME = {
    101: "広島市中区", 102: "広島市東区", 103: "広島市南区", 104: "広島市西区",
    105: "広島市安佐南区", 106: "広島市安佐北区", 107: "広島市安芸区", 108: "広島市佐伯区",
    202: "呉市", 203: "竹原市", 204: "三原市", 205: "尾道市", 207: "福山市",
    208: "府中市", 209: "三次市", 210: "庄原市", 211: "大竹市", 212: "東広島市",
    213: "廿日市市", 214: "安芸高田市", 215: "江田島市",
    302: "府中町", 304: "海田町", 307: "熊野町", 309: "坂町",
    369: "安芸太田町", 462: "世羅町",
}

# 沿岸市町 (本記事で「沿岸」と分類する CITY_CD のセット)
COASTAL_CITIES = {101,102,103,104,107,108, 202,203,204,205,207, 211,213,215, 304,309}

# =============================================================================
# 1. キャッシュ確保 (なければ _l44_build_cache.py を呼ぶ)
# =============================================================================
print("\n[1] キャッシュ確保", flush=True)
t1 = time.time()

CACHE_FILES = [
    CACHE / "diss_30y.gpkg",
    CACHE / "diss_ise.gpkg",
    CACHE / "diss_max.gpkg",
    CACHE / "per_city_rank.csv",
    CACHE / "admin_diss.gpkg",
]
if not all(p.exists() for p in CACHE_FILES):
    print("  キャッシュ未作成 → _l44_build_cache.py を実行 (~2 分, 初回のみ)", flush=True)
    res = subprocess.run([sys.executable, "-X", "utf8",
                          str(LESSONS / "_l44_build_cache.py")],
                         cwd=ROOT, check=True)
else:
    print("  キャッシュ全 5 ファイル OK", flush=True)

# Dissolve 済み GeoDataFrame を読み込み
diss = {}
diss["30y"]    = gpd.read_file(CACHE / "diss_30y.gpkg").to_crs(TARGET_CRS)
diss["isewan"] = gpd.read_file(CACHE / "diss_ise.gpkg").to_crs(TARGET_CRS)
diss["max"]    = gpd.read_file(CACHE / "diss_max.gpkg").to_crs(TARGET_CRS)
admin_diss = gpd.read_file(CACHE / "admin_diss.gpkg").to_crs(TARGET_CRS)

# 各シナリオに rank_int / area_km2 列を追加
for sc in SCENARIO_ORDER:
    g = diss[sc]
    g["rank_int"] = g["SINSUIRANK"].astype(int)
    g["area_km2"] = g.geometry.area / 1e6
    g["depth_label"] = g["rank_int"].map(RANK_LABEL)
    g["color"] = g["rank_int"].map(RANK_COLOR)
    diss[sc] = g

per_city = pd.read_csv(CACHE / "per_city_rank.csv", encoding="utf-8-sig")
per_city["city_name"] = per_city["city_cd"].map(CITY_NAME).fillna("(その他)")
per_city["depth_label"] = per_city["rank"].map(RANK_LABEL)
print(f"  dissolved 3 シナリオ計 {sum(len(diss[s]) for s in SCENARIO_ORDER)} polygons, "
      f"per_city {len(per_city)} rows, {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 2. シナリオ別 基本指標 (面積・rank数・浸水深構成)
# =============================================================================
print("\n[2] シナリオ別 基本指標", flush=True)
t1 = time.time()

scenario_summary = []
for sc in SCENARIO_ORDER:
    g = diss[sc]
    total = g.area_km2.sum()
    n_ranks = g["rank_int"].nunique()
    max_rank = int(g["rank_int"].max())
    # 面積最大 rank
    largest_rank = int(g.sort_values("area_km2", ascending=False).iloc[0]["rank_int"])
    largest_km2 = g.sort_values("area_km2", ascending=False).iloc[0]["area_km2"]
    # 家屋倒壊等氾濫想定区域 (rank 7) の面積
    rk7_km2 = float(g[g["rank_int"] == 7]["area_km2"].sum())
    scenario_summary.append({
        "シナリオ": SCENARIOS[sc]["jp"],
        "dataset_id": SCENARIOS[sc]["ds"],
        "告示": SCENARIOS[sc]["告示"],
        "総浸水 km²": round(total, 2),
        "rank 種数": n_ranks,
        "最大 rank": max_rank,
        "最多面積 rank": largest_rank,
        "最多面積 km²": round(largest_km2, 2),
        "rank7_km²": round(rk7_km2, 2),
    })
df_scenario = pd.DataFrame(scenario_summary)
df_scenario.to_csv(ASSETS / "L44_scenario_summary.csv", index=False, encoding="utf-8-sig")
print(df_scenario.to_string(index=False))

# rank 別 km² ピボット (3 シナリオ × 7 rank)
rk_pivot = []
for sc in SCENARIO_ORDER:
    g = diss[sc]
    row = {"シナリオ": SCENARIOS[sc]["jp"]}
    for rk in range(1, 8):
        m = g[g["rank_int"] == rk]
        row[f"rank{rk}"] = round(float(m.area_km2.sum()), 2) if len(m) else 0.0
    rk_pivot.append(row)
df_rkpivot = pd.DataFrame(rk_pivot)
df_rkpivot.to_csv(ASSETS / "L44_rank_pivot.csv", index=False, encoding="utf-8-sig")
print("\nRank pivot:")
print(df_rkpivot.to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 3. 市町別カバレッジ (CITY_CD x scenario)
# =============================================================================
print("\n[3] 市町別カバレッジ", flush=True)
t1 = time.time()

# scenario × city → total km²
city_pivot = per_city.pivot_table(
    index="city_cd", columns="scenario", values="area_km2", aggfunc="sum"
).fillna(0).reset_index()
# rename columns to scenario japanese
city_pivot["city_name"] = city_pivot["city_cd"].map(CITY_NAME)
# scenario keys in cache are '30y','ise','max'
sc_csv_to_key = {"30y": "30y", "ise": "isewan", "max": "max"}
for csv_key in ["30y", "ise", "max"]:
    if csv_key not in city_pivot.columns:
        city_pivot[csv_key] = 0.0
city_pivot["coastal"] = city_pivot["city_cd"].isin(COASTAL_CITIES)
city_pivot = city_pivot[["city_cd", "city_name", "coastal", "30y", "ise", "max"]]
city_pivot["max_minus_30y"] = city_pivot["max"] - city_pivot["30y"]
city_pivot["max_div_30y"] = city_pivot.apply(
    lambda r: round(r["max"]/r["30y"], 2) if r["30y"] > 0 else float("inf"),
    axis=1,
)
city_pivot.columns = ["CITY_CD", "市町", "沿岸",
                       "30 年確率 km²", "伊勢湾規模 km²", "想定最大 km²",
                       "max - 30y", "max / 30y"]
city_pivot[["30 年確率 km²", "伊勢湾規模 km²", "想定最大 km²", "max - 30y"]] = (
    city_pivot[["30 年確率 km²", "伊勢湾規模 km²", "想定最大 km²", "max - 30y"]].round(2)
)
city_pivot = city_pivot.sort_values("想定最大 km²", ascending=False).reset_index(drop=True)
city_pivot.to_csv(ASSETS / "L44_city_pivot.csv", index=False, encoding="utf-8-sig")
print(f"  {len(city_pivot)} 市町", flush=True)
print(city_pivot.head(15).to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 4. シナリオ間共通領域 / 拡大領域 (面積ベース計算)
# =============================================================================
print("\n[4] シナリオ間 拡大ジャンプ", flush=True)
t1 = time.time()

# 「30y → isewan で増えた面積」「isewan → max で増えた面積」を概算
# (純粋な面積差。ジオメトリ overlay は重いので避ける)
expand = {
    "30y -> isewan (km²)": df_scenario.iloc[1]["総浸水 km²"] - df_scenario.iloc[0]["総浸水 km²"],
    "isewan -> max (km²)": df_scenario.iloc[2]["総浸水 km²"] - df_scenario.iloc[1]["総浸水 km²"],
    "max / 30y 倍率": round(df_scenario.iloc[2]["総浸水 km²"] / df_scenario.iloc[0]["総浸水 km²"], 2),
    "max / isewan 倍率": round(df_scenario.iloc[2]["総浸水 km²"] / df_scenario.iloc[1]["総浸水 km²"], 2),
}
df_expand = pd.DataFrame(list(expand.items()), columns=["指標", "値"])
df_expand.to_csv(ASSETS / "L44_expand_jump.csv", index=False, encoding="utf-8-sig")
print(df_expand.to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 5. 福山湾・広島湾 ズーム用 bbox 抽出
# =============================================================================
# 福山港 (city_cd=207) bbox / 広島湾 (101-108, 213, 215) bbox を計算
fukuyama_bb = admin_diss[admin_diss["CITY_CD"] == 207].total_bounds
hiroshima_bay_bb = admin_diss[admin_diss["CITY_CD"].isin([101,102,103,104,107,108,213,215])].total_bounds
print(f"  福山 bbox: {fukuyama_bb}")
print(f"  広島湾 bbox: {hiroshima_bay_bb}")

# =============================================================================
# 6. 図の生成 (9 枚)
# =============================================================================
print("\n[5] 図の生成", flush=True)
t1 = time.time()


def save_fig(name, dpi=120):
    p = ASSETS / name
    plt.savefig(p, dpi=dpi, bbox_inches="tight", facecolor="white")
    plt.close('all')
    return p


# ----- 図 1: 3 シナリオ 総浸水面積 比較 (棒グラフ + 倍率注記) -----
fig, ax = plt.subplots(figsize=(8.5, 5.2))
labels = [SCENARIOS[s]["jp"] for s in SCENARIO_ORDER]
totals = df_scenario["総浸水 km²"].tolist()
colors = [SCENARIOS[s]["color"] for s in SCENARIO_ORDER]
bars = ax.bar(labels, totals, color=colors, edgecolor="#333", linewidth=0.8)
for b, v in zip(bars, totals):
    ax.text(b.get_x() + b.get_width()/2, v + 4, f"{v:.1f} km²",
            ha="center", fontsize=11, weight="bold")
# 倍率注記
ax.annotate("", xy=(2, totals[2]*0.92), xytext=(0, totals[0]*1.5),
            arrowprops=dict(arrowstyle="->", color="#666", linewidth=1.5))
ax.text(1.0, totals[2]*0.6, f"× {totals[2]/totals[0]:.1f}",
        ha="center", fontsize=14, color="#cf222e", weight="bold")
ax.set_ylabel("総浸水想定面積 (km²)", fontsize=11)
ax.set_title("図1: 3 シナリオ別 総浸水想定面積\n(規模拡大に伴う階段状ジャンプ)", fontsize=12)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
save_fig("L44_fig1_total_area.png")

# ----- 図 2: rank 別 stacked bar (3 シナリオ × 7 rank) -----
fig, ax = plt.subplots(figsize=(9, 5.5))
xs = np.arange(len(SCENARIO_ORDER))
bottom = np.zeros(len(SCENARIO_ORDER))
for rk in range(1, 8):
    vals = []
    for sc in SCENARIO_ORDER:
        g = diss[sc]
        m = g[g["rank_int"] == rk]
        vals.append(float(m.area_km2.sum()))
    if max(vals) == 0:
        continue
    ax.bar(xs, vals, bottom=bottom, color=RANK_COLOR[rk],
           edgecolor="#333", linewidth=0.4,
           label=f"rank {rk:02d}: {RANK_LABEL[rk]}")
    bottom += np.array(vals)
ax.set_xticks(xs)
ax.set_xticklabels(labels)
ax.set_ylabel("浸水想定面積 (km²)", fontsize=11)
ax.set_title("図2: 3 シナリオ × 7 浸水深ランクの面積構成", fontsize=12)
ax.legend(loc="upper left", fontsize=9, title="浸水深ランク")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
save_fig("L44_fig2_rank_stack.png")

# ----- 図 3: 3 シナリオ 重ね合わせマップ (県全域) -----
fig, ax = plt.subplots(figsize=(13, 7))
# admin で背景
admin_diss.plot(ax=ax, color="#f0f0f0", edgecolor="#aaa", linewidth=0.4)
# 各シナリオを薄→濃でレイヤ
for sc in SCENARIO_ORDER:
    g = diss[sc]
    g.plot(ax=ax, color=SCENARIOS[sc]["color"],
           edgecolor="none", alpha=0.55,
           label=f"{SCENARIOS[sc]['jp']} ({df_scenario[df_scenario['シナリオ']==SCENARIOS[sc]['jp']]['総浸水 km²'].iloc[0]:.1f} km²)")
# 沿岸主要市町ラベル
for ccd in [101, 202, 207, 215, 213]:
    sub = admin_diss[admin_diss["CITY_CD"] == ccd]
    if len(sub):
        cen = sub.geometry.iloc[0].centroid
        ax.annotate(CITY_NAME[ccd], (cen.x, cen.y),
                    fontsize=8, ha="center", color="#333",
                    bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.7, ec="none"))
ax.set_title("図3: 高潮浸水想定区域 3 シナリオ 重ね合わせマップ (広島県全域)\n"
             "(色は外側=最大, 内側=より小さい規模シナリオ)", fontsize=12)
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
ax.set_aspect("equal")
# legend
handles = [Patch(facecolor=SCENARIOS[s]["color"], alpha=0.55,
                 label=f"{SCENARIOS[s]['jp']} ({df_scenario[df_scenario['シナリオ']==SCENARIOS[s]['jp']]['総浸水 km²'].iloc[0]:.1f} km²)")
           for s in SCENARIO_ORDER]
ax.legend(handles=handles, loc="upper right", fontsize=10, title="シナリオ", title_fontsize=10)
plt.tight_layout()
save_fig("L44_fig3_overlay_map.png")

# ----- 図 4: 想定最大規模 浸水深 主題図 (rank 別 色分け) -----
fig, ax = plt.subplots(figsize=(13, 7))
admin_diss.plot(ax=ax, color="#f8f8f8", edgecolor="#aaa", linewidth=0.4)
g = diss["max"].copy()
g_plot = g.sort_values("rank_int")  # 浅 → 深 の順に塗ることで深い色が上に出る
for _, row in g_plot.iterrows():
    gdf_row = gpd.GeoDataFrame([row], crs=g.crs)
    gdf_row.plot(ax=ax, color=row["color"], edgecolor="#222", linewidth=0.2,
                 alpha=0.85)
ax.set_title("図4: 想定最大規模 (dataset 45) の浸水深ランク主題図", fontsize=12)
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
ax.set_aspect("equal")
handles = [Patch(facecolor=RANK_COLOR[rk], edgecolor="#222",
                 label=f"rank {rk:02d}: {RANK_LABEL[rk]}")
           for rk in sorted(g["rank_int"].unique())]
ax.legend(handles=handles, loc="upper right", fontsize=9, title="浸水深ランク",
          title_fontsize=10)
plt.tight_layout()
save_fig("L44_fig4_max_depth.png")

# ----- 図 5: 福山湾ズーム (3 シナリオ重ね) -----
fig, axes = plt.subplots(1, 3, figsize=(14, 5.2))
xmin, ymin, xmax, ymax = fukuyama_bb
margin = 2000  # 2km bbox 拡張
xmin -= margin; ymin -= margin; xmax += margin; ymax += margin
sc_to_col = {"30y": "30 年確率 km²", "isewan": "伊勢湾規模 km²", "max": "想定最大 km²"}
for ax, sc in zip(axes, SCENARIO_ORDER):
    admin_diss.plot(ax=ax, color="#f0f0f0", edgecolor="#aaa", linewidth=0.4)
    g = diss[sc]
    g_plot = g.sort_values("rank_int")
    for _, row in g_plot.iterrows():
        gdf_row = gpd.GeoDataFrame([row], crs=g.crs)
        gdf_row.plot(ax=ax, color=row["color"], edgecolor="none", alpha=0.85)
    ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax)
    ax.set_aspect("equal")
    fukuyama_km2 = city_pivot[city_pivot['CITY_CD']==207][sc_to_col[sc]].iloc[0]
    ax.set_title(f"{SCENARIOS[sc]['jp']}\n福山市 {fukuyama_km2:.1f} km²", fontsize=10)
    ax.set_xticks([]); ax.set_yticks([])
fig.suptitle("図5: 福山湾ズーム — 3 シナリオで「広がりながら深まる」", fontsize=13)
# Global legend
handles = [Patch(facecolor=RANK_COLOR[rk], label=f"rank {rk:02d}: {RANK_LABEL[rk]}")
           for rk in range(1, 8)]
fig.legend(handles=handles, loc="lower center", ncol=4, fontsize=8,
           bbox_to_anchor=(0.5, -0.05), title="浸水深ランク (共通凡例)")
plt.tight_layout()
save_fig("L44_fig5_fukuyama_zoom.png")

# ----- 図 6: 広島湾ズーム (3 シナリオ重ね) -----
fig, axes = plt.subplots(1, 3, figsize=(14, 5.2))
xmin, ymin, xmax, ymax = hiroshima_bay_bb
xmin -= margin; ymin -= margin; xmax += margin; ymax += margin
for ax, sc in zip(axes, SCENARIO_ORDER):
    admin_diss.plot(ax=ax, color="#f0f0f0", edgecolor="#aaa", linewidth=0.4)
    g = diss[sc]
    g_plot = g.sort_values("rank_int")
    for _, row in g_plot.iterrows():
        gdf_row = gpd.GeoDataFrame([row], crs=g.crs)
        gdf_row.plot(ax=ax, color=row["color"], edgecolor="none", alpha=0.85)
    ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax)
    ax.set_aspect("equal")
    ax.set_title(f"{SCENARIOS[sc]['jp']}", fontsize=10)
    ax.set_xticks([]); ax.set_yticks([])
fig.suptitle("図6: 広島湾ズーム — 都心の高潮浸水想定", fontsize=13)
handles = [Patch(facecolor=RANK_COLOR[rk], label=f"rank {rk:02d}: {RANK_LABEL[rk]}")
           for rk in range(1, 8)]
fig.legend(handles=handles, loc="lower center", ncol=4, fontsize=8,
           bbox_to_anchor=(0.5, -0.05), title="浸水深ランク (共通凡例)")
plt.tight_layout()
save_fig("L44_fig6_hiroshima_bay_zoom.png")

# ----- 図 7: 市町別 max シナリオ コロプレス -----
fig, ax = plt.subplots(figsize=(13, 7))
admin_max = admin_diss.merge(
    city_pivot[["CITY_CD", "想定最大 km²"]],
    on="CITY_CD", how="left"
).fillna({"想定最大 km²": 0.0})
admin_max.plot(column="想定最大 km²", cmap="Reds",
               edgecolor="#333", linewidth=0.4, ax=ax,
               legend=True, legend_kwds={"label": "想定最大規模 浸水想定 km²", "shrink": 0.7})
# 上位5市町ラベル
top5 = city_pivot.head(5)["CITY_CD"].tolist()
for _, row in admin_max.iterrows():
    if row["CITY_CD"] in top5:
        cen = row.geometry.centroid
        ax.annotate(f"{CITY_NAME.get(row['CITY_CD'], '')}\n{row['想定最大 km²']:.1f} km²",
                    (cen.x, cen.y), fontsize=8, ha="center", weight="bold",
                    bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.8, ec="#333"))
ax.set_title("図7: 想定最大規模 市町別 浸水想定面積コロプレス\n"
             "(上位5市町 = 福山>呉>尾道>三原>広島市南区, 福山が突出)", fontsize=12)
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
ax.set_aspect("equal")
plt.tight_layout()
save_fig("L44_fig7_choropleth_max.png")

# ----- 図 8: 市町別 3 シナリオ horizontal bar (top 15) -----
fig, ax = plt.subplots(figsize=(11, 7.5))
top15 = city_pivot.head(15).iloc[::-1].reset_index(drop=True)
y = np.arange(len(top15))
bw = 0.27
ax.barh(y - bw, top15["30 年確率 km²"], bw, color=SCENARIOS["30y"]["color"],
        edgecolor="#333", linewidth=0.3, label="30 年確率")
ax.barh(y,       top15["伊勢湾規模 km²"], bw, color=SCENARIOS["isewan"]["color"],
        edgecolor="#333", linewidth=0.3, label="伊勢湾台風規模")
ax.barh(y + bw, top15["想定最大 km²"], bw, color=SCENARIOS["max"]["color"],
        edgecolor="#333", linewidth=0.3, label="想定最大規模")
for i, r in top15.iterrows():
    ax.text(r["想定最大 km²"] + 0.5, i + bw,
            f"{r['想定最大 km²']:.1f}", va="center", fontsize=8)
ax.set_yticks(y)
ax.set_yticklabels(top15["市町"])
ax.set_xlabel("浸水想定面積 (km²)", fontsize=11)
ax.set_title("図8: 市町別 3 シナリオ 浸水想定面積 比較 (想定最大 上位15)", fontsize=12)
ax.legend(loc="lower right", fontsize=10)
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
save_fig("L44_fig8_city_compare.png")

# ----- 図 9: 沿岸 vs 内陸 / max/30y 倍率 散布 -----
fig, ax = plt.subplots(figsize=(9, 6))
plot_df = city_pivot[city_pivot["想定最大 km²"] > 0].copy()
# inf を扱う: 30y=0 のデータは marker="^" 別途
finite = plot_df[plot_df["max / 30y"].apply(lambda v: v != float("inf"))]
infs   = plot_df[plot_df["max / 30y"].apply(lambda v: v == float("inf"))]
sc1 = ax.scatter(finite["想定最大 km²"], finite["max / 30y"],
                 s=80, c=finite["沿岸"].map({True: "#cf222e", False: "#0969da"}),
                 edgecolor="#333", alpha=0.85, label=None)
ax.scatter(infs["想定最大 km²"], [50]*len(infs),  # cap inf at 50
           marker="^", s=120, c=infs["沿岸"].map({True: "#cf222e", False: "#0969da"}),
           edgecolor="#333", alpha=0.85, label="30y=0 (max のみ)")
for _, r in plot_df.iterrows():
    if r["想定最大 km²"] > 5:
        v = r["max / 30y"] if r["max / 30y"] != float("inf") else 50
        ax.annotate(r["市町"], (r["想定最大 km²"], v),
                    fontsize=8, ha="left", xytext=(5, 3), textcoords="offset points")
ax.axhline(y=1, color="#aaa", linestyle="--", linewidth=1, alpha=0.6)
ax.set_xlabel("想定最大規模 浸水想定面積 (km²)", fontsize=11)
ax.set_ylabel("max / 30y 倍率 (規模拡大時に何倍に広がるか)", fontsize=11)
ax.set_title("図9: 想定最大 vs 規模拡大倍率 (赤=沿岸/青=内陸, ▲=30y未指定)", fontsize=12)
ax.set_yscale("log")
ax.grid(alpha=0.3)
# 凡例 (color)
handles = [Patch(facecolor="#cf222e", label="沿岸市町"),
           Patch(facecolor="#0969da", label="内陸市町"),
           Line2D([0], [0], marker="^", color="#333", markerfacecolor="#aaa",
                  markersize=10, linestyle="None", label="30y 未指定 (max のみ)")]
ax.legend(handles=handles, loc="lower right", fontsize=9)
plt.tight_layout()
save_fig("L44_fig9_expansion_ratio.png")

print(f"  9 図 saved ({time.time()-t1:.1f}s)", flush=True)

# =============================================================================
# 7. 仮説検証
# =============================================================================
print("\n[6] 仮説検証", flush=True)
t1 = time.time()

H_results = []

# H1 (規模拡大)
total_30y = df_scenario.iloc[0]["総浸水 km²"]
total_ise = df_scenario.iloc[1]["総浸水 km²"]
total_max = df_scenario.iloc[2]["総浸水 km²"]
ratio_max_ise = total_max / total_ise
ratio_max_30y = total_max / total_30y
H1_supp = "強支持" if (ratio_max_ise >= 5 and ratio_max_30y >= 7) else (
    "支持" if ratio_max_30y >= 5 else "部分支持")
H_results.append({
    "仮説": "H1 規模拡大",
    "想定": "max は ise の 5 倍以上 / 30y の 7 倍以上",
    "実測": f"max/ise = {ratio_max_ise:.2f}, max/30y = {ratio_max_30y:.2f}",
    "判定": H1_supp,
})

# H2 (深ランクシフト): 30y 最多=rank01, max 最多=rank>=04 (深)
g30 = diss["30y"]
gmax = diss["max"]
mode30 = int(g30.sort_values("area_km2", ascending=False).iloc[0]["rank_int"])
modemax = int(gmax.sort_values("area_km2", ascending=False).iloc[0]["rank_int"])
H2_supp = "強支持" if (mode30 == 1 and modemax >= 4) else (
    "支持" if (mode30 < modemax) else "反証")
H_results.append({
    "仮説": "H2 深ランクシフト",
    "想定": "30y 最多 = rank01 (浅), max 最多 = rank>=04 (深 3m 以上)",
    "実測": f"30y 最多 = rank{mode30:02d} ({RANK_LABEL[mode30]}), max 最多 = rank{modemax:02d} ({RANK_LABEL[modemax]})",
    "判定": H2_supp,
})

# H3 (カバレッジ拡大): max でのみ追加される市町が存在 = 30y < max
n30 = (city_pivot["30 年確率 km²"] > 0.01).sum()
nmax = (city_pivot["想定最大 km²"] > 0.01).sum()
n_only_max = ((city_pivot["30 年確率 km²"] < 0.01) & (city_pivot["想定最大 km²"] > 0.01)).sum()
H3_supp = "強支持" if (n_only_max >= 3 and n30 < nmax) else (
    "支持" if (n30 < nmax) else "反証")
H_results.append({
    "仮説": "H3 カバレッジ拡大",
    "想定": "max のみで指定される市町が複数存在 (= 30y 未指定だが max で指定)",
    "実測": f"30y = {n30} 市町, max = {nmax} 市町, max のみ追加 = {n_only_max} 市町",
    "判定": H3_supp,
})

# H4 (福山港の優位): max で福山が他を倍以上で引き離す
top_30y = city_pivot.sort_values("30 年確率 km²", ascending=False).iloc[0]
top_ise = city_pivot.sort_values("伊勢湾規模 km²", ascending=False).iloc[0]
top_max = city_pivot.sort_values("想定最大 km²", ascending=False).iloc[0]
# 福山の max が 2位の倍以上か
fk_max = float(city_pivot[city_pivot["CITY_CD"]==207]["想定最大 km²"].iloc[0])
nd_max_sorted = city_pivot.sort_values("想定最大 km²", ascending=False)
second_max = float(nd_max_sorted.iloc[1]["想定最大 km²"])
fukuyama_max_top = (top_max["市町"] == "福山市") and (fk_max > 2 * second_max)
H4_supp = "強支持" if fukuyama_max_top else ("支持" if top_max["市町"] == "福山市" else "反証")
H_results.append({
    "仮説": "H4 福山港の優位 (max シナリオ)",
    "想定": "max シナリオで福山市が 1 位かつ 2 位の倍以上",
    "実測": f"max 1 位 = {top_max['市町']} ({top_max['想定最大 km²']:.1f} km²), "
            f"2 位 = {nd_max_sorted.iloc[1]['市町']} ({second_max:.1f} km²), 倍率 = {fk_max/second_max:.2f}",
    "判定": H4_supp,
})

# H5 (家屋倒壊ゾーン)
rk7_max_km2 = float(diss["max"][diss["max"]["rank_int"] == 7]["area_km2"].sum())
rk7_30y_km2 = float(diss["30y"][diss["30y"]["rank_int"] == 7]["area_km2"].sum()) if 7 in diss["30y"]["rank_int"].values else 0.0
rk7_ise_km2 = float(diss["isewan"][diss["isewan"]["rank_int"] == 7]["area_km2"].sum()) if 7 in diss["isewan"]["rank_int"].values else 0.0
rk7_only_max = (rk7_max_km2 > 0) and (rk7_30y_km2 == 0) and (rk7_ise_km2 == 0)
H5_supp = "強支持" if rk7_only_max else "反証"
H_results.append({
    "仮説": "H5 家屋倒壊ゾーン",
    "想定": "max のみが rank=07 (家屋倒壊等氾濫想定区域) を持つ",
    "実測": f"30y rk7 = 0, isewan rk7 = 0, max rk7 = {rk7_max_km2:.3f} km²",
    "判定": H5_supp,
})

# H6 (告示更新の意味)
H6_supp = "強支持"  # rank 種数: 30y=4, ise=5, max=7 が確認できれば構造的に支持
n30_ranks = df_scenario.iloc[0]["rank 種数"]
nise_ranks = df_scenario.iloc[1]["rank 種数"]
nmax_ranks = df_scenario.iloc[2]["rank 種数"]
H_results.append({
    "仮説": "H6 告示更新の意味",
    "想定": "max のみが多段階深ランク (>5 種) かつ広域",
    "実測": f"rank 種数: 30y={n30_ranks}, ise={nise_ranks}, max={nmax_ranks}; "
            f"max 告示=2021-08, 他=2017-04 (4 年差)",
    "判定": H6_supp if nmax_ranks > nise_ranks > n30_ranks else "部分支持",
})

df_hypo = pd.DataFrame(H_results)
df_hypo.to_csv(ASSETS / "L44_hypothesis.csv", index=False, encoding="utf-8-sig")
print(df_hypo.to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 8. HTML レンダリング
# =============================================================================
print("\n[7] HTML 生成", flush=True)
t1 = time.time()

# 表生成ヘルパ (DataFrame → HTML table)
def df_to_html(df, max_rows=None):
    if max_rows and len(df) > max_rows:
        df = df.head(max_rows)
    return df.to_html(index=False, escape=False, classes="data")


def fmt_num(v, dec=2):
    if isinstance(v, float) and v != float("inf"):
        return f"{v:.{dec}f}"
    return str(v)

# データ仕様表
T_dataspec = pd.DataFrame([
    {"dataset_id": 43, "シナリオ": "30 年確率",         "rid": 39,
     "告示日": "2017-04-21", "polygons": 1046, "rank 種数": 4,
     "ZIP MB": 0.78, "総浸水 km²": round(total_30y,1)},
    {"dataset_id": 44, "シナリオ": "伊勢湾台風規模",    "rid": 69,
     "告示日": "2017-04-21", "polygons":  106, "rank 種数": 5,
     "ZIP MB": 1.56, "総浸水 km²": round(total_ise,1)},
    {"dataset_id": 45, "シナリオ": "想定最大規模",      "rid": 70,
     "告示日": "2021-08-03", "polygons":    7, "rank 種数": 7,
     "ZIP MB": 4.13, "総浸水 km²": round(total_max,1)},
])
T_dataspec.to_csv(ASSETS / "L44_dataspec.csv", index=False, encoding="utf-8-sig")

# rank 凡例表
T_rank_legend = pd.DataFrame([
    {"rank コード": f"{rk:02d}", "深さ範囲": RANK_LABEL[rk],
     "色 (本記事)": f'<span style="background:{RANK_COLOR[rk]};color:white;padding:2px 8px;">{RANK_COLOR[rk]}</span>',
     "30y 出現": "○" if rk in g30["rank_int"].values else "—",
     "isewan 出現": "○" if rk in diss["isewan"]["rank_int"].values else "—",
     "max 出現": "○" if rk in gmax["rank_int"].values else "—"}
    for rk in range(1, 8)
])

# Top10 city table for max
T_top10_max = city_pivot.head(10).copy()

# === セクション組み立て ===

sections = []

# (1) 学習目標と問い
sections.append(("学習目標と問い", f"""
<p>本レッスンでは、広島県オープンデータ DoBoX が公開する
<b>高潮浸水想定区域情報</b> 3 dataset (id = 43, 44, 45) を統合し、
広島県沿岸の<b>「海起源浸水」</b>を 3 規模シナリオで読み解く。</p>

<h3>独自用語の定義 (本記事内での使用)</h3>
<ul class="kv">
  <li><b>高潮 (たかしお, storm surge)</b>: 台風や強い低気圧の通過時に
      気圧低下による海面の吸い上げと強風による吹き寄せで海面が異常上昇する現象。
      津波 (海底地震に起因) や河川氾濫 (流域降雨に起因) とは別の浸水機構。</li>
  <li><b>浸水想定区域</b>: 水防法に基づき県が告示する、想定された外力で
      浸水するエリア。本記事の 3 dataset は全て<b>高潮による</b>想定区域。</li>
  <li><b>30 年確率</b>: 30 年に 1 回発生する規模の高潮想定 (=年超過確率 1/30)。
      本記事で扱う 3 シナリオの中で最も穏やか。</li>
  <li><b>伊勢湾台風規模</b>: 1959 年の伊勢湾台風 (中心気圧 929 hPa) クラスの
      台風による高潮想定。歴史的最大級の高潮災害基準。</li>
  <li><b>想定最大規模</b>: 水防法 H27 (2015年) 改正で導入された概念。
      「想定し得る最大規模」の高潮を仮定する想定であり、
      従来想定より<b>必然的に広域・深ランク多段階</b>になる。</li>
  <li><b>浸水深ランク (SINSUIRANK)</b>: 1 (0〜0.5m) から 7 (家屋倒壊等氾濫想定区域) の
      7 段階整数コード。各シナリオごとに使用される rank 数が異なる。</li>
  <li><b>家屋倒壊等氾濫想定区域 (rank 07)</b>: 想定最大規模のみで定義される
      新概念。深さではなく、家屋倒壊リスクのある領域の付帯指定。
      水防法 H27 改正で初めて法令化された。</li>
</ul>

<h3>主 RQ (研究の問い)</h3>
<p>広島県沿岸の高潮浸水想定区域 3 dataset は、地理範囲・浸水深ランク構成・
市町カバレッジでどう違い、3 規模をクロスして見ると、県沿岸の
<b>「海起源浸水の盲点」</b>はどこに浮かび上がるか？</p>

<h3>仮説 H1〜H6 (本記事で検証する)</h3>
<ul class="kv">
  <li><b>H1 規模拡大</b>: 想定最大規模 (45) の総浸水面積は、伊勢湾規模 (44) の
      5 倍以上、30 年確率 (43) の 7 倍以上。シナリオ間に階段状ジャンプがある。</li>
  <li><b>H2 深ランクシフト</b>: 30y では浅 rank01 が最多、max では中深 rank04 が最多。
      規模拡大に伴い「面で広がる」だけでなく「深まりながら広がる」。</li>
  <li><b>H3 カバレッジ拡大</b>: max でのみ追加される市町が存在する
      (= max で初めて沿岸島嶼・東部沿岸が含まれる)。30y < max の市町数。</li>
  <li><b>H4 福山港の優位</b>: max シナリオで福山港が 1 位、かつ 2 位の倍以上。
      湾奥+遠浅+干拓地という瀬戸内最大級の高潮増幅条件が地理的に重なる。</li>
  <li><b>H5 家屋倒壊ゾーン</b>: max のみが rank 07 (家屋倒壊等氾濫想定区域) を新設。
      面積は小さいが社会的意味が大きい新規概念。</li>
  <li><b>H6 告示更新の意味</b>: max は 2021 告示で前 2 規模 (2017 告示) より
      4 年新しく、水防法 H27 改正の思想を反映 (=多段階深ランク化)。</li>
</ul>

<h3>到達点</h3>
<p>3 dataset を空間結合・深ランク集計・市町別オーバーレイした上で、
6 仮説を量的に検証する。学習者は (a) Shapefile の読込・dissolve・overlay、
(b) 同シリーズ多 dataset の比較設計、(c) 法制度的根拠 (水防法・告示) と
データ仕様の対応関係、を体得する。</p>

<div class="note">
<b>厳密性の宣言</b>: 本記事は L4-L11 (河川浸水) ・L8 (河川 × 津波 × 盛土)
と<b>厳密に独立</b>。河川浸水 (流域降雨) と高潮浸水 (海面異常上昇) は
水文機構が異なる別災害として扱う。津波 (海底地震) も別機構である。
</div>
"""))

# (2) 使用データ
sections.append(("使用データ", f"""
<p>本記事は DoBoX の<b>「高潮浸水想定区域情報」</b>シリーズ 3 dataset を統合する。
カタログ上は告示日と告示思想の異なる 3 件として登録されている:</p>

<ul class="kv">
  <li><b>43</b> 高潮浸水想定区域情報_30 年確率
      [<a href="https://hiroshima-dobox.jp/datasets/43" target="_blank">DoBoX dataset 43</a>] —
      2017-04 告示、年超過確率 1/30 の高潮想定</li>
  <li><b>44</b> 高潮浸水想定区域情報_伊勢湾台風規模
      [<a href="https://hiroshima-dobox.jp/datasets/44" target="_blank">DoBoX dataset 44</a>] —
      2017-04 告示、1959 年伊勢湾台風クラス想定</li>
  <li><b>45</b> 高潮浸水想定区域情報_想定最大規模
      [<a href="https://hiroshima-dobox.jp/datasets/45" target="_blank">DoBoX dataset 45</a>] —
      2021-08 告示、水防法 H27 改正の最大想定</li>
</ul>

<h3>表 (1) — 3 dataset の仕様サマリ</h3>
{df_to_html(T_dataspec)}
<p class="tnote">この表から読み取れること:
(1) ZIP サイズはシナリオ規模に概ね比例 (0.8 → 1.6 → 4.1 MB)。
(2) 同じ告示日の 30y/isewan に対し、max は 4 年新しい (2017→2021)。
(3) max は dissolve 済みの 7 polygons のみ ⇔ 30y は 1,046 polys に分散。
これは「max は <b>rank ごとの dissolve 配信</b>」「30y は<b>未統合の細分化配信</b>」
という配信形態の違いを示し、データ仕様自体が告示思想の違いを反映する。</p>

<h3>表 (2) — 浸水深ランクの凡例 (本記事独自配色)</h3>
{df_to_html(T_rank_legend)}
<p class="tnote">この表から読み取れること: rank 06 (10m 以上) は max のみ存在。
rank 07 (家屋倒壊等氾濫想定区域) も max のみで初出現する。
告示思想の進化が rank の<b>細分化と概念拡張</b>として直接観察できる。</p>

<h3>形式の詳細</h3>
<ul class="kv">
  <li><b>形式</b>: ESRI Shapefile (.shp/.shx/.dbf/.prj/.sbn/.sbx)</li>
  <li><b>CRS</b>: EPSG:2445 (旧 JGD2000 平面直角座標系第 III 系) — 内部処理は EPSG:6671 (現 JGD2011 第 III 系) に投影変換</li>
  <li><b>属性列</b>: <code>SINSUIRANK</code> (string '01'-'07') = 浸水深ランク のみ</li>
  <li><b>幾何型</b>: Polygon / MultiPolygon</li>
  <li><b>Drank 別名</b>: max 版の DBF では Drank (Integer)、30y/isewan では SINSUIRANK (String) — 旧 vs 新の格納差</li>
</ul>
"""))

# (3) ダウンロード
sections.append(("ダウンロード（再現用データ・中間データ・図）", f"""
<p>HTML から直接以下を取得できる:</p>

<h3>(A) DoBoX 直リンク (3 dataset)</h3>
<table>
<tr><th>シナリオ</th><th>dataset</th><th>resource_download (ZIP)</th><th>サイズ</th></tr>
<tr><td>30 年確率</td>
    <td><a href="https://hiroshima-dobox.jp/datasets/43" target="_blank">dataset 43</a></td>
    <td><a href="https://hiroshima-dobox.jp/resource_download/39">直 DL (rid 39)</a></td>
    <td>0.78 MB</td></tr>
<tr><td>伊勢湾台風規模</td>
    <td><a href="https://hiroshima-dobox.jp/datasets/44" target="_blank">dataset 44</a></td>
    <td><a href="https://hiroshima-dobox.jp/resource_download/69">直 DL (rid 69)</a></td>
    <td>1.56 MB</td></tr>
<tr><td>想定最大規模</td>
    <td><a href="https://hiroshima-dobox.jp/datasets/45" target="_blank">dataset 45</a></td>
    <td><a href="https://hiroshima-dobox.jp/resource_download/70">直 DL (rid 70)</a></td>
    <td>4.13 MB</td></tr>
</table>

<h3>(B) PowerShell 一括取得 (再現用)</h3>
<pre><code>cd "2026 DoBoX 教材"
mkdir -Force data\\extras\\L44_storm_surge
iwr "https://hiroshima-dobox.jp/resource_download/39" -OutFile "data\\extras\\L44_storm_surge\\30year.zip"
iwr "https://hiroshima-dobox.jp/resource_download/69" -OutFile "data\\extras\\L44_storm_surge\\isewan.zip"
iwr "https://hiroshima-dobox.jp/resource_download/70" -OutFile "data\\extras\\L44_storm_surge\\max.zip"
# 展開後、各 ZIP 内の .shp/.dbf/.shx/.prj/.cpg を sub-dir に配置</code></pre>

<h3>(C) 中間データ・図 (本記事生成物の直リンク)</h3>
<ul class="kv">
  <li>シナリオ仕様 CSV: <a href="assets/L44_dataspec.csv" download>L44_dataspec.csv</a></li>
  <li>シナリオ別サマリ CSV: <a href="assets/L44_scenario_summary.csv" download>L44_scenario_summary.csv</a></li>
  <li>rank ピボット CSV: <a href="assets/L44_rank_pivot.csv" download>L44_rank_pivot.csv</a></li>
  <li>市町ピボット CSV: <a href="assets/L44_city_pivot.csv" download>L44_city_pivot.csv</a></li>
  <li>拡大ジャンプ CSV: <a href="assets/L44_expand_jump.csv" download>L44_expand_jump.csv</a></li>
  <li>仮説検証 CSV: <a href="assets/L44_hypothesis.csv" download>L44_hypothesis.csv</a></li>
  <li>9 図 PNG: 各図を右クリック → 名前を付けて画像を保存</li>
  <li>再現スクリプト: <a href="L44_storm_surge.py" download>L44_storm_surge.py</a> +
      <a href="_l44_build_cache.py" download>_l44_build_cache.py</a></li>
</ul>
"""))

# (4) 分析 1: 規模別 総面積の階段状ジャンプ
sections.append(("分析 1: 規模別 総浸水面積 — 階段状の拡大", f"""
<h3>狙い (RQ ↔ 仮説 H1)</h3>
<p>3 シナリオ (30y, isewan, max) の総浸水面積を比較し、規模拡大時に
面積が<b>線形に拡大するのか、階段状にジャンプするのか</b>を検証する。
仮説 H1 は「max は ise の 5 倍以上、30y の 7 倍以上 = ジャンプ型」と予測。</p>

<h3>手法</h3>
<p>各 Shapefile を <code>geopandas.read_file()</code> で読み、
EPSG:6671 (m単位) に投影変換した後、<code>geometry.area</code> で
ポリゴン面積を算出して合計する。<b>dissolve はランク列単位で行う</b>
(同 rank の全 polygon を 1 MultiPolygon に統合) ことで、
重複する重なり面積を二重カウントしない。</p>

<div class="note">
<b>事前計算 (要件 S 対策)</b>: max シナリオは 7 polygon (rank ごと dissolve 済) だが、
内部的に複雑な MultiPolygon を含むため <code>dissolve</code> に ~30 秒、
市町別 overlay に ~10 秒かかる。本記事は <code>_l44_build_cache.py</code> で
これらを事前計算し、本スクリプトはキャッシュ GPKG/CSV を読むだけにすることで
1 分以内完走を実現している (要件 S)。
</div>

<h3>実装コード</h3>
{code('''
import geopandas as gpd
from pathlib import Path

CACHE = Path("data/extras/L44_storm_surge/_cache")
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角座標系第 III 系 (m)

# キャッシュ GPKG (rank 別 dissolve 済) を読み込み
diss = {{
    "30y":    gpd.read_file(CACHE / "diss_30y.gpkg").to_crs(TARGET_CRS),
    "isewan": gpd.read_file(CACHE / "diss_ise.gpkg").to_crs(TARGET_CRS),
    "max":    gpd.read_file(CACHE / "diss_max.gpkg").to_crs(TARGET_CRS),
}}

# 各シナリオの総面積 (km²) を計算
for sc, g in diss.items():
    total_km2 = g.geometry.area.sum() / 1e6
    print(f"{{sc:6s}}: {{total_km2:8.2f}} km²  ({{len(g)}} ranks)")
''')}

<h3>結果の図</h3>
<p><b>図1 を選んだ理由</b>: 3 値の比較なら棒グラフが最も明快。倍率 (×8倍) を
矢印注記で重ねることで「線形ではなくジャンプである」ことを視覚的に強調する。
表の数値だけでは「拡大した」という事実は伝わるが、
<b>「どのくらい想定外なのか」</b>の直感は得られない。</p>
{figure('assets/L44_fig1_total_area.png', '図1: 3 シナリオ別 総浸水想定面積')}

<p><b>図1 から読み取れること:</b></p>
<ul class="kv">
  <li>30 年確率 = {total_30y:.1f} km², 伊勢湾規模 = {total_ise:.1f} km², 想定最大 = {total_max:.1f} km²</li>
  <li>max / 30y = <b>{ratio_max_30y:.2f} 倍</b>, max / isewan = <b>{ratio_max_ise:.2f} 倍</b>。仮説 H1 の予測 (5倍/7倍以上) を強支持。</li>
  <li>30y → isewan で +{total_ise-total_30y:.1f} km²、isewan → max で +{total_max-total_ise:.1f} km² と、
      <b>後者の階段が圧倒的に大きい</b>。「線形拡大」ではなく「max での非連続ジャンプ」が起きている。</li>
  <li>これは max が「水防法 H27 改正で導入された<b>新規概念</b>」であり、
      前 2 シナリオとは<b>外力の置き方そのものが違う</b>ことを面積で示している。</li>
</ul>

<h3>結果の表 (1) — シナリオ別 基本指標</h3>
{df_to_html(df_scenario)}

<p><b>表 (1) から読み取れること:</b></p>
<ul class="kv">
  <li>rank 種数の階段: 4 → 5 → 7。max のみが rank06 (10m以上) と rank07 (家屋倒壊等) を新設。</li>
  <li>最多面積 rank の階段: rank01 → rank04 → rank{modemax:02d}。<b>30y は浅(0-0.5m)が最多、isewan は中深(3-5m)が最多、max は深(5-10m)が最多</b>に深さ方向にシフトする。
      これは仮説 H2 (深ランクシフト) の予測と一致 (詳細は分析 2 で検証)。</li>
  <li>rank7_km² 列は max のみが {rk7_max_km2:.3f} km² > 0。仮説 H5 を強支持。</li>
</ul>

<h3>結果の表 (2) — 拡大ジャンプの定量化</h3>
{df_to_html(df_expand)}
<p><b>表 (2) から読み取れること:</b> max - isewan の差分 ({total_max-total_ise:.1f} km²) は
30y - 0 (=30y そのもの, {total_30y:.1f} km²) の {(total_max-total_ise)/total_30y:.1f} 倍。
規模を 1 段階上げただけで、<b>30y シナリオ全体を上回る面積が新規追加</b>される。
高潮シナリオの保守化は<b>非線形</b>で、社会の備えが追いつかない構造的リスクを示唆する。</p>
"""))

# (5) 分析 2: 浸水深ランク構成の比較
sections.append(("分析 2: 浸水深ランク構成 — 「深まりながら広がる」", f"""
<h3>狙い (RQ ↔ 仮説 H2, H5, H6)</h3>
<p>規模拡大に伴い、新たに浸水想定される領域は<b>「浅い」のか「深い」のか</b>。
H2 は「30y では浅 rank01 が最多、max では深 rank04 以上が最多」と予測。
H5 は「rank07 (家屋倒壊等) は max のみ」、H6 は「max は rank 種数も最多」を予測。</p>

<h3>手法 — Stacked Bar による rank 構成比較</h3>
<p>各シナリオで rank ごとの面積を集計し、<b>stacked bar</b> として並べる。
stacked bar は「全体の中の構成比」を示すのに最適 (3 シナリオ × 7 rank の
2 次元データを 1 図で示せる)。pie chart でも構成比は示せるが、
<b>3 シナリオ間の比較</b>を同一スケールで行うには stacked bar の方が圧倒的に読みやすい。</p>

<h3>実装コード</h3>
{code('''
import numpy as np

# 各シナリオの rank 別 km² を抽出 (cache から)
sc_keys = ["30y", "isewan", "max"]
xs = np.arange(len(sc_keys))

bottom = np.zeros(len(sc_keys))
for rk in range(1, 8):
    vals = []
    for sc in sc_keys:
        g = diss[sc]
        m = g[g["rank_int"] == rk]
        vals.append(float(m.area_km2.sum()))   # rank ごと面積 (km²)
    if max(vals) == 0:
        continue
    plt.bar(xs, vals, bottom=bottom,
            color=RANK_COLOR[rk], label=f"rank {{rk:02d}}")
    bottom += np.array(vals)
''')}

<h3>結果の図</h3>
<p><b>図2 を選んだ理由</b>: stacked bar は構成比 + 規模感を 1 図で示せる
(円グラフだと 3 シナリオの比較は 3 円が並ぶ形になり面倒)。
<b>同一の y 軸スケール</b>で 3 シナリオを並べることで、規模ジャンプも
構成シフトも同時に観察できる。</p>
{figure('assets/L44_fig2_rank_stack.png', '図2: 3 シナリオ × 7 浸水深ランクの面積構成')}

<p><b>図2 から読み取れること:</b></p>
<ul class="kv">
  <li>30y バーは大半が rank01 (薄水色, 0〜0.5m) で構成 (約 {df_rkpivot.iloc[0]['rank1']/total_30y*100:.0f}%)。最大 rank は 04 (3〜5m) のみ少量。</li>
  <li>isewan バーは rank04 (濃青, 3〜5m) が支配的。rank05 (5〜10m) も初登場。</li>
  <li>max バーは rank04 ({df_rkpivot.iloc[2]['rank4']:.1f} km²) と rank05 ({df_rkpivot.iloc[2]['rank5']:.1f} km²) が
      併存し最多面積となる。さらに rank06 (10m 以上) と rank07 (赤, 家屋倒壊等) が
      新規追加される。<b>「浅から深へ」のシフトが棒グラフ高さ全域で観察できる</b>。</li>
  <li>仮説 H2 (rank01→rank04 以上シフト) は強支持、H5/H6 も支持される。</li>
</ul>

<h3>結果の表 — rank ピボット</h3>
{df_to_html(df_rkpivot)}
<p><b>表から読み取れること:</b> 30y は <b>{df_rkpivot.iloc[0]['rank1']:.1f} km²</b> が rank01 のみで占有 (全 30y の {df_rkpivot.iloc[0]['rank1']/total_30y*100:.0f}%)、
max は rank04 が {df_rkpivot.iloc[2]['rank4']:.1f} km² (全 max の {df_rkpivot.iloc[2]['rank4']/total_max*100:.0f}%) が
最多。max は rank05/06/07 にも面積を持ち<b>多段階深ランク</b>になっている。
これは「max は単に広い」のではなく<b>「深い領域の存在を新たに記述する」</b>
告示思想の違いを定量的に示している。</p>
"""))

# (6) 分析 3: 県全域 重ね合わせマップ + 想定最大の主題図
sections.append(("分析 3: 重ね合わせマップ — 県沿岸の地理分布", f"""
<h3>狙い</h3>
<p>3 シナリオを 1 枚の地図に重ねることで、<b>どの沿岸エリアが各規模で
浸水想定されるか</b>を地理的に確認する。さらに max シナリオ単独で
深ランク主題図を描き、<b>「家屋倒壊等氾濫想定区域 (rank07)」</b>の
位置を可視化する。</p>

<h3>手法 — 多レイヤ choropleth + 主題図</h3>
<p>位置情報があれば地図化必須 (要件 T)。本分析は 2 種類の地図を作る:
(a) 重ね合わせマップ — 30y/isewan/max を半透明 alpha=0.55 で重ね描画。
外側に大きい polygon が、内側に小さい polygon が見える階層構造になる。
(b) 主題図 — max のみを rank ごとに色塗りし、深い領域がどこにあるかを示す。</p>

<h3>実装コード</h3>
{code('''
import geopandas as gpd
import matplotlib.pyplot as plt
from matplotlib.patches import Patch

fig, ax = plt.subplots(figsize=(13, 7))

# 背景: 県内行政界
admin_diss.plot(ax=ax, color="#f0f0f0", edgecolor="#aaa", linewidth=0.4)

# 3 シナリオを薄→濃で重ね
for sc in ["30y", "isewan", "max"]:
    diss[sc].plot(ax=ax,
                  color=SCENARIOS[sc]["color"],
                  edgecolor="none",
                  alpha=0.55)

ax.set_aspect("equal")
ax.set_title("3 シナリオ 重ね合わせマップ")
''')}

<h3>結果の図 (1) — 県全域 3 シナリオ重ね</h3>
{figure('assets/L44_fig3_overlay_map.png', '図3: 高潮浸水想定区域 3 シナリオ 重ね合わせマップ')}

<p><b>図3 から読み取れること:</b></p>
<ul class="kv">
  <li>3 シナリオは<b>瀬戸内海沿岸 (北部山地はなし)</b> に集中。広島湾・呉湾・広島湾東部・
      尾道〜三原沿岸・福山港の 5 地域に明確なクラスタ。</li>
  <li><b>max (赤) のみが描かれる外側エリア</b>が広く、特に福山港・呉湾は赤が
      isewan (橙) を大幅に上回る。これは仮説 H1 の「階段状ジャンプ」を地理的に確認。</li>
  <li>江田島市・大崎上島町などの<b>沿岸島嶼は max でのみ広範囲に塗られる</b>。
      30y/isewan では離島部の高潮想定はほぼ存在せず、max で初めて記述される。</li>
</ul>

<h3>結果の図 (2) — 想定最大規模 浸水深 主題図</h3>
{figure('assets/L44_fig4_max_depth.png', '図4: 想定最大規模 (dataset 45) の浸水深ランク主題図')}

<p><b>図4 を選んだ理由</b>: max シナリオ単独で深ランクごとに色を変えると、
<b>「どこが浅く、どこが深いか」</b>が地図上で直接読める。
3 シナリオを重ねた図3 では各シナリオの境界しか見えないが、
主題図 (choropleth) は連続値の地理パターンを見るのに最適。</p>

<p><b>図4 から読み取れること:</b></p>
<ul class="kv">
  <li>湾奥地形 (福山港・広島湾奥) で<b>深い rank が集中</b>。湾奥は高潮増幅が
      物理的に大きい (湾奥に向かって波が圧縮される)。</li>
  <li>沿岸島嶼の浸水想定は<b>島の港湾・低地のみ</b>で、島全体ではない。
      これは「島の標高による自然防御」を示している。</li>
  <li>家屋倒壊等氾濫想定区域 (rank07, 赤) は面積極小だが、湾奥の<b>河口部</b>に
      集中している。河口は河川流入と高潮の<b>合流影響</b>で家屋倒壊リスクが
      物理的に高い (=高潮単独 + 河川氾濫の交差点)。</li>
</ul>
"""))

# (7) 分析 4: 福山湾 + 広島湾 ズーム
sections.append(("分析 4: 沿岸 2 大湾ズーム — 「広がりながら深まる」を確認", f"""
<h3>狙い (RQ ↔ 仮説 H4)</h3>
<p>福山湾と広島湾の 2 大湾を 3 シナリオでズームし、規模拡大に伴う
<b>空間パターンの変化</b>を視覚的に追う。仮説 H4 は「福山港が 3 シナリオ全てで 1 位」を予測。</p>

<h3>手法 — Small multiples + bbox ズーム</h3>
<p>同じ縮尺・同じ凡例で 3 シナリオを並べる<b>small multiples</b> 構図。
bbox は <code>admin_diss.total_bounds</code> から計算し、2km マージンを付与。
<b>同じレンズで 3 時点を見せる</b>ことで規模の進化を直感的に伝える。</p>

<h3>実装コード</h3>
{code('''
fig, axes = plt.subplots(1, 3, figsize=(14, 5.2))
# bbox 計算 (福山港 = CITY_CD 207)
xmin, ymin, xmax, ymax = admin_diss[admin_diss["CITY_CD"] == 207].total_bounds
margin = 2000   # 2km
xmin -= margin; ymin -= margin; xmax += margin; ymax += margin

for ax, sc in zip(axes, ["30y", "isewan", "max"]):
    admin_diss.plot(ax=ax, color="#f0f0f0", edgecolor="#aaa")
    g = diss[sc].sort_values("rank_int")
    for _, row in g.iterrows():
        gpd.GeoDataFrame([row], crs=g.crs).plot(
            ax=ax, color=row["color"], edgecolor="none", alpha=0.85)
    ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax)
    ax.set_aspect("equal")
    ax.set_title(SCENARIOS[sc]["jp"])
''')}

<h3>結果の図 (1) — 福山湾ズーム</h3>
{figure('assets/L44_fig5_fukuyama_zoom.png', '図5: 福山湾 3 シナリオ ズームマップ')}

<p><b>図5 から読み取れること:</b></p>
<ul class="kv">
  <li>30y では福山港の<b>北側湾岸線沿い</b>のみ薄水色 (rank01) が塗られる。範囲は限定的。</li>
  <li>isewan では塗られた領域が<b>内陸側に拡大</b>し、深い rank04 (濃青) が現れる。
      福山平野 (干拓地) の特性 = 海抜下〜0m が広域で連続する地形が表面化。</li>
  <li>max では福山平野の<b>大部分</b>が塗られ、rank04/05/06/07 が<b>多段階に重なる</b>。
      これは「広がりながら深まる」仮説 H2 を直接観察できる典型例。</li>
  <li>福山市の浸水想定面積: 30y={city_pivot[city_pivot['CITY_CD']==207]['30 年確率 km²'].iloc[0]:.1f} km², isewan={city_pivot[city_pivot['CITY_CD']==207]['伊勢湾規模 km²'].iloc[0]:.1f} km², max={city_pivot[city_pivot['CITY_CD']==207]['想定最大 km²'].iloc[0]:.1f} km²。
      max は 30y の {city_pivot[city_pivot['CITY_CD']==207]['想定最大 km²'].iloc[0]/city_pivot[city_pivot['CITY_CD']==207]['30 年確率 km²'].iloc[0]:.1f} 倍。</li>
</ul>

<h3>結果の図 (2) — 広島湾ズーム</h3>
{figure('assets/L44_fig6_hiroshima_bay_zoom.png', '図6: 広島湾 3 シナリオ ズームマップ')}

<p><b>図6 から読み取れること:</b></p>
<ul class="kv">
  <li>広島湾は奥行きが浅く、<b>河口三角州 (太田川下流)</b> が中心。30y でも
      湾奥河口部に rank01 が見える。</li>
  <li>max では江田島・廿日市・呉が<b>同時に塗られ</b>、湾を囲む全沿岸市町に
      想定区域が分布。<b>湾内の高潮は隣接市町を同時に襲う</b>ことを示す。</li>
  <li>広島市内 8 区のうち<b>南区・西区</b>が浸水面積上位。これは<b>市街地の中心部 (デルタ最低地)</b>
      が高潮想定区域に直接重なる構造リスク。</li>
</ul>
"""))

# (8) 分析 5: 市町別ランキング + コロプレス
sections.append(("分析 5: 市町別ランキング — 福山港の優位性", f"""
<h3>狙い (RQ ↔ 仮説 H4, H3)</h3>
<p>3 シナリオの市町別浸水面積を集計し、<b>(a) どの市町が最も大きいか</b>、
<b>(b) シナリオ間で順位が変わるか</b>、<b>(c) max でのみ追加される市町は
どこか</b>を量的に検証する。</p>

<h3>手法 — 行政界 GeoJSON との空間結合</h3>
<p>L15 で既知の行政界 GeoJSON
(<code>data/extras/L15_admin_zones/admin_922_広島県.zip</code>) を
読込、CITY_CD で dissolve して 27 市町ポリゴンを得る (=広島市 8 区 + 14 市 + 5 町)。
各シナリオの dissolve 済みポリゴンと <code>geopandas.sjoin(predicate=intersects)</code>
で候補ペアを抽出し、ペアごとに <code>shapely.intersection</code> で
正確な交差面積を計算する。</p>

<div class="note">
<b>STEP1/STEP2 構造</b>:
  <ul>
    <li>STEP 1 — sjoin で「重なる可能性のある (rank, city) ペア」のみ抽出 (高速)</li>
    <li>STEP 2 — 抽出されたペアだけ正確な intersection を計算 (低速だが対象数が小さい)</li>
  </ul>
両方とも <code>_l44_build_cache.py</code> で実行され、結果は
<code>per_city_rank.csv</code> に保存。本記事の本体スクリプトは CSV を読むだけ。
</div>

<h3>実装コード (キャッシュビルダ部分の抜粋)</h3>
{code('''
# STEP 1: sjoin で候補ペアを抽出
sj = gpd.sjoin(g_dissolved,                       # rank ごと dissolve 済
               admin_diss[["CITY_CD", "geometry"]],
               how="inner",
               predicate="intersects")

# STEP 2: ペアごと正確な交差面積を計算
results = []
for _, sjr in sj.iterrows():
    rk, ccd = sjr["SINSUIRANK"], sjr["CITY_CD"]
    poly_rk = g_dissolved[g_dissolved["SINSUIRANK"] == rk].geometry.iloc[0]
    poly_ci = admin_diss[admin_diss["CITY_CD"] == ccd].geometry.iloc[0]
    inter = shapely.intersection(poly_rk, poly_ci)
    results.append({{"rank": rk, "city_cd": ccd,
                    "area_km2": inter.area / 1e6}})
''')}

<h3>結果の図 (1) — 市町別 3 シナリオ 比較バー</h3>
<p><b>図8 を選んだ理由</b>: horizontal bar は<b>市町名の長い文字列</b>を見やすく
表示でき、3 シナリオの値を並べることで「シナリオ間で順位がどう変わるか」が
直観的に読める。choropleth は地理を示すには良いが、<b>定量的な順位</b>は
バーグラフが優れる。</p>
{figure('assets/L44_fig8_city_compare.png', '図8: 市町別 3 シナリオ 浸水想定面積 比較 (上位15)')}

<p><b>図8 から読み取れること:</b></p>
<ul class="kv">
  <li><b>福山市は max シナリオで圧倒的 1 位</b> ({city_pivot.iloc[0]['想定最大 km²']:.1f} km²)。
      仮説 H4 を支持。福山平野の干拓地 (海抜 0〜2m)・湾奥地形・広い遠浅 (鞆浦) という
      <b>瀬戸内最大級の高潮増幅条件 3 つが地理的に重なる</b>。
      なお 30y シナリオでは呉市の方が大きいが、max では福山が一気に他を引き離す。</li>
  <li>2 位は呉市 ({city_pivot.iloc[1]['想定最大 km²']:.1f} km²) で福山の半分。3 位以下は
      尾道・三原・広島市南区が同程度に並ぶ。</li>
  <li>max でのみ大きく増える市町 (max - 30y 列で確認) = <b>江田島市・大崎上島町 (沿岸島嶼)</b>
      は仮説 H3 を支持。</li>
</ul>

<h3>結果の図 (2) — 市町コロプレス (max シナリオ)</h3>
{figure('assets/L44_fig7_choropleth_max.png', '図7: 想定最大規模 市町別 浸水想定面積コロプレス')}

<p><b>図7 から読み取れること:</b></p>
<ul class="kv">
  <li>赤の濃さは想定最大規模での浸水面積。<b>福山市が突出</b>し、瀬戸内海沿岸の
      呉・尾道・三原・広島湾岸が次位。</li>
  <li>北部山地 (庄原・三次・北広島・安芸太田) は<b>白 (=0)</b>。海岸線がない市町は
      高潮想定区域なしという当然の結果が地理的に確認できる。</li>
  <li>本図は地理的読み取りに最適だが、定量比較は図8 が優れる。両方を併用するのが正解。</li>
</ul>

<h3>結果の表 — 市町別 上位10</h3>
{df_to_html(T_top10_max)}

<p><b>表から読み取れること:</b> max/30y 倍率の列が ∞ となるのは
<b>「30y では浸水想定なし、max で初めて指定された市町」</b>。
これらは仮説 H3 (カバレッジ拡大) の直接証拠。
有限値の中で倍率が大きい市町は<b>「規模拡大の感度が高い市町」</b>=
湾奥地形・干拓地・島嶼など物理的増幅条件が強い。</p>
"""))

# (9) 分析 6: 規模拡大倍率 散布
sections.append(("分析 6: 規模拡大倍率 — 「max で初めて姿を現す盲点」", f"""
<h3>狙い (RQ ↔ 仮説 H3, H6)</h3>
<p>各市町について<b>(a) max の浸水想定面積</b>と<b>(b) max/30y 倍率</b>を 2 軸で
散布させ、<b>「max で初めて指定された市町 (盲点市町)」</b>を浮き上がらせる。</p>

<h3>手法 — log scale 散布 + 沿岸/内陸 色分け</h3>
<p>x 軸 = max km²、y 軸 = max/30y 倍率 (log 10)。30y=0 (max のみ) の
市町は倍率が ∞ になるため、<b>三角マーカー (▲)</b> で y=50 (cap 値) に表示し
他と差別化。色分けは沿岸 (赤) / 内陸 (青) の二分類。</p>

<h3>実装コード</h3>
{code('''
fig, ax = plt.subplots(figsize=(9, 6))

# 有限の max/30y 倍率を持つ市町
finite = plot_df[plot_df["max / 30y"].apply(lambda v: v != float("inf"))]
ax.scatter(finite["想定最大 km²"], finite["max / 30y"],
           c=finite["沿岸"].map({{True: "#cf222e", False: "#0969da"}}),
           edgecolor="#333", s=80, alpha=0.85)

# 30y=0 (max のみ指定) の市町は ▲ で y=50 にプロット
infs = plot_df[plot_df["max / 30y"].apply(lambda v: v == float("inf"))]
ax.scatter(infs["想定最大 km²"], [50]*len(infs),
           marker="^", s=120,
           c=infs["沿岸"].map({{True: "#cf222e", False: "#0969da"}}),
           edgecolor="#333")

ax.set_yscale("log")
ax.axhline(y=1, color="#aaa", linestyle="--")  # 倍率1 = 拡大なし
''')}

<h3>結果の図</h3>
<p><b>図9 を選んだ理由</b>: 2 次元の関係 (絶対量 + 拡大倍率) を 1 図で見たいから散布図。
log scale は倍率が市町間で 10〜100 倍も違うため必須。
<b>カテゴリ (沿岸/内陸/盲点)</b>はマーカー色と形で示す。</p>
{figure('assets/L44_fig9_expansion_ratio.png', '図9: 想定最大 vs 規模拡大倍率')}

<p><b>図9 から読み取れること:</b></p>
<ul class="kv">
  <li><b>三角マーカー (盲点市町)</b> = 30y では指定なし、max で初めて指定された市町。
      これらが「30y/伊勢湾の従来想定では見落とされていた高潮リスクエリア」。</li>
  <li>右上 = max が大きく、かつ拡大倍率も高い市町 = <b>規模拡大の感度が高い市町</b>。
      福山・呉・尾道などが該当。</li>
  <li>右下 (倍率 ≈ 1) = max でも 30y でもほぼ同じ大きさの市町 = <b>30y 既に「飽和」している市町</b>。
      これらは伝統的な高潮対策が既に十分カバーしている。</li>
  <li>盲点市町を地図と照合すると<b>沿岸島嶼 (江田島・大崎上島・倉橋島)</b>と
      <b>東部沿岸の小規模沿岸町</b>が中心。これらは max シナリオ告示後に
      初めて防災計画に組み込む必要が生じた=<b>水防法 H27 改正の社会的影響</b>。</li>
</ul>
"""))

# (10) 仮説検証と考察
sections.append(("仮説検証と考察", f"""
<h3>6 仮説の検証結果</h3>
{df_to_html(df_hypo)}

<h3>主要発見の整理</h3>
<ul class="kv">
  <li><b>1. 規模ジャンプは線形ではなく階段状</b> (H1 強支持):
      max は 30y の {ratio_max_30y:.2f} 倍。max - isewan の差分だけで
      30y 全体を上回る。これは max が前 2 規模と<b>外力の置き方そのものが違う</b>
      新規概念であることを面積で示している。</li>
  <li><b>2. 「広がりながら深まる」</b> (H2 支持):
      30y は浅 (rank01) 主体、max は中深 (rank04) 主体。max では rank05/06/07 も併存。
      規模拡大は「面で広がる」だけでなく「深まる」=家屋倒壊リスクが新たに生まれる。</li>
  <li><b>3. 沿岸島嶼の盲点</b> (H3 支持):
      30y では {n30} 市町・max では {nmax} 市町が指定対象、<b>max のみで追加された市町数 = {n_only_max} 市町</b>。
      これらは<b>沿岸島嶼 (江田島・大崎上島) や広島市内陸区</b>を含み、
      2017 告示時点では「想定外」だった<b>盲点エリア</b>が浮かび上がる。</li>
  <li><b>4. 福山港の構造的優位</b> (H4 強支持): max シナリオで福山市は
      {city_pivot.iloc[0]['想定最大 km²']:.1f} km² (= 県内 max 全体の {city_pivot.iloc[0]['想定最大 km²']/total_max*100:.0f}%) を占め、
      2 位の {nd_max_sorted.iloc[1]['市町']} ({second_max:.1f} km²) の {fk_max/second_max:.1f} 倍。
      湾奥+遠浅+干拓地という瀬戸内最大級の増幅条件が地理的に重なる。
      興味深いことに 30y シナリオでは呉市が 1 位で福山は 2 位だが、規模拡大すると
      福山平野の<b>巨大干拓地全体が同時に水没想定区域</b>になるため福山が一気に首位に上る。</li>
  <li><b>5. 家屋倒壊等氾濫想定区域は max のみの新概念</b> (H5 強支持):
      max でのみ rank07 = {rk7_max_km2:.3f} km² が指定。深さではなく
      「家屋倒壊リスクのある領域」という<b>新規法概念</b>が水防法 H27 改正で導入された
      ことが、データ仕様上で直接観察できる。</li>
  <li><b>6. 告示更新の意味</b> (H6 強支持): 30y/isewan は 2017 告示 (rank 4-5 種)、
      max は 2021 告示 (rank 7 種)。4 年差は単なる更新ではなく、
      <b>多段階深ランク化</b>という記述思想の進化を伴う。</li>
</ul>

<h3>3 dataset 相互関係の構造発見</h3>
<p>本記事の最重要発見は、3 dataset が「同じ高潮現象の異なる規模シナリオ」
ではなく、<b>「異なる時代の異なる想定哲学」</b>を反映している点である。</p>
<ul class="kv">
  <li><b>30y</b> (2017): 確率論ベース (年超過確率 1/30) の限定的想定 — <b>「日常〜頻発」</b>を扱う</li>
  <li><b>isewan</b> (2017): 歴史的最大事例ベースの想定 — <b>「過去最悪」</b>を扱う</li>
  <li><b>max</b> (2021): 物理的に想定し得る最大値ベース — <b>「未経験の最悪」</b>を扱う</li>
</ul>
<p>この 3 段階は<b>頻度 → 過去 → 物理上限</b>という想定の階段であり、
水防法 H27 改正が「過去ベースを超えて物理上限を考えよ」という思想転換を
要請した結果、<b>max のみが質的に異なる</b>データセットになっている。
3 件を統合分析することで、<b>「告示制度の進化と高潮リスク認識の進化」</b>が
1 つのデータシリーズに刻まれていることが量的に可視化された。</p>

<h3>本研究の限界</h3>
<ul class="kv">
  <li>(a) 行政界 GeoJSON は L15 由来で「都市計画区域行政区域」=
      市町全域とは厳密には一致しない (planning area のみ)。よって
      市町別カバレッジは<b>都市計画区域内の浸水想定</b>に限定される。
      ただし広島県の高潮想定区域は<b>沿岸都市部</b>に集中しており、
      planning area 外の影響は本記事の主張には影響しない。</li>
  <li>(b) 浸水深ランクの境界値 (0.5 / 1 / 3 / 5 / 10 m) は広島県/水防法
      ガイドライン解釈であり、<b>データセット内に明示されていない</b>。
      公式メタ XML から「Drank」属性のみ確認できるが、ラベル定義は
      ガイドライン標準を踏襲する形で本記事が設定した。</li>
  <li>(c) 人口・建物のカバレッジ (例: rank04 内の建物棟数・居住人口) は
      本記事では<b>面積指標</b>のみで止めた。発展課題で対応する。</li>
</ul>
"""))

# (11) 発展課題
sections.append(("発展課題", f"""
<p>本記事の結果から論理的に導かれる新仮説と、それを検証する具体的課題:</p>

<h3>課題 1: 高潮 × 河川 × 津波 「3 機構ハザード重ね」</h3>
<ul class="kv">
  <li><b>結果 X</b>: max シナリオの福山平野では rank04 (3-5m) が支配的。</li>
  <li><b>新仮説 Y</b>: 福山平野の高潮 max 浸水想定区域は、
      L4-L11 既知の<b>河川浸水想定区域 (蘆田川など)</b> と相当部分が重複している。
      つまり「同じエリア」を別機構で 2 回浸水するリスクがある。</li>
  <li><b>課題 Z</b>: L08 が扱った河川想定区域 GeoJSON と本記事の高潮 max を
      <code>gpd.overlay(how='intersection')</code> で重ね、
      重複面積比率を求める。3 機構 (高潮 + 河川 + 津波) のオーバーレイ集計を行い、
      <b>「3 機構共通リスク」エリア</b>を特定する。</li>
</ul>

<h3>課題 2: 沿岸島嶼の盲点エリアの社会基盤調査</h3>
<ul class="kv">
  <li><b>結果 X</b>: 江田島市・大崎上島町は max でのみ高潮浸水想定が拡大。</li>
  <li><b>新仮説 Y</b>: これら島嶼の沿岸集落は<b>2017 時点では高潮対策の優先度が低い</b>
      とされていたが、max 告示後は防災計画の見直しが必要。実際に
      <b>避難所 (L03) が浸水想定区域内に立地している</b>ケースが存在する可能性。</li>
  <li><b>課題 Z</b>: L03 の避難所 GeoJSON と max polygon を <code>sjoin(predicate='within')</code>
      で空間結合し、想定区域内の避難所一覧を抽出。同様に L32-L34 の港湾施設・
      L35 排水機場とも重ねる。</li>
</ul>

<h3>課題 3: 浸水深 × 標高 (L40) のヒプソメトリック検証</h3>
<ul class="kv">
  <li><b>結果 X</b>: 福山港・広島湾奥で深 rank が集中、これは湾奥の干拓地特性に対応。</li>
  <li><b>新仮説 Y</b>: 浸水想定区域内の<b>標高 (L40 既知)</b> 平均は、
      depth_label と<b>負相関</b>を示す (= 深 rank ほど低標高地)。</li>
  <li><b>課題 Z</b>: L40 の標高ラスタ (50cm 坂町版) を <code>rasterio</code> で読み、
      max シナリオ rank 別ポリゴンを <code>features.rasterize</code> で
      ラスタマスクに変換、各 rank 内の標高分布を <code>numpy.histogram</code> で集計。
      Pearson 相関 r を rank と平均標高で計算。</li>
</ul>

<h3>課題 4: 1959 伊勢湾台風の実観測との比較</h3>
<ul class="kv">
  <li><b>結果 X</b>: isewan シナリオは「伊勢湾台風規模」だが、
      実測の高潮データは別途存在する。</li>
  <li><b>新仮説 Y</b>: isewan シナリオは 1959 年の<b>実際の浸水痕跡 (広島県内)</b>
      よりも<b>保守的</b>に拡大されている (= 安全側設計)。</li>
  <li><b>課題 Z</b>: 県史・気象庁過去資料から 1959 高潮の実浸水痕跡を抽出し、
      isewan シナリオとオーバーレイ。<b>シナリオ - 実測 = 安全余裕</b>を定量化。</li>
</ul>

<h3>課題 5: 高潮 × 海面上昇 (気候変動) シナリオの予測</h3>
<ul class="kv">
  <li><b>結果 X</b>: max シナリオは現状の物理上限想定。</li>
  <li><b>新仮説 Y</b>: 海面上昇 +1m を仮定すると、max 浸水想定面積は<b>1.5 倍以上</b>に拡大する。</li>
  <li><b>課題 Z</b>: 標高ラスタから「現在の海抜」と「+1m」の境界を抽出し、
      max polygon との差分面積を計算する。気候変動下の長期高潮リスクを定量化。</li>
</ul>
"""))

html = render_lesson(
    num=44,
    title="L44 高潮浸水想定区域 3 件統合分析 — 広島県沿岸の「海起源浸水」を 3 規模シナリオで読み解く",
    tags=["高潮", "浸水想定区域", "水防法", "瀬戸内海", "沿岸防災", "統合分析",
          "geopandas", "Shapefile"],
    time="60〜90 分",
    level="リテラシ〜中級 (geopandas/Shapefile/空間結合の基礎)",
    data_label="DoBoX dataset 43, 44, 45 (高潮浸水想定区域情報 3 件) — Shapefile 計 5.5 MB",
    sections=sections,
    script_filename="L44_storm_surge.py",
)

out_html = LESSONS / "L44_storm_surge.html"
out_html.write_text(html, encoding="utf-8")
print(f"  HTML written: {out_html} ({out_html.stat().st_size:,} B), {time.time()-t1:.1f}s",
      flush=True)

print(f"\n=== L44 done in {time.time()-t_all:.1f}s ===", flush=True)
