# -*- coding: utf-8 -*-
r"""브리프 02 — 경기 31개 시군 1인가구의 연령 구성 2015 → 2024

    py prep.py            (run.py 가 analysis/ 를 cwd로 잡고 실행한다)

하는 일
  ① KOSIS OpenAPI 에서 원자료를 받아 raw.csv 로 남긴다 (재현성)
  ② 시군별 청년(34세 이하) · 노년(65세 이상) 비중을 아령형 차트로 그린다

★ 숫자는 전부 여기서 계산한다. 본문에 손으로 옮겨 적지 않는다.
  1호에서 틀린 3건은 전부 본문에 손으로 쓴 숫자였다 (doc_wiki L011 부수교훈).
  이 스크립트가 찍는 [대장] 블록의 값을 수치대장.csv 에 그대로 옮긴다.

자료
  통계청 KOSIS · 인구총조사(등록센서스)
  「성 및 연령별 1인가구 - 시군구」 orgId=101 tblId=DT_1PL1502
  itmId T00=1인가구 · objL1 = 경기도(31) + 31개 시군 · objL2 = 연령 16구간 · prdSe=Y

  ※ 이 표는 구(舊) 행정구역 코드를 쓴다 — 경기도가 41 이 아니라 31 이다.
    주민등록(DT_1B04005N, 브리프 01)의 41xxx 코드를 그대로 넣으면 0건이 온다.
"""
import csv, sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))   # 저장소 루트의 kosis.py
import kosis

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# ⛔ 그림 저장은 정본(`chartkit.save`)을 지난다 〔2026-09-21〕 — 그래야
#   ⑴ 폰용 그림이 같이 나가고(`analysis/폰/`) ⑵ 「폰에서 읽히나」 관문이 돈다.
#   `plt.savefig` 를 직접 부르면 그 둘이 통째로 꺼진다.
import chartkit as ck

try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

plt.rcParams["font.family"] = ["Malgun Gothic", "sans-serif"]   # '맑은 고딕'은 미등록명 → 경고만 쏟아진다
plt.rcParams["axes.unicode_minus"] = False

ORG, TBL, ITM = "101", "DT_1PL1502", "T00"          # T00 = 1인가구(남녀 계)
# ★ 코드는 한 줄도 적지 않는다 — 전부 이름으로 쓰고 kosis.codes_for 가 메타에서 찾아온다.
#   이 표는 코드가 라벨과 한 칸 어긋나 있고(065=60~64세, 070=65~69세),
#   지역은 구 행정구역 코드라 경기도가 41이 아니라 31이다. 둘 다 조용히 틀린다 (ds_wiki L008).
GG = ["경기도"]
# 31개 시군. 일반구(장안구 등)와 동부·읍부·면부 집계행은 이름을 안 부르므로 애초에 안 들어온다.
SIGUN = ["수원시", "성남시", "의정부시", "안양시", "부천시", "광명시", "평택시", "동두천시",
         "안산시", "고양시", "과천시", "구리시", "남양주시", "오산시", "시흥시", "군포시",
         "의왕시", "하남시", "용인시", "파주시", "이천시", "안성시", "김포시", "화성시",
         "광주시", "양주시", "포천시", "여주시", "연천군", "가평군", "양평군"]
AGE = ["합계", "20세미만", "20~24세", "25~29세", "30~34세", "35~39세", "40~44세", "45~49세",
       "50~54세", "55~59세", "60~64세", "65~69세", "70~74세", "75~79세", "80~84", "85세이상"]
YOUNG = ["20세미만", "20~24세", "25~29세", "30~34세"]           # 34세 이하
OLD   = ["65~69세", "70~74세", "75~79세", "80~84", "85세이상"]  # 65세 이상
Y0, Y1 = "2015", "2024"
HL = "안산시"

# 청년 / 노년. 주황은 브리프 01과 같은 값이고, 파랑은 01의 #3d5a80 에서 채도를 올렸다
# — 두 색을 나란히 쓰는 범주형에서는 #3d5a80 이 채도 하한(0.1) 미달로 회색처럼 읽힌다
# (dataviz validate_palette.js: chroma 0.071 FAIL). #2f6fb5 는 전 항목 통과.
C_Y, C_O = "#2f6fb5", "#c8482a"
C_HL = "#ef5b3b"


def share(a, bins):
    return round(100 * sum(a[b] for b in bins) / a["합계"], 2)


# 이름 → 코드. 표에 없는 이름이면 여기서 죽는다 (조용히 다른 구간을 집지 않는다).
C_REGION = kosis.codes_for(ORG, TBL, "A", GG + SIGUN, prefix="31")
C_AGE    = kosis.codes_for(ORG, TBL, "B", AGE)
print(f"[코드] 지역 {len(C_REGION)}개 · 연령 {len(C_AGE)}개 — 이름으로 해석함")
print(f"[대장] 청년 원문위치 → {kosis.cite(ORG, TBL, 'B', YOUNG)}")
print(f"[대장] 노년 원문위치 → {kosis.cite(ORG, TBL, 'B', OLD)}")

d0, d1 = (kosis.fetch(ORG, TBL, y, objL1=C_REGION, objL2=C_AGE, itmId=ITM,
                      expect_l1=len(C_REGION), expect_l2=len(C_AGE))
          for y in (Y0, Y1))
print(f"[수집] {Y0} {len(d0)}개 · {Y1} {len(d1)}개 지역")

rows = []
for nm in sorted(d1):
    if nm == "경기도":
        continue
    a0, a1 = d0[nm], d1[nm]
    # 비중뿐 아니라 인원수도 남긴다. 비중만 있으면 경기도 값이 가중평균이라
    # 검증 관문(C2)이 원자료에서 재계산할 방법이 없다 — 31개 시군 비중의 단순평균은
    # 경기도 비중과 다른 값이다.
    rows.append({
        "시군": nm,
        f"1인가구_{Y0}": a0["합계"], f"1인가구_{Y1}": a1["합계"],
        f"청년_{Y0}": sum(a0[b] for b in YOUNG), f"청년_{Y1}": sum(a1[b] for b in YOUNG),
        f"노년_{Y0}": sum(a0[b] for b in OLD),   f"노년_{Y1}": sum(a1[b] for b in OLD),
        f"청년비중_{Y0}": share(a0, YOUNG), f"청년비중_{Y1}": share(a1, YOUNG),
        f"노년비중_{Y0}": share(a0, OLD),   f"노년비중_{Y1}": share(a1, OLD),
        "노년빼기청년": round(share(a1, OLD) - share(a1, YOUNG), 2),
    })
# 정렬 기준은 「노년비중 − 청년비중」이다. 청년비중만으로 정렬하면 안양(25.8/28.6)처럼
# 청년비중은 높은데 노년형인 시군이 섞여 들어가 청년형/노년형 경계가 끊긴다.
rows.sort(key=lambda r: r["노년빼기청년"])

with open("raw.csv", "w", encoding="utf-8-sig", newline="") as f:
    w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
    w.writeheader()
    w.writerows(rows)

g0, g1 = d0["경기도"], d1["경기도"]
gy1, go1 = share(g1, YOUNG), share(g1, OLD)
gy0, go0 = share(g0, YOUNG), share(g0, OLD)
older = [r for r in rows if r["노년빼기청년"] > 0]          # 노년형 시군
younger = [r for r in rows if r["노년빼기청년"] < 0]        # 청년형 시군
by_y = sorted(rows, key=lambda r: -r[f"청년비중_{Y1}"])
top_y, bot_y = by_y[0], by_y[-1]
by_o = sorted(rows, key=lambda r: -r[f"노년비중_{Y1}"])
ypos = {r["시군"]: len(rows) - 1 - i for i, r in enumerate(rows)}   # 차트 y좌표
hl = [r for r in rows if r["시군"] == HL][0]

# 31개 시군 합이 경기도 값과 맞는지 — 맞아야 대장의 aggsum 파생식이 성립한다
for lab, col, ref in ((f"1인가구 {Y0}", f"1인가구_{Y0}", g0["합계"]),
                      (f"1인가구 {Y1}", f"1인가구_{Y1}", g1["합계"]),
                      (f"청년   {Y1}", f"청년_{Y1}", sum(g1[b] for b in YOUNG)),
                      (f"노년   {Y1}", f"노년_{Y1}", sum(g1[b] for b in OLD))):
    s = sum(r[col] for r in rows)
    print(f"[대조] {lab} — 31개 시군 합 {s:,} vs 경기도 {ref:,} "
          f"→ {'일치' if s == ref else '★불일치(대장 파생식 조정 필요)'}")

# ── 차트 ── 아령형: 시군마다 청년 비중과 노년 비중을 선으로 잇는다
fig, ax = plt.subplots(figsize=(7.6, 9.0))
names = [r["시군"] for r in rows]
ys = list(range(len(rows)))[::-1]                     # 위쪽이 청년 비중 높은 시군
for y, r in zip(ys, rows):
    a, b = r[f"노년비중_{Y1}"], r[f"청년비중_{Y1}"]
    ax.plot([a, b], [y, y], color="#c9ced8", lw=1.6, zorder=1,
            solid_capstyle="round")
    ax.scatter([a], [y], s=46, color=C_O, zorder=3, edgecolors="white", linewidths=1.1)
    ax.scatter([b], [y], s=46, color=C_Y, zorder=3, edgecolors="white", linewidths=1.1)
ax.axvline(gy1, color=C_Y, lw=1.0, ls="--", alpha=.55)
ax.axvline(go1, color=C_O, lw=1.0, ls="--", alpha=.55)
# 평균선 라벨은 표 아래로 뺀다 — 위쪽에 두면 최상단 시군 행을 덮는다.
# 두 선이 2%p 밖에 안 떨어져 있어 같은 높이에 나란히 쓰면 글자끼리 겹친다 → 2단으로.
ax.text(gy1 + .5, -.75, f"경기 평균 청년 {gy1:.1f}%", color=C_Y, fontsize=8.6,
        va="center", ha="left")
ax.text(go1 - .5, -1.75, f"경기 평균 노년 {go1:.1f}%", color=C_O, fontsize=8.6,
        va="center", ha="right")
# 안산은 색이 아니라 배경 띠 + 굵은 글씨로 강조한다 — 강조색을 쓰면 노년 계열색과 헷갈린다
ax.axhspan(ypos[HL] - .5, ypos[HL] + .5, color="#eef1f6", zorder=0)
ax.set_yticks(ys)
ax.set_yticklabels(names)
for lab in ax.get_yticklabels():
    if lab.get_text() == HL:
        lab.set_color("#15171c"); lab.set_fontweight("bold")
# 청년형 / 노년형 경계 — 12:19 을 눈으로 셀 수 있게 선 하나로 가른다
sep = len(rows) - len(younger) - .5
ax.axhline(sep, color="#98a2b3", lw=.9, ls=(0, (5, 4)))
ax.text(53.4, sep - .55, f"↓ 노년형 {len(older)}개 시군", color="#6b7280",
        fontsize=8.8, ha="right", va="top")
ax.text(53.4, sep + .55, f"↑ 청년형 {len(younger)}개 시군", color="#6b7280",
        fontsize=8.8, ha="right", va="bottom")

# 청년비중 최고·최저 두 곳만 직접 라벨 — 모든 점에 숫자를 붙이지 않는다.
# 라벨은 점의 바깥쪽 옆에 같은 높이로 붙인다. 위로 올리면(y+.55) 행 간격이 1이라
# 바로 윗 시군의 라벨처럼 읽힌다 — 실제로 37.9%가 화성시 줄에 찍혔다.
for r in (top_y, bot_y):
    y = ypos[r["시군"]]
    a, b = r[f"노년비중_{Y1}"], r[f"청년비중_{Y1}"]
    lo, hi = (a, b) if a < b else (b, a)
    c_lo, c_hi = (C_O, C_Y) if a < b else (C_Y, C_O)
    ax.text(lo - 1.5, y, f"{lo:.1f}%", color=c_lo, fontsize=8.4,
            ha="right", va="center", fontweight="bold")
    ax.text(hi + 1.5, y, f"{hi:.1f}%", color=c_hi, fontsize=8.4,
            ha="left", va="center", fontweight="bold")
ax.scatter([], [], s=46, color=C_Y, label="34세 이하")
ax.scatter([], [], s=46, color=C_O, label="65세 이상")
# 범례는 오른쪽 위로 — 아래쪽은 연천·양평의 노년 점(47%대)이 차지하고 있다
ax.legend(loc="upper right", frameon=False, fontsize=9.4, handletextpad=.3)
ax.set_xlim(2.5, 54)      # 왼쪽 여백은 최좌측 점(양평 10.1%)의 직접 라벨 자리
ax.set_ylim(-2.5, len(rows) + .6)      # 아래 두 줄은 경기 평균선 라벨 자리
ax.set_xlabel(f"{Y1}년 1인가구 중 해당 연령대가 차지하는 비중 (%)", fontsize=10)
ax.tick_params(axis="y", labelsize=9.5, length=0)
ax.tick_params(axis="x", labelsize=9)
for s in ("top", "right", "left"):
    ax.spines[s].set_visible(False)
ax.spines["bottom"].set_color("#cfd4de")
ax.grid(axis="x", color="#eef0f4", lw=.8)
ax.set_axisbelow(True)
fig.tight_layout()
ck.save(fig, "chart.png", dpi=130)

print(f"[저장] raw.csv {len(rows)}행 · chart.png")
cy0, cy1 = sum(g0[b] for b in YOUNG), sum(g1[b] for b in YOUNG)
co0, co1 = sum(g0[b] for b in OLD),   sum(g1[b] for b in OLD)
print("\n[대장] 수치대장.csv 에 옮길 값 ─────────────────")
print(f"  경기도 1인가구 {Y0}          {g0['합계']:,}")
print(f"  경기도 1인가구 {Y1}          {g1['합계']:,}")
print(f"  경기도 1인가구 증가율        {100*(g1['합계']/g0['합계']-1):+.2f}%")
print(f"  경기 청년 1인가구 {Y0}/{Y1}  {cy0:,} → {cy1:,}  ({100*(cy1/cy0-1):+.2f}%)")
print(f"  경기 노년 1인가구 {Y0}/{Y1}  {co0:,} → {co1:,}  ({100*(co1/co0-1):+.2f}%)")
print(f"  경기 청년비중 {Y0} / {Y1}     {gy0:.2f}% → {gy1:.2f}%  ({gy1-gy0:+.2f}%p)")
print(f"  경기 노년비중 {Y0} / {Y1}     {go0:.2f}% → {go1:.2f}%  ({go1-go0:+.2f}%p)")
print(f"  노년형 시군 수               {len(older)}개 / {len(rows)}개")
print(f"  청년형 시군 수               {len(younger)}개 / {len(rows)}개")
print(f"  청년비중 최고                {top_y['시군']} {top_y[f'청년비중_{Y1}']:.2f}%")
print(f"  청년비중 최저                {bot_y['시군']} {bot_y[f'청년비중_{Y1}']:.2f}%")
print(f"  청년비중 격차                {top_y[f'청년비중_{Y1}'] - bot_y[f'청년비중_{Y1}']:.2f}%p")
print(f"  노년비중 최고                {by_o[0]['시군']} {by_o[0][f'노년비중_{Y1}']:.2f}%")
print(f"  노년비중 최저                {by_o[-1]['시군']} {by_o[-1][f'노년비중_{Y1}']:.2f}%")
print(f"  {HL} 1인가구 {Y1}          {hl[f'1인가구_{Y1}']:,}")
print(f"  {HL} 청년/노년 {Y1}        {hl[f'청년비중_{Y1}']:.2f}% / {hl[f'노년비중_{Y1}']:.2f}%")
print(f"  {HL} 노년비중 {Y0}→{Y1}    {hl[f'노년비중_{Y0}']:.2f}% → {hl[f'노년비중_{Y1}']:.2f}%")
