# -*- coding: utf-8 -*-
r"""2호 — 경기 31개 시군의 「아이 잔존비」 (2015~2024)

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

무엇을 하나
  **잔존비 = 그 해 0~4세 주민등록인구 ÷ 직전 5년 출생아 합.**
  1 을 넘으면 그 시군은 태어난 것보다 많은 아이가 «살고 있다»(순유입),
  1 밑이면 태어난 아이 일부가 «다른 곳에 산다»(순유출).

왜 이 지표인가 — 이 매체가 아직 안 한 것(사다리 L3)
  0~4세 인구 표 하나로는 「줄었다」까지만 말할 수 있고 «왜 줄었나»를 못 가른다
  (`03_분석_고도화_방안.md` 한계 #3, 사다리 L3). 이 호는 **표 둘을 붙인다** —
  주민등록인구(있는 아이)와 인구동향조사(태어난 아이). 한 표로는 안 나오는 값이다.

  ⚠ **2026-08-29 정정 — 「브리프 01~08 은 전부 표 하나였다」고 적었던 것은 거짓이다.**
    B03 이 DT_1PL1502+DT_1IN1509 를, B05 가 DT_1YL20581+DT_1B04005N 를 이미 붙였다.
    2호는 «세 번째» 결합이다. 새로운 것은 코드 체계가 다른 두 계열(41↔31)을 붙였다는
    점뿐이고, 그마저 시군 이름이 정확히 일치해 kosis.codes_for 가 대신 풀어 줬다.
    키가 안 맞아 대응표를 손으로 만들고 가드를 건 **B03 의 결합이 더 어렵다.**

★ 코드 체계가 다르다 — 이 결합의 전부가 여기에 걸려 있다
  주민등록 DT_1B04005N 은 **신(新) 코드(경기도=41)**,
  인구동향 DT_1B81A23 은 **구(舊) 코드(경기도=31)**다.
  코드로 붙이면 조용히 어긋난다 (ds_wiki L008). **이름으로 붙인다.**
  kosis.py 가 이름→코드를 표마다 따로 찾아 주므로, 여기서는 이름만 쓴다.

★ 「줄었다」와 「빠져나갔다」는 다르다 — 이 글의 뼈대
  0~4세는 29개 시군에서 줄었다. 그런데 파주는 잔존비 1.2 대이고 군포는 0.7 대다.
  같은 「감소」인데 원인이 반대다. 한 표만 보면 이 둘이 같아 보인다.

★ 잔존비는 «순»이동이다
  들어온 아이와 나간 아이를 가르지 못한다. 100명이 들어오고 100명이 나가도 1.0 이다.
  그래서 본문은 「순유입」이라고만 쓰고 「이사 왔다」로 단정하지 않는다.

자료
  ① 통계청 KOSIS · 주민등록인구현황
     「행정구역(읍면동)별/5세별 주민등록인구(2011년~)」 DT_1B04005N · itmId=T2(총인구수)
     각 연 12월 기준 · 신(新) 행정구역 코드(경기도=41) · **내국인만**
  ② 통계청 KOSIS · 인구동향조사(출생)
     「시군구/출생아수, 합계출산율」 DT_1B81A23 · itmId=T1(출생아수)
     각 연도 · 구(舊) 행정구역 코드(경기도=31)
  ⛔ 2025 년은 쓰지 않는다 — 출생아수 2025 는 **잠정치이고 100명 단위로 반올림**돼 있어
     시군 합이 도 전체와 54명 어긋난다 (`sources.json` 함정). 2024 를 마지막 해로 둔다.
"""
import csv
import sys
from pathlib import Path

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

import chartkit as ck          # 색·폰트·축은 여기 한 곳에서 온다 (호마다 다시 적지 않는다)

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

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

ck.setup()

POP_TBL, POP_ITM = "DT_1B04005N", "T2"     # 주민등록 — 신 코드(41)
BIR_TBL, BIR_ITM = "DT_1B81A23", "T1"      # 인구동향 — 구 코드(31)
DIE_TBL, DIE_ITM = "DT_1B80A18", "T2"      # 사망 — 구 코드(31) · 신뢰성 검토 전용
# ⚠ 사망표는 0~4세가 «한 칸이 아니다» — 「0세」와 「1 - 4세」 두 칸을 더해야 한다(실측).
#   축 순서도 다르다: SBB(성별) → YRE(연령) → S(지역). 지역이 «세 번째»다.
DIE_AGE = ["0세", "1 - 4세"]
GG = "경기도"
AGE0_4 = "0 - 4세"

# 두 개의 «창». 창 = (인구 기준연도, 출생 5개년)
창 = {"2019": ("2019", ("2015", "2019")),
      "2024": ("2024", ("2020", "2024"))}

# ★ 신뢰성 검토용 «대조창» — 발행판에서 «기준 연도만» 한 해씩 옮긴 것.
#   전처리 선택 하나만 바꿔서 결론이 흔들리는지 재기 위한 것이고, 본문 수치가 아니다.
#   ⚠ 흔들어 봤더니 실제로 뒤집혔다(6곳). 그 값을 본문에 싣는다 — 숨기지 않는다.
대조창 = {"2023": ("2023", ("2019", "2023")),
          "2022": ("2022", ("2018", "2022"))}

SGG = ["수원시", "성남시", "의정부시", "안양시", "부천시", "광명시", "평택시", "동두천시",
       "안산시", "고양시", "과천시", "구리시", "남양주시", "오산시", "시흥시", "군포시",
       "의왕시", "하남시", "용인시", "파주시", "이천시", "안성시", "김포시", "화성시",
       "광주시", "양주시", "포천시", "여주시", "연천군", "가평군", "양평군"]

경계 = 0.02      # 잔존비 1.0 에서 이만큼 안이면 「경계」로 본다 (문턱 판정의 견고성)

# ★ 색은 chartkit 정본을 쓴다 — 여기서 다시 선언하지 않는다.
#   PRIMARY(짙은 파랑)와 ACCENT(밝은 주황)는 명도차 23.6 이라 흑백·색각이상에서도 갈린다.
#   NEUTRAL 은 «맥락»색이라 데이터 계열로 오해되지 않을 만큼 옅다 — 경계 시군에 쓴다.
C_IN, C_OUT, C_EDGE = ck.PRIMARY, ck.ACCENT, ck.NEUTRAL


# ── 수집 ────────────────────────────────────────────────────────────
def 인구(y):
    """{지역: 0~4세}. 지역 수가 32(31시군+도)가 아니면 그 자리에서 죽는다."""
    rec = kosis.fetch_rows("101", POP_TBL, y, sel={"A": [GG] + SGG, "B": [AGE0_4]},
                           prefix={"A": "41"}, itm=POP_ITM, quiet=True)
    out = {r["A"]: r["값"] for r in rec}
    if len(out) != len(SGG) + 1:
        sys.exit(f"✖ {y}년 주민등록 지역 {len(out)}개 — {len(SGG)+1}개여야 합니다")
    return out


def 출생(y0, y1):
    """{지역: {연도: 출생아}}. 시군마다 5개 연도가 다 있어야 한다.
    ⚠ 연천·가평·양평은 2023 년에 코드가 바뀌었다(31350→31550 등). 메타가 옛 코드에
      「(변동전)」을 붙여 이름이 겹치지 않으므로 이름으로 고르면 이어진다 — 다만
      «이어졌는지»를 믿지 말고 연도 개수를 센다."""
    rec = kosis.fetch_rows("101", BIR_TBL, (y0, y1), sel={"A": [GG] + SGG},
                           prefix={"A": "31"}, itm=BIR_ITM, quiet=True)
    out = {}
    for r in rec:
        out.setdefault(r["A"], {})[r["연도"]] = r["값"]
    필요 = int(y1) - int(y0) + 1
    나쁨 = {k: sorted(v) for k, v in out.items() if len(v) != 필요}
    if 나쁨 or len(out) != len(SGG) + 1:
        sys.exit(f"✖ 출생아수 수집 이상 — 지역 {len(out)}개 / 연도 부족 {나쁨}")
    return out


def 영유아사망(y0, y1):
    """경기도 0~4세 사망자 합 (신뢰성 검토용 — 본문 수치가 아니다).

    ★ 이 표는 이 호의 «본 분석»에 안 쓴다. 「보정하면 얼마나 움직이나」를 값으로 내려고만 쓴다.
      그래서 도 전체 한 줄만 받는다 — 시군별까지 받으면 이 호가 다른 물건이 된다.
    """
    r = kosis.fetch_rows("101", DIE_TBL, (y0, y1),
                         sel={"S": [GG], "SBB": ["계"], "YRE": DIE_AGE},
                         prefix={"S": "31"}, itm=DIE_ITM, quiet=True)
    필요 = (int(y1) - int(y0) + 1) * len(DIE_AGE)
    if len(r) != 필요:
        sys.exit(f"✖ 사망 {len(r)}건 — {필요}건이어야 합니다 (연도 {y0}~{y1} × 연령 {len(DIE_AGE)}칸)")
    return sum(x["값"] for x in r)


print("[수집] 주민등록 0~4세 · 출생아수 · 영유아 사망")
전체창 = dict(창, **대조창)
pop = {k: 인구(v[0]) for k, v in 전체창.items()}
bir = {k: 출생(*v[1]) for k, v in 전체창.items()}

사망_2024창 = 영유아사망("2020", "2024")
print(f"  · 영유아 사망 2020~2024 경기도 {사망_2024창:,}명 (0세 + 1~4세)")

# ★ 검산 — 시군 합이 도 전체와 맞는가. 안 맞으면 결합 자체가 틀린 것이다.
for w in 창:      # 대조창은 본문 수치가 아니라 검산 대상이 아니다
    for 이름, d in (("주민등록", pop[w]), ("출생아수", {k: sum(v.values()) for k, v in bir[w].items()})):
        합 = sum(d[s] for s in SGG)
        if 합 != d[GG]:
            sys.exit(f"✖ {w}창 {이름}: 시군 합 {합:,} ≠ 경기도 {d[GG]:,}")
        print(f"  · {w}창 {이름} 시군 합 = 경기도 {d[GG]:,} ✓")


# ── 계산 ────────────────────────────────────────────────────────────
def 잔존비(w, s):
    return pop[w][s] / sum(bir[w][s].values())


rows = []
for s in SGG + [GG]:
    r19, r24 = 잔존비("2019", s), 잔존비("2024", s)
    시군인가 = s != GG
    rows.append({
        "시군": s,
        "유형": "시군" if 시군인가 else "도전체",
        "인구0_4_2019": pop["2019"][s],
        "인구0_4_2024": pop["2024"][s],
        "출생합_1519": sum(bir["2019"][s].values()),
        "출생합_2024": sum(bir["2024"][s].values()),
        "잔존비_2019": round(r19, 3),
        "잔존비_2024": round(r24, 3),
        "잔존비차": round(r24 - r19, 3),
        "인구증감": pop["2024"][s] - pop["2019"][s],
        # ↓ 세는 열. B08 과 같은 방식 — aggsum 으로 세고, aggcount 는 「0」도 세므로 쓰지 않는다
        "넘음": int(r24 > 1),
        "밑돎": int(r24 < 1),
        "같은편": int((r19 - 1) * (r24 - 1) > 0),
        "경계근접": int(abs(r24 - 1) <= 경계),
        "인구늘어남": int(pop["2024"][s] > pop["2019"][s]),
        # ↓ 신뢰성 검토 — 기준 연도만 옮긴 대조판
        "잔존비_2023": round(잔존비("2023", s), 3),
        "잔존비_2022": round(잔존비("2022", s), 3),
        # ★ 문턱 폭도 «값»이다 — 본문이 「±0.02」라고 쓰면 독자가 읽는 수치이고,
        #   관문 C4 는 대장에 없는 수치를 잡는다. 편집 판단을 코드 상수로만 두면
        #   본문의 그 숫자는 «어디서 왔는지 모르는 숫자»가 된다.
        "경계폭": 경계,
    })

# ★ 「기준 연도를 한 해씩 옮기면 편(1 초과/미만)이 뒤집히는가」
#   ⚠ 경계 근처라 뒤집히는 것과, 값이 크게 움직여 뒤집히는 것은 다르다 — 둘 다 센다.
for r in rows:
    편 = {r[c] > 1 for c in ("잔존비_2024", "잔존비_2023", "잔존비_2022")}
    r["기준연도뒤집힘"] = int(len(편) > 1)
    폭 = max(r[c] for c in ("잔존비_2024", "잔존비_2023", "잔존비_2022")) \
        - min(r[c] for c in ("잔존비_2024", "잔존비_2023", "잔존비_2022"))
    r["기준연도흔들림"] = round(폭, 3)
    # 경계(±경계) 밖인데도 뒤집힌 곳 — 「경계라서 그렇다」로 설명이 안 되는 자리
    r["경계밖뒤집힘"] = int(r["기준연도뒤집힘"] and not r["경계근접"])
    r["넘음_2023"] = int(r["잔존비_2023"] > 1)

시군행 = [r for r in rows if r["유형"] == "시군"]
도 = next(r for r in rows if r["유형"] == "도전체")

# ★ 신뢰성 검토 ② — 영유아 사망을 보정하면 잔존비가 얼마나 움직이나
#   보정하면 «분모»(태어난 아이)에서 그동안 숨진 아이를 빼야 하므로 잔존비는 올라간다.
#   ⚠ 시군별로는 안 받았으므로 이 값은 «도 전체»에 대한 것이다. 그 한계를 검증 칸에 적는다.
#   ⚠ 빈 칸을 남기지 않는다 — sanity 의 결측 검사는 «목록에 적힌 칸»만 보므로,
#     빈 칸을 두고 목록에서 빼면 그 칸은 영영 검사 밖이 된다(조용한 실패).
#     경계폭 때와 같이 «도 전체 값»을 모든 행에 채우고 이름으로 그 사실을 밝힌다.
도사망 = 사망_2024창
도보정 = round(도["인구0_4_2024"] / (도["출생합_2024"] - 도사망), 4)
for r in rows:
    r["영유아사망_도"] = 도사망
    r["사망보정잔존비_도"] = 도보정

out = Path("raw.csv")
with out.open("w", newline="", encoding="utf-8-sig") as f:
    w = csv.DictWriter(f, fieldnames=list(rows[0]))
    w.writeheader()
    w.writerows(rows)
print(f"[저장] {out}  {len(rows)}행")


# ── 대장에 넣을 값 (손으로 옮기지 말고 여기서 읽는다) ──────────────────
def 값(s, k):
    return next(r[k] for r in rows if r["시군"] == s)


import statistics as st
r24 = [r["잔존비_2024"] for r in 시군행]
최고 = max(시군행, key=lambda r: r["잔존비_2024"])
최저 = min(시군행, key=lambda r: r["잔존비_2024"])

print("\n── 수치대장용 ──────────────────────────────")
print(f"N01 시군수                     {len(시군행)}")
print(f"N02 경기 0~4세 2024            {도['인구0_4_2024']:,}")
print(f"N03 경기 0~4세 2019            {도['인구0_4_2019']:,}")
print(f"N04 경기 0~4세 감소율          {(도['인구0_4_2024']-도['인구0_4_2019'])/도['인구0_4_2019']*100:.2f} %")
print(f"N05 경기 출생합 2020~2024      {도['출생합_2024']:,}")
print(f"N06 경기 잔존비                {도['잔존비_2024']:.3f}")
print(f"N07 시군 잔존비 평균           {st.mean(r24):.3f}")
print(f"N08 시군 잔존비 중앙값         {st.median(r24):.3f}")
print(f"N09 최고 {최고['시군']:<6}          {최고['잔존비_2024']:.3f}")
print(f"N10 최저 {최저['시군']:<6}          {최저['잔존비_2024']:.3f}")
print(f"N11 잔존비 범위(최고-최저)     {최고['잔존비_2024']-최저['잔존비_2024']:.3f}")
print(f"N12 1 넘은 시군 수             {sum(r['넘음'] for r in 시군행)}")
print(f"N13 1 밑돈 시군 수             {sum(r['밑돎'] for r in 시군행)}")
print(f"N14 경계(±{경계}) 안 시군 수    {sum(r['경계근접'] for r in 시군행)}")
print(f"N15 두 창 같은 편 시군 수      {sum(r['같은편'] for r in 시군행)}")
print(f"N16 0~4세 늘어난 시군 수       {sum(r['인구늘어남'] for r in 시군행)}")
print(f"\n── 신뢰성 검토 ② 사망 보정 ──")
print(f"N58 영유아 사망 2020~2024      {사망_2024창:,}")
print(f"N59 사망 보정 잔존비           {도보정:.4f}  (원래 {도['잔존비_2024']:.3f})")
print(f"N60 움직임                     {도보정 - 도['잔존비_2024']:+.4f}"
      f"  ← 경계 {경계} 보다 {'작다' if abs(도보정-도['잔존비_2024']) < 경계 else '크다'}")

뒤집힌 = [r["시군"] for r in 시군행 if r["기준연도뒤집힘"]]
경계밖 = [r["시군"] for r in 시군행 if r["경계밖뒤집힘"]]
가장흔들린 = max(시군행, key=lambda r: r["기준연도흔들림"])
print(f"\n── 신뢰성 검토 (기준 연도를 2022·2023 으로 옮겨 봄) ──")
print(f"N52 편이 뒤집힌 시군 수        {len(뒤집힌)}  — {', '.join(뒤집힌)}")
print(f"N53 그중 경계 밖에서 뒤집힘     {len(경계밖)}  — {', '.join(경계밖)}")
print(f"N54 가장 크게 흔들린 곳         {가장흔들린['시군']} {가장흔들린['기준연도흔들림']:.3f}")
print(f"    {가장흔들린['시군']}: 2024 {가장흔들린['잔존비_2024']:.3f} · 2023 {가장흔들린['잔존비_2023']:.3f} · 2022 {가장흔들린['잔존비_2022']:.3f}")
for k in ("2024", "2023", "2022"):
    넘 = sum(1 for r in 시군행 if r["잔존비_" + k] > 1)
    print(f"    기준 {k}: 1 초과 {넘}곳 · 1 미만 {sum(1 for r in 시군행 if r['잔존비_'+k] < 1)}곳")

for s in ("파주시", "군포시", "하남시", "양주시", "안산시"):
    print(f"    {s}  0~4세증감 {값(s,'인구증감'):>8,}  잔존비 19창 {값(s,'잔존비_2019'):.3f} → 24창 {값(s,'잔존비_2024'):.3f}")


# ── 그림 ────────────────────────────────────────────────────────────
def 색(v):
    return C_EDGE if abs(v - 1) <= 경계 else (C_IN if v > 1 else C_OUT)


order = sorted(시군행, key=lambda r: r["잔존비_2024"])
fig, ax = plt.subplots(figsize=(9.4, 8.8))
# ★ 막대를 0 이 아니라 «1.0» 에서 뻗게 한다.
#   0 에서 뻗으면 축을 0.7 에서 자를 수밖에 없고, 그러면 0.76 과 1.51 의 «길이 비»가
#   실제 비율과 달라져 눈이 속는다. 1.0 은 이 지표의 자연스러운 기준점이다
#   (태어난 만큼 살고 있다). 편차 막대는 그 기준에서 얼마나 벗어났는지를 곧이 보여준다.
ax.barh([r["시군"] for r in order], [r["잔존비_2024"] - 1 for r in order], left=1,
        color=[색(r["잔존비_2024"]) for r in order], height=.72)
ax.axvline(1, color=ck.INK, lw=1.4, zorder=5)
ax.axvline(도["잔존비_2024"], color=ck.SUB, lw=1.0, ls="--", zorder=4)
for i, r in enumerate(order):
    v = r["잔존비_2024"]
    쪽 = 1 if v >= 1 else -1
    ax.text(v + .008 * 쪽, i, f"{v:.2f}", va="center", fontsize=8.8,
            color=ck.글자색(색(v)), ha="left" if 쪽 > 0 else "right")
ax.set_xlim(0.70, 1.60)
ax.set_ylim(-2.1, len(order) - .3)
ax.text(도["잔존비_2024"] + .006, -1.5, f'경기도 {도["잔존비_2024"]:.2f}',
        color=ck.SUB, fontsize=9.5, va="center")
ax.text(1 - .008, -1.5, "1.0  태어난 만큼 살고 있다", color=ck.INK,
        fontsize=9.5, va="center", ha="right", fontweight="bold")
ax.set_xlabel("잔존비 = 2024년 0~4세 인구 ÷ 2020~2024년 출생아 합   (막대는 1.0 에서 뻗는다)",
              fontsize=10.5, color=ck.INK)
ax.set_title("아이는 태어난 곳에 살고 있지 않다 — 경기 31개 시군", fontsize=13.5, pad=14)
ck.tidy(ax, grid="x")
ck.save(fig, "chart1.png", dpi=170)
print("\n[저장] chart1.png")

# 두 창 — 기울기 그래프
def 겹침풀기(항목, 최소간격):
    """(값, 라벨, 색) 목록의 y 를 최소간격 이상 벌린다 — 이름표가 겹치면 못 읽는다."""
    항목 = sorted(항목, key=lambda x: x[0])
    y = [x[0] for x in 항목]
    for i in range(1, len(y)):
        if y[i] - y[i - 1] < 최소간격:
            y[i] = y[i - 1] + 최소간격
    return [(y[i], 항목[i][1], 항목[i][2]) for i in range(len(항목))]


fig, ax = plt.subplots(figsize=(8.4, 8.2))
# ⚠ 선에 쓰는 색과 «그 선 이름표»의 색은 다르다 — 글자는 대비 4.5 를 넘어야 한다.
강조 = {"하남시": C_OUT, "김포시": C_OUT, "시흥시": C_OUT,
        "양주시": C_IN, "과천시": C_IN}
글자 = {nm: ck.글자색(c) for nm, c in 강조.items()}
for r in 시군행:
    nm = r["시군"]
    c = 강조.get(nm, C_EDGE)
    ax.plot([0, 1], [r["잔존비_2019"], r["잔존비_2024"]], color=c,
            lw=2.4 if nm in 강조 else 1.0, zorder=3 if nm in 강조 else 1)
ax.plot([0, 1], [도["잔존비_2019"], 도["잔존비_2024"]], color=ck.INK, lw=2.0,
        ls="--", zorder=4)

왼 = [(값(n, "잔존비_2019"), f'{값(n, "잔존비_2019"):.2f} {n}', 글자[n]) for n in 강조]
왼 += [(도["잔존비_2019"], f'{도["잔존비_2019"]:.2f} 경기도', ck.INK)]
오 = [(값(n, "잔존비_2024"), f'{n} {값(n, "잔존비_2024"):.2f}', 글자[n]) for n in 강조]
오 += [(도["잔존비_2024"], f'경기도 {도["잔존비_2024"]:.2f}', ck.INK)]
for y, s, c in 겹침풀기(왼, .035):
    ax.text(-0.04, y, s, color=c, fontsize=10, va="center", ha="right", fontweight="bold")
for y, s, c in 겹침풀기(오, .035):
    ax.text(1.04, y, s, color=c, fontsize=10, va="center", fontweight="bold")

ax.axhline(1, color=ck.ZERO, lw=1.1, zorder=2)
ax.text(0.5, 1.004, "1.0", color=ck.SUB, fontsize=9.5, ha="center")
ax.set_xticks([0, 1])
ax.set_xticklabels(["2015~2019년생\n(2019년 0~4세)", "2020~2024년생\n(2024년 0~4세)"], fontsize=10.5)
ax.set_xlim(-0.42, 1.42)
ax.set_ylabel("잔존비", fontsize=10.5, color=ck.INK)
ax.set_title("신도시가 채워지면 잔존비는 내려온다 — 굵은 선은 위아래 다섯 곳", fontsize=13, pad=14)
ck.tidy(ax, grid="y", 아래축=False)
ck.save(fig, "chart2.png", dpi=170)
print("[저장] chart2.png")
print("\n완료 — run.py 가 다음 단계(sanity → verify)로 넘어갑니다.")
