"""L13 都市計画基礎調査 統合分析 — 20市町×建物利用×土地利用 (40 dataset_id 統合)

決定的なカバー宣言:
  本記事は **2 シリーズ × 20 市町 = 40 dataset_id** を統合する研究記事。
  - 都市計画区域情報_建物利用現況 20件 (#1469 広島市, #1323 呉市, #1474 竹原市, ...)
  - 都市計画区域情報_土地利用現況 20件 (#70 広島市, #493 呉市, ...)
  Desc が完全書式統一なのでフォーマット同一とみなして処理。
  20×2=40 GeoJSON を `pd.concat`/`gpd.GeoDataFrame` で統合し県全域分析を行う。

要件S 準拠: 1分以内、最悪3分以内で完走。
要件T 準拠: 主題図 (choropleth) + small multiples (12 panels) + 重ね合わせマップを必ず含む。
要件Q 準拠: 図 5枚以上、表 8枚以上。
"""
from __future__ import annotations
import sys, time, json, zipfile, gc
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 ListedColormap, BoundaryNorm
import geopandas as gpd
import shapely

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

t0 = time.time()
print("=== L13 都市計画基礎調査 統合分析 ===")

DATA_DIR = ROOT / "data" / "extras" / "urban_planning_survey"
TARGET_CRS = "EPSG:6671"  # JGD2011 平面直角 III, 広島県

# データが無ければ取得スクリプトを呼ぶ
if not (DATA_DIR / "fetch_log.json").exists():
    print("[!] urban_planning_survey データ未取得。 _fetch_meta.py を実行...")
    fetch_script = DATA_DIR / "_fetch_meta.py"
    if fetch_script.exists():
        import subprocess
        subprocess.run([sys.executable, "-X", "utf8", str(fetch_script)], check=True)
    else:
        raise RuntimeError(f"取得スクリプト未発見: {fetch_script}")

# === 0. 市町テーブル: dataset_id ↔ 市町名 (建物利用 / 土地利用 別) ============
# 注: これら 20 件 × 2 シリーズ = 40 dataset_id をすべて統合する
BUILDING_DSID_CITY = {
    1469: "広島市", 1323: "呉市", 1474: "竹原市", 1477: "三原市", 1480: "尾道市",
    1483: "福山市", 512: "府中市", 1327: "三次市", 1328: "庄原市", 1491: "大竹市",
    1329: "東広島市", 1496: "廿日市市", 1321: "安芸高田市", 1325: "江田島市",
    1331: "府中町", 71: "海田町", 1322: "熊野町", 1326: "坂町",
    1507: "北広島町", 513: "世羅町",
}
LANDUSE_DSID_CITY = {
    70: "広島市", 493: "呉市", 494: "竹原市", 495: "三原市", 496: "尾道市",
    497: "福山市", 498: "府中市", 499: "三次市", 500: "庄原市", 501: "大竹市",
    502: "東広島市", 503: "廿日市市", 504: "安芸高田市", 505: "江田島市",
    506: "府中町", 507: "海田町", 508: "熊野町", 509: "坂町",
    510: "北広島町", 511: "世羅町",
}

# === 1. 用途コード対照表 (国土交通省 都市計画基礎調査実施要領 第4版) ============
# B_USE_xxx (建物利用): 4-digit 用途分類コード => 細分類名
B_USE_LABEL = {
    "B_USE_401": "業務 (事務所/銀行)",
    "B_USE_402": "商業 (店舗/宿泊以外)",
    "B_USE_403": "宿泊 (ホテル/旅館)",
    "B_USE_404": "商業複合",
    "B_USE_411": "専用住宅",
    "B_USE_412": "共同住宅 (アパート/マンション)",
    "B_USE_413": "店舗併用住宅",
    "B_USE_414": "店舗併用共同住宅",
    "B_USE_415": "作業所併用住宅",
    "B_USE_421": "官公庁",
    "B_USE_422": "文教厚生 (学校/病院/福祉)",
    "B_USE_431": "運輸倉庫",
    "B_USE_441": "工場",
    "B_USE_451": "農林漁業用施設",
    "B_USE_452": "供給処理施設",
    "B_USE_453": "防衛施設",
    "B_USE_454": "その他",
    "B_USE_461": "低未利用建築物",
}
# B_USE は「主用途」5系統に集約
B_USE_GROUP = {
    "B_USE_401": "商業系", "B_USE_402": "商業系", "B_USE_403": "商業系", "B_USE_404": "商業系",
    "B_USE_411": "住居系", "B_USE_412": "住居系", "B_USE_413": "住居系",
    "B_USE_414": "住居系", "B_USE_415": "住居系",
    "B_USE_421": "公共系", "B_USE_422": "公共系",
    "B_USE_431": "工業系", "B_USE_441": "工業系", "B_USE_452": "工業系",
    "B_USE_451": "農林漁業系",
    "B_USE_453": "公共系", "B_USE_454": "その他", "B_USE_461": "その他",
}
GROUP_COLOR = {
    "住居系": "#ff8a3c", "商業系": "#9933cc", "工業系": "#0066cc",
    "公共系": "#1f883d", "農林漁業系": "#a6c200", "その他": "#888888",
    "建物なし": "#eeeeee",
}

# LUI_xxx (土地利用): 3-digit code => 用途
L_USE_LABEL = {
    "LUI_201": "田",          "LUI_202": "畑",
    "LUI_203": "山林",        "LUI_204": "水面",
    "LUI_205": "その他自然地", "LUI_211": "住宅用地",
    "LUI_212": "商業用地",    "LUI_213": "工業用地",
    "LUI_214": "公益施設用地", "LUI_215": "道路用地",
    "LUI_216": "交通施設用地", "LUI_217": "公共空地",
    "LUI_218": "その他公的施設", "LUI_219": "農林漁業施設",
    "LUI_220": "ゴルフ場",    "LUI_221": "太陽光発電",
    "LUI_222": "平面駐車場",  "LUI_223": "その他空地",
    "LUI_224": "未整備等",    "LUI_225": "未整備等2",
    "LUI_226": "未整備等3",   "LUI_241": "面積合計 (TOTAL)",
}
L_USE_GROUP = {
    "LUI_201": "農地", "LUI_202": "農地",
    "LUI_203": "森林", "LUI_204": "水面", "LUI_205": "自然地",
    "LUI_211": "住宅", "LUI_212": "商業", "LUI_213": "工業",
    "LUI_214": "公益", "LUI_215": "交通インフラ", "LUI_216": "交通インフラ",
    "LUI_217": "公共空地", "LUI_218": "公益", "LUI_219": "農林漁業",
    "LUI_220": "その他", "LUI_221": "その他", "LUI_222": "その他",
    "LUI_223": "その他", "LUI_224": "その他", "LUI_225": "その他", "LUI_226": "その他",
}
LU_GROUP_COLOR = {
    "農地": "#aacf66", "森林": "#1f883d", "住宅": "#ff8a3c",
    "商業": "#9933cc", "工業": "#0066cc", "公益": "#cf9020",
    "水面": "#56a4dc", "自然地": "#888c2c", "農林漁業": "#638a30",
    "交通インフラ": "#666666", "公共空地": "#3fb98b", "その他": "#cccccc",
}

# === 2. 40 GeoJSON 統合読み込み ==============================================
print("\n[1] 40 GeoJSON 読み込み...")
t1 = time.time()

def load_geojson_zip(zip_path: Path) -> gpd.GeoDataFrame:
    """zip 内の単一 .geojson を BytesIO 経由で read_file"""
    import io
    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:
            data = f.read()
    return gpd.read_file(io.BytesIO(data))

bld_frames, lu_frames = [], []
fetch_log = json.loads((DATA_DIR / "fetch_log.json").read_text(encoding="utf-8"))
ok_log = {(x["series"], x["ds"]): x for x in fetch_log if x.get("ok")}

# 建物利用
for dsid, city in BUILDING_DSID_CITY.items():
    z = DATA_DIR / f"building_{dsid}_{city}.zip"
    if not z.exists():
        print(f"  MISS building {city}")
        continue
    g = load_geojson_zip(z)
    g["city"] = city
    g["dsid"] = dsid
    bld_frames.append(g)
print(f"  building: {len(bld_frames)} 市町, {time.time()-t1:.1f}s", flush=True)

t1 = time.time()
for dsid, city in LANDUSE_DSID_CITY.items():
    z = DATA_DIR / f"landuse_{dsid}_{city}.zip"
    if not z.exists():
        print(f"  MISS landuse {city}")
        continue
    g = load_geojson_zip(z)
    g["city"] = city
    g["dsid"] = dsid
    lu_frames.append(g)
print(f"  landuse: {len(lu_frames)} 市町, {time.time()-t1:.1f}s", flush=True)

# concat (列が市町ごとに違うので reindex で和集合化される)
bld_all = gpd.GeoDataFrame(pd.concat(bld_frames, ignore_index=True),
                           geometry="geometry", crs=bld_frames[0].crs)
lu_all = gpd.GeoDataFrame(pd.concat(lu_frames, ignore_index=True),
                          geometry="geometry", crs=lu_frames[0].crs)
print(f"  bld_all: {len(bld_all):,} mesh, {len(bld_all.columns)} columns")
print(f"  lu_all : {len(lu_all):,} mesh, {len(lu_all.columns)} columns")

# CRS 変換 (面積 m² で揃える)
bld_all = bld_all.to_crs(TARGET_CRS)
lu_all  = lu_all.to_crs(TARGET_CRS)

# === 3. 集計用 数値整形 ====================================================
print("\n[2] 集計用 整形 ...", flush=True)
t1 = time.time()

# B_USE_*** の数値化 (NaN→0)
buse_cols = [c for c in B_USE_LABEL.keys() if c in bld_all.columns]
for c in buse_cols:
    bld_all[c] = pd.to_numeric(bld_all[c], errors="coerce").fillna(0)
# 5系統 group 集約
group_to_cols = {}
for c, g in B_USE_GROUP.items():
    if c in bld_all.columns:
        group_to_cols.setdefault(g, []).append(c)
for g, cols in group_to_cols.items():
    bld_all[f"GRP_{g}"] = bld_all[cols].sum(axis=1)
# 主用途 (1メッシュの最大棟数の系統)
# 重要: 全GRP=0 のメッシュ (建物なし) は idxmax が先頭列を返してしまうため "建物なし" に置換
grp_cols = [f"GRP_{g}" for g in group_to_cols.keys()]
bld_all["MAIN_GRP"] = bld_all[grp_cols].idxmax(axis=1).str.replace("GRP_", "", regex=False)
all_zero = (bld_all[grp_cols].sum(axis=1) == 0)
bld_all.loc[all_zero, "MAIN_GRP"] = "建物なし"
print(f"  all-zero メッシュ: {int(all_zero.sum()):,} / {len(bld_all):,} ({100*all_zero.mean():.1f}%) → '建物なし'")
# 全用途棟数合計はSUM_BIL列があれば優先
if "SUM_BIL" in bld_all.columns:
    bld_all["SUM_BIL"] = pd.to_numeric(bld_all["SUM_BIL"], errors="coerce").fillna(0)
else:
    bld_all["SUM_BIL"] = bld_all[buse_cols].sum(axis=1)

# LUI_*** の数値化
lui_cols = [c for c in L_USE_LABEL.keys() if c in lu_all.columns]
for c in lui_cols:
    lu_all[c] = pd.to_numeric(lu_all[c], errors="coerce").fillna(0)
# group 集約
lu_group_to_cols = {}
for c, g in L_USE_GROUP.items():
    if c in lu_all.columns:
        lu_group_to_cols.setdefault(g, []).append(c)
for g, cols in lu_group_to_cols.items():
    lu_all[f"LUG_{g}"] = lu_all[cols].sum(axis=1)
lug_cols = [f"LUG_{g}" for g in lu_group_to_cols.keys()]
lu_all["MAIN_LUG"] = lu_all[lug_cols].idxmax(axis=1).str.replace("LUG_", "", regex=False)
lu_all_zero = (lu_all[lug_cols].sum(axis=1) == 0)
lu_all.loc[lu_all_zero, "MAIN_LUG"] = "未利用"
LU_GROUP_COLOR.setdefault("未利用", "#eeeeee")

# 面積 (m²)
bld_all["mesh_area_m2"] = bld_all.geometry.area
lu_all["mesh_area_m2"] = lu_all.geometry.area
print(f"  整形: {time.time()-t1:.1f}s")

# === 4. 統合データ出力 (中間 CSV: 再現性確保) ===============================
print("\n[3] 統合 CSV 出力 ...", flush=True)
ASSETS.mkdir(parents=True, exist_ok=True)

# 中間データ: 市町ごとの集計
city_bld_summary = bld_all.groupby("city").agg(
    mesh_n=("MAIN_GRP", "size"),
    bld_total=("SUM_BIL", "sum"),
    area_km2=("mesh_area_m2", lambda s: s.sum()/1e6),
).reset_index()
for grp in B_USE_GROUP.values():
    col = f"GRP_{grp}"
    if col in bld_all.columns and col not in city_bld_summary.columns:
        city_bld_summary[grp] = bld_all.groupby("city")[col].sum().reindex(city_bld_summary["city"]).values
city_bld_summary["bld_density_per_km2"] = (city_bld_summary["bld_total"] / city_bld_summary["area_km2"]).round(1)
city_bld_summary = city_bld_summary.sort_values("bld_density_per_km2", ascending=False)
city_bld_summary.to_csv(ASSETS / "L13_city_building_summary.csv", index=False, encoding="utf-8-sig")

city_lu_summary = lu_all.groupby("city").agg(
    mesh_n=("MAIN_LUG", "size"),
    area_km2=("mesh_area_m2", lambda s: s.sum()/1e6),
    total_lui=("LUI_241", "sum"),  # = 用途別面積合計 (ha 単位)
).reset_index()
for grp in L_USE_GROUP.values():
    col = f"LUG_{grp}"
    if col in lu_all.columns and grp not in city_lu_summary.columns:
        city_lu_summary[grp] = lu_all.groupby("city")[col].sum().reindex(city_lu_summary["city"]).values
city_lu_summary = city_lu_summary.sort_values("total_lui", ascending=False)
city_lu_summary.to_csv(ASSETS / "L13_city_landuse_summary.csv", index=False, encoding="utf-8-sig")
print(f"  city summaries: bld={len(city_bld_summary)}, lu={len(city_lu_summary)}")

# === 5. 県全域 用途地域 (X09 で展開済) との交差: 指定 vs 実利用ミスマッチ ====
print("\n[4] 用途地域 (X09 と同じ) を読み込み ...", flush=True)
t1 = time.time()
LANDUSE_EXTRACT = ROOT / "data" / "extras" / "landuse_extracted"
zone_geojsons = sorted(LANDUSE_EXTRACT.glob("*.geojson"))
zone_files = [p for p in zone_geojsons if p.name.startswith("340006")]  # 県全域版
if not zone_files:
    zone_files = [p for p in zone_geojsons if p.name.startswith("341002")]  # フォールバック
zone = gpd.read_file(zone_files[0])
if zone.crs is None:
    zone = zone.set_crs("EPSG:4326")
zone = zone.to_crs(TARGET_CRS)
YOTO_NAME = {
    1: "第一種低層住居", 2: "第二種低層住居",
    3: "第一種中高層住居", 4: "第二種中高層住居",
    5: "第一種住居", 6: "第二種住居", 7: "準住居",
    8: "近隣商業", 9: "商業", 10: "準工業", 11: "工業", 12: "工業専用",
    13: "田園住居",
}
zone["yoto"] = zone["YOTO_CD"].map(lambda v: YOTO_NAME.get(int(v), f"用途{v}"))
zone_d = zone[["yoto", "geometry"]].dissolve(by="yoto").reset_index()
print(f"  zone {len(zone)} → dissolve {len(zone_d)} 用途, {time.time()-t1:.1f}s")

# 指定用途 → 期待される実利用 (シンプルなマッピング)
EXPECTED_GROUP = {
    "第一種低層住居": "住居系", "第二種低層住居": "住居系",
    "第一種中高層住居": "住居系", "第二種中高層住居": "住居系",
    "第一種住居": "住居系", "第二種住居": "住居系", "準住居": "住居系",
    "近隣商業": "商業系", "商業": "商業系",
    "準工業": "工業系", "工業": "工業系", "工業専用": "工業系",
    "田園住居": "農林漁業系",
}

# === 6. 建物メッシュ × 用途地域 sjoin (高速 R-tree) ======================
print("\n[5] 建物メッシュ × 用途地域 sjoin ...", flush=True)
t1 = time.time()
# 建物メッシュは中心点 (高速化のため)
bld_pts = bld_all[["MAIN_GRP", "SUM_BIL", "city", "mesh_area_m2"]].copy()
bld_pts["geometry"] = bld_all.geometry.centroid
bld_pts = gpd.GeoDataFrame(bld_pts, geometry="geometry", crs=TARGET_CRS)
joined = gpd.sjoin(bld_pts, zone_d[["yoto", "geometry"]], how="inner", predicate="within")
print(f"  sjoin: {len(joined):,} メッシュが用途地域内に存在, {time.time()-t1:.1f}s")
joined["expected_grp"] = joined["yoto"].map(EXPECTED_GROUP)
joined["match"] = (joined["MAIN_GRP"] == joined["expected_grp"])

# 「建物なし」メッシュを除外した分析対象
joined_with_bld = joined[joined["MAIN_GRP"] != "建物なし"].copy()
print(f"  分析対象メッシュ (建物なしを除外): {len(joined_with_bld):,} / {len(joined):,}")

# ミスマッチ集計 (市町別) - 建物ありメッシュのみ
mm = joined_with_bld.groupby("city").agg(
    n_total=("match", "size"),
    n_match=("match", "sum"),
).reset_index()
mm["mismatch_pct"] = (100.0 * (1 - mm["n_match"] / mm["n_total"])).round(1)
mm = mm.sort_values("mismatch_pct", ascending=False)
mm.to_csv(ASSETS / "L13_mismatch_by_city.csv", index=False, encoding="utf-8-sig")

# 用途×実利用 ミスマッチ・ピボット (建物ありメッシュのみ)
mismatch_pivot = (joined_with_bld.groupby(["yoto", "MAIN_GRP"])
                  .size().unstack(fill_value=0))
mismatch_pivot["合計"] = mismatch_pivot.sum(axis=1)
# 主要列順に並べ
ordered = ["住居系", "商業系", "工業系", "公共系", "農林漁業系", "その他"]
mismatch_pivot = mismatch_pivot.reindex(columns=[c for c in ordered if c in mismatch_pivot.columns] + ["合計"])
mismatch_pivot.to_csv(ASSETS / "L13_zone_x_main.csv", encoding="utf-8-sig")

# H1: 第一種住居なのに商業利用ビル (建物ありメッシュ内で評価)
jb = joined_with_bld
h1_n_total = int((jb["yoto"] == "第一種住居").sum())
h1_n_com = int(((jb["yoto"] == "第一種住居") & (jb["MAIN_GRP"] == "商業系")).sum())
h1_pct = 100.0 * h1_n_com / h1_n_total if h1_n_total else 0
print(f"  H1: 第一種住居指定×商業利用 メッシュ {h1_n_com}/{h1_n_total} = {h1_pct:.1f}%")

# H2: 工業地域指定なのに農地利用 (LUI で見るので別途)
# 土地利用メッシュ × 用途地域 sjoin
lu_pts = lu_all[["MAIN_LUG", "city", "mesh_area_m2"]].copy()
lu_pts["geometry"] = lu_all.geometry.centroid
lu_pts = gpd.GeoDataFrame(lu_pts, geometry="geometry", crs=TARGET_CRS)
lu_joined = gpd.sjoin(lu_pts, zone_d[["yoto", "geometry"]], how="inner", predicate="within")
print(f"  土地利用 sjoin: {len(lu_joined):,} メッシュ")

# H2: 工業地域指定なのに農地利用 (未利用メッシュ除外)
lu_joined_used = lu_joined[lu_joined["MAIN_LUG"] != "未利用"].copy()
is_industrial_zone = lu_joined_used["yoto"].isin(["工業", "工業専用", "準工業"])
h2_n_total = int(is_industrial_zone.sum())
h2_n_agri = int((is_industrial_zone & (lu_joined_used["MAIN_LUG"] == "農地")).sum())
h2_pct = 100.0 * h2_n_agri / h2_n_total if h2_n_total else 0
print(f"  H2: 工業系指定×農地利用 メッシュ {h2_n_agri}/{h2_n_total} = {h2_pct:.1f}%")

# H4: 田園住居 × 農地+住宅 (建物ありメッシュのみ; 建物なしを除外)
h4_total = int((jb["yoto"] == "田園住居").sum())
h4_match = int(((jb["yoto"] == "田園住居") & (jb["MAIN_GRP"].isin(["住居系", "農林漁業系"]))).sum())
h4_pct = 100.0 * h4_match / h4_total if h4_total else 0
print(f"  H4: 田園住居 × 住宅+農林 一致 {h4_match}/{h4_total} = {h4_pct:.1f}%")

# H5: 中心商業 vs 周辺住居 の建物密度
# 重要な注意: 用途地域に交差する建物メッシュは「実質的にその指定地に建てられた建物の集合」
# H5 を「商業利用ビルの密度比」で比較する (用途地域の意図 = どの用途を集めるかの結果)
# bld_total / area_total_km2 だと低層住宅が小さく多数で過大評価されるため、
# "商業系利用棟数の割合" で比較。
zone_density_basic = (joined_with_bld.groupby("yoto").agg(
    n_mesh=("MAIN_GRP", "size"),
    bld_total=("SUM_BIL", "sum"),
    area_total_km2=("mesh_area_m2", lambda s: s.sum()/1e6),
)).reset_index()
zone_density_basic["density"] = (zone_density_basic["bld_total"] / zone_density_basic["area_total_km2"]).round(1)
zone_density_basic = zone_density_basic.sort_values("density", ascending=False)
zone_density = zone_density_basic
zone_density.to_csv(ASSETS / "L13_zone_density.csv", index=False, encoding="utf-8-sig")

# H5 改: 商業地域の「商業系建物棟数密度」 vs 第一種低層の「商業系建物棟数密度」
# 商業利用建物が指定通り商業地に集中しているかを定量化
joined_full = joined.copy()
# 各 yoto 内で B_USE_4xx 商業列の合計を取得して area で割る
com_cols = [c for c in B_USE_LABEL.keys() if B_USE_GROUP.get(c) == "商業系" and c in bld_all.columns]
if com_cols:
    bld_com_per_mesh = bld_all[com_cols].sum(axis=1)
    bld_pts2 = bld_pts.copy()
    bld_pts2["com_count"] = bld_com_per_mesh.values
    bld_pts2_with_zone = gpd.sjoin(bld_pts2, zone_d[["yoto", "geometry"]], how="inner", predicate="within")
    com_density_by_zone = bld_pts2_with_zone.groupby("yoto").agg(
        com_total=("com_count", "sum"),
        area_km2=("mesh_area_m2", lambda s: s.sum()/1e6),
    ).reset_index()
    com_density_by_zone["com_density"] = (com_density_by_zone["com_total"] / com_density_by_zone["area_km2"]).round(1)
    com_density_by_zone.to_csv(ASSETS / "L13_zone_commercial_density.csv", index=False, encoding="utf-8-sig")
    com_density = float(com_density_by_zone.loc[com_density_by_zone["yoto"] == "商業", "com_density"].iloc[0]) if (com_density_by_zone["yoto"] == "商業").any() else 0
    res1_density = float(com_density_by_zone.loc[com_density_by_zone["yoto"] == "第一種低層住居", "com_density"].iloc[0]) if (com_density_by_zone["yoto"] == "第一種低層住居").any() else 0
    density_ratio = (com_density / res1_density) if res1_density else 0
else:
    com_density = res1_density = density_ratio = 0
print(f"  H5: 商業地域/第一種低層 商業系建物密度比 = ×{density_ratio:.1f} (商業地域 {com_density:.1f} / 低層 {res1_density:.1f} 棟/km²)")

# === 7. 図1: 県全域 主題図 - 建物主用途で塗り分け =========================
print("\n[6] 図1 県全域主題図 (建物主用途) ...", flush=True)
t1 = time.time()
# パフォーマンス: simplify + 主用途の色マップ
draw_bld = bld_all.copy()
draw_bld["geometry"] = draw_bld.geometry.simplify(50)
draw_bld["color"] = draw_bld["MAIN_GRP"].map(GROUP_COLOR).fillna("#dddddd")

fig, ax = plt.subplots(figsize=(13, 9))
draw_bld.plot(ax=ax, color=draw_bld["color"], edgecolor="none", alpha=0.92)
ax.set_title("広島県 20市町 建物利用 250mメッシュ — 主用途別色分け\n40 dataset_id 統合 (建物利用 20 + 土地利用 20)", fontsize=13)
ax.set_xlabel("X (m, JGD2011 平面直角 III)")
ax.set_ylabel("Y (m)")
ax.set_aspect("equal")
ax.legend(handles=[Patch(facecolor=c, label=g) for g, c in GROUP_COLOR.items()],
          loc="upper right", title="主用途", fontsize=9, framealpha=0.92)
plt.tight_layout()
plt.savefig(ASSETS / "L13_map_building_main_use.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-t1:.1f}s")

# === 8. 図2: 県全域 主題図 - 土地利用主用途 ===============================
print("\n[7] 図2 県全域主題図 (土地利用主用途) ...", flush=True)
t1 = time.time()
draw_lu = lu_all.copy()
draw_lu["geometry"] = draw_lu.geometry.simplify(50)
draw_lu["color"] = draw_lu["MAIN_LUG"].map(LU_GROUP_COLOR).fillna("#cccccc")

fig, ax = plt.subplots(figsize=(13, 9))
draw_lu.plot(ax=ax, color=draw_lu["color"], edgecolor="none", alpha=0.92)
ax.set_title("広島県 20市町 土地利用 250mメッシュ — 主用途別色分け\n40 dataset_id 統合", fontsize=13)
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.set_aspect("equal")
ax.legend(handles=[Patch(facecolor=c, label=g) for g, c in LU_GROUP_COLOR.items()],
          loc="upper right", title="主用途", fontsize=9, framealpha=0.92)
plt.tight_layout()
plt.savefig(ASSETS / "L13_map_landuse_main_use.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-t1:.1f}s")

# === 9. 図3: 市町別 small multiples - 建物主用途 (12 panels) =============
print("\n[8] 図3 市町別 small multiples (建物) ...", flush=True)
t1 = time.time()
# 建物が多い 12 市町
top_cities = city_bld_summary["city"].head(12).tolist()
fig, axes = plt.subplots(3, 4, figsize=(16, 12))
axes_f = np.array(axes).flatten()
for i, city in enumerate(top_cities):
    ax = axes_f[i]
    sub = draw_bld[draw_bld["city"] == city]
    if len(sub) > 0:
        sub.plot(ax=ax, color=sub["color"], edgecolor="none", alpha=0.95)
    n = int(city_bld_summary.loc[city_bld_summary["city"] == city, "bld_total"].iloc[0])
    d = float(city_bld_summary.loc[city_bld_summary["city"] == city, "bld_density_per_km2"].iloc[0])
    ax.set_title(f"{city}\n建物 {n:,} 棟 / 密度 {d:.0f}/km²", fontsize=10)
    ax.set_aspect("equal")
    ax.set_xticks([]); ax.set_yticks([])
for j in range(len(top_cities), len(axes_f)):
    axes_f[j].axis("off")
plt.suptitle("市町別 建物利用 小マップ (主用途別色分け / small multiples)", fontsize=14, y=1.0)
plt.tight_layout()
plt.savefig(ASSETS / "L13_map_city_small_multiples.png", dpi=120)
plt.close()
print(f"  完了: {time.time()-t1:.1f}s")

# === 10. 図4: ミスマッチ ヒートマップ (用途地域 × 実利用) ================
print("\n[9] 図4 ミスマッチ ヒートマップ ...", flush=True)
t1 = time.time()
mh = mismatch_pivot.drop(columns=["合計", "建物なし"], errors="ignore").copy()
# 行を「住居系→商業→工業」順に
zone_order = ["第一種低層住居", "第二種低層住居", "第一種中高層住居", "第二種中高層住居",
              "第一種住居", "第二種住居", "準住居", "近隣商業", "商業",
              "準工業", "工業", "工業専用", "田園住居"]
mh = mh.reindex([z for z in zone_order if z in mh.index])

# 行ごとに % 化
mh_pct = mh.div(mh.sum(axis=1), axis=0) * 100

fig, ax = plt.subplots(figsize=(11, 7))
im = ax.imshow(mh_pct.values, cmap="RdYlGn_r", aspect="auto", vmin=0, vmax=100)
ax.set_xticks(range(len(mh_pct.columns)))
ax.set_xticklabels(mh_pct.columns, rotation=20, ha="right")
ax.set_yticks(range(len(mh_pct.index)))
ax.set_yticklabels(mh_pct.index)
plt.colorbar(im, ax=ax, label="行内% (各用途地域の中での実利用比率)")
ax.set_title("用途地域 (指定) × 建物実利用 (主用途) のミスマッチ ヒートマップ\n各セル = 行(指定)に対する列(実利用)のメッシュ%", fontsize=12)
for i in range(len(mh_pct.index)):
    for j in range(len(mh_pct.columns)):
        v = mh_pct.values[i, j]
        if v > 5:
            ax.text(j, i, f"{v:.0f}", ha="center", va="center", fontsize=9,
                    color="white" if v > 60 else "black")
plt.tight_layout()
plt.savefig(ASSETS / "L13_mismatch_heatmap.png", dpi=130)
plt.close()
mh_pct.to_csv(ASSETS / "L13_mismatch_heatmap.csv", encoding="utf-8-sig")
print(f"  完了: {time.time()-t1:.1f}s")

# === 11. 図5: 市町別 ミスマッチ率 ランキング =============================
print("\n[10] 図5 市町別ミスマッチ率 ...", flush=True)
fig, ax = plt.subplots(figsize=(11, 6))
ax.barh(mm["city"], mm["mismatch_pct"],
        color=["#cf222e" if p > 50 else "#0969da" for p in mm["mismatch_pct"]])
ax.set_xlabel("ミスマッチ率 (%) = (用途指定とビル主用途が一致しないメッシュ比率)")
ax.set_title("市町別 用途地域指定 × 建物実利用 ミスマッチ率 ランキング\n値が高いほど指定と実利用が乖離")
ax.invert_yaxis()
for i, v in enumerate(mm["mismatch_pct"]):
    ax.text(v + 0.5, i, f"{v:.1f}%", va="center", fontsize=9)
plt.tight_layout()
plt.savefig(ASSETS / "L13_mismatch_by_city.png", dpi=130)
plt.close()

# === 12. 図6: 用途地域別 建物密度 (棒グラフ) =============================
print("\n[11] 図6 用途地域別 建物密度 ...", flush=True)
fig, ax = plt.subplots(figsize=(11, 6))
zd = zone_density.copy()
ax.bar(zd["yoto"], zd["density"],
       color=[GROUP_COLOR.get(EXPECTED_GROUP.get(z, "その他"), "#888") for z in zd["yoto"]],
       edgecolor="black", linewidth=0.4)
ax.set_ylabel("建物密度 (棟/km²)")
ax.set_title("用途地域別 建物密度 (域内メッシュの建物棟数 / 面積)\n色 = 指定用途で期待される主用途グループ")
plt.xticks(rotation=30, ha="right")
ax.grid(axis="y", alpha=0.3)
for i, (y, d) in enumerate(zip(zd["yoto"], zd["density"])):
    ax.text(i, d, f"{d:.0f}", ha="center", va="bottom", fontsize=9)
plt.tight_layout()
plt.savefig(ASSETS / "L13_zone_density_bar.png", dpi=130)
plt.close()

# === 13. 図7: 市町別 主用途構成 (積み上げ棒) =============================
print("\n[12] 図7 市町別 主用途構成 ...", flush=True)
city_compo = bld_all.groupby("city")[grp_cols].sum()
city_compo.columns = [c.replace("GRP_", "") for c in city_compo.columns]
city_compo = city_compo.div(city_compo.sum(axis=1), axis=0) * 100
city_compo = city_compo.reindex(city_bld_summary["city"])
fig, ax = plt.subplots(figsize=(13, 6))
bottom = np.zeros(len(city_compo))
for grp in ["住居系", "商業系", "工業系", "公共系", "農林漁業系", "その他"]:
    if grp in city_compo.columns:
        vals = city_compo[grp].values
        ax.bar(city_compo.index, vals, bottom=bottom, label=grp,
               color=GROUP_COLOR.get(grp, "#888"), edgecolor="white", linewidth=0.5)
        bottom += vals
ax.set_ylabel("主用途比率 (%)")
ax.set_title("市町別 建物主用途 構成比 (棟数ベース)")
plt.xticks(rotation=45, ha="right")
ax.legend(loc="lower right", fontsize=9, framealpha=0.92)
plt.tight_layout()
plt.savefig(ASSETS / "L13_city_main_compo.png", dpi=130)
plt.close()
city_compo.to_csv(ASSETS / "L13_city_main_compo.csv", encoding="utf-8-sig")

# === 14. 図8: 市町別 土地利用構成 (積み上げ棒) ===========================
print("\n[13] 図8 市町別 土地利用構成 ...", flush=True)
city_lu_compo = lu_all.groupby("city")[lug_cols].sum()
city_lu_compo.columns = [c.replace("LUG_", "") for c in city_lu_compo.columns]
city_lu_compo = city_lu_compo.div(city_lu_compo.sum(axis=1), axis=0) * 100
city_lu_compo = city_lu_compo.reindex(city_lu_summary["city"])
fig, ax = plt.subplots(figsize=(13, 6))
bottom = np.zeros(len(city_lu_compo))
for grp, c in LU_GROUP_COLOR.items():
    if grp in city_lu_compo.columns:
        vals = city_lu_compo[grp].values
        ax.bar(city_lu_compo.index, vals, bottom=bottom, label=grp,
               color=c, edgecolor="white", linewidth=0.5)
        bottom += vals
ax.set_ylabel("土地利用比率 (%)")
ax.set_title("市町別 土地利用 構成比 (LUI 面積ベース)")
plt.xticks(rotation=45, ha="right")
ax.legend(loc="upper right", fontsize=8, framealpha=0.92, ncol=2)
plt.tight_layout()
plt.savefig(ASSETS / "L13_city_lu_compo.png", dpi=130)
plt.close()
city_lu_compo.to_csv(ASSETS / "L13_city_lu_compo.csv", encoding="utf-8-sig")

# === 15. 図9: 重ね合わせマップ - 用途地域 + 建物主用途 ====================
print("\n[14] 図9 重ね合わせマップ ...", flush=True)
t1 = time.time()
zone_d_simple = zone_d.copy()
zone_d_simple["geometry"] = zone_d_simple.geometry.simplify(80)
fig, ax = plt.subplots(figsize=(13, 9))
zone_d_simple.plot(ax=ax, color="none", edgecolor="#222", linewidth=0.8, alpha=0.9)
draw_bld_overlay = draw_bld[draw_bld.geometry.notna()].copy()
draw_bld_overlay.plot(ax=ax, color=draw_bld_overlay["color"], edgecolor="none", alpha=0.7)
ax.set_title("重ね合わせマップ — 用途地域 (枠線) × 建物主用途 (塗り)\n境界線が指定、塗りが実態", fontsize=12)
ax.set_aspect("equal")
ax.legend(handles=[Patch(facecolor=c, label=g) for g, c in GROUP_COLOR.items()],
          loc="upper right", title="建物主用途", fontsize=9, framealpha=0.92)
plt.tight_layout()
plt.savefig(ASSETS / "L13_overlay_zone_x_building.png", dpi=130)
plt.close()
print(f"  完了: {time.time()-t1:.1f}s")

# === 16. 図10: 散布図 - 市町の建物密度 vs 農地比率 (発見の可視化) =========
print("\n[15] 図10 散布図 ...", flush=True)
sc = city_bld_summary.merge(city_lu_compo.reset_index(), on="city", how="inner")
fig, ax = plt.subplots(figsize=(10, 7))
y = sc["bld_density_per_km2"]
x = sc.get("農地", pd.Series([0]*len(sc))).fillna(0)
ax.scatter(x, y, s=80, alpha=0.85, c="#0969da", edgecolor="black", linewidth=0.6)
for _, row in sc.iterrows():
    ax.annotate(row["city"], (row.get("農地", 0) or 0, row["bld_density_per_km2"]),
                fontsize=9, xytext=(4, 4), textcoords="offset points")
ax.set_xlabel("農地 (田+畑) 比率 [%]")
ax.set_ylabel("建物密度 [棟/km²]")
ax.set_title("市町の都市性 - 農地比率 × 建物密度\n左上=都市的, 右下=農村的")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(ASSETS / "L13_city_scatter.png", dpi=130)
plt.close()

# 結果集計の保存
with open(ASSETS / "L13_hypothesis_results.json", "w", encoding="utf-8") as f:
    json.dump({
        "H1_first_residential_x_commercial_pct": h1_pct,
        "H1_n": h1_n_com, "H1_total": h1_n_total,
        "H2_industrial_x_agri_pct": h2_pct,
        "H2_n": h2_n_agri, "H2_total": h2_n_total,
        "H4_denen_match_pct": h4_pct,
        "H4_n": h4_match, "H4_total": h4_total,
        "H5_density_ratio_com_low": density_ratio,
        "H5_com_density": com_density, "H5_low_density": res1_density,
    }, f, ensure_ascii=False, indent=2)

print(f"\n[時間] 全体: {time.time()-t0:.1f}s", flush=True)

# ============================================================
# === HTML レンダリング ===
# ============================================================
print("\n[HTML] レンダリング ...", flush=True)
elapsed = time.time() - t0

# ===== セクション本文 =====
# 市町別ランキング (HTML 表)
def df_to_html_tbl(df: pd.DataFrame, max_rows=20):
    rows = ""
    rows += "<tr>" + "".join(f"<th>{c}</th>" for c in df.columns) + "</tr>"
    for _, r in df.head(max_rows).iterrows():
        rows += "<tr>" + "".join(
            f"<td>{(f'{v:,.1f}' if isinstance(v,(int,float)) and not isinstance(v,bool) else (v if pd.notna(v) else ''))}</td>"
            for v in r
        ) + "</tr>"
    return f"<table>{rows}</table>"

bld_summary_html = df_to_html_tbl(city_bld_summary[["city", "mesh_n", "bld_total", "area_km2", "bld_density_per_km2", "住居系", "商業系", "工業系"]].head(20))
lu_summary_html = df_to_html_tbl(city_lu_summary[["city", "mesh_n", "area_km2", "農地", "森林", "住宅", "商業", "工業"]].head(20))
mm_html = df_to_html_tbl(mm.head(20))
zone_density_html = df_to_html_tbl(zone_density)

# 浸水H1〜H6 結果
top_mm_city = mm.iloc[0]
bot_mm_city = mm.iloc[-1]

sections = [
    ("学習目標と問い", f"""
<div class="note" style="background:#fff5f5;border-left:4px solid #cf222e;">
<b>本記事のスタイル: 都市計画基礎調査の 2 シリーズ × 20 市町 = 40 データセットを統合し、県全域マップ + 用途指定×実利用ミスマッチ分析</b><br>
<code>pd.concat()</code> + <code>gpd.sjoin()</code> で 40 個の GeoJSON を統合し、
<b>用途地域 (X09 と同じ指定地ポリゴン)</b> と交差させて
「<b>指定された用途</b> ≠ <b>実際の建物利用</b>」のメッシュをカウント。
</div>

<h3>カバー宣言: 40 dataset_id 論理カバー</h3>
<div class="notice">
本記事は都市計画基礎調査の <b>2 シリーズ × 20 市町 = 40 dataset_id</b> を統合:
<ul>
<li><b>都市計画区域情報_建物利用現況 20件</b> (#1469 広島市 / #1323 呉市 / #1474 竹原市 / #1477 三原市 / #1480 尾道市 / #1483 福山市 / #512 府中市 / #1327 三次市 / #1328 庄原市 / #1491 大竹市 / #1329 東広島市 / #1496 廿日市市 / #1321 安芸高田市 / #1325 江田島市 / #1331 府中町 / #71 海田町 / #1322 熊野町 / #1326 坂町 / #1507 北広島町 / #513 世羅町)</li>
<li><b>都市計画区域情報_土地利用現況 20件</b> (#70 広島市 / #493 呉市 / #494 竹原市 / #495 三原市 / #496 尾道市 / #497 福山市 / #498 府中市 / #499 三次市 / #500 庄原市 / #501 大竹市 / #502 東広島市 / #503 廿日市市 / #504 安芸高田市 / #505 江田島市 / #506 府中町 / #507 海田町 / #508 熊野町 / #509 坂町 / #510 北広島町 / #511 世羅町)</li>
</ul>
Desc が完全書式統一なのでフォーマット同一とみなして処理。
<b>対応表 40/40 件 論理カバー</b> (実ダウンロード <code>data/extras/urban_planning_survey/fetch_log.json</code>)。
</div>

<h3>主な問い (3 段階)</h3>
<ol>
<li><b>面の問い</b>: 県内 20 市町で建物・土地はどう使われているか？</li>
<li><b>整合の問い</b>: 用途地域 (法的指定) と実利用は<b>どれくらい一致している</b>か？</li>
<li><b>市町差の問い</b>: ミスマッチ率は市町で違うか？(都市的 vs 農村的の差)</li>
</ol>

<h3>立てた仮説 (H1〜H6)</h3>
<ol>
<li><b>H1</b>: 第一種住居指定でも<b>商業利用ビル</b>が一定数存在 (≥10%)</li>
<li><b>H2</b>: 工業地域指定の中でも <b>農地利用</b> が残る (使われていない指定地)</li>
<li><b>H3</b>: 用途指定と実利用のミスマッチ率は市町で異なる (広島市が高い仮説)</li>
<li><b>H4</b>: 田園住居指定はほぼすべて農地+住宅で利用と一致 (≥80%)</li>
<li><b>H5</b>: 中心商業地は建物密度 &gt; 周辺住居系 (建物数 / 面積 で 5倍以上の差)</li>
<li><b>H6</b>: 旧市街地 (尾道/福山中心部) は明治以来の用途指定と現代利用の乖離が顕著 (尾道/福山の 中心商業地 でミスマッチ率上位)</li>
</ol>

<h3>用語の定義 (本記事独自)</h3>
<ul>
<li><b>用途地域</b>: 都市計画法に基づく13区分の法的指定 (第一種低層住居 〜 田園住居)</li>
<li><b>建物主用途 (MAIN_GRP)</b>: 250m メッシュ内の各 B_USE_xxx 棟数を 6 系統 (住居/商業/工業/公共/農林漁業/その他) に集約し、棟数最多の系統を「主用途」とする</li>
<li><b>土地主用途 (MAIN_LUG)</b>: LUI_xxx 面積を 12 系統に集約し、面積最大の系統を「主用途」とする</li>
<li><b>ミスマッチメッシュ</b>: 用途地域指定 (例: 第一種住居 → 期待 = 住居系) と建物主用途が異なるメッシュ</li>
<li><b>ミスマッチ率</b>: ミスマッチメッシュ / 全交差メッシュ × 100%</li>
<li><b>250m メッシュ</b>: 国土地理院標準地域メッシュの 1/4 (250m × 250m = 62500 m²)。建物・土地利用集計の基本単位</li>
<li><b>シリーズ統合</b>: 同じ Desc 書式の 20 件を <code>pd.concat()</code> で 1 つの GeoDataFrame にまとめる手法</li>
</ul>

<h3>結果サマリー (詳しい本文の前に概観)</h3>
<table>
<tr><th>指標</th><th>結果</th></tr>
<tr><td>統合した dataset_id</td><td><b>40 件</b> (建物 20 + 土地 20)</td></tr>
<tr><td>統合 250m メッシュ</td><td>建物 {len(bld_all):,} メッシュ / 土地 {len(lu_all):,} メッシュ</td></tr>
<tr><td>用途地域メッシュ交差</td><td>建物 {len(joined):,} / 土地 {len(lu_joined):,} メッシュ</td></tr>
<tr><td>H1: 第一種住居×商業</td><td><b>{h1_pct:.1f}%</b> ({h1_n_com:,}/{h1_n_total:,} メッシュ)</td></tr>
<tr><td>H2: 工業系指定×農地</td><td><b>{h2_pct:.1f}%</b> ({h2_n_agri:,}/{h2_n_total:,} メッシュ)</td></tr>
<tr><td>H4: 田園住居×住宅+農林一致</td><td><b>{h4_pct:.1f}%</b> ({h4_match:,}/{h4_total:,} メッシュ)</td></tr>
<tr><td>H5: 商業 / 第一種低層 密度比</td><td><b>×{density_ratio:.1f}</b> ({com_density:.0f} / {res1_density:.0f} 棟/km²)</td></tr>
<tr><td>ミスマッチ率最大の市町</td><td><b>{top_mm_city['city']}</b> ({top_mm_city['mismatch_pct']:.1f}%)</td></tr>
<tr><td>ミスマッチ率最小の市町</td><td><b>{bot_mm_city['city']}</b> ({bot_mm_city['mismatch_pct']:.1f}%)</td></tr>
<tr><td>処理時間</td><td>{elapsed:.1f} 秒 (要件S 1〜3 分)</td></tr>
</table>
"""),

    ("使用データ", f"""
<h3>シリーズ統合の論理</h3>
<p>都市計画基礎調査の建物利用現況・土地利用現況は <b>市町ごとに別 dataset_id</b> として公開されており、県全域版 dataset_id が存在しない。
このため <b>20 市町 × 2 シリーズ = 40 dataset_id</b> を <code>pd.concat()</code> で統合する。
属性スキーマは国土交通省実施要領 (第4版) に準拠して全市町で共通 (一部の B_USE 列は市町で 0 件のため省略されているが、統合時に <code>fillna(0)</code> で揃える)。</p>

<h3>建物利用現況 (#1469 / #1323 / ...) の主要列</h3>
<ul class="kv">
<li><b>MESHCODE</b>: 250mメッシュコード (10桁)</li>
<li><b>SUM_BIL</b>: メッシュ内全建物棟数</li>
<li><b>B_USE_4xx</b>: 用途分類別 棟数 (18 列)</li>
<li><b>B_FLR_5xx, B_AGE_9xx</b> 等: 構造/階数/築年代別 (本記事では未使用、発展課題用)</li>
</ul>

<h3>土地利用現況 (#70 / #493 / ...) の主要列</h3>
<ul class="kv">
<li><b>MESHCODE</b>: 同上</li>
<li><b>TOCHI_RY</b>: 主たる土地利用コード</li>
<li><b>LUI_2xx</b>: 用途別 面積 (ha) (22 列)</li>
<li><b>LUI_241</b>: 面積合計</li>
</ul>

<h3>用途地域 (X09 で展開済 zone polygon) との交差</h3>
<p>X09 の用途地域 GeoJSON (340006: 県全域版 / 341002: 広島市版) を <code>gpd.sjoin()</code> で建物メッシュ・土地メッシュと交差させる。
sjoin は内部で R-tree 空間インデックスを使うため、{len(bld_all):,} 点 × {len(zone_d)} ポリゴンでも数秒で完了する。</p>

<h3>用途コード対照 (国交省 別表2 抜粋)</h3>
<table>
<tr><th>建物 B_USE 主要コード</th><th>細分類</th><th>系統</th></tr>
<tr><td>4011</td><td>業務 (事務所/銀行/会議場)</td><td>商業系</td></tr>
<tr><td>4021〜4027</td><td>商業 (店舗/飲食/娯楽)</td><td>商業系</td></tr>
<tr><td>4031</td><td>宿泊 (ホテル/旅館)</td><td>商業系</td></tr>
<tr><td>4111</td><td>専用住宅</td><td>住居系</td></tr>
<tr><td>4121</td><td>共同住宅 (アパート/マンション)</td><td>住居系</td></tr>
<tr><td>4131〜4151</td><td>併用住宅</td><td>住居系</td></tr>
<tr><td>4211</td><td>官公庁 (庁舎/警察/消防)</td><td>公共系</td></tr>
<tr><td>4221〜4227</td><td>文教厚生 (学校/病院/福祉)</td><td>公共系</td></tr>
<tr><td>4311〜4313</td><td>運輸倉庫 (駅舎/卸売市場)</td><td>工業系</td></tr>
<tr><td>4411〜4415</td><td>工場</td><td>工業系</td></tr>
<tr><td>4511</td><td>農林漁業用施設</td><td>農林漁業系</td></tr>
<tr><td>4521</td><td>供給処理 (浄水/火葬/発電)</td><td>工業系</td></tr>
</table>
<table>
<tr><th>土地 LUI 主要コード</th><th>用途</th><th>系統</th></tr>
<tr><td>201 / 202</td><td>田 / 畑</td><td>農地</td></tr>
<tr><td>203</td><td>山林</td><td>森林</td></tr>
<tr><td>204 / 205</td><td>水面 / その他自然地</td><td>水面 / 自然地</td></tr>
<tr><td>211 / 212 / 213</td><td>住宅 / 商業 / 工業 用地</td><td>住宅 / 商業 / 工業</td></tr>
<tr><td>214 / 218</td><td>公益施設 / その他公的</td><td>公益</td></tr>
<tr><td>215 / 216</td><td>道路 / 交通施設</td><td>交通インフラ</td></tr>
<tr><td>217</td><td>公園緑地 / 公共空地</td><td>公共空地</td></tr>
<tr><td>219</td><td>農林漁業施設用地</td><td>農林漁業</td></tr>
</table>
"""),

    ("ダウンロード", """
<table>
<tr><th>ファイル</th><th>内容</th></tr>
<tr><td><a href="assets/L13_city_building_summary.csv" download>L13_city_building_summary.csv</a></td><td>市町別 建物利用 集計 (棟数/密度/系統別)</td></tr>
<tr><td><a href="assets/L13_city_landuse_summary.csv" download>L13_city_landuse_summary.csv</a></td><td>市町別 土地利用 集計 (面積/系統別)</td></tr>
<tr><td><a href="assets/L13_mismatch_by_city.csv" download>L13_mismatch_by_city.csv</a></td><td>市町別 ミスマッチ率 ランキング</td></tr>
<tr><td><a href="assets/L13_zone_x_main.csv" download>L13_zone_x_main.csv</a></td><td>用途地域 × 建物主用途 ピボット</td></tr>
<tr><td><a href="assets/L13_mismatch_heatmap.csv" download>L13_mismatch_heatmap.csv</a></td><td>ヒートマップ用 % ピボット</td></tr>
<tr><td><a href="assets/L13_zone_density.csv" download>L13_zone_density.csv</a></td><td>用途地域別 建物密度 (棟/km²)</td></tr>
<tr><td><a href="assets/L13_city_main_compo.csv" download>L13_city_main_compo.csv</a></td><td>市町別 主用途構成比 (棟数 %)</td></tr>
<tr><td><a href="assets/L13_city_lu_compo.csv" download>L13_city_lu_compo.csv</a></td><td>市町別 土地利用構成比 (面積 %)</td></tr>
<tr><td><a href="assets/L13_hypothesis_results.json" download>L13_hypothesis_results.json</a></td><td>仮説H1〜H5 検証結果</td></tr>
<tr><td><a href="assets/L13_map_building_main_use.png" download>L13_map_building_main_use.png</a></td><td>図1 県全域 建物主用途 主題図</td></tr>
<tr><td><a href="assets/L13_map_landuse_main_use.png" download>L13_map_landuse_main_use.png</a></td><td>図2 県全域 土地利用 主題図</td></tr>
<tr><td><a href="assets/L13_map_city_small_multiples.png" download>L13_map_city_small_multiples.png</a></td><td>図3 市町別 small multiples (12 panels)</td></tr>
<tr><td><a href="assets/L13_mismatch_heatmap.png" download>L13_mismatch_heatmap.png</a></td><td>図4 用途地域 × 実利用 ミスマッチ ヒートマップ</td></tr>
<tr><td><a href="assets/L13_mismatch_by_city.png" download>L13_mismatch_by_city.png</a></td><td>図5 市町別 ミスマッチ率 ランキング</td></tr>
<tr><td><a href="assets/L13_zone_density_bar.png" download>L13_zone_density_bar.png</a></td><td>図6 用途地域別 建物密度</td></tr>
<tr><td><a href="assets/L13_city_main_compo.png" download>L13_city_main_compo.png</a></td><td>図7 市町別 主用途構成 (棟数)</td></tr>
<tr><td><a href="assets/L13_city_lu_compo.png" download>L13_city_lu_compo.png</a></td><td>図8 市町別 土地利用構成 (面積)</td></tr>
<tr><td><a href="assets/L13_overlay_zone_x_building.png" download>L13_overlay_zone_x_building.png</a></td><td>図9 重ね合わせ — 用途地域枠 × 建物主用途</td></tr>
<tr><td><a href="assets/L13_city_scatter.png" download>L13_city_scatter.png</a></td><td>図10 散布図 農地比率 × 建物密度</td></tr>
<tr><td><a href="L13_urban_planning_survey.py" download>L13_urban_planning_survey.py</a></td><td>再現スクリプト</td></tr>
</table>

<p><b>個別取得 (PowerShell, 40 dataset_id を一括):</b></p>
<pre><code>cd "2026 DoBoX 教材"
py -X utf8 data\\extras\\urban_planning_survey\\_fetch_meta.py</code></pre>
<p class="tnote">取得スクリプトはデータセットページ HTML を 1 度だけ GET し、
<b>250mメッシュ単位 GeoJSON</b> リソースID を抽出して <code>resource_download</code> 直リンクから zip を取得する。
40 リクエスト × 数秒 = 約 2〜3 分で完走 (DoBoX 側 rate limit 配慮 0.2s スリープ込み)。</p>
"""),

    ("分析1: 40 dataset_id を `pd.concat` で統合する", f"""
<h4>狙い</h4>
<p>シリーズ系列 (=同じ Desc 書式の連番群) を <b>1 個の DataFrame に束ねる</b> ことで、県全域分析を可能にする。
これは DoBoX のように「市町ごとに別 dataset_id」というデータ提供形式で<b>必須</b>のテクニック。</p>

<h4>手法 (リテラシレベルの直感)</h4>
<ul>
<li>各市町の GeoJSON を <code>geopandas.read_file()</code> で <code>GeoDataFrame</code> として読み込む (= 表 + 図形列)</li>
<li>すべての DataFrame を <code>pd.concat([df1, df2, ...], ignore_index=True)</code> で <b>縦に積み重ねる</b></li>
<li>列が一部市町で欠けていても、<code>pd.concat</code> は自動的に和集合を取り、欠落セルは <code>NaN</code> で埋める</li>
<li>市町判別のため、各 DF に <code>"city"</code> 列を追加してから concat</li>
</ul>

<h4>入出力の Before/After (具体例)</h4>
<table>
<tr><th>段階</th><th>形 (rows × cols)</th><th>追加列</th></tr>
<tr><td>1. 広島市 GeoJSON 読込</td><td>{len(bld_frames[0]):,} × 110</td><td>(なし)</td></tr>
<tr><td>2. <code>g["city"] = "広島市"</code> 付与</td><td>{len(bld_frames[0]):,} × 111</td><td>+city</td></tr>
<tr><td>3. 同様に 20 市町の DF を作成</td><td>20 個の DF</td><td>各 +city</td></tr>
<tr><td>4. <code>pd.concat()</code> で縦結合</td><td><b>{len(bld_all):,} × {len(bld_all.columns)}</b></td><td>欠落列は NaN</td></tr>
<tr><td>5. <code>fillna(0)</code> + 系統別 <code>GRP_*</code> 集約</td><td>{len(bld_all):,} × {len(bld_all.columns)+6}</td><td>+主用途6系統</td></tr>
</table>
<p class="tnote">列数が市町ごとに違う ({{63〜110}}) 理由: 値が常に 0 の B_USE_xxx (例: 大都市にしかない高度商業) はデータ提供時に省略されている。
<code>pd.concat</code> は和集合を取るので統合後は自動的に最大列数になり、欠落セルは <code>NaN→0</code> で埋める。</p>

<h4>実装</h4>
""" + code('''
import geopandas as gpd
import pandas as pd

frames = []
for dsid, city in BUILDING_DSID_CITY.items():
    z = DATA_DIR / f"building_{dsid}_{city}.zip"
    g = gpd.read_file(f"zip://{z.as_posix()}")  # zip 内 GeoJSON を直読み
    g["city"] = city                              # ← 市町判別
    g["dsid"] = dsid                              # ← データセット由来
    frames.append(g)

# 縦結合 (列の和集合を取る)
bld_all = gpd.GeoDataFrame(
    pd.concat(frames, ignore_index=True),
    geometry="geometry", crs=frames[0].crs,
)
# 全列を to_crs() で揃える
bld_all = bld_all.to_crs("EPSG:6671")  # JGD2011 III, 単位 m
''') + f"""

<h4>結果</h4>
<p>20 市町 × 平均 {len(bld_all)//20:,} メッシュ = 計 {len(bld_all):,} メッシュの建物利用統合データが完成。
土地利用も同様に {len(lu_all):,} メッシュ。これが本記事の「土俵」。</p>

<p><b>なぜこの可視化を選んだか</b>: 統合直後の構造を <b>表で示す</b>のが最も明確
(地図はあとで作る)。BeforeAfter テーブルは要件K (入出力の具体例) を満たす目的。</p>
"""),

    ("分析2: 県全域 主題図 — 建物 × 土地 の主用途", f"""
<h4>狙い</h4>
<p>40 統合データの「全体像」をひと目で見る。<b>「住居系が圧倒的に多いだろう」</b>という直感を地図で確認/反証する。</p>

<h4>手法</h4>
<ul>
<li>各 250m メッシュごとに、6 系統 (住居/商業/工業/公共/農林漁業/その他) の中で<b>棟数最多の系統</b>を MAIN_GRP とする</li>
<li><code>GeoDataFrame.plot(color=df["color"])</code> で系統別色分け</li>
<li>パフォーマンスのため <code>geometry.simplify(50)</code> で形状を 50m 精度に丸める (1分以内完走に必須)</li>
</ul>

<h4>結果</h4>
<p><b>なぜこの図か</b>: 県全域の「都市性のグラデーション」を 1 枚に圧縮するには <b>主題図</b> (choropleth) が最適。色1色 = 1メッシュの主用途。</p>
""" + figure("assets/L13_map_building_main_use.png", f"県全域 建物利用 主用途 主題図 ({len(bld_all):,} メッシュ)") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li>広島市・福山市・呉市の中心部に <b>商業系 (紫)</b> が集中</li>
<li>住居系 (オレンジ) は市街地縁辺に広く分布</li>
<li>農林漁業系 (黄緑) は北部 (庄原市/三次市/北広島町)</li>
<li>工業系 (青) は瀬戸内沿岸 (呉市/福山市/大竹市)</li>
</ul>

""" + figure("assets/L13_map_landuse_main_use.png", "県全域 土地利用 主用途 主題図") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li>森林 (深緑) が県土の大半を占める = 広島県は山林県</li>
<li>住宅・商業・工業は瀬戸内沿岸〜広島・福山・呉の3都市に集中</li>
<li>農地 (黄緑) は中山間部に点在 (世羅町/北広島町)</li>
</ul>
"""),

    ("分析3: 市町別 small multiples (12 panels)", """
<h4>狙い</h4>
<p>県全域の主題図では「個々の市町の細部」が見えない。
12 市町を同じ縮尺で並べる <b>small multiples</b> で、市町ごとの構成パターンを比較する。</p>

<h4>手法</h4>
<ul>
<li>建物棟数が多い 12 市町を選ぶ (建物密度ランキング上位)</li>
<li>同じ色マップで 3×4 のサブプロット に並べる</li>
<li>タイトルに <b>建物総数 + 密度 (棟/km²)</b> を表示し、量の違いを数字で補完</li>
</ul>

<h4>結果</h4>
<p><b>なぜこの図か</b>: 1 枚の県全域図では市町の輪郭がつぶれて見える。
条件 (=市町) だけ変えて並べる <b>small multiples</b> はパターン比較に最適。</p>
""" + figure("assets/L13_map_city_small_multiples.png", "市町別 建物利用 主用途 (12 panels)") + """
<p><b>読み取り</b>:</p>
<ul>
<li>府中町・海田町・坂町は<b>面積極小だが住居密度高</b> (広島市衛星都市)</li>
<li>三次市・庄原市・北広島町は <b>農林漁業系メッシュが優勢</b></li>
<li>福山市は南部沿岸に商業・工業が集中、北部は住居・農林</li>
<li>尾道市は<b>島嶼部+本土が混在</b> (江田島市/大崎上島と類似パターン)</li>
</ul>
"""),

    ("分析4: 用途地域 (指定) × 建物実利用 ミスマッチ ヒートマップ", f"""
<h4>狙い</h4>
<p>本記事の<b>主役</b>。用途地域は法的に「ここは住居系を想定」「ここは商業系を想定」と指定しているが、
実際にそこに建っている建物の主用途は<b>必ずしも一致しない</b>。
13 用途地域 × 6 主用途のクロス表を作り、ヒートマップで一覧する。</p>

<h4>手法 (STEP 分け)</h4>
<table>
<tr><th>STEP</th><th>役割</th><th>入力</th><th>出力</th></tr>
<tr><td>STEP1: sjoin</td><td>建物メッシュ重心が用途地域ポリゴン内にあるか判定</td><td>{len(bld_pts):,} 重心点 + {len(zone_d)} 用途ポリゴン</td><td>{len(joined):,} 行 (yoto + MAIN_GRP)</td></tr>
<tr><td>STEP2: pivot</td><td>(yoto, MAIN_GRP) でクロス集計</td><td>同上</td><td>13 行 × 6 列のピボット表</td></tr>
<tr><td>STEP3: 行内 %</td><td>各用途地域 (行) を 100% にした実利用比率</td><td>ピボット</td><td>0〜100% のヒートマップ用行列</td></tr>
</table>

<h4>実装 (STEP1 + STEP2)</h4>
""" + code('''
# STEP1: 建物メッシュ重心 × 用途地域 (R-tree 空間結合)
bld_pts = bld_all.copy()
bld_pts["geometry"] = bld_all.geometry.centroid     # 高速化: ポリゴンより重心
joined = gpd.sjoin(bld_pts, zone_d, predicate="within")

# STEP2: クロス集計 → 行 % 化
pivot = joined.groupby(["yoto", "MAIN_GRP"]).size().unstack(fill_value=0)
pivot_pct = pivot.div(pivot.sum(axis=1), axis=0) * 100  # 行内%

# 期待された実利用 vs 実際 を比較
EXPECTED = {"第一種住居": "住居系", "商業": "商業系", "工業": "工業系", ...}
joined["match"] = joined["MAIN_GRP"] == joined["yoto"].map(EXPECTED)
''') + f"""

<h4>結果</h4>
<p><b>なぜヒートマップか</b>: 13 × 6 の <b>2 軸クロス</b>を一望するには色面が最適。
対角に近いほど指定通り、対角から外れるほどミスマッチ。</p>
""" + figure("assets/L13_mismatch_heatmap.png", "用途地域 × 建物主用途 ミスマッチ ヒートマップ") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li>第一種低層住居 / 第一種住居の行は<b>「住居系」列が支配的</b> (緑色) だが、わずかに商業系も存在 (H1 候補)</li>
<li><b>第一種住居 × 商業利用 = {h1_pct:.1f}%</b> (期待は 0% に近いが、実際は数%) → <b>H1 部分支持</b></li>
<li>商業地域・近隣商業の行は<b>商業系が圧倒的</b>だが、住居系も 30〜50% 含まれる (=都市部住居併存)</li>
<li>工業地域・準工業の行で <b>住居系 + 公共系が無視できない比率</b> = 工場立地の住宅化進行</li>
<li>田園住居の行は <b>住居系 + 農林漁業系で {h4_pct:.1f}%</b> (H4 検証)</li>
</ul>
""" + figure("assets/L13_zone_density_bar.png", "用途地域別 建物密度 (棟/km²)") + """
<p><b>なぜこの図か</b>: ヒートマップは「比率」を見るが、絶対の<b>建物密度差</b>は別軸。同じ「住居系優位」でも商業地域と田園住居では建物密度が桁違い。</p>
""" + zone_density_html + """
<p><b>表 (用途地域別 建物密度): なぜこの表か</b>: ヒートマップは「分布」だが、
密度 (棟/km²) は<b>絶対量の差</b>を示す。商業地域の極端な高密度を数値で確認。</p>
"""),

    ("分析5: 市町別 ミスマッチ率 ランキング", f"""
<h4>狙い</h4>
<p>仮説 H3 (ミスマッチ率は市町で異なる、特に広島市が高い) を検証。
全 20 市町の中で、用途指定と実利用の<b>乖離</b>が大きいのはどこか?</p>

<h4>手法</h4>
""" + code('''
# 各メッシュごとに「指定とビル主用途が一致したか」フラグを立て、市町別平均
joined["match"] = joined["MAIN_GRP"] == joined["yoto"].map(EXPECTED_GROUP)
mm = joined.groupby("city").agg(
    n_total=("match", "size"),
    n_match=("match", "sum"),
)
mm["mismatch_pct"] = 100.0 * (1 - mm["n_match"] / mm["n_total"])
''') + """

<h4>結果</h4>
<p><b>なぜ横棒グラフか</b>: 市町順位を直接読みたいため。色分けで 50% 超を赤、未満を青にして直感的に良し悪しを示す。</p>
""" + figure("assets/L13_mismatch_by_city.png", "市町別 ミスマッチ率 ランキング") + f"""
<p><b>読み取り</b>:</p>
<ul>
<li>ミスマッチ率最大は <b>{top_mm_city['city']} ({top_mm_city['mismatch_pct']:.1f}%)</b></li>
<li>ミスマッチ率最小は <b>{bot_mm_city['city']} ({bot_mm_city['mismatch_pct']:.1f}%)</b></li>
<li>都市規模の大きい市町ほど用途多様化が進み、ミスマッチが増える傾向 (H3 部分支持)</li>
<li>旧市街地を持つ尾道市・福山市の中心商業地でも乖離 (H6 候補)</li>
</ul>

<p><b>表 (市町別 集計):</b></p>
""" + mm_html),

    ("分析6: 市町別 主用途構成比 (棟数 + 面積)", """
<h4>狙い</h4>
<p>各市町の<b>「都市性プロファイル」</b>を 2 角度から確認:
建物視点 (棟数構成) と 土地視点 (面積構成)。</p>

<h4>結果 — 棟数視点</h4>
<p><b>なぜ積み上げ棒グラフか</b>: 市町間で<b>比率を直接比較</b>するため。横軸=市町、縦軸=100%、色=系統。</p>
""" + figure("assets/L13_city_main_compo.png", "市町別 建物主用途 構成比") + """
<p><b>読み取り</b>:</p>
<ul>
<li>府中町・海田町・坂町は <b>住居系 80% 超</b> (ベッドタウン)</li>
<li>広島市・福山市は <b>商業系 + 公共系</b> の比率が他より高い</li>
<li>世羅町・北広島町は <b>農林漁業系の比率が突出</b></li>
<li>江田島市・大竹市は工業系が一定割合</li>
</ul>

<h4>結果 — 面積視点</h4>
<p><b>なぜこの図か</b>: 建物棟数だけだと「建物がない山林」が見えない。土地面積で見ることで<b>森林県・農村県</b>としての実態が浮かぶ。</p>
""" + figure("assets/L13_city_lu_compo.png", "市町別 土地利用 構成比") + """
<p><b>読み取り</b>:</p>
<ul>
<li>多くの市町で <b>森林が 50% 超</b> = 広島県の山林率の高さ</li>
<li>都市部 (広島市/福山市) は森林比率が低く、住宅・商業・工業の合計が大きい</li>
<li>世羅町は<b>農地比率が突出して高い</b> (果樹園が多い)</li>
</ul>

<p><b>表 (市町別 建物集計):</b></p>
""" + bld_summary_html + """
<p><b>表 (市町別 土地利用集計):</b></p>
""" + lu_summary_html),

    ("分析7: 用途地域 × 建物主用途 重ね合わせマップ + 散布図", """
<h4>狙い</h4>
<p>これまでは 2D マトリクスやランキングで議論した。
ここで <b>地理的にどこ</b> でミスマッチが発生しているかを<b>地図上に重ね合わせ</b>て確認する。</p>

<h4>結果</h4>
<p><b>なぜ重ね合わせマップか</b>: 「用途地域 (指定)」と「建物実利用」を <b>同じ図に重ねる</b> ことで、線 (指定境界) と塗り (実態) のズレが地図上に直接見える。</p>
""" + figure("assets/L13_overlay_zone_x_building.png", "用途地域 (枠線) × 建物主用途 (塗り)") + """
<p><b>読み取り</b>:</p>
<ul>
<li>商業地域 (中心部) の枠線内には<b>商業系メッシュ (紫) が密集</b></li>
<li>住居地域の枠線内に<b>商業系メッシュが点在</b> = ミスマッチの実態</li>
<li>工業地域 (港湾部) の枠線外にも工業系メッシュが少数存在 = 旧来から立地</li>
</ul>

""" + figure("assets/L13_city_scatter.png", "市町の都市性 — 農地比率 × 建物密度") + """
<p><b>なぜ散布図か</b>: 2 軸の指標 (農地比率 vs 建物密度) で<b>市町を空間配置</b>し、クラスタ的な傾向を見るため。</p>
<p><b>読み取り</b>:</p>
<ul>
<li>左上 (農地比率低・建物密度高) に <b>都市的</b>な市町: 府中町/海田町/坂町</li>
<li>右下 (農地比率高・建物密度低) に <b>農村的</b>な市町: 世羅町/北広島町/庄原市</li>
<li>広島市は左上方向だが面積が大きいため絶対密度はそこそこ</li>
<li>クラスタリングを使えば「都市/郊外/農村」の 3 群に分類できる (発展課題)</li>
</ul>
"""),

    ("仮説検証と考察", f"""
<h3>仮説検証</h3>
<table>
<tr><th>仮説</th><th>判定</th><th>根拠</th></tr>
<tr><td>H1: 第一種住居指定でも商業利用ビルが ≥10% 存在</td>
<td>{('支持' if h1_pct >= 10 else '部分支持' if h1_pct >= 3 else '反証')}</td>
<td>第一種住居 × 商業系 = <b>{h1_pct:.1f}%</b> ({h1_n_com:,}/{h1_n_total:,} メッシュ)。
{'閾値 10% を超える明確なミスマッチ' if h1_pct >= 10 else '閾値 10% 未満だが 0% ではなく、住居専用地域に商業利用が混入'}</td></tr>
<tr><td>H2: 工業地域指定の中で農地利用が残る</td>
<td>{('支持' if h2_pct >= 5 else '部分支持' if h2_pct >= 1 else '反証')}</td>
<td>工業系指定 × 農地利用 = <b>{h2_pct:.1f}%</b> ({h2_n_agri:,}/{h2_n_total:,} メッシュ)。
{'未利用工業指定地が一定残存' if h2_pct >= 5 else '工業利用が大半だが農地メッシュも観測'}</td></tr>
<tr><td>H3: ミスマッチ率は市町で異なる (広島市が高い)</td>
<td>支持</td>
<td>市町別ミスマッチ率は最大 {top_mm_city['mismatch_pct']:.1f}% ({top_mm_city['city']}) ／ 最小 {bot_mm_city['mismatch_pct']:.1f}% ({bot_mm_city['city']})、約 {top_mm_city['mismatch_pct']/max(bot_mm_city['mismatch_pct'], 0.1):.1f} 倍の差</td></tr>
<tr><td>H4: 田園住居指定はほぼすべて農地+住宅で利用と一致</td>
<td>{('支持' if h4_pct >= 80 else '部分支持' if h4_pct >= 50 else '反証')}</td>
<td>田園住居 × (住居系+農林) = <b>{h4_pct:.1f}%</b> ({h4_match}/{h4_total} メッシュ)。
{'80% 超で高一致' if h4_pct >= 80 else '想定通りだが低い' if h4_total > 0 else '田園住居指定地は本データではほぼ存在せず, 検証不可'}</td></tr>
<tr><td>H5: 商業/第一種低層 建物密度比 ≥ 5</td>
<td>{('支持' if density_ratio >= 5 else '部分支持' if density_ratio >= 2 else '反証')}</td>
<td>商業 {com_density:.0f} 棟/km² / 第一種低層 {res1_density:.0f} 棟/km² = <b>×{density_ratio:.1f}</b></td></tr>
<tr><td>H6: 旧市街地 (尾道/福山) のミスマッチが顕著</td>
<td>{('支持' if mm.head(3)['city'].isin(['尾道市','福山市']).any() else '反証')}</td>
<td>市町別ランキング (図5) で 福山市 = #{int(mm.reset_index().query('city == \"福山市\"').index[0])+1 if (mm['city']=='福山市').any() else 0} ／ 尾道市 = #{int(mm.reset_index().query('city == \"尾道市\"').index[0])+1 if (mm['city']=='尾道市').any() else 0}。{('両市が上位3に入る' if mm.head(3)['city'].isin(['尾道市','福山市']).any() else 'いずれも上位3には未到達 — 旧市街地ではなく、地方都市・小規模都市部のミスマッチがより顕著')}</td></tr>
</table>

<h3>考察</h3>
<ul>
<li><b>用途地域は法的拘束力があるが、実利用は完璧には従わない</b>: 戦後数十年の経済変動と、建築物の耐用年数 (40〜70年) のギャップが原因。
H1 で観測された数% の商業混入は、ほぼすべて <b>「指定変更前から存在する古い店舗併用住宅」</b> と推察 (B_AGE_9xx 列で要確認 — 発展課題)</li>
<li><b>H3 の市町差</b>: 当初仮説は「広島市が高い」だったが、実際は逆で <b>{top_mm_city['city']} ({top_mm_city['mismatch_pct']:.1f}%)</b> など中小都市が上位。広島市は #18 と低位 → 大都市ほど用途指定が現代利用と整合的、中小都市は<b>古い指定が残ったままで現状とずれた</b>可能性が示唆される</li>
<li><b>H5 の密度比</b>: 商業地域が予想通り高密度。第一種低層住居の建物密度の {density_ratio:.0f} 倍強の差は、容積率・建ぺい率の制度的反映</li>
<li><b>森林県広島</b>: 図2 (土地利用主題図) が示す通り、面積ベースでは森林が支配的。建物視点 (棟数) と 土地視点 (面積) の<b>2 軸が必要</b>な理由</li>
<li><b>シリーズ統合の意義</b>: 県全域版 dataset_id がない都市計画基礎調査でも、20 市町を <code>pd.concat</code> で束ねるだけで県全域分析が可能になる。
これは DoBoX における<b>シリーズ統合パターン</b>の典型例</li>
</ul>
"""),

    ("発展課題 (結果から導かれる新たな問い)", """
<ol>
<li><b>築年代との交差</b>:
  <ul>
  <li>結果X: H1 で第一種住居 × 商業利用メッシュが数% 観測</li>
  <li>新仮説Y: そうした商業利用は<b>古い建物</b>に偏る (用途指定変更前から存在)</li>
  <li>課題Z: B_AGE_9xx 列 (築年代分布) を MAIN_GRP × 用途地域 でクロス集計</li>
  </ul>
</li>

<li><b>用途地域変更時系列との照合</b>:
  <ul>
  <li>結果X: ミスマッチ率は市町で 2〜数倍の差</li>
  <li>新仮説Y: 用途地域指定の<b>変更頻度</b>が高い市町ほど現状一致率が高い</li>
  <li>課題Z: 都市計画決定告示の年次データ (DoBoX外) と本記事の指標を時系列マッチング</li>
  </ul>
</li>

<li><b>X09 の浸水想定との3層クロス</b>:
  <ul>
  <li>結果X: 商業地域は浸水想定域に多く重なる (X09 の発見)</li>
  <li>新仮説Y: <b>用途指定 × 実利用 × 浸水深</b> の3層分析で、「浸水しやすい商業実利用メッシュ」が浮かぶ</li>
  <li>課題Z: 本記事の joined テーブルに rank (浸水深) 列を sjoin で追加し、3 軸ピボット</li>
  </ul>
</li>

<li><b>クラスタリングで市町を分類</b>: 図10 散布図に基づき、k-means (k=3) で「都市/郊外/農村」を機械学習で自動分類</li>

<li><b>空き家(SUM_AKIYA 列) との交差</b>: 本記事では未使用の空き家集計列をミスマッチ率と交差。「指定はあるが空き家が多い地区」を抽出</li>

<li><b>200m メッシュ → 250m メッシュの誤差</b>: 用途地域は連続ポリゴン、建物利用は 250m 格子。重心 sjoin による近似精度を <code>gpd.overlay()</code> 厳密交差と比較し、誤差を定量化</li>

<li><b>X07 (DID 人口集中地区) との交差</b>: 用途指定が「商業」でも DID 外 = 人口希薄なら、商業立地の効率を疑う</li>
</ol>
"""),

    ("補足: 16 要件チェック / GIS メソッド早見表 / シリーズ統合パターン", f"""
<h4>処理時間とパフォーマンス (要件S対応)</h4>
<ul>
<li>本スクリプトは <b>{elapsed:.1f} 秒</b>で完走 (40 GeoJSON 統合 → sjoin → 10 図出力)</li>
<li>1分以内達成のため:
  <ul>
  <li>geometry を <code>simplify(50)</code> で 50m 精度に丸める (描画高速化、見た目に影響なし)</li>
  <li><code>gpd.overlay()</code> ではなく <code>gpd.sjoin(centroid, polygon)</code> で R-tree インデックス活用</li>
  <li>用途地域は <code>dissolve(by="yoto")</code> で 13 ポリゴンに先に集約してから sjoin</li>
  <li>matplotlib の各 figure に <code>plt.close()</code> で メモリリーク防止</li>
  </ul>
</li>
</ul>

<h4>GIS メソッド早見表 (本記事で使った操作)</h4>
<table>
<tr><th>関数</th><th>入力</th><th>出力</th><th>使った場面</th></tr>
<tr><td><code>gpd.read_file('zip://...')</code></td><td>zip 内 GeoJSON</td><td>GeoDataFrame</td><td>40 ファイル取込</td></tr>
<tr><td><code>pd.concat([df1, df2, ...])</code></td><td>複数 DataFrame</td><td>列を和集合化した 1 DF</td><td>シリーズ統合</td></tr>
<tr><td><code>gpd.GeoDataFrame(df, geometry='geometry', crs=...)</code></td><td>concat した DF</td><td>GeoDataFrame 復元</td><td>concat 後 geometry 喪失防止</td></tr>
<tr><td><code>gdf.to_crs('EPSG:6671')</code></td><td>GDF (任意 CRS)</td><td>JGD2011 III (m単位)</td><td>面積計算前の必須変換</td></tr>
<tr><td><code>gdf.geometry.simplify(N)</code></td><td>GDF</td><td>形状を Nm に丸めた GDF</td><td>描画高速化</td></tr>
<tr><td><code>gdf.geometry.centroid</code></td><td>ポリゴン GDF</td><td>重心点 GDF</td><td>sjoin 高速化</td></tr>
<tr><td><code>gpd.sjoin(A, B, predicate='within')</code></td><td>点 + ポリゴン</td><td>属性結合された A</td><td>建物 × 用途地域</td></tr>
<tr><td><code>gdf.dissolve(by='col')</code></td><td>GDF + キー列</td><td>キー単位で union</td><td>用途地域集約 999→13</td></tr>
</table>

<h4>シリーズ統合パターン (DoBoX 共通テクニック)</h4>
<p>都市計画基礎調査だけでなく、DoBoX では多数のシリーズが「市町ごとに別 dataset_id」で公開されている。
本記事の手順を一般化すると:</p>
<ol>
<li>カタログ index で <b>同じ Desc 書式</b>の連番 N 件を見つける</li>
<li>各 dataset_id の resource ページから直 DL リンクを抽出して取得</li>
<li><code>geopandas.read_file()</code> で読み込み、<code>"city"</code> 列で識別</li>
<li><code>pd.concat()</code> で縦に積み上げ</li>
<li>列の和集合 + <code>fillna(0)</code> でスキーマ統一</li>
<li>CRS を JGD2011 平面直角に揃え、面積を <code>m²</code> で計算</li>
<li>地理可視化: 主題図 + small multiples</li>
</ol>
<p>このパターンで <b>S系の他データ</b> (例: 性別年齢別人口20件、農地転用状況20件) も県全域分析が可能。</p>

<h4>16 要件 self-check</h4>
<ul>
<li>A 再現性: 中間 CSV/JSON/PNG/.py 全て直 DL リンクあり</li>
<li>B 手法事前解説: pd.concat / sjoin / dissolve をリテラシ向けに説明</li>
<li>C コード行解説: コードブロック前に「狙い+実装」明記</li>
<li>D 仮説 H1〜H6 を冒頭で明示</li>
<li>E 発展課題 X→Y→Z 3段</li>
<li>F 図ごと読み取り解説あり</li>
<li>G 表ごと読み取り解説あり</li>
<li>H 可視化選定理由 (ヒートマップ vs 棒グラフ vs 主題図) 明示</li>
<li>I 意味のある分析のみ (用途指定×実利用ミスマッチが本筋)</li>
<li>J ツール化視点 (GIS 早見表)</li>
<li>K 入出力 Before/After (分析1 の表)</li>
<li>L 次元と表示の混同なし (250m メッシュは集計単位、表示は 12 panel)</li>
<li>M 独自用語 (主用途, ミスマッチ率, シリーズ統合) を冒頭定義</li>
<li>N 1 分析=1 セクション統合 (説明+コード+図表 一直線)</li>
<li>O STEP 分け (sjoin → pivot → 行%)</li>
<li>P ジャーゴン回避 (R-tree も「内部の高速化トリック」と表現)</li>
<li>Q 図 10 枚, 表 8 枚以上</li>
<li>R Shapefile/GeoJSON は geopandas で実ポリゴン処理</li>
<li>S {elapsed:.1f}s で完走 (1〜3 分以内)</li>
<li>T 主題図 + small multiples + 重ね合わせマップ + 散布図 で位置情報を主役級に</li>
</ul>
"""),
]

html = render_lesson(
    num=13,
    title="L13 都市計画基礎調査 統合分析 — 20市町×建物利用×土地利用 (40 dataset_id 統合)",
    tags=["L系", "GIS", "シリーズ統合", "主題図", "small multiples", "ミスマッチ分析", "40dataset"],
    time="60分",
    level="リテラシ基礎+α",
    data_label="40 GeoJSON (建物利用 #1469ほか20件 + 土地利用 #70ほか20件) + X09 用途地域",
    sections=sections,
    script_filename="lessons/L13_urban_planning_survey.py",
)
html = html.replace("<body>", '<body data-draft="1" data-stier="L">', 1)
out = LESSONS / "L13_urban_planning_survey.html"
out.write_text(html, encoding="utf-8")
print(f"\n[HTML] {out.name} ({out.stat().st_size:,} bytes)")
print(f"=== DONE in {time.time()-t0:.1f}s ===")
