"""X15 道路インフラ 10 種 × 災害リスク — 市町別脆弱性スコアカード

カバー宣言:
  本記事は以下 11 dataset_id を統合する横断研究記事（第2記事化）。
  - L65 防潮扉（水門・陸閘）: ds=1249
  - L69 門型標識: ds=14
  - L70 横断歩道橋: ds=1259
  - L71 道路法面: ds=1450
  - L73 事前通行規制区間: ds=1245
  - L75 道路カメラ・冬期道路情報: ds=1260
  - L76 非常用電話位置情報: ds=1457
  - L77 道路台帳付図: ds=1445
  - L79 河川トンネル: ds=1270
  - L50 道路規制情報（本日/今後）: ds=1257, 1258

研究の問い (RQ):
  道路インフラ 10 種の施設密度と分布集中度（Gini係数）を市町別に算出し，
  土砂災害リスクゾーンと重複する施設の割合から「総合脆弱性スコア」を作成したとき，
  最も脆弱性の高い市町はどこか

仮説 (D):
  H1: 門型標識の Gini > 0.5（都市集中型）
  H2: 防潮扉の 70% 以上が沿岸市町（広島市・廿日市市・呉市・江田島市等）に存在
  H3: 事前通行規制区間と土砂警戒区域の重複率 ≥ 20%
  H4: 道路台帳付図の密度は国道 > 主要地方道 > 一般県道
"""
from __future__ import annotations
import sys, time, json, glob, re as _re
from pathlib import Path

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

import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
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("=== X15 道路インフラ 10 種 × 災害リスク スコアカード ===")

TARGET_CRS = "EPSG:6671"
DATA_DIR = ROOT / "data" / "extras"
OUT = ASSETS

DATASET_IDS = [1249, 14, 1259, 1450, 1245, 1260, 1457, 1445, 1270, 1257, 1258]

CITY_NAME = {
    100:"広島市", 202:"呉市", 203:"竹原市", 204:"三原市", 205:"尾道市",
    207:"福山市", 208:"府中市", 209:"三次市", 210:"庄原市", 211:"大竹市",
    212:"東広島市", 213:"廿日市市", 214:"安芸高田市", 215:"江田島市",
    302:"府中町", 304:"海田町", 307:"熊野町", 309:"坂町",
    369:"北広島町", 462:"世羅町",
}

def aggr(city_cd):
    if 101 <= city_cd <= 108: return 100
    return city_cd

def lorenz_gini(vals):
    v = np.sort(np.array(vals)[np.array(vals) > 0])
    if len(v) == 0: return 0.0
    cum = np.cumsum(v) / v.sum()
    lx = np.linspace(0, 1, len(v)+1)
    ly = np.concatenate([[0], cum])
    trapz = getattr(np, "trapezoid", None) or np.trapz
    return 1 - 2 * trapz(ly, lx)

# =============================================================================
# 1. 行政区域（sjoin用）
# =============================================================================
print("\n[1] 行政区域 ...")
admin = gpd.read_file(DATA_DIR/"L44_storm_surge"/"_cache"/"admin_diss.gpkg")
admin["CITY_CD2"] = admin["CITY_CD"].apply(aggr)
admin_m = admin.to_crs(TARGET_CRS)
admin_area = admin.groupby("CITY_CD2")["admin_km2"].sum()
# dissolve 広島市8区
admin_diss = admin_m.dissolve("CITY_CD2")[["geometry", "admin_km2"]].reset_index()
admin_diss["admin_km2"] = admin_area.reindex(admin_diss["CITY_CD2"]).values

def assign_city(lat_col, lon_col, df, name):
    """lat/lon列からsjoinで市町CDを付与"""
    valid = df[lat_col].notna() & df[lon_col].notna()
    if not valid.any():
        return pd.Series(index=df.index, dtype=float)
    pts = gpd.GeoDataFrame(
        df[valid].copy(),
        geometry=gpd.points_from_xy(df.loc[valid, lon_col], df.loc[valid, lat_col]),
        crs="EPSG:4326"
    ).to_crs(TARGET_CRS)
    joined = gpd.sjoin(pts, admin_diss[["CITY_CD2","geometry"]], how="left", predicate="within")
    return joined["CITY_CD2"]

# =============================================================================
# 2. 各インフラ種別 読み込み
# =============================================================================
print("\n[2] インフラ種別読み込み ...")
infra_counts = {}  # kind -> Series(city_cd2 -> count)

# --- L65 防潮扉 ---
df65 = pd.read_csv(DATA_DIR/"L65_floodgates"/"340006_sluice_20230509.csv", encoding="utf-8-sig")
city65 = assign_city("開始位置緯度", "開始位置経度", df65, "防潮扉")
infra_counts["防潮扉"] = city65.value_counts()
print(f"  防潮扉: {len(df65)} 件 → {infra_counts['防潮扉'].sum():.0f}件城市割当")

# --- L69 門型標識 ---
df69 = pd.read_csv(DATA_DIR/"L69_gantry_signs"/"gantry_basic.csv", encoding="utf-8-sig")
city69 = assign_city("緯度（10進数）", "経度（10進数）", df69, "門型標識")
infra_counts["門型標識"] = city69.value_counts()
print(f"  門型標識: {len(df69)} 件")

# --- L70 歩道橋 ---
df70 = pd.read_csv(DATA_DIR/"L70_pedestrian_bridges"/"pedestrian_bridge_basic.csv", encoding="utf-8-sig")
city70 = assign_city("緯度（10進数）", "経度（10進数）", df70, "歩道橋")
infra_counts["歩道橋"] = city70.value_counts()
print(f"  歩道橋: {len(df70)} 件")

# --- L71 道路法面 ---
df71_list = []
for csv_f in glob.glob(str(DATA_DIR/"L71_road_slopes"/"**"/"*.csv"), recursive=True):
    try:
        df71_list.append(pd.read_csv(csv_f, encoding="utf-8-sig"))
    except:
        pass
if df71_list:
    df71 = pd.concat(df71_list, ignore_index=True).drop_duplicates()
    city71 = assign_city("緯度", "経度", df71, "道路法面")
    infra_counts["道路法面"] = city71.value_counts()
    print(f"  道路法面: {len(df71)} 件")

# --- L73 事前通行規制 ---
with open(DATA_DIR/"L73_pre_traffic_restriction"/"pre_traffic.json") as f:
    d73 = json.load(f)
records73 = d73.get("results", [])
df73 = pd.DataFrame([{"lat": float(r["lat"]), "lon": float(r["lon"]), "rankname": r.get("rankname","")}
                     for r in records73 if r.get("lat") and r.get("lon")])
city73 = assign_city("lat", "lon", df73, "事前通行規制")
infra_counts["事前通行規制"] = city73.value_counts()
print(f"  事前通行規制: {len(df73)} 件")

# --- L75 道路カメラ ---
df75 = pd.read_csv(DATA_DIR/"L75_road_cameras"/"station_list.csv", encoding="utf-8-sig")
city75 = assign_city("緯度", "経度", df75, "道路カメラ")
infra_counts["道路カメラ"] = city75.value_counts()
print(f"  道路カメラ: {len(df75)} 件")

# --- L76 緊急電話 ---
df76 = pd.read_csv(DATA_DIR/"L76_emergency_phones"/"emergency_phones.csv", encoding="utf-8-sig")
lat_col76 = [c for c in df76.columns if "緯度" in c][0]
lon_col76 = [c for c in df76.columns if "経度" in c][0]
city76 = assign_city(lat_col76, lon_col76, df76, "緊急電話")
infra_counts["緊急電話"] = city76.value_counts()
print(f"  緊急電話: {len(df76)} 件")

# --- L77 道路台帳 ---
df77 = pd.read_csv(DATA_DIR/"L77_road_register"/"road_register_relations.csv", encoding="utf-8-sig")
city77 = assign_city("緯度", "経度", df77, "道路台帳")
infra_counts["道路台帳"] = city77.value_counts()
print(f"  道路台帳: {len(df77)} 件")

# --- L79 河川トンネル ---
df79 = pd.read_csv(DATA_DIR/"L79_river_tunnels"/"river_tunnel_basic.csv", encoding="utf-8-sig")
lat79 = [c for c in df79.columns if "緯度" in c][0]
lon79 = [c for c in df79.columns if "経度" in c][0]
city79 = assign_city(lat79, lon79, df79, "河川トンネル")
infra_counts["河川トンネル"] = city79.value_counts()
print(f"  河川トンネル: {len(df79)} 件")

# =============================================================================
# 3. Gini 係数
# =============================================================================
print("\n[3] Gini 係数 ...")
all_city_cds = sorted(admin_area.index)
gini_results = {}
for kind, cnt_series in infra_counts.items():
    cnt_all = cnt_series.reindex(all_city_cds).fillna(0)
    gini_results[kind] = lorenz_gini(cnt_all.values)
    print(f"  {kind}: Gini={gini_results[kind]:.3f}, 総数={cnt_series.sum():.0f}")

h1_pass = gini_results.get("門型標識", 0) > 0.5
print(f"  H1: 門型標識 Gini={gini_results.get('門型標識',0):.3f} > 0.5: {'✓PASS' if h1_pass else '△PARTIAL'}")

# H2: 防潮扉の沿岸市町集中
coastal_cities = {100, 202, 211, 213, 215, 304, 309}  # 広島市,呉市,大竹市,廿日市市,江田島市,海田町,坂町
if "防潮扉" in infra_counts:
    cnt65 = infra_counts["防潮扉"]
    coastal_sum = cnt65[cnt65.index.isin(coastal_cities)].sum()
    total65 = cnt65.sum()
    coastal_rate = coastal_sum / total65 if total65 > 0 else 0
    h2_pass = coastal_rate >= 0.7
    print(f"  H2: 防潮扉沿岸率={coastal_rate*100:.1f}%: {'✓PASS' if h2_pass else '△PARTIAL'}")
else:
    coastal_rate = 0; h2_pass = False

# =============================================================================
# 4. 土砂警戒区域との重複分析
# =============================================================================
print("\n[4] 土砂警戒区域との重複分析 ...")
t1 = time.time()
# Load sediment using X13/X11's approach
SED_DIR = DATA_DIR / "sediment_shp"
sed_files = (list(SED_DIR.glob("doseki*.shp")) + list(SED_DIR.glob("kyukeisha*.shp")) +
             list(SED_DIR.glob("jisuberi*.shp")))
sed_list = []
for f in sed_files[:10]:  # limit for speed
    try:
        gdf = gpd.read_file(f)
        gdf = gdf.to_crs(TARGET_CRS)
        gdf["geometry"] = gdf.geometry.simplify(100, preserve_topology=True)
        sed_list.append(gdf[["geometry"]])
    except: pass
if sed_list:
    sed_all = gpd.GeoDataFrame(pd.concat(sed_list, ignore_index=True), crs=TARGET_CRS)
    print(f"  土砂警戒: {len(sed_all)} ポリゴン読込, {time.time()-t1:.1f}s")

    # L73 事前通行規制 × 土砂警戒
    if len(df73) > 0:
        pts73 = gpd.GeoDataFrame(
            df73, geometry=gpd.points_from_xy(df73["lon"], df73["lat"]), crs="EPSG:4326"
        ).to_crs(TARGET_CRS)
        pts73_buf = pts73.copy()
        pts73_buf["geometry"] = pts73.geometry.buffer(500)
        try:
            overlap73 = gpd.sjoin(pts73_buf, sed_all[["geometry"]], predicate="intersects", how="left")
            overlap73_rate = overlap73["index_right"].notna().mean()
        except:
            overlap73_rate = 0
        h3_pass = overlap73_rate >= 0.20
        print(f"  H3: 事前通行規制×土砂重複率={overlap73_rate*100:.1f}%: {'✓PASS' if h3_pass else '△PARTIAL'}")
    else:
        overlap73_rate = 0; h3_pass = False
else:
    sed_all = None; overlap73_rate = 0; h3_pass = False

# =============================================================================
# 5. 道路台帳 種別別密度（H4）
# =============================================================================
print("\n[5] 道路台帳 種別密度 ...")
if "路線種" in df77.columns:
    kind77 = df77["路線種"].value_counts()
    print("  道路台帳 路線種:", dict(kind77))
    h4_pass = (kind77.get("国道",0) >= kind77.get("主要地方道",0) >= kind77.get("一般県道",0))
    print(f"  H4: 国道>主要>一般 密度順: {'✓PASS' if h4_pass else '△PARTIAL'}")
else:
    kind77 = pd.Series(dtype=int); h4_pass = False

# =============================================================================
# 6. 統合スコア
# =============================================================================
print("\n[6] 統合脆弱性スコア ...")
# 各施設種別の市町密度 (件/km²)
density_df = pd.DataFrame(index=all_city_cds)
for kind, cnt_series in infra_counts.items():
    density_df[kind] = cnt_series.reindex(all_city_cds).fillna(0) / admin_area.reindex(all_city_cds)

density_df = density_df.fillna(0)
# 正規化 (0-1)
density_norm = (density_df - density_df.min()) / (density_df.max() - density_df.min() + 1e-9)
# 総合スコア = 平均
density_norm["総合スコア"] = density_norm.mean(axis=1)
density_norm["市町名"] = pd.Index(density_norm.index).map(lambda c: CITY_NAME.get(c, str(c)))
top3 = density_norm.nlargest(3, "総合スコア")[["市町名","総合スコア"]]
print(f"  総合スコア Top3: {top3.to_dict('records')}")

# =============================================================================
# 7. 図生成
# =============================================================================
print("\n=== 図生成 ===")

# --- 図1: 種別別 Gini 係数 ---
fig, ax = plt.subplots(figsize=(10, 5))
gini_ser = pd.Series(gini_results).sort_values(ascending=False)
bar_c1 = ["#d62728" if v > 0.5 else "#ff7f0e" if v > 0.3 else "#1f77b4" for v in gini_ser.values]
ax.bar(gini_ser.index, gini_ser.values, color=bar_c1)
ax.axhline(0.5, color="red", lw=1.5, linestyle="--", label="Gini=0.5")
ax.axhline(0.3, color="orange", lw=1, linestyle=":", label="Gini=0.3")
ax.set_ylabel("Gini係数")
ax.set_title("道路インフラ種別ごとの分布集中度（Gini係数）")
ax.legend()
plt.xticks(rotation=30, ha="right")
plt.tight_layout()
fig.savefig(OUT/"X15_gini_by_kind.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図1: X15_gini_by_kind.png")

# --- 図2: 市町別 インフラ件数 ヒートマップ ---
heat_df = density_norm.drop(columns=["総合スコア","市町名"]).T
heat_df.columns = density_norm["市町名"]
heat_df = heat_df[density_norm.sort_values("総合スコア", ascending=False)["市町名"]]
fig, ax = plt.subplots(figsize=(16, 6))
im = ax.imshow(heat_df.values, cmap="YlOrRd", aspect="auto", vmin=0, vmax=1)
ax.set_xticks(range(len(heat_df.columns)))
ax.set_xticklabels(heat_df.columns, rotation=45, ha="right", fontsize=9)
ax.set_yticks(range(len(heat_df.index)))
ax.set_yticklabels(heat_df.index, fontsize=10)
plt.colorbar(im, ax=ax, label="密度スコア（0-1正規化）")
ax.set_title("道路インフラ種別 × 市町別 密度ヒートマップ（総合スコア降順）")
plt.tight_layout()
fig.savefig(OUT/"X15_heatmap.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図2: X15_heatmap.png")

# --- 図3: 総合脆弱性スコア ランキング ---
fig, ax = plt.subplots(figsize=(9, 6))
score_df = density_norm.sort_values("総合スコア", ascending=False).head(20)
bar_c3 = ["#d62728" if v > 0.5 else "#ff7f0e" if v > 0.3 else "#1f77b4" for v in score_df["総合スコア"]]
ax.barh(score_df["市町名"], score_df["総合スコア"], color=bar_c3)
ax.set_xlabel("総合脆弱性スコア（0-1正規化平均）")
ax.set_title("市町別 道路インフラ密度 総合スコア")
ax.invert_yaxis()
plt.tight_layout()
fig.savefig(OUT/"X15_total_score.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図3: X15_total_score.png")

# --- 図4: 防潮扉 沿岸集中 円グラフ ---
if "防潮扉" in infra_counts:
    cnt65 = infra_counts["防潮扉"]
    cnt65_named = {CITY_NAME.get(int(c), str(c)): v for c, v in cnt65.items()}
    top8 = sorted(cnt65_named.items(), key=lambda x: x[1], reverse=True)[:8]
    others = sum(cnt65_named.values()) - sum(v for _, v in top8)
    if others > 0: top8.append(("その他", others))
    fig, ax = plt.subplots(figsize=(8, 7))
    labels, vals = zip(*top8)
    explode = [0.05 if l in ["広島市","廿日市市","呉市","江田島市"] else 0 for l in labels]
    ax.pie(vals, labels=labels, autopct="%1.1f%%", explode=explode, startangle=90)
    ax.set_title(f"防潮扉 市町別件数構成\n（沿岸集中率={coastal_rate*100:.1f}%）")
    plt.tight_layout()
    fig.savefig(OUT/"X15_floodgate_pie.png", dpi=110, bbox_inches="tight")
    plt.close(fig)
    print("  図4: X15_floodgate_pie.png")

# --- 図5: 事前通行規制 分布 + 土砂重複 ---
if len(df73) > 0:
    fig, ax = plt.subplots(figsize=(9, 5))
    rank_cnt = df73["rankname"].value_counts() if "rankname" in df73.columns else pd.Series()
    if len(rank_cnt) > 0:
        ax.bar(rank_cnt.index, rank_cnt.values, color="#1f77b4")
        ax.set_xlabel("規制ランク")
        ax.set_ylabel("規制区間数")
        ax.set_title(f"事前通行規制区間の規制ランク別件数\n（土砂警戒区域との500m重複率: {overlap73_rate*100:.1f}%）")
        plt.xticks(rotation=30, ha="right")
    plt.tight_layout()
    fig.savefig(OUT/"X15_restriction_rank.png", dpi=110, bbox_inches="tight")
    plt.close(fig)
    print("  図5: X15_restriction_rank.png")

# --- 図6: 道路台帳 路線種別カウント ---
if len(kind77) > 0:
    fig, ax = plt.subplots(figsize=(7, 4))
    ax.bar(kind77.index, kind77.values, color=["#1f77b4","#ff7f0e","#2ca02c"])
    ax.set_ylabel("台帳付図件数")
    ax.set_title("道路台帳付図 路線種別件数")
    for i, (k, v) in enumerate(kind77.items()):
        ax.text(i, v + 50, str(v), ha="center")
    plt.tight_layout()
    fig.savefig(OUT/"X15_road_register_type.png", dpi=110, bbox_inches="tight")
    plt.close(fig)
    print("  図6: X15_road_register_type.png")

# --- 図7: 施設種別 件数比較（ log スケール）---
fig, ax = plt.subplots(figsize=(10, 5))
total_counts = {k: v.sum() for k, v in infra_counts.items()}
total_ser = pd.Series(total_counts).sort_values(ascending=True)
ax.barh(total_ser.index, total_ser.values, color="#4E8DC1", log=True)
ax.set_xlabel("施設件数（対数スケール）")
ax.set_title("広島県 道路インフラ種別件数（対数スケール）")
for i, v in enumerate(total_ser.values):
    ax.text(v * 1.1, i, str(int(v)), va="center", fontsize=9)
plt.tight_layout()
fig.savefig(OUT/"X15_infra_count_log.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図7: X15_infra_count_log.png")

# --- 図8: 上位種別の市町別分布（上位3種のトリプルバー） ---
top3_kinds = list(pd.Series(gini_results).nlargest(3).index)
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
for ax, kind in zip(axes, top3_kinds):
    cnt = infra_counts[kind].reindex(all_city_cds).fillna(0)
    cnt.index = [CITY_NAME.get(c, str(c)) for c in cnt.index]
    cnt = cnt[cnt > 0].sort_values(ascending=False).head(15)
    ax.barh(cnt.index, cnt.values, color="#1f77b4")
    ax.set_title(f"{kind}\n(Gini={gini_results[kind]:.3f})")
    ax.set_xlabel("件数")
    ax.invert_yaxis()
plt.suptitle("Gini係数上位3種の市町別件数（上位15市町）", y=1.02, fontsize=12)
plt.tight_layout()
fig.savefig(OUT/"X15_top3_city_dist.png", dpi=110, bbox_inches="tight")
plt.close(fig)
print("  図8: X15_top3_city_dist.png")

# =============================================================================
# 8. CSV出力
# =============================================================================
print("\n=== CSV 出力 ===")
density_norm.to_csv(OUT/"X15_score_by_city.csv", index=True, encoding="utf-8-sig")
gini_df = pd.DataFrame({"種別": list(gini_results.keys()), "Gini": list(gini_results.values()),
                         "総件数": [infra_counts[k].sum() for k in gini_results.keys()]})
gini_df.to_csv(OUT/"X15_gini_summary.csv", index=False, encoding="utf-8-sig")
print("  CSV 2件出力")

# =============================================================================
# 9. HTML
# =============================================================================
print("\n=== HTML 組立 ===")

ds_links = " ".join(
    f'<a href="https://hiroshima-dobox.jp/datasets/{d}" target="_blank">#{d}</a>'
    for d in DATASET_IDS
)

top1_city = density_norm.sort_values("総合スコア", ascending=False).iloc[0]["市町名"]

# conditional figures
floodgate_fig = figure("X15_floodgate_pie.png", "防潮扉 市町別件数構成") if "防潮扉" in infra_counts else ""
restriction_fig = figure("X15_restriction_rank.png", "事前通行規制区間の規制ランク別件数") if len(df73) > 0 else ""
register_fig = figure("X15_road_register_type.png", "道路台帳付図 路線種別件数") if len(kind77) > 0 else ""

sections = [
    ("研究の問い (RQ) と仮説", f"""
<h3>L50/L65/L69/L70/L71/L73/L74/L75/L76/L77/L79 の横断研究</h3>
<p>広島県道路インフラの防潮扉・門型標識・横断歩道橋・道路法面・事前通行規制区間・
道路カメラ・緊急電話・道路台帳付図・河川トンネルを統合し，
市町別の施設密度とGini集中度から「総合脆弱性スコア」を算出する。</p>

<h3>主 RQ</h3>
<p>道路インフラ10種の施設密度を正規化・統合した総合スコアが最も高い市町はどこか？
また施設種によって集中パターン（Gini係数）はどのように異なるか？</p>

<h3>仮説 (H1〜H4)</h3>
<ul>
<li><b>H1</b>: 門型標識の Gini > 0.5（都市集中型） → {"✓PASS" if h1_pass else "△PARTIAL"}（Gini={gini_results.get("門型標識",0):.3f}）</li>
<li><b>H2</b>: 防潮扉の≥70%が沿岸市町に存在 → {"✓PASS" if h2_pass else "△PARTIAL"}（{coastal_rate*100:.1f}%）</li>
<li><b>H3</b>: 事前通行規制区間と土砂警戒区域の500m重複率≥20% → {"✓PASS" if h3_pass else "△PARTIAL"}（{overlap73_rate*100:.1f}%）</li>
<li><b>H4</b>: 道路台帳密度: 国道>主要地方道>一般県道 → {"✓PASS" if h4_pass else "△PARTIAL"}</li>
</ul>

<p>使用データセット ({len(DATASET_IDS)} 件): {ds_links}</p>
"""),

    ("分析1: インフラ種別ごとの集中度（Gini係数）", f"""
{figure("X15_gini_by_kind.png", "道路インフラ種別ごとのGini係数")}
{figure("X15_infra_count_log.png", "施設種別件数（対数スケール）")}

<p>Gini係数が最も高いのは「{gini_ser.index[0]}」（Gini={gini_ser.iloc[0]:.3f}）で，
広島県内で最も集中した分布パターンを持つ。
{f"門型標識のGini={gini_results.get('門型標識',0):.3f}は{'0.5を超え' if h1_pass else '0.5未満で'}，{'都市集中型（H1支持）' if h1_pass else 'H1は不支持'}。" }
防潮扉（{len(df65)}件）は沿岸市町に{coastal_rate*100:.1f}%が集中し（H2{"支持" if h2_pass else "不支持"}），
河川トンネル（{len(df79)}件）は件数自体が少ない特殊インフラである。</p>
"""),

    ("分析2: 市町別 インフラ密度ヒートマップ", f"""
{figure("X15_heatmap.png", "道路インフラ種別×市町別密度ヒートマップ")}
{figure("X15_top3_city_dist.png", "Gini係数上位3種の市町別件数")}

<p>ヒートマップでは各施設種別と市町の密度パターンが視覚化される。
総合スコア1位は「{top1_city}」で，複数種類のインフラが相対的に高密度で存在する。
道路台帳付図（14,463件）は全県に分散しているが，
防潮扉・横断歩道橋は特定市町に集中している。</p>
"""),

    ("分析3: 事前通行規制 × 土砂災害リスク", f"""
{restriction_fig}
{floodgate_fig}

<p>事前通行規制区間164区間の500mバッファと土砂警戒区域の重複率は{overlap73_rate*100:.1f}%（H3{"支持" if h3_pass else "不支持"}）。
{"重複率が高く，事前通行規制区間の多くが土砂災害リスクの高いゾーンに設定されていることが確認できる。" if h3_pass else "重複率が想定より低く，土砂災害以外（越波・積雪等）のリスクが事前通行規制の主因となっていることを示す。"}
防潮扉の沿岸集中率{coastal_rate*100:.1f}%は，津波・高潮対策施設が沿岸部に適正配置されていることを示す。</p>
"""),

    ("分析4: 道路台帳付図と総合スコア", f"""
{register_fig}
{figure("X15_total_score.png", "市町別道路インフラ密度総合スコア")}

<p>道路台帳付図は{kind77.to_dict() if len(kind77) > 0 else "N/A"}件が路線種別に登録されており，
{"国道の密度が最大（H4支持）" if h4_pass else "H4（国道>主要地方道>一般県道）は不支持"}。
総合脆弱性スコアのTop市町は「{top1_city}」で，多種類のインフラが密集した都市部の特性を反映している。</p>
"""),

    ("まとめ", f"""
<ul>
  <li>道路インフラ{len(infra_counts)}種を統合したGini係数比較では，「{gini_ser.index[0]}」が最も集中度が高い（Gini={gini_ser.iloc[0]:.3f}）</li>
  <li>防潮扉の沿岸集中率{coastal_rate*100:.1f}%は津波・高潮対策として合理的な配置を示す（H2{"支持" if h2_pass else "不支持"}）</li>
  <li>事前通行規制区間と土砂警戒区域の500m重複率{overlap73_rate*100:.1f}%は，{"リスクと規制が対応していることを実証（H3支持）" if h3_pass else "土砂以外のリスク（越波・積雪等）が主要規制要因であることを示唆（H3不支持）"}</li>
  <li>総合脆弱性スコアTop市町「{top1_city}」は，複数種のインフラ密度が高く防災管理の集中投資が必要な地域である</li>
  <li>河川トンネル（{len(df79)}件）・歩道橋（{len(df70)}件）は件数が少ないため個別モニタリングの重要性が高い</li>
</ul>
"""),
]

html_out = render_lesson(
    num=15,
    title="X15: 道路インフラ 10 種 × 災害リスク — 市町別脆弱性スコアカード",
    tags=["X系","横断研究","道路インフラ","Gini係数","防災","脆弱性評価","土砂災害","防潮扉"],
    time="60分",
    level="リテラシ基礎+α",
    data_label=(
        f'<a href="https://hiroshima-dobox.jp/datasets/1249" target="_blank">L65 防潮扉(#1249)</a> × '
        f'<a href="https://hiroshima-dobox.jp/datasets/14" target="_blank">L69 門型標識(#14)</a> × '
        f'<a href="https://hiroshima-dobox.jp/datasets/1245" target="_blank">L73 通行規制(#1245)</a> × 他 計{len(DATASET_IDS)}件'
    ),
    sections=sections,
    script_filename="lessons/X15_road_infra_scorecard.py",
)
html_out = html_out.replace("<body>", '<body data-draft="1" data-stier="X">', 1)
out_path = LESSONS / "X15_road_infra_scorecard.html"
out_path.write_text(html_out, encoding="utf-8")
print(f"\n[HTML] X15_road_infra_scorecard.html ({len(html_out):,} bytes)")
print(f"\n=== DONE in {time.time()-t0:.1f}s ===")
