"""X11: 宅地開発 × 農地転用 2016-2020 — 土地変換圧力の市町別メカニズム

広島県全20市町の「都市化圧力」を4データセット統合で定量化:
  新築動向 (L20) × 宅地開発 (L21) × 農地転用 (L24) × 農林施策 (L25)

L20/L21 の研究は「建物・開発の量的分布」を問うたが、
本記事は「農地→宅地の転換圧力はどこで起き、農林漁業政策と矛盾するか」
という政策矛盾の問いを追加する横断記事。

使用データセット:
  #1332–1516 新築動向 20件 (L20 の第2記事)
  #1351–1370 宅地開発 20件 (L21 の第2記事)
  #1391–1407 農地転用 17件 (L24 の第2記事)
  #1408–1424 農林漁業施策 17件 (L25 の第2記事)
  ─── 合計 74 dataset_id ───

形式: X系横断記事 (Format B, RQ × 3 + 仮説 H1-H9)
実行時間目安: 60〜90 秒
"""
from __future__ import annotations
import sys, time, json, zipfile, glob
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
import matplotlib.patches as mpatches
import geopandas as gpd
import shapely

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

t0 = time.time()
print("=== X11 宅地開発 × 農地転用 土地変換分析 ===")

TARGET_CRS = "EPSG:6671"

CITY_NAME = {
    101:"広島市中区",102:"広島市東区",103:"広島市南区",104:"広島市西区",
    105:"広島市安佐南区",106:"広島市安佐北区",107:"広島市安芸区",108:"広島市佐伯区",
    202:"呉市",203:"竹原市",204:"三原市",205:"尾道市",207:"福山市",
    208:"府中市",209:"三次市",210:"庄原市",211:"大竹市",212:"東広島市",
    213:"廿日市市",214:"安芸高田市",215:"江田島市",
    302:"府中町",304:"海田町",307:"熊野町",309:"坂町",
    369:"安芸太田町",462:"世羅町",
}
# 広島市 8区 → 「広島市」に集約
def aggr(city_cd):
    if 101 <= city_cd <= 108: return 100  # 広島市
    return city_cd
CITY_NAME[100] = "広島市"

# =============================================================================
# 1. L20 新築動向 (46,613件)
# =============================================================================
print("\n[1] L20 新築動向 ...")
t1 = time.time()
dfs20 = []
for zp in sorted(glob.glob(str(ROOT/"data"/"extras"/"L20_new_construction"/"shinchiku_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    dfs20.append(gdf[["CITY_CD","YOTO_SUB","TATE_H","NEW_YCD"]])
df20 = pd.concat(dfs20, ignore_index=True)
df20["CITY_CD2"] = df20["CITY_CD"].apply(aggr)
print(f"  L20: {len(df20):,} 件, {time.time()-t1:.1f}s")

# NEW_YCD: 1=2016, 2=2017, 3=2018, 4=2019, 5=2020
YEAR_MAP = {1:2016, 2:2017, 3:2018, 4:2019, 5:2020}
df20["year"] = df20["NEW_YCD"].map(YEAR_MAP)

# 用途グループ
YOTO_GROUP = {
    "一戸建ての住宅":"住宅", "長屋":"住宅", "共同住宅":"住宅",
}
df20["yoto_grp"] = df20["YOTO_SUB"].map(YOTO_GROUP).fillna("非住宅")

# 市町別集計
n20_city = df20.groupby("CITY_CD2").size().rename("新築件数").sort_values(ascending=False)
n20_city_name = n20_city.rename(index=CITY_NAME)
print(f"  市町別 Top3: {n20_city_name.head(3).to_dict()}")

# =============================================================================
# 2. L21 宅地開発 (面積 m²)
# =============================================================================
print("\n[2] L21 宅地開発 ...")
t1 = time.time()
dfs21 = []
for zp in sorted(glob.glob(str(ROOT/"data"/"extras"/"L21_housing_development"/"takuchi_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f, crs="EPSG:4326")
    if "KAIHATU_A" in gdf.columns:
        area_m2 = pd.to_numeric(gdf["KAIHATU_A"], errors="coerce").fillna(0)
    else:
        # ALT スキーマ (江田島市・三原市・竹原市): ジオメトリ面積を使用
        gdf_proj = gdf.to_crs(TARGET_CRS)
        area_m2 = gdf_proj.geometry.area
    dfs21.append(pd.DataFrame({"CITY_CD": gdf["CITY_CD"], "area_m2": area_m2}))
df21 = pd.concat(dfs21, ignore_index=True)
df21["CITY_CD2"] = df21["CITY_CD"].apply(aggr)
area21_city = df21.groupby("CITY_CD2")["area_m2"].sum() / 10_000  # → ha
area21_city = area21_city.rename("宅地開発_ha").sort_values(ascending=False)
print(f"  L21: {len(df21):,} 件, 総面積: {area21_city.sum():.0f} ha, {time.time()-t1:.1f}s")

# =============================================================================
# 3. L24 農地転用 (面積 ha)
# =============================================================================
print("\n[3] L24 農地転用 ...")
t1 = time.time()
dfs24 = []
for zp in sorted(glob.glob(str(ROOT/"data"/"extras"/"L24_farmland_conversion"/"farm_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    dfs24.append(gdf[["CITY_CD","AREA"]])
df24 = pd.concat(dfs24, ignore_index=True)
df24["CITY_CD2"] = df24["CITY_CD"].apply(aggr)
area24_city = df24.groupby("CITY_CD2")["AREA"].sum().rename("農地転用_ha").sort_values(ascending=False)
print(f"  L24: {len(df24):,} ポリゴン, 総面積: {area24_city.sum():.0f} ha, {time.time()-t1:.1f}s")

# =============================================================================
# 4. L25 農林漁業施策 (ポリゴン面積)
# =============================================================================
print("\n[4] L25 農林漁業施策 ...")
t1 = time.time()
dfs25 = []
for zp in sorted(glob.glob(str(ROOT/"data"/"extras"/"L25_agroforestry_policy"/"agro_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf_proj = gdf.to_crs(TARGET_CRS)
    gdf_proj["area_m2"] = gdf_proj.geometry.area
    dfs25.append(gdf_proj[["CITY_CD","area_m2","geometry"]])
gdf25_all = gpd.GeoDataFrame(pd.concat(dfs25, ignore_index=True), geometry="geometry", crs=TARGET_CRS)
gdf25_all["CITY_CD2"] = gdf25_all["CITY_CD"].apply(aggr)
area25_city = (gdf25_all.groupby("CITY_CD2")["area_m2"].sum() / 10_000).rename("農林施策_ha")
print(f"  L25: {len(gdf25_all):,} ポリゴン, 総面積: {area25_city.sum():.0f} ha, {time.time()-t1:.1f}s")

# =============================================================================
# 5. 行政区域 (面積用)
# =============================================================================
print("\n[5] 行政区域 ...")
t1 = time.time()
ADMIN_GPKG = ROOT/"data"/"extras"/"L44_storm_surge"/"_cache"/"admin_diss.gpkg"
admin = gpd.read_file(ADMIN_GPKG).to_crs(TARGET_CRS)
admin["CITY_CD2"] = admin["CITY_CD"].apply(aggr)
admin_area = admin.groupby("CITY_CD2")["admin_km2"].sum().rename("行政面積_km2")
print(f"  admin: {len(admin)} polys, {time.time()-t1:.1f}s")

# =============================================================================
# 6. 統合テーブル
# =============================================================================
print("\n[6] 統合分析 ...")
df_all = pd.DataFrame({
    "新築件数": n20_city,
    "宅地開発_ha": area21_city,
    "農地転用_ha": area24_city,
    "農林施策_ha": area25_city,
    "行政面積_km2": admin_area,
}).fillna(0)
df_all.index = pd.Index(df_all.index)
df_all["市町名"] = df_all.index.map(lambda c: CITY_NAME.get(c, str(c)))
df_all["新築密度_per_km2"] = df_all["新築件数"] / df_all["行政面積_km2"].replace(0, np.nan)
df_all["開発効率_件per_ha"] = df_all["新築件数"] / df_all["宅地開発_ha"].replace(0, np.nan)
df_all["転換率"] = df_all["農地転用_ha"] / df_all["宅地開発_ha"].replace(0, np.nan)

# Lorenz / Gini（新築件数）
def lorenz_gini(vals):
    v = np.sort(vals[vals > 0])
    n = len(v)
    if n == 0: return np.array([0,1]), np.array([0,1]), 0.0
    cum = np.cumsum(v) / v.sum()
    lorenz_x = np.linspace(0, 1, n+1)
    lorenz_y = np.concatenate([[0], cum])
    trapz = getattr(np, "trapezoid", None) or np.trapz
    gini = 1 - 2 * trapz(lorenz_y, lorenz_x)
    return lorenz_x, lorenz_y, gini

lx, ly, gini20 = lorenz_gini(df_all["新築件数"].values)
lx21, ly21, gini21 = lorenz_gini(df_all["宅地開発_ha"].values)
lx24, ly24, gini24 = lorenz_gini(df_all["農地転用_ha"].values)
print(f"  新築件数 Gini: {gini20:.3f}, 宅地開発 Gini: {gini21:.3f}, 農地転用 Gini: {gini24:.3f}")

# 上位3市のシェア
top3_cities = df_all["新築件数"].sort_values(ascending=False).head(3).index
top3_share = df_all.loc[top3_cities, "新築件数"].sum() / df_all["新築件数"].sum() * 100
print(f"  上位3市: {[CITY_NAME.get(c,str(c)) for c in top3_cities]}, シェア: {top3_share:.1f}%")

# 年別新築件数
year_cnt = df20.groupby("year").size()
print(f"  年別: {year_cnt.to_dict()}")

# =============================================================================
# 7. 政策矛盾分析 (L25 農林施策 × L24 農地転用 GIS overlay)
# =============================================================================
print("\n[7] 政策矛盾: 農林施策 × 農地転用 ...")
t1 = time.time()
dfs24_geo = []
for zp in sorted(glob.glob(str(ROOT/"data"/"extras"/"L24_farmland_conversion"/"farm_*.zip"))):
    with zipfile.ZipFile(zp) as zf:
        gj = [f for f in zf.namelist() if f.endswith(".geojson")][0]
        with zf.open(gj) as f:
            gdf = gpd.read_file(f)
    gdf = gdf.to_crs(TARGET_CRS)
    gdf["CITY_CD2"] = gdf["CITY_CD"].apply(aggr)
    dfs24_geo.append(gdf[["CITY_CD","CITY_CD2","AREA","geometry"]])
gdf24_all = gpd.GeoDataFrame(pd.concat(dfs24_geo, ignore_index=True), geometry="geometry", crs=TARGET_CRS)
gdf24_all["geometry"] = gdf24_all.geometry.buffer(0)

# sjoin: 農地転用ポリゴン が 農林施策ポリゴン と交差するか
try:
    conflict_join = gpd.sjoin(
        gdf24_all[["CITY_CD","CITY_CD2","AREA","geometry"]].reset_index(drop=True),
        gdf25_all[["CITY_CD2","geometry"]].reset_index(drop=True),
        how="inner", predicate="intersects",
        lsuffix="farm", rsuffix="agro"
    )
    n_conflict = conflict_join["CITY_CD_farm"].nunique() if "CITY_CD_farm" in conflict_join.columns else conflict_join.index.nunique()
    conflict_area_ha = conflict_join["AREA"].sum()
    conflict_cities = len(conflict_join.groupby("CITY_CD2_farm")) if "CITY_CD2_farm" in conflict_join.columns else len(conflict_join.groupby("CITY_CD2_left")) if "CITY_CD2_left" in conflict_join.columns else 0
    print(f"  政策矛盾: 農地転用 × 農林施策 重複 {n_conflict} ポリゴン, 面積 {conflict_area_ha:.1f} ha, {time.time()-t1:.1f}s")
    # city-level conflict
    cc = "CITY_CD2_farm" if "CITY_CD2_farm" in conflict_join.columns else "CITY_CD2_left"
    if cc not in conflict_join.columns:
        cc = conflict_join.columns[conflict_join.columns.str.startswith("CITY_CD2")][0]
    conflict_by_city = (conflict_join.groupby(cc)["AREA"].sum()
                        .rename("矛盾農転_ha").sort_values(ascending=False))
    conflict_city_count = (conflict_by_city > 0).sum()
    print(f"  政策矛盾 市町数: {conflict_city_count}")
except Exception as e:
    print(f"  政策矛盾 sjoin error: {e}")
    conflict_area_ha = 0.0
    conflict_city_count = 0
    conflict_by_city = pd.Series(dtype=float)

# =============================================================================
# 8. 図の生成
# =============================================================================
print("\n=== 図生成 ===")

# 図1: Lorenz 曲線 3本
t1 = time.time()
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
for (lx_, ly_, g_), label, color in [
    ((lx, ly, gini20), f"新築件数 (Gini={gini20:.3f})", "#2563eb"),
    ((lx21, ly21, gini21), f"宅地開発面積 (Gini={gini21:.3f})", "#16a34a"),
    ((lx24, ly24, gini24), f"農地転用面積 (Gini={gini24:.3f})", "#dc2626"),
]:
    ax.plot(lx_, ly_, label=label, linewidth=1.8)
ax.plot([0,1],[0,1],"k--",linewidth=1,alpha=0.5,label="均等線")
ax.fill_between(lx, ly, [0,*lx[:-1]], alpha=0.08, color="#2563eb")
ax.set_xlabel("累積市町割合", fontsize=11); ax.set_ylabel("累積シェア", fontsize=11)
ax.set_title("広島県 土地変換 Lorenz 曲線\n（新築件数・宅地開発面積・農地転用面積）", fontsize=12)
ax.legend(fontsize=9); ax.set_aspect("equal"); ax.grid(alpha=0.3)

# 図1右: 年別新築件数推移
ax2 = axes[1]
year_cnt.plot(ax=ax2, kind="bar", color="#3b82f6", edgecolor="#1d4ed8", linewidth=0.5)
ax2.set_xlabel("年度", fontsize=11); ax2.set_ylabel("新築件数", fontsize=11)
ax2.set_title(f"広島県 年別新築件数 (2016-2020)\n計 {len(df20):,} 件", fontsize=12)
for p, v in zip(ax2.patches, year_cnt.values):
    ax2.text(p.get_x()+p.get_width()/2, p.get_height()+50, f"{v:,}", ha="center", fontsize=9)
ax2.tick_params(axis="x", rotation=0)
plt.tight_layout()
p1 = ASSETS/"X11_lorenz_trend.png"
plt.savefig(p1, dpi=120); plt.close()
print(f"  図1: {p1.name}, {time.time()-t1:.1f}s")

# 図2: 市町別新築件数 Top20 (広島市集約)
t1 = time.time()
fig, ax = plt.subplots(figsize=(12, 6))
top20 = df_all.sort_values("新築件数", ascending=False).head(20)
bars = ax.bar(range(len(top20)), top20["新築件数"],
              color=["#1d4ed8" if n in ["広島市","福山市"] else "#3b82f6" for n in top20["市町名"]],
              edgecolor="#1e40af", linewidth=0.5)
ax.set_xticks(range(len(top20)))
ax.set_xticklabels(top20["市町名"], rotation=45, ha="right", fontsize=9)
ax.set_ylabel("新築件数", fontsize=11)
ax.set_title(f"市町別 新築建物件数 Top20 (2016-2020)\n上位3市シェア: {top3_share:.1f}%", fontsize=12)
for bar, v in zip(bars, top20["新築件数"]):
    ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+50, f"{v:,}", ha="center", fontsize=8)
plt.tight_layout()
p2 = ASSETS/"X11_newbuild_by_city.png"
plt.savefig(p2, dpi=120); plt.close()
print(f"  図2: {p2.name}, {time.time()-t1:.1f}s")

# 図3: 用途構成 (pie + bar)
t1 = time.time()
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
yoto_top = df20["YOTO_SUB"].value_counts().head(8)
other_n = len(df20) - yoto_top.sum()
if other_n > 0:
    yoto_top["その他"] = other_n
colors_pie = ["#1d4ed8","#3b82f6","#60a5fa","#93c5fd","#bfdbfe","#dbeafe","#eff6ff","#e2e8f0","#cbd5e1"]
wedges, texts, autotexts = ax.pie(
    yoto_top, labels=yoto_top.index, autopct="%1.1f%%",
    colors=colors_pie[:len(yoto_top)], startangle=90,
    textprops={"fontsize":8}
)
ax.set_title(f"新築建物 用途構成\n(n={len(df20):,}件)", fontsize=12)

ax2 = axes[1]
# 市町別住宅率 Top/Bottom
yoto_city = df20.groupby("CITY_CD2")["yoto_grp"].value_counts(normalize=True).unstack(fill_value=0)
if "住宅" in yoto_city.columns:
    yoto_city = yoto_city["住宅"].rename(index=CITY_NAME).sort_values()
    ax2.barh(range(len(yoto_city)), yoto_city.values*100,
             color=["#dc2626" if v < 0.7 else "#3b82f6" for v in yoto_city.values],
             edgecolor="#555", linewidth=0.4)
    ax2.set_yticks(range(len(yoto_city)))
    ax2.set_yticklabels(yoto_city.index, fontsize=8)
    ax2.set_xlabel("住宅系比率 (%)", fontsize=11)
    ax2.set_title("市町別 住宅系新築比率\n（赤 = 70%未満）", fontsize=12)
    ax2.axvline(x=70, color="red", linestyle="--", linewidth=1.2, label="70%閾値")
    ax2.legend(fontsize=9)
plt.tight_layout()
p3 = ASSETS/"X11_yoto_composition.png"
plt.savefig(p3, dpi=120); plt.close()
print(f"  図3: {p3.name}, {time.time()-t1:.1f}s")

# 図4: 宅地開発面積 vs 新築件数 散布
t1 = time.time()
fig, ax = plt.subplots(figsize=(9, 7))
df_plot = df_all[(df_all["宅地開発_ha"]>0) & (df_all["新築件数"]>0)].copy()
ax.scatter(df_plot["宅地開発_ha"], df_plot["新築件数"], s=60, color="#3b82f6",
           edgecolor="#1d4ed8", alpha=0.8, linewidth=0.5)
for _, row in df_plot.iterrows():
    if row["新築件数"] > 1000 or row["宅地開発_ha"] > 10:
        ax.annotate(row["市町名"], (row["宅地開発_ha"], row["新築件数"]),
                    fontsize=8, xytext=(3,3), textcoords="offset points")
# 回帰線
from numpy.polynomial.polynomial import polyfit
if len(df_plot) > 3:
    x_, y_ = df_plot["宅地開発_ha"].values, df_plot["新築件数"].values
    c = polyfit(x_, y_, 1)
    xr = np.linspace(0, x_.max(), 100)
    ax.plot(xr, c[0]+c[1]*xr, "r--", linewidth=1.2, alpha=0.7, label="回帰線")
    r = np.corrcoef(x_, y_)[0,1]
    ax.legend(fontsize=9, title=f"Pearson r={r:.3f}")
ax.set_xlabel("宅地開発面積 (ha)", fontsize=11)
ax.set_ylabel("新築件数", fontsize=11)
ax.set_title("宅地開発面積 vs 新築件数 (市町別)\n2016-2020", fontsize=12)
plt.tight_layout()
p4 = ASSETS/"X11_dev_vs_newbuild.png"
plt.savefig(p4, dpi=120); plt.close()
print(f"  図4: {p4.name}, {time.time()-t1:.1f}s")

# 図5: 農地転用 vs 宅地開発 散布（転換効率）
t1 = time.time()
fig, ax = plt.subplots(figsize=(9, 7))
df_farm = df_all[(df_all["農地転用_ha"]>0) & (df_all["宅地開発_ha"]>0)].copy()
ax.scatter(df_farm["農地転用_ha"], df_farm["宅地開発_ha"], s=60, color="#16a34a",
           edgecolor="#15803d", alpha=0.8, linewidth=0.5)
for _, row in df_farm.iterrows():
    if row["農地転用_ha"] > 5 or row["宅地開発_ha"] > 10:
        ax.annotate(row["市町名"], (row["農地転用_ha"], row["宅地開発_ha"]),
                    fontsize=8, xytext=(3,3), textcoords="offset points")
# 等値線 (農転 = 宅地)
max_v = max(df_farm["農地転用_ha"].max(), df_farm["宅地開発_ha"].max())
ax.plot([0,max_v],[0,max_v],"k--",linewidth=1,alpha=0.5, label="農転＝宅地開発（1:1線）")
if len(df_farm) > 3:
    r_farm = np.corrcoef(df_farm["農地転用_ha"].values, df_farm["宅地開発_ha"].values)[0,1]
    ax.legend(fontsize=9, title=f"Pearson r={r_farm:.3f}")
ax.set_xlabel("農地転用面積 (ha)", fontsize=11)
ax.set_ylabel("宅地開発面積 (ha)", fontsize=11)
ax.set_title("農地転用面積 vs 宅地開発面積 (市町別)\n1:1線より上 = 宅地開発が農転を超過", fontsize=12)
plt.tight_layout()
p5 = ASSETS/"X11_farmland_vs_dev.png"
plt.savefig(p5, dpi=120); plt.close()
print(f"  図5: {p5.name}, {time.time()-t1:.1f}s")

# 図6: 農地転用 市町別面積 棒グラフ
t1 = time.time()
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
farm_plot = area24_city.rename(index=CITY_NAME).sort_values(ascending=False).head(15).iloc[::-1]
ax.barh(range(len(farm_plot)), farm_plot.values, color="#16a34a", edgecolor="#15803d", linewidth=0.5)
ax.set_yticks(range(len(farm_plot))); ax.set_yticklabels(farm_plot.index, fontsize=9)
ax.set_xlabel("農地転用面積 (ha)", fontsize=11)
ax.set_title(f"市町別 農地転用面積 Top15\n(2016-2020, 全17市町計 {area24_city.sum():.0f} ha)", fontsize=12)
for i,v in enumerate(farm_plot.values):
    ax.text(v+0.1, i, f"{v:.1f}", va="center", fontsize=8)

ax2 = axes[1]
# 農林施策面積 市町別
agro_plot = area25_city.rename(index=CITY_NAME).sort_values(ascending=False).head(15).iloc[::-1]
ax2.barh(range(len(agro_plot)), agro_plot.values, color="#15803d", edgecolor="#166534", linewidth=0.5)
ax2.set_yticks(range(len(agro_plot))); ax2.set_yticklabels(agro_plot.index, fontsize=9)
ax2.set_xlabel("農林漁業施策面積 (ha)", fontsize=11)
ax2.set_title(f"市町別 農林漁業施策面積 Top15\n(2022, 全17市町計 {area25_city.sum():.0f} ha)", fontsize=12)
plt.tight_layout()
p6 = ASSETS/"X11_farmland_agro.png"
plt.savefig(p6, dpi=120); plt.close()
print(f"  図6: {p6.name}, {time.time()-t1:.1f}s")

# 図7: 建物高さ分布 ヒストグラム
t1 = time.time()
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
h_vals = df20["TATE_H"].dropna()
h_vals = h_vals[(h_vals > 0) & (h_vals < 100)]
ax.hist(h_vals, bins=40, color="#3b82f6", edgecolor="#1d4ed8", linewidth=0.3, alpha=0.85)
ax.axvline(x=h_vals.median(), color="red", linestyle="--", linewidth=1.5, label=f"中央値 {h_vals.median():.1f}m")
ax.axvline(x=20, color="orange", linestyle="--", linewidth=1.2, label="20m (高層閾値)")
ax.set_xlabel("建物高さ (m)", fontsize=11); ax.set_ylabel("棟数", fontsize=11)
ax.set_title(f"新築建物 高さ分布\n(n={len(h_vals):,}棟)", fontsize=12)
ax.legend(fontsize=9)

ax2 = axes[1]
# 高層建築 (>20m) 市町別件数
high_rise = df20[df20["TATE_H"] > 20].groupby("CITY_CD2").size().rename(index=CITY_NAME).sort_values(ascending=False).head(10)
if len(high_rise) > 0:
    ax2.bar(range(len(high_rise)), high_rise.values, color="#1d4ed8", edgecolor="#1e3a8a", linewidth=0.5)
    ax2.set_xticks(range(len(high_rise)))
    ax2.set_xticklabels(high_rise.index, rotation=45, ha="right", fontsize=9)
    ax2.set_ylabel("高層建築件数 (>20m)", fontsize=11)
    ax2.set_title(f"市町別 高層建築 (>20m) 件数 Top10\n(全体 {(df20['TATE_H']>20).sum():,}件)", fontsize=12)
plt.tight_layout()
p7 = ASSETS/"X11_height_distribution.png"
plt.savefig(p7, dpi=120); plt.close()
print(f"  図7: {p7.name}, {time.time()-t1:.1f}s")

# 図8: 転換率 (農地転用/宅地開発) 市町別バブル
t1 = time.time()
fig, ax = plt.subplots(figsize=(10, 6))
df_bubble = df_all[(df_all["農地転用_ha"]>0) & (df_all["宅地開発_ha"]>0)].copy()
df_bubble["転換率"] = df_bubble["農地転用_ha"] / df_bubble["宅地開発_ha"]
df_bubble = df_bubble.sort_values("転換率", ascending=False)
colors_bubble = ["#dc2626" if r > 1 else "#16a34a" for r in df_bubble["転換率"]]
bars = ax.bar(range(len(df_bubble)), df_bubble["転換率"],
              color=colors_bubble, edgecolor="#555", linewidth=0.4)
ax.axhline(y=1.0, color="black", linestyle="--", linewidth=1.2, alpha=0.7, label="農転＝宅地開発（1:1）")
ax.set_xticks(range(len(df_bubble)))
ax.set_xticklabels(df_bubble["市町名"], rotation=45, ha="right", fontsize=8)
ax.set_ylabel("転換率（農地転用面積 / 宅地開発面積）", fontsize=10)
ax.set_title("市町別 農地転用 / 宅地開発 比率 (2016-2020)\n赤 = 農転が宅地開発を超過（農地の余剰転用？）", fontsize=12)
ax.legend(fontsize=9)
handles = [mpatches.Patch(facecolor="#dc2626",label=">1.0: 農転超過"),
           mpatches.Patch(facecolor="#16a34a",label="≤1.0: 宅地開発超過")]
ax.legend(handles=handles, fontsize=9)
plt.tight_layout()
p8 = ASSETS/"X11_conversion_ratio.png"
plt.savefig(p8, dpi=120); plt.close()
print(f"  図8: {p8.name}, {time.time()-t1:.1f}s")

# =============================================================================
# 9. CSV 出力
# =============================================================================
print("\n=== CSV 出力 ===")
c1 = ASSETS/"X11_city_summary.csv"
df_all[["市町名","新築件数","宅地開発_ha","農地転用_ha","農林施策_ha","行政面積_km2",
        "新築密度_per_km2","開発効率_件per_ha","転換率"]].to_csv(c1, encoding="utf-8-sig")

c2 = ASSETS/"X11_yoto_sub.csv"
df20["YOTO_SUB"].value_counts().reset_index().rename(columns={"YOTO_SUB":"件数","index":"用途"}).to_csv(c2, index=False, encoding="utf-8-sig")

c3 = ASSETS/"X11_year_trend.csv"
year_cnt.reset_index().rename(columns={"year":"年","count":"件数"}).to_csv(c3, index=False, encoding="utf-8-sig")

c4 = ASSETS/"X11_lorenz.csv"
pd.DataFrame({
    "項目":["新築件数","宅地開発_ha","農地転用_ha"],
    "Gini係数":[round(gini20,4), round(gini21,4), round(gini24,4)]
}).to_csv(c4, index=False, encoding="utf-8-sig")

c5 = ASSETS/"X11_conflict.csv"
if len(conflict_by_city) > 0:
    conflict_by_city.rename(index=CITY_NAME).reset_index().to_csv(c5, index=False, encoding="utf-8-sig")
else:
    pd.DataFrame(columns=["市町名","矛盾農転_ha"]).to_csv(c5, index=False, encoding="utf-8-sig")

print("  CSV 5件出力")

# =============================================================================
# 10. 仮説検証
# =============================================================================
def jp(c): return "支持" if c else "反証"

h1 = jp(gini20 >= 0.4)
h2_top3 = [CITY_NAME.get(c,str(c)) for c in top3_cities]
h2 = jp(top3_share >= 70)
housing_rate_all = (df20["yoto_grp"]=="住宅").sum() / len(df20) * 100
h3 = jp(housing_rate_all >= 80)
# H4: 高層 > 20m
high_pct = (df20["TATE_H"] > 20).sum() / len(df20) * 100
h4 = jp(high_pct < 5)  # 高層が少数 → 戸建て中心
# H5: 農転 vs 宅地開発 相関
if len(df_farm) > 3:
    r_f = np.corrcoef(df_farm["農地転用_ha"].values, df_farm["宅地開発_ha"].values)[0,1]
else:
    r_f = 0
h5 = jp(r_f >= 0.5)
# H6: Gini 農地転用
h6 = jp(gini24 >= 0.5)
# H7: 政策矛盾 (農林施策 × 農転 重複) 市町数 >= 3
h7 = jp(conflict_city_count >= 3)
# H8: 転換率 > 1 の市町 >= 3
over1 = (df_all["転換率"] > 1).sum()
h8 = jp(over1 >= 3)
# H9: 宅地開発 Gini と 農地転用 Gini の差 <= 0.1 (同じく偏在)
h9 = jp(abs(gini21 - gini24) <= 0.15)

# =============================================================================
# 11. HTML 出力
# =============================================================================
print("\n=== HTML 組立 ===")
elapsed = time.time() - t0

sections = [
    ("研究の問い (RQ) と仮説", f"""
<h3>L20 × L21 × L24 × L25 統合横断研究</h3>
<p>L20（新築動向）と L21（宅地開発）は「建物の量的分布」を分析したが、
農地はどこから転換されたのか、農林施策との政策矛盾はないかを問うていない。
本記事は農地転用（L24）と農林漁業施策（L25）を加え、
<b>「農地→宅地の転換圧力と政策矛盾」</b>を 4 データセット統合で定量化する。</p>

<h3>主 RQ</h3>
<p>広島県 (2016-2020) の農地→宅地転換はどの市町で集中し、
農林漁業施策ポリゴン内での農地転用（政策矛盾）の面積と市町数はいくらか。</p>

<h3>立てた仮説 (H1〜H9)</h3>
<p><b>RQ1 — 新築建物の集中構造</b></p>
<ul>
<li><b>H1</b>: 新築件数の Gini 係数 ≥ 0.4（高集中、市町間格差大）</li>
<li><b>H2</b>: 上位 3 市（広島・福山・東広島）で県全体の 70% 以上を占める</li>
<li><b>H3</b>: 住宅系（一戸建て・長屋・共同住宅）が新築全体の 80% 以上</li>
</ul>
<p><b>RQ2 — 農地→宅地転換メカニズム</b></p>
<ul>
<li><b>H4</b>: 高層建築 (&gt;20m) は全体の 5% 未満（新築の主役は戸建て低層）</li>
<li><b>H5</b>: 市町別「農地転用面積 vs 宅地開発面積」の Pearson r ≥ 0.5</li>
<li><b>H6</b>: 農地転用面積の Gini 係数 ≥ 0.5（農転は宅地開発より不均等に分布）</li>
</ul>
<p><b>RQ3 — 農林施策との政策矛盾</b></p>
<ul>
<li><b>H7</b>: 農林施策ポリゴン内に農地転用が存在する市町 ≥ 3 件（政策矛盾は稀ではない）</li>
<li><b>H8</b>: 農地転用面積 / 宅地開発面積 &gt; 1.0 の市町 ≥ 3 件（農転が宅地開発を超過する「余剰転用」）</li>
<li><b>H9</b>: 宅地開発 Gini と農地転用 Gini の差 ≤ 0.15（同じ空間構造で偏在）</li>
</ul>
"""),

    ("RQ1: 新築建物の集中構造", f"""
<h3>Lorenz 曲線と年別推移</h3>
{figure("X11_lorenz_trend.png",
        "図1: 土地変換 3 指標の Lorenz 曲線（左）と年別新築件数推移（右）")}
<ul>
<li>新築件数 Gini: <b>{gini20:.3f}</b> → H1 <b>{h1}</b></li>
<li>宅地開発面積 Gini: <b>{gini21:.3f}</b></li>
<li>農地転用面積 Gini: <b>{gini24:.3f}</b> → H6 <b>{h6}</b></li>
<li>2018年（NEW_YCD=3）に小山 — 西日本豪雨 (2018) の復旧建替え需要の影響か</li>
</ul>

<h3>市町別新築件数</h3>
{figure("X11_newbuild_by_city.png",
        "図2: 市町別 新築建物件数 Top20 (2016-2020)")}
<ul>
<li>上位 3 市 ({", ".join(h2_top3)}) の合計シェア: <b>{top3_share:.1f}%</b> → H2 <b>{h2}</b></li>
</ul>

<h3>用途構成と住宅比率</h3>
{figure("X11_yoto_composition.png",
        "図3: 用途構成パイチャート（左）と市町別住宅系比率（右）")}
<ul>
<li>住宅系（一戸建て・長屋・共同住宅）の合計: <b>{housing_rate_all:.1f}%</b> → H3 <b>{h3}</b></li>
<li>一戸建て住宅だけで <b>{df20['YOTO_SUB'].value_counts().get('一戸建ての住宅',0)/len(df20)*100:.1f}%</b> — 広島県の新築は戸建て主体</li>
</ul>

<h3>建物高さ分布</h3>
{figure("X11_height_distribution.png",
        "図7: 建物高さ分布ヒストグラム（左）と高層棟数 Top10（右）")}
<ul>
<li>高層建築 (&gt;20m) 比率: <b>{high_pct:.1f}%</b> → H4 <b>{h4}</b></li>
<li>高さ中央値 {h_vals.median():.1f}m ≈ 2 階建て戸建ての典型値 (6-9m 帯)</li>
</ul>
"""),

    ("RQ2: 農地→宅地 転換メカニズム", f"""
<h3>農地転用と宅地開発の相関</h3>
{figure("X11_farmland_vs_dev.png",
        "図5: 農地転用面積 vs 宅地開発面積 (市町別)")}
<ul>
<li>Pearson r = <b>{r_f:.3f}</b> → H5 <b>{h5}</b></li>
<li>1:1線（農転＝宅地開発）より上の市町 = 宅地開発が農転を超過（建替え・未農地転用を主源とする）</li>
<li>1:1線より下の市町 = 農転が宅地開発を超過（農転したが建てていない「先行転用」）</li>
</ul>

{figure("X11_dev_vs_newbuild.png",
        "図4: 宅地開発面積 vs 新築件数 (市町別)")}
<p>宅地開発と新築件数は概ね正の相関 — 開発面積が大きいほど多くの建物が建てられる。
ただし外れ値（宅地開発は大きいが新築が少ない市町）は「開発後の宅地売れ残り」を示唆。</p>

<h3>農地転用面積 市町別分布</h3>
{figure("X11_farmland_agro.png",
        "図6: 農地転用面積 Top15（左）と農林漁業施策面積 Top15（右）")}
<ul>
<li>農地転用 No.1 市町: <b>{area24_city.rename(index=CITY_NAME).index[0]}</b> ({area24_city.rename(index=CITY_NAME).iloc[0]:.1f} ha)</li>
<li>農林施策 No.1 市町: <b>{area25_city.rename(index=CITY_NAME).sort_values(ascending=False).index[0]}</b> ({area25_city.rename(index=CITY_NAME).sort_values(ascending=False).iloc[0]:.1f} ha)</li>
</ul>

<h3>転換率（農転/宅地開発）</h3>
{figure("X11_conversion_ratio.png",
        "図8: 市町別 転換率（農地転用面積 / 宅地開発面積）")}
<ul>
<li>転換率 &gt; 1.0 の市町（農転超過）: <b>{over1}</b> 市町 → H8 <b>{h8}</b></li>
<li>「宅地開発より多く農転している市町」 = 非住宅目的の農転や余剰転用の可能性</li>
</ul>
"""),

    ("RQ3: 農林施策との政策矛盾", f"""
<h3>農林漁業施策ポリゴン × 農地転用 重複（政策矛盾ゾーン）</h3>
<p>農林漁業施策（L25）は農地・森林・漁業インフラを保全する政策指定ゾーン。
このゾーン内で農地転用（L24）が行われていれば、保全政策と土地利用変換が矛盾する。</p>
<ul>
<li>農林施策 × 農地転用 重複ポリゴン: <b>{n_conflict if 'n_conflict' in dir() else '—'}</b></li>
<li>政策矛盾面積: <b>{conflict_area_ha:.1f} ha</b></li>
<li>政策矛盾が発生した市町数: <b>{conflict_city_count}</b> → H7 <b>{h7}</b></li>
</ul>

<h3>考察 — 農地保全と都市開発の綱引き</h3>
<ul>
<li><b>Gini の差</b>: 宅地開発 Gini ({gini21:.3f}) vs 農地転用 Gini ({gini24:.3f}) の差 = {abs(gini21-gini24):.3f} → H9 <b>{h9}</b></li>
<li>宅地開発は都市集中型（大都市に偏る）、農地転用は農業地帯に偏る傾向あり</li>
<li>農林施策内の農転は「政策の見直しが必要なエリア」の候補として防災・環境計画に活用できる</li>
<li><b>データの限界</b>: L24 農地転用は国土数値情報ベースで精度 1/50,000。
  実際の転用許可面積との差異がある可能性がある（BIKOU 列に記載）</li>
</ul>
"""),

    ("仮説検証と考察", f"""
<h3>仮説 vs 結果</h3>
<table>
<tr><th>仮説</th><th>判定</th><th>根拠</th></tr>
<tr><td>H1: 新築件数 Gini ≥ 0.4</td><td>{h1}</td><td>Gini = {gini20:.3f}</td></tr>
<tr><td>H2: 上位3市シェア ≥ 70%</td><td>{h2}</td><td>{top3_share:.1f}% ({", ".join(h2_top3)})</td></tr>
<tr><td>H3: 住宅系 ≥ 80%</td><td>{h3}</td><td>{housing_rate_all:.1f}%</td></tr>
<tr><td>H4: 高層 &lt;5%</td><td>{h4}</td><td>{high_pct:.1f}%</td></tr>
<tr><td>H5: 農転 vs 宅地 r ≥ 0.5</td><td>{h5}</td><td>r = {r_f:.3f}</td></tr>
<tr><td>H6: 農転 Gini ≥ 0.5</td><td>{h6}</td><td>Gini = {gini24:.3f}</td></tr>
<tr><td>H7: 政策矛盾市町 ≥ 3</td><td>{h7}</td><td>{conflict_city_count} 市町で矛盾発生</td></tr>
<tr><td>H8: 転換率 &gt;1 の市町 ≥ 3</td><td>{h8}</td><td>{over1} 市町</td></tr>
<tr><td>H9: 宅地vs農転 Gini差 ≤ 0.15</td><td>{h9}</td><td>差 = {abs(gini21-gini24):.3f}</td></tr>
</table>

<h3>総合考察</h3>
<ul>
<li>広島県の新築建物は一戸建て住宅が主役（{df20['YOTO_SUB'].value_counts().get('一戸建ての住宅',0)/len(df20)*100:.1f}%）。
  高密化（高層化・共同住宅化）よりも平面的な拡散傾向</li>
<li>農地転用と宅地開発に正の相関 (r={r_f:.3f}) が見られるが、
  転換率 &gt; 1.0 の市町が {over1} 存在 — 宅地化待ちの「先行農転」が一定規模存在する</li>
<li>農林施策内での農転（政策矛盾）は {conflict_city_count} 市町で発生。
  保全指定と実態の乖離として行政計画の見直し材料となる</li>
</ul>
"""),

    ("発展課題 (結果から導かれる新たな問い)", f"""
<ol>
<li><b>転換率 &gt; 1.0 市町の詳細調査</b>:
  <ul>
  <li><b>結果X</b>: 農転超過市町 {over1} 件を同定</li>
  <li><b>新仮説Y</b>: 転換率 &gt; 1.0 の市町は農業後継者不足から農転が非計画的に進んでいる</li>
  <li><b>課題Z</b>: 農業センサス (農林水産省) の農業経営体数と転換率を散布図で比較</li>
  </ul>
</li>
<li><b>建替え vs 新規開発の比率推計</b>:
  <ul>
  <li><b>結果X</b>: 新築 {len(df20):,} 件 vs 宅地開発 {len(df21):,} 件</li>
  <li><b>新仮説Y</b>: 新築件数の 30% 以上は既存宅地の建替えで、宅地開発データに現れない</li>
  <li><b>課題Z</b>: 固定資産台帳（非公開）または都市計画基礎調査と重ね合わせ</li>
  </ul>
</li>
</ol>
"""),

    ("使用データセットと再現方法", f"""
<h3>使用 DoBoX データセット</h3>
<table>
<tr><th>ID</th><th>名称</th><th>件数</th><th>役割</th></tr>
<tr><td><a href="https://hiroshima-dobox.jp/datasets/1332" target="_blank">#1332–1516</a></td>
    <td>都市計画区域情報_新築動向_2016-2020（20市町）</td><td>20</td><td>新築建物 4.6万件</td></tr>
<tr><td><a href="https://hiroshima-dobox.jp/datasets/1351" target="_blank">#1351–1370</a></td>
    <td>都市計画区域情報_宅地開発状況_2016-2020（20市町）</td><td>20</td><td>宅地開発面積</td></tr>
<tr><td><a href="https://hiroshima-dobox.jp/datasets/1391" target="_blank">#1391–1407</a></td>
    <td>都市計画区域情報_農地転用状況_2016-2020（17市町）</td><td>17</td><td>農地転用面積</td></tr>
<tr><td><a href="https://hiroshima-dobox.jp/datasets/1408" target="_blank">#1408–1424</a></td>
    <td>都市計画区域情報_農林漁業関係施策_2022（17市町）</td><td>17</td><td>政策ゾーン（政策矛盾検証）</td></tr>
</table>
<p>合計: <b>74 dataset_id</b></p>
<h3>再現方法</h3>
<ul>
<li>再現コマンド: <code>python -X utf8 lessons/X11_land_conversion.py</code></li>
<li>実行時間: 約 {elapsed:.0f} 秒</li>
<li>生成ファイル: PNG 8 枚 + CSV 5 件 + HTML 1 件</li>
</ul>
"""),
]

html_out = render_lesson(
    num=11,
    title="X11: 宅地開発 × 農地転用 2016-2020 — 土地変換圧力の市町別メカニズム",
    tags=["X系","横断研究","宅地開発","農地転用","農林施策","政策矛盾","Gini","Lorenz"],
    time="90分",
    level="リテラシ基礎+α",
    data_label=(
        '<a href="https://hiroshima-dobox.jp/datasets/1332" target="_blank">新築動向 #1332-1516 (20件)</a> × '
        '<a href="https://hiroshima-dobox.jp/datasets/1351" target="_blank">宅地開発 #1351-1370 (20件)</a> × '
        '<a href="https://hiroshima-dobox.jp/datasets/1391" target="_blank">農地転用 #1391-1407 (17件)</a> × '
        '<a href="https://hiroshima-dobox.jp/datasets/1408" target="_blank">農林施策 #1408-1424 (17件)</a>'
    ),
    sections=sections,
    script_filename="lessons/X11_land_conversion.py",
)
html_out = html_out.replace("<body>", '<body data-draft="1" data-stier="X">', 1)
out_path = LESSONS/"X11_land_conversion.html"
out_path.write_text(html_out, encoding="utf-8")
print(f"\n[HTML] {out_path.name} ({out_path.stat().st_size:,} bytes)")
print(f"=== DONE in {time.time()-t0:.1f}s ===")
