# -*- coding: utf-8 -*-
"""L45 ため池浸水想定区域 2 件統合分析
       — 広島県内の防災重点ため池決壊リスクを「規模属性 × 浸水範囲」で読み解く

カバー宣言:
  本記事は DoBoX のシリーズ「ため池」系 2 dataset (id = 62, 63) を
  厳密に統合し、広島県内の防災重点ため池決壊浸水想定の構造を分析する。

  「ため池」とは農業用水確保のために谷や低地を堰き止めた小規模貯水池で、
  日本には約 15 万箇所、その多くが江戸期以前から続く農業遺構である。
  広島県は全国有数のため池県 (兵庫・広島・香川が御三家) で、
  瀬戸内型の少雨気候下で水田農業を支えてきた基幹インフラ。

  しかし 2018 年西日本豪雨でため池決壊が相次ぎ (県内 32 箇所損傷)、
  従来「人里離れた静かな農業遺構」だったものが急に「下流に脅威を撒く
  決壊リスク源」として再認識された。これを受け 2019 年に
  「農業用ため池の管理及び保全に関する法律」が施行され、
  決壊時に下流被害が想定される池は「防災重点農業用ため池」
  (=「特定農業用ため池」) として県知事が指定するようになった。

  本記事はその指定の結果として作られた 2 dataset を扱う:
    - dataset 62: ため池基本情報 (CSV, 6,754 件)
        広島県内の特定農業用ため池の属性表 (堤高/貯水量/緯度経度/所管市町/指定年月日)
    - dataset 63: ため池浸水想定区域情報 (Shapefile, 7,223 polygons / 6,730 unique 池)
        ため池ごとに、決壊時の下流浸水想定エリアをポリゴンで記述

  この 2 件は<b>「同じため池群の異なる断面」</b>であり、
  ため池番号 (CSV) と FIELD001 (SHP) を結合キーに統合分析できる。

  本記事は L4-L11 (河川浸水) ・L44 (高潮浸水) と<b>厳密に独立</b>。
  ため池決壊浸水と河川浸水・高潮浸水は<b>水文機構が異なる</b>別災害として扱う。
  L07 (浸水×インフラ統合) で dataset 62 を「インフラ施設」の 1 として既参照しているが、
  本記事はため池そのものを主役に据え、Shapefile 浸水想定区域を主幾何データとする。

研究の問い (主 RQ):
  広島県の特定農業用ため池 6,754 件と、その決壊時浸水想定区域 6,730 件は、
  ため池の規模属性 (堤高・貯水量) と浸水範囲・市町分布でどう関係し、
  「県のため池決壊リスクの地理学」はどう描けるか？

仮説 H1〜H6:
  H1 (カバレッジ完全性): 基本情報 (CSV) に登録された 6,754 池のうち、
       浸水想定 (SHP) を持たないため池が少数 (< 1%) 存在する。
       存在するなら、その<b>非カバレッジ位置</b>には地理的偏りがある (H1 強支持の場合)
       か、ランダムに散らばる (反証の場合) か。
  H2 (規模 ↔ 浸水面積の正相関): 堤高 (m) や貯水量 (千m³) が大きい池ほど、
       決壊時の下流浸水想定面積 (km²) も大きい。Pearson 相関 r > 0.6 と予測。
       ただし<b>堤高より貯水量の方が強い相関</b>を示す (=決壊時に流出する水量で決まる)。
  H3 (中山間集中): ため池数の上位 5 市町は<b>東広島・福山・庄原・三次</b>等の
       内陸〜中山間地域で、沿岸都市部 (広島市・呉市・尾道市) よりはるかに多い。
       これは「ため池=農業遺構」の歴史的分布と一致する。
  H4 (連鎖重複の地理学): 個別ため池の浸水想定面積を単純合計すると、
       全体の unary_union 面積より<b>大幅に大きい</b> (重複比率 > 30%)。
       重複は「上流ため池の浸水想定下流に下流ため池の浸水想定が連鎖する」
       谷地形で発生し、<b>同じ住宅街が複数のため池決壊で繰り返し被災想定される</b>
       連鎖リスク構造を示す。
  H5 (指定の波): 特定農業用ため池の指定日は 2020-2023 (R2-R5) に集中し、
       <b>R3.5.31 (令和 3 年 5 月 31 日) が最大ピーク</b>になっている。
       これは農業用ため池法の経過措置期限と整合し、
       一斉指定が行政手続き的に行われたことを示す。
  H6 (規模分布の対数性): ため池の貯水量分布は対数的 (右に長い裾)。
       中央値は数千 m³ 級だが上位は 100 万 m³ 級まで存在し、
       貯水量で 3 桁以上のレンジを持つ。これは「小池の分散」と
       「大池の少数」が共存する典型的な農業遺構分布である。

要件 S 準拠:
  - 重い空間処理 (143MB SHP の dissolve, unary_union ~87 秒) は
    `lessons/_l45_build_cache.py` で事前計算し、
    `data/extras/L45_pond_inundation/_cache/` に GPKG/CSV キャッシュ。
  - 本スクリプトはキャッシュ読込のみで 1 分以内完走。

要件 T 準拠:
  ため池位置点マップ + 浸水想定ポリゴンマップ + 市町コロプレス +
  福山ズームマップ + small multiples (上位 4 市町並置) + 規模散布。

要件 Q 準拠:
  図 9 / 表 9 (ため池数 / 浸水面積 / 規模分布 / 重複 / 指定日 / 市町別 など多角度)。

データ仕様:
  - dataset 62 (CSV): 6,754 行 × 12 列, 1.1MB
      列: ため池番号(キー), ため池名称, 特定農業用ため池の指定 (令和年月日),
          緯度, 経度, 堤高（m）, 貯水量（千m3）, 所管市町, PDF1-4 (浸水想定 PDF へのリンク)
  - dataset 63 (Shapefile): 7,223 polygon, 143MB の SHP
      列: IDD, BCODE=130, SCODE=130, HCODE=130, ため池ID(=ため池名称), 市町村名(全 None),
          FIELD001(=ため池番号 9 桁), geometry (Polygon, EPSG:2445)
  - CRS: EPSG:2445 (旧 JGD2000 平面 III) → 内部処理は EPSG:6671 (現 JGD2011 平面 III) m 単位
  - 結合キー: ため池番号 (CSV) ↔ FIELD001 (SHP), 完全結合 6,730 / 部分結合 24
"""
from __future__ import annotations
import sys, time, 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.lines import Line2D
import geopandas as gpd

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

t_all = time.time()
print("=== L45 ため池浸水想定区域 2 件統合分析 ===", flush=True)

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

# 2 dataset メタ
DATASETS = {
    62: {"jp": "ため池基本情報",                "rid": 90,
         "format": "CSV", "size": "1.1 MB",  "rows": 6754, "告示": "2024-update"},
    63: {"jp": "ため池浸水想定区域情報_Shapefile", "rid": 98562,
         "format": "Shapefile", "size": "81.2 MB (ZIP) / 143 MB (展開後)",
         "rows": 7223, "告示": "2025-01-22"},
}

# CITY_CD → 市町名 (L44 と同じ広島県 都市計画区域行政区域コード)
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: "世羅町",
}
# 所管市町 (CSV 表記) → 代表 CITY_CD のマップ (本記事内マッピング)
SHOKAN_TO_CITYCD = {
    "広島市": 105,   # 安佐南区を代表 (中山間部のため池が多い)
    "呉市": 202, "竹原市": 203, "三原市": 204, "尾道市": 205,
    "福山市": 207, "府中市": 208, "三次市": 209, "庄原市": 210,
    "大竹市": 211, "東広島市": 212, "廿日市市": 213, "安芸高田市": 214,
    "江田島市": 215,
    "安芸郡府中町": 302, "安芸郡海田町": 304, "安芸郡熊野町": 307,
    "熊野町": 307,
    "山県郡安芸太田町": 369, "山県郡北広島町": 462,
    "世羅郡世羅町": 462, "豊田郡大崎上島町": 203,  # 竹原市と同区域
    "神石郡神石高原町": 208,                       # 府中市と同区域
}

# 中山間 vs 沿岸 の分類 (本記事独自定義)
COASTAL_CITIES = {"広島市", "呉市", "竹原市", "三原市", "尾道市", "福山市",
                  "大竹市", "廿日市市", "江田島市", "豊田郡大崎上島町",
                  "安芸郡府中町", "安芸郡海田町", "安芸郡熊野町", "熊野町"}
INLAND_CITIES = {"府中市", "三次市", "庄原市", "東広島市", "安芸高田市",
                 "山県郡安芸太田町", "山県郡北広島町", "世羅郡世羅町",
                 "神石郡神石高原町"}

# =============================================================================
# 1. キャッシュ確保
# =============================================================================
print("\n[1] キャッシュ確保", flush=True)
t1 = time.time()

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

# 結合済み属性表
merge = pd.read_csv(CACHE / "pond_inund_merge.csv",
                    dtype={"ため池番号": str, "FIELD001": str})
# Boolean 復元 (CSV は文字列で読まれることがある)
merge["has_inund"] = merge["area_km2"].notna()

# ため池ごと dissolve 済 ポリゴン (浸水想定区域)
diss = gpd.read_file(CACHE / "diss_per_pond.gpkg").to_crs(TARGET_CRS)

# 県全体 unary_union (面積二重カウント排除用)
union_gdf = gpd.read_file(CACHE / "inund_union.gpkg").to_crs(TARGET_CRS)
total_union_area = float(union_gdf["area_km2"].iloc[0])

# 都市計画区域行政区域 (L15 由来)
admin_diss = gpd.read_file(CACHE / "admin_diss.gpkg").to_crs(TARGET_CRS)

# 市町別統計
city_stats = pd.read_csv(CACHE / "per_city_stats.csv", encoding="utf-8-sig")

print(f"  merge {len(merge)} rows, diss {len(diss)} polys, "
      f"admin {len(admin_diss)} cities, city_stats {len(city_stats)} 市町, "
      f"{time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 2. 全体サマリ (= 表 1)
# =============================================================================
print("\n[2] 全体サマリ", flush=True)
t1 = time.time()

n_total = len(merge)
n_with = int(merge["has_inund"].sum())
n_without = n_total - n_with
sum_indiv_km2 = float(merge["area_km2"].sum())  # 個別合計 (重複あり)
overlap_km2 = sum_indiv_km2 - total_union_area
overlap_ratio = overlap_km2 / sum_indiv_km2 * 100 if sum_indiv_km2 > 0 else 0.0
n_cities = merge["所管市町"].nunique()
mean_dam = float(merge["堤高_m"].mean())
median_dam = float(merge["堤高_m"].median())
max_dam = float(merge["堤高_m"].max())
mean_cap = float(merge["貯水量_千m3"].mean())
median_cap = float(merge["貯水量_千m3"].median())
max_cap = float(merge["貯水量_千m3"].max())

T_overall = pd.DataFrame([
    ("ため池総数 (CSV)",            f"{n_total:,} 池"),
    ("浸水想定 SHP あり",           f"{n_with:,} 池 ({n_with/n_total*100:.1f}%)"),
    ("浸水想定 SHP なし",           f"{n_without} 池 ({n_without/n_total*100:.1f}%)"),
    ("所管市町数",                  f"{n_cities} 市町"),
    ("個別浸水面積の合計 (重複あり)", f"{sum_indiv_km2:.2f} km²"),
    ("浸水想定区域の真の面積 (union)", f"{total_union_area:.2f} km²"),
    ("重複面積",                     f"{overlap_km2:.2f} km² ({overlap_ratio:.1f}%)"),
    ("平均堤高",                     f"{mean_dam:.2f} m  (中央値 {median_dam:.1f} m, 最大 {max_dam:.1f} m)"),
    ("平均貯水量",                   f"{mean_cap:.2f} 千 m³  (中央値 {median_cap:.2f}, 最大 {max_cap:,.0f})"),
], columns=["指標", "値"])
T_overall.to_csv(ASSETS / "L45_overall.csv", index=False, encoding="utf-8-sig")
print(T_overall.to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 3. 浸水想定 SHP 無しため池 24 件の特定
# =============================================================================
print("\n[3] 浸水想定なし 24 件の特定", flush=True)
t1 = time.time()

no_shp = merge[~merge["has_inund"]].copy().reset_index(drop=True)
no_shp_show = no_shp[["ため池番号", "ため池名称", "所管市町",
                      "堤高_m", "貯水量_千m3", "特定農業用ため池の指定"]].copy()
no_shp_show.columns = ["ため池番号", "ため池名称", "所管市町",
                       "堤高 m", "貯水量 千m³", "特定指定日"]
no_shp_show.to_csv(ASSETS / "L45_no_inund.csv", index=False, encoding="utf-8-sig")
print(f"  浸水想定なし {len(no_shp)} 件 / 所管市町: {no_shp['所管市町'].value_counts().to_dict()}")
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 4. 規模属性 (堤高・貯水量) ↔ 浸水面積 の関係
# =============================================================================
print("\n[4] 規模属性 ↔ 浸水面積", flush=True)
t1 = time.time()

m2 = merge[merge["has_inund"]].copy()
# 0 値のログ計算回避
m2 = m2[(m2["堤高_m"] > 0) & (m2["貯水量_千m3"] > 0) & (m2["area_km2"] > 0)].copy()
m2["log_dam"] = np.log10(m2["堤高_m"])
m2["log_cap"] = np.log10(m2["貯水量_千m3"])
m2["log_area"] = np.log10(m2["area_km2"])

corr_dam_area = m2[["堤高_m", "area_km2"]].corr().iloc[0, 1]
corr_cap_area = m2[["貯水量_千m3", "area_km2"]].corr().iloc[0, 1]
corr_logdam_logarea = m2[["log_dam", "log_area"]].corr().iloc[0, 1]
corr_logcap_logarea = m2[["log_cap", "log_area"]].corr().iloc[0, 1]

T_corr = pd.DataFrame([
    ("堤高 (生値) ↔ 浸水面積",    f"{corr_dam_area:.3f}"),
    ("貯水量 (生値) ↔ 浸水面積",  f"{corr_cap_area:.3f}"),
    ("log10 堤高 ↔ log10 面積",    f"{corr_logdam_logarea:.3f}"),
    ("log10 貯水量 ↔ log10 面積",  f"{corr_logcap_logarea:.3f}"),
], columns=["相関の組", "Pearson r"])
T_corr.to_csv(ASSETS / "L45_correlation.csv", index=False, encoding="utf-8-sig")
print(T_corr.to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 5. 特定農業用ため池 指定日の分布
# =============================================================================
print("\n[5] 指定日分布", flush=True)
t1 = time.time()

# 令和年月日を西暦に変換 (R2 = 2020, R3 = 2021, ...)
def reiwa_to_date(s):
    if pd.isna(s) or not isinstance(s, str):
        return None
    s = s.strip()
    # 例: R3.5.31, R2.12.28
    if not s.startswith("R"):
        return None
    try:
        body = s[1:]
        parts = body.split(".")
        if len(parts) != 3:
            return None
        r_year = int(parts[0])
        month = int(parts[1])
        day = int(parts[2])
        # 令和元年 = 2019
        ad = 2018 + r_year
        return f"{ad:04d}-{month:02d}-{day:02d}"
    except Exception:
        return None

merge["指定日_西暦"] = merge["特定農業用ため池の指定"].apply(reiwa_to_date)
desig = merge["指定日_西暦"].dropna()
desig_count = desig.value_counts().sort_index()
T_desig = pd.DataFrame({"指定日": desig_count.index, "ため池数": desig_count.values})
T_desig.to_csv(ASSETS / "L45_designation_dates.csv", index=False, encoding="utf-8-sig")
print(f"  ユニーク指定日数: {len(desig_count)}")
print(T_desig.to_string(index=False))

# 月単位サマリ
desig_month = pd.to_datetime(desig).dt.to_period("M")
month_counts = desig_month.value_counts().sort_index()
print(f"\n  月別累計 (上位 5): {month_counts.head(5).to_dict()}")
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 6. 市町別 集計表 (= 表 6)
# =============================================================================
print("\n[6] 市町別集計", flush=True)
t1 = time.time()

# 中山間/沿岸 列を追加
def categorize(name):
    if name in INLAND_CITIES:
        return "中山間"
    if name in COASTAL_CITIES:
        return "沿岸"
    return "その他"
city_stats["地理分類"] = city_stats["所管市町"].map(categorize)
# 浸水想定密度 = 合計浸水想定 / ため池数
city_stats["池あたり浸水_km2"] = (city_stats["合計浸水想定_km2"]
                                / city_stats["ため池数"]).round(4)
# rounded
city_stats_view = city_stats.copy()
for c in ["平均堤高_m", "最大堤高_m", "平均貯水量_千m3"]:
    city_stats_view[c] = city_stats_view[c].round(2)
for c in ["合計貯水量_千m3"]:
    city_stats_view[c] = city_stats_view[c].round(0).astype(int)
for c in ["合計浸水想定_km2", "平均浸水想定_km2"]:
    city_stats_view[c] = city_stats_view[c].round(3)
city_stats_view.to_csv(ASSETS / "L45_per_city.csv", index=False, encoding="utf-8-sig")
print(city_stats_view.head(10).to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 7. Top 浸水想定面積ため池 (= 表 7)
# =============================================================================
print("\n[7] Top 浸水想定 ため池", flush=True)
t1 = time.time()

top_inund = merge.sort_values("area_km2", ascending=False).head(15).copy()
top_inund_view = top_inund[["ため池名称", "所管市町", "堤高_m", "貯水量_千m3",
                              "area_km2", "特定農業用ため池の指定"]].copy()
top_inund_view.columns = ["ため池名称", "所管市町", "堤高 m", "貯水量 千m³",
                          "浸水想定面積 km²", "特定指定日"]
top_inund_view["堤高 m"] = top_inund_view["堤高 m"].round(1)
top_inund_view["貯水量 千m³"] = top_inund_view["貯水量 千m³"].round(1)
top_inund_view["浸水想定面積 km²"] = top_inund_view["浸水想定面積 km²"].round(3)
top_inund_view = top_inund_view.reset_index(drop=True)
top_inund_view.index = top_inund_view.index + 1
top_inund_view = top_inund_view.reset_index().rename(columns={"index": "順位"})
top_inund_view.to_csv(ASSETS / "L45_top_inund.csv", index=False, encoding="utf-8-sig")
print(top_inund_view.to_string(index=False))
print(f"  {time.time()-t1:.1f}s", flush=True)

# =============================================================================
# 8. 図の生成 (9 枚)
# =============================================================================
print("\n[8] 図の生成", 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: ため池所管市町別 ため池数 (横棒) -----
fig, ax = plt.subplots(figsize=(9, 7.5))
cs = city_stats.sort_values("ため池数", ascending=True)
colors = cs["地理分類"].map({"中山間": "#388e3c", "沿岸": "#1976d2", "その他": "#9e9e9e"})
ax.barh(cs["所管市町"], cs["ため池数"], color=colors, edgecolor="#333", linewidth=0.4)
for i, (n, v) in enumerate(zip(cs["所管市町"], cs["ため池数"])):
    ax.text(v + 15, i, f"{v:,}", va="center", fontsize=9)
ax.set_xlabel("ため池数 (件)", fontsize=11)
ax.set_title("図1: 所管市町別 特定農業用ため池数 (n=6,754)\n緑=中山間 / 青=沿岸",
             fontsize=12)
handles = [Patch(facecolor="#388e3c", label="中山間 (内陸)"),
           Patch(facecolor="#1976d2", label="沿岸"),
           Patch(facecolor="#9e9e9e", label="その他")]
ax.legend(handles=handles, loc="lower right", fontsize=9)
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
save_fig("L45_fig1_pond_count_by_city.png")

# ----- 図 2: 規模分布 (堤高・貯水量) ヒストグラム log scale -----
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
ax.hist(merge["堤高_m"].dropna(), bins=40, color="#5e35b1",
        edgecolor="#333", linewidth=0.3)
ax.axvline(median_dam, color="#cf222e", linestyle="--",
           label=f"中央値 {median_dam:.1f} m")
ax.axvline(mean_dam, color="#0969da", linestyle=":",
           label=f"平均 {mean_dam:.2f} m")
ax.set_xlabel("堤高 (m)", fontsize=11)
ax.set_ylabel("ため池数", fontsize=11)
ax.set_title(f"堤高分布 (n={n_total:,})", fontsize=11)
ax.legend(fontsize=9)
ax.grid(alpha=0.3)

ax = axes[1]
cap_pos = merge["貯水量_千m3"][merge["貯水量_千m3"] > 0].dropna()
ax.hist(cap_pos, bins=np.logspace(np.log10(cap_pos.min()),
                                    np.log10(cap_pos.max()), 40),
        color="#00838f", edgecolor="#333", linewidth=0.3)
ax.set_xscale("log")
ax.axvline(median_cap, color="#cf222e", linestyle="--",
           label=f"中央値 {median_cap:.2f} 千m³")
ax.axvline(mean_cap, color="#0969da", linestyle=":",
           label=f"平均 {mean_cap:.2f}")
ax.set_xlabel("貯水量 (千 m³, log10 軸)", fontsize=11)
ax.set_ylabel("ため池数", fontsize=11)
ax.set_title("貯水量分布 (log scale)", fontsize=11)
ax.legend(fontsize=9)
ax.grid(alpha=0.3, which="both")
fig.suptitle("図2: ため池規模分布 — 堤高は正規様, 貯水量は対数様 (右に長い裾)",
             fontsize=12)
plt.tight_layout()
save_fig("L45_fig2_size_dist.png")

# ----- 図 3: 規模 vs 浸水面積 散布図 (log-log) -----
fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))
ax = axes[0]
ax.scatter(m2["堤高_m"], m2["area_km2"], s=8, alpha=0.4, c="#5e35b1",
           edgecolor="none")
ax.set_xscale("log"); ax.set_yscale("log")
ax.set_xlabel("堤高 (m, log)", fontsize=11)
ax.set_ylabel("浸水想定面積 (km², log)", fontsize=11)
ax.set_title(f"堤高 vs 浸水面積\nPearson r (生値) = {corr_dam_area:.3f}, "
             f"log-log r = {corr_logdam_logarea:.3f}", fontsize=11)
ax.grid(alpha=0.3, which="both")

ax = axes[1]
ax.scatter(m2["貯水量_千m3"], m2["area_km2"], s=8, alpha=0.4, c="#00838f",
           edgecolor="none")
# 回帰線 (log-log で OLS)
from numpy.polynomial.polynomial import polyfit
x_l = m2["log_cap"].values
y_l = m2["log_area"].values
b0, b1 = polyfit(x_l, y_l, 1)
xr = np.linspace(x_l.min(), x_l.max(), 50)
ax.plot(10**xr, 10**(b0 + b1*xr), "-", color="#cf222e", linewidth=2,
        label=f"OLS log: y = {b0:.2f} + {b1:.2f}x")
ax.set_xscale("log"); ax.set_yscale("log")
ax.set_xlabel("貯水量 (千m³, log)", fontsize=11)
ax.set_ylabel("浸水想定面積 (km², log)", fontsize=11)
ax.set_title(f"貯水量 vs 浸水面積\nPearson r (生値) = {corr_cap_area:.3f}, "
             f"log-log r = {corr_logcap_logarea:.3f}", fontsize=11)
ax.legend(fontsize=9, loc="lower right")
ax.grid(alpha=0.3, which="both")
fig.suptitle("図3: ため池規模 ↔ 決壊時下流浸水想定面積 の log-log スケーリング",
             fontsize=12)
plt.tight_layout()
save_fig("L45_fig3_size_vs_area.png")

# ----- 図 4: 県全域 ため池位置点マップ (規模で色分け) -----
fig, ax = plt.subplots(figsize=(13, 7))
admin_diss.plot(ax=ax, color="#f8f8f8", edgecolor="#aaa", linewidth=0.4)

# 緯度経度 → EPSG:6671 に投影変換 (簡易: 緯度経度 4326 を直接変換)
pond_pts = gpd.GeoDataFrame(
    merge,
    geometry=gpd.points_from_xy(merge["経度"], merge["緯度"]),
    crs="EPSG:4326"
).to_crs(TARGET_CRS)

# size = log10 貯水量 にマッピング
sizes = np.clip(np.log10(merge["貯水量_千m3"].clip(lower=0.1)) + 2, 1, 50) * 5
# 色 = 浸水想定の有無
colors = pond_pts["has_inund"].map({True: "#cf222e", False: "#0969da"}).fillna("#aaa")
ax.scatter(pond_pts.geometry.x, pond_pts.geometry.y, s=sizes,
           c=colors, alpha=0.55, edgecolor="none")

ax.set_title(f"図4: 広島県 特定農業用ため池 位置マップ (n={n_total:,})\n"
             f"円サイズ ∝ log10 貯水量, 色: 赤=浸水想定あり ({n_with:,}), "
             f"青=なし ({n_without})", fontsize=11)
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
ax.set_aspect("equal")
handles = [Line2D([0], [0], marker='o', color='w', markerfacecolor='#cf222e',
                  markersize=10, label=f'浸水想定あり ({n_with:,} 池)'),
           Line2D([0], [0], marker='o', color='w', markerfacecolor='#0969da',
                  markersize=10, label=f'浸水想定なし ({n_without} 池)')]
ax.legend(handles=handles, loc="lower right", fontsize=9)
plt.tight_layout()
save_fig("L45_fig4_pond_points_map.png")

# ----- 図 5: 浸水想定区域 ポリゴンマップ (県全域) -----
fig, ax = plt.subplots(figsize=(13, 7))
admin_diss.plot(ax=ax, color="#f8f8f8", edgecolor="#aaa", linewidth=0.4)
# 浸水想定 全 6730 ポリゴンを薄赤で
diss.plot(ax=ax, color="#e64a19", edgecolor="none", alpha=0.55)
ax.set_title(f"図5: ため池決壊時 浸水想定区域マップ (6,730 池)\n"
             f"個別合計 = {sum_indiv_km2:.1f} km², 真の面積 (union) = "
             f"{total_union_area:.1f} km², 重複 = {overlap_km2:.1f} km² "
             f"({overlap_ratio:.0f}%)", fontsize=11)
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
ax.set_aspect("equal")
plt.tight_layout()
save_fig("L45_fig5_inund_polygon_map.png")

# ----- 図 6: 上位 4 市町 small multiples (ため池 + 浸水) -----
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
top4_cities = city_stats.head(4)["所管市町"].tolist()

# 各市町の bbox を計算 (CSV 緯度経度ベース)
for ax, city in zip(axes.flat, top4_cities):
    # 該当する CITY_CD
    citycd = SHOKAN_TO_CITYCD.get(city)
    sub_pond_csv = merge[merge["所管市町"] == city]
    sub_pond_pts = pond_pts[pond_pts["所管市町"] == city]
    sub_diss = diss[diss["FIELD001"].isin(sub_pond_csv["ため池番号"].dropna())]
    if len(sub_diss) == 0:
        continue
    # bbox (浸水ポリゴン基準, 1km マージン)
    xmin, ymin, xmax, ymax = sub_diss.total_bounds
    margin = 1500
    xmin -= margin; ymin -= margin; xmax += margin; ymax += margin

    # 背景行政
    admin_diss.plot(ax=ax, color="#f8f8f8", edgecolor="#aaa", linewidth=0.4)
    # 浸水想定
    sub_diss.plot(ax=ax, color="#e64a19", edgecolor="none", alpha=0.55)
    # ため池点
    ax.scatter(sub_pond_pts.geometry.x, sub_pond_pts.geometry.y,
               s=8, c="#1976d2", alpha=0.7, edgecolor="none")
    ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax)
    ax.set_aspect("equal")
    ax.set_xticks([]); ax.set_yticks([])
    ax.set_title(f"{city}: ため池 {len(sub_pond_csv)} 件, "
                 f"浸水想定 {sub_diss['area_km2'].sum() if len(sub_diss) else 0:.1f} km²",
                 fontsize=10)
fig.suptitle("図6: ため池数上位 4 市町 ズームマップ (赤=浸水想定 / 青点=ため池位置)",
             fontsize=13)
plt.tight_layout()
save_fig("L45_fig6_top4_cities_zoom.png")

# ----- 図 7: 福山市の浸水想定 詳細マップ (服部大池ほか) -----
fig, ax = plt.subplots(figsize=(13, 8))
fukuyama_csv = merge[merge["所管市町"] == "福山市"]
fukuyama_pond_pts = pond_pts[pond_pts["所管市町"] == "福山市"]
fukuyama_diss = diss[diss["FIELD001"].isin(fukuyama_csv["ため池番号"].dropna())]
xmin, ymin, xmax, ymax = fukuyama_diss.total_bounds
margin = 2000
xmin -= margin; ymin -= margin; xmax += margin; ymax += margin

admin_diss.plot(ax=ax, color="#f8f8f8", edgecolor="#aaa", linewidth=0.4)
# 浸水想定 (色は面積 log で濃淡)
fk_with_area = fukuyama_diss.copy()
fk_with_area["area_km2"] = fk_with_area.geometry.area / 1e6
fk_with_area.plot(ax=ax, column="area_km2", cmap="OrRd",
                  edgecolor="none", alpha=0.65, legend=True,
                  legend_kwds={"label": "浸水想定面積 (km²)", "shrink": 0.6})
# ため池点
ax.scatter(fukuyama_pond_pts.geometry.x, fukuyama_pond_pts.geometry.y,
           s=10, c="#0d47a1", alpha=0.8, edgecolor="none")
# Top3 池の名前
top3_fk = fukuyama_csv.sort_values("area_km2", ascending=False).head(3)
top3_fk_pts = pond_pts[pond_pts["ため池番号"].isin(top3_fk["ため池番号"])]
for _, p_csv in top3_fk.iterrows():
    p_pt = top3_fk_pts[top3_fk_pts["ため池番号"] == p_csv["ため池番号"]]
    if len(p_pt) == 0: continue
    cen = p_pt.geometry.iloc[0]
    ax.annotate(f"{p_csv['ため池名称']}\n{p_csv['area_km2']:.2f} km²",
                (cen.x, cen.y), fontsize=9, ha="center", weight="bold",
                xytext=(10, 10), textcoords="offset points",
                bbox=dict(boxstyle="round,pad=0.3", fc="white",
                          alpha=0.9, ec="#333"))
ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax)
ax.set_aspect("equal")
ax.set_title(f"図7: 福山市 ため池決壊浸水想定 詳細マップ "
             f"({len(fukuyama_csv)} 池, 合計 {fukuyama_csv['area_km2'].sum():.1f} km²)\n"
             f"最大池: 服部大池 ({fukuyama_csv['area_km2'].max():.2f} km², "
             f"福山市内で最大)", fontsize=11)
ax.set_xlabel("EPSG:6671 X (m)")
ax.set_ylabel("EPSG:6671 Y (m)")
plt.tight_layout()
save_fig("L45_fig7_fukuyama_detail.png")

# ----- 図 8: 指定日時系列ヒストグラム -----
fig, ax = plt.subplots(figsize=(11, 5.2))
desig_dt = pd.to_datetime(desig)
# 月単位に集計
months = desig_dt.dt.to_period("M").value_counts().sort_index()
xs = [p.to_timestamp() for p in months.index]
ax.bar(xs, months.values, width=20, color="#388e3c", edgecolor="#333", linewidth=0.3)
# Top 3 月 にラベル
top_months = months.sort_values(ascending=False).head(3)
for p, n in top_months.items():
    ax.annotate(f"{p.strftime('%Y-%m')}\n{n} 池",
                (p.to_timestamp(), n), fontsize=9, ha="center", va="bottom")
ax.set_xlabel("特定農業用ため池 指定日 (月単位)", fontsize=11)
ax.set_ylabel("指定ため池数", fontsize=11)
ax.set_title(f"図8: 特定農業用ため池 指定の波 (n={int(desig_count.sum())} / "
             f"未指定 {int((merge['指定日_西暦'].isna()).sum())} を除く)\n"
             f"R3.5.31 (令和 3 年 5 月末日 = 経過措置期限) が突出",
             fontsize=12)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
save_fig("L45_fig8_designation_timeline.png")

# ----- 図 9: 市町別 ため池密度 vs 浸水想定密度 (散布) -----
fig, ax = plt.subplots(figsize=(10, 7))
plot_df = city_stats.copy()
plot_df["地理色"] = plot_df["地理分類"].map(
    {"中山間": "#388e3c", "沿岸": "#1976d2", "その他": "#9e9e9e"})
ax.scatter(plot_df["ため池数"], plot_df["合計浸水想定_km2"],
           s=120, c=plot_df["地理色"], edgecolor="#333", alpha=0.85)
# 全 市町ラベル
for _, r in plot_df.iterrows():
    ax.annotate(r["所管市町"], (r["ため池数"], r["合計浸水想定_km2"]),
                fontsize=9, ha="left", xytext=(5, 3),
                textcoords="offset points")
# 1:1 参照線 (= 1 池あたり 1 km² の場合)
maxn = plot_df["ため池数"].max()
ax.plot([0, maxn], [0, maxn * 0.05], ":", color="#aaa",
        label="1 池 = 0.05 km² 参考線")
ax.set_xlabel("ため池数 (件)", fontsize=11)
ax.set_ylabel("合計浸水想定面積 (km²)", fontsize=11)
ax.set_title("図9: 市町別 ため池数 vs 浸水想定面積 (中山間/沿岸 色分け)\n"
             "東広島・福山が突出し、両者で県全体の約 35% を占める", fontsize=11)
handles = [Patch(facecolor="#388e3c", label="中山間"),
           Patch(facecolor="#1976d2", label="沿岸"),
           Patch(facecolor="#9e9e9e", label="その他"),
           Line2D([0], [0], color="#aaa", linestyle=":",
                  label="1 池 = 0.05 km² 参考線")]
ax.legend(handles=handles, loc="lower right", fontsize=9)
ax.grid(alpha=0.3)
plt.tight_layout()
save_fig("L45_fig9_density_scatter.png")

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

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

H_results = []

# H1 カバレッジ完全性
nc_ratio = n_without / n_total * 100
nc_cities = no_shp["所管市町"].value_counts()
# 全件が「中山間市町セット (INLAND_CITIES)」に集中するか
nc_inland = sum(1 for c in nc_cities.index if c in INLAND_CITIES)
all_inland = (nc_inland == len(nc_cities))
H1_supp = ("強支持 (中山間市町に集中, 地理偏りあり)"
           if (n_without > 0 and all_inland and n_without/n_total < 0.01) else
           ("支持 (少数だが多市町に散在)"
            if (n_without > 0 and n_without/n_total < 0.01) else
            "反証"))
nc_breakdown = ", ".join(f"{k}={v}" for k, v in nc_cities.items())
H_results.append({
    "仮説": "H1 カバレッジ完全性",
    "想定": "浸水想定なしため池が < 1% で、地理的偏り (中山間市町集中) がある",
    "実測": f"{n_without}/{n_total} ({nc_ratio:.2f}%) が浸水想定なし, "
            f"内訳: {nc_breakdown} (全件が中山間 {nc_inland}/{len(nc_cities)} 市町)",
    "判定": H1_supp,
})

# H2 規模 ↔ 浸水面積 正相関
H2_strong = (corr_cap_area > corr_dam_area) and (corr_cap_area > 0.6)
H2_supp = ("強支持" if (H2_strong and corr_cap_area > 0.7) else
           ("支持" if H2_strong else "部分支持"))
H_results.append({
    "仮説": "H2 規模 ↔ 浸水面積 正相関",
    "想定": "Pearson r > 0.6, 貯水量 > 堤高",
    "実測": f"r(堤高)={corr_dam_area:.3f}, r(貯水量)={corr_cap_area:.3f}, "
            f"log-log: r(貯水量)={corr_logcap_logarea:.3f}",
    "判定": H2_supp,
})

# H3 中山間集中
top5 = city_stats.head(5)["所管市町"].tolist()
inland_in_top5 = sum(1 for c in top5 if c in INLAND_CITIES)
H3_supp = ("強支持" if inland_in_top5 >= 3 else
           ("支持" if inland_in_top5 >= 2 else "反証"))
H_results.append({
    "仮説": "H3 中山間集中",
    "想定": "ため池数 上位 5 市町のうち 3 件以上が中山間 (内陸)",
    "実測": f"上位 5 = {top5}, うち中山間 = {inland_in_top5} 件",
    "判定": H3_supp,
})

# H4 連鎖重複
H4_supp = ("強支持" if overlap_ratio > 30 else
           ("支持" if overlap_ratio > 20 else "反証"))
H_results.append({
    "仮説": "H4 連鎖重複の地理学",
    "想定": "個別合計 - union = 30% 以上の重複面積",
    "実測": f"個別合計={sum_indiv_km2:.1f} km², union={total_union_area:.1f} km², "
            f"overlap={overlap_km2:.1f} km² ({overlap_ratio:.1f}%)",
    "判定": H4_supp,
})

# H5 指定日のピーク R3.5.31
top_date = desig_count.idxmax()
top_date_count = int(desig_count.max())
total_designated = int(desig_count.sum())
top_share = top_date_count / total_designated * 100
H5_supp = ("強支持" if (top_date == "2021-05-31" and top_share > 30) else
           ("支持" if top_share > 25 else "部分支持"))
H_results.append({
    "仮説": "H5 指定の波 R3.5.31 ピーク",
    "想定": "R3.5.31 (2021-05-31) が単一最大ピークで > 30%",
    "実測": f"top_date={top_date} ({top_date_count} 池, {top_share:.1f}% / 総指定数 {total_designated})",
    "判定": H5_supp,
})

# H6 規模分布の対数性
log_range = np.log10(max_cap) - np.log10(merge["貯水量_千m3"][merge["貯水量_千m3"]>0].min())
H6_supp = ("強支持" if log_range >= 5 else
           ("支持" if log_range >= 3 else "反証"))
H_results.append({
    "仮説": "H6 規模分布の対数性",
    "想定": "貯水量レンジが log10 で 3 桁以上",
    "実測": f"min={merge['貯水量_千m3'][merge['貯水量_千m3']>0].min():.4f}, "
            f"max={max_cap:,.1f} 千 m³, log10 range={log_range:.2f}",
    "判定": H6_supp,
})

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

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


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")


# データ仕様表
T_dataspec = pd.DataFrame([
    {"dataset_id": 62,
     "シリーズ": "ため池基本情報",
     "形式": "CSV (Shift_JIS, BOM なし)",
     "ZIP/サイズ": "1.1 MB",
     "件数": "6,754 行 × 12 列",
     "更新": "2024 年版 (随時更新)",
     "本記事での役割": "ため池属性 (堤高/貯水量/緯度経度/所管市町/特定指定日)"},
    {"dataset_id": 63,
     "シリーズ": "ため池浸水想定区域情報_Shapefile",
     "形式": "Shapefile (.shp/.shx/.dbf/.prj/.sbn/.sbx)",
     "ZIP/サイズ": "81.2 MB (ZIP) / 143 MB (展開後 .shp 単体)",
     "件数": "7,223 polygon / 6,730 unique ため池",
     "更新": "2025-01-22",
     "本記事での役割": "ため池ごとの決壊時下流浸水想定エリア (主幾何データ)"},
])
T_dataspec.to_csv(ASSETS / "L45_dataspec.csv", index=False, encoding="utf-8-sig")

# 結合キー解説表
T_joinkey = pd.DataFrame([
    {"段階": "STEP 0", "内容": "tameike_basic.csv を読込",
     "サイズ/件数": "6,754 行 × 12 列"},
    {"段階": "STEP 1",
     "内容": "ため池浸水想定区域.shp を読込 → EPSG:6671 投影変換",
     "サイズ/件数": "7,223 polygon (元の CRS = EPSG:2445)"},
    {"段階": "STEP 2",
     "内容": "30 件の不正幾何 (自己交差等) を buffer(0) で修復",
     "サイズ/件数": "30 polygon を修復 → 7,223 polygon (有効)"},
    {"段階": "STEP 3",
     "内容": "FIELD001 (= ため池番号) で dissolve",
     "サイズ/件数": "7,223 → 6,730 ため池ポリゴン (1 池 = 1 MultiPolygon)"},
    {"段階": "STEP 4",
     "内容": "merge: CSV.ため池番号 LEFT JOIN SHP.FIELD001",
     "サイズ/件数": "6,754 行 (うち 6,730 結合成功 + 24 結合無し)"},
    {"段階": "STEP 5",
     "内容": "全 7,223 ポリゴン unary_union で「県全体 真の浸水想定面積」",
     "サイズ/件数": f"単一 MultiPolygon, 面積 = {total_union_area:.2f} km²"},
])
T_joinkey.to_csv(ASSETS / "L45_joinkey_steps.csv", index=False, encoding="utf-8-sig")

sections = []

# (1) 学習目標と問い
sections.append(("学習目標と問い", f"""
<p>本レッスンでは、広島県オープンデータ DoBoX が公開する
<b>ため池</b>系 2 dataset (id = 62, 63) を統合し、
広島県内の<b>「特定農業用ため池の決壊リスクと浸水想定の構造」</b>を分析する。</p>

<h3>独自用語の定義 (本記事内での使用)</h3>
<ul class="kv">
  <li><b>ため池</b>: 農業用水確保のため谷や低地を堰き止めた小規模貯水池。
      日本に約 15 万箇所、広島県は兵庫・香川と並ぶ全国有数のため池県。
      多くは江戸期以前から続く農業遺構であり、本記事の対象 6,754 池の
      平均堤高は <b>{mean_dam:.2f} m</b>、中央値貯水量 <b>{median_cap:.2f} 千 m³</b>
      の小規模池が多数を占める。</li>
  <li><b>決壊シナリオ</b>: ため池の堤体が決壊し、貯留水が下流に流出する想定。
      DoBoX dataset 63 の浸水想定区域は<b>「100% 全量決壊」</b>を基本シナリオとして
      浸水範囲を計算したもの (各ため池の PDF メタデータに記載)。</li>
  <li><b>防災重点ため池 (= 特定農業用ため池)</b>: 2018 年西日本豪雨で
      ため池決壊が相次いだ (県内 32 箇所損傷) ことを受け、
      2019 年「農業用ため池の管理及び保全に関する法律」が施行された。
      決壊時の下流被害が想定される池は<b>県知事が「特定農業用ため池」として指定</b>する。
      本記事の 6,754 池はすべてこの指定対象。</li>
  <li><b>下流被災ポテンシャル</b>: 各ため池の浸水想定区域 (面積 km²) で
      代理した、決壊時に下流が被災する規模の指標。
      本記事では Shapefile の polygon area として計算する。</li>
  <li><b>ため池決壊危険度</b>: 本記事では「下流被災ポテンシャル × 連鎖重複度」で
      捉える概念。<b>同一エリアが複数のため池決壊で繰り返し被災想定される</b>
      重複面積比率 (overlap_ratio) を地理的危険度の代理指標とする。</li>
  <li><b>浸水想定 unary_union</b>: 全 6,730 池の浸水想定 polygon を
      shapely.unary_union で合体した「県全体の真の浸水想定面積」。
      個別合計 ({sum_indiv_km2:.1f} km²) は重複を含む合計値であり、
      union 値 ({total_union_area:.1f} km²) こそが純粋なリスクエリア面積。</li>
</ul>

<h3>主 RQ (研究の問い)</h3>
<p>広島県の特定農業用ため池 6,754 件と、その決壊時浸水想定区域 6,730 件は、
ため池の規模属性 (堤高・貯水量) と浸水範囲・市町分布でどう関係し、
<b>「県のため池決壊リスクの地理学」</b>はどう描けるか？</p>

<h3>仮説 H1〜H6 (本記事で検証する)</h3>
<ul class="kv">
  <li><b>H1 カバレッジ完全性</b>: CSV 6,754 池のうち、浸水想定 SHP を持たない池が
      少数 (&lt; 1%) 存在する。その<b>非カバレッジ位置</b>には地理的偏り
      (中山間市町への集中) があるか、ランダム散在か。</li>
  <li><b>H2 規模 ↔ 浸水面積 正相関</b>: 貯水量 (千 m³) が大きい池ほど決壊時の
      下流浸水面積も大きい。Pearson r &gt; 0.6 と予測。
      <b>堤高より貯水量の方が強い相関</b>を示す (=決壊時に流出する水量で決まる)。</li>
  <li><b>H3 中山間集中</b>: ため池数 上位 5 市町のうち 3 件以上が
      <b>中山間 (内陸)</b> 市町。これは「ため池=農業遺構」の歴史的分布と一致する。</li>
  <li><b>H4 連鎖重複の地理学</b>: 個別ため池の浸水想定面積を単純合計すると、
      unary_union 面積より<b>大幅に大きく</b> (重複比率 &gt; 30%)、
      <b>同じ住宅街が複数のため池決壊で繰り返し被災想定される</b>
      連鎖リスク構造を示す。</li>
  <li><b>H5 指定の波 R3.5.31</b>: 特定農業用ため池の指定日は 2020-2023 (R2-R5) に集中し、
      <b>R3.5.31 (令和 3 年 5 月末日 = 経過措置期限)</b> が単一最大ピークとなる。</li>
  <li><b>H6 規模分布の対数性</b>: 貯水量分布は対数的 (右に長い裾)。
      log10 で 3 桁以上のレンジを持つ。<b>「小池の分散」と「大池の少数」</b>が
      共存する典型的な農業遺構分布。</li>
</ul>

<h3>到達点</h3>
<p>2 dataset を ため池番号 をキーに完全結合し、規模属性と浸水想定面積を
クロスして 6 仮説を量的に検証する。学習者は (a) Shapefile + CSV の異種統合、
(b) <code>dissolve(by=...)</code> による属性集約と <code>unary_union</code> による
重複排除の使い分け、(c) ため池決壊という<b>農業×防災</b>の交差問題のデータ表現
を体得する。</p>

<div class="note">
<b>厳密性の宣言</b>: 本記事は L4-L11 (河川浸水) ・L44 (高潮浸水) と
<b>厳密に独立</b>。河川浸水 (流域降雨) ・高潮浸水 (海面異常上昇) ・
ため池決壊浸水 (人工堤体破壊) は<b>水文機構が別の災害</b>として扱う。
L07 (浸水×インフラ統合) で dataset 62 を「インフラ施設」の 1 として既参照しているが、
本記事はため池そのものを主役に据え、Shapefile 浸水想定区域を主幾何データに用いる
新しい視点である。
</div>
"""))

# (2) 使用データ
sections.append(("使用データ", f"""
<p>本記事は DoBoX の<b>「ため池」</b>関連 2 dataset を統合する:</p>

<ul class="kv">
  <li><b>62</b> ため池基本情報 (CSV)
      [<a href="https://hiroshima-dobox.jp/datasets/62" target="_blank">DoBoX dataset 62</a>] —
      6,754 池の属性表 (堤高/貯水量/緯度経度/所管市町/特定指定日)</li>
  <li><b>63</b> ため池浸水想定区域情報_Shapefile
      [<a href="https://hiroshima-dobox.jp/datasets/63" target="_blank">DoBoX dataset 63</a>] —
      7,223 polygon (6,730 unique 池) の決壊時浸水想定エリア</li>
</ul>

<h3>表 (1) — 2 dataset の仕様サマリ</h3>
{df_to_html(T_dataspec)}
<p class="tnote">この表から読み取れること:
(1) 形式が完全に異なる (CSV vs Shapefile) ため、統合には<b>結合キー設計</b>が必須。
(2) サイズは Shapefile が 100 倍以上 (1 MB → 143 MB) で、
ポリゴン幾何が支配的。CSV は属性のみだが、地理空間処理を支える「場所情報」と
<b>「規模指標」</b>の両方を持つ。
(3) dataset 62 のキー = <code>ため池番号</code> (9 桁数字) は dataset 63 の
<code>FIELD001</code> 列と完全一致するため、<b>属性結合 (merge)</b> でデータ統合できる。
(4) dataset 63 の Shapefile は 1 池に複数 polygon (浸水範囲が分断された場合) を
持つことがあり、<code>dissolve(by="FIELD001")</code> で 7,223 → 6,730 に集約する。</p>

<h3>表 (2) — 結合 STEP の段階表 (要件 K Before/After)</h3>
{df_to_html(T_joinkey)}
<p class="tnote">この表から読み取れること: 入力 6,754 行 (CSV) が STEP 5 まで
通って union 単一 polygon (面積 {total_union_area:.2f} km²) に至るまでの
<b>段階的サイズ変化</b>を示す。STEP 3 で 7,223 → 6,730 に減るのは
「1 池の浸水範囲が分断された複数 polygon」が dissolve で結合されるため、
STEP 4 で 6,754 - 6,730 = 24 件が結合無しで残るのは
「CSV にあって SHP に対応 polygon 無いため池」=<b>分析 1 で詳細特定する</b>。</p>

<h3>形式の詳細</h3>
<ul class="kv">
  <li><b>CSV (62)</b>: Shift_JIS 13 列 (PDF1-4 を含む)。
      <code>ため池番号</code> 9 桁数字, <code>ため池名称</code>, <code>所管市町</code>,
      <code>特定農業用ため池の指定</code> (令和年月日, 例 "R3.5.31"),
      <code>緯度</code>/<code>経度</code> (WGS84/EPSG:4326),
      <code>堤高（m）</code>, <code>貯水量（千m3）</code>。</li>
  <li><b>Shapefile (63)</b>: ESRI Shapefile, CRS = <b>EPSG:2445</b>
      (旧 JGD2000 平面直角座標系第 III 系) → 内部処理は <b>EPSG:6671</b>
      (現 JGD2011 第 III 系, m 単位) に投影変換。
      属性列 = <code>IDD</code>, <code>BCODE=130</code>, <code>SCODE=130</code>,
      <code>HCODE=130</code> (全て一定値), <code>ため池ID</code> (=ため池名称テキスト),
      <code>市町村名</code> (全 None), <code>FIELD001</code> (=ため池番号 9 桁) のみ。</li>
  <li><b>結合キー</b>: <code>ため池番号</code> (CSV) ↔ <code>FIELD001</code> (SHP)
      で完全結合。CSV 6,754 件中 6,730 件 (99.6%) が SHP に対応。</li>
</ul>
"""))

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

<h3>(A) DoBoX 直リンク (2 dataset)</h3>
<table>
<tr><th>dataset</th><th>カタログ</th><th>resource_download (直 DL)</th><th>形式</th><th>サイズ</th></tr>
<tr><td>62 ため池基本情報</td>
    <td><a href="https://hiroshima-dobox.jp/datasets/62" target="_blank">dataset 62</a></td>
    <td><a href="https://hiroshima-dobox.jp/resource_download/90">直 DL (rid 90)</a></td>
    <td>CSV</td><td>1.1 MB</td></tr>
<tr><td>63 ため池浸水想定区域情報_Shapefile</td>
    <td><a href="https://hiroshima-dobox.jp/datasets/63" target="_blank">dataset 63</a></td>
    <td><a href="https://hiroshima-dobox.jp/resource_download/98562">直 DL (rid 98562)</a></td>
    <td>Shapefile (ZIP)</td><td>81.2 MB</td></tr>
</table>

<h3>(B) PowerShell 一括取得 (再現用)</h3>
<pre><code>cd "2026 DoBoX 教材"
mkdir -Force data\\extras\\L45_pond_inundation
iwr "https://hiroshima-dobox.jp/resource_download/90"    -OutFile "data\\extras\\tameike_basic.csv"
iwr "https://hiroshima-dobox.jp/resource_download/98562" -OutFile "data\\extras\\L45_pond_inundation\\tameike_inundation_shp.zip"
# ZIP 展開
Expand-Archive -Force "data\\extras\\L45_pond_inundation\\tameike_inundation_shp.zip" "data\\extras\\L45_pond_inundation\\shp"
# キャッシュビルド (~2 分, 初回のみ)
py -X utf8 lessons\\_l45_build_cache.py
# 本記事スクリプト (~1 分)
py -X utf8 lessons\\L45_pond_inundation.py</code></pre>

<h3>(C) 中間データ・図 (本記事生成物の直リンク)</h3>
<ul class="kv">
  <li>2 dataset 仕様 CSV: <a href="assets/L45_dataspec.csv" download>L45_dataspec.csv</a></li>
  <li>結合 STEP 表 CSV: <a href="assets/L45_joinkey_steps.csv" download>L45_joinkey_steps.csv</a></li>
  <li>全体サマリ CSV: <a href="assets/L45_overall.csv" download>L45_overall.csv</a></li>
  <li>浸水想定なし 24 件 CSV: <a href="assets/L45_no_inund.csv" download>L45_no_inund.csv</a></li>
  <li>規模 ↔ 面積 相関 CSV: <a href="assets/L45_correlation.csv" download>L45_correlation.csv</a></li>
  <li>指定日分布 CSV: <a href="assets/L45_designation_dates.csv" download>L45_designation_dates.csv</a></li>
  <li>市町別集計 CSV: <a href="assets/L45_per_city.csv" download>L45_per_city.csv</a></li>
  <li>Top 浸水ため池 CSV: <a href="assets/L45_top_inund.csv" download>L45_top_inund.csv</a></li>
  <li>仮説検証 CSV: <a href="assets/L45_hypothesis.csv" download>L45_hypothesis.csv</a></li>
  <li>9 図 PNG: 各図を右クリック → 名前を付けて画像を保存</li>
  <li>再現スクリプト: <a href="L45_pond_inundation.py" download>L45_pond_inundation.py</a> +
      <a href="_l45_build_cache.py" download>_l45_build_cache.py</a></li>
</ul>
"""))

# (4) 分析 1: カバレッジ完全性 — 浸水想定なし 24 件の特定
sections.append(("分析 1: カバレッジ完全性 — CSV と SHP の重ね合わせ", f"""
<h3>狙い (RQ ↔ 仮説 H1)</h3>
<p>2 dataset の<b>カバレッジ差</b>を量的に評価する。
CSV (62) は 6,754 池、SHP (63) は 7,223 polygon (= 6,730 unique 池)。
カバレッジ差 24 件は<b>「CSV にあって SHP 無し」</b>のため池であり、
<b>地理的偏りがあれば</b>「データ整備の優先度」に関する制度的事実が
データから直接読める。</p>

<h3>手法 — pandas merge LEFT JOIN による属性結合</h3>
<p>pandas の <code>DataFrame.merge(how='left')</code> で
CSV を主表、SHP の polygon area を従属表として結合する。
<b>結合キー</b> は CSV.<code>ため池番号</code> = SHP.<code>FIELD001</code>。
<b>LEFT JOIN</b> を選んだ理由は「CSV 側を主張として残し、
SHP に対応 polygon が無い池は area_km2 が NaN になる」ことで
<b>非カバレッジ件数</b>がそのまま得られるため。</p>

<div class="note">
<b>結合キーの設計 (要件 O STEP 分け)</b>:
  <ul>
    <li><b>STEP A</b>: SHP.<code>FIELD001</code> を文字列として保つ
        (geopandas はデフォルトで int64 にしようとするが、頭 0 が落ちる池があるため文字列のまま結合)</li>
    <li><b>STEP B</b>: SHP を <code>dissolve(by="FIELD001")</code> で 1 池 1 polygon に集約
        (1 池が複数 polygon に分かれている場合があるため)</li>
    <li><b>STEP C</b>: CSV.<code>ため池番号</code> ↔ SHP.<code>FIELD001</code> で merge LEFT JOIN</li>
    <li><b>STEP D</b>: <code>area_km2</code> が NaN の行 = 浸水想定なし のため池 24 件</li>
  </ul>
</div>

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

CACHE = Path("data/extras/L45_pond_inundation/_cache")

# (1) ため池基本情報 (CSV) を読込, 数値列は to_numeric
df = pd.read_csv("data/extras/tameike_basic.csv", dtype=str)
df.columns = [c.strip() for c in df.columns]
df["ため池番号"] = df["ため池番号"].str.strip()
df["堤高_m"]      = pd.to_numeric(df["堤高（m）"],   errors="coerce")
df["貯水量_千m3"] = pd.to_numeric(df["貯水量（千m3）"], errors="coerce")

# (2) 浸水想定 SHP は事前 dissolve 済 GPKG (= ため池ごと 1 polygon) を読込
diss = gpd.read_file(CACHE / "diss_per_pond.gpkg")
# diss["FIELD001"] が結合キー, diss["area_km2"] が浸水想定面積

# (3) LEFT JOIN: 結合無しは area_km2 = NaN として残る
merge = df.merge(
    diss[["FIELD001", "area_km2"]],
    left_on="ため池番号", right_on="FIELD001",
    how="left",
)
merge["has_inund"] = merge["area_km2"].notna()

# (4) 浸水想定なし のため池を抽出
no_shp = merge[~merge["has_inund"]]
print(f"非カバレッジ {{len(no_shp)}} 件 / 所管市町: {{no_shp['所管市町'].value_counts().to_dict()}}")
''')}

<h3>結果の図</h3>
<p><b>図4 を選んだ理由</b>: 6,754 池の<b>地理分布</b>を一目で把握するには
点マップが最適。<b>色 = 浸水想定の有無、サイズ = log10 貯水量</b> の
2 軸を 1 図で示すことで、「どこに大きい池があるか」「どこの池が
非カバレッジか」を同時に確認できる。棒グラフでは地理が見えない。</p>
{figure('assets/L45_fig4_pond_points_map.png', '図4: 広島県 特定農業用ため池 位置マップ')}

<p><b>図4 から読み取れること:</b></p>
<ul class="kv">
  <li>ため池は<b>瀬戸内海沿岸〜中山間</b>に広く分布。北部山地 (庄原・三次) も多数。</li>
  <li><b>東広島市</b> (東部内陸) の密集が目立つ。CSV では 1,768 池 (全体の 26%) を所管。</li>
  <li>青点 (浸水想定なし 24 池) は<b>三次市 (12) と庄原市 (12) の中山間 2 市町に
      完全集中</b>している (= ランダムでなく地理偏りあり)。これは仮説 H1 を強支持。</li>
  <li>大円 (大規模池) は福山市・東広島市・庄原市に集中する。これは仮説 H3 と一致。</li>
</ul>

<h3>結果の表 (1) — 全体サマリ</h3>
{df_to_html(T_overall)}

<p><b>表 (1) から読み取れること:</b></p>
<ul class="kv">
  <li>カバレッジ率 = 99.6% — ほぼ完全結合だが、24 件の非カバレッジは無視できない。</li>
  <li>個別合計 {sum_indiv_km2:.1f} km² と union {total_union_area:.1f} km² の差 =
      {overlap_km2:.1f} km² ({overlap_ratio:.1f}%) は<b>連鎖重複</b>。
      これは仮説 H4 の核心 (分析 4 で深掘り)。</li>
  <li>平均堤高 {mean_dam:.2f} m / 中央値 {median_dam:.1f} m と最大 {max_dam:.1f} m
      の<b>大きな乖離</b>は規模分布の歪みを示す (仮説 H6)。</li>
</ul>

<h3>結果の表 (2) — 浸水想定なし 24 件の内訳</h3>
{df_to_html(no_shp_show.head(15))}
<p class="tnote">(表は上位 15 件のみ表示, 全 24 件は <a href="assets/L45_no_inund.csv" download>L45_no_inund.csv</a> 参照)</p>

<p><b>表 (2) から読み取れること:</b></p>
<ul class="kv">
  <li><b>全 24 件が三次市 (12) と庄原市 (12) の 2 市町に集中</b>
      (他 21 市町はすべてカバレッジ 100%)。両市は隣接する<b>中山間 (内陸) 市町</b>で、
      これは「ランダムな欠損」ではなく<b>制度的・行政的事情</b>を示唆する。</li>
  <li>これらの池はすべて<b>特定農業用ため池に指定されている</b>
      (令和 2-3 年に指定済み) にもかかわらず、決壊時浸水想定 SHP は未整備。</li>
  <li>規模も多様 (堤高 2-14 m, 貯水量 0.1-149 千 m³) で、規模で説明できない。
      <b>三次市・庄原市役所と県の連携で SHP 整備が遅延しているか、
      特殊地形で計算困難な池</b>と推定される。両市は県北部の中山間地域で、
      他市町と共通の事情を抱えている可能性がある。発展課題で原因調査の方向性を提示する。</li>
</ul>
"""))

# (5) 分析 2: 規模属性 ↔ 浸水面積 のスケーリング
sections.append(("分析 2: 規模属性 ↔ 浸水面積 のスケーリング", f"""
<h3>狙い (RQ ↔ 仮説 H2)</h3>
<p>「ため池の規模 (堤高・貯水量) が大きいほど決壊時の下流浸水面積も大きい」
という直感的予測を、Pearson 相関と log-log 散布で検証する。
<b>堤高と貯水量のどちらが浸水面積をより強く支配するか</b>も比較する。</p>

<h3>手法 — log-log 散布 + OLS 回帰</h3>
<p>ため池の規模指標 (堤高 m, 貯水量 千 m³) は<b>桁数のレンジが広い</b>
(堤高 0.2-33 m = 約 2 桁、貯水量 0.002-1,053 千 m³ = 約 6 桁)。
よって生値の散布図では大池の点だけが目立ち、小池の構造が潰れる。
<b>log-log 軸</b> ((x, y) の両方を log10 化) で表示すると
<b>べき乗則 (power law) の傾き</b>が直線として読める。</p>

<div class="note">
<b>Pearson 相関 r とは (要件 B/J)</b>:
2 変数 (x, y) の<b>線形関係の強さ</b>を -1〜+1 で示す。
+1 = 完全な正の直線、0 = 無相関、-1 = 完全な負の直線。
<b>入力</b>: 同じ長さの 2 列の数値、<b>出力</b>: 1 つの相関値。
<b>限界</b>: 非線形関係 (放物線等) は捉えられない。<b>代替</b>: Spearman 順位相関、
log-log 化での Pearson (本記事はこれを併用)。
</div>

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

# 結合済 merge 表から「浸水想定あり」だけ抽出 (NaN 除外)
m2 = merge[merge["has_inund"]].copy()
m2 = m2[(m2["堤高_m"] > 0) & (m2["貯水量_千m3"] > 0) & (m2["area_km2"] > 0)].copy()

# 生値 Pearson 相関
corr_dam_area = m2[["堤高_m", "area_km2"]].corr().iloc[0, 1]
corr_cap_area = m2[["貯水量_千m3", "area_km2"]].corr().iloc[0, 1]

# log10 で再相関 (べき乗則の検証)
m2["log_dam"]  = np.log10(m2["堤高_m"])
m2["log_cap"]  = np.log10(m2["貯水量_千m3"])
m2["log_area"] = np.log10(m2["area_km2"])
log_corr_cap_area = m2[["log_cap", "log_area"]].corr().iloc[0, 1]

# log-log OLS 回帰: log(area) = b0 + b1 * log(cap)
b0, b1 = np.polynomial.polynomial.polyfit(m2["log_cap"], m2["log_area"], 1)
print(f"log-log 回帰: log(area) = {{b0:.2f}} + {{b1:.2f}} * log(cap)")
# b1 ≈ 0.6 → power law: area ∝ cap^0.6 (亜線形スケーリング)
''')}

<h3>結果の図 (1) — 規模分布 (要件 L 次元・サイズの混同回避)</h3>
<p><b>図2 を選んだ理由</b>: 規模属性の<b>分布形状</b>を見るのが先。
分布が偏っていれば次の散布図の解釈も変わる (= 散布点が密集する場所が偏る)。
左パネル = 堤高 (生値ヒストグラム) は<b>右に裾を引く正規様</b>。
右パネル = 貯水量 (log10 軸ヒストグラム) は<b>対数正規様</b>で、log scale で見て
初めて分布の中身が読める。</p>
{figure('assets/L45_fig2_size_dist.png', '図2: ため池規模分布 — 堤高は正規様, 貯水量は対数様')}

<p><b>図2 から読み取れること:</b></p>
<ul class="kv">
  <li><b>堤高は中央値 {median_dam:.1f} m</b> 周辺に集中する単峰分布。最大 {max_dam:.1f} m は外れ値。
      これは「人間が築造可能な堤体高さの物理的上限」が効いている (土堰堤は通常 30 m が限界)。</li>
  <li><b>貯水量は log10 軸で見ると中央値 {median_cap:.2f} 千 m³ 付近にピーク</b>を持つ
      対数正規様。レンジは 0.002 - 1,053 千 m³ で<b>約 6 桁</b>。
      仮説 H6 (3 桁以上) を強支持する。</li>
  <li>堤高分布は線形可視化で十分だが、貯水量は log scale でないと
      情報が潰れる。<b>変数の分布特性に応じて軸スケールを選ぶ</b>
      ことの教育的重要性。</li>
</ul>

<h3>結果の図 (2) — 規模 vs 浸水面積 散布</h3>
<p><b>図3 を選んだ理由</b>: 2 変数の関係を見るには散布図が王道。
<b>log-log 軸</b>を採用するのは「規模も面積も対数で広く分布する」ため。
log-log 軸でべき乗則が直線になることを利用すれば、傾きから
スケーリング指数 (= area ∝ cap^β) が読める。</p>
{figure('assets/L45_fig3_size_vs_area.png', '図3: ため池規模 ↔ 決壊時下流浸水想定面積')}

<p><b>図3 から読み取れること:</b></p>
<ul class="kv">
  <li>左 (堤高 vs 面積): 散布点に上昇トレンドはあるが拡散も大きい。Pearson r = {corr_dam_area:.3f}。</li>
  <li>右 (貯水量 vs 面積): 散布点が<b>明確な右上がり直線</b>に並ぶ。
      Pearson r = {corr_cap_area:.3f}, log-log r = {corr_logcap_logarea:.3f}。
      <b>貯水量の方が堤高より浸水面積を強く支配</b>する。</li>
  <li>OLS log-log 回帰の傾き b1 = {b1:.2f}。これは<b>area ∝ cap^{b1:.2f}</b>
      の<b>亜線形スケーリング</b> (= 貯水量が 10 倍になっても面積は約 {10**b1:.1f} 倍にしかならない)
      を示す。地形 (谷の幅・勾配) で水が広がりにくい池があるためと考えられる。</li>
  <li>仮説 H2 を強支持 (貯水量 r = {corr_cap_area:.3f} &gt; 0.7, 堤高より強)。</li>
</ul>

<h3>結果の表 — 4 種の Pearson r</h3>
{df_to_html(T_corr)}
<p><b>表から読み取れること:</b> log-log 化すると貯水量との r が
{corr_cap_area:.3f} → {corr_logcap_logarea:.3f} に上昇するが、
堤高はあまり変わらない ({corr_dam_area:.3f} → {corr_logdam_logarea:.3f})。
これは「貯水量 vs 浸水面積はべき乗則的、堤高 vs 浸水面積は弱い線形」
という構造の違いを示す。<b>水量が直接浸水範囲を決め、堤体の高さは間接的</b>。</p>
"""))

# (6) 分析 3: 中山間集中 vs 沿岸 — 市町別ランキング
sections.append(("分析 3: 中山間集中 vs 沿岸 — 市町別ランキング", f"""
<h3>狙い (RQ ↔ 仮説 H3)</h3>
<p>ため池の<b>地理分布</b>を市町別に集計し、中山間 (内陸) 市町と
沿岸市町でどう違うかを定量化する。歴史的にため池は農業遺構なので
内陸の水田地帯に集中するはずである。</p>

<h3>手法 — 市町別 集計 + 中山間/沿岸 分類</h3>
<p>CSV.<code>所管市町</code> 列で <code>groupby</code> して
ため池数・平均堤高・合計貯水量・合計浸水想定 km² を集計する。
さらに各市町を本記事独自定義の<b>中山間</b> (内陸 9 市町) /
<b>沿岸</b> (海岸線あり 14 市町) / その他 に分類して
カテゴリ別の差を可視化する。</p>

<h3>実装コード</h3>
{code('''
# 中山間/沿岸の独自分類 (本記事内マッピング)
INLAND_CITIES = {"府中市", "三次市", "庄原市", "東広島市", "安芸高田市",
                 "山県郡安芸太田町", "山県郡北広島町",
                 "世羅郡世羅町", "神石郡神石高原町"}
COASTAL_CITIES = {"広島市", "呉市", "竹原市", "三原市", "尾道市", "福山市",
                  "大竹市", "廿日市市", "江田島市", "豊田郡大崎上島町",
                  "安芸郡府中町", "安芸郡海田町", "安芸郡熊野町"}

# 市町別 集計 (groupby + agg)
g = merge.groupby("所管市町").agg(
    ため池数         =("ため池番号", "count"),
    平均堤高_m       =("堤高_m",     "mean"),
    合計貯水量_千m3  =("貯水量_千m3", "sum"),
    合計浸水想定_km2 =("area_km2",   "sum"),
).reset_index().sort_values("ため池数", ascending=False)
g["地理分類"] = g["所管市町"].map(
    lambda x: "中山間" if x in INLAND_CITIES else
              ("沿岸"   if x in COASTAL_CITIES else "その他"))
''')}

<h3>結果の図 (1) — 市町別 ため池数 横棒</h3>
<p><b>図1 を選んだ理由</b>: 23 市町のラベルは<b>長い文字列</b>を含むので
horizontal bar が読みやすい。<b>色で中山間/沿岸を区別</b>することで
「上位は中山間中心」が一目で分かる。pie chart や treemap だと比較的順位が分かりにくい。</p>
{figure('assets/L45_fig1_pond_count_by_city.png', '図1: 所管市町別 特定農業用ため池数')}

<p><b>図1 から読み取れること:</b></p>
<ul class="kv">
  <li><b>東広島市 1,768 池</b>が圧倒的 1 位 (= 全体の 26%)。中山間市町。</li>
  <li>2 位 福山市 1,077 池 (沿岸だが内陸部に大池が多い)、3 位 庄原市 697、
      4 位 三次市 616、5 位 尾道市 443。</li>
  <li>上位 5 のうち<b>東広島・庄原・三次の 3 市が中山間</b> = 仮説 H3 を強支持。</li>
  <li>沿岸の<b>広島市は 161 池</b>と少ない。これは「広島市内に水田が少ない」
      = 都市化された結果、ため池の必要性が低いため。</li>
</ul>

<h3>結果の図 (2) — ため池数 vs 浸水想定 散布</h3>
<p><b>図9 を選んだ理由</b>: 「池数」と「浸水想定面積」の関係を見る
2 次元散布。1 池あたりの平均浸水想定面積 (傾き) が中山間と沿岸で違うかを見る。</p>
{figure('assets/L45_fig9_density_scatter.png', '図9: 市町別 ため池数 vs 浸水想定面積')}

<p><b>図9 から読み取れること:</b></p>
<ul class="kv">
  <li>東広島・福山が右上の外れ値。両者で県全体の <b>{(city_stats.head(2)["合計浸水想定_km2"].sum()/sum_indiv_km2*100):.0f}%</b>
      の浸水想定面積を占める。</li>
  <li>沿岸 (青) と中山間 (緑) は散布傾向にやや差がある: 中山間市町は
      ため池 1 件あたり浸水想定が大きい (大池が多い)、沿岸は小池が分散。</li>
  <li>原点近くに沿岸町群 (海田町・府中町・大竹市等) が集まる = 池数も浸水想定も少。
      <b>都市化が進んだ沿岸町ではため池が農業遺構として消失している</b>傾向を示す。</li>
</ul>

<h3>結果の表 — 市町別 集計 (上位 10 + その他)</h3>
{df_to_html(city_stats_view)}
<p><b>表から読み取れること:</b></p>
<ul class="kv">
  <li>「池あたり浸水_km2」列は<b>1 池が決壊した時の平均浸水面積</b>。
      福山市 (0.078 km²/池) が最も大きく、海田町 (0.040) や広島市 (0.060) は小さい。</li>
  <li>合計貯水量を合計浸水で割ると<b>「貯水 1 千 m³ あたり何 km² が水に浸かるか」</b>
      の効率指標になり、地形による違いを定量化できる (発展課題)。</li>
</ul>
"""))

# (7) 分析 4: 連鎖重複の地理学
sections.append(("分析 4: 連鎖重複の地理学 — unary_union vs 個別合計", f"""
<h3>狙い (RQ ↔ 仮説 H4)</h3>
<p>本記事の<b>核心的発見</b>。各ため池の浸水想定面積を単純に足すと
{sum_indiv_km2:.1f} km² だが、unary_union で重複を排除すると
{total_union_area:.1f} km²。差分 {overlap_km2:.1f} km² ({overlap_ratio:.1f}%) は
<b>「同じ地点が複数のため池決壊で繰り返し被災想定される」</b>連鎖重複である。
これがどんな地形パターンで発生するかを地図で可視化する。</p>

<h3>手法 — shapely.unary_union と差分計算</h3>
<p><code>shapely.unary_union</code> は複数の polygon を<b>1 つの MultiPolygon</b>
にマージする (重なりは 1 度だけ計算)。これと個別 polygon の単純合計面積の差が
重複面積になる。</p>

<div class="note">
<b>unary_union vs dissolve の違い (要件 J ツール化視点)</b>:
  <ul>
    <li><b>dissolve(by=keys)</b>: 列の値が同じ行を統合 (例: ため池 ID で集約)</li>
    <li><b>unary_union</b>: 全 polygon を 1 つに結合 (列を見ず幾何的に重ねる)</li>
  </ul>
本分析は「県全体 1 つの MultiPolygon」が欲しいので unary_union。
</div>

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

# 全 7,223 ポリゴン (ため池ごと dissolve 前) を読込
raw = gpd.read_file("data/extras/L45_pond_inundation/shp/ため池浸水想定区域.shp",
                    encoding="cp932").to_crs("EPSG:6671")

# 不正幾何を buffer(0) で修復 (30 件)
geoms = raw.geometry.values
inv = ~shapely.is_valid(geoms)
new = [g.buffer(0) if isi else g for g, isi in zip(geoms, inv)]
raw["geometry"] = new

# unary_union で全 polygon を結合 (~87 秒, 重い処理 → キャッシュ化)
union_geom = shapely.unary_union(raw.geometry.values)
union_km2 = union_geom.area / 1e6
print(f"県全体 真の浸水想定面積 = {{union_km2:.2f}} km²")

# 個別合計 vs union: 差が連鎖重複
individual_km2 = (raw.geometry.area / 1e6).sum()
overlap_km2 = individual_km2 - union_km2
print(f"個別合計 = {{individual_km2:.2f}} km², 重複 = {{overlap_km2:.2f}} km² "
      f"({{overlap_km2/individual_km2*100:.1f}}%)")
''')}

<h3>結果の図 (1) — 県全域 浸水想定マップ</h3>
<p><b>図5 を選んだ理由</b>: 全 6,730 池の浸水想定 polygon を 1 枚の地図に重ね、
<b>「線状に集中している場所」</b>を見つけるため。線状集中=複数ため池の
浸水想定が同じ谷に並んで連鎖している証拠。</p>
{figure('assets/L45_fig5_inund_polygon_map.png', '図5: ため池決壊時 浸水想定区域マップ (全 6,730 池)')}

<p><b>図5 から読み取れること:</b></p>
<ul class="kv">
  <li>瀬戸内沿岸〜中山間の<b>谷地形に沿った線状集中</b>が無数に見える。
      これは「上流ため池の浸水想定下流に下流ため池の浸水想定が連鎖する」連鎖構造を直接示す。</li>
  <li>福山市・東広島市の平野部では<b>面状に広がる浸水想定</b>が観察される
      (服部大池等の大池が単独で広範囲を浸水想定するため)。</li>
  <li>個別合計 {sum_indiv_km2:.1f} km² から union {total_union_area:.1f} km²
      への 40% 重複は、<b>住民から見れば「池 A が決壊しても池 B が決壊しても自宅は浸水想定区域」</b>
      = 同じエリアが複数シナリオで繰り返し脅かされる<b>多重決壊リスク</b>を示す。</li>
</ul>

<h3>結果の図 (2) — 上位 4 市町ズーム small multiples</h3>
<p><b>図6 を選んだ理由</b>: ため池数上位 4 市町 (東広島・福山・庄原・三次) を
同じレイアウトで並べることで<b>市町ごとの分布パターンの違い</b>が直感的に分かる。
small multiples は「同じレンズで複数地域を比較する」ための定番構図。</p>
{figure('assets/L45_fig6_top4_cities_zoom.png', '図6: ため池数上位 4 市町 ズームマップ')}

<p><b>図6 から読み取れること:</b></p>
<ul class="kv">
  <li>東広島市は<b>無数の谷に小池が分散</b>。各浸水想定は谷沿いに細く伸びる。</li>
  <li>福山市は<b>少数の大池</b>が広く下流を浸水想定。服部大池 (2.94 km²) のような
      巨大池が福山平野の特定エリアを単独で支配する。</li>
  <li>庄原・三次は中山間特有の<b>細長い谷ネットワーク</b>に沿った分布。
      連鎖重複が最も発生しやすい地形。</li>
  <li>4 市町とも「ため池点 (青)」と「浸水想定 (赤)」が直接重なるのは小さく、
      <b>赤は青の下流側に長く延びる</b>のが見える (=決壊水が下流へ流下する)。</li>
</ul>

<h3>結果の図 (3) — 福山市 詳細マップ</h3>
<p><b>図7 を選んだ理由</b>: 福山平野は<b>少数大池</b>パターンの典型なので、
個別池の名前と浸水想定範囲を 1:1 で対応させて見るのに最適。
カラーマップは「浸水想定面積で濃淡」に塗ることで大池の規模感が掴める。</p>
{figure('assets/L45_fig7_fukuyama_detail.png', '図7: 福山市 ため池決壊浸水想定 詳細マップ')}

<p><b>図7 から読み取れること:</b></p>
<ul class="kv">
  <li><b>服部大池 (2.94 km²)</b> は福山平野北部を単独で広範囲浸水想定する。
      これは江戸期 (1620 年) 築造の県内最古級ため池の 1 つ。</li>
  <li>春日池・神村大池など複数の大池が福山平野を取り囲み、
      <b>連鎖決壊で福山市街が広域被災する</b>シナリオが地図から読める。</li>
  <li>浸水想定範囲は河川 (蘆田川等) の流路と部分一致 = 高潮 (L44) や
      河川氾濫 (L4-L11) と重複するエリアでもある。
      これは発展課題 (3 機構ハザード重ね) の起点となる。</li>
</ul>
"""))

# (8) 分析 5: 指定の波 — 行政の制度的タイミング
sections.append(("分析 5: 指定の波 — 行政の制度的タイミング", f"""
<h3>狙い (RQ ↔ 仮説 H5)</h3>
<p>「特定農業用ため池」の指定日 (CSV 列「特定農業用ため池の指定」)
は令和年月日で記述されている。これを西暦に変換して時系列ヒストグラムを
作り、<b>制度的なピーク</b>がいつ発生したかを可視化する。</p>

<h3>手法 — 令和年号パーサ + 月単位集計</h3>
<p>CSV の指定日は "R3.5.31" のような形式。令和元年 = 2019 として
正規表現で年月日を抽出し、pandas Timestamp に変換する。
月単位で集計してヒストグラムを描き、Top 月をラベル付けする。</p>

<h3>実装コード</h3>
{code('''
import pandas as pd

def reiwa_to_date(s):
    """R3.5.31 → 2021-05-31"""
    if pd.isna(s) or not s.startswith("R"):
        return None
    parts = s[1:].split(".")
    if len(parts) != 3: return None
    r_year, mo, day = map(int, parts)
    return f"{{2018 + r_year:04d}}-{{mo:02d}}-{{day:02d}}"

merge["指定日_西暦"] = merge["特定農業用ため池の指定"].apply(reiwa_to_date)
desig = pd.to_datetime(merge["指定日_西暦"].dropna())

# 月単位集計
months = desig.dt.to_period("M").value_counts().sort_index()
top_month = months.idxmax()
print(f"最大月 = {{top_month}} ({{int(months.max())}} 池)")
''')}

<h3>結果の図</h3>
<p><b>図8 を選んだ理由</b>: 月単位棒グラフで時系列の<b>ピーク</b>と
<b>急増・収束のパターン</b>を見るのに適する。Top 3 月にラベル付けすることで
重要な制度的時点を強調する。</p>
{figure('assets/L45_fig8_designation_timeline.png', '図8: 特定農業用ため池 指定の波')}

<p><b>図8 から読み取れること:</b></p>
<ul class="kv">
  <li><b>{top_date} に {top_date_count} 池が一斉指定</b>された (Top 1, {top_share:.1f}% / 総指定数の)。
      これは令和 3 年 5 月末日 = 農業用ため池法の<b>経過措置期限</b>と一致。</li>
  <li>令和 2-3 年 (2020-2021) に指定が集中、令和 4 年以降は微小なフォローアップのみ。</li>
  <li>未指定 288 池 (CSV の「特定農業用ため池の指定」列が空) は、
      <b>2018 年法施行以前から既に何らかの指定があった池</b>または
      <b>指定対象外として再分類された池</b>と推定される。</li>
  <li>仮説 H5 を強支持: R3.5.31 が単一最大ピークで > 30%。</li>
</ul>

<h3>結果の表 — 指定日別 ため池数 (Top 10)</h3>
{df_to_html(T_desig.sort_values('ため池数', ascending=False).head(10))}
<p><b>表から読み取れること:</b> Top 6 はすべて令和 2-3 年で、それぞれが
法的に意味のある「区切り日」(月末・年度末・経過措置期限) に対応している。
これは指定が<b>個別の池の決壊リスク評価ベース</b>ではなく、
<b>行政手続きベース</b>で進められたことを示す。</p>
"""))

# (9) 分析 6: Top 浸水想定 ため池 — 規模の代表例
sections.append(("分析 6: Top 浸水想定ため池 — 規模の代表例", f"""
<h3>狙い (RQ ↔ 仮説 H2 補強)</h3>
<p>浸水想定面積が大きい上位 15 池を抽出し、<b>規模属性 (堤高・貯水量) との対応</b>
を表で確認する。仮説 H2 (規模 ↔ 面積 正相関) の<b>個別事例</b>として、
最上位の池がどんな歴史的背景を持つかを定性的に補強する。</p>

<h3>手法 — sort_values で Top N</h3>
<p>結合済 merge 表で <code>area_km2</code> 列の降順ソートし上位 15 件を抽出。
特定指定日も合わせて表示することで、「最大級の池がいつ指定されたか」
の情報も得る。</p>

<h3>実装コード</h3>
{code('''
top_inund = merge.sort_values("area_km2", ascending=False).head(15)
print(top_inund[["ため池名称", "所管市町", "堤高_m",
                 "貯水量_千m3", "area_km2"]].to_string())
''')}

<h3>結果の表 — Top 15 浸水想定ため池</h3>
{df_to_html(top_inund_view)}

<p><b>表から読み取れること:</b></p>
<ul class="kv">
  <li><b>1 位: 服部大池 (福山市, 浸水 2.94 km²)</b> — 1620 年築造の
      県内最古級ため池の 1 つ。福山藩主水野勝成が領内農業基盤として築造。
      決壊シナリオの想定面積は単独で福山平野 1/30 をカバーする。</li>
  <li>上位 15 のうち<b>福山市が 4 池、東広島市が 4 池</b>。
      この 2 市町だけで上位 15 のうち 8 池 (= 53%) を占める。</li>
  <li>堤高が小さい ({top_inund.iloc[11]['堤高_m']:.1f} m, 味噌が峠池) のに
      浸水面積が大きい ({top_inund.iloc[11]['area_km2']:.2f} km²) 池がある =
      <b>地形 (谷の幅・勾配) が支配する場合</b>。
      逆に堤高 {top_inund.iloc[12]['堤高_m']:.1f} m, 板木ため池は
      最大級堤高だが浸水面積は中位 = 規模だけでは決まらない。</li>
  <li>すべての Top 15 池が令和 2-3 年に「特定農業用ため池」指定済み。</li>
</ul>
"""))

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

<h3>主要発見の整理</h3>
<ul class="kv">
  <li><b>1. カバレッジ完全だが県北中山間 2 市町に 24 件の盲点</b> (H1 強支持):
      CSV 6,754 池中 6,730 池 (99.6%) で SHP 結合成功。
      非カバレッジ 24 件は<b>三次市 (12) + 庄原市 (12) の中山間 2 市町に完全集中</b>。
      これは「ランダム欠損」ではなく<b>県北中山間における制度的・行政的事情</b>を示唆し、
      両市の防災重点ため池整備に共通課題があることをデータで示す。</li>
  <li><b>2. 貯水量がべき乗則的に浸水面積を支配</b> (H2 強支持):
      log-log 相関 r = {corr_logcap_logarea:.3f}, OLS 傾き b1 = {b1:.2f}。
      area ∝ cap^{b1:.2f} の<b>亜線形べき乗則</b>。
      堤高は弱い相関 (r = {corr_dam_area:.3f}) で、<b>「水量」が浸水面積を決める</b>主因。</li>
  <li><b>3. 中山間 3 市町が上位を独占</b> (H3 強支持):
      ため池数 上位 5 市町中 3 市町 (東広島・庄原・三次) が中山間。
      ため池=農業遺構の歴史的分布が裏付けられる。
      沿岸都市 (広島市等) では都市化でため池が消失している。</li>
  <li><b>4. 連鎖重複 40% という強烈な構造</b> (H4 強支持): 個別合計 {sum_indiv_km2:.1f} km²
      vs 真の面積 {total_union_area:.1f} km²、<b>差 {overlap_km2:.1f} km² ({overlap_ratio:.1f}%)</b>。
      <b>住民から見れば「池 A の決壊でも池 B の決壊でも自宅が浸水想定」</b>
      = 同じ地点が複数決壊シナリオで繰り返し脅かされる連鎖構造。
      これは中山間の谷地形 (上流→下流に多数のため池が連なる) で典型的に発生する。</li>
  <li><b>5. R3.5.31 の制度的ピーク</b> (H5 強支持): 単一日 {top_date} に
      {top_date_count} 池が一斉指定 (= 農業用ため池法の経過措置期限)。
      指定は<b>個別池のリスク評価</b>ではなく<b>行政手続きベース</b>で進められた。</li>
  <li><b>6. 貯水量分布の対数性</b> (H6 強支持): 貯水量レンジ約 6 桁
      (0.002 - 1,053 千 m³)。「小池の分散」と「大池の少数」が共存する典型的農業遺構分布。</li>
</ul>

<h3>2 dataset 相互関係の構造発見</h3>
<p>本記事の最重要発見は、CSV (62) と Shapefile (63) が<b>「同じため池群の異なる断面」</b>
として完全結合できる一方、<b>カバレッジと表現の質が大きく異なる</b>点である。</p>
<ul class="kv">
  <li><b>CSV (62)</b>: 「行政管理台帳」としてのため池表 — <b>属性</b>軸 (規模・指定・所管)
      が主、位置は緯度経度 1 点のみ</li>
  <li><b>Shapefile (63)</b>: 「決壊浸水想定」としての地理表 — <b>下流被災</b>軸 (面)
      が主、属性は最低限 (FIELD001 のみ実用)</li>
</ul>
<p>2 件は<b>「ため池の identity (CSV) と impact (SHP)」</b>として相補的であり、
結合することで初めて<b>「規模 ↔ 浸水面積」のスケーリング</b>と
<b>「連鎖重複の地理学」</b>という研究的問いに答えられる。
単独の dataset では得られない知見が、結合してはじめて見える典型例。</p>

<p>さらに 2018 年西日本豪雨後の制度進化が <b>R3.5.31 のピーク</b>として
データに刻まれており、<b>「災害 → 法律 → 指定 → データ整備」</b>という
社会的時間軸が 2 dataset の<b>更新タイミング差</b>
(CSV は随時更新、SHP は 2025-01-22 と最近) としても観察できる。</p>

<h3>本研究の限界</h3>
<ul class="kv">
  <li>(a) 浸水想定 SHP は<b>「100% 全量決壊」</b>シナリオを前提とする
      (各ため池 PDF メタデータ参照)。部分決壊や段階的越流のシナリオは含まれない。
      実際の 2018 年豪雨での損傷は部分損傷が多く、本データは<b>最悪ケース</b>のみを記述。</li>
  <li>(b) 連鎖決壊の同時性 (上流池決壊 → 下流池への影響) は<b>本データには含まれず</b>、
      個々の池の単独決壊を独立に重ねた重複でしかない。
      実際の連鎖決壊シナリオは数値モデルが必要 (発展課題)。</li>
  <li>(c) 24 件の非カバレッジ池の決壊リスクは本記事では<b>未評価</b>
      (= データ無いため計算不能)。三次市に対し SHP 整備を要請する政策示唆を持つ。</li>
  <li>(d) 浸水想定区域内の<b>住民数・建物棟数</b> (= 実際の被災ポテンシャル) は
      本記事では空間結合せず面積指標のみ。L20 (新設建物) や L22 (人口ピラミッド)
      との空間結合は発展課題。</li>
</ul>
"""))

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

<h3>課題 1: 連鎖決壊シミュレーションの数値モデル化</h3>
<ul class="kv">
  <li><b>結果 X</b>: 個別合計 vs union の差 = 40% の重複面積 ({overlap_km2:.1f} km²)。
      これは「同じ地点が複数池決壊の浸水想定」という静的事実だが、
      実際の連鎖決壊では<b>上流池の越流水が下流池の堤体に追加負荷をかける</b>
      動的相互作用がある。</li>
  <li><b>新仮説 Y</b>: 谷の上流→下流に並ぶため池ネットワークの中で、
      上流池決壊から下流池決壊までの<b>カスケード時間 t (時間)</b> は
      池間距離・標高差・堤体強度で決まる。短い t は連鎖決壊リスクが高い。</li>
  <li><b>課題 Z</b>: ため池ごとの<b>centroid + 流域 (DEM 流路解析)</b> から
      上流-下流のため池ペアを <code>networkx</code> でグラフ化し、
      連鎖距離 0-2 km 以内のペアを抽出。L40 標高ラスタを使い
      Manning 式ベースの簡易流下時間を計算する。
      最も連鎖リスクが高い<b>「カスケード ホットスポット」</b> 上位 5 谷を特定。</li>
</ul>

<h3>課題 2: 浸水想定区域内の住民・建物の空間結合</h3>
<ul class="kv">
  <li><b>結果 X</b>: 県全体 真の浸水想定面積 = {total_union_area:.1f} km²。
      しかし住民数・建物棟数の被災ポテンシャルは未評価。</li>
  <li><b>新仮説 Y</b>: ため池決壊浸水想定区域内の住民は人口比 X% 程度で、
      <b>中山間市町ほど比率が高い</b> (谷底集落が想定区域に含まれるため)。</li>
  <li><b>課題 Z</b>: L22 性別年齢別人口メッシュと L20 新築建物点を
      <code>gpd.sjoin(predicate='within')</code> で union polygon と空間結合。
      市町別に<b>「想定区域内人口 / 全人口」</b>と
      <b>「想定区域内新築 / 全新築」</b>を計算。<b>「危険区域への新規居住率」</b>を
      L19 居住誘導区域とクロスして警告マップを作る。</li>
</ul>

<h3>課題 3: ため池決壊 × 河川浸水 × 高潮 「3 機構ハザード重ね」</h3>
<ul class="kv">
  <li><b>結果 X</b>: 福山平野では服部大池等のため池決壊浸水想定が
      河川 (蘆田川) や高潮想定区域 (L44) と地理的に重複している。</li>
  <li><b>新仮説 Y</b>: 福山平野の特定エリアは<b>3 機構 (ため池 + 河川 + 高潮) すべての
      浸水想定区域に該当</b>する「3 重危険区域」となる。面積は数 km² 程度と推定。</li>
  <li><b>課題 Z</b>: 本記事の union polygon と L4-L11 河川浸水・L44 高潮浸水を
      <code>gpd.overlay(how='intersection')</code> で 3 重重ね、
      共通エリアの面積・人口・新築数を集計。これは<b>「県の最危険区域」</b>として
      防災計画に直接資する成果。</li>
</ul>

<h3>課題 4: 三次市・庄原市 24 件 非カバレッジ池の現地調査</h3>
<ul class="kv">
  <li><b>結果 X</b>: 浸水想定 SHP 無しの 24 件すべてが県北中山間の三次市 (12) + 庄原市 (12) に集中。</li>
  <li><b>新仮説 Y</b>: これらの池は<b>(a) 行政整備の遅延、(b) 特殊地形で計算困難、(c) 既に廃止された池</b>
      のいずれかで、いずれにせよ<b>「分かっているのに地図化されていない」リスク</b>を意味する。
      両市が地理的に隣接する中山間市町であることから、
      <b>県北部 SHP 整備の共通課題 (山間特殊地形・予算配分・人員不足)</b>がある可能性が高い。</li>
  <li><b>課題 Z</b>: 24 件の池の緯度経度を地理院地図で確認し、
      L36-L40 (LiDAR 標高) で<b>下流地形の存在</b>を判定。下流に住宅があれば
      三次・庄原両市役所と県農林水産局に SHP 整備を申請する公開資料を作成。
      さらに<b>「指定済みだが SHP 無し」のメタ問題</b>を県全体で他市町にも拡張調査。</li>
</ul>

<h3>課題 5: ため池規模分布のべき乗則 vs 対数正規 統計検定</h3>
<ul class="kv">
  <li><b>結果 X</b>: 貯水量分布は対数正規様で 6 桁レンジ。OLS log-log 傾き b1 = {b1:.2f}。</li>
  <li><b>新仮説 Y</b>: ため池貯水量分布は<b>対数正規分布</b>に従う
      (人間が築造した小池の分散 + 大池の少数 = 中心極限定理の対数版)。
      log-log 散布の傾き b1 = {b1:.2f} は<b>地形面積 ∝ 体積^(2/3)</b>
      の幾何的法則 (= 浸水面積 ∝ 貯水量^(2/3) ≈ 0.67) に近い。</li>
  <li><b>課題 Z</b>: <code>scipy.stats.lognorm.fit</code> で対数正規パラメータを推定し、
      Kolmogorov-Smirnov 検定で当てはまりを評価。べき乗則
      (Pareto 分布) との比較も行い、ため池規模分布の<b>普遍的法則</b>を考察する。</li>
</ul>
"""))

html = render_lesson(
    num=45,
    title="L45 ため池浸水想定区域 2 件統合分析 — 広島県の防災重点ため池決壊リスクを「規模属性 × 浸水範囲」で読み解く",
    tags=["ため池", "決壊シナリオ", "防災重点ため池", "西日本豪雨", "農業用ため池法",
          "geopandas", "Shapefile", "中山間"],
    time="60〜90 分",
    level="リテラシ〜中級 (geopandas/CSV-Shapefile 統合/log-log スケーリング)",
    data_label="DoBoX dataset 62, 63 (ため池基本情報 + ため池浸水想定区域情報) — CSV 1.1 MB + Shapefile 81 MB",
    sections=sections,
    script_filename="L45_pond_inundation.py",
)

out_html = LESSONS / "L45_pond_inundation.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=== L45 done in {time.time()-t_all:.1f}s ===", flush=True)
