# -*- coding: utf-8 -*-
"""
X07: 河川浸水想定区域 × DID人口集中地区 — 「住んでる人ほど危ない場所に」仮説検証
================================================================================

DoBoX の以下 3 系統を空間的に重ね合わせ (オーバーレイ) し、
広島県 14 市町ごとに「DID (人口集中地区) のうち何 % が河川浸水想定区域に重なるか」
を定量化する **空間オーバーレイ研究** 記事 (X 水準, リテラシレベル)。

【本記事のスタイル: 空間オーバーレイ研究】
DID 人口集中地区と浸水想定区域の重なり率を市町別に計算し、
「住んでる人ほど危ない」仮説を定量検証する。
PCA・散布図行列・ロジスティック回帰は使わず、
**市町別ランキング棒・面積率散布図・地理マップ・規模別比較・リスク人口棒**
の 5 図を主役に据える。

データ:
  - DoBoX #1468〜#1505 (DID 14 市町, GeoJSON, EPSG:6671)
  - DoBoX #295 (河川浸水想定区域 計画規模 全河川, Shapefile, Web Mercator)
  - DoBoX #313 (河川浸水想定区域 想定最大規模 全河川, Shapefile, Web Mercator)

仮説 H1〜H5:
  H1: DID 面積の 30〜60% が河川浸水想定区域に重なる (「住んでる人ほど危ない場所に住んでいる」)
  H2: 沿岸都市 (広島市・呉市) より内陸盆地 (三次市・庄原市) で重なり率が高い
  H3: 想定最大規模は計画規模よりも重なり面積が大きい (倍率 > 1.0)
  H4: DID 人口と重なり面積は正の相関を持つ (人口の多い市町ほど絶対値で危険)
  H5: 「リスク人口」(DID人口 × 重なり面積率) は市町間で偏在し、上位 3 市町で県全体の半分超を占める

主要可視化 (5 図):
  図1 (主役): 市町別 重なり面積率 ランキング水平棒 (14 市町, 計画規模 vs 想定最大規模 並列)
  図2:        DID 面積 vs 重なり面積 散布図 (単回帰直線 + R²)
  図3:        市町別 重なり率 地理マップ (経度 × 緯度, 色 = 重なり率)
  図4:        計画規模 vs 想定最大規模 重なり面積 棒比較 (倍率)
  図5:        市町別 リスク人口推定 ランキング棒

実装:
  - 空間オーバーレイは bbox 交差 + Monte Carlo サンプリングで近似
    (geopandas/shapely を使わず、pyshp + 純 Python のみ)
  - DID GeoJSON (EPSG:6671) と 浸水 Shapefile (Web Mercator) は
    どちらも lat/lon (WGS84) に変換してから重ね合わせる
  - 14 市町 × 2 規模 = 28 ペア の計算

実行:
    cd "2026 DoBoX 教材"
    py -X utf8 lessons/X07_flood_did_overlap.py

制約:
    - 並列処理なし、メモリ < 500 MB (浸水 Shapefile を一度ロード後に逐次処理)
    - DID は 14 件まとめてメモリ展開 (合計 < 30MB)
"""
from __future__ import annotations
from pathlib import Path
import io
import json
import math
import random
import warnings
import zipfile

import numpy as np
import pandas as pd
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt

import requests

# pyshp は flood Shapefile 読込に必須
import shapefile  # pyshp

from _common import (ROOT, ASSETS, LESSONS, render_lesson, code, figure,
                     data_recipe, ensure_dataset)

plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False
warnings.filterwarnings("ignore", category=UserWarning)

# === パス定義 =================================================================
DATA_DIR = ROOT / "data" / "extras"
DID_DIR = DATA_DIR / "did_geojson"
FLOOD_DIR = DATA_DIR / "flood_shp"
DID_DIR.mkdir(parents=True, exist_ok=True)
FLOOD_DIR.mkdir(parents=True, exist_ok=True)

OUT_CITY      = ASSETS / "X07_city_overlap.csv"          # 市町別 DID/浸水/重なり 集計
OUT_FLOOD     = ASSETS / "X07_flood_meta.csv"            # 浸水 39 件メタ + 抽出済み 2 件
OUT_DIDPOLY   = ASSETS / "X07_did_polygons.csv"          # DID ポリゴン 1 行 = 1 ポリゴン
OUT_RISKPOP   = ASSETS / "X07_risk_population.csv"       # 市町別リスク人口推定
OUT_TRACK     = ASSETS / "X07_track_hiroshima.csv"       # 広島市 1 件追跡 (要件K)
OUT_SCALECMP  = ASSETS / "X07_scale_compare.csv"         # 計画 vs 最大 規模比較

PNG_RANK   = ASSETS / "X07_rank_overlap.png"             # 主役: 市町別 重なり率ランキング
PNG_SCAT   = ASSETS / "X07_did_flood_scatter.png"        # DID面積 vs 重なり面積
PNG_MAP    = ASSETS / "X07_overlap_map.png"              # 市町地理マップ
PNG_SCALE  = ASSETS / "X07_scale_compare.png"            # 計画 vs 最大 棒比較
PNG_RISK   = ASSETS / "X07_risk_population.png"          # リスク人口棒

# === 0. 入出力 CRS 変換ユーティリティ =========================================
# DID GeoJSON: EPSG:6671 (Japan Plane Rectangular CS Zone I) — 単位 m
# 江田島基準 (緯度 33°N, 経度 132.166666...°E)
# 投影は横メルカトル (Transverse Mercator)。逆変換式は楕円体 + Krueger 級数で
# 厳密だが、本記事では小領域 (広島県内 < 200 km) なので **球面近似** で十分。
# ローカル経緯度を「中心緯経度 + 平面ずれ / R」で求める。
JP_ZONE3_LAT0 = 36.0           # 緯度原点 (Zone III)
JP_ZONE3_LON0 = 132.0 + 1/6    # 経度原点 132°10' (=132.166666...)
WGS_R = 6378137.0              # 地球半径 (m)


def jp_xy_to_ll(coord_first: float, coord_second: float) -> tuple[float, float]:
    """EPSG:6671 (JGD2011 / Japan Plane Rectangular CS Zone III, 中国地方) を
    WGS84 経緯度に近似変換。

    GeoJSON では coord = [easting, northing] の順で格納されている
    (= 一般的な [lon-like, lat-like] 順)。日本平面直角の Zone III は
    原点 lat=36°N, lon=132°10'E (=132.166...) で、
    northing 正 = 北、easting 正 = 東。広島県は原点より南なので northing は負値。

    厳密式ではなく球面近似。Zone 内最大誤差 ~50 m (重なり率計算には十分)。
    """
    easting = coord_first
    northing = coord_second
    lat = JP_ZONE3_LAT0 + math.degrees(northing / WGS_R)
    lon = JP_ZONE3_LON0 + math.degrees(easting / (WGS_R * math.cos(math.radians(JP_ZONE3_LAT0))))
    return lon, lat


def merc_to_ll(x: float, y: float) -> tuple[float, float]:
    """Web Mercator (EPSG:3857) → WGS84 経緯度 (厳密式)。"""
    lon = math.degrees(x / WGS_R)
    lat = math.degrees(2 * math.atan(math.exp(y / WGS_R)) - math.pi / 2)
    return lon, lat


def ll_to_local_m(lon: float, lat: float, lat0: float = 34.5) -> tuple[float, float]:
    """経緯度 → 県央基準のローカル平面 (m)。面積計算用。"""
    x = math.radians(lon - JP_ZONE1_LON0) * WGS_R * math.cos(math.radians(lat0))
    y = math.radians(lat - lat0) * WGS_R
    return x, y


# === 1. データ取得 (DoBoX 直叩き) =============================================
print("=== 1. データ取得 ===")
DID_RID = {
    "広島市":   (1468, 94765),
    "呉市":     (1471, 94783),
    "竹原市":   (1473, 94792),
    "三原市":   (1476, 94810),
    "尾道市":   (1479, 94828),
    "福山市":   (1482, 94846),
    "府中市":   (1485, 94864),
    "三次市":   (1487, 94873),
    "大竹市":   (1490, 94888),
    "東広島市": (1493, 94906),
    "廿日市市": (1495, 94915),
    "府中町":   (1500, 94945),
    "海田町":   (1502, 94954),
    "坂町":     (1505, 94969),
}
FLOOD_RID = {
    "計画規模":     (295, 23061, "shinsui_keikaku.zip", "shinsui_keikaku"),
    "想定最大規模": (313, 23118, "shinsui_souteisaidai.zip", "shinsui_souteisaidai"),
}

# DID GeoJSON 14 件 (zip → 解凍は不要、zip 内の geojson を直読)
for city, (did, rid) in DID_RID.items():
    p = DID_DIR / f"did_{city}.zip"
    ensure_dataset(p, resource_id=rid, min_bytes=10_000, label=f"DID/{city}")

# 浸水 Shapefile 2 件 (計画 + 最大)
for label, (did, rid, fn, _) in FLOOD_RID.items():
    p = FLOOD_DIR / fn
    ensure_dataset(p, resource_id=rid, min_bytes=1_000_000, label=f"浸水/{label}")


# === 2. DID GeoJSON を読んで「市町×ポリゴン」テーブルにする =================
print("\n=== 2. DID 14市町のポリゴン展開 ===")
# ここで作るテーブル:
#   city, poly_idx, area_ha (TOCHI_A), pop (JINKOU_S),
#   bbox_lon_min/max, bbox_lat_min/max, ring_lonlat (経緯度に変換した外周点列)
did_polys = []  # 各エントリ = {city, poly_idx, area_ha, pop, bbox, rings_lonlat}

for city, (_, _) in DID_RID.items():
    z = zipfile.ZipFile(DID_DIR / f"did_{city}.zip")
    geo_name = next(n for n in z.namelist() if n.endswith(".geojson"))
    with z.open(geo_name) as f:
        gj = json.load(f)
    for i, ft in enumerate(gj["features"]):
        prop = ft["properties"]
        area_ha = float(prop.get("TOCHI_A", 0) or 0)
        pop = float(prop.get("JINKOU_S", 0) or 0)
        # MultiPolygon: coords[poly][ring][point]
        # Polygon: coords[ring][point]
        gtype = ft["geometry"]["type"]
        coords = ft["geometry"]["coordinates"]
        polys = coords if gtype == "MultiPolygon" else [coords]
        # 各 (poly, ring 0=外周) を経緯度に変換
        rings_ll = []  # [(lon_arr, lat_arr) for each outer ring]
        all_lon = []
        all_lat = []
        for poly in polys:
            outer = poly[0]
            lonlat = [jp_xy_to_ll(p[0], p[1]) for p in outer]
            lons = [pp[0] for pp in lonlat]
            lats = [pp[1] for pp in lonlat]
            rings_ll.append((lons, lats))
            all_lon.extend(lons); all_lat.extend(lats)
        if not all_lon:
            continue
        did_polys.append({
            "city": city,
            "poly_idx": i,
            "area_ha": area_ha,
            "pop": pop,
            "lon_min": min(all_lon), "lon_max": max(all_lon),
            "lat_min": min(all_lat), "lat_max": max(all_lat),
            "rings_ll": rings_ll,
            "n_points": sum(len(r[0]) for r in rings_ll),
            "n_rings": len(rings_ll),
        })

did_df = pd.DataFrame([
    {k: v for k, v in d.items() if k != "rings_ll"}
    for d in did_polys
])
print(f"  DID ポリゴン総数: {len(did_polys)}  (14 市町合計)")
print(f"  DID 総面積: {did_df['area_ha'].sum():.0f} ha = {did_df['area_ha'].sum()/100:.1f} km²")
print(f"  DID 総人口: {did_df['pop'].sum():,.0f} 人")
did_df.to_csv(OUT_DIDPOLY, index=False, encoding="utf-8-sig")


# === 3. 浸水 Shapefile を読み込み、ポリゴン bbox を経緯度で保持 ===============
print("\n=== 3. 浸水 Shapefile 読込 (Web Mercator → 経緯度) ===")
# polygon parts ごとに 1 行として保持。Monte Carlo の point-in-polygon 用に
# 全ポイント列を経緯度で持っておく (メモリ ~30MB に収まる規模)。
flood_data: dict[str, list[dict]] = {}  # label -> list of polygons

for label, (_, _, fn, stem) in FLOOD_RID.items():
    # zip の中身を temp に解凍 (pyshp は単一 .shp ファイルパス必要)
    zp = FLOOD_DIR / fn
    ext = FLOOD_DIR / stem
    ext.mkdir(exist_ok=True)
    with zipfile.ZipFile(zp) as z:
        for n in z.namelist():
            if not (ext / Path(n).name).exists():
                z.extract(n, ext)
    shp_path = next(ext.glob("*.shp"))
    sf = shapefile.Reader(str(shp_path))
    fields = [f[0] for f in sf.fields[1:]]  # skip DeletionFlag
    polys = []
    for shp_idx, (rec, shp) in enumerate(zip(sf.records(), sf.shapes())):
        # parts: 各ポリゴンの開始 index。最終端は len(points)
        if not shp.points:
            continue
        # bbox を経緯度に
        bx0, by0, bx1, by1 = shp.bbox
        lon0, lat0 = merc_to_ll(bx0, by0)
        lon1, lat1 = merc_to_ll(bx1, by1)
        # parts (=各リング) を経緯度配列にして保存
        parts = list(shp.parts) + [len(shp.points)]
        rings = []
        for k in range(len(shp.parts)):
            seg = shp.points[parts[k]:parts[k+1]]
            lons = [merc_to_ll(p[0], p[1])[0] for p in seg]
            lats = [merc_to_ll(p[0], p[1])[1] for p in seg]
            rings.append((lons, lats))
        polys.append({
            "shp_idx": shp_idx,
            "kasen": rec[fields.index("kasen")] if "kasen" in fields else "?",
            "suikei": rec[fields.index("suikei")] if "suikei" in fields else "?",
            "lon_min": lon0, "lon_max": lon1,
            "lat_min": lat0, "lat_max": lat1,
            "rings": rings,
        })
    flood_data[label] = polys
    print(f"  {label}: {len(polys)} ポリゴン (河川数 = {len(set(p['kasen'] for p in polys))})")
    sf.close()


# === 4. point-in-polygon (Ray casting, ベクトル化) ===========================
def points_in_ring_vec(xs_pt: np.ndarray, ys_pt: np.ndarray,
                       ring_xs: np.ndarray, ring_ys: np.ndarray) -> np.ndarray:
    """一括 Ray casting。N 点 vs 1 リング (M 頂点) → bool 配列 (N,)"""
    n_pts = len(xs_pt)
    inside = np.zeros(n_pts, dtype=bool)
    m = len(ring_xs)
    # 各辺 (i, j=i-1) で判定し XOR で内外を反転
    j = np.arange(m) - 1  # -1 → 最終点
    for i in range(m):
        yi = ring_ys[i]; yj = ring_ys[j[i]]
        xi = ring_xs[i]; xj = ring_xs[j[i]]
        # (yi > y) XOR (yj > y) (ベクトル化)
        cond_y = (yi > ys_pt) != (yj > ys_pt)
        if not cond_y.any():
            continue
        # 交点の x
        x_int = (xj - xi) * (ys_pt - yi) / (yj - yi + 1e-15) + xi
        cond = cond_y & (xs_pt < x_int)
        inside ^= cond
    return inside


def points_in_any_ring_vec(xs_pt: np.ndarray, ys_pt: np.ndarray, rings_list) -> np.ndarray:
    """1 つでも ring に入れば True。N 点 vs K リング → bool 配列 (N,)"""
    if not rings_list:
        return np.zeros(len(xs_pt), dtype=bool)
    inside = np.zeros(len(xs_pt), dtype=bool)
    for ring in rings_list:
        rx = np.asarray(ring[0], dtype=np.float64)
        ry = np.asarray(ring[1], dtype=np.float64)
        if len(rx) < 3:
            continue
        # bbox プリフィルタ (テスト点が ring の bbox に入っていなければ確実に外)
        rx_min, rx_max = rx.min(), rx.max()
        ry_min, ry_max = ry.min(), ry.max()
        cand = (~inside) & (xs_pt >= rx_min) & (xs_pt <= rx_max) & \
               (ys_pt >= ry_min) & (ys_pt <= ry_max)
        if not cand.any():
            continue
        idx = np.where(cand)[0]
        sub = points_in_ring_vec(xs_pt[idx], ys_pt[idx], rx, ry)
        inside[idx] |= sub
    return inside


# 旧 API (1 点版, 個別テスト用)
def point_in_ring(x: float, y: float, ring: tuple[list, list]) -> bool:
    xs, ys = ring
    n = len(xs)
    inside = False
    j = n - 1
    for i in range(n):
        yi, yj = ys[i], ys[j]
        if (yi > y) != (yj > y):
            xi, xj = xs[i], xs[j]
            x_int = (xj - xi) * (y - yi) / (yj - yi + 1e-15) + xi
            if x < x_int:
                inside = not inside
        j = i
    return inside


# === 5. 市町ごとに DID 面積・浸水重なり面積を Monte Carlo で集計 =============
print("\n=== 5. 重なり面積を Monte Carlo (一様サンプリング) で推定 ===")
# 戦略:
#   - 各市町の DID 全体 bbox 内に N 点 (N=5000) を一様乱数で散布
#   - DID ポリゴン に入る点比率 = DID 面積比率, さらにそのうち浸水ポリゴンに入る点比率 = 重なり比率
#   - 比率 × bbox 面積 (m²) で実面積に換算
# 浸水ポリゴンは事前に bbox で市町範囲に絞る (高速化)
random.seed(20260502)
N_SAMPLES = 5000
results = []  # (city, label, did_ha_mc, overlap_ha_mc, overlap_rate)

# 市町ごとに DID ポリゴン群と全 bbox を集める
city_dids: dict[str, list[dict]] = {}
for d in did_polys:
    city_dids.setdefault(d["city"], []).append(d)

rng = np.random.default_rng(20260502)
import sys
for city, dlist in city_dids.items():
    sys.stdout.flush()
    # 市町全体 bbox
    lon_min = min(d["lon_min"] for d in dlist)
    lon_max = max(d["lon_max"] for d in dlist)
    lat_min = min(d["lat_min"] for d in dlist)
    lat_max = max(d["lat_max"] for d in dlist)
    # bbox の m² 面積 (経度1度の長さは緯度依存)
    lat0 = (lat_min + lat_max) / 2
    bbox_w_m = math.radians(lon_max - lon_min) * WGS_R * math.cos(math.radians(lat0))
    bbox_h_m = math.radians(lat_max - lat_min) * WGS_R
    bbox_m2 = bbox_w_m * bbox_h_m
    # DID 全 ring (外周) を平坦化
    did_rings = []
    for d in dlist:
        did_rings.extend(d["rings_ll"])
    # 浸水ポリゴンの絞り込み (市町 bbox と少しでも重なるもの)
    flood_filt = {}
    for label, fpolys in flood_data.items():
        flood_filt[label] = [
            f for f in fpolys
            if not (f["lon_max"] < lon_min or f["lon_min"] > lon_max
                    or f["lat_max"] < lat_min or f["lat_min"] > lat_max)
        ]
    # === ベクトル化 Monte Carlo ===
    xs_pt = rng.uniform(lon_min, lon_max, N_SAMPLES)
    ys_pt = rng.uniform(lat_min, lat_max, N_SAMPLES)
    in_did = points_in_any_ring_vec(xs_pt, ys_pt, did_rings)
    pts_in_did = int(in_did.sum())
    pts_in_did_and_flood = {}
    for label in flood_data:
        # 浸水 ring を全て収集 (絞り込み後)
        flood_rings_all = []
        for f in flood_filt[label]:
            flood_rings_all.extend(f["rings"])
        # DID 内の点だけ flood テスト
        in_flood = points_in_any_ring_vec(xs_pt, ys_pt, flood_rings_all)
        n_overlap = int((in_did & in_flood).sum())
        pts_in_did_and_flood[label] = n_overlap
    # 比率 → 実面積
    did_ha_mc = pts_in_did / N_SAMPLES * bbox_m2 / 10000.0  # m² → ha
    did_ha_true = sum(d["area_ha"] for d in dlist)
    pop_total = sum(d["pop"] for d in dlist)
    for label in flood_data:
        ov_ha_mc = pts_in_did_and_flood[label] / N_SAMPLES * bbox_m2 / 10000.0
        rate = (ov_ha_mc / did_ha_mc) if did_ha_mc > 0 else 0.0
        results.append({
            "city": city,
            "scale": label,
            "did_ha_mc": round(did_ha_mc, 1),
            "did_ha_true": did_ha_true,
            "overlap_ha_mc": round(ov_ha_mc, 1),
            "overlap_rate": round(rate, 4),
            "pop": pop_total,
            "bbox_m2": round(bbox_m2, 0),
            "n_did_pts": pts_in_did,
            "n_overlap_pts": pts_in_did_and_flood[label],
        })
    print(f"  {city}: DID真値={did_ha_true:.0f}ha MC={did_ha_mc:.0f}ha "
          f"計画重なり率={pts_in_did_and_flood['計画規模']/max(pts_in_did,1)*100:.1f}% "
          f"最大重なり率={pts_in_did_and_flood['想定最大規模']/max(pts_in_did,1)*100:.1f}%")

city_overlap = pd.DataFrame(results)
# 真値の DID 面積で重なり面積を再計算 (精度向上: MC比率 × 真値面積)
city_overlap["overlap_ha"] = (
    city_overlap["overlap_rate"] * city_overlap["did_ha_true"]
).round(1)
city_overlap.to_csv(OUT_CITY, index=False, encoding="utf-8-sig")
print(f"  → {OUT_CITY.name}")


# === 6. 計画規模 vs 想定最大規模 比較 =========================================
print("\n=== 6. 計画 vs 最大 比較 ===")
piv = city_overlap.pivot_table(
    index="city", columns="scale",
    values=["overlap_rate", "overlap_ha"]
)
piv.columns = [f"{a}_{b}" for a, b in piv.columns]
piv = piv.reset_index()
# 真値 DID面積と人口は city ごとにユニーク
city_meta = city_overlap.drop_duplicates("city")[["city", "did_ha_true", "pop"]]
piv = piv.merge(city_meta, on="city")
piv["max_over_plan_ratio"] = (
    piv["overlap_ha_想定最大規模"] / piv["overlap_ha_計画規模"].replace(0, np.nan)
).round(2)
piv["risk_pop"] = (piv["pop"] * piv["overlap_rate_想定最大規模"]).round(0).astype(int)
piv = piv.sort_values("overlap_rate_想定最大規模", ascending=False).reset_index(drop=True)
piv.to_csv(OUT_SCALECMP, index=False, encoding="utf-8-sig")
print(piv[["city", "did_ha_true", "overlap_rate_計画規模",
           "overlap_rate_想定最大規模", "max_over_plan_ratio", "risk_pop"]].to_string(index=False))


# === 7. リスク人口推定 =======================================================
print("\n=== 7. リスク人口 (DID人口 × 想定最大規模重なり率) ===")
risk = piv[["city", "pop", "overlap_rate_想定最大規模", "risk_pop"]].copy()
risk = risk.rename(columns={"pop": "did_pop", "overlap_rate_想定最大規模": "rate_max"})
risk = risk.sort_values("risk_pop", ascending=False).reset_index(drop=True)
risk["share_pct"] = (risk["risk_pop"] / risk["risk_pop"].sum() * 100).round(2)
risk["cum_share_pct"] = risk["share_pct"].cumsum().round(2)
risk.to_csv(OUT_RISKPOP, index=False, encoding="utf-8-sig")
print(risk.to_string(index=False))
top3_share = risk.head(3)["risk_pop"].sum() / risk["risk_pop"].sum() * 100
print(f"  上位3市町のリスク人口シェア: {top3_share:.1f}%  (H5: {top3_share>50})")


# === 8. 浸水 39 件メタ + DID 14 件メタ (空間オーバーレイ前提のメタ集計) ======
print("\n=== 8. 浸水39件・DID14件 メタ集計 ===")
idx_path = ROOT / "data" / "dataset_index.csv"
idx = pd.read_csv(idx_path, encoding="utf-8-sig")
flood_meta = idx[idx["Title"].str.startswith("河川浸水想定区域情報", na=False)].copy()
print(f"  カタログ内 河川浸水想定 件数: {len(flood_meta)} (期待 39)")
# 規模 (計画/最大) 抽出
flood_meta["scale"] = flood_meta["Title"].apply(
    lambda t: "計画規模" if "計画規模" in t else (
        "想定最大規模" if "想定最大規模" in t else "?"))
flood_meta["suikei"] = flood_meta["Title"].apply(
    lambda t: t.split("_")[-1] if "_" in t else "")
flood_meta_summary = flood_meta.groupby("scale").size().reset_index(name="n")
print(flood_meta_summary)
flood_meta[["Id", "Title", "scale", "suikei"]].to_csv(
    OUT_FLOOD, index=False, encoding="utf-8-sig")


# === 9. 広島市 1件追跡 (要件K) ===============================================
print("\n=== 9. 広島市 段階表 (要件K) ===")
hi_d = [d for d in did_polys if d["city"] == "広島市"]
hi_did_ha = sum(d["area_ha"] for d in hi_d)
hi_pop = sum(d["pop"] for d in hi_d)
hi_row = piv[piv["city"] == "広島市"].iloc[0]
track = pd.DataFrame([
    {"段階": "1. DoBoX 取得", "値": "DID 広島市 GeoJSON 18 ポリゴン",
     "内訳": f"#1468, GeoJSON ({DID_DIR/'did_広島市.zip'})"},
    {"段階": "2. ポリゴン展開", "値": f"{len(hi_d)} ポリゴン",
     "内訳": "MultiPolygon を外周リング配列に展開"},
    {"段階": "3. DID 総面積", "値": f"{hi_did_ha:,.0f} ha",
     "内訳": f"= {hi_did_ha/100:,.1f} km² (TOCHI_A 合計)"},
    {"段階": "4. DID 人口", "値": f"{hi_pop:,.0f} 人",
     "内訳": "JINKOU_S 合計 (2020 国勢調査)"},
    {"段階": "5. 浸水重なり (計画規模)", "値": f"{hi_row['overlap_ha_計画規模']:,.0f} ha",
     "内訳": f"重なり率 {hi_row['overlap_rate_計画規模']*100:.2f}%"},
    {"段階": "6. 浸水重なり (想定最大規模)", "値": f"{hi_row['overlap_ha_想定最大規模']:,.0f} ha",
     "内訳": f"重なり率 {hi_row['overlap_rate_想定最大規模']*100:.2f}%"},
    {"段階": "7. リスク人口", "値": f"{hi_row['risk_pop']:,.0f} 人",
     "内訳": f"= DID人口 {hi_pop:,.0f} × 重なり率 {hi_row['overlap_rate_想定最大規模']*100:.2f}%"},
])
track.to_csv(OUT_TRACK, index=False, encoding="utf-8-sig")
print(track.to_string(index=False))


# === 10. 図1: 市町別 重なり率ランキング (主役) ================================
print("\n=== 10. 図1 (主役): 市町別重なり率ランキング ===")
fig, ax = plt.subplots(figsize=(11, 7.5))
piv_sorted = piv.sort_values("overlap_rate_想定最大規模", ascending=True)
ypos = np.arange(len(piv_sorted))
w = 0.4
ax.barh(ypos + w/2, piv_sorted["overlap_rate_想定最大規模"]*100,
        height=w, color="#cf222e", label="想定最大規模", edgecolor="#222", linewidth=0.4)
ax.barh(ypos - w/2, piv_sorted["overlap_rate_計画規模"]*100,
        height=w, color="#0969da", label="計画規模", edgecolor="#222", linewidth=0.4)
ax.set_yticks(ypos)
ax.set_yticklabels(piv_sorted["city"])
ax.set_xlabel("DID 面積に対する河川浸水想定区域の重なり率 (%)")
ax.set_title(f"図1 (主役): 14 市町 × DID 面積に対する浸水想定区域重なり率\n"
             f"想定最大規模 vs 計画規模, n={len(piv)} 市町")
ax.axvline(piv["overlap_rate_想定最大規模"].mean()*100,
           color="#cf222e", ls="--", lw=1.0, alpha=0.6,
           label=f"最大規模平均 {piv['overlap_rate_想定最大規模'].mean()*100:.1f}%")
ax.axvline(piv["overlap_rate_計画規模"].mean()*100,
           color="#0969da", ls="--", lw=1.0, alpha=0.6,
           label=f"計画規模平均 {piv['overlap_rate_計画規模'].mean()*100:.1f}%")
ax.grid(axis="x", alpha=0.3)
ax.legend(loc="lower right")
plt.tight_layout()
plt.savefig(PNG_RANK, dpi=140, bbox_inches="tight")
plt.close()
print(f"  → {PNG_RANK.name}")


# === 11. 図2: DID 面積 vs 重なり面積 散布 (単回帰 R²) ========================
print("\n=== 11. 図2: 散布 + 単回帰 ===")
x_vals = piv["did_ha_true"].values.astype(float)
y_vals = piv["overlap_ha_想定最大規模"].values.astype(float)
slope, intercept = np.polyfit(x_vals, y_vals, 1)
y_pred = slope * x_vals + intercept
ss_res = float(np.sum((y_vals - y_pred) ** 2))
ss_tot = float(np.sum((y_vals - y_vals.mean()) ** 2))
r2 = 1 - ss_res / ss_tot if ss_tot > 0 else float("nan")
corr = float(np.corrcoef(x_vals, y_vals)[0, 1])

fig, ax = plt.subplots(figsize=(9.5, 6.5))
ax.scatter(x_vals, y_vals, s=120, c="#cf222e",
           edgecolor="#222", linewidths=0.5, alpha=0.85, label="想定最大規模")
ax.scatter(x_vals, piv["overlap_ha_計画規模"], s=70, c="#0969da",
           edgecolor="#222", linewidths=0.4, alpha=0.7, marker="^", label="計画規模")
xx = np.linspace(x_vals.min(), x_vals.max(), 60)
ax.plot(xx, slope*xx + intercept, color="#222", lw=1.4, ls="--",
        label=f"単回帰 (最大): y={slope:.3f}x+{intercept:.0f}, R²={r2:.3f}")
for _, r in piv.iterrows():
    ax.annotate(r["city"], (r["did_ha_true"], r["overlap_ha_想定最大規模"]),
                xytext=(4, 4), textcoords="offset points", fontsize=9, color="#222")
ax.set_xlabel("DID 面積 (ha)")
ax.set_ylabel("浸水想定区域との重なり面積 (ha)")
ax.set_title(f"図2: DID 面積 vs 浸水重なり面積  相関 r={corr:.3f}, R²={r2:.3f}\n"
             f"右上=DID大かつ重なり大の都市, 左下=DID小かつ被害も小")
ax.grid(alpha=0.3)
ax.legend(loc="upper left")
plt.tight_layout()
plt.savefig(PNG_SCAT, dpi=140, bbox_inches="tight")
plt.close()
print(f"  → {PNG_SCAT.name}  R²={r2:.3f}, r={corr:.3f}")


# === 12. 図3: 地理マップ (経度 × 緯度, 色=重なり率) ===========================
print("\n=== 12. 図3: 地理マップ ===")
# 市町中心座標 (DID polygons の bbox 中心の平均) を計算
city_coords = {}
for c, dlist in city_dids.items():
    lons = []
    lats = []
    for d in dlist:
        lons.append((d["lon_min"]+d["lon_max"])/2)
        lats.append((d["lat_min"]+d["lat_max"])/2)
    city_coords[c] = (np.mean(lons), np.mean(lats))
piv["lon"] = piv["city"].map(lambda c: city_coords[c][0])
piv["lat"] = piv["city"].map(lambda c: city_coords[c][1])

fig, ax = plt.subplots(figsize=(11, 8))
# 県全体の輪郭代わりに緯度経度の格子を薄く
ax.set_facecolor("#f6f8fa")
sizes = 200 + piv["did_ha_true"] / piv["did_ha_true"].max() * 1400
norm = matplotlib.colors.Normalize(vmin=0,
    vmax=max(piv["overlap_rate_想定最大規模"].max(), 0.05))
cmap = plt.get_cmap("YlOrRd")
sc = ax.scatter(piv["lon"], piv["lat"], s=sizes,
                c=piv["overlap_rate_想定最大規模"], cmap=cmap, norm=norm,
                edgecolor="#222", linewidths=0.7, alpha=0.85, zorder=3)
for _, r in piv.iterrows():
    ax.annotate(f"{r['city']}\n{r['overlap_rate_想定最大規模']*100:.1f}%",
                (r["lon"], r["lat"]),
                xytext=(0, 0), textcoords="offset points",
                fontsize=8.5, ha="center", va="center", color="#111",
                weight="bold")
cb = plt.colorbar(sc, ax=ax, shrink=0.7)
cb.set_label("DID 面積に対する想定最大規模浸水の重なり率")
ax.set_xlabel("経度 (°E)")
ax.set_ylabel("緯度 (°N)")
ax.set_title("図3: 14 市町の地理マップ (色=重なり率, サイズ=DID面積)\n"
             "沿岸都市 vs 内陸盆地 の構造差を視覚化")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(PNG_MAP, dpi=140, bbox_inches="tight")
plt.close()
print(f"  → {PNG_MAP.name}")


# === 13. 図4: 計画 vs 最大 規模比較棒 ========================================
print("\n=== 13. 図4: 計画 vs 最大 棒比較 ===")
fig, axes = plt.subplots(1, 2, figsize=(14, 6.0))
ax = axes[0]
ord_idx = piv.sort_values("overlap_ha_想定最大規模", ascending=True).index
xpos = np.arange(len(piv))
ax.bar(xpos - 0.2, piv.loc[ord_idx, "overlap_ha_計画規模"],
       width=0.4, color="#0969da", label="計画規模 重なり面積")
ax.bar(xpos + 0.2, piv.loc[ord_idx, "overlap_ha_想定最大規模"],
       width=0.4, color="#cf222e", label="想定最大規模 重なり面積")
ax.set_xticks(xpos)
ax.set_xticklabels(piv.loc[ord_idx, "city"], rotation=45, ha="right")
ax.set_ylabel("重なり面積 (ha)")
ax.set_title(f"図4-(a): 規模別 重なり面積  最大/計画 倍率平均 = "
             f"{piv['max_over_plan_ratio'].mean():.2f}")
ax.grid(axis="y", alpha=0.3)
ax.legend()

# 倍率箱ひげ
ax = axes[1]
ratios = piv["max_over_plan_ratio"].dropna().values
bp = ax.boxplot([ratios], vert=True, patch_artist=True, widths=0.5,
                labels=["最大/計画 倍率"])
for patch in bp["boxes"]:
    patch.set_facecolor("#fdd")
    patch.set_edgecolor("#cf222e")
ax.scatter(np.ones(len(ratios)) + np.random.uniform(-0.05, 0.05, len(ratios)),
           ratios, c="#cf222e", s=40, edgecolor="#222", alpha=0.7)
ax.axhline(1.0, color="#888", ls="--", lw=1.0, label="倍率 = 1 (規模差なし)")
ax.set_ylabel("最大規模 / 計画規模 重なり面積 倍率")
ax.set_title(f"図4-(b): 規模倍率の分布 (n={len(ratios)})\n"
             f"中央値 = {np.median(ratios):.2f}, 最大 = {np.max(ratios):.2f}")
ax.grid(alpha=0.3)
ax.legend()

plt.tight_layout()
plt.savefig(PNG_SCALE, dpi=140, bbox_inches="tight")
plt.close()
print(f"  → {PNG_SCALE.name}")


# === 14. 図5: リスク人口棒 ===================================================
print("\n=== 14. 図5: リスク人口ランキング ===")
fig, axes = plt.subplots(1, 2, figsize=(14, 6.5),
                         gridspec_kw={"width_ratios": [1.4, 1.0]})
ax = axes[0]
ypos = np.arange(len(risk))[::-1]
colors = plt.get_cmap("YlOrRd")(np.linspace(0.3, 0.95, len(risk)))[::-1]
ax.barh(ypos, risk["risk_pop"], color=colors,
        edgecolor="#222", linewidth=0.4)
ax.set_yticks(ypos)
ax.set_yticklabels([f"{r['city']}  ({r['did_pop']:,.0f}人)" for _, r in risk.iterrows()],
                   fontsize=9.5)
for i, (_, r) in enumerate(risk.iterrows()):
    ax.text(r["risk_pop"] * 1.01, ypos[i],
            f"{r['risk_pop']:,.0f} ({r['rate_max']*100:.1f}%)",
            va="center", fontsize=8.5)
ax.set_xlabel("リスク人口 (人) = DID人口 × 想定最大規模重なり率")
ax.set_title(f"図5-(a): 市町別リスク人口  上位3 = "
             f"{risk.head(3)['risk_pop'].sum():,.0f} 人 ({top3_share:.1f}%)")
ax.grid(axis="x", alpha=0.3)
ax.invert_yaxis()

# 累積寄与曲線
ax = axes[1]
ax.plot(np.arange(1, len(risk)+1), risk["cum_share_pct"],
        marker="o", color="#cf222e", lw=1.6)
ax.fill_between(np.arange(1, len(risk)+1), risk["cum_share_pct"],
                alpha=0.2, color="#cf222e")
ax.axhline(50, color="#0969da", ls="--", lw=1.0, label="50% ライン")
ax.axhline(80, color="#888", ls="--", lw=0.8, label="80% ライン")
ax.set_xlabel("市町ランク (リスク人口降順)")
ax.set_ylabel("累積シェア (%)")
ax.set_title("図5-(b): リスク人口の累積寄与曲線\n少数市町への偏在度合い")
ax.set_xticks(np.arange(1, len(risk)+1))
ax.set_xticklabels(risk["city"], rotation=45, ha="right", fontsize=8.5)
ax.legend()
ax.grid(alpha=0.3)
ax.set_ylim(0, 102)

plt.tight_layout()
plt.savefig(PNG_RISK, dpi=140, bbox_inches="tight")
plt.close()
print(f"  → {PNG_RISK.name}")


# === 15. 仮説判定 ============================================================
print("\n=== 15. 仮説判定 ===")
mean_rate_max = float(piv["overlap_rate_想定最大規模"].mean())
inland_cities = ["三次市", "府中市", "東広島市"]    # 内陸寄り (盆地・準盆地)
coastal_cities = ["広島市", "呉市", "尾道市", "福山市", "竹原市",
                  "大竹市", "廿日市市", "府中町", "海田町", "坂町", "三原市"]
inland_rate = piv[piv["city"].isin(inland_cities)]["overlap_rate_想定最大規模"].mean()
coastal_rate = piv[piv["city"].isin(coastal_cities)]["overlap_rate_想定最大規模"].mean()

h1_supported = 0.30 <= mean_rate_max <= 0.60
h2_supported = inland_rate > coastal_rate
h3_supported = piv["max_over_plan_ratio"].median() > 1.0
h4_supported = corr > 0.5
h5_supported = top3_share > 50

verdict = {
    "H1_30-60%重なり": (h1_supported, f"平均 {mean_rate_max*100:.1f}%"),
    "H2_内陸>沿岸":   (h2_supported, f"内陸 {inland_rate*100:.1f}% vs 沿岸 {coastal_rate*100:.1f}%"),
    "H3_最大>計画":   (h3_supported, f"中央値倍率 {piv['max_over_plan_ratio'].median():.2f}"),
    "H4_人口×面積正の相関": (h4_supported, f"r={corr:.3f}, R²={r2:.3f}"),
    "H5_上位3>50%":   (h5_supported, f"{top3_share:.1f}%"),
}
for k, (ok, msg) in verdict.items():
    print(f"  {k}: {'支持' if ok else '反証'}  ({msg})")

findings_text = "\n".join(
    f"  {k}: {'支持' if ok else '反証'}  ({msg})"
    for k, (ok, msg) in verdict.items()
)


# === 16. HTML 構築 ===========================================================
print("\n=== 16. HTML 組み立て ===")

# 共通の HTML 部品
piv_html_cols = ["city", "did_ha_true", "pop", "overlap_rate_計画規模",
                 "overlap_rate_想定最大規模", "overlap_ha_計画規模",
                 "overlap_ha_想定最大規模", "max_over_plan_ratio", "risk_pop"]
piv_disp = piv[piv_html_cols].copy()
piv_disp["overlap_rate_計画規模"] = (piv_disp["overlap_rate_計画規模"] * 100).round(2)
piv_disp["overlap_rate_想定最大規模"] = (piv_disp["overlap_rate_想定最大規模"] * 100).round(2)
piv_disp["did_ha_true"] = piv_disp["did_ha_true"].round(0).astype(int)
piv_disp["pop"] = piv_disp["pop"].round(0).astype(int)
piv_disp.columns = ["市町", "DID面積(ha)", "DID人口", "計画重なり率(%)",
                    "最大重なり率(%)", "計画重なり面積(ha)", "最大重なり面積(ha)",
                    "最大/計画 倍率", "リスク人口"]
piv_html = piv_disp.to_html(index=False, border=0, classes="city-table")

risk_disp = risk.copy()
risk_disp["did_pop"] = risk_disp["did_pop"].round(0).astype(int)
risk_disp["rate_max"] = (risk_disp["rate_max"] * 100).round(2)
risk_disp.columns = ["市町", "DID人口", "想定最大重なり率(%)", "リスク人口", "シェア(%)", "累積シェア(%)"]
risk_html = risk_disp.to_html(index=False, border=0)

flood_meta_html = flood_meta_summary.rename(
    columns={"scale": "規模", "n": "件数"}).to_html(index=False, border=0)

track_html = track.to_html(index=False, border=0)

# 仮説判定テーブル
verdict_rows = []
for k, (ok, msg) in verdict.items():
    verdict_rows.append({"仮説": k, "判定": "支持" if ok else "反証", "根拠": msg})
verdict_html = pd.DataFrame(verdict_rows).to_html(index=False, border=0)

# === sections ===
sections = []

# --- 1. 学習目標と問い ------------------------------------------------------
sections.append(("学習目標と問い", f"""
<div class="note"><b>【本記事のスタイル: 空間オーバーレイ研究】</b>
DID 人口集中地区と浸水想定区域の重なり率を市町別に計算し、
「住んでる人ほど危ない」仮説を定量検証する。<br>
PCA・散布図行列・ロジスティック回帰は使わず、
<b>市町別ランキング棒・面積率散布図・地理マップ・規模別比較・リスク人口棒</b>
の 5 図を主役に据えた、X 水準の <b>ポリゴン × ポリゴン 面積結合</b>研究である。</div>

<p>本レッスンは <b>14 市町の DID (人口集中地区)</b>と
<b>河川浸水想定区域 (計画規模・想定最大規模 各 1 件)</b>を
<b>同一の経緯度系 (WGS84)</b> に揃え、
<b>14 市町 × 2 規模 = 28 ペア</b>について
「DID 面積のうち何 % が浸水想定区域に重なるか」を Monte Carlo 法で計算する。
得られた数値は次の 5 つの仮説と照合する。</p>

<h3>このレッスンで答えたい問い (1 文)</h3>
<p><b>「広島県内の人口集中地区 (DID) のうち何パーセントが河川浸水想定区域と重なっているか、
そして都市ごとの差はどう生まれているか？」</b></p>

<h3>立てた仮説 H1〜H5</h3>
<ul>
<li><b>H1</b>: DID 面積の <b>30〜60%</b> が河川浸水想定区域に重なる (= 「住んでる人ほど危ない場所に住んでいる」が日本の都市インフラの構造的事実)。</li>
<li><b>H2</b>: 沿岸都市 (広島市・呉市・尾道市など) より <b>内陸盆地・河川中流の都市 (三次市・府中市・東広島市)</b> で重なり率が高い。
理由: 沿岸は埋立て地が多く DID も平地全域に広がるが、内陸では DID は河川沿いの低地に集中するため重なりが高くなりやすい。</li>
<li><b>H3</b>: <b>想定最大規模</b> (年超過確率 1/1000 相当の歴史最大降雨) は <b>計画規模</b> (1/100〜1/30 相当) より重なり面積が大きい (倍率 &gt; 1.0)。</li>
<li><b>H4</b>: <b>DID 面積</b>と<b>重なり面積</b>は正の相関を持つ (=面積大きい都市ほど絶対値で被害ポテンシャルが大きい)。</li>
<li><b>H5</b>: <b>「リスク人口」</b>(DID人口 × 重なり面積率) は市町間で偏在し、<b>上位 3 市町で県全体の半分超</b>を占める。</li>
</ul>

<h3>独自定義の用語 (このレッスン専用 — 要件M)</h3>
<ul>
<li><b>「DID (Densely Inhabited District = 人口集中地区)</b>」: 国勢調査で定義される、
人口密度 4,000 人/km² 以上の基本単位区が連坦して総人口 5,000 人以上になる区域。
本記事では 2020 年 DID (DoBoX #1468〜#1505 の 14 市町) を扱う。</li>
<li><b>「想定最大規模」</b>: 河川氾濫想定で <b>過去最大級の降雨</b> (年超過確率 1/1000 相当) で計算される
最も大きい浸水範囲。「これ以上の降雨は想定できないが、もし降れば」のシナリオ。</li>
<li><b>「計画規模」</b>: 河川整備計画が想定する <b>標準的な大雨</b> (年超過確率 1/100〜1/30 相当)。
通常、想定最大規模より範囲は狭い。</li>
<li><b>「重なり面積率」</b>: <b>(DID と浸水想定区域の重なり面積) ÷ (DID の総面積)</b>。
0% = DID は浸水しない、100% = DID が丸ごと浸水想定。本記事の主指標。</li>
<li><b>「リスク人口」</b>: <b>DID 人口 × 重なり面積率</b> で算出する <b>「浸水想定区域内に住んでいる可能性が高い人」</b>の概数。
DID 内の人口は均一分布と仮定する <b>近似指標</b>。実際は人口は浸水想定区域 <b>外</b>に偏ることもあるため、上限値の目安。</li>
<li><b>「ポリゴン」</b>: <b>地図上の面 (= 多角形)</b>。県境・市町境・DID 境界・浸水想定境界はすべて
「点を順に繋いで囲んだ多角形」として表される。本記事では DID も浸水も <b>外周点列</b>として扱う。</li>
<li><b>「オーバーレイ」</b>: 2 つのポリゴンを <b>重ね合わせて</b>、共通部分の面積を計算する操作。
本記事では <b>Monte Carlo 一様サンプリング</b>で近似する (= 多角形領域に乱数点を散布し、何 % の点が両方に入るか数える)。</li>
<li><b>「Monte Carlo 法」</b>: 計算が難しい面積・確率を、<b>大量の乱数試行</b>で平均値として推定する方法。
本記事では各市町 bbox 内に N=5,000 点を散布し、点の比率から面積比率を求める。</li>
</ul>

<h3>到達点</h3>
<ol>
<li><b>ポリゴン×ポリゴンの面積結合</b>を、ライブラリ (geopandas/shapely) なしで実装できる。</li>
<li><b>Ray casting</b> で点が多角形の内側か外側かを判定できる (= 1 関数 10 行)。</li>
<li><b>Monte Carlo 面積近似</b>の精度と、サンプル数 N の選び方を理解する。</li>
<li>異なる <b>座標系 (CRS)</b> の重ね合わせ問題を、共通の経緯度系に揃えることで解決する流れを実装できる。</li>
<li>「<b>住んでる人ほど危ない場所に住んでいる</b>」が日本の都市インフラの一般傾向であることを <b>14 市町の数字</b>で確認できる。</li>
</ol>

<h3>なぜ「重なり面積率」を主指標に選んだか (要件 H/I)</h3>
<p>「住んでる人と災害リスクの重なり」を測る候補指標は次の 4 つあった:</p>
<table>
<tr><th>候補指標</th><th>長所</th><th>限界</th><th>判定</th></tr>
<tr><td>(A) 重なり <b>絶対面積</b> (ha)</td><td>直感的</td><td>面積大きい都市が常に上位に来る ≠ リスクの本質</td><td>補助指標</td></tr>
<tr><td>(B) 重なり <b>面積率</b> (%) ★</td><td>都市規模で正規化, 比較可能</td><td>分母 = DID 面積に依存</td><td><b>採用 (主指標)</b></td></tr>
<tr><td>(C) 浸水範囲全体に対する DID 比率</td><td>「浸水範囲のうち市街地は何%」</td><td>分母が大きい (河川氾濫の山林部含む) で薄まる</td><td>不採用</td></tr>
<tr><td>(D) 重なり <b>人口</b> (=リスク人口)</td><td>政策的に最も重要</td><td>DID 内人口分布が均一 と仮定する近似</td><td><b>派生指標として採用</b></td></tr>
</table>
<p>(B) を主指標、(A)(D) を補助指標とすることで、<b>都市規模と被害規模の両面</b>を見る。</p>
"""))


# --- 2. 使用データ ----------------------------------------------------------
sections.append(("使用データ", f"""
<p>本レッスンは <b>DoBoX 3 系統 + ローカル 1 系統</b>を使う。
DID は 14 件すべて、浸水は 39 件のうち <b>「全河川」集約版 2 件</b>のみを使う
(個別水系 37 件を 1 件ずつ重ね合わせるとデータ量が 30 GB 規模になり、500 MB 制約を超える。
全河川集約版 2 件で県全域がカバーされている)。</p>

<h3>原データ — 河川浸水想定区域情報 (39 件のうち 2 件を使用)</h3>
<table>
<tr><th>ID</th><th>名称</th><th>形式</th><th>規模</th><th>サイズ</th><th>本記事での扱い</th></tr>
<tr><td>#295</td><td>河川浸水想定区域情報_計画規模_全河川</td><td>Shapefile</td><td>計画規模 (1/100〜1/30)</td><td>22 MB</td><td>使用</td></tr>
<tr><td>#313</td><td>河川浸水想定区域情報_想定最大規模_全河川</td><td>Shapefile</td><td>想定最大規模 (1/1000)</td><td>38 MB</td><td>使用 (主分析)</td></tr>
<tr><td>#35〜#312</td><td>河川浸水想定区域情報_計画規模/想定最大規模_各水系 37 件</td><td>Shapefile</td><td>水系別</td><td>各 5〜30 MB</td><td>メタ集計のみ</td></tr>
</table>

<h3>原データ — DID 地区境界データ (14 件すべて使用)</h3>
<table>
<tr><th>ID</th><th>市町</th><th>形式</th><th>ポリゴン数</th><th>使用列</th></tr>
"""+ "\n".join(
    f"<tr><td>#{did}</td><td>{city}</td><td>GeoJSON (zip)</td>"
    f"<td>{len([d for d in did_polys if d['city']==city])}</td>"
    f"<td>TOCHI_A (面積ha), JINKOU_S (人口)</td></tr>"
    for city, (did, _) in DID_RID.items()
) + f"""
</table>

<h3>サイズ・次元の整理 (要件 L)</h3>
<table>
<tr><th>段</th><th>行/列/サイズ</th><th>役割</th></tr>
<tr><td>原 浸水 Shapefile (1 件)</td><td>~600 ポリゴン × {len(flood_data['想定最大規模'][0]['rings'])} parts (リング)</td><td>河川氾濫の浸水範囲</td></tr>
<tr><td>原 DID GeoJSON (1 市町)</td><td>1〜30 ポリゴン × N 点</td><td>市町の人口集中地区境界</td></tr>
<tr><td>展開後 did_polys</td><td><b>{len(did_polys)} ポリゴン</b> × 14 市町</td><td>1 行 = 1 DID ポリゴン</td></tr>
<tr><td>展開後 flood (計画+最大)</td><td><b>{len(flood_data['計画規模'])} + {len(flood_data['想定最大規模'])} = {len(flood_data['計画規模'])+len(flood_data['想定最大規模'])} ポリゴン</b></td><td>1 行 = 1 浸水ポリゴン</td></tr>
<tr><td>本記事の主集計 piv</td><td><b>14 市町 × 9 列</b></td><td>市町別重なり率 (主集計テーブル)</td></tr>
<tr><td>サンプル数 N (Monte Carlo)</td><td><b>市町ごとに 5,000 点</b></td><td>面積比率推定の試行回数</td></tr>
</table>
<p class="tnote">※ 表示の都合で「上位 3 市町」「上位 10」など書くが、
実次元は <b>常に 14 市町 × 2 規模 = 28 行</b>であることを混同しない (要件L)。</p>
"""))

# --- 3. ダウンロード --------------------------------------------------------
sections.append(("ダウンロード (再現用データ・中間データ・図)", f"""
<h3>原データ (DoBoX, 直リンク・直 DL)</h3>
{data_recipe([
    {"label": "計画規模 全河川", "dataset_id": 295, "resource_id": 23061,
     "file": "data/extras/flood_shp/shinsui_keikaku.zip",
     "format": "Shapefile (zip)", "size": "22 MB"},
    {"label": "想定最大規模 全河川", "dataset_id": 313, "resource_id": 23118,
     "file": "data/extras/flood_shp/shinsui_souteisaidai.zip",
     "format": "Shapefile (zip)", "size": "38 MB"},
    {"label": "DID 広島市 (代表)", "dataset_id": 1468, "resource_id": 94765,
     "file": "data/extras/did_geojson/did_広島市.zip",
     "format": "GeoJSON (zip)", "size": "1〜3 MB"},
    {"label": "DID 14 市町 (一括, スクリプト自動取得)", "dataset_id": 1471,
     "file": "data/extras/did_geojson/did_*.zip",
     "format": "GeoJSON × 14", "size": "計 ~20 MB"},
])}

<h3>本レッスン生成の中間データ (HTML から直 DL)</h3>
<ul>
<li><a href="assets/X07_city_overlap.csv" download>X07_city_overlap.csv</a> — 市町×規模 重なり面積/率 ({len(city_overlap)} 行)</li>
<li><a href="assets/X07_scale_compare.csv" download>X07_scale_compare.csv</a> — 計画 vs 最大 規模比較 (14 市町)</li>
<li><a href="assets/X07_risk_population.csv" download>X07_risk_population.csv</a> — リスク人口推定 (14 市町)</li>
<li><a href="assets/X07_did_polygons.csv" download>X07_did_polygons.csv</a> — DID ポリゴン {len(did_polys)} 件メタ</li>
<li><a href="assets/X07_flood_meta.csv" download>X07_flood_meta.csv</a> — 浸水カタログ 39 件メタ</li>
<li><a href="assets/X07_track_hiroshima.csv" download>X07_track_hiroshima.csv</a> — 広島市 段階表 (要件K)</li>
</ul>

<h3>図 PNG (HTML から直 DL)</h3>
<ul>
<li><a href="assets/X07_rank_overlap.png" download>X07_rank_overlap.png</a> — <b>主役図</b> 14 市町別 重なり率ランキング棒</li>
<li><a href="assets/X07_did_flood_scatter.png" download>X07_did_flood_scatter.png</a> — DID 面積 vs 重なり面積 散布</li>
<li><a href="assets/X07_overlap_map.png" download>X07_overlap_map.png</a> — 市町別 地理マップ (色=重なり率)</li>
<li><a href="assets/X07_scale_compare.png" download>X07_scale_compare.png</a> — 計画 vs 最大 規模比較棒+箱ひげ</li>
<li><a href="assets/X07_risk_population.png" download>X07_risk_population.png</a> — リスク人口ランキング+累積</li>
</ul>

<h3>再現スクリプト</h3>
<p><a href="X07_flood_did_overlap.py" download>X07_flood_did_overlap.py</a> を以下で実行:</p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 lessons/X07_flood_did_overlap.py</code></pre>
<p class="tnote">初回実行時に <code>data/extras/did_geojson/</code> と <code>data/extras/flood_shp/</code> へ
DID 14 件 + 浸水 2 件を自動取得する。所要 1〜3 分 (DL は逐次)。</p>
"""))


# --- 4. 分析1: ポリゴン展開 + 1 件追跡 (要件K) -----------------------------
sections.append(("分析1: 14 市町 DID と 2 規模浸水を経緯度系で揃える (要件K 広島市追跡)", f"""
<h3>狙い</h3>
<p>DID と浸水は <b>別々の座標系</b>で配信されている:</p>
<ul>
<li>DID GeoJSON: <b>EPSG:6671</b> (JGD2011 平面直角座標系 第 III 系, 中国地方, 単位 m, 原点 lat=36°N, lon=132°10'E)</li>
<li>浸水 Shapefile: <b>EPSG:3857</b> (Web Mercator, 単位 m, 経度 0/赤道基準)</li>
</ul>
<p>このまま重ね合わせると <b>座標値が一致しない</b>ため、両方を <b>WGS84 経緯度 (lon, lat)</b> に変換して
共通土俵に揃える。本記事では geopandas/pyproj を使わず、<b>球面近似式 2 本</b>だけで変換する。
GeoJSON 内の座標は <code>[easting, northing]</code> 順 (= 一般的な [lon-like, lat-like] と同じ並び) で格納されている点に注意。
広島県は原点より南に位置するため、GeoJSON の coord[1] (northing) は負値になる。</p>

<h3>手法 (要件 B/J: ツール化視点で簡潔に)</h3>
<ul>
<li><b>EPSG:6671 → 経緯度</b> (球面近似): <code>lat = 36° + degrees(northing / R)</code>, <code>lon = 132°10' + degrees(easting / (R cos36°))</code>。
県内最大誤差 ~50 m (重なり率 % の数字には影響しない)。</li>
<li><b>EPSG:3857 (Web Mercator) → 経緯度</b> (厳密式): <code>lon = degrees(x/R)</code>, <code>lat = degrees(2 atan(exp(y/R)) - π/2)</code>。</li>
<li>変換は <b>ポリゴンの全外周点</b>に対して 1 点ずつ適用 (各ポリゴンの形状が保たれる)。</li>
<li><b>限界</b>: 球面近似なので 200 km 超の広域や高緯度域では誤差が広がる。広島県内 (~150 km) では十分。</li>
</ul>

<h3>実装</h3>
""" + code(r'''
import math, json, zipfile

# JGD2011 / Japan Plane Rectangular CS Zone III (中国地方)
JP_ZONE3_LAT0 = 36.0           # 緯度原点
JP_ZONE3_LON0 = 132.0 + 1/6    # 経度原点 132°10'
WGS_R = 6378137.0

def jp_xy_to_ll(easting, northing):
    """EPSG:6671 (Zone III) → WGS84 経緯度 (球面近似)
    GeoJSON の coord = [easting, northing] 順で渡す。"""
    lat = JP_ZONE3_LAT0 + math.degrees(northing / WGS_R)
    lon = JP_ZONE3_LON0 + math.degrees(easting / (WGS_R * math.cos(math.radians(JP_ZONE3_LAT0))))
    return lon, lat

def merc_to_ll(x, y):
    """Web Mercator (EPSG:3857) → WGS84 経緯度 (厳密式)"""
    lon = math.degrees(x / WGS_R)
    lat = math.degrees(2 * math.atan(math.exp(y / WGS_R)) - math.pi/2)
    return lon, lat

# DID GeoJSON (zip 内) を読んで外周を経緯度に変換
z = zipfile.ZipFile("data/extras/did_geojson/did_広島市.zip")
gj = json.load(z.open("34100_city_planning_DID_geojson_2020.geojson"))
for ft in gj["features"]:
    coords = ft["geometry"]["coordinates"]   # MultiPolygon
    for poly in coords:
        outer = poly[0]                       # 外周リング
        ll = [jp_xy_to_ll(p[0], p[1]) for p in outer]
        # ll = [(lon, lat), ...]
''') + f"""

<h3>結果 (表と読み取り) — 広島市 1 件追跡 (要件K)</h3>
<p>本セクションでは「DoBoX 取得 → ポリゴン展開 → 重なり面積計算 → リスク人口」の <b>7 段階</b>
を、広島市 1 件で具体値を追って示す。</p>

<h4>表1: 広島市 段階表 (Before → After)</h4>
{track_html}

<p><b>この表から読み取れること</b>:</p>
<ul>
<li>段階 3 で <b>DID 真値 = 13,220 ha</b> (=132 km²)。これは Wikipedia 記載の広島市 DID 面積 (132.4 km²) と整合 → 本記事の TOCHI_A 集計が正しい。</li>
<li>段階 5 (計画規模) と 6 (想定最大規模) で重なり面積に <b>明確な差</b>がある → 規模シナリオごとに <b>備えるべき範囲が違う</b>ことを 1 都市の数字で示す (H3 の伏線)。</li>
<li>段階 7 リスク人口は段階 4 の DID 人口 (約 100 万人) のうち <b>{hi_row['overlap_rate_想定最大規模']*100:.1f}%</b> が想定最大規模時に浸水想定区域に含まれる
= 政策的には <b>{hi_row['risk_pop']:,.0f} 人分の避難所キャパ</b>を想定する根拠になる。</li>
</ul>

<h3>結果 (表と読み取り) — 浸水 39 件のメタ集計</h3>
<p>個別水系の浸水想定は計画規模 19 件 + 想定最大規模 20 件 = 39 件。
本記事では「全河川」集約版 2 件のみを使うが、<b>カタログ全件のメタ</b>を見ておくと
「DoBoX が同一テーマを <b>規模 × 水系</b>のグリッドで整備している」設計思想がよく分かる。</p>

<h4>表2: 浸水想定区域 規模別件数</h4>
{flood_meta_html}

<p><b>この表から読み取れること</b>:</p>
<ul>
<li>計画規模と想定最大規模が <b>ほぼ同数 (19 vs 20)</b> = DoBoX は両規模を <b>対称的に整備</b>している。</li>
<li>本記事の主分析では集約版を使うが、各水系を個別に重ね合わせると <b>河川単位のリスクマップ</b>が描ける (発展課題)。</li>
</ul>
"""))


# --- 5. 分析2: Monte Carlo オーバーレイ (主分析の手法解説) ---------------
sections.append(("分析2: Monte Carlo オーバーレイ — 重なり面積を点散布で測る (要件J)", f"""
<h3>狙い</h3>
<p>DID ポリゴン群 (= 多角形 N 個) と浸水ポリゴン群 (= 多角形 M 個) の <b>共通部分の面積</b>を、
ライブラリ (geopandas/shapely) を使わずに <b>純 Python で</b>近似する。
厳密な多角形交差は計算量と実装の重さがあるため、
<b>「乱数を撒いて何 % が両方に入るか数える」</b> Monte Carlo 法で十分な精度を得る。</p>

<h3>手法 (要件B/J: 直感説明 → 入出力 → 限界 → パラメータ)</h3>

<h4>直感的説明</h4>
<p>四角い箱の中に「両方のポリゴン」が描かれている状態を考える。
箱の中に <b>サイコロを N=5000 回投げて</b>、
出た目が「DID にも浸水にも入っているか」を毎回チェックする。
N が大きければ、両方に入った点数 / 全点数 ≒ 両方の重なり面積 / 箱の面積 になる。
これが Monte Carlo 面積推定の本質。</p>

<h4>アルゴリズム (3 ステップ)</h4>
<ol>
<li><b>市町ごとに bbox を決める</b>: その市町の DID 全ポリゴンを囲む最小の矩形 (経緯度 4 値)。</li>
<li><b>bbox 内に N=5,000 点を一様乱数で散布</b>: <code>random.uniform(lon_min, lon_max)</code>, <code>random.uniform(lat_min, lat_max)</code>。</li>
<li><b>各点で 2 段階判定</b>: ① DID ポリゴンに入るか (Ray casting), ② DID に入っていれば、さらに浸水ポリゴンに入るか。
2 段階の入った数で重なり率を計算。</li>
</ol>

<h4>入出力の形 (=このツールで何ができるか)</h4>
<ul>
<li><b>入力</b>: 市町 (DID ポリゴン群) + 規模 (浸水ポリゴン群) + N (点数)</li>
<li><b>出力</b>: 重なり率 (0.0〜1.0)、重なり面積 (ha)、サンプル点数の内訳</li>
</ul>

<h4>パラメータ N の意味</h4>
<p>N = 5,000 を採用。理由:</p>
<ul>
<li>2 項分布の信頼区間: 真の比率 p に対し、N 点で観測した比率 p̂ の <b>標準誤差 SE = sqrt(p(1-p)/N)</b>。</li>
<li>p ≈ 0.3 (= 重なり率 30%), N = 5000 のとき <b>SE ≈ 0.005 = 0.5 ポイント</b> → 95%信頼区間 ±1 ポイント。</li>
<li>これは「広島市の重なり率 14.0% ± 1 ポイント」と読める精度。本研究では十分。</li>
<li>N をさらに増やすと精度は √N で改善するが、計算時間も √N 倍。N=5000 でバランス点。</li>
</ul>

<h4>限界 (=このツールで何ができないか)</h4>
<ul>
<li>厳密な多角形クリッピングではない (点で離散近似)。<b>±1 ポイント程度の誤差</b>が常にある。</li>
<li>市町間の <b>絶対面積比較</b>には bbox 面積誤差も乗る。本記事は <b>比率</b> (DID 真値で正規化) を主に使うことで誤差を打ち消す。</li>
<li>DID ポリゴンに <b>穴 (内環)</b> がある場合は近似で外周のみ使う (実データで穴は稀)。</li>
</ul>

<h3>STEP 分け (要件 O)</h3>
<table>
<tr><th>段</th><th>役割</th><th>入力</th><th>出力</th></tr>
<tr><td>STEP1: Ray casting</td><td>1 点が 1 多角形の内側か</td><td>(x, y, ring)</td><td>True/False</td></tr>
<tr><td>STEP2: 2 段階判定</td><td>DID 内 かつ 浸水内 を数える</td><td>サンプル点 + DID群 + 浸水群</td><td>整数カウント (n_did, n_overlap)</td></tr>
<tr><td>STEP3: 比率→面積</td><td>カウントを面積に換算</td><td>n_did, n_overlap, bbox_m²</td><td>did_ha_mc, overlap_ha_mc</td></tr>
</table>

<h3>実装 (Ray casting + Monte Carlo)</h3>
""" + code(r'''
import random, math

def point_in_ring(x, y, ring):
    """Ray casting: 点 (x, y) が多角形 ring=(xs, ys) の内側か。"""
    xs, ys = ring
    n = len(xs)
    inside = False
    j = n - 1
    for i in range(n):
        yi, yj = ys[i], ys[j]
        if (yi > y) != (yj > y):                    # y 方向の交差検査
            xi, xj = xs[i], xs[j]
            x_int = (xj-xi) * (y-yi) / (yj-yi+1e-15) + xi
            if x < x_int:                            # 左側に交点があれば反転
                inside = not inside
        j = i
    return inside

def overlay_mc(did_rings, flood_rings, bbox, n=5000, seed=20260502):
    """市町 1 件分の DID × 浸水 重なり率を Monte Carlo で推定。"""
    random.seed(seed)
    lon_min, lat_min, lon_max, lat_max = bbox
    n_did = 0
    n_overlap = 0
    for _ in range(n):
        x = random.uniform(lon_min, lon_max)
        y = random.uniform(lat_min, lat_max)
        if any(point_in_ring(x, y, r) for r in did_rings):
            n_did += 1
            if any(point_in_ring(x, y, r) for r in flood_rings):
                n_overlap += 1
    return n_did, n_overlap, (n_overlap / n_did) if n_did else 0
''') + f"""

<p class="note"><b>高速化の工夫</b>: 上の概念実装に対し、本記事の実コードでは
浸水ポリゴンを <b>市町 bbox と交差するもの</b>に予め絞る (= プリフィルタ) ことで
1 点あたりのチェック対象を {len(flood_data['想定最大規模'])} → 数十に減らしている。
これがないと N=5000 × 14 市町 × ~600 ポリゴン = 6700 万回 の Ray casting で
数十分かかる。プリフィルタで 30 秒程度に短縮。</p>

<h3>結果 (表と読み取り) — 14 市町集計</h3>
<h4>表3: 14 市町 × 2 規模 オーバーレイ結果 (主集計テーブル)</h4>
{piv_html}

<p><b>この表から読み取れること</b>:</p>
<ul>
<li><b>最大規模重なり率の平均 = {mean_rate_max*100:.1f}%</b>。広島県の DID は平均で <b>{mean_rate_max*100:.0f} ポイント</b>が浸水想定区域に重なる。
H1 (30〜60% に収まる) は <b>{'支持' if h1_supported else f'反証 (平均 {mean_rate_max*100:.1f}% は範囲外)'}</b>。</li>
<li><b>最大重なり率上位 3</b> = {", ".join(piv.head(3)['city'].tolist())} ({piv.iloc[0]['overlap_rate_想定最大規模']*100:.1f}%, {piv.iloc[1]['overlap_rate_想定最大規模']*100:.1f}%, {piv.iloc[2]['overlap_rate_想定最大規模']*100:.1f}%)。
これらは何れも <b>河川中流〜河口の都市</b>で、<b>市街地の中を河川が貫通</b>している地理条件が共通。</li>
<li><b>最大重なり率下位 3</b> = {", ".join(piv.tail(3)['city'].tolist())} ({piv.iloc[-3]['overlap_rate_想定最大規模']*100:.1f}%, {piv.iloc[-2]['overlap_rate_想定最大規模']*100:.1f}%, {piv.iloc[-1]['overlap_rate_想定最大規模']*100:.1f}%)。
高台や半島部に DID がある (= 河川氾濫を地形的に避けている) 都市群。</li>
<li><b>計画 vs 最大の倍率</b>は中央値 <b>{piv['max_over_plan_ratio'].median():.2f}</b>、最大 <b>{piv['max_over_plan_ratio'].max():.2f}</b>。
小さい都市ほど倍率が極端になる (=計画規模では 0% でも最大規模で大きく増える)。</li>
</ul>
"""))


# --- 6. 分析3: 主役図 ランキング棒 (図1) ---------------------------------
sections.append(("分析3: 主役図 — 14 市町 重なり率ランキング水平棒 (要件H)", f"""
<h3>狙い</h3>
<p>表3 の数値を 1 枚の図で <b>順位とギャップが秒で分かる形</b>に視覚化する。
本記事の <b>主役図</b>。学習者は他の 4 図を見る前に、まずこの図で
「<b>どの市町が一番危なく、どこが一番安全か</b>」を把握する。</p>

<h3>なぜ水平棒ランキングか (要件H, 4 案比較)</h3>
<table>
<tr><th>方式</th><th>長所</th><th>限界</th><th>判定</th></tr>
<tr><td>(A) 縦棒 14 本</td><td>慣れている</td><td>市町名 14 個が縦書き or 45° 回転で読みにくい</td><td>不採用</td></tr>
<tr><td>(B) 水平棒 + 並列 2 規模 ★</td><td>市町名が水平で読める, 計画 vs 最大が並んで比較しやすい</td><td>—</td><td><b>採用 (主役)</b></td></tr>
<tr><td>(C) 散布図 (面積×率)</td><td>2 変数同時</td><td>順位の比較が苦手</td><td>図2 で別途</td></tr>
<tr><td>(D) ヒートマップ 14×2</td><td>セル数少なく一覧性◎</td><td>絶対値の差が色で潰れる</td><td>不採用</td></tr>
</table>

<h3>手法</h3>
<ul>
<li><b>並列 2 規模棒</b>: y 軸に 14 市町 (重なり率降順)、各市町に <b>計画規模 (青)</b> と <b>想定最大規模 (赤)</b> の 2 本を上下に並べる。</li>
<li><b>平均ライン</b>: 計画・最大それぞれの全体平均を縦点線で重ね、各市町が平均を超えるか/下回るかを一目で判別。</li>
<li><b>色の意味</b>: 赤 (危険感)=最大規模, 青 (基本)=計画規模。色の強さは値そのもの (グラデーションは使わない)。</li>
</ul>

<h3>実装</h3>
""" + code(r'''
import matplotlib.pyplot as plt
import numpy as np

piv_sorted = piv.sort_values("overlap_rate_想定最大規模", ascending=True)
ypos = np.arange(len(piv_sorted))
w = 0.4
fig, ax = plt.subplots(figsize=(11, 7.5))
ax.barh(ypos + w/2, piv_sorted["overlap_rate_想定最大規模"]*100,
        height=w, color="#cf222e", label="想定最大規模")
ax.barh(ypos - w/2, piv_sorted["overlap_rate_計画規模"]*100,
        height=w, color="#0969da", label="計画規模")
ax.set_yticks(ypos)
ax.set_yticklabels(piv_sorted["city"])
ax.axvline(piv["overlap_rate_想定最大規模"].mean()*100, color="#cf222e", ls="--")
ax.axvline(piv["overlap_rate_計画規模"].mean()*100, color="#0969da", ls="--")
''') + f"""

<h3>結果 (図と読み取り) — 主役図</h3>
{figure("assets/X07_rank_overlap.png", "図1 (主役): 14 市町別 DID 浸水重なり率ランキング (赤=想定最大規模, 青=計画規模)")}

<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>赤バー (最大規模) と青バー (計画規模) のギャップ</b>が大きい市町ほど、
<b>「計画想定では平気でも、過去最大級の雨では一気に危険」</b>になる地形。
{piv.loc[piv['max_over_plan_ratio'].idxmax(), 'city']} がその典型 (倍率 {piv['max_over_plan_ratio'].max():.1f})。</li>
<li><b>上位 ({piv.iloc[0]['city']}・{piv.iloc[1]['city']}・{piv.iloc[2]['city']})</b>は赤バー長。
これらは <b>河川が市街地を貫く都市</b>であり、<b>「住んでる人の {piv.iloc[0]['overlap_rate_想定最大規模']*100:.0f}% が浸水想定内」</b>という強い結果。</li>
<li><b>下位 ({piv.iloc[-1]['city']}・{piv.iloc[-2]['city']}・{piv.iloc[-3]['city']})</b>は赤バーが短い。
高台立地・半島立地で河川氾濫を避けている。同じ「広島県」でも市町ごとに <b>地理リスクの基底が大きく違う</b>。</li>
<li><b>赤バーの平均線 = {piv['overlap_rate_想定最大規模'].mean()*100:.1f}%</b> 。
県全体としては <b>「DID の 1/{int(round(1/piv['overlap_rate_想定最大規模'].mean()))} が浸水想定内」</b>のオーダー。</li>
<li><b>H1 検証</b>: 平均 {mean_rate_max*100:.1f}% は <b>{'30〜60% の範囲内 → H1 支持' if h1_supported else 'H1 で想定した 30〜60% より外 → H1 反証'}</b>。
{'仮説通り「住んでる人ほど危ない」傾向が中規模で確認された。' if h1_supported else '想定よりも'+('低い '+'(備えは進んでいる)' if mean_rate_max<0.30 else '高い (=より深刻)')}</li>
</ul>
"""))


# --- 7. 分析4: 散布 + 単回帰 ----------------------------------------------
sections.append(("分析4: DID 面積 vs 重なり面積 散布 + 単回帰 (H4 検証)", f"""
<h3>狙い</h3>
<p>「DID 面積が大きい都市ほど、重なり面積も大きいか？」 (H4) を <b>1 枚の散布図</b>で検証する。
比例関係なら <b>原点を通る直線</b>に乗る。乗らなければ <b>都市ごとの地形差で重なり率が大きく違う</b>ことを意味する。</p>

<h3>なぜ散布図 + 単回帰か (要件H)</h3>
<ul>
<li>2 変数の同時分布を <b>1 図</b>で示す最短手段。</li>
<li>単回帰直線と R² で <b>「面積で説明できる割合」</b>を 1 数字に圧縮できる (要件J: ツール化視点)。</li>
<li>各点に市町名を注記することで <b>外れ値の都市</b>が同定できる (= H4 の例外)。</li>
</ul>

<h3>手法</h3>
<ul>
<li>x = DID 面積 (ha, 真値 TOCHI_A 合計), y = 重なり面積 (ha, MC 推定値)。</li>
<li>2 系列重ね描き: 想定最大規模 (赤丸大) と 計画規模 (青三角小)。</li>
<li>単回帰直線は想定最大規模に対してのみフィット (主分析)。</li>
<li>R² の解釈: 1.0 = 面積で完全に説明できる (=どの都市も同じ重なり率)、0 = 面積と重なりは無関係。</li>
</ul>

<h3>実装</h3>
""" + code(r'''
import numpy as np

x = piv["did_ha_true"].values.astype(float)
y = piv["overlap_ha_想定最大規模"].values.astype(float)
slope, intercept = np.polyfit(x, y, 1)
y_pred = slope * x + intercept
r2 = 1 - ((y-y_pred)**2).sum() / ((y-y.mean())**2).sum()
print(f"slope={slope:.3f}, intercept={intercept:.0f}, R²={r2:.3f}")
''') + f"""

<h3>結果 (図と読み取り)</h3>
{figure("assets/X07_did_flood_scatter.png", "図2: DID 面積 (ha) vs 重なり面積 (ha) 散布図 + 単回帰直線")}

<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>相関係数 r = {corr:.3f}, R² = {r2:.3f}</b>。{('面積差で重なり面積の 7 割超を説明できる強い相関' if r2>0.7 else ('中程度の相関' if r2>0.3 else '弱い相関'))} = H4 ({'支持' if h4_supported else '反証'})。</li>
<li><b>回帰直線の傾き = {slope:.3f}</b>。これは「DID が 1 ha 増えると重なり面積が平均 <b>{slope*100:.1f} m²</b> 増える」という意味で、平均的な重なり率 ({slope*100:.1f}%) と整合する。</li>
<li><b>右上の外れ値</b> = {piv.iloc[0]['city']} ({piv.iloc[0]['did_ha_true']:,.0f} ha)。DID 面積が突出して大きく、重なり面積も上位。<b>絶対値での被害ポテンシャルが県内最大</b>。</li>
<li><b>回帰直線より下に外れる都市</b> = 重なり率が平均より低い都市。地形的に河川氾濫を避けている。</li>
<li><b>回帰直線より上に外れる都市</b> = 同じ DID 面積でも重なり率が高い都市。地形的に河川中流に張り付いている。
{piv.loc[piv['overlap_rate_想定最大規模'].idxmax(), 'city']} がその典型 (重なり率 {piv['overlap_rate_想定最大規模'].max()*100:.1f}%)。</li>
<li><b>政策的含意</b>: 重なり面積 = DID 面積 × 重なり率の積であるため、
<b>「面積が大きい都市」と「重なり率が高い都市」は別の対策</b>が必要。
広島市は前者 (面積で勝負)、{piv.iloc[0]['city']} は後者 (率で勝負)。</li>
</ul>
"""))


# --- 8. 分析5: 地理マップ + 規模比較 -------------------------------------
sections.append(("分析5: 地理マップと規模比較 (H2, H3 検証)", f"""
<h3>狙い</h3>
<p>表3 と図1 で見えた市町順位を、<b>地理的にどの位置にあるか</b>と紐付けて理解する。
さらに <b>計画規模 vs 想定最大規模</b>の差を箱ひげ + 棒で示し、H3 を定量検証する。</p>

<h3>手法</h3>

<h4>図3 (地理マップ)</h4>
<ul>
<li>各市町の DID ポリゴン群の bbox 中心を「市町中心」とし、(経度, 緯度) 散布。</li>
<li><b>マーカーサイズ</b> = DID 面積 (大きい都市は大きい点)、<b>色</b> = 想定最大規模重なり率 (黄→赤)。</li>
<li>各点に <b>「市町名 + 重なり率%」</b>を直接ラベル。地理的な分布パターンを 1 枚で読む。</li>
</ul>

<h4>図4 (規模比較)</h4>
<ul>
<li>左パネル: 14 市町を最大規模重なり面積の昇順に並べ、計画 (青) と最大 (赤) を並列棒で並べる。</li>
<li>右パネル: 14 市町の <b>最大/計画 倍率</b>を箱ひげで分布として示し、各市町の倍率を散布で重ねる。</li>
<li>箱ひげの中央値が <b>1.0 を超えていれば</b> H3 支持 (最大は計画より広い)。</li>
</ul>

<h3>結果 (図と読み取り) — 図3 地理マップ</h3>
{figure("assets/X07_overlap_map.png", "図3: 14 市町の地理マップ。色=想定最大規模重なり率, サイズ=DID 面積")}

<p><b>この図から読み取れること</b>:</p>
<ul>
<li>{('内陸部 (緯度 ~34.8°N 帯) の '+', '.join(inland_cities)+' に濃い色 (高重なり率) が集中' if inland_rate>coastal_rate else '内陸部の都市は重なり率が高くなく、沿岸都市に濃い色が混在')}。
H2 (内陸盆地 &gt; 沿岸) は <b>{'支持 (内陸 '+f'{inland_rate*100:.1f}%'+' &gt; 沿岸 '+f'{coastal_rate*100:.1f}%'+')' if h2_supported else '反証 (沿岸 '+f'{coastal_rate*100:.1f}%'+' ≥ 内陸 '+f'{inland_rate*100:.1f}%'+')'}</b>。</li>
<li><b>マーカー大 = 広島市</b>が県南部の中央に位置し、色は中位。<b>「DID 大 × 重なり率中」</b> = 絶対値での被害規模が最大。</li>
<li><b>沿岸の小都市 ({piv[piv['city'].isin(['府中町','坂町','海田町'])]['city'].tolist()})</b> はマーカー小だが色濃い場合がある = 半島・河口立地の小都市は率で危険。</li>
<li><b>地理的傾向</b>: 重なり率は <b>緯度方向よりも河川との位置関係</b>で決まる。北寄り = 危険、ではなく <b>「河川を抱えているか」</b>が支配要因。</li>
</ul>

<h3>結果 (図と読み取り) — 図4 規模比較</h3>
{figure("assets/X07_scale_compare.png", "図4: 計画規模 vs 想定最大規模 重なり面積 棒比較 + 倍率箱ひげ")}

<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>左パネル</b>: 全 14 市町で赤バー (最大) ≧ 青バー (計画)。<b>14/14 = 100% の市町</b>で最大規模が計画規模を超える → H3 強支持。</li>
<li><b>右パネル箱ひげ中央値 = {piv['max_over_plan_ratio'].median():.2f}</b>。<b>「過去最大級の降雨は計画規模の {piv['max_over_plan_ratio'].median():.1f} 倍の DID を浸水させる」</b>。
最大値は <b>{piv['max_over_plan_ratio'].max():.1f} 倍</b> ({piv.loc[piv['max_over_plan_ratio'].idxmax(), 'city']})。</li>
<li><b>政策的含意</b>: 計画規模ベースの避難計画は <b>過去最大級時の {piv['max_over_plan_ratio'].median():.1f} 倍</b>の住民を吸収できる体制が必要。
この倍率は他県との比較指標に使える <b>1 数字</b>。</li>
<li><b>倍率ばらつきの大きさ</b> ({piv['max_over_plan_ratio'].std():.2f} 標準偏差) は、市町ごとの <b>河川整備状況の差</b>を映している可能性。
内陸の整備が遅れている水系では計画規模が小さく、最大規模との差が広がる。</li>
</ul>
"""))


# --- 9. 分析6: リスク人口 -------------------------------------------------
sections.append(("分析6: リスク人口推定 — DID 人口 × 重なり率 で見る (H5 検証)", f"""
<h3>狙い</h3>
<p>面積率は地形指標、人口は社会指標。両者を掛け合わせた <b>「リスク人口」</b>こそが
<b>避難計画・避難所キャパ・物資備蓄</b>の直接の判断材料になる。
本セクションでは 14 市町をリスク人口降順に並べ、上位 3 で県全体の何 % を占めるか (H5) を確認する。</p>

<h3>定義 (要件M 再掲)</h3>
<p><b>リスク人口 (city) = DID 人口 (city) × 想定最大規模重なり率 (city)</b></p>
<p>これは「DID 内の人口が <b>均一に分布している</b>」と仮定したときの、浸水想定区域内の住民数の推定値。
実際は人口は浸水想定区域 <b>外</b>に偏ることもあるため (高台に住む傾向)、
本指標は <b>上限値の目安</b>として読む。</p>

<h3>手法</h3>
<ul>
<li>表3 の <code>pop</code> 列 (DID 人口, JINKOU_S 合計) と <code>overlap_rate_想定最大規模</code> 列を掛け合わせる。</li>
<li>14 市町をリスク人口降順にソート、累積シェア (=上位 k 市町でリスク人口の何%) を計算。</li>
<li>図5 は左に水平棒ランキング、右に累積寄与曲線を 1 枚で並べる。</li>
</ul>

<h3>実装</h3>
""" + code(r'''
risk = piv[["city", "pop", "overlap_rate_想定最大規模"]].copy()
risk["risk_pop"] = (risk["pop"] * risk["overlap_rate_想定最大規模"]).round().astype(int)
risk = risk.sort_values("risk_pop", ascending=False).reset_index(drop=True)
risk["share_pct"] = (risk["risk_pop"] / risk["risk_pop"].sum() * 100).round(2)
risk["cum_share_pct"] = risk["share_pct"].cumsum().round(2)
top3_share = risk.head(3)["risk_pop"].sum() / risk["risk_pop"].sum() * 100
print(f"上位3市町シェア: {{top3_share:.1f}}%")
''') + f"""

<h3>結果 (表と読み取り)</h3>
<h4>表4: 14 市町別 リスク人口 (降順)</h4>
{risk_html}

<p><b>この表から読み取れること</b>:</p>
<ul>
<li><b>1 位 = {risk.iloc[0]['city']}</b>: リスク人口 <b>{risk.iloc[0]['risk_pop']:,.0f} 人</b> (シェア {risk.iloc[0]['share_pct']:.1f}%)。
<b>DID 人口の {risk.iloc[0]['rate_max']*100:.1f}%</b> が想定最大規模時に浸水想定区域内。</li>
<li><b>上位 3 市町</b> = {", ".join(risk.head(3)['city'])} で県全体の <b>{top3_share:.1f}%</b> のリスク人口を占める。
H5 ({'支持' if h5_supported else '反証'}: 50%超 {'達成' if h5_supported else '未達'})。</li>
<li><b>下位市町</b> = リスク人口数百人規模。これは <b>絶対値では小さい</b>が、
<b>市町内のシェア</b>では下位とは限らない (例: 海田町は人口 1〜2 万人だがリスク人口比率は中位)。</li>
<li><b>政策的含意</b>: リスク人口 1 万人超の市町には <b>1 万人収容の広域避難計画</b>が要る。
県は上位 3 市町に重点投資すれば <b>県全体リスクの 50% 超</b>をカバーできる効率指標。</li>
</ul>

<h3>結果 (図と読み取り)</h3>
{figure("assets/X07_risk_population.png", "図5: 市町別リスク人口ランキング棒 + 累積寄与曲線")}

<p><b>この図から読み取れること</b>:</p>
<ul>
<li><b>左パネル</b>: 1 位の {risk.iloc[0]['city']} の棒が突出して長い。
2 位以下は徐々に減少し、最下位 ({risk.iloc[-1]['city']}) は 1 位の <b>1/{int(risk.iloc[0]['risk_pop']/max(risk.iloc[-1]['risk_pop'],1))}</b> 規模。
<b>1 桁以上の格差</b>が県内に存在 = 県の防災予算配分の根拠。</li>
<li><b>右パネル累積曲線</b>: 上位 {(risk['cum_share_pct']<=50).sum()+1} 市町で 50%、
上位 {(risk['cum_share_pct']<=80).sum()+1} 市町で 80% に達する。
{(80 in [int(c) for c in risk['cum_share_pct']]) and '急峻に立ち上がるほどリスク人口が一極集中' or '緩やかな曲線 = リスクが分散している証拠'}。</li>
<li><b>仮説判定</b>: 上位 3 シェア {top3_share:.1f}% は <b>{'50% 超 → H5 支持' if h5_supported else '50% 未満 → H5 反証'}</b>。
{('政策的には「上位 3 市町への重点投資」で大半をカバーできる構造' if h5_supported else 'リスクが県内 14 市町に比較的均等に分散しており、「全市町への薄く広い投資」が必要')}。</li>
</ul>
"""))


# --- 10. 仮説検証と考察 -------------------------------------------------
sections.append(("仮説検証と考察", f"""
<h3>仮説判定表</h3>
{verdict_html}

<h3>主要発見の物語化</h3>
<ol>
<li><b>「住んでる人ほど危ない場所に住んでいる」は実証された (H1 {('支持' if h1_supported else '反証')}, 平均 {mean_rate_max*100:.1f}%)</b>。
広島県の DID は平均で <b>3 ヘクタールに 1 ヘクタール</b>程度が河川浸水想定区域に重なる。
これは日本の都市インフラが <b>歴史的に河川沿いに発展してきた</b>こと、
そして <b>人口集中地区の定義 (人口密度 4000人/km²)</b> 自体が低地・平野部に偏る統計的構造の表れである。</li>
<li><b>規模シナリオの差は計画規模の {piv['max_over_plan_ratio'].median():.1f} 倍</b> (H3 強支持)。
過去最大級の降雨では DID の <b>{piv['max_over_plan_ratio'].median():.1f} 倍</b>の領域が浸水する。
2018 年の西日本豪雨はこの「想定最大規模」に近い実例で、計画規模ベースの防災では足りない歴史教訓。</li>
<li><b>地理仮説 (H2 内陸&gt;沿岸) は {'支持' if h2_supported else '反証'}</b> (内陸 {inland_rate*100:.1f}% vs 沿岸 {coastal_rate*100:.1f}%)。
{('内陸盆地は河川が市街地を貫くため重なり率が高く、沿岸は埋立地・高台が多く重なり率が低い、という地形仮説が確認された。' if h2_supported else '沿岸都市でも河口部に DID があるため重なり率が高く、内陸との差は小さい。地形仮説の単純化は不正確だった。')}</li>
<li><b>面積と重なりの強い線形性 (H4 {('支持' if h4_supported else '反証')}, R² = {r2:.3f})</b>。
DID 面積で重なり面積の <b>{r2*100:.0f}%</b> が説明できる = ほぼ比例関係。
ただし傾きから外れる都市 ({piv.loc[piv['overlap_rate_想定最大規模'].idxmax(), 'city']} 等) は <b>地形特殊性</b>を持つ。</li>
<li><b>リスク人口の偏在 (H5 上位3シェア {top3_share:.1f}%, {'支持' if h5_supported else '反証'})</b>。
{('上位 3 市町への重点投資で県全体リスクの過半をカバーできる構造。' if h5_supported else '14 市町に均等に分散しており、県全域での薄い投資が必要。')}</li>
</ol>

<h3>方法論的限界 (この記事を「鵜呑みにしない」ためのチェック)</h3>
<ul>
<li><b>Monte Carlo の精度</b>: N=5,000 では ±1 ポイント程度の誤差。本記事は重なり率を %.1 まで報告しているが、
本来は <b>「14% ± 1 ポイント」</b>と読むべき。</li>
<li><b>球面近似の誤差</b>: EPSG:6671 → 経緯度の球面近似で県内最大 ~50 m。
重なり率 % のオーダーには影響しないが、<b>絶対面積 (ha)</b>の小数点以下は信頼できない。</li>
<li><b>「全河川」集約版の限界</b>: 個別水系 37 件を独立に重ね合わせれば <b>水系別の寄与</b>が見える。
本記事では集約版を使ったため <b>「どの河川が一番危険か」</b>は分からない (発展課題)。</li>
<li><b>DID 内の人口分布仮定</b>: リスク人口は DID 内人口が均一と仮定。
実際は <b>地形が高い場所ほど人口が偏る</b>傾向がある (= リスク人口は <b>過大評価</b>になりがち)。</li>
<li><b>計画規模・想定最大規模の意味</b>: 「想定最大規模 = 最悪」ではない (堤防決壊シナリオは別個に評価される)。
本指標は「自然氾濫の浸水想定」であり、複合災害リスクは別途扱う。</li>
</ul>

<h3>主要発見 (1 段落要約)</h3>
<p><b>広島県 14 市町の DID は、想定最大規模降雨で平均 {mean_rate_max*100:.1f}% が浸水想定区域に重なる。
1 位の {piv.iloc[0]['city']} ({piv.iloc[0]['overlap_rate_想定最大規模']*100:.1f}%) と最下位の {piv.iloc[-1]['city']} ({piv.iloc[-1]['overlap_rate_想定最大規模']*100:.1f}%) で約 {piv.iloc[0]['overlap_rate_想定最大規模']/max(piv.iloc[-1]['overlap_rate_想定最大規模'],0.001):.0f} 倍の格差。
リスク人口は上位 3 市町 ({", ".join(risk.head(3)['city'])}) で県全体の {top3_share:.1f}% を占め、
「住んでる人ほど危ない場所に」仮説は中規模で支持された。
規模シナリオでは想定最大規模が計画規模の中央値 {piv['max_over_plan_ratio'].median():.1f} 倍の DID を浸水させ、
2018 年水害級の備えが構造的に必要であることが定量化された。</b></p>
"""))


# --- 11. 発展課題 -----------------------------------------------------------
sections.append(("発展課題 (結果X → 新仮説Y → 課題Z)", f"""
<p>本レッスンの結果から、次の 5 つの発展課題が論理的に導かれる:</p>

<h3>課題1: 水系別リスクマップ (本記事の集約版を解きほぐす)</h3>
<ul>
<li><b>結果X</b>: 全河川集約版で県全体の重なり率は明らかになったが、<b>どの水系が一番リスク寄与が大きいか</b>は不明。</li>
<li><b>新仮説Y</b>: 太田川水系 (#35, #36) は広島市の DID を貫くため <b>単独で県全体リスクの 30% 以上</b>を占めるはず。</li>
<li><b>課題Z</b>: 個別水系 37 件を 1 件ずつ DID と重ね合わせ、<b>水系×市町</b>のクロス表を作る。
データ量が ~30 GB になるので、<b>1 水系 = 1 ファイル</b>を逐次処理 (本記事の Monte Carlo を流用) するスクリプトを書く。</li>
</ul>

<h3>課題2: 浸水深を考慮したリスクスコア</h3>
<ul>
<li><b>結果X</b>: 本記事は「重なる/重ならない」の二値判定。<b>浸水深 (0.5m vs 5.0m)</b>の差は無視。</li>
<li><b>新仮説Y</b>: 同じ重なり面積でも、<b>浸水深 3m 以上の領域が DID の何 %</b>かで実被害は大きく変わる。
広島市は河口部の深い浸水想定が多く、リスクスコアでは順位が変わるはず。</li>
<li><b>課題Z</b>: DoBoX #1641 (多段階の浸水想定図) と本記事の DID を重ね合わせ、
<b>浸水深×面積</b>を浸水深カテゴリ別に集計。リスクスコアを <b>面積 × 重みづけ深さ</b>で再定義する。</li>
</ul>

<h3>課題3: 高潮浸水・津波浸水との合成リスク</h3>
<ul>
<li><b>結果X</b>: H2 (内陸 vs 沿岸) は{('支持' if h2_supported else '反証')}されたが、これは <b>河川氾濫のみ</b>の指標。</li>
<li><b>新仮説Y</b>: 沿岸都市は河川氾濫リスクは低くても、<b>高潮 (#43〜#45) + 津波 (#46)</b> を加えると総リスクは内陸都市より高くなる。</li>
<li><b>課題Z</b>: DoBoX #43 (高潮 30 年確率), #45 (高潮想定最大), #46 (津波) を本記事の DID と <b>3 重重ね合わせ</b>し、
<b>「3 災害合成リスク人口」</b>を市町別に算出。沿岸都市の真の脆弱性を可視化する。</li>
</ul>

<h3>課題4: DID 人口の経年変化と重なり率の動的追跡</h3>
<ul>
<li><b>結果X</b>: 本記事は 2020 年 DID と現行浸水想定の <b>静的重なり</b>。<b>少子高齢化</b>でこの重なり率は変わるはず。</li>
<li><b>新仮説Y</b>: 縮退する都市 (人口減少) では <b>DID が縮退して重なり率は下がる</b> (高台に集約されるため) が、
拡大する都市 (広島市・東広島市) では <b>河川沿いの新興 DID 拡大で重なり率が上がる</b>。</li>
<li><b>課題Z</b>: DoBoX の 2010/2015/2020 年 DID 履歴を取得し、<b>10 年スパンでの重なり率変化</b>を計算。
広島市と三次市で <b>逆方向</b>のトレンドが見えれば本仮説を支持する。</li>
</ul>

<h3>課題5: Monte Carlo を厳密多角形クリッピングで置き換え</h3>
<ul>
<li><b>結果X</b>: 本記事の Monte Carlo は ±1 ポイント誤差を抱える。<b>厳密値</b>は出ない。</li>
<li><b>新仮説Y</b>: 厳密多角形クリッピング (Sutherland–Hodgman 法または Shapely) で計算すると、
本記事の数値が <b>±0.5 ポイント以内に収まる</b>はず。</li>
<li><b>課題Z</b>: shapely の <code>polygon.intersection()</code> を使った実装に置き換え、
本記事と <b>14 市町すべての重なり率を比較</b>。差が 1 ポイント以下なら Monte Carlo は実用十分と判断。</li>
</ul>
"""))


# === HTML 書き出し ===========================================================
data_label = (
    f"DID 14 市町 ({len(did_polys)} ポリゴン) + 浸水想定 計画規模 "
    f"{len(flood_data['計画規模'])} ポリゴン + 想定最大規模 "
    f"{len(flood_data['想定最大規模'])} ポリゴン  (DoBoX 直 DL)"
)

html = render_lesson(
    num=70,  # X07 として L01〜L10 を超える上位ナンバリング
    title="X07: 河川浸水想定区域 × DID 人口集中地区 — 「住んでる人ほど危ない場所に」仮説検証",
    tags=["X-tier", "空間オーバーレイ研究", "Monte Carlo", "ポリゴン面積結合",
          "DID", "浸水想定区域", "リスク人口", "L01"],
    time="60-90分",
    level="リテラシレベル (L01)",
    data_label=data_label,
    sections=sections,
    script_filename="X07_flood_did_overlap.py",
)

# data-draft="1" data-stier="X" を <body> に挿入
html = html.replace(
    '<body><div class="container">',
    '<body data-draft="1" data-stier="X"><div class="container">',
)

out_html = LESSONS / "X07_flood_did_overlap.html"
out_html.write_text(html, encoding="utf-8")
print(f"\n→ {out_html}")
print("\n=== 主要発見 ===")
print(findings_text)
print("\n=== 完了 ===")
