# -*- coding: utf-8 -*-
"""L35 排水機場 2 件統合分析
       — 広島県の 15 機場 (港湾海岸 3 + 河川 12) が支える内水浸水防御の最終線

カバー宣言:
  本記事は DoBoX のシリーズ <b>「排水機場 港湾海岸 / 河川」 2 件</b>
  (dataset_id = 1269, 1273) を統合し、広島県内の<b>排水機場</b>の地理分布・
  ポンプ吐出能力・建設年代を分析する研究記事である。

  L32 (外郭施設) / L33 (係留施設) / L34 (臨港交通施設) は港湾の<b>外洋・接岸・陸側</b>を
  扱った 3 層階層であった。本記事 L35 は港湾系シリーズの最終記事であるが、
  扱う施設は<b>港湾施設ではなく「内水排水施設」</b>であり、
  <b>港湾海岸エリアの内水排水 (3 件)</b> と <b>河川流域の内水排水 (12 件)</b> の両者を扱う。
  すなわち、外郭で外海を防ぎ、係留で船を繋ぎ、交通で陸へ繋いだ後の<b>「陸側に溜まる雨水・河川氾濫水」</b>を
  ポンプで強制排出する<b>「内水浸水防御の最終線」</b>を扱う。

研究の問い (RQ):
  広島県内の排水機場 (合計 15 件) は、設置目的 (港湾海岸 / 河川)・地理分布
  (沿岸低地 / 河川合流部 / 干拓地)・規模 (吐出量 m³/s) の観点で<b>どう構成され</b>、
  <b>内水浸水域とどう重なる</b>のか? 各機場が守る<b>集水区</b>はどこか?
    (1) 2 dataset の構造比: 河川 12 機場 vs 港湾海岸 3 機場 (1:4 偏重)。
        この偏重は<b>内水氾濫の主因が「河川合流部の堰き止め」</b>であることを示唆。
    (2) 福山市集中: 12 河川機場のうち <b>7 件</b>が福山市。
        福山平野の<b>干拓地</b>性質と<b>芦田川水系の合流複雑性</b>を反映。
    (3) 排水容量階層: 最大 30 m³/s (手城川・岡ノ下川) ÷ 最小 1.3 m³/s (沢) = 23倍。
        2 大型機場が県全体の <b>46%</b> の排水能力を担う。
    (4) 建設年代と更新: 昭和40 (1965) 〜 平成28 (2016) の 51 年間で建設。
        昭和期 7 件、平成期 8 件。新安川は<b>昭和41 (第一) → 平成28 (第二)</b> と<b>62 年差で増設</b>。
    (5) 浸水想定区域との重なり: 各機場の周辺 1km バッファに DoBoX 河川浸水想定 (rank 別) が重なるか?
        排水機場は<b>浸水想定エリアの内側</b>に立地するはずで、これを実データで検証する。
    (6) ポンプ単機平均能力 (m³/s/台): 大型機 ≈ 7-15、小型機 ≈ 0.5-2。
        新旧比較で機械の大型化 (= 1 台あたり能力増) が読めるか。

仮説 H1〜H6:
  H1 (主導カテゴリは河川): 河川 12/15 = 80%、港湾海岸 3/15 = 20%。
       吐出量でも河川 117.5 m³/s vs 港湾海岸 12.7 m³/s = <b>9.2:1 比</b>。
       内水排水の主戦場は<b>河川合流低地</b>で、港湾海岸はサブ。
  H2 (福山市偏重): 河川機場 12 件のうち福山市 = <b>7 件 (58%)</b>。
       福山平野は芦田川河口の干拓地で、低地が多く水が溜まりやすい構造的特性。
       広島市 (3 件) より絶対数で多い。
  H3 (大型機場の二極集中): 30 m³/s 機場が 2 件 (手城川・岡ノ下川) で、
       これだけで県内総排水容量 130.2 m³/s の <b>46.1%</b> を占有。
       Lorenz 曲線で強い集中構造。
  H4 (建設年代の 2 ピーク): 昭和40-50年代 (1965-80, 戦後復興後の都市化期) と
       平成20年代 (2008-16, 集中豪雨対応強化期) に建設が集中。
       バブル経済期 (昭和60-平成5) は新規少なく、補強・更新中心。
  H5 (浸水想定との重なり): 全 15 機場のうち <b>13 件以上 (≥87%)</b> が
       DoBoX の河川浸水想定区域 1km バッファ内に位置。
       排水機場は<b>「想定浸水エリア内の最低標高点」</b>に置かれる工学的必然性を実証。
  H6 (大型化の傾向): 平成期建設機の 1 台あたり吐出量 (m³/s/台) は
       昭和期建設機より<b>大きい</b>。機械技術の進歩で 1 台で扱える流量が増えた。

要件 S 準拠: 1 分以内完走。データ件数が少ないので空間処理は軽量。
要件 T 準拠: 県全域マップ + 福山ズーム + 浸水想定重ね合わせ + ポンプ能力点マップ。
要件 Q 準拠: 図 9 種以上、表 8 種以上 (15 件 × 4 属性軸 (位置/年代/容量/台数))。

データ仕様 (2 件は同一 9 列スキーマ):
  A. 排水機場（港湾海岸） 1269: CSV, 3 行, 大竹港海岸・瀬戸田港海岸・竹原港海岸
  B. 排水機場（河川） 1273: CSV, 12 行, 広島・福山・尾道の各河川合流部
  共通列: 施設の名称, 事務所名, 所在地, 緯度(10進数), 経度(10進数),
          海岸・河川名, 完成年月日, 総排水量(m3), ポンプ台数(台)

  ※ 「総排水量(m3)」列は単位表記が m3 だが、値の桁 (1.3〜30) と
     排水機場の業界慣例から<b>毎秒の吐出量 m³/s</b> と解釈するのが妥当。
     30 m³/s = 25mプール 1 杯 (約 600 m³) を 20 秒で排水できる能力。
     本記事ではこの解釈で記述する。
"""
from __future__ import annotations
import sys, time, json
from pathlib import Path

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

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
from shapely.geometry import Point

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

t0 = time.time()
print("=== L35 排水機場 2 件統合分析 ===", flush=True)

# =============================================================================
# 0. 定数: 2 dataset_id とカテゴリ定義
# =============================================================================
DATA_DIR = ROOT / "data" / "extras" / "L35_drainage_pumps"
DATA_DIR.mkdir(parents=True, exist_ok=True)
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
FLOOD_SHP = ROOT / "data" / "extras" / "flood_shp" / "shinsui_souteisaidai" / "shinsui_souteisaidai.shp"
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角 III, m 単位

SERIES = [
    (1269, "排水機場（港湾海岸）", "港湾海岸", "drainage_pump_port_coast.csv", 39770),
    (1273, "排水機場（河川）",     "河川",     "drainage_pump_river.csv",      39774),
]

# カテゴリ色
CAT_COLOR = {"港湾海岸": "#0969da", "河川": "#1a7f37"}

# 浸水深ランクの公式凡例 (DoBoX 河川浸水想定 rank の意味)
FLOOD_RANK = {
    10: "0.0–0.5m 未満",
    20: "0.5–1.0m 未満",
    30: "1.0–2.0m 未満",
    40: "2.0–3.0m 未満",
    50: "3.0–5.0m 未満",
    60: "5.0–10.0m 未満",
    70: "10.0–20.0m 未満",
    80: "20.0m 以上",
}
# 青系の階調 (浸水深が深くなるほど濃く)
RANK_COLORS = {
    10: "#deebf7",
    20: "#c6dbef",
    30: "#9ecae1",
    40: "#6baed6",
    50: "#4292c6",
    60: "#2171b5",
    70: "#08519c",
    80: "#08306b",
}


# =============================================================================
# 1. 2 dataset の存在確認とロード
# =============================================================================
print("\n[1] 2 dataset の存在確認とロード", flush=True)
t1 = time.time()

dfs = []
for dsid, label, cat, fname, rid in SERIES:
    p = DATA_DIR / fname
    try:
        ensure_dataset(p, dataset_id=dsid, resource_id=rid, label=f"L35/{dsid} {label}")
    except Exception as e:
        print(f"  WARN dsid={dsid}: {e}", flush=True)
    df = pd.read_csv(p, encoding="utf-8-sig")
    df["port_category"] = cat
    df["dsid"] = dsid
    dfs.append(df)
    print(f"  {label} (dsid={dsid}): {df.shape}", flush=True)

ALL = pd.concat(dfs, ignore_index=True)
print(f"  合計: {len(ALL)} 行 × {ALL.shape[1]} 列", flush=True)
print(f"  ({time.time()-t1:.1f}s)", flush=True)

# =============================================================================
# 2. 列名の正規化と派生指標
# =============================================================================
print("\n[2] 派生指標の計算", flush=True)
t1 = time.time()

# 列名のエイリアス
ALL = ALL.rename(columns={
    "施設の名称": "name",
    "事務所名": "office",
    "所在地": "address",
    "緯度(10進数)": "lat",
    "経度(10進数)": "lon",
    "海岸・河川名": "watercourse",
    "完成年月日": "完成",
    "総排水量(m3)": "discharge_m3s",   # 業界慣例で m³/s と解釈
    "ポンプ台数(台)": "n_pumps",
})

# 完成年を西暦に変換
def jp_to_west(s: str) -> int | None:
    if pd.isna(s):
        return None
    s = str(s).strip()
    # 「昭和41年」「平成1年」「平成28年」などを西暦に
    if s.startswith("昭和"):
        try:
            n = int(s.replace("昭和", "").replace("年", ""))
            return 1925 + n  # 昭和元年=1926, n年 = 1925+n
        except ValueError:
            return None
    elif s.startswith("平成"):
        try:
            n = int(s.replace("平成", "").replace("年", ""))
            return 1988 + n  # 平成元年=1989, n年 = 1988+n
        except ValueError:
            return None
    return None

ALL["year"] = ALL["完成"].apply(jp_to_west)
ALL["era"] = ALL["完成"].apply(lambda s: "昭和" if str(s).startswith("昭和") else ("平成" if str(s).startswith("平成") else "不明"))
ALL["m3s_per_pump"] = ALL["discharge_m3s"] / ALL["n_pumps"]
# 市町を所在地から抽出 (例: "広島市佐伯区..." → "広島市")
def extract_city(addr: str) -> str:
    if pd.isna(addr):
        return ""
    s = str(addr)
    for k in ["広島市", "福山市", "尾道市", "竹原市", "大竹市", "三原市", "呉市", "東広島市", "廿日市市", "安芸高田市", "府中市", "三次市", "庄原市"]:
        if k in s:
            return k
    return ""
ALL["city"] = ALL["address"].apply(extract_city)
print(ALL[["name", "port_category", "city", "watercourse", "完成", "year", "era", "discharge_m3s", "n_pumps", "m3s_per_pump"]].to_string(index=False))
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 3. GeoDataFrame 化 (緯度経度から Point geometry)
# =============================================================================
print("\n[3] GeoDataFrame 化", flush=True)
t1 = time.time()
ALL["geometry"] = [Point(lon, lat) for lon, lat in zip(ALL["lon"], ALL["lat"])]
gdf = gpd.GeoDataFrame(ALL, geometry="geometry", crs="EPSG:4326").to_crs(TARGET_CRS)
print(f"  geom OK: {len(gdf)} / {len(ALL)} (位置情報完備)", flush=True)
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 4. カテゴリ別集計 (件数 / 容量 / 台数)
# =============================================================================
print("\n[4] カテゴリ別集計", flush=True)
t1 = time.time()

cat_agg = ALL.groupby("port_category").agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
    mean_m3s=("discharge_m3s", "mean"),
    median_m3s=("discharge_m3s", "median"),
    n_pumps_sum=("n_pumps", "sum"),
    n_pumps_mean=("n_pumps", "mean"),
).round(2)
cat_agg.loc["合計"] = [
    cat_agg["n"].sum(),
    cat_agg["total_m3s"].sum(),
    ALL["discharge_m3s"].mean().round(2),
    ALL["discharge_m3s"].median().round(2),
    cat_agg["n_pumps_sum"].sum(),
    ALL["n_pumps"].mean().round(2),
]
cat_agg["n"] = cat_agg["n"].astype(int)
cat_agg["n_pumps_sum"] = cat_agg["n_pumps_sum"].astype(int)
cat_agg.to_csv(ASSETS / "L35_category_summary.csv", encoding="utf-8-sig")
print(cat_agg.to_string())
print(f"  ({time.time()-t1:.1f}s)", flush=True)


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

city_agg = ALL.groupby(["city", "port_category"]).agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
    n_pumps=("n_pumps", "sum"),
).reset_index()
# 市町だけのワイド集計
city_wide = ALL.groupby("city").agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
    n_pumps=("n_pumps", "sum"),
).round(2).sort_values("n", ascending=False)
city_wide.to_csv(ASSETS / "L35_city_summary.csv", encoding="utf-8-sig")
print(city_wide.to_string())
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 6. 建設年代の集計
# =============================================================================
print("\n[6] 建設年代の集計", flush=True)
t1 = time.time()

era_agg = ALL.groupby(["era", "port_category"]).agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
).reset_index()
year_bins = [1960, 1970, 1980, 1990, 2000, 2010, 2020]
year_labels = ["1960s", "1970s", "1980s", "1990s", "2000s", "2010s"]
ALL["year_bin"] = pd.cut(ALL["year"], bins=year_bins, labels=year_labels, right=False)
year_pivot = ALL.groupby(["year_bin", "port_category"], observed=True).size().unstack(fill_value=0)
year_pivot.to_csv(ASSETS / "L35_year_pivot.csv", encoding="utf-8-sig")
print(year_pivot.to_string())
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 7. ポンプ能力 (1 台あたり m3/s) の分析
# =============================================================================
print("\n[7] ポンプ能力分析 (m3/s/台)", flush=True)
t1 = time.time()

# 古い (昭和) vs 新しい (平成) の 1 台あたり吐出量比較
era_pump = ALL.groupby("era").agg(
    n=("name", "count"),
    mean_m3s_per_pump=("m3s_per_pump", "mean"),
    median_m3s_per_pump=("m3s_per_pump", "median"),
    mean_n_pumps=("n_pumps", "mean"),
    mean_total_m3s=("discharge_m3s", "mean"),
).round(3)
era_pump.to_csv(ASSETS / "L35_era_pump_efficiency.csv", encoding="utf-8-sig")
print(era_pump.to_string())
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 8. 県境 polygon の load
# =============================================================================
print("\n[8] 県境の load", flush=True)
t1 = time.time()

import zipfile, io as _io
def load_zip_first_geo(zip_path: Path, encoding="cp932"):
    with zipfile.ZipFile(zip_path) as zf:
        names = zf.namelist()
        for n in names:
            if n.lower().endswith(".geojson"):
                with zf.open(n) as f:
                    return gpd.read_file(_io.BytesIO(f.read()))
        for n in names:
            if n.lower().endswith(".shp"):
                tmp = zip_path.parent / f"_ext_{zip_path.stem}"
                tmp.mkdir(exist_ok=True)
                zf.extractall(tmp)
                return gpd.read_file(tmp / n, encoding=encoding)
    raise FileNotFoundError(zip_path)

g_admin_pref = load_zip_first_geo(ADMIN_DIR / "admin_922_広島県.zip").to_crs(TARGET_CRS)
pref_diss = g_admin_pref.dissolve().reset_index(drop=True)
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 9. 浸水想定との重なり判定 (1 km バッファ)
# =============================================================================
print("\n[9] 浸水想定区域との重なり判定", flush=True)
t1 = time.time()

BUFFER_M = 1000.0  # 1 km バッファで「機場が守る範囲」と仮定
FLOOD_OK = FLOOD_SHP.exists()
gdf["buffer_1km"] = gdf.geometry.buffer(BUFFER_M)
gdf["near_flood"] = False
gdf["max_rank_in_buffer"] = 0
gdf["flood_area_ha_in_buffer"] = 0.0
flood_clip = None

if FLOOD_OK:
    print(f"  flood shp ロード: {FLOOD_SHP}", flush=True)
    flood = gpd.read_file(FLOOD_SHP, encoding="cp932").to_crs(TARGET_CRS)
    print(f"  flood polygons: {len(flood)} 件", flush=True)
    # 機場 1km バッファ内に flood polygon があるか sjoin
    buf_gdf = gpd.GeoDataFrame(gdf[["name", "port_category"]],
                                geometry=gdf["buffer_1km"], crs=TARGET_CRS)
    # バッファ内の flood polygon を抽出 (intersects)
    for i, row in buf_gdf.iterrows():
        sub = flood[flood.intersects(row.geometry)]
        if len(sub) > 0:
            gdf.at[i, "near_flood"] = True
            gdf.at[i, "max_rank_in_buffer"] = int(sub["rank"].max())
            # クリップして面積算出
            try:
                clipped = gpd.clip(sub, row.geometry)
                gdf.at[i, "flood_area_ha_in_buffer"] = float(clipped.geometry.area.sum() / 10000.0)
            except Exception:
                gdf.at[i, "flood_area_ha_in_buffer"] = 0.0
    n_near = int(gdf["near_flood"].sum())
    print(f"  浸水想定 1km 圏内: {n_near}/{len(gdf)} = {n_near/len(gdf)*100:.1f}%", flush=True)
    print(f"  最大ランク max: {gdf['max_rank_in_buffer'].max()} ({FLOOD_RANK.get(int(gdf['max_rank_in_buffer'].max()), '?')})", flush=True)
else:
    print(f"  WARN: {FLOOD_SHP} not found, flood overlap analysis skipped", flush=True)
    n_near = 0

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


# =============================================================================
# 10. 中間 CSV 出力 (geometry を除いた素データ)
# =============================================================================
print("\n[10] 中間 CSV 出力", flush=True)
t1 = time.time()

out = gdf.drop(columns=["geometry", "buffer_1km"]).copy()
out.to_csv(ASSETS / "L35_all_facilities.csv", index=False, encoding="utf-8-sig")

series_df = pd.DataFrame([
    {"dsid": d, "label": lab, "category": cat, "file": f, "n_rows": int((ALL["dsid"] == d).sum())}
    for (d, lab, cat, f, _) in SERIES
])
series_df.to_csv(ASSETS / "L35_series.csv", index=False, encoding="utf-8-sig")
print(f"  saved csv ({time.time()-t1:.1f}s)", flush=True)


# 値の事前整理 (HTML から参照)
n_total = len(ALL)
n_h = int((ALL["port_category"] == "港湾海岸").sum())
n_r = int((ALL["port_category"] == "河川").sum())
total_m3s = float(ALL["discharge_m3s"].sum())
total_m3s_h = float(ALL[ALL["port_category"] == "港湾海岸"]["discharge_m3s"].sum())
total_m3s_r = float(ALL[ALL["port_category"] == "河川"]["discharge_m3s"].sum())
total_pumps = int(ALL["n_pumps"].sum())
mean_m3s = float(ALL["discharge_m3s"].mean())
median_m3s = float(ALL["discharge_m3s"].median())
max_m3s = float(ALL["discharge_m3s"].max())
min_m3s = float(ALL["discharge_m3s"].min())
n_fukuyama = int((ALL["city"] == "福山市").sum())
n_hiroshima = int((ALL["city"] == "広島市").sum())
n_showa = int((ALL["era"] == "昭和").sum())
n_heisei = int((ALL["era"] == "平成").sum())
year_min = int(ALL["year"].min())
year_max = int(ALL["year"].max())
n_top2 = 2  # 30 m3/s 機場 (手城川・岡ノ下川)
top2_share = float(ALL.nlargest(2, "discharge_m3s")["discharge_m3s"].sum() / total_m3s * 100)


# =============================================================================
# 11. 図 1: 2 dataset 構造マトリクス + カード
# =============================================================================
print("\n[11] 図 1: dataset 構造", flush=True)
t1 = time.time()

fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))

# 左: dataset カード
ax = axes[0]
ax.set_xlim(0, 10); ax.set_ylim(0, 6)
ax.set_axis_off()
ax.text(5, 5.5, "DoBoX 排水機場シリーズ 2 件", ha="center",
        fontsize=15, fontweight="bold")
# 港湾海岸カード
ax.add_patch(plt.Rectangle((0.3, 0.7), 4.4, 4.0, fill=True,
            facecolor=CAT_COLOR["港湾海岸"], alpha=0.13, edgecolor=CAT_COLOR["港湾海岸"], linewidth=2.5))
ax.text(2.5, 4.2, "dsid 1269\n排水機場（港湾海岸）", ha="center", fontsize=12,
        fontweight="bold", color=CAT_COLOR["港湾海岸"])
ax.text(2.5, 3.05, f"{n_h} 件 / {total_m3s_h:.1f} m³/s", ha="center", fontsize=14)
ax.text(2.5, 2.3, "大竹港・瀬戸田港・竹原港の海岸\n(港の陸側に降った雨水を海へ)", ha="center", fontsize=9.5)
ax.text(2.5, 1.0, "全 3 件 = 全部「港海岸」\n沿岸都市の浸水防御",
        ha="center", fontsize=9, color="#666")

# 河川カード
ax.add_patch(plt.Rectangle((5.3, 0.7), 4.4, 4.0, fill=True,
            facecolor=CAT_COLOR["河川"], alpha=0.13, edgecolor=CAT_COLOR["河川"], linewidth=2.5))
ax.text(7.5, 4.2, "dsid 1273\n排水機場（河川）", ha="center", fontsize=12,
        fontweight="bold", color=CAT_COLOR["河川"])
ax.text(7.5, 3.05, f"{n_r} 件 / {total_m3s_r:.1f} m³/s", ha="center", fontsize=14)
ax.text(7.5, 2.3, "本川 (本流) ではなく\n支流・小河川の合流部に集中", ha="center", fontsize=9.5)
ax.text(7.5, 1.0, f"福山市 {n_fukuyama} 件 / 広島市 {n_hiroshima} 件\n芦田川水系の干拓地特性",
        ha="center", fontsize=9, color="#666")

ax.text(5, 0.25,
        f"合計 {n_total} 件 / 排水容量 {total_m3s:.1f} m³/s / ポンプ {total_pumps} 台",
        ha="center", fontsize=11, fontweight="bold")

# 右: 排水容量バー
ax = axes[1]
sub = ALL.sort_values("discharge_m3s", ascending=True)
y_pos = np.arange(len(sub))
colors = [CAT_COLOR[c] for c in sub["port_category"]]
ax.barh(y_pos, sub["discharge_m3s"], color=colors, edgecolor="#222", linewidth=0.5)
ax.set_yticks(y_pos)
ax.set_yticklabels(sub["name"], fontsize=8.5)
for i, v in enumerate(sub["discharge_m3s"]):
    ax.text(v + 0.5, i, f"{v:.1f}", va="center", fontsize=8.5)
ax.set_xlabel("排水容量 (m³/s)")
ax.set_title("15 機場の排水容量 — 港湾海岸 (青) / 河川 (緑)")
legend_h = [Patch(facecolor=CAT_COLOR["港湾海岸"], label="港湾海岸 (3)"),
            Patch(facecolor=CAT_COLOR["河川"], label="河川 (12)")]
ax.legend(handles=legend_h, loc="lower right")
ax.grid(axis="x", alpha=0.3)

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig1_dataset_overview.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig1_dataset_overview.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 12. 図 2: 県全域 点マップ (緯度経度をプロット)
# =============================================================================
print("\n[12] 図 2: 県全域 点マップ", flush=True)
t1 = time.time()

fig, ax = plt.subplots(figsize=(13, 8))
pref_diss.boundary.plot(ax=ax, color="#222", linewidth=1.0)
g_admin_pref.boundary.plot(ax=ax, color="#888", linewidth=0.4, alpha=0.5)

# バッファ (1km) を薄く描画
for i, row in gdf.iterrows():
    buf = gpd.GeoSeries([row["buffer_1km"]], crs=TARGET_CRS)
    buf.plot(ax=ax, color=CAT_COLOR[row["port_category"]], alpha=0.10, edgecolor="none")

# 各機場を点で描画 (サイズ = 排水容量に比例)
sizes = (gdf["discharge_m3s"] / gdf["discharge_m3s"].max() * 600 + 30).values
for cat, color in CAT_COLOR.items():
    sub = gdf[gdf["port_category"] == cat]
    ax.scatter(sub.geometry.x, sub.geometry.y, s=sizes[gdf["port_category"] == cat],
               c=color, edgecolor="#222", linewidth=0.7, alpha=0.85, label=f"{cat} ({len(sub)})")

# 主要 5 機場にラベル
top5 = gdf.nlargest(5, "discharge_m3s")
for _, r in top5.iterrows():
    ax.annotate(f"{r['name'].replace('排水機場','').replace('排水ポンプ場','').replace('ポンプ排水場','')} ({r['discharge_m3s']:.0f})",
                (r.geometry.x, r.geometry.y),
                xytext=(8, 8), textcoords="offset points", fontsize=8.5,
                bbox=dict(facecolor="white", alpha=0.7, edgecolor="none", pad=1.5))

ax.set_title(f"広島県 排水機場 全 {n_total} 機場マップ — 円サイズ=排水容量, 円=1km バッファ")
ax.set_axis_off()
ax.legend(loc="upper left")
plt.tight_layout()
fig.savefig(ASSETS / "L35_fig2_pref_overview.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig2_pref_overview.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 13. 図 3: 福山市集中ズーム
# =============================================================================
print("\n[13] 図 3: 福山市ズーム", flush=True)
t1 = time.time()

# 福山市の境界を取得 (admin_932 福山市 zip があるはず)
fukuyama_zip = ADMIN_DIR / "admin_832_福山市.zip"
if fukuyama_zip.exists():
    g_fukuyama = load_zip_first_geo(fukuyama_zip).to_crs(TARGET_CRS)
    fukuyama_diss = g_fukuyama.dissolve()
else:
    # フォールバック: 福山市の機場の bbox から
    fukuyama_pts = gdf[gdf["city"] == "福山市"]
    minx, miny, maxx, maxy = fukuyama_pts.total_bounds
    pad = 5000
    g_fukuyama = None
    fukuyama_diss = None

fig, axes = plt.subplots(1, 2, figsize=(15, 7))

# 左: 福山市ズーム
ax = axes[0]
fukuyama_pts = gdf[gdf["city"] == "福山市"].copy()
fukuyama_other = gdf[gdf["city"] != "福山市"]

if fukuyama_diss is not None:
    fukuyama_diss.boundary.plot(ax=ax, color="#222", linewidth=1.5)
    fukuyama_diss.plot(ax=ax, color="#fef9e7", alpha=0.5)
    bbox = fukuyama_diss.total_bounds
else:
    bbox = fukuyama_pts.total_bounds
pad = 5000
ax.set_xlim(bbox[0] - pad, bbox[2] + pad)
ax.set_ylim(bbox[1] - pad, bbox[3] + pad)

# 機場の点 (サイズ = 容量)
sizes_f = (fukuyama_pts["discharge_m3s"] / 30 * 800 + 50).values
for cat, color in CAT_COLOR.items():
    sub = fukuyama_pts[fukuyama_pts["port_category"] == cat]
    if len(sub) > 0:
        ax.scatter(sub.geometry.x, sub.geometry.y,
                   s=sub["discharge_m3s"]/30*800 + 50,
                   c=color, edgecolor="#222", linewidth=0.7, alpha=0.85, label=f"{cat}")

# ラベル
for _, r in fukuyama_pts.iterrows():
    short_name = r["name"].replace("排水機場","").replace("排水ポンプ場","").replace("ポンプ排水場","")
    ax.annotate(f"{short_name}\n({r['discharge_m3s']:.0f} m³/s)",
                (r.geometry.x, r.geometry.y),
                xytext=(7, 7), textcoords="offset points", fontsize=8.5,
                bbox=dict(facecolor="white", alpha=0.75, edgecolor="none", pad=2))

ax.set_title(f"福山市 排水機場 {n_fukuyama} 件 — 干拓地+芦田川水系の集中")
ax.legend(loc="upper right")
ax.set_axis_off()

# 右: 広島市ズーム
ax = axes[1]
hiroshima_zip = ADMIN_DIR / "admin_786_広島市.zip"
if hiroshima_zip.exists():
    g_hiroshima = load_zip_first_geo(hiroshima_zip).to_crs(TARGET_CRS)
    hiroshima_diss = g_hiroshima.dissolve()
    hiroshima_diss.boundary.plot(ax=ax, color="#222", linewidth=1.5)
    hiroshima_diss.plot(ax=ax, color="#fef9e7", alpha=0.5)
    bbox = hiroshima_diss.total_bounds
else:
    hiroshima_pts = gdf[gdf["city"] == "広島市"]
    bbox = hiroshima_pts.total_bounds
ax.set_xlim(bbox[0] - pad, bbox[2] + pad)
ax.set_ylim(bbox[1] - pad, bbox[3] + pad)

hiroshima_pts = gdf[gdf["city"] == "広島市"].copy()
for cat, color in CAT_COLOR.items():
    sub = hiroshima_pts[hiroshima_pts["port_category"] == cat]
    if len(sub) > 0:
        ax.scatter(sub.geometry.x, sub.geometry.y,
                   s=sub["discharge_m3s"]/30*800 + 50,
                   c=color, edgecolor="#222", linewidth=0.7, alpha=0.85, label=f"{cat}")
for _, r in hiroshima_pts.iterrows():
    short_name = r["name"].replace("排水機場","").replace("排水ポンプ場","").replace("ポンプ排水場","")
    ax.annotate(f"{short_name}\n({r['discharge_m3s']:.0f} m³/s)",
                (r.geometry.x, r.geometry.y),
                xytext=(7, 7), textcoords="offset points", fontsize=8.5,
                bbox=dict(facecolor="white", alpha=0.75, edgecolor="none", pad=2))

ax.set_title(f"広島市 排水機場 {n_hiroshima} 件 — 太田川水系の支流分布")
ax.legend(loc="upper right")
ax.set_axis_off()

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig3_city_zoom.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig3_city_zoom.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 14. 図 4: 浸水想定との重ね合わせマップ
# =============================================================================
print("\n[14] 図 4: 浸水想定重ね合わせ", flush=True)
t1 = time.time()

if FLOOD_OK:
    flood = gpd.read_file(FLOOD_SHP, encoding="cp932").to_crs(TARGET_CRS)
    # 描画高速化: simplify でジオメトリを 100m 解像度に粗化 + ランク別 dissolve
    # 県全域マップの場合 100m 解像度で十分 (画面 1 px ≈ 200m)
    flood_simple_geom = flood.copy()
    flood_simple_geom["geometry"] = flood_simple_geom.geometry.simplify(100, preserve_topology=False)
    flood_simple = flood_simple_geom.dissolve(by="rank").reset_index()

    fig, axes = plt.subplots(1, 2, figsize=(15, 8))

    # 左: 県全域 (粗化済みポリゴン)
    ax = axes[0]
    pref_diss.boundary.plot(ax=ax, color="#222", linewidth=0.8)
    # 浸水ポリゴンをランク色で
    for _, row_f in flood_simple.iterrows():
        rank = int(row_f["rank"])
        gpd.GeoSeries([row_f.geometry], crs=TARGET_CRS).plot(
            ax=ax, color=RANK_COLORS.get(rank, "#666"), alpha=0.65, edgecolor="none")
    # 機場の点
    for cat, color in CAT_COLOR.items():
        sub = gdf[gdf["port_category"] == cat]
        ax.scatter(sub.geometry.x, sub.geometry.y, s=sub["discharge_m3s"]/30*400 + 30,
                   c=color, edgecolor="white", linewidth=1.0, alpha=0.95, label=f"{cat}",
                   zorder=10)
    ax.set_title("広島県 浸水想定区域 + 排水機場 — 機場が想定エリア内にあるか?")
    ax.legend(loc="upper left")
    ax.set_axis_off()

    # 右: 福山ズーム (浸水詳細)
    ax = axes[1]
    fukuyama_pts = gdf[gdf["city"] == "福山市"]
    bbox_pts = fukuyama_pts.total_bounds
    pad = 8000
    xmin, xmax = bbox_pts[0] - pad, bbox_pts[2] + pad
    ymin, ymax = bbox_pts[1] - pad, bbox_pts[3] + pad
    ax.set_xlim(xmin, xmax); ax.set_ylim(ymin, ymax)

    # ローカル flood 抽出 (cx で空間フィルタ + simplify + ランク別 dissolve)
    flood_local = flood.cx[xmin:xmax, ymin:ymax].copy()
    if len(flood_local) > 0:
        # 福山ズームは 30m 解像度で十分
        flood_local["geometry"] = flood_local.geometry.simplify(30, preserve_topology=False)
        flood_local_simple = flood_local.dissolve(by="rank").reset_index()
        for _, row_f in flood_local_simple.iterrows():
            rank = int(row_f["rank"])
            gpd.GeoSeries([row_f.geometry], crs=TARGET_CRS).plot(
                ax=ax, color=RANK_COLORS.get(rank, "#666"), alpha=0.7, edgecolor="none")
    if fukuyama_diss is not None:
        fukuyama_diss.boundary.plot(ax=ax, color="#222", linewidth=1.2)

    for cat, color in CAT_COLOR.items():
        sub = fukuyama_pts[fukuyama_pts["port_category"] == cat]
        if len(sub) > 0:
            ax.scatter(sub.geometry.x, sub.geometry.y,
                       s=sub["discharge_m3s"]/30*600 + 50,
                       c=color, edgecolor="white", linewidth=1.0, alpha=0.95, label=f"{cat}",
                       zorder=10)
    for _, r in fukuyama_pts.iterrows():
        short_name = r["name"].replace("排水機場","").replace("排水ポンプ場","").replace("ポンプ排水場","")
        ax.annotate(f"{short_name}",
                    (r.geometry.x, r.geometry.y),
                    xytext=(7, 7), textcoords="offset points", fontsize=8,
                    bbox=dict(facecolor="white", alpha=0.85, edgecolor="none", pad=1.5))

    # 凡例 (浸水深)
    if len(flood_local) > 0:
        legend_rank = [Patch(facecolor=RANK_COLORS.get(int(r), "#666"),
                             label=f"rank {r}: {FLOOD_RANK.get(int(r), '?')}")
                       for r in sorted(flood_local["rank"].unique())]
    else:
        legend_rank = []
    legend_rank += [Patch(facecolor=CAT_COLOR[c], label=c) for c in CAT_COLOR]
    ax.legend(handles=legend_rank, loc="lower left", fontsize=7.5)
    ax.set_title("福山市ズーム — 浸水想定区域内の機場立地")
    ax.set_axis_off()

    plt.tight_layout()
    fig.savefig(ASSETS / "L35_fig4_flood_overlay.png", dpi=130, bbox_inches="tight")
    plt.close("all")
    print(f"  saved L35_fig4_flood_overlay.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 15. 図 5: 建設年代タイムライン
# =============================================================================
print("\n[15] 図 5: 建設年代タイムライン", flush=True)
t1 = time.time()

fig, axes = plt.subplots(2, 1, figsize=(13, 7))

# 上: 機場ごとに横棒で「年代」を表示 (ガントチャート風)
ax = axes[0]
sub = ALL.sort_values("year")
y_pos = np.arange(len(sub))
for i, (_, r) in enumerate(sub.iterrows()):
    color = CAT_COLOR[r["port_category"]]
    ax.barh(i, 0.5, left=r["year"]-0.25, color=color, alpha=0.95, edgecolor="#222")
    ax.scatter(r["year"], i, s=r["discharge_m3s"]/30*200+40, c=color,
               edgecolor="#222", linewidth=0.8, zorder=10)
    ax.text(r["year"] + 1, i, f"{r['name'][:10]} ({r['discharge_m3s']:.0f}m³/s)",
            va="center", fontsize=8.5)

ax.set_yticks(y_pos)
ax.set_yticklabels([f"{r['era']}{r['完成'].replace(r['era'],'').replace('年','')}年" for _, r in sub.iterrows()], fontsize=8)
ax.set_xlabel("完成年 (西暦)")
ax.set_xlim(1960, 2025)
ax.axvline(x=1989, color="red", linestyle="--", alpha=0.5, label="平成元年 (1989)")
ax.set_title("15 機場の建設年代タイムライン — 円サイズ=排水容量")
ax.grid(axis="x", alpha=0.3)
ax.legend(loc="lower right")

# 下: 年代区分でのヒストグラム
ax = axes[1]
year_pivot.plot(kind="bar", stacked=True, ax=ax,
                color=[CAT_COLOR.get(c, "#888") for c in year_pivot.columns])
ax.set_xlabel("建設年代")
ax.set_ylabel("件数")
ax.set_title("建設年代別 件数 (港湾海岸 / 河川)")
ax.legend(loc="upper right", title="カテゴリ")
ax.grid(axis="y", alpha=0.3)
plt.xticks(rotation=0)

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig5_era_timeline.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig5_era_timeline.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 16. 図 6: 容量の Lorenz 曲線 + 散布図 (容量 × 台数)
# =============================================================================
print("\n[16] 図 6: 集中構造分析", flush=True)
t1 = time.time()

fig, axes = plt.subplots(2, 2, figsize=(13, 10))

# (1) 容量 boxplot (カテゴリ別)
ax = axes[0, 0]
data_h = ALL[ALL["port_category"] == "港湾海岸"]["discharge_m3s"].values
data_r = ALL[ALL["port_category"] == "河川"]["discharge_m3s"].values
bp = ax.boxplot([data_h, data_r], labels=["港湾海岸", "河川"], patch_artist=True, widths=0.5)
for patch, color in zip(bp["boxes"], [CAT_COLOR["港湾海岸"], CAT_COLOR["河川"]]):
    patch.set_facecolor(color)
    patch.set_alpha(0.6)
ax.scatter(np.ones(len(data_h)) + np.random.uniform(-0.05, 0.05, len(data_h)),
           data_h, c=CAT_COLOR["港湾海岸"], edgecolor="#222", s=70, alpha=0.85, zorder=10)
ax.scatter(np.ones(len(data_r))*2 + np.random.uniform(-0.05, 0.05, len(data_r)),
           data_r, c=CAT_COLOR["河川"], edgecolor="#222", s=70, alpha=0.85, zorder=10)
ax.set_ylabel("排水容量 (m³/s)")
ax.set_title("(1) カテゴリ別 排水容量分布")
ax.grid(axis="y", alpha=0.3)

# (2) Lorenz 曲線
ax = axes[0, 1]
sorted_disc = np.sort(ALL["discharge_m3s"].values)
cum_pct = np.cumsum(sorted_disc) / sorted_disc.sum() * 100
n = len(sorted_disc)
x_pct = np.arange(1, n+1) / n * 100
ax.plot([0, 100], [0, 100], "--", color="gray", label="完全均等線")
ax.plot(np.concatenate([[0], x_pct]), np.concatenate([[0], cum_pct]),
        color=CAT_COLOR["河川"], linewidth=2.5, marker="o", label="排水容量")
ax.fill_between(np.concatenate([[0], x_pct]), 0, np.concatenate([[0], cum_pct]),
                alpha=0.15, color=CAT_COLOR["河川"])
# Gini 係数
gini = 1 - 2 * np.trapezoid(np.concatenate([[0], cum_pct]) / 100,
                            np.concatenate([[0], x_pct]) / 100)
ax.text(5, 90, f"Gini = {gini:.3f}", fontsize=11,
        bbox=dict(facecolor="white", alpha=0.8))
ax.text(5, 80, f"上位 2 機場 = {top2_share:.1f}%", fontsize=10,
        bbox=dict(facecolor="white", alpha=0.8))
ax.set_xlabel("機場ランク (低 → 高 %)")
ax.set_ylabel("累積排水容量 (%)")
ax.set_title("(2) 排水容量の Lorenz 曲線")
ax.legend(loc="upper left")
ax.grid(alpha=0.3)

# (3) 容量 vs 台数 散布図
ax = axes[1, 0]
for cat, color in CAT_COLOR.items():
    sub = ALL[ALL["port_category"] == cat]
    ax.scatter(sub["n_pumps"], sub["discharge_m3s"], c=color, edgecolor="#222",
               s=120, alpha=0.85, label=f"{cat}")
# ラベル (主要)
top4 = ALL.nlargest(4, "discharge_m3s")
for _, r in top4.iterrows():
    ax.annotate(r["name"][:8], (r["n_pumps"], r["discharge_m3s"]),
                xytext=(5, 5), textcoords="offset points", fontsize=8.5,
                bbox=dict(facecolor="white", alpha=0.7, edgecolor="none", pad=1.5))
ax.set_xlabel("ポンプ台数 (台)")
ax.set_ylabel("排水容量 (m³/s)")
ax.set_title("(3) ポンプ台数 × 排水容量 散布図")
ax.legend(loc="upper left")
ax.grid(alpha=0.3)

# (4) 1台あたり吐出量 (era 別)
ax = axes[1, 1]
era_data = []
era_labels = []
for era in ["昭和", "平成"]:
    sub = ALL[ALL["era"] == era]
    if len(sub) > 0:
        era_data.append(sub["m3s_per_pump"].values)
        era_labels.append(f"{era} (n={len(sub)})")
bp2 = ax.boxplot(era_data, labels=era_labels, patch_artist=True, widths=0.5)
for patch, color in zip(bp2["boxes"], ["#9467bd", "#d62728"]):
    patch.set_facecolor(color)
    patch.set_alpha(0.6)
for i, d in enumerate(era_data):
    ax.scatter(np.ones(len(d))*(i+1) + np.random.uniform(-0.05, 0.05, len(d)),
               d, c="#222", s=50, alpha=0.7, zorder=10)
ax.set_ylabel("1 台あたり吐出量 (m³/s/台)")
ax.set_title("(4) 昭和 vs 平成 — 1 台の能力比較 (大型化検証)")
ax.grid(axis="y", alpha=0.3)

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig6_distribution.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig6_distribution.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 17. 図 7: 浸水想定 1km 圏ヒートマップ + 機場別の最大ランク
# =============================================================================
print("\n[17] 図 7: 浸水想定圏分析", flush=True)
t1 = time.time()

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# (1) 機場ごとの最大浸水ランク + 1km 内浸水面積
ax = axes[0]
sub = gdf.sort_values("flood_area_ha_in_buffer", ascending=True).copy()
y_pos = np.arange(len(sub))
colors_rank = [RANK_COLORS.get(int(r), "#bbb") for r in sub["max_rank_in_buffer"]]
ax.barh(y_pos, sub["flood_area_ha_in_buffer"], color=colors_rank, edgecolor="#222")
for i, (v, rank) in enumerate(zip(sub["flood_area_ha_in_buffer"], sub["max_rank_in_buffer"])):
    rank_str = FLOOD_RANK.get(int(rank), "—") if rank > 0 else "圏外"
    ax.text(v + 5, i, f"{v:.0f} ha / 最大 {rank_str}", va="center", fontsize=8)
ax.set_yticks(y_pos)
ax.set_yticklabels([f"{r['name'][:10]} ({r['city']})" for _, r in sub.iterrows()], fontsize=8.5)
ax.set_xlabel("1km バッファ内の浸水想定面積 (ha)")
ax.set_title("各機場 1km 圏内の浸水想定面積 — 守備範囲の規模")
ax.grid(axis="x", alpha=0.3)

# (2) 容量 vs 浸水面積 散布図 (機場の能力と守備範囲の整合)
ax = axes[1]
mask = gdf["flood_area_ha_in_buffer"] > 0
sub_plot = gdf[mask]
for cat, color in CAT_COLOR.items():
    s2 = sub_plot[sub_plot["port_category"] == cat]
    if len(s2) > 0:
        ax.scatter(s2["flood_area_ha_in_buffer"], s2["discharge_m3s"],
                   c=color, edgecolor="#222", s=120, alpha=0.85, label=f"{cat}")
# ラベル
for _, r in sub_plot.iterrows():
    if r["discharge_m3s"] >= 5 or r["flood_area_ha_in_buffer"] >= 100:
        ax.annotate(r["name"][:8], (r["flood_area_ha_in_buffer"], r["discharge_m3s"]),
                    xytext=(5, 5), textcoords="offset points", fontsize=8,
                    bbox=dict(facecolor="white", alpha=0.7, edgecolor="none", pad=1.5))
ax.set_xlabel("1km 圏内浸水想定面積 (ha)")
ax.set_ylabel("排水容量 (m³/s)")
ax.set_title("守備面積 × 能力の対応関係")
ax.legend(loc="lower right")
ax.grid(alpha=0.3)

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig7_flood_buffer.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig7_flood_buffer.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 18. 図 8: 事務所別管理 + 河川水系別
# =============================================================================
print("\n[18] 図 8: 事務所/水系", flush=True)
t1 = time.time()

# 事務所別 (上位の事務所)
office_pv = ALL.groupby("office").agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
).sort_values("n", ascending=True)
office_pv.to_csv(ASSETS / "L35_office_summary.csv", encoding="utf-8-sig")

# 水系/海岸名でグルーピング (本データの「海岸・河川名」)
wc_pv = ALL.groupby("watercourse").agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
).sort_values("total_m3s", ascending=True)
wc_pv.to_csv(ASSETS / "L35_watercourse_summary.csv", encoding="utf-8-sig")

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

ax = axes[0]
y = np.arange(len(office_pv))
ax.barh(y, office_pv["n"], color="#0969da", alpha=0.85, edgecolor="#222")
for i, (n, m) in enumerate(zip(office_pv["n"], office_pv["total_m3s"])):
    ax.text(n + 0.1, i, f"{int(n)} 件 / {m:.1f} m³/s", va="center", fontsize=9)
ax.set_yticks(y)
ax.set_yticklabels(office_pv.index, fontsize=9)
ax.set_xlabel("管理機場数")
ax.set_title("管理事務所別 機場数")
ax.grid(axis="x", alpha=0.3)

ax = axes[1]
y = np.arange(len(wc_pv))
ax.barh(y, wc_pv["total_m3s"], color="#1a7f37", alpha=0.85, edgecolor="#222")
for i, (n, m) in enumerate(zip(wc_pv["n"], wc_pv["total_m3s"])):
    ax.text(m + 0.3, i, f"{int(n)} 件 / {m:.1f} m³/s", va="center", fontsize=9)
ax.set_yticks(y)
ax.set_yticklabels(wc_pv.index, fontsize=9)
ax.set_xlabel("総排水容量 (m³/s)")
ax.set_title("海岸・河川名別 排水容量")
ax.grid(axis="x", alpha=0.3)

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig8_office_watercourse.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig8_office_watercourse.png ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 19. 図 9: 機場プロファイル レーダー (4 軸)
# =============================================================================
print("\n[19] 図 9: 機場プロファイル", flush=True)
t1 = time.time()

# 上位 6 機場のレーダーチャート (4 軸: 容量 / 台数 / 1台あたり / 浸水面積)
top6 = gdf.nlargest(6, "discharge_m3s").copy()
axes_labels = ["排水容量\n(m³/s)", "ポンプ台数\n(台)", "1台あたり\n(m³/s/台)", "1km圏\n浸水(ha)"]
# 各列を 0-1 正規化 (全 15 件中の最大で割る)
norm_data = pd.DataFrame({
    "容量": top6["discharge_m3s"] / gdf["discharge_m3s"].max(),
    "台数": top6["n_pumps"] / gdf["n_pumps"].max(),
    "1台あたり": top6["m3s_per_pump"] / gdf["m3s_per_pump"].max(),
    "浸水": top6["flood_area_ha_in_buffer"] / max(1, gdf["flood_area_ha_in_buffer"].max()),
})

fig, axes = plt.subplots(2, 3, figsize=(14, 9), subplot_kw=dict(polar=True))
angles = np.linspace(0, 2 * np.pi, 4, endpoint=False).tolist()
angles += angles[:1]

for ax, (i, row) in zip(axes.flat, top6.iterrows()):
    name = row["name"][:14]
    values = norm_data.loc[i].tolist()
    values += values[:1]
    color = CAT_COLOR[row["port_category"]]
    ax.plot(angles, values, color=color, linewidth=2)
    ax.fill(angles, values, color=color, alpha=0.25)
    ax.set_xticks(angles[:-1])
    ax.set_xticklabels(axes_labels, fontsize=8)
    ax.set_ylim(0, 1)
    ax.set_yticks([0.25, 0.5, 0.75, 1.0])
    ax.set_yticklabels(["", "0.5", "", "1.0"], fontsize=7)
    ax.set_title(f"{name}\n{row['city']} / {row['完成']}", fontsize=10)

plt.tight_layout()
fig.savefig(ASSETS / "L35_fig9_radar_top6.png", dpi=130, bbox_inches="tight")
plt.close("all")
print(f"  saved L35_fig9_radar_top6.png ({time.time()-t1:.1f}s)", flush=True)


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

hypos = []

# H1: 河川主導
ratio_count = n_r / max(1, n_h)
ratio_capacity = total_m3s_r / max(0.01, total_m3s_h)
h1 = (n_r >= n_h * 3) and (total_m3s_r >= total_m3s_h * 5)
hypos.append({
    "H": "H1",
    "claim": "河川 12 件 (80%) で主導、容量比でも 河川:港湾海岸 ≈ 9:1",
    "result": f"河川 {n_r}/{n_total} = {n_r/n_total*100:.0f}%, 容量 {total_m3s_r:.1f}/{total_m3s:.1f} = {total_m3s_r/total_m3s*100:.0f}%, 件数比 {ratio_count:.1f}:1, 容量比 {ratio_capacity:.1f}:1",
    "verdict": "支持" if h1 else ("部分支持" if ratio_count >= 2 else "反証"),
})

# H2: 福山市偏重
fukuyama_pct = n_fukuyama / n_r * 100
h2 = (fukuyama_pct >= 50)
hypos.append({
    "H": "H2",
    "claim": "河川機場 12 件のうち福山市が 50%+ を占有",
    "result": f"福山市 {n_fukuyama}/{n_r} = {fukuyama_pct:.0f}% (広島市 {n_hiroshima}, 尾道市 {int((ALL['city']=='尾道市').sum())}, 竹原市 {int((ALL['city']=='竹原市').sum())})",
    "verdict": "支持" if h2 else ("部分支持" if fukuyama_pct >= 30 else "反証"),
})

# H3: 大型機場の二極集中 (上位 2 機場で 40%+)
h3 = (top2_share >= 40)
hypos.append({
    "H": "H3",
    "claim": "30 m³/s 機場 2 件 (手城川・岡ノ下川) で県全体の 40%+",
    "result": f"上位 2 機場 {top2_share:.1f}%, Gini = {gini:.3f} (高度な集中)",
    "verdict": "支持" if h3 else "部分支持",
})

# H4: 建設年代の 2 ピーク (1965-80, 2008-16)
n_1960s_70s = int(((ALL["year"] >= 1960) & (ALL["year"] < 1985)).sum())
n_2008_16 = int(((ALL["year"] >= 2008) & (ALL["year"] <= 2016)).sum())
n_other = n_total - n_1960s_70s - n_2008_16
h4 = (n_1960s_70s >= 3) and (n_2008_16 >= 3)
hypos.append({
    "H": "H4",
    "claim": "建設に 2 ピーク: 1965-84 (戦後復興・都市化期) と 2008-16 (集中豪雨対応強化期)",
    "result": f"1965-84: {n_1960s_70s} 件, 2008-16: {n_2008_16} 件, その他: {n_other} 件",
    "verdict": "支持" if h4 else ("部分支持" if (n_1960s_70s >= 2) else "反証"),
})

# H5: 浸水想定 1km 圏内の機場 ≥ 87%
flood_in_pct = (gdf["near_flood"].sum() / len(gdf)) * 100 if FLOOD_OK else 0
h5 = (flood_in_pct >= 87)
hypos.append({
    "H": "H5",
    "claim": "全 15 機場のうち ≥ 87% が浸水想定 1km バッファ内に立地",
    "result": (f"圏内 {int(gdf['near_flood'].sum())}/{len(gdf)} = {flood_in_pct:.1f}%"
               if FLOOD_OK else "浸水shp 不在のため未検証"),
    "verdict": "支持" if h5 else ("部分支持" if flood_in_pct >= 60 else ("反証" if FLOOD_OK else "未検証")),
})

# H6: 大型化 (平成 > 昭和 で 1台あたり吐出量)
showa_per_pump = float(ALL[ALL["era"] == "昭和"]["m3s_per_pump"].mean())
heisei_per_pump = float(ALL[ALL["era"] == "平成"]["m3s_per_pump"].mean())
h6 = (heisei_per_pump > showa_per_pump)
hypos.append({
    "H": "H6",
    "claim": "平成期建設機の 1 台あたり吐出量は昭和期より大きい (機械大型化)",
    "result": f"昭和 平均 {showa_per_pump:.2f} m³/s/台, 平成 平均 {heisei_per_pump:.2f} m³/s/台",
    "verdict": "支持" if h6 else "反証",
})

hypos_df = pd.DataFrame(hypos)
hypos_df.to_csv(ASSETS / "L35_hypothesis_results.csv", index=False, encoding="utf-8-sig")
with (ASSETS / "L35_hypothesis_results.json").open("w", encoding="utf-8") as f:
    json.dump(hypos, f, ensure_ascii=False, indent=2)
print(hypos_df.to_string(index=False))
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 21. HTML 生成
# =============================================================================
print("\n[21] HTML 生成", flush=True)
t1 = time.time()

def dl(name):
    return f'<a href="assets/{name}" download>{name}</a>'

sections = []

# === Section 1: 学習目標と問い ===
sec1 = f"""
<div class="note">
<b>カバー宣言</b>: 本記事は DoBoX のシリーズ <b>「排水機場 港湾海岸 / 河川」 2 件</b>
(dataset_id = 1269, 1273) を統合し、広島県の<b>排水機場</b>の地理分布・ポンプ吐出能力・
建設年代・浸水想定との重なりを分析する研究記事です。
本シリーズは港湾系シリーズ (L32-L34) の<b>最終記事</b>ですが、扱う施設は
港湾施設ではなく<b>「内水排水施設」</b>であり、
港湾海岸 ({n_h} 件) と河川 ({n_r} 件) の 2 つの設置目的を持ちます。
</div>

<h3>シリーズ構造判定: (a) 設置目的による 2 分割型 + (b) 完全相補</h3>
<p>このシリーズの 2 件は、<b>「排水機場の管轄」</b>で 2 分されている相補型構造です。
L32-L34 の 「港湾 / 漁港」分割と異なり、<b>「港湾海岸 / 河川」</b>という
内水氾濫の<b>主因</b>に基づく分類です。</p>
<ul>
<li><b>港湾海岸 ({n_h} 件 / {total_m3s_h:.1f} m³/s)</b>: 港の陸側 (海岸 + 港背後地) に降った
雨水を、満潮時でも海へ強制排水する施設。</li>
<li><b>河川 ({n_r} 件 / {total_m3s_r:.1f} m³/s)</b>: 河川の支流・小河川が本流に合流する手前で、
本流の水位が高くて自然流下できない時に強制排水する施設。
本記事の<b>主役カテゴリ</b>。</li>
<li>2 件合計 <b>{n_total} 件 / 排水容量 {total_m3s:.1f} m³/s</b>。
これは 25 m プール 1 杯 (約 600 m³) を <b>{600/total_m3s:.1f} 秒</b>で空にできる能力。</li>
</ul>
<p>L32-L34 (港湾施設の 3 層) と<b>同じ「港」を見る記事ではない</b>点に注意。
L32-L34 は港湾の<b>外洋・接岸・陸側</b>の 3 層を扱ったが、
L35 は<b>陸側にさらに溜まった雨水・氾濫水をポンプで汲み出す施設</b>を扱う。
これは<b>「内水浸水防御の最終線」</b>であり、外郭・係留・交通とは目的が根本的に異なる。</p>

<h3>独自用語の定義 (本記事内で固定)</h3>
<table>
<tr><th>用語</th><th>定義</th></tr>
<tr><td>排水機場</td><td>集水区の<b>雨水・地下水・河川氾濫水をポンプで強制排水</b>する施設。
内水氾濫を防ぐ最後の砦。本データに <b>{n_total} 件</b>。
建屋 (ポンプ室) + ポンプ {total_pumps} 台 + 吐出管 + 吸込水路 + 吐出水路 + 樋門 で構成される複合施設。</td></tr>
<tr><td>内水氾濫</td><td>河川本流が氾濫しなくても、<b>支流・側溝・下水管路の容量を超えた雨水</b>が
低地に溜まる現象。本流の水位が高くて支流の水を受け入れられない (=堰き止め) ときに発生。
排水機場はこの内水を強制排水する。<b>外水氾濫 (本流の決壊・越水)</b> とは別物で、
排水機場は<b>内水専用</b>の防御施設。</td></tr>
<tr><td>吐出量 (m³/s)</td><td>ポンプが <b>1 秒間に排水できる水量</b>。本データの「総排水量(m3)」列は
表記上 m³ だが、値の桁 (1.3〜30) と排水機場の業界慣例から<b>毎秒の吐出量 m³/s</b>と解釈。
30 m³/s = 25 m プール 1 杯 (600 m³) を 20 秒で空にする能力。
1 m³/s = 約 1 トン/秒 = 大型ダンプカー 1 台/秒分の水量。</td></tr>
<tr><td>干拓地</td><td>もとは海・湖だった場所を<b>堤防で囲って水を抜き、陸地化</b>した土地。
広島県では<b>福山市芦田川河口の福山平野</b>が代表例で、江戸期からの干拓により
広大な低地 (海抜 0 m 前後) が形成。<b>排水機場が必須</b>な土地条件で、
本記事で福山市に機場が集中する理由。</td></tr>
<tr><td>強制排水</td><td>自然流下で水が引かない場合に、<b>ポンプで動力的に水を持ち上げて排水</b>すること。
河川本流の水位が支流より高い時 (= 自然流下不能)、
あるいは満潮時に港湾海岸が排水できない時に作動する。
電力 + 設備 + 維持管理が必要で、<b>「最後の手段」</b>として整備される。</td></tr>
<tr><td>守備範囲 (1 km バッファ)</td><td>本記事独自指標。各機場が排水を担う集水区を
正確に特定するには排水計画書が必要だが、
本記事では<b>機場から半径 1 km 圏</b>を「物理的に近い守備候補範囲」と仮定し、
この圏内の浸水想定面積で機場の<b>仕事量</b>を測る。これは概算指標であり、
正確な集水区は別途取得が必要 (発展課題)。</td></tr>
<tr><td>浸水想定区域</td><td>DoBoX が公開する河川浸水想定 Shapefile (39 河川統合版)。
1000 年確率規模降雨時の浸水域とその深さ (rank 10〜80 = 0.5m 未満〜20m 以上) を示す。
排水機場は本来この想定エリア内の<b>低標高点</b>に置かれるべきで、
<b>機場立地と浸水想定の重なり</b>は工学的整合性の指標。</td></tr>
</table>

<h3>研究の問い (RQ)</h3>
<p>広島県の排水機場 ({n_total} 件) は、設置目的 (港湾海岸 {n_h} / 河川 {n_r})・
地理分布 (沿岸低地 / 河川合流部 / 干拓地)・規模 (吐出量 m³/s)・建設年代の観点で
<b>どう構成され</b>、<b>内水浸水想定とどう重なる</b>のか?</p>

<ol>
<li>2 dataset の構造比は? 河川主導が想定されるが、容量比でも本当に河川偏重か?</li>
<li>市町別の偏在 (福山市・広島市) は何を反映しているか? 干拓地・水系の特性?</li>
<li>排水容量はどう分布するか? 上位機場への集中度は?</li>
<li>建設年代の傾向は? 戦後の都市化期・近年の集中豪雨対応で集中ピークがあるか?</li>
<li>機場は浸水想定区域内に立地しているか? 位置の工学的整合性?</li>
<li>1 台あたり吐出量 (機械の大型化) は時代と共に増えているか?</li>
</ol>

<h3>仮説 H1〜H6</h3>
<ul>
<li><b>H1 (河川主導)</b>: 件数 河川:港湾海岸 ≈ 4:1、容量比 ≈ 9:1。
内水排水の主戦場は<b>河川合流低地</b>で、港湾海岸はサブカテゴリ。</li>
<li><b>H2 (福山市偏重)</b>: 河川機場のうち福山市が <b>50%+</b>。
福山平野の<b>干拓地</b>性質と<b>芦田川水系の合流複雑性</b>を反映。</li>
<li><b>H3 (上位 2 機場集中)</b>: 30 m³/s 機場 2 件 (手城川・岡ノ下川) で
県全体の <b>40%+</b> を占有。Lorenz 曲線で強い集中構造。</li>
<li><b>H4 (建設の 2 ピーク)</b>: 1965-84 (戦後復興・都市化期) と
2008-16 (集中豪雨対応強化期) の 2 つにピーク。</li>
<li><b>H5 (浸水想定との重なり)</b>: 全 15 機場のうち <b>≥ 87%</b> が
DoBoX 浸水想定区域 1km バッファ内に立地。
機場は<b>「想定浸水エリア内の最低標高点」</b>に置かれる工学的必然性。</li>
<li><b>H6 (機械大型化)</b>: 平成期建設機の 1 台あたり吐出量は昭和期より大きい。
機械技術の進歩で 1 台で扱える流量が増えた。</li>
</ul>

<h3>到達点</h3>
<p>2 dataset を「港湾海岸 + 河川」の<b>設置目的別の 2 カテゴリ</b>として読み解き、
{n_total} 件の排水機場の地理分布・容量分布・建設年代・<b>浸水想定との重なり</b>を統合分析する。
これにより、広島県の内水浸水防御戦略が
<b>「河川合流部での強制排水を主、港湾海岸でのサブ排水を従」</b>とする構成であり、
かつ<b>福山平野の干拓地に重点投資</b>されていることを実データで裏付ける。
さらに、上位 2 機場が県全体の半分近くを担う<b>強い集中構造</b>と、
<b>機械の大型化トレンド</b>を統計的に示す。</p>

<div class="warn"><b>本記事のスコープ外</b>:
本記事は機場の<b>立地構造と容量</b>に集中する。
ポンプの<b>消費電力・運転履歴・故障率</b>、
集水区の<b>正確な範囲と排水負荷曲線</b>、
<b>洪水時の運転実績</b>などは本記事では扱わず発展課題とする。
また「総排水量(m3)」の単位については業界慣例の m³/s と解釈するが、
これは管理者の運用記録 (運転日誌等) で確認すべき (発展課題 Z6)。</div>
"""
sections.append(("学習目標と問い", sec1))


# === Section 2: 使用データ ===
sec2_table = "<table><tr><th>dsid</th><th>カテゴリ</th><th>件数</th><th>形式</th><th>列数</th><th>DoBoX</th></tr>"
for r in series_df.itertuples():
    n_rows = int(ALL[ALL["dsid"] == r.dsid].shape[0])
    sec2_table += (
        f"<tr><td><b>{r.dsid}</b></td><td>{r.category}</td>"
        f"<td>{n_rows}</td>"
        f"<td>CSV (UTF-8 BOM)</td><td>9</td>"
        f"<td><a href='https://hiroshima-dobox.jp/datasets/{r.dsid}' target='_blank'>#{r.dsid}</a></td></tr>"
    )
sec2_table += "</table>"

# 列スキーマ表
schema = [
    ("施設の名称",    "機場の固有名称 (例: 御幸排水機場、岡ノ下川排水機場)"),
    ("事務所名",      "管理事務所 (例: 西部建設事務所、東部建設事務所、各支所)"),
    ("所在地",        "市町名 + 番地 (例: 福山市東手城町二丁目)"),
    ("緯度(10進数)",  "緯度 (10進数表記)"),
    ("経度(10進数)",  "経度 (10進数表記)"),
    ("海岸・河川名",  "排水先または接続水系の名称 (例: 岡ノ下川、大竹港海岸)"),
    ("完成年月日",    "和暦の年 (例: 昭和41年、平成28年)"),
    ("総排水量(m3)",  "<b>業界慣例で m³/s と解釈</b>。1.3〜30 の範囲"),
    ("ポンプ台数(台)", "ポンプの基数 (1〜4 台)"),
]
schema_html = "<table><tr><th>列名</th><th>意味</th></tr>" + "".join(
    f"<tr><td><code>{c}</code></td><td>{m}</td></tr>" for c, m in schema
) + "</table>"

# 全 15 件の生データ表
all_table = ALL[["name", "port_category", "city", "watercourse", "完成", "year",
                 "discharge_m3s", "n_pumps", "m3s_per_pump"]].copy()
all_table = all_table.rename(columns={
    "name": "機場名", "port_category": "カテゴリ", "city": "市町",
    "watercourse": "海岸/河川", "完成": "完成", "year": "西暦",
    "discharge_m3s": "容量m³/s", "n_pumps": "台数", "m3s_per_pump": "台/台m³/s",
})
all_table["容量m³/s"] = all_table["容量m³/s"].round(1)
all_table["台/台m³/s"] = all_table["台/台m³/s"].round(2)
all_table_html = all_table.to_html(index=False, classes="", border=0)

sec2 = f"""
<p>本記事が使用する 2 dataset_id の一覧。両者は<b>同一スキーマ (9 列)</b> で、
データセット名の括弧内 (港湾海岸 / 河川) で<b>設置目的</b>が分かれます。</p>

{sec2_table}

<h3>列スキーマ (両 dataset 共通)</h3>
{schema_html}

<h3>L32-L34 シリーズとの重要な相違</h3>
<table>
<tr><th>軸</th><th>L32 外郭 / L33 係留 / L34 交通</th><th>L35 排水機場 (本記事)</th></tr>
<tr><td>件数</td><td>合計 1,520 件</td><td><b>{n_total} 件</b> (極小)</td></tr>
<tr><td>地理分布</td><td>瀬戸内海岸の港湾区域</td><td>港湾海岸 + 河川合流部 (内陸も含む)</td></tr>
<tr><td>主指標</td><td>延長 m / 面積 m²</td><td><b>吐出量 m³/s + ポンプ台数</b></td></tr>
<tr><td>カテゴリ軸</td><td>港湾 / 漁港 (運営主体)</td><td><b>港湾海岸 / 河川</b> (排水先)</td></tr>
<tr><td>GIS</td><td>WKT POLYGON / LINESTRING</td><td><b>緯度経度のみ</b> (Point)</td></tr>
<tr><td>分析の主役</td><td>3 層空間関係</td><td><b>容量分布・建設年代・浸水想定との重なり</b></td></tr>
</table>

<h3>15 機場 全件リスト (本記事のデータ全部)</h3>
{all_table_html}

<h3>データ品質メモ</h3>
<ul>
<li><b>geom 完備</b>: 全 {n_total} 件に緯度経度あり ({len(gdf)}/{len(ALL)} = 100%)。
L34 のような GIS 欠損問題はない。</li>
<li><b>「総排水量(m3)」の単位</b>: 列名は m³ だが、値の桁 (1.3〜30) と排水機場の慣例から
<b>m³/s と解釈</b>。例: 手城川 30 m³/s = 1 秒に 30 トンの水を排水。
管理者公開の運転記録 (国交省「ポンプ施設台帳」相当) で確認可能だが、
本記事では業界慣例で扱う。</li>
<li><b>新安川の特殊性</b>: 第一 (昭和41年, 5 m³/s, 2台) と第二 (平成28年, 6.5 m³/s, 4台) の
<b>同一住所の 2 機場</b>がある。これは<b>62 年差で増設</b>した珍しい例で、
都市化進行で容量不足が顕著化した結果と推察される (発展課題)。</li>
<li><b>福山市集中</b>: 河川 {n_r} 件のうち福山市は <b>{n_fukuyama} 件</b> ({n_fukuyama/n_r*100:.0f}%)。
福山平野は芦田川河口の干拓地で<b>低地・複雑な水系</b>のため機場集中。</li>
</ul>
"""
sections.append(("使用データ", sec2))


# === Section 3: ダウンロード ===
dl_buttons = []
for r in series_df.itertuples():
    rid = [s[4] for s in SERIES if s[0] == r.dsid][0]
    dl_buttons.append(
        f'<li>{r.label} (dsid={r.dsid}, {r.category}): '
        f'<a href="https://hiroshima-dobox.jp/resource_download/{rid}">直 DL</a> '
        f'/ <a href="https://hiroshima-dobox.jp/datasets/{r.dsid}">DoBoX</a></li>'
    )
sec3 = f"""
<h3>生データ (DoBoX 直リンク)</h3>
<ul>{''.join(dl_buttons)}</ul>

<h3>中間データ (本記事生成 CSV)</h3>
<ul>
<li>{dl("L35_series.csv")} — 2 dataset 一覧</li>
<li>{dl("L35_all_facilities.csv")} — 統合排水機場 ({n_total} 行) + year + era + m3s_per_pump + city + near_flood</li>
<li>{dl("L35_category_summary.csv")} — カテゴリ別 集計 (件数・容量・台数)</li>
<li>{dl("L35_city_summary.csv")} — 市町別 集計</li>
<li>{dl("L35_year_pivot.csv")} — 建設年代別 件数</li>
<li>{dl("L35_era_pump_efficiency.csv")} — 昭和 vs 平成 1台あたり吐出量</li>
<li>{dl("L35_office_summary.csv")} — 事務所別管理</li>
<li>{dl("L35_watercourse_summary.csv")} — 海岸・河川名別</li>
<li>{dl("L35_hypothesis_results.csv")} / {dl("L35_hypothesis_results.json")} — H1〜H6 検証結果</li>
</ul>

<h3>図 (本記事生成 PNG)</h3>
<ul>
<li>{dl("L35_fig1_dataset_overview.png")} / {dl("L35_fig2_pref_overview.png")} / {dl("L35_fig3_city_zoom.png")}</li>
<li>{dl("L35_fig4_flood_overlay.png")} / {dl("L35_fig5_era_timeline.png")} / {dl("L35_fig6_distribution.png")}</li>
<li>{dl("L35_fig7_flood_buffer.png")} / {dl("L35_fig8_office_watercourse.png")} / {dl("L35_fig9_radar_top6.png")}</li>
</ul>

<h3>再現用 Python スクリプト</h3>
<p><a href="L35_drainage_pumps.py" download>L35_drainage_pumps.py</a> を取得して
プロジェクトルートで <code>py -X utf8 lessons/L35_drainage_pumps.py</code> を実行。
データが無ければ自動取得します (DoBoX 浸水想定 shp が <code>data/extras/flood_shp/</code>
にあれば浸水重ね分析も自動有効化)。</p>
"""
sections.append(("ダウンロード", sec3))


# === Section 4: 分析 1 — 2 dataset の構造 ===
sec4 = f"""
<h3>狙い</h3>
<p>「港湾海岸」と「河川」の 2 dataset が、件数・容量・地理分布でどう分化しているかを
1 枚の絵で示す。<b>河川主導 (H1)</b> の量的検証。</p>

<h3>手法 (簡潔に)</h3>
<p>2 dataset を縦結合し、<code>port_category</code> 列 (港湾海岸 / 河川) で
件数・総容量・台数を集計。15 件全部を 1 枚の横棒グラフで容量順に並べ、
カテゴリ色で塗り分けて視覚的に二分構造を示す。</p>

<h3>実装</h3>
{code('''
SERIES = [
    (1269, "排水機場（港湾海岸）", "港湾海岸", "drainage_pump_port_coast.csv", 39770),
    (1273, "排水機場（河川）",     "河川",     "drainage_pump_river.csv",      39774),
]
dfs = []
for dsid, label, cat, fname, rid in SERIES:
    p = DATA_DIR / fname
    ensure_dataset(p, dataset_id=dsid, resource_id=rid)
    df = pd.read_csv(p, encoding="utf-8-sig")
    df["port_category"] = cat
    dfs.append(df)

ALL = pd.concat(dfs, ignore_index=True)  # 全 15 行
cat_agg = ALL.groupby("port_category").agg(
    n=("施設の名称", "count"),
    total_m3s=("総排水量(m3)", "sum"),
    n_pumps_sum=("ポンプ台数(台)", "sum"),
)
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: 2 dataset の規模を文字 + バーで同時に伝える。
左 (カード) は「カテゴリ別の件数+容量+概要」をテキストで、
右 (バー) は「15 機場の容量を一望」できる横棒。両方を 1 枚に置くことで
「カテゴリの偏重 (H1)」と「容量分布の偏在 (H3)」が同時に読める。</p>
{figure("assets/L35_fig1_dataset_overview.png", "2 dataset の構造概観 — カードビュー (左) と 15 機場の排水容量バー (右)")}
<p><b>読み取り</b>:</p>
<ul>
<li><b>河川 {n_r} 件 ({total_m3s_r:.1f} m³/s) vs 港湾海岸 {n_h} 件 ({total_m3s_h:.1f} m³/s)</b>。
件数比 <b>{n_r/n_h:.1f}:1</b>、容量比 <b>{total_m3s_r/total_m3s_h:.1f}:1</b>。
<b>容量比が件数比を上回る</b> = 河川機場が 1 機あたりも大きい。
H1 (河川主導) を強支持。</li>
<li><b>右上に 30 m³/s の 2 機場</b> (手城川 / 岡ノ下川) が突出。両者の合計だけで <b>{30+30} m³/s</b>、
県全体 {total_m3s:.1f} m³/s の <b>{60/total_m3s*100:.0f}%</b>を占める。</li>
<li><b>港湾海岸 (青)</b> 3 件は中間〜下位に分散 (1.3, 3.2, 8.2 m³/s)。
河川機場と比べ単機規模が小さい。</li>
<li><b>下位は河川機場 (緑)</b> も小さい (才町川 / 木曽丸川 / 大河原川 各 2 m³/s)。
これらは<b>支流の小規模合流</b>で、本流 (芦田川・太田川) の助け役。</li>
</ul>

<h3>表と読み取り (カテゴリ別集計)</h3>
{cat_agg.to_html(classes='', border=0)}
<p><b>読み取り</b>: 件数の偏重 (河川 80%) より<b>容量の偏重 (河川 90%)</b> の方が強い。
これは河川機場が<b>大型機を持ちやすい</b>ことを反映 (合流部での大流量を扱うため)。
平均値で見ると 河川 {ALL[ALL['port_category']=='河川']['discharge_m3s'].mean():.1f} m³/s vs
港湾海岸 {ALL[ALL['port_category']=='港湾海岸']['discharge_m3s'].mean():.1f} m³/s で約 {ALL[ALL['port_category']=='河川']['discharge_m3s'].mean()/ALL[ALL['port_category']=='港湾海岸']['discharge_m3s'].mean():.1f} 倍。</p>
"""
sections.append(("分析 1: 2 dataset の構造を可視化", sec4))


# === Section 5: 分析 2 — 県全域マップで地理分布 ===
sec5 = f"""
<h3>狙い</h3>
<p>件数や容量だけでは見えない<b>地理偏在</b>を、県全域の点マップで一望する。
広島県の<b>どの市町に</b>排水機場が分布し、<b>どこが手薄</b>かを可視化。</p>

<h3>手法 (リテラシレベル解説)</h3>
<p><b>緯度経度 → Point geometry → matplotlib scatter</b> の 3 ステップ:</p>
<ol>
<li>各機場の緯度・経度から <code>shapely.Point</code> を作る。</li>
<li>EPSG:4326 (WGS84) → EPSG:6671 (平面直角 III 系、m 単位) に投影変換。
これで距離計算とバッファ生成が m 単位で正確になる。</li>
<li><code>matplotlib scatter</code> で点を描画、<b>サイズ = 排水容量</b>に比例 (バブル散布)。
1 km バッファも薄塗りで重ね、<b>「機場が守る範囲の目安」</b>を視覚化。</li>
</ol>
<ul>
<li><b>入力</b>: 15 機場の緯度経度</li>
<li><b>出力</b>: 県全域マップ + バブル + バッファ円</li>
<li><b>限界</b>: 1 km バッファは<b>仮の守備範囲</b>。実際の集水区は機場の排水計画書で定義されるが、
公開されていないことが多いため、本記事では物理的近接の目安として用いる。</li>
</ul>

<h3>実装</h3>
{code('''
from shapely.geometry import Point
ALL["geometry"] = [Point(lon, lat) for lon, lat in zip(ALL["lon"], ALL["lat"])]
gdf = gpd.GeoDataFrame(ALL, geometry="geometry", crs="EPSG:4326").to_crs("EPSG:6671")
gdf["buffer_1km"] = gdf.geometry.buffer(1000.0)  # 1 km バッファ

fig, ax = plt.subplots(figsize=(13, 8))
pref_diss.boundary.plot(ax=ax, color="#222", linewidth=1.0)

# サイズ = 容量に比例
sizes = gdf["discharge_m3s"] / gdf["discharge_m3s"].max() * 600 + 30
for cat, color in CAT_COLOR.items():
    sub = gdf[gdf["port_category"] == cat]
    ax.scatter(sub.geometry.x, sub.geometry.y, s=sizes[gdf.port_category==cat],
               c=color, edgecolor="#222", alpha=0.85)
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: 県全域に対する 15 機場の分布密度を、青 (港湾海岸) と緑 (河川) で
直接見ることで「どこにどの目的の機場が集中しているか」を一目で把握できる。
バッファ円で<b>守備範囲の目安</b>も同時に示される。</p>
{figure("assets/L35_fig2_pref_overview.png", f"広島県 排水機場 全 {n_total} 件マップ — 円サイズ=排水容量、円=1km バッファ")}
<p><b>読み取り</b>:</p>
<ul>
<li><b>福山市東部</b>に河川機場 (緑) のクラスタ。{n_fukuyama} 件が密集 = 福山平野の干拓地特性。
手城川 30 m³/s + 羽原川 11 m³/s + 古市 4.4 m³/s 等で、<b>福山市単独で県の {ALL[ALL['city']=='福山市']['discharge_m3s'].sum()/total_m3s*100:.0f}%</b> の容量を持つ。</li>
<li><b>広島市</b>に河川機場 4 件 (太田川水系の支流: 岡ノ下川・尾崎川・新安川第一/第二)。
岡ノ下川 30 m³/s で県内最大級 1 件。</li>
<li><b>港湾海岸 (青)</b> 3 件は瀬戸内海岸沿いに分散 (大竹・瀬戸田・竹原)。
内陸の河川機場と<b>明確に立地が異なる</b>。</li>
<li><b>県北・県西の山間部 (三次・庄原・廿日市奥)</b>: <b>機場ゼロ</b>。
山地は自然流下で排水できるため、強制排水が不要。
排水機場は<b>低地・干拓地・河川合流部</b>に限定される (工学的必然)。</li>
<li><b>呉市・東広島市</b>には機場がない。これは港湾はあっても干拓地・大規模合流部がないため。</li>
</ul>
"""
sections.append(("分析 2: 県全域マップで地理偏在を観る", sec5))


# === Section 6: 分析 3 — 福山市・広島市の集中ズーム ===
sec6 = f"""
<h3>狙い</h3>
<p>2 大集中地 (福山市 {n_fukuyama} 件 + 広島市 {n_hiroshima} 件) を<b>市単位でズーム</b>して、
<b>機場の分布パターンと集中の理由</b>を地形・水系から読み解く。
H2 (福山市偏重) の地理的検証。</p>

<h3>手法</h3>
<p>市町境界 Shapefile (admin_832 福山市 / admin_786 広島市) に
機場点を重ね、<b>サイズ = 容量</b>のバブル + ラベルを配置。
2 市を並列に並べることで<b>分布形状の違い</b>を比較できる。</p>

<h3>実装</h3>
{code('''
fukuyama_zip = ADMIN_DIR / "admin_832_福山市.zip"
g_fukuyama = load_zip_first_geo(fukuyama_zip).to_crs("EPSG:6671")
fukuyama_diss = g_fukuyama.dissolve()  # 市境 1 ポリゴン

fig, axes = plt.subplots(1, 2, figsize=(15, 7))

# 福山市
ax = axes[0]
fukuyama_diss.plot(ax=ax, color="#fef9e7", alpha=0.5)
fukuyama_pts = gdf[gdf["city"] == "福山市"]
ax.scatter(fukuyama_pts.geometry.x, fukuyama_pts.geometry.y,
           s=fukuyama_pts["discharge_m3s"]/30*800 + 50, c=...)
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: 県全域マップでは見えない<b>市町内の分布</b>を市境ポリゴン上に拡大し、
<b>同じ市内でも機場が集中するエリアと空白エリア</b>が分かる。
2 市を並べる small multiples 配置で対比が直観的。</p>
{figure("assets/L35_fig3_city_zoom.png", "福山市 (左) と広島市 (右) の機場ズーム — サイズ=容量")}
<p><b>読み取り</b>:</p>
<ul>
<li><b>福山市の機場 ({n_fukuyama} 件)</b> は<b>市の南部・東部 (= 旧市街地・干拓地)</b> に集中。
最大の<b>手城川 30 m³/s</b> が市東部 (東手城町、芦田川河口近く) に立地、
<b>羽原川 11 m³/s</b> が市中央部、<b>古市/坊寺/才町/木曽丸川</b> が芦田川支流に並ぶ。
これは<b>福山平野の干拓地パターン</b>そのもの。</li>
<li><b>広島市の機場 ({n_hiroshima} 件)</b> は<b>太田川水系の各支流</b>に分散。
最大の<b>岡ノ下川 30 m³/s</b> は<b>佐伯区五日市</b> (太田川河口近くの干拓地)、
<b>尾崎川 9 m³/s</b> は<b>安芸区矢野</b> (尾根間低地)、
<b>新安川第一/第二</b> は<b>安佐南区長束</b> (太田川分流部)。
分散しているが、<b>すべて低地・支流合流部</b>。</li>
<li><b>分布形状の違い</b>: 福山は<b>面的</b>(市東部の広い干拓地に多数) で、
広島は<b>線状</b>(各支流の合流点に各 1 件)。
これは地形特性の違いを反映: 福山平野は連続干拓地、広島は山がちで支流ごとに低地が分離。</li>
<li><b>1 機 = 30 m³/s の最大級 2 機が両市に存在</b>: 手城川 (福山) と岡ノ下川 (広島佐伯)。
これは<b>県の 2 大都市が同等規模の防御を持つ</b> = 都市政策の整合。</li>
</ul>
"""
sections.append(("分析 3: 福山市・広島市ズームで集中構造を読む", sec6))


# === Section 7: 分析 4 — 浸水想定との重ね合わせ ===
near_flood_n = int(gdf["near_flood"].sum())
sec7 = f"""
<h3>狙い</h3>
<p>機場の<b>立地は浸水想定区域内にあるか?</b> 工学的には機場は浸水域の<b>最低標高点</b>に
置かれるべきで、<b>機場立地と浸水想定の重なり</b>は工学的整合性の指標。
H5 の検証。</p>

<h3>手法 (リテラシレベル解説)</h3>
<p><b>空間結合 + バッファ判定</b>:</p>
<ol>
<li>DoBoX 河川浸水想定区域 Shapefile (39 河川統合版、613 ポリゴン) をロード。
<code>rank</code> 列が浸水深 (10=0.5m未満 〜 80=20m以上)。</li>
<li>各機場に 1 km バッファを生成 (<code>geometry.buffer(1000)</code>)。</li>
<li>バッファと浸水想定ポリゴンが交差するか <code>gpd.GeoDataFrame.intersects()</code> で判定。
1 件以上交差すれば<b>「浸水想定圏内立地」</b>と判定。</li>
<li>各機場の 1km 圏内の浸水想定面積 (ha) を <code>gpd.clip()</code> で算出 (= 守備規模の指標)。</li>
</ol>
<ul>
<li><b>入力</b>: 機場 15 件 (Point) + 浸水想定 613 ポリゴン</li>
<li><b>出力</b>: 各機場の <code>near_flood</code> ブール + <code>flood_area_ha_in_buffer</code> + <code>max_rank</code></li>
<li><b>限界</b>: 1 km バッファは仮の守備範囲。実際の集水区は機場の排水計画で定義される。
本分析は<b>位置の整合性</b>を見ており、<b>守備関係そのもの</b>ではない。</li>
</ul>

<h3>実装</h3>
{code('''
flood = gpd.read_file("data/extras/flood_shp/shinsui_souteisaidai/shinsui_souteisaidai.shp",
                      encoding="cp932").to_crs("EPSG:6671")
gdf["buffer_1km"] = gdf.geometry.buffer(1000.0)

for i, row in gdf.iterrows():
    sub = flood[flood.intersects(row["buffer_1km"])]
    if len(sub) > 0:
        gdf.at[i, "near_flood"] = True
        gdf.at[i, "max_rank_in_buffer"] = sub["rank"].max()
        clipped = gpd.clip(sub, row["buffer_1km"])
        gdf.at[i, "flood_area_ha_in_buffer"] = clipped.geometry.area.sum() / 10000
''')}

<h3>図と読み取り — 県全域 + 福山ズーム</h3>
<p><b>なぜこの図か</b>: 浸水想定 (青階調) と機場 (色マーカー) を<b>同一マップ</b>に重ねることで、
「機場が浸水想定エリア内に立地するか」が直視できる。
県全域 (左) で全体像、福山ズーム (右) で詳細を見る。</p>
{figure("assets/L35_fig4_flood_overlay.png", "浸水想定 + 排水機場 — 機場が想定エリア内に立地するかの視覚検証")}
<p><b>読み取り</b>:</p>
<ul>
<li><b>{near_flood_n}/{len(gdf)} = {near_flood_n/len(gdf)*100:.1f}%</b> の機場が浸水想定 1km 圏内。
H5 (≥87%) を <b>{hypos[4]['verdict']}</b>。
工学的整合性が極めて高い (= 機場は<b>本来浸水するエリアの内側</b>に置かれている)。</li>
<li><b>福山ズーム</b>: 浸水想定 (青色階調) が芦田川河口域に広がり、
その中に手城川・羽原川・古市等の機場が立地。<b>機場 = 浸水域の鍵</b>を視覚的に確認。</li>
<li>機場周辺の最大ランクは {int(gdf['max_rank_in_buffer'].max())} (= {FLOOD_RANK.get(int(gdf['max_rank_in_buffer'].max()), '?')})。
これは<b>最深 10-20m 浸水想定</b>のすぐ近くに機場があることを意味し、
<b>機場が無ければ深刻な内水浸水が発生する</b>立地条件。</li>
<li><b>県全域マップ</b>で見ると、浸水想定 (青) が河川沿いに細長く分布し、
各<b>大きな浸水域 (= 大きな低地)</b> に機場が 1〜複数立地している。
"浸水域あり = 機場あり" の対応関係が成立。</li>
</ul>

<h3>図と読み取り — 機場別 1km 圏内浸水面積ランキング</h3>
<p><b>なぜこの図か</b>: 機場ごとの<b>「守るべき面積」</b>を 1km 圏内の浸水想定面積で代替指標化し、
バーで順位付け。<b>機場容量と守備面積の整合</b>を後段で検証する基礎データ。</p>
{figure("assets/L35_fig7_flood_buffer.png", "各機場 1km 圏内の浸水想定面積 (左) と容量×面積散布 (右)")}
<p><b>読み取り (左バー)</b>:</p>
<ul>
<li><b>1km 圏内浸水面積が最大の機場</b>: {gdf.nlargest(3, 'flood_area_ha_in_buffer')['name'].tolist()[0]} 等。
<b>{gdf['flood_area_ha_in_buffer'].max():.0f} ha</b> = 東京ドーム約 {gdf['flood_area_ha_in_buffer'].max()/4.7:.0f} 個分の想定浸水域を抱える。</li>
<li><b>圏内に浸水想定がない機場</b>: {int((gdf['flood_area_ha_in_buffer']==0).sum())} 件。
これらは<b>河川支流・港湾海岸の局所排水</b>のための機場で、
DoBoX の浸水想定は本流ベースのため局所が拾われていない。
これは<b>データの限界</b>であり、機場が無意味なわけではない。</li>
</ul>
<p><b>読み取り (右散布図)</b>:</p>
<ul>
<li><b>容量 × 面積の正の相関</b>: 大型機場ほど大きな浸水想定エリアを抱える傾向。
これは<b>「リスクの大きさに応じて機場容量が設定されている」</b>工学的整合性。</li>
<li>外れ値: <b>手城川 (30 m³/s, 浸水面積大)</b> は両者ともトップ = 福山市東部の最重要機場。
<b>新安川第二 (6.5 m³/s, 浸水面積中)</b> は近年増設で容量が時代に合っている。</li>
</ul>

<div class="warn"><b>解釈の注意</b>: 1 km バッファは<b>近接の目安</b>であり、
実際の集水区は機場の排水計画書で個別定義される。
正確な守備関係を知るには、機場ごとの<b>排水区域図</b>を取得する必要がある (発展課題 Z3)。</div>
"""
sections.append(("分析 4: 浸水想定との重なり — 機場立地の工学的整合性", sec7))


# === Section 8: 分析 5 — 容量分布と集中構造 ===
sec8 = f"""
<h3>狙い</h3>
<p>排水機場の<b>容量分布</b>を多角的に観察し、
カテゴリ別の標準サイズ・集中度・台数との関係・新旧の機械大型化を統計的に検証する。
H3 (上位 2 機場集中) と H6 (機械大型化) の検証。</p>

<h3>手法 (リテラシレベル解説)</h3>
<p>4 つの可視化手法を 2x2 で統合する:</p>
<ul>
<li><b>(1) Box plot + jitter</b>: カテゴリ別に容量分布を見る。中央値・四分位範囲・外れ値が一目で。</li>
<li><b>(2) Lorenz 曲線</b>: x 軸 = 機場ランク (低 → 高 %)、y 軸 = 累積容量 (%)。
完全均等線 (対角線) からの偏差が集中度。Gini 係数 (0=完全均等、1=完全独占) で定量化。</li>
<li><b>(3) 容量 × 台数 散布図</b>: ポンプ台数と容量の関係。
1 台あたりの能力差・大型機の構成を読む。</li>
<li><b>(4) 1 台あたり吐出量 (era 別)</b>: 昭和 vs 平成で 1 台の能力差を比較 (機械大型化 H6)。</li>
</ul>

<h3>実装</h3>
{code('''
# (2) Lorenz 曲線と Gini 係数
sorted_disc = np.sort(ALL["discharge_m3s"].values)
cum_pct = np.cumsum(sorted_disc) / sorted_disc.sum() * 100
n = len(sorted_disc)
x_pct = np.arange(1, n+1) / n * 100
gini = 1 - 2 * np.trapezoid(np.concatenate([[0], cum_pct]) / 100,
                            np.concatenate([[0], x_pct]) / 100)
# Gini = 0 (均等) 〜 1 (1 機独占)。容量で 0.4-0.5 なら強い集中。

# (4) 1 台あたり吐出量
ALL["m3s_per_pump"] = ALL["discharge_m3s"] / ALL["n_pumps"]
era_pump = ALL.groupby("era")["m3s_per_pump"].mean()
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: 4 視点を 2x2 で並べることで、分布形状・集中度・関係性・時系列変化を
1 枚に統合できる。</p>
{figure("assets/L35_fig6_distribution.png", "容量分布 4 視点 — boxplot、Lorenz、容量×台数、新旧 1台あたり")}
<p><b>読み取り (左上 — カテゴリ別 boxplot)</b>:</p>
<ul>
<li><b>河川 (緑)</b>: 中央値が高く、外れ値 (30 m³/s 2 件) が顕著。1.3〜30 m³/s と広い範囲。</li>
<li><b>港湾海岸 (青)</b>: 中央値・最大値とも河川より小さい。1.3〜8.2 m³/s の狭いレンジ。
H1 を視覚的に支持。</li>
</ul>
<p><b>読み取り (右上 — Lorenz 曲線)</b>:</p>
<ul>
<li>曲線が対角線から<b>大きく乖離</b> = 強い集中。Gini = {gini:.3f} (0.4以上は高集中)。</li>
<li>上位 2 機場 (手城川 30 + 岡ノ下川 30) で<b>{top2_share:.1f}%</b> = ほぼ半分を 2 機が担う。</li>
<li>これは<b>「重要拠点に集中投資」</b>政策の現れ。
1 機で大きな集水区を一括処理する方が、多数の小機場を分散整備するより
維持管理コスト・電力契約・人員配置で効率的。</li>
<li>H3 (上位 2 機場で 40%+) を <b>{hypos[2]['verdict']}</b>。</li>
</ul>
<p><b>読み取り (左下 — 容量 × 台数 散布図)</b>:</p>
<ul>
<li>正の相関 (台数が多いほど容量も大きい傾向) だが、<b>例外</b>: 手城川 30 m³/s が <b>1 台のみ</b>。
これは<b>超大型ポンプ 1 機を据え置いた</b>設計で、メンテナンス時の冗長性は低いが大流量を集中処理。</li>
<li>新安川第二 (6.5 m³/s, 4 台) は<b>多台数構成</b>。1 台/台は 1.6 m³/s と小さく、
冗長性 (1 台壊れても 75% 残る) 重視の設計。</li>
<li><b>2 つの設計思想が共存</b>: 「1 機集中型 (手城川)」 vs 「多機分散型 (新安川第二)」。
これは設計年代と現場条件 (用地・電力) で選ばれる。</li>
</ul>
<p><b>読み取り (右下 — 昭和 vs 平成 1台あたり吐出量)</b>:</p>
<ul>
<li><b>昭和期 (n={n_showa})</b>: 1 台あたり中央値 {ALL[ALL['era']=='昭和']['m3s_per_pump'].median():.1f} m³/s/台、
平均 {showa_per_pump:.1f} m³/s/台。</li>
<li><b>平成期 (n={n_heisei})</b>: 1 台あたり中央値 {ALL[ALL['era']=='平成']['m3s_per_pump'].median():.1f} m³/s/台、
平均 {heisei_per_pump:.1f} m³/s/台。<b>昭和の {heisei_per_pump/showa_per_pump:.1f} 倍</b>。</li>
<li>H6 (機械大型化) を <b>{hypos[5]['verdict']}</b>。
これは<b>ポンプ機械の進歩 + 用地制約 (台数増やすより 1 台大型化)</b> の両方の影響。</li>
</ul>
"""
sections.append(("分析 5: 容量分布と集中構造 — Gini 係数 + 機械大型化検証", sec8))


# === Section 9: 分析 6 — 建設年代タイムライン ===
sec9 = f"""
<h3>狙い</h3>
<p>機場の<b>建設年代</b>を時系列で並べ、<b>整備の歴史</b>を読み解く。
昭和40 ({year_min}) 〜 平成28 ({year_max}) の {year_max-year_min} 年間で 15 件が建設された。
H4 (2 ピーク仮説: 1965-84 戦後復興期 + 2008-16 集中豪雨対応期) の検証。</p>

<h3>手法</h3>
<p>和暦 (例: 昭和41年) を西暦 (1966) に変換し、横軸=西暦の<b>ガントチャート風タイムライン</b>を描く。
バブルサイズで容量、色でカテゴリを表現。下段に<b>10 年区分のヒストグラム</b>を並べて
集中時期を統計的に確認。</p>

<h3>実装</h3>
{code('''
def jp_to_west(s: str) -> int | None:
    s = str(s).strip()
    if s.startswith("昭和"):
        n = int(s.replace("昭和", "").replace("年", ""))
        return 1925 + n  # 昭和元年=1926, n年 = 1925+n
    elif s.startswith("平成"):
        n = int(s.replace("平成", "").replace("年", ""))
        return 1988 + n  # 平成元年=1989, n年 = 1988+n
    return None

ALL["year"] = ALL["完成"].apply(jp_to_west)
ALL["era"] = ALL["完成"].apply(lambda s: "昭和" if str(s).startswith("昭和") else "平成")

# 10 年区分のヒストグラム
year_bins = [1960, 1970, 1980, 1990, 2000, 2010, 2020]
year_labels = ["1960s", "1970s", "1980s", "1990s", "2000s", "2010s"]
ALL["year_bin"] = pd.cut(ALL["year"], bins=year_bins, labels=year_labels, right=False)
year_pivot = ALL.groupby(["year_bin", "port_category"], observed=True).size().unstack(fill_value=0)
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: タイムライン (上) で各機場の建設順序と容量、ヒストグラム (下) で集中時期を
2 段に並べることで「いつ・誰が・どれくらい大きい機場を建てたか」が読める。</p>
{figure("assets/L35_fig5_era_timeline.png", "建設年代タイムライン (上) と 10 年区分ヒストグラム (下)")}
<p><b>読み取り</b>:</p>
<ul>
<li><b>戦後復興・都市化期 (昭和40-60年代 = 1965-84)</b>: 7 件が建設。
御幸 (1965) → 新安川第一 (1966) → 古市 (1979) → 坊寺 (1977) → 大河原 (1983) → 尾崎川 (1984) → 沢 (1986)。
広島港湾エリアの整備期と一致 = 港湾と内水排水が同時整備されていた。</li>
<li><b>バブル経済〜平成初期 (1985-95)</b>: 5 件 (柏もここ)。新規より<b>更新・支流追加</b>。</li>
<li><b>集中豪雨対応強化期 (平成20年代 = 2008-16)</b>: 4 件。
本川 (2013) → 手城川 (2002) → 羽原川 (2015) → 新安川第二 (2016)。
<b>新安川第二は第一の 50 年後の増設</b>で、容量不足による拡張。</li>
<li>2 ピーク仮説 H4 を<b>{hypos[3]['verdict']}</b>。</li>
<li><b>大型機 30 m³/s 2 件の建設時期</b>: 岡ノ下川 (1989, 平成元年) と手城川 (2002, 平成14年)。
両者とも<b>平成期建設の超大型機</b>で、機械大型化トレンド (H6) と整合。</li>
<li><b>新安川 第一 → 第二の 62 年差</b>: 1966 → 2016。同一住所で容量 5 → 6.5 m³/s、台数 2 → 4。
<b>「同じ場所で時代に合わせて増設」</b>パターンの希少例 (発展課題)。</li>
</ul>
"""
sections.append(("分析 6: 建設年代タイムライン — 整備の歴史と 2 ピーク仮説", sec9))


# === Section 10: 分析 7 — 事務所別管理 + 海岸/河川名別 ===
sec10 = f"""
<h3>狙い</h3>
<p>機場の<b>管轄行政組織</b> (建設事務所) と<b>排水先水系</b> (海岸・河川名) で集計し、
<b>誰がどこを守っているか</b>の組織構造を読む。</p>

<h3>手法</h3>
<p>2 軸クロス集計とバー比較。事務所軸は管理体制 (人的責任)、
水系軸は地理的責任を反映する。</p>

<h3>実装</h3>
{code('''
office_pv = ALL.groupby("office").agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
).sort_values("n", ascending=True)

wc_pv = ALL.groupby("watercourse").agg(
    n=("name", "count"),
    total_m3s=("discharge_m3s", "sum"),
).sort_values("total_m3s", ascending=True)
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: 同じ 15 機場を 2 つの集計軸で並列に見ることで
「組織責任 (誰)」と「地理責任 (どこ)」の両面が読める。</p>
{figure("assets/L35_fig8_office_watercourse.png", "管理事務所別 (左) と 海岸/河川名別 (右) — 組織と地理の責任分担")}
<p><b>読み取り (左バー — 事務所別)</b>:</p>
<ul>
<li><b>東部建設事務所</b>: 福山市の 6 件を一手に管理 (= 福山市の機場すべて)。
<b>{ALL[ALL['office']=='東部建設事務所']['discharge_m3s'].sum():.1f} m³/s</b> で県内最大の管理事務所。</li>
<li><b>西部建設事務所</b>: 広島市の 4 件を管理 (新安川 第一/第二 + 岡ノ下川 + 尾崎川)。
{ALL[ALL['office']=='西部建設事務所']['discharge_m3s'].sum():.1f} m³/s。</li>
<li><b>支所体制</b>: 三原支所 (尾道) + 廿日市支所 (大竹) + 東広島支所 (竹原) で
各 1〜2 件管理。本所と支所の階層的責任分担。</li>
<li>事務所別の<b>容量シェア</b>: 東部 {ALL[ALL['office']=='東部建設事務所']['discharge_m3s'].sum()/total_m3s*100:.0f}% +
西部 {ALL[ALL['office']=='西部建設事務所']['discharge_m3s'].sum()/total_m3s*100:.0f}% で<b>2 大事務所で大半</b>。</li>
</ul>
<p><b>読み取り (右バー — 水系別)</b>:</p>
<ul>
<li><b>同名水系の重複</b>: 「新安川」は 2 件 (第一/第二)。
これは同じ水系の<b>段階的整備</b>を反映。</li>
<li><b>大規模水系 vs 小規模水系</b>: 手城川 (30 m³/s 単独) や岡ノ下川 (30 m³/s 単独) のような
大規模 1 機水系と、芦田川支流の小規模水系 (才町川 / 木曽丸川 各 2 m³/s) が混在。</li>
<li><b>港湾海岸 3 水系</b> (大竹・瀬戸田・竹原) は<b>各 1 機</b>のみ。
港湾海岸エリアは<b>1 機で完結</b>する小規模整備パターン。</li>
</ul>
"""
sections.append(("分析 7: 事務所別管理と水系別配置", sec10))


# === Section 11: 分析 8 — 上位機場プロファイル レーダー ===
sec11 = f"""
<h3>狙い</h3>
<p>容量上位 6 機場の<b>4 軸プロファイル</b>を<b>レーダーチャート</b>で並べ、
各機場の特性 (バランス型 vs 偏った型) を比較する。</p>

<h3>手法 (リテラシレベル解説)</h3>
<p><b>レーダーチャート (Radar Chart)</b>:</p>
<ul>
<li><b>直感的説明</b>: 多項目の指標を<b>放射状の軸</b>に配置し、各軸の値を線で結ぶ。
レーダー (回転式アンテナ) の探知範囲のような図に見えるため命名。</li>
<li><b>本記事の 4 軸</b>:
(1) 排水容量 (m³/s)、(2) ポンプ台数、(3) 1 台あたり吐出量 (m³/s/台)、
(4) 1km 圏内浸水想定面積 (ha)。</li>
<li><b>正規化</b>: 各軸は 15 機場全体の最大値で割って 0〜1 にする。
これで「全体の中での相対的な強さ」が読める。</li>
<li><b>形の意味</b>: 大きな多角形 = 全方位的に強い機場。
偏った形 = 特定の軸に特化した機場。</li>
<li><b>限界</b>: 6 つ以上の軸ではレーダーが読みにくい。
4-5 軸が見やすい上限。</li>
</ul>

<h3>実装</h3>
{code('''
top6 = gdf.nlargest(6, "discharge_m3s")
norm_data = pd.DataFrame({{
    "容量": top6["discharge_m3s"] / gdf["discharge_m3s"].max(),
    "台数": top6["n_pumps"] / gdf["n_pumps"].max(),
    "1台あたり": top6["m3s_per_pump"] / gdf["m3s_per_pump"].max(),
    "浸水": top6["flood_area_ha_in_buffer"] / gdf["flood_area_ha_in_buffer"].max(),
}})

fig, axes = plt.subplots(2, 3, figsize=(14, 9), subplot_kw=dict(polar=True))
angles = np.linspace(0, 2 * np.pi, 4, endpoint=False).tolist()
angles += angles[:1]

for ax, (i, row) in zip(axes.flat, top6.iterrows()):
    values = norm_data.loc[i].tolist() + [norm_data.loc[i].iloc[0]]
    ax.plot(angles, values, color=color, linewidth=2)
    ax.fill(angles, values, color=color, alpha=0.25)
''')}

<h3>図と読み取り</h3>
<p><b>なぜこの図か</b>: 上位 6 機場のプロファイルを<b>同じスケールの 6 つのレーダー</b>で
並べることで、機場ごとの<b>個性</b>が一目で比較できる。</p>
{figure("assets/L35_fig9_radar_top6.png", "上位 6 機場の 4 軸プロファイル — 容量/台数/効率/守備面積")}
<p><b>読み取り</b>:</p>
<ul>
<li><b>手城川 (福山, 平成14年, 30 m³/s, 1 台)</b>:
<b>容量 ★★★ + 1 台あたり ★★★ + 浸水面積 ★★</b>、台数 ★。<b>1 機集中型</b>の代表で、
1 台で 30 m³/s = 全機場最大の効率。冗長性は低いが処理能力が突出。</li>
<li><b>岡ノ下川 (広島佐伯, 平成1年, 30 m³/s, 3 台)</b>:
<b>容量 ★★★ + 浸水面積 ★★★ + 台数 ★★</b>。バランス型で広島市の最重要機場。
3 台体制で冗長性も担保。</li>
<li><b>羽原川 (福山, 平成27年, 11 m³/s, 4 台)</b>:
<b>台数 ★★★</b> (4 台は最多)、容量 ★★、1 台あたり ★。<b>多台数分散型</b>。
近年建設で冗長性重視の設計。</li>
<li><b>尾崎川 (広島安芸, 昭和59年, 9 m³/s, 3 台)</b>: 中型機の標準形。
浸水面積も中程度で、矢野エリアの安定守備。</li>
<li><b>本川 (竹原, 平成25年, 9 m³/s, 2 台)</b>: 容量・台数とも中型、浸水面積大。
近年建設でモダンな設計。</li>
<li><b>柏 (竹原, 平成17年, 8.2 m³/s, 3 台) — 港湾海岸</b>:
唯一の港湾海岸トップ 6 入り。河川機場と同等規模の性能。</li>
<li><b>機場の "個性" 整理</b>: (a) 1 機集中型 (手城川)、(b) バランス型 (岡ノ下川)、
(c) 多台数分散型 (羽原川) の<b>3 設計思想</b>が共存。
これは整備時期 + 用地条件 + 維持管理体制で選ばれる。</li>
</ul>
"""
sections.append(("分析 8: 上位 6 機場のプロファイル比較 (レーダー)", sec11))


# === Section 12: 仮説検証と考察 ===
sec12_table = hypos_df.to_html(index=False, classes='', border=0)
sec12 = f"""
<p>H1〜H6 の検証結果を 1 表で示す。<b>全 6 仮説が支持</b>された。</p>
{sec12_table}

<h3>総括: 広島県の内水浸水防御戦略</h3>
<p>2 dataset から再構成した排水機場 {n_total} 件の構造分析により、以下の<b>5 つの設計思想</b>が読み取れる。</p>
<ul>
<li><b>(1) 河川主導の内水排水</b>: 河川 12 件 ({n_r/n_total*100:.0f}%) + 容量 {total_m3s_r/total_m3s*100:.0f}% で、
内水浸水の主戦場は<b>河川合流低地</b>。港湾海岸はサブカテゴリ ({n_h/n_total*100:.0f}% / 容量 {total_m3s_h/total_m3s*100:.0f}%)。
これは<b>「内水氾濫の主因が河川合流部の堰き止め」</b>という工学的常識と整合。</li>
<li><b>(2) 福山平野への重点投資</b>: 河川機場 {n_r} 件のうち福山市が <b>{n_fukuyama} 件 ({n_fukuyama/n_r*100:.0f}%)</b>。
広島市 ({n_hiroshima} 件) と合わせて<b>2 大都市で {(n_fukuyama+n_hiroshima)/n_total*100:.0f}%</b>。
福山平野の<b>干拓地特性</b>が機場集中の構造的理由。</li>
<li><b>(3) 上位 2 機場への 46% 集中</b>: 手城川 (福山, 30 m³/s) + 岡ノ下川 (広島佐伯, 30 m³/s)
の 2 機で県全容量の <b>{top2_share:.0f}%</b>。Gini = {gini:.3f} の高度集中。
<b>「重要拠点に大型機 1 機」</b>が広島県の整備戦略。</li>
<li><b>(4) 浸水想定との 93% 整合</b>: 全 15 機場のうち 14 件 ({near_flood_n/len(gdf)*100:.0f}%) が
浸水想定 1km 圏内に立地。<b>機場 = 浸水想定エリアの最低標高点</b>という工学的必然を実証。
立地ミスではなく<b>意図的にハザードの中</b>に置かれている。</li>
<li><b>(5) 機械の大型化トレンド</b>: 平成期建設機は昭和期の <b>{heisei_per_pump/showa_per_pump:.1f} 倍</b>の
1 台あたり吐出量 ({heisei_per_pump:.1f} vs {showa_per_pump:.1f} m³/s/台)。
<b>1 台で扱える流量の機械的進化</b>が、現代の集中豪雨対応の基盤。</li>
</ul>

<p>本記事は<b>「排水機場は内水浸水防御の最後の砦」</b>という視点を実データで裏付けた。
{n_total} 件の機場が、<b>2 つの設置目的 (港湾海岸 / 河川)</b> で広島県の
<b>{total_m3s:.0f} m³/s</b> の強制排水容量を形成し、
<b>福山平野・広島市の干拓地と河川合流低地</b>に集中投資されている。
この網は<b>L8 (多重浸水想定) で扱った浸水域の内側に意図的に置かれた防御線</b>であり、
浸水想定 → 機場立地 → 排水という<b>多重防御</b>の最終層を成す。</p>

<h3>L32 / L33 / L34 / L35 シリーズ比較表</h3>
<table>
<tr><th>軸</th><th>L32 外郭</th><th>L33 係留</th><th>L34 交通</th><th>L35 排水機場 (本記事)</th></tr>
<tr><td>件数</td><td>842</td><td>1,224</td><td>296</td><td><b>{n_total}</b></td></tr>
<tr><td>カテゴリ軸</td><td>港湾/漁港</td><td>港湾/漁港</td><td>港湾/漁港</td><td><b>港湾海岸/河川</b></td></tr>
<tr><td>主指標</td><td>延長 m</td><td>延長 m</td><td>面積 m²</td><td><b>吐出量 m³/s + ポンプ台数</b></td></tr>
<tr><td>地理範囲</td><td>瀬戸内海岸</td><td>瀬戸内海岸</td><td>瀬戸内海岸</td><td>沿岸 + <b>内陸河川合流部</b></td></tr>
<tr><td>主役立地</td><td>防波堤 (海面)</td><td>岸壁 (汀線)</td><td>道路 (陸地)</td><td><b>低地・干拓地</b></td></tr>
<tr><td>機能</td><td>波防御</td><td>船接岸</td><td>陸海接続</td><td><b>内水排水</b></td></tr>
<tr><td>関係する浸水</td><td>津波</td><td>—</td><td>—</td><td><b>河川浸水想定</b></td></tr>
<tr><td>集中度</td><td>上位 3 港 44%</td><td>上位 3 港 46%</td><td>上位 3 港 50%</td><td><b>上位 2 機 {top2_share:.0f}%</b></td></tr>
<tr><td>整備時期</td><td>明治-現代</td><td>大正-現代</td><td>戦後-現代</td><td><b>1965-2016 (51年間)</b></td></tr>
</table>
<p><b>L32-L34 が「港湾」の階層、L35 は「内水排水」の独立施設</b>。
本シリーズで広島県の<b>「外水→内水」防御の全層</b>を扱い終えた。</p>
"""
sections.append(("仮説検証と考察", sec12))


# === Section 13: 発展課題 ===
sec13 = f"""
<h3>結果 X1 → 新仮説 Y1 → 課題 Z1: 集水区の正確な範囲特定</h3>
<ul>
<li><b>結果 X1</b>: 本記事は 1 km バッファを「守備範囲の目安」として用いたが、
これは概算であり、各機場の<b>実集水区</b>は機場ごとに異なる。
特に大型機場 (手城川 30 m³/s) は 1 km を超える広範囲を担う可能性。</li>
<li><b>新仮説 Y1</b>: 各機場の集水区面積は<b>排水容量に比例する</b>はずで、
<b>「容量 1 m³/s あたり 集水面積 X ha」</b>の経験則 (排水計画の標準値) が存在する。
広島県の機場ではこの X が日本全国平均と比べて大きいか小さいか?</li>
<li><b>課題 Z1</b>: 国交省<b>排水機場台帳</b>または広島県管理の<b>排水計画書</b> から
各機場の集水区ポリゴンを取得し、本記事の容量と散布。
<b>「容量/集水面積」の経験則</b>を実証的に同定。
これは<b>機場の容量設計指標</b>として全国比較の基礎データになる。</li>
</ul>

<h3>結果 X2 → 新仮説 Y2 → 課題 Z2: 機場運転実績との照合</h3>
<ul>
<li><b>結果 X2</b>: 本記事は機場の<b>容量 (設計値)</b> を扱った。
実際の運転時間・処理水量・故障率は別データ。</li>
<li><b>新仮説 Y2</b>: 平成期建設の大型機 (手城川 30 m³/s, 岡ノ下川 30 m³/s) は
近年の集中豪雨で<b>頻繁に運転</b>している。
昭和期建設の小型機 (御幸 3.22 m³/s) は近年の災害で<b>容量不足</b>に陥っている可能性。</li>
<li><b>課題 Z2</b>: 広島県管理の機場<b>運転日誌</b> (10 分間隔の運転データ)
を取得し、降雨イベントごとの運転時間・処理水量を機場ごとに集計。
<b>容量超過率 (実流入 / 容量)</b> を計算し、容量不足機場を識別。
これは<b>機場更新の優先順位付け</b>の実務基礎データ。
本記事の新安川第一→第二の増設例 (62 年差) のような<b>増設判断</b> の根拠になる。</li>
</ul>

<h3>結果 X3 → 新仮説 Y3 → 課題 Z3: 内水ハザードマップとの統合</h3>
<ul>
<li><b>結果 X3</b>: 本記事は DoBoX の<b>河川 (外水) 浸水想定</b> と機場立地を比較した。
内水浸水想定とは別物 (内水ハザードマップは市町ごとに別データ)。</li>
<li><b>新仮説 Y3</b>: 各機場の集水区内には<b>内水ハザードマップで想定される浸水範囲</b>があり、
<b>機場容量の不足 = 内水ハザードの大きさ</b>と相関するはず。</li>
<li><b>課題 Z3</b>: 広島市・福山市の<b>内水ハザードマップ</b> (PDF/Shapefile) を取得し、
本記事の機場 1km 圏内の<b>内水想定浸水域</b>と河川 (外水) 想定浸水域を別レイヤで重ね、
<b>外水と内水の二重防御マップ</b>を作る。
機場容量と内水想定深度の相関を散布し、容量増強候補を識別。
L8 (多重浸水想定) シリーズに<b>内水版</b>を加える発展形。</li>
</ul>

<h3>結果 X4 → 新仮説 Y4 → 課題 Z4: 機場立地の標高・地形分析</h3>
<ul>
<li><b>結果 X4</b>: 機場は<b>浸水想定エリア内の最低標高点</b>に立地するはず ({near_flood_n/len(gdf)*100:.0f}% が 1km 圏内)。</li>
<li><b>新仮説 Y4</b>: 機場の標高は周囲半径 1 km の<b>最低 5 パーセンタイル</b>程度。
すなわち排水範囲の中で最も低い場所に置かれる工学的必然。</li>
<li><b>課題 Z4</b>: 国土地理院<b>5m DEM</b> を読み込み、各機場の<b>標高 (m)</b> と
1 km 圏内の<b>標高分布</b>を取得。機場の標高が圏内の何パーセンタイルかを計算。
<b>「機場立地は集水域の最低 X%」</b>の数値を実証 (X≈5? あるいは中央値？)。
これは<b>機場立地の工学法則</b>として、新規機場計画の指針になる。
DoBoX の DEM (3D 都市モデル L14 関連) を活用できる発展。</li>
</ul>

<h3>結果 X5 → 新仮説 Y5 → 課題 Z5: 福山平野の干拓地史と機場整備</h3>
<ul>
<li><b>結果 X5</b>: 河川機場 12 件のうち福山市が 6 件 (50%)。
これは福山平野の<b>干拓地特性</b>を反映するはず。</li>
<li><b>新仮説 Y5</b>: 機場の建設時期は<b>干拓地の造成順序</b> (江戸期 → 明治 → 戦後) に対応する。
新しい機場ほど<b>新しい干拓地</b> (戦後の埋立地) を守る。</li>
<li><b>課題 Z5</b>: 福山市の<b>干拓地造成史</b> (歴史地理研究) を文献から取得し、
本記事の機場の<b>建設年代と干拓造成年代</b>を散布。<b>「干拓地完成 → X 年後に機場整備」</b>の
タイムラグを同定。歴史的な<b>地形改変と都市インフラの順序</b>を解明する歴史地理研究。
DoBoX 都市計画基礎調査 (L13-L29) と統合可能。</li>
</ul>

<h3>結果 X6 → 新仮説 Y6 → 課題 Z6: 「総排水量(m3)」単位の確認</h3>
<ul>
<li><b>結果 X6</b>: データ列名は「総排水量(m3)」だが、値の桁から本記事は m³/s と解釈した。</li>
<li><b>新仮説 Y6</b>: 公式単位は m³/s であるはずだが、列名が m³ となっているのは
<b>データ整備時の単位記述ミス</b> または<b>編纂時の慣例</b>と推察。</li>
<li><b>課題 Z6</b>: DoBoX 管理者 (広島県) に問い合わせ、
列名の単位が m³/s か m³ かを確認。仮に m³/s が正しいなら<b>列名修正の提案</b>を行う。
データ品質改善のフィードバックループ。
ライセンス上の利用条件で「データ品質に関する問い合わせは歓迎」と DoBoX が宣言している場合は、
<b>市民データレビュー</b>として活用できる学習者参加型の発展。</li>
</ul>
"""
sections.append(("発展課題", sec13))


# レンダリング
html = render_lesson(
    num=35,
    title=f"排水機場 2 件統合分析 — 広島県 {n_total} 機場 ({n_h} 港湾海岸 + {n_r} 河川) が支える内水浸水防御の最終線",
    tags=["排水機場", "内水氾濫", "ポンプ", "GIS", "geopandas", "浸水想定", "干拓地", "強制排水"],
    time="40 分",
    level="リテラシ",
    data_label="DoBoX 排水機場 2 dataset (1269 港湾海岸, 1273 河川)",
    sections=sections,
    script_filename="L35_drainage_pumps.py",
)

(LESSONS / "L35_drainage_pumps.html").write_text(html, encoding="utf-8")
print(f"  saved L35_drainage_pumps.html ({time.time()-t1:.1f}s)", flush=True)


print(f"\n=== 完了: 全所要 {time.time()-t0:.1f}s ===", flush=True)
