01 데이터 입수 — 취업자·실질부가가치·시나리오 전망¶

목적. 취업계수 방식 노동수요 전망에 필요한 입력을 불러오고 검증한다.

2026-10 정비판. 보고서1은 거시 기준선을 다시 작성하였다. BOKDPM의 장기 정상상태를 현재 여건에 맞추어 고치고(잠재성장률 1.8%, 물가목표 2%, 실질유가 일정), 실질금리 외생 투입을 1차 차분으로 바꾸었으며, 외생 경로에 전달되지 않던 시나리오 레버를 연결한 뒤 산업 전망과 정합화를 다시 계산하였다. 이 패키지는 그 결과인 5장 정비판 최종 전망으로 주 투입을 바꾸었다. 취업계수의 기준 수준·추세·계수 경로를 정하는 방법과 취업자·부가가치 이력 자료는 바꾸지 않았다. 이전 판(2026-10-08 판)의 투입·산출 파일은 data/published_prev/에 보존하여 재현 점검과 비교에 쓴다.

입력 내용 출처 쓰임
data/inputs/emp_section_q.csv 산업 대분류(A–U)별 취업자, 분기, 계절조정·원계열, 2013Q1–2025Q4 국가데이터처(옛 통계청) 경제활동인구조사(ECOS 901Y027) 취업계수
data/inputs/va36_hist_q.csv 36개 산업 실질부가가치, 계절조정, 분기, 2013Q1–2025Q4 한국은행 국민계정(ECOS 200Y104) 취업계수
data/inputs/va36_reconciled_annual.csv 36개 산업 시나리오(S1–S5) 실질부가가치 전망, 연간, 2026–2035 보고서1 5장 정비판 최종 전망(5장 재현 패키지 forecast_annual_36.csv) 주 산출물 투입
data/published_prev/va36_reconciled_annual_prev.csv 같은 형식, 정비 이전 5장 최종 전망(2026-09-03 판) 이전 판(2026-10-08 판) 패키지의 주 투입 이전 판 재현·비교
data/published_prev/labor_demand_viewer_data_prev.json 이전 판 노동수요 산출 파일(포털 뷰어 형식) 이전 판(2026-10-08 판) 패키지의 주 산출물 이전 판 재현 대조
data/inputs/scenario_viewer_39q_data.json 36개 산업 시나리오 실질부가가치 전망, 분기, 2026Q1–2035Q4(정합화 이전) 포털 「산업별 시나리오 전망」 게시 파일(2026-08-04) 최초 판(v0) 재현 투입
data/section_map_36_19.csv 36개 산업 → 대분류 19개 대응표 본 연구 설계 취업계수

모든 입력은 공개 통계이거나 본 연구 모형의 공개 산출물이다.

산출물. data/interim/emp_section_annual_sa.csv, emp_section_annual_nsa.csv, va36_hist_annual.csv, va36_recon_annual.csv, va36_recon_prev_annual.csv, va36_scen_annual.csv

기본 실행은 고정 입력만 쓴다. ONLINE=1로 실행하면 6절에서 ECOS와 포털의 공개 원천을 다시 받아 고정 입력과 대조한다(ECOS 인증키는 환경변수 API_KEY_BOK).

In [1]:
import os, json
import numpy as np, pandas as pd
import config as C
import labor_demand as F
pd.set_option("display.float_format", lambda v: f"{v:,.3f}")
C.setup()
Python 3.12.10 · pandas 2.2.3 · numpy 2.4.6 · matplotlib 3.11.0
  스위치: ONLINE=0 · FULL=0 · API_KEY_BOK 없음
  취업자(분기)            있음  data\inputs\emp_section_q.csv
  실질VA 이력(분기)        있음  data\inputs\va36_hist_q.csv
  주 투입: 정비판 VA       있음  data\inputs\va36_reconciled_annual.csv
  이전 판 투입: VA        있음  data\published_prev\va36_reconciled_annual_prev.csv
  이전 판 산출 JSON       있음  data\published_prev\labor_demand_viewer_data_prev.json
  최초 판 투입: 분기 VA     있음  data\inputs\scenario_viewer_39q_data.json
  최초 판 게시 JSON       있음  data\published\labor_demand_viewer_data_v0_pre-reconciliation.json
  산업 대응표             있음  data\section_map_36_19.csv

1. 고정 입력 확인¶

각 파일의 SHA-256을 config.INPUT_SHA256에 기록된 값과 대조한다. 하나라도 다르면 실행을 멈춘다.

In [2]:
rows = []
for f, want in C.INPUT_SHA256.items():
    p = os.path.join(C.DATA, f)
    h = F.sha256(p)
    rows.append({"파일(data/ 기준)": f, "바이트": os.path.getsize(p), "SHA-256(앞 16자)": h[:16], "기록값 일치": h == want})
chk = pd.DataFrame(rows)
display(chk)
assert chk["기록값 일치"].all(), "고정 입력이 기록된 SHA-256과 다르다"
파일(data/ 기준) 바이트 SHA-256(앞 16자) 기록값 일치
0 inputs/emp_section_q.csv 64507 d631cdcc9aec591e True
1 inputs/va36_hist_q.csv 94279 465641eba29c51a8 True
2 inputs/va36_reconciled_annual.csv 85765 4085f853bbc9ee18 True
3 inputs/scenario_viewer_39q_data.json 66341 3873c3c575a6c1b1 True
4 section_map_36_19.csv 2231 b02dbd44aaa03371 True
5 published_prev/va36_reconciled_annual_prev.csv 85770 779114acd0168928 True
6 published_prev/labor_demand_viewer_data_prev.json 17222 f3346aee10c08f89 True

2. 취업자 — 경제활동인구조사 산업 대분류¶

ECOS 901Y027은 한국표준산업분류 대분류 21개(A–U)의 취업자를 2013년부터 제공한다. 계산에는 분기 계절조정 계열(구분코드 I28B)을 쓰며, 연간 값은 4개 분기의 평균이다. 원계열(I28A)은 02의 명세 비교에만 쓴다. T(가구 내 고용활동)와 U(국제 및 외국기관)는 36개 산업 부가가치 분류에 대응시키지 않으므로 계산에서 제외한다. 두 대분류의 비중은 2013년 0.7%, 2016년 이후 0.3% 수준이다. 계절조정은 대분류별로 따로 하므로 대분류 합계와 전체 취업자 계절조정 계열 사이에는 수천 명 이내의 차이가 있다.

In [3]:
emp_q = {adj: F.read_employment(C.EMP_Q, adj) for adj in ["계절조정", "원계열"]}
for adj, q in emp_q.items():
    print(f"{adj}: {q.index.min()}–{q.index.max()} · {q.shape[0]}개 분기 · 대분류 {q.shape[1] - 1}개 + 전체")
emp_a = {adj: F.annual_employment(q) for adj, q in emp_q.items()}
a = emp_a["계절조정"]
tu = a[["T", "U"]].sum(axis=1)
pd.DataFrame({"전체(천명)": a["전체"], "A–S 합계(천명)": a[F.SECTIONS].sum(axis=1),
              "T+U(천명)": tu, "T+U 비중(%)": 100 * tu / a["전체"],
              "A–U 합계 − 전체(천명)": a[F.SECTIONS + ["T", "U"]].sum(axis=1) - a["전체"]}).loc[[2013, 2016, 2019, 2022, 2023, 2024, 2025]]
계절조정: 2013Q1–2025Q4 · 52개 분기 · 대분류 21개 + 전체
원계열: 2013Q1–2025Q4 · 52개 분기 · 대분류 21개 + 전체
Out[3]:
전체(천명) A–S 합계(천명) T+U(천명) T+U 비중(%) A–U 합계 − 전체(천명)
연도
2013 25,298.250 25,113.500 180.250 0.712 -4.500
2016 26,408.750 26,321.500 86.250 0.327 -1.000
2019 27,123.750 27,039.750 86.500 0.319 2.500
2022 28,091.250 27,993.000 96.250 0.343 -2.000
2023 28,418.750 28,321.250 94.500 0.333 -3.000
2024 28,577.250 28,482.250 94.500 0.331 -0.500
2025 28,769.000 28,681.500 91.000 0.316 3.500

3. 실질부가가치 이력 — 36개 산업¶

한국은행 경제활동별 GDP(계절조정, 실질, 분기) 36개 말단 산업이다. 단위는 십억 원(2020년 연쇄가격)이고, 연간 값은 4개 분기의 합계이다. 대응표는 각 산업을 하나의 대분류에 배정한다. 제조업(C)은 13개, 건설업(F)은 4개, 전기·가스(D)와 정보통신(J)은 각 2개 산업을 묶는다.

In [4]:
va_q = F.read_va_hist(C.VA_HIST_Q)
smap = pd.read_csv(C.SECTION_MAP, dtype={"산업코드": str})
sec_of = dict(zip(smap["산업명"], smap["대분류"]))
assert set(va_q.columns) == set(sec_of), "대응표와 부가가치 자료의 산업 이름이 다르다"
assert sorted(set(sec_of.values())) == F.SECTIONS, "대응표의 대분류가 A–S 19개가 아니다"
va_a = F.annual_va(va_q)
print(f"실질부가가치: {va_q.index.min()}–{va_q.index.max()} · {va_q.shape[0]}개 분기 · 산업 {va_q.shape[1]}개")
print(f"36개 산업 합계: 2013년 {va_a.loc[2013].sum() / 1e3:,.1f}조 원 → 2025년 {va_a.loc[2025].sum() / 1e3:,.1f}조 원")
smap.groupby(["대분류", "대분류명"])["산업명"].apply(lambda s: " · ".join(s)).to_frame("36개 산업")
실질부가가치: 2013Q1–2025Q4 · 52개 분기 · 산업 36개
36개 산업 합계: 2013년 1,599.2조 원 → 2025년 2,178.5조 원
Out[4]:
36개 산업
대분류 대분류명
A 농업, 임업 및 어업 농림어업
B 광업 광업
C 제조업 음식료품 제조업 · 섬유 및 가죽제품 제조업 · 목재, 종이, 인쇄 및 복제업 · ...
D 전기, 가스, 증기 및 공기조절 공급업 전기업 · 가스, 증기 및 공기조절 공급업
E 수도, 하수 및 폐기물 처리, 원료 재생업 수도, 하수 및 폐기물 처리, 원료 재생업
F 건설업 주거용 건물 건설업 · 비주거용 건물 건설업 · 토목건설업 · 건축보수업
G 도매 및 소매업 도소매업
H 운수 및 창고업 운수업
I 숙박 및 음식점업 숙박 및 음식점업
J 정보통신업 통신업 · 출판, 방송, 영상, 정보서비스
K 금융 및 보험업 금융 및 보험업
L 부동산업 부동산업
M 전문, 과학 및 기술 서비스업 전문, 과학 및 기술관련 서비스업
N 사업시설 관리, 사업 지원 및 임대 서비스업 사업지원 서비스업
O 공공행정, 국방 및 사회보장 행정 공공행정, 국방 및 사회보장
P 교육 서비스업 교육서비스업
Q 보건업 및 사회복지 서비스업 의료, 보건업 및 사회복지서비스업
R 예술, 스포츠 및 여가관련 서비스업 예술, 스포츠 및 여가관련 서비스업
S 협회 및 단체, 수리 및 기타 개인 서비스업 기타 서비스업

4. 시나리오 실질부가가치 전망 — 주 투입, 이전 판 투입, 최초 판 투입¶

**주 투입(정비판)**은 보고서1 5장의 2026-10 정비판 최종 전망이다. 산업 VARX 합산 경로를 총량 전망에 맞추어 비례배분(산업 구성비 유지)하고 기존 전망과 결합한 연간 값이다.

이전 판 투입은 정비 이전의 5장 최종 전망(2026-09-03 판)이며, 이전 판(2026-10-08 판) 노동수요가 투입한 값이다. 파일 형식과 산업·연도·시나리오 구성은 주 투입과 같다.

최초 판 투입은 2026-08-04 포털 「산업별 시나리오 전망」에 게시된 정합화 이전 분기 전망(연간 값은 4개 분기 합계)이다. 같은 날 게시된 노동수요 자료(최초 판, v0)가 이 값을 투입했으며, 이 패키지는 그 파일을 바이트 단위까지 재현하는 데에만 이 자료를 쓴다.

표의 연평균 증가율은 2025년 실적(36개 산업 합계)에서 2035년 전망까지의 값이다.

In [5]:
recon = F.read_reconciled(C.RECON_CSV)
recon_prev = F.read_reconciled(C.PREV_RECON_CSV)
scen_a = F.scenario_annual(F.read_scenario_json(C.SCEN_JSON))
for s in C.SCENARIOS:
    for d in (recon, recon_prev, scen_a):
        assert set(d[s].columns) == set(sec_of) and list(d[s].index) == C.FORECAST_YEARS
b25 = va_a.loc[2025].sum()
print(f"2025년 실적(36개 산업 합계): {b25 / 1e3:,.1f}조 원")
pd.DataFrame({s: {"정비판 2026(조원)": recon[s].loc[2026].sum() / 1e3,
                  "정비판 2035(조원)": recon[s].loc[2035].sum() / 1e3,
                  "정비판 연평균(%)": F.cagr(b25, recon[s].loc[2035].sum(), 10),
                  "이전 판 2035(조원)": recon_prev[s].loc[2035].sum() / 1e3,
                  "이전 판 연평균(%)": F.cagr(b25, recon_prev[s].loc[2035].sum(), 10),
                  "정비판 − 이전 판 2035(조원)": (recon[s].loc[2035].sum() - recon_prev[s].loc[2035].sum()) / 1e3,
                  "최초 판 2035(조원)": scen_a[s].loc[2035].sum() / 1e3,
                  "최초 판 연평균(%)": F.cagr(b25, scen_a[s].loc[2035].sum(), 10)} for s in C.SCENARIOS}).T.round(2)
2025년 실적(36개 산업 합계): 2,178.5조 원
Out[5]:
정비판 2026(조원) 정비판 2035(조원) 정비판 연평균(%) 이전 판 2035(조원) 이전 판 연평균(%) 정비판 − 이전 판 2035(조원) 최초 판 2035(조원) 최초 판 연평균(%)
S1 2,221.070 2,609.030 1.820 2,688.520 2.130 -79.490 3,285.330 4.190
S2 2,198.320 2,605.860 1.810 2,650.290 1.980 -44.430 3,218.600 3.980
S3 2,233.440 2,612.180 1.830 2,708.630 2.200 -96.450 3,312.210 4.280
S4 2,233.580 2,607.700 1.810 2,740.690 2.320 -132.990 3,307.090 4.260
S5 2,203.210 2,610.230 1.820 2,664.240 2.030 -54.010 3,259.990 4.110

5. 주 투입의 원본 대조 — 5장 재현 패키지¶

주 투입은 5장 재현 패키지의 동결 산출물 s5-results/data/published/forecast_annual_36.csv(실적 2000–2025년, S1–S5 2026–2035년)에서 S1–S5·2026–2035년 행만 문자 그대로 옮긴 파일이다. 이 패키지가 5장 패키지와 같은 폴더 구조(repro/report-gva/)에 있으면 다음을 확인한다.

  • 원본의 SHA-256이 config.SOURCE_SHA256과 같은지
  • 원본에서 추린 행이 고정 입력과 바이트 단위로 같은지
  • 원본의 2025년 실적 행이 va36_hist_q.csv의 2025년 4개 분기 합계와 같은지

배포 묶음처럼 원본이 없으면 이 절을 건너뛴다. 결과는 보고만 하고 실행을 멈추지 않는다. 고정 입력 자체의 동일성은 1절의 SHA-256 대조가 보장한다.

In [6]:
if not os.path.exists(C.S5_SOURCE):
    print(f"5장 원본이 없어 건너뛴다({os.path.relpath(C.S5_SOURCE, C.HERE)}). 기록된 원본 SHA-256: {C.SOURCE_SHA256[:16]}…")
else:
    raw = open(C.S5_SOURCE, "rb").read()
    hs = F.sha256_bytes(raw)
    same = F.reconciled_subset_bytes(raw) == open(C.RECON_CSV, "rb").read()
    src = pd.read_csv(C.S5_SOURCE)
    a25 = src[(src["시나리오"] == "실적") & (src["연도"] == 2025)].set_index("산업")["실질VA_십억원"]
    d25 = (a25 - va_a.loc[2025, a25.index]).abs().max()
    tag = ("기록값(정비판)과 일치" if hs == C.SOURCE_SHA256 else
           "이전 판 원본(2026-09-03 판)과 일치" if hs == C.PREV_SOURCE_SHA256 else "기록값과 다름 — 5장 산출물이 바뀌었으므로 주 투입을 다시 만들어야 한다")
    display(pd.DataFrame([
        {"항목": "원본 SHA-256", "결과": f"{hs[:16]}… · {tag}"},
        {"항목": "원본의 S1–S5·2026–2035 행 = 고정 입력", "결과": "바이트 단위 일치" if same else "다름"},
        {"항목": "원본 2025년 실적 = va36_hist_q 2025년 합계", "결과": f"{len(a25)}개 산업 · 최대 차이 {d25:.2e}십억 원"}]))
항목 결과
0 원본 SHA-256 ae396b0eead6d696… · 기록값(정비판)과 일치
1 원본의 S1–S5·2026–2035 행 = 고정 입력 바이트 단위 일치
2 원본 2025년 실적 = va36_hist_q 2025년 합계 36개 산업 · 최대 차이 7.28e-12십억 원

6. 공개 원천과 대조(ONLINE=1)¶

  • 통합 번들(gva-scenario-bundle.zip)을 내려받아 주 투입의 원본(industry/output/forecast_annual_36.csv)이 고정 입력과 같은지 확인한다. 번들이 아직 이전 판 원본(2026-09-03 판)을 담고 있으면 그 사실을 보고한다.
  • 포털의 최초 판 투입 자료(scenario_viewer_39q_data.json)의 SHA-256을 확인한다.
  • 포털의 노동수요 JSON(현행 주소와 최초 판 주소)을 data/interim/portal/에 내려받는다. 산출물과의 비교는 03_결과.ipynb에서 한다.
  • ECOS에서 취업자(901Y027)와 실질부가가치(200Y104)를 다시 받아 비교한다. 계절조정 계열은 새 자료가 들어올 때마다 과거 값이 다시 추정되므로 수집 시점에 따라 작은 차이가 생길 수 있다.

차이는 보고만 하고 실행을 멈추지 않는다. 취업자·부가가치 고정 입력은 2026-10-08에 수집했다.

In [7]:
if not C.ONLINE:
    print("ONLINE=0: 고정 입력을 그대로 쓴다. 공개 원천과 다시 대조하려면 ONLINE=1로 실행한다.")
else:
    os.makedirs(C.PORTAL_DL, exist_ok=True)
    res = []
    zp = os.path.join(C.PORTAL_DL, "gva-scenario-bundle.zip")
    h, st = F.try_download(C.URL_BUNDLE, zp)
    if h is None:
        res.append({"항목": "통합 번들", "결과": st})
    else:
        raw = F.bundle_member(zp, C.BUNDLE_MEMBER)
        hm = F.sha256_bytes(raw)
        sub = F.reconciled_subset(raw)
        fix = pd.read_csv(C.RECON_CSV)
        m = fix.merge(sub, on=["시나리오", "연도", "산업"], how="outer", suffixes=("_고정", "_번들"))
        dmax = (m["실질VA_십억원_고정"] - m["실질VA_십억원_번들"]).abs().max()
        tag = ("정비판 원본과 일치" if hm == C.SOURCE_SHA256 else
               "이전 판 원본(2026-09-03 판)과 일치 — 포털 번들이 아직 갱신되지 않았다" if hm == C.PREV_SOURCE_SHA256 else
               "기록값과 다름(" + hm[:12] + "…)")
        res.append({"항목": f"번들 {C.BUNDLE_MEMBER}",
                    "결과": f"SHA-256 {tag} · S1–S5·2026–2035 {len(sub)}행, 고정 입력과 최대 차이 {dmax:.3f}십억 원"})
    h, st = F.try_download(C.URL_SCEN, os.path.join(C.PORTAL_DL, "scenario_viewer_39q_data.json"))
    res.append({"항목": "포털 scenario_viewer_39q_data.json",
                "결과": st if h is None else ("SHA-256 일치" if h == C.INPUT_SHA256["inputs/scenario_viewer_39q_data.json"] else f"SHA-256 다름({h[:12]}…)")})
    for name, url in [("labor_demand_viewer_data.json", C.URL_MAIN),
                      ("labor_demand_viewer_data_v0_pre-reconciliation.json", C.URL_V0)]:
        h, st = F.try_download(url, os.path.join(C.PORTAL_DL, name))
        res.append({"항목": f"포털 {name}", "결과": st if h is None else f"내려받음(SHA-256 {h[:12]}…), 03에서 비교"})
    key = os.environ.get(C.KEY_ENV)
    if not key:
        res.append({"항목": "ECOS", "결과": f"환경변수 {C.KEY_ENV}가 없어 건너뛴다"})
    else:
        e_new = F.fetch_employment(key, "2013Q1", "2025Q4")
        e_old = pd.read_csv(C.EMP_Q, dtype={"분기": str})
        m = e_old.merge(e_new, on=["분기", "대분류", "구분"], how="outer", suffixes=("_고정", "_재수집"))
        for adj in ["계절조정", "원계열"]:
            d = m[m["구분"] == adj]
            diff = (d["취업자_천명_고정"] - d["취업자_천명_재수집"]).abs()
            res.append({"항목": f"ECOS 901Y027 취업자({adj})",
                        "결과": f"{len(d)}개 값 · 최대 차이 {diff.max():.1f}천 명 · 결측 {int(diff.isna().sum())}개"})
        v_new = F.fetch_va36(key, smap["산업코드"].tolist())
        v_old = pd.read_csv(C.VA_HIST_Q, dtype={"분기": str, "산업코드": str})
        mv = v_old.merge(v_new, on=["분기", "산업코드"], how="outer", suffixes=("_고정", "_재수집"))
        dv = (mv["실질VA_십억원_고정"] - mv["실질VA_십억원_재수집"]).abs()
        res.append({"항목": "ECOS 200Y104 실질부가가치",
                    "결과": f"{len(mv)}개 값 · 최대 차이 {dv.max():.1f}십억 원 · 결측 {int(dv.isna().sum())}개"})
    display(pd.DataFrame(res))
ONLINE=0: 고정 입력을 그대로 쓴다. 공개 원천과 다시 대조하려면 ONLINE=1로 실행한다.

7. 산출물¶

다음 노트북(02_분석.ipynb)은 아래 중간 산출물과 대응표를 읽는다.

In [8]:
emp_a["계절조정"].to_csv(os.path.join(C.INTERIM, "emp_section_annual_sa.csv"), encoding="utf-8-sig")
emp_a["원계열"].to_csv(os.path.join(C.INTERIM, "emp_section_annual_nsa.csv"), encoding="utf-8-sig")
va_a.to_csv(os.path.join(C.INTERIM, "va36_hist_annual.csv"), encoding="utf-8-sig")
def long(d):
    return pd.concat([v.stack().rename("실질VA_십억원").reset_index().set_axis(["연도", "산업", "실질VA_십억원"], axis=1).assign(시나리오=s)
                      for s, v in d.items()])[["시나리오", "연도", "산업", "실질VA_십억원"]]
long(recon).to_csv(os.path.join(C.INTERIM, "va36_recon_annual.csv"), index=False, encoding="utf-8-sig")
long(recon_prev).to_csv(os.path.join(C.INTERIM, "va36_recon_prev_annual.csv"), index=False, encoding="utf-8-sig")
long(scen_a).to_csv(os.path.join(C.INTERIM, "va36_scen_annual.csv"), index=False, encoding="utf-8-sig")
for f in sorted(os.listdir(C.INTERIM)):
    p = os.path.join(C.INTERIM, f)
    if os.path.isfile(p):
        print(f"{f:<28} {os.path.getsize(p):>8,d} bytes")
emp_section_annual_nsa.csv      2,120 bytes
emp_section_annual_sa.csv       2,130 bytes
va36_hist_annual.csv            5,184 bytes
va36_recon_annual.csv          85,765 bytes
va36_recon_prev_annual.csv     85,770 bytes
va36_scen_annual.csv           86,440 bytes