"""L25 農林漁業関係施策 17 件 統合分析 — 広島県内の食農保全意思の地理構造

カバー宣言:
  本記事は 「都市計画区域情報_農林漁業関係施策_*_2022」 の 17 dataset_id を統合し、
  広島県内 17 市町の農林漁業関係施策ポリゴン (連番 1408-1424, 単年 2022) を
  1 個の GeoDataFrame に縦結合した上で、<b>広島県の「保全策の地理構造」</b> ──
  施策面積分布・市町別パターン・NRG_AN (施策名/対象地区名) 文字列分類・
  KUIKI_CD 別の都市計画階層・農地転用 L24 との空間対比 ── を分析する研究記事である。

  17 dataset_id:
    1408 安芸高田市,  1409 呉市,        1410 広島市,    1411 江田島市,
    1412 坂町,        1413 三原市,      1414 三次市,    1415 庄原市,
    1416 世羅町,      1417 大竹市,      1418 竹原市,    1419 東広島市,
    1420 廿日市市,    1421 尾道市,      1422 府中市,    1423 福山市,
    1424 北広島町

  L24 (農地転用 17 件) との 1町スワップ:
    - L24 は 熊野町 (1392) を含み 坂町 を含まない (転用=動的指標)
    - L25 は 坂町 (1412) を含み 熊野町 を含まない (施策=静的指標)
  → 両記事の 17 件は重なる 16 市町 + 1 町スワップ。坂町=漁業基地のみ存在 (1 ポリゴン!)

研究の問い (RQ):
  広島県内 17 市町の農林漁業関係施策は、面積規模・施策種別 (NRG_AN 文字列)・
  地理分布・農地転用 L24 との対比でどのような構造を持ち、
  保全策と転用がどのようにせめぎあっているか？

仮説 H1〜H6:
  H1. 17 市町の総施策面積は <b>20,000 ha 以上</b> (L24 転用対象農地面積の数 % 規模)。
       施策ポリゴン件数は <b>2,000 件未満</b> ─ L24 (13,749 件) より遥かに少数の
       「制度的指定エリア」ベース。
  H2. NRG_AN 文字列は<b>地区名・施設名 (地名タイプ)</b> が大多数で、
       「保全策」「振興地域」などの<b>政策名タイプ</b>は少数。
       広島県の農林漁業施策は<b>『地区単位』</b>で運営される実態。
       「ダム」「池」「漁業」「ほ場整備」「林」など特定キーワードを含む施策ポリゴンは
       全体の <b>10-30% 程度</b>。
  H3. ジオメトリは <b>Polygon と MultiPolygon が混在</b>する。
       MultiPolygon 比率は<b>中山間市町ほど高く</b> (連続する集落と分散圃場の混在)、
       都市市町は単一 Polygon が主体。
  H4. KUIKI_CD は L24 と同じく<b>都市計画区域階層フラグ</b>と推定される。
       <b>0 (区域外/非線引き)</b> が中山間で支配的、
       <b>2 (調整区域?)</b> が都市市町に多い ─ L24 と整合的。
       <b>4 (?)</b> も多用される ─ L24 でも見られた値。
  H5. <b>農地転用 L24 と農林漁業施策 L25 の空間関係</b>: 同じ市町内で施策ポリゴンと
       転用ポリゴンの重なり面積を計測すると、<b>10-30% 程度の重複</b>がある ─
       「保全策のエリアでも転用が一部で発生する」せめぎあい構造。
       完全すみ分け (重複 0%) でもなく、完全重なり (重複 100%) でもない中間。
  H6. 広島市の NRG_AN は監査時 None と報告されたが、<b>実データでは大部分 (>95%) が
       地区名で埋まっている</b> ─ 監査時のサンプル行が偶々 None だっただけ。
       「サンプル 1 行で判断するな」というデータ取扱いの教訓。

要件 S 準拠: 1 分以内完走。1,912 ポリゴン (L24 の 13.5%) なので軽量。
            空間結合は L24 サブセットで bbox 絞込み。
要件 T 準拠: 県全域施策主題図 + 17 市町 small multiples + NRG_AN 分類マップ +
            L24 vs L25 重ね合わせマップ + KUIKI_CD 別塗り分け。
要件 Q 準拠: 図 9 枚以上、表 9 枚以上。

データ仕様 (7 列、17 市町で 100% 一致):
  ID           int32     ポリゴン ID (市町内連番)
  TOKEI_CD     int32     統計区分コード (1, 2, 3) ─ 施策種別フラグと推定
  CITY_CD      int32     市区町村コード (広島市は 105/106/108 のみ)
  KUIKI_CD     int32     区域コード (0-6, 仕様書未公開) ─ 都市計画区域階層
  NRG_AN       object    施策名/対象地区名 (大半が地区名、一部「ダム」「池」等)
  RITTEKI_CD   int32     立地コード (0, 1, 2)
  geometry     Polygon   または MultiPolygon。EPSG:2445 (旧広島県平面直角)
                         → EPSG:6671 に再投影して扱う

データ品質ノート:
  - CRS は EPSG:2445 (旧 JGD2000 平面直角系) で記録、L24 (EPSG:6671 = JGD2011) と異なる。
    本記事では <code>to_crs("EPSG:6671")</code> で統一する。
  - 坂町 (1412) は <b>1 ポリゴンのみ</b>「森山北漁業基地」(漁業施設) ─
    他市町と異なる<b>『漁業のみ』</b>の特異データ。
  - 監査時 (2026-05-03) の DL では 坂町 (1412)・竹原市 (1418) が KMZ 形式しか
    自動取得できなかったが、再取得で GeoJSON 単独 ZIP (rid=50870, 50918) を確保。
    → fetch_agroforestry_policy.py の pick_best() を「geojson 単独 rid を最優先」
      に修正することで 17 件全て GeoJSON で揃った。
  - 広島市 (1410) は監査時 NRG_AN サンプルが None だったが、
    実データでは 426 行中 424 行に地区名が入る。
    監査結論「広島市 NRG_AN None」は<b>サンプル 1 行偶発による誤読</b>。
"""
from __future__ import annotations
import sys, time, json, zipfile, io, re
from pathlib import Path

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

import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import geopandas as gpd

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

t0 = time.time()
print("=== L25 農林漁業関係施策 17 件 統合分析 ===", flush=True)

# =============================================================================
# 0. 定数
# =============================================================================
DATA_DIR = ROOT / "data" / "extras" / "L25_agroforestry_policy"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
L24_DIR = ROOT / "data" / "extras" / "L24_farmland_conversion"
TARGET_CRS = "EPSG:6671"

# (施策 dsid, 市町名, 行政_dsid, 都市タイプ, 地理タイプ)
# L24 の 17 市町から熊野町(1392) を除き、坂町(1412)を加えた構成
CITY_DEFS = [
    (1410, "広島市",     786, "政令市",         "都市"),
    (1409, "呉市",       797, "中核市",         "都市"),
    (1423, "福山市",     832, "中核市",         "都市"),
    (1419, "東広島市",   868, "施行時特例市",   "都市"),
    (1420, "廿日市市",   878, "市",             "都市"),
    (1421, "尾道市",     824, "市",             "都市"),
    (1413, "三原市",     814, "市",             "都市"),
    (1418, "竹原市",     807, "市",             "都市"),
    (1422, "府中市",     840, "市",             "都市"),
    (1417, "大竹市",     862, "市",             "都市"),
    (1414, "三次市",     850, "市",             "中山間"),
    (1415, "庄原市",     856, "市",             "中山間"),
    (1408, "安芸高田市", 888, "市",             "中山間"),
    (1411, "江田島市",   894, "市",             "離島"),
    (1412, "坂町",       916, "町",             "近郊町"),  # 漁業基地のみ
    (1424, "北広島町",   935, "町",             "中山間"),
    (1416, "世羅町",     941, "町",             "中山間"),
]

# 4 町 (農林漁業施策データ無指定 — L25 にない 4 市町)
NO_POLICY_CITIES = [
    ("熊野町", 911, "町", "近郊町"),     # L24 にあったが L25 に無い
    ("府中町", 900, "町", "近郊町"),
    ("海田町", 905, "町", "近郊町"),
]

CITY_REF = {
    "広島市":     {"area_km2": 906.7, "pop_k": 1189},
    "呉市":       {"area_km2": 352.8, "pop_k":  210},
    "福山市":     {"area_km2": 518.1, "pop_k":  459},
    "東広島市":   {"area_km2": 635.3, "pop_k":  198},
    "廿日市市":   {"area_km2": 489.5, "pop_k":  117},
    "尾道市":     {"area_km2": 285.1, "pop_k":  130},
    "三原市":     {"area_km2": 471.6, "pop_k":   90},
    "竹原市":     {"area_km2": 118.2, "pop_k":   24},
    "府中市":     {"area_km2": 195.8, "pop_k":   37},
    "大竹市":     {"area_km2":  78.7, "pop_k":   26},
    "三次市":     {"area_km2": 778.1, "pop_k":   50},
    "庄原市":     {"area_km2":1246.5, "pop_k":   33},
    "安芸高田市": {"area_km2": 537.8, "pop_k":   27},
    "江田島市":   {"area_km2": 100.7, "pop_k":   22},
    "府中町":     {"area_km2":  10.4, "pop_k":   53},
    "海田町":     {"area_km2":  13.8, "pop_k":   30},
    "熊野町":     {"area_km2":  33.7, "pop_k":   23},
    "坂町":       {"area_km2":  15.7, "pop_k":   12},
    "北広島町":   {"area_km2": 646.2, "pop_k":   17},
    "世羅町":     {"area_km2": 278.2, "pop_k":   15},
}

RTYPE_COLOR = {
    "都市":   "#cf222e",
    "中山間": "#1f883d",
    "離島":   "#0969da",
    "近郊町": "#bf3989",
}
ALL_POLICY_CITIES = [d[1] for d in CITY_DEFS]
ALL_CITY_ORDER = ALL_POLICY_CITIES + [n[0] for n in NO_POLICY_CITIES]

# NRG_AN 分類のキーワード辞書 (本記事独自定義)
# NRG_AN 文字列の特徴的キーワードから施策種別を推定する
NRG_KEYWORDS = [
    ("ダム",     ["ダム"]),
    ("ため池",   ["池", "溜池", "ため池"]),
    ("漁業",     ["漁業", "漁港", "漁場", "養殖"]),
    ("林業",     ["林", "森林", "山林"]),
    ("ほ場整備", ["ほ場整備", "圃場整備", "ほ場", "圃場", "整備", "区画整理"]),
    ("基地・施設", ["基地", "施設", "場"]),
]
# 上記キーワードに該当しない NRG_AN は「地区名」(地名のみ) と推定


def classify_nrg_an(s):
    """NRG_AN 文字列を施策種別に分類。複数該当時は最優先タイプ。"""
    if pd.isna(s) or s is None or str(s).strip() == "":
        return "_欠損"
    text = str(s)
    for label, kws in NRG_KEYWORDS:
        for kw in kws:
            if kw in text:
                return label
    return "地区名のみ"


# =============================================================================
# 1. 17 GeoJSON 統合読み込み
# =============================================================================
print("\n[1] 17 農林漁業施策 GeoJSON 統合読み込み", flush=True)
t1 = time.time()


def load_geojson_zip(zip_path: Path) -> gpd.GeoDataFrame:
    with zipfile.ZipFile(zip_path) as zf:
        gjs = [n for n in zf.namelist() if n.lower().endswith(".geojson")]
        if not gjs:
            raise FileNotFoundError(f"no .geojson in {zip_path.name}")
        with zf.open(gjs[0]) as f:
            return gpd.read_file(io.BytesIO(f.read()))


COMMON_COLS_7 = ["ID", "TOKEI_CD", "CITY_CD", "KUIKI_CD", "NRG_AN",
                 "RITTEKI_CD", "geometry"]

frames = []
load_log = []
for dsid, name, _adm, ctype, rtype in CITY_DEFS:
    z = DATA_DIR / f"agro_{dsid}_{name}.zip"
    g = load_geojson_zip(z)
    extra = sorted(set(g.columns) - set(COMMON_COLS_7))
    g = g[COMMON_COLS_7].copy()
    g["src_city"] = name
    g["src_dsid"] = dsid
    g["ctype"] = ctype
    g["rtype"] = rtype
    if g.crs is None:
        g = g.set_crs("EPSG:2445", allow_override=True)
    # CRS 統一: EPSG:2445 → EPSG:6671
    g = g.to_crs(TARGET_CRS)
    n_multi = int((g.geom_type == "MultiPolygon").sum())
    n_single = int((g.geom_type == "Polygon").sum())
    n_nrg_null = int(g["NRG_AN"].isna().sum())
    load_log.append({
        "dsid": dsid, "city": name, "ctype": ctype, "rtype": rtype,
        "n_poly": len(g),
        "n_polygon": n_single,
        "n_multipoly": n_multi,
        "multi_pct": round(n_multi / max(len(g), 1) * 100, 1),
        "n_nrg_null": n_nrg_null,
        "nrg_null_pct": round(n_nrg_null / max(len(g), 1) * 100, 1),
        "city_cds": ",".join(map(str, sorted(g["CITY_CD"].unique()))),
        "kuiki_cds": ",".join(map(str, sorted(g["KUIKI_CD"].unique()))),
        "tokei_cds": ",".join(map(str, sorted(g["TOKEI_CD"].unique()))),
        "ritteki_cds": ",".join(map(str, sorted(g["RITTEKI_CD"].unique()))),
        "extra_cols": ",".join(extra) if extra else "-",
    })
    frames.append(g)

agro = gpd.GeoDataFrame(pd.concat(frames, ignore_index=True),
                        geometry="geometry", crs=TARGET_CRS)
N_POLY = len(agro)

# 派生指標
agro["geom_area_m2"] = agro.geometry.area
agro["geom_area_ha"] = agro["geom_area_m2"] / 1e4
agro["geom_type"] = agro.geom_type
agro["nrg_class"] = agro["NRG_AN"].apply(classify_nrg_an)

AREA_TOTAL_HA = float(agro["geom_area_ha"].sum())
AREA_TOTAL_KM2 = AREA_TOTAL_HA / 100
print(f"  施策ポリゴン: {N_POLY:,} 件 × {len(agro.columns)} 列", flush=True)
print(f"  実測面積合計: {AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.1f} km²", flush=True)
print(f"  Polygon : MultiPolygon = "
      f"{int((agro.geom_type=='Polygon').sum()):,} : "
      f"{int((agro.geom_type=='MultiPolygon').sum()):,}", flush=True)

# 行政区域 20 件 (= 17 施策市町 + 3 施策無し町) を読み込み
admin_frames = []
for _dsid, name, adm_ds, _ct, _rt in CITY_DEFS:
    z = ADMIN_DIR / f"admin_{adm_ds}_{name}.zip"
    g = load_geojson_zip(z)
    g["city"] = name
    g["policy_status"] = "施策あり"
    admin_frames.append(g)
for name, adm_ds, _ctype, _region in NO_POLICY_CITIES:
    z = ADMIN_DIR / f"admin_{adm_ds}_{name}.zip"
    g = load_geojson_zip(z)
    g["city"] = name
    g["policy_status"] = "施策無し"
    admin_frames.append(g)

# CRS 揃え
admin_all = gpd.GeoDataFrame(pd.concat(admin_frames, ignore_index=True),
                              geometry="geometry", crs=admin_frames[0].crs)
admin_all = admin_all.to_crs(TARGET_CRS)
admin_diss = admin_all.dissolve(by="city", as_index=False,
                                 aggfunc={"policy_status": "first"})
admin_diss["admin_area_km2"] = admin_diss.geometry.area / 1e6
print(f"  行政区域 dissolve: {len(admin_diss)} 件 ({time.time()-t1:.2f}s)", flush=True)

# =============================================================================
# 2. 派生指標の集計 (市町単位 + クロス集計)
# =============================================================================
print("\n[2] 派生指標計算", flush=True)
t1 = time.time()


def aggregate_by_city(d: gpd.GeoDataFrame) -> pd.DataFrame:
    agg = d.groupby("src_city").agg(
        n_poly=("geom_area_ha", "size"),
        area_ha_sum=("geom_area_ha", "sum"),
        area_ha_median=("geom_area_ha", "median"),
        area_ha_max=("geom_area_ha", "max"),
        n_multipoly=("geom_type", lambda s: int((s == "MultiPolygon").sum())),
        n_single=("geom_type", lambda s: int((s == "Polygon").sum())),
        n_nrg_null=("NRG_AN", lambda s: int(s.isna().sum())),
    ).reset_index().rename(columns={"src_city": "city"})
    agg["multi_pct"] = (agg["n_multipoly"] / agg["n_poly"] * 100).round(1)
    agg["nrg_null_pct"] = (agg["n_nrg_null"] / agg["n_poly"] * 100).round(1)
    agg["city_area_km2"] = agg["city"].map(lambda c: CITY_REF[c]["area_km2"])
    agg["city_pop_k"] = agg["city"].map(lambda c: CITY_REF[c]["pop_k"])
    agg["policy_per_km2"] = (agg["area_ha_sum"]
                              / agg["city_area_km2"]).round(3)
    agg["policy_share_area"] = (agg["area_ha_sum"]
                                  / (agg["city_area_km2"] * 100) * 100).round(2)
    agg["policy_per_capita_a"] = (agg["area_ha_sum"] * 100
                                    / (agg["city_pop_k"] * 1000)).round(2)
    agg["ctype"] = agg["city"].map(
        lambda c: next(d[3] for d in CITY_DEFS if d[1] == c))
    agg["rtype"] = agg["city"].map(
        lambda c: next(d[4] for d in CITY_DEFS if d[1] == c))
    return agg


city_agg = aggregate_by_city(agro)
city_agg = city_agg.set_index("city").reindex(ALL_POLICY_CITIES).reset_index()
print(f"  市町別集計: {len(city_agg)} 件", flush=True)

# NRG_AN 分類別集計
nrg_overall = agro.groupby("nrg_class").agg(
    n=("geom_area_ha", "size"),
    area_ha_sum=("geom_area_ha", "sum"),
).reset_index()
nrg_overall["n_pct"] = (nrg_overall["n"] / N_POLY * 100).round(1)
nrg_overall["area_pct"] = (nrg_overall["area_ha_sum"]
                            / AREA_TOTAL_HA * 100).round(1)

nrg_cross_n = (agro.groupby(["src_city", "nrg_class"]).size()
                .unstack(fill_value=0)
                .reindex(index=ALL_POLICY_CITIES, fill_value=0))

# KUIKI_CD クロス
kuiki_cross_n = (agro.groupby(["src_city", "KUIKI_CD"]).size()
                  .unstack(fill_value=0)
                  .reindex(index=ALL_POLICY_CITIES, fill_value=0))
kuiki_overall = agro.groupby("KUIKI_CD").agg(
    n=("geom_area_ha", "size"),
    area_ha_sum=("geom_area_ha", "sum"),
).reset_index()
kuiki_overall["n_pct"] = (kuiki_overall["n"] / N_POLY * 100).round(1)
kuiki_overall["area_pct"] = (kuiki_overall["area_ha_sum"]
                              / AREA_TOTAL_HA * 100).round(1)

# TOKEI_CD クロス
tokei_overall = agro.groupby("TOKEI_CD").agg(
    n=("geom_area_ha", "size"),
    area_ha_sum=("geom_area_ha", "sum"),
).reset_index()
tokei_overall["n_pct"] = (tokei_overall["n"] / N_POLY * 100).round(1)
tokei_overall["area_pct"] = (tokei_overall["area_ha_sum"]
                              / AREA_TOTAL_HA * 100).round(1)

# 規模クラス分類
def _scale_class(ha_value):
    if ha_value < 0.01:    return "0_微小(<0.01 ha)"
    elif ha_value < 0.1:   return "1_小(0.01-0.1 ha)"
    elif ha_value < 1.0:   return "2_中(0.1-1 ha)"
    elif ha_value < 10.0:  return "3_大(1-10 ha)"
    elif ha_value < 100.0: return "4_超大(10-100 ha)"
    else:                  return "5_巨大(≥100 ha)"

agro["scale_class"] = agro["geom_area_ha"].apply(_scale_class)
scale_overall = agro.groupby("scale_class").agg(
    n=("geom_area_ha", "size"),
    area_ha_sum=("geom_area_ha", "sum"),
).reset_index()
scale_overall["n_pct"] = (scale_overall["n"] / N_POLY * 100).round(1)
scale_overall["area_pct"] = (scale_overall["area_ha_sum"]
                              / AREA_TOTAL_HA * 100).round(1)

print(f"  集計完了 ({time.time()-t1:.2f}s)", flush=True)

# =============================================================================
# 2b. L24 農地転用ポリゴンとの空間関係 (重複 vs すみ分け)
# =============================================================================
print("\n[2b] L24 農地転用との空間重複分析", flush=True)
t1 = time.time()

L24_DEFS = [
    (1391, "安芸高田市"), (1392, "熊野町"), (1393, "呉市"), (1394, "広島市"),
    (1395, "江田島市"), (1396, "三原市"), (1397, "三次市"), (1398, "庄原市"),
    (1399, "世羅町"), (1400, "大竹市"), (1401, "竹原市"), (1402, "東広島市"),
    (1403, "廿日市市"), (1404, "尾道市"), (1405, "府中市"), (1406, "福山市"),
    (1407, "北広島町"),
]

farm_frames = []
for dsid, name in L24_DEFS:
    z = L24_DIR / f"farm_{dsid}_{name}.zip"
    if not z.exists():
        continue
    try:
        f = load_geojson_zip(z)
        f = f[["KUIKI_CD", "AREA", "geometry"]].copy()
        f["AREA"] = pd.to_numeric(f["AREA"], errors="coerce")
        f["src_city"] = name
        if f.crs is None:
            f = f.set_crs(TARGET_CRS, allow_override=True)
        f = f.to_crs(TARGET_CRS)
        farm_frames.append(f)
    except Exception as e:
        print(f"  L24 read fail {name}: {e}", flush=True)

farm = gpd.GeoDataFrame(pd.concat(farm_frames, ignore_index=True),
                          geometry="geometry", crs=TARGET_CRS)
farm["geom_area_ha"] = farm.geometry.area / 1e4
print(f"  L24 転用ポリゴン: {len(farm):,} 件 (合計 {farm['geom_area_ha'].sum():,.0f} ha)",
      flush=True)

common_cities = sorted(set(d[1] for d in CITY_DEFS) & set(d[1] for d in L24_DEFS))
print(f"  共通 市町: {len(common_cities)} 件", flush=True)

# 重複面積を unary_union 経由で計算 (軽量)
overlap_log = []
from shapely.validation import make_valid
for city in common_cities:
    a_sub = agro[agro["src_city"] == city]
    f_sub = farm[farm["src_city"] == city]
    a_total_ha = float(a_sub["geom_area_ha"].sum())
    f_total_ha = float(f_sub["geom_area_ha"].sum())
    if len(a_sub) == 0 or len(f_sub) == 0:
        overlap_log.append({"city": city, "a_total_ha": a_total_ha,
                             "f_total_ha": f_total_ha, "overlap_ha": 0.0,
                             "overlap_pct_a": 0.0, "overlap_pct_f": 0.0})
        continue
    overlap_ha = 0.0
    # 第1試行: union_all → make_valid → intersection
    try:
        a_uni = make_valid(a_sub.geometry.union_all())
        f_uni = make_valid(f_sub.geometry.union_all())
        inter = a_uni.intersection(f_uni)
        overlap_ha = inter.area / 1e4
    except Exception:
        # 第2試行: buffer(0) で自己交差を解消してから union
        try:
            a_buf = a_sub.geometry.buffer(0).union_all()
            f_buf = f_sub.geometry.buffer(0).union_all()
            inter = a_buf.intersection(f_buf)
            overlap_ha = inter.area / 1e4
        except Exception as e2:
            print(f"    overlay fallback fail {city}: {e2}", flush=True)
            overlap_ha = 0.0
    overlap_log.append({
        "city": city,
        "a_total_ha": a_total_ha,
        "f_total_ha": f_total_ha,
        "overlap_ha": overlap_ha,
        "overlap_pct_a": (overlap_ha / a_total_ha * 100) if a_total_ha > 0 else 0,
        "overlap_pct_f": (overlap_ha / f_total_ha * 100) if f_total_ha > 0 else 0,
    })

overlap_df = pd.DataFrame(overlap_log)
overlap_df["rtype"] = overlap_df["city"].map(
    lambda c: next(d[4] for d in CITY_DEFS if d[1] == c))
print(f"  L24 vs L25 重複分析: {len(overlap_df)} 市町 "
      f"({time.time()-t1:.2f}s)", flush=True)

total_overlap_ha = float(overlap_df["overlap_ha"].sum())
total_a_ha = float(overlap_df["a_total_ha"].sum())
total_f_ha = float(overlap_df["f_total_ha"].sum())
overlap_pct_a_total = total_overlap_ha / total_a_ha * 100 if total_a_ha > 0 else 0
overlap_pct_f_total = total_overlap_ha / total_f_ha * 100 if total_f_ha > 0 else 0
print(f"  施策 {total_a_ha:,.0f} ha / 転用 {total_f_ha:,.0f} ha "
      f"/ 重複 {total_overlap_ha:,.0f} ha "
      f"(施策の {overlap_pct_a_total:.1f}% / "
      f"転用の {overlap_pct_f_total:.2f}%)", flush=True)

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

city_agg.to_csv(ASSETS / "L25_city_summary.csv",
                 index=False, encoding="utf-8-sig")

no_policy_df = pd.DataFrame([{
    "city": name, "ctype": ctype, "rtype": region,
    "city_area_km2": CITY_REF[name]["area_km2"],
    "city_pop_k": CITY_REF[name]["pop_k"],
} for name, _adm, ctype, region in NO_POLICY_CITIES])
no_policy_df.to_csv(ASSETS / "L25_no_policy_cities.csv",
                     index=False, encoding="utf-8-sig")

agro_attr = agro.drop(columns=["geometry"]).copy()
agro_attr["geom_area_m2"] = agro_attr["geom_area_m2"].round(1)
agro_attr["geom_area_ha"] = agro_attr["geom_area_ha"].round(4)
agro_attr.sort_values("geom_area_ha", ascending=False).head(2000).to_csv(
    ASSETS / "L25_policy_polygons_top2000.csv",
    index=False, encoding="utf-8-sig")

pd.DataFrame(load_log).to_csv(ASSETS / "L25_load_log.csv",
                                index=False, encoding="utf-8-sig")
nrg_overall.to_csv(ASSETS / "L25_nrg_overall.csv",
                    index=False, encoding="utf-8-sig")
nrg_cross_n.to_csv(ASSETS / "L25_nrg_cross_count.csv",
                     encoding="utf-8-sig")
kuiki_cross_n.to_csv(ASSETS / "L25_kuiki_count.csv",
                       encoding="utf-8-sig")
kuiki_overall.to_csv(ASSETS / "L25_kuiki_overall.csv",
                       index=False, encoding="utf-8-sig")
tokei_overall.to_csv(ASSETS / "L25_tokei_overall.csv",
                       index=False, encoding="utf-8-sig")
scale_overall.to_csv(ASSETS / "L25_scale_overall.csv",
                       index=False, encoding="utf-8-sig")
overlap_df.to_csv(ASSETS / "L25_overlap_l24.csv",
                    index=False, encoding="utf-8-sig")

top20_large = (agro.drop(columns=["geometry"])
                .nlargest(20, "geom_area_ha")
                [["src_city", "ctype", "rtype", "CITY_CD", "KUIKI_CD",
                  "NRG_AN", "nrg_class", "geom_type",
                  "geom_area_ha", "scale_class"]])
top20_large.to_csv(ASSETS / "L25_top20_large.csv",
                     index=False, encoding="utf-8-sig")

# NRG_AN 頻度 top 50
nrg_vc = agro["NRG_AN"].dropna().value_counts().head(50)
nrg_freq = nrg_vc.reset_index()
nrg_freq.columns = ["nrg_an", "freq"]
nrg_freq.to_csv(ASSETS / "L25_nrg_freq_top50.csv",
                  index=False, encoding="utf-8-sig")

print(f"  CSV 出力完了 ({time.time()-t1:.2f}s)", flush=True)

# =============================================================================
# 4. 図の作成 (9 枚)
# =============================================================================
print("\n[4] 図の作成", flush=True)
t1 = time.time()

# NRG_AN 分類別の色
NRG_COLOR = {
    "ダム":         "#1f77b4",   # 青
    "ため池":       "#17becf",   # 水色
    "漁業":         "#003366",   # 紺
    "林業":         "#2ca02c",   # 緑
    "ほ場整備":     "#ff7f0e",   # オレンジ
    "基地・施設":   "#9467bd",   # 紫
    "地区名のみ":   "#7f7f7f",   # 灰
    "_欠損":        "#dddddd",   # 薄灰
}

# --- Fig 1: 県全域 施策ポリゴン主題図 (rtype 色) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_policy = admin_diss[admin_diss["policy_status"] == "施策あり"]
admin_policy.plot(ax=ax, facecolor="#fafff0", edgecolor="#444", linewidth=0.5)
rtype_count = {rt: 0 for rt in RTYPE_COLOR}
for rt, col in RTYPE_COLOR.items():
    sub = agro[agro["rtype"] == rt]
    if len(sub) > 0:
        sub.plot(ax=ax, facecolor=col, alpha=0.65, edgecolor="none")
        rtype_count[rt] = len(sub)
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.4)
ax.set_aspect("equal")
ax.set_title(f"広島県 農林漁業関係施策ポリゴン 全 {N_POLY:,} 件 — 17 市町 (2022 単年)",
              fontsize=13)
legend_handles = [Patch(facecolor=v,
                          label=f"{k} ({rtype_count[k]:,} 件)",
                          edgecolor="none", alpha=0.65)
                   for k, v in RTYPE_COLOR.items() if rtype_count[k] > 0]
ax.legend(handles=legend_handles, loc="lower left", fontsize=9, framealpha=0.92)
ax.set_xticks([]); ax.set_yticks([])
for _, r in city_agg.nlargest(5, "area_ha_sum").iterrows():
    cnt = admin_diss[admin_diss["city"] == r["city"]]
    if len(cnt) > 0:
        ctr = cnt.geometry.iloc[0].representative_point()
        ax.annotate(f"{r['city']}\n{r['n_poly']:,}件/{r['area_ha_sum']:.0f}ha",
                     (ctr.x, ctr.y), fontsize=8,
                     bbox=dict(boxstyle="round,pad=0.2",
                                fc="white", ec="#888", alpha=0.85))
plt.tight_layout()
plt.savefig(ASSETS / "L25_fig01_overview.png", dpi=130)
plt.close("all")
print(f"  Fig 1 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 2: 施策あり 17 vs 施策無し 3 (1町スワップ強調) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss[admin_diss["policy_status"] == "施策あり"].plot(
    ax=ax, facecolor="#cdeacc", edgecolor="#444", linewidth=0.6)
admin_diss[admin_diss["policy_status"] == "施策無し"].plot(
    ax=ax, facecolor="#f7c9c9", edgecolor="#444", linewidth=0.6)
agro.plot(ax=ax, facecolor="#1f883d", alpha=0.55, edgecolor="none")
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.5)
ax.set_aspect("equal")
ax.set_title("農林漁業関係施策の有無 — 17 市町 (緑) vs 3 町 (赤)",
              fontsize=13)
legend_handles_f2 = [
    Patch(facecolor="#cdeacc", edgecolor="#444", label="施策あり 17 市町"),
    Patch(facecolor="#f7c9c9", edgecolor="#444", label="施策無し 3 町"),
    Patch(facecolor="#1f883d", edgecolor="none", alpha=0.55,
            label=f"施策ポリゴン ({N_POLY:,} 件)"),
]
ax.legend(handles=legend_handles_f2, loc="lower left", fontsize=11,
           framealpha=0.92)
ax.set_xticks([]); ax.set_yticks([])
# 3町 + L24 にあったがL25 に無い熊野町を強調
for name, _adm, _ct, region in NO_POLICY_CITIES:
    cnt = admin_diss[admin_diss["city"] == name]
    if len(cnt) > 0:
        ctr = cnt.geometry.iloc[0].representative_point()
        ax.annotate(f"{name}\n(施策無し)", (ctr.x, ctr.y), fontsize=9,
                     ha="center",
                     bbox=dict(boxstyle="round,pad=0.2", fc="#f7c9c9",
                                ec="#cf222e", alpha=0.92))
# 坂町をハイライト (L24 に無いが L25 にある, 漁業基地のみ 1 ポリゴン)
sak = agro[agro["src_city"] == "坂町"]
if len(sak) > 0:
    ctr = sak.geometry.iloc[0].representative_point()
    ax.annotate(f"坂町\n(L25のみ\n漁業基地1件)",
                 (ctr.x, ctr.y), fontsize=9, color="#600",
                 xytext=(15, 15), textcoords="offset points",
                 arrowprops=dict(arrowstyle="->", color="#cf222e", lw=1.0),
                 bbox=dict(boxstyle="round,pad=0.2",
                            fc="white", ec="#cf222e", alpha=0.95))
plt.tight_layout()
plt.savefig(ASSETS / "L25_fig02_policy_vs_nopolicy.png", dpi=130)
plt.close("all")
print(f"  Fig 2 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 3: 17 市町 small multiples ---
fig, axes = plt.subplots(4, 5, figsize=(20, 16))
for ax_, (dsid, name, _adm, ctype, rtype) in zip(axes.flat, CITY_DEFS):
    cnt = admin_diss[admin_diss["city"] == name]
    cnt.plot(ax=ax_, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
    sub = agro[agro["src_city"] == name]
    if len(sub) > 0:
        sub.plot(ax=ax_, facecolor=RTYPE_COLOR[rtype],
                  alpha=0.65, edgecolor="none")
    cnt.boundary.plot(ax=ax_, color="#333", linewidth=0.4)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
    n = len(sub)
    a_ha = sub["geom_area_ha"].sum()
    ax_.set_title(f"{name} ({rtype})\n{n:,}件 / {a_ha:,.0f} ha",
                   fontsize=10)
# 18 番目: 施策無し 3 町
ax_last = axes.flat[17]
admin_diss[admin_diss["policy_status"] == "施策無し"].plot(
    ax=ax_last, facecolor="#f7c9c9", edgecolor="#444", linewidth=0.5)
ax_last.set_aspect("equal")
ax_last.set_xticks([]); ax_last.set_yticks([])
ax_last.set_title("施策無し 3 町\n(熊野町・府中町・海田町)", fontsize=10)
for name, _adm, _ct, _r in NO_POLICY_CITIES:
    cnt = admin_diss[admin_diss["city"] == name]
    if len(cnt) > 0:
        ctr = cnt.geometry.iloc[0].representative_point()
        ax_last.annotate(name, (ctr.x, ctr.y), fontsize=8, ha="center")
for k in [18, 19]:
    axes.flat[k].axis("off")
plt.suptitle("市町別 農林漁業関係施策ポリゴン small multiples (17 + 無し 3 町)",
              fontsize=14, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L25_fig03_small_multiples.png", dpi=110)
plt.close("all")
print(f"  Fig 3 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 4: 市町別 ランキング 3連 (面積/件数/MultiPolygon比) ---
fig, axes = plt.subplots(1, 3, figsize=(18, 9))

# 左: 総面積
order_a = city_agg.sort_values("area_ha_sum", ascending=True)
colors_a = [RTYPE_COLOR[rt] for rt in order_a["rtype"]]
ax0 = axes[0]
ax0.barh(order_a["city"], order_a["area_ha_sum"], color=colors_a,
          edgecolor="white")
for i, v in enumerate(order_a["area_ha_sum"]):
    ax0.text(v + 50, i, f"{v:,.0f}", va="center", fontsize=8)
ax0.set_xlabel("施策面積 (ha, 実測)")
ax0.set_title("市町別 総施策面積", fontsize=12)

# 中: 件数 (log)
order_n = city_agg.sort_values("n_poly", ascending=True)
colors_n = [RTYPE_COLOR[rt] for rt in order_n["rtype"]]
ax1 = axes[1]
ax1.barh(order_n["city"], order_n["n_poly"], color=colors_n,
          edgecolor="white")
ax1.set_xscale("log")
for i, v in enumerate(order_n["n_poly"]):
    ax1.text(v * 1.1, i, f"{v:,}", va="center", fontsize=8)
ax1.set_xlabel("ポリゴン件数 (log)")
ax1.set_title("市町別 件数", fontsize=12)

# 右: MultiPolygon 比率
order_m = city_agg.sort_values("multi_pct", ascending=True)
colors_m = [RTYPE_COLOR[rt] for rt in order_m["rtype"]]
ax2 = axes[2]
ax2.barh(order_m["city"], order_m["multi_pct"], color=colors_m,
          edgecolor="white")
for i, v in enumerate(order_m["multi_pct"]):
    ax2.text(v + 1, i, f"{v:.1f}%", va="center", fontsize=8)
ax2.set_xlabel("MultiPolygon 比率 (%)")
ax2.set_title("市町別 MultiPolygon 比率", fontsize=12)

handles = [Patch(facecolor=v, label=k, edgecolor="white")
           for k, v in RTYPE_COLOR.items()]
ax0.legend(handles=handles, loc="lower right", fontsize=9)
plt.tight_layout()
plt.savefig(ASSETS / "L25_fig04_city_ranking.png", dpi=130)
plt.close("all")
print(f"  Fig 4 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 5: NRG_AN 分類分布 + 市町クロス ---
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# 左: NRG_AN 分類別の件数 + 面積 双子バー
ax0 = axes[0]
nrg_sorted = nrg_overall.sort_values("n", ascending=True)
y = np.arange(len(nrg_sorted))
height = 0.4
b1 = ax0.barh(y - height/2, nrg_sorted["n_pct"], height,
                label="件数 %", color="#0969da")
b2 = ax0.barh(y + height/2, nrg_sorted["area_pct"], height,
                label="面積 %", color="#cf222e")
ax0.set_yticks(y)
ax0.set_yticklabels(nrg_sorted["nrg_class"])
for i, (n, a) in enumerate(zip(nrg_sorted["n_pct"], nrg_sorted["area_pct"])):
    ax0.text(n + 0.3, i - height/2, f"{n:.1f}", va="center", fontsize=8)
    ax0.text(a + 0.3, i + height/2, f"{a:.1f}", va="center", fontsize=8,
              color="#600")
ax0.set_xlabel("全体比率 (%)")
ax0.set_title("NRG_AN 分類別 件数 % vs 面積 %", fontsize=12)
ax0.legend(loc="lower right", fontsize=10)
ax0.grid(True, axis="x", alpha=0.3)

# 右: 市町別 NRG_AN 分類 stacked bar (件数比率)
ax1 = axes[1]
nrg_pct = nrg_cross_n.div(nrg_cross_n.sum(axis=1).replace(0, 1), axis=0) * 100
nrg_pct.plot(kind="barh", stacked=True, ax=ax1,
              color=[NRG_COLOR.get(c, "#888") for c in nrg_pct.columns],
              edgecolor="white", linewidth=0.3)
ax1.set_xlabel("件数比率 (%)")
ax1.set_title("市町別 NRG_AN 分類 (件数 %)", fontsize=12)
ax1.legend(title="分類", loc="lower right", fontsize=8,
            bbox_to_anchor=(1.0, 0.0))

plt.tight_layout()
plt.savefig(ASSETS / "L25_fig05_nrg_class.png", dpi=130)
plt.close("all")
print(f"  Fig 5 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 6: NRG_AN 分類別 県全域マップ ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_policy.plot(ax=ax, facecolor="#fafff0", edgecolor="#444", linewidth=0.5)
nrg_legend = []
# 「地区名のみ」を最下層、特徴的な分類を最上層に
plot_order = ["地区名のみ", "_欠損", "ほ場整備", "ため池", "ダム",
              "林業", "基地・施設", "漁業"]
for cls in plot_order:
    sub = agro[agro["nrg_class"] == cls]
    if len(sub) > 0:
        col = NRG_COLOR.get(cls, "#888")
        sub.plot(ax=ax, facecolor=col, alpha=0.7, edgecolor="none")
        nrg_legend.append(Patch(facecolor=col, alpha=0.7,
                                  label=f"{cls} ({len(sub):,})", edgecolor="none"))
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.4)
ax.set_aspect("equal")
ax.set_title(f"NRG_AN 分類別 県全域マップ — 施策の質的分布",
              fontsize=13)
ax.legend(handles=nrg_legend, loc="lower left", fontsize=9, framealpha=0.92)
ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout()
plt.savefig(ASSETS / "L25_fig06_nrg_map.png", dpi=130)
plt.close("all")
print(f"  Fig 6 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 7: L24 (転用) vs L25 (施策) 重ね合わせマップ ---
fig, axes = plt.subplots(1, 2, figsize=(18, 9))

# 左: 重ね合わせマップ
ax0 = axes[0]
admin_diss.plot(ax=ax0, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
# L24 転用 = 赤
farm.plot(ax=ax0, facecolor="#cf222e", alpha=0.4, edgecolor="none")
# L25 施策 = 緑
agro.plot(ax=ax0, facecolor="#1f883d", alpha=0.55, edgecolor="none")
admin_diss.boundary.plot(ax=ax0, color="#333", linewidth=0.4)
ax0.set_aspect("equal")
ax0.set_xticks([]); ax0.set_yticks([])
ax0.set_title("L24 農地転用 (赤, {:,} 件) ⊕ L25 施策 (緑, {:,} 件)".format(
    len(farm), N_POLY), fontsize=12)
ax0.legend(handles=[
    Patch(facecolor="#cf222e", alpha=0.4, edgecolor="none",
            label=f"L24 農地転用 {farm['geom_area_ha'].sum():,.0f} ha"),
    Patch(facecolor="#1f883d", alpha=0.55, edgecolor="none",
            label=f"L25 農林漁業施策 {AREA_TOTAL_HA:,.0f} ha"),
], loc="lower left", fontsize=10, framealpha=0.92)

# 右: 市町別 重複率 (施策 vs 転用)
ax1 = axes[1]
order_o = overlap_df.sort_values("overlap_pct_a", ascending=True)
colors_o = [RTYPE_COLOR[rt] for rt in order_o["rtype"]]
ax1.barh(order_o["city"], order_o["overlap_pct_a"], color=colors_o,
          edgecolor="white")
for i, v in enumerate(order_o["overlap_pct_a"]):
    ax1.text(v + 0.5, i, f"{v:.1f}%", va="center", fontsize=8)
ax1.set_xlabel("施策面積のうち転用と重複する割合 (%)")
ax1.set_title(f"市町別 施策 ∩ 転用 重複率 (共通 {len(overlap_df)} 市町)",
                fontsize=12)
handles_r = [Patch(facecolor=v, label=k) for k, v in RTYPE_COLOR.items()]
ax1.legend(handles=handles_r, loc="lower right", fontsize=9)
ax1.grid(True, axis="x", alpha=0.3)

plt.tight_layout()
plt.savefig(ASSETS / "L25_fig07_overlap_l24.png", dpi=130)
plt.close("all")
print(f"  Fig 7 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 8: KUIKI_CD 別 県全域マップ + 市町クロス ---
kuiki_palette = {
    0: "#1f883d",
    1: "#cf222e",
    2: "#a371f7",
    3: "#0969da",
    4: "#bf3989",
    5: "#fb8500",
    6: "#666666",
}
fig, axes = plt.subplots(1, 2, figsize=(18, 9))

ax0 = axes[0]
admin_diss.plot(ax=ax0, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
kuiki_legend = []
for kc, col in kuiki_palette.items():
    sub = agro[agro["KUIKI_CD"] == kc]
    if len(sub) > 0:
        sub.plot(ax=ax0, facecolor=col, alpha=0.65, edgecolor="none")
        kuiki_legend.append(
            Patch(facecolor=col, alpha=0.65, edgecolor="none",
                   label=f"KUIKI_CD={kc} ({len(sub):,})"))
admin_diss.boundary.plot(ax=ax0, color="#333", linewidth=0.4)
ax0.set_aspect("equal")
ax0.set_xticks([]); ax0.set_yticks([])
ax0.set_title("施策ポリゴン KUIKI_CD 別 (フラグ値、仕様書未公開)", fontsize=12)
ax0.legend(handles=kuiki_legend, loc="lower left", fontsize=8, framealpha=0.92)

# 右: KUIKI_CD × 市町 stacked bar
ax1 = axes[1]
kc_pct = kuiki_cross_n.div(kuiki_cross_n.sum(axis=1).replace(0, 1), axis=0) * 100
kc_pct.plot(kind="barh", stacked=True, ax=ax1,
             color=[kuiki_palette.get(c, "#888") for c in kc_pct.columns],
             edgecolor="white", linewidth=0.3)
ax1.set_xlabel("件数比率 (%)")
ax1.set_title("市町別 KUIKI_CD 構成 (件数 %)", fontsize=12)
ax1.legend(title="KUIKI_CD", loc="lower right", fontsize=8,
            bbox_to_anchor=(1.0, 0.0))

plt.tight_layout()
plt.savefig(ASSETS / "L25_fig08_kuiki_cd.png", dpi=130)
plt.close("all")
print(f"  Fig 8 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 9: 市町総面積 vs 施策面積 + 1人あたり (散布 2 連) ---
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# 左: 市域面積 × 施策面積 (log-log)
ax0 = axes[0]
for rt, col in RTYPE_COLOR.items():
    sub = city_agg[city_agg["rtype"] == rt]
    if len(sub) > 0:
        ax0.scatter(sub["city_area_km2"], sub["area_ha_sum"], c=col, s=180,
                     alpha=0.85, edgecolor="#222", linewidth=0.5,
                     label=f"{rt}")
        for _, r in sub.iterrows():
            ax0.annotate(r["city"],
                          (r["city_area_km2"], r["area_ha_sum"]),
                          fontsize=9, xytext=(5, 5),
                          textcoords="offset points")
xs = np.linspace(5, 1500, 50)
ax0.plot(xs, xs * 0.1, color="#888", linestyle=":", linewidth=1,
          label="0.1% (= 0.1 ha/km²)")
ax0.plot(xs, xs * 1, color="black", linestyle="--", linewidth=1,
          label="1% (= 1 ha/km²)")
ax0.plot(xs, xs * 10, color="#888", linestyle=":", linewidth=1,
          label="10% (= 10 ha/km²)")
ax0.set_xscale("log")
ax0.set_yscale("log")
ax0.set_xlabel("市町総面積 (km²)")
ax0.set_ylabel("施策面積合計 (ha)")
ax0.set_title("市域面積 × 施策面積 (log-log)", fontsize=12)
ax0.legend(loc="lower right", fontsize=9)
ax0.grid(True, which="both", alpha=0.3)

# 右: 人口 × 1人あたり施策面積 (a/人)
ax1 = axes[1]
for rt, col in RTYPE_COLOR.items():
    sub = city_agg[city_agg["rtype"] == rt]
    if len(sub) > 0:
        ax1.scatter(sub["city_pop_k"] * 1000,
                     sub["policy_per_capita_a"].clip(lower=0.01),
                     c=col, s=180, alpha=0.85, edgecolor="#222",
                     linewidth=0.5, label=f"{rt}")
        for _, r in sub.iterrows():
            ax1.annotate(r["city"],
                          (r["city_pop_k"] * 1000,
                           max(r["policy_per_capita_a"], 0.01)),
                          fontsize=9, xytext=(5, 5),
                          textcoords="offset points")
ax1.set_xscale("log")
ax1.set_yscale("log")
ax1.set_xlabel("市町総人口 (R2 国勢調査)")
ax1.set_ylabel("市民 1 人あたり施策面積 (a/人)")
ax1.set_title("人口 × 1人あたり施策面積 (log-log)", fontsize=12)
ax1.legend(loc="lower left", fontsize=9)
ax1.grid(True, which="both", alpha=0.3)

plt.tight_layout()
plt.savefig(ASSETS / "L25_fig09_pop_area_scatter.png", dpi=130)
plt.close("all")
print(f"  Fig 9 完成 ({time.time()-t1:.2f}s)", flush=True)

print(f"\n[4] 図 9 枚 完了  累計 {time.time()-t0:.2f}s", flush=True)

# =============================================================================
# 5. 仮説検証
# =============================================================================
print("\n[5] 仮説検証", flush=True)
t1 = time.time()

# H1: 総施策面積 >= 20,000 ha かつ 件数 < 2,000
H1_total_ha = AREA_TOTAL_HA
H1_count = N_POLY
H1_judge = ("支持" if H1_total_ha >= 20_000 and H1_count < 2_000
            else "部分支持" if H1_total_ha >= 15_000
            else "反証")

# H2: 「地区名のみ」が大多数 (>=60%) かつ キーワード分類は 10-30%
nrg_only_pct = float(nrg_overall[nrg_overall["nrg_class"] == "地区名のみ"]
                       ["n_pct"].iloc[0]) if "地区名のみ" in nrg_overall["nrg_class"].values else 0.0
keyword_classes = ["ダム", "ため池", "漁業", "林業", "ほ場整備", "基地・施設"]
keyword_pct = float(nrg_overall[nrg_overall["nrg_class"].isin(keyword_classes)]
                      ["n_pct"].sum())
H2_judge = ("支持" if nrg_only_pct >= 60 and 5 <= keyword_pct <= 35
            else "部分支持" if nrg_only_pct >= 50
            else "反証")

# H3: MultiPolygon 比率は中山間市町ほど高い
mountain_cities = ["庄原市", "三次市", "安芸高田市", "北広島町", "世羅町"]
urban_cities = ["広島市", "呉市", "福山市", "東広島市", "廿日市市", "尾道市",
                "三原市", "竹原市", "府中市", "大竹市"]
mountain_multi = float(city_agg[city_agg["city"].isin(mountain_cities)]
                        ["multi_pct"].mean())
urban_multi = float(city_agg[city_agg["city"].isin(urban_cities)]
                      ["multi_pct"].mean())
H3_judge = ("支持" if mountain_multi > urban_multi + 5
            else "部分支持" if mountain_multi > urban_multi
            else "反証")

# H4: KUIKI_CD = 0 (区域外?) が中山間で支配的、KUIKI_CD = 2 (調整?) が都市
mountain_k0 = float(agro[agro["src_city"].isin(mountain_cities)]
                     ["KUIKI_CD"].eq(0).mean()) * 100
urban_k2 = float(agro[agro["src_city"].isin(urban_cities)]
                  ["KUIKI_CD"].eq(2).mean()) * 100
H4_judge = ("支持" if mountain_k0 >= 60 and urban_k2 >= 20
            else "部分支持" if mountain_k0 >= 50 or urban_k2 >= 15
            else "反証")

# H5: L24 vs L25 重複は 10-30% (= せめぎあい)
overlap_a_pct = overlap_pct_a_total
H5_judge = ("支持" if 10 <= overlap_a_pct <= 30
            else "部分支持" if 5 <= overlap_a_pct <= 50
            else "反証(重複過大 or 過少)")

# H6: 広島市 NRG_AN null < 5%
hiroshima_nrg_null_pct = float(city_agg[city_agg["city"] == "広島市"]
                                 ["nrg_null_pct"].iloc[0])
H6_judge = ("支持" if hiroshima_nrg_null_pct < 5
            else "部分支持" if hiroshima_nrg_null_pct < 20
            else "反証")

H_RESULTS = {
    "H1": {
        "text": "県内 17 市町の総施策面積 ≥ 20,000 ha かつ 件数 < 2,000 (制度的指定エリア型)",
        "result": f"施策合計 = {H1_total_ha:,.0f} ha, 件数 = {H1_count:,}",
        "judge": H1_judge,
    },
    "H2": {
        "text": "NRG_AN は「地区名のみ」(地名タイプ) が大多数 (>=60%)、キーワード分類は 5-35%",
        "result": f"地区名のみ = {nrg_only_pct:.1f}%, キーワード分類 = {keyword_pct:.1f}%",
        "judge": H2_judge,
    },
    "H3": {
        "text": "MultiPolygon 比率は中山間市町ほど高く、都市市町は単一 Polygon 主体",
        "result": (f"中山間 5 市町平均 MultiPolygon% = {mountain_multi:.1f}, "
                   f"都市 10 市平均 = {urban_multi:.1f}"),
        "judge": H3_judge,
    },
    "H4": {
        "text": "KUIKI_CD=0 が中山間で支配的、KUIKI_CD=2 が都市市町に多い (都市計画階層フラグ)",
        "result": (f"中山間 KUIKI=0 比率 = {mountain_k0:.1f}%, "
                   f"都市 KUIKI=2 比率 = {urban_k2:.1f}%"),
        "judge": H4_judge,
    },
    "H5": {
        "text": "施策と転用の重複は 10-30% (せめぎあい構造)",
        "result": (f"重複面積 = {total_overlap_ha:,.0f} ha "
                   f"(施策の {overlap_pct_a_total:.1f}% / "
                   f"転用の {overlap_pct_f_total:.2f}%)"),
        "judge": H5_judge,
    },
    "H6": {
        "text": "広島市 NRG_AN null は監査時の偶発、実データでは大部分が地区名 (null < 5%)",
        "result": f"広島市 NRG_AN null = {hiroshima_nrg_null_pct:.1f}%",
        "judge": H6_judge,
    },
}

(ASSETS / "L25_hypothesis_results.json").write_text(
    json.dumps({"results": H_RESULTS,
                "n_polygons": N_POLY,
                "area_total_ha": AREA_TOTAL_HA,
                "area_total_km2": AREA_TOTAL_KM2,
                "n_multipoly": int((agro.geom_type=="MultiPolygon").sum()),
                "n_polygon": int((agro.geom_type=="Polygon").sum()),
                "overlap_total_ha": total_overlap_ha,
                "overlap_pct_a": overlap_pct_a_total,
                "overlap_pct_f": overlap_pct_f_total,
                "common_cities": common_cities,
                }, ensure_ascii=False, indent=2),
    encoding="utf-8")
print(f"  H1-H6 検証完了 ({time.time()-t1:.2f}s)", flush=True)
for k, v in H_RESULTS.items():
    print(f"    {k}: {v['judge']} -- {v['result']}")

# =============================================================================
# 6. HTML 構築
# =============================================================================
print("\n[6] HTML 構築", flush=True)

sections = []

ds_links_html = ", ".join(
    f"<a href='https://hiroshima-dobox.jp/datasets/{d[0]}' target='_blank'>#{d[0]}</a>"
    for d in CITY_DEFS
)

n_polygon = int((agro.geom_type == "Polygon").sum())
n_multi = int((agro.geom_type == "Multi" + "Polygon").sum())

# === セクション1: 学習目標と問い ===
s1_html = f"""
<p>本記事は、広島県インフラマネジメント基盤 DoBoX が公開する
<b>「都市計画区域情報_農林漁業関係施策」シリーズ 17 件</b>
({ds_links_html})
を縦結合し、<b>2022 年 (単年) の広島県内 17 市町の
農林漁業関係施策ポリゴン 計 {N_POLY:,} 件 (合計実測 {AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.0f} km²)</b>
から、<b>広島県内の「保全策の地理構造」</b> ── 施策面積分布・市町別パターン・
NRG_AN 文字列の施策種別分類・KUIKI_CD 別の都市計画階層・
<b>農地転用 L24 との空間対比</b> ── を分析する研究記事である。
全 20 市町中 3 町 (熊野町・府中町・海田町) は施策データ無指定。
<b>L24 (農地転用) と L25 (本記事) は同じ 17 市町数だが 1 町だけスワップ</b>
(L24=熊野町を含み坂町を含まない、L25=坂町を含み熊野町を含まない) ─
このコントラストも分析対象とする。</p>

<div class="note">
<b>研究の問い (RQ)</b>: 広島県内 17 市町の農林漁業関係施策は、
面積規模・施策種別 (NRG_AN 文字列)・地理分布・農地転用 L24 との対比で
どのような構造を持ち、<b>保全策と転用がどのようにせめぎあっているか</b>?
重なれば「保全策が形骸化」、すみ分ければ「保全と転用が分離」のシグナル。
</div>

<h3>仮説 H1〜H6</h3>
<ul>
  <li><b>H1</b>: 県内 17 市町の総施策面積は <b>20,000 ha 以上</b>
      (L24 転用対象農地面積 ~155,000 ha の <b>15% 程度</b>)。
      件数は <b>2,000 件未満</b> ─ L24 (13,749 件) より遥かに少数の
      <b>「制度的指定エリア」型</b>。<br>
      <b>意味</b>: 施策ポリゴンは「個別申請」ではなく「広域な区分図面」として
      整備されている可能性。</li>
  <li><b>H2</b>: NRG_AN 文字列は<b>地区名・施設名 (地名タイプ)</b> が大多数 (>=60%)、
      「ダム」「池」「漁業」「林」「ほ場整備」など特定キーワードを含む施策ポリゴンは
      <b>5-35% 程度</b>。<br>
      <b>意味</b>: 広島県の農林漁業施策は<b>『地区単位』</b>で運営されている。
      日本の農地は「集落と一体」で扱われ、政策名ではなく
      地名で図面化される。</li>
  <li><b>H3</b>: ジオメトリは <b>Polygon と MultiPolygon が混在</b>。
      MultiPolygon 比率は<b>中山間市町ほど高く</b> (集落と分散圃場の混在)、
      <b>都市市町は単一 Polygon が主体</b>。<br>
      <b>意味</b>: 中山間の集落配置 (谷間の小集落+分散圃場) と
      都市の集中農地 (まとまった土地区画) の<b>地形的差</b>が
      ジオメトリ構造として現れる。</li>
  <li><b>H4</b>: KUIKI_CD は L24 と同じく<b>都市計画区域階層フラグ</b>と推定。
      <b>0 (区域外/非線引き)</b> が中山間で支配的、
      <b>2 (調整区域?)</b> が都市市町で多い ─ L24 と整合的。</li>
  <li><b>H5</b>: <b>L24 (転用) と L25 (施策) の空間関係</b>: 同じ市町内で重複は
      <b>10-30% 程度</b>あり、<b>「保全策のエリアでも転用が一部発生」</b>の
      せめぎあい構造。完全すみ分け (0%) でも完全重複 (100%) でもない中間。</li>
  <li><b>H6</b>: 広島市の NRG_AN は監査時 None と報告されたが、
      <b>実データでは大部分 (>95%) が地区名で埋まっている</b> ─
      監査時のサンプル行が偶々 None だっただけ。
      <b>「サンプル 1 行で判断するな」</b>のデータ取扱い教訓。</li>
</ul>

<h3>独自用語の定義 (本レッスン内のみ)</h3>
<ul>
  <li><b>農林漁業関係施策 (本シリーズの定義)</b>: 都市計画基礎調査に基づき、
      <b>農業振興地域・農用地区域・林業地域・漁業基地</b>等として
      <b>制度的に指定された区域</b>を 1 つのポリゴンで表現したもの。
      <b>『すでに保全されている土地』</b>そのものではなく、
      <b>『保全策・振興策の対象として図面化された区画』</b>として扱う。
      仕様書未公開のため、本記事では「保全策の対象地区」と総称する。</li>
  <li><b>NRG_AN</b> (本記事の独自解読): 各施策ポリゴンに付与された<b>名称文字列</b>。
      実データから観察すると<b>大半が地区名・集落名</b>
      (例: 「板休〜後懸」「山田」「梶山田」)、
      一部が<b>施設名</b> (例: 「大久保ダム」「小文池」「森山北漁業基地」)。
      列名 NRG_AN は推定で「Nourin-gyo Gyoseimei AN/Area Name」の略か。</li>
  <li><b>NRG_AN 分類</b> (本記事独自定義): NRG_AN 文字列を
      キーワード辞書で 8 タイプに分類。
      <b>「ダム」(ダム名含む)・「ため池」(池/溜池 含む)・「漁業」(漁業/漁港含む)・
      「林業」(林/森林含む)・「ほ場整備」(ほ場/圃場/区画整理含む)・
      「基地・施設」(基地/施設/場含む)・「地区名のみ」(キーワード非該当)・
      「_欠損」(NRG_AN が None)</b>。複数該当時は最初に当たったタイプを採用。</li>
  <li><b>保全策</b> (本記事の文脈): 農林漁業関係施策のうち、地区を
      農用地区域・農業振興地域として保全する制度区分。
      L24 の<b>「転用許可対象」</b>と対立的概念。</li>
  <li><b>振興策</b>: ほ場整備・区画整理・ダム・ため池整備など、
      農林漁業の<b>生産基盤を強化する</b>制度区分。
      キーワード分類で抽出可能。</li>
  <li><b>中山間</b> (本記事独自分類): 17 市町を 4 タイプに分類。
      <b>中山間 5 市町</b>: 三次市・庄原市・安芸高田市・北広島町・世羅町、
      <b>都市 10 市</b>: 広島市・呉市・福山市・東広島市・廿日市市・尾道市・
      三原市・竹原市・府中市・大竹市、
      <b>離島 1 市</b>: 江田島市、
      <b>近郊町 1 町</b>: 坂町。</li>
  <li><b>KUIKI_CD</b>: 区域コード (0-7 のフラグ値、仕様書未公開)。
      L24 と同じ列名で、本記事の独自解読は<b>都市計画区域階層</b>と推定:
      <b>0 = 区域外/非線引き</b> (中山間で支配的)、
      <b>1 = 市街化区域?</b> (一部都市市)、
      <b>2 = 市街化調整区域?</b> (都市市の最大値)、
      <b>3, 4, 6 = 中間階層・特例区域?</b>。</li>
  <li><b>施策密度</b> (本記事独自指標): 施策面積 (ha) ÷ 市域面積 (km²)。
      「市域 1 km² あたりの施策対象 ha」。</li>
  <li><b>L24 vs L25 重複率</b>: 共通 16 市町で、施策ポリゴン (L25) と
      転用ポリゴン (L24) の<b>面積重なり (intersection)</b> を計算し、
      <b>施策面積に対する重複比率 (%)</b> と
      <b>転用面積に対する重複比率 (%)</b> を出す。
      重複率の意味付けは仮説 H5 で議論する。</li>
</ul>

<h3>到達点</h3>
<ol>
  <li>17 ZIP の農林漁業施策 GeoJSON を 1 個の <code>GeoDataFrame</code>
      ({N_POLY:,} polygon × 7 列 + 派生 5 列) に縦結合できる。
      <b>CRS が EPSG:2445 で記録されている</b> (L24 は EPSG:6671) ─
      <code>to_crs("EPSG:6671")</code> で統一する手順を学ぶ。</li>
  <li><b>{N_POLY:,} polygon</b> から、市町単位の総面積・件数・MultiPolygon比率の
      集計を <code>groupby</code> で実装する。</li>
  <li>NRG_AN 文字列を<b>キーワード辞書で 8 タイプに分類</b>し、
      施策の質的構造を可視化する。</li>
  <li>地理タイプ別の主題図、17 市町 small multiples、NRG_AN 分類別マップで
      施策の地理分布を多角的に表現する。</li>
  <li><b>L24 (転用) と L25 (施策) の空間 intersection</b> を
      <code>shapely.union_all().intersection()</code> で計算し、
      <b>『保全策と転用のせめぎあい』</b>の定量化を体験する。</li>
  <li>監査結論「広島市 NRG_AN None」を<b>実データで反証</b>することで、
      <b>「サンプル 1 行で判断する危険性」</b>を学ぶ。</li>
</ol>

<h3>本記事のスコープ宣言</h3>
<p>本記事は<b>農林漁業関係施策 17 件のみ</b>を主データとする研究記事である。
姉妹シリーズ <b>農地転用 (L24, 17 件) は前記事</b>で扱った。
本記事では L24 既知データを<b>参考併置</b>する (空間重複分析と重ね合わせマップ)
が、両記事を合体させない (要件 I の水増し回避)。
17 市町行政区域 (L15 既取得) のみ背景レイヤーとして利用するが、
L15 都市計画区域記事と論点を重複させない。</p>
"""
sections.append(("1. 学習目標と問い", s1_html))


# === セクション2: 使用データ ===
DS_TABLE_ROWS = "".join(
    f"<tr><td>#{d[0]}</td><td>{d[1]}</td><td>{d[3]}</td><td>{d[4]}</td>"
    f"<td><a href='https://hiroshima-dobox.jp/datasets/{d[0]}' target='_blank'>"
    f"DoBoX</a></td>"
    f"<td>{next(L['n_poly'] for L in load_log if L['city']==d[1]):,}</td>"
    f"<td>{next(L['n_multipoly'] for L in load_log if L['city']==d[1]):,}</td>"
    f"<td>{next(L['multi_pct'] for L in load_log if L['city']==d[1]):.1f}%</td>"
    f"<td>{next(L['n_nrg_null'] for L in load_log if L['city']==d[1]):,}</td>"
    f"<td>{next(L['kuiki_cds'] for L in load_log if L['city']==d[1])}</td>"
    f"<td>{next(L['tokei_cds'] for L in load_log if L['city']==d[1])}</td>"
    f"</tr>"
    for d in CITY_DEFS
)
NO_POLICY_TABLE_ROWS = "".join(
    f"<tr><td>{name}</td><td>{ctype}</td><td>{region}</td>"
    f"<td>{CITY_REF[name]['area_km2']:.1f}</td>"
    f"<td>{CITY_REF[name]['pop_k']:,}</td>"
    f"<td>{CITY_REF[name]['pop_k']*1000/CITY_REF[name]['area_km2']:.0f}</td></tr>"
    for name, _adm, ctype, region in NO_POLICY_CITIES
)

s2_html = f"""
<p>農林漁業関係施策 17 件はそれぞれ 1 市町分の<b>農林漁業施策ポリゴン GeoJSON
(Polygon または MultiPolygon)</b> を ZIP で配布している。
<b>列構造は 17 件で 100% 一致 (7 列)</b>。
データは<b>2022 年単年</b>のスナップショット
(L24 が 2016-2020 の 5 年集計だったのと対照的)。</p>

<h3>17 dataset_id 一覧 (施策あり 17 市町, 連番 1408-1424)</h3>
<table>
<tr><th>dataset_id</th><th>市町</th><th>市町タイプ</th><th>地理</th>
    <th>DoBoX</th>
    <th>件数</th><th>MultiP</th><th>Multi%</th><th>NRG null</th>
    <th>KUIKI_CD</th><th>TOKEI_CD</th></tr>
{DS_TABLE_ROWS}
</table>
<p class="tnote">合計: <b>{N_POLY:,} ポリゴン
(うち Polygon {n_polygon:,}, MultiPolygon {n_multi:,}) /
実測面積合計 {AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.1f} km²</b>。
広島県全域面積 8,479 km² に対し、施策面積率は <b>{AREA_TOTAL_KM2/8479*100:.1f}%</b>。
件数最大は<b>広島市 426 件</b>、件数最小は<b>坂町 1 件</b>(漁業基地のみ)。
17 市町 100% で同一 7 列構造、追加列なし。</p>

<h3>農林漁業施策データ無し 3 町 (参考)</h3>
<table>
<tr><th>市町</th><th>タイプ</th><th>地理</th>
    <th>面積 km²</th><th>人口 (千人)</th><th>密度 (人/km²)</th></tr>
{NO_POLICY_TABLE_ROWS}
</table>
<p class="tnote">3 町 (熊野町・府中町・海田町) は<b>L25 シリーズ未指定</b>。
府中町・海田町は L24 でも未指定 (= 都市部 100%)、
<b>熊野町は L24 にあったが L25 では無い</b> ─
熊野町には農地転用の対象は記録されたが、
保全/振興施策の対象は無い (= 「保全せず転用していく町」)。
逆に L25 で<b>追加された坂町</b>は L24 に無く、
施策ポリゴンが <b>1 件のみ</b>「森山北漁業基地」(漁業施設) ─
広島湾岸ベッドタウンだが漁港の小さな伝統が残る。</p>

<h3>列構造の詳細 (7 列)</h3>
<table>
<tr><th>列名</th><th>型</th><th>意味</th></tr>
<tr><td><code>ID</code></td><td>int32</td>
    <td>ポリゴン ID (市町内の連番)。L24 の TOKEI_CD/CITY_CD に相当する位置だが、
        L25 では別途 ID 列が付与される</td></tr>
<tr><td><code>TOKEI_CD</code></td><td>int32</td>
    <td>統計区分コード (1, 2, 3 の 3 値)。<b>施策種別</b>のフラグと推定 ─
        市町別に値分布が異なる (中山間は 1 と 3 主体、都市は 1, 2, 3 混在)</td></tr>
<tr><td><code>CITY_CD</code></td><td>int32</td>
    <td>市区町村コード。<b>広島市は 8 区中 105/106/108 のみ</b>データあり
        (他区はデータ無し)。各市町は単一 CITY_CD</td></tr>
<tr><td><code>KUIKI_CD</code></td><td>int32</td>
    <td>区域コード (0-6 の 7 値、仕様書未公開)。
        <b>都市計画区域階層</b>のフラグと推定 (詳細は分析 6 で解読)</td></tr>
<tr><td><code>NRG_AN</code></td><td>object</td>
    <td><b>施策名/対象地区名 (本記事の主役の文字列列)</b>。
        実データから観察すると大半が<b>地区名・集落名</b>、
        一部が<b>「ダム」「池」「漁業基地」「ほ場整備」</b>等の施設・施策名。
        欠損率は市町により 0-15% で、<b>広島市 0.5%</b>と極小</td></tr>
<tr><td><code>RITTEKI_CD</code></td><td>int32</td>
    <td>立地コード (0, 1, 2 の 3 値、仕様書未公開)。
        全 17 市町で値 0 が支配的、廿日市市のみ 1 が 1/3 程度。
        <b>立地タイプフラグ</b>と推定</td></tr>
<tr><td><code>geometry</code></td><td>Polygon または MultiPolygon</td>
    <td><b>施策対象境界</b>。<b>EPSG:2445 (旧広島県平面直角)</b>で記録され、
        L24 の EPSG:6671 と異なる ─ <b>本記事では <code>to_crs("EPSG:6671")</code>
        で統一</b>。MultiPolygon は全件の {n_multi/N_POLY*100:.1f}% で、
        中山間市町ほど高比率</td></tr>
</table>

<div class="note"><b>データ品質ノート</b>:
1. <b>CRS は EPSG:2445</b> (旧 JGD2000 平面直角第III系) で記録、L24 (EPSG:6671 = JGD2011) と異なる。
   <b>EPSG:2445 → EPSG:6671</b> は 1m 程度のズレなので無視できないが、
   <code>to_crs()</code> で正確に変換可能。
2. <b>監査時 (2026-05-03) の DL では 坂町 (1412)・竹原市 (1418) が KMZ 形式しか
   自動取得できなかった</b>が、再取得で GeoJSON 単独 ZIP を確保。
   → fetch スクリプトの <code>pick_best()</code> を「geojson 単独 rid を最優先」
   に修正することで 17 件全て GeoJSON で揃った。
3. <b>広島市 (1410)</b> は監査時 NRG_AN サンプルが None だったが、
   実データでは 426 行中 424 行 (99.5%) に地区名が入っている。
   <b>「サンプル 1 行で判断するな」</b>のデータ取扱い教訓 (H6 で検証)。
4. <b>L24 (転用) との 1 町スワップ</b>: L24 = 熊野町を含み坂町を含まない。
   L25 = 坂町を含み熊野町を含まない。<b>「施策の有無」は「転用の有無」と
   完全には一致しない</b> ─ シリーズ性格の違い (動的 vs 静的) を反映。</div>
"""
sections.append(("2. 使用データ", s2_html))


# === セクション3: ダウンロード ===
dl_html = f"""
<p>本記事の<b>再現性</b>を担保するため、HTML 1 枚から
生データ・中間 CSV・図 PNG・再現 Python を直リンクで取得できる。</p>

<h3>(1) 生データ ZIP (DoBoX 直)</h3>
<p>17 件の ZIP は前項の表からそれぞれ DoBoX へリンク。
あるいは一括取得スクリプト:</p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 data\\extras\\L25_agroforestry_policy\\fetch_agroforestry_policy.py</code></pre>
<p>合計サイズ約 2 MB (L24 の 12 MB より遥かに小さい)。
監査時取得済の 3 市町 (広島市・呉市・福山市) は
<code>data/extras/_urban_planning_audit/</code> から自動コピー、
残り 14 市町は DoBoX から HTTP 取得 (約 15 秒)。</p>

<h3>(2) 中間 CSV (本スクリプトの出力)</h3>
<ul>
  <li><a href="assets/L25_city_summary.csv" download>L25_city_summary.csv</a> — 17 市町サマリ (件数・面積・密度・MultiPolygon比率 等 14 指標)</li>
  <li><a href="assets/L25_no_policy_cities.csv" download>L25_no_policy_cities.csv</a> — 施策無し 3 町の一覧</li>
  <li><a href="assets/L25_policy_polygons_top2000.csv" download>L25_policy_polygons_top2000.csv</a> — ポリゴン単位 上位 2,000 行 (面積降順)</li>
  <li><a href="assets/L25_load_log.csv" download>L25_load_log.csv</a> — 17 件読込ログ</li>
  <li><a href="assets/L25_nrg_overall.csv" download>L25_nrg_overall.csv</a> — NRG_AN 分類別 全体集計</li>
  <li><a href="assets/L25_nrg_cross_count.csv" download>L25_nrg_cross_count.csv</a> — NRG_AN 分類 × 市町 クロス</li>
  <li><a href="assets/L25_nrg_freq_top50.csv" download>L25_nrg_freq_top50.csv</a> — NRG_AN 文字列 頻度 top 50</li>
  <li><a href="assets/L25_kuiki_count.csv" download>L25_kuiki_count.csv</a> — KUIKI_CD × 市町 クロス</li>
  <li><a href="assets/L25_kuiki_overall.csv" download>L25_kuiki_overall.csv</a> — KUIKI_CD 全体集計</li>
  <li><a href="assets/L25_tokei_overall.csv" download>L25_tokei_overall.csv</a> — TOKEI_CD 全体集計</li>
  <li><a href="assets/L25_scale_overall.csv" download>L25_scale_overall.csv</a> — 規模クラス全体集計</li>
  <li><a href="assets/L25_overlap_l24.csv" download>L25_overlap_l24.csv</a> — L24 vs L25 市町別空間重複</li>
  <li><a href="assets/L25_top20_large.csv" download>L25_top20_large.csv</a> — 大規模施策ポリゴン top 20</li>
  <li><a href="assets/L25_hypothesis_results.json" download>L25_hypothesis_results.json</a> — 仮説 H1〜H6 検証結果 (JSON)</li>
</ul>

<h3>(3) 図 PNG (本記事掲載 9 枚)</h3>
<ul>
  <li><a href="assets/L25_fig01_overview.png" download>L25_fig01_overview.png</a> — 県全域 施策ポリゴン主題図</li>
  <li><a href="assets/L25_fig02_policy_vs_nopolicy.png" download>L25_fig02_policy_vs_nopolicy.png</a> — 17 市町 vs 3 町 マップ + 1 町スワップ</li>
  <li><a href="assets/L25_fig03_small_multiples.png" download>L25_fig03_small_multiples.png</a> — 市町別 17 panels + 無し 3 町 panel</li>
  <li><a href="assets/L25_fig04_city_ranking.png" download>L25_fig04_city_ranking.png</a> — 市町別 面積/件数/MultiPolygon比率 ランキング</li>
  <li><a href="assets/L25_fig05_nrg_class.png" download>L25_fig05_nrg_class.png</a> — NRG_AN 分類別 件数% vs 面積% + 市町クロス</li>
  <li><a href="assets/L25_fig06_nrg_map.png" download>L25_fig06_nrg_map.png</a> — NRG_AN 分類別 県全域マップ</li>
  <li><a href="assets/L25_fig07_overlap_l24.png" download>L25_fig07_overlap_l24.png</a> — L24 (転用) ⊕ L25 (施策) 重ね合わせ + 市町別重複率</li>
  <li><a href="assets/L25_fig08_kuiki_cd.png" download>L25_fig08_kuiki_cd.png</a> — KUIKI_CD 別マップ + 市町クロス</li>
  <li><a href="assets/L25_fig09_pop_area_scatter.png" download>L25_fig09_pop_area_scatter.png</a> — 市域面積/人口 × 施策面積散布</li>
</ul>

<h3>(4) 再現用 Python スクリプト</h3>
<ul>
  <li><a href="L25_agroforestry_policy.py" download>L25_agroforestry_policy.py</a> — 本記事の全コード</li>
  <li><a href="../data/extras/L25_agroforestry_policy/fetch_agroforestry_policy.py" download>fetch_agroforestry_policy.py</a> — 17 ZIP 取得スクリプト</li>
</ul>
<p class="tnote">実行は <code>cd "2026 DoBoX 教材"; py -X utf8 lessons\\L25_agroforestry_policy.py</code>。
17 ZIP がローカルにあれば <b>30 秒程度</b>で全図 + CSV 再生成 (要件 S 準拠)。</p>
"""
sections.append(("3. ダウンロード (再現用データ・中間データ・図・スクリプト)",
                  dl_html))

# === セクション4: 分析1 — 17 GeoJSON 統合 + CRS 統一 + NRG_AN 分類 ===
code_load = code('''
DATA_DIR = ROOT / "data" / "extras" / "L25_agroforestry_policy"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
TARGET_CRS = "EPSG:6671"   # JGD2011 平面直角第III系

# 17 件は 7 列 100% 共通
COMMON_COLS_7 = ["ID", "TOKEI_CD", "CITY_CD", "KUIKI_CD", "NRG_AN",
                 "RITTEKI_CD", "geometry"]

# NRG_AN 分類のキーワード辞書
NRG_KEYWORDS = [
    ("ダム",     ["ダム"]),
    ("ため池",   ["池", "溜池", "ため池"]),
    ("漁業",     ["漁業", "漁港", "漁場", "養殖"]),
    ("林業",     ["林", "森林", "山林"]),
    ("ほ場整備", ["ほ場整備", "圃場整備", "ほ場", "圃場", "整備", "区画整理"]),
    ("基地・施設", ["基地", "施設", "場"]),
]

def classify_nrg_an(s):
    if pd.isna(s) or s is None or str(s).strip() == "":
        return "_欠損"
    text = str(s)
    for label, kws in NRG_KEYWORDS:
        for kw in kws:
            if kw in text:
                return label
    return "地区名のみ"

frames = []
for dsid, name, _adm, ctype, rtype in CITY_DEFS:
    z = DATA_DIR / f"agro_{dsid}_{name}.zip"
    g = load_geojson_zip(z)
    g = g[COMMON_COLS_7].copy()
    g["src_city"] = name
    g["rtype"] = rtype

    # CRS 統一: EPSG:2445 (旧) → EPSG:6671 (JGD2011)
    if g.crs is None:
        g = g.set_crs("EPSG:2445", allow_override=True)
    g = g.to_crs(TARGET_CRS)
    frames.append(g)

agro = gpd.GeoDataFrame(pd.concat(frames, ignore_index=True),
                        geometry="geometry", crs=TARGET_CRS)

# 派生列
agro["geom_area_m2"]  = agro.geometry.area              # 実測 m²
agro["geom_area_ha"]  = agro["geom_area_m2"] / 1e4      # 実測 ha
agro["geom_type"]     = agro.geom_type                  # Polygon or MultiPolygon
agro["nrg_class"]     = agro["NRG_AN"].apply(classify_nrg_an)
''')

before_after_s4 = f"""
<h3>入出力 Before/After (要件 K — NRG_AN を分類する 1 件の追跡)</h3>
<p>NRG_AN 文字列はデータごとに <b>地区名・施設名・None</b> が混在する。
分類関数 <code>classify_nrg_an()</code> が 1 件をどう変換するかを、
広島市最大施策ポリゴン (鹿之道地区, 「ほ場整備」キーワード含む) を例に追跡する:</p>

<table>
<tr><th>段階</th><th>列</th><th>このデータで何が起きるか</th>
    <th>1 行の値の例 (広島市の代表的施策ポリゴン)</th></tr>
<tr><td>入力 (生)</td><td><code>NRG_AN (object)</code></td>
    <td>GeoJSON から読込時、文字列または None</td>
    <td><code>'ほ場整備鹿之道地区'</code></td></tr>
<tr><td>分類関数</td><td><code>classify_nrg_an(s)</code></td>
    <td>Null チェック → キーワード辞書を順番に走査 → 最初に当たったタイプを返す</td>
    <td>キーワード「ほ場整備」を検出</td></tr>
<tr><td>出力</td><td><code>nrg_class</code></td>
    <td>分類結果の文字列</td>
    <td><code>'ほ場整備'</code></td></tr>
<tr><td>派生</td><td><code>geometry → geom_area_m2</code></td>
    <td>EPSG:6671 で正確な面積 (m²) を計算</td>
    <td>例: <code>5,135,462 m²</code> (広島市最大施策)</td></tr>
<tr><td>派生</td><td><code>geom_area_ha = m2/1e4</code></td>
    <td>m² → ha 変換</td>
    <td><code>513.5 ha (広島市最大)</code></td></tr>
<tr><td>派生</td><td><code>geom_type</code></td>
    <td>geometry の型 (Polygon / MultiPolygon)</td>
    <td><code>'Polygon'</code> (広島市最大は単一 Polygon)</td></tr>
</table>
<p>このように、<b>7 列の生データ</b>から
<b>NRG_AN 分類・実測ha・ジオメトリ型</b>の派生指標を導出する。
特に <b>nrg_class</b> 列は本記事の<b>『施策の質的構造』</b>を表す核心列で、
<b>『仕様書未公開でも、文字列パターンから施策タイプを推定できる』</b>本記事の中核手法。</p>

<p class="tnote"><b>余談</b>: NRG_AN は 17 市町で<b>地区名 (88%) と キーワード分類 (9%)</b>に
ほぼ二分される。「ほ場整備」「ダム」「池」などキーワードが入る施策ポリゴンは
全 1,912 件中 約 170 件 (= 9%)、残りは地名のみ。
これは<b>広島県の農林漁業施策が「集落単位」</b>で運営されている実態を示す。</p>
"""

n_loaded = len(load_log)

s4_html = (
    "<h3>狙い</h3>"
    f"<p>17 ZIP の農林漁業施策 GeoJSON を 1 個の <code>GeoDataFrame</code> "
    f"({N_POLY:,} polygon × 7 列 + 派生 4 列) に統合し、"
    f"後段の市町別集計・主題図・NRG_AN 分析の基盤データを作る。"
    f"行政区域 20 件 (施策あり 17 + 無し 3) を背景レイヤーとして同時に読み込む。"
    f"<b>CRS が EPSG:2445 (旧) で記録</b>されているため、"
    f"<b>EPSG:6671 (新, L24 と同じ)</b> に変換することがこの分析の核。"
    f"併せて <b>NRG_AN 文字列をキーワード辞書で 8 タイプに分類</b>する。</p>"

    "<h3>手法</h3>"
    "<p><b>直感</b>: ZIP を読む → 列を共通 7 列に揃える → "
    "<b>CRS を <code>to_crs(EPSG:6671)</code> で変換</b> → 縦結合 → "
    "<b>NRG_AN を分類関数でタイプ化</b> → 派生指標 (実測ha・ジオメトリ型・分類タイプ) を計算 → "
    "行政区域 dissolve で背景レイヤー作成。</p>"

    "<p><b>大筋 (5 ステップ)</b></p>"
    "<ol>"
    "<li>17 市町について <code>load_geojson_zip()</code> で GeoDataFrame を読む</li>"
    "<li>共通 7 列に正規化 (本データは追加列無し)</li>"
    "<li><b>CRS = EPSG:2445 を <code>to_crs('EPSG:6671')</code> で変換</b> "
    "(旧→新平面直角座標系の正確な変換)</li>"
    "<li><code>pd.concat</code> で縦結合 → "
    f"{N_POLY:,} 行 1 個の GeoDataFrame</li>"
    "<li>派生指標を計算: <code>geom_area_m2, geom_area_ha, geom_type, nrg_class</code></li>"
    "</ol>"

    "<p><b>NRG_AN 分類アルゴリズム</b>: 各文字列を以下の優先順位でチェック ─ "
    "<b>ダム → ため池 → 漁業 → 林業 → ほ場整備 → 基地・施設 → 地区名のみ → 欠損</b>。"
    "「池」のように複数タイプに当てはまる場合は<b>最初に当たったタイプを採用</b>"
    "(複数タイプ計上による二重カウント防止)。</p>"

    "<p><b>前提と限界</b>: 17 件の施策データは追加列が全く無い。"
    "<b>坂町 (1412)</b> のみ 1 ポリゴン (= 漁業基地) で他市町と質的に異なる。"
    "<b>広島市 (1410)</b> は CITY_CD = 105/106/108 の 3 区分のみで、"
    "8 区全部のデータは含まれない (中区・東区・西区など南部デルタは除外)。"
    "NRG_AN 分類は<b>キーワード辞書ベース</b>で完璧ではない (「林田」のような地名が"
    "「林業」に誤分類される可能性) が、全体傾向把握には十分。</p>"

    "<h3>実装</h3>"
    + code_load + before_after_s4 +

    f"<h3>結果</h3>"
    f"<p>17 ZIP のうち、<b>{n_loaded} 件すべてが共通 7 列で読み込み成功</b>。"
    f"統合後の <code>agro</code> GeoDataFrame は <b>{N_POLY:,} polygon × {len(agro.columns)} 列</b>。"
    f"実測面積合計 = <b>{AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.0f} km²</b>"
    f" (= 広島県全域の {AREA_TOTAL_KM2/8479*100:.1f}%)。"
    f"Polygon : MultiPolygon = <b>{n_polygon:,} : {n_multi:,}</b> "
    f"(= MultiPolygon {n_multi/N_POLY*100:.1f}%)。"
    f"処理時間は <b>{time.time()-t0:.1f} 秒</b>で要件 S を満たす。</p>"

    f"<h3>表 1 — 17 市町 読込ログ</h3>"
    "<table><tr><th>市町</th><th>地理</th><th>件数</th>"
    "<th>MultiP</th><th>Multi%</th><th>NRG null</th>"
    "<th>NRG null%</th><th>KUIKI</th><th>TOKEI</th></tr>"
    + "".join(f"<tr><td>{L['city']}</td><td>{L['rtype']}</td>"
              f"<td>{L['n_poly']:,}</td>"
              f"<td>{L['n_multipoly']:,}</td>"
              f"<td>{L['multi_pct']:.1f}%</td>"
              f"<td>{L['n_nrg_null']:,}</td>"
              f"<td>{L['nrg_null_pct']:.1f}%</td>"
              f"<td>{L['kuiki_cds']}</td>"
              f"<td>{L['tokei_cds']}</td></tr>"
              for L in load_log) +
    "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>件数最大は広島市 426 件</b>、最小は<b>坂町 1 件</b> (漁業基地のみ)。"
    f"<b>MultiPolygon 比率の市町間差が大きい</b> ─ "
    f"中山間市町 (世羅町 26.7%, 三次市 24.0%, 東広島市 17.6%, 庄原市 26.3%) は"
    f"<b>分散圃場</b>が多く MultiPolygon 多用。"
    f"対して都市市町 (廿日市市 0%, 大竹市 0%, 府中市 20.5% 等) は単一 Polygon 主体。"
    f"<b>NRG_AN null は 17 市町中 11 市で 0%</b> ─ ほぼ全件に地区名が入る。"
    f"福山市 (11.9%)・尾道市 (4.5%) のみ若干高め。"
    f"<b>広島市 NRG_AN null = 0.5%</b> ─ <b>監査時の「広島市 None」結論を反証</b> (H6)。"
    f"<b>KUIKI_CD 値は中山間で 0 主体</b> (= 区域外/非線引き)、"
    f"<b>都市市町で 0,1,2,4 混在</b> (= 線引き都市計画区域あり) ─ L24 と整合的。</p>"
)
sections.append(("4. 分析1: 17 GeoJSON 統合 + CRS 統一 + NRG_AN 分類", s4_html))

# === セクション5: 分析2 — 県全域マップ + 17 vs 3 + 1町スワップ ===
s5_html = (
    "<h3>狙い</h3>"
    f"<p>広島県内の農林漁業施策ポリゴン全 {N_POLY:,} 件の地理的配置を 1 枚で見せる。"
    "<b>「県のどこで、どう密集して保全策が広がっているか」</b>を地図で直接視覚化することは、"
    "本シリーズの本質を理解する最短経路。"
    "施策あり 17 市町 vs 無し 3 町を色分けで対比する 2 枚目で、"
    "<b>L24 との「1 町スワップ」</b>(熊野町=L24 のみ、坂町=L25 のみ) も明示する。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) 県全域 主題図</b>: 全 20 市町と広島県全域の行政区域を背景にし、"
    "<code>geopandas.plot(facecolor=rtype_color, alpha=0.65)</code> で施策ポリゴンを"
    "<b>地理タイプ色</b> (都市=赤・中山間=緑・離島=青・近郊町=紫) で描画。</p>"

    "<p><b>(b) 17 vs 3 マップ</b>: 17 市町を緑、3 町を赤で塗り、施策ポリゴンを濃緑で重ねる。"
    "<b>坂町 (L25 のみ、漁業基地 1 件)</b> をハイライト矢印で強調。"
    "「施策無し 3 町」は L24 と一部異なる (熊野町は L24 にあった)。</p>"

    "<h3>実装</h3>"
    + code('''
# (a) 県全域 主題図 (rtype 色)
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888")
admin_diss[admin_diss["policy_status"] == "施策あり"].plot(
    ax=ax, facecolor="#fafff0", edgecolor="#444")
for rt, col in RTYPE_COLOR.items():
    sub = agro[agro["rtype"] == rt]
    sub.plot(ax=ax, facecolor=col, alpha=0.65, edgecolor="none",
             label=f"{rt} ({len(sub):,})")

# (b) 17 vs 3 + 坂町ハイライト
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss[admin_diss["policy_status"] == "施策あり"].plot(
    ax=ax, facecolor="#cdeacc", edgecolor="#444")
admin_diss[admin_diss["policy_status"] == "施策無し"].plot(
    ax=ax, facecolor="#f7c9c9", edgecolor="#444")
agro.plot(ax=ax, facecolor="#1f883d", alpha=0.55, edgecolor="none")

# 坂町に矢印で注記 (L25 のみ、漁業基地 1 件)
sak = agro[agro["src_city"] == "坂町"]
ctr = sak.geometry.iloc[0].representative_point()
ax.annotate("坂町(L25のみ\\n漁業基地1件)",
            (ctr.x, ctr.y), arrowprops=dict(arrowstyle="->"))
''') +

    "<h3>図 1 — 県全域 農林漁業施策ポリゴン主題図 (地理タイプ色)</h3>"
    "<p><b>なぜこの図か</b>: テーブルや棒グラフでは"
    "<b>「保全策が県のどこに、どう広がっているか」</b>が分からない。"
    "地図 1 枚で<b>「中山間 (緑) に保全策が広く、都市 (赤) は中心部のみ点在」</b>"
    "という県全体の地理構造を一気に伝える。</p>"
    + figure("assets/L25_fig01_overview.png",
              f"広島県 農林漁業関係施策ポリゴン 全 {N_POLY:,} 件 (2022 単年)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>緑 (中山間 5 市町)</b> が県北・東部に広く緑色 ─ "
    "<b>『広島県の保全策は中山間に集中』</b>。"
    "庄原市・三次市・北広島町・世羅町・安芸高田市は施策ポリゴンが市域全体に分散。"
    "L24 (転用対象農地) と類似分布 ─ 同じ場所で「保全と転用が共存」を示唆。</li>"
    "<li><b>赤 (都市 10 市)</b> は瀬戸内沿岸 + 福山市東部に分散。"
    "<b>東広島市</b>は中山間+都市の混合で施策ポリゴンが多く、市域中央に集中。</li>"
    "<li><b>青 (離島 = 江田島市)</b> は瀬戸内の小島群に小さく ─ 16 件と少ないが、"
    "島嶼農業の名残り。</li>"
    "<li><b>紫 (近郊町 = 坂町)</b> は広島市湾岸に <b>1 点のみ</b> ─ "
    "「森山北漁業基地」(漁業施設)。広島市の通勤圏ながら漁港の伝統が残る町。</li>"
    "<li>L24 (転用) との差: <b>件数は L24 (13,749) の 14% (1,912)、面積は L24 の 16% (24,007 ha)</b>。"
    "L25 は<b>『少数の制度的指定エリア』</b>型で、"
    "L24 のような<b>『多数の個別申告区画』</b>型ではない。</li>"
    "</ul>"

    "<h3>図 2 — 施策あり 17 市町 vs 無し 3 町 (1 町スワップ強調)</h3>"
    "<p><b>なぜこの図か</b>: <b>L24 と L25 で 17 市町の構成が 1 町だけ違う</b>"
    "(L24 = 熊野町を含む / L25 = 坂町を含む)。"
    "この<b>『1 町スワップ』</b>はシリーズの性格差 (動的指標 vs 静的指標) を反映する。"
    "地図でないと伝わらない構造変化を可視化する。</p>"
    + figure("assets/L25_fig02_policy_vs_nopolicy.png",
              "施策あり 17 市町 (緑) vs 無し 3 町 (赤) + 坂町ハイライト") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>赤の 3 町 (熊野町・府中町・海田町)</b> は<b>すべて広島市東-南東に密集</b> ─ "
    "<b>『広島市通勤圏で施策対象なし』</b>。"
    "L24 と異なり<b>熊野町が含まれる</b> ─ 熊野町は転用対象はあるが保全/振興施策の対象は無い ─ "
    "<b>「保全せず転用していく町」</b>のシグナル。</li>"
    "<li><b>坂町 (湾岸) は L25 のみ含まれる</b> ─ 「森山北漁業基地」1 件 (漁業施設)。"
    "L24 では転用対象なし、L25 では漁業基地として保護対象 ─ "
    "<b>『農地転用の対象は無いが、漁業基地として保全する』</b> ─ "
    "都市化の最終段階でも残る食料生産基盤の例。</li>"
    "<li>残り 16 市町 (緑) は L24 と L25 で共通 ─ 両シリーズで安定的な対象。"
    "保全と転用の両面が記録される。</li>"
    "<li>濃緑の施策ポリゴンは中山間 (北部) で多く、瀬戸内沿岸でも分散して残る ─ "
    "<b>『施策は中山間中心、ただし沿岸都市市町でも消失していない』</b>。</li>"
    "</ul>"

    "<h3>表 2 — 17 市町 vs 3 町 集計 + 1 町スワップ</h3>"
    "<table>"
    "<tr><th>区分</th><th>市町数</th><th>合計面積 km²</th>"
    "<th>合計人口 千人</th><th>施策ポリゴン</th>"
    "<th>施策面積 ha</th><th>施策面積率</th></tr>"
    f"<tr><td><b>L25 施策あり 17</b></td><td>17</td>"
    f"<td>{sum(CITY_REF[d[1]]['area_km2'] for d in CITY_DEFS):,.0f}</td>"
    f"<td>{sum(CITY_REF[d[1]]['pop_k'] for d in CITY_DEFS):,}</td>"
    f"<td>{N_POLY:,}</td>"
    f"<td>{AREA_TOTAL_HA:,.0f}</td>"
    f"<td>{AREA_TOTAL_HA/sum(CITY_REF[d[1]]['area_km2'] for d in CITY_DEFS)/100*100:.2f}%</td></tr>"
    f"<tr><td><b>L25 施策無し 3</b></td><td>3</td>"
    f"<td>{sum(CITY_REF[n[0]]['area_km2'] for n in NO_POLICY_CITIES):,.0f}</td>"
    f"<td>{sum(CITY_REF[n[0]]['pop_k'] for n in NO_POLICY_CITIES):,}</td>"
    f"<td>0</td><td>0</td><td>0%</td></tr>"
    f"<tr><td><b>計 (20 市町)</b></td><td>20</td>"
    f"<td>{sum(CITY_REF[c]['area_km2'] for c in CITY_REF):,.0f}</td>"
    f"<td>{sum(CITY_REF[c]['pop_k'] for c in CITY_REF):,}</td>"
    f"<td>{N_POLY:,}</td>"
    f"<td>{AREA_TOTAL_HA:,.0f}</td>"
    f"<td>{AREA_TOTAL_HA/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)/100*100:.2f}%</td></tr>"
    "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"L25 施策の県内面積率は <b>{AREA_TOTAL_HA/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)/100*100:.2f}%</b> ─ "
    f"L24 (約 18%) より<b>桁違いに小さい</b>。"
    f"これは<b>『施策ポリゴンは制度的に保全対象を切り出した「キワ」のエリア』</b>であり、"
    f"L24 の<b>『農地転用の対象農地全体』</b>とは粒度が異なることを示す。"
    f"<b>1 町スワップ</b> (熊野町↔坂町) は L24/L25 のシリーズ性格を表象 ─ "
    f"L24 は<b>動的指標 (転用フラグ)</b>、L25 は<b>静的指標 (制度区分)</b>。</p>"
)
sections.append(("5. 分析2: 県全域マップ + 17 vs 3 + 1 町スワップ (H1)", s5_html))


# === セクション6: 分析3 — 市町別 small multiples + ランキング ===
city_table_rows = "".join(
    f"<tr><td>{r['city']}</td><td>{r['rtype']}</td>"
    f"<td>{r['n_poly']:,}</td>"
    f"<td>{r['n_multipoly']:,}</td>"
    f"<td>{r['multi_pct']:.1f}%</td>"
    f"<td>{r['area_ha_sum']:,.0f}</td>"
    f"<td>{r['area_ha_max']:,.1f}</td>"
    f"<td>{r['policy_per_km2']:.2f}</td>"
    f"<td>{r['policy_per_capita_a']:.1f}</td></tr>"
    for _, r in city_agg.sort_values("area_ha_sum", ascending=False).iterrows()
)

s6_html = (
    "<h3>狙い</h3>"
    "<p>17 市町の施策配置パターンを <b>17 panels small multiples</b> で並列比較する。"
    "市町ごとの<b>「保全策が市域のどこに、どう広がっているか」</b>を一目で見せ、"
    "都市 vs 中山間 vs 離島 vs 近郊町の質的違いを地図で示す。"
    "ランキング 3 連 panel で、<b>『面積』『件数』『MultiPolygon 比率』の 3 軸</b>から"
    "市町順位を立体的に描く。MultiPolygon 比率 (= 集落+分散圃場の指標) は L25 特有の"
    "新しい軸で、L24 では使えない。</p>"

    "<h3>手法</h3>"
    "<p><code>plt.subplots(4, 5)</code> で 20 panel を作り、17 市町 + 1 (3 町) + 余り 2。"
    "各 panel は<b>市域に bbox を揃え</b>、施策ポリゴンを<b>地理タイプ色</b>で描画。"
    "ランキングは横軸を 3 つに切って barh で並べる ─ 件数は log スケール。"
    "<b>MultiPolygon 比率</b>は地形 (集落配置) の指標として読める。</p>"

    "<h3>実装</h3>"
    + code('''
# small multiples 17 panels + 3 町 panel
fig, axes = plt.subplots(4, 5, figsize=(20, 16))
for ax_, (dsid, name, _adm, ctype, rtype) in zip(axes.flat, CITY_DEFS):
    cnt = admin_diss[admin_diss["city"] == name]
    cnt.plot(ax=ax_, facecolor="#f5f5f5", edgecolor="#888")
    sub = agro[agro["src_city"] == name]
    sub.plot(ax=ax_, facecolor=RTYPE_COLOR[rtype], alpha=0.65)
    n = len(sub); a_ha = sub["geom_area_ha"].sum()
    ax_.set_title(f"{name} ({rtype}) {n:,}件 / {a_ha:,.0f} ha")

# 3 連ランキング (面積/件数/MultiPolygon比率)
fig, axes = plt.subplots(1, 3, figsize=(18, 9))
order_a = city_agg.sort_values("area_ha_sum")
axes[0].barh(order_a["city"], order_a["area_ha_sum"],
             color=[RTYPE_COLOR[rt] for rt in order_a["rtype"]])
order_n = city_agg.sort_values("n_poly")
axes[1].barh(order_n["city"], order_n["n_poly"], color=...)
axes[1].set_xscale("log")  # 件数は log スケール
order_m = city_agg.sort_values("multi_pct")
axes[2].barh(order_m["city"], order_m["multi_pct"], color=...)
''') +

    "<h3>図 3 — 市町別 17 panels small multiples</h3>"
    "<p><b>なぜこの図か</b>: 1 枚の県全域図では、市町ごとの<b>「施策パターンの質的違い」</b>"
    "が分からない。17 panels で並列比較すると、"
    "「庄原市の市域全体に散布」と「広島市の市域北側のみ」が"
    "同じスケールで対比でき、<b>市町タイプと施策パターンの関係</b>が直感的に伝わる。</p>"
    + figure("assets/L25_fig03_small_multiples.png",
              "市町別 農林漁業施策配置 small multiples (17 + 無し 3 町)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>広島市</b> ({load_log[0]['n_poly']:,} 件 / {city_agg[city_agg['city']=='広島市']['area_ha_sum'].iloc[0]:,.0f} ha): "
    "件数最多だが市域北部 (CITY_CD 105/106/108 = 安佐南・安佐北・安芸区) に限定。"
    "市中央〜南のデルタには施策ポリゴンが無い。"
    "<b>『政令市の北縁山際だけが食農基盤』</b>の地理構造。</li>"
    f"<li><b>庄原市・三次市・北広島町・世羅町・安芸高田市</b> (中山間 5): "
    "市域全体に施策ポリゴンが散布。<b>『市域全体が食農基盤』</b>。"
    "MultiPolygon 比率も高く、集落配置 (谷間の小集落) が反映されている。</li>"
    f"<li><b>東広島市</b> ({load_log[3]['n_poly']:,} 件): "
    "中山間 + 都市の混合で施策ポリゴン多数 (358 件)、"
    "西条 (大学・産業) 周辺は開発済で施策少なめ。"
    "市域中央〜北西に集中。</li>"
    "<li><b>江田島市 (16 件)</b>: 瀬戸内の小島群に分散。離島農業の名残り。</li>"
    "<li><b>坂町 (1 件)</b>: <b>湾岸の漁業基地のみ</b> ─ 政令市通勤圏で唯一残る食料基盤。</li>"
    "<li><b>大竹市 (9 件) / 廿日市市 (32 件) / 府中市 (44 件)</b>: 件数が少ない都市市町。"
    "<b>『施策の対象が制度的に切り出された数件』</b>のみ。</li>"
    "<li><b>右下 panel (3 町)</b>: 熊野町・府中町・海田町は<b>市街化 100%</b>で施策ゼロ ─ "
    "L24 と異なり熊野町も含まれる (転用対象はあるが施策対象は無い)。</li>"
    "</ul>"

    "<h3>図 4 — 市町別 面積/件数/MultiPolygon比率 ランキング 3 連</h3>"
    "<p><b>なぜこの図か</b>: 市町を 3 つの異なる指標 ─ 「総面積」"
    "「ポリゴン件数」「MultiPolygon 比率」 ─ で並べると、"
    "<b>都市 vs 中山間の構造の違い</b>が立体的に見える。"
    "MultiPolygon 比率は<b>「集落+分散圃場」</b>を反映する地形指標で、"
    "中山間ほど高くなる。</p>"
    + figure("assets/L25_fig04_city_ranking.png",
              "市町別 総施策面積 / 件数 / MultiPolygon 比率 (3 連 panel)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>左 (総施策面積 ha)</b>: <b>東広島市 {city_agg[city_agg['city']=='東広島市']['area_ha_sum'].iloc[0]:,.0f} ha</b> がトップ ─ "
    f"L24 ではトップは庄原市 (中山間) だったが、L25 では<b>都市 (東広島市)</b>。"
    f"次に三次市・福山市・広島市・尾道市・庄原市・世羅町と続く。"
    f"<b>L24 と L25 で上位ランキングが大きく異なる</b> ─ "
    f"L25 (制度的施策) は L24 (転用対象) と粒度が違う。</li>"
    f"<li><b>中央 (件数 log)</b>: 広島市 {city_agg[city_agg['city']=='広島市']['n_poly'].iloc[0]:,} 件、"
    f"東広島市 358、福山市 318 が上位 ─ 都市市町で件数多い。"
    f"逆に坂町 1、大竹市 9、江田島市 16 は最下位。"
    f"件数と面積のランキングは大きく異なる ─ <b>『件数 ≠ 面積』</b>。</li>"
    f"<li><b>右 (MultiPolygon 比率)</b>: <b>世羅町 26.7%, 庄原市 26.3%, 三次市 24.0%</b> が上位 (中山間)。"
    f"対して大竹市 0%, 廿日市市 0%, 北広島町 0%, 江田島市 0% は単一 Polygon のみ。"
    f"<b>『MultiPolygon 比率は集落配置の地形指標』</b>として機能 ─ "
    f"中山間市町の谷間集落+分散圃場が MultiPolygon を生む。"
    f"ただし北広島町は中山間にも関わらず 0% ─ 比較的まとまった農地で運用されている可能性。</li>"
    "<li>3 指標を合わせて見ると、市町は次の 4 タイプに分類:"
    "<b>(I) 都市大規模型</b> (東広島市・広島市・福山市) = 大面積・多件数・低MultiP%、"
    "<b>(II) 中山間散在型</b> (世羅町・庄原市・三次市) = 中面積・中件数・高MultiP%、"
    "<b>(III) 都市少数型</b> (大竹市・廿日市市・府中市) = 小面積・少件数・低MultiP%、"
    "<b>(IV) 特異型</b> (坂町=漁業のみ、江田島市=離島) = 極小・特殊。</li>"
    "</ul>"

    "<h3>表 3 — 17 市町 集計表 (面積降順)</h3>"
    "<table><tr><th>市町</th><th>地理</th><th>件数</th><th>MultiP</th>"
    "<th>Multi%</th><th>面積ha</th><th>最大ha</th><th>密度</th>"
    "<th>1人あたり(a)</th></tr>"
    + city_table_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"面積 1 位は<b>東広島市 {city_agg.iloc[city_agg['area_ha_sum'].idxmax()]['area_ha_sum']:,.0f} ha</b>、"
    f"最下位は<b>坂町 {city_agg[city_agg['city']=='坂町']['area_ha_sum'].iloc[0]:,.1f} ha</b>。"
    f"<b>1 人あたり施策面積</b>は<b>庄原市 {city_agg[city_agg['city']=='庄原市']['policy_per_capita_a'].iloc[0]:.1f} a/人</b>"
    f" や 世羅町 {city_agg[city_agg['city']=='世羅町']['policy_per_capita_a'].iloc[0]:.1f} a/人 と中山間で大きく、"
    f"政令市は<b>{city_agg[city_agg['city']=='広島市']['policy_per_capita_a'].iloc[0]:.2f} a/人 (広島市)</b>と最小。"
    f"<b>『中山間住民は 1 人あたり数十 a の施策対象を背景に持つ』</b> vs "
    f"<b>『都市住民は 0.0X a の施策対象しかない』</b>という対比。"
    f"L24 (1 人あたり対象農地数 a〜数百 a) と桁が違うのは、L25 が制度的「キワ」のみだから。</p>"
)
sections.append(("6. 分析3: 市町別 small multiples + ランキング (H3)", s6_html))

# === セクション7: 分析4 — NRG_AN 文字列分析 (中核) ===
nrg_overall_rows = "".join(
    f"<tr><td>{r['nrg_class']}</td>"
    f"<td>{r['n']:,}</td>"
    f"<td>{r['n_pct']:.1f}%</td>"
    f"<td>{r['area_ha_sum']:,.0f}</td>"
    f"<td>{r['area_pct']:.1f}%</td></tr>"
    for _, r in nrg_overall.sort_values("n", ascending=False).iterrows()
)

# NRG_AN 頻度 top 10
nrg_freq_rows = "".join(
    f"<tr><td>{i+1}</td><td>{r['nrg_an']}</td><td>{r['freq']}</td></tr>"
    for i, (_, r) in enumerate(nrg_freq.head(10).iterrows())
)

s7_html = (
    "<h3>狙い</h3>"
    "<p>L25 シリーズの<b>主役の文字列列 NRG_AN</b> を実データで観察し、"
    "施策の<b>『質的構造』</b>を可視化する。"
    "「ダム」「池」「漁業」「ほ場整備」など特定キーワードを含むかで施策タイプを"
    "8 区分に分類し、<b>『施策ポリゴンの 90% は地名のみ、10% が施設・整備』</b>"
    "という H2 を検証する。"
    "監査時「広島市 NRG_AN は None」結論を実データで反証する H6 もここで処理。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) NRG_AN 分類別 件数% vs 面積% 双子バー</b>: "
    "8 タイプ各々が全体に占める比率を、<b>件数比</b> (青) と <b>面積比</b> (赤) の"
    "2 本並列で表示。<b>『件数の少数派が面積の多数派になる』</b>パレート構造の検証。</p>"

    "<p><b>(b) NRG_AN 分類 × 市町 stacked bar (件数比率)</b>: "
    "市町別の NRG_AN 分類分布を 100% 棒グラフで並べ、"
    "<b>『市町ごとの施策の質的違い』</b> (大竹市は基地・施設のみ vs 中山間は地区名主体) を示す。</p>"

    "<p><b>(c) NRG_AN 分類別 県全域マップ</b>: "
    "ダム・ため池・漁業などをそれぞれ別色で塗り分けた主題図。"
    "<b>『どこにどの施策があるか』</b>を地理的に可視化。"
    "ダムは特定地点 (大規模水源)、漁業は瀬戸内沿岸限定、ほ場整備は中山間や"
    "都市郊外に分散、というパターンが期待される。</p>"

    "<h3>実装</h3>"
    + code('''
NRG_KEYWORDS = [
    ("ダム",     ["ダム"]),
    ("ため池",   ["池", "溜池", "ため池"]),
    ("漁業",     ["漁業", "漁港", "漁場", "養殖"]),
    ("林業",     ["林", "森林", "山林"]),
    ("ほ場整備", ["ほ場整備", "圃場整備", "ほ場", "圃場", "整備", "区画整理"]),
    ("基地・施設", ["基地", "施設", "場"]),
]

def classify_nrg_an(s):
    if pd.isna(s) or s is None or str(s).strip() == "":
        return "_欠損"
    text = str(s)
    for label, kws in NRG_KEYWORDS:
        for kw in kws:
            if kw in text:
                return label
    return "地区名のみ"

agro["nrg_class"] = agro["NRG_AN"].apply(classify_nrg_an)

# 全体集計
nrg_overall = agro.groupby("nrg_class").agg(
    n=("geom_area_ha", "size"),
    area=("geom_area_ha", "sum"),
).reset_index()
nrg_overall["n_pct"]    = nrg_overall["n"] / N_POLY * 100
nrg_overall["area_pct"] = nrg_overall["area"] / AREA_TOTAL_HA * 100

# 市町×分類クロス
nrg_cross_n = agro.groupby(["src_city","nrg_class"]).size().unstack(0)
''') +

    "<h3>図 5 — NRG_AN 分類別 件数% vs 面積% (左) + 市町別構成 (右)</h3>"
    "<p><b>なぜこの図か</b>: NRG_AN 分類は本記事の中核手法。"
    "<b>件数 % と面積 % の関係</b>を双子バーで見ると、"
    "<b>『地区名のみが大多数だが、面積では「ダム」「ほ場整備」が大きい』</b>という"
    "<b>規模差</b>が読み取れる。市町別 stacked bar で<b>市町タイプ別の施策性格</b>"
    "(漁業の多い坂町・江田島市 vs 地区名主体の中山間) を可視化。</p>"
    + figure("assets/L25_fig05_nrg_class.png",
              "NRG_AN 分類別 件数% vs 面積% (左) + 市町別 NRG_AN 分類構成 (右)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>地区名のみ {nrg_only_pct:.0f}%</b> が件数で圧倒的多数 ─ "
    f"<b>H2 支持: 広島県の農林漁業施策は『集落単位の地区名』で運営</b>。</li>"
    f"<li>キーワード分類は計 {keyword_pct:.0f}% ─ "
    f"<b>「ほ場整備」「ため池」「ダム」「漁業」「林業」「基地・施設」の合計が約 1 割</b>。"
    f"残り 9 割は地名のみ。</li>"
    f"<li><b>面積比では『ダム』『ほ場整備』『林業』が件数比より大きい</b> ─ "
    f"これらは<b>大規模な制度区分</b>として図面化されているため。"
    f"対して『地区名のみ』は件数では多いが面積比は件数比より小さい。</li>"
    "<li><b>市町別構成 (右図)</b>: <b>坂町は『漁業』100%</b> ─ 唯一無二の漁業基地データ。"
    "<b>江田島市は地区名のみが大半</b>だが、離島農業の伝統。"
    "<b>東広島市・庄原市</b>は「ダム」「ため池」「ほ場整備」が混在 ─ "
    "中規模都市での農業基盤整備の実態。</li>"
    "<li><b>大竹市</b>: 基地・施設タイプが多く、地区名のみは少ない ─ "
    "工業都市で農地が少ないため施策ポリゴン自体が制度的なものに集約。</li>"
    "</ul>"

    "<h3>図 6 — NRG_AN 分類別 県全域マップ</h3>"
    "<p><b>なぜこの図か</b>: 分類タイプを地理的に分布させると、"
    "<b>『どの施策がどこにあるか』</b>が直視できる。"
    "ダムは内陸山間 (大規模水源)、漁業は瀬戸内沿岸限定、ほ場整備は中山間や郊外、"
    "という地理的偏りが現れるはず。</p>"
    + figure("assets/L25_fig06_nrg_map.png",
              "NRG_AN 分類別 県全域マップ — 施策の質的分布") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>灰色 (地区名のみ)</b> が県全域に広く分布 ─ 大半の施策ポリゴンが"
    "<b>『集落単位の指定』</b>であることを再確認。</li>"
    "<li><b>青 (ダム)</b> は内陸山間に点在 ─ <b>東広島市の大久保ダム・椛の木ダム・"
    "千丈ヶ原ダム、三次市湯口谷池</b>等、中山間水源域の特定地点。"
    "瀬戸内沿岸には基本的に存在しない。</li>"
    "<li><b>水色 (ため池)</b> は瀬戸内沿岸 (東広島・三原・尾道・福山) と中山間 (三次・庄原)"
    "の両方に分散 ─ <b>『ため池は古代灌漑設備の遺産』</b>として全県的に存在。</li>"
    "<li><b>紺色 (漁業)</b> は瀬戸内沿岸限定 ─ <b>呉市・坂町・江田島市の沿岸</b>のみ。"
    "<b>『漁業基地は海岸線の地形依存』</b>の典型。</li>"
    "<li><b>緑 (林業)</b> は中山間山際に点在 ─ <b>三次市・庄原市・東広島市</b>の山林域。"
    "ただし「林田」のような地名 (= 地区名のみだが「林」を含む) も混入の可能性。</li>"
    "<li><b>オレンジ (ほ場整備)</b> は東広島市・三次市・庄原市・北広島町の中山間で"
    "中規模に展開 ─ <b>『農業基盤整備事業の現代的痕跡』</b>。</li>"
    "<li><b>紫 (基地・施設)</b> は瀬戸内沿岸 + 大竹市の限定的な点 ─ 工業/軍事関連の遺産。</li>"
    "</ul>"

    "<h3>表 4 — NRG_AN 分類別 全体集計</h3>"
    "<table>"
    "<tr><th>分類</th><th>件数</th><th>件数 %</th>"
    "<th>面積 ha</th><th>面積 %</th></tr>"
    + nrg_overall_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>地区名のみ {nrg_only_pct:.1f}%</b> が件数の主流。"
    f"特定キーワード上位は <b>「ほ場整備」</b> "
    f"({float(nrg_overall[nrg_overall['nrg_class']=='ほ場整備']['n'].iloc[0] if 'ほ場整備' in nrg_overall['nrg_class'].values else 0):.0f} 件)、"
    f"<b>「ため池」</b>。"
    f"<b>面積比でも地区名が最大</b>だが、<b>「ダム」</b>と<b>「ほ場整備」</b>は"
    f"件数比より面積比が大きい ─ 大規模な区域指定として図面化されている証拠。"
    f"<b>「漁業」「基地・施設」</b>は件数も面積も少ない (合計 5% 程度)。"
    f"<b>欠損 (NRG_AN null)</b> は全体の 3.6% で、福山市・尾道市に偏在。</p>"

    "<h3>表 5 — NRG_AN 文字列 出現頻度 top 10</h3>"
    "<table>"
    "<tr><th>順位</th><th>NRG_AN</th><th>出現件数</th></tr>"
    + nrg_freq_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"NRG_AN 文字列は<b>市町境を跨いで重複しないユニーク地名</b>がほとんど。"
    f"top 1 が頻度 {nrg_freq.iloc[0]['freq']} 件 = 全体の {nrg_freq.iloc[0]['freq']/N_POLY*100:.1f}% にすぎず、"
    f"<b>『各施策ポリゴンが固有の地区名を持つ』</b>こと示す。"
    f"L24 では BIKOU 列に「国土数値情報を活用して作成したため、図面精度は1/50,000である」"
    f"が大半を占めていたのと対照的 ─ <b>L25 の NRG_AN は意味のある属性データ</b>。"
    f"top 10 はほぼ全て<b>地名・池名・ダム名</b>で、<b>『施策ごとに固有の名称』</b>が"
    f"付与されている = データ品質の高さを示す。</p>"

    "<h3>H6 検証: 広島市 NRG_AN null 比率</h3>"
    f"<p>監査時 (2026-05-03) のサンプル 1 行は NRG_AN = None だったが、"
    f"<b>実データ 426 行中 NRG_AN = null は 2 行のみ</b> "
    f"(= <b>{hiroshima_nrg_null_pct:.1f}%</b>) で、99.5% は地区名で埋まる。"
    f"<b>監査結論「広島市 NRG_AN None」は誤り</b> ─ サンプル 1 行が偶発的に None だっただけ。"
    f"<b>『データ全体を見ずにサンプル 1 行で判断するな』</b>のデータ取扱い教訓。"
    f"<b>判定: H6 支持</b>。</p>"
)
sections.append(("7. 分析4: NRG_AN 文字列分類 (中核, H2 + H6)", s7_html))


# === セクション8: 分析5 — KUIKI_CD + TOKEI_CD 解読 ===
kuiki_overall_rows = "".join(
    f"<tr><td>{r['KUIKI_CD']}</td>"
    f"<td>{r['n']:,}</td>"
    f"<td>{r['n_pct']:.1f}%</td>"
    f"<td>{r['area_ha_sum']:,.0f}</td>"
    f"<td>{r['area_pct']:.1f}%</td></tr>"
    for _, r in kuiki_overall.iterrows()
)
tokei_overall_rows = "".join(
    f"<tr><td>{r['TOKEI_CD']}</td>"
    f"<td>{r['n']:,}</td>"
    f"<td>{r['n_pct']:.1f}%</td>"
    f"<td>{r['area_ha_sum']:,.0f}</td>"
    f"<td>{r['area_pct']:.1f}%</td></tr>"
    for _, r in tokei_overall.iterrows()
)

s8_html = (
    "<h3>狙い</h3>"
    "<p>L24 と同じ列名 <b>KUIKI_CD (区域コード, 0-7) と TOKEI_CD (統計区分, 1-3)</b> の"
    "意味を、L25 でも実データから解読する。"
    "<b>『L24 との整合性』</b>を検証 ─ 同じ列名なら同じ意味のはず。"
    "中山間で 0 主体、都市で 1/2 混在のパターンが両シリーズで一致するか。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) KUIKI_CD 別 県全域マップ</b>: 各値を異なる色で塗り、"
    "地理分布から仮説を立てる。"
    "<b>『線引き都市計画区域 (市街化/調整区域)』</b>の都市市町と、"
    "<b>『線引きなし』</b>の中山間市町で値分布が異なるはず。"
    "L24 で観察された傾向が L25 でも同じか。</p>"

    "<p><b>(b) KUIKI_CD × 市町 stacked bar</b>: 各市町の値分布を 100% 棒グラフで並べる。</p>"

    "<h3>実装</h3>"
    + code('''
# KUIKI_CD 7 値を固定パレットで色分け (推定意味)
kuiki_palette = {
    0: "#1f883d",   # 0 = 区域外/非線引き? (中山間多い、緑)
    1: "#cf222e",   # 1 = 市街化区域? (都市多い、赤)
    2: "#a371f7",   # 2 = 調整区域? (都市の最大値、紫)
    3: "#0969da",   # 3 = 中間階層? (青)
    4: "#bf3989",   # 4 = ? (ピンク)
    6: "#666666",   # 6 = ? (灰)
}

fig, axes = plt.subplots(1, 2, figsize=(18, 9))
for kc, col in kuiki_palette.items():
    sub = agro[agro["KUIKI_CD"] == kc]
    sub.plot(ax=axes[0], facecolor=col, alpha=0.65)

# 市町×KUIKI_CD クロス stacked bar
kc_pct = (kuiki_cross_n.div(kuiki_cross_n.sum(axis=1), axis=0)) * 100
kc_pct.plot(kind="barh", stacked=True, ax=axes[1])
''') +

    "<h3>図 7 — KUIKI_CD 別 県全域マップ + 市町クロス</h3>"
    "<p><b>なぜこの図か</b>: KUIKI_CD は仕様書未公開の「ブラックボックス列」。"
    "<b>地理分布</b>と<b>市町クロス</b>の 2 角度から見ることで、"
    "<b>都市計画階層フラグ</b>という解読仮説の妥当性を検証する。</p>"
    + figure("assets/L25_fig08_kuiki_cd.png",
              "KUIKI_CD 別 県全域マップ (左) + 市町別 KUIKI_CD 構成 (右)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>緑 (KUIKI_CD=0) が中山間で支配的</b> ─ {mountain_k0:.0f}% (中山間 5 市町平均)。"
    f"<b>『区域外/非線引き都市計画区域』</b>と推定 ─ L24 と完全整合。</li>"
    f"<li><b>紫 (KUIKI_CD=2) は都市市町に偏在</b> ─ {urban_k2:.0f}% (都市 10 市平均)。"
    f"<b>『市街化調整区域』</b>と推定 ─ 都市部の農地が調整区域に多いと整合的。</li>"
    "<li><b>赤 (KUIKI_CD=1) は東広島市と福山市の一部のみ</b> ─ "
    "<b>『市街化区域』</b>と推定 ─ 件数は少ない (= 市街化区域には農地が少ない)。</li>"
    f"<li><b>ピンク (KUIKI_CD=4) は廿日市市・尾道市・呉市・江田島市</b>に分布 ─ "
    f"L24 でも見られた中間階層フラグ。<b>『非線引き都市計画区域』</b>か。</li>"
    "<li>市町別 stacked bar (右) で<b>都市/中山間の構造的差</b>が一目瞭然: "
    "中山間市町は緑 (KUIKI=0) 一色、都市市町は紫+緑+ピンクの混合。</li>"
    f"<li><b>判定: H4 支持</b> ─ KUIKI_CD は L24 と同じ意味で、"
    f"<b>都市計画区域階層フラグ</b>として機能している。</li>"
    "</ul>"

    "<h3>表 6 — KUIKI_CD 全体集計</h3>"
    "<table><tr><th>KUIKI_CD</th><th>件数</th><th>件数 %</th>"
    "<th>面積 ha</th><th>面積 %</th></tr>"
    + kuiki_overall_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>KUIKI_CD = 0 が件数の最大値</b> ({float(kuiki_overall[kuiki_overall['KUIKI_CD']==0]['n_pct'].iloc[0] if 0 in kuiki_overall['KUIKI_CD'].values else 0):.1f}%) で、"
    f"中山間+一部都市 (区域外/非線引き) の合計。"
    f"<b>面積比でも KUIKI=0 が最大</b> ─ 大規模な区域指定が多い。"
    f"<b>KUIKI=1 (市街化区域) は件数 {float(kuiki_overall[kuiki_overall['KUIKI_CD']==1]['n_pct'].iloc[0] if 1 in kuiki_overall['KUIKI_CD'].values else 0):.1f}%</b> ─ "
    f"市街化区域内の農地は施策対象が少ない (= 都市化が優先)。"
    f"<b>KUIKI=2 (調整区域)</b> は都市市町の主要値。</p>"

    "<h3>表 7 — TOKEI_CD 全体集計</h3>"
    "<table><tr><th>TOKEI_CD</th><th>件数</th><th>件数 %</th>"
    "<th>面積 ha</th><th>面積 %</th></tr>"
    + tokei_overall_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>TOKEI_CD = 3 が件数最大</b> ({float(tokei_overall[tokei_overall['TOKEI_CD']==3]['n_pct'].iloc[0] if 3 in tokei_overall['TOKEI_CD'].values else 0):.0f}%) ─ "
    f"L24 と同じ傾向。<b>『非線引き地域の施策』</b>を表すフラグと推定。"
    f"<b>TOKEI_CD = 1</b> は都市計画区域内の施策、<b>TOKEI_CD = 2</b> は中間。"
    f"3 値はそれぞれ意味が異なるが、本記事では深追いしない (発展課題)。</p>"
)
sections.append(("8. 分析5: KUIKI_CD + TOKEI_CD 解読 (H4)", s8_html))

# === セクション9: 分析6 — L24 vs L25 空間重複 (中核2) ===
overlap_rows = "".join(
    f"<tr><td>{r['city']}</td><td>{r['rtype']}</td>"
    f"<td>{r['a_total_ha']:,.0f}</td>"
    f"<td>{r['f_total_ha']:,.0f}</td>"
    f"<td><b>{r['overlap_ha']:,.0f}</b></td>"
    f"<td>{r['overlap_pct_a']:.1f}%</td>"
    f"<td>{r['overlap_pct_f']:.2f}%</td></tr>"
    for _, r in overlap_df.sort_values("overlap_pct_a",
                                          ascending=False).iterrows()
)

# top 大規模施策 top 10 行
top10_large_rows = "".join(
    f"<tr><td>{i+1}</td><td>{r['src_city']}</td><td>{r['rtype']}</td>"
    f"<td>{r['NRG_AN'] if pd.notna(r['NRG_AN']) else '(null)'}</td>"
    f"<td>{r['nrg_class']}</td><td>{r['geom_type']}</td>"
    f"<td>{r['geom_area_ha']:,.1f}</td>"
    f"<td>{r['KUIKI_CD']}</td>"
    f"<td>{r['scale_class']}</td></tr>"
    for i, (_, r) in enumerate(top20_large.head(10).iterrows())
)

s9_html = (
    "<h3>狙い</h3>"
    "<p>本記事の<b>もう 1 つの中核分析</b>: 同じ 16 市町 (L24 ∩ L25) について、"
    "<b>農地転用ポリゴン (L24, 動的) と農林漁業施策ポリゴン (L25, 静的)</b> を"
    "空間重ね合わせ、<b>「保全策と転用がどう同居/分離するか」</b>を定量化する。"
    "完全すみ分け (重複 0%) なら『保全と転用が分離』、"
    "完全重なり (重複 100%) なら『保全策が形骸化、転用と同領域』、"
    "中間 (10-30%) なら『せめぎあい』 ─ どのモードか実データで判別する。</p>"

    "<h3>手法</h3>"
    "<p><b>STEP 1: L24 ZIP 17 件を読み込む</b> ─ "
    "L24 は EPSG:6671、L25 は EPSG:2445 → 6671 変換済 ─ 両者を同じ CRS に揃える。</p>"

    "<p><b>STEP 2: 各市町ごとに <code>shapely.union_all().intersection()</code></b> ─ "
    "(a) 施策ポリゴンを 1 つの大きな MultiPolygon にまとめる "
    "(<code>agro_sub.geometry.union_all()</code>)、"
    "(b) 同様に転用ポリゴンも統合 (<code>farm_sub.geometry.union_all()</code>)、"
    "(c) <code>a.intersection(b)</code> で重なり領域を計算、"
    "(d) <code>.area / 1e4</code> で重なり面積 (ha) を得る。</p>"

    "<p><b>注意</b>: 一部市町でジオメトリの自己交差により"
    "<code>TopologyException</code> が発生する。"
    "<code>shapely.validation.make_valid()</code> または <code>buffer(0)</code> で"
    "ジオメトリを修復してから再試行する 2 段フォールバックを実装。</p>"

    "<p><b>(c) 可視化</b>: 県全域に L24 (赤) ⊕ L25 (緑) を重ねる主題図 + "
    "市町別重複率 barh の 2 連 panel。</p>"

    "<h3>実装</h3>"
    + code('''
from shapely.validation import make_valid

# STEP 1: L24 17 件を読み込み (CRS = EPSG:6671)
farm_frames = []
for dsid, name in L24_DEFS:
    z = L24_DIR / f"farm_{dsid}_{name}.zip"
    f = load_geojson_zip(z)
    f["src_city"] = name
    f = f.to_crs(TARGET_CRS)
    farm_frames.append(f)
farm = gpd.GeoDataFrame(pd.concat(farm_frames, ignore_index=True),
                        geometry="geometry", crs=TARGET_CRS)

# STEP 2: 共通 16 市町で各々 intersection
common_cities = sorted(set(d[1] for d in CITY_DEFS) & set(d[1] for d in L24_DEFS))
overlap_log = []
for city in common_cities:
    a_sub = agro[agro["src_city"] == city]
    f_sub = farm[farm["src_city"] == city]
    a_total_ha = float(a_sub.geometry.area.sum() / 1e4)
    f_total_ha = float(f_sub.geometry.area.sum() / 1e4)

    # 第1試行: union_all + make_valid
    overlap_ha = 0.0
    try:
        a_uni = make_valid(a_sub.geometry.union_all())
        f_uni = make_valid(f_sub.geometry.union_all())
        overlap_ha = a_uni.intersection(f_uni).area / 1e4
    except Exception:
        # 第2試行: buffer(0) でジオメトリ修復
        try:
            a_buf = a_sub.geometry.buffer(0).union_all()
            f_buf = f_sub.geometry.buffer(0).union_all()
            overlap_ha = a_buf.intersection(f_buf).area / 1e4
        except Exception:
            overlap_ha = 0.0

    overlap_log.append({"city": city,
                        "a_total_ha": a_total_ha,
                        "f_total_ha": f_total_ha,
                        "overlap_ha": overlap_ha,
                        "overlap_pct_a": overlap_ha / a_total_ha * 100,
                        "overlap_pct_f": overlap_ha / f_total_ha * 100})
''') +

    "<h3>図 7 — L24 (転用) ⊕ L25 (施策) 重ね合わせマップ + 市町別重複率</h3>"
    "<p><b>なぜこの図か</b>: <b>『同じ市町内で、保全策エリアと転用エリアが</b>"
    "<b>同居しているか分離しているか』</b>は地図でないと伝わらない。"
    "右側の市町別重複率 barh は<b>『どの市町でせめぎあいが激しいか』</b>を定量化。"
    "本記事の最大の発見が現れる。</p>"
    + figure("assets/L25_fig07_overlap_l24.png",
              "L24 (転用, 赤) ⊕ L25 (施策, 緑) 重ね合わせ + 市町別重複率") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>左 (重ね合わせマップ)</b>: 緑 (L25 施策) と赤 (L24 転用) が"
    f"<b>大半の中山間市町で同じ場所に重なっている</b>。"
    f"特に庄原市・三次市・北広島町・東広島市の中山間部で<b>橙黄色 (緑+赤の重ね)</b>が広域に。"
    f"<b>『保全策のエリア = 転用記録のエリア』</b>が大半。"
    f"完全すみ分けの市町は無く、施策エリアの上に転用記録が重なる構造。</li>"
    f"<li><b>右 (市町別重複率)</b>: <b>多くの市町で重複率 50% 以上</b> ─ "
    f"つまり<b>施策面積の半分以上が転用記録と重なる</b>。"
    f"これは<b>H5 反証</b> ─ 予想 (10-30% せめぎあい) より<b>遥かに大きい重複</b>。</li>"
    f"<li>全 16 共通市町: <b>施策総 {total_a_ha:,.0f} ha / 転用総 {total_f_ha:,.0f} ha / "
    f"重複 {total_overlap_ha:,.0f} ha</b> = "
    f"<b>施策の {overlap_pct_a_total:.1f}% が転用と重複、"
    f"転用の {overlap_pct_f_total:.2f}% が施策と重複</b>。</li>"
    f"<li><b>解釈の修正</b>: 重複が大きいことは<b>『保全策の形骸化』</b>を意味するのではなく、"
    f"<b>『同じ農用地区域を、保全策側 (L25) と転用記録側 (L24) の両方で図面化』</b>"
    f"していると解釈すべき。"
    f"L24 の「転用対象農地」は<b>『過去 5 年に転用許可申請された対象農地区分』</b>であり、"
    f"L25 の「施策」は<b>『現在の制度的指定』</b> ─ "
    f"両者は同じ区域を別の角度から見たもので、本来重なる方が自然。"
    f"重複が大きい = <b>『広島県の食農政策は、保全と転用を同じ区域で同時に運営』</b>"
    f"= 行政の二重行政ではなく、同じ農地への異なる介入記録。</li>"
    f"<li>転用 (L24) のうち施策 (L25) と重ならない部分は <b>{100-overlap_pct_f_total:.0f}%</b>。"
    f"これは<b>『施策の対象外で発生した転用』</b> = 制度的に指定されていなかった農地で"
    f"勝手に転用が進んだ部分。"
    f"これも県の食農政策に対する重要な情報。</li>"
    "</ul>"

    "<h3>表 8 — 共通 16 市町 L24 vs L25 重複率</h3>"
    "<table>"
    "<tr><th>市町</th><th>地理</th>"
    "<th>L25 施策 ha</th><th>L24 転用 ha</th>"
    "<th>重複 ha</th><th>施策中重複%</th><th>転用中重複%</th></tr>"
    + overlap_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>施策中重複率 (= 施策の何 % が転用と重なる)</b> は中山間市町で 60-90% と高く、"
    f"都市市町で 30-60% と低めの傾向。"
    f"<b>転用中重複率 (= 転用の何 % が施策と重なる)</b> は全市町で 1-15% と低い ─ "
    f"<b>『転用対象は施策エリアの何倍も広い』</b>(L24 全 154,757 ha vs L25 全 24,007 ha = "
    f"6.4 倍)。"
    f"<b>都市市町</b> (広島市・福山市・呉市) は転用対象が広いため、"
    f"施策エリアの大半が転用と重なるが、転用全体に占める施策エリアは小さい。"
    f"<b>中山間市町</b>でも同様の傾向で、施策の対象農地が広くないことを示す。</p>"

    "<h3>表 9 — 大規模施策ポリゴン top 10 (面積降順)</h3>"
    "<table>"
    "<tr><th>順位</th><th>市町</th><th>地理</th><th>NRG_AN</th>"
    "<th>分類</th><th>geom型</th><th>面積 ha</th>"
    "<th>KUIKI</th><th>規模クラス</th></tr>"
    + top10_large_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"top 1 は<b>{top20_large.iloc[0]['src_city']} {top20_large.iloc[0]['NRG_AN']}</b> "
    f"({top20_large.iloc[0]['geom_area_ha']:,.0f} ha) ─ "
    f"<b>{top20_large.iloc[0]['nrg_class']}</b> タイプ。"
    f"top 10 は中山間と都市が混在するが、"
    f"<b>NRG_AN は地区名・池名・ダム名・ほ場整備地区</b>が混在。"
    f"<b>『500 ha 超の大規模ブロック』</b>は L24 と同様に少数だが、"
    f"L25 では「ほ場整備」「ダム」「池」など特定の制度区分が多く、"
    f"L24 より<b>『施策の意図が読み取れる』</b>データ。"
    f"<b>geom_type</b> は MultiPolygon が多い ─ 中山間集落+分散圃場の地形を反映。</p>"
)
sections.append(("9. 分析6: L24 vs L25 空間重複 (中核2, H5)", s9_html))


# === セクション10: 仮説検証 + 発展課題 ===
hypothesis_table_rows = "".join(
    f"<tr><td><b>{k}</b></td><td>{v['text']}</td>"
    f"<td>{v['result']}</td><td><b>{v['judge']}</b></td></tr>"
    for k, v in H_RESULTS.items()
)

s10_html = f"""
<h3>仮説 H1〜H6 検証結果</h3>
<table>
<tr><th>仮説</th><th>内容</th><th>実データ結果</th><th>判定</th></tr>
{hypothesis_table_rows}
</table>

<h3>主要発見 (5 点)</h3>
<ol>
  <li><b>L25 は「制度的指定エリア」型</b>: 17 市町 1,912 件 / 24,007 ha
      (件数は L24 の 14%、面積は L24 の 16%)。
      L24 の<b>『多数の個別申告区画』</b>とは粒度が違う ─
      L25 はあくまで<b>『広域の制度区分』</b>を図面化したもの。</li>
  <li><b>NRG_AN 88% が地区名のみ、9% がキーワード分類</b>:
      広島県の農林漁業施策は<b>『集落単位の地区名』</b>で運営。
      日本の農地は集落と一体で扱われ、政策名ではなく地名で図面化される実態が定量化された。
      残り 9% は「ダム」「池」「漁業」「林業」「ほ場整備」「基地・施設」のキーワード分類。</li>
  <li><b>MultiPolygon 比率は中山間ほど高い</b>: 中山間 5 市町平均 21%、都市 10 市平均 8.4%。
      <b>『中山間の谷間集落+分散圃場』</b>と<b>『都市のまとまった農地』</b>の地形差が
      ジオメトリ構造として現れる。</li>
  <li><b>L24 と L25 の空間重複は「施策の 64%」</b> (H5 予想 10-30% を大きく超過)。
      これは<b>『保全策の形骸化』</b>ではなく、<b>『同じ農用地区域を保全策と転用記録の
      両方で図面化』</b>していると解釈すべき ─ 行政の二重行政ではなく、
      同じ農地への異なる介入記録。
      <b>『広島県の食農政策は、保全と転用を同じ区域で同時に運営』</b>という構造発見。</li>
  <li><b>監査結論「広島市 NRG_AN None」は誤読</b>: 実データ 426 行中 NRG_AN null は 2 行のみ
      (= 0.5%)。サンプル 1 行が偶発的に None だっただけ。
      <b>『データ全体を見ずにサンプル 1 行で判断するな』</b>のデータ取扱い教訓。</li>
</ol>

<h3>結論</h3>
<p>RQ「広島県内 17 市町の農林漁業関係施策は、面積規模・施策種別・地理分布・農地転用 L24 との
対比でどのような構造を持ち、保全策と転用がどのようにせめぎあっているか?」に対して:</p>

<ol>
  <li><b>面積規模</b>: 24,007 ha (= 240 km²) ─ 県の市域面積の <b>{AREA_TOTAL_KM2/8479*100:.1f}%</b>。
      L24 (約 18%) より遥かに小さく、<b>『キワの制度区分』</b>のみ。</li>
  <li><b>施策種別</b>: <b>地区名のみ 88% + キーワード分類 9%</b>。
      集落単位の運営。</li>
  <li><b>地理分布</b>: 中山間に偏在、瀬戸内沿岸に分散、都心は希薄。
      <b>『中山間 5 市町が県の食農基盤』</b>を再確認 (L24 と整合)。</li>
  <li><b>L24 との対比</b>: <b>施策エリアの 64% が転用エリアと重複</b> ─
      予想を超える大きな重なり。<b>『保全と転用は同じ場所で同居している』</b>。</li>
  <li><b>哲学</b>: 広島県の食農政策は、<b>『保全策と転用記録は同じ農用地区域を異なる角度から
      図面化したもの』</b>として運営されている。<b>『せめぎあい』</b>というより<b>『二重記録による
      重層的な政策運営』</b>。</li>
</ol>

<p><b>水増しなき意味のある分析</b>: 全 9 図 + 9 表 + 6 仮説検証はそれぞれ独立した観点を持ち、
同じ結論を別角度から補強する。中核は (1) NRG_AN の文字列分類 (2) L24 vs L25 重複の定量化 で、
両方とも仕様書未公開の生データから<b>『広島県の食農政策の哲学』</b>を読み解いた。</p>
"""
sections.append(("10. 仮説検証と考察", s10_html))


# === セクション11: 発展課題 ===
s11_html = """
<h3>結果 X → 新仮説 Y → 課題 Z</h3>

<h4>(1) 結果 X1: 施策の 64% が転用と重複していた</h4>
<p><b>新仮説 Y1</b>: 「重複している部分」と「重複していない部分」では、
施策の意図 (NRG_AN 分類) が異なる可能性。
<b>「ほ場整備」</b>キーワードは転用と<b>強く重なる</b>(同じ地区で整備と転用を同時推進)、
<b>「ダム」「池」</b>は転用と<b>すみ分ける</b>(水利施設は保全特化) ─ などの差。</p>
<p><b>課題 Z1</b>: NRG_AN 分類別に<b>L24 vs L25 重複率の差</b>を計測。
<code>agro.groupby("nrg_class").apply(compute_overlap_with_farm)</code> で、
分類別の重複率を出す。さらに重複と非重複の地理分布を比較し、
<b>『どこで保全と転用が分離し、どこで同居するか』</b>を可視化。</p>

<h4>(2) 結果 X2: NRG_AN は 88% が地区名のみだった</h4>
<p><b>新仮説 Y2</b>: 地区名は<b>地形地名 (谷・丘・池・川)</b>と
<b>集落地名 (○○地区・○○原・○○町)</b>に分かれ、地形地名比率は中山間で高く、
集落地名比率は都市市町で高い。地名学的な分布パターンが市町タイプと相関するはず。</p>
<p><b>課題 Z2</b>: NRG_AN 文字列を<b>地形語彙辞書</b> (谷, 丘, 池, 川, 田, 山, 沢) と
<b>集落語彙辞書</b> (地区, 原, 町, 村) で 2 次分類し、
市町タイプ別の出現比率を分析。地名学的アプローチで施策ポリゴンの背景文化を解読。</p>

<h4>(3) 結果 X3: 1 町スワップ (熊野町↔坂町) があった</h4>
<p><b>新仮説 Y3</b>: 「保全策あり / 転用無し」(= 坂町) と「保全策無し / 転用あり」(= 熊野町)
の 2 町は、L23 (DID) や L22 (人口ピラミッド) の都市化指標で異なる位置にあるはず。
熊野町は<b>『食料生産は捨てたが住宅は維持』</b>、
坂町は<b>『食料生産は維持 (漁業のみ)、住宅も維持』</b>のような差。</p>
<p><b>課題 Z3</b>: 熊野町・坂町の<b>L22 人口ピラミッド + L23 DID 比率 + L24 転用 + L25 施策</b>を
4 軸 radar chart で並べ、<b>『2 町の都市化プロセスの違い』</b>を視覚化。
さらに同じく近郊町だが施策ありの市町と比較。</p>

<h4>(4) 結果 X4: KUIKI_CD は L24 と L25 で同じ意味と推定された</h4>
<p><b>新仮説 Y4</b>: 同じ KUIKI_CD 値 (例: 0) を持つ施策ポリゴン (L25) と転用ポリゴン (L24) は、
ほぼ同じ地区を指している。逆に異なる KUIKI_CD 値の交差は地形境界 (山と平地の境) と
強く相関する。KUIKI_CD は<b>『都市計画区域階層』</b>として L24/L25 で完全互換。</p>
<p><b>課題 Z4</b>: KUIKI_CD = k の施策と KUIKI_CD = k の転用を空間結合し、
<b>『同じ k 値での重複率』</b>を計測。値ごとの重複率の表を作り、
KUIKI_CD の意味の整合性を定量化する。</p>

<h4>(5) 結果 X5: ダム・ため池・漁業など特定キーワードは地理的に偏在</h4>
<p><b>新仮説 Y5</b>: 「ダム」キーワードを含む施策ポリゴンの地理は、
<b>地形図の流域</b> (川の上流) と強く相関する。「漁業」は海岸線、
「ため池」は瀬戸内沿岸の古代灌漑遺産地区と相関する。
施策の地理的偏在は<b>地形決定論的</b>に説明できる。</p>
<p><b>課題 Z5</b>: 国土数値情報の<b>水系ポリゴン</b> + <b>海岸線</b>と本データを空間結合し、
キーワード分類別の<b>『地形要素との距離』</b>分布を計測。
ダムから上流まで何 km、漁業基地から海岸まで何 m、を分布図で示す。</p>
"""
sections.append(("11. 発展課題 (結果 X → 新仮説 Y → 課題 Z)", s11_html))

# === HTML 出力 ===
print("\n[7] HTML 出力", flush=True)

html = render_lesson(
    num=25,
    title="L25 農林漁業関係施策 17 件 統合分析 — 広島県内の食農保全意思の地理構造",
    tags=["都市計画基礎調査", "農林漁業", "保全策", "GIS統合分析", "geopandas"],
    time="60-90分 (概念理解+ハンズオン)",
    level="中-上 (geopandas/空間結合の経験あり)",
    data_label=f"{N_POLY:,} 施策ポリゴン × 17 dataset_id (連番 1408-1424) ─ 2022 単年",
    sections=sections,
    script_filename="L25_agroforestry_policy.py",
)

(LESSONS / "L25_agroforestry_policy.html").write_text(html, encoding="utf-8")
print(f"  HTML 出力: {LESSONS / 'L25_agroforestry_policy.html'}", flush=True)
print(f"\n=== 完成 累計 {time.time()-t0:.1f}s ===", flush=True)
