"""L23 DID地区境界 14 件 統合分析 — 広島県の都市部・非都市部の地理構造

カバー宣言:
  本記事は 「都市計画区域情報_DID地区境界データ_*_2020」 の 14 dataset_id を統合し、
  R2 国勢調査ベースで広島県内 14 DID 指定市町の <b>人口集中地区 (DID) 計 48 ポリゴン</b>
  を 1 個の GeoDataFrame に縦結合した上で、<b>広島県の「都市部」の地理構造</b>を分析する研究記事である。
  全 20 市町中 6 市町は DID 無指定 ─ そのコントラストも分析対象とする。

  14 dataset_id (連番性なし、性別年齢別人口の +1 オフセット):
    1468 広島市,    1471 呉市,      1473 竹原市,    1476 三原市,
    1479 尾道市,    1482 福山市,    1485 府中市,    1487 三次市,
    1490 大竹市,    1493 東広島市,  1495 廿日市市,
    1500 府中町,    1502 海田町,    1505 坂町

  6 市町 (DID 無指定): 庄原市, 安芸高田市, 江田島市, 熊野町, 北広島町, 世羅町

研究の問い (RQ):
  広島県内 14 DID 指定市町の DID は、面積・人口・密度・連続性・市町間配分の観点で
  どのような地理構造を持ち、県全域の<b>都市部 vs 非都市部</b>の地理はどう描けるか？
  DID 指定の有無は何を物語るのか？

仮説 H1〜H6:
  H1. <b>広島市の DID 集中度が圧倒的</b>。県内 48 DID のうち 18 (37.5%) が広島市、
       DID 人口 1,840,000 のうち 100 万人超 (>56%) が広島市の 8 区に集中。
       「広島市が広島県の都市部の中核」が定量的に確認される。
  H2. DID 個数は<b>市町規模に強く相関</b>するが厳密比例ではない。
       政令市の広島市 (18 DID, 8 区) > 中核市の福山市 (6) > 呉市 (5) で、
       <b>「都市の核がいくつあるか」</b>を示す指標となる。
       8 つの市町は DID が <b>1 つだけ</b>で「単核都市」、広島・呉・福山は「多核都市」。
  H3. DID 人口密度は <b>市町間で 3 倍以上の格差</b> がある。
       <b>府中町</b> (CITY_CD=302, 人口 5万に対し DID 1ヶ所) が県内最高密度、
       小規模町でも DID は超高密度。中山間市町の DID は密度がやや低く境界線にある。
  H4. 14 DID 指定市町の<b>市域に占める DID 面積の割合</b>は典型的に <b>5% 未満</b>で、
       「都市と呼ばれる市町でも 95% 以上は非 DID」という地理的事実が見える。
       府中町・海田町・坂町など小規模ベッドタウンのみ DID 占有率が 30% を超える。
  H5. <b>居住誘導区域 (LIP, L19) との重なり</b>は意味のあるクロス。
       LIP 策定 8 市町の DID 内に LIP がほぼ完全に内包される (DID ∩ LIP / DID ≥ 50%)。
       LIP は「DID を継承して、より縮約した将来の居住核」として設計されている、
       というコンパクトシティ政策の論理を実データで確認する。
  H6. <b>DID 無指定 6 市町</b> (庄原市・安芸高田市・江田島市・熊野町・北広島町・世羅町)
       は<b>人口 3 万以下が 5/6</b>、平均人口密度が低い (50-150 人/km²)。
       「中山間 + 離島 + 広島近郊町の 3 タイプ」に分類できる。

要件 S 準拠: 1 分以内完走。48 ポリゴン + 21 行政区域 + 8 LIP は計算が極めて軽量。
要件 T 準拠: 県全域 DID 主題図、市町別 small multiples 14 panel、
            DID 人口密度 choropleth、DID 無し vs ありマップ、
            LIP との重ね合わせマップ、DID 面積比 ranking。
要件 Q 準拠: 図 9 枚以上、表 9 枚以上。

データ仕様 (9 列、14 市町で 100% 一致):
  TOKEI_CD     int32     統計区分コード (本データ全件 1)
  CITY_CD      int32     市区町村コード (広島市は 8 区を 101-108 で分割)
  KUIKI_CD     int32     区域コード (本データ全件 1, DID 区域識別)
  ZU_NO        object    DID 地区番号 ('a','b','c'... アルファベット)
  TOCHI_A      int32     DID 面積 (ha 単位、geom.area/1e4 とほぼ一致)
  JINKOU_S     int32     DID 人口 (R2 国勢調査ベース)
  BIKOU        object    備考 (本データ全件 None)
  RITTEKI_CD   int32     立地コード (1 / 2 の 2 値、仕様書未公開)
  geometry     MultiPolygon  EPSG:6671 (JGD2011 平面直角第III系)

DID 定義 (国勢調査):
  人口集中地区 (Densely Inhabited District) は、原則として
  <b>国勢調査の調査区 (1km² 程度) のうち人口密度が 4,000 人/km² 以上のものが連担し</b>、
  その全体の人口が 5,000 人以上となる地域。
  「市制施行された自治体すべてが DID を持つ」わけではなく、
  人口集積が DID 基準に満たない 5 市町と、6 町村は DID 無指定である。

データ品質ノート:
  ds=1487 (三次市) は GeoJSON リソースが 府中市 (ds=1485) のデータを誤配布していた。
  本記事では Shapefile リソース (rid=94876) から geojson に変換し直して使用する
  (fetch スクリプト経由)。
"""
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("=== L23 DID地区境界 14 件 統合分析 ===", flush=True)

# =============================================================================
# 0. 定数: 14 DID 指定市町、6 DID 無指定市町、行政区域マッピング
# =============================================================================
DATA_DIR = ROOT / "data" / "extras" / "L23_did_boundary"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
LIP_DIR = ROOT / "data" / "extras" / "L19_residential_induction"
TARGET_CRS = "EPSG:6671"

# (DID dsid, 市町名, 行政_dsid, 都市タイプ, LIP dsid (0 = LIP 無し))
CITY_DEFS = [
    (1468, "広島市",     786, "政令市",         795),
    (1471, "呉市",       797, "中核市",         805),
    (1482, "福山市",     832, "中核市",         838),
    (1493, "東広島市",   868, "施行時特例市",   876),
    (1495, "廿日市市",   878, "市",             886),
    (1479, "尾道市",     824, "市",               0),
    (1476, "三原市",     814, "市",             822),
    (1473, "竹原市",     807, "市",             812),
    (1485, "府中市",     840, "市",             848),
    (1490, "大竹市",     862, "市",               0),
    (1487, "三次市",     850, "市",               0),
    (1500, "府中町",     900, "町",               0),
    (1502, "海田町",     905, "町",               0),
    (1505, "坂町",       916, "町",               0),
]

# DID 無指定 6 市町 (= L22 の 20 市町 - 14 DID 市町)
NO_DID_CITIES = [
    ("庄原市",       856, "市", "中山間"),
    ("安芸高田市",   888, "市", "中山間"),
    ("江田島市",     894, "市", "離島"),
    ("熊野町",       911, "町", "広島近郊町"),
    ("北広島町",     935, "町", "中山間"),
    ("世羅町",       941, "町", "中山間"),
]

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

# 市町タイプ別カラー (L22 と共通)
CTYPE_COLOR = {
    "政令市": "#cf222e", "中核市": "#bf3989",
    "施行時特例市": "#a371f7", "市": "#0969da", "町": "#1f883d",
}
DID_CITY_ORDER = [d[1] for d in CITY_DEFS]
ALL_CITY_ORDER = DID_CITY_ORDER + [n[0] for n in NO_DID_CITIES]


# =============================================================================
# 1. 14 GeoJSON 統合読み込み + 行政区域背景
# =============================================================================
print("\n[1] 14 DID 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.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()))


# 9 共通列 (全市町に存在)
COMMON_COLS_9 = ["TOKEI_CD", "CITY_CD", "KUIKI_CD", "ZU_NO",
                 "TOCHI_A", "JINKOU_S", "BIKOU", "RITTEKI_CD", "geometry"]

frames = []
load_log = []
for dsid, name, _adm, ctype, _lip in CITY_DEFS:
    z = DATA_DIR / f"did_{dsid}_{name}.zip"
    g = load_geojson_zip(z)
    extra = sorted(set(g.columns) - set(COMMON_COLS_9))
    g = g[COMMON_COLS_9].copy()
    g["src_city"] = name
    g["src_dsid"] = dsid
    g["ctype"] = ctype
    if g.crs is None:
        g = g.set_crs(TARGET_CRS, allow_override=True)
    load_log.append({
        "dsid": dsid, "city": name, "ctype": ctype,
        "n_did": len(g),
        "tochi_a_ha": int(g["TOCHI_A"].sum()),
        "jinkou_s": int(g["JINKOU_S"].sum()),
        "city_cds": ",".join(map(str, sorted(g["CITY_CD"].unique()))),
        "zu_no": ",".join(g["ZU_NO"].astype(str).tolist()),
        "extra_cols": ",".join(extra) if extra else "-",
    })
    frames.append(g)

did = gpd.GeoDataFrame(pd.concat(frames, ignore_index=True),
                       geometry="geometry", crs=frames[0].crs)
did = did.to_crs(TARGET_CRS)
N_DID = len(did)
JINKOU_TOTAL_DID = int(did["JINKOU_S"].sum())
TOCHI_TOTAL_HA = int(did["TOCHI_A"].sum())
print(f"  DID ポリゴン: {N_DID} 件 × {len(did.columns)} 列", flush=True)
print(f"  DID 人口総計: {JINKOU_TOTAL_DID:,} 人  (R2 国勢調査ベース)", flush=True)
print(f"  DID 面積総計: {TOCHI_TOTAL_HA:,} ha "
      f"= {TOCHI_TOTAL_HA/100:.1f} km²", flush=True)

# 行政区域 21 件 (= 20 市町 + 広島県全域) を読み込み、各市町を dissolve
admin_frames = []
for _dsid, name, adm_ds, _ct, _lip in CITY_DEFS:
    z = ADMIN_DIR / f"admin_{adm_ds}_{name}.zip"
    g = load_geojson_zip(z)
    g["city"] = name
    g["did_status"] = "DID指定"
    admin_frames.append(g)
for name, adm_ds, _ctype, _region in NO_DID_CITIES:
    z = ADMIN_DIR / f"admin_{adm_ds}_{name}.zip"
    g = load_geojson_zip(z)
    g["city"] = name
    g["did_status"] = "DID無指定"
    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={"did_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. 派生指標の計算 (DID 単位 + 市町単位)
# =============================================================================
print("\n[2] 派生指標計算", flush=True)
t1 = time.time()

# DID 単位: geometry から実面積、密度、コンパクトネス
did["geom_area_m2"] = did.geometry.area
did["geom_area_ha"] = did["geom_area_m2"] / 1e4
did["geom_area_km2"] = did["geom_area_m2"] / 1e6
# 人口密度 (人/km²)
did["density_per_km2"] = did["JINKOU_S"] / did["geom_area_km2"].clip(lower=1e-6)
# コンパクトネス (Polsby-Popper 指数) = 4πA / P²、円=1、線=0
did["geom_perimeter"] = did.geometry.length
did["compactness"] = (4 * np.pi * did["geom_area_m2"]
                      / did["geom_perimeter"].clip(lower=1) ** 2)
# 連続性: MultiPolygon の構成 polygon 数 (飛び地数)
did["n_parts"] = did.geometry.apply(
    lambda g: len(list(g.geoms)) if g.geom_type == "MultiPolygon" else 1)

# 市町単位
def aggregate_by_city(d: gpd.GeoDataFrame) -> pd.DataFrame:
    agg = d.groupby("src_city").agg(
        n_did=("ZU_NO", "size"),
        did_pop=("JINKOU_S", "sum"),
        did_area_ha=("TOCHI_A", "sum"),
        density_min=("density_per_km2", "min"),
        density_med=("density_per_km2", "median"),
        density_max=("density_per_km2", "max"),
        compact_med=("compactness", "median"),
        n_parts_total=("n_parts", "sum"),
    ).reset_index().rename(columns={"src_city": "city"})
    agg["did_area_km2"] = agg["did_area_ha"] / 100
    agg["did_density"] = (agg["did_pop"] / agg["did_area_km2"]).round(0)
    agg["city_area_km2"] = agg["city"].map(lambda c: CITY_REF[c]["area_km2"])
    agg["city_pop_k"] = agg["city"].map(lambda c: CITY_REF[c]["pop_k"])
    agg["did_share_area"] = (agg["did_area_km2"] / agg["city_area_km2"] * 100).round(2)
    agg["did_share_pop"] = (agg["did_pop"] / (agg["city_pop_k"] * 1000) * 100).round(1)
    agg["ctype"] = agg["city"].map(
        lambda c: next(d[3] for d in CITY_DEFS if d[1] == c))
    return agg


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

# 6 DID 無指定市町: 集計用データ
no_did_agg = pd.DataFrame([{
    "city": name,
    "ctype": ctype,
    "region": 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_DID_CITIES])

# 広島県全体での DID 集中度
HIROSHIMA_TOTAL_POP_K = 2780  # R2 国勢調査ベース 広島県約 278 万人
DID_SHARE_PREF = JINKOU_TOTAL_DID / (HIROSHIMA_TOTAL_POP_K * 1000) * 100
print(f"  広島県全体に占める DID 人口シェア: {DID_SHARE_PREF:.1f}%", flush=True)
# 14 DID 市町の合計人口
sum14_pop_k = sum(CITY_REF[d[1]]["pop_k"] for d in CITY_DEFS)
DID_SHARE_14CITIES = JINKOU_TOTAL_DID / (sum14_pop_k * 1000) * 100
print(f"  14 DID 市町合計に占める DID 人口シェア: {DID_SHARE_14CITIES:.1f}%",
      flush=True)
print(f"  ({time.time()-t1:.2f}s)", flush=True)

# =============================================================================
# 3. LIP (居住誘導区域) との空間結合 (DID ∩ LIP)
# =============================================================================
print("\n[3] LIP (L19 居住誘導区域) との空間結合", flush=True)
t1 = time.time()

LIP_DEFS = [(d[4], d[1]) for d in CITY_DEFS if d[4] != 0]  # 8 市町
lip_frames = []
for lip_dsid, name in LIP_DEFS:
    z = LIP_DIR / f"jukyo_{lip_dsid}_{name}.zip"
    g = load_geojson_zip(z)
    g["src_city"] = name
    g["lip_dsid"] = lip_dsid
    if g.crs is None:
        g = g.set_crs(TARGET_CRS, allow_override=True)
    lip_frames.append(g[["src_city", "lip_dsid", "KUIKI_CD",
                         "RITTEKI_CD", "geometry"]])
lip = gpd.GeoDataFrame(pd.concat(lip_frames, ignore_index=True),
                       geometry="geometry", crs=lip_frames[0].crs)
lip = lip.to_crs(TARGET_CRS)
# 市町別 LIP 面積
lip_by_city = lip.dissolve(by="src_city", as_index=False)
lip_by_city["lip_area_km2"] = lip_by_city.geometry.area / 1e6

# 市町別 DID dissolve
did_by_city = did.dissolve(by="src_city", as_index=False)
did_by_city["did_area_km2_g"] = did_by_city.geometry.area / 1e6

# DID ∩ LIP の重なり面積 (8 市町ごと)
overlap_rows = []
for _dsid, name, _adm, _ct, lip_dsid in CITY_DEFS:
    if lip_dsid == 0:
        continue
    d_city = did_by_city[did_by_city["src_city"] == name]
    l_city = lip_by_city[lip_by_city["src_city"] == name]
    if len(d_city) == 0 or len(l_city) == 0:
        continue
    inter = gpd.overlay(d_city[["src_city", "geometry"]],
                         l_city[["src_city", "geometry"]],
                         how="intersection", keep_geom_type=False)
    inter_area_km2 = float(inter.geometry.area.sum()) / 1e6
    d_area = float(d_city.geometry.area.sum()) / 1e6
    l_area = float(l_city.geometry.area.sum()) / 1e6
    overlap_rows.append({
        "city": name,
        "did_area_km2": round(d_area, 2),
        "lip_area_km2": round(l_area, 2),
        "did_n_lip_km2": round(inter_area_km2, 2),
        "lip_in_did_pct": round(inter_area_km2 / l_area * 100, 1)
                          if l_area > 0 else 0,
        "did_covered_by_lip_pct": round(inter_area_km2 / d_area * 100, 1)
                                  if d_area > 0 else 0,
    })
overlap_df = pd.DataFrame(overlap_rows)
print(f"  LIP 8 市町 ∩ DID 計算完了 ({time.time()-t1:.2f}s)", flush=True)

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

# 14 DID 市町集計
city_agg.to_csv(ASSETS / "L23_city_summary.csv",
                index=False, encoding="utf-8-sig")

# 6 DID 無指定市町
no_did_agg.to_csv(ASSETS / "L23_no_did_cities.csv",
                  index=False, encoding="utf-8-sig")

# DID 単位 (48 行) — geometry を除いた属性表
did_attr = did.drop(columns=["geometry"]).copy()
did_attr["geom_area_ha"] = did_attr["geom_area_ha"].round(1)
did_attr["geom_area_km2"] = did_attr["geom_area_km2"].round(3)
did_attr["density_per_km2"] = did_attr["density_per_km2"].round(0)
did_attr["compactness"] = did_attr["compactness"].round(3)
did_attr.to_csv(ASSETS / "L23_did_polygons.csv",
                index=False, encoding="utf-8-sig")

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

# LIP ∩ DID
overlap_df.to_csv(ASSETS / "L23_lip_overlap.csv",
                   index=False, encoding="utf-8-sig")

# 上位/下位 DID の表
top10_pop = did_attr.nlargest(10, "JINKOU_S")[
    ["src_city", "ZU_NO", "CITY_CD", "JINKOU_S", "TOCHI_A",
     "density_per_km2", "compactness", "n_parts"]]
top10_pop.to_csv(ASSETS / "L23_top10_did_pop.csv",
                  index=False, encoding="utf-8-sig")

top10_dens = did_attr.nlargest(10, "density_per_km2")[
    ["src_city", "ZU_NO", "CITY_CD", "JINKOU_S", "TOCHI_A",
     "density_per_km2", "compactness", "n_parts"]]
top10_dens.to_csv(ASSETS / "L23_top10_did_density.csv",
                   index=False, encoding="utf-8-sig")

bot10_dens = did_attr.nsmallest(10, "density_per_km2")[
    ["src_city", "ZU_NO", "CITY_CD", "JINKOU_S", "TOCHI_A",
     "density_per_km2", "compactness", "n_parts"]]
bot10_dens.to_csv(ASSETS / "L23_bot10_did_density.csv",
                   index=False, encoding="utf-8-sig")

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

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

# --- Fig 1: 県全域 DID 主題図 (48 polygon, 市町タイプ色) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
# DID 指定 14 市町は明るい色
admin_did = admin_diss[admin_diss["did_status"] == "DID指定"]
admin_did.plot(ax=ax, facecolor="#fafff0", edgecolor="#444", linewidth=0.5)
# DID ポリゴンを市町タイプ色で
ctype_count = {ct: 0 for ct in CTYPE_COLOR}
for ct, col in CTYPE_COLOR.items():
    sub = did[did["ctype"] == ct]
    if len(sub) > 0:
        sub.plot(ax=ax, facecolor=col, alpha=0.78, edgecolor="#222",
                  linewidth=0.4)
        ctype_count[ct] = len(sub)
# 市町境界を最上層に
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.4)
ax.set_aspect("equal")
ax.set_title(f"広島県 DID 地区境界 全 {N_DID} 件 — 14 DID 指定市町 "
              f"(R2国勢調査ベース, 2020)", fontsize=13)
legend_handles_f1 = [Patch(facecolor=v, label=f"{k} DID ({ctype_count[k]} 件)",
                             edgecolor="#222", alpha=0.78)
                       for k, v in CTYPE_COLOR.items() if ctype_count[k] > 0]
ax.legend(handles=legend_handles_f1, loc="lower left", fontsize=9, framealpha=0.92)
ax.set_xticks([]); ax.set_yticks([])
# 都市名ラベル (DID >= 3 のみ)
for _, r in city_agg.iterrows():
    if r["n_did"] >= 3 or r["did_pop"] >= 50_000:
        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_did']}DID/{r['did_pop']:,}人",
                         (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 / "L23_fig01_did_overview.png", dpi=130)
plt.close("all")
print(f"  Fig 1 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 2: DID 指定 14 vs 無指定 6 市町 マップ ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss[admin_diss["did_status"] == "DID指定"].plot(
    ax=ax, facecolor="#fee0b6", edgecolor="#444", linewidth=0.6)
admin_diss[admin_diss["did_status"] == "DID無指定"].plot(
    ax=ax, facecolor="#cfe9d8", edgecolor="#444", linewidth=0.6)
did.plot(ax=ax, facecolor="#cf222e", alpha=0.85, edgecolor="#600",
          linewidth=0.5)
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.5)
ax.set_aspect("equal")
ax.set_title("DID 指定の有無 — 14 市町 (オレンジ) vs 6 市町 (緑)",
              fontsize=13)
legend_handles_f2 = [
    Patch(facecolor="#fee0b6", edgecolor="#444", label="DID 指定 14 市町"),
    Patch(facecolor="#cfe9d8", edgecolor="#444", label="DID 無指定 6 市町"),
    Patch(facecolor="#cf222e", edgecolor="#600", alpha=0.85,
            label=f"DID ポリゴン ({N_DID} 件)"),
]
ax.legend(handles=legend_handles_f2, loc="lower left", fontsize=11, framealpha=0.92)
ax.set_xticks([]); ax.set_yticks([])
# 6 DID 無指定市町に名前
for name, _adm, _ct, region in NO_DID_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 / "L23_fig02_did_vs_no_did.png", dpi=130)
plt.close("all")
print(f"  Fig 2 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 3: DID 人口密度 choropleth (DID polygon 単位) ---
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
# 4,000 人/km² を切り出し色で強調
did_plot = did.copy()
did_plot["dens_clip"] = did_plot["density_per_km2"].clip(2000, 12000)
did_plot.plot(ax=ax, column="dens_clip", cmap="YlOrRd",
               vmin=2000, vmax=12000, alpha=0.85,
               edgecolor="#222", linewidth=0.4)
admin_diss.boundary.plot(ax=ax, color="#333", linewidth=0.4)
ax.set_aspect("equal")
sm = plt.cm.ScalarMappable(cmap="YlOrRd", norm=Normalize(2000, 12000))
cb = plt.colorbar(sm, ax=ax, fraction=0.03, pad=0.02)
cb.set_label("DID 人口密度 (人/km²)\n4000 人/km² が DID 認定基準")
cb.ax.axhline(4000, color="black", linewidth=1.2)
ax.set_title(f"DID 単位 人口密度 主題図 ({N_DID} polygon)",
              fontsize=13)
ax.set_xticks([]); ax.set_yticks([])
# 高密度 top3 にラベル
for _, r in did.nlargest(3, "density_per_km2").iterrows():
    ctr = r.geometry.representative_point()
    ax.annotate(f"{r['src_city']} {r['ZU_NO']}\n{r['density_per_km2']:.0f}人/km²",
                 (ctr.x, ctr.y), fontsize=8,
                 xytext=(8, 8), textcoords="offset points",
                 bbox=dict(boxstyle="round,pad=0.25", fc="white",
                            ec="#cf222e", alpha=0.92))
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig03_did_density.png", dpi=130)
plt.close("all")
print(f"  Fig 3 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 4: 市町別 DID small multiples (14 panels, 同一スケール) ---
# bbox の市域全体を各 panel で揃える (相対位置と絶対サイズが同時に伝わる)
fig, axes = plt.subplots(3, 5, figsize=(18, 12))
for ax_, (dsid, name, _adm, ctype, _lip) 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_did = did[did["src_city"] == name]
    sub_did.plot(ax=ax_, facecolor=CTYPE_COLOR[ctype],
                  alpha=0.85, edgecolor="#222", linewidth=0.4)
    cnt.boundary.plot(ax=ax_, color="#333", linewidth=0.4)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
    p = sub_did["JINKOU_S"].sum()
    a = sub_did["TOCHI_A"].sum() / 100  # km²
    n = len(sub_did)
    ax_.set_title(f"{name} ({ctype})\n"
                   f"{n}DID / {p:,}人 / {a:.1f}km²", fontsize=10)
# 残り 1 マスを 6 DID 無指定市町表示
ax_last = axes.flat[14]
admin_diss[admin_diss["did_status"] == "DID無指定"].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("DID 無指定 6 市町\n(参考)", fontsize=10)
for name, _adm, _ct, _r in NO_DID_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=7, ha="center")
plt.suptitle("市町別 DID 配置 small multiples (14 DID 指定市町 + 6 DID 無指定)",
              fontsize=14, y=1.00)
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig04_small_multiples.png", dpi=120)
plt.close("all")
print(f"  Fig 4 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 5: DID 面積 vs DID 人口 (log-log + 密度等値線) ---
fig, ax = plt.subplots(figsize=(11, 9))
for ct, col in CTYPE_COLOR.items():
    sub = did[did["ctype"] == ct]
    if len(sub) > 0:
        ax.scatter(sub["TOCHI_A"], sub["JINKOU_S"], c=col, s=80, alpha=0.78,
                    edgecolor="#222", linewidth=0.5,
                    label=f"{ct} ({len(sub)})")
# 4000 人/km² の DID 認定線 (人口=面積_ha × 4000/100 = 面積×40)
xs = np.array([20, 5000])
ax.plot(xs, xs * 40, color="black", linestyle="--", linewidth=1.5,
         label="DID 認定線 (4,000人/km²)")
# 6,000 人/km² 線
ax.plot(xs, xs * 60, color="#cf222e", linestyle=":", linewidth=1.5,
         label="6,000人/km² 線")
# 上位 5 DID にラベル
for _, r in did.nlargest(5, "JINKOU_S").iterrows():
    ax.annotate(f"{r['src_city']}{r['ZU_NO']}",
                 (r["TOCHI_A"], r["JINKOU_S"]), fontsize=9,
                 xytext=(5, 5), textcoords="offset points")
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlabel("DID 面積 (TOCHI_A, ha)")
ax.set_ylabel("DID 人口 (JINKOU_S, 人)")
ax.set_title(f"DID 面積 × 人口 (log-log, n={N_DID})",
              fontsize=13)
ax.legend(loc="upper left", fontsize=10)
ax.grid(True, which="both", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig05_area_pop_loglog.png", dpi=130)
plt.close("all")
print(f"  Fig 5 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 6: 市町別 DID 個数バー + DID 人口比 + 占有率 (3 連 panel) ---
fig, axes = plt.subplots(1, 3, figsize=(17, 8))
order_n = city_agg.sort_values("n_did", ascending=True)
ax0 = axes[0]
colors_n = [CTYPE_COLOR[ct] for ct in order_n["ctype"]]
ax0.barh(order_n["city"], order_n["n_did"], color=colors_n,
          edgecolor="white")
for i, v in enumerate(order_n["n_did"]):
    ax0.text(v + 0.2, i, str(v), va="center", fontsize=9)
ax0.set_xlabel("DID 個数")
ax0.set_title("市町別 DID 個数\n(都市の核がいくつあるか)", fontsize=12)

# 中: DID 人口 / 市総人口 (DID 集中率)
order_share = city_agg.sort_values("did_share_pop", ascending=True)
colors_s = [CTYPE_COLOR[ct] for ct in order_share["ctype"]]
ax1 = axes[1]
ax1.barh(order_share["city"], order_share["did_share_pop"],
          color=colors_s, edgecolor="white")
for i, v in enumerate(order_share["did_share_pop"]):
    ax1.text(v + 1, i, f"{v:.1f}%", va="center", fontsize=9)
ax1.set_xlabel("DID 人口 / 市総人口 (%)")
ax1.set_title("DID 集中率 — 市民の何割が DID に住むか", fontsize=12)
ax1.set_xlim(0, 105)

# 右: DID 面積 / 市総面積 (DID 面積占有率)
order_a = city_agg.sort_values("did_share_area", ascending=True)
colors_a = [CTYPE_COLOR[ct] for ct in order_a["ctype"]]
ax2 = axes[2]
ax2.barh(order_a["city"], order_a["did_share_area"],
          color=colors_a, edgecolor="white")
for i, v in enumerate(order_a["did_share_area"]):
    ax2.text(v + 0.5, i, f"{v:.1f}%", va="center", fontsize=9)
ax2.set_xlabel("DID 面積 / 市総面積 (%)")
ax2.set_title("DID 面積占有率 — 市域の何割が DID か", fontsize=12)
# タイプ凡例
handles = [Patch(facecolor=v, label=k, edgecolor="white")
           for k, v in CTYPE_COLOR.items()]
ax0.legend(handles=handles, loc="lower right", fontsize=8)
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig06_count_share.png", dpi=130)
plt.close("all")
print(f"  Fig 6 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 7: 市町総人口 (R2) vs DID 人口 (集中率の絶対比較) ---
fig, ax = plt.subplots(figsize=(12, 9))
# 14 DID 市町
for ct, col in CTYPE_COLOR.items():
    sub = city_agg[city_agg["ctype"] == ct]
    if len(sub) > 0:
        ax.scatter(sub["city_pop_k"] * 1000, sub["did_pop"], c=col, s=160,
                    alpha=0.85, edgecolor="#222", linewidth=0.5,
                    label=f"DID指定 {ct}")
        for _, r in sub.iterrows():
            ax.annotate(r["city"], (r["city_pop_k"] * 1000, r["did_pop"]),
                         fontsize=9, xytext=(6, 4),
                         textcoords="offset points")
# 6 DID 無指定市町: 横軸のみ (DID 人口 = 0)
for _, r in no_did_agg.iterrows():
    ax.scatter(r["city_pop_k"] * 1000, 0, marker="x", c="#666", s=120,
                linewidth=2)
    ax.annotate(r["city"] + "\n(DID 無)", (r["city_pop_k"] * 1000, 0),
                 fontsize=8, color="#444",
                 xytext=(0, -22), textcoords="offset points",
                 ha="center")
# 100% ライン (DID 人口 = 市総人口)
xs = np.linspace(10000, 1500000, 50)
ax.plot(xs, xs, color="black", linestyle="--", linewidth=1,
         label="100% 集中ライン")
ax.plot(xs, xs * 0.5, color="#888", linestyle=":", linewidth=1,
         label="50% 集中ライン")
ax.set_xscale("log")
ax.set_yscale("symlog", linthresh=10000)
ax.set_xlabel("市町総人口 (R2 国勢調査)")
ax.set_ylabel("DID 人口 (合計)")
ax.set_title("市町総人口 vs DID 人口 — DID 無 6 市町は ×印 (DID人口=0)",
              fontsize=12)
ax.legend(loc="lower right", fontsize=9)
ax.grid(True, which="both", alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig07_pop_vs_did_pop.png", dpi=130)
plt.close("all")
print(f"  Fig 7 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 8: LIP (居住誘導区域) との重ね合わせマップ ---
fig, axes = plt.subplots(2, 4, figsize=(18, 10))
LIP_CITIES_PLOT = [d[1] for d in CITY_DEFS if d[4] != 0]
for ax_, name in zip(axes.flat, LIP_CITIES_PLOT):
    cnt = admin_diss[admin_diss["city"] == name]
    cnt.plot(ax=ax_, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
    sub_did = did[did["src_city"] == name]
    sub_lip = lip[lip["src_city"] == name]
    sub_did.plot(ax=ax_, facecolor="#cf222e", alpha=0.55,
                  edgecolor="#600", linewidth=0.4)
    sub_lip.plot(ax=ax_, facecolor="#1f883d", alpha=0.55,
                  edgecolor="#0a5", linewidth=0.4)
    cnt.boundary.plot(ax=ax_, color="#333", linewidth=0.4)
    ax_.set_aspect("equal")
    ax_.set_xticks([]); ax_.set_yticks([])
    ov = overlap_df[overlap_df["city"] == name].iloc[0]
    ax_.set_title(f"{name}\n"
                   f"DID {ov['did_area_km2']:.1f} / LIP {ov['lip_area_km2']:.1f} / "
                   f"重 {ov['did_n_lip_km2']:.1f} km²",
                   fontsize=9)
    if ax_ is axes.flat[0]:
        legend_handles_f8 = [
            Patch(facecolor="#cf222e", alpha=0.55, edgecolor="#600", label="DID"),
            Patch(facecolor="#1f883d", alpha=0.55, edgecolor="#0a5",
                    label="LIP (居住誘導)"),
        ]
        ax_.legend(handles=legend_handles_f8, loc="lower right",
                    fontsize=8, framealpha=0.92)
plt.suptitle("LIP (居住誘導区域 L19) ∩ DID (本記事) — 8 LIP 策定市町の比較",
              fontsize=13, y=1.01)
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig08_lip_overlay.png", dpi=130)
plt.close("all")
print(f"  Fig 8 完成 ({time.time()-t1:.2f}s)", flush=True)

# --- Fig 9: DID 人口密度 + コンパクトネス 散布 ---
fig, ax = plt.subplots(figsize=(11, 9))
for ct, col in CTYPE_COLOR.items():
    sub = did[did["ctype"] == ct]
    if len(sub) > 0:
        ax.scatter(sub["density_per_km2"], sub["compactness"], c=col, s=80,
                    alpha=0.78, edgecolor="#222", linewidth=0.5,
                    label=f"{ct} ({len(sub)})")
ax.axvline(4000, color="black", linestyle="--", linewidth=1,
            label="DID 認定線 (4,000人/km²)")
# 上位ラベル
for _, r in did.nlargest(3, "compactness").iterrows():
    ax.annotate(f"{r['src_city']}{r['ZU_NO']}",
                 (r["density_per_km2"], r["compactness"]), fontsize=9)
for _, r in did.nsmallest(3, "compactness").iterrows():
    ax.annotate(f"{r['src_city']}{r['ZU_NO']} (細長)",
                 (r["density_per_km2"], r["compactness"]), fontsize=9,
                 color="#666")
ax.set_xlabel("DID 人口密度 (人/km²)")
ax.set_ylabel("コンパクトネス (Polsby-Popper, 1=円, 0=線)")
ax.set_title(f"DID 形状 × 密度 散布 (n={N_DID})", fontsize=13)
ax.legend(loc="upper right", fontsize=9)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L23_fig09_compactness.png", dpi=130)
plt.close("all")
print(f"  Fig 9 完成 ({time.time()-t1:.2f}s)", flush=True)

print(f"\n[5] 図 9 枚 完了  累計 {time.time()-t0:.2f}s", flush=True)

# =============================================================================
# 6. 仮説検証
# =============================================================================
print("\n[6] 仮説検証", flush=True)
t1 = time.time()

# H1: 広島市 DID 集中度
hiroshima_did_n = int(city_agg[city_agg["city"] == "広島市"]["n_did"].iloc[0])
hiroshima_did_pop = int(city_agg[city_agg["city"] == "広島市"]["did_pop"].iloc[0])
H1_judge = ("支持" if hiroshima_did_n / N_DID > 0.30
                       and hiroshima_did_pop / JINKOU_TOTAL_DID > 0.50
            else "部分支持")

# H2: DID 個数の分布
n_did_only_one = int((city_agg["n_did"] == 1).sum())
n_did_multi = int((city_agg["n_did"] >= 3).sum())
H2_judge = "支持" if n_did_only_one >= 5 and n_did_multi >= 3 else "部分支持"

# H3: 密度格差
dens_max = float(city_agg["did_density"].max())
dens_min = float(city_agg["did_density"].min())
dens_ratio = dens_max / max(dens_min, 1)
dens_max_city = city_agg.loc[city_agg["did_density"].idxmax(), "city"]
dens_min_city = city_agg.loc[city_agg["did_density"].idxmin(), "city"]
H3_judge = "支持" if dens_ratio >= 3.0 else "部分支持"

# H4: 占有率 < 5%
n_under_5 = int((city_agg["did_share_area"] < 5).sum())
n_over_30 = int((city_agg["did_share_area"] >= 30).sum())
H4_judge = "支持" if n_under_5 >= 8 and n_over_30 >= 2 else "部分支持"

# H5: LIP ⊆ DID (LIP の 50% 以上が DID 内)
lip_in_did_high = int((overlap_df["lip_in_did_pct"] >= 50).sum())
H5_judge = ("支持" if lip_in_did_high >= 6
            else "部分支持" if lip_in_did_high >= 4
            else "反証")

# H6: DID 無 6 市町は人口 3 万以下が 5/6
small_no_did = int((no_did_agg["city_pop_k"] <= 30).sum())
H6_judge = "支持" if small_no_did >= 5 else "部分支持"

H_RESULTS = {
    "H1": {
        "text": "広島市が DID 件数の 30% 超 + DID 人口の 50% 超を占める",
        "result": f"広島市 DID 数 {hiroshima_did_n}/{N_DID}={hiroshima_did_n/N_DID*100:.1f}%, "
                  f"DID 人口 {hiroshima_did_pop:,}/{JINKOU_TOTAL_DID:,}="
                  f"{hiroshima_did_pop/JINKOU_TOTAL_DID*100:.1f}%",
        "judge": H1_judge,
    },
    "H2": {
        "text": "DID 個数 1 つの市町が 5 件以上 + 3+ DID の市町も 3 件以上",
        "result": f"単核 DID (n=1) {n_did_only_one} 市町、"
                  f"多核 DID (n>=3) {n_did_multi} 市町",
        "judge": H2_judge,
    },
    "H3": {
        "text": "市町間 DID 平均人口密度の最大/最小 ≥ 3 倍",
        "result": f"max={dens_max_city} {dens_max:.0f}人/km², "
                  f"min={dens_min_city} {dens_min:.0f}人/km², "
                  f"ratio={dens_ratio:.2f}x",
        "judge": H3_judge,
    },
    "H4": {
        "text": "市町の DID 面積占有率 5% 未満が 8 市町以上 + 30% 超が 2 市町以上",
        "result": f"占有率 <5% は {n_under_5} 市町、"
                  f">=30% は {n_over_30} 市町",
        "judge": H4_judge,
    },
    "H5": {
        "text": "LIP の 50% 以上が DID 内 (LIP ⊆ DID)",
        "result": f"LIP_in_DID >= 50% は {lip_in_did_high}/8 市町, "
                  f"中央値 {overlap_df['lip_in_did_pct'].median():.1f}%",
        "judge": H5_judge,
    },
    "H6": {
        "text": "DID 無 6 市町のうち 5+ 件が人口 3 万以下",
        "result": f"人口 ≤3 万: {small_no_did}/6 件",
        "judge": H6_judge,
    },
}

(ASSETS / "L23_hypothesis_results.json").write_text(
    json.dumps({"results": H_RESULTS,
                "n_did_polygons": N_DID,
                "n_did_cities": len(CITY_DEFS),
                "n_no_did_cities": len(NO_DID_CITIES),
                "did_pop_total": JINKOU_TOTAL_DID,
                "did_area_total_ha": TOCHI_TOTAL_HA,
                "did_share_prefecture_pct": float(DID_SHARE_PREF),
                "did_share_14cities_pct": float(DID_SHARE_14CITIES),
                }, 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']}")

# =============================================================================
# 7. HTML 構築
# =============================================================================
print("\n[7] 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>「都市計画区域情報_DID地区境界データ」シリーズ 14 件</b>
({ds_links_html})
を縦結合し、<b>R2 国勢調査ベースで広島県内 14 DID 指定市町の
人口集中地区 (DID) 計 {N_DID} ポリゴン</b>から、
<b>広島県の都市部の地理構造</b> ── 全県 DID 配置・市町別 DID 個数と規模・人口密度・
DID 面積占有率・居住誘導区域 (LIP) との関係 ── を分析する研究記事である。
DID は<b>「都市そのもの」</b>を定義する国勢調査の中心概念であり、
DID の地理は<b>都市計画・コンパクトシティ政策の出発点</b>である。
全 20 市町中 6 市町は DID 無指定 ── そのコントラストも研究対象とする。</p>

<div class="note">
<b>研究の問い (RQ)</b>: 広島県内 14 DID 指定市町の DID は、
面積・人口・密度・連続性・市町間配分の観点でどのような地理構造を持ち、
県全域の<b>都市部 vs 非都市部</b>の地理はどう描けるか？
DID 指定の有無は何を物語るのか？
</div>

<h3>仮説 H1〜H6</h3>
<ul>
  <li><b>H1</b>: <b>広島市の DID 集中度が圧倒的</b>。県内 {N_DID} DID のうち
      {hiroshima_did_n} ({hiroshima_did_n/N_DID*100:.0f}%) が広島市、
      DID 人口 {JINKOU_TOTAL_DID:,} のうち
      {hiroshima_did_pop:,} ({hiroshima_did_pop/JINKOU_TOTAL_DID*100:.0f}%) が
      広島市の 8 区に集中。
      「広島市が広島県の都市部の中核」が定量的に確認される。</li>
  <li><b>H2</b>: DID 個数は<b>市町規模に強く相関</b>するが厳密比例ではない。
      政令市の広島市 (18 DID, 8 区) &gt; 中核市の福山市 (6) &gt; 呉市 (5) で、
      <b>「都市の核がいくつあるか」</b>を示す指標となる。
      8 つの市町は DID が <b>1 つだけ</b>で「単核都市」、
      広島・呉・福山・東広島・尾道・廿日市・大竹は「多核都市 (n≥2)」。</li>
  <li><b>H3</b>: DID 人口密度は<b>市町間で 3 倍以上の格差</b>がある。
      <b>府中町</b> (人口 5万強の小さな町に DID 1ヶ所) が県内最高密度
      ({dens_max:.0f}人/km²)、小規模町でも DID は超高密度。
      中山間市町の DID は密度が DID 認定線の 4,000 人/km² ぎりぎり。</li>
  <li><b>H4</b>: 14 DID 指定市町の<b>市域に占める DID 面積の割合</b>は
      典型的に <b>5% 未満</b>で、「都市と呼ばれる市町でも 95% 以上は非 DID」
      という地理的事実が見える。府中町・海田町・坂町の小規模ベッドタウンのみ
      DID 占有率が 30% を超える ── 「町全体がほぼ都市」。</li>
  <li><b>H5</b>: <b>居住誘導区域 (LIP, L19) との重なり</b>は意味のあるクロス。
      LIP 策定 8 市町の DID 内に LIP がほぼ完全に内包される
      (LIP_in_DID 中央値 ≥ 50%)。LIP は「DID を継承して、より縮約した
      将来の居住核」として設計されている、というコンパクトシティ政策の論理を
      実データで確認する。</li>
  <li><b>H6</b>: <b>DID 無指定 6 市町</b> (庄原市・安芸高田市・江田島市・
      熊野町・北広島町・世羅町) は<b>人口 3 万以下が 5/6 件</b>、
      平均人口密度が低い。「中山間 + 離島 + 広島近郊町」の 3 タイプに分類できる。</li>
</ul>

<h3>独自用語の定義 (本レッスン内のみ)</h3>
<ul>
  <li><b>DID (Densely Inhabited District, 人口集中地区)</b>:
      国勢調査の調査区 (約 1km² 程度のメッシュ) のうち
      <b>人口密度 4,000 人/km² 以上のものが連担し</b>、
      その全体の人口が 5,000 人以上となる地域。
      <b>「都市そのもの」を定義する国勢調査の中心概念</b>で、
      行政上の「市町指定」とは独立に決まる。
      総務省統計局が国勢調査ごとに告示する。</li>
  <li><b>連担</b>: 「連続して接している」の地理用語。
      DID 認定では <b>4,000 人/km² 超のメッシュが地続きで広がる</b> ことを指す。
      飛び地的に高密度メッシュがあっても DID にはならない。</li>
  <li><b>JINKOU_S</b>: 各 DID ポリゴンの<b>合計人口</b> (R2 国勢調査ベース)。
      DID は人口 5,000 人以上が認定基準のため、最低でも 5,000 を超える。</li>
  <li><b>TOCHI_A</b>: DID 面積、<b>単位 ha (ヘクタール)</b>。
      <code>geom_area_m² / 10,000 ≈ TOCHI_A</code> で約 1.0 倍の比率
      (本データで実測 0.998-1.000)。</li>
  <li><b>ZU_NO</b>: 市町内の DID 地区番号。
      <code>'a', 'b', 'c', ...</code> のアルファベット 1 文字。
      広島市は 8 区にまたがる 18 DID で <code>a-r</code>、福山市は 6 DID で <code>a-f</code>。</li>
  <li><b>CITY_CD</b>: 市区町村コード。
      広島市は <b>8 区を 101-108 で分割</b>するため、
      広島市の DID は CITY_CD={{101..108}} の 8 通り。
      他の市町は単一 CITY_CD。</li>
  <li><b>RITTEKI_CD</b>: 立地コード (1 / 2 の 2 値)。仕様書未公開のため参考扱い。</li>
  <li><b>DID 個数</b> (本記事独自集計): 1 市町に存在する DID polygon 数。
      <b>「都市の核がいくつあるか」</b>を表す指標。
      1 個=単核都市、3+ 個=多核都市 (本記事の独自分類)。</li>
  <li><b>DID 集中率</b> (本記事独自指標): <code>DID 人口 / 市総人口</code>。
      「市民の何割が DID に住んでいるか」。
      府中町 (1.0 ≈ 全員 DID) ↔ 三次市・三原市 (低い、町全体に住宅が散在)。</li>
  <li><b>DID 面積占有率</b> (本記事独自指標): <code>DID 面積 / 市総面積</code>。
      「市域の何割が DID か」。
      ベッドタウン町 (府中町・海田町・坂町) で >30%、
      広い中山間都市 (三次・三原) では 1% 程度。</li>
  <li><b>コンパクトネス (Polsby-Popper 指数)</b>:
      <code>4πA / P²</code>。完全な円なら 1.0、細長い形ほど 0 に近づく。
      DID の形状が「コンパクトな都市核」か「線的な集落」か判定する指標。</li>
  <li><b>多核都市 / 単核都市</b> (本記事独自分類):
      <b>多核 (multi-core)</b> = DID 個数 ≥ 2 (広島・呉・福山・東広島・尾道・廿日市・大竹)、
      <b>単核 (mono-core)</b> = DID 個数 = 1 (8 市町)。
      多核都市の DID は地形・歴史的に分離した複数の市街地に対応する。</li>
  <li><b>LIP (Location Inducement Plan, 居住誘導区域)</b>:
      L19 で扱う「立地適正化計画」内の居住誘導区域。
      <b>「将来この区域に人口を集約する」</b>と自治体が定めた区域。
      DID は<b>「現在の都市部」</b>、LIP は<b>「将来の都市部」</b>。</li>
</ul>

<h3>到達点</h3>
<ol>
  <li>14 ZIP の DID GeoJSON を 1 個の <code>GeoDataFrame</code>
      ({N_DID} polygon × 9 列 + 派生 5 列) に縦結合できる。
      内 1 件 (三次市 ds=1487) は GeoJSON リソースが他市町データを誤配布していたため、
      Shapefile リソースから geojson に変換する手順も学ぶ。</li>
  <li><b>{N_DID} polygon</b> から、市町単位の DID 個数・面積・人口・密度・占有率の
      集計を <code>groupby</code> で実装する。</li>
  <li>DID polygon を <code>geopandas.plot(column=...)</code> で
      <b>主題図 (choropleth)</b>化し、人口密度の地理分布を可視化する。</li>
  <li>14 市町 small multiples で<b>「市町ごとの DID 配置パターン」</b>を一目で比較。
      「核がいくつあって、市域のどこに位置するか」を学ぶ。</li>
  <li>居住誘導区域 (LIP) との空間的<b>オーバーレイ</b>で、
      コンパクトシティ政策の「現在 (DID) → 将来 (LIP)」の関係を実証する。</li>
  <li>DID 無指定 6 市町と DID 指定 14 市町を対比して、
      「都市指定の有無の地理的意味」を理解する。</li>
</ol>

<h3>本記事のスコープ宣言</h3>
<p>本記事は<b>DID 地区境界 14 件のみ</b>を主データとする研究記事である。
L19 居住誘導区域 8 件は<b>「現在の都市核 vs 将来の集約核」のコントラスト</b>を
描く参考データとして使用するが、L19 主導の居住誘導分析記事と合体させない
(要件 I 違反の水増し回避)。
L22 性別年齢別人口の町丁単位データとの<b>クロス分析 (DID 内 vs 外の高齢化率)</b>
は<b>発展課題</b>に留める。
14 DID 市町と 6 DID 無指定市町のコントラスト (人口・面積・地理タイプ) は
本データ + 行政区域で完結するため<b>分析に含める</b>。</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>"
    f"<td><a href='https://hiroshima-dobox.jp/datasets/{d[0]}' target='_blank'>"
    f"DoBoX</a></td>"
    f"<td>{next(L['n_did'] for L in load_log if L['city']==d[1]):,}</td>"
    f"<td>{next(L['jinkou_s'] for L in load_log if L['city']==d[1]):,}</td>"
    f"<td>{next(L['tochi_a_ha'] 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"</tr>"
    for d in CITY_DEFS
)
NO_DID_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_DID_CITIES
)

s2_html = f"""
<p>DID 地区境界 14 件はそれぞれ 1 市町分の <b>DID polygon GeoJSON (MultiPolygon)</b>
を ZIP で配布している。<b>列構造は 14 件で 100% 一致 (9 列)</b>。</p>

<h3>14 dataset_id 一覧 (DID 指定市町)</h3>
<table>
<tr><th>dataset_id</th><th>市町</th><th>市町タイプ</th><th>DoBoX</th>
    <th>DID数</th><th>DID人口</th><th>DID面積(ha)</th><th>CITY_CD</th></tr>
{DS_TABLE_ROWS}
</table>
<p class="tnote">合計: <b>{N_DID} DID polygon / DID 人口 {JINKOU_TOTAL_DID:,} 人 /
DID 面積 {TOCHI_TOTAL_HA:,} ha = {TOCHI_TOTAL_HA/100:.1f} km²</b>
(R2 国勢調査ベース)。広島県全域人口約 278 万人に対し、
DID 集中率は <b>{DID_SHARE_PREF:.1f}%</b> ─
県民の約 2/3 が DID 内に居住している。</p>

<h3>DID 無指定 6 市町 (参考)</h3>
<table>
<tr><th>市町</th><th>タイプ</th><th>地理タイプ</th>
    <th>面積 km²</th><th>人口 (千人)</th><th>密度 (人/km²)</th></tr>
{NO_DID_TABLE_ROWS}
</table>
<p class="tnote">DID 無指定の 6 市町は<b>「中山間」「離島」「広島近郊町」</b>の 3 タイプ。
人口密度は最大の熊野町 (683/km²) でも DID 認定線の 4,000 人/km² の 1/6 程度。</p>

<h3>列構造の詳細 (9 列)</h3>
<table>
<tr><th>列名</th><th>型</th><th>意味</th></tr>
<tr><td><code>TOKEI_CD</code></td><td>int32</td>
    <td>統計区分コード。本データ全件 1 (定数)</td></tr>
<tr><td><code>CITY_CD</code></td><td>int32</td>
    <td>市区町村コード。<b>広島市は 8 区を 101-108 で分割</b>するため、
        広島市内の DID は 8 通りの CITY_CD を持つ</td></tr>
<tr><td><code>KUIKI_CD</code></td><td>int32</td>
    <td>区域コード。本データ全件 1 (DID 区域識別、定数)。
        三次市の Shapefile 経由データのみ <code>3</code> となるが、
        意味解釈に大きな影響なし</td></tr>
<tr><td><code>ZU_NO</code></td><td>object</td>
    <td><b>DID 地区番号</b> ('a','b','c'... アルファベット)。
        市町内で連番。広島市は <code>a-r</code> の 18 文字、
        福山市は <code>a-f</code></td></tr>
<tr><td><code>TOCHI_A</code></td><td>int32</td>
    <td><b>DID 面積 (ha)</b>。単位はヘクタール。
        <code>geom.area / 10,000</code> とほぼ完全に一致 (実測 0.998-1.000 倍)</td></tr>
<tr><td><code>JINKOU_S</code></td><td>int32</td>
    <td><b>DID 人口</b>。R2 国勢調査ベース。
        DID 認定基準により最小 5,000 人以上が原則</td></tr>
<tr><td><code>BIKOU</code></td><td>object</td>
    <td>備考。本データでは全件 None</td></tr>
<tr><td><code>RITTEKI_CD</code></td><td>int32</td>
    <td>立地コード (1 / 2 の 2 値、仕様書未公開のため参考扱い)</td></tr>
<tr><td><code>geometry</code></td><td>MultiPolygon</td>
    <td><b>DID ポリゴン境界</b>。EPSG:6671 (JGD2011 平面直角第III系)。
        本記事の<b>主役データ</b>。市内に飛び地のある DID は MultiPolygon が複数 part</td></tr>
</table>

<div class="note"><b>データ品質ノート</b>:
ds=1487 (三次市) は GeoJSON リソースが <b>府中市 (ds=1485) のデータを誤配布</b>
していた (sha256 ハッシュ完全一致で検出)。
本記事では Shapefile リソース (rid=94876) から geojson に変換し直して使用。
fetch スクリプト経由で自動修復される。
このような<b>「公開データの実例的な品質問題」</b>は教材として正面から扱う。</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>14 件の ZIP は前項の表からそれぞれ DoBoX へリンク。
あるいは一括取得スクリプト:</p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 data\\extras\\L23_did_boundary\\fetch_did_boundary.py</code></pre>
<p>合計サイズ約 1.7 MB。監査時に取得済の 3 市町 (広島市・呉市・福山市) は
<code>data/extras/_urban_planning_audit/</code> から自動コピー、
残り 11 市町は DoBoX から HTTP 取得 (約 10 秒)。
三次市 (ds=1487) のみ Shapefile から GeoJSON への変換手順を含む。</p>

<h3>(2) 中間 CSV (本スクリプトの出力)</h3>
<ul>
  <li><a href="assets/L23_city_summary.csv" download>L23_city_summary.csv</a> — 14 DID 市町サマリ (n_did, did_pop, did_area, density, share_pop, share_area 等 12 指標)</li>
  <li><a href="assets/L23_no_did_cities.csv" download>L23_no_did_cities.csv</a> — DID 無指定 6 市町の一覧 (面積・人口・地理タイプ)</li>
  <li><a href="assets/L23_did_polygons.csv" download>L23_did_polygons.csv</a> — DID polygon 単位 ({N_DID} 行) 全属性 + 派生 (geom_area, density, compactness, n_parts)</li>
  <li><a href="assets/L23_load_log.csv" download>L23_load_log.csv</a> — 14 件読込ログ (件数・人口・面積・CITY_CD・ZU_NO 列挙)</li>
  <li><a href="assets/L23_lip_overlap.csv" download>L23_lip_overlap.csv</a> — DID ∩ LIP 重ね合わせ (8 LIP 策定市町、面積・包含率)</li>
  <li><a href="assets/L23_top10_did_pop.csv" download>L23_top10_did_pop.csv</a> — DID 人口 top 10 polygon</li>
  <li><a href="assets/L23_top10_did_density.csv" download>L23_top10_did_density.csv</a> — DID 人口密度 top 10 polygon</li>
  <li><a href="assets/L23_bot10_did_density.csv" download>L23_bot10_did_density.csv</a> — DID 人口密度 bot 10 polygon</li>
  <li><a href="assets/L23_hypothesis_results.json" download>L23_hypothesis_results.json</a> — 仮説 H1〜H6 検証結果 (JSON)</li>
</ul>

<h3>(3) 図 PNG (本記事掲載 9 枚)</h3>
<ul>
  <li><a href="assets/L23_fig01_did_overview.png" download>L23_fig01_did_overview.png</a> — 県全域 DID 主題図 (48 polygon)</li>
  <li><a href="assets/L23_fig02_did_vs_no_did.png" download>L23_fig02_did_vs_no_did.png</a> — DID 指定 vs 無指定 マップ</li>
  <li><a href="assets/L23_fig03_did_density.png" download>L23_fig03_did_density.png</a> — DID 人口密度 主題図</li>
  <li><a href="assets/L23_fig04_small_multiples.png" download>L23_fig04_small_multiples.png</a> — 市町別 DID 14 panels + 無指定 panel</li>
  <li><a href="assets/L23_fig05_area_pop_loglog.png" download>L23_fig05_area_pop_loglog.png</a> — 面積×人口 log-log + 認定線</li>
  <li><a href="assets/L23_fig06_count_share.png" download>L23_fig06_count_share.png</a> — DID 個数 + 集中率 + 占有率 3連</li>
  <li><a href="assets/L23_fig07_pop_vs_did_pop.png" download>L23_fig07_pop_vs_did_pop.png</a> — 市総人口 vs DID 人口 (DID 無 6 市町併置)</li>
  <li><a href="assets/L23_fig08_lip_overlay.png" download>L23_fig08_lip_overlay.png</a> — LIP ∩ DID 8 panels</li>
  <li><a href="assets/L23_fig09_compactness.png" download>L23_fig09_compactness.png</a> — DID 形状コンパクトネス × 密度</li>
</ul>

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

# === セクション4: 分析1 — 統合読み込み + 行政区域連結 ===
code_load = code('''
DATA_DIR  = ROOT / "data" / "extras" / "L23_did_boundary"
ADMIN_DIR = ROOT / "data" / "extras" / "L15_admin_zones"
TARGET_CRS = "EPSG:6671"

# 14 件は 9 列 100% 共通
COMMON_COLS_9 = ["TOKEI_CD", "CITY_CD", "KUIKI_CD", "ZU_NO",
                 "TOCHI_A", "JINKOU_S", "BIKOU", "RITTEKI_CD", "geometry"]

frames = []
for dsid, name, _adm, ctype, _lip in CITY_DEFS:
    z = DATA_DIR / f"did_{dsid}_{name}.zip"
    g = load_geojson_zip(z)             # ZIP 内の単一 .geojson 読込
    g = g[COMMON_COLS_9].copy()         # 列正規化 (9 列)
    g["src_city"] = name
    g["src_dsid"] = dsid
    g["ctype"] = ctype
    frames.append(g)

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

# 派生列
did["geom_area_m2"]    = did.geometry.area
did["geom_area_ha"]    = did["geom_area_m2"] / 1e4
did["geom_area_km2"]   = did["geom_area_m2"] / 1e6
did["density_per_km2"] = did["JINKOU_S"] / did["geom_area_km2"].clip(lower=1e-6)
# Polsby-Popper コンパクトネス (1=円, 0=線)
did["compactness"]     = (4 * np.pi * did["geom_area_m2"]
                          / did.geometry.length.clip(lower=1) ** 2)
# MultiPolygon の part 数 (飛び地数)
did["n_parts"]         = did.geometry.apply(
    lambda g: len(list(g.geoms)) if g.geom_type == "MultiPolygon" else 1)
''')

before_after_s4 = """
<h3>入出力 Before/After (具体例: 1 DID polygon が派生指標に変換される)</h3>
<table>
<tr><th>段階</th><th>列</th><th>このデータで何が起きるか</th><th>1 行の値の例 (広島市 ZU_NO=a)</th></tr>
<tr><td>入力</td><td><code>JINKOU_S</code></td>
    <td>DID 内人口 (国勢調査の生データ)</td>
    <td><code>203,202 (人)</code></td></tr>
<tr><td>入力</td><td><code>TOCHI_A</code></td>
    <td>DID 面積 (ha 単位)</td>
    <td><code>2,507 (ha) = 25.07 km²</code></td></tr>
<tr><td>入力</td><td><code>geometry</code></td>
    <td>DID 境界 MultiPolygon (m 単位、EPSG:6671)</td>
    <td><code>POLYGON ((...300 頂点...))</code></td></tr>
<tr><td>派生</td><td><code>geom_area_km2 = geom.area / 1e6</code></td>
    <td>geometry 由来の正確な面積 (km²)</td>
    <td><code>25.064 km² (TOCHI_A の 0.999 倍 ─ ほぼ一致)</code></td></tr>
<tr><td>派生</td><td><code>density_per_km2 = JINKOU_S / geom_area_km2</code></td>
    <td>DID 内の人口密度 (人/km²)</td>
    <td><code>8,107 人/km² (DID 認定線 4,000 の 2 倍)</code></td></tr>
<tr><td>派生</td><td><code>compactness = 4πA/P²</code></td>
    <td>形状の円形度 (1=円, 0=線)</td>
    <td><code>0.382 (細長い都市軸を反映)</code></td></tr>
<tr><td>派生</td><td><code>n_parts</code></td>
    <td>MultiPolygon の構成 polygon 数 (飛び地数)</td>
    <td><code>3 (川や山で分断された 3 つの部分)</code></td></tr>
</table>
<p>このように、<b>9 列の生データ</b>から
<b>密度・形状・連続性</b>の派生指標を導出することで、
DID の地理構造を多角的に評価できる。
特に <b>density と compactness の組み合わせ</b>は
「コンパクトな高密度都市核」と「線的な集落」を区別する強力な指標。</p>
"""

n_loaded = len(load_log)
n_extra_cols_city = sum(1 for L in load_log if L["extra_cols"] != "-")

s4_html = (
    "<h3>狙い</h3>"
    f"<p>14 ZIP の DID GeoJSON を 1 個の <code>GeoDataFrame</code> "
    f"({N_DID} polygon × 9 列 + 派生 5 列) に統合し、"
    f"後段の市町別集計・主題図・LIP オーバーレイの基盤データを作る。"
    f"行政区域 21 件 (DID 14 + 無指定 6 + 広島県全域 1) を背景レイヤーとして同時に読み込む。</p>"

    "<h3>手法</h3>"
    "<p><b>直感</b>: ZIP を読む → 列を共通 9 列に揃える → 縦結合 → CRS 変換 → "
    "派生指標 (面積/密度/コンパクトネス/飛び地数) を計算 → 行政区域 dissolve で背景レイヤー作成。</p>"

    "<p><b>大筋 (5 ステップ)</b></p>"
    "<ol>"
    "<li>14 市町について <code>load_geojson_zip()</code> で GeoDataFrame を読む</li>"
    "<li>共通 9 列に正規化 (本データは追加列無し、L22 の Shape_Area/Shape_Leng のような付加列も無い)</li>"
    "<li>派生列 <code>src_city / src_dsid / ctype</code> を付与</li>"
    "<li><code>pd.concat</code> で縦結合 → "
    f"{N_DID} 行 1 個の GeoDataFrame</li>"
    "<li>派生指標 5 つを計算: <code>geom_area_km2, density_per_km2, "
    "compactness (Polsby-Popper), n_parts (飛び地数), src_city</code></li>"
    "<li>行政区域 21 件を読込 (DID 指定 14 + 無指定 6 + 広島県全域)、"
    "<code>dissolve(by='city')</code> で市町単位の境界を作成</li>"
    "</ol>"

    "<p><b>前提と限界</b>: 14 件の DID データは追加列が全く無く、L22 の庄原市のような"
    "付加列ケースは無い。三次市 (ds=1487) の GeoJSON リソースは"
    "府中市 (ds=1485) のデータを誤配布していた事例があり、"
    "<b>fetch スクリプトで Shapefile から geojson 変換</b>する追加処理を入れた。"
    "コンパクトネス指標 (Polsby-Popper) は<b>境界線の凹凸</b>に敏感で、"
    "実距離で精密に面積測定された MultiPolygon でも 0.3-0.5 程度の値が出る "
    "(完全な円形を期待しない)。</p>"

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

    f"<h3>結果</h3>"
    f"<p>14 ZIP のうち、<b>{n_loaded} 件すべてが共通 9 列で読み込み成功</b>。"
    f"統合後の <code>did</code> GeoDataFrame は <b>{N_DID} polygon × 14 列</b>"
    f"(9 共通 + src_city + src_dsid + ctype + 派生 5)。"
    f"処理時間は <b>{time.time()-t0:.1f} 秒</b>で要件 S を満たす。</p>"

    f"<h3>表 1 — 14 DID 市町読込ログ</h3>"
    "<table><tr><th>市町</th><th>タイプ</th><th>DID数</th>"
    "<th>DID人口</th><th>DID面積(ha)</th><th>CITY_CD群</th>"
    "<th>ZU_NO</th></tr>"
    + "".join(f"<tr><td>{L['city']}</td><td>{L['ctype']}</td>"
              f"<td>{L['n_did']}</td><td>{L['jinkou_s']:,}</td>"
              f"<td>{L['tochi_a_ha']:,}</td>"
              f"<td>{L['city_cds']}</td>"
              f"<td>{L['zu_no']}</td></tr>"
              for L in load_log) +
    "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>広島市 18 DID</b>が圧倒的に多く、CITY_CD={{101..108}} の 8 区にまたがる。"
    f"次が福山市 6, 呉市 5, 東広島市 4, 尾道市 3, 廿日市市 3 と続く。"
    f"<b>8 市町は DID が 1 つのみ</b>で、ZU_NO は <code>'a'</code> 単独。"
    f"単核都市 vs 多核都市の構造の違いが ZU_NO のリストから直視できる。"
    f"広島市の <code>a-r</code> という 18 文字は、地理的にも「都市の核が 18 ある」"
    f"(8 区 × 平均 2-3 DID) ことを示している。</p>"
)
sections.append(("4. 分析1: 14 DID GeoJSON 統合 + 派生指標", s4_html))

# === セクション5: 分析2 — 県全域 DID 主題図 + DID 指定 vs 無指定 ===
s5_html = (
    "<h3>狙い</h3>"
    "<p>広島県内の DID 全 48 polygon の地理的配置を 1 枚で見せる。"
    "<b>「都市部はどこで、非都市部はどこか」</b>を地図で直接視覚化することは、"
    "DID 概念の本質を理解する最短経路。"
    "DID 指定 14 市町 vs 無指定 6 市町を色分けで対比する 2 枚目で、"
    "<b>「都市指定の有無」</b>の地理的意味も同時に伝える。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) 県全域 DID 主題図</b>: 全 21 市町 (= 14 DID + 6 無指定 + 広島県全域)"
    "の行政区域を背景にし、"
    "<code>geopandas.plot(facecolor=ctype_color, alpha=0.78)</code> で DID polygon を"
    "市町タイプ色 (政令市赤・中核市紫・施行時特例市紫・市青・町緑) で描画。"
    "凡例とラベル ('広島市 18DID/103 万人' のような注記) で要約する。</p>"

    "<p><b>(b) DID 指定 vs 無指定 マップ</b>: 14 DID 市町をオレンジ、"
    "6 無指定市町を緑で塗り、DID polygon を赤で重ねる。"
    "「無指定市町には DID 赤が乗らない」を一目で見せる。</p>"

    "<h3>実装</h3>"
    + code('''
# (a) 県全域 DID 主題図
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5", edgecolor="#888", linewidth=0.4)
admin_did = admin_diss[admin_diss["did_status"] == "DID指定"]
admin_did.plot(ax=ax, facecolor="#fafff0", edgecolor="#444", linewidth=0.5)
for ct, col in CTYPE_COLOR.items():
    sub = did[did["ctype"] == ct]
    sub.plot(ax=ax, facecolor=col, alpha=0.78, edgecolor="#222",
             linewidth=0.4, label=f"{ct} DID ({len(sub)} 件)")

# (b) DID 指定 vs 無指定
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss[admin_diss["did_status"] == "DID指定"].plot(
    ax=ax, facecolor="#fee0b6", edgecolor="#444", linewidth=0.6)
admin_diss[admin_diss["did_status"] == "DID無指定"].plot(
    ax=ax, facecolor="#cfe9d8", edgecolor="#444", linewidth=0.6)
did.plot(ax=ax, facecolor="#cf222e", alpha=0.85, edgecolor="#600",
         linewidth=0.5)
''') +

    "<h3>図 1 — 県全域 DID 主題図 (48 polygon, 市町タイプ色)</h3>"
    "<p><b>なぜこの図か</b>: テーブルや棒グラフでは"
    "<b>「DID が県のどこに、どう連なっているか」</b>が分からない。"
    "地図 1 枚で「沿岸部 (広島・呉・福山) に DID が集中、中山間 (北・東部) に DID が無い」"
    "という県全体の地理構造を一気に伝える。</p>"
    + figure("assets/L23_fig01_did_overview.png",
              f"広島県 DID 地区境界 全 {N_DID} polygon (R2国勢調査)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>広島市の DID 帯</b>が県中央部にひときわ大きく赤く広がる (8 区にまたがる 18 polygon)。"
    f"瀬戸内海沿岸の地形に沿って東西に伸びる<b>「広島デルタ + 山際の市街地」</b>形状。</li>"
    f"<li><b>福山市</b>が県東端に紫色の塊として独立 (6 polygon)。"
    f"芦田川流域に沿った帯状の DID 配置で、市の南東部に集中。</li>"
    f"<li><b>呉市</b>は瀬戸内に飛び石状に 5 polygon ─ "
    f"造船業ベースの分散市街地、島しょ部にも DID あり。</li>"
    f"<li>広島市周辺の<b>町部 (府中町・海田町・坂町)</b> は緑の小さな点 ─ "
    f"政令市の通勤圏内に取り込まれた小さな DID。</li>"
    f"<li><b>北部・東部 (中山間)</b> には DID がほぼ無い ─ "
    f"庄原・北広島・安芸高田・世羅町は DID 無指定エリア。</li>"
    f"<li>三次市 (中山間) は人口 5 万あるが DID は 1 つだけ "
    f"(三次盆地の中心部)、市域の大部分は山林。</li>"
    "</ul>"

    "<h3>図 2 — DID 指定 vs 無指定 14 市町 vs 6 市町</h3>"
    "<p><b>なぜこの図か</b>: <b>「都市指定の地理境界」</b>を一目で見せる。"
    "DID 無指定 6 市町は<b>『中山間 + 離島 + 広島近郊町』</b>の 3 タイプに明確に分類される ─ "
    "これは地図でないと伝わらない (テーブルでは抽象的)。</p>"
    + figure("assets/L23_fig02_did_vs_no_did.png",
              "DID 指定 14 市町 (オレンジ) vs DID 無指定 6 市町 (緑)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>緑 (DID 無指定) は県境 (北部) と離島 (江田島) と広島近郊 1 町 (熊野) に分類される</b>。"
    f"県内 5 つの中山間町 (庄原・北広島・安芸高田・世羅 + 熊野市部分) と"
    f"離島の江田島市 + 広島近郊町の熊野町。</li>"
    f"<li><b>オレンジ (DID 指定) は瀬戸内海沿岸ベルト + 東広島盆地 + 三次盆地</b>。"
    f"地形と都市の関係が直接見える。</li>"
    f"<li>DID polygon (赤) は DID 指定市町の中央部にまとまる ─ "
    f"<b>市域全体の都市化ではなく、市域内の特定の都市核</b>が DID。</li>"
    f"<li>三次市 (山間) は DID 指定だが市域の大半は山林 ─ "
    f"<b>「DID 指定」は市町の地理特性ではなく『市内に都市部がある』ことを示す</b>。</li>"
    f"<li><b>熊野町 (DID 無指定の広島近郊町)</b> はやや特殊で、"
    f"人口 23,000 だが DID 認定基準を満たすほど集約していない (集落が分散)。"
    f"DID 指定の境界が「広島近郊」だけでは決まらないことを示唆。</li>"
    "</ul>"

    "<h3>表 2 — 14 DID 指定市町 vs 6 DID 無指定市町 サマリ</h3>"
    "<table>"
    "<tr><th>区分</th><th>市町数</th><th>合計面積 km²</th>"
    "<th>合計人口 千人</th><th>DID 数</th><th>DID 人口 千人</th>"
    "<th>DID 集中率</th></tr>"
    f"<tr><td><b>DID 指定 14</b></td><td>14</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_DID}</td>"
    f"<td>{JINKOU_TOTAL_DID//1000:,}</td>"
    f"<td>{DID_SHARE_14CITIES:.1f}%</td></tr>"
    f"<tr><td><b>DID 無指定 6</b></td><td>6</td>"
    f"<td>{sum(CITY_REF[n[0]]['area_km2'] for n in NO_DID_CITIES):,.0f}</td>"
    f"<td>{sum(CITY_REF[n[0]]['pop_k'] for n in NO_DID_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_DID}</td>"
    f"<td>{JINKOU_TOTAL_DID//1000:,}</td>"
    f"<td>{JINKOU_TOTAL_DID/(sum(CITY_REF[c]['pop_k'] for c in CITY_REF)*1000)*100:.1f}%</td></tr>"
    "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"DID 指定 14 市町は<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>人口では {sum(CITY_REF[d[1]]['pop_k'] for d in CITY_DEFS)/sum(CITY_REF[c]['pop_k'] for c in CITY_REF)*100:.1f}%</b>を占める ─ "
    f"DID 無指定 6 市町は<b>面積で {sum(CITY_REF[n[0]]['area_km2'] for n in NO_DID_CITIES)/sum(CITY_REF[c]['area_km2'] for c in CITY_REF)*100:.1f}%</b>あるのに"
    f"人口は <b>{sum(CITY_REF[n[0]]['pop_k'] for n in NO_DID_CITIES)/sum(CITY_REF[c]['pop_k'] for c in CITY_REF)*100:.1f}%</b>。"
    f"県全域の DID 集中率は <b>{DID_SHARE_PREF:.1f}%</b> ─ 県民の約 2/3 が DID 内に居住。"
    f"<b>「14 市町の都市部に県人口の大半が集中する」</b>のが広島県の地理構造。</p>"
)
sections.append(("5. 分析2: 県全域 DID 主題図 + DID 指定 vs 無指定 (H1, H6)", s5_html))

# === セクション6: 分析3 — 市町別 small multiples + 個数バー ===
city_table_rows = "".join(
    f"<tr><td>{r['city']}</td><td>{r['ctype']}</td>"
    f"<td>{r['n_did']}</td>"
    f"<td>{r['did_pop']:,}</td>"
    f"<td>{r['did_area_km2']:.1f}</td>"
    f"<td>{r['did_density']:,.0f}</td>"
    f"<td>{r['did_share_pop']:.1f}%</td>"
    f"<td>{r['did_share_area']:.2f}%</td></tr>"
    for _, r in city_agg.sort_values("n_did", ascending=False).iterrows()
)

s6_html = (
    "<h3>狙い</h3>"
    "<p>14 市町の DID 配置パターンを <b>14 panels small multiples</b> で並列比較する。"
    "市町ごとの<b>「都市の核がいくつあって、市域のどこに位置するか」</b>を一目で見せ、"
    "単核都市 (n=1) と多核都市 (n≥2) の質的違いを地図で示す。</p>"

    "<h3>手法</h3>"
    "<p><code>plt.subplots(3, 5)</code> で 15 panel を作り、14 市町の DID をそれぞれ"
    "<b>市町タイプ色</b>で描画。最後の 1 panel は DID 無指定 6 市町の参考表示。"
    "各 panel のタイトルに DID 数 / 人口 / 面積を併記。"
    "panel ごとに bbox は市域に揃える (= 各市町の市域全体を表示) ので"
    "<b>市域に対する DID の相対的な大きさ</b>が伝わる。</p>"

    "<h3>実装</h3>"
    + code('''
fig, axes = plt.subplots(3, 5, figsize=(18, 12))
for ax_, (dsid, name, _adm, ctype, _lip) in zip(axes.flat, CITY_DEFS):
    cnt = admin_diss[admin_diss["city"] == name]
    cnt.plot(ax=ax_, facecolor="#f5f5f5", edgecolor="#888")
    sub_did = did[did["src_city"] == name]
    sub_did.plot(ax=ax_, facecolor=CTYPE_COLOR[ctype], alpha=0.85)
    cnt.boundary.plot(ax=ax_, color="#333")
    p, a, n = sub_did["JINKOU_S"].sum(), sub_did["TOCHI_A"].sum()/100, len(sub_did)
    ax_.set_title(f"{name} ({ctype}) {n}DID / {p:,}人 / {a:.1f}km²")

# 個数 bar
order_n = city_agg.sort_values("n_did", ascending=True)
ax.barh(order_n["city"], order_n["n_did"],
        color=[CTYPE_COLOR[ct] for ct in order_n["ctype"]])
''') +

    "<h3>図 3 — 市町別 DID 配置 14 panels small multiples</h3>"
    "<p><b>なぜこの図か</b>: 1 枚の県全域図では、市町ごとの<b>「DID パターンの質的違い」</b>"
    "が分からない。14 panels で並列比較すると、"
    "「広島市の 18 DID 帯」と「府中町の 1 DID で町域ほぼ全部」が"
    "同じスケールで対比でき、<b>都市規模と DID 配置の関係</b>が直感的に伝わる。</p>"
    + figure("assets/L23_fig04_small_multiples.png",
              "市町別 DID 配置 small multiples (14 DID panel + 6 DID 無指定 panel)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>広島市</b> (18 DID): 市域の南半分 (8 区の中心部) が DID で連なる。"
    "デルタ地帯 + 各区中心部 + 山際の住宅地。<b>多核都市の最大例</b>。</li>"
    "<li><b>福山市</b> (6 DID): 市の南東部 (旧福山城下町 + 駅周辺) に集中、"
    "東部の他の集落も DID 化。芦田川河口部に都市核が固まる。</li>"
    "<li><b>呉市</b> (5 DID): 沿岸部に飛び石状に 5 つ。"
    "本土側 + 旧倉橋町 (島嶼部) で、地形分断による多核構造。</li>"
    "<li><b>東広島市</b> (4 DID): 西条 (大学・産業)、八本松、黒瀬、安芸津 ─ "
    "合併後の旧町中心が個別に DID 化。<b>合併由来の多核</b>。</li>"
    "<li><b>尾道市・廿日市市</b> (各 3 DID): 沿岸の細長い地形で帯状に分散。</li>"
    "<li><b>大竹市</b> (2 DID): 工業都市の本体 + 飛び地の小集落。</li>"
    "<li><b>単核 8 市町</b> (竹原・三原・府中・三次・府中町・海田町・坂町): "
    "1 つの DID が町の大部分を占める ─ <b>「町=DID」の構造</b>。"
    "府中町・海田町・坂町は町域ほぼ全体が 1 DID で塗られる。</li>"
    "<li><b>三次市</b> (中山間都市): 市域の大半は山林、"
    "中央の三次盆地の小さな赤点が唯一の DID。<b>都市と非都市の同居</b>。</li>"
    "<li><b>右下 panel (DID 無指定 6 市町)</b>: 庄原・北広島・安芸高田・世羅は"
    "県北の山間町、江田島は離島、熊野は広島近郊だが集落分散型。"
    "「DID 認定基準を満たさなかった」共通点が地形/分散度から見える。</li>"
    "</ul>"

    "<h3>図 4 — 市町別 DID 個数 + DID 集中率 + DID 面積占有率</h3>"
    "<p><b>なぜこの図か</b>: 市町を 3 つの異なる指標 ─ 「DID がいくつあるか」"
    "「市民の何割が DID に住むか」「市域の何割が DID か」 ─ で並べると、"
    "<b>大都市と小都市の構造の違い</b>が立体的に見える。"
    "1 つの指標だけでは見えない<b>『都市らしさ』の多軸性</b>を伝える。</p>"
    + figure("assets/L23_fig06_count_share.png",
              "市町別 DID 個数 / 集中率 / 面積占有率 (3 連 panel)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>左 (DID 個数)</b>: 広島市 18 が他を圧倒。福山市 6, 呉市 5 の中核市の後、"
    f"<b>8 市町が DID 1 つだけ</b>(単核)。多核都市は 6 市町のみ。</li>"
    f"<li><b>中央 (DID 集中率)</b>: 府中町 96%, 海田町 92%, 坂町 77% ─ "
    f"<b>「町ぐるみ DID」</b>の小都市。"
    f"逆に三次市・三原市・廿日市市・東広島市は <b>30-50%</b> ─ "
    f"<b>「町外居住者が多い拡散型市町」</b>。</li>"
    f"<li><b>右 (DID 面積占有率)</b>: 府中町 56%, 海田町 35%, 坂町 17% が上位 ─ "
    f"小規模町は<b>市域の 30%+ が DID</b>。"
    f"政令市・中核市でも 15% 以下 (広島市 14.6%) ─ "
    f"<b>「都市の中身は思ったより少ない」</b>。"
    f"中山間都市 (三次市・三原市) は 1-2% で、市域の大半は非都市。</li>"
    "<li>3 指標を合わせて見ると、市町は次の 4 タイプに分類できる: "
    "<b>(I) 多核大都市</b> (n=18, 集中 80%, 占有 14%) = 広島市、"
    "<b>(II) 多核中核市</b> (n=5-6, 集中 60-70%, 占有 8-12%) = 呉・福山、"
    "<b>(III) 単核小都市</b> (n=1, 集中 90%+, 占有 30%+) = 府中町・海田町・坂町、"
    "<b>(IV) 拡散市</b> (n=1-3, 集中 30-50%, 占有 1-5%) = 三次市・三原市・廿日市市など。</li>"
    "</ul>"

    "<h3>表 3 — 14 DID 市町 集計表 (個数降順)</h3>"
    "<table><tr><th>市町</th><th>タイプ</th><th>DID数</th>"
    "<th>DID人口</th><th>DID面積 km²</th><th>密度 人/km²</th>"
    "<th>集中率</th><th>占有率</th></tr>"
    + city_table_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"DID 数の最大は<b>広島市 18</b>、最小は <b>1 が 8 市町</b>。"
    f"<b>n=1 の市町</b>でも DID 集中率は 30-100% と幅広い ─ "
    f"単核都市の中にも『町ぐるみ型 (府中町)』と『中心部だけ型 (三次市)』の質的差がある。"
    f"密度は<b>{dens_max_city} {dens_max:,.0f} ~ {dens_min_city} {dens_min:,.0f} 人/km² で"
    f"{dens_ratio:.2f} 倍の格差</b> ─ 同じ DID でも市町間で密度が大きく違う。"
    f"占有率上位は小規模町 (府中町・海田町・坂町) で 17-56%、"
    f"政令市・中核市は 8-15%、中山間市は 1-2% という<b>3 階層構造</b>。</p>"
)
sections.append(("6. 分析3: 市町別 small multiples + 個数/集中率/占有率 (H2, H4)",
                  s6_html))

# === セクション7: 分析4 — DID 人口密度 主題図 + 上位/下位 ===
top_dens_rows = "".join(
    f"<tr><td>{i+1}</td><td>{r['src_city']}</td><td>{r['ZU_NO']}</td>"
    f"<td>{r['CITY_CD']}</td><td>{r['JINKOU_S']:,}</td>"
    f"<td>{r['TOCHI_A']}</td><td><b>{r['density_per_km2']:.0f}</b></td>"
    f"<td>{r['compactness']:.2f}</td>"
    f"<td>{r['n_parts']}</td></tr>"
    for i, (_, r) in enumerate(top10_dens.iterrows())
)
bot_dens_rows = "".join(
    f"<tr><td>{i+1}</td><td>{r['src_city']}</td><td>{r['ZU_NO']}</td>"
    f"<td>{r['CITY_CD']}</td><td>{r['JINKOU_S']:,}</td>"
    f"<td>{r['TOCHI_A']}</td><td><b>{r['density_per_km2']:.0f}</b></td>"
    f"<td>{r['compactness']:.2f}</td>"
    f"<td>{r['n_parts']}</td></tr>"
    for i, (_, r) in enumerate(bot10_dens.iterrows())
)

s7_html = (
    "<h3>狙い</h3>"
    "<p>DID polygon 単位で<b>人口密度の地理</b>を可視化する。"
    "DID 認定線 (4,000 人/km²) を基準に、各 DID がどの程度上回っているかを色で示し、"
    "<b>「都市らしさの強度」</b>を地理的に把握する。</p>"

    "<h3>手法</h3>"
    "<p><code>geopandas.plot(column='density_per_km2', cmap='YlOrRd')</code> で"
    "polygon を密度で塗り分け。vmin=2000, vmax=12000 にクリップ"
    "(DID 認定線 4,000 を中央に置く)。"
    "凡例 colorbar に水平線で 4,000 ラインを描画して、認定基準を視覚化する。</p>"

    "<h3>実装</h3>"
    + code('''
# 人口密度 (geometry の正確な面積から計算)
did["density_per_km2"] = did["JINKOU_S"] / (did.geometry.area / 1e6).clip(lower=1e-6)

# choropleth (DID polygon 単位)
fig, ax = plt.subplots(figsize=(13, 9))
admin_diss.plot(ax=ax, facecolor="#f5f5f5")
did_plot = did.copy()
did_plot["dens_clip"] = did_plot["density_per_km2"].clip(2000, 12000)
did_plot.plot(ax=ax, column="dens_clip", cmap="YlOrRd",
              vmin=2000, vmax=12000, alpha=0.85)
sm = plt.cm.ScalarMappable(cmap="YlOrRd", norm=Normalize(2000, 12000))
cb = plt.colorbar(sm, ax=ax)
cb.set_label("DID 人口密度 (人/km²)\\n4000 が DID 認定線")
cb.ax.axhline(4000, color="black", linewidth=1.2)  # 4000 ラインを明示
''') +

    "<h3>図 5 — DID 単位 人口密度 主題図</h3>"
    "<p><b>なぜこの図か</b>: <b>「同じ DID でも密度は均一ではない」</b>"
    "という事実を地図で見せる。<code>YlOrRd</code> (黄→橙→赤) は"
    "<b>連続値の段階を視覚的に強調</b>するパレットで、"
    "高密度 DID と低密度 DID を直感的に区別できる。</p>"
    + figure("assets/L23_fig03_did_density.png",
              "DID polygon 単位 人口密度 主題図 (n={} polygon)".format(N_DID)) +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>濃い赤 (>9,000人/km²)</b> は広島市・府中町・海田町・福山市の中心 DID。"
    f"<b>『広島都市圏』の通勤圏で密度が極端に高い</b>。</li>"
    f"<li><b>淡黄 (~3,500-4,500 人/km²)</b> は中山間市町 (三次・三原・竹原・大竹の一部)。"
    f"DID 認定線ぎりぎりで<b>『DID として薄い都市』</b>。"
    f"地形の制約や住宅地の散在で密度が稼げない。</li>"
    f"<li>密度上位 3 は <b>{', '.join(top10_dens.head(3)['src_city'].tolist())}</b> ─ "
    f"中央政府機関・主要駅・都心部に集中。</li>"
    f"<li>密度下位 3 は <b>{', '.join(bot10_dens.head(3)['src_city'].tolist())}</b> ─ "
    f"瀬戸内海島嶼部や中山間盆地で、DID として最低限の密度のみ確保。</li>"
    f"<li>同じ広島市内でも、中区 (a, b, c) と佐伯区・安佐南区の周辺 DID では"
    f"密度が 2-3 倍違う ─ <b>多核都市内での密度勾配</b>が見える。</li>"
    "</ul>"

    "<h3>表 4 — DID 人口密度 top 10 polygon</h3>"
    "<table><tr><th>順位</th><th>市町</th><th>ZU_NO</th>"
    "<th>CITY_CD</th><th>人口</th>"
    "<th>面積 ha</th><th>密度 人/km²</th><th>コンパクト</th>"
    "<th>飛び地</th></tr>"
    + top_dens_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"密度トップは <b>{top10_dens.iloc[0]['src_city']} {top10_dens.iloc[0]['ZU_NO']} "
    f"{top10_dens.iloc[0]['density_per_km2']:.0f} 人/km²</b>。"
    f"全国の高密度都市 (東京都心 ~15,000) と同水準ではないが、"
    f"県内ではトップクラス。"
    f"上位 10 件はすべて<b>広島市 + 府中町 + 海田町 + 福山市の中心 DID</b>。"
    f"高密度 DID はコンパクトネス指標も比較的高め (0.4 前後) で、"
    f"<b>『密で円形に近い都市核』</b>がトップに入る。</p>"

    "<h3>表 5 — DID 人口密度 bot 10 polygon</h3>"
    "<table><tr><th>順位</th><th>市町</th><th>ZU_NO</th>"
    "<th>CITY_CD</th><th>人口</th>"
    "<th>面積 ha</th><th>密度 人/km²</th><th>コンパクト</th>"
    "<th>飛び地</th></tr>"
    + bot_dens_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"下位は<b>密度 2,500-3,500 人/km²</b> で、DID 認定線 4,000 をやや下回る polygon もある。"
    f"これは<b>『連担すれば部分的に 4,000 を下回ってもよい』</b>という DID 定義の柔軟性を示す。"
    f"下位 10 件には中山間 (竹原・三次・大竹) の DID が多く、"
    f"<b>飛び地数 (n_parts) が複数</b>のものが目立つ ─ "
    f"小集落をつないで連担とみなす『細長い DID』のパターン。</p>"
)
sections.append(("7. 分析4: DID 人口密度 主題図 + top/bot 分析 (H3)", s7_html))

# === セクション8: 分析5 — 面積×人口 log-log + コンパクトネス×密度 ===
top_pop_rows = "".join(
    f"<tr><td>{i+1}</td><td>{r['src_city']}</td><td>{r['ZU_NO']}</td>"
    f"<td>{r['CITY_CD']}</td><td><b>{r['JINKOU_S']:,}</b></td>"
    f"<td>{r['TOCHI_A']}</td><td>{r['density_per_km2']:.0f}</td>"
    f"<td>{r['compactness']:.2f}</td>"
    f"<td>{r['n_parts']}</td></tr>"
    for i, (_, r) in enumerate(top10_pop.iterrows())
)

s8_html = (
    "<h3>狙い</h3>"
    "<p>DID 面積と人口の関係を log-log 散布で示し、"
    "<b>「都市の規模分布」</b>と<b>「密度の都市タイプ依存」</b>を可視化する。"
    "コンパクトネス指数 × 密度の散布図で、<b>形状と密度の関係</b>も同時に確認する。</p>"

    "<h3>手法</h3>"
    "<p><b>(a) log-log 散布</b>: x軸 = TOCHI_A (ha)、y軸 = JINKOU_S (人)、"
    "両対数。傾き 1 の直線は密度一定を意味し、"
    "<b>4,000人/km² の DID 認定線</b>を <code>y = 40·x</code> で重ね描き "
    "(x が ha なので 4,000人/km² = 40 人/ha)。"
    "市町タイプで色分けする。</p>"

    "<p><b>(b) コンパクトネス × 密度 散布</b>: "
    "x軸 = density_per_km2、y軸 = compactness (Polsby-Popper)。"
    "<b>「密度が高い + コンパクト」の DID は理想的な都市核</b>、"
    "<b>「密度が低い + 細長」の DID は線形集落</b>。</p>"

    "<h3>実装</h3>"
    + code('''
# 面積×人口 log-log + 認定線
fig, ax = plt.subplots(figsize=(11, 9))
for ct, col in CTYPE_COLOR.items():
    sub = did[did["ctype"] == ct]
    ax.scatter(sub["TOCHI_A"], sub["JINKOU_S"], c=col, label=ct)
xs = np.array([20, 5000])
ax.plot(xs, xs * 40, ls="--", label="DID 認定線 (4,000人/km²)")  # ha 単位
ax.set_xscale("log"); ax.set_yscale("log")

# コンパクトネス × 密度
fig, ax = plt.subplots(figsize=(11, 9))
for ct, col in CTYPE_COLOR.items():
    sub = did[did["ctype"] == ct]
    ax.scatter(sub["density_per_km2"], sub["compactness"], c=col, label=ct)
ax.axvline(4000, ls="--", color="black", label="DID 認定線")
''') +

    "<h3>図 6 — DID 面積 × DID 人口 (log-log + 認定線)</h3>"
    "<p><b>なぜこの図か</b>: 面積と人口は<b>4 桁にまたがる広いレンジ</b>"
    "(20 ha ~ 5,000 ha, 5,000 人 ~ 200,000 人)、"
    "log-log でないと小さい DID が原点付近で潰れて見えなくなる。"
    "認定線を重ねることで、<b>「DID 内の密度ばらつき」</b>が直感的に伝わる。</p>"
    + figure("assets/L23_fig05_area_pop_loglog.png",
              "DID 面積 × DID 人口 (log-log 散布)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>大半の DID は認定線 (4,000 人/km²) と 6,000 人/km² 線の間に位置</b> ─ "
    "DID 定義通りの密度。</li>"
    f"<li>右上の極端な 1 点は<b>{top10_pop.iloc[0]['src_city']} {top10_pop.iloc[0]['ZU_NO']}</b> "
    f"({top10_pop.iloc[0]['JINKOU_S']:,} 人, "
    f"{top10_pop.iloc[0]['TOCHI_A']} ha) ─ 県内最大の都市核。</li>"
    "<li><b>左下 (小規模 DID, 5,000-10,000 人)</b> は呉市・廿日市市・尾道市の沿岸"
    "小集落 ─ 飛び石的に分散した小さな都市核。</li>"
    "<li>市町タイプ色で見ると、<b>政令市 (赤)</b> は左から右までの全レンジに分布、"
    "<b>町 (緑)</b> は中央付近 (中規模) に固まる ─ 大都市は規模分布が広い。</li>"
    "<li><b>傾きはほぼ 1</b> = 面積と人口の比 (= 密度) は DID 間で大きく違わない。"
    "ただし詳細に見ると<b>『大規模 DID は密度がやや低め』</b>の傾向 (周辺住宅地を含むため)。</li>"
    "</ul>"

    "<h3>図 7 — コンパクトネス × 人口密度 散布</h3>"
    "<p><b>なぜこの図か</b>: 密度と形状はそれぞれ独立した『都市らしさ』の側面。"
    "両者の散布を見ると<b>『コンパクトな高密度都市核』vs『線的な低密度集落』</b>"
    "の二極構造があるかを定量的に判定できる。</p>"
    + figure("assets/L23_fig09_compactness.png",
              "DID コンパクトネス × 人口密度 散布") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li><b>右上 (高密度 + コンパクト)</b> = 府中町・海田町・坂町の単核 DID ─ "
    "<b>『町ぐるみ DID』は形状が良い</b>。</li>"
    "<li><b>左下 (中密度 + 細長)</b> = 尾道市・三原市の沿岸 DID ─ "
    "海岸沿いの線的市街地で形状が悪い。</li>"
    "<li>広島市 (赤) は密度バラつきが大きく、"
    "コンパクトネスは中程度 (0.3-0.5) ─ <b>多核都市の各核は形状が均一でない</b>。</li>"
    "<li>DID 認定線 (4,000 人/km²) はコンパクトネスとは独立 ─ "
    "<b>『密度基準だけで都市を定義する』DID 定義の限界</b>を可視化。"
    "形状も含めると別のクラスタが見える。</li>"
    "</ul>"

    "<h3>表 6 — DID 人口 top 10 polygon</h3>"
    "<table><tr><th>順位</th><th>市町</th><th>ZU_NO</th>"
    "<th>CITY_CD</th><th>人口</th>"
    "<th>面積 ha</th><th>密度 人/km²</th><th>コンパクト</th>"
    "<th>飛び地</th></tr>"
    + top_pop_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"人口トップは <b>{top10_pop.iloc[0]['src_city']} {top10_pop.iloc[0]['ZU_NO']} "
    f"{top10_pop.iloc[0]['JINKOU_S']:,} 人</b>"
    f" (広島市 8 区中の最大 DID)。"
    f"上位 10 件中 <b>広島市が 5+ 件</b>を占め、福山市・呉市・東広島市などが続く。"
    f"<b>「人口集中地区」の名にふさわしく、トップ DID は地方中核都市の"
    f"主要駅・繁華街周辺</b>に対応する。"
    f"top10 の人口合計は<b>{top10_pop['JINKOU_S'].sum():,} 人</b>"
    f"で、これだけで全 DID 人口 ({JINKOU_TOTAL_DID:,}) の "
    f"{top10_pop['JINKOU_S'].sum()/JINKOU_TOTAL_DID*100:.1f}% を占める ─ "
    f"<b>『DID 内のさらなる集中』</b>が見える。</p>"
)
sections.append(("8. 分析5: 面積×人口 log-log + コンパクトネス×密度 散布", s8_html))

# === セクション9: 分析6 — 市町総人口 vs DID 人口 + DID 無 6 市町 ===
no_did_rows = "".join(
    f"<tr><td>{r['city']}</td><td>{r['ctype']}</td>"
    f"<td>{r['region']}</td>"
    f"<td>{r['city_area_km2']:.1f}</td>"
    f"<td>{r['city_pop_k']:,}</td>"
    f"<td>{r['city_density']:.0f}</td></tr>"
    for _, r in no_did_agg.iterrows()
)

s9_html = (
    "<h3>狙い</h3>"
    "<p>市町の総人口と DID 人口の関係を散布で示し、"
    "<b>「都市らしさが市町規模にどう依存するか」</b>を可視化する。"
    "DID 無指定 6 市町を×印で同じ散布に重ね、"
    "<b>「人口があっても DID にならない閾値」</b>を観察する。</p>"

    "<h3>手法</h3>"
    "<p>x軸 = 市総人口、y軸 = DID 人口、市町タイプ色で散布。"
    "「100% 集中ライン」(y=x) と「50% 集中ライン」(y=0.5x) を破線で重ね、"
    "<b>市町ごとの DID 集中率</b>を視覚的に判定。"
    "DID 無指定 6 市町は y=0 の x 印として配置し、"
    "<b>「同じ人口規模でも DID 化する/しない」</b>の境界を観察する。</p>"

    "<h3>実装</h3>"
    + code('''
# 14 DID 市町: 散布
for ct, col in CTYPE_COLOR.items():
    sub = city_agg[city_agg["ctype"] == ct]
    ax.scatter(sub["city_pop_k"]*1000, sub["did_pop"], c=col, s=160)

# 6 DID 無指定市町: x=人口, y=0 の x 印
for _, r in no_did_agg.iterrows():
    ax.scatter(r["city_pop_k"]*1000, 0, marker="x", c="#666", s=120)

# 100% / 50% ライン
xs = np.linspace(10000, 1500000, 50)
ax.plot(xs, xs, ls="--", label="100% 集中")
ax.plot(xs, xs*0.5, ls=":", label="50% 集中")
ax.set_xscale("log"); ax.set_yscale("symlog", linthresh=10000)
''') +

    "<h3>図 8 — 市町総人口 vs DID 人口 (DID 無 6 市町併置)</h3>"
    "<p><b>なぜこの図か</b>: 市町別 14 件のデータを 1 軸ずつ"
    "(個数・占有率・集中率) で見せた前節 fig 06 を、"
    "<b>『総人口を横軸にとった散布図』</b>に統合する。"
    "DID 無 6 市町を混ぜると<b>「人口があっても DID にならない例外」</b>"
    "の存在が一目で分かる ─ これは1次元のテーブルでは伝わらない。</p>"
    + figure("assets/L23_fig07_pop_vs_did_pop.png",
              "市町総人口 vs DID 人口 (DID 無 6 市町は ×印で y=0 に配置)") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    "<li>大都市 (広島市・福山市・呉市) は<b>50% 集中ラインの上</b>に位置 ─ "
    "市民の半分以上が DID 内。</li>"
    "<li>中山間都市 (三次市・三原市・廿日市市) は<b>50% ライン下</b> ─ "
    "市民の半分以下しか DID 内に住まない。</li>"
    "<li>小規模町 (府中町・海田町・坂町) は <b>100% ライン近く</b> ─ "
    "町民のほぼ全員が DID 内 (= 町ぐるみ DID)。</li>"
    "<li><b>×印 (DID 無 6 市町)</b>: 熊野町 (人口 23,000) は廿日市市 (DID あり 人口 117,000) と"
    "近い人口階層だが DID 無 ─ <b>『人口だけでは DID 化を予測できない』</b>。"
    "集落の集約度・地形・歴史的市街地形成が決め手。</li>"
    "<li>江田島市 (人口 22,000, 離島) と熊野町 (23,000, 近郊) と竹原市 (24,000, DID あり) は"
    "<b>同じ人口階層</b>だが地形/歴史で DID 化が分岐する。</li>"
    "<li>3 万人以下の市町は DID 化の境目 ─ <b>「2-3 万人が DID 化の閾値」</b>と推測される"
    "(竹原市・大竹市・坂町は DID あり、熊野町・江田島市・世羅町・北広島町・安芸高田市は無)。</li>"
    "</ul>"

    "<h3>表 7 — DID 無指定 6 市町 詳細</h3>"
    "<table><tr><th>市町</th><th>タイプ</th><th>地理タイプ</th>"
    "<th>面積 km²</th><th>人口 千人</th><th>密度 人/km²</th></tr>"
    + no_did_rows + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"DID 無指定 6 市町は<b>『中山間 4 + 離島 1 + 広島近郊町 1』</b>のタイプ分布。"
    f"全市町の<b>人口密度は最大の熊野町 683 人/km² が最高</b> ─ "
    f"DID 認定線 4,000 の 1/6 程度。"
    f"<b>『市町平均密度が低い + 集落が分散 → DID 化しない』</b>のパターン。"
    f"江田島市は離島で集落が小島ごとに分散、"
    f"中山間 4 市町 (庄原・北広島・安芸高田・世羅) は山間に集落が散在、"
    f"熊野町は広島市近郊だが筆形地形で集落が線状に細長く伸びるため認定基準を満たさない。</p>"
)
sections.append(("9. 分析6: 市町総人口 vs DID 人口 + DID 無 6 市町分析 (H6)",
                  s9_html))

# === セクション10: 分析7 — LIP × DID オーバーレイ ===
overlap_rows_html = "".join(
    f"<tr><td>{r['city']}</td>"
    f"<td>{r['did_area_km2']:.2f}</td>"
    f"<td>{r['lip_area_km2']:.2f}</td>"
    f"<td>{r['did_n_lip_km2']:.2f}</td>"
    f"<td><b>{r['lip_in_did_pct']:.1f}%</b></td>"
    f"<td>{r['did_covered_by_lip_pct']:.1f}%</td></tr>"
    for _, r in overlap_df.iterrows()
)

s10_html = (
    "<h3>狙い</h3>"
    "<p>居住誘導区域 (LIP, L19 で扱った 8 件) と DID の<b>空間的オーバーレイ</b>"
    "を計算する。LIP は<b>『将来この区域に人口を集約する』</b>と自治体が定めた区域、"
    "DID は<b>『現在の人口集中地区』</b>。"
    "両者の重なり率 LIP_in_DID で<b>「現在の都市核 → 将来の集約核」</b>の連続性を検証する。</p>"

    "<h3>手法</h3>"
    "<p>LIP は L19 で取得済の 8 件 (広島市・呉市・竹原市・三原市・福山市・府中市・"
    "東広島市・廿日市市) を読込。各市町について"
    "<code>geopandas.overlay(did_city, lip_city, how='intersection')</code> で"
    "重なり面積を計算。<b>LIP_in_DID = 重なり / LIP 全体</b>"
    "(LIP の何割が DID 内か)、"
    "<b>DID_covered_by_LIP = 重なり / DID 全体</b> (DID の何割が LIP 化されたか) を出す。"
    "前者が<b>『将来計画の現実性』</b>、後者が<b>『縮約意図の強さ』</b>を表す。</p>"

    "<h3>実装</h3>"
    + code('''
# 8 LIP 策定市町を読込
lip = gpd.GeoDataFrame(pd.concat([
    load_geojson_zip(LIP_DIR / f"jukyo_{lip_dsid}_{name}.zip").assign(src_city=name)
    for lip_dsid, name in LIP_DEFS]))

# 市町別 dissolve
did_by_city = did.dissolve(by="src_city", as_index=False)
lip_by_city = lip.dissolve(by="src_city", as_index=False)

# 8 市町ごとに intersection
for _dsid, name, _adm, _ct, lip_dsid in CITY_DEFS:
    if lip_dsid == 0: continue
    inter = gpd.overlay(did_by_city[did_by_city["src_city"]==name],
                        lip_by_city[lip_by_city["src_city"]==name],
                        how="intersection")
    inter_km2 = inter.geometry.area.sum() / 1e6
    overlap_rows.append({"city": name, "lip_in_did_pct":
                          inter_km2 / lip_area * 100, ...})
''') +

    "<h3>図 9 — LIP ∩ DID 8 panels (8 LIP 策定市町)</h3>"
    "<p><b>なぜこの図か</b>: 数値表 (LIP_in_DID = 80% など) では実感がわかない。"
    "<b>赤 (DID) と緑 (LIP) を重ね描き</b>することで、"
    "「LIP は DID にどれだけ似ているか／縮約しているか」が視覚的に伝わる。"
    "8 panels で並列表示し、市町ごとの政策スタイルの違いも見せる。</p>"
    + figure("assets/L23_fig08_lip_overlay.png",
              "LIP (居住誘導 L19) ∩ DID (本記事) — 8 LIP 策定市町") +
    "<p><b>この図から読み取れること</b></p>"
    "<ul>"
    f"<li><b>広島市・福山市</b> は LIP が DID をほぼ覆う ─ "
    f"<b>『現在の都市核を将来も維持する』</b>方針。"
    f"LIP_in_DID は <b>{overlap_df[overlap_df['city']=='広島市']['lip_in_did_pct'].iloc[0]:.0f}% / "
    f"{overlap_df[overlap_df['city']=='福山市']['lip_in_did_pct'].iloc[0]:.0f}%</b>。</li>"
    f"<li><b>東広島市</b> は LIP が DID を超えて広めに設定 ─ "
    f"<b>『大学・産業の発展で DID 拡大予測』</b>のスタンス。"
    f"LIP_in_DID = {overlap_df[overlap_df['city']=='東広島市']['lip_in_did_pct'].iloc[0]:.0f}%。</li>"
    f"<li><b>呉市・三原市</b> は LIP が DID より<b>明らかに小さい</b> ─ "
    f"<b>『縮約方針』</b>。人口減少に備えて将来の集約核を絞る。"
    f"DID_covered_by_LIP = {overlap_df[overlap_df['city']=='呉市']['did_covered_by_lip_pct'].iloc[0]:.0f}% (DID の半分以下を LIP 化)。</li>"
    f"<li><b>竹原市・府中市</b> は LIP と DID がほぼ同サイズで重なる ─ "
    f"<b>『現状維持型』</b>。</li>"
    "<li>廿日市市 は山地・島しょ部の DID は LIP に含まれない ─ "
    "<b>『沿岸部のみを重点居住区にする』</b>明確な縮約。</li>"
    "</ul>"

    "<h3>表 8 — DID ∩ LIP 重ね合わせ (8 LIP 策定市町)</h3>"
    "<table><tr><th>市町</th>"
    "<th>DID 面積 km²</th><th>LIP 面積 km²</th><th>重なり km²</th>"
    "<th>LIP_in_DID</th><th>DID_covered_by_LIP</th></tr>"
    + overlap_rows_html + "</table>"
    "<p><b>この表から読み取れること</b>: "
    f"<b>LIP_in_DID 中央値は {overlap_df['lip_in_did_pct'].median():.1f}%</b> ─ "
    f"つまり<b>『LIP の半分前後が DID 内』</b>。"
    f"これは<b>「LIP = DID の継承」</b>という素朴な仮説を支持する一方、"
    f"50% という数字は<b>「LIP は DID をそのまま使うのではなく、"
    f"少し変形して将来の集約核を作っている」</b>ことも示す。"
    f"市町別では広島市・福山市の LIP が DID をほぼ覆う一方、"
    f"廿日市市 ({overlap_df[overlap_df['city']=='廿日市市']['lip_in_did_pct'].iloc[0]:.0f}%) は"
    f"LIP がかなり外れている ─ 山地部の DID を LIP から除外している。"
    f"<b>「LIP は将来計画、DID は現状実態」</b>の差分が政策スタイルの違いを浮かび上がらせる。</p>"
)
sections.append(("10. 分析7: LIP (居住誘導 L19) との空間オーバーレイ (H5)",
                  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 (広島市集中度)</b>: {H_RESULTS['H1']['judge']}。"
    f"広島市は DID 数 {hiroshima_did_n}/{N_DID} ({hiroshima_did_n/N_DID*100:.1f}%)、"
    f"DID 人口 {hiroshima_did_pop:,}/{JINKOU_TOTAL_DID:,} ({hiroshima_did_pop/JINKOU_TOTAL_DID*100:.1f}%)。"
    f"政令市 1 つで県の都市部の過半を占める ─ <b>『広島県=広島市+それ以外』</b>の地理構造。</li>"
    f"<li><b>H2 (単核 vs 多核)</b>: {H_RESULTS['H2']['judge']}。"
    f"単核 (n=1) {n_did_only_one} 市町、多核 (n>=3) {n_did_multi} 市町。"
    f"単核 8 市町と多核 6 市町の二極構造。"
    f"<b>合併・地形・歴史</b>がこの二極を作っている。</li>"
    f"<li><b>H3 (密度格差)</b>: {H_RESULTS['H3']['judge']}。"
    f"密度 max/min = {dens_ratio:.2f} 倍。"
    f"小規模町 (府中町) と中山間都市 (三次市) で密度が大きく違う ─ "
    f"<b>同じ DID でも『都市の濃さ』に差</b>がある。</li>"
    f"<li><b>H4 (面積占有率の二極)</b>: {H_RESULTS['H4']['judge']}。"
    f"占有率 <5% の中山間市 ({n_under_5} 件) と"
    f">=30% のベッドタウン町 ({n_over_30} 件) の二極。"
    f"<b>『市域の何割が都市か』は市町タイプで決まる</b>。</li>"
    f"<li><b>H5 (LIP ⊆ DID)</b>: {H_RESULTS['H5']['judge']}。"
    f"LIP_in_DID 中央値 {overlap_df['lip_in_did_pct'].median():.1f}%。"
    f"50% 以上が DID 内の市町は {lip_in_did_high}/8 件 ─ "
    f"<b>『LIP は DID を継承するが完全一致ではない』</b>"
    f"= 自治体は将来計画で DID を縮約・拡張している。</li>"
    f"<li><b>H6 (DID 無 6 市町は小人口)</b>: {H_RESULTS['H6']['judge']}。"
    f"6 市町中 {small_no_did} 市町が人口 3 万以下。"
    f"<b>『中山間 + 離島 + 集落分散の近郊町』</b>の 3 タイプ。"
    f"DID 化の境界線は人口 2-3 万人 + 集落集約度。</li>"
    "</ul>"

    "<h3>本記事の主要発見 5 点</h3>"
    "<ol>"
    f"<li><b>『広島市が県の都市部の中核』を定量化</b>: 県 DID 数の {hiroshima_did_n/N_DID*100:.1f}%、"
    f"DID 人口の {hiroshima_did_pop/JINKOU_TOTAL_DID*100:.1f}% が広島市 8 区。"
    f"中核市 (呉・福山) を合わせても県 DID 人口の {(hiroshima_did_pop+int(city_agg[city_agg['city']=='呉市']['did_pop'].iloc[0])+int(city_agg[city_agg['city']=='福山市']['did_pop'].iloc[0]))/JINKOU_TOTAL_DID*100:.1f}%。"
    f"<b>政令市・中核市 3 つで県都市部の 8 割超</b>。</li>"
    "<li><b>『単核都市 8 vs 多核都市 6』の二極構造</b>: 単核都市は「町ぐるみ DID」"
    "(府中町など) と「中心部のみ DID」(三次市など) の 2 サブタイプに分かれる。"
    "多核都市は政令市 (広島市) と合併由来 (東広島市) と地形分断 (呉市・尾道市) の混在。</li>"
    "<li><b>『町ぐるみ DID』の小規模町</b>: 府中町・海田町・坂町は DID 占有率 17-56%、"
    "DID 集中率 77-96% で<b>町域のほぼ全部が都市</b>。"
    "「町=町ぐるみ DID」という地理現象は、政令市 (広島市) のベッドタウン化が"
    "<b>町という行政単位を都市に変えた</b>結果。</li>"
    f"<li><b>LIP_in_DID 中央値 {overlap_df['lip_in_did_pct'].median():.1f}% = LIP は DID を継承</b>: "
    f"立地適正化計画の居住誘導区域は、現在の DID をほぼ受け継いでいるが、"
    f"市町ごとに縮約・拡張のスタンスが違う。"
    f"<b>呉市・三原市は『将来縮約』</b>、<b>東広島市は『将来拡張』</b>の対照が"
    f"オーバーレイで定量化された。</li>"
    "<li><b>DID 無指定 6 市町の 3 タイプ</b>: "
    "中山間 (庄原・北広島・安芸高田・世羅)・離島 (江田島)・"
    "近郊集落分散 (熊野町)。<b>同じ人口規模 (2-3 万) でも DID 化する/しないは集約度</b>。"
    "竹原市 (人口 24,000) と熊野町 (23,000) の DID 有無の対比はその典型。</li>"
    "</ol>"
)
sections.append(("11. 仮説検証と考察", s11_html))

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

<h3>発展課題 1: DID 内 vs 外の高齢化率と人口ピラミッド比較 (L22 連携)</h3>
<p><b>結果 X</b>: 本記事で県内 DID 48 polygon の地理が定量化された。
L22 では町丁単位の高齢化率・年少率・性比が計算済。
両者を空間結合すれば<b>『DID 内町丁 vs DID 外町丁』</b>の人口構造比較が可能。</p>
<p><b>新仮説 Y</b>: DID 内町丁の高齢化率は DID 外町丁より<b>10%pt 以上低く</b>、
年少率は 5%pt 以上高い。<b>『人口集中地区は構造的に若い』</b>。
中山間市町 (三次市・三原市) でも DID 内 (中心市街地) と DID 外 (集落) で
構造が分裂し、市町平均では捉えきれない局地差が見える。</p>
<p><b>課題 Z</b>: <code>geopandas.sjoin(pop, did, how='left', predicate='intersects')</code>
で町丁に DID 内/外フラグを付与し、4 群構成 (年少/生産/前期高齢/後期高齢) を
DID 内外でクロス比較。20 市町 small multiples で
DID 内 vs 外の人口ピラミッドを並列。
<b>『DID 内ほど若い』が市町間で一貫するか</b>を検証。</p>

<h3>発展課題 2: DID 経年変化 (H27 vs R2) — 都市の縮小と拡大</h3>
<p><b>結果 X</b>: 本記事は R2 (2020) DID の 1 時点スナップショット。
人口減少社会では、5 年前 (H27) との差分が政策上重要。</p>
<p><b>新仮説 Y</b>: 中山間都市 (三次市・三原市) の DID は<b>5 年で面積が縮小</b>し、
中心部だけに後退している (郊外集落が DID 認定基準を割り込む)。
逆に東広島市の DID は<b>面積が拡大</b>し (大学・産業発展)、
広島市は緩やかに北側へ拡張 (郊外住宅地化)。</p>
<p><b>課題 Z</b>: 政府統計 e-Stat から H27 国勢調査 DID 境界 GIS を取得 →
ZU_NO で対応付けして R2 と空間差分計算 →
<code>fig: H27 DID 境界 (薄青) + R2 DID 境界 (赤)</code> 重ね描き →
ΔDID 面積 / ΔDID 人口の市町ランキング。
<b>『DID の縮小と拡大の地理』</b>が見える。
R7 (2025) のデータが出れば 10 年スパン 3 点比較。</p>

<h3>発展課題 3: DID 認定基準の検証 — 4,000 人/km² 連担条件</h3>
<p><b>結果 X</b>: 本記事の DID 単位密度 (= JINKOU_S/geometry面積) は<b>下位 polygon で 2,500-3,500</b> と
DID 認定線 (4,000) を下回るものが見つかった。
これは<b>『連担すれば部分的に下回ってもよい』</b>という DID 定義の柔軟性を示すが、
具体的にどの程度緩和されているか定量的には不明。</p>
<p><b>新仮説 Y</b>: 国勢調査メッシュ (1/4 地域メッシュ ≈ 500m) を使えば、
DID polygon 内のメッシュ単位密度を計算でき、
<b>『DID polygon 全体の平均密度は 4,000 を下回ることもあるが、
構成メッシュの 70%+ は 4,000 超』</b>のような連担条件が定量できる。</p>
<p><b>課題 Z</b>: e-Stat から R2 国勢調査 1/4 地域メッシュ 人口を取得 →
DID polygon と空間結合 → polygon 内メッシュ別密度ヒストグラム →
<b>「polygon 平均は低くても構成メッシュの大半は 4,000 超」</b>を確認。
DID 定義を実データで再現する作業。
<b>『国勢調査の中心概念をデータから自分で再構築する』</b>体験となる。</p>
"""
sections.append(("12. 発展課題", s12_html))

# =============================================================================
# 8. HTML 出力
# =============================================================================
print("\n[8] HTML 出力", flush=True)
title = "L23 DID地区境界 14件 統合分析 — 広島県の都市部・非都市部の地理構造"
tags = ["都市計画", "DID", "人口集中地区", "14市町統合",
        "国勢調査", "都市核", "コンパクトシティ", "geopandas", "DoBoX"]
data_label = (
    "DoBoX DID地区境界 14件 (#1468,1471,1473,1476,1479,1482,1485,1487,"
    "1490,1493,1495,1500,1502,1505)"
)
html = render_lesson(
    num=23, title=title, tags=tags,
    time="30〜40 分", level="中級 (GIS + 都市地理学)",
    data_label=data_label, sections=sections,
    script_filename="L23_did_boundary.py",
)
out = LESSONS / "L23_did_boundary.html"
out.write_text(html, encoding="utf-8")
print(f"  -> {out}")
print(f"\n=== L23 DONE  Total {time.time()-t0:.2f}s ===")
