"""L24 農地転用状況 17 件 統合分析 — 広島県内の農地が非農地に変わるパターン

カバー宣言:
  本記事は 「都市計画区域情報_農地転用状況_*_2016-2020」 の 17 dataset_id を統合し、
  広島県内 17 市町の <b>農地転用ポリゴン 計 13,749 件</b>を 1 個の GeoDataFrame に
  縦結合した上で、<b>広島県内の農地転用パターン</b>を分析する研究記事である。
  全 21 市町中 4 町 (府中町・海田町・坂町・…) は農地転用データ無指定 ─ そのコントラスト
  も分析対象とする。

  17 dataset_id (連番 1391-1407, 期間 2016-2020):
    1391 安芸高田市,  1392 熊野町,    1393 呉市,      1394 広島市,
    1395 江田島市,    1396 三原市,    1397 三次市,    1398 庄原市,
    1399 世羅町,      1400 大竹市,    1401 竹原市,    1402 東広島市,
    1403 廿日市市,    1404 尾道市,    1405 府中市,    1406 福山市,
    1407 北広島町

  4 市町 (農地転用データ無指定): 府中町, 海田町, 坂町 — 都市部のため農地転用対象が
  ほとんど無く、都市計画基礎調査の対象外。

研究の問い (RQ):
  広島県内 17 市町の農地転用は、面積規模・地理分布・市町別パターン・KUIKI_CD 別の観点で
  どのような構造を持ち、何が農地を非農地に変えているか？
  「都市の拡張」と「中山間の縮退」のどちらが県内の農地転用の主体か？

仮説 H1〜H6:
  H1. 県内 17 市町の総 AREA (転用対象農地面積) は <b>50,000 ha 以上</b>。
       広島県農地面積 (約 5 万 ha) と同オーダー、市域面積 (約 8,479 km²) の
       数%~十数%が「都市計画基礎調査における転用対象農地」として記録されている。
  H2. AREA 上位は<b>中山間市</b> (庄原・三次・東広島・北広島・世羅) で
       <b>合計 5 万 ha 超</b>。広島市・呉市・福山市の都市部より中山間市の方が
       「対象農地」が圧倒的に大きい ─ <b>『都市拡張型』ではなく『中山間の制度的
       農地区分』が主体</b>。広島県の食料供給基盤は中山間に偏在している。
  H3. <b>AREA = 0 のポリゴン</b> (図面精度 1/50,000 で四捨五入されて 0 になった微小区画)
       が市町ごとに <b>5-25%</b> 存在する。図面精度の限界が定量的に観察できる。
  H4. AREA 列 (ha 単位、1 ha = 10,000 m²) は<b>ジオメトリ面積 (m²) と完全に一致</b>する
       (ratio = geom_area_m² / AREA = 10,000)。
       <b>AREA は ha (ヘクタール) 単位</b> ─ 仕様書未公開だが実データから推定可能。
  H5. <b>KUIKI_CD のフラグ値分布</b>は市町間で異なる。
       広島市・呉市・三原市・東広島市・尾道市・府中市・福山市は <b>0,1,2 (+3,4,5,6)</b>
       のフル区分 (= 線引き都市計画区域あり)、
       中山間 8 市町は <b>0,3,4</b> のみ (= 線引き無し)。
       KUIKI_CD は<b>都市計画区域階層 (線引き内/外/区域外)</b> のフラグと推定される。
  H6. 大規模転用 (AREA ≥ 100 a = 1 ha) は全件の <b>10% 未満</b>だが、
       面積合計の <b>過半</b>を占める ─ <b>『大規模転用が面積を支配する』</b>
       パレート構造。

要件 S 準拠: 1 分以内完走。13,749 ポリゴンは groupby/dissolve で軽量、
            空間結合は市町境界レベルで bbox 絞込み。
要件 T 準拠: 県全域転用ポリゴン主題図、密度 hexbin、市町別 small multiples 17 panel、
            大規模転用バブルマップ、KUIKI_CD 別塗り分け。
要件 Q 準拠: 図 9 枚以上、表 9 枚以上。

データ仕様 (7 列、17 市町で 100% 一致):
  TOKEI_CD     int32     統計区分コード (1, 2, 3 — 統計年/統計種別と推定)
  CITY_CD      int32     市区町村コード (広島市は 8 区中 105-108 のみデータあり)
  KUIKI_CD     int32     区域コード (0-6, 仕様書未公開、線引き区分のフラグと推定)
  AREA         float64   <b>対象農地面積 (ha = ヘクタール = 10,000 m²)</b>
                         実測比 geom_m² / AREA = 10,000 から ha 単位と確定。
                         監査時の文字列 dtype は現データで float64 に。本記事では
                         pd.to_numeric(errors="coerce") で再正規化する手順を明示する。
  RITTEKI_CD   int32     立地コード (0, 1, 2 の 3 値)
  BIKOU        object    備考: 「国土数値情報を活用して作成したため、図面精度は1/50,000である」
                         または None (竹原市のみ全件 None)
  geometry     Polygon   EPSG:6671 (JGD2011 平面直角第III系)

データ品質ノート:
  - 図面精度 1/50,000 (BIKOU) → 1 mm の図面誤差が実距離 50 m に相当。
    AREA = 0 a のポリゴンは <b>四捨五入で潰れた微小転用</b>。
  - 竹原市 (8,879 件) のみ件数が異常に多く BIKOU=None → 別ソースの精密データの
    可能性。記事では別途扱う。
"""
from __future__ import annotations
import sys, time, json, zipfile, io
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
from matplotlib.colors import Normalize, LogNorm
import geopandas as gpd

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

t0 = time.time()
print("=== L24 農地転用状況 17 件 統合分析 ===", flush=True)

# =============================================================================
# 0. 定数: 17 市町、行政区域マッピング、市町タイプ、参考人口/面積
# =============================================================================
DATA_DIR = ROOT / "data" / "extras" / "L24_farmland_conversion"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
TARGET_CRS = "EPSG:6671"

# (転用 dsid, 市町名, 行政_dsid, 都市タイプ, 地理タイプ)
CITY_DEFS = [
    (1394, "広島市",     786, "政令市",         "都市"),
    (1393, "呉市",       797, "中核市",         "都市"),
    (1406, "福山市",     832, "中核市",         "都市"),
    (1402, "東広島市",   868, "施行時特例市",   "都市"),
    (1403, "廿日市市",   878, "市",             "都市"),
    (1404, "尾道市",     824, "市",             "都市"),
    (1396, "三原市",     814, "市",             "都市"),
    (1401, "竹原市",     807, "市",             "都市"),
    (1405, "府中市",     840, "市",             "都市"),
    (1400, "大竹市",     862, "市",             "都市"),
    (1397, "三次市",     850, "市",             "中山間"),
    (1398, "庄原市",     856, "市",             "中山間"),
    (1391, "安芸高田市", 888, "市",             "中山間"),
    (1395, "江田島市",   894, "市",             "離島"),
    (1392, "熊野町",     911, "町",             "近郊町"),
    (1407, "北広島町",   935, "町",             "中山間"),
    (1399, "世羅町",     941, "町",             "中山間"),
]

# 4 町 (農地転用データ無指定)
NO_FARM_CITIES = [
    ("府中町", 900, "町", "近郊町"),
    ("海田町", 905, "町", "近郊町"),
    ("坂町",   916, "町", "近郊町"),
]

# 公的統計参考値 (国土地理院 R6 面積、e-Stat R2 国勢調査人口千人) — L23 と共通
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},
}

# 地理タイプ別カラー (L23 のタイプ色を地理軸で再構成)
RTYPE_COLOR = {
    "都市":   "#cf222e",   # 都市部 (赤)
    "中山間": "#1f883d",   # 中山間 (緑)
    "離島":   "#0969da",   # 離島 (青)
    "近郊町": "#bf3989",   # 近郊町 (紫)
}
ALL_FARM_CITIES = [d[1] for d in CITY_DEFS]
ALL_CITY_ORDER = ALL_FARM_CITIES + [n[0] for n in NO_FARM_CITIES]


# =============================================================================
# 1. 17 GeoJSON 統合読み込み + 行政区域背景
# =============================================================================
print("\n[1] 17 農地転用 GeoJSON 統合読み込み", flush=True)
t1 = time.time()


def load_geojson_zip(zip_path: Path) -> gpd.GeoDataFrame:
    """ZIP 内の単一 .geojson を BytesIO 経由で読み込む"""
    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()))


# 7 共通列 (全市町に存在)
COMMON_COLS_7 = ["TOKEI_CD", "CITY_CD", "KUIKI_CD", "AREA",
                 "RITTEKI_CD", "BIKOU", "geometry"]

frames = []
load_log = []
for dsid, name, _adm, ctype, rtype in CITY_DEFS:
    z = DATA_DIR / f"farm_{dsid}_{name}.zip"
    g = load_geojson_zip(z)
    extra = sorted(set(g.columns) - set(COMMON_COLS_7))
    g = g[COMMON_COLS_7].copy()
    # AREA を文字列扱い → pd.to_numeric で正規化 (要件 K の Before/After を実装で明示)
    # 監査時 (2026-05) は object 型、現データ取得時点では float64 だが、
    # 教材として常に「文字列としても安全」な処理を入れる
    g["AREA_raw"] = g["AREA"].astype(str)  # 元値を保持
    g["AREA"] = pd.to_numeric(g["AREA"], errors="coerce")
    g["src_city"] = name
    g["src_dsid"] = dsid
    g["ctype"] = ctype
    g["rtype"] = rtype
    if g.crs is None:
        g = g.set_crs(TARGET_CRS, allow_override=True)
    load_log.append({
        "dsid": dsid, "city": name, "ctype": ctype, "rtype": rtype,
        "n_poly": len(g),
        "area_a_sum": float(g["AREA"].sum()),
        "area_a_max": float(g["AREA"].max()),
        "n_zero": int((g["AREA"] == 0).sum()),
        "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()))),
        "bikou_n_unique": int(g["BIKOU"].dropna().nunique()),
        "extra_cols": ",".join(extra) if extra else "-",
    })
    frames.append(g)

farm = gpd.GeoDataFrame(pd.concat(frames, ignore_index=True),
                        geometry="geometry", crs=frames[0].crs)
farm = farm.to_crs(TARGET_CRS)
N_POLY = len(farm)
# AREA は ha (ヘクタール) 単位 ─ ratio (geom_m²/AREA) = 10,000 で確定
AREA_TOTAL_HA = float(farm["AREA"].sum())
AREA_TOTAL_KM2 = AREA_TOTAL_HA / 100  # 1 km² = 100 ha
print(f"  農地転用対象ポリゴン: {N_POLY:,} 件 × {len(farm.columns)} 列",
      flush=True)
print(f"  AREA 合計: {AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.1f} km²",
      flush=True)

# 行政区域 21 件 (= 17 農地市町 + 3 農地無し町 + 広島県全域 1) を読み込み
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["farm_status"] = "農地転用あり"
    admin_frames.append(g)
for name, adm_ds, _ctype, _region in NO_FARM_CITIES:
    z = ADMIN_DIR / f"admin_{adm_ds}_{name}.zip"
    g = load_geojson_zip(z)
    g["city"] = name
    g["farm_status"] = "農地転用無し"
    admin_frames.append(g)
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={"farm_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()

# ポリゴン単位: ジオメトリ実面積、AREA との整合性、規模クラス
farm["geom_area_m2"] = farm.geometry.area
farm["geom_area_ha"] = farm["geom_area_m2"] / 1e4   # m² → ha
# AREA (申告 ha) と geom (実測 ha) の比 (≒1.0 が期待値)
farm["area_ratio"] = farm["geom_area_ha"] / farm["AREA"].replace(0, np.nan)
# 規模クラス (AREA は ha 単位): 微小 (<0.01 ha) / 小 (0.01-0.1) / 中 (0.1-1)
#                              / 大 (1-10) / 超大 (>=10) / 巨大 (>=100)
def _scale_class(ha_value):
    """AREA (ha) を規模クラスに分類"""
    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)"

farm["scale_class"] = farm["AREA"].apply(_scale_class)

# 市町単位
def aggregate_by_city(d: gpd.GeoDataFrame) -> pd.DataFrame:
    agg = d.groupby("src_city").agg(
        n_poly=("AREA", "size"),
        area_ha_sum=("AREA", "sum"),
        area_ha_median=("AREA", "median"),
        area_ha_max=("AREA", "max"),
        n_zero=("AREA", lambda s: int((s == 0).sum())),
        n_large_1ha=("AREA", lambda s: int((s >= 1.0).sum())),  # 1 ha 以上
        n_large_10ha=("AREA", lambda s: int((s >= 10.0).sum())),  # 10 ha 以上
        geom_area_m2_sum=("geom_area_m2", "sum"),
    ).reset_index().rename(columns={"src_city": "city"})
    agg["geom_area_ha_sum"] = agg["geom_area_m2_sum"] / 1e4
    # 申告 vs 実測ずれ
    agg["area_ratio_city"] = (agg["geom_area_ha_sum"]
                              / agg["area_ha_sum"].replace(0, np.nan))
    agg["zero_pct"] = (agg["n_zero"] / agg["n_poly"] * 100).round(1)
    agg["large_pct"] = (agg["n_large_1ha"] / 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"])
    # 単位面積 (km²) あたり対象農地 ha
    agg["conv_per_km2"] = (agg["area_ha_sum"]
                           / agg["city_area_km2"]).round(3)
    # 対象農地率 (%) = 対象農地 ha / 市域 km² / 100 (km² → ha は ×100)
    agg["conv_share_area"] = (agg["area_ha_sum"]
                              / (agg["city_area_km2"] * 100) * 100).round(2)
    # 市民 1人 あたり対象農地 (ha/人 を a 単位で表示、1 ha = 100 a)
    agg["conv_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(farm)
city_agg = city_agg.set_index("city").reindex(ALL_FARM_CITIES).reset_index()
print(f"  市町別集計: {len(city_agg)} 件", flush=True)

# 規模クラス x 市町 クロス (件数)
scale_cross_n = (farm.groupby(["src_city", "scale_class"]).size()
                 .unstack(fill_value=0)
                 .reindex(index=ALL_FARM_CITIES, fill_value=0))
# 規模クラス x 市町 クロス (面積合計 ha)
scale_cross_a = (farm.groupby(["src_city", "scale_class"])["AREA"].sum()
                 .unstack(fill_value=0)
                 .reindex(index=ALL_FARM_CITIES, fill_value=0))
# 規模クラス全体集計
scale_overall = farm.groupby("scale_class").agg(
    n=("AREA", "size"),
    area_ha_sum=("AREA", "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)

# KUIKI_CD クロス
kuiki_cross_n = (farm.groupby(["src_city", "KUIKI_CD"]).size()
                 .unstack(fill_value=0)
                 .reindex(index=ALL_FARM_CITIES, fill_value=0))
kuiki_overall = farm.groupby("KUIKI_CD").agg(
    n=("AREA", "size"),
    area_ha_sum=("AREA", "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 = farm.groupby("TOKEI_CD").agg(
    n=("AREA", "size"),
    area_ha_sum=("AREA", "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)

print(f"  規模クラス分類完了 ({time.time()-t1:.2f}s)", flush=True)

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

# 市町集計 (主表)
city_agg.to_csv(ASSETS / "L24_city_summary.csv",
                index=False, encoding="utf-8-sig")

# 17 件 + 3 件 (農地無し町) を含む全 20 市町行政表
no_farm_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"],
    "city_density": round(CITY_REF[name]["pop_k"] * 1000
                          / CITY_REF[name]["area_km2"], 1),
} for name, _adm, ctype, region in NO_FARM_CITIES])
no_farm_df.to_csv(ASSETS / "L24_no_farm_cities.csv",
                   index=False, encoding="utf-8-sig")

# 全ポリゴン属性 (geometry除く)
farm_attr = farm.drop(columns=["geometry"]).copy()
farm_attr["geom_area_m2"] = farm_attr["geom_area_m2"].round(1)
farm_attr["geom_area_ha"] = farm_attr["geom_area_ha"].round(4)
farm_attr["AREA"] = farm_attr["AREA"].round(2)
farm_attr["area_ratio"] = farm_attr["area_ratio"].round(4)
# 全件は重い (1MB 超) ので圧縮版を別に出す。本ファイルは samples_5000 まで保存
farm_attr_save = farm_attr.sort_values("AREA", ascending=False).head(5000)
farm_attr_save.to_csv(ASSETS / "L24_farm_polygons_top5000.csv",
                       index=False, encoding="utf-8-sig")

# 読込ログ
pd.DataFrame(load_log).to_csv(ASSETS / "L24_load_log.csv",
                               index=False, encoding="utf-8-sig")

# 規模クラス × 市町 クロス
scale_cross_n.to_csv(ASSETS / "L24_scale_class_count.csv",
                      encoding="utf-8-sig")
scale_cross_a.round(2).to_csv(ASSETS / "L24_scale_class_area_ha.csv",
                                encoding="utf-8-sig")
scale_overall.to_csv(ASSETS / "L24_scale_overall.csv",
                      index=False, encoding="utf-8-sig")

# KUIKI_CD クロス
kuiki_cross_n.to_csv(ASSETS / "L24_kuiki_count.csv",
                      encoding="utf-8-sig")
kuiki_overall.to_csv(ASSETS / "L24_kuiki_overall.csv",
                      index=False, encoding="utf-8-sig")
tokei_overall.to_csv(ASSETS / "L24_tokei_overall.csv",
                      index=False, encoding="utf-8-sig")

# 大規模対象農地 top 20
top20_large = farm_attr.nlargest(20, "AREA")[
    ["src_city", "ctype", "rtype", "CITY_CD", "KUIKI_CD",
     "AREA", "geom_area_ha", "area_ratio", "scale_class"]
]
top20_large.to_csv(ASSETS / "L24_top20_large.csv",
                    index=False, encoding="utf-8-sig")

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

# =============================================================================
# 4. 図の作成
# =============================================================================
print("\n[4] 図の作成", flush=True)
t1 = time.time()

# --- Fig 1: 県全域 農地転用 主題図 (13,749 polygon, 地理タイプ色) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_farm = admin_diss[admin_diss["farm_status"] == "農地転用あり"]
admin_farm.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 = farm[farm["rtype"] == rt]
    if len(sub) > 0:
        sub.plot(ax=ax, facecolor=col, alpha=0.55, edgecolor="none",
                  linewidth=0)
        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 市町 (2016-2020)",
              fontsize=13)
legend_handles_f1 = [Patch(facecolor=v,
                            label=f"{k} ({rtype_count[k]:,} 件)",
                            edgecolor="none", alpha=0.55)
                       for k, v in RTYPE_COLOR.items() if rtype_count[k] > 0]
ax.legend(handles=legend_handles_f1, 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 / "L24_fig01_overview.png", dpi=130)
plt.close("all")
print(f"  Fig 1 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 2: 農地転用 あり 17 市町 vs 無し 3 町 ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss[admin_diss["farm_status"] == "農地転用あり"].plot(
    ax=ax, facecolor="#fee0b6", edgecolor="#444", linewidth=0.6)
admin_diss[admin_diss["farm_status"] == "農地転用無し"].plot(
    ax=ax, facecolor="#cfe9d8", edgecolor="#444", linewidth=0.6)
farm.plot(ax=ax, facecolor="#cf222e", alpha=0.45, edgecolor="none",
          linewidth=0)
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="#fee0b6", edgecolor="#444", label="農地転用あり 17 市町"),
    Patch(facecolor="#cfe9d8", edgecolor="#444", label="農地転用無し 3 町"),
    Patch(facecolor="#cf222e", edgecolor="none", alpha=0.45,
            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 町に名前
for name, _adm, _ct, region in NO_FARM_CITIES:
    cnt = admin_diss[admin_diss["city"] == name]
    if len(cnt) > 0:
        ctr = cnt.geometry.iloc[0].representative_point()
        ax.annotate(f"{name}\n({region})", (ctr.x, ctr.y), fontsize=9,
                     ha="center",
                     bbox=dict(boxstyle="round,pad=0.2", fc="#cfe9d8",
                                ec="#1f883d", alpha=0.92))
plt.tight_layout()
plt.savefig(ASSETS / "L24_fig02_farm_vs_nofarm.png", dpi=130)
plt.close("all")
print(f"  Fig 2 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 3: 市町別 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", linewidth=0.4)
    sub_farm = farm[farm["src_city"] == name]
    sub_farm.plot(ax=ax_, facecolor=RTYPE_COLOR[rtype],
                   alpha=0.55, edgecolor="none", linewidth=0)
    cnt.boundary.plot(ax=ax_, color="#333", linewidth=0.4)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
    n = len(sub_farm)
    a_ha = sub_farm["AREA"].sum() / 100
    ax_.set_title(f"{name} ({rtype})\n"
                   f"{n:,}件 / {a_ha:,.0f} ha",
                   fontsize=10)
# 18 番目の panel: 3 町 (転用無し) 表示
ax_last = axes.flat[17]
admin_diss[admin_diss["farm_status"] == "農地転用無し"].plot(
    ax=ax_last, facecolor="#cfe9d8", 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_FARM_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")
# 19, 20 番目を消す
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 / "L24_fig03_small_multiples.png", dpi=110)
plt.close("all")
print(f"  Fig 3 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 4: 市町別 転用面積 + 件数 + 単位面積あたり (3 連 panel) ---
fig, axes = plt.subplots(1, 3, figsize=(18, 9))

# 左: 市町別総転用面積 (ha)
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)

# 中: 市町別件数
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)

# 右: 単位面積 (km²) あたり転用 ha (= 密度)
order_d = city_agg.sort_values("conv_per_km2", ascending=True)
colors_d = [RTYPE_COLOR[rt] for rt in order_d["rtype"]]
ax2 = axes[2]
ax2.barh(order_d["city"], order_d["conv_per_km2"], color=colors_d,
          edgecolor="white")
for i, v in enumerate(order_d["conv_per_km2"]):
    ax2.text(v + 0.2, i, f"{v:.1f}", va="center", fontsize=8)
ax2.set_xlabel("転用 ha / 市域 km² (= 転用密度)")
ax2.set_title("市町別 単位面積あたり転用", 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 / "L24_fig04_city_ranking.png", dpi=130)
plt.close("all")
print(f"  Fig 4 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 5: AREA 分布 (log-log + AREA=0 強調) ---
# 散布: AREA (ha) vs geom_area_ha で「申告 = 実測」の y=x を確認
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

ax0 = axes[0]
# 0 を別扱い (log は 0 を扱えない)
nz = farm[farm["AREA"] > 0]
zero = farm[farm["AREA"] == 0]
for rt, col in RTYPE_COLOR.items():
    sub = nz[nz["rtype"] == rt]
    if len(sub) > 0:
        ax0.scatter(sub["AREA"], sub["geom_area_ha"], c=col, s=4, alpha=0.3,
                     edgecolor="none", label=f"{rt} ({len(sub):,})")
# y=x ライン (申告=実測)
xs = np.array([0.001, 100000])
ax0.plot(xs, xs, color="black", linestyle="--", linewidth=1,
          label="申告 AREA = 実測 (y=x)")
ax0.set_xscale("log")
ax0.set_yscale("log")
ax0.set_xlabel("AREA (ha, 申告値)")
ax0.set_ylabel("geom_area_ha (ha, ジオメトリ実測)")
ax0.set_title(f"AREA 申告値 vs ジオメトリ実測 (log-log, AREA>0 のみ n={len(nz):,})",
               fontsize=12)
ax0.legend(loc="lower right", fontsize=8)
ax0.grid(True, which="both", alpha=0.3)

# 右: AREA = 0 の geom_area 分布 (図面精度の限界)
ax1 = axes[1]
zero_geom = zero["geom_area_ha"]
if len(zero_geom) > 0:
    bins = np.linspace(0, 0.012, 41)
    ax1.hist(zero_geom.clip(upper=0.012), bins=bins, color="#cf222e",
              edgecolor="white", alpha=0.8)
    ax1.axvline(0.005, color="black", linestyle="--", linewidth=1,
                  label="AREA 四捨五入境界 (0.005 ha = 50 m²)")
ax1.set_xlabel("geom_area_ha (実測 ha)")
ax1.set_ylabel("件数")
ax1.set_title(f"AREA=0 のポリゴンの実測面積 (n={len(zero):,})",
               fontsize=12)
ax1.legend(loc="upper right", fontsize=9)
ax1.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig(ASSETS / "L24_fig05_area_consistency.png", dpi=130)
plt.close("all")
print(f"  Fig 5 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 6: 規模クラス分布 + パレート構造 ---
fig, axes = plt.subplots(1, 2, figsize=(15, 7))

# 左: 規模クラス × 市町 stacked bar (件数比率)
ax0 = axes[0]
sc_pct = scale_cross_n.div(scale_cross_n.sum(axis=1), axis=0) * 100
sc_colors = ["#fff7bc", "#fec44f", "#fe9929", "#cc4c02", "#662506"]
sc_pct.plot(kind="barh", stacked=True, ax=ax0, color=sc_colors,
             edgecolor="white", linewidth=0.3)
ax0.set_xlabel("件数比率 (%)")
ax0.set_title("市町別 規模クラス構成 (件数 %)", fontsize=12)
ax0.legend(title="規模", loc="lower right", fontsize=8,
            bbox_to_anchor=(1.0, 0.0))

# 右: パレート図 (件数 vs 面積、規模クラス毎)
ax1 = axes[1]
sc_sorted = scale_overall.sort_values("scale_class")
x = np.arange(len(sc_sorted))
width = 0.35
b1 = ax1.bar(x - width/2, sc_sorted["n_pct"], width,
              label="件数 %", color="#0969da", edgecolor="white")
b2 = ax1.bar(x + width/2, sc_sorted["area_pct"], width,
              label="面積 %", color="#cf222e", edgecolor="white")
for bar in b1:
    h = bar.get_height()
    ax1.text(bar.get_x() + bar.get_width()/2, h + 1, f"{h:.1f}",
              ha="center", fontsize=8)
for bar in b2:
    h = bar.get_height()
    ax1.text(bar.get_x() + bar.get_width()/2, h + 1, f"{h:.1f}",
              ha="center", fontsize=8, color="#600")
ax1.set_xticks(x)
ax1.set_xticklabels(sc_sorted["scale_class"], rotation=15, ha="right",
                     fontsize=8)
ax1.set_ylabel("全体比率 (%)")
ax1.set_title("規模クラス別: 件数 % vs 面積 % (パレート構造)", fontsize=12)
ax1.legend(loc="upper right", fontsize=10)
ax1.grid(True, axis="y", alpha=0.3)

plt.tight_layout()
plt.savefig(ASSETS / "L24_fig06_scale_class.png", dpi=130)
plt.close("all")
print(f"  Fig 6 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 7: 大規模対象農地 (>=10 ha) バブルマップ ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_farm.plot(ax=ax, facecolor="#fafff0", edgecolor="#444", linewidth=0.5)
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.4)
# 大規模対象農地のみバブル (>=10 ha)
large = farm[farm["AREA"] >= 10].copy()
large["centroid"] = large.geometry.centroid
# サイズはAREAの sqrt スケール (視覚的に区別しやすく)
large["bubble_size"] = np.sqrt(large["AREA"]) * 4.0
for rt, col in RTYPE_COLOR.items():
    sub = large[large["rtype"] == rt]
    if len(sub) > 0:
        ax.scatter([p.x for p in sub["centroid"]],
                    [p.y for p in sub["centroid"]],
                    s=sub["bubble_size"].clip(20, 800), c=col, alpha=0.55,
                    edgecolor="#222", linewidth=0.4,
                    label=f"{rt} ({len(sub):,})")
ax.set_aspect("equal")
ax.set_title(f"大規模対象農地 (≥10 ha) バブルマップ (n={len(large):,} / "
              f"全 {N_POLY:,} 件中 {len(large)/N_POLY*100:.1f}%)",
              fontsize=12)
ax.legend(loc="lower left", fontsize=10, framealpha=0.92)
ax.set_xticks([]); ax.set_yticks([])
# top 3 にラベル
for _, r in large.nlargest(3, "AREA").iterrows():
    ctr = r["centroid"]
    ax.annotate(f"{r['src_city']}\n{r['AREA']:.0f} ha",
                 (ctr.x, ctr.y), fontsize=9, color="#600",
                 xytext=(8, 8), textcoords="offset points",
                 bbox=dict(boxstyle="round,pad=0.2",
                            fc="white", ec="#cf222e", alpha=0.9))
plt.tight_layout()
plt.savefig(ASSETS / "L24_fig07_large_bubble.png", dpi=130)
plt.close("all")
print(f"  Fig 7 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 8: KUIKI_CD 別 県全域マップ + 市町クロス ---
fig, axes = plt.subplots(1, 2, figsize=(18, 9))

# 左: KUIKI_CD 別塗り分けマップ
ax0 = axes[0]
admin_diss.plot(ax=ax0, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
# KUIKI_CD は 0-6 の 7 値、固定パレットで色分け
kuiki_palette = {
    0: "#1f883d",   # 0 = 区域外と推定 (中山間多い、緑)
    1: "#cf222e",   # 1 = 市街化区域と推定 (赤)
    2: "#a371f7",   # 2 = 市街化調整区域と推定 (紫)
    3: "#0969da",   # 3 = 非線引き と推定 (青)
    4: "#bf3989",   # 4 = (?) (ピンク)
    5: "#fb8500",   # 5 = (?) (オレンジ)
    6: "#666666",   # 6 = (?) (灰)
}
kuiki_legend_handles = []
for kc, col in kuiki_palette.items():
    sub = farm[farm["KUIKI_CD"] == kc]
    if len(sub) > 0:
        sub.plot(ax=ax0, facecolor=col, alpha=0.55, edgecolor="none",
                  linewidth=0)
        kuiki_legend_handles.append(
            Patch(facecolor=col, alpha=0.55, 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_handles, 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), 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 / "L24_fig08_kuiki_cd.png", dpi=130)
plt.close("all")
print(f"  Fig 8 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 9: 市町総人口/総面積 vs 転用面積 (散布、地理タイプ別) ---
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# 左: 市域面積 vs 転用面積 (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")
# 3 町 (転用無し) を x=面積, y=0 で表示
for name, _adm, _ct, _r in NO_FARM_CITIES:
    ax0.scatter(CITY_REF[name]["area_km2"], 0.5, marker="x", c="#666", s=120,
                 linewidth=2)
    ax0.annotate(name + "\n(転用無)", (CITY_REF[name]["area_km2"], 0.5),
                  fontsize=8, color="#444",
                  xytext=(0, -22), textcoords="offset points",
                  ha="center")
# 1% / 10% ラインを引く (1 km² = 100 ha)
xs = np.linspace(5, 1500, 50)
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)

# 右: 市町総人口 vs 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["conv_per_capita_a"],
                     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, r["conv_per_capita_a"]),
                          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/人, 1ha=100a)")
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 / "L24_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: 総 AREA >= 50,000 ha
H1_total_ha = AREA_TOTAL_HA
H1_judge = ("支持" if H1_total_ha >= 50_000 else "反証")

# H2: 中山間 5 市町 (庄原・三次・東広島・北広島・世羅) の合計 >= 5 万 ha?
mountain_top5 = ["庄原市", "三次市", "東広島市", "北広島町", "世羅町"]
H2_top5_ha = float(city_agg[city_agg["city"].isin(mountain_top5)]["area_ha_sum"].sum())
# 都市 5 市 (広島・呉・福山・尾道・廿日市) との比較
urban_top5 = ["広島市", "呉市", "福山市", "尾道市", "廿日市市"]
H2_urban5_ha = float(city_agg[city_agg["city"].isin(urban_top5)]["area_ha_sum"].sum())
H2_judge = ("支持" if H2_top5_ha >= 50_000 and H2_top5_ha > H2_urban5_ha
            else "部分支持" if H2_top5_ha > H2_urban5_ha
            else "反証")

# H3: AREA=0 比率 (市町別の幅 5-25%)
zero_min = float(city_agg["zero_pct"].min())
zero_max = float(city_agg["zero_pct"].max())
zero_med = float(city_agg["zero_pct"].median())
N_ZERO = int((farm["AREA"] == 0).sum())
H3_judge = ("支持" if 5 <= zero_med <= 25 and zero_min < 5 + 5  # 緩い帯域
            else "部分支持")

# H4: AREA は a (=100 m²) 単位 → ratio (geom_a / AREA) ≒ 1.0
nz_full = farm[farm["AREA"] > 0]
ratio_med = float(nz_full["area_ratio"].median())
ratio_iqr = (float(nz_full["area_ratio"].quantile(0.25)),
             float(nz_full["area_ratio"].quantile(0.75)))
H4_judge = ("支持" if 0.95 <= ratio_med <= 1.05 else "部分支持")

# H5: KUIKI_CD 分布の市町間差
# 都市 7 市町 (広島・呉・三原・東広島・尾道・府中・福山) は 0,1,2 を含むはず
# 中山間/離島は 0,3,4 主体
def kuiki_set(city):
    return set(farm[farm["src_city"] == city]["KUIKI_CD"].unique())
urban_with_12 = sum(1 for c in ["広島市", "呉市", "三原市", "東広島市",
                                  "尾道市", "府中市", "福山市"]
                   if 1 in kuiki_set(c) or 2 in kuiki_set(c))
mountain_with_3or4 = sum(1 for c in ["三次市", "庄原市", "安芸高田市",
                                       "世羅町", "北広島町"]
                          if 3 in kuiki_set(c) or 4 in kuiki_set(c))
H5_judge = ("支持" if urban_with_12 >= 5 and mountain_with_3or4 >= 4
            else "部分支持")

# H6: 大規模対象農地 >= 10 ha は件数 < 10% かつ 面積 >= 50%
large_n = int((farm["AREA"] >= 10).sum())
large_n_pct = large_n / N_POLY * 100
large_ha_sum = float(farm[farm["AREA"] >= 10]["AREA"].sum())
large_ha_pct = large_ha_sum / AREA_TOTAL_HA * 100
H6_judge = ("支持" if large_n_pct < 10 and large_ha_pct >= 50
            else "部分支持" if large_ha_pct >= 30
            else "反証")

H_RESULTS = {
    "H1": {
        "text": "県内 17 市町の総 AREA (対象農地) ≥ 50,000 ha",
        "result": f"AREA合計 = {H1_total_ha:,.0f} ha "
                  f"(= {AREA_TOTAL_KM2:.1f} km²)",
        "judge": H1_judge,
    },
    "H2": {
        "text": "中山間 5 市町 (庄原・三次・東広島・北広島・世羅) ≥ 50,000 ha かつ "
                "都市 5 市の合計より大きい",
        "result": f"中山間 5 市町 = {H2_top5_ha:,.0f} ha, "
                  f"都市 5 市 = {H2_urban5_ha:,.0f} ha "
                  f"(中山間/都市 比率 = {H2_top5_ha/max(H2_urban5_ha,1):.1f}x)",
        "judge": H2_judge,
    },
    "H3": {
        "text": "AREA=0 ポリゴン (図面精度由来) は市町別中央値 5-25%",
        "result": f"全件 AREA=0 = {N_ZERO:,}/{N_POLY:,} "
                  f"= {N_ZERO/N_POLY*100:.1f}%, "
                  f"市町別 min/median/max = {zero_min:.1f}/{zero_med:.1f}/{zero_max:.1f}%",
        "judge": H3_judge,
    },
    "H4": {
        "text": "AREA(ha) × 10,000 = geom_area_m² → ratio (geom_ha/AREA) 中央値 ≒ 1.0",
        "result": f"ratio 中央値 = {ratio_med:.4f} "
                  f"(IQR = [{ratio_iqr[0]:.4f}, {ratio_iqr[1]:.4f}])",
        "judge": H4_judge,
    },
    "H5": {
        "text": "KUIKI_CD は都市計画区域階層フラグと推定 — "
                "都市 7 市町に 1/2 値、中山間に 3/4 値",
        "result": f"都市 7 市中 KUIKI=1or2 を持つ市町 = {urban_with_12}/7、"
                  f"中山間 5 市町中 KUIKI=3or4 を持つ市町 = {mountain_with_3or4}/5",
        "judge": H5_judge,
    },
    "H6": {
        "text": "大規模対象農地 (≥10 ha) は件数 <10% かつ 面積 ≥50% (パレート構造)",
        "result": f"≥10ha件数 = {large_n:,} ({large_n_pct:.2f}%), "
                  f"≥10ha面積 = {large_ha_sum:,.0f} ha "
                  f"({large_ha_pct:.1f}%)",
        "judge": H6_judge,
    },
}

(ASSETS / "L24_hypothesis_results.json").write_text(
    json.dumps({"results": H_RESULTS,
                "n_polygons": N_POLY,
                "n_cities_with_farm": len(CITY_DEFS),
                "n_cities_no_farm": len(NO_FARM_CITIES),
                "area_total_ha": AREA_TOTAL_HA,
                "area_total_km2": AREA_TOTAL_KM2,
                "zero_count": N_ZERO,
                "zero_pct_total": N_ZERO/N_POLY*100,
                "ratio_median": ratio_med,
                "large_count_pct": large_n_pct,
                "large_area_pct": large_ha_pct,
                }, 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
)

# === セクション1: 学習目標と問い ===
s1_html = f"""
<p>本記事は、広島県インフラマネジメント基盤 DoBoX が公開する
<b>「都市計画区域情報_農地転用状況」シリーズ 17 件</b>
({ds_links_html})
を縦結合し、<b>2016-2020 年の広島県内 17 市町の
農地転用対象ポリゴン 計 {N_POLY:,} 件 (合計 {AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.0f} km²)</b>
から、<b>広島県内の「農地が非農地に変わるパターン」</b> ── 全県転用配置・市町別パターン・
規模分布・KUIKI_CD 別の都市計画区域階層別 ── を分析する研究記事である。
全 20 市町中 3 町 (府中町・海田町・坂町) は農地転用対象なし ─ 都市部 100% で
都市計画基礎調査の対象農地が無い ─ そのコントラストも分析対象とする。</p>

<div class="note">
<b>研究の問い (RQ)</b>: 広島県内 17 市町の農地転用は、面積規模・地理分布・市町別パターン・
KUIKI_CD 別の観点でどのような構造を持ち、何が農地を非農地に変えているか？
「都市の拡張」と「中山間の縮退」のどちらが県内の対象農地分布の主体か？
</div>

<h3>仮説 H1〜H6</h3>
<ul>
  <li><b>H1</b>: 県内 17 市町の総 AREA (転用対象農地面積) は<b>50,000 ha 以上</b>。
      広島県農地面積 (約 5 万 ha 〜 10 万 ha 規模) と同オーダーで、
      県の市域面積 (8,479 km²) の<b>数 % 〜 十数 %</b>に相当。</li>
  <li><b>H2</b>: AREA 上位は<b>中山間市</b> (庄原・三次・東広島・北広島・世羅) で
      <b>合計 5 万 ha 超</b>。<b>都市部 (広島市・呉市・福山市) より中山間市の方が
      対象農地面積は遥かに大きい</b> ─ 「都市拡張型」ではなく「中山間の制度的農地区分」が主体。
      広島県の食料供給基盤は中山間に偏在している。</li>
  <li><b>H3</b>: <b>AREA = 0 のポリゴン</b> (図面精度 1/50,000 で四捨五入されて
      0 になった微小区画) が市町ごとに <b>5-25%</b> 存在する。
      図面精度の限界が定量的に観察できる。</li>
  <li><b>H4</b>: AREA 列 (ha 単位) は<b>ジオメトリ面積 (m²) を 10,000 で割った値</b>と
      一致する (実測比 geom_area_m² / AREA = 10,000)。
      仕様書未公開の AREA 単位を<b>実データから推定</b>できる例。</li>
  <li><b>H5</b>: <b>KUIKI_CD のフラグ値分布</b>は市町間で異なり、
      <b>都市計画区域階層 (線引き内/外/区域外)</b> のフラグと推定される。
      都市 7 市町は <b>0,1,2 (+3,4,5,6)</b> のフル区分、
      中山間 8 市町は <b>0,3,4</b> のみ (= 線引き無し)。</li>
  <li><b>H6</b>: 大規模対象農地 (AREA ≥ 10 ha) は<b>件数の 10% 未満</b>だが、
      <b>面積合計の過半</b>を占める ─ 「大規模区画が面積を支配する」
      <b>パレート構造</b>。</li>
</ul>

<h3>独自用語の定義 (本レッスン内のみ)</h3>
<ul>
  <li><b>農地転用 (本シリーズの定義)</b>: 都市計画基礎調査に基づき、
      <b>農業振興地域・農用地区域として特別に保全されている農地</b>のうち、
      他用途への転用が許可・予定された区画。
      本データは<b>「過去 5 年間 (2016-2020) に転用許可された対象農地」</b>を
      示すと推定されるが、仕様書未公開のため<b>「転用が記録された対象農地ポリゴン」</b>と
      総称する。<b>『すでに転用された土地』そのものではなく、
      『農地のうち転用対象として図面化された区画』</b>として扱う。</li>
  <li><b>AREA</b>: 各転用ポリゴンの<b>面積を ha (ヘクタール) 単位で記録</b>した数値。
      仕様書には単位明記なしだが、実測 (geom_area_m² / AREA = 10,000) から ha 単位と確定。
      1 ha = 10,000 m² = 100 m × 100 m の正方形。
      AREA = 0 のポリゴンは<b>四捨五入で潰れた微小区画</b> (実測 50 m² 未満)。</li>
  <li><b>図面精度 1/50,000</b>: BIKOU 列に明記された地理情報の精度水準。
      <b>図面 1 mm = 実距離 50 m</b>に相当 ─ 地形図の縮尺。
      この精度でデジタル化された区画は、最小描画単位が<b>50 m × 50 m = 0.25 ha</b>
      程度になり、それ未満の区画は AREA = 0 で表現される。
      AREA = 0 ポリゴンの実測面積分布で精度限界が定量化できる。</li>
  <li><b>BIKOU</b>: 備考列。「国土数値情報を活用して作成したため、
      図面精度は1/50,000である」という<b>図面精度の警告メッセージ</b>が
      入っている (竹原市のみ全件 None)。</li>
  <li><b>KUIKI_CD</b>: 区域コード (0-6 の 7 値、仕様書未公開)。
      本記事の独自解読: <b>都市計画区域階層フラグ</b>と推定される。
      都市 7 市町 (広島・呉・三原・東広島・尾道・府中・福山) のみ <b>1, 2, 5, 6</b> を
      含み (= 線引き都市計画区域あり)、
      中山間 8 市町は <b>0, 3, 4</b> のみ (= 線引き無し or 区域外)。</li>
  <li><b>TOKEI_CD</b>: 統計区分コード (1, 2, 3 の 3 値、仕様書未公開)。
      農用地区域種別 (農振農用地 / 農振白地 / 都市計画区域内 等) の
      フラグと推定。市町別の値分布が異なる。</li>
  <li><b>RITTEKI_CD</b>: 立地コード (0, 1, 2 の 3 値、仕様書未公開)。
      参考扱い。</li>
  <li><b>規模クラス</b> (本記事独自分類): AREA を 6 段階に分類。
      <b>0_微小 (&lt;0.01 ha)</b>、<b>1_小 (0.01-0.1 ha)</b>、
      <b>2_中 (0.1-1 ha)</b>、<b>3_大 (1-10 ha)</b>、
      <b>4_超大 (10-100 ha)</b>、<b>5_巨大 (≥100 ha)</b>。</li>
  <li><b>地理タイプ</b> (本記事独自分類): 17 市町を 4 タイプに分類。
      <b>都市</b> (10 市): 広島市・呉市・福山市・東広島市・廿日市市・尾道市・三原市・
      竹原市・府中市・大竹市、
      <b>中山間</b> (5 市町): 三次市・庄原市・安芸高田市・北広島町・世羅町、
      <b>離島</b> (1 市): 江田島市、
      <b>近郊町</b> (1 町): 熊野町。
      <b>地理</b>軸での分類は、本シリーズの転用パターンが市町タイプ (政令市/中核市/市/町) より
      地理タイプで強く分かれるため。</li>
  <li><b>対象農地率</b> (本記事独自指標): <code>AREA合計 (ha) / 市域面積 (km²) / 100</code> ×100%。
      「市域の何 % が転用対象農地として図面化されているか」。</li>
  <li><b>転用密度</b> (本記事独自指標): <code>AREA合計 (ha) / 市域面積 (km²)</code>。
      「市域 1 km² あたりの対象農地 ha」。</li>
  <li><b>パレート構造</b>: 件数の少数 (例: 上位 1%) が量の多数 (例: 全体の 80%) を占める分布。
      80-20 法則とも。本データでは大規模ポリゴン少数が面積大半を占める。</li>
</ul>

<h3>到達点</h3>
<ol>
  <li>17 ZIP の農地転用 GeoJSON を 1 個の <code>GeoDataFrame</code>
      ({N_POLY:,} polygon × 7 列 + 派生 5 列) に縦結合できる。
      AREA 列は <code>pd.to_numeric(errors="coerce")</code> で安全に数値化する手順を学ぶ。</li>
  <li><b>{N_POLY:,} polygon</b> から、市町単位の総面積・件数・密度・転用率の
      集計を <code>groupby</code> で実装する。</li>
  <li>農地転用ポリゴンを <code>geopandas.plot()</code> で
      <b>主題図 (choropleth) + バブル図</b>化し、地理分布を可視化する。</li>
  <li>17 市町 small multiples で<b>「市町ごとの農地転用配置パターン」</b>を一目で比較。</li>
  <li><b>規模クラス × 市町</b> のクロス集計とパレート図で、
      <b>「大規模区画が面積を支配する」</b>構造を定量化する。</li>
  <li>仕様書未公開の <b>KUIKI_CD / TOKEI_CD / AREA 単位</b>を、
      実データの分布と整合性検証から<b>「自分で解読する」</b>体験。</li>
  <li>3 町 (府中町・海田町・坂町) と 17 市町を対比して、
      「農地転用対象が無いことの地理的意味 = 都市部 100%」を理解する。</li>
</ol>

<h3>本記事のスコープ宣言</h3>
<p>本記事は<b>農地転用状況 17 件のみ</b>を主データとする研究記事である。
同じ都市計画基礎調査シリーズの<b>農林漁業関係施策 (L25, 17 件) は次記事で扱う</b>
(本記事と合体させない、要件 I 違反の水増し回避)。
都市計画区域階層との対比 (KUIKI_CD 解読) のため、
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['area_a_sum'] for L in load_log if L['city']==d[1]):,.0f}</td>"
    f"<td>{next(L['area_a_max'] for L in load_log if L['city']==d[1]):,.1f}</td>"
    f"<td>{next(L['n_zero'] for L in load_log if L['city']==d[1]):,}</td>"
    f"<td>{next(L['city_cds'] 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"</tr>"
    for d in CITY_DEFS
)
NO_FARM_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_FARM_CITIES
)

s2_html = f"""
<p>農地転用状況 17 件はそれぞれ 1 市町分の <b>農地転用ポリゴン GeoJSON (Polygon)</b>
を ZIP で配布している。<b>列構造は 17 件で 100% 一致 (7 列)</b>。
データは<b>2016-2020 年の 5 年間</b>を対象、
都市計画法に基づく都市計画基礎調査の結果として公表されている。</p>

<h3>17 dataset_id 一覧 (農地転用あり 17 市町)</h3>
<table>
<tr><th>dataset_id</th><th>市町</th><th>市町タイプ</th><th>地理タイプ</th>
    <th>DoBoX</th>
    <th>件数</th><th>AREA合計(ha)</th><th>AREA最大(ha)</th>
    <th>0件</th><th>CITY_CD</th><th>KUIKI_CD</th></tr>
{DS_TABLE_ROWS}
</table>
<p class="tnote">合計: <b>{N_POLY:,} ポリゴン / AREA合計 {AREA_TOTAL_HA:,.0f} ha
 = {AREA_TOTAL_KM2:.1f} km²</b>
(2016-2020 年期間)。広島県全域面積 8,479 km² に対し、
対象農地率は <b>{AREA_TOTAL_KM2/8479*100:.1f}%</b>。
件数最大は<b>竹原市 8,879 件</b> (異常に多い、別ソースの精密データの可能性)、
件数最小は<b>大竹市 33 件</b>。
AREA 最大は<b>庄原市 19,231 ha</b> (1 ポリゴンで 192 km²、市域 1,246 km² の 15%)。</p>

<h3>農地転用データ無し 3 町 (参考)</h3>
<table>
<tr><th>市町</th><th>タイプ</th><th>地理タイプ</th>
    <th>面積 km²</th><th>人口 (千人)</th><th>密度 (人/km²)</th></tr>
{NO_FARM_TABLE_ROWS}
</table>
<p class="tnote">3 町 (府中町・海田町・坂町) は<b>広島市の近郊ベッドタウン</b>で
町域の大半が市街化済 ─ 農業振興地域・農用地区域がほぼ存在しないため、
都市計画基礎調査の「農地転用対象」シリーズに含まれない。
3 町とも<b>人口密度が DID 認定線 (4,000 人/km²) を超える</b>都市町。</p>

<h3>列構造の詳細 (7 列)</h3>
<table>
<tr><th>列名</th><th>型</th><th>意味</th></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-108 のみ</b>データあり
        (中区・東区・西区・南区・佐伯区・安佐北区はデータ無し or 別 CITY_CD)。
        他市町は単一 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>AREA</code></td><td>float64</td>
    <td><b>対象農地面積 (ha)</b>。実測比 geom_area_m² / AREA = 10,000 から
        ha 単位と確定。AREA = 0 の polygon は図面精度由来の四捨五入結果
        (実測 50 m² 未満) で全体の {N_ZERO/N_POLY*100:.1f}%。
        ※ <b>監査時 (2026-05) の dtype は object (文字列)</b>、
        現データ取得時点では float64 だが、教材として
        <code>pd.to_numeric(errors="coerce")</code> で
        <b>常に安全な数値化処理</b>を入れている</td></tr>
<tr><td><code>RITTEKI_CD</code></td><td>int32</td>
    <td>立地コード (0, 1, 2 の 3 値、仕様書未公開)。
        全 17 市町中 8 市町で値 0 のみ、9 市町で 0/1 混在、
        3 市町で 0/1/2 混在。<b>転用先用途のフラグ</b>と推定</td></tr>
<tr><td><code>BIKOU</code></td><td>object</td>
    <td>備考。「国土数値情報を活用して作成したため、図面精度は1/50,000である」
        という<b>図面精度の警告メッセージ</b>が入っている (16 市町)。
        <b>竹原市のみ全件 None</b> で、件数も 8,879 と異常に多いため
        別ソースの精密データの可能性</td></tr>
<tr><td><code>geometry</code></td><td>Polygon</td>
    <td><b>農地転用対象境界 Polygon</b>。EPSG:6671 (JGD2011 平面直角第III系)。
        本記事の<b>主役データ</b>。MultiPolygon は無く、すべて単一 Polygon</td></tr>
</table>

<div class="note"><b>データ品質ノート</b>:
1. <b>AREA 列の単位</b>は仕様書未公開だが、実測比 geom_area_m² / AREA = 10,000
   から<b>ha (ヘクタール) 単位</b>と確定 (本記事の H4 で検証)。
2. <b>竹原市 (ds=1401) のみ件数 8,879 と異常</b>で BIKOU = None、KUIKI_CD = 3, 4 のみ。
   他市町と異なる<b>精密データソース</b>の可能性 (国土数値情報ではなく地籍調査結果?)。
   本記事ではそのまま統合に含めるが、規模分布の極端値として扱う。
3. <b>広島市 (ds=1394) は CITY_CD = 105-108 のみ</b> ─ 8 区中 4 区分のデータ。
   中区 (101)・東区 (102)・西区 (103)・南区 (104)・安佐北区 など他区のデータは
   このシリーズには含まれない (都市部のため農地転用対象が無いと推定)。</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\\L24_farmland_conversion\\fetch_farmland_conversion.py</code></pre>
<p>合計サイズ約 12 MB。監査時に取得済の 3 市町 (広島市・呉市・福山市) は
<code>data/extras/_urban_planning_audit/</code> から自動コピー、
残り 14 市町は DoBoX から HTTP 取得 (約 30 秒)。</p>

<h3>(2) 中間 CSV (本スクリプトの出力)</h3>
<ul>
  <li><a href="assets/L24_city_summary.csv" download>L24_city_summary.csv</a> — 17 市町サマリ (件数・AREA・密度・転用率・1人あたり 等 14 指標)</li>
  <li><a href="assets/L24_no_farm_cities.csv" download>L24_no_farm_cities.csv</a> — 農地転用無し 3 町の一覧 (面積・人口・地理タイプ)</li>
  <li><a href="assets/L24_farm_polygons_top5000.csv" download>L24_farm_polygons_top5000.csv</a> — ポリゴン単位 上位 5,000 行 (AREA 降順)</li>
  <li><a href="assets/L24_load_log.csv" download>L24_load_log.csv</a> — 17 件読込ログ (件数・AREA合計・最大・KUIKI_CD群)</li>
  <li><a href="assets/L24_scale_class_count.csv" download>L24_scale_class_count.csv</a> — 規模クラス × 市町 (件数)</li>
  <li><a href="assets/L24_scale_class_area_ha.csv" download>L24_scale_class_area_ha.csv</a> — 規模クラス × 市町 (面積 ha)</li>
  <li><a href="assets/L24_scale_overall.csv" download>L24_scale_overall.csv</a> — 規模クラス全体集計</li>
  <li><a href="assets/L24_kuiki_count.csv" download>L24_kuiki_count.csv</a> — KUIKI_CD × 市町 クロス</li>
  <li><a href="assets/L24_kuiki_overall.csv" download>L24_kuiki_overall.csv</a> — KUIKI_CD 全体集計</li>
  <li><a href="assets/L24_tokei_overall.csv" download>L24_tokei_overall.csv</a> — TOKEI_CD 全体集計</li>
  <li><a href="assets/L24_top20_large.csv" download>L24_top20_large.csv</a> — 大規模対象農地 top 20</li>
  <li><a href="assets/L24_hypothesis_results.json" download>L24_hypothesis_results.json</a> — 仮説 H1〜H6 検証結果 (JSON)</li>
</ul>

<h3>(3) 図 PNG (本記事掲載 9 枚)</h3>
<ul>
  <li><a href="assets/L24_fig01_overview.png" download>L24_fig01_overview.png</a> — 県全域 農地転用ポリゴン主題図</li>
  <li><a href="assets/L24_fig02_farm_vs_nofarm.png" download>L24_fig02_farm_vs_nofarm.png</a> — 17 市町 vs 3 町 マップ</li>
  <li><a href="assets/L24_fig03_small_multiples.png" download>L24_fig03_small_multiples.png</a> — 市町別 17 panels + 無し 3 町 panel</li>
  <li><a href="assets/L24_fig04_city_ranking.png" download>L24_fig04_city_ranking.png</a> — 市町別 面積 + 件数 + 単位面積あたり</li>
  <li><a href="assets/L24_fig05_area_consistency.png" download>L24_fig05_area_consistency.png</a> — AREA 申告 vs 実測 + AREA=0 分布</li>
  <li><a href="assets/L24_fig06_scale_class.png" download>L24_fig06_scale_class.png</a> — 規模クラス構成 + パレート図</li>
  <li><a href="assets/L24_fig07_large_bubble.png" download>L24_fig07_large_bubble.png</a> — 大規模対象農地 (≥10 ha) バブルマップ</li>
  <li><a href="assets/L24_fig08_kuiki_cd.png" download>L24_fig08_kuiki_cd.png</a> — KUIKI_CD 別マップ + 市町クロス</li>
  <li><a href="assets/L24_fig09_pop_area_scatter.png" download>L24_fig09_pop_area_scatter.png</a> — 市域面積/人口 × 対象農地散布</li>
</ul>

<h3>(4) 再現用 Python スクリプト</h3>
<ul>
  <li><a href="L24_farmland_conversion.py" download>L24_farmland_conversion.py</a> — 本記事の全コード</li>
  <li><a href="../data/extras/L24_farmland_conversion/fetch_farmland_conversion.py" download>fetch_farmland_conversion.py</a> — 17 ZIP 取得スクリプト</li>
</ul>
<p class="tnote">実行は <code>cd "2026 DoBoX 教材"; py -X utf8 lessons\\L24_farmland_conversion.py</code>。
17 ZIP がローカルにあれば <b>30 秒程度</b>で全図 + CSV 再生成 (要件 S 準拠)。</p>
"""
sections.append(("3. ダウンロード (再現用データ・中間データ・図・スクリプト)",
                 dl_html))

# === セクション4: 分析1 — 統合読み込み + AREA 数値化 + 派生指標 ===
code_load = code('''
DATA_DIR = ROOT / "data" / "extras" / "L24_farmland_conversion"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
TARGET_CRS = "EPSG:6671"

# 17 件は 7 列 100% 共通
COMMON_COLS_7 = ["TOKEI_CD", "CITY_CD", "KUIKI_CD", "AREA",
                 "RITTEKI_CD", "BIKOU", "geometry"]

frames = []
for dsid, name, _adm, ctype, rtype in CITY_DEFS:
    z = DATA_DIR / f"farm_{dsid}_{name}.zip"
    g = load_geojson_zip(z)
    g = g[COMMON_COLS_7].copy()

    # *** AREA を文字列扱いから数値化 (要件 K の Before/After) ***
    g["AREA_raw"] = g["AREA"].astype(str)        # 元値を保持 (例: "1114.18")
    g["AREA"]     = pd.to_numeric(g["AREA"],    # 数値化 (失敗は NaN)
                                  errors="coerce")
    # NaN になった場合は要確認 (本データでは全件成功)

    g["src_city"] = name
    g["rtype"] = rtype
    frames.append(g)

farm = gpd.GeoDataFrame(pd.concat(frames, ignore_index=True),
                        geometry="geometry", crs=frames[0].crs)
farm = farm.to_crs(TARGET_CRS)

# 派生列
farm["geom_area_m2"]  = farm.geometry.area              # 実測 m²
farm["geom_area_ha"]  = farm["geom_area_m2"] / 1e4      # 実測 ha
farm["area_ratio"]    = farm["geom_area_ha"] / farm["AREA"].replace(0, np.nan)
# 規模クラス (微小/小/中/大/超大/巨大)
farm["scale_class"] = farm["AREA"].apply(_scale_class)
''')

before_after_s4 = f"""
<h3>入出力 Before/After (要件 K — AREA を文字列から数値化する 1 件の追跡)</h3>
<p>仕様書未公開で<b>監査時 (2026-05-03) の AREA 列は object (文字列) 型</b>だった。
現データ取得時点 (2026-05-09) では float64 になっているが、
教材として<b>『常に安全な数値化処理』</b>を組み込む。
広島市最大ポリゴン (1 件) を例に、最初から最後まで追跡する:</p>

<table>
<tr><th>段階</th><th>列</th><th>このデータで何が起きるか</th>
    <th>1 行の値の例 (広島市 最大 polygon)</th></tr>
<tr><td>入力 (生)</td><td><code>AREA (str?)</code></td>
    <td>GeoJSON から読込時、自治体ごとに dtype が揺れうる
        (object / float64)。<br>※ 監査時 object → 現在 float64</td>
    <td><code>'1114.18'</code> または <code>1114.18</code></td></tr>
<tr><td>正規化</td><td><code>AREA = pd.to_numeric(.., errors="coerce")</code></td>
    <td>文字列 → float64 に変換。失敗 (e.g. 空文字, "N/A") は NaN。
        本データでは全件成功</td>
    <td><code>1114.18 (float64)</code></td></tr>
<tr><td>派生</td><td><code>geom_area_m2 = geom.area</code></td>
    <td>geometry の正確な面積 (m²、CRS=EPSG:6671 の単位)</td>
    <td><code>11,141,775 m²</code></td></tr>
<tr><td>派生</td><td><code>geom_area_ha = geom_area_m2 / 1e4</code></td>
    <td>m² を ha に変換 (1 ha = 10,000 m²)</td>
    <td><code>1114.18 ha (申告 AREA と完全一致!)</code></td></tr>
<tr><td>派生</td><td><code>area_ratio = geom_area_ha / AREA</code></td>
    <td>申告と実測の整合性検証 (1.000 が完全一致)</td>
    <td><code>1.0000 (整合)</code></td></tr>
<tr><td>派生</td><td><code>scale_class = _scale_class(AREA)</code></td>
    <td>規模クラスに分類 (微小/小/中/大/超大/巨大)</td>
    <td><code>'5_巨大(≥100 ha)'</code></td></tr>
</table>
<p>このように、<b>7 列の生データ</b>から
<b>規模・整合性・実測ha</b>の派生指標を導出することで、
農地転用ポリゴンの分布を多角的に評価できる。
特に <b>area_ratio</b> 列は<b>「申告データと実測ジオメトリの整合性検証」</b>として機能し、
<b>『仕様書未公開でも、データから単位を確定できる』</b>本記事の中核手法。</p>

<p class="tnote"><b>余談</b>: AREA 単位は ha と確定したが、
全 {N_POLY:,} ポリゴンの中央値 area_ratio = {ratio_med:.4f} で
IQR = [{ratio_iqr[0]:.3f}, {ratio_iqr[1]:.3f}]。
<b>誤差 ±2% 以内</b>に収まり、データの内部整合性は良好。
小ポリゴンほど誤差が大きく (図面精度の限界)、
大ポリゴンほど誤差は 0 に近づく (相対誤差が縮む)。</p>
"""

n_loaded = len(load_log)

s4_html = (
    "<h3>狙い</h3>"
    f"<p>17 ZIP の農地転用 GeoJSON を 1 個の <code>GeoDataFrame</code> "
    f"({N_POLY:,} polygon × 7 列 + 派生 5 列) に統合し、"
    f"後段の市町別集計・主題図・規模分析の基盤データを作る。"
    f"行政区域 21 件 (転用対象 17 + 無し 3 + 広島県全域 1) を背景レイヤーとして同時に読み込む。"
    f"<b>AREA 列の単位確定</b> (仕様書未公開) と"
    f"<b>文字列→数値の安全変換</b>がこの分析の核。</p>"

    "<h3>手法</h3>"
    "<p><b>直感</b>: ZIP を読む → 列を共通 7 列に揃える → "
    "<b>AREA を pd.to_numeric で安全に数値化</b> → 縦結合 → CRS 変換 → "
    "派生指標 (実測 ha・整合性比・規模クラス) を計算 → 行政区域 dissolve で背景レイヤー作成。</p>"

    "<p><b>大筋 (5 ステップ)</b></p>"
    "<ol>"
    "<li>17 市町について <code>load_geojson_zip()</code> で GeoDataFrame を読む</li>"
    "<li>共通 7 列に正規化 (本データは追加列無し)</li>"
    "<li><b>AREA 列を <code>pd.to_numeric(errors='coerce')</code> で安全に数値化</b> "
    "(失敗時 NaN で型崩れ防止 — 監査時の object dtype 対応)</li>"
    "<li><code>pd.concat</code> で縦結合 → "
    f"{N_POLY:,} 行 1 個の GeoDataFrame</li>"
    "<li>派生指標 5 つを計算: <code>geom_area_m2, geom_area_ha, "
    "area_ratio (申告/実測), scale_class (規模分類)</code></li>"
    "</ol>"

    "<p><b>前提と限界</b>: 17 件の AREA データは追加列が全く無い。"
    "<b>竹原市 (ds=1401)</b> のみ件数が 8,879 と異常に多く BIKOU=None で、"
    "他市町と<b>異なる精密データソース</b>の可能性 (本記事では補正せず合体)。"
    "<b>広島市 (ds=1394)</b> は CITY_CD = 105-108 の 4 区分のみで、"
    "8 区全部のデータは含まれない (都心部 4 区は対象農地ほぼ無し)。"
    "<b>『仕様書未公開でも、データから読める』</b>のが本シリーズの面白さで、"
    "本記事はその<b>解読プロセス</b>を教材化している。</p>"

    "<h3>実装</h3>"
    + code_load + before_after_s4 +

    f"<h3>結果</h3>"
    f"<p>17 ZIP のうち、<b>{n_loaded} 件すべてが共通 7 列で読み込み成功</b>。"
    f"統合後の <code>farm</code> GeoDataFrame は <b>{N_POLY:,} polygon × 12 列</b>"
    f"(7 共通 + AREA_raw + src_city + src_dsid + ctype + rtype + 派生 4)。"
    f"AREA 合計 = <b>{AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.0f} km²</b>"
    f" (= 広島県全域の {AREA_TOTAL_KM2/8479*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>AREA計(ha)</th><th>AREA最大(ha)</th><th>AREA=0件</th>"
    "<th>CITY_CD</th><th>KUIKI_CD群</th></tr>"
    + "".join(f"<tr><td>{L['city']}</td><td>{L['rtype']}</td>"
              f"<td>{L['n_poly']:,}</td>"
              f"<td>{L['area_a_sum']:,.0f}</td>"
              f"<td>{L['area_a_max']:,.1f}</td>"
              f"<td>{L['n_zero']:,} ({L['n_zero']/L['n_poly']*100:.0f}%)</td>"
              f"<td>{L['city_cds']}</td>"
              f"<td>{L['kuiki_cds']}</td></tr>"
              for L in load_log) +
    "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>件数最大は竹原市 8,879</b>で異常に多く、AREA最大はわずか 5.4 ha ─ "
    f"<b>『大量の微小ポリゴン』</b>で他市町と質的に異なる。"
    f"<b>AREA最大は庄原市 19,231 ha</b> (1 ポリゴン!) で、市域 1,246 km² の 15%。"
    f"<b>AREA=0 比率は市町間で大きく違う</b>: 大竹市 0% (33 件全部に値あり) ↔ "
    f"竹原市 13% ↔ 庄原市 6% ↔ 広島市 18%。"
    f"これは<b>図面精度の市町間差</b>を示す。"
    f"<b>KUIKI_CD 群</b>は中山間市は 0,3,4 のみ (= 線引き無し)、"
    f"都市市は 0,1,2 (+ 5,6) のフル区分 (= 線引きあり) という構造的差が見える。</p>"
)
sections.append(("4. 分析1: 17 GeoJSON 統合 + AREA 数値化 + 派生指標", s4_html))

# === セクション5: 分析2 — 県全域マップ + 17 vs 3 ===
s5_html = (
    "<h3>狙い</h3>"
    f"<p>広島県内の農地転用ポリゴン全 {N_POLY:,} 件の地理的配置を 1 枚で見せる。"
    "<b>「県のどこで、どう密集して農地転用対象が広がっているか」</b>を地図で直接視覚化することは、"
    "本シリーズの本質を理解する最短経路。"
    "農地転用あり 17 市町 vs 無し 3 町を色分けで対比する 2 枚目で、"
    "<b>「農地転用対象がない地域 = 都市 100%」</b>の地理的意味も同時に伝える。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) 県全域 主題図</b>: 全 20 市町 (= 17 + 無し 3) と広島県全域の"
    "行政区域を背景にし、"
    "<code>geopandas.plot(facecolor=rtype_color, alpha=0.55)</code> で農地転用ポリゴンを"
    "<b>地理タイプ色</b> (都市=赤・中山間=緑・離島=青・近郊町=紫) で描画。"
    "ポリゴン枠線は描かない (alpha 重なりで密度が読める)。</p>"

    "<p><b>(b) 17 vs 3 マップ</b>: 17 市町をオレンジ、"
    "3 町を緑で塗り、農地転用ポリゴンを赤で重ねる。"
    "「3 町には赤が乗らない = 都市 100% で農地転用対象なし」を一目で見せる。</p>"

    "<h3>実装</h3>"
    + code('''
# (a) 県全域 主題図 (rtype 色)
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888")
admin_farm = admin_diss[admin_diss["farm_status"] == "農地転用あり"]
admin_farm.plot(ax=ax, facecolor="#fafff0", edgecolor="#444")
for rt, col in RTYPE_COLOR.items():
    sub = farm[farm["rtype"] == rt]
    sub.plot(ax=ax, facecolor=col, alpha=0.55, edgecolor="none",
             label=f"{rt} ({len(sub):,})")

# (b) 17 vs 3
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss[admin_diss["farm_status"] == "農地転用あり"].plot(
    ax=ax, facecolor="#fee0b6", edgecolor="#444")
admin_diss[admin_diss["farm_status"] == "農地転用無し"].plot(
    ax=ax, facecolor="#cfe9d8", edgecolor="#444")
farm.plot(ax=ax, facecolor="#cf222e", alpha=0.45, edgecolor="none")
''') +

    "<h3>図 1 — 県全域 農地転用ポリゴン主題図 (地理タイプ色)</h3>"
    "<p><b>なぜこの図か</b>: テーブルや棒グラフでは"
    "<b>「対象農地が県のどこに、どう広がっているか」</b>が分からない。"
    "地図 1 枚で<b>「中山間 (緑) に対象農地が広く、都市 (赤) は中心部のみ」</b>"
    "という県全体の地理構造を一気に伝える。</p>"
    + figure("assets/L24_fig01_overview.png",
              f"広島県 農地転用対象ポリゴン 全 {N_POLY:,} 件 (2016-2020)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>緑 (中山間 5 市町)</b> が県北・東部に広く緑色に染まる ─ "
    "<b>『広島県の食料供給基盤は中山間にある』</b>。"
    "庄原市・三次市・東広島市・北広島町・世羅町は対象農地が市域全体に広がる。</li>"
    "<li><b>赤 (都市 10 市)</b> は瀬戸内沿岸ベルト + 福山市東部に分散 ─ "
    "都市中心部以外の周辺に対象農地が散在する形。"
    "広島市は CITY_CD=105-108 の 4 区分のみで、デルタ平野の周辺山際に対象農地。</li>"
    "<li><b>青 (離島 = 江田島市)</b> は瀬戸内の小島群に小さく赤い斑点 ─ "
    "離島での農業基盤の名残り。</li>"
    "<li><b>紫 (近郊町 = 熊野町)</b> は広島市近郊に小規模、"
    "対象農地は限定的。</li>"
    "<li>3 町 (府中町・海田町・坂町) には赤の緑も無い = "
    "<b>『市街化 100% で農地転用対象が存在しない』</b>。</li>"
    "</ul>"

    "<h3>図 2 — 農地転用あり 17 市町 vs 無し 3 町</h3>"
    "<p><b>なぜこの図か</b>: <b>「対象農地ゼロの地理境界」</b>を一目で見せる。"
    "3 町 (府中町・海田町・坂町) は<b>すべて広島市近郊のベッドタウン</b> ─ "
    "これは地図でないと伝わらない。"
    "「政令市の通勤圏に取り込まれた小規模町は、"
    "対象農地が消滅する」という都市化の最終段階を示す。</p>"
    + figure("assets/L24_fig02_farm_vs_nofarm.png",
              "農地転用あり 17 市町 (オレンジ) vs 無し 3 町 (緑)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>緑の 3 町 (府中町・海田町・坂町)</b> はすべて<b>広島市東-南東に密集</b> ─ "
    "<b>『広島市の通勤圏に取り込まれた近郊町』</b>"
    "= 都市計画基礎調査の対象農地ゼロ。"
    "L23 で府中町・海田町・坂町が DID 占有率 17-56% の<b>『町ぐるみ DID』</b>と"
    "同定された通り、農業基盤が消失している。</li>"
    "<li><b>オレンジの 17 市町</b>は県全域に広がる ─ "
    "都市部の広島・呉・福山も含めて、市域内に農地区分が残る。</li>"
    "<li><b>赤 (転用ポリゴン)</b> は中山間 (北部・北東部) に最も濃く、"
    "都市中心部 (広島市デルタ・福山市駅前) には乗らない ─ "
    "<b>『対象農地は中山間に偏在』</b>の地理構造が直視できる。</li>"
    "<li>同じ<b>近郊町</b>でも、府中町・海田町・坂町 (転用無し) と"
    "熊野町 (転用あり) は別グループ ─ <b>地形差</b>"
    "(府中町・海田町・坂町は平地、熊野町は山間に集落) が決定的。</li>"
    "</ul>"

    "<h3>表 2 — 17 市町 vs 3 町 集計</h3>"
    "<table>"
    "<tr><th>区分</th><th>市町数</th><th>合計面積 km²</th>"
    "<th>合計人口 千人</th><th>転用ポリゴン</th>"
    "<th>AREA合計 ha</th><th>対象農地率</th></tr>"
    f"<tr><td><b>農地転用あり 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:.1f}%</td></tr>"
    f"<tr><td><b>農地転用無し 3</b></td><td>3</td>"
    f"<td>{sum(CITY_REF[n[0]]['area_km2'] for n in NO_FARM_CITIES):,.0f}</td>"
    f"<td>{sum(CITY_REF[n[0]]['pop_k'] for n in NO_FARM_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:.1f}%</td></tr>"
    "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"17 市町は<b>面積では県の {sum(CITY_REF[d[1]]['area_km2'] for d in CITY_DEFS)/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)*100:.1f}%</b>を占め、"
    f"対象農地率は<b>{AREA_TOTAL_HA/sum(CITY_REF[d[1]]['area_km2'] for d in CITY_DEFS)/100*100:.1f}%</b>。"
    f"3 町は面積で <b>{sum(CITY_REF[n[0]]['area_km2'] for n in NO_FARM_CITIES)/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)*100:.2f}%</b>"
    f"のみだが対象農地ゼロ ─ <b>『都市部の対象農地は完全に消失している』</b>。"
    f"県全域の対象農地率は <b>{AREA_TOTAL_HA/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)/100*100:.1f}%</b> ─ "
    f"県の市域面積の約 18% が「都市計画基礎調査における転用対象農地」として図面化されている。</p>"
)
sections.append(("5. 分析2: 県全域マップ + 17 vs 3 (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['area_ha_sum']:,.0f}</td>"
    f"<td>{r['area_ha_max']:,.1f}</td>"
    f"<td>{r['conv_per_km2']:.2f}</td>"
    f"<td>{r['conv_share_area']:.1f}%</td>"
    f"<td>{r['conv_per_capita_a']:.0f}</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>『面積』『件数』『転用密度』の 3 軸</b>から"
    "市町順位を立体的に描く。</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 スケールで広島市と大竹市の 270 倍格差を圧縮表示。</p>"

    "<h3>実装</h3>"
    + code('''
# small multiples 17 panels
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_farm = farm[farm["src_city"] == name]
    sub_farm.plot(ax=ax_, facecolor=RTYPE_COLOR[rtype], alpha=0.55)
    n = len(sub_farm); a_ha = sub_farm["AREA"].sum()
    ax_.set_title(f"{name} ({rtype}) {n:,}件 / {a_ha:,.0f} ha")

# 3 連ランキング (面積/件数/密度)
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_d = city_agg.sort_values("conv_per_km2")
axes[2].barh(order_d["city"], order_d["conv_per_km2"], color=...)
''') +

    "<h3>図 3 — 市町別 17 panels small multiples</h3>"
    "<p><b>なぜこの図か</b>: 1 枚の県全域図では、市町ごとの<b>「転用パターンの質的違い」</b>"
    "が分からない。17 panels で並列比較すると、"
    "「庄原市の市域 8 割が緑」と「府中市の市域中央に小さい緑」が"
    "同じスケールで対比でき、<b>市町タイプと転用パターンの関係</b>が直感的に伝わる。</p>"
    + figure("assets/L24_fig03_small_multiples.png",
              "市町別 農地転用配置 small multiples (17 + 無し 3 町)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>庄原市</b> ({load_log[CITY_DEFS.index((1398,'庄原市',856,'市','中山間'))]['n_poly']:,} 件 / "
    f"{load_log[CITY_DEFS.index((1398,'庄原市',856,'市','中山間'))]['area_a_sum']:,.0f} ha): "
    "市域全体が緑に染まる ─ <b>県内最大の対象農地</b>。"
    "中山間都市の典型で、市域の大半が農地区分。</li>"
    f"<li><b>三次市・北広島町・世羅町</b>: 同じく中山間で、市域の半分以上が緑。</li>"
    f"<li><b>東広島市</b>: 中山間 + 都市の混合で、市域中央〜南が薄い緑。"
    "西条 (大学・産業) 周辺は開発済で対象農地少なめ。</li>"
    f"<li><b>竹原市</b> ({load_log[CITY_DEFS.index((1401,'竹原市',807,'市','都市'))]['n_poly']:,} 件): "
    "件数が圧倒的に多いが、各ポリゴンが微小 (max 5.4 ha)。"
    "<b>『大量の微小区画』</b> = 別ソースの精密データの可能性。</li>"
    f"<li><b>大竹市</b> ({load_log[CITY_DEFS.index((1400,'大竹市',862,'市','都市'))]['n_poly']:,} 件): "
    "件数最少 ─ 工業都市で対象農地そのものが少ない。"
    "1 件 / 13.4 ha という<b>大きな単一ブロック</b>が中心で、"
    "<b>「件数少なくも面積中規模」</b>のパターン。</li>"
    "<li><b>広島市</b>: 政令市だが対象農地は<b>市域北・北西の山際</b>に限定 ─ "
    "中区・西区など南部のデルタ地帯に対象農地は無い。</li>"
    "<li><b>離島 (江田島市)</b>: 瀬戸内の島々に小さい青い斑点 ─ "
    "島嶼農業の名残り。</li>"
    "<li><b>右下 panel (3 町)</b>: 府中町・海田町・坂町は<b>市街化 100%</b>で"
    "対象農地ゼロ ─ 「町ぐるみベッドタウン」の最終形態。</li>"
    "</ul>"

    "<h3>図 4 — 市町別 面積/件数/密度 ランキング 3 連</h3>"
    "<p><b>なぜこの図か</b>: 市町を 3 つの異なる指標 ─ 「総面積」"
    "「ポリゴン件数」「単位面積あたり転用密度」 ─ で並べると、"
    "<b>都市 vs 中山間の構造の違い</b>が立体的に見える。"
    "1 つの指標だけでは見えない<b>『市町タイプの多軸性』</b>を伝える。</p>"
    + figure("assets/L24_fig04_city_ranking.png",
              "市町別 総転用面積 / 件数 / 単位面積あたり転用 (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"次が東広島市・三次市・北広島町・世羅町と続き、<b>上位 5 件すべてが中山間</b>。"
    f"政令市・中核市 (広島・呉・福山) は中山間の半分以下。</li>"
    f"<li><b>中央 (件数 log)</b>: 竹原市 {load_log[10]['n_poly']:,} 件が異常 (他の 10-50 倍)、"
    f"次が東広島・尾道・福山・三次。"
    f"<b>件数と面積はパラレルでない</b> ─ 件数多くてもポリゴン微小なら面積小さい。</li>"
    f"<li><b>右 (転用密度 ha/km²)</b>: <b>世羅町 {city_agg[city_agg['city']=='世羅町']['conv_per_km2'].iloc[0]:.1f}</b>、"
    f"<b>庄原市 {city_agg[city_agg['city']=='庄原市']['conv_per_km2'].iloc[0]:.1f}</b>、"
    f"<b>北広島町 {city_agg[city_agg['city']=='北広島町']['conv_per_km2'].iloc[0]:.1f}</b> が上位 ─ "
    f"<b>『中山間市町ほど市域 1 km² あたり対象農地 ha が大きい』</b>。"
    f"= <b>市域に占める農地比率が高い</b>。"
    f"離島 (江田島市) と都市市は密度が低い。</li>"
    "<li>3 指標を合わせて見ると、市町は次の 4 タイプに分類できる: "
    "<b>(I) 中山間広域型</b> (庄原・三次・北広島・世羅) = 大面積・中件数・高密度、"
    "<b>(II) 都市部分散型</b> (尾道・福山・三原) = 中面積・多件数・低密度、"
    "<b>(III) 微小密集型</b> (竹原) = 中面積・極多件数・中密度、"
    "<b>(IV) 都心希薄型</b> (広島・呉・大竹・廿日市) = 小面積・少件数・低密度。</li>"
    "</ul>"

    "<h3>表 3 — 17 市町 集計表 (面積降順)</h3>"
    "<table><tr><th>市町</th><th>地理</th><th>件数</th>"
    "<th>AREA計(ha)</th><th>AREA最大(ha)</th><th>密度</th>"
    "<th>対象農地率</th><th>1人あたり(a)</th></tr>"
    + city_table_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"AREA 合計 1 位は<b>庄原市 {city_agg.iloc[city_agg['area_ha_sum'].idxmax()]['area_ha_sum']:,.0f} ha</b> "
    f"(市域の {city_agg.iloc[city_agg['area_ha_sum'].idxmax()]['conv_share_area']:.1f}%)、"
    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']=='世羅町']['conv_per_capita_a'].iloc[0]:.0f} a/人 (世羅町)</b>"
    f" や {city_agg[city_agg['city']=='庄原市']['conv_per_capita_a'].iloc[0]:.0f} a/人 (庄原市) と桁違いに大きく、"
    f"政令市は<b>{city_agg[city_agg['city']=='広島市']['conv_per_capita_a'].iloc[0]:.0f} a/人 (広島市)</b>と最小。"
    f"<b>『中山間住民は 1 人あたり 1 ha 規模の農地を背景に持つ』</b> vs "
    f"<b>『都市住民は 0.0X ha 規模の対象農地しかない』</b>という対比が定量化された。</p>"
)
sections.append(("6. 分析3: 市町別 small multiples + ランキング (H2)", s6_html))


# === セクション7: 分析4 — AREA 単位検証 + AREA=0 微小区画 ===
top_large_rows = "".join(
    f"<tr><td>{i+1}</td><td>{r['src_city']}</td><td>{r['rtype']}</td>"
    f"<td>{r['CITY_CD']}</td><td>{r['KUIKI_CD']}</td>"
    f"<td><b>{r['AREA']:,.1f}</b></td>"
    f"<td>{r['geom_area_ha']:,.1f}</td>"
    f"<td>{r['area_ratio']:.4f}</td>"
    f"<td>{r['scale_class']}</td></tr>"
    for i, (_, r) in enumerate(top20_large.head(10).iterrows())
)

s7_html = (
    "<h3>狙い</h3>"
    "<p>仕様書未公開の<b>AREA 列の単位</b>を、ジオメトリ実測との比較から確定する。"
    "AREA = 0 のポリゴンが大量にある (全 18%) 事実は<b>図面精度 1/50,000 の限界</b>を"
    "示すデータ品質の話。これらを定量的に解明することで、"
    "<b>「仕様書未公開の公開データを安全に使う方法」</b>を学習者に伝える。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) AREA 申告 vs 実測 散布図 (log-log)</b>: "
    "x軸 = AREA (申告 ha)、y軸 = geom_area_ha (m² から換算した実測 ha)。"
    "y = x の対角線が「申告 = 実測」を意味する。"
    "<b>『AREA × 10,000 = m²』</b>であれば、両軸 ha で y = x にぴったり乗る。"
    "別の単位 (a だったら 100 倍ずれ、m² だったら 10,000 倍ずれ) ならズレる。"
    "AREA = 0 を除外 (log は 0 を扱えない)。</p>"

    "<p><b>(b) AREA = 0 の geom_area 分布</b>: "
    "AREA = 0 ポリゴンの実測面積をヒストグラム化。"
    "<b>『どの程度小さいから 0 に丸められたか』</b>を可視化することで、"
    "図面精度 1/50,000 の<b>四捨五入境界</b>を逆算する。"
    "ha 単位での 0 とは<b>0.005 未満 = 50 m² 未満</b>。</p>"

    "<h3>実装</h3>"
    + code('''
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# (a) AREA 申告 vs 実測 散布
nz = farm[farm["AREA"] > 0]   # AREA = 0 を除外
for rt, col in RTYPE_COLOR.items():
    sub = nz[nz["rtype"] == rt]
    axes[0].scatter(sub["AREA"], sub["geom_area_ha"], c=col, s=4, alpha=0.3)
xs = np.array([0.001, 100000])
axes[0].plot(xs, xs, "--", color="black", label="y=x (申告=実測)")
axes[0].set_xscale("log"); axes[0].set_yscale("log")

# (b) AREA = 0 の geom_area 分布
zero = farm[farm["AREA"] == 0]
axes[1].hist(zero["geom_area_ha"].clip(upper=0.012), bins=41,
             color="#cf222e", edgecolor="white")
axes[1].axvline(0.005, ls="--", label="四捨五入境界 (0.005 ha = 50 m²)")
''') +

    "<h3>図 5 — AREA 申告 vs ジオメトリ実測 (左) + AREA=0 の実測面積分布 (右)</h3>"
    "<p><b>なぜこの図か</b>: AREA 列の単位確定は<b>『仕様書なしのデータと向き合う』</b>"
    "教材の核心。log-log 散布で y = x ラインに点が乗ることで <b>『申告 ≒ 実測』</b>が"
    "視覚的に確認できる。"
    "右側の AREA=0 ポリゴンの実測ヒストグラムは、図面精度の限界を直視させる。</p>"
    + figure("assets/L24_fig05_area_consistency.png",
              "AREA 申告値 vs ジオメトリ実測 (左, log-log) + AREA=0 の実測面積分布 (右)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>左図</b>: 全 {len(nz_full):,} ポリゴン (AREA>0) の点が"
    f"<b>y=x の対角線にぴったり乗る</b> ─ "
    f"<b>『AREA は ha 単位で確定』</b>。"
    f"area_ratio 中央値 = {ratio_med:.4f}, IQR = [{ratio_iqr[0]:.4f}, {ratio_iqr[1]:.4f}]、"
    f"誤差は ±2% 以内。</li>"
    f"<li>大ポリゴン (右上) ほど誤差が小さく y=x に密着 ─ "
    f"<b>絶対誤差は固定でも相対誤差は縮む</b>。"
    f"小ポリゴン (左下) は y=x からやや外れる ─ <b>図面精度の限界が相対誤差として顕在化</b>。</li>"
    f"<li><b>右図</b>: AREA=0 ポリゴン {N_ZERO:,} 件の<b>実測面積</b>は<b>0 - 0.005 ha (= 0-50 m²)</b>に集中。"
    f"AREA 申告値の<b>四捨五入境界 0.005 ha</b>を境に分布が切れる。"
    f"<b>『図面精度 1/50,000 → 50 m²/件 の最小描画単位』</b>が定量的に確認された。</li>"
    f"<li>図面精度の限界以下 ({N_ZERO:,} 件 = 全 {N_ZERO/N_POLY*100:.0f}%) は"
    f"<b>『真に 0 ではなく、データから抜け落ちた小区画』</b>。"
    f"これらのデータ利用には<b>注意が必要</b> (合計面積を過小評価する可能性)。</li>"
    "</ul>"

    "<h3>表 4 — 大規模対象農地 top 10</h3>"
    "<table><tr><th>順位</th><th>市町</th><th>地理</th>"
    "<th>CITY_CD</th><th>KUIKI_CD</th>"
    "<th>AREA(ha)</th><th>実測(ha)</th><th>area_ratio</th>"
    "<th>規模クラス</th></tr>"
    + top_large_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>top 1 は庄原市 19,231 ha</b> (1 ポリゴン!) ─ "
    f"市域 1,246 km² の 15% が単一 polygon で覆われている。"
    f"<b>area_ratio = {top20_large.iloc[0]['area_ratio']:.4f}</b> で実測と完全一致。"
    f"top 10 は<b>すべて中山間 (庄原・三次・東広島・北広島・世羅・安芸高田)</b>で、"
    f"AREA が <b>500 ha 以上</b>の<b>「巨大ブロック」</b>。"
    f"これらは<b>『市町全域を覆う農用地区域指定エリア』</b>と推定 ─ "
    f"個別の転用許可ではなく、<b>制度上の区分図面</b>と思われる。"
    f"上位 10 件の AREA 合計 = <b>{top20_large.head(10)['AREA'].sum():,.0f} ha</b>"
    f" = 全 AREA の<b>{top20_large.head(10)['AREA'].sum()/AREA_TOTAL_HA*100:.1f}%</b>を占める ─ "
    f"<b>パレート構造の極端な実例</b>。</p>"
)
sections.append(("7. 分析4: AREA 単位検証 + 微小区画分析 (H3, H4)", s7_html))


# === セクション8: 分析5 — 規模クラス分布 + パレート構造 ===
scale_overall_rows = "".join(
    f"<tr><td>{r['scale_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 scale_overall.sort_values("scale_class").iterrows()
)

s8_html = (
    "<h3>狙い</h3>"
    "<p>農地転用ポリゴンを<b>規模クラス 6 段階</b>に分類し、"
    "<b>『何件のポリゴンが、どれくらいの面積を占めるか』</b>のパレート構造を可視化する。"
    "市町別の規模構成も比較し、<b>『市町タイプによって転用ポリゴンの規模が違う』</b>"
    "（中山間=巨大、都市=小規模）の二極構造を確認する。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) 規模クラス × 市町 stacked bar (件数比率)</b>: "
    "市町別に規模クラス分布を 100% 棒グラフで並べ、"
    "<b>『竹原市は微小ばかり』『庄原市は大きいの混在』</b>の質的違いを示す。</p>"

    "<p><b>(b) パレート図 (件数 % vs 面積 %, 規模クラス毎)</b>: "
    "横軸 = 規模クラス、縦軸 = 全体に占める比率 (件数 % と 面積 % を 2 本並列)。"
    "<b>『規模クラス間で件数 % と面積 % が逆転する』</b>= "
    "「数の少数派が量の多数派」のパレート構造を直視する。</p>"

    "<h3>実装</h3>"
    + code('''
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)"

farm["scale_class"] = farm["AREA"].apply(_scale_class)

# 規模 × 市町 クロス → stacked bar
sc = farm.groupby(["src_city", "scale_class"]).size().unstack(fill_value=0)
sc_pct = sc.div(sc.sum(axis=1), axis=0) * 100
sc_pct.plot(kind="barh", stacked=True)

# パレート図 (件数 % vs 面積 %)
overall = farm.groupby("scale_class").agg(n=("AREA","size"),
                                           area=("AREA","sum"))
overall["n_pct"]    = overall["n"] / N_POLY * 100
overall["area_pct"] = overall["area"] / AREA_TOTAL_HA * 100
ax.bar(overall.index, overall["n_pct"], label="件数 %")
ax.bar(overall.index + width, overall["area_pct"], label="面積 %")
''') +

    "<h3>図 6 — 市町別 規模構成 + パレート図</h3>"
    "<p><b>なぜこの図か</b>: 単純な平均値や合計では <b>『規模分布の歪み』</b>が見えない。"
    "stacked bar で市町ごとの規模構成を直視し、パレート図で件数 % vs 面積 % の"
    "<b>逆転現象</b>を視覚化することで、<b>『大規模ポリゴンが面積を支配する』</b>"
    "を一目で伝える。</p>"
    + figure("assets/L24_fig06_scale_class.png",
              "市町別 規模クラス構成 (左, 件数 %) + 全体パレート図 (右)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>左図 (市町別 stacked)</b>: <b>竹原市は黄色 (微小+小) が 95% 超</b> ─ "
    "他市町と異なる<b>『微小ポリゴン特化』</b>のデータ構造。"
    "<b>庄原市・三次市・北広島町</b>は赤 (大・超大・巨大) の比率が高い ─ "
    "<b>『中山間市町ほど大規模ポリゴン主体』</b>。"
    "<b>大竹市</b>は中・大の混合で件数が少ない (33 件)。</li>"
    "<li><b>右図 (パレート)</b>: <b>件数 % は微小・小・中で 90% 超</b>"
    f" ({scale_overall.iloc[0]['n_pct']+scale_overall.iloc[1]['n_pct']+scale_overall.iloc[2]['n_pct']:.0f}%)、"
    f"<b>面積 % は超大・巨大で 80% 超</b> "
    f"({scale_overall[scale_overall['scale_class'].isin(['4_超大(10-100 ha)','5_巨大(≥100 ha)'])]['area_pct'].sum():.0f}%)。"
    "<b>『件数の少数派 (10%) が面積の多数派 (90%) を占める』</b>"
    "という<b>パレート構造</b>が定量化された。</li>"
    "<li>規模クラスごとに件数 % と面積 % が<b>クロスオーバー</b>する点が中規模 (1-10 ha)。"
    "そこより小さい規模は『件数 ≫ 面積』、大きい規模は『面積 ≫ 件数』。</li>"
    "<li>政策的含意: <b>個別の小規模転用許可</b> (件数大) は実質的な面積影響が小さく、"
    "<b>少数の大規模制度区分</b> (面積大) が県の農地ストックを左右する。</li>"
    "</ul>"

    "<h3>表 5 — 規模クラス全体集計 (パレート構造)</h3>"
    "<table>"
    "<tr><th>規模クラス</th><th>件数</th><th>件数 %</th>"
    "<th>面積 ha</th><th>面積 %</th></tr>"
    + scale_overall_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>0_微小 ({scale_overall[scale_overall['scale_class']=='0_微小(<0.01 ha)']['n'].iloc[0]:,} 件 = "
    f"{scale_overall[scale_overall['scale_class']=='0_微小(<0.01 ha)']['n_pct'].iloc[0]:.0f}%)</b> は"
    f"件数で全体の 1/3 を占めるが、面積では <b>0%</b>。"
    f"<b>5_巨大 ({scale_overall[scale_overall['scale_class']=='5_巨大(≥100 ha)']['n'].iloc[0]:,} 件 = "
    f"{scale_overall[scale_overall['scale_class']=='5_巨大(≥100 ha)']['n_pct'].iloc[0]:.1f}%)</b>"
    f"はわずか件数の 0.X%だが、<b>面積で 50% 超</b>を占める ─ "
    f"<b>『1 件の巨大区画 = 数千件の微小区画』</b>。"
    f"これは<b>「データ集計時に件数で重み付けすると、面積実態を見誤る」</b>という"
    f"教訓 ─ 必ず<b>面積加重</b>で集計せよ。</p>"

    "<h3>図 7 — 大規模対象農地 (≥10 ha) バブルマップ</h3>"
    "<p><b>なぜこの図か</b>: パレート構造の上位 8% (≥10 ha) を<b>地理的に</b>確認する。"
    "県全域マップでは大小ポリゴンが alpha 重ねで判別困難。"
    "ここではバブル (centroid 中心、AREA に比例した大きさ) で<b>大規模区画の地理的偏在</b>"
    "を可視化する。</p>"
    + figure("assets/L24_fig07_large_bubble.png",
              "大規模対象農地 (≥10 ha) バブルマップ") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>緑バブル (中山間)</b> が県北・北東の大規模 ─ <b>庄原・三次・北広島・世羅</b>が大バブル群。"
    f"これら市町の<b>『農用地区域』</b>が制度的に巨大ブロックで指定されている。</li>"
    f"<li><b>赤バブル (都市)</b> は瀬戸内沿岸に小〜中バブル ─ "
    f"<b>東広島市西部 (広島-福山幹線沿い)</b>に中規模バブル群、"
    f"<b>福山市東部</b>に小バブル散在。"
    f"<b>『都市計画区域内でも農地が完全消失していない』</b>を示す。</li>"
    f"<li><b>青バブル (江田島市)</b> は離島の中央に中規模 ─ 旧海軍跡地周辺の島内農地。</li>"
    f"<li><b>top 3</b>: 庄原市 19,231 ha、庄原市 5,069 ha、庄原市 3,824 ha ─ "
    f"<b>1 つの市が他を圧倒</b>。"
    f"top 3 がすべて庄原市 = <b>『庄原市の制度的大区画』</b>が県の対象農地構造を支配。</li>"
    "</ul>"
)
sections.append(("8. 分析5: 規模クラス分布 + パレート構造 + 大規模バブル (H6)",
                  s8_html))


# === セクション9: 分析6 — 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()
)

s9_html = (
    "<h3>狙い</h3>"
    "<p>仕様書未公開の<b>KUIKI_CD (区域コード, 0-6 の 7 値)</b> と"
    "<b>TOKEI_CD (統計区分コード, 1-3 の 3 値)</b> の意味を、"
    "市町別の値分布と地理的位置から<b>『推定』</b>する。"
    "これは<b>『公開オープンデータの仕様書がない場合に、自分で意味を解読する』</b>"
    "という研究プロセスの実演である。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) KUIKI_CD 別 県全域マップ</b>: 0-6 を異なる色で塗り、"
    "地理分布から仮説を立てる。"
    "<b>『線引き都市計画区域 (市街化/調整区域)』</b>の都市市町と、"
    "<b>『線引きなし』</b>の中山間市町で値分布が異なるはず。</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 = (?) (ピンク)
    5: "#fb8500",   # 5 = (?) (オレンジ)
    6: "#666666",   # 6 = (?) (灰)
}

# (a) KUIKI_CD 別 県全域マップ
fig, axes = plt.subplots(1, 2, figsize=(18, 9))
admin_diss.plot(ax=axes[0], facecolor="#f5f5f5")
for kc, col in kuiki_palette.items():
    sub = farm[farm["KUIKI_CD"] == kc]
    sub.plot(ax=axes[0], facecolor=col, alpha=0.55,
             label=f"KUIKI_CD={kc} ({len(sub):,})")

# (b) KUIKI_CD × 市町 stacked bar
kc_cross = farm.groupby(["src_city","KUIKI_CD"]).size().unstack(fill_value=0)
kc_pct = kc_cross.div(kc_cross.sum(axis=1), axis=0) * 100
kc_pct.plot(kind="barh", stacked=True, ax=axes[1])
''') +

    "<h3>図 8 — KUIKI_CD 別 県全域マップ (左) + 市町別 KUIKI_CD 構成 (右)</h3>"
    "<p><b>なぜこの図か</b>: <b>KUIKI_CD = 0-6</b>の 7 値が何を意味するか、"
    "テーブルだけでは分からない。地図に色分けして"
    "<b>『地理的にどう分布するか』</b>を見せると、"
    "都市計画区域の階層構造と整合する分布が見えれば仮説が支持される。</p>"
    + figure("assets/L24_fig08_kuiki_cd.png",
              "KUIKI_CD 別 県全域マップ + 市町別構成") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>左図 (地図)</b>: <b>緑 (KUIKI_CD=0)</b> は中山間市町 (庄原・三次・北広島・世羅) を"
    f"中心に分布 ─ <b>『都市計画区域外の農地』</b>と推定。"
    f"<b>赤 (KUIKI_CD=1)</b> は都市市町の中心部 ─ <b>『市街化区域内の農地』</b>と推定。"
    f"<b>紫 (KUIKI_CD=2)</b> は都市市町の周辺 ─ <b>『市街化調整区域内の農地』</b>と推定。"
    f"<b>青 (KUIKI_CD=3)</b>と<b>ピンク (KUIKI_CD=4)</b>は中山間広く分布 ─ "
    f"<b>『非線引き都市計画区域 + 区域外』</b>と推定。</li>"
    f"<li><b>右図 (市町構成)</b>: <b>都市 7 市町 (広島・呉・三原・東広島・尾道・府中・福山)</b>"
    f"は赤+紫を持つ ─ 線引き都市計画区域の存在を示唆。"
    f"<b>中山間 8 市町 (三次・庄原・安芸高田・世羅・北広島・熊野・江田島 + 廿日市の山間部)</b>は"
    f"緑+青+ピンクのみで赤紫を持たない ─ <b>『線引きなし』</b>を示唆。"
    f"<b>竹原市</b>は青+ピンクのみで KUIKI_CD = {{3, 4}} の 2 値だけ ─ "
    f"異常な精密データの特徴。</li>"
    f"<li>地理分布と市町構成の<b>両方が KUIKI_CD = 都市計画区域階層フラグ</b>"
    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> が件数 % で<b>{kuiki_overall[kuiki_overall['KUIKI_CD']==0]['n_pct'].iloc[0]:.0f}%</b> ─ "
    f"中山間に多い「都市計画区域外」の農地と推定 (面積も最大)。"
    f"<b>KUIKI_CD = 3, 4</b> が件数 %で大きい ─ <b>『非線引き / 区域外』</b>の混在と推定。"
    f"<b>KUIKI_CD = 1, 2</b> は件数 %は小さいが、これらは<b>『市街化区域 + 調整区域』</b>"
    f"= 都市計画区域内の農地で、<b>都市拡張の最前線</b>。"
    f"<b>KUIKI_CD = 5, 6</b> は極めて少ない (広島市のみ) ─ "
    f"<b>『広島市特有のサブ区分』</b>(政令市の独自 KUIKI 値か?)。</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 = 1</b> が件数で<b>{tokei_overall[tokei_overall['TOKEI_CD']==1]['n_pct'].iloc[0]:.0f}%</b>と最大 ─ "
    f"<b>『農用地区域指定 (主要)』</b>と推定。"
    f"<b>TOKEI_CD = 3</b> は件数で次点 ─ "
    f"<b>『農業振興地域内農用地白地』</b>と推定。"
    f"<b>TOKEI_CD = 2</b> は最少 ─ "
    f"<b>『その他』</b>と推定。"
    f"<b>仕様書未公開のため確定できない</b>が、"
    f"<b>『市町間でも値分布のパターンが類似 (= 制度上の意味があるはず)』</b>"
    f"と推測される。発展課題で都市計画基礎調査要綱との照合により確定する。</p>"
)
sections.append(("9. 分析6: KUIKI_CD / TOKEI_CD 解読 (H5)", s9_html))


# === セクション10: 分析7 — 市域面積 vs AREA + 1人あたり ===
s10_html = (
    "<h3>狙い</h3>"
    "<p>市町の総面積・総人口と AREA の関係を log-log 散布で示し、"
    "<b>「市町規模と対象農地の関係」</b>と<b>「1 人あたり対象農地の地理タイプ依存」</b>を可視化する。"
    "農地転用無し 3 町を ×印で同じ散布に重ね、"
    "<b>「対象農地ゼロの境界」</b>を観察する。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) 市域面積 vs AREA (log-log)</b>: x軸 = 市町面積 km²、y軸 = AREA 合計 ha。"
    "1% / 10% の対象農地率ライン (= 1 ha/km², 10 ha/km²) を補助線として引く。</p>"

    "<p><b>(b) 市町総人口 vs 1 人あたり対象農地 (a/人)</b>: "
    "市町人口 vs 市民 1 人あたり対象農地。中山間市は<b>1 万 a/人 = 100 ha/人</b>規模、"
    "都市市は<b>0.X a/人</b>と桁違い。地理タイプで 4 グループにクラスタ化されるはず。</p>"

    "<h3>実装</h3>"
    + code('''
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# (a) 市域面積 vs AREA
for rt, col in RTYPE_COLOR.items():
    sub = city_agg[city_agg["rtype"] == rt]
    axes[0].scatter(sub["city_area_km2"], sub["area_ha_sum"],
                    c=col, s=180, label=rt)
# 3 町 (転用無し): x=面積, y≒0
for name, _adm, _ct, _r in NO_FARM_CITIES:
    axes[0].scatter(CITY_REF[name]["area_km2"], 0.5, marker="x", c="#666")
xs = np.linspace(5, 1500, 50)
axes[0].plot(xs, xs * 1, "--", label="1% 対象農地率")
axes[0].plot(xs, xs * 10, ":", label="10% 対象農地率")
axes[0].set_xscale("log"); axes[0].set_yscale("log")

# (b) 人口 vs 1 人あたり
for rt, col in RTYPE_COLOR.items():
    sub = city_agg[city_agg["rtype"] == rt]
    axes[1].scatter(sub["city_pop_k"]*1000, sub["conv_per_capita_a"],
                    c=col, s=180, label=rt)
axes[1].set_xscale("log"); axes[1].set_yscale("log")
''') +

    "<h3>図 9 — 市域面積 × AREA (左) + 人口 × 1 人あたり (右)</h3>"
    "<p><b>なぜこの図か</b>: 市町の対象農地は<b>市域規模に依存する</b>ことが想像できるが、"
    "log-log 散布で<b>『市域 1% ライン vs 10% ライン』</b>を引くと、"
    "市町の<b>『対象農地率階層』</b>が一目で分かる。"
    "1 人あたり指標と組み合わせると、<b>『地理タイプによる構造的違い』</b>"
    "が立体的に見える。</p>"
    + figure("assets/L24_fig09_pop_area_scatter.png",
              "市域面積 × AREA (log-log) + 市町人口 × 1 人あたり対象農地") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>左図 (面積 vs AREA)</b>: <b>中山間市町 (緑)</b> は <b>10% ラインの上</b>に位置 ─ "
    f"市域の 10% 以上が対象農地。庄原市は 15% の極端値。"
    f"<b>都市市 (赤)</b> は <b>1% ライン付近</b> ─ 対象農地率が低い。"
    f"<b>離島 (青) と近郊町 (紫)</b> は中間 (1-10%)。"
    f"3 町 (×印) は y ≒ 0 ─ 対象農地ゼロ。</li>"
    f"<li><b>右図 (人口 vs 1 人あたり)</b>: <b>中山間市町 (緑)</b> は<b>500-2,000 a/人</b>"
    f" (= 5-20 ha/人 = 50,000-200,000 m²/人) ─ <b>『1 人で 1-2 ha の農地基盤』</b>。"
    f"<b>都市市 (赤)</b> は<b>1-30 a/人</b> ─ 1 人あたり 1 反 (= 0.1 ha) 程度。"
    f"<b>近郊町 (紫 = 熊野町)</b> は中山間並みで 100 a/人 ≒ 1 ha/人。"
    f"<b>『地理タイプで 1-2 桁の差』</b>が定量化された。</li>"
    f"<li>政策的含意: 中山間市町の住民は<b>『対象農地に対して責任ある人口』</b>が"
    f"少ない (1 人で広大な区域を扱う)、"
    f"対する都市住民は<b>『農地は他人事』</b>になりがち。"
    f"これは<b>食料政策・農地保全政策</b>に大きな含意がある。</li>"
    "</ul>"

    "<h3>表 8 — 17 市町 + 3 町 詳細 (rtype 別合計)</h3>"
    "<table>"
    "<tr><th>地理タイプ</th><th>市町数</th><th>合計面積 km²</th>"
    "<th>合計人口 千人</th><th>AREA ha</th>"
    "<th>対象農地率 %</th><th>1人あたり a</th></tr>"
    + "".join(
        f"<tr><td><b>{rt}</b></td>"
        f"<td>{int(sub['city_area_km2'].count())}</td>"
        f"<td>{sub['city_area_km2'].sum():,.0f}</td>"
        f"<td>{sub['city_pop_k'].sum():,}</td>"
        f"<td>{sub['area_ha_sum'].sum():,.0f}</td>"
        f"<td>{sub['area_ha_sum'].sum()/(sub['city_area_km2'].sum()*100)*100:.1f}%</td>"
        f"<td>{sub['area_ha_sum'].sum()*100/(sub['city_pop_k'].sum()*1000):.1f}</td></tr>"
        for rt, sub in [(rt, city_agg[city_agg["rtype"] == rt])
                          for rt in RTYPE_COLOR.keys()]
    )
    + f"<tr><td><b>農地転用無し町</b></td><td>{len(NO_FARM_CITIES)}</td>"
    f"<td>{sum(CITY_REF[n[0]]['area_km2'] for n in NO_FARM_CITIES):.0f}</td>"
    f"<td>{sum(CITY_REF[n[0]]['pop_k'] for n in NO_FARM_CITIES):,}</td>"
    f"<td>0</td><td>0%</td><td>0</td></tr>"
    + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>『中山間 5 市町』</b>は面積 {city_agg[city_agg['rtype']=='中山間']['city_area_km2'].sum():,.0f} km² (県の "
    f"{city_agg[city_agg['rtype']=='中山間']['city_area_km2'].sum()/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)*100:.0f}%) で"
    f" AREA = <b>{city_agg[city_agg['rtype']=='中山間']['area_ha_sum'].sum():,.0f} ha</b> ─ "
    f"<b>県の対象農地の {city_agg[city_agg['rtype']=='中山間']['area_ha_sum'].sum()/AREA_TOTAL_HA*100:.0f}% を 5 市町が占める</b>。"
    f"対する<b>『都市 10 市』</b>は面積 {city_agg[city_agg['rtype']=='都市']['city_area_km2'].sum():,.0f} km² (県の "
    f"{city_agg[city_agg['rtype']=='都市']['city_area_km2'].sum()/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)*100:.0f}%) だが"
    f" AREA = <b>{city_agg[city_agg['rtype']=='都市']['area_ha_sum'].sum():,.0f} ha</b> "
    f"({city_agg[city_agg['rtype']=='都市']['area_ha_sum'].sum()/AREA_TOTAL_HA*100:.0f}%)。"
    f"<b>『中山間が県の食料供給基盤を背負う』</b>地理構造が明確。"
    f"近郊町 (熊野町) と離島 (江田島市) は補助的な役割。</p>"
)
sections.append(("10. 分析7: 市域面積/人口 vs 対象農地 (H2 補強)", s10_html))


# === セクション11: 仮説検証 ===
hr_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()
)

s11_html = (
    "<p>分析 1〜7 の結果から、仮説 H1〜H6 を改めて検証する。</p>"

    "<table>"
    "<tr><th>仮説</th><th>仮説内容</th><th>検証結果</th><th>判定</th></tr>"
    + hr_rows + "</table>"

    "<h3>総括</h3>"
    "<ul>"
    f"<li><b>H1 (総 AREA ≥ 50,000 ha)</b>: {H_RESULTS['H1']['judge']}。"
    f"AREA合計 {AREA_TOTAL_HA:,.0f} ha = {AREA_TOTAL_KM2:.0f} km²、"
    f"県市域面積の <b>{AREA_TOTAL_KM2/8479*100:.1f}%</b>。"
    f"これは<b>『広島県の市域の約 18% が転用対象農地として図面化されている』</b>"
    f"という大規模さを示す。</li>"
    f"<li><b>H2 (中山間 5 市町優位)</b>: {H_RESULTS['H2']['judge']}。"
    f"中山間 5 市町 {H2_top5_ha:,.0f} ha vs 都市 5 市 {H2_urban5_ha:,.0f} ha = "
    f"<b>{H2_top5_ha/max(H2_urban5_ha,1):.1f} 倍</b>の格差。"
    f"<b>『広島県の食料供給基盤は中山間に偏在』</b>という地理的事実が定量化された。</li>"
    f"<li><b>H3 (AREA=0 微小ポリゴン)</b>: {H_RESULTS['H3']['judge']}。"
    f"全件 AREA=0 = {N_ZERO:,}/{N_POLY:,} ({N_ZERO/N_POLY*100:.1f}%)、"
    f"市町別中央値 {zero_med:.1f}% (min={zero_min:.1f}, max={zero_max:.1f})。"
    f"<b>『図面精度 1/50,000 → 50 m²/件 が四捨五入で 0 になる』</b>が確認された。</li>"
    f"<li><b>H4 (AREA は ha 単位)</b>: {H_RESULTS['H4']['judge']}。"
    f"area_ratio 中央値 {ratio_med:.4f} (≒ 1.000)。"
    f"<b>『仕様書未公開でも、ジオメトリとの整合性チェックで単位確定可能』</b>"
    f"という研究プロセスを実演。</li>"
    f"<li><b>H5 (KUIKI_CD = 区域階層)</b>: {H_RESULTS['H5']['judge']}。"
    f"都市 7 市町すべてが KUIKI=1 or 2 を保有、"
    f"中山間 5 市町すべてが KUIKI=3 or 4 を保有。"
    f"<b>『仕様書未公開のフラグ列の意味を、地理分布から推定』</b>できた。"
    f"市町ごとの値分布が線引き有無と完全一致 ─ KUIKI_CD = 都市計画区域階層フラグ"
    f"の仮説は強く支持される。</li>"
    f"<li><b>H6 (パレート構造)</b>: {H_RESULTS['H6']['judge']}。"
    f"≥10ha 件数 = {large_n:,} ({large_n_pct:.2f}%)、"
    f"≥10ha 面積 = {large_ha_sum:,.0f} ha ({large_ha_pct:.1f}%)。"
    f"<b>『件数の 7% が面積の 96% を支配』</b>という極端なパレート構造。"
    f"件数集計で見る『多数派』と面積集計で見る『多数派』は別物。</li>"
    "</ul>"

    "<h3>本記事の主要発見 5 点</h3>"
    "<ol>"
    f"<li><b>『中山間が県の食料供給基盤を背負う』を定量化</b>: "
    f"中山間 5 市町 (庄原・三次・東広島・北広島・世羅) が"
    f"県対象農地の <b>{city_agg[city_agg['rtype']=='中山間']['area_ha_sum'].sum()/AREA_TOTAL_HA*100:.0f}%</b>を占める。"
    f"庄原市単独で県全体の {city_agg[city_agg['city']=='庄原市']['area_ha_sum'].iloc[0]/AREA_TOTAL_HA*100:.0f}%。"
    f"<b>『農地転用＝都市拡張』のイメージは誤り</b>で、"
    f"県の対象農地は中山間の制度的指定が主体。</li>"
    f"<li><b>仕様書未公開の AREA 単位を解読</b>: "
    f"実測比 geom_m²/AREA = 10,000 から<b>『AREA = ha 単位』</b>と確定。"
    f"area_ratio 中央値 {ratio_med:.4f}、IQR ±2% 以内。"
    f"<b>『仕様書なしでもデータから単位を確定する』</b>研究プロセスの実演。</li>"
    f"<li><b>図面精度 1/50,000 の限界を可視化</b>: "
    f"AREA=0 ポリゴン {N_ZERO:,} 件 ({N_ZERO/N_POLY*100:.0f}%) の実測面積が"
    f"<b>0-50 m² に集中</b>。図面精度 1 mm = 50 m が"
    f"<b>最小描画単位 50 m² (= 0.005 ha)</b>に対応。"
    f"<b>『公開データの誤差を定量化する手順』</b>を学ぶ。</li>"
    f"<li><b>KUIKI_CD = 都市計画区域階層フラグ</b>を解読: "
    f"都市 7 市町は KUIKI = 0,1,2 (+5,6) を保有 (= 線引きあり)、"
    f"中山間 8 市町は KUIKI = 0,3,4 のみ (= 線引きなし)。"
    f"<b>『仕様書未公開のフラグ列を、地理分布と市町構成から推定』</b>。"
    f"これにより市街化区域内/調整区域内/区域外の対象農地を区別可能。</li>"
    f"<li><b>パレート構造の極端な実例</b>: "
    f"大規模 (≥10 ha) ポリゴン {large_n:,} 件 (件数の {large_n_pct:.1f}%) が"
    f"<b>面積の {large_ha_pct:.0f}% を支配</b>。"
    f"top 1 (庄原市 19,231 ha) だけで県全体の "
    f"{19231/AREA_TOTAL_HA*100:.0f}%。"
    f"<b>『集計時に件数で重み付けすると面積実態を見誤る』</b>という"
    f"統計分析の教訓。</li>"
    "</ol>"
)
sections.append(("11. 仮説検証と考察", s11_html))


# === セクション12: 発展課題 ===
s12_html = """
<p>本記事で得られた結果から導かれる<b>新たな問い</b>と、
それを検証するための<b>具体的な発展手順</b>を 3 つ提示する。</p>

<h3>発展課題 1: 農林漁業施策 (L25) との空間オーバーレイ ─ 保全 vs 転用</h3>
<p><b>結果 X</b>: 本記事で県内 17 市町の対象農地 14,459 ポリゴン (155,111 ha) の地理が定量化された。
都市計画基礎調査の同じ期間 (2016-2020) で、もう 1 つのシリーズ
<b>「農林漁業関係施策」(L25, 17 件)</b> が公開されており、
これは<b>『国・県の農地保全策が適用される区域』</b>を示す。</p>
<p><b>新仮説 Y</b>: 農林漁業施策エリアと農地転用対象エリアは<b>地理的に重なる
(= 同じ農地に保全策と転用区分が同居)</b>か、
<b>相補的に分布する (= 保全区域と転用区域は別)</b>のどちらか。
広島県は中山間振興策 (中山間直接支払等) を多く実施するため、
<b>「中山間 5 市町で重なり率 ≥ 80%」</b>と予想される。</p>
<p><b>課題 Z</b>: <code>geopandas.overlay(farm, sisaku, how='intersection')</code> で
17 市町ごとに重なり面積を計算 →
<b>「保全 ∩ 転用 / 転用」</b>の包含率を市町別に集計 →
17 panels small multiples で重なり地理を可視化 →
<b>『広島県の中山間農地は保全と転用が同居している』</b>仮説を検証。
本記事の次記事 L25 でこれを実装する予定。</p>

<h3>発展課題 2: 都市計画区域 (L15) との整合性検証 ─ KUIKI_CD の意味確定</h3>
<p><b>結果 X</b>: 本記事で KUIKI_CD = 都市計画区域階層フラグ (推定) が
都市市町と中山間市町で値分布が異なることを確認した。
ただし<b>『どの値が市街化区域』か</b>は推定の域を出ない。</p>
<p><b>新仮説 Y</b>: L15 の<b>市街化区域 / 市街化調整区域 ポリゴン (14 + 14 件)</b>と
本記事の対象農地ポリゴンを<b>空間結合</b>すれば、
KUIKI_CD = 1 が市街化区域、KUIKI_CD = 2 が市街化調整区域、
KUIKI_CD = 3, 4 が非線引きまたは区域外、と確定できる。
<b>『各 KUIKI 値ポリゴンの 80% 以上が、対応する都市計画区域内に位置する』</b>
ことを定量的に確認する。</p>
<p><b>課題 Z</b>: L15 都市計画区域データを取得 →
<code>gpd.sjoin(farm, kuiki_zones, predicate='within')</code> で
各転用ポリゴンに区域種別を付与 →
KUIKI_CD × 区域種別のクロス集計 →
<b>『KUIKI_CD = 1 のポリゴンは市街化区域内』</b>等の対応関係を表で示す。
発展手順としては <code>data/extras/_urban_planning_audit/</code> の市街化区域 ZIP を活用。</p>

<h3>発展課題 3: 5 年後 (R7=2025) との比較 — 転用速度の地理</h3>
<p><b>結果 X</b>: 本記事は 2016-2020 (5 年間) のスナップショット。
転用が静的か動的かを語るには 1 期しかなく不十分。</p>
<p><b>新仮説 Y</b>: 次期都市計画基礎調査 (2021-2025) では、
<b>(a) 都市市の調整区域内ポリゴン</b>が<b>都市開発で減少</b>する一方、
<b>(b) 中山間市町の区域外ポリゴン</b>は<b>耕作放棄で『非農地』扱いに変わる</b> ─
両ベクトルで対象農地は<b>『縮小』</b>すると予想される。
庄原市・三次市の中山間ポリゴンは<b>5 年で 5-10% 縮減</b>。</p>
<p><b>課題 Z</b>: 都市計画基礎調査の次期版が公開されたら (R8 頃想定)、
<b>同じ ID 体系で 17 市町分の差分計算</b>。
ポリゴン重なり率 (Jaccard 係数) を市町別に算出 →
<b>「Δ 面積 = 縮小と新規追加」</b>を定量化 →
中山間と都市で減少パターンの違いを比較 →
<b>『どこで耕作放棄が進み、どこで転用が進むか』</b>の地理を描く。
これは現時点では未来課題だが、本記事の手法 (load_geojson_zip + AREA 数値化 + 派生)
を流用すれば即実装可能な再現性を確保している。</p>
"""
sections.append(("12. 発展課題", s12_html))

# =============================================================================
# 7. HTML 出力
# =============================================================================
print("\n[7] HTML 出力", flush=True)
title = "L24 農地転用状況 17件 統合分析 — 広島県内の農地が非農地に変わるパターン"
tags = ["都市計画", "農地転用", "農用地区域", "17市町統合",
        "規模分布", "パレート構造", "geopandas", "DoBoX",
        "図面精度", "都市計画基礎調査"]
data_label = (
    "DoBoX 農地転用状況 17件 (#1391-1407)"
)
html = render_lesson(
    num=24, title=title, tags=tags,
    time="30〜40 分", level="中級 (GIS + 都市計画 + データ品質)",
    data_label=data_label, sections=sections,
    script_filename="L24_farmland_conversion.py",
)
out = LESSONS / "L24_farmland_conversion.html"
out.write_text(html, encoding="utf-8")
print(f"  -> {out}")
print(f"\n=== L24 DONE  Total {time.time()-t0:.2f}s ===")
