01 데이터 입수 — 분기 자료·외생 경로·BOKDPM 기준선·기존 전망¶

목적. 5장의 전망·정합화와 전달 산출물 계산에 필요한 입력을 불러오고 검증한다.

2026-10 정비판. 이전 판은 4장의 바텀업 전망(2026-08-28)과 총량 전망(2026-09-01)을 동결 입력으로 받았다. 정비판은 3장 정비 결과(외생 6종, BOKDPM 기준선)로 이 패키지 안에서 바텀업·총량 전망을 다시 계산한다. 계산에 쓰는 명세는 4장에서 선택한 것과 같다(산업 = 차분·산업연관표·VARX, 총량 = 수준·산업연관표·VARX). 이전 판 공표 산출물은 data/published_prev/에 두고 비교와 이전 판 재현 점검에만 쓴다.

입력 내용 출처
upstream/levels_{36,62,69,agg}.csv 산업·총량 분기 실질부가가치(계절조정, 십억 원) 2장 산출물
upstream/io_linkage_{36,62,69}.csv 산업연관 연계 행렬(산업연관표 사전) 2장 산출물
upstream/exog_hist_v2.csv, exog_scen_v2.csv 외생 6종 투입형 과거값(2000Q1–2025Q4)과 S1–S5 경로(2026Q1–2035Q4) — 정비판(실질금리 1차 차분, 인력수급 전망 노동공급, 레버 연결) 3장 산출물(2026-10 정비)
upstream/bokdpm_lgdp_2000_2035.csv BOKDPM 실질GDP(100·ln): 과거 평활값 + 기준선 전망(잠재성장률 1.8% 수렴) 3장 산출물(2026-10 정비)
prev_keis_va_62.csv 기존 전망(2024–2034판) 62분류 실질부가가치, 2000–2035, 백만 원 KEIS 전달 엑셀 VA 시트 추출
template_62_VA_v1.xlsx 전달 템플릿(62분류 VA, 2000–2034) KEIS 전달 양식
data/published_prev/ 이전 판 공표 산출물(바텀업·총량·정합화 전망, 요약·총계, 전달본 값, 카드 자료) 2026-09-03 공표

산출물. data/interim/input_check.csv, upstream_check.csv, prev_total_keis.csv, prev_version_totals.csv

In [1]:
import os
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")   # numpy보다 먼저 지정한다(config.py 주석 참조)
import json
import numpy as np, pandas as pd
import config as C
import gva_results as R
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 · openpyxl 3.1.5 · matplotlib 3.11.0
  드리프트 재설정 목표 1.8% · 수축 가중치 w=0.4(이전 판 0.45) · 원본 엑셀 대조: 없음
  기존 전망 62     있음  data\inputs\prev_keis_va_62.csv
  전달 템플릿       있음  data\inputs\template_62_VA_v1.xlsx
  분기 자료(상류)    있음  data\inputs\upstream
  정비판 기록       있음  data\published
  이전 판 공표      있음  data\published_prev

1. 동결 파일 대조¶

config.INPUT_SHA256에 기록한 SHA-256과 현재 파일을 대조한다. 하나라도 다르면 이후 계산 결과를 기록 수치와 비교할 수 없으므로 여기서 멈춘다. 원 프로젝트 경로와 수집 경위는 data/inputs/PROVENANCE.md에 적었다.

In [2]:
rows = []
for rel, h in C.INPUT_SHA256.items():
    p = os.path.join(C.DATA, rel)
    got = R.sha256(p) if os.path.exists(p) else ""
    rows.append({"파일": rel, "크기(bytes)": os.path.getsize(p) if got else 0, "SHA-256(앞 16자리)": got[:16], "일치": got == h})
chk = pd.DataFrame(rows)
chk.to_csv(os.path.join(C.INTERIM, "input_check.csv"), index=False, encoding="utf-8-sig")
print(f"대조한 파일 {len(chk)}개 · 일치 {int(chk['일치'].sum())}개")
assert chk["일치"].all(), "SHA-256이 다른 파일이 있다: " + ", ".join(chk.loc[~chk["일치"], "파일"])
chk.head(8)
대조한 파일 33개 · 일치 33개
Out[2]:
파일 크기(bytes) SHA-256(앞 16자리) 일치
0 inputs/prev_keis_va_62.csv 122295 3fcdf8dea130b00a True
1 inputs/template_62_VA_v1.xlsx 51987 952ddeb5933b7abc True
2 inputs/upstream/bokdpm_lgdp_2000_2035.csv 3857 47988ac9d5959298 True
3 inputs/upstream/exog_hist_v2.csv 5521 d142ff2992b5ae3e True
4 inputs/upstream/exog_scen_v2.csv 11096 f45f8007d7b88faf True
5 inputs/upstream/io_linkage_36.csv 11035 cf11efc744586d35 True
6 inputs/upstream/io_linkage_62.csv 67817 3f747a97c3804541 True
7 inputs/upstream/io_linkage_69.csv 79382 1f57d01c6867b456 True

2. 분기 자료와 외생 경로¶

산업 패널은 표본 2000Q1–2025Q4(104분기)를 쓴다. 62·69분류는 36분류를 세분한 것이므로 분기별 산업 합계가 세 분류에서 같아야 한다. 외생 6종의 과거값은 104분기, 시나리오 경로는 S1–S5 × 40분기이며 결측이 없어야 한다. BOKDPM 실질GDP는 2000Q1–2035Q4(144분기)이다.

In [3]:
rows = []
for k, p in C.UP.items():
    df = pd.read_csv(p, index_col=0, encoding="utf-8-sig")
    rows.append({"자료": k, "파일": os.path.relpath(p, C.HERE), "행": df.shape[0], "열": df.shape[1],
                 "첫 행": str(df.index[0]), "끝 행": str(df.index[-1]), "결측": int(df.isna().sum().sum())})
up = pd.DataFrame(rows)
up.to_csv(os.path.join(C.INTERIM, "upstream_check.csv"), index=False, encoding="utf-8-sig")
qs = [q for q in pd.read_csv(C.UP["levels_62"], index_col=0).index if "2000Q1" <= q <= "2025Q4"]
sums = pd.DataFrame({r: pd.read_csv(C.UP[f"levels_{r}"], index_col=0).reindex(qs).drop(columns="총량GDP").sum(axis=1) for r in C.RES})
gap = float((sums.max(axis=1) - sums.min(axis=1)).max())
print(f"분기 산업 합계의 분류 간 최대 차이: {gap:.1e}십억 원")
assert gap < 1e-6
ex_s = pd.read_csv(C.UP["exog_scen"], encoding="utf-8-sig")
assert sorted(ex_s["시나리오"].unique()) == C.SCENARIOS and len(ex_s) == 200 and not ex_s.isna().any().any()
lg = pd.read_csv(C.UP["lgdp"], index_col=0, encoding="utf-8-sig")["LGDP_kor"]
ga = R.bokdpm_annual_gdp(lg)
print(f"BOKDPM 실질GDP 연간 증가율(%): " + ", ".join(f"{y} {100 * (ga[y] / ga[y - 1] - 1):.2f}" for y in [2025, 2026, 2028, 2030, 2035]))
up
분기 산업 합계의 분류 간 최대 차이: 3.5e-10십억 원
BOKDPM 실질GDP 연간 증가율(%): 2025 1.83, 2026 2.19, 2028 2.24, 2030 1.89, 2035 1.82
Out[3]:
자료 파일 행 열 첫 행 끝 행 결측
0 levels_36 data\inputs\upstream\levels_36.csv 184 37 1980Q1 2025Q4 0
1 levels_62 data\inputs\upstream\levels_62.csv 104 63 2000Q1 2025Q4 0
2 levels_69 data\inputs\upstream\levels_69.csv 104 70 2000Q1 2025Q4 0
3 levels_agg data\inputs\upstream\levels_agg.csv 184 2 1980Q1 2025Q4 0
4 io_36 data\inputs\upstream\io_linkage_36.csv 36 36 농림어업 기타 서비스업 0
5 io_62 data\inputs\upstream\io_linkage_62.csv 62 62 1.농업 62.수리ㆍ개인서비스업 0
6 io_69 data\inputs\upstream\io_linkage_69.csv 69 69 1.농업 토목건설 0
7 exog_hist data\inputs\upstream\exog_hist_v2.csv 104 6 2000Q1 2025Q4 0
8 exog_scen data\inputs\upstream\exog_scen_v2.csv 200 7 S1 S5 0
9 lgdp data\inputs\upstream\bokdpm_lgdp_2000_2035.csv 144 1 2000Q1 2035Q4 0

3. 기존 전망(2024–2034판)¶

한국고용정보원에 전달된 기존 전망 엑셀(62분류 부가가치·총산출, 불변 백만 원, 2000–2050)의 VA 시트에서 2000–2035년 열을 추출한 것이다. 2023년까지는 실적이고 2024년부터는 전망이다. 전산업 행은 62개 산업의 합이며, 정합화의 기존 전망 성장인자는 이 전산업 행에서 만든다. 전달 템플릿(template_62_VA_v1.xlsx)은 같은 시트를 2034년까지 자른 것이므로 2000–2034년 값이 추출본과 같아야 한다.

In [4]:
prev = pd.read_csv(C.PREV_KEIS, encoding="utf-8-sig")
ptot = prev[prev["번호"] == 0].set_index("연도")["VA_백만원"]
psum = prev[prev["번호"] > 0].groupby("연도")["VA_백만원"].sum()
print(f"산업 {prev.loc[prev['번호'] > 0, '번호'].nunique()}개 · {prev['연도'].min()}–{prev['연도'].max()}"
      f" · 전산업 행 − 산업 합 최대 상대차 {((ptot - psum).abs() / ptot).max():.1e}")
assert ((ptot - psum).abs() / ptot).max() < 1e-12

tmpl = R.extract_va_sheet(C.TEMPLATE, "62분류_불변VA", 2000, 2034)
m = tmpl.merge(prev, on=["번호", "연도"], suffixes=("_템플릿", "_기존"))
d_tmpl = (m["VA_백만원_템플릿"] - m["VA_백만원_기존"]).abs().max()
print(f"전달 템플릿 2000–2034 대 기존 전망 추출본: {len(m)}칸 대조, 최대 절대차 {d_tmpl:.2e}백만 원")
assert len(m) == len(tmpl) and d_tmpl < 1e-6

if C.SOURCE_DIR:                                   # 선택: 원본 엑셀에서 다시 추출해 동결 CSV와 대조
    fn, sheet = C.SOURCE_XLSX["prev_keis_va_62.csv"]
    src = os.path.join(C.SOURCE_DIR, fn)
    re_ex = R.extract_va_sheet(src, sheet, 2000, 2035)
    dd = (re_ex["VA_백만원"].values - prev["VA_백만원"].values)
    print(f"원본 엑셀 재추출 대조: {len(re_ex)}칸, 최대 절대차 {np.abs(dd).max():.2e}백만 원 (원본 SHA-256 {R.sha256(src)[:16]}…)")
    assert len(re_ex) == len(prev) and np.abs(dd).max() < 1e-5
else:
    print("원본 엑셀 재추출 대조: VAF_SOURCE_DIR이 없어 건너뛴다(동결 CSV를 쓴다).")

pt = pd.DataFrame({"전산업_백만원": ptot, "증가율_%": ptot.pct_change() * 100})
pt.to_csv(os.path.join(C.INTERIM, "prev_total_keis.csv"), encoding="utf-8-sig")
print(f"기존 전망 전산업: 2024년 {ptot[2024]/1e6:,.1f}조 → 2034년 {ptot[2034]/1e6:,.1f}조, 연평균 {R.cagr(ptot[2024], ptot[2034], 10):.2f}%")
pt.loc[2023:2035].T
산업 62개 · 2000–2035 · 전산업 행 − 산업 합 최대 상대차 3.9e-16
전달 템플릿 2000–2034 대 기존 전망 추출본: 2205칸 대조, 최대 절대차 4.77e-07백만 원
원본 엑셀 재추출 대조: VAF_SOURCE_DIR이 없어 건너뛴다(동결 CSV를 쓴다).
기존 전망 전산업: 2024년 2,292.2조 → 2034년 2,694.5조, 연평균 1.63%
Out[4]:
연도 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035
전산업_백만원 2,247,177,700.000 2,292,202,400.000 2,315,124,424.000 2,352,166,414.784 2,393,329,327.043 2,436,409,254.929 2,481,295,288.770 2,525,452,043.662 2,568,862,636.658 2,611,510,600.783 2,653,379,932.051 2,694,455,129.796 2,734,721,231.247
증가율_% 1.583 2.004 1.000 1.600 1.750 1.800 1.842 1.780 1.719 1.660 1.603 1.548 1.494

4. 이전 판 공표 산출물 — 비교 기준¶

이전 판(2026-09-03 공표)의 바텀업·총량·정합화 전망을 요약한다. 정비판과의 비교(03 표 6)와 이전 판 전달본 재현 점검(03 검증)에 쓴다.

In [5]:
rows = []
for lab, f in [("바텀업 62", "bottomup_annual_62.csv"), ("총량 VARX", "topdown_annual_AGG.csv"), ("정합화 62", "forecast_annual_62.csv")]:
    t = R.totals(R.read_long(os.path.join(C.PREV_DIR, f)))
    a = t.loc[C.BASE_YEAR, "실적"]
    for s in C.SCENARIOS:
        rows.append({"구분": lab, "시나리오": s, "2025(조원)": a / 1e3, "2035(조원)": t.loc[C.END_YEAR, s] / 1e3,
                     "연평균(%)": R.cagr(a, t.loc[C.END_YEAR, s], 10)})
pv = pd.DataFrame(rows)
pv.to_csv(os.path.join(C.INTERIM, "prev_version_totals.csv"), index=False, encoding="utf-8-sig")
pv.pivot(index="시나리오", columns="구분", values="연평균(%)").style.format("{:.2f}")
Out[5]:
구분 바텀업 62 정합화 62 총량 VARX
시나리오      
S1 4.88 2.13 2.49
S2 4.29 1.98 2.23
S3 5.17 2.20 2.63
S4 5.66 2.32 2.85
S5 4.56 2.03 2.32

5. 산출물¶

다음 노트북(02_분석.ipynb)은 data/inputs/의 입력을 직접 읽는다. 아래 중간 산출물은 입수 단계의 점검 기록이다.

In [6]:
for f in sorted(os.listdir(C.INTERIM)):
    p = os.path.join(C.INTERIM, f)
    print(f"{f:<28} {os.path.getsize(p):>8,d} bytes")
fill_check_prev.xlsx           42,463 bytes
input_check.csv                 2,164 bytes
prev_total_keis.csv             1,533 bytes
prev_version_totals.csv           986 bytes
upstream_check.csv                792 bytes