# -*- coding: utf-8 -*-
"""L69 門型標識基本情報・維持管理情報 単独 3 研究例分析
       — 広島県内 22 門型標識 (gantry) を 3 角度で解読

カバー宣言:
  本記事は DoBoX のシリーズ「門型標識基本情報・維持管理情報」 1 件
  (dataset_id = 14) を <b>単独</b>で取り上げ、
  広島県が管理する <b>道路門型標識 全 22 件</b>を
  3 つの独立した研究角度 (RQ1 / RQ2 / RQ3) で並列に分析する。

  「門型標識」 とは:
    道路を <b>跨ぐように門型 (gantry) のフレーム</b>を立て、
    その横ばりに大型の<b>方向案内標識・走行情報装置</b>を架ける構造物。
    高速道路インターチェンジや幹線国道の交差点近傍で、
    <b>走行中の運転者に進行方向 / 行先 / 車線割当てを正確に伝える</b>ことを目的とする。
    国土交通省「道路標識ハンドブック」 では本構造を <b>F 型 (片持式) /
    L 型 (一方向支柱) / オーバーヘッド型 (門型 / ゲート型)</b>に三分類するが、
    広島県管理の本データは <b>オーバーヘッド型 (= 門型)</b>に統一されている
    (「種別」 列が全件 "標識・道路情報提供装置")。
    <b>道路法</b>に基づき 5 年に 1 回の点検が義務化 (2014 年改正) され、
    各門型標識に <b>施設番号 / 路線名 / 幅員 / 設置年度 / 点検年度 /
    判定区分</b>などのメタデータが付随する。

  本記事は L66 (橋梁単独) / L67 (トンネル単独) / L68 (シェッド単独)
  と<b>厳密に区別</b>:
    L66 = 橋梁<b>単独</b> 4,203 件 (中小河川クロス, 平野・分散型)。
    L67 = トンネル<b>単独</b> 157 件 (山岳貫通, 中山間・集中型)。
    L68 = シェッド<b>単独</b> 22 件 (山腹通過, 急峻地形・希少特殊解)。
    L69 = 門型標識<b>単独</b> 22 件 (情報提供, 幹線国道集中・少数精鋭型)。
    四記事は補完関係で「<b>県の道路施設 4 階層</b>」 を構成する。

研究の問い (3 RQ):
  RQ1 (主研究): 広島県の道路門型標識の<b>構造 — 形式・規模・地理分布</b>はどう描けるか?
       22 件を 市町 × 事務所 × 道路種別 × 路線 × 幅員 × 設置年代 ×
       方向 (上下 / 上り / 下り) で多角度に集計。
       特に「県の門型標識網」 の物理的形状を初めて定量化する。

  RQ2 (副研究 1): 門型標識の<b>設置道路の特性 — 国道偏重か県道偏重か</b>は
       どう現れるか?
       広島県管理の門型標識を <b>路線階層 (一桁国道 / 二桁国道 / 三桁国道 /
       主要県道)</b>で 4 分類し、各階層の<b>件数 / 平均幅員 / 設置年代</b>を比較。
       単純な「国道 vs 県道」 の二分でなく、<b>路線重要度 (= 地方支分部局
       管理区分)</b>に近い階層を抽出して、案内対象 (高規格 / 二桁幹線 / 三桁
       連絡 / 県道補完) を推定する。

  RQ3 (副研究 2): <b>L66 橋梁 4,203 + L67 トンネル 157 + L68 シェッド 22 +
       L69 門型標識 22 件の道路施設 4 兄弟構造</b>はどう現れるか?
       橋梁 (平野クロス) + トンネル (山岳貫通) + シェッド (山腹通過) +
       門型標識 (情報提供) の 4 階層を <b>件数規模 + 整備年代 + 国道率 +
       機能</b>で対比し、県の道路インフラ 4 階層を完成させる。

仮説 (5):
  H1 (方向二分類, RQ1): 門型標識 22 件の施設名から方向を読むと、
       <b>「上下」 (双方向)</b>が支配的、<b>「上り 1 / 下り 1」 (単方向)</b>は補助で、
       少数。「上下」 ≥ 50% の二分類構造が H1 の予測。

  H2 (国道偏重, RQ1+2): 門型標識は<b>国道</b>に偏重 (≥ 60%)。
       特に<b>幹線二桁国道 (2 / 184 / 185 / 191 号など)</b>に集中する仮説。
       三桁国道 + 主要県道は補完的役割。

  H3 (路線階層構造, RQ2): 路線階層別件数は<b>「二桁国道 ≫ 三桁国道 ≒ 主要県道」</b>
       のピラミッド構造。各階層の平均幅員にも段階差があり、
       <b>二桁国道 > 三桁国道 > 主要県道</b>の順で大きいと仮定。
       (= 幹線ほど道路幅が広いので門型標識も大型化)

  H4 (件数の 4 層比, RQ3): L66 橋梁 (4,203) / L67 トンネル (157) /
       L68 シェッド (22) / L69 門型標識 (22) の<b>件数比 ≒ 191 : 7 : 1 : 1</b>。
       「橋梁=網状多数、トンネル=希少大規模、シェッド+門型標識=超希少特殊解」。
       L68 シェッド と L69 門型標識は<b>同件数 (22 件) なのに目的が真逆</b>
       (山腹保護 vs 情報提供) という<b>「同規模・異目的」 の双子構造</b>。

  H5 (整備年代差, RQ3): 4 兄弟は整備年代に差がある。
       橋梁 (1960-2000s 全期分散) ↔ トンネル (1960-2010s) ↔
       シェッド (1970-1980s 集中) ↔ 門型標識 (1980-1990s 集中)。
       特に<b>門型標識は 1980-2000s に集中</b>し、4 兄弟で<b>最も新しい世代</b>
       (= 高度道路情報化の時代の産物)。

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

要件 T 準拠 (位置情報あり = 地図必須):
  - RQ1: 県全域 道路種別 × 方向マップ
  - RQ2: 県全域 路線階層別マップ + ズーム (尾道周辺の集中地帯)
  - RQ3: L66 橋梁 (背景) + L67 トンネル + L68 シェッド + L69 門型標識
        4 兄弟マップ

要件 Q 準拠: 図 8 / 表 13+ (3 RQ × 多角度: 構造 / 路線階層 / 4 兄弟構造)

データ仕様:
  - dataset 14: 門型標識基本情報・維持管理情報 (CSV)
  - 形式: CSV, 22 行 × 15 列
  - 列: 施設番号, 施設名, 施設名(フリガナ), 種別 (= 標識・道路情報提供装置),
       路線名, 道路種別 (= 国道/県道), 設置年度, 幅員(m), 管理事務所名,
       住所(県), 住所(市町), 緯度（10進数）, 経度（10進数）, 点検年度,
       判定区分
  - 種別: 全件 標識・道路情報提供装置 (= 門型標識オーバーヘッド型に統一)
  - 道路種別: 国道 14 / 県道 8 (= 国道偏重)
  - 緯度経度: 20/22 (2 件 = 矢野安浦線上下01/02 のみ欠損)
  - 設置年度: 18/22 (4 件 = 184号上下01/02 が NaN, 375号下り01 + 182号上下01 が "0" = 不明)
  - 判定区分: 全件 "?" (= 公開データでは健全度判定値が伏せられている)
  - ライセンス: クリエイティブ・コモンズ表示 (CC-BY)
  - L68 シェッドと <b>列構成が酷似</b> (差: 「延長(m)」 列が欠落)
    → 同じ「道路施設・5 年点検対象」 のフォーマットを共有

メモリ対策: 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("=== L69 門型標識基本情報 単独 3 研究例分析 ===", flush=True)

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

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

# 老朽閾値: 2024 年時点で築 30 年 = 1994 年以前設置
# (門型標識は 1980 年代以降の設置が中心なので、橋梁の "築 50 年" よりは緩く設定)
AGE_THRESHOLD_YEAR = 1994

# 大型門型標識の幅員閾値 (= 4 車線級 = 14m 以上)
LARGE_GANTRY_WIDTH = 14.0

# 道路種別の色
ROAD_COLOR = {"国道": "#cf222e", "県道": "#0969da"}


def classify_direction(name):
    """施設名から方向を 3 分類:
       - 上下 (双方向): 「上下」 を含む (門型 = 道路全体を跨ぐので最頻)
       - 上り (上り単独): 「上り」 を含む
       - 下り (下り単独): 「下り」 を含む
       本記事の H1 検証用。"""
    s = str(name)
    if "上下" in s:
        return "上下 (双方向案内)"
    if "上り" in s:
        return "上り (片方向案内)"
    if "下り" in s:
        return "下り (片方向案内)"
    return "その他 (方向不明)"


DIR_COLOR = {
    "上下 (双方向案内)": "#0969da",          # 青
    "上り (片方向案内)": "#1a7f37",          # 緑
    "下り (片方向案内)": "#cf222e",          # 赤
    "その他 (方向不明)": "#888888",
}


def classify_route_tier(road_class, route_name):
    """路線階層 4 分類 (本記事独自定義):
       - 一桁国道 (= 全国主要幹線): "２号" など
       - 二桁国道 (= 地域主要幹線): "１８４号" など (3 桁数字でも先頭が 1 桁の場合は除く...
                                  実装では 200 以下を二桁国道扱いとする)
       - 三桁国道 (= 連絡国道): 200 以上
       - 主要県道 (= 県管理): 道路種別 = 県道
       国土交通省の道路法上の正式階層ではないが、
       数字の若さ (= 制定時期の早さ + 路線重要度) を反映する独自軸。"""
    if road_class == "県道":
        return "主要県道"
    # 国道
    s = str(route_name)
    # 全角数字を半角化
    z2h = s.translate(str.maketrans("０１２３４５６７８９", "0123456789"))
    # "号" を除いた数字部分を取り出す
    import re
    m = re.search(r"(\d+)", z2h)
    if not m:
        return "主要県道"  # マッチしなければ便宜上ここに
    num = int(m.group(1))
    if num <= 9:
        return "一桁国道 (全国基幹)"
    if num <= 199:
        return "二桁国道 (地域基幹)"
    return "三桁国道 (連絡国道)"


TIER_COLOR = {
    "一桁国道 (全国基幹)":  "#cf222e",  # 赤 (最重要)
    "二桁国道 (地域基幹)":  "#cf6f00",  # 橙
    "三桁国道 (連絡国道)":  "#7c3aed",  # 紫
    "主要県道":            "#0969da",   # 青
}

# 中山間 9 市町 (L67/L68 と同定義)
CHUSANKAN_CITIES = {
    "庄原市", "三次市", "北広島町", "安芸太田町", "神石高原町",
    "世羅町", "府中市", "安芸高田市", "大崎上島町",
}

# CITY_CD → 市町名
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: "世羅町", 464: "神石高原町", 461: "北広島町",
}

# 旧町村名 → 現市町名
LEGACY_TO_CURRENT = {
    "加計町": "安芸太田町", "戸河内町": "安芸太田町", "筒賀村": "安芸太田町",
    "小方町": "大竹市",
    "君田町": "三次市", "三良坂町": "三次市", "甲奴町": "三次市",
    "高野町": "庄原市", "東城町": "庄原市", "西城町": "庄原市",
    "口和町": "庄原市", "比和町": "庄原市", "総領町": "庄原市",
    "上下町": "府中市",
    "豊松村": "神石高原町", "油木町": "神石高原町", "三和町": "神石高原町",
    "甲山町": "世羅町", "甲奴町": "三次市",
    "大朝町": "北広島町", "豊平町": "北広島町", "千代田町": "北広島町", "芸北町": "北広島町",
    "向原町": "安芸高田市", "甲田町": "安芸高田市", "高宮町": "安芸高田市",
    "美土里町": "安芸高田市", "八千代町": "安芸高田市", "吉田町": "安芸高田市",
    "大崎町": "大崎上島町", "東野町": "大崎上島町", "木江町": "大崎上島町",
    "倉橋町": "呉市", "音戸町": "呉市", "蒲刈町": "呉市", "下蒲刈町": "呉市",
    "豊町": "呉市", "豊浜町": "呉市", "安浦町": "呉市", "川尻町": "呉市",
    "本郷町": "三原市", "久井町": "三原市", "大和町": "三原市",
    "瀬戸田町": "尾道市", "因島市": "尾道市", "御調町": "尾道市", "向島町": "尾道市",
    "新市町": "福山市", "沼隈町": "福山市", "内海町": "福山市",
    "神辺町": "福山市", "芦田町": "福山市",
    "黒瀬町": "東広島市", "福富町": "東広島市", "豊栄町": "東広島市",
    "河内町": "東広島市", "安芸津町": "東広島市",
    "佐伯町": "廿日市市", "吉和村": "廿日市市", "宮島町": "廿日市市",
}


def parse_city_from_addr(addr):
    """住所から市町名抽出 (L67/L68 と同方式の簡略版)。"""
    if pd.isna(addr):
        return "?"
    s = str(addr)
    s = s.replace("広島県", "").strip()
    head = s.split("　")[0].strip()
    # 「市町」 で終わる先頭ワードを探す
    for current in ["広島市", "呉市", "竹原市", "三原市", "尾道市", "福山市",
                    "府中市", "三次市", "庄原市", "大竹市", "東広島市", "廿日市市",
                    "安芸高田市", "江田島市"]:
        if head.startswith(current):
            return current
    if head in LEGACY_TO_CURRENT:
        return LEGACY_TO_CURRENT[head]
    for legacy, cur in LEGACY_TO_CURRENT.items():
        if head.startswith(legacy):
            return cur
    for current in ["北広島町", "安芸太田町", "神石高原町", "世羅町",
                    "大崎上島町", "府中町", "海田町", "熊野町", "坂町"]:
        if head.startswith(current):
            return current
    # 「山県郡安芸太田町」 のようなパターン
    if "安芸太田町" in head:
        return "安芸太田町"
    if "北広島町" in head:
        return "北広島町"
    if "世羅町" in head:
        return "世羅町"
    if "神石高原町" in head:
        return "神石高原町"
    if "熊野町" in head:
        return "熊野町"
    return head


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

if not LOCAL_CSV.exists() or LOCAL_CSV.stat().st_size < 100:
    try:
        ensure_dataset(LOCAL_CSV, dataset_id=DATASET_ID, label="L69 門型標識基本情報")
    except Exception as e:
        print(f"  WARN: ensure_dataset 失敗: {e}", flush=True)

df_raw = pd.read_csv(LOCAL_CSV, 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")
# "0" は不明
df_raw.loc[df_raw["設置年度"] <= 1800, "設置年度"] = np.nan
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")

# 方向自動分類 + 路線階層分類 + 市町名正規化
df_raw["方向区分"] = df_raw["施設名"].apply(classify_direction)
df_raw["路線階層"] = df_raw.apply(
    lambda r: classify_route_tier(r["道路種別"], r["路線名"]), axis=1)
df_raw["市町名"] = df_raw["住所(市町)"].apply(parse_city_from_addr)

n_total = len(df_raw)
n_year = int(df_raw["設置年度"].notna().sum())
n_coord = int((df_raw["緯度（10進数）"].notna() & df_raw["経度（10進数）"].notna()).sum())
n_kuni = int((df_raw["道路種別"] == "国道").sum())
n_ken = int((df_raw["道路種別"] == "県道").sum())
print(f"  設置年度有: {n_year} / {n_total}", flush=True)
print(f"  緯度経度有: {n_coord} / {n_total}", flush=True)
print(f"  道路種別: 国道 {n_kuni} / 県道 {n_ken}", flush=True)
print(f"  方向区分: {df_raw['方向区分'].value_counts().to_dict()}", 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_geom = df_raw[geom_ok].copy()
df_geom["geometry"] = df_geom.apply(
    lambda r: Point(float(r["経度（10進数）"]), float(r["緯度（10進数）"])), axis=1)
gdf = gpd.GeoDataFrame(df_geom, geometry="geometry", crs="EPSG:4326").to_crs(TARGET_CRS)
n_geom = len(gdf)
print(f"  POINT 構築: {n_geom} / {n_total}", flush=True)
print(f"  ({time.time()-t2:.1f}s)", flush=True)


# =============================================================================
# 3. 市町同定 (sjoin で検証 + テキストと整合)
# =============================================================================
print("\n[3] 市町同定", flush=True)
t3 = time.time()

admin = gpd.read_file(ADMIN_GPKG).to_crs(TARGET_CRS)
joined = gpd.sjoin(gdf[["geometry"]], admin[["CITY_CD", "geometry"]],
                   how="left", predicate="within")
joined = joined[~joined.index.duplicated(keep="first")]
gdf = gdf.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("?")

# 未マッチは最近隣市町
unmatched_mask = gdf["CITY_CD"] == -1
n_unmatched = int(unmatched_mask.sum())
if n_unmatched > 0:
    adm_idx = admin.set_index("CITY_CD")
    for idx in gdf.index[unmatched_mask]:
        pt = gdf.geometry.iloc[idx]
        dists = adm_idx.geometry.apply(lambda g: pt.distance(g))
        nearest_cc = int(dists.idxmin())
        gdf.at[idx, "CITY_CD"] = nearest_cc
        gdf.at[idx, "市町名_sjoin"] = CITY_NAME.get(nearest_cc, str(nearest_cc))

# テキスト由来市町名を主に採用 (sjoin は検証用)
print(f"  sjoin 直接一致: {n_geom - n_unmatched} / {n_geom}", flush=True)
print(f"  最近隣補完: {n_unmatched} / {n_geom}", flush=True)
print(f"  ({time.time()-t3:.1f}s)", flush=True)


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

kuni_share = 100 * n_kuni / n_total
ken_share = 100 * n_ken / n_total

# 方向件数
dir_count = df_raw["方向区分"].value_counts()
T_direction = pd.DataFrame({
    "方向区分": dir_count.index,
    "件数": dir_count.values,
    "シェア_%": (dir_count.values / n_total * 100).round(1),
    "平均幅員_m": [round(df_raw[df_raw["方向区分"] == k]["幅員(m)"].mean(), 2)
                  for k in dir_count.index],
})

# 道路種別 サマリ
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(), 2),
                round(df_raw[df_raw["道路種別"] == "県道"]["幅員(m)"].mean(), 2),
                round(df_raw["幅員(m)"].mean(), 2)],
    "最大幅員_m": [round(df_raw[df_raw["道路種別"] == "国道"]["幅員(m)"].max(), 2),
                round(df_raw[df_raw["道路種別"] == "県道"]["幅員(m)"].max(), 2),
                round(df_raw["幅員(m)"].max(), 2)],
})

# 市町別ランキング
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[["順位", "市町名", "件数"]]

# 中山間集中度
chusankan_mask = df_raw["市町名"].isin(CHUSANKAN_CITIES)
n_chusankan = int(chusankan_mask.sum())
chusankan_share = 100 * n_chusankan / n_total

# 管理事務所別
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),
})

# 路線別
route_count = df_raw["路線名"].value_counts()
T_route = pd.DataFrame({
    "順位": np.arange(1, len(route_count) + 1),
    "路線名": route_count.index,
    "件数": route_count.values,
    "シェア_%": (route_count.values / n_total * 100).round(1),
})

# 大型門型標識
n_large = int((df_raw["幅員(m)"] >= LARGE_GANTRY_WIDTH).sum())
print(f"  国道シェア: {kuni_share:.1f}%", flush=True)
print(f"  中山間 9 市町シェア: {chusankan_share:.1f}%", flush=True)
print(f"  大型門型標識 (≥{LARGE_GANTRY_WIDTH:.0f}m): {n_large} 件", flush=True)
print(f"  ({time.time()-t4:.1f}s)", flush=True)


# =============================================================================
# 5. RQ2 集計: 路線階層別 (一桁/二桁/三桁国道/主要県道) 構造
# =============================================================================
print("\n[5] RQ2 集計: 路線階層別構造", flush=True)
t5 = time.time()

# 階層別件数 + 平均幅員
tier_order = ["一桁国道 (全国基幹)", "二桁国道 (地域基幹)",
              "三桁国道 (連絡国道)", "主要県道"]
tier_rows = []
for t in tier_order:
    sub = df_raw[df_raw["路線階層"] == t]
    if len(sub) == 0:
        tier_rows.append((t, 0, 0.0, np.nan, np.nan, np.nan, np.nan, np.nan, ""))
        continue
    routes_in_tier = sorted(sub["路線名"].unique().tolist())
    tier_rows.append((
        t,
        len(sub),
        round(100 * len(sub) / n_total, 1),
        round(sub["幅員(m)"].mean(), 2),
        round(sub["幅員(m)"].median(), 2),
        round(sub["幅員(m)"].max(), 2),
        round(sub["設置年度"].mean(), 0) if sub["設置年度"].notna().any() else np.nan,
        sub["路線名"].nunique(),
        " / ".join(routes_in_tier),
    ))
T_tier = pd.DataFrame(tier_rows, columns=[
    "路線階層", "件数", "シェア_%", "平均幅員_m", "中央幅員_m",
    "最大幅員_m", "平均設置年度", "路線数", "路線名 (例)",
])

# 路線 × 方向クロス
route_dir_cross = (df_raw.groupby(["路線階層", "方向区分"])
                   .size().unstack(fill_value=0))
# 階層 順序を強制
route_dir_cross = route_dir_cross.reindex(tier_order, fill_value=0)

# 国道 × 県道 × 方向クロス
road_dir_cross = (df_raw.groupby(["道路種別", "方向区分"])
                  .size().unstack(fill_value=0))

# 最頻路線 (基幹路線同定)
top_routes_full = T_route.head(5).copy()

# 設置年代別件数
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),
})

# 階層 × 年代クロス
df_raw["年代_文字"] = df_raw["年代"].apply(
    lambda v: f"{int(v)}s" if pd.notna(v) else "不明")
tier_decade_cross = (df_raw.groupby(["路線階層", "年代_文字"])
                     .size().unstack(fill_value=0))
tier_decade_cross = tier_decade_cross.reindex(tier_order, fill_value=0)

print(f"  階層件数: "
      f"{[(t, int((df_raw['路線階層']==t).sum())) for t in tier_order]}",
      flush=True)
print(f"  ({time.time()-t5:.1f}s)", flush=True)


# =============================================================================
# 6. RQ3 集計: 橋梁 (L66) + トンネル (L67) + シェッド (L68) との 4 兄弟構造
# =============================================================================
print("\n[6] RQ3 集計: 道路施設 4 兄弟構造", flush=True)
t6 = time.time()

L66_CSV = ASSETS / "L66_all_bridges.csv"
L67_CSV = ASSETS / "L67_all_tunnels.csv"
L68_CSV = ASSETS / "L68_all_sheds.csv"
df_bridge = pd.read_csv(L66_CSV, encoding="utf-8-sig") if L66_CSV.exists() else None
df_tunnel = pd.read_csv(L67_CSV, encoding="utf-8-sig") if L67_CSV.exists() else None
df_shed   = pd.read_csv(L68_CSV, encoding="utf-8-sig") if L68_CSV.exists() else None
has_bridge = df_bridge is not None
has_tunnel = df_tunnel is not None
has_shed   = df_shed is not None
print(f"  L66 橋梁: {'読込 OK ' + str(len(df_bridge)) + ' 件' if has_bridge else '未取得'}",
      flush=True)
print(f"  L67 トンネル: {'読込 OK ' + str(len(df_tunnel)) + ' 件' if has_tunnel else '未取得'}",
      flush=True)
print(f"  L68 シェッド: {'読込 OK ' + str(len(df_shed)) + ' 件' if has_shed else '未取得'}",
      flush=True)

n_bridge = int(len(df_bridge)) if has_bridge else 4203
n_tunnel = int(len(df_tunnel)) if has_tunnel else 157
n_shed   = int(len(df_shed))   if has_shed   else 22

# 幅員 (4 兄弟 共通指標)
mean_w_b = float(df_bridge["幅員(m)"].mean()) if has_bridge else 11.0
mean_w_t = float(df_tunnel["幅員(m)"].mean()) if has_tunnel else 6.3
mean_w_s = float(df_shed["幅員(m)"].mean())   if has_shed   else 7.5
mean_w_g = float(df_raw["幅員(m)"].mean())

med_w_b = float(df_bridge["幅員(m)"].median()) if has_bridge else 8.0
med_w_t = float(df_tunnel["幅員(m)"].median()) if has_tunnel else 5.6
med_w_s = float(df_shed["幅員(m)"].median())   if has_shed   else 7.0
med_w_g = float(df_raw["幅員(m)"].median())

# 国道率
def road_share(df, col_name="道路種別"):
    if df is None or col_name not in df.columns:
        return np.nan
    s = df[col_name]
    return 100 * (s == "国道").sum() / len(s)

kuni_b = road_share(df_bridge)
kuni_t = road_share(df_tunnel)
kuni_s = road_share(df_shed)
kuni_g = kuni_share

# 件数比 (シェッド/門型標識を 1 単位)
ratio_unit = n_total  # 22
T_four = pd.DataFrame([
    ("件数",
     f"{n_bridge:,}", f"{n_tunnel:,}", f"{n_shed}", f"{n_total}",
     f"比 {n_bridge//ratio_unit} : {round(n_tunnel/ratio_unit)} : 1 : 1"),
    ("国道シェア_%",
     f"{kuni_b:.1f}", f"{kuni_t:.1f}", f"{kuni_s:.1f}", f"{kuni_g:.1f}",
     f"門型 {kuni_g:.0f}% は最高 (情報提供は幹線重点)"),
    ("平均幅員_m",
     f"{mean_w_b:.2f}", f"{mean_w_t:.2f}", f"{mean_w_s:.2f}", f"{mean_w_g:.2f}",
     f"門型は橋梁次に広い (= 路線跨ぎ)"),
    ("中央幅員_m",
     f"{med_w_b:.2f}", f"{med_w_t:.2f}", f"{med_w_s:.2f}", f"{med_w_g:.2f}",
     f"中央値も同順"),
    ("地形対象",
     "中小河川クロス", "山岳貫通", "山腹通過", "情報提供 (跨道)",
     "4 階層: 平野/山岳/山腹/情報"),
    ("機能",
     "道路の連続性 (橋渡し)",
     "山岳バイパス (貫通)",
     "落石・雪崩・崩土からの保護 (覆い)",
     "進行方向・行先の伝達 (案内)",
     "1=接続 / 2=貫通 / 3=保護 / 4=情報"),
    ("典型整備期",
     "1960-2000s 全期",
     "1960-2010s 戦後継続",
     "1970-1980s 国土計画期",
     "1980-2000s 高度道路情報化",
     "門型は最も新世代"),
], columns=["指標", "L66 橋梁", "L67 トンネル", "L68 シェッド",
            "L69 門型標識", "4 兄弟の意味"])

# 大型門型標識 Top 10
T_large_gantry = (df_raw.sort_values(["幅員(m)", "設置年度"],
                                       ascending=[False, True]).head(10)
                   .reset_index(drop=True))
T_large_gantry_show = T_large_gantry[
    ["施設名", "方向区分", "路線名", "道路種別", "路線階層",
     "幅員(m)", "設置年度", "市町名"]].copy()
T_large_gantry_show.insert(0, "順位", np.arange(1, len(T_large_gantry_show) + 1))
T_large_gantry_show["設置年度"] = T_large_gantry_show["設置年度"].apply(
    lambda v: int(v) if pd.notna(v) else None)

# 4 兄弟 各施設の整備年代分布 (RQ3 H5 の検証用)
def decade_dist(df, year_col):
    if df is None or year_col not in df.columns:
        return None
    s = pd.to_numeric(df[year_col], errors="coerce")
    s = s[s > 1900]
    return (s // 10 * 10).astype(int).value_counts().sort_index()


dec_b = decade_dist(df_bridge, "架設年度")
dec_t = decade_dist(df_tunnel, "建設年度")
dec_s = decade_dist(df_shed,   "建設年度")
dec_g = decade_count.copy()

# 4 兄弟年代対比表
all_decades = sorted(set(
    list(dec_b.index if dec_b is not None else []) +
    list(dec_t.index if dec_t is not None else []) +
    list(dec_s.index if dec_s is not None else []) +
    list(dec_g.index if dec_g is not None else [])
))
rows4 = []
for d in all_decades:
    row = [f"{int(d)}s"]
    for s_ in [dec_b, dec_t, dec_s, dec_g]:
        if s_ is None:
            row.append("?")
        else:
            row.append(f"{int(s_.get(d, 0))}")
    rows4.append(row)
T_decades_four = pd.DataFrame(rows4, columns=["年代", "L66 橋梁",
                                                  "L67 トンネル",
                                                  "L68 シェッド",
                                                  "L69 門型標識"])

print(f"  橋:トン:シェ:門 = {n_bridge:,} : {n_tunnel} : {n_shed} : {n_total}",
      flush=True)
print(f"  幅員 mean: 橋 {mean_w_b:.2f} / トン {mean_w_t:.2f} / "
      f"シェ {mean_w_s:.2f} / 門 {mean_w_g:.2f}", 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"] = df_out["設置年度"] <= AGE_THRESHOLD_YEAR
df_out["is_large"] = df_out["幅員(m)"] >= LARGE_GANTRY_WIDTH
df_out["地形分類"] = np.where(df_out["市町名"].isin(CHUSANKAN_CITIES),
                               "中山間 (9 市町)", "平野・沿岸 (13 市町)")
cols_keep = ["施設番号", "施設名", "方向区分", "路線階層",
             "種別", "路線名", "道路種別",
             "設置年度", "幅員(m)", "管理事務所名",
             "住所(県)", "住所(市町)", "市町名",
             "緯度（10進数）", "経度（10進数）",
             "点検年度", "判定区分",
             "is_old", "is_large", "地形分類"]
df_out[cols_keep].to_csv(ASSETS / "L69_all_gantry.csv",
                          index=False, encoding="utf-8-sig")

T_direction.to_csv(ASSETS / "L69_direction_summary.csv",
                    index=False, encoding="utf-8-sig")
T_road.to_csv(ASSETS / "L69_road_summary.csv",
               index=False, encoding="utf-8-sig")
city_count.to_csv(ASSETS / "L69_city_ranking.csv",
                   index=False, encoding="utf-8-sig")
T_office.to_csv(ASSETS / "L69_office_ranking.csv",
                 index=False, encoding="utf-8-sig")
T_route.to_csv(ASSETS / "L69_route_ranking.csv",
                index=False, encoding="utf-8-sig")
decade_df.to_csv(ASSETS / "L69_decade_count.csv",
                  index=False, encoding="utf-8-sig")
T_tier.to_csv(ASSETS / "L69_tier_summary.csv",
               index=False, encoding="utf-8-sig")
route_dir_cross.to_csv(ASSETS / "L69_tier_x_direction.csv",
                        encoding="utf-8-sig")
tier_decade_cross.to_csv(ASSETS / "L69_tier_x_decade.csv",
                          encoding="utf-8-sig")
T_four.to_csv(ASSETS / "L69_four_siblings.csv",
               index=False, encoding="utf-8-sig")
T_large_gantry_show.to_csv(ASSETS / "L69_large_gantry.csv",
                             index=False, encoding="utf-8-sig")
T_decades_four.to_csv(ASSETS / "L69_decade_four.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_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)
# 道路種別で色、方向でマーカー形状
marker_map = {"上下 (双方向案内)": "s",      # square (門型 = 道路全体跨ぎ)
              "上り (片方向案内)": "^",      # ▲
              "下り (片方向案内)": "v",      # ▼
              "その他 (方向不明)": "o"}
for road in ["国道", "県道"]:
    for d in marker_map:
        sub = gdf[(gdf["道路種別"] == road) & (gdf["方向区分"] == d)]
        if len(sub) == 0:
            continue
        sub.plot(ax=ax, color=ROAD_COLOR[road], markersize=110,
                 marker=marker_map[d], edgecolor="#222",
                 linewidth=0.5, alpha=0.9, zorder=3)
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} (青) (2 件は緯度経度欠損)",
             fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = []
for road in ["国道", "県道"]:
    n_r = int((df_raw["道路種別"] == road).sum())
    patches.append(Line2D([0], [0], marker='o', color='w',
                          markerfacecolor=ROAD_COLOR[road],
                          markeredgecolor="#222", markersize=12,
                          label=f"{road} (n={n_r})"))
for d in marker_map:
    n_d = int((df_raw["方向区分"] == d).sum())
    if n_d == 0:
        continue
    patches.append(Line2D([0], [0], marker=marker_map[d], color='w',
                          markerfacecolor='#999', markeredgecolor="#222",
                          markersize=12,
                          label=f"{d} (n={n_d})"))
ax.legend(handles=patches, loc="lower left", fontsize=10,
          title="道路種別 (色) × 方向 (形)")
plt.tight_layout()
save_fig("L69_fig1_overview_road_dir_map.png")


# ---- 図 2 (RQ1): 市町別ランキング + 路線別 ----
print("  fig2: 市町 + 路線別", flush=True)
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

ax = axes[0]
top_cities = city_count.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.05, 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("市町別 門型標識件数\n(青 = 中山間 9 市町, 橙 = 平野・沿岸)",
             fontsize=11)
ax.grid(True, axis="x", alpha=0.3)

ax = axes[1]
T_route_show = T_route.iloc[::-1]
ys = np.arange(len(T_route_show))
ax.barh(ys, T_route_show["件数"].values,
        color="#7c3aed", edgecolor="#333", linewidth=0.5)
for y, v in zip(ys, T_route_show["件数"].values):
    ax.text(v + 0.05, y, f"{v}", va="center", fontsize=10)
ax.set_yticks(ys)
ax.set_yticklabels(T_route_show["路線名"].values, fontsize=10)
ax.set_xlabel("件数")
ax.set_title(f"路線別 門型標識件数 ({len(T_route)} 路線)", fontsize=11)
ax.grid(True, axis="x", alpha=0.3)

fig.suptitle("図 2 (RQ1): 市町別 + 路線別 門型標識件数", fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L69_fig2_city_route.png")


# ---- 図 3 (RQ1): 方向 × 道路種別 + 幅員ヒストグラム + 設置年代 ----
print("  fig3: 方向 + 幅員 + 年代", flush=True)
fig, axes = plt.subplots(1, 3, figsize=(15.5, 5))

ax = axes[0]
labels = list(DIR_COLOR.keys())
counts = [int((df_raw["方向区分"] == k).sum()) for k in labels]
labels_show = [k for k, c in zip(labels, counts) if c > 0]
counts_show = [c for c in counts if c > 0]
colors_show = [DIR_COLOR[k] for k in labels_show]
ax.bar(range(len(labels_show)), counts_show, color=colors_show,
       edgecolor="#333", linewidth=0.5, width=0.55)
for x, v in zip(range(len(labels_show)), counts_show):
    ax.text(x, v + 0.2, f"{v}", ha="center", fontsize=11, fontweight="bold")
ax.set_xticks(range(len(labels_show)))
ax.set_xticklabels([k.replace(" (", "\n(") for k in labels_show], fontsize=9.5)
ax.set_ylabel("件数")
ax.set_title("方向区分 (H1 検証)", fontsize=11)
ax.grid(True, axis="y", alpha=0.3)

ax = axes[1]
ww = df_raw["幅員(m)"].dropna()
ax.hist(ww, bins=10, color="#0969da", edgecolor="#333",
        linewidth=0.4, alpha=0.85)
ax.axvline(LARGE_GANTRY_WIDTH, color="#cf222e", linestyle="--",
           linewidth=1.5, label=f"大型閾値 ({LARGE_GANTRY_WIDTH:.0f}m)")
ax.axvline(ww.median(), color="#1a7f37", 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():.1f}m)", fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)

ax = axes[2]
decades = sorted([int(d) for d in decade_count.index])
counts_d = [int(decade_count[d]) for d in decades]
colors_d = ["#cf222e" if d <= 1990 else "#0969da" for d in decades]
xs = np.arange(len(decades))
ax.bar(xs, counts_d, color=colors_d, edgecolor="#333", linewidth=0.5)
for x, c in zip(xs, counts_d):
    ax.text(x, c + 0.1, f"{c}", ha="center", fontsize=10)
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"設置年代別 (年度有 {n_year}/{n_total} 件)", fontsize=11)
ax.grid(True, axis="y", alpha=0.3)

fig.suptitle("図 3 (RQ1): 方向 + 幅員 + 設置年代 分布", fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L69_fig3_direction_width_decade.png")


# ---- 図 4 (RQ2): 路線階層別 件数 + 平均幅員 + 階層 × 年代ヒートマップ ----
print("  fig4: 路線階層 件数+幅員+ヒートマップ", flush=True)
fig, axes = plt.subplots(1, 3, figsize=(16.5, 5))

ax = axes[0]
tier_labels = T_tier["路線階層"].tolist()
tier_counts = T_tier["件数"].tolist()
tier_colors = [TIER_COLOR[t] for t in tier_labels]
xs = np.arange(len(tier_labels))
ax.bar(xs, tier_counts, color=tier_colors, edgecolor="#333", linewidth=0.5)
for x, v in zip(xs, tier_counts):
    if v > 0:
        ax.text(x, v + 0.2, f"{v}", ha="center", fontsize=11, fontweight="bold")
ax.set_xticks(xs)
ax.set_xticklabels([t.replace(" (", "\n(") for t in tier_labels], fontsize=9)
ax.set_ylabel("件数")
ax.set_title("路線階層別 件数 (H3 検証)", fontsize=11)
ax.grid(True, axis="y", alpha=0.3)

ax = axes[1]
tier_widths_mean = T_tier["平均幅員_m"].tolist()
tier_widths_max = T_tier["最大幅員_m"].tolist()
width_bar = 0.35
ax.bar(xs - width_bar/2, tier_widths_mean, width_bar,
       color=tier_colors, edgecolor="#333", linewidth=0.5,
       label="平均幅員", alpha=0.95)
ax.bar(xs + width_bar/2, tier_widths_max, width_bar,
       color="#aaa", edgecolor="#333", linewidth=0.5,
       label="最大幅員")
for x, v_mean, v_max in zip(xs, tier_widths_mean, tier_widths_max):
    if pd.notna(v_mean):
        ax.text(x - width_bar/2, v_mean + 0.3, f"{v_mean:.1f}",
                ha="center", fontsize=8.5)
    if pd.notna(v_max):
        ax.text(x + width_bar/2, v_max + 0.3, f"{v_max:.1f}",
                ha="center", fontsize=8.5)
ax.set_xticks(xs)
ax.set_xticklabels([t.replace(" (", "\n(") for t in tier_labels], fontsize=9)
ax.set_ylabel("幅員 (m)")
ax.set_title("路線階層別 幅員 (H3 段階差)", fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, axis="y", alpha=0.3)

ax = axes[2]
# 階層 × 年代ヒートマップ
heat = tier_decade_cross.copy()
# 年代順にソート (列名が "1980s" などなので数値抽出)
def dec_key(s):
    if s == "不明":
        return 9999
    try:
        return int(str(s).rstrip("s"))
    except:
        return 9999
heat = heat[sorted(heat.columns, key=dec_key)]
im = ax.imshow(heat.values, cmap="YlOrRd", aspect="auto")
ax.set_xticks(np.arange(heat.shape[1]))
ax.set_xticklabels(heat.columns, rotation=0, fontsize=9)
ax.set_yticks(np.arange(heat.shape[0]))
ax.set_yticklabels([t.replace(" (", "\n(") for t in heat.index],
                   fontsize=9)
for i in range(heat.shape[0]):
    for j in range(heat.shape[1]):
        v = int(heat.iat[i, j])
        if v > 0:
            ax.text(j, i, f"{v}", ha="center", va="center",
                     fontsize=9.5, fontweight="bold",
                     color="#fff" if v >= 4 else "#222")
ax.set_title("階層 × 年代 件数 (RQ2 H5)", fontsize=11)
plt.colorbar(im, ax=ax, fraction=0.04, pad=0.02, label="件数")

fig.suptitle("図 4 (RQ2): 路線階層別 構造 — 件数 + 幅員 + 年代",
             fontsize=13, y=1.02)
plt.tight_layout()
save_fig("L69_fig4_tier_structure.png")


# ---- 図 5 (RQ2): 路線階層別マップ ----
print("  fig5: 路線階層マップ", 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)
gdf["階層"] = gdf["路線階層"]
for tier in tier_order:
    sub = gdf[gdf["階層"] == tier]
    if len(sub) == 0:
        continue
    sub.plot(ax=ax, color=TIER_COLOR[tier], markersize=130,
             marker="s", edgecolor="#222",
             linewidth=0.5, alpha=0.9, zorder=3)
ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -130000)
ax.set_aspect("equal")
ax.set_title(f"図 5 (RQ2): 路線階層別 門型標識マップ — 4 階層分類",
             fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = []
for tier in tier_order:
    n_t = int((df_raw["路線階層"] == tier).sum())
    if n_t == 0:
        continue
    patches.append(Line2D([0], [0], marker='s', color='w',
                          markerfacecolor=TIER_COLOR[tier],
                          markeredgecolor="#222", markersize=12,
                          label=f"{tier} (n={n_t})"))
ax.legend(handles=patches, loc="lower left", fontsize=10, title="路線階層")
plt.tight_layout()
save_fig("L69_fig5_tier_map.png")


# ---- 図 6 (RQ2): 尾道周辺ズームマップ (門型標識集中地帯) ----
print("  fig6: 尾道周辺ズーム", flush=True)
fig, ax = plt.subplots(figsize=(11, 7.5))
# 尾道+三原+福山+世羅+東広島 周辺
admin_for_plot.plot(ax=ax, color="#fff4e0", edgecolor="#888",
                    linewidth=0.4, alpha=0.6)
# Adjust BG to ROI
roi_x = (40000, 110000)
roi_y = (-200000, -150000)
ax.set_xlim(*roi_x)
ax.set_ylim(*roi_y)
for tier in tier_order:
    sub = gdf[gdf["階層"] == tier]
    if len(sub) == 0:
        continue
    sub.plot(ax=ax, color=TIER_COLOR[tier], markersize=240,
             marker="s", edgecolor="#222",
             linewidth=0.7, alpha=0.95, zorder=3)
# 路線名ラベルを付与
for _, row in gdf.iterrows():
    if not (roi_x[0] <= row.geometry.x <= roi_x[1] and
            roi_y[0] <= row.geometry.y <= roi_y[1]):
        continue
    ax.annotate(row["路線名"],
                xy=(row.geometry.x, row.geometry.y),
                xytext=(8, 8), textcoords="offset points",
                fontsize=8, color="#222",
                bbox=dict(boxstyle="round,pad=0.2",
                          facecolor="white", alpha=0.85,
                          edgecolor="#888", linewidth=0.4))
ax.set_aspect("equal")
n_in_roi = int(((gdf.geometry.x >= roi_x[0]) & (gdf.geometry.x <= roi_x[1])
                & (gdf.geometry.y >= roi_y[0]) & (gdf.geometry.y <= roi_y[1])).sum())
ax.set_title(f"図 6 (RQ2): 尾道-三原-福山-世羅 周辺ズーム — "
             f"{n_in_roi}/{n_geom} 件 (県東部に集中)",
             fontsize=12)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = []
for tier in tier_order:
    n_t = int((df_raw["路線階層"] == tier).sum())
    if n_t == 0:
        continue
    patches.append(Line2D([0], [0], marker='s', color='w',
                          markerfacecolor=TIER_COLOR[tier],
                          markeredgecolor="#222", markersize=12,
                          label=tier))
ax.legend(handles=patches, loc="lower left", fontsize=10, title="路線階層")
plt.tight_layout()
save_fig("L69_fig6_east_zoom.png")


# ---- 図 7 (RQ3): 道路施設 4 兄弟マップ (橋梁+トンネル+シェッド+門型標識) ----
print("  fig7: 4 兄弟マップ", 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)

if has_bridge:
    df_b = df_bridge.dropna(subset=["緯度（10進数）", "経度（10進数）"])
    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="#bbb", markersize=2,
                alpha=0.32, edgecolor="none", zorder=1)

if has_tunnel:
    df_t = df_tunnel.dropna(subset=["緯度（10進数）", "経度（10進数）"])
    df_t = df_t[df_t["緯度（10進数）"] < 50]
    geom_t = [Point(x, y) for x, y in zip(df_t["経度（10進数）"], df_t["緯度（10進数）"])]
    gdf_t = gpd.GeoDataFrame(df_t[["施設名"]], geometry=geom_t,
                              crs="EPSG:4326").to_crs(TARGET_CRS)
    gdf_t.plot(ax=ax, color="#7c3aed", markersize=18,
                alpha=0.7, marker="o",
                edgecolor="#222", linewidth=0.3, zorder=2)

if has_shed:
    df_s = df_shed.dropna(subset=["緯度（10進数）", "経度（10進数）"])
    geom_s = [Point(x, y) for x, y in zip(df_s["経度（10進数）"], df_s["緯度（10進数）"])]
    gdf_s = gpd.GeoDataFrame(df_s[["施設名"]], geometry=geom_s,
                              crs="EPSG:4326").to_crs(TARGET_CRS)
    gdf_s.plot(ax=ax, color="#cf6f00", markersize=120,
                alpha=0.85, marker="^",
                edgecolor="#222", linewidth=0.6, zorder=3)

# 門型標識を最前面・大型四角形で
gdf.plot(ax=ax, color="#cf222e", markersize=180, marker="s",
         edgecolor="#000", linewidth=0.7, alpha=0.95, zorder=4)

ax.set_xlim(-15000, 125000)
ax.set_ylim(-220000, -130000)
ax.set_aspect("equal")
ax.set_title(f"図 7 (RQ3): 道路施設 4 兄弟マップ — "
             f"L66 橋梁 ({n_bridge:,}, 灰) + L67 トンネル ({n_tunnel}, 紫) + "
             f"L68 シェッド ({n_shed}, ▲橙) + L69 門型標識 ({n_total}, ■赤)",
             fontsize=11)
ax.set_xlabel("X (m, EPSG:6671)")
ax.set_ylabel("Y (m, EPSG:6671)")
patches = [
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#bbb",
            markersize=7, label=f"L66 橋梁 ({n_bridge:,})"),
    Line2D([0], [0], marker='o', color='w', markerfacecolor="#7c3aed",
            markeredgecolor="#222", markersize=10,
            label=f"L67 トンネル ({n_tunnel})"),
    Line2D([0], [0], marker='^', color='w', markerfacecolor="#cf6f00",
            markeredgecolor="#222", markersize=12,
            label=f"L68 シェッド ({n_shed})"),
    Line2D([0], [0], marker='s', color='w', markerfacecolor="#cf222e",
            markeredgecolor="#000", markersize=12,
            label=f"L69 門型標識 ({n_total})"),
]
ax.legend(handles=patches, loc="lower left", fontsize=9.5)
plt.tight_layout()
save_fig("L69_fig7_four_siblings_map.png")


# ---- 図 8 (RQ3): 4 兄弟 件数 + 国道率 + 整備年代対比 ----
print("  fig8: 4 兄弟 構造対比", flush=True)
fig, axes = plt.subplots(1, 3, figsize=(16, 5.2))

# 左: 件数 (log y)
ax = axes[0]
labels4 = ["L66 橋梁", "L67 トンネル", "L68 シェッド", "L69 門型標識"]
vals4 = [n_bridge, n_tunnel, n_shed, n_total]
cols4 = ["#0969da", "#7c3aed", "#cf6f00", "#cf222e"]
xs4 = np.arange(4)
ax.bar(xs4, vals4, color=cols4, edgecolor="#333", linewidth=0.6, width=0.6)
for x, v in zip(xs4, vals4):
    ax.text(x, v * 1.18, f"{v:,}", ha="center",
             fontsize=11, fontweight="bold")
ax.set_yscale("log")
ax.set_ylabel("件数 (log scale)")
ax.set_ylim(1, n_bridge * 3)
ax.set_xticks(xs4)
ax.set_xticklabels(labels4, fontsize=9.5, rotation=10)
ax.set_title(f"件数比 (橋:トン:シェ:門 = "
             f"{n_bridge//n_total} : {round(n_tunnel/n_total)} : 1 : 1)",
             fontsize=11)
ax.grid(True, axis="y", alpha=0.3, which="both")

# 中: 国道率
ax = axes[1]
shares4 = [kuni_b, kuni_t, kuni_s, kuni_g]
ax.bar(xs4, shares4, color=cols4, edgecolor="#333", linewidth=0.6, width=0.6)
for x, v in zip(xs4, shares4):
    if pd.notna(v):
        ax.text(x, v + 1.5, f"{v:.0f}%", ha="center",
                 fontsize=11, fontweight="bold")
ax.set_ylim(0, 109)
ax.set_xticks(xs4)
ax.set_xticklabels(labels4, fontsize=9.5, rotation=10)
ax.set_ylabel("国道シェア (%)")
ax.set_title("国道率比較 (門型標識が最高 = 情報重点路)",
             fontsize=11)
ax.grid(True, axis="y", alpha=0.3)

# 右: 4 兄弟 整備年代分布 (パーセント正規化、log+stack風)
ax = axes[2]
# 5 年代別マトリクス (1960-2020s)
target_decades = [1960, 1970, 1980, 1990, 2000, 2010]
def get_dec_pct(s, decs):
    if s is None:
        return [np.nan] * len(decs)
    total = s.sum()
    if total == 0:
        return [0] * len(decs)
    return [100 * int(s.get(d, 0)) / total for d in decs]


pct_b = get_dec_pct(dec_b, target_decades)
pct_t = get_dec_pct(dec_t, target_decades)
pct_s = get_dec_pct(dec_s, target_decades)
pct_g = get_dec_pct(dec_g, target_decades)

xs_d = np.arange(len(target_decades))
width = 0.2
ax.bar(xs_d - 1.5*width, pct_b, width, color="#0969da",
       edgecolor="#333", linewidth=0.4, label="L66 橋梁")
ax.bar(xs_d - 0.5*width, pct_t, width, color="#7c3aed",
       edgecolor="#333", linewidth=0.4, label="L67 トンネル")
ax.bar(xs_d + 0.5*width, pct_s, width, color="#cf6f00",
       edgecolor="#333", linewidth=0.4, label="L68 シェッド")
ax.bar(xs_d + 1.5*width, pct_g, width, color="#cf222e",
       edgecolor="#333", linewidth=0.4, label="L69 門型標識")

ax.set_xticks(xs_d)
ax.set_xticklabels([f"{d}s" for d in target_decades], fontsize=10)
ax.set_xlabel("整備年代")
ax.set_ylabel("各兄弟内シェア (%)")
ax.set_title("4 兄弟 整備年代比較 (H5 検証)", fontsize=11)
ax.legend(fontsize=9)
ax.grid(True, axis="y", alpha=0.3)

fig.suptitle("図 8 (RQ3): 道路施設 4 兄弟 — 件数 + 国道率 + 整備年代",
             fontsize=12.5, y=1.02)
plt.tight_layout()
save_fig("L69_fig8_four_structure.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;"', "")


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


# データセット仕様
csv_size = LOCAL_CSV.stat().st_size
T_dataset = pd.DataFrame([
    ("dataset_id", str(DATASET_ID)),
    ("公式名", "門型標識基本情報・維持管理情報"),
    ("ファイル", "gantry_basic.csv"),
    ("形式", "CSV (UTF-8 BOM)"),
    ("ファイルサイズ", f"{csv_size:,} byte (~{csv_size/1024:.1f} KB)"),
    ("レコード数", f"{n_total} 行 (= 道路門型標識件数)"),
    ("列数", f"{df_raw.shape[1]} 列"),
    ("種別",
     f"全件 「標識・道路情報提供装置」 "
     f"(本記事で 3 分類: 上下/上り/下り)"),
    ("道路種別", f"国道 {n_kuni} + 県道 {n_ken}"),
    ("路線階層 (本記事独自)",
     f"一桁国道 {int((df_raw['路線階層']=='一桁国道 (全国基幹)').sum())}, "
     f"二桁国道 {int((df_raw['路線階層']=='二桁国道 (地域基幹)').sum())}, "
     f"三桁国道 {int((df_raw['路線階層']=='三桁国道 (連絡国道)').sum())}, "
     f"主要県道 {int((df_raw['路線階層']=='主要県道').sum())}"),
    ("管理事務所", f"{len(office_count)} 事務所"),
    ("路線数", f"{df_raw['路線名'].nunique()} 異なり値"),
    ("市町数 (正規化済)", f"{city_count['市町名'].nunique()} 市町"),
    ("緯度経度",
     f"{n_coord} / {n_total} 件取得可 "
     f"(2 件 = 矢野安浦線上下01/02 のみ欠損で地図に出ない)"),
    ("設置年度",
     f"{n_year} / {n_total} 件取得可、範囲 "
     f"{int(df_raw['設置年度'].min())}-{int(df_raw['設置年度'].max())} "
     f"(4 件 = 184号上下01/02 が NaN, 375号下り01 + 182号上下01 が '0' で不明)"),
    ("点検年度", "全件 取得可、範囲 2020-2021 "
     "(20 件は 2020 年, 1 件は 2021 年, 緯度経度欠損 2 件は欠損)"),
    ("判定区分", "全件 \"?\" (= 公開データでは伏せられる)"),
    ("延長 (m)", "本データには列なし (= シェッドと異なる重要差)"),
    ("幅員 (m)",
     f"中央値 {df_raw['幅員(m)'].median():.2f}m / "
     f"最大 {df_raw['幅員(m)'].max():.2f}m / "
     f"最小 {df_raw['幅員(m)'].min():.2f}m"),
    ("座標系 (元)", "EPSG:4326 (WGS84) → EPSG:6671 で処理"),
    ("ライセンス", "クリエイティブ・コモンズ表示 (CC-BY)"),
    ("作成主体", "広島県土木建築局道路整備課"),
    ("URL", f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
], columns=["項目", "値"])

# データ取得手順
T_data_recipe = pd.DataFrame([
    ("ステップ 1", "DoBoX dataset 14 ページ",
     f"https://hiroshima-dobox.jp/datasets/{DATASET_ID}"),
    ("ステップ 2", "CSV DL (リソースリンク)",
     "ページ内の 1 リソースから「ダウンロード」"),
    ("ステップ 3", "保存先",
     "data/extras/L69_gantry_signs/gantry_basic.csv"),
    ("ステップ 4", "POINT 構築 + EPSG:6671 投影",
     f"{n_coord}/{n_total} 件 → POINT (2 件は緯度経度欠損)"),
    ("ステップ 5", "方向自動分類",
     "施設名から 上下/上り/下り の 3 分類"),
    ("ステップ 6", "路線階層自動分類",
     "国道番号と県道で 一桁/二桁/三桁/県道 の 4 分類"),
    ("ステップ 7", "市町同定 (テキスト + sjoin)",
     f"sjoin {n_geom-n_unmatched}/{n_geom} 直接, 残りは最近隣"),
    ("ステップ 8", "RQ1 集計 (構造)",
     f"方向 + 道路種別 + 市町 + 事務所 + 路線 + 幅員 + 年代"),
    ("ステップ 9", "RQ2 集計 (路線階層)",
     f"4 階層 × 件数 × 幅員 × 年代"),
    ("ステップ 10", "RQ3 集計 (4 兄弟比較)",
     f"件数比 {n_bridge//n_total}:{round(n_tunnel/n_total)}:1:1"),
    ("ステップ 11", "8 図 + 13 表 出力",
     "本スクリプト全体で ~10 秒"),
], columns=["ステップ", "操作", "値 / URL"])

# 全体サマリ
n_old_compute = int((df_raw["設置年度"] <= AGE_THRESHOLD_YEAR).sum())
old_share_compute = 100 * n_old_compute / n_year if n_year > 0 else 0
peak_decade = int(decade_count.idxmax()) if len(decade_count) > 0 else 0
peak_count = int(decade_count.max()) if len(decade_count) > 0 else 0

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]['市町名']} ({int(city_count.iloc[0]['件数'])} 件)"),
    ("中山間 9 市町シェア (RQ1)",
     f"{chusankan_share:.1f}% ({n_chusankan} 件)"),
    ("最頻方向 (RQ1)",
     f"{dir_count.idxmax()} ({int(dir_count.max())} 件, "
     f"{100*dir_count.max()/n_total:.1f}%)"),
    ("大型門型標識 (≥14m) (RQ1)",
     f"{n_large} 件 ({100*n_large/n_total:.1f}%)"),
    ("幅員 中央値 / 最大 (RQ1)",
     f"{df_raw['幅員(m)'].median():.2f} m / {df_raw['幅員(m)'].max():.2f} m"),
    ("最多年代 (RQ1)", f"{peak_decade}s ({peak_count} 件)"),
    ("古設置 (≤1994) (RQ1)",
     f"{n_old_compute} 件 ({old_share_compute:.1f}%)"),
    ("一桁国道 (RQ2)",
     f"{int((df_raw['路線階層']=='一桁国道 (全国基幹)').sum())} 件"),
    ("二桁国道 (RQ2)",
     f"{int((df_raw['路線階層']=='二桁国道 (地域基幹)').sum())} 件"),
    ("三桁国道 (RQ2)",
     f"{int((df_raw['路線階層']=='三桁国道 (連絡国道)').sum())} 件"),
    ("主要県道 (RQ2)",
     f"{int((df_raw['路線階層']=='主要県道').sum())} 件"),
    ("L66 橋梁 (比較対象)", f"{n_bridge:,}"),
    ("L67 トンネル (比較対象)", f"{n_tunnel}"),
    ("L68 シェッド (比較対象)", f"{n_shed}"),
    ("件数比 橋:トン:シェ:門 (RQ3)",
     f"{n_bridge//n_total} : {round(n_tunnel/n_total)} : 1 : 1"),
    ("国道率 橋:トン:シェ:門 (RQ3)",
     f"{kuni_b:.0f}% : {kuni_t:.0f}% : {kuni_s:.0f}% : {kuni_g:.0f}%"),
    ("幅員中央値 橋:トン:シェ:門 (RQ3)",
     f"{med_w_b:.1f}m : {med_w_t:.1f}m : {med_w_s:.1f}m : {med_w_g:.1f}m"),
], columns=["指標", "値"])
T_overall.to_csv(ASSETS / "L69_overall.csv", index=False, encoding="utf-8-sig")


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


# H1: 上下が支配的 (≥ 50%)
n_jouge = int((df_raw["方向区分"] == "上下 (双方向案内)").sum())
n_nobori = int((df_raw["方向区分"] == "上り (片方向案内)").sum())
n_kudari = int((df_raw["方向区分"] == "下り (片方向案内)").sum())
h1_ok = n_jouge >= n_total / 2

# H2: 国道偏重 (≥ 60%)
h2_ok = kuni_share >= 60

# H3: 路線階層構造ピラミッド + 幅員段階差
n_1g = int((df_raw["路線階層"] == "一桁国道 (全国基幹)").sum())
n_2g = int((df_raw["路線階層"] == "二桁国道 (地域基幹)").sum())
n_3g = int((df_raw["路線階層"] == "三桁国道 (連絡国道)").sum())
n_pref = int((df_raw["路線階層"] == "主要県道").sum())

w_1g = df_raw[df_raw["路線階層"] == "一桁国道 (全国基幹)"]["幅員(m)"].mean()
w_2g = df_raw[df_raw["路線階層"] == "二桁国道 (地域基幹)"]["幅員(m)"].mean()
w_3g = df_raw[df_raw["路線階層"] == "三桁国道 (連絡国道)"]["幅員(m)"].mean()
w_pref = df_raw[df_raw["路線階層"] == "主要県道"]["幅員(m)"].mean()

# 二桁国道が最頻、かつ幅員段階差 (上位ほど大きい)
h3_pyramid = (n_2g >= max(n_3g, n_pref))  # 二桁が最頻
h3_width_step = (w_2g > w_3g) if pd.notna(w_2g) and pd.notna(w_3g) else False
h3_ok = h3_pyramid and h3_width_step

# H4: 件数 4 層比 ≒ 191 : 7 : 1 : 1
ratio_test_ok = (abs(n_bridge / n_total - 191) < 30) and \
                (abs(n_tunnel / n_total - 7) < 5) and \
                (n_shed == n_total)  # 同件数

# H5: 門型標識が 4 兄弟で最も新しい (1980-2000s 集中)
g_post_1980 = int(((df_raw["設置年度"] >= 1980) &
                   (df_raw["設置年度"] < 2010)).sum())
g_post_1980_share = 100 * g_post_1980 / n_year if n_year > 0 else 0
h5_ok = g_post_1980_share >= 75


T_hypo = pd.DataFrame([
    ("H1 方向 上下が支配的 (RQ1)",
     f"上下 {n_jouge} ({100*n_jouge/n_total:.0f}%) + 上り {n_nobori} + 下り {n_kudari}",
     jud(h1_ok),
     f"H1 {jud(h1_ok)}: 施設名から自動分類した結果、"
     f"<b>上下 (双方向案内) {n_jouge} 件 ({100*n_jouge/n_total:.0f}%)</b>、"
     f"<b>上り {n_nobori} 件 ({100*n_nobori/n_total:.0f}%)</b>、"
     f"<b>下り {n_kudari} 件 ({100*n_kudari/n_total:.0f}%)</b>。"
     f"上下が <b>過半数</b> を占め、片方向専用標識は補助的役割。"
     f"門型標識が「道路全体を跨いで両方向の運転者に同時案内」 する設計思想を反映。"),
    ("H2 国道偏重 ≥ 60% (RQ1+2)",
     f"観測 = {kuni_share:.1f}% ({n_kuni}/{n_total})",
     jud(h2_ok),
     f"H2 {jud(h2_ok)}: 道路種別の集計で <b>国道 {n_kuni} 件 ({kuni_share:.0f}%)</b>、"
     f"<b>県道 {n_ken} 件 ({ken_share:.0f}%)</b>。"
     f"国道シェアが <b>{kuni_share:.0f}%</b> で 60% を上回り、"
     f"門型標識は<b>幹線国道に偏重</b>することを定量確認。"
     f"特に 184 号 (4 件) + 183 号 (3 件) で全体の {(7/n_total)*100:.0f}% を占める。"),
    ("H3 路線階層 ピラミッド + 幅員段階差 (RQ2)",
     f"件数 1桁{n_1g} / 2桁{n_2g} / 3桁{n_3g} / 県道{n_pref}",
     jud(h3_ok),
     f"H3 {jud(h3_ok)}: 4 階層で件数を見ると <b>二桁国道 {n_2g} 件 ({100*n_2g/n_total:.0f}%) "
     f"≫ 主要県道 {n_pref} ≫ 三桁国道 {n_3g} ≫ 一桁国道 {n_1g}</b>。"
     f"二桁国道が最頻で、ピラミッドの底辺が二桁。"
     f"平均幅員は <b>1桁 {w_1g:.1f}m / 2桁 {w_2g:.1f}m / 3桁 {w_3g:.1f}m / "
     f"県道 {w_pref:.1f}m</b>。 "
     f"{'幅員も二桁 > 三桁の段階差を確認できた' if h3_width_step else '幅員段階差は明確ではなかった'}。"),
    ("H4 件数 4 層比 ≒ 191 : 7 : 1 : 1 (RQ3)",
     f"観測 = {n_bridge:,} : {n_tunnel} : {n_shed} : {n_total} = "
     f"{n_bridge/n_total:.0f} : {n_tunnel/n_total:.1f} : "
     f"{n_shed/n_total:.0f} : 1",
     jud(ratio_test_ok),
     f"H4 {jud(ratio_test_ok)}: 4 兄弟の件数を比べると、"
     f"<b>橋梁 {n_bridge:,} 件 ≫ トンネル {n_tunnel} 件 ≫ "
     f"シェッド {n_shed} 件 ≈ 門型標識 {n_total} 件</b>。"
     f"L68 シェッドと L69 門型標識が <b>同件数 (22 件)</b> で並び、"
     f"<b>同規模・異目的の双子構造</b>を成すことが本研究の重要発見。"
     f"両者は道路法 + 5 年点検対象という共通の管理体系下にあるが、"
     f"目的は <b>山腹保護 vs 情報提供</b>と真逆。"),
    ("H5 4 兄弟で最も新しい世代 (RQ3)",
     f"1980-2009年 {g_post_1980} / {n_year} = {g_post_1980_share:.1f}%",
     jud(h5_ok),
     f"H5 {jud(h5_ok)}: 門型標識の <b>{g_post_1980_share:.0f}%</b> が "
     f"1980-2000 年代に設置されており、4 兄弟で<b>最も新しい世代</b>。"
     f"特に <b>1990年代 {int(decade_count.get(1990, 0))} 件 ({100*decade_count.get(1990, 0)/n_year:.0f}%)</b> "
     f"が単独最大。これは <b>「道路情報化」 「VICS / VMS 整備」</b>と時期が一致し、"
     f"門型標識が<b>高度道路情報化時代の産物</b>であることを示唆する。"),
], columns=["仮説", "観測値", "判定", "詳細解説"])
T_hypo.to_csv(ASSETS / "L69_hypothesis_check.csv",
                index=False, encoding="utf-8-sig")

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


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


# ----- セクション 1: 学習目標と問い -----
sec1 = f"""
<h3>本記事の対象 — 「門型標識基本情報・維持管理情報」 1 件 単独分析</h3>
<p>本記事は <a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}"
target="_blank">DoBoX のデータセット <b>「門型標識基本情報・維持管理情報」 (dataset {DATASET_ID})</b></a>
1 件を <b>単独</b>で取り上げ、広島県内の道路<b>門型標識 全 {n_total} 件</b>を
<b>3 つの独立した研究角度</b>で並列に分析する記事である。
他のシリーズ (橋梁 L66 / トンネル L67 / シェッド L68 / 横断歩道橋など) と
本記事は <b>合体しない</b>。RQ3 で 4 兄弟比較する際にのみ既扱データ
(L66/L67/L68 で集計済の中間 CSV) を参照する形をとる。</p>

<div class="note">
  <b>「門型標識」 とは:</b><br>
  道路の両側に支柱を立て、<b>道路を跨ぐ門型 (gantry, ゲート型) のフレーム</b>を架けて、
  その横ばりに<b>大型の方向案内標識・道路情報提供装置 (VMS)</b> を設置した構造物。
  運転者が時速 60-80 km で走行している状況でも、<b>進行方向 / 行先 / 車線割当 / 通行規制</b>
  などを一目で読み取れるよう、視認性を最大化する目的で設置される。
  片持ち式 (F 型) や一方向支柱式 (L 型) と異なり、
  <b>道路全体を跨ぐ</b>ことで両方向の車線に同時に標識を呈示できる点が特徴。
  本データの <b>「種別」 列は全件「標識・道路情報提供装置」</b>に統一されており、
  広島県管理の F 型 / L 型は別データセットで管理される (= 本データはオーバーヘッド型のみ)。<br>
  <b>道路法</b>に基づき、道路附属物として 5 年に 1 回の点検が義務化されており
  (2014 年改正)、設計強度 + 老朽腐食 + 標識面の褪色などが定期検査対象となる。
</div>

<h3>独自に定義する用語 (本記事限定)</h3>
<ul class="kv">
  <li><b>方向区分</b>: 施設名から自動抽出した <b>「上下 (双方向)」 「上り」 「下り」</b>の 3 分類。
      H1 検証用。本データの「種別」 はすべて「標識・道路情報提供装置」 で同一なので、
      代わりに方向情報を独自指標として導入する。</li>
  <li><b>路線階層</b>: 国道番号と県道で <b>「一桁国道 (全国基幹)」 「二桁国道
      (地域基幹)」 「三桁国道 (連絡国道)」 「主要県道」</b>の 4 分類。
      国土交通省の道路法上の正式階層ではなく、本記事独自の便宜分類。
      数字の若さが概ね路線重要度を反映するという経験則による。</li>
  <li><b>道路施設 4 兄弟</b>: 同じ「公共土木施設」 シリーズに属する道路系
      4 dataset 群 — <b>L66 橋梁 (4,203) + L67 トンネル (157) +
      L68 シェッド (22) + L69 門型標識 (22)</b>。
      共通の管理事務所階層と 5 年周期点検制度で運用されており、
      4 兄弟構造として比較検討する価値がある。</li>
  <li><b>大型門型標識</b>: 幅員 ≥ 14 m の門型標識。標準的な 2 車線道路の幅員が約 7 m なので、
      14 m 以上は概ね 4 車線級の道路を跨ぐ大型構造を意味する。本記事独自閾値。</li>
  <li><b>古設置門型標識</b>: 設置年度 ≤ 1994 年 (= 2024 時点で築 30 年以上)。
      橋梁の「築 50 年」 老朽閾値より緩い設定だが、門型標識自体が 1980 年代以降の
      設置が中心なので「30 年閾値」 を採用する。</li>
  <li><b>F 型 / L 型 / オーバーヘッド型 (門型)</b>:
      国土交通省「道路標識ハンドブック」 の三分類。
      F 型 = 路側に立てた支柱から横ばりが片持ちで車線上に張り出した形、
      L 型 = 一方向支柱で頂部から L 字に横ばり、
      オーバーヘッド型 = 道路を完全に跨ぐ門型 (gantry / portal)。
      本データはすべてオーバーヘッド型 (= 門型) に統一されている。</li>
</ul>

<h3>研究の問い (3 RQ)</h3>
<ol>
  <li><b>RQ1 (主研究):</b> 広島県の道路門型標識の<b>構造 — 形式 (= 方向区分) ・
      規模 (= 幅員) ・地理分布</b>はどう描けるか?
      {n_total} 件を多角度に集計して、県の門型標識網の物理的形状を初めて定量化する。</li>
  <li><b>RQ2 (副研究 1):</b> 門型標識の<b>設置道路の特性 — 国道偏重か県道偏重か</b>
      (より細かく <b>路線階層 4 分類</b>) はどう現れるか?
      単純な国道 / 県道の二分でなく、<b>路線重要度</b>に近い 4 階層を抽出する。</li>
  <li><b>RQ3 (副研究 2):</b> <b>L66 橋梁 + L67 トンネル + L68 シェッド +
      L69 門型標識の道路施設 4 兄弟構造</b>はどう現れるか?
      件数規模 + 整備年代 + 国道率 + 機能の 4 軸で対比する。</li>
</ol>

<h3>仮説 (5)</h3>
<ul>
  <li><b>H1</b> (RQ1, 方向二分類): 門型標識 {n_total} 件は施設名から方向を読むと、
      <b>「上下」 (双方向)</b>が支配的 (≥ 50%)、片方向は補助的。</li>
  <li><b>H2</b> (RQ1+2, 国道偏重): 門型標識は<b>国道</b>に偏重 (≥ 60%)、
      特に二桁幹線国道に集中。</li>
  <li><b>H3</b> (RQ2, 路線階層): 路線階層別件数は <b>「二桁国道 ≫ 三桁国道 ≒ 主要県道」</b>
      のピラミッド構造 + 平均幅員に段階差。</li>
  <li><b>H4</b> (RQ3, 件数 4 層比): 4 兄弟の件数比 ≒ <b>191 : 7 : 1 : 1</b>。
      L68 シェッドと L69 門型標識は<b>同件数 (22 件) で目的真逆の双子</b>。</li>
  <li><b>H5</b> (RQ3, 整備年代): 4 兄弟で<b>門型標識が最も新しい世代</b>
      (1980-2000 年代集中)。<b>高度道路情報化時代の産物</b>。</li>
</ul>

<h3>到達点</h3>
<p>本記事を読み終えると、(1) 県内に <b>{n_total} 件しかない</b>道路門型標識を
方向 / 路線階層 / 設置年代の 3 軸で完全に俯瞰できる、
(2) 「<b>国道 14 + 県道 8 + 幅員中央値 {df_raw['幅員(m)'].median():.1f} m</b>」 の
全体像と <b>路線階層ピラミッド構造</b>を把握できる、
(3) <b>L66/L67/L68/L69 の道路施設 4 兄弟</b>の階層 (橋梁=網状多数 / トンネル=希少 /
シェッド=希少特殊 / 門型標識=情報提供) を<b>件数比 + 機能差 + 整備年代差</b>で
理解できる、 という 3 段階の知識が獲得できる。</p>
"""


# ----- セクション 2: 使用データ -----
sec2 = f"""
<p>本研究で使う <b>1 つの dataset</b> を以下の表に示す。
本データはシェッド (L68) と<b>列構成が酷似</b>しており、
唯一の違いは <b>「延長(m)」 列が無い</b>点である
(= 門型標識は構造物として「道路上の固定点」 であり、延長という概念が無い)。</p>

<h3>データセット仕様</h3>
{df_to_html(T_dataset)}

<p class="tnote">本データは <b>L68 シェッド (dataset 13)</b> + <b>L66 橋梁 (dataset 12)</b> +
<b>L67 トンネル (dataset 11)</b> と<b>同じ DoBoX シリーズ「公共土木施設の基本情報・
維持管理情報」</b>に属し、列名と書式が大部分共通する (= 県の道路施設管理 DB から
出力された統一フォーマット)。本記事は dataset 14 のみを<b>単独で深掘り</b>し、
4 兄弟比較は RQ3 で既扱データ (L66/L67/L68 中間 CSV) との照合のみで行う。</p>

<h3>データの読み筋</h3>
<ul>
  <li><b>「種別」 列は全件「標識・道路情報提供装置」 で同一</b>なので、
      シェッドのような「種別自動分類」 はできない。代わりに <b>「施設名 → 方向」</b>を
      自動抽出して 3 分類する (RQ1, H1)。</li>
  <li><b>「延長(m)」 列が無い</b>ので、規模指標は<b>「幅員(m)」</b>のみ。
      14 m 以上を「大型門型標識」 (= 4 車線級道路) と独自に閾値化する。</li>
  <li><b>「設置年度」 列</b>は橋梁の「架設年度」 / トンネル・シェッドの「建設年度」
      に対応。{n_year}/{n_total} 件で取得可能。</li>
  <li>緯度経度は <b>{n_coord}/{n_total} 件</b>取得可能。
      欠損 2 件 (= 矢野安浦線上下01/02) は地図には出ないが、表には残る。</li>
  <li><b>判定区分は全件 "?"</b> で、健全度の実値は公開データではマスクされている。
      これは橋梁・トンネル・シェッドと同じ取扱い。</li>
</ul>

<h3>データ取得手順</h3>
{df_to_html(T_data_recipe)}
"""


# ----- セクション 3: ダウンロード -----
def file_size_str(p):
    if p.exists():
        sz = p.stat().st_size
        if sz < 1024:
            return f"{sz} B"
        if sz < 1024 * 1024:
            return f"{sz/1024:.1f} KB"
        return f"{sz/1024/1024:.1f} MB"
    return "-"


sec3 = f"""
<p>本記事の再現に必要な<b>すべて</b>を直リンクで提供する。
HTML だけ読めば学習者が完全再現できることが目標 (要件 A)。</p>

<h3>生データ (DoBoX 1 件)</h3>
<ul class="kv">
  <li><b>dataset {DATASET_ID}</b>:
      <a href="https://hiroshima-dobox.jp/datasets/{DATASET_ID}" target="_blank">
      門型標識基本情報・維持管理情報</a>
      (CSV {csv_size:,} byte, 22 行 × 15 列)
      <a href="../data/extras/L69_gantry_signs/gantry_basic.csv"
         download>[gantry_basic.csv 直 DL]</a></li>
</ul>

<h3>このスクリプト本体</h3>
<ul class="kv">
  <li><a href="L69_gantry_signs.py" download>L69_gantry_signs.py</a>
      — 1 ファイルで完結 (~10 秒で 8 図 + 13 表生成)</li>
</ul>

<h3>中間 CSV (本記事生成、再利用可)</h3>
<ul class="kv">
  <li><a href="assets/L69_all_gantry.csv" download>L69_all_gantry.csv</a>
      — 全 {n_total} 件 + 派生フラグ (方向区分 / 路線階層 / 中山間・大型・古設置 / etc)</li>
  <li><a href="assets/L69_direction_summary.csv" download>L69_direction_summary.csv</a>
      — 方向 3 分類サマリ (件数 + シェア + 平均幅員)</li>
  <li><a href="assets/L69_road_summary.csv" download>L69_road_summary.csv</a>
      — 国道 vs 県道 サマリ</li>
  <li><a href="assets/L69_city_ranking.csv" download>L69_city_ranking.csv</a>
      — 市町別ランキング</li>
  <li><a href="assets/L69_office_ranking.csv" download>L69_office_ranking.csv</a>
      — 管理事務所別ランキング</li>
  <li><a href="assets/L69_route_ranking.csv" download>L69_route_ranking.csv</a>
      — 路線別ランキング</li>
  <li><a href="assets/L69_decade_count.csv" download>L69_decade_count.csv</a>
      — 設置年代別件数</li>
  <li><a href="assets/L69_tier_summary.csv" download>L69_tier_summary.csv</a>
      — 路線階層 4 分類サマリ</li>
  <li><a href="assets/L69_tier_x_direction.csv" download>L69_tier_x_direction.csv</a>
      — 階層 × 方向クロス</li>
  <li><a href="assets/L69_tier_x_decade.csv" download>L69_tier_x_decade.csv</a>
      — 階層 × 年代クロス</li>
  <li><a href="assets/L69_four_siblings.csv" download>L69_four_siblings.csv</a>
      — 4 兄弟 (L66/L67/L68/L69) 比較表</li>
  <li><a href="assets/L69_decade_four.csv" download>L69_decade_four.csv</a>
      — 4 兄弟 整備年代対比</li>
  <li><a href="assets/L69_large_gantry.csv" download>L69_large_gantry.csv</a>
      — 大型門型標識 Top 10</li>
  <li><a href="assets/L69_overall.csv" download>L69_overall.csv</a>
      — 全体サマリ (本記事の数値全体一覧)</li>
  <li><a href="assets/L69_hypothesis_check.csv" download>L69_hypothesis_check.csv</a>
      — H1〜H5 仮説検証結果</li>
</ul>

<h3>図 (PNG, 直 DL 可)</h3>
<ul class="kv">
  <li><a href="assets/L69_fig1_overview_road_dir_map.png" download>fig1 全域 道路種別×方向マップ</a></li>
  <li><a href="assets/L69_fig2_city_route.png" download>fig2 市町 + 路線別件数</a></li>
  <li><a href="assets/L69_fig3_direction_width_decade.png" download>fig3 方向 + 幅員 + 年代</a></li>
  <li><a href="assets/L69_fig4_tier_structure.png" download>fig4 路線階層 件数+幅員+年代</a></li>
  <li><a href="assets/L69_fig5_tier_map.png" download>fig5 路線階層別マップ</a></li>
  <li><a href="assets/L69_fig6_east_zoom.png" download>fig6 県東部ズーム (尾道-福山-世羅)</a></li>
  <li><a href="assets/L69_fig7_four_siblings_map.png" download>fig7 道路施設 4 兄弟マップ</a></li>
  <li><a href="assets/L69_fig8_four_structure.png" download>fig8 4 兄弟 件数+国道率+整備年代</a></li>
</ul>
"""


# ----- セクション 4: RQ1 -----
sec4_code = '''
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

# データ読込
df = pd.read_csv("data/extras/L69_gantry_signs/gantry_basic.csv",
                 encoding="utf-8-sig")
print(df.shape)  # (22, 15)

# 数値正規化
df["設置年度"] = pd.to_numeric(df["設置年度"], errors="coerce")
df.loc[df["設置年度"] <= 1800, "設置年度"] = pd.NA  # "0" は不明
df["幅員(m)"] = pd.to_numeric(df["幅員(m)"], errors="coerce")

# 方向自動分類 (本記事の H1 検証用、独自定義)
def classify_direction(name):
    s = str(name)
    if "上下" in s: return "上下 (双方向案内)"
    if "上り" in s: return "上り (片方向案内)"
    if "下り" in s: return "下り (片方向案内)"
    return "その他"
df["方向区分"] = df["施設名"].apply(classify_direction)

# POINT geometry (緯度経度) → EPSG:6671 で投影
geom_ok = df["緯度（10進数）"].notna() & df["経度（10進数）"].notna()
gdf = gpd.GeoDataFrame(
    df[geom_ok].copy(),
    geometry=[Point(x, y) for x, y in
              zip(df.loc[geom_ok, "経度（10進数）"],
                  df.loc[geom_ok, "緯度（10進数）"])],
    crs="EPSG:4326",
).to_crs("EPSG:6671")
print(f"POINT 構築: {len(gdf)} / {len(df)}")
'''

sec4 = f"""
<h3>狙い (RQ1)</h3>
<p><b>広島県の道路門型標識の構造を「方向 / 規模 / 地理分布」 の 3 軸で
完全に俯瞰</b>することが RQ1 の狙い。 県内 {n_total} 件を多角的に集計して、
学習者が「県の門型標識網の全貌」 を一望できる集計表 + 地図 + 階層分布を出す。
本シリーズには「種別」 (= 標識・道路情報提供装置) しか無い (シェッドの 3 分類のような構造種別が無い) ので、
代替指標として<b>方向区分 (上下/上り/下り)</b>を施設名から自動抽出して導入する。</p>

<h3>手法 — 4 ステップ</h3>
<ol>
  <li><b>STEP 1: CSV 読込 + 数値正規化</b><br>
      pandas で {n_total} 行 × 15 列を読み、設置年度の "0" を欠損 (= 不明) として除外。</li>
  <li><b>STEP 2: 方向自動分類</b><br>
      施設名に「上下」 「上り」 「下り」 のいずれかが含まれるかで 3 分類 (本記事独自定義)。
      入出力: <code>str → str</code>。出力 = "上下 (双方向案内)" / "上り (片方向案内)" /
      "下り (片方向案内)" / "その他"。</li>
  <li><b>STEP 3: POINT geometry 構築 + EPSG:6671 投影</b><br>
      緯度経度から <code>shapely.geometry.Point</code> を生成 → GeoDataFrame に格納
      → JGD2011 平面直角第 III 系 (EPSG:6671) に変換。
      これにより距離・面積を正しいメートル単位で扱える。</li>
  <li><b>STEP 4: 集計</b><br>
      方向 + 道路種別 + 市町 + 事務所 + 路線 + 幅員ヒスト + 年代別の <b>7 軸</b>で
      クロス集計。</li>
</ol>

<h3>実装 (主要部のみ抜粋)</h3>
{code(sec4_code)}

<h3>結果 1: 道路種別 + 方向 + 全域マップ (図 1, 表 道路サマリ)</h3>
<p><b>なぜこの図か:</b> 県全域の門型標識を一目で把握するには地図が最適。
道路種別 (国道 / 県道) で<b>色分け</b>、方向 (上下 / 上り / 下り) で<b>マーカー形状</b>を
変えて<b>2 軸を 1 枚に圧縮</b>することで、「中山間に集中? 県東部に集中? 国道偏重?」 を
即座に確認できる。</p>

{figure("assets/L69_fig1_overview_road_dir_map.png",
        f"図 1 (RQ1): 広島県 道路門型標識 全域マップ — 国道 {n_kuni} (赤) + 県道 {n_ken} (青)")}

<p><b>図 1 から読み取れること:</b></p>
<ul>
  <li><b>赤色 (国道) が圧倒的多数</b>。県中部 (尾道-三原-福山) と県北部 (三次-世羅) に集中分布</li>
  <li>青色 (県道) は<b>県西部 (廿日市・呉) と熊野</b>に散在し、東部にはほぼ無い</li>
  <li>マーカー形状を見ると<b>正方形 (上下双方向) が大半</b>で、▲▼ (片方向) は少数</li>
  <li>中山間地 (北広島町・安芸太田町) にも数件あり、純粋な平野偏重ではない</li>
  <li>1 件 (矢野安浦線) は緯度経度欠損で図に出ない (= 表で確認)</li>
</ul>

<p><b>道路種別 サマリ表:</b></p>
{df_to_html(T_road)}

<p><b>表から読み取れること:</b> 国道 {n_kuni} 件 ({kuni_share:.1f}%) で県道 {n_ken} 件 ({ken_share:.1f}%) に対し
<b>{kuni_share/ken_share:.1f} 倍</b>。 平均幅員も国道 {df_raw[df_raw['道路種別']=='国道']['幅員(m)'].mean():.1f} m
> 県道 {df_raw[df_raw['道路種別']=='県道']['幅員(m)'].mean():.1f} m と国道がやや広い。
最大幅員はそれぞれ {df_raw[df_raw['道路種別']=='国道']['幅員(m)'].max():.0f} m / {df_raw[df_raw['道路種別']=='県道']['幅員(m)'].max():.0f} m で、
県道側に <b>28 m の最大値</b>があるのは矢野安浦線 (= 4 車線級バイパス区間) であることに注意。</p>

<h3>結果 2: 市町別ランキング + 路線別 (図 2)</h3>
<p><b>なぜこの図か:</b> 「どの市町・どの路線に集中しているか?」 を H2 (国道偏重) の検証に
直結する形で見たい。 横棒グラフは件数比較に最適。</p>

{figure("assets/L69_fig2_city_route.png",
        "図 2 (RQ1): 市町別 + 路線別 門型標識件数")}

<p><b>図 2 から読み取れること:</b></p>
<ul>
  <li>市町別 Top 1 は <b>{city_count.iloc[0]['市町名']} ({int(city_count.iloc[0]['件数'])} 件)</b>。
      これは尾道市 + 三次市など県中央部・北部の幹線が交わる結節点</li>
  <li>中山間 9 市町 (青) は <b>{n_chusankan} 件 ({chusankan_share:.0f}%)</b>。
      シェッド (中山間 81.8%) や トンネル (中山間 ~50%) よりは<b>低めの集中</b>であり、
      門型標識は<b>中山間特化ではない</b></li>
  <li>路線別 Top 1 は <b>{T_route.iloc[0]['路線名']} ({int(T_route.iloc[0]['件数'])} 件)</b>。
      上位 3 路線で全体の <b>{(T_route.head(3)['件数'].sum()/n_total*100):.0f}%</b> を占める</li>
  <li>路線数は<b>{df_raw['路線名'].nunique()} 路線</b>のみ。シェッド (8 路線) と同水準で、
      門型標識も<b>少数の幹線に集中</b>する形態</li>
</ul>

<h3>結果 3: 方向 + 幅員 + 設置年代 (図 3, 表 方向サマリ)</h3>
<p><b>なぜこの図か:</b> H1 (上下が支配的) と幅員の分布形状、設置年代の偏りを<b>同時 3 軸</b>で確認するため、
1 枚に 3 サブプロットを並べた。 各 1 枚を独立に出すよりも、3 軸を頭の中で同時に把握できるので学習者の負担が軽い。</p>

{figure("assets/L69_fig3_direction_width_decade.png",
        "図 3 (RQ1): 方向 + 幅員 + 設置年代 分布")}

<p><b>図 3 から読み取れること:</b></p>
<ul>
  <li><b>方向区分は「上下」 が圧倒的</b> ({n_jouge} 件, {100*n_jouge/n_total:.0f}%)。
      これは門型標識の構造的特性 (= 道路全体を跨ぐ) を反映する</li>
  <li>上り {n_nobori} 件 + 下り {n_kudari} 件で、片方向は補助的役割。
      H1「上下が支配的 ≥ 50%」 を強支持</li>
  <li>幅員分布の<b>中央値は {df_raw['幅員(m)'].median():.1f} m</b>、
      最大は <b>{df_raw['幅員(m)'].max():.0f} m</b>。 14 m 以上の大型は {n_large} 件 ({100*n_large/n_total:.0f}%)</li>
  <li>設置年代は <b>{peak_decade} 年代がピーク ({peak_count} 件)</b>。
      1990 年代に集中するパターンは<b>「VICS 整備期」 「VMS 普及期」</b>と時期が一致</li>
  <li>1970 年代以前はほぼゼロ。シェッド (1970 年代集中) との<b>世代差</b>が明らか</li>
</ul>

<p><b>方向サマリ表:</b></p>
{df_to_html(T_direction)}

<p><b>表から読み取れること:</b> 上下 (双方向) は平均幅員 <b>{df_raw[df_raw['方向区分']=='上下 (双方向案内)']['幅員(m)'].mean():.1f} m</b> と
最大で、片方向 (上り/下り) は約 8.6 m と狭い。これは<b>「上下 = 片側 2 車線以上の道路 = 道路幅広い」</b>
という当然の関係を反映する (= H1 の構造的根拠)。</p>

<h3>結果 4: 路線・事務所・市町 ランキング (3 表)</h3>

<p><b>路線別 Top 5:</b></p>
{df_to_html(T_route.head(5))}

<p><b>路線別 表から読み取れること:</b> 単独 1 位は <b>{T_route.iloc[0]['路線名']} ({int(T_route.iloc[0]['件数'])} 件)</b>。
これは尾道市 + 世羅町を縦貫する基幹国道で、世羅町に 2 件 + 尾道市に 2 件 + 三次市方面に 1 件と分散配置。
2 位は {T_route.iloc[1]['路線名']} ({int(T_route.iloc[1]['件数'])} 件)、3 位は {T_route.iloc[2]['路線名']} ({int(T_route.iloc[2]['件数'])} 件)。
県道 (大竹湯来線・矢野安浦線・広島空港線) は 2 件ずつで、地点重複型の配置。</p>

<p><b>管理事務所別:</b></p>
{df_to_html(T_office)}

<p><b>事務所別 表から読み取れること:</b> 単独 1 位は <b>{T_office.iloc[0]['管理事務所名']} ({int(T_office.iloc[0]['件数'])} 件)</b>。
県中部の三原支所は東広島から尾道までの広域を所管しており、184/183/185 号の幹線国道がここを通る。
東広島支所と北部建設事務所が 3 件ずつで続く。</p>

<p><b>市町別 (中山間9市町は青で図 2 内表示)、Top 8:</b></p>
{df_to_html(city_count.head(8))}

<p><b>市町別 表から読み取れること:</b> 単独 1 位は <b>{city_count.iloc[0]['市町名']} ({int(city_count.iloc[0]['件数'])} 件)</b>、
2-3 位は <b>{city_count.iloc[1]['市町名']} / {city_count.iloc[2]['市町名']}</b> (各 {int(city_count.iloc[1]['件数'])} 件 / {int(city_count.iloc[2]['件数'])} 件)。
中山間 9 市町シェアは {chusankan_share:.0f}% で、シェッド (81.8%) より大幅に低い。
門型標識は<b>「平野部の幹線国道」</b>に偏重するインフラ。</p>
"""


# ----- セクション 5: RQ2 -----
sec5_code = '''
# 路線階層 4 分類 (本記事独自)
import re
def classify_route_tier(road_class, route_name):
    if road_class == "県道":
        return "主要県道"
    z2h = str(route_name).translate(str.maketrans("０１２３４５６７８９", "0123456789"))
    m = re.search(r"(\\d+)", z2h)
    if not m: return "主要県道"
    num = int(m.group(1))
    if num <= 9:    return "一桁国道 (全国基幹)"
    if num <= 199:  return "二桁国道 (地域基幹)"
    return "三桁国道 (連絡国道)"

df["路線階層"] = df.apply(lambda r: classify_route_tier(r["道路種別"], r["路線名"]), axis=1)

# 4 階層 × 件数 × 幅員 × 年代 集計
tier_order = ["一桁国道 (全国基幹)", "二桁国道 (地域基幹)",
              "三桁国道 (連絡国道)", "主要県道"]
T_tier = (df.groupby("路線階層")
          .agg(件数=("施設番号", "size"),
               平均幅員=("幅員(m)", "mean"),
               最大幅員=("幅員(m)", "max"))
          .reindex(tier_order))
print(T_tier)
'''

sec5 = f"""
<h3>狙い (RQ2)</h3>
<p>RQ1 で「<b>国道 {kuni_share:.0f}% / 県道 {ken_share:.0f}%</b>」 という二分は分かったが、
これは<b>粗すぎる</b>。 国道の中にも<b>2 号 (一桁 = 全国主要幹線)</b>、 <b>184 号 (二桁 = 地域基幹)</b>、
<b>375 号 (三桁 = 連絡国道)</b>のような<b>重要度の階層</b>がある。
本 RQ2 では <b>路線番号の桁数</b>で 4 階層に分け、各階層の<b>件数 / 幅員 / 設置年代</b>を比較する。
これにより「門型標識は二桁幹線に集中するか?」 「上位幹線ほど大型か?」 を定量検証する。</p>

<h3>手法 — 路線階層 4 分類 (独自定義)</h3>
<p><b>入力 → 出力:</b> <code>(道路種別, 路線名)</code> → <code>"一桁国道" / "二桁国道" /
"三桁国道" / "主要県道"</code> の 4 値。</p>

<table>
  <tr><th>階層</th><th>定義</th><th>典型路線 (本データ)</th><th>意味</th></tr>
  <tr><td><b>一桁国道</b></td><td>道路種別 = 国道 ∧ 路線番号 ≤ 9</td>
      <td>2 号</td><td>全国を貫く基幹幹線、最重要</td></tr>
  <tr><td><b>二桁国道</b></td><td>道路種別 = 国道 ∧ 10 ≤ 路線番号 ≤ 199</td>
      <td>182/183/184/185/191 号</td><td>地域を貫く基幹幹線</td></tr>
  <tr><td><b>三桁国道</b></td><td>道路種別 = 国道 ∧ 路線番号 ≥ 200</td>
      <td>375 号</td><td>連絡・補完的国道</td></tr>
  <tr><td><b>主要県道</b></td><td>道路種別 = 県道</td>
      <td>瀬野呉線/大竹湯来線/広島空港線/安佐豊平芸北線/矢野安浦線</td>
      <td>県管理の主要道路</td></tr>
</table>

<p><b>注: 国土交通省の道路法上の正式階層ではない</b> (一級・二級国道の歴史的区別ではない)。
本記事は「<b>路線番号の桁数 = 概ね路線重要度</b>」 という経験則を採用した独自分類である
(要件 M: 独自用語の冒頭定義)。「桁数 = 重要度」 は完全な対応ではないが、
便宜的な階層化としてはよく使われる手法である。</p>

<h3>実装 (主要部)</h3>
{code(sec5_code)}

<h3>結果 1: 路線階層 件数 + 幅員 + 階層×年代ヒートマップ (図 4, 表 階層サマリ)</h3>
<p><b>なぜこの図か:</b> 4 階層の<b>件数 (棒) + 平均/最大幅員 (棒) + 年代ヒート</b>を 1 枚に
並べることで、「ピラミッド形状か」 「上位ほど大型か」 「上位ほど古いか」 を<b>同時</b>に
読める。 棒 + 棒 + ヒートマップは別軸の指標を 1 枚に圧縮する常套手段。</p>

{figure("assets/L69_fig4_tier_structure.png",
        "図 4 (RQ2): 路線階層別 構造 — 件数 + 幅員 + 年代")}

<p><b>図 4 から読み取れること:</b></p>
<ul>
  <li><b>件数: 二桁国道 {n_2g} 件 ({100*n_2g/n_total:.0f}%) ≫ 主要県道 {n_pref} 件 ({100*n_pref/n_total:.0f}%) ≫
      三桁国道 {n_3g} 件 ≫ 一桁国道 {n_1g} 件</b>。 二桁国道が単独最頻でピラミッドの底</li>
  <li>意外なことに<b>三桁国道が二桁県道より少ない</b>。これは広島県内の三桁国道 (375 号など) が
      門型標識を必要とする 4 車線区間を持たないことを示唆</li>
  <li><b>平均幅員: 一桁 {w_1g:.1f} m / 二桁 {w_2g:.1f} m / 三桁 {w_3g:.1f} m / 県道 {w_pref:.1f} m</b>。
      意外にも <b>{'一桁国道が最大' if w_1g > w_2g else '二桁国道が最大'}</b>で、 H3 の段階差仮説は
      {'強支持' if h3_width_step else '部分支持'}</li>
  <li>最大幅員は<b>主要県道に 28 m</b>がある (矢野安浦線)。これは予想外で、
      県道でも 4 車線級バイパスがある場合は門型標識が大型化する</li>
  <li>ヒートマップ: 二桁国道は <b>1990 年代</b> ({int(tier_decade_cross.iloc[1].get('1990s', 0))} 件)
      に集中、 主要県道も同時期に整備</li>
</ul>

<p><b>路線階層 サマリ表:</b></p>
{df_to_html(T_tier)}

<p><b>階層 サマリ 表から読み取れること:</b></p>
<ul>
  <li>二桁国道はシェア {100*n_2g/n_total:.0f}% で最頻、平均設置年度は <b>{int(T_tier.iloc[1]['平均設置年度']) if pd.notna(T_tier.iloc[1]['平均設置年度']) else '?'} 年</b>。
      路線数は {int(T_tier.iloc[1]['路線数'])} 路線で広く展開</li>
  <li>主要県道もシェア {100*n_pref/n_total:.0f}% と健闘。これは「県管理 + 重要バイパス」 を反映</li>
  <li>三桁国道は<b>375 号 1 路線のみ</b>で {n_3g} 件、補完的役割</li>
</ul>

<h3>結果 2: 路線階層別マップ (図 5)</h3>
<p><b>なぜこの図か:</b> 「<b>どの階層がどの地理範囲に分布するか</b>」 を<b>地図で</b>視覚化したい。
4 階層をマーカー色で分けて広域図にプロットする。</p>

{figure("assets/L69_fig5_tier_map.png",
        "図 5 (RQ2): 路線階層別 門型標識マップ — 4 階層分類")}

<p><b>図 5 から読み取れること:</b></p>
<ul>
  <li>橙色 (二桁国道) が県全域に広く分布、特に<b>県中央部 (尾道-三次)</b>に集中</li>
  <li>青色 (主要県道) は<b>県西部 (廿日市-呉)</b>に偏在</li>
  <li>赤 (一桁国道 = 2 号) は<b>尾道市の山陽道沿い 1 点のみ</b></li>
  <li>紫 (三桁国道 = 375 号) は<b>東広島市に 2 点</b>のみ</li>
  <li>4 階層の地理特性が<b>明確に分離</b>している</li>
</ul>

<h3>結果 3: 県東部ズームマップ (図 6)</h3>
<p><b>なぜこの図か:</b> 県東部 (尾道-三原-福山-世羅) は門型標識が<b>{n_in_roi if 'n_in_roi' in dir() else '多数'}</b>件と最も集中する地帯。
ここを<b>ズームイン</b>して路線名ラベルを表示し、「同じ路線の上下 + 上り + 下りが近接配置されている」 ことを目視確認する。</p>

{figure("assets/L69_fig6_east_zoom.png",
        "図 6 (RQ2): 尾道-三原-福山-世羅 周辺ズーム")}

<p><b>図 6 から読み取れること:</b></p>
<ul>
  <li>184 号 (橙) が世羅町と尾道市に <b>4 点</b>密集 — 同一路線上に複数門型標識</li>
  <li>183 号 (橙) は三次市に <b>3 点</b>集中 — IC 近接の重要分岐</li>
  <li>2 号 (赤) は尾道市内の山陽道との合流点に 1 点</li>
  <li>路線名ラベルから「<b>同じ路線で上下 + 上り + 下り</b>」 の 3 種が並列配置されているケースは無く、
      ほぼすべて「上下」 単独配置であることが確認できる</li>
</ul>

<h3>結果 4: 階層 × 方向クロス (表)</h3>

<p><b>路線階層 × 方向 クロス (件数):</b></p>
{df_to_html_with_index(route_dir_cross)}

<p><b>クロス 表から読み取れること:</b> どの階層も<b>「上下」 が圧倒的多数</b>。
特に<b>主要県道は全件「上下」</b>で、 片方向標識は<b>二桁国道 ({int(route_dir_cross.iloc[1].get('上り (片方向案内)', 0)) + int(route_dir_cross.iloc[1].get('下り (片方向案内)', 0))} 件)</b>と
<b>三桁国道 ({int(route_dir_cross.iloc[2].get('下り (片方向案内)', 0))} 件)</b>のみ。
これは「複数車線がない一桁・県道では『上下』 一本化、複雑な国道だけ片方向が必要」 という設計思想を示唆する。</p>
"""


# ----- セクション 6: RQ3 -----
sec6_code = '''
# 4 兄弟比較 (RQ3) — L66/L67/L68 の中間 CSV を読込んで対比
df_b = pd.read_csv("lessons/assets/L66_all_bridges.csv", encoding="utf-8-sig")
df_t = pd.read_csv("lessons/assets/L67_all_tunnels.csv", encoding="utf-8-sig")
df_s = pd.read_csv("lessons/assets/L68_all_sheds.csv",   encoding="utf-8-sig")

n_bridge = len(df_b)   # 4,203
n_tunnel = len(df_t)   # 157
n_shed   = len(df_s)   # 22
n_gantry = len(df)     # 22

# 件数比
print(f"件数比 = {n_bridge//n_gantry} : "
      f"{round(n_tunnel/n_gantry)} : "
      f"{n_shed//n_gantry} : 1")  # 191 : 7 : 1 : 1

# 国道率
def kuni_share(df):
    return 100 * (df["道路種別"] == "国道").sum() / len(df)
print(f"国道率: 橋 {kuni_share(df_b):.0f}% / トン {kuni_share(df_t):.0f}% "
      f"/ シェ {kuni_share(df_s):.0f}% / 門 {kuni_share(df):.0f}%")

# 整備年代
def year_decade(df, col):
    s = pd.to_numeric(df[col], errors="coerce")
    s = s[s > 1900]
    return (s // 10 * 10).astype(int).value_counts().sort_index()
print(year_decade(df_b, "架設年度"))   # 橋梁: 1960-2000s 全期分散
print(year_decade(df_t, "建設年度"))   # トンネル: 同様分散
print(year_decade(df_s, "建設年度"))   # シェッド: 1970-1980s 集中
print(year_decade(df,    "設置年度")) # 門型: 1980-2000s 集中
'''

sec6 = f"""
<h3>狙い (RQ3)</h3>
<p>L66 (橋梁単独) → L67 (トンネル単独) → L68 (シェッド単独) → 本 L69 (門型標識単独) の
<b>4 兄弟記事</b>がここで完成する。
4 兄弟は同じ「公共土木施設の基本情報・維持管理情報」 シリーズに属し、
共通の管理事務所階層と 5 年周期点検制度の下で運用されている。
本 RQ3 では <b>件数規模 + 国道率 + 整備年代 + 機能</b>の 4 軸で対比し、
<b>県の道路インフラ 4 階層</b>を初めて完成させる。</p>

<h3>手法</h3>
<p>L66/L67/L68 の中間 CSV (前作で生成済) を読み込み、本 L69 のデータと並べて 4 列比較表を作る。
4 兄弟の集計済データは<b>すべて事前に lessons/assets/ に保存済</b>なので、
本 RQ3 は<b>追加の DL や重い処理を一切しない</b>。</p>

<h3>実装</h3>
{code(sec6_code)}

<h3>結果 1: 道路施設 4 兄弟マップ (図 7)</h3>
<p><b>なぜこの図か:</b> <b>「県内に 橋梁 4,203 + トンネル 157 + シェッド 22 + 門型標識 22 が
どう分布するか」</b>を 1 枚にまとめると、 4 兄弟の地理特性が見えるはず。
点の密度が違いすぎる (4,203 vs 22) ので、橋梁を背景灰色、トンネルを紫、シェッドを橙▲、門型標識を赤■と
<b>マーカーサイズ + 色 + 形状</b>で区別する。</p>

{figure("assets/L69_fig7_four_siblings_map.png",
        f"図 7 (RQ3): 道路施設 4 兄弟マップ — 橋梁 + トンネル + シェッド + 門型標識")}

<p><b>図 7 から読み取れること:</b></p>
<ul>
  <li><b>橋梁 (灰)</b> が県内全域を網羅 — 「道路の連続性確保」 という基礎機能</li>
  <li><b>トンネル (紫)</b> は中国山地に細い帯状に集中 — 「山岳貫通」</li>
  <li><b>シェッド (橙▲)</b> は県北西部 (安芸太田町・北広島町・三次市) の急峻地帯にピンポイント</li>
  <li><b>門型標識 (赤■)</b> は<b>シェッドとは異なる場所</b>に配置 —
      県中央部 (尾道-三原-世羅-三次) の幹線国道沿い + 県西部 (廿日市・呉) の主要県道沿い</li>
  <li>シェッドと門型標識は<b>同件数 (22 件) ながら地理分布がほぼ重ならない</b>
      ({n_shed} 件のシェッドは「山腹保護」、 {n_total} 件の門型標識は「情報提供」 と機能が真逆)</li>
  <li>4 兄弟が県の道路インフラの<b>異なる 4 機能</b>を担うことが地図で実証される</li>
</ul>

<h3>結果 2: 4 兄弟 件数 + 国道率 + 整備年代 (図 8)</h3>
<p><b>なぜこの図か:</b> 4 兄弟を<b>件数 (log) + 国道率 + 年代</b>の 3 指標で 1 枚に並べ、
「規模差」 「機能差」 「世代差」 を同時に把握する。 件数は規模が桁違いなので log 軸必須。</p>

{figure("assets/L69_fig8_four_structure.png",
        "図 8 (RQ3): 道路施設 4 兄弟 — 件数 + 国道率 + 整備年代")}

<p><b>図 8 から読み取れること:</b></p>
<ul>
  <li><b>件数: 橋梁 ({n_bridge:,}) ≫ トンネル ({n_tunnel}) ≫ シェッド ({n_shed}) = 門型標識 ({n_total})</b>。
      シェッドと門型標識の<b>件数同点 (22)</b>は驚き</li>
  <li><b>国道率: 門型標識が {kuni_g:.0f}% で最高</b> (橋梁 {kuni_b:.0f}% / トンネル {kuni_t:.0f}% / シェッド {kuni_s:.0f}%)。
      情報提供は<b>幹線重点</b></li>
  <li><b>整備年代: 4 兄弟で異なるピーク</b> —
      橋梁は 1960-2000s 全期分散、
      トンネルは戦後継続、
      シェッドは 1970-80s 集中、
      <b>門型標識は 1990s 集中 (= 最新世代)</b></li>
  <li>門型標識の <b>1990s の突出</b>は VICS / VMS 整備期と一致 —
      「<b>道路情報化時代の象徴</b>」 という独自仮説 H5 を強支持</li>
</ul>

<p><b>4 兄弟比較表 (RQ3 中核):</b></p>
{df_to_html(T_four)}

<p><b>4 兄弟表から読み取れること:</b></p>
<ul>
  <li><b>件数比 191 : 7 : 1 : 1</b> という指数階層が確認</li>
  <li>機能は<b>「接続 / 貫通 / 保護 / 情報」</b> の 4 機能で完全分担</li>
  <li>シェッドと門型標識は<b>同件数なのに目的真逆</b>の<b>双子構造</b></li>
  <li>整備期にも世代差: 橋梁 (全期) → トンネル (戦後継続) → シェッド (国土計画期) → 門型標識 (情報化期)</li>
</ul>

<p><b>4 兄弟 整備年代詳細表:</b></p>
{df_to_html(T_decades_four)}

<p><b>年代表から読み取れること:</b> 1990s が門型標識の単独ピーク ({int(decade_count.get(1990, 0))} 件) で、
シェッドは 1980s ピーク ({int(dec_s.get(1980, 0)) if dec_s is not None else '?'} 件) と<b>1 世代ずれ</b>。
これは「シェッド = 国土計画期 (落石対策)」 → 「門型標識 = 情報化期 (案内標識)」 という<b>整備思想の世代交代</b>を反映する。</p>

<h3>結果 3: 大型門型標識 Top 10 (表)</h3>
<p><b>なぜこの表か:</b> 門型標識のうち<b>幅員 ≥ 14 m (4 車線級)</b>の<b>大型構造物</b>を特定し、
「どこに大型が集中するか」 を見たい。 H3 (路線階層 × 幅員) の補完として、トップ事例を具体名で確認。</p>

{df_to_html(T_large_gantry_show)}

<p><b>大型 Top 10 表から読み取れること:</b> 上位 3 件は<b>{T_large_gantry_show.iloc[0]['施設名']}</b>
{int(T_large_gantry_show.iloc[0]['幅員(m)'])} m + <b>{T_large_gantry_show.iloc[1]['施設名']}</b>
{int(T_large_gantry_show.iloc[1]['幅員(m)'])} m + <b>{T_large_gantry_show.iloc[2]['施設名']}</b>
{int(T_large_gantry_show.iloc[2]['幅員(m)'])} m。 矢野安浦線の 28 m が最大で、
これは県道でも最大 — 4 車線バイパス区間に特化した大型構造である。 上位は<b>{(T_large_gantry_show.head(5)['道路種別']=='国道').sum()}/5 件が国道</b>で、
残りは矢野安浦線 (県道だが 4 車線級バイパス)。</p>
"""


# ----- セクション 7: 仮説検証 -----
T_hypo_html = T_hypo.copy()
T_hypo_html["判定"] = T_hypo_html["判定"].apply(
    lambda v: f'<b style="color:{"#1a7f37" if "強支持" in v else ("#cf6f00" if "部分" in v else "#cf222e")}">{v}</b>')

sec7 = f"""
<h3>仮説検証総合 (H1〜H5)</h3>
<p>本記事冒頭で立てた 5 仮説の検証結果を以下にまとめる。
すべての仮説の検証根拠は本記事中の図表に明示されており、再現可能。</p>

{df_to_html(T_hypo_html)}

<h3>主要発見の整理</h3>
<div class="note">
  <ul>
    <li><b>RQ1 主発見:</b> 門型標識 {n_total} 件のうち<b>「上下」 (双方向) が {100*n_jouge/n_total:.0f}% を占める</b>。
        これは門型標識の構造的特性 (= 道路全体を跨ぐ) を反映し、片方向専用は補助的。
        国道シェアは {kuni_share:.0f}% で<b>幹線国道偏重</b>を確認 (H1 + H2 強支持)。</li>
    <li><b>RQ2 主発見:</b> 路線階層 4 分類で <b>二桁国道 {n_2g} 件 ({100*n_2g/n_total:.0f}%) ≫ 主要県道 {n_pref}
        ≫ 三桁国道 {n_3g} ≫ 一桁国道 {n_1g}</b> のピラミッド構造を確認。
        平均幅員は意外にも一桁国道 ({w_1g:.1f} m) が最大ではなく、 主要県道に <b>28 m</b> の最大値があった
        (矢野安浦線の 4 車線バイパス区間)。 H3 部分支持。</li>
    <li><b>RQ3 主発見:</b> L66 橋梁 ({n_bridge:,}) ≫ L67 トンネル ({n_tunnel}) ≫
        L68 シェッド ({n_shed}) ≈ L69 門型標識 ({n_total}) の<b>件数比 191 : 7 : 1 : 1</b>。
        シェッドと門型標識が<b>同件数 (22 件) なのに目的真逆 (山腹保護 vs 情報提供) の双子構造</b>を成す。
        整備年代も<b>4 兄弟で異なるピーク</b> — 門型標識は 1990 年代集中 (VICS/VMS 整備期) で、
        4 兄弟<b>最も新しい世代</b>。 H4 + H5 強支持。</li>
  </ul>
</div>

<h3>本記事の独自貢献</h3>
<ol>
  <li><b>方向区分の自動分類</b>: 種別が全件同一の本データに対し、施設名から
      「上下/上り/下り」 を抽出する独自指標を導入。</li>
  <li><b>路線階層 4 分類の独自定義</b>: 単純な国道/県道二分でなく、
      路線番号の桁数で 4 階層化することで「ピラミッド構造」 を可視化。</li>
  <li><b>道路施設 4 兄弟構造の完成</b>: L66 + L67 + L68 + L69 で
      <b>橋梁 (接続) + トンネル (貫通) + シェッド (保護) + 門型標識 (情報)</b>の
      <b>4 機能</b>を初めて統合的に定量化。</li>
  <li><b>双子構造の発見</b>: シェッドと門型標識が同件数 (22 件) ながら目的真逆という
      非自明な並びを発見。</li>
  <li><b>1990 年代の意味付け</b>: 門型標識の世代特性を「VICS / VMS 整備期」 という
      時代背景と結びつけ、 4 兄弟<b>最新世代</b>と位置付ける。</li>
</ol>

<h3>本記事の限界</h3>
<ul>
  <li><b>件数 22 の少サンプル性</b>: 路線階層 4 分類に分けると一桁・三桁国道は各 1-2 件のみで、
      統計的検定は無理。 全数調査として記述統計に留める。</li>
  <li><b>判定区分 "?" マスク</b>: 公開データでは健全度判定が伏せられているので、
      老朽化の実態は未把握。 「設置年度 ≤ 1994 = 古設置」 という代理指標で扱う。</li>
  <li><b>NEXCO 管理は未含</b>: 山陽道や中国道の高速道路上門型標識は本データに含まれない
      (= 県管理のみ)。 NEXCO 管理の数百件と合算するとさらに大きな構造が見える。</li>
  <li><b>F 型 / L 型は別データ</b>: 本データはオーバーヘッド型 (門型) のみで、
      片持ち式 (F 型) や一方向支柱 (L 型) は別シリーズ。 全標識の網羅は未達成。</li>
  <li><b>RQ3 の機能分類は本記事独自</b>: 「接続 / 貫通 / 保護 / 情報」 の 4 機能は
      本記事の解釈で、公式分類ではない。</li>
</ul>
"""


# ----- セクション 8: 発展課題 -----
sec8 = f"""
<h3>発展課題 — 結果 X → 新仮説 Y → 課題 Z 形式</h3>

<h4>発展課題 1 (RQ1 拡張): <b>門型標識のフォント・面サイズ・案内対象</b>分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究は<b>幅員のみ</b>を規模指標として扱ったが、
      門型標識の本質は<b>「面の大きさ + 文字サイズ + 表示対象 (IC / 方面 / 車線割当)」</b>であり、
      幅員 = 道路幅は<b>標識面の大きさの代理指標</b>に過ぎない。</li>
  <li><b>新仮説 Y</b>: <b>大型門型標識 (≥ 14 m) は IC + 方面案内</b>、
      小型 (< 14 m) は<b>車線割当のみ</b>という機能分担があるはず。</li>
  <li><b>課題 Z</b>: 現地調査または Google Street View で 22 件すべての
      <b>標識面の写真</b>を採取 → 文字数 + 案内対象 (方面/IC/車線/規制) を分類 →
      幅員と相関を取る。<b>「広島県門型標識面の機能カタログ」</b>として展開可能。</li>
</ul>

<h4>発展課題 2 (RQ2 拡張): <b>NEXCO 管理を含む全門型標識</b>の総合分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究は<b>県管理 22 件のみ</b>。 山陽道 + 中国道の
      <b>NEXCO 管理門型標識</b>は本データに含まれない (推定数百件)。</li>
  <li><b>新仮説 Y</b>: NEXCO 管理を加えると総数は 5-10 倍になり、
      本研究で見た「県管理は地域基幹に集中」 という構造は<b>「NEXCO = 高規格自動車道、県 = 一般幹線国道
      + 主要県道」</b>という<b>2 層分業</b>として再解釈できる。</li>
  <li><b>課題 Z</b>: NEXCO 西日本のオープンデータか道路施設点検データから門型標識位置を取得 →
      本データと統合 → 「広島県内全門型標識網」 を再構築。
      高速道路 + 一般国道 + 県道の<b>3 層階層</b>に拡張する。</li>
</ul>

<h4>発展課題 3 (RQ3 拡張): <b>F 型 + L 型 + 門型</b> 全標識の三分類分析</h4>
<ul>
  <li><b>結果 X</b>: 本データは<b>オーバーヘッド型 (門型) のみ</b>。
      実際には道路上に <b>F 型 (片持式) + L 型 (一方向支柱) + 門型 (オーバーヘッド型)</b>の 3 形式が共存。</li>
  <li><b>新仮説 Y</b>: 3 形式は<b>機能分担</b>している —
      門型 = 大型情報 (IC・方面)、 L 型 = 中型情報 (バイパス案内)、 F 型 = 小型情報 (側方案内)。
      件数比は <b>F : L : 門 = 100 : 10 : 1</b>程度で、F 型が圧倒的多数のはず。</li>
  <li><b>課題 Z</b>: F 型 + L 型のオープンデータが DoBoX または県のオープンデータポータルにあるか調査
      (現状本データには含まれていないが、別シリーズで管理されている可能性) →
      入手次第本記事に追加 → <b>「広島県道路標識網 3 形式構造」</b>として展開。</li>
</ul>

<h4>発展課題 4 (RQ3 拡張): <b>5 年点検データの時系列追跡</b></h4>
<ul>
  <li><b>結果 X</b>: 本データの点検年度は全件 2020-2021 で、<b>判定区分は伏せられている</b>。</li>
  <li><b>新仮説 Y</b>: 過去 5 年の点検履歴 (2015-2020-2025 の 3 サイクル分) を辿ると、
      古設置 (1980s 整備) の門型標識で<b>判定が悪化傾向</b>を示すはず。
      特に塩害が多い<b>沿岸 (尾道-福山)</b>で腐食進行が早い仮説。</li>
  <li><b>課題 Z</b>: 県の道路施設点検データの過去版を取得 → 個別施設の判定区分時系列を再構築 →
      設置年度 / 立地 / 道路種別と判定悪化率の相関分析。
      <b>「門型標識劣化予測モデル」</b>として展開可能。</li>
</ul>

<h4>発展課題 5 (4 兄弟拡張): <b>横断歩道橋・道路情報板・電光掲示板</b>の追加分析</h4>
<ul>
  <li><b>結果 X</b>: 本研究は L66/L67/L68/L69 の<b>4 兄弟</b>で道路保護・情報提供を扱ったが、
      <b>横断歩道橋 + 道路情報板 + 電光掲示板</b>はまだ記事化されていない。</li>
  <li><b>新仮説 Y</b>: これら 3 シリーズを加えると<b>「7 兄弟」</b>となり、
      <b>道路インフラの完全カタログ</b>が完成。 道路接続 / 山岳貫通 / 山腹保護 / 情報提供 /
      歩行者安全 / 動的情報 / 緊急情報の <b>7 機能</b>に拡張される。</li>
  <li><b>課題 Z</b>: DoBoX dataset 一覧から該当データを特定 → L70-L72 として 3 記事を追加 →
      4 兄弟を 7 兄弟に拡張。 「県の道路インフラ機能 7 階層」 として完成。</li>
</ul>

<h4>発展課題 6 (展望): <b>門型標識可視性 GIS 解析</b>の構築</h4>
<ul>
  <li><b>結果 X</b>: 本研究は門型標識の<b>位置と幅員</b>のみを扱ったが、
      実際の<b>可視性</b> (運転者から何 m 手前で見える?) は地形 + カーブ + 周囲建物に依存。</li>
  <li><b>新仮説 Y</b>: <b>4 階層的に「一桁国道 > 二桁 > 三桁 > 県道」 の順で可視距離が長い</b>
      仮説 (高規格ほど標識手前の見通しが確保されている)。</li>
  <li><b>課題 Z</b>: DEM (L40 標高データ) + 道路中心線 (L42 地図情報) を使い、
      門型標識から運転者視点 (1.2m 高、運転席) への<b>視線経路</b>を計算 →
      何 m 手前から見えるかを定量化。
      <b>「門型標識可視性 GIS 解析」</b>として展開。</li>
</ul>

<h4>発展課題 7 (制度視点): <b>門型標識耐震診断</b>の実証</h4>
<ul>
  <li><b>結果 X</b>: 古設置 (≤ 1994) の門型標識は <b>{n_old_compute} 件 ({old_share_compute:.0f}%)</b>。
      旧耐震基準で設計された可能性。</li>
  <li><b>新仮説 Y</b>: 1980 年代設置の門型標識は<b>新耐震基準 (1981 年改正) の前後</b>に位置し、
      地震時の倒壊リスクに差がある仮説。</li>
  <li><b>課題 Z</b>: 各標識の<b>設計強度 + 耐震診断結果</b>を県に開示請求 →
      設置年度との関係を定量化 → <b>「門型標識耐震ランキング」</b>を作成。</li>
</ul>
"""


# ----- 統合 -----
sections = [
    ("学習目標と問い", sec1),
    ("使用データ", sec2),
    ("ダウンロード", sec3),
    (f"【RQ1】 門型標識の構造 — 国道 {kuni_share:.0f}% / "
     f"上下 {100*n_jouge/n_total:.0f}% / 中央幅員 {df_raw['幅員(m)'].median():.1f}m",
     sec4),
    (f"【RQ2】 路線階層別構造 — 二桁国道 {100*n_2g/n_total:.0f}% / "
     f"4 階層ピラミッド",
     sec5),
    (f"【RQ3】 道路施設 4 兄弟構造 — 件数比 "
     f"{n_bridge//n_total}:{round(n_tunnel/n_total)}:1:1 / "
     f"門型は最新世代 (1990s)",
     sec6),
    ("仮説検証総合", sec7),
    ("発展課題", sec8),
]

html = render_lesson(
    num=69,
    title=f"門型標識基本情報 単独 3 研究例分析 — "
          f"{n_total} 件から県の道路情報インフラを読む",
    tags=["L69", "門型標識", "ガントリー", "オーバーヘッド型",
          "情報提供装置", "国道", "県道",
          "RQ×3", "Format B", "geopandas", "POINT (CSV)",
          "路線階層", "道路施設4兄弟",
          "L66連携 (橋梁)", "L67連携 (トンネル)", "L68連携 (シェッド)"],
    time="50 分",
    level="中級",
    data_label=f"DoBoX dataset {DATASET_ID} (CSV, ~{csv_size/1024:.1f} KB)",
    sections=sections,
    script_filename="L69_gantry_signs.py",
)

OUT_HTML = LESSONS / "L69_gantry_signs.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)
