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
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에 적었다.
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개
| 파일 | 크기(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분기)이다.
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
| 자료 | 파일 | 행 | 열 | 첫 행 | 끝 행 | 결측 | |
|---|---|---|---|---|---|---|---|
| 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년 값이 추출본과 같아야 한다.
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%
| 연도 | 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 검증)에 쓴다.
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}")
| 구분 | 바텀업 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/의 입력을 직접 읽는다. 아래 중간 산출물은 입수 단계의 점검 기록이다.
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