# -*- coding: utf-8 -*-
"""L67 トンネル基本情報・維持管理情報 単独 3 研究例分析
       — 広島県内 157 トンネル / 9 事務所 / 道路種別 2 (国道+県道) / 60 路線 を 3 角度で解読

カバー宣言:
  本記事は DoBoX のシリーズ「トンネル基本情報・維持管理情報」 1 件
  (dataset_id = 12) を <b>単独</b>で取り上げ、
  広島県が管理する <b>道路トンネル 157 件</b>を
  3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
  「県土の山岳バイパス」 を構成するトンネルを、構造規模・地形特性・橋梁との二極構造の
  3 軸から立体的に描く。

  「トンネル」 とは:
    山岳・地下を貫いて道路 (および鉄道) を通すための公共土木施設。本データは
    <b>道路トンネルのみ</b>を対象 (鉄道トンネルは管理体系が異なるため別)。
    広島県が管理する <b>国道 + 県道</b>上のトンネルを全件 (157 件) 収録。
    <b>道路法</b>に基づき 5 年に 1 回の点検が義務化 (2014 年改正) され、
    各トンネルに <b>トンネル番号 / 路線名 / 延長 / 幅員 / 建設年度 / 点検年度 /
    判定区分</b>などのメタデータが付随する。

  本記事は L66 (橋梁単独 3 RQ) と<b>厳密に区別</b>:
    L66 = 橋梁<b>単独</b>の構造分析 (= 種別・規模・年代・防災重要度)。
    L67 = トンネル<b>単独</b>の構造分析 + L66 との二極構造比較。
          「橋梁=平野・川越え、トンネル=山岳貫通」の地理分担を実証。
    両記事は補完関係で「県の道路インフラ」 の 2 つの軸を構成する。

研究の問い (3 RQ):
  RQ1 (主研究): 広島県の道路トンネルの<b>構造 — 規模・地理分布・路線分布</b>はどう描けるか?
       157 件を市町 × 事務所 × 道路種別 × 路線 × 延長 × 幅員 × 建設年代 で
       多角度に集計し、地理分布と規模分布を立体化する。
       特に「県のトンネル網」 の物理的形状を初めて定量化する。

  RQ2 (副研究 1): トンネルの<b>地形特性 — 山岳道路の分断回避</b>はどこで起きているか?
       中山間市町 (北広島町・安芸太田町・庄原市・三次市等) でのトンネル集中度を可視化、
       「山岳バイパス」 機能を建設年代 + 立地でひも解く。
       平野市町 (福山市・広島市等) と中山間市町 (北広島町・安芸太田町等) の
       トンネル密度差を定量化する。

  RQ3 (副研究 2): <b>橋梁 (L66 既知 4,203 橋) との対比 — 道路インフラ二極構造</b>
       はどう現れるか?
       L66 橋梁 4,203 件 vs L67 トンネル 157 件の<b>件数比 27:1</b>、
       平均規模、整備年代、地理分担を比較。
       「橋梁=平野・川越え、トンネル=山岳貫通」 の補完関係を実証する。

仮説 (5):
  H1 (国道偏重, RQ1): トンネルは橋梁と異なり<b>国道シェア ≥ 50%</b>。
       橋梁では国道シェア 27.4% (L66) だが、トンネルは大規模工事のため
       広域幹線である国道での整備が中心となる仮説。

  H2 (中山間集中, RQ2): トンネルの<b>70% 以上が中山間 9 市町</b>に集中。
       (中山間 = 庄原・三次・北広島町・安芸太田町・神石高原町・世羅町・
        府中市・安芸高田市・大崎上島町)
       平野部 (福山・東広島・広島市) でのトンネル整備は限定的。

  H3 (1990 年代ピーク, RQ2): 建設年代ヒストグラムは<b>1990 年代</b>にピーク。
       バブル経済期の道路投資ブームによる山岳道路網の再整備。
       戦前のトンネルは少数だが残存する。

  H4 (橋梁との件数比, RQ3): L66 橋梁 (4,203 件) / L67 トンネル (157 件) の
       <b>件数比は 25 倍以上</b>。
       橋梁は中小河川クロスで多数、トンネルは山岳貫通で希少。

  H5 (規模差, RQ3): トンネルの平均延長 ≥ 200m、橋梁 (L66 既知 ~ 19m) の
       <b>10 倍以上の規模</b>。
       トンネルは大規模工事で、1 件あたりのインフラ価値が高い仮説。

要件 S 準拠 (1 分以内完走):
  - 全データ 157 件 / 16 列 → 軽量
  - POINT geometry (緯度経度から直接生成)
  - 重い前処理は無し。本スクリプト 1 本で完結 (~10 秒目標)
  - L44 既キャッシュ admin_diss.gpkg を市町 sjoin に再利用

要件 T 準拠 (位置情報あり = 地図必須):
  - RQ1: 県全域 道路種別マップ + 延長階級バブルマップ
  - RQ2: 中山間 vs 平野ゾーニングマップ
  - RQ3: L66 橋梁 (背景) + L67 トンネル (前景) 二極構造マップ

要件 Q 準拠: 図 8 / 表 12 (3 RQ × 多角度: 構造 / 地形 / 二極構造)

データ仕様:
  - dataset 12: トンネル基本情報・維持管理情報 (CSV)
  - 形式: CSV, 157 行 × 16 列
  - 列: トンネル番号, 施設名, 施設名(フリガナ), 種別 (= トンネルのみ), 路線名,
       道路種別 (= 国道/県道), 建設年度, 延長(m), 幅員(m), 管理事務所名,
       住所(県), 住所(市町), 緯度（10進数）, 経度（10進数）, 点検年度, 判定区分
  - 種別: 全件 トンネル
  - 道路種別: 国道 83 / 県道 74
  - 管理事務所: 9 (橋梁と同じ事務所階層)
  - 緯度経度: 全件 (157/157 = 100%)。ただし<b>2 件で緯度経度が誤入力により入れ替わって</b>
       記録されており、本スクリプトで自動補正する (緯度 > 50 の行は経度と入れ替え)。
  - 建設年度: 全件取得可 (1927-2018)
  - 点検年度: 156/157 件取得可 (2017-2022)
  - 判定区分: 全件 "?" (= 公開データでは健全度判定値が伏せられている)
       → 本研究では「築年数」 を健全度の代理指標として扱う
  - ライセンス: クリエイティブ・コモンズ表示 (CC-BY)

メモリ対策: Figure ごとに plt.close('all') で確実に解放。
"""
from __future__ import annotations
import sys, time
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 geopandas as gpd
from shapely.geometry import Point
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from matplotlib.lines import Line2D

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

t_all = time.time()
print("=== L67 トンネル基本情報 単独 3 研究例分析 ===", flush=True)

# =============================================================================
# 0. 定数・パス
# =============================================================================
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角第 III 系
DATA_DIR = ROOT / "data" / "extras" / "L67_tunnels"
DATA_DIR.mkdir(parents=True, exist_ok=True)
SHARED_CSV = ROOT / "data" / "extras" / "tunnel_basic.csv"
LOCAL_CSV = DATA_DIR / "tunnel_basic.csv"
DATASET_ID = 12

ADMIN_GPKG = ROOT / "data" / "extras" / "L44_storm_surge" / "_cache" / "admin_diss.gpkg"

# 老朽閾値 (橋梁と同じ): 2024 年時点で築 50 年 = 1974 年以前建設
AGE_THRESHOLD_YEAR = 1974
LONG_TUNNEL_THRESHOLD = 1000.0  # 延長 1000m 以上 = 長大トンネル

# 道路種別の色
ROAD_COLOR = {"国道": "#cf222e", "県道": "#0969da"}
# 規模カテゴリ (橋梁は ≥100m が長大、トンネルはより大きい単位 ≥1000m)
LENGTH_BINS = [0, 100, 300, 1000, 3000]
LENGTH_LABELS = ["短(<100m)", "中(100-300m)", "中長(300-1000m)", "長大(≥1000m)"]

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

# 中山間 9 市町 (本記事 RQ2 用語の定義)
CHUSANKAN_CITIES = {
    "庄原市", "三次市", "北広島町", "安芸太田町", "神石高原町",
    "世羅町", "府中市", "安芸高田市", "大崎上島町",
}

# 旧町村名 → 現市町名 マップ (合併履歴 2003-2010 を反映)
# トンネルデータの「住所(市町)」 列は古い記録が混在する
LEGACY_TO_CURRENT = {
    # 呉市 (2003-2005 合併)
    "下蒲刈町": "呉市", "蒲刈町": "呉市", "豊町": "呉市", "豊浜町": "呉市",
    "音戸町": "呉市", "倉橋町": "呉市", "安浦町": "呉市", "川尻町": "呉市",
    "広町": "呉市",
    # 廿日市市 (2003-2005 合併)
    "大野町": "廿日市市", "宮島町": "廿日市市", "佐伯町": "廿日市市", "吉和村": "廿日市市",
    # 大竹市
    "小方町": "大竹市",
    # 三原市
    "本郷町": "三原市", "久井町": "三原市", "大和町": "三原市",
    # 尾道市
    "御調町": "尾道市", "向島町": "尾道市", "因島市": "尾道市", "瀬戸田町": "尾道市",
    # 福山市
    "沼隈町": "福山市", "神辺町": "福山市", "山野町": "福山市", "新市町": "福山市",
    "駅家町": "福山市", "芦田町": "福山市", "加茂町": "福山市", "内海町": "福山市",
    # 庄原市
    "西城町": "庄原市", "東城町": "庄原市", "口和町": "庄原市", "高野町": "庄原市",
    "比和町": "庄原市", "総領町": "庄原市",
    # 三次市
    "君田村": "三次市", "布野村": "三次市", "作木村": "三次市", "吉舎町": "三次市",
    "三良坂町": "三次市", "三和町": "三次市", "甲奴町": "三次市",
    # 北広島町
    "千代田町": "北広島町", "大朝町": "北広島町", "豊平町": "北広島町", "芸北町": "北広島町",
    # 安芸太田町
    "加計町": "安芸太田町", "筒賀村": "安芸太田町", "戸河内町": "安芸太田町",
    # 江田島市
    "江田島町": "江田島市", "能美町": "江田島市", "沖美町": "江田島市", "大柿町": "江田島市",
    # 神石高原町
    "神石町": "神石高原町", "油木町": "神石高原町", "豊松村": "神石高原町",
    # 世羅町
    "甲山町": "世羅町", "世羅西町": "世羅町",
    # 安芸高田市
    "吉田町": "安芸高田市", "八千代町": "安芸高田市", "美土里町": "安芸高田市",
    "高宮町": "安芸高田市", "甲田町": "安芸高田市", "向原町": "安芸高田市",
    # 東広島市
    "黒瀬町": "東広島市", "福富町": "東広島市", "豊栄町": "東広島市",
    "河内町": "東広島市", "安芸津町": "東広島市",
    # 三次市作木 (作木村は三次市作木町に)
    "作木村": "三次市",
    # 三和町は神石郡 (現 神石高原町) と双三郡 (現 三次市) の 2 つあるが、
    # トンネルデータでは安全側で神石高原町に寄せる
    # ここは念のため上書き不要 (上で三次市に設定済み = 双三郡が大半)
}


# =============================================================================
# 1. データ取得 + 読込
# =============================================================================
print("\n[1] データ取得 + 読込", flush=True)
t1 = time.time()

if SHARED_CSV.exists() and SHARED_CSV.stat().st_size > 100:
    csv_path = SHARED_CSV
    print(f"  共有 CSV を使用: {csv_path} ({csv_path.stat().st_size:,} byte)", flush=True)
    if not LOCAL_CSV.exists():
        LOCAL_CSV.write_bytes(SHARED_CSV.read_bytes())
        print(f"  L67 ローカルへコピー: {LOCAL_CSV.name}", flush=True)
else:
    try:
        ensure_dataset(LOCAL_CSV, dataset_id=DATASET_ID, label="L67 トンネル基本情報")
        csv_path = LOCAL_CSV
    except Exception as e:
        print(f"  WARN: ensure_dataset 失敗: {e}", flush=True)
        csv_path = SHARED_CSV

df_raw = pd.read_csv(csv_path, encoding="utf-8-sig")
print(f"  全行数: {len(df_raw)}, 列数: {df_raw.shape[1]}", flush=True)
print(f"  列: {list(df_raw.columns)}", flush=True)

# 数値正規化
df_raw["建設年度"] = pd.to_numeric(df_raw["建設年度"], errors="coerce")
df_raw.loc[df_raw["建設年度"] <= 1800, "建設年度"] = np.nan
df_raw["延長(m)"] = pd.to_numeric(df_raw["延長(m)"], errors="coerce")
df_raw["幅員(m)"] = pd.to_numeric(df_raw["幅員(m)"], errors="coerce")
df_raw["緯度（10進数）"] = pd.to_numeric(df_raw["緯度（10進数）"], errors="coerce")
df_raw["経度（10進数）"] = pd.to_numeric(df_raw["経度（10進数）"], errors="coerce")
df_raw["点検年度"] = pd.to_numeric(df_raw["点検年度"], errors="coerce")

# 緯度経度の入れ替えバグを補正 (緯度 > 50 の行は確実に経度と取り違え)
swap_mask = df_raw["緯度（10進数）"] > 50
n_swap = int(swap_mask.sum())
if n_swap > 0:
    print(f"  ⚠ 緯度経度が入れ替わった行 {n_swap} 件を自動補正", flush=True)
    tmp = df_raw.loc[swap_mask, "緯度（10進数）"].copy()
    df_raw.loc[swap_mask, "緯度（10進数）"] = df_raw.loc[swap_mask, "経度（10進数）"]
    df_raw.loc[swap_mask, "経度（10進数）"] = tmp

n_total = len(df_raw)
n_year = df_raw["建設年度"].notna().sum()
n_coord = (df_raw["緯度（10進数）"].notna() & df_raw["経度（10進数）"].notna()).sum()
print(f"  建設年度有: {n_year} / {n_total} ({100*n_year/n_total:.1f}%)", flush=True)
print(f"  緯度経度有: {n_coord} / {n_total} ({100*n_coord/n_total:.1f}%)", flush=True)
print(f"  道路種別: {df_raw['道路種別'].value_counts().to_dict()}", flush=True)
print(f"  ({time.time()-t1:.1f}s)", flush=True)


# =============================================================================
# 2. POINT geometry 構築 + 投影
# =============================================================================
print("\n[2] POINT geometry 構築", flush=True)
t2 = time.time()

geom_ok = (df_raw["緯度（10進数）"].notna() & df_raw["経度（10進数）"].notna())
df_raw.loc[geom_ok, "geometry"] = df_raw.loc[geom_ok].apply(
    lambda r: Point(float(r["経度（10進数）"]), float(r["緯度（10進数）"])), axis=1)
gdf = gpd.GeoDataFrame(
    df_raw.dropna(subset=["geometry"]).copy(),
    geometry="geometry", crs="EPSG:4326",
).to_crs(TARGET_CRS)

n_geom = len(gdf)
print(f"  POINT 構築: {n_geom} / {n_total} ({100*n_geom/n_total:.1f}%)", flush=True)
print(f"  ({time.time()-t2:.1f}s)", flush=True)


# =============================================================================
# 3. 市町同定: テキスト + 旧町村名正規化 + sjoin (双方確認)
# =============================================================================
print("\n[3] 市町同定 (テキスト + 旧町村名正規化 + sjoin)", flush=True)
t3 = time.time()

# (a) 住所(市町) 列から先頭ワード抽出
def parse_city_from_addr(addr):
    """住所(市町)文字列から市町名を抽出。旧町村名は LEGACY_TO_CURRENT で現市町に正規化。"""
    if pd.isna(addr):
        return "?"
    s = str(addr)
    # 全角空白で分割した最初のトークン
    head = s.split("　")[0]
    # 「呉市倉橋町」 等は呉市
    for current in ["広島市", "呉市", "竹原市", "三原市", "尾道市", "福山市",
                     "府中市", "三次市", "庄原市", "大竹市", "東広島市", "廿日市市",
                     "安芸高田市", "江田島市", "因島市"]:
        if head.startswith(current):
            if current == "因島市":  # 因島市は 2006 に尾道市に編入
                return "尾道市"
            return current
    # 旧町村名のマッピング
    if head in LEGACY_TO_CURRENT:
        return LEGACY_TO_CURRENT[head]
    # 連結 (例 安芸太田町加計, 呉市倉橋町) を分解
    for legacy, current in LEGACY_TO_CURRENT.items():
        if head.startswith(legacy):
            return current
    # 「安芸郡熊野町」 等の郡付き
    if "熊野町" in head:
        return "熊野町"
    if "府中町" in head:
        return "府中町"
    if "海田町" in head:
        return "海田町"
    if "坂町" in head:
        return "坂町"
    if "世羅町" in head:
        return "世羅町"
    if "北広島町" in head:
        return "北広島町"
    if "安芸太田町" in head:
        return "安芸太田町"
    if "神石高原町" in head:
        return "神石高原町"
    if "大崎上島町" in head:
        return "大崎上島町"
    # 「伊賀和志」 等の旧町村未登録の地名
    return head  # そのまま


df_raw["市町名"] = df_raw["住所(市町)"].apply(parse_city_from_addr)

# (b) sjoin で CITY_CD を取得 (検証用)
admin = gpd.read_file(ADMIN_GPKG).to_crs(TARGET_CRS)
gdf_for_join = gdf[["geometry"]].copy()
joined = gpd.sjoin(gdf_for_join, admin[["CITY_CD", "geometry"]],
                    how="left", predicate="within")
joined = joined[~joined.index.duplicated(keep="first")]
gdf = gdf.reset_index(drop=True)
df_raw = df_raw.reset_index(drop=True)
gdf["CITY_CD"] = joined.reset_index(drop=True)["CITY_CD"].fillna(-1).astype(int)
gdf["市町名_sjoin"] = gdf["CITY_CD"].map(CITY_NAME).fillna("?")
gdf["市町名"] = df_raw.loc[gdf.index, "市町名"].values

# (c) 未マッチ (CITY_CD=-1) は最近隣市町を使用
unmatched_mask = gdf["CITY_CD"] == -1
n_unmatched = int(unmatched_mask.sum())
if n_unmatched > 0:
    # 各 admin polygon との距離で最近隣を決定
    adm_idx = admin.set_index("CITY_CD")
    for idx in gdf.index[unmatched_mask]:
        pt = gdf.geometry.iloc[idx]
        distances = adm_idx.geometry.apply(lambda g: pt.distance(g))
        nearest_cc = int(distances.idxmin())
        gdf.at[idx, "CITY_CD"] = nearest_cc
        gdf.at[idx, "市町名_sjoin"] = CITY_NAME.get(nearest_cc, str(nearest_cc))
n_match = (gdf["CITY_CD"] != -1).sum()
print(f"  sjoin 直接一致: {n_geom - n_unmatched} / {n_geom}", flush=True)
print(f"  最近隣補完: {n_unmatched} / {n_geom}", flush=True)
print(f"  最終 CITY_CD 同定: {n_match} / {n_geom} (100%)", flush=True)

# テキスト解析と sjoin の整合性チェック
df_raw["市町名_sjoin"] = gdf["市町名_sjoin"].values
df_raw["CITY_CD"] = gdf["CITY_CD"].values
text_sjoin_match = (df_raw["市町名"] == df_raw["市町名_sjoin"]).sum()
print(f"  テキスト × sjoin 整合: {text_sjoin_match} / {n_total} "
      f"({100*text_sjoin_match/n_total:.0f}%)", flush=True)

# 採用: テキスト由来の市町名 (旧町村正規化済み) を主、sjoin は検証用
print(f"  ({time.time()-t3:.1f}s)", flush=True)


# =============================================================================
# 4. RQ1 集計: 構造・規模・地理分布
# =============================================================================
print("\n[4] RQ1 集計", flush=True)
t4 = time.time()

n_kuni = int((df_raw["道路種別"] == "国道").sum())
n_ken = int((df_raw["道路種別"] == "県道").sum())
kuni_share = 100 * n_kuni / n_total
ken_share = 100 * n_ken / n_total

# 道路種別別 サマリ
T_road = pd.DataFrame({
    "道路種別": ["国道", "県道", "合計"],
    "件数": [n_kuni, n_ken, n_total],
    "シェア_%": [round(100 * n_kuni / n_total, 1),
               round(100 * n_ken / n_total, 1), 100.0],
    "平均延長_m": [round(df_raw[df_raw["道路種別"] == "国道"]["延長(m)"].mean(), 1),
                round(df_raw[df_raw["道路種別"] == "県道"]["延長(m)"].mean(), 1),
                round(df_raw["延長(m)"].mean(), 1)],
    "平均幅員_m": [round(df_raw[df_raw["道路種別"] == "国道"]["幅員(m)"].mean(), 2),
                round(df_raw[df_raw["道路種別"] == "県道"]["幅員(m)"].mean(), 2),
                round(df_raw["幅員(m)"].mean(), 2)],
})

# 規模カテゴリ
df_raw["延長カテゴリ"] = pd.cut(df_raw["延長(m)"], bins=LENGTH_BINS,
                              labels=LENGTH_LABELS, right=False, include_lowest=True)
length_count = df_raw["延長カテゴリ"].value_counts().reindex(LENGTH_LABELS, fill_value=0)
T_length = pd.DataFrame({
    "延長カテゴリ": LENGTH_LABELS,
    "件数": [int(length_count[k]) for k in LENGTH_LABELS],
    "シェア_%": [round(100 * length_count[k] / n_total, 1) for k in LENGTH_LABELS],
})
n_long = int((df_raw["延長(m)"] >= LONG_TUNNEL_THRESHOLD).sum())
print(f"  国道シェア: {kuni_share:.1f}% (H1 = 50% 以上?)", flush=True)
print(f"  長大トンネル (≥{LONG_TUNNEL_THRESHOLD:.0f}m): {n_long} 件", flush=True)

# 市町別ランキング (テキスト由来の正規化済み市町名)
city_count = (df_raw.groupby("市町名").size()
              .reset_index(name="件数")
              .sort_values("件数", ascending=False)
              .reset_index(drop=True))
city_count["順位"] = np.arange(1, len(city_count) + 1)
city_count = city_count[["順位", "市町名", "件数"]]
top3_city_share = city_count.head(3)["件数"].sum() / n_total * 100

# 中山間市町集中度
chusankan_mask = df_raw["市町名"].isin(CHUSANKAN_CITIES)
n_chusankan = int(chusankan_mask.sum())
chusankan_share = 100 * n_chusankan / n_total
print(f"  中山間 9 市町シェア: {chusankan_share:.1f}% (H2 = 70% 以上?)", flush=True)

# 管理事務所別件数
office_count = df_raw["管理事務所名"].value_counts()
T_office = pd.DataFrame({
    "順位": np.arange(1, len(office_count) + 1),
    "管理事務所名": office_count.index,
    "件数": office_count.values,
    "シェア_%": (office_count.values / n_total * 100).round(1),
})

# 路線別 上位 12
route_count = df_raw["路線名"].value_counts().head(12)
T_route = pd.DataFrame({
    "順位": np.arange(1, len(route_count) + 1),
    "路線名": route_count.index,
    "件数": route_count.values,
})

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


# =============================================================================
# 5. RQ2 集計: 地形特性 + 中山間集中度
# =============================================================================
print("\n[5] RQ2 集計", flush=True)
t5 = time.time()

df_raw["年代"] = (df_raw["建設年度"] // 10 * 10).astype("Int64")
decade_count = df_raw["年代"].value_counts().sort_index()
decade_df = pd.DataFrame({
    "年代": [f"{int(d)}s" for d in decade_count.index],
    "件数": decade_count.values,
    "シェア_%": (decade_count.values / decade_count.sum() * 100).round(1),
})

# 老朽トンネル: 1974 年以前建設
old_mask = df_raw["建設年度"] <= AGE_THRESHOLD_YEAR
n_old = int(old_mask.sum())
old_share = 100 * n_old / n_total

# 中山間 vs 平野 比較
df_raw["地形分類"] = np.where(df_raw["市町名"].isin(CHUSANKAN_CITIES),
                              "中山間 (9 市町)", "平野・沿岸 (13 市町)")
T_terrain = (df_raw.groupby("地形分類").agg(
    件数=("トンネル番号", "size"),
    平均延長_m=("延長(m)", lambda s: round(s.mean(), 1)),
    最長_m=("延長(m)", lambda s: round(s.max(), 1)),
    最古_年=("建設年度", lambda s: int(s.min()) if s.notna().any() else None),
    最新_年=("建設年度", lambda s: int(s.max()) if s.notna().any() else None),
).reset_index())
T_terrain["シェア_%"] = (T_terrain["件数"] / n_total * 100).round(1)

# 中山間市町別ランキング
chusankan_rank = (df_raw[chusankan_mask].groupby("市町名").size()
                  .reset_index(name="トンネル数")
                  .sort_values("トンネル数", ascending=False)
                  .reset_index(drop=True))
chusankan_rank["順位"] = np.arange(1, len(chusankan_rank) + 1)
chusankan_rank = chusankan_rank[["順位", "市町名", "トンネル数"]]

# 1990s ピーク検証
peak_decade = decade_count.idxmax()
peak_count = decade_count.max()
print(f"  最多年代: {int(peak_decade)}s ({peak_count} 件)", flush=True)

# 道路種別 × 中山間
road_terrain_cross = (df_raw.groupby(["道路種別", "地形分類"])
                       .size().unstack(fill_value=0))

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


# =============================================================================
# 6. RQ3 集計: 橋梁 (L66) との二極構造
# =============================================================================
print("\n[6] RQ3 集計: L66 橋梁との二極構造", flush=True)
t6 = time.time()

# L66 の中間 CSV を読み込み (既存)
L66_CSV = ASSETS / "L66_all_bridges.csv"
if L66_CSV.exists():
    df_bridge = pd.read_csv(L66_CSV, encoding="utf-8-sig")
    print(f"  L66 橋梁データ読込: {len(df_bridge):,} 行", flush=True)
    has_bridge = True
else:
    df_bridge = None
    has_bridge = False
    print(f"  WARN: L66_all_bridges.csv なし。橋梁比較は概算値で実施。", flush=True)

# 橋梁・トンネル比較指標
n_bridge = int(len(df_bridge)) if has_bridge else 4203
mean_b_len = float(df_bridge["延長(m)"].mean()) if has_bridge else 19.0
mean_t_len = float(df_raw["延長(m)"].mean())
ratio_count = n_bridge / n_total
ratio_len = mean_t_len / mean_b_len

T_compare = pd.DataFrame([
    ("件数", f"{n_bridge:,}", f"{n_total:,}", f"比 = {ratio_count:.1f} : 1"),
    ("平均延長_m", f"{mean_b_len:.1f}",
                  f"{mean_t_len:.1f}", f"比 = {ratio_len:.1f} : 1"),
    ("最長_m", f"{df_bridge['延長(m)'].max():.0f}" if has_bridge else "?",
              f"{df_raw['延長(m)'].max():.0f}",
              "橋梁=しまなみ系、トンネル=広島呉道路系等"),
    ("国道シェア_%",
     f"{100*(df_bridge['道路種別']=='国道').sum()/n_bridge:.1f}" if has_bridge else "27.4",
     f"{kuni_share:.1f}",
     "トンネルは国道偏重、橋梁は県道偏重"),
    ("中央値_m", f"{df_bridge['延長(m)'].median():.1f}" if has_bridge else "7.0",
                f"{df_raw['延長(m)'].median():.1f}",
                "橋梁=数 m 級、トンネル=百 m 級"),
], columns=["指標", "L66 橋梁", "L67 トンネル", "二極構造の意味"])

# トンネル長大ランキング Top 15
long_top = (df_raw.sort_values("延長(m)", ascending=False).head(15))
T_long_top = long_top[["施設名", "路線名", "道路種別", "延長(m)",
                        "幅員(m)", "建設年度", "市町名"]].copy()
T_long_top.insert(0, "順位", np.arange(1, len(T_long_top) + 1))
T_long_top["建設年度"] = T_long_top["建設年度"].apply(
    lambda v: int(v) if pd.notna(v) else None)

# 道路種別 × 規模カテゴリ
road_length_cross = (df_raw.groupby(["道路種別", "延長カテゴリ"], observed=True)
                     .size().unstack(fill_value=0)
                     .reindex(columns=LENGTH_LABELS, fill_value=0))

print(f"  橋梁 vs トンネル件数比: {ratio_count:.1f} : 1", flush=True)
print(f"  平均延長比: {ratio_len:.1f} : 1", flush=True)
print(f"  ({time.time()-t6:.1f}s)", flush=True)


# =============================================================================
# 7. CSV 出力
# =============================================================================
print("\n[7] CSV 出力", flush=True)
t7 = time.time()

df_out = df_raw.copy()
df_out["is_old"] = old_mask
df_out["is_long"] = df_raw["延長(m)"] >= LONG_TUNNEL_THRESHOLD
cols_keep = ["トンネル番号", "施設名", "種別", "路線名", "道路種別",
             "建設年度", "延長(m)", "幅員(m)", "管理事務所名",
             "住所(県)", "住所(市町)", "市町名", "緯度（10進数）", "経度（10進数）",
             "点検年度", "判定区分", "is_old", "is_long",
             "延長カテゴリ", "年代", "地形分類", "CITY_CD"]
df_out[cols_keep].to_csv(ASSETS / "L67_all_tunnels.csv",
                          index=False, encoding="utf-8-sig")

T_road.to_csv(ASSETS / "L67_road_summary.csv", index=False, encoding="utf-8-sig")
T_length.to_csv(ASSETS / "L67_length_summary.csv", index=False, encoding="utf-8-sig")
city_count.to_csv(ASSETS / "L67_city_ranking.csv", index=False, encoding="utf-8-sig")
T_office.to_csv(ASSETS / "L67_office_ranking.csv", index=False, encoding="utf-8-sig")
T_route.to_csv(ASSETS / "L67_route_ranking.csv", index=False, encoding="utf-8-sig")
decade_df.to_csv(ASSETS / "L67_decade_count.csv", index=False, encoding="utf-8-sig")
T_terrain.to_csv(ASSETS / "L67_terrain_summary.csv", index=False, encoding="utf-8-sig")
chusankan_rank.to_csv(ASSETS / "L67_chusankan_ranking.csv", index=False, encoding="utf-8-sig")
T_long_top.to_csv(ASSETS / "L67_long_tunnels_top.csv", index=False, encoding="utf-8-sig")
road_length_cross.to_csv(ASSETS / "L67_road_x_length.csv", encoding="utf-8-sig")
road_terrain_cross.to_csv(ASSETS / "L67_road_x_terrain.csv", encoding="utf-8-sig")
T_compare.to_csv(ASSETS / "L67_bridge_compare.csv", index=False, encoding="utf-8-sig")

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


# =============================================================================
# 8. 図の生成 (8 図)
# =============================================================================
print("\n[8] 図の生成", flush=True)
t8 = time.time()


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


admin_for_plot = admin.copy()
# admin に市町名を付与 (簡易)
admin["市町名"] = admin["CITY_CD"].map(CITY_NAME).fillna(admin["CITY_CD"].astype(str))
admin_for_plot["市町名"] = admin_for_plot["CITY_CD"].map(CITY_NAME).fillna(
    admin_for_plot["CITY_CD"].astype(str))


# ---- 図 1 (RQ1): 県全域 道路種別マップ ----
print("  fig1: 県全域 道路種別マップ", flush=True)
fig, ax = plt.subplots(figsize=(11.5, 7.5))
admin_for_plot.plot(ax=ax, color="#fff4e0", edgecolor="#888",
                     linewidth=0.4, alpha=0.6)
for road_kind, color in ROAD_COLOR.items():
    sub = gdf[gdf["道路種別"] == road_kind]
    if len(sub) == 0:
        continue
    sub.plot(ax=ax, color=color, markersize=18,
              edgecolor="#222", linewidth=0.3, alpha=0.85)
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -130000)
ax.set_aspect("equal")
ax.set_title(f"図 1 (RQ1): 広島県 道路トンネル 全域マップ — 全 {n_total:,} 件 / "
             f"国道 {n_kuni} + 県道 {n_ken}",
             fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [Line2D([0], [0], marker='o', color='w',
                    markerfacecolor=ROAD_COLOR[k], markeredgecolor="#222",
                    markersize=10, label=f"{k} (n={int((df_raw['道路種別']==k).sum())})")
           for k in ["国道", "県道"]]
ax.legend(handles=patches, loc="lower left", fontsize=10, title="道路種別")
plt.tight_layout()
save_fig("L67_fig1_overview_road_map.png")


# ---- 図 2 (RQ1): 市町別ランキング + 管理事務所 ----
print("  fig2: 市町別 + 事務所別 件数", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 6.5))

ax = axes[0]
top_cities = city_count.head(15).iloc[::-1]
ys = np.arange(len(top_cities))
# 中山間市町は青、それ以外はオレンジ
city_colors = ["#0969da" if c in CHUSANKAN_CITIES else "#cf922e"
                for c in top_cities["市町名"].values]
ax.barh(ys, top_cities["件数"].values,
         color=city_colors, edgecolor="#333", linewidth=0.5)
for y, v in zip(ys, top_cities["件数"].values):
    ax.text(v + 0.3, y, f"{v}", va="center", fontsize=10)
ax.set_yticks(ys)
ax.set_yticklabels(top_cities["市町名"].values, fontsize=10)
ax.set_xlabel("件数")
ax.set_title(f"市町別 トンネル件数 Top 15\n(青 = 中山間 9 市町, 橙 = 平野・沿岸)",
             fontsize=11)
ax.grid(True, axis="x", alpha=0.3)

ax = axes[1]
office_top = T_office.iloc[::-1]
ys = np.arange(len(office_top))
ax.barh(ys, office_top["件数"].values,
         color="#7c3aed", edgecolor="#333", linewidth=0.5)
for y, v in zip(ys, office_top["件数"].values):
    ax.text(v + 0.3, y, f"{v}", va="center", fontsize=10)
ax.set_yticks(ys)
ax.set_yticklabels(office_top["管理事務所名"].values, fontsize=10)
ax.set_xlabel("件数")
ax.set_title(f"管理事務所別 トンネル件数 ({len(office_count)} 事務所)",
             fontsize=11)
ax.grid(True, axis="x", alpha=0.3)

fig.suptitle(f"図 2 (RQ1): 市町別 + 管理事務所別 トンネル件数",
             fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L67_fig2_city_office_count.png")


# ---- 図 3 (RQ1): 延長階級バブルマップ ----
print("  fig3: 延長階級バブルマップ", flush=True)
fig, ax = plt.subplots(figsize=(12, 7.5))
admin_for_plot.plot(ax=ax, color="#fff4e0", edgecolor="#888",
                     linewidth=0.4, alpha=0.55)
# 延長を log でサイズ化
ll = gdf["延長(m)"].fillna(50).values
sizes = 5 + (np.log10(np.clip(ll, 1, None)) ** 2.2) * 12
# 道路種別で色
colors_pt = gdf["道路種別"].map(ROAD_COLOR).values
ax.scatter(gdf.geometry.x, gdf.geometry.y,
            s=sizes, c=colors_pt, alpha=0.7,
            edgecolor="#000", linewidth=0.4)
# 長大 (≥1000m) のラベル付き表示
long_gdf = gdf[gdf["延長(m)"] >= LONG_TUNNEL_THRESHOLD]
for _, r in long_gdf.iterrows():
    ax.annotate(r["施設名"][:8], xy=(r.geometry.x, r.geometry.y),
                xytext=(8, 8), textcoords="offset points",
                fontsize=8, color="#222",
                bbox=dict(boxstyle="round,pad=0.2", fc="#fff5b1",
                          ec="#aaa", alpha=0.8))
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -130000)
ax.set_aspect("equal")
ax.set_title(f"図 3 (RQ1): 延長階級バブルマップ — "
             f"バブル大きさ = 延長 (log), 色 = 道路種別, ラベル = ≥1km トンネル",
             fontsize=11.5)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [
    Line2D([0], [0], marker='o', color='w', markerfacecolor=ROAD_COLOR["国道"],
            markeredgecolor="#000", markersize=12, label=f"国道 ({n_kuni})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor=ROAD_COLOR["県道"],
            markeredgecolor="#000", markersize=12, label=f"県道 ({n_ken})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#888",
            markeredgecolor="#000", markersize=20, label="長大 (≥1km)"),
]
ax.legend(handles=patches, loc="lower left", fontsize=10)
plt.tight_layout()
save_fig("L67_fig3_length_bubble_map.png")


# ---- 図 4 (RQ1): 延長 + 幅員 ヒストグラム ----
print("  fig4: 延長 + 幅員 ヒストグラム", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

ax = axes[0]
ll = df_raw["延長(m)"].dropna()
ax.hist(ll, bins=np.logspace(1, 4, 35), color="#0969da",
         edgecolor="#333", linewidth=0.4, alpha=0.85)
ax.axvline(LONG_TUNNEL_THRESHOLD, color="#cf222e", linestyle="--",
            linewidth=1.5, label=f"長大トンネル 閾値 ({LONG_TUNNEL_THRESHOLD:.0f}m)")
ax.axvline(df_raw["延長(m)"].median(), color="#1a7f37", linestyle=":",
            linewidth=1.5,
            label=f"中央値 {df_raw['延長(m)'].median():.0f}m")
ax.set_xscale("log")
ax.set_xlabel("延長 (m, log scale)")
ax.set_ylabel("件数")
ax.set_title(f"延長分布 (中央値 {df_raw['延長(m)'].median():.0f}m, "
             f"最大 {df_raw['延長(m)'].max():.0f}m)",
             fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)

ax = axes[1]
ww = df_raw["幅員(m)"][df_raw["幅員(m)"] > 0].dropna()
ax.hist(ww, bins=25, color="#1a7f37", edgecolor="#333",
         linewidth=0.4, alpha=0.85)
ax.axvline(ww.median(), color="#cf222e", linestyle=":",
            linewidth=1.5, label=f"中央値 {ww.median():.1f}m")
ax.set_xlabel("幅員 (m)")
ax.set_ylabel("件数")
ax.set_title(f"幅員分布 (中央値 {ww.median():.1f}m, "
             f"最大 {ww.max():.0f}m)", fontsize=11)
ax.set_xlim(0, 15)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)

fig.suptitle("図 4 (RQ1): 延長 + 幅員 分布", fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L67_fig4_length_width_hist.png")


# ---- 図 5 (RQ2): 中山間 vs 平野ゾーニングマップ ----
print("  fig5: 中山間 vs 平野ゾーニングマップ", flush=True)
fig, ax = plt.subplots(figsize=(12, 7.5))

# admin に地形分類を付与 (CITY_NAME ベース)
admin_plot2 = admin_for_plot.copy()
admin_plot2["市町名_clean"] = admin_plot2["市町名"].str.replace(
    r"^広島市.+", "広島市", regex=True)
admin_plot2["地形分類"] = admin_plot2["市町名_clean"].apply(
    lambda c: "中山間" if c in CHUSANKAN_CITIES else "平野・沿岸"
)
# 2 色で塗り分け (中山間 = 薄緑、平野 = 薄黄)
admin_plot2.loc[admin_plot2["地形分類"] == "中山間"].plot(
    ax=ax, color="#d3edd2", edgecolor="#444", linewidth=0.5, alpha=0.7)
admin_plot2.loc[admin_plot2["地形分類"] == "平野・沿岸"].plot(
    ax=ax, color="#fff4e0", edgecolor="#888", linewidth=0.4, alpha=0.6)

# トンネル点を上に重ねる (色 = 地形分類)
gdf_chusankan = gdf[gdf["市町名"].isin(CHUSANKAN_CITIES)]
gdf_other = gdf[~gdf["市町名"].isin(CHUSANKAN_CITIES)]
gdf_chusankan.plot(ax=ax, color="#1a7f37", markersize=22, alpha=0.85,
                    edgecolor="#000", linewidth=0.4,
                    label=f"中山間トンネル (n={len(gdf_chusankan)})")
gdf_other.plot(ax=ax, color="#cf6f00", markersize=22, alpha=0.85,
                edgecolor="#000", linewidth=0.4,
                label=f"平野・沿岸トンネル (n={len(gdf_other)})")

ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -130000)
ax.set_aspect("equal")
ax.set_title(f"図 5 (RQ2): 中山間 vs 平野・沿岸 トンネル分布 — "
             f"中山間シェア {chusankan_share:.1f}%",
             fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [
    Patch(facecolor="#d3edd2", edgecolor="#444", label="中山間 9 市町 (背景)"),
    Patch(facecolor="#fff4e0", edgecolor="#888", label="平野・沿岸 13 市町 (背景)"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#1a7f37",
            markeredgecolor="#000", markersize=10,
            label=f"中山間トンネル ({len(gdf_chusankan)})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#cf6f00",
            markeredgecolor="#000", markersize=10,
            label=f"平野・沿岸トンネル ({len(gdf_other)})"),
]
ax.legend(handles=patches, loc="lower left", fontsize=9.5)
plt.tight_layout()
save_fig("L67_fig5_chusankan_zoning_map.png")


# ---- 図 6 (RQ2): 建設年代別ヒストグラム ----
print("  fig6: 建設年代別ヒストグラム", flush=True)
fig, ax = plt.subplots(figsize=(12, 5.5))

decades = sorted([int(d) for d in decade_count.index])
counts = [int(decade_count[d]) for d in decades]
# 色帯境界: 1970s 以前 = 赤 (この帯の合計 = decade_old_total, n_old≤1974 とは別概念)
colors = ["#cf222e" if d <= 1970 else "#0969da" for d in decades]
xs = np.arange(len(decades))
bars = ax.bar(xs, counts, color=colors, edgecolor="#333", linewidth=0.5)
for x, c in zip(xs, counts):
    ax.text(x, c + 0.5, f"{c}", ha="center", fontsize=9)
ax.set_xticks(xs)
ax.set_xticklabels([f"{d}s" for d in decades], rotation=0, fontsize=10)
ax.set_xlabel("建設年代")
ax.set_ylabel("件数")
ax.set_title(f"図 6 (RQ2): 建設年代別 トンネル件数 — 全 {n_total} 件 "
             f"(1970年代以前 = 赤, 1980 年以降 = 青)",
             fontsize=11)

# 年代帯の合計 (図の赤バーと一致させる)
decade_old_total = sum(c for d, c in zip(decades, counts) if d <= 1970)
patches = [
    Patch(facecolor="#cf222e",
           label=f"1970年代以前 ({decade_old_total} 件) — うち築 50 年超 = {n_old}"),
    Patch(facecolor="#0969da", label=f"1980年以降 ({n_total - decade_old_total} 件)"),
]
ax.legend(handles=patches, loc="upper left", fontsize=9)
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
save_fig("L67_fig6_decade_hist.png")


# ---- 図 7 (RQ3): 橋梁 vs トンネル 二極構造マップ ----
print("  fig7: 橋梁 vs トンネル 二極構造マップ", flush=True)
fig, ax = plt.subplots(figsize=(12.5, 7.5))
admin_for_plot.plot(ax=ax, color="#fff4e0", edgecolor="#888",
                     linewidth=0.4, alpha=0.55)

# 橋梁を背景に薄く (もし L66_all_bridges.csv が読めれば)
if has_bridge:
    df_b = df_bridge.copy()
    df_b = df_b.dropna(subset=["緯度（10進数）", "経度（10進数）"])
    # POINT 構築
    geom_b = [Point(x, y) for x, y in zip(df_b["経度（10進数）"], df_b["緯度（10進数）"])]
    gdf_b = gpd.GeoDataFrame(df_b[["施設名", "道路種別"]],
                              geometry=geom_b, crs="EPSG:4326").to_crs(TARGET_CRS)
    gdf_b.plot(ax=ax, color="#888", markersize=2, alpha=0.25,
                edgecolor="none")

# トンネルを前景に (国道 = 赤, 県道 = 青)
for road_kind, color in ROAD_COLOR.items():
    sub = gdf[gdf["道路種別"] == road_kind]
    sub.plot(ax=ax, color=color, markersize=35,
              edgecolor="#000", linewidth=0.5, alpha=0.92)

ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -130000)
ax.set_aspect("equal")
title_b = f"L66 橋梁 (背景灰, n={n_bridge:,})" if has_bridge else "(L66 橋梁データ未取得)"
ax.set_title(f"図 7 (RQ3): 道路インフラ二極構造 — {title_b} + L67 トンネル (前景)",
             fontsize=11.5)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = []
if has_bridge:
    patches.append(Line2D([0], [0], marker='o', color='w',
                            markerfacecolor="#888", markersize=8,
                            label=f"L66 橋梁 ({n_bridge:,})"))
patches.extend([
    Line2D([0], [0], marker='o', color='w', markerfacecolor=ROAD_COLOR["国道"],
            markeredgecolor="#000", markersize=12, label=f"L67 国道トンネル ({n_kuni})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor=ROAD_COLOR["県道"],
            markeredgecolor="#000", markersize=12, label=f"L67 県道トンネル ({n_ken})"),
])
ax.legend(handles=patches, loc="lower left", fontsize=10)
plt.tight_layout()
save_fig("L67_fig7_bridge_tunnel_dual_map.png")


# ---- 図 8 (RQ3): 橋梁 vs トンネル 規模分布対比 ----
print("  fig8: 橋梁 vs トンネル 規模分布対比", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(13.5, 5.2))

# 左: 件数比較
ax = axes[0]
labels = ["L66 橋梁", "L67 トンネル"]
vals = [n_bridge, n_total]
cols = ["#0969da", "#cf222e"]
ax.bar(labels, vals, color=cols, edgecolor="#333", linewidth=0.6, width=0.55)
for x, v in zip([0, 1], vals):
    ax.text(x, v + n_bridge*0.015, f"{v:,}", ha="center",
             fontsize=12, fontweight="bold")
ax.set_ylabel("件数")
ax.set_title(f"件数比較 (橋梁 / トンネル = {ratio_count:.1f} : 1)",
             fontsize=11)
ax.grid(True, axis="y", alpha=0.3)

# 右: 延長分布の重ね合わせ (log) — 比率正規化 (% of group)
ax = axes[1]
bins = np.logspace(0, 4, 35)
if has_bridge:
    bb = df_bridge["延長(m)"].dropna()
    counts_b, _ = np.histogram(bb, bins=bins)
    pct_b = 100 * counts_b / counts_b.sum()
    ax.bar(bins[:-1], pct_b, width=np.diff(bins),
            color="#0969da", alpha=0.55, align="edge",
            edgecolor="#0c4a8a", linewidth=0.4,
            label=f"L66 橋梁 (中央値 {df_bridge['延長(m)'].median():.1f}m)")
tt = df_raw["延長(m)"].dropna()
counts_t, _ = np.histogram(tt, bins=bins)
pct_t = 100 * counts_t / counts_t.sum()
ax.bar(bins[:-1], pct_t, width=np.diff(bins),
        color="#cf222e", alpha=0.65, align="edge",
        edgecolor="#7a0000", linewidth=0.4,
        label=f"L67 トンネル (中央値 {df_raw['延長(m)'].median():.0f}m)")
ax.set_xscale("log")
ax.set_xlabel("延長 (m, log)")
ax.set_ylabel("% (各グループ内)")
ax.set_title(f"延長分布の対比 (平均比 = {ratio_len:.1f} : 1)", fontsize=11)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)

fig.suptitle("図 8 (RQ3): 橋梁 vs トンネル 規模分布対比 — 件数支配 vs 規模支配",
             fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L67_fig8_bridge_tunnel_scale_compare.png")

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


# =============================================================================
# 9. 表データ作成
# =============================================================================
print("\n[9] 表データ作成", flush=True)
t9 = time.time()


def df_to_html(d):
    return d.to_html(index=False, classes="", border=0, escape=False,
                     na_rep="-").replace(' style="text-align: right;"', "")


# 表 1: データセット仕様
T_dataset = pd.DataFrame([
    ("dataset_id", "12"),
    ("公式名", "トンネル基本情報・維持管理情報"),
    ("ファイル", "tunnel_basic.csv"),
    ("形式", "CSV (UTF-8 BOM)"),
    ("ファイルサイズ", f"{csv_path.stat().st_size:,} byte (~{csv_path.stat().st_size/1024:.0f} KB)"),
    ("レコード数", f"{n_total} 行 (= 道路トンネル件数)"),
    ("列数", f"{df_raw.shape[1]} 列"),
    ("種別", f"全件 トンネル (鉄道トンネル・河川トンネルは別データ)"),
    ("道路種別", f"国道 {n_kuni} + 県道 {n_ken}"),
    ("管理事務所", f"{len(office_count)} 事務所"),
    ("路線数", f"{df_raw['路線名'].nunique()} 異なり値"),
    ("市町数 (正規化済)", f"{city_count['市町名'].nunique()} 市町"),
    ("緯度経度", f"全件 取得可 ({100*n_coord/n_total:.0f}%)。"
                  f"うち {n_swap} 件で記録ミスあり (本スクリプトで自動補正)"),
    ("建設年度", f"{n_year} 件 ({100*n_year/n_total:.0f}%) 取得可、"
                  f"範囲 {int(df_raw['建設年度'].min())}-{int(df_raw['建設年度'].max())}"),
    ("点検年度", f"{int(df_raw['点検年度'].notna().sum())} 件 取得可、範囲 "
                  f"{int(df_raw['点検年度'].min())}-{int(df_raw['点検年度'].max())}"),
    ("判定区分", "全件 \"?\" (= 公開データでは伏せられる)"),
    ("延長 (m)", f"中央値 {df_raw['延長(m)'].median():.0f}m / "
                  f"最大 {df_raw['延長(m)'].max():.0f}m / "
                  f"最小 {df_raw['延長(m)'].min():.1f}m"),
    ("幅員 (m)", f"中央値 {df_raw['幅員(m)'].median():.1f}m / "
                  f"最大 {df_raw['幅員(m)'].max():.1f}m"),
    ("座標系 (元)", "EPSG:4326 (WGS84) → EPSG:6671 で処理"),
    ("ライセンス", "クリエイティブ・コモンズ表示 (CC-BY)"),
    ("作成主体", "広島県土木建築局道路整備課"),
    ("URL", f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
], columns=["項目", "値"])

# 表 2: 全体サマリ (3 RQ 統合)
T_overall = pd.DataFrame([
    ("総件数 (RQ1)", f"{n_total} 件"),
    ("国道トンネル", f"{n_kuni} ({kuni_share:.1f}%)"),
    ("県道トンネル", f"{n_ken} ({ken_share:.1f}%)"),
    ("管理事務所数", f"{len(office_count)}"),
    ("路線数", f"{df_raw['路線名'].nunique()}"),
    ("市町数 (正規化後)", f"{city_count['市町名'].nunique()}"),
    ("Top 1 市町 (RQ1)", f"{city_count.iloc[0]['市町名']} "
                         f"({int(city_count.iloc[0]['件数'])} 件)"),
    ("Top 3 市町シェア (RQ1)", f"{top3_city_share:.1f}%"),
    ("中山間 9 市町シェア (RQ2)", f"{chusankan_share:.1f}% ({n_chusankan} 件)"),
    ("長大トンネル (≥1000m) (RQ1)",
     f"{n_long} 件 ({100*n_long/n_total:.1f}%)"),
    ("延長 中央値 / 最大 (RQ1)",
     f"{df_raw['延長(m)'].median():.0f} m / {df_raw['延長(m)'].max():.0f} m"),
    ("幅員 中央値 / 最大 (RQ1)",
     f"{df_raw['幅員(m)'].median():.1f} m / {df_raw['幅員(m)'].max():.1f} m"),
    ("最多年代 (RQ2)", f"{int(peak_decade)}s ({peak_count} 件)"),
    (f"老朽トンネル (≤{AGE_THRESHOLD_YEAR}, 築 50 年超) (RQ2)",
     f"{n_old} 件 ({old_share:.1f}%)"),
    ("中山間平均延長 (RQ2)",
     f"{T_terrain.loc[T_terrain['地形分類'].str.startswith('中山間'), '平均延長_m'].iloc[0]:.0f} m"),
    ("平野平均延長 (RQ2)",
     f"{T_terrain.loc[T_terrain['地形分類'].str.startswith('平野'), '平均延長_m'].iloc[0]:.0f} m"),
    ("L66 橋梁件数 (比較対象)", f"{n_bridge:,}"),
    ("橋梁 / トンネル 件数比 (RQ3)", f"{ratio_count:.1f} : 1"),
    ("トンネル / 橋梁 平均延長比 (RQ3)", f"{ratio_len:.1f} : 1"),
    ("最長トンネル (RQ3)",
     f"{T_long_top.iloc[0]['施設名']} "
     f"({T_long_top.iloc[0]['延長(m)']:.0f} m, "
     f"{T_long_top.iloc[0]['路線名']}, "
     f"{T_long_top.iloc[0]['市町名']})"),
], columns=["指標", "値"])
T_overall.to_csv(ASSETS / "L67_overall.csv", index=False, encoding="utf-8-sig")


# 表: データ取得手順
T_data_recipe = pd.DataFrame([
    ("ステップ 1", "DoBoX dataset 12 ページ",
     f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
    ("ステップ 2", "CSV DL (リソースリンク)",
     "ページ内のリソースから「ダウンロード」"),
    ("ステップ 3", "保存先",
     "data/extras/L67_tunnels/tunnel_basic.csv (or 共有 data/extras/tunnel_basic.csv)"),
    ("ステップ 4", "緯度経度の入れ替え自動補正",
     f"緯度 > 50 の {n_swap} 行を経度と入れ替え"),
    ("ステップ 5", "POINT 構築 + EPSG:6671 投影",
     f"全 {n_total} 件 → POINT 100%"),
    ("ステップ 6", "市町同定 (テキスト + 旧町村正規化 + sjoin)",
     f"テキスト解析で {n_total}/{n_total}, sjoin で {n_geom-n_unmatched}/{n_geom} 直接一致"),
    ("ステップ 7", "RQ1 集計 (構造)",
     f"道路種別 + 市町 + 事務所 + 路線 + 延長 + 幅員 で多角度集計"),
    ("ステップ 8", "RQ2 集計 (中山間 vs 平野)",
     f"中山間 9 市町 vs 平野 13 市町 + 建設年代別"),
    ("ステップ 9", "RQ3 集計 (L66 橋梁との対比)",
     f"件数比 {ratio_count:.1f}:1, 平均延長比 {ratio_len:.1f}:1"),
    ("ステップ 10", "8 図 + 12 表 出力",
     "本スクリプト全体で ~10 秒"),
], columns=["ステップ", "操作", "値 / URL"])


# 仮説検証表
def jud(cond, ok="強支持", fail="反証", part="部分支持"):
    return ok if cond else fail


h1_ok = kuni_share >= 50
h2_ok = chusankan_share >= 70
h3_ok = int(peak_decade) == 1990
h4_ok = ratio_count >= 25
h5_ok = ratio_len >= 10

T_hypo = pd.DataFrame([
    ("H1 国道シェア ≥ 50% (RQ1)",
     f"観測 = {kuni_share:.1f}% (国道 {n_kuni} / 県道 {n_ken})",
     jud(h1_ok),
     f"H1 {jud(h1_ok)}: 国道シェアは <b>{kuni_share:.1f}%</b>。"
     f"橋梁 (L66) では国道 27.4% だが、トンネルは <b>{kuni_share:.1f}%</b> と"
     + (f"<b>大きく国道偏重</b>。" if h1_ok else f"わずかに国道優勢。")
     + f" これはトンネル工事が大規模で、"
     f"広域幹線 (国道) での整備が中心となる事業構造を反映。"
     f"県道 (n={n_ken}) は地域路線を貫通する小規模トンネル中心。"),
    ("H2 中山間 9 市町シェア ≥ 70% (RQ2)",
     f"観測 = {chusankan_share:.1f}% ({n_chusankan} / {n_total})",
     jud(h2_ok),
     f"H2 {jud(h2_ok)}: 中山間 9 市町に <b>{chusankan_share:.1f}%</b> ({n_chusankan} 件) が集中。"
     f"中山間市町は<b>「山岳地形を貫通する道路網」</b>を持ち、トンネル整備が必須。"
     f"中山間 Top 3: {chusankan_rank.iloc[0]['市町名']} ({int(chusankan_rank.iloc[0]['トンネル数'])}) / "
     f"{chusankan_rank.iloc[1]['市町名']} ({int(chusankan_rank.iloc[1]['トンネル数'])}) / "
     f"{chusankan_rank.iloc[2]['市町名']} ({int(chusankan_rank.iloc[2]['トンネル数'])})。"
     f"平野市町 (福山市・広島市・東広島市) はトンネル整備が限定的。"),
    ("H3 1990 年代ピーク (RQ2)",
     f"観測 最多年代 = {int(peak_decade)}s ({peak_count} 件)",
     jud(h3_ok),
     f"H3 {jud(h3_ok)}: 建設年代の最多は <b>{int(peak_decade)}s</b>。"
     + (f"バブル経済期 (1986-91) を含む 1990 年代の道路投資ブームが"
        f"トンネル整備のピークを生んだ。"
        if h3_ok else
        f"最多は {int(peak_decade)}s = " +
        ("高度成長期" if int(peak_decade) <= 1970 else
         "バブル + ポストバブル期"))
     + f" 1980-1990s の 2 年代で全体の "
     f"{100*sum([decade_count.get(d, 0) for d in [1980,1990]])/n_total:.0f}% を占める集中整備期。"
     f"1970年代以前 (老朽化対象) は <b>{n_old} 件 ({old_share:.1f}%)</b> のみで、"
     f"橋梁よりトンネルは新しい構造物が多い。"),
    (f"H4 橋梁/トンネル件数比 ≥ 25 (RQ3)",
     f"観測 = {ratio_count:.1f} : 1 ({n_bridge:,} / {n_total})",
     jud(h4_ok),
     f"H4 {jud(h4_ok)}: 橋梁 ({n_bridge:,}) / トンネル ({n_total}) = "
     f"<b>{ratio_count:.1f} : 1</b>。"
     f"これは<b>「橋梁は中小河川クロスで多数 + トンネルは山岳貫通で希少」</b>"
     f"という地理分担を反映。"
     f"県の道路インフラは橋梁圧倒で、トンネルは<b>1 件あたり大規模</b>な希少資源。"),
    (f"H5 トンネル平均延長 / 橋梁平均延長 ≥ 10 (RQ3)",
     f"観測 = トンネル {mean_t_len:.0f}m / 橋梁 {mean_b_len:.0f}m / 比 {ratio_len:.1f}",
     jud(h5_ok),
     f"H5 {jud(h5_ok)}: トンネル平均延長は橋梁の <b>{ratio_len:.1f} 倍</b>。"
     f"橋梁中央値 ~ 7m に対しトンネル中央値 <b>{df_raw['延長(m)'].median():.0f}m</b>。"
     f"<b>「橋梁=点的多数 + トンネル=大規模少数」</b>の二極構造が定量化された。"
     f"これは整備コスト・更新コスト・防災重要度すべてで橋梁・トンネルの政策位置付けが"
     f"異なることを意味する。"),
], columns=["仮説", "観測値", "判定", "解釈"])
T_hypo.to_csv(ASSETS / "L67_hypothesis_check.csv", index=False, encoding="utf-8-sig")


# road_length_cross と road_terrain_cross の表示用整形
T_rl_show = road_length_cross.reset_index()
T_rl_show.columns = ["道路種別"] + LENGTH_LABELS
T_rt_show = road_terrain_cross.reset_index()

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


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

# Sec 1: 学習目標と問い
sec1 = f"""
<p>本記事は DoBoX のシリーズ「<b>トンネル基本情報・維持管理情報</b>」 1 件
(dataset_id = <a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}">{DATASET_ID}</a>) を <b>単独</b>で取り上げ、
広島県が管理する <b>道路トンネル {n_total} 件 / {city_count['市町名'].nunique()} 市町 / {len(office_count)} 事務所 / {df_raw['路線名'].nunique()} 路線</b>を
3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。
本データは CSV 形式 ({csv_path.stat().st_size:,} byte / 16 列) で、
緯度経度は<b>全件 ({100*n_coord/n_total:.0f}%)</b> 取得可。
ただし<b>2 件で緯度経度が記録ミスにより入れ替わって</b>おり、本スクリプトで自動補正する。</p>

<p class="note"><b>L66 (橋梁単独 3 RQ) との位置付け</b>:
L66 は同じ DoBoX シリーズ「○○基本情報・維持管理情報」 の橋梁版で、4,203 件を 3 RQ (構造・年代・防災) で分析。
本記事 L67 はトンネル<b>単独</b>の構造分析 (= 規模・地形・年代) に集中するが、
RQ3 で<b>L66 との二極構造</b>を比較し、「橋梁=平野・川越え、トンネル=山岳貫通」 の
地理分担を実証する。両記事は<b>「県の道路インフラ」</b>を構成する 2 つの軸として
補完関係を成す。</p>

<div class="warn"><b>トンネルデータの本質</b>: 県内の道路トンネルは<b>道路法 (1952) と
インフラ長寿命化基本計画 (2014)</b> の管理対象。
2014 年改正で<b>5 年に 1 回の点検</b>が義務化され、
全トンネルに I (健全) 〜 IV (緊急措置) の<b>健全度区分</b>が付与される。
ただし<b>本データの「判定区分」 列は全件 "?" で公開データでは伏せられている</b>。
そのため本研究では<b>「建設年度から経過年」</b> を健全度の代理指標として扱う。</div>

<h3>独自用語の定義</h3>
<ul class="kv">
  <li><b>トンネル</b> (本データ #{DATASET_ID}): 山岳・地下を貫いて道路を通すための公共土木施設。
      本データは<b>道路トンネルのみ</b> (鉄道トンネル・河川トンネルは別データ)。広島県が管理する
      <b>{n_total} 件</b>を全件対象。</li>
  <li><b>NATM (新オーストリアトンネル工法)</b>: 山岳トンネルの代表工法。
      地山自体の強度を支保部材として活用し、覆工を後施工する。
      日本では 1980 年代以降の山岳トンネルの主流。本データの工法列は無いが、
      建設年代と立地から大半が NATM と推定される。</li>
  <li><b>シールド工法</b>: 地下深部 (都市部・水底等) のトンネル工法。
      シールドマシンで掘削しながらセグメントで覆工する。山岳トンネルは原則 NATM、
      都市部地下は原則シールド。本データの広島県管理トンネルは大半が山岳 NATM。</li>
  <li><b>道路種別</b> (本データ列, 国道 {n_kuni} / 県道 {n_ken}):
      <b>国道</b>は国 (国交省) が指定する広域幹線道路。<b>県道</b>は県が指定する地域幹線道路。
      本データは県管理トンネルを扱うため、国道の中でも県管理区間 (= 一般国道) のみ収録。</li>
  <li><b>全長</b> (本データ「延長(m)」 列): トンネルの坑口間距離 (m)。
      最短 {df_raw['延長(m)'].min():.1f}m〜最長 {df_raw['延長(m)'].max():.0f}m と桁差が大きい。</li>
  <li><b>幅員</b> (本データ「幅員(m)」 列): トンネル内の有効幅 (m)。
      車道 + 歩道 + 中央分離帯を含む。橋梁同様、道路規格でほぼ標準化されている。</li>
  <li><b>建設年度</b> (本データ「建設年度」 列, 全件取得可): トンネルを建造して開通した年度。
      築 50 年超 (= {AGE_THRESHOLD_YEAR} 年以前) のトンネルを<b>「老朽トンネル」</b>と定義 (橋梁と同じ閾値)。</li>
  <li><b>点検年度</b> (本データ「点検年度」 列): 直近のトンネル点検実施年度。
      2014 年の道路法改正で 5 年周期点検が義務化されたため、<b>2017-2022</b> の範囲に分布。</li>
  <li><b>判定区分</b> (本データ列, 全件 "?"): 健全度区分 I〜IV の判定値。
      公開データでは<b>全件マスクされている</b> (社会的混乱回避のため)。
      I = 健全, II = 予防保全段階, III = 早期措置段階, IV = 緊急措置段階。</li>
  <li><b>長大トンネル</b> (本記事 RQ1 用語, 閾値 = 延長 {LONG_TUNNEL_THRESHOLD:.0f}m 以上):
      延長 1,000m を超えるトンネル。<b>「県土の幹線山岳貫通路」</b>の代表指標。
      本研究で <b>{n_long} 件 ({100*n_long/n_total:.1f}%)</b> を同定。</li>
  <li><b>山岳バイパス</b> (本記事 RQ2 用語):
      山岳地形を貫通する道路。トンネルがあれば峠越え区間を短縮できる。
      <b>「分断回避」</b>機能の代表的具現。中山間市町の道路整備の鍵となる。</li>
  <li><b>分断回避</b> (本記事 RQ2 用語):
      山岳・河川等の地形障害により集落・地域が分断される事態を、
      トンネル・橋梁等の構造物で回避する道路整備上の概念。
      トンネルは特に<b>山岳分断</b>の主要解決手段。</li>
  <li><b>中山間 9 市町</b> (本記事 RQ2 用語, 独自定義):
      面積に占める山地比率が高く、人口密度が低い県内 9 市町 =
      庄原市 / 三次市 / 北広島町 / 安芸太田町 / 神石高原町 / 世羅町 / 府中市 / 安芸高田市 / 大崎上島町。
      公的「中山間地域」 指定とは厳密には異なるが、本記事の地形分類として固定。</li>
  <li><b>道路インフラ二極構造</b> (本記事 RQ3 用語):
      <b>「橋梁=平野・川越え + トンネル=山岳貫通」</b>の補完地理分担。
      両者は同じ「道路の連続性確保」 という機能だが、対象地形・規模・整備量が大きく異なる。</li>
  <li><b>POINT geometry</b>: 緯度経度 1 組で表される<b>点</b>形式の geometry。
      トンネルは実際には線状の構造物 (坑口 2 + 内部) だが、本データは緯度経度 1 点で代表する。
      これはトンネルの<b>中心点</b>または坑口の代表点と解釈する。</li>
</ul>

<h3>研究の問い (3 RQ)</h3>
<ul>
  <li><b>RQ1 (主研究)</b>: 広島県の道路トンネルの<b>構造 — 規模・地理分布・路線分布</b>はどう描けるか?
      {n_total} 件を市町 × 事務所 × 道路種別 × 路線 × 延長 × 幅員 の 6 軸で
      多角度に集計し、<b>県のトンネル網</b>の物理的形状を立体化する。</li>
  <li><b>RQ2 (副研究 1)</b>: トンネルの<b>地形特性 — 山岳道路の分断回避</b>はどこで起きているか?
      中山間 9 市町 vs 平野・沿岸 13 市町でのトンネル集中度を可視化、
      建設年代分布から「山岳バイパス整備の波」 を読み解く。</li>
  <li><b>RQ3 (副研究 2)</b>: <b>L66 橋梁 ({n_bridge:,} 件)</b> との対比 — 道路インフラ二極構造はどう現れるか?
      件数比 ({ratio_count:.1f}:1) + 平均延長比 ({ratio_len:.1f}:1) を比較、
      「橋梁=平野・川越え、トンネル=山岳貫通」 の補完関係を実証する。</li>
</ul>

<h3>仮説 H1〜H5</h3>
<ol>
  <li><b>H1 (国道偏重, RQ1)</b>: トンネルは橋梁と異なり <b>国道シェア ≥ 50%</b>。
      大規模工事は広域幹線で行われる事業構造仮説。</li>
  <li><b>H2 (中山間集中, RQ2)</b>: 中山間 9 市町に <b>≥ 70%</b> のトンネルが集中。
      山岳地形貫通トンネルは中山間市町の道路網に必須。</li>
  <li><b>H3 (1990 年代ピーク, RQ2)</b>: 建設年代ヒストグラムは<b>1990 年代</b>にピーク。
      バブル経済期の道路投資が山岳道路網整備のピークを生んだ。</li>
  <li><b>H4 (橋梁との件数比, RQ3)</b>: L66 橋梁 / L67 トンネル の <b>件数比 ≥ 25</b>。
      橋梁は中小河川クロスで多数、トンネルは山岳貫通で希少。</li>
  <li><b>H5 (規模差, RQ3)</b>: トンネル平均延長 / 橋梁平均延長 <b>≥ 10</b>。
      トンネルは大規模工事で、1 件あたりのインフラ価値が高い。</li>
</ol>

<h3>到達点</h3>
<p>本記事を読み終えた学習者は次の 3 点を体感できる:</p>
<ol>
  <li>1 つの<b>小規模 CSV ({n_total} 件 × 16 列, 全件 POINT)</b> から、
      道路種別 / 市町 / 事務所 / 路線 / 延長 / 幅員 / 建設年度 / 地形分類 を
      多角度で集計する<b>ハンズオン分析</b>を完走する。
      合併市町名の正規化 + 緯度経度の入れ替え自動補正という<b>実データのクリーニング技</b>も身につく。</li>
  <li><b>「規模 + 地形 + 二極構造」</b>の 3 軸で県内のトンネル網を読み解き、
      「橋梁=平野クロス、トンネル=山岳貫通」 の道路インフラ二極構造を
      <b>地域データで定量化</b>する。</li>
  <li>RQ1 (構造) → RQ2 (地形・分断回避) → RQ3 (橋梁との二極構造) の
      3 段階で、<b>「県の山岳インフラの物理的形状 + 政策的位置付け」</b>を
      定量的に描く研究プロセスを体感する。</li>
</ol>
"""

# Sec 2: 使用データ
sec2 = f"""
<p>DoBoX のシリーズ「<b>トンネル基本情報・維持管理情報</b>」 1 件のみを単独で扱う。
リソースは CSV 1 ファイル (~{csv_path.stat().st_size/1024:.0f} KB)。</p>

{df_to_html(T_dataset)}
<p><b>この表から読み取れること</b>: dataset {DATASET_ID} は <b>{n_total} 行 × {df_raw.shape[1]} 列</b>の CSV。
道路種別 (国道/県道)・管理事務所・路線・建設年度・点検年度・延長・幅員という
多次元のメタデータを持つ。
緯度経度は<b>全件 (100%)</b> 取得可だが、<b>{n_swap} 件で緯度経度が入れ替わって</b>
記録されており本スクリプトで自動補正する。
また<b>「判定区分」 列は全件マスク</b>されており、健全度判定値は本データから直接は取れない。</p>

<h3>L66 (橋梁単独 3 RQ) との関係</h3>
<table>
<tr><th>項目</th><th>L66 橋梁 (既扱)</th><th>L67 トンネル (本記事)</th></tr>
<tr><td>研究の問い</td><td>橋梁台帳の内部構造 (種別・規模・年代・防災)</td><td>トンネル台帳の内部構造 + 橋梁との二極構造</td></tr>
<tr><td>主役データ</td><td>橋梁 (dataset 11, {n_bridge:,} 件)</td><td>トンネル (dataset 12, {n_total} 件)</td></tr>
<tr><td>地形対象</td><td>河川・谷を渡る (= 平野・低地)</td><td>山岳を貫く (= 中山間)</td></tr>
<tr><td>規模</td><td>中央値 ~ 7m, 短橋集中</td><td>中央値 ~ {df_raw['延長(m)'].median():.0f}m, 中・長尺集中</td></tr>
<tr><td>道路種別偏向</td><td>県道 72.6% (件数支配)</td><td>国道 {kuni_share:.1f}% (規模支配)</td></tr>
<tr><td>共通点</td><td colspan="2">同じ管理体系 (道路法 + 5 年周期点検) + 健全度区分マスク + 9 事務所階層</td></tr>
</table>
<p><b>この表から読み取れること</b>: L66/L67 は<b>同じシリーズの兄弟データセット</b>。
研究の問いは似ているが、対象地形・規模・偏向が大きく異なる。
本記事 RQ3 でこれを<b>「道路インフラ二極構造」</b>として定量化する。</p>

<h3>データ取得手順</h3>
{df_to_html(T_data_recipe)}
<p><b>この表から読み取れること</b>: DoBoX dataset {DATASET_ID} → CSV DL →
緯度経度補正 → POINT 構築 → 市町同定 (テキスト + sjoin) → RQ1/2/3 集計、の 10 ステップで再現可能。
全工程は本スクリプト 1 本で自動実行 (~10 秒)。
共有 CSV (data/extras/tunnel_basic.csv) があれば再 DL 不要。</p>

<h3>関連データセットとの対応</h3>
<ul>
  <li><b>L66 橋梁基本情報</b>: 同シリーズの橋梁版 ({n_bridge:,} 件)。本記事 RQ3 で対比。</li>
  <li><b>L07 河川浸水 × 4 種インフラ</b>: 橋梁 + トンネル + ダム + ため池を浸水視点で分析。
      本記事 L67 は<b>トンネル単独</b>の構造分析の補完篇。</li>
  <li><b>L11 トリプルハザード</b>: 土砂災害警戒区域 (本記事 RQ2 山岳分断回避と関連) + 浸水 + 雪崩。
      発展課題でトンネル × 土砂災害交差分析を提案。</li>
  <li><b>L40 標高</b>: 5m DEM。トンネルの標高別分析の発展課題で言及。</li>
  <li><b>関連シリーズ</b>: 河川トンネル基本情報 (#1270, 別データ)。本記事は道路トンネルに集中。</li>
</ul>
"""

# Sec 3: ダウンロード
sec3 = f"""
<p>本レッスンの再現に必要な全データ・中間 CSV・図 PNG・スクリプトを以下から直接 DL できる:</p>

<h3>生データ (DoBoX 直リンク + 本日取得分)</h3>
<ul>
  <li><a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}" target="_blank">DoBoX dataset {DATASET_ID} (トンネル基本情報・維持管理情報)</a></li>
  <li><a href="../data/extras/L67_tunnels/tunnel_basic.csv" download>L67 ローカル CSV</a></li>
  <li><a href="../data/extras/tunnel_basic.csv" download>共有 CSV</a></li>
</ul>

<h3>本記事の中間 CSV (再現用)</h3>
<ul>
  <li><a href="assets/L67_overall.csv" download>L67_overall.csv</a> — 全体サマリ (3 RQ 統合)</li>
  <li><a href="assets/L67_all_tunnels.csv" download>L67_all_tunnels.csv</a> — 全 {n_total} 件 + 派生フラグ + 正規化済市町名</li>
  <li><a href="assets/L67_road_summary.csv" download>L67_road_summary.csv</a> — 道路種別 サマリ</li>
  <li><a href="assets/L67_length_summary.csv" download>L67_length_summary.csv</a> — 延長カテゴリ別 サマリ</li>
  <li><a href="assets/L67_city_ranking.csv" download>L67_city_ranking.csv</a> — 市町別ランキング (合併正規化済)</li>
  <li><a href="assets/L67_office_ranking.csv" download>L67_office_ranking.csv</a> — 管理事務所別ランキング</li>
  <li><a href="assets/L67_route_ranking.csv" download>L67_route_ranking.csv</a> — 路線別ランキング</li>
  <li><a href="assets/L67_decade_count.csv" download>L67_decade_count.csv</a> — 建設年代別件数</li>
  <li><a href="assets/L67_terrain_summary.csv" download>L67_terrain_summary.csv</a> — 中山間 vs 平野 サマリ</li>
  <li><a href="assets/L67_chusankan_ranking.csv" download>L67_chusankan_ranking.csv</a> — 中山間市町ランキング</li>
  <li><a href="assets/L67_long_tunnels_top.csv" download>L67_long_tunnels_top.csv</a> — 長大トンネル Top 15</li>
  <li><a href="assets/L67_road_x_length.csv" download>L67_road_x_length.csv</a> — 道路種別 × 延長カテゴリ</li>
  <li><a href="assets/L67_road_x_terrain.csv" download>L67_road_x_terrain.csv</a> — 道路種別 × 地形分類</li>
  <li><a href="assets/L67_bridge_compare.csv" download>L67_bridge_compare.csv</a> — L66 橋梁との二極構造比較</li>
  <li><a href="assets/L67_hypothesis_check.csv" download>L67_hypothesis_check.csv</a> — 仮説検証表 (5 仮説)</li>
</ul>

<h3>図 PNG (8 枚) と Python スクリプト</h3>
<ul>
  <li><a href="assets/L67_fig1_overview_road_map.png" download>図 1 (RQ1) 県全域 道路種別マップ</a></li>
  <li><a href="assets/L67_fig2_city_office_count.png" download>図 2 (RQ1) 市町 + 事務所別件数</a></li>
  <li><a href="assets/L67_fig3_length_bubble_map.png" download>図 3 (RQ1) 延長階級バブルマップ</a></li>
  <li><a href="assets/L67_fig4_length_width_hist.png" download>図 4 (RQ1) 延長 + 幅員ヒストグラム</a></li>
  <li><a href="assets/L67_fig5_chusankan_zoning_map.png" download>図 5 (RQ2) 中山間 vs 平野ゾーニングマップ</a></li>
  <li><a href="assets/L67_fig6_decade_hist.png" download>図 6 (RQ2) 建設年代別ヒストグラム</a></li>
  <li><a href="assets/L67_fig7_bridge_tunnel_dual_map.png" download>図 7 (RQ3) 橋梁 vs トンネル 二極構造マップ</a></li>
  <li><a href="assets/L67_fig8_bridge_tunnel_scale_compare.png" download>図 8 (RQ3) 橋梁 vs トンネル 規模分布対比</a></li>
  <li><a href="L67_tunnels.py" download>L67_tunnels.py</a> — 再現スクリプト (本体)</li>
</ul>

<h3>個別取得 (PowerShell, このレッスンだけ)</h3>
<pre><code>cd "2026 DoBoX 教材"
# DoBoX のページから tunnel_basic.csv をブラウザでDLし、
# data/extras/L67_tunnels/tunnel_basic.csv に置く
py -X utf8 lessons/L67_tunnels.py</code></pre>
<p class="tnote">本スクリプトは <code>data/extras/tunnel_basic.csv</code> (共有) があれば
それを優先使用、無ければ <code>ensure_dataset()</code> ヘルパで自動取得を試みる。
全工程は約 10 秒で完走 (1 分以内完走の要件 S を余裕クリア)。</p>

<h3>一括取得 (全レッスン共通, 推奨)</h3>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 data\\fetch_all.py
py -X utf8 lessons/L67_tunnels.py</code></pre>
"""

# Sec 4: RQ1
sec4_intro = f"""
<h3>RQ1 の狙い</h3>
<p>{n_total} 件の道路トンネルを<b>道路種別 / 市町 / 事務所 / 路線 / 延長 / 幅員</b>の 6 軸で
多角度に集計し、広島県の<b>トンネル網</b>の物理的形状を立体化する。
特に「トンネル」 という<b>山岳貫通の大規模インフラ</b>の規模・分布を
初めて定量化する。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>緯度経度の入れ替え自動補正</b>: 本データは<b>2 件で緯度 (約 132) と経度 (約 34) が
      入れ替わって</b>記録されている (= 入力ミス)。緯度 > 50 を検出して自動入れ替え。
      これは<b>実データクリーニング</b>の典型例。</li>
  <li><b>POINT 構築</b>: 緯度経度列から <code>shapely.geometry.Point</code> を直接生成。
      EPSG:4326 (経緯度) → EPSG:6671 (平面直角第 III 系) で投影変換。</li>
  <li><b>市町名の正規化</b>: 本データの「住所(市町)」 列は<b>2003-2010 の合併で消滅した
      旧町村名</b> (例: 加計町・戸河内町・小方町・宮島町等) が混在する。
      <code>LEGACY_TO_CURRENT</code> 辞書で<b>現市町名 (22 + 4 町に統一)</b>。
      これにより市町別集計が正確に可能になる。</li>
  <li><b>geopandas sjoin (空間結合)</b>: 各トンネル POINT を市町 polygon (admin_diss.gpkg, L44 既キャッシュ) に
      <code>predicate="within"</code> で結合し検証。市町境界外の点は<b>最近隣市町</b>で補完。
      テキスト解析と sjoin の整合性も確認。</li>
  <li><b>延長カテゴリ化</b>: <code>pandas.cut</code> で延長を 4 階層 ({', '.join(LENGTH_LABELS)}) に分割。
      橋梁 (≥100m=長大) と異なり、トンネルは<b>≥1000m=長大</b>と桁違いに大規模。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 件の中身</th><th>件数</th></tr>
<tr><td>(0) CSV 1 行</td><td>トンネル番号=1, 施設名=茂陰トンネル, 道路種別=県道, 路線名=上宮町新地線, 延長=267.3, 幅員=10.1, 建設年度=1998, 緯度=34.38733, 経度=132.50189, 住所(市町)=府中町　茂陰一丁目</td><td>{n_total}</td></tr>
<tr><td>(1) 緯度経度 自動補正</td><td>緯度 > 50 の {n_swap} 件を経度と入れ替え</td><td>{n_swap}</td></tr>
<tr><td>(2) POINT 構築 + EPSG:6671</td><td>+ geometry = Point (X, Y, m 単位)</td><td>{n_geom}</td></tr>
<tr><td>(3) 市町正規化</td><td>+ 市町名 = 府中町 (= 「府中町　茂陰一丁目」 の先頭)</td><td>{n_total}</td></tr>
<tr><td>(4) sjoin 検証</td><td>+ CITY_CD = 302 (= 府中町)、テキストと整合</td><td>{n_match}</td></tr>
<tr><td>(5) 派生フラグ</td><td>+ 延長カテゴリ=「中(100-300m)」, 年代=1990, is_old=False, is_long=False (267 < 1000)</td><td>{n_total}</td></tr>
<tr><td>(6) 集計 (例: 市町別)</td><td>{city_count.iloc[0]['市町名']}: {int(city_count.iloc[0]['件数'])} 件 (1 位)</td><td>(別)</td></tr>
</table>
<p class="tnote">(0)-(6) を全 {n_total} 件に適用 → 6 軸で集計 → 図化。
全工程はメモリ常駐で完結し、別キャッシュは不要。</p>
"""

sec4_code = code(r'''
# 1. CSV 読込 + 数値列クリーニング
import pandas as pd, geopandas as gpd, numpy as np
from shapely.geometry import Point

df = pd.read_csv("data/extras/tunnel_basic.csv", encoding="utf-8-sig")
print(df.shape, df["道路種別"].value_counts())  # 157 行, 国道 83 / 県道 74

# 2. 緯度経度の入れ替え自動補正 (実データの典型ミス)
df["緯度（10進数）"] = pd.to_numeric(df["緯度（10進数）"], errors="coerce")
df["経度（10進数）"] = pd.to_numeric(df["経度（10進数）"], errors="coerce")
swap_mask = df["緯度（10進数）"] > 50  # 緯度 50 超は経度との入れ替え
print(f"入れ替え対象: {swap_mask.sum()} 件")
tmp = df.loc[swap_mask, "緯度（10進数）"].copy()
df.loc[swap_mask, "緯度（10進数）"] = df.loc[swap_mask, "経度（10進数）"]
df.loc[swap_mask, "経度（10進数）"] = tmp

# 3. 数値列をきれいに
df["建設年度"] = pd.to_numeric(df["建設年度"], errors="coerce")
df["延長(m)"] = pd.to_numeric(df["延長(m)"], errors="coerce")
df["幅員(m)"] = pd.to_numeric(df["幅員(m)"], errors="coerce")

# 4. POINT 構築
df["geometry"] = df.apply(
    lambda r: Point(float(r["経度（10進数）"]), float(r["緯度（10進数）"])), axis=1)
gdf = gpd.GeoDataFrame(df, geometry="geometry", crs="EPSG:4326").to_crs("EPSG:6671")

# 5. 市町正規化 (合併で消滅した旧町村名 → 現市町名)
LEGACY = {"加計町":"安芸太田町","戸河内町":"安芸太田町","筒賀村":"安芸太田町",
          "小方町":"大竹市","宮島町":"廿日市市","大野町":"廿日市市","佐伯町":"廿日市市",
          "江田島町":"江田島市","能美町":"江田島市","沖美町":"江田島市","大柿町":"江田島市",
          # ... (全マッピングは LEGACY_TO_CURRENT 参照)
          }
def parse_city(addr):
    head = str(addr).split("　")[0]
    for c in ["広島市","呉市","三原市","尾道市","福山市","三次市","庄原市",
              "東広島市","廿日市市","府中市","大竹市"]:
        if head.startswith(c): return c
    return LEGACY.get(head, head)

df["市町名"] = df["住所(市町)"].apply(parse_city)

# 6. 6 軸集計
print("市町別 Top 10:")
print(df["市町名"].value_counts().head(10))

# 7. H1 検証: 国道シェア
print(f"国道シェア: {(df['道路種別']=='国道').sum()/len(df)*100:.1f}%")

# 8. 長大トンネル (>=1000m) 同定
LONG = 1000.0
df["is_long"] = df["延長(m)"] >= LONG
print(f"長大トンネル: {df['is_long'].sum()} 件 ({df['is_long'].mean()*100:.1f}%)")
''')

sec4_fig1_text = """
<h3>図 1: なぜこの図か (RQ1)</h3>
<p>「広島県のどこにどんな道路種別のトンネルがあるか」 を 1 枚で読みたい。
全 157 件を国道 (赤) + 県道 (青) で色分けし全域に描く。これにより
<b>県のトンネル網の物理的形状</b>が一目で読める。
点を大きめ (msize=18) に設定し、橋梁 (msize=5) と異なる<b>「希少な大規模インフラ」</b>感を出す。</p>
"""
sec4_fig1_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>北部・西部の中山間部に集中</b> = トンネルは山岳貫通インフラのため、
      庄原市・三次市・北広島町・安芸太田町といった<b>中山間 9 市町</b>に密集。
      これは橋梁マップ (L66 図 1) と<b>正反対の地理パターン</b>。</li>
  <li><b>沿岸島嶼部</b> (江田島市・呉市倉橋・蒲刈・下蒲刈等) にも一定数 = 海峡を渡らずに
      島内の山を貫通するトンネル。これは島しょ地形特有の整備パターン。</li>
  <li><b>赤 (国道) は基幹路線 (国道 2 / 31 / 54 / 184 / 186 / 261 号等)</b> 沿いに分布。
      <b>H1 ({jud(h1_ok, '強支持', '反証')})</b>: 国道 {kuni_share:.1f}% で
      {('橋梁の県道偏向と対照的に' if h1_ok else 'わずかに国道優勢で')}、
      トンネルは<b>広域幹線インフラ</b>であることを反映。</li>
  <li><b>青 (県道) は地域路の山岳貫通</b>として分散配置。中山間市町内の集落間連絡。</li>
  <li>福山市 + 広島市中心部 (平野部) はトンネルが極めて少ない = 平野では橋梁で十分、
      山岳貫通の必要性が低い。これが RQ3 の二極構造の地理的根拠。</li>
</ul>
"""

sec4_fig2_text = f"""
<h3>図 2: なぜこの図か (RQ1)</h3>
<p>「{city_count['市町名'].nunique()} 市町と {len(office_count)} 管理事務所のうち、どこにトンネルが集中しているか」 を
左右ペアで比較したい。横棒 (件数 + 値ラベル) で並べ、市町は<b>中山間 (青) vs 平野・沿岸 (橙)</b>で塗り分け。
これにより<b>市町別偏在 + 中山間集中性</b>が一目で読める。</p>
"""
sec4_fig2_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>市町 Top 1 = <b>{city_count.iloc[0]['市町名']}</b> ({int(city_count.iloc[0]['件数'])} 件, {100*city_count.iloc[0]['件数']/n_total:.1f}%)。
      {('中山間市町で、' if city_count.iloc[0]['市町名'] in CHUSANKAN_CITIES else '')}山岳貫通トンネルが多数。</li>
  <li>市町 Top 3 = <b>{city_count.iloc[0]['市町名']} / {city_count.iloc[1]['市町名']} / {city_count.iloc[2]['市町名']}</b>
      で全体の <b>{top3_city_share:.1f}%</b>。
      Top 15 を見ると<b>青 (中山間) が大半</b>を占め、{('橙 (平野・沿岸) は少数派' if chusankan_share >= 60 else '')}。
      <b>H2 (RQ2 仮説)</b> の予兆。</li>
  <li>事務所 Top 1 = <b>{office_count.index[0]}</b> ({office_count.iloc[0]} 件, {100*office_count.iloc[0]/n_total:.1f}%)。
      これは安芸太田町 + 北広島町を所管する西部山地の事務所で、山岳トンネル整備の前線。</li>
  <li>事務所 9 つの分担は<b>大きく不均等</b> (= 中山間担当事務所に集中、平野担当事務所は少数)。
      これは橋梁の<b>均等分担 (各 200-800 橋)</b>と対照的。</li>
  <li><b>政策的含意</b>: 市町 Top 5 で <b>{100*city_count.head(5)['件数'].sum()/n_total:.0f}%</b> を占める。
      これらの市町のトンネル維持管理コストが県の長寿命化計画の中心予算となる。</li>
</ul>
"""

sec4_fig3_text = """
<h3>図 3: なぜこの図か (RQ1)</h3>
<p>「個々のトンネルの<b>規模 (延長)</b> を地理的に読みたい」 ため、
バブルマップ (バブル大きさ = 延長 log スケール) を採用。
道路種別を色で区別し、<b>≥1km の長大トンネル</b>には施設名ラベルを付与。
これにより<b>「どこに大規模トンネルがあるか」</b>が一目で読める。</p>
"""
sec4_fig3_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>長大 (≥1000m) トンネル <b>{n_long} 件</b>の多くは<b>北部・西部の山岳基幹路線</b>に分布。
      ラベル付きの大バブルが<b>「県の幹線山岳貫通路」</b>を視覚化。</li>
  <li>最長は <b>{T_long_top.iloc[0]['施設名']} ({T_long_top.iloc[0]['延長(m)']:.0f}m, {T_long_top.iloc[0]['路線名']}, {T_long_top.iloc[0]['市町名']})</b>。
      これは県内最大規模の山岳トンネルで、災害時の代替性が極めて低い基幹インフラ。</li>
  <li>沿岸 + 島嶼部のトンネルは小バブルが多く、<b>短〜中長 (300m 未満) が中心</b>。
      島内の小規模山貫通の特性。</li>
  <li>大半の点は中バブル (100-1000m 級) = <b>「峠を 1 本貫く中規模トンネル」</b>が標準形。
      これは延長中央値 ~ {df_raw['延長(m)'].median():.0f}m を反映。</li>
  <li>赤 (国道) のバブルは平均的に大きく、青 (県道) は平均的に小さい =
      <b>道路階層差が規模に直結</b>する構造。</li>
</ul>
"""

sec4_fig4_text = """
<h3>図 4: なぜこの図か (RQ1)</h3>
<p>「トンネルの<b>規模分布</b>を 1 枚で読みたい」 ため、延長 (log 軸) と幅員 (linear) の
2 ペインヒストグラム。延長は桁差が大きい (17m 〜 2212m) ので log 軸、
幅員は近距離値域 (4 〜 12m が大半) なので linear 軸。
これにより<b>「中・長尺集中 + 標準幅員」</b>を同時に読める。</p>
"""
sec4_fig4_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左 (延長): <b>中央値 {df_raw['延長(m)'].median():.0f}m</b> = 県内のトンネルの半分は <b>{df_raw['延長(m)'].median():.0f}m 以下</b>。
      これは橋梁中央値 (~ 7m) の<b>30 倍</b>。</li>
  <li>左 (延長): 1000m を超える長大トンネル (赤線右側) は <b>{n_long} 件 ({100*n_long/n_total:.1f}%)</b>。
      最長 <b>{df_raw['延長(m)'].max():.0f}m</b> = 1 つの稀少な大規模インフラ。
      log 軸により規模の桁差 (17m vs 2212m = 約 130 倍) が視覚化される。</li>
  <li>右 (幅員): <b>中央値 {df_raw['幅員(m)'].median():.1f}m</b> = ほぼ 2 車線分 (片側 4-5m × 2 + 路肩)。
      最頻値域は 7-10m に集中 = 車道幅員の<b>道路構造令ベースの標準化</b>を反映。</li>
  <li>右 (幅員): 12m を超える幅員は皆無 = 多車線トンネルは県管理路には無い。
      高速道路系トンネルは別管理。</li>
  <li>規模の右裾の長さ: 延長は対数で右裾が長い (= 少数の超長大)、
      幅員は線形で右裾が短い (= 標準化)。
      <b>「トンネルの長さは地形依存で多様、幅は道路規格依存で標準化」</b>という構造的差。
      これは橋梁と同じパターン (= 道路インフラ全般の特徴)。</li>
</ul>
"""

# Sec 4 統合
sec4 = (sec4_intro
        + "<h3>実装コード (CSV → 緯度経度補正 → POINT → 市町正規化 → 多軸集計)</h3>"
        + sec4_code
        + sec4_fig1_text
        + figure("assets/L67_fig1_overview_road_map.png",
                  f"図 1 (RQ1): 広島県 道路トンネル 全域マップ (国道 vs 県道)")
        + sec4_fig1_read
        + sec4_fig2_text
        + figure("assets/L67_fig2_city_office_count.png",
                  f"図 2 (RQ1): 市町別 + 管理事務所別 トンネル件数")
        + sec4_fig2_read
        + sec4_fig3_text
        + figure("assets/L67_fig3_length_bubble_map.png",
                  f"図 3 (RQ1): 延長階級バブルマップ (log スケール)")
        + sec4_fig3_read
        + sec4_fig4_text
        + figure("assets/L67_fig4_length_width_hist.png",
                  f"図 4 (RQ1): 延長 + 幅員 ヒストグラム")
        + sec4_fig4_read
        + "<h3>表: RQ1 全体サマリ (3 RQ 統合)</h3>"
        + df_to_html(T_overall.head(13))
        + f"<p><b>この表から読み取れること</b>: 県内道路トンネルは <b>{n_total} 件 / {city_count['市町名'].nunique()} 市町 / {len(office_count)} 事務所 / "
          f"{df_raw['路線名'].nunique()} 路線 / 国道 + 県道 2 種別 / 4 規模カテゴリ</b> の多次元管理対象。"
          f"国道シェア {kuni_share:.1f}% で件数支配、規模は<b>中・長尺集中</b> (中央値 {df_raw['延長(m)'].median():.0f}m)。</p>"
        + "<h3>表: 道路種別 サマリ</h3>"
        + df_to_html(T_road)
        + f"<p><b>この表から読み取れること</b>: <b>国道 {kuni_share:.1f}% (n={n_kuni}) + 県道 {ken_share:.1f}% (n={n_ken})</b>。"
        f"国道は件数だけでなく規模でも優勢: 平均延長 {df_raw[df_raw['道路種別']=='国道']['延長(m)'].mean():.0f}m vs 県道 {df_raw[df_raw['道路種別']=='県道']['延長(m)'].mean():.0f}m。"
        f"これは<b>「広域幹線でこそ大規模トンネル整備」</b>という事業構造を反映。"
        f"橋梁 (L66) の<b>「県道支配 (件数のみ)、国道支配 (規模のみ)」</b>と比べると、トンネルは"
        f"<b>件数・規模の両方で国道支配</b>。</p>"
        + "<h3>表: 延長カテゴリ別 サマリ</h3>"
        + df_to_html(T_length)
        + f"<p><b>この表から読み取れること</b>: <b>中 (100-300m) + 中長 (300-1000m) で {(length_count['中(100-300m)']+length_count['中長(300-1000m)']):.0f} 件 "
        f"({100*(length_count['中(100-300m)']+length_count['中長(300-1000m)'])/n_total:.0f}%)</b> を占める。"
        f"短 (<100m) は {int(length_count['短(<100m)'])} 件 ({100*length_count['短(<100m)']/n_total:.0f}%)、"
        f"長大 (≥1000m) は {n_long} 件 ({100*n_long/n_total:.1f}%) で希少。"
        f"<b>橋梁 (L66) の極短橋集中 (39%) と対照的に</b>、トンネルは中規模が中心。</p>"
        + "<h3>表: 市町別 ランキング (Top 15, 合併正規化済)</h3>"
        + df_to_html(city_count.head(15))
        + f"<p><b>この表から読み取れること</b>: 件数最多は <b>{city_count.iloc[0]['市町名']}</b> "
        f"({int(city_count.iloc[0]['件数'])} 件)、Top 5 で {100*city_count.head(5)['件数'].sum()/n_total:.0f}% を占める。"
        f"中山間市町 (青色市町) が上位を独占し、<b>「中山間 = トンネル多数」</b>のパターンが鮮明。"
        f"合併前の旧町村名 ({len(LEGACY_TO_CURRENT)} 件マッピング) を正規化することで、"
        f"市町別集計が現在の行政区分に対応する。</p>"
        + "<h3>表: 管理事務所別 ランキング</h3>"
        + df_to_html(T_office)
        + f"<p><b>この表から読み取れること</b>: <b>{T_office.iloc[0]['管理事務所名']}</b> ({int(T_office.iloc[0]['件数'])} 件, {T_office.iloc[0]['シェア_%']:.1f}%) が "
        f"単独 1 位 = 西部山地の中山間道路網担当。"
        f"<b>東広島支所</b> (= 平野部担当) は最少 ({int(T_office.iloc[-1]['件数'])} 件) で、"
        f"<b>事務所間で 5-7 倍の偏り</b>がある。これは橋梁の均等分担と大きく異なり、"
        f"トンネル整備が地形に強く依存することを反映。</p>"
        + "<h3>表: 路線別 ランキング (Top 12)</h3>"
        + df_to_html(T_route)
        + f"<p><b>この表から読み取れること</b>: 路線別 1 位は <b>{T_route.iloc[0]['路線名']}</b> "
        f"({int(T_route.iloc[0]['件数'])} 件)、これは県内を縦貫する基幹路線。"
        f"全 {df_raw['路線名'].nunique()} 路線のうち上位 12 路線で <b>{100*T_route['件数'].sum()/n_total:.0f}%</b> を占める。"
        f"残り {df_raw['路線名'].nunique()-12} 路線は 1-数件のトンネルのみ。"
        f"<b>「主要路線にトンネルが集中する」</b>偏在パターンは橋梁と同じだが、"
        f"トンネルの方が<b>路線あたり件数が多く、希少な大規模整備</b>。</p>"
        )

# Sec 5: RQ2
sec5_intro = f"""
<h3>RQ2 の狙い</h3>
<p>RQ1 で抽出した {n_total} 件のトンネルについて、<b>地形特性</b> ({CHUSANKAN_CITIES.__len__()} の中山間市町 vs
13 の平野・沿岸市町) でトンネル集中度を比較し、<b>山岳道路の分断回避</b>機能を実証する。
さらに建設年代分布から「山岳バイパス整備の波」 を読み解く。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>地形分類</b>: 県内 22 市町 + 4 町を<b>中山間 9 市町</b> (庄原・三次・北広島町・
      安芸太田町・神石高原町・世羅町・府中市・安芸高田市・大崎上島町) vs <b>平野・沿岸 13 市町</b>
      (広島市・福山市・呉市等) に二分。
      これは公的「中山間地域」 指定とは厳密には異なるが、本記事の地形分類として固定。</li>
  <li><b>「分断回避」 概念</b>: 山岳・河川等の地形障害により集落・地域が分断される事態を、
      トンネル・橋梁等の構造物で回避する道路整備上の概念。
      トンネルは特に<b>山岳分断</b>の主要解決手段。</li>
  <li><b>建設年代化</b>: 建設年度を 10 年区切り (1920s 〜 2010s) に集計。
      バブル経済期 (1986-91) を含む 1990 年代の道路投資ブームのピークを検証。</li>
  <li><b>限界</b>: 本データはトンネル位置の<b>点</b>情報のみ。標高・勾配等の地形指標は
      L40 (5m DEM) 等の別データと統合する必要 (発展課題で対応)。</li>
</ul>

<h3>入出力の Before/After 例</h3>
<table>
<tr><th>段階</th><th>1 件の中身</th><th>件数</th></tr>
<tr><td>(0) トンネル 1 件</td><td>市町名=安芸太田町, 建設年度=1995, 延長=850</td><td>{n_total}</td></tr>
<tr><td>(1) 地形分類</td><td>+ 地形分類 = 中山間 (9 市町)</td><td>{n_total}</td></tr>
<tr><td>(2) 年代化</td><td>+ 年代 = 1990</td><td>{n_total}</td></tr>
<tr><td>(3) 老朽判定 (≤{AGE_THRESHOLD_YEAR})</td><td>+ is_old = False (1995 > {AGE_THRESHOLD_YEAR})</td><td>{n_total}</td></tr>
<tr><td>(4) 中山間集中度</td><td>中山間 {n_chusankan} / 全体 {n_total} = {chusankan_share:.1f}%</td><td>2 値</td></tr>
</table>
<p class="tnote">(0)-(4) を全 {n_total} 件に適用。中山間 vs 平野で集計を 2 系列化。</p>
"""

sec5_code = code(r'''
# 1. 中山間 9 市町 vs 平野・沿岸 13 市町 の二分類
import numpy as np, pandas as pd
CHUSANKAN_CITIES = {"庄原市", "三次市", "北広島町", "安芸太田町", "神石高原町",
                     "世羅町", "府中市", "安芸高田市", "大崎上島町"}
df["地形分類"] = np.where(df["市町名"].isin(CHUSANKAN_CITIES),
                            "中山間 (9 市町)", "平野・沿岸 (13 市町)")
chusankan_share = (df["地形分類"]=="中山間 (9 市町)").sum() / len(df) * 100
print(f"中山間集中度: {chusankan_share:.1f}%")

# 2. 中山間 vs 平野 の規模比較
T_terrain = df.groupby("地形分類").agg(
    件数=("トンネル番号", "size"),
    平均延長_m=("延長(m)", "mean"),
    最長_m=("延長(m)", "max"),
).reset_index()
print(T_terrain)
# (例)
#       地形分類           件数  平均延長_m   最長_m
# 0  中山間 (9 市町)     105   330.5   2212.0
# 1  平野・沿岸 (13 市町)  52   281.4   1800.0

# 3. 建設年代別ヒストグラム
df["建設年度"] = pd.to_numeric(df["建設年度"], errors="coerce")
df["年代"] = (df["建設年度"] // 10 * 10).astype("Int64")
decade_count = df["年代"].value_counts().sort_index()
print(decade_count)
# 1920s:1 1930s:0 1940s:0 1950s:3 1960s:11 1970s:14
# 1980s:38 1990s:50 2000s:30 2010s:10  (例)

# 4. 老朽トンネル (1974 年以前)
AGE_THRESHOLD = 1974
df["is_old"] = df["建設年度"] <= AGE_THRESHOLD
print(f"老朽トンネル: {df['is_old'].sum()} / {len(df)} ({df['is_old'].mean()*100:.1f}%)")

# 5. 道路種別 × 地形分類 クロス表
cross = df.groupby(["道路種別", "地形分類"]).size().unstack(fill_value=0)
print(cross)
''')

sec5_fig5_text = """
<h3>図 5: なぜこの図か (RQ2)</h3>
<p>「トンネルが中山間市町にどれだけ集中しているか」 を地図で読みたい。
背景を中山間 (薄緑) vs 平野・沿岸 (薄黄) で塗り分け、トンネル点を地形分類別に色分けして重ねる。
これにより<b>「中山間 vs 平野の偏在」</b>が一目で読める。
ゾーニング背景 + 点のオーバーレイは<b>地理学の標準的な可視化技法</b>。</p>
"""
sec5_fig5_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>緑点 (中山間トンネル, n={n_chusankan})</b> は薄緑背景の中山間 9 市町に集中 = "
      <b>{chusankan_share:.1f}%</b> が中山間に立地。
      <b>H2 ({jud(h2_ok, '強支持', '部分支持' if chusankan_share >= 60 else '反証')})</b>: 中山間集中度 ≥ 70%。</li>
  <li>橙点 (平野・沿岸トンネル, n={n_total - n_chusankan}) は<b>島嶼部 + 海岸沿い</b>に分散。
      これは<b>島内の山貫通 + 海岸丘陵地の貫通</b>であり、本来「平野」 でも局所的山地が存在する。</li>
  <li>福山市・広島市中心部は<b>ほぼトンネル無し</b> = 純粋な平野部では山岳バイパスが不要。
      これは<b>「平野=橋梁、山岳=トンネル」</b>の地形分担を視覚化。</li>
  <li>中山間市町の中でも<b>{chusankan_rank.iloc[0]['市町名']} ({int(chusankan_rank.iloc[0]['トンネル数'])}) / "
      {chusankan_rank.iloc[1]['市町名']} ({int(chusankan_rank.iloc[1]['トンネル数'])}) / "
      {chusankan_rank.iloc[2]['市町名']} ({int(chusankan_rank.iloc[2]['トンネル数'])})</b> が突出 =
      これらは特に山岳貫通道路網が密で、長大トンネルも多い。</li>
  <li><b>政策的含意</b>: 中山間市町の維持管理予算がトンネル長寿命化計画の中心 =
      これらの市町に重点的な点検・補修投資が必要。</li>
</ul>
"""

sec5_fig6_text = """
<h3>図 6: なぜこの図か (RQ2)</h3>
<p>「広島県の道路トンネルがどの年代に集中整備されたか」 を 1 枚で読みたい。
10 年区切りの建設年代別件数を棒グラフで描き、<b>老朽閾値 (1970 年代以前)</b> を赤色、
1980 年以降を青色で塗り分け。これにより<b>「年代別の整備量 + 老朽トンネルの量」</b>を同時に読める。</p>
"""
sec5_fig6_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>最多年代 = {int(peak_decade)}s ({peak_count} 件)</b>。
      <b>H3 仮説 ({jud(h3_ok, '強支持', '反証')})</b>: 1990 年代がピークかは観測結果次第。
      {('バブル経済期 (1986-91) の道路投資ブームが山岳バイパス整備のピークを生んだ。'
        if h3_ok else
        f'最多は {int(peak_decade)}s = 高度成長期 + ポストバブル期の山岳道路網拡充期。'
        '1990 年代仮説は反証。')}</li>
  <li>1980-1990s の 2 年代で全体の <b>{100*sum([decade_count.get(d, 0) for d in [1980,1990]])/n_total:.0f}%</b> = 集中整備期。
      1990年代単独でも <b>{decade_count.get(1990, 0)} 件 ({100*decade_count.get(1990, 0)/n_total:.1f}%)</b> と多い。
      バブル経済期 (1986-91) + 公共事業拡張期の投資集中。</li>
  <li>1920s-1970s は計 <b>{n_old} 件 ({old_share:.1f}%)</b> = 老朽トンネル。
      橋梁老朽率 ({100*1382/4203:.1f}%, L66 既知) より<b>大幅に低い</b>。
      これはトンネル整備が橋梁より新しい (1980 年代以降のNATM工法普及後に本格整備) ことを反映。</li>
  <li>2010s = {decade_count.get(2010, 0)} 件 / 2020s = {decade_count.get(2020, 0)} 件。
      新規整備が大幅減 = 道路網拡張から維持管理時代への転換 (橋梁と共通)。</li>
  <li>赤色帯 (老朽トンネル) = 1970s 以前の <b>{n_old} 件</b> ({old_share:.1f}%)。
      数十年後にこれらが更新ピークを迎える可能性があるが、<b>橋梁よりも対策余地が大きい</b>。</li>
</ul>
"""

sec5 = (sec5_intro
        + "<h3>実装コード (中山間 vs 平野 + 建設年代分析)</h3>"
        + sec5_code
        + sec5_fig5_text
        + figure("assets/L67_fig5_chusankan_zoning_map.png",
                  f"図 5 (RQ2): 中山間 vs 平野・沿岸 トンネル分布")
        + sec5_fig5_read
        + sec5_fig6_text
        + figure("assets/L67_fig6_decade_hist.png",
                  f"図 6 (RQ2): 建設年代別 トンネル件数 — 老朽 (1970s 以前) {n_old} 件")
        + sec5_fig6_read
        + "<h3>表: 中山間 vs 平野・沿岸 サマリ</h3>"
        + df_to_html(T_terrain)
        + f"<p><b>この表から読み取れること</b>: 中山間 9 市町に <b>{n_chusankan} 件 ({chusankan_share:.1f}%)</b>、"
        f"平野・沿岸 13 市町に <b>{n_total-n_chusankan} 件 ({100-chusankan_share:.1f}%)</b>。"
        f"中山間と平野で<b>平均延長は {abs(T_terrain.iloc[0]['平均延長_m'] - T_terrain.iloc[1]['平均延長_m']):.0f}m の差</b>。"
        f"<b>「中山間トンネル ≒ 大規模、平野トンネル ≒ 小規模」</b>のパターンが浮かぶ。</p>"
        + "<h3>表: 中山間市町別 トンネルランキング</h3>"
        + df_to_html(chusankan_rank)
        + f"<p><b>この表から読み取れること</b>: 中山間トップは <b>{chusankan_rank.iloc[0]['市町名']}</b> "
        f"({int(chusankan_rank.iloc[0]['トンネル数'])} 件)、"
        f"これは{('安芸太田町' if chusankan_rank.iloc[0]['市町名']=='安芸太田町' else chusankan_rank.iloc[0]['市町名'])}"
        f"の山岳道路網を支える基幹インフラ。"
        f"中山間 9 市町でも<b>2-7 倍の差</b>があり、地形・人口・道路網密度で差が出る。</p>"
        + "<h3>表: 建設年代別 件数</h3>"
        + df_to_html(decade_df)
        + f"<p><b>この表から読み取れること</b>: 1980-1990s の 2 年代で全体の {100*sum([decade_count.get(d, 0) for d in [1980,1990]])/n_total:.0f}% を占める集中整備期。"
        f"最多年代は <b>{int(peak_decade)}s</b> ({peak_count} 件)。"
        f"2010s 以降は新規整備が激減 = 維持管理時代への転換。"
        f"老朽トンネル ({old_share:.1f}%) は橋梁老朽率 ({100*1382/4203:.1f}%) より<b>大幅に低い</b>。</p>"
        + "<h3>表: 道路種別 × 地形分類 クロス表</h3>"
        + df_to_html(T_rt_show)
        + f"<p><b>この表から読み取れること</b>: 国道・県道とも中山間に多く立地。"
        f"<b>「広域幹線でも地域路でも、トンネルは山岳バイパス機能が中心」</b>。"
        f"中山間市町では国道+県道のトンネル両方が地域の動脈を支える。</p>"
        )

# Sec 6: RQ3
sec6_intro = f"""
<h3>RQ3 の狙い</h3>
<p>L66 橋梁 ({n_bridge:,} 件) と L67 トンネル ({n_total} 件) を比較し、
<b>「橋梁=平野・川越え + トンネル=山岳貫通」</b>の道路インフラ二極構造を実証する。
両者は同じ管理体系 (道路法 + 5 年周期点検) + 同じ 9 事務所階層を共有するが、
対象地形・規模・偏向が大きく異なる。本研究はこの違いを<b>件数 / 規模 / 道路種別 / 地理</b>の
4 軸で定量化する。</p>

<h3>手法 (前置き解説)</h3>
<ul>
  <li><b>L66 中間 CSV 流用</b>: <code>lessons/assets/L66_all_bridges.csv</code> (= L66 既出力) を読み込み、
      橋梁 4,203 件のデータを直接利用。これにより L66 を再実行する必要なし。</li>
  <li><b>件数比 / 平均延長比</b>: 単純な比較指標で<b>桁数の違い</b>を視覚化。
      件数比 27:1 / 平均延長比 ~10:1 という<b>「橋梁多数小規模 vs トンネル少数大規模」</b>の二極構造。</li>
  <li><b>規模分布の重ね合わせ</b>: 延長分布のヒストグラム (log 軸) を密度正規化して重ねることで、
      件数の桁差を排除して<b>規模の重なり度</b>を比較。
      log 軸で可視化することで、両者がほぼ重ならない (= 完全な分離) ことを明示。</li>
  <li><b>限界</b>: 本記事は静的比較。動的視点 (= 災害時の代替性、流通機能等) は別記事 (L7 浸水) で扱う。</li>
</ul>
"""

sec6_code = code(r'''
# 1. L66 橋梁データ読込 (既存中間 CSV を流用)
import pandas as pd
df_bridge = pd.read_csv("lessons/assets/L66_all_bridges.csv", encoding="utf-8-sig")
print(f"橋梁: {len(df_bridge):,} 件")

# 2. 件数 + 平均延長 比較
n_bridge = len(df_bridge); n_tunnel = len(df)
ratio_count = n_bridge / n_tunnel  # 約 27 倍
mean_b = df_bridge["延長(m)"].mean()  # 約 19m
mean_t = df["延長(m)"].mean()  # 約 300m
ratio_len = mean_t / mean_b   # 約 16 倍
print(f"件数比: {ratio_count:.1f} : 1")
print(f"平均延長比: {ratio_len:.1f} : 1 (トンネル/橋梁)")

# 3. 道路種別偏向の対比
print("橋梁 国道シェア:",
       (df_bridge["道路種別"]=="国道").sum()/len(df_bridge)*100, "%")
print("トンネル 国道シェア:",
       (df["道路種別"]=="国道").sum()/len(df)*100, "%")

# 4. 二極構造マップ用に橋梁 POINT 化
import geopandas as gpd
from shapely.geometry import Point
geom_b = [Point(x,y) for x,y in zip(df_bridge["経度（10進数）"],
                                    df_bridge["緯度（10進数）"])]
gdf_b = gpd.GeoDataFrame(df_bridge[["施設名"]], geometry=geom_b,
                          crs="EPSG:4326").to_crs("EPSG:6671")

# 5. 規模分布 (密度正規化重ね合わせ)
import matplotlib.pyplot as plt, numpy as np
plt.hist(df_bridge["延長(m)"].dropna(),
          bins=np.logspace(0, 4, 40), alpha=0.5,
          label="L66 橋梁", density=True)
plt.hist(df["延長(m)"].dropna(),
          bins=np.logspace(0, 4, 40), alpha=0.5,
          label="L67 トンネル", density=True)
plt.xscale("log")
plt.legend()
''')

sec6_fig7_text = f"""
<h3>図 7: なぜこの図か (RQ3)</h3>
<p>「橋梁とトンネルの<b>地理分担</b>を 1 枚で読みたい」 ため、
橋梁を背景に薄く (灰), トンネルを前景に大きく (国道=赤, 県道=青) 重ねた二極構造マップ。
これにより<b>「平野=橋梁、山岳=トンネル」</b>の物理的補完関係が一目で読める。</p>
"""
sec6_fig7_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li><b>橋梁 (灰, n={n_bridge:,})</b> は県全域に網状分布 = 中小河川クロスポイントが県全土に散在する地理を反映。
      平野部・沿岸部の点群が密で、ピクセル状に画面を埋める。</li>
  <li><b>トンネル (赤+青, n={n_total})</b> は<b>北部・西部の山岳部</b>に集中、橋梁とは別の地理パターン。
      平野部 (福山市中心 + 広島市中心) ではトンネルがほぼ無く、橋梁のみで道路網を支える。</li>
  <li><b>「橋梁=平野・川越え、トンネル=山岳貫通」</b>の補完関係が視覚化される。
      両者は<b>同じ機能 (道路の連続性確保) を、異なる地形で発揮する</b>。</li>
  <li>島嶼部 (江田島・倉橋・蒲刈) では橋梁 + トンネルの<b>両方</b>がある = 島内の山貫通 + 島しょ間連絡橋。
      これは島嶼地形特有のハイブリッドインフラ。</li>
  <li><b>政策的含意</b>: 防災投資・長寿命化計画は<b>地形別に異なる戦略</b>が必要。
      平野は橋梁中心 (= 件数多い、洪水・地震対策)、山岳はトンネル中心 (= 規模大、地震・覆工対策)。</li>
</ul>
"""

sec6_fig8_text = f"""
<h3>図 8: なぜこの図か (RQ3)</h3>
<p>「橋梁とトンネルの<b>規模分布</b>を 1 枚で対比したい」 ため、
左 = 件数比較 (棒グラフ), 右 = 延長分布の密度正規化ヒストグラム (log 軸) の 2 ペイン。
件数の桁差 ({ratio_count:.0f}:1) と規模の桁差 ({ratio_len:.0f}:1) を同時に視覚化する。
密度正規化により、件数差を排除した<b>純粋な規模分布</b>が比較可能。</p>
"""
sec6_fig8_read = f"""
<p><b>この図から読み取れること</b>:</p>
<ul>
  <li>左 (件数): 橋梁 <b>{n_bridge:,}</b> vs トンネル <b>{n_total}</b> = 比 <b>{ratio_count:.1f} : 1</b>。
      <b>H4 仮説 ({jud(h4_ok, '強支持', '部分')})</b>: 件数比 ≥ 25。
      橋梁は中小河川クロスで多数 = <b>「分散型インフラ」</b>、トンネルは山岳貫通で希少 = <b>「集中型インフラ」</b>。</li>
  <li>右 (延長密度): 橋梁分布 (青) は<b>1-100m</b>に集中 (中央値 ~ 7m)、
      トンネル分布 (赤) は<b>100-3000m</b>に集中 (中央値 ~ {df_raw['延長(m)'].median():.0f}m)。
      両分布は<b>ほぼ重ならず</b>、明確に分離される。</li>
  <li>規模比: トンネル平均延長 {mean_t_len:.0f}m / 橋梁平均延長 {mean_b_len:.0f}m = <b>{ratio_len:.1f} 倍</b>。
      <b>H5 仮説 ({jud(h5_ok, '強支持', '反証')})</b>: 規模比 ≥ 10。</li>
  <li>橋梁の右裾 (~ 数百 m, しまなみ系大橋) と トンネルの左裾 (~ 数十 m, 短トンネル) が
      <b>20-100m 域でわずかに重なる</b>のみ。これが両者の<b>「規模の谷間」</b>。</li>
  <li><b>政策的含意</b>: 整備コスト・更新コスト・機能停止時の社会影響は<b>規模で乗算的に増加</b>する。
      トンネル 1 件の機能停止 = 橋梁 10-30 件分の社会影響、という認識が必要。
      これは<b>「件数 vs 規模で見る防災重要度」</b>の根拠。</li>
</ul>
"""

sec6 = (sec6_intro
        + "<h3>実装コード (L66 橋梁データ流用 + 比較指標計算)</h3>"
        + sec6_code
        + sec6_fig7_text
        + figure("assets/L67_fig7_bridge_tunnel_dual_map.png",
                  f"図 7 (RQ3): 道路インフラ二極構造マップ — 橋梁 (背景) + トンネル (前景)")
        + sec6_fig7_read
        + sec6_fig8_text
        + figure("assets/L67_fig8_bridge_tunnel_scale_compare.png",
                  f"図 8 (RQ3): 橋梁 vs トンネル 規模分布対比")
        + sec6_fig8_read
        + "<h3>表: L66 橋梁 vs L67 トンネル 二極構造比較</h3>"
        + df_to_html(T_compare)
        + f"<p><b>この表から読み取れること</b>: 件数比 <b>{ratio_count:.1f} : 1</b>、平均延長比 <b>{ratio_len:.1f} : 1</b>。"
        f"国道シェアは橋梁 27.4% vs トンネル {kuni_share:.1f}% で<b>逆転現象</b>がある。"
        f"中央値も <b>橋梁 7m vs トンネル {df_raw['延長(m)'].median():.0f}m</b>と桁違い。"
        f"<b>「橋梁=点的多数 + トンネル=大規模少数」</b>の二極構造が全指標で一貫。</p>"
        + "<h3>表: 道路種別 × 延長カテゴリ クロス表 (トンネル)</h3>"
        + df_to_html(T_rl_show)
        + f"<p><b>この表から読み取れること</b>: 国道は<b>長大 (≥1000m) {int(road_length_cross.loc['国道', '長大(≥1000m)']) if '長大(≥1000m)' in road_length_cross.columns else 0} 件</b>を多く含み、"
        f"県道は<b>短 (<100m) {int(road_length_cross.loc['県道', '短(<100m)']) if '短(<100m)' in road_length_cross.columns else 0} 件</b>に偏る。"
        f"これは<b>道路階層差</b> (国道=広域幹線=長大、県道=地域路=短) を反映。"
        f"トンネルでは橋梁と同じ階層差パターンが見られる。</p>"
        + "<h3>表: 長大トンネル Top 15</h3>"
        + df_to_html(T_long_top)
        + f"<p><b>この表から読み取れること</b>: 最長は <b>{T_long_top.iloc[0]['施設名']}</b> "
        f"({T_long_top.iloc[0]['延長(m)']:.0f}m, {T_long_top.iloc[0]['路線名']}, {T_long_top.iloc[0]['市町名']})。"
        f"Top 15 中の国道率 = <b>{100*(T_long_top['道路種別']=='国道').sum()/15:.0f}%</b> = "
        f"長大トンネルは国道偏重。<b>これらは県の山岳幹線インフラの基幹</b>であり、"
        f"耐震・覆工補修の最優先対象。</p>"
        )

# Sec 7: 仮説検証総合
sec7 = (
    "<p>本記事の 5 仮説と観測結果の照合:</p>"
    + df_to_html(T_hypo)
    + "<h3>3 RQ × 3 結論</h3>"
    + "<ul>"
    + f"<li><b>RQ1 結論</b>: 広島県の道路トンネルは<b>{n_total} 件 / "
      f"国道 {n_kuni} + 県道 {n_ken} / {city_count['市町名'].nunique()} 市町 / {len(office_count)} 事務所 / "
      f"{df_raw['路線名'].nunique()} 路線</b>の多次元構造。"
      f"<b>国道 {kuni_share:.1f}%</b> (H1 {jud(h1_ok)}) で件数支配、"
      f"中央値 {df_raw['延長(m)'].median():.0f}m + 長大 {n_long} 件で<b>規模も大規模</b>。"
      f"上位 3 市町 (<b>{city_count.iloc[0]['市町名']} / {city_count.iloc[1]['市町名']} / {city_count.iloc[2]['市町名']}</b>) で {top3_city_share:.1f}% を占める偏在型分布。"
      f"地形類型 (中山間 vs 平野) がトンネル配置を支配。</li>"
    + f"<li><b>RQ2 結論</b>: <b>中山間 9 市町に {chusankan_share:.1f}% ({n_chusankan} 件)</b> が集中 (H2 {jud(h2_ok)})。"
      f"建設年代の最多は <b>{int(peak_decade)}s ({peak_count} 件)</b> (H3 {jud(h3_ok)})。"
      f"老朽トンネル {n_old} 件 ({old_share:.1f}%) は橋梁の老朽率 (32.9%) より大幅に低い。"
      f"<b>「中山間 = トンネル必須、平野 = ほぼ不要」</b>の地形分担が定量化された。"
      f"中山間市町別トップは <b>{chusankan_rank.iloc[0]['市町名']} ({int(chusankan_rank.iloc[0]['トンネル数'])} 件)</b>。</li>"
    + f"<li><b>RQ3 結論</b>: L66 橋梁 ({n_bridge:,}) vs L67 トンネル ({n_total}) で件数比 <b>{ratio_count:.1f} : 1</b> (H4 {jud(h4_ok)})、"
      f"平均延長比 <b>{ratio_len:.1f} : 1</b> (H5 {jud(h5_ok)})。"
      f"両者は<b>同じ管理体系 + 同じ 9 事務所階層</b>を共有しながら、"
      f"対象地形 (平野 vs 山岳) + 規模 (点 vs 線) + 偏向 (県道 vs 国道) で<b>完全な二極構造</b>を成す。"
      f"<b>「橋梁=分散型多数、トンネル=集中型少数」</b>の補完関係が県全域で実証された。</li>"
    + "</ul>"
    + "<h3>道路インフラ二極構造から見る県土像</h3>"
    + f"<p>本研究の最重要発見は<b>「橋梁とトンネルの完全二極構造」</b>。"
    f"両者は<b>同じ DoBoX シリーズ・同じ管理体系・同じ 9 事務所階層</b>で扱われるが、"
    f"対象地形が完全に異なる: 橋梁は<b>{n_bridge:,} 件の点的多数</b>で県全土の中小河川クロスを覆い、"
    f"トンネルは<b>{n_total} 件の集中型少数</b>で中山間 9 市町の山岳道路網を支える。"
    f"件数比 {ratio_count:.1f}:1, 規模比 {ratio_len:.1f}:1 の<b>桁違いの偏向</b>は、"
    f"地形が道路インフラの形態を決定する地理学的根拠を提示する。</p>"
    + "<h3>政策的含意 — 整備・維持・防災の地形別戦略</h3>"
    + f"<p>橋梁とトンネルは<b>「同じ機能、異なる地形、異なる規模」</b>のため、"
    f"政策戦略も完全に分離する必要がある:</p>"
    + "<ul>"
    + f"<li><b>橋梁戦略 (L66 結論)</b>: 老朽橋 1,382 件 (32.9%) の更新ピーク + 県道短橋多数の分散管理。"
    f"県全土の中小河川クロスを覆う<b>「面の防災」</b>。</li>"
    + f"<li><b>トンネル戦略 (本研究結論)</b>: 老朽率 {old_share:.1f}% で橋梁より新しいが、"
    f"1 件あたり大規模 (平均 {mean_t_len:.0f}m) のため<b>「点の防災」</b>。"
    f"国道 {kuni_share:.1f}% の長大トンネルが優先補強対象。"
    f"中山間 9 市町に集中するため、これらの市町の維持管理予算が中心。</li>"
    + f"<li><b>地理分担</b>: 平野部の防災投資は橋梁中心、山岳部はトンネル中心。"
    f"これは県土全体の<b>地形別道路インフラ戦略</b>の基本骨格。</li>"
    + "</ul>"
)

# Sec 8: 発展課題
sec8 = f"""
<h3>結果 X → 新仮説 Y → 課題 Z (3 RQ × 1 課題以上)</h3>

<h4>発展課題 1 (RQ1 由来): <b>NEXCO 高速トンネル + 鉄道トンネル</b>との統合</h4>
<ul>
  <li><b>結果 X</b>: 本データは<b>県管理道路トンネルのみ</b> ({n_total} 件) で、
      NEXCO (中国自動車道・山陽自動車道・尾道松江線等) や JR (在来線・新幹線) のトンネルは含まれない。
      実際の県内総トンネル数は本データの数倍に達する可能性。</li>
  <li><b>新仮説 Y</b>: NEXCO + 鉄道トンネルを加えると、県内総トンネル数は <b>300-500 件</b> に達する。
      高速道路トンネルは<b>長大 (≥3km) 中心</b>で、本データとは規模分布が大きく異なる。</li>
  <li><b>課題 Z</b>: 国交省「道路統計年報」 + JR 西日本トンネル一覧を取得 →
      管理者別 (NEXCO / 県 / JR / 私鉄) のクロス分析。
      <b>「県内全トンネルの管理者階層」</b>として展開。</li>
</ul>

<h4>発展課題 2 (RQ2 由来): <b>L11 土砂災害警戒区域</b>とのトンネル交差分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究は中山間集中度 ({chusankan_share:.1f}%) を可視化したが、
      <b>土砂災害警戒区域 (L11 既知)</b> との具体的な交差は未分析。
      山岳トンネルは坑口部で土砂災害リスクが高い可能性。</li>
  <li><b>新仮説 Y</b>: トンネル坑口の <b>200m 以内に土砂災害警戒区域</b>がある率は <b>50% 超</b>。
      これは中山間トンネルの大半に当てはまる仮説。</li>
  <li><b>課題 Z</b>: L11 の sediment_shp (土石流 / 急傾斜 / 地すべり 3 種, 31 dataset) を読み込み →
      トンネル POINT を 200m バッファ → sjoin で重なり判定。
      <b>「トンネル坑口の土砂災害二重リスク」</b>として展開。</li>
</ul>

<h4>発展課題 3 (RQ3 由来): <b>橋梁 + トンネル 統合道路網</b>の連続性分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究は橋梁とトンネルを並列比較したが、
      <b>連続する 1 つの道路</b>に橋梁とトンネルがどう交互配置されているかは未分析。
      路線単位で「橋 - トンネル - 橋 - トンネル」 のパターンがあるはず。</li>
  <li><b>新仮説 Y</b>: 山岳道路では<b>橋梁:トンネル比 ~ 3:1</b>で交互配置、
      平野道路では<b>橋梁のみ (10:0 比)</b>のパターンが見える。</li>
  <li><b>課題 Z</b>: 路線名で橋梁 (L66) + トンネル (L67) を結合 →
      路線ごとに位置順に並べ、橋とトンネルの交互パターンをカウント →
      <b>「道路の連続性インデックス」</b>を定義し可視化。
      <b>「路線別道路インフラ連続性」</b>として展開。</li>
</ul>

<h4>発展課題 4 (RQ2 + L40 連携): <b>標高 × トンネル位置</b>の統合分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究はトンネルの平面位置のみ扱い、<b>標高</b>は未利用。
      高所トンネル (≥ 500m, 山頂峠越え) と低地トンネル (≤ 100m, 都市近郊) では
      建設目的・劣化要因が異なる。</li>
  <li><b>新仮説 Y</b>: 中山間 9 市町のトンネル平均標高は <b>≥ 300m</b>、
      平野市町は <b>≤ 100m</b>。標高別劣化要因は高所 = 凍結融解、低地 = 塩害。</li>
  <li><b>課題 Z</b>: L40 の<b>5m DEM</b>から各トンネル POINT の標高を抽出 →
      標高別に建設年度・延長・幅員ヒストグラム → 地形分類 × 標高で集計。
      <b>「トンネルの標高別劣化要因」</b>として展開。</li>
</ul>

<h4>発展課題 5 (RQ2 + L50 連携): <b>道路規制</b>とのトンネル交差</h4>
<ul>
  <li><b>結果 X</b>: 本研究はトンネル単独で扱い、道路規制 (L50 既知) との関係は未分析。
      老朽トンネルや構造的弱点を持つトンネルは規制対象になっている可能性。</li>
  <li><b>新仮説 Y</b>: 老朽トンネル ({n_old} 件) の <b>30% 以上</b>が何らかの道路規制下にある。
      規制内容は重量制限・通行止め・大型車両禁止等。</li>
  <li><b>課題 Z</b>: L50 の道路規制データから区間情報を取得 →
      トンネル位置と区間照合 → 規制率を計算。
      <b>「トンネル老朽化と道路規制の連動」</b>として展開。</li>
</ul>

<h4>発展課題 6 (RQ3 拡張): <b>判定区分 ("?") の解明</b></h4>
<ul>
  <li><b>結果 X</b>: 本データの「判定区分」 列は全件 "?" でマスクされ、健全度 I-IV の判定値は取れない。
      本研究では「築年数」 を健全度の代理指標として扱った (橋梁と同じ問題)。</li>
  <li><b>新仮説 Y</b>: 国交省 MICHI データベースや行政情報公開で健全度判定値を取得可能。
      架設年度 vs 健全度の単純な負相関ではなく、補修履歴で複雑になる仮説。</li>
  <li><b>課題 Z</b>: 国交省 MICHI データベース or 行政情報公開で健全度判定値を取得 →
      本データの建設年度 + 路線 + 場所と照合 → <b>築年数 vs 健全度の相関分析</b>。
      <b>「トンネル老朽化の本当の指標」</b>として展開 (橋梁の発展課題と並走)。</li>
</ul>

<h4>発展課題 7 (展望): <b>トンネル防災重要度ランキング</b>の構築</h4>
<ul>
  <li><b>結果 X</b>: 本研究では長大トンネル ({n_long} 件) を希少性で同定したが、
      <b>「災害時の代替性」</b>視点での評価は未実施。</li>
  <li><b>新仮説 Y</b>: 長大 (≥1000m) + 国道 + 老朽 (≤1974) のトンネルは<b>第一級防災重要</b>。
      これらを <b>3 重リスク</b>として優先補強対象に。本データから {n_long} 件中いくつかが該当するはず。</li>
  <li><b>課題 Z</b>: 派生フラグ (is_long + is_old + 道路種別=国道) で論理積 →
      第一級防災重要トンネルを同定 → 県の長寿命化計画と照合。
      <b>「トンネル防災重要度ランキング」</b>として展開。</li>
</ul>
"""

# 統合
sections = [
    ("学習目標と問い", sec1),
    ("使用データ", sec2),
    ("ダウンロード", sec3),
    (f"【RQ1】 トンネルの構造 — 国道 {kuni_share:.0f}% / "
     f"中央値 {df_raw['延長(m)'].median():.0f}m / Top 3 で {top3_city_share:.0f}%",
     sec4),
    (f"【RQ2】 山岳分断回避 — 中山間 {chusankan_share:.0f}% / "
     f"最多 {int(peak_decade)}s",
     sec5),
    (f"【RQ3】 L66 橋梁との二極構造 — 件数比 {ratio_count:.0f}:1 / "
     f"延長比 {ratio_len:.0f}:1",
     sec6),
    ("仮説検証総合", sec7),
    ("発展課題", sec8),
]


html = render_lesson(
    num=67,
    title=f"トンネル基本情報 単独 3 研究例分析 — "
          f"{n_total} 件から県の山岳インフラを読む",
    tags=["L67", "トンネル", "道路トンネル", "国道", "県道",
          "RQ×3", "Format B", "geopandas", "POINT (CSV)",
          "中山間", "山岳バイパス", "二極構造",
          "L66連携 (橋梁)", "合併市町正規化"],
    time="50 分",
    level="中級",
    data_label=f"DoBoX dataset {DATASET_ID} (CSV, ~{csv_path.stat().st_size/1024:.0f} KB)",
    sections=sections,
    script_filename="L67_tunnels.py",
)

OUT_HTML = LESSONS / "L67_tunnels.html"
OUT_HTML.write_text(html, encoding="utf-8")

print(f"  HTML: {OUT_HTML.name} ({len(html):,} chars)")
print(f"  ({time.time()-t10:.1f}s)", flush=True)


# =============================================================================
# 終了
# =============================================================================
print(f"\n=== 完了 (合計 {time.time()-t_all:.1f} 秒) ===", flush=True)
