01 데이터 입수 — 산업 패널·외생 6종·유가 원천¶
목적. 외생 6종 진단에 필요한 동결 입력을 불러오고, 파일이 온전한지와 입력 사이의 변환 관계가 맞는지 확인한다.
입력(data/inputs/) |
내용 | 출처 | 공개 |
|---|---|---|---|
levels_agg.csv |
총량 모형 패널: 실질 GDP(계절조정, 십억원) 1980Q1–2025Q4 | 보고서1 2장 산출물(ECOS) | 공개 |
levels_sa.csv, levels_62.csv, levels_69.csv |
36·62·69분류 분기 실질부가가치(십억원) | 보고서1 2장 산출물(ECOS·KOSIS·산업연관표) | 공개 |
io_linkage_W.csv, io_linkage_62.csv, io_linkage_69.csv |
산업연관 강도 행렬(IO 사전) | 보고서1 2장 산출물(한국은행 산업연관표) | 공개 |
exog_scenarios_v2.csv |
외생 6종 수준값, S1–S5, 2000Q1–2035Q4 | 보고서1 3장 산출물(BOKDPM 평활·전망 + 시나리오 편차) | 공개 |
exog_hist_v2.csv, exog_scen_v2.csv |
위 수준값을 VARX 투입 형태(수준·차분·로그차분)로 바꾼 과거값·시나리오 경로 | 보고서1 3장 산출물 | 공개 |
dsge_exog_paths.csv |
시나리오 외생 충격 경로(oil_g 등) |
보고서1 3장 산출물 | 공개 |
bokdpm_obs_2000_2025.csv |
BOKDPM 관측자료 20종(LOIL_us 포함) |
ECOS·FRED 수집본 | 공개 |
fred_POILWTIUSDM.csv, fred_POILDUBUSDM.csv |
WTI·두바이유 월별 가격(달러/배럴), 1992-01–2026-07 | FRED(IMF 국제상품가격) | 공개 |
varx_coef_frozen.npz |
원 파이프라인으로 추정한 VARX 계수 4종(총량·36·62·69) | 본 연구 모형 산출물 | 공개 |
postreform/exog_hist_v2.csv, postreform/exog_scen_v2.csv |
2026-10 정비판 외생 6종(실질금리 1차 차분)의 과거값·시나리오 경로 | 보고서1 3장 정비판 산출물(5장 패키지 입력과 같은 파일) | 공개 |
출처와 원래 위치는 data/inputs/PROVENANCE.md에 정리하였다. 공표 SHAP 값(data/published/shap_*.json)은 03의 검증 기준이다.
data/published/jeongbi/의 5장 기록 사본(62분류 정비판 분해 shap_62_s5.csv, 드리프트 재설정 drift_anchor_s5.csv)은 02 10절의 대조 기준이다.
산출물. data/interim/input_inventory.csv, data/interim/oil_quarterly_100ln.csv, (ONLINE=1) data/interim/fred_*_online.csv
import os, json
import config as C # BLAS 스레드 설정이 적용되도록 numpy보다 먼저 불러온다
import numpy as np, pandas as pd
import exog_diagnosis as F
pd.set_option("display.float_format", lambda v: f"{v:,.3f}")
pd.set_option("display.width", 200)
C.setup()
Python 3.12.10 · pandas 2.2.3 · numpy 2.4.6 ONLINE=0 (FRED 유가 재수집) FULL=0 (확장 점검) FRED 키 없음(공개 CSV 사용) 외생 과거(정상형) 있음 data\inputs\exog_hist_v2.csv 외생 시나리오(정상형) 있음 data\inputs\exog_scen_v2.csv 외생 수준 있음 data\inputs\exog_scenarios_v2.csv 시나리오 충격 있음 data\inputs\dsge_exog_paths.csv BOKDPM 관측 있음 data\inputs\bokdpm_obs_2000_2025.csv FRED WTI 있음 data\inputs\fred_POILWTIUSDM.csv FRED 두바이 있음 data\inputs\fred_POILDUBUSDM.csv 동결 계수 있음 data\inputs\varx_coef_frozen.npz 패널 총량 있음 data\inputs\levels_agg.csv 패널 36 있음 data\inputs\levels_sa.csv 패널 62 있음 data\inputs\levels_62.csv 패널 69 있음 data\inputs\levels_69.csv
1. 동결 입력과 SHA-256¶
config.INPUT_SHA256에 적힌 해시와 파일 해시가 모두 같아야 다음 단계로 넘어간다.
rows = []
for name, h in C.INPUT_SHA256.items():
p = os.path.join(C.INPUTS, name)
got = F.sha256(p)
shape = ""
if name.endswith(".csv"):
d = pd.read_csv(p, encoding="utf-8-sig")
shape = f"{d.shape[0]}×{d.shape[1]}"
elif name.endswith(".npz"):
z = np.load(p, allow_pickle=False)
shape = " · ".join(f"{k}{z[k].shape}" for k in z.files if k.startswith("B_"))
rows.append({"파일": name, "크기(bytes)": os.path.getsize(p), "형태": shape, "SHA-256(앞 16자리)": got[:16], "일치": got == h})
inv = pd.DataFrame(rows)
inv.to_csv(os.path.join(C.INTERIM, "input_inventory.csv"), index=False, encoding="utf-8-sig")
assert inv["일치"].all(), "동결 입력의 SHA-256이 다르다 — data/inputs/ 파일을 확인한다"
inv
| 파일 | 크기(bytes) | 형태 | SHA-256(앞 16자리) | 일치 | |
|---|---|---|---|---|---|
| 0 | postreform/exog_hist_v2.csv | 5521 | 104×7 | d142ff2992b5ae3e | True |
| 1 | postreform/exog_scen_v2.csv | 11096 | 200×8 | f45f8007d7b88faf | True |
| 2 | postreform/exog_levels_2000_2035.csv | 43925 | 720×8 | 748cc232732adb73 | True |
| 3 | bokdpm_obs_2000_2025.csv | 33329 | 104×21 | 8ae2f6709bf15265 | True |
| 4 | dsge_exog_paths.csv | 7540 | 200×7 | b5fe00a341fe6763 | True |
| 5 | exog_hist_v2.csv | 5481 | 104×7 | 0943ba6160f6faf4 | True |
| 6 | exog_scen_v2.csv | 11034 | 200×8 | f490a1cb17390289 | True |
| 7 | exog_scenarios_v2.csv | 43830 | 720×8 | 79e4a02cd4c3e7c2 | True |
| 8 | fred_POILDUBUSDM.csv | 12117 | 415×2 | cc766e2524a02c7c | True |
| 9 | fred_POILWTIUSDM.csv | 12095 | 415×2 | ac49d9350ae20bf0 | True |
| 10 | io_linkage_62.csv | 67817 | 62×63 | 3f747a97c3804541 | True |
| 11 | io_linkage_69.csv | 79382 | 69×70 | 1f57d01c6867b456 | True |
| 12 | io_linkage_W.csv | 11035 | 36×37 | cf11efc744586d35 | True |
| 13 | levels_62.csv | 105355 | 104×64 | 00469e892aac4742 | True |
| 14 | levels_69.csv | 122145 | 104×71 | c8a5adcedfa4635c | True |
| 15 | levels_agg.csv | 4748 | 184×3 | f04021ff282e5387 | True |
| 16 | levels_sa.csv | 51177 | 184×38 | ccfc8dad3c8cd7b0 | True |
| 17 | varx_coef_frozen.npz | 328559 | B_agg(23, 1) · B_36(163, 36) · B_62(267, 62) ·... | 2442a16d850a1891 | True |
2. 산업 패널 — 총량·36·62·69¶
네 패널 모두 추정 표본 2000Q1–2025Q4(104분기)에 결측이 없어야 한다. 총량 모형은 실질 GDP 한 계열을 수준으로, 산업 모형은 각 산업 실질부가가치를 로그차분으로 쓴다. 62·69분류의 산업 합계는 같다(69분류는 62분류 일부를 분화한 것이다).
SAMPLE = F.SAMPLE
rows, ann = [], {}
for name, spec in C.MODELS.items():
lev = pd.read_csv(spec["levels"], index_col=0)
inds = [c for c in lev.columns if c != spec["gdp"]]
sub = lev[inds].reindex(SAMPLE)
a = F.q_to_annual(sub).sum(axis=1)
ann[name] = a
rows.append({"패널": name, "명세": spec["spec"], "산업 수": len(inds), "전체 기간": f"{lev.index[0]}–{lev.index[-1]}",
"표본 결측": int(sub.isna().sum().sum()), "2025 합계(조원)": a.loc[2025] / 1e3})
panels = pd.DataFrame(rows).set_index("패널")
assert panels["표본 결측"].eq(0).all(), "추정 표본에 결측이 있다"
assert abs(ann["62"] - ann["69"]).max() < 1e-3, "62·69 산업 합계가 다르다"
panels
| 명세 | 산업 수 | 전체 기간 | 표본 결측 | 2025 합계(조원) | |
|---|---|---|---|---|---|
| 패널 | |||||
| 총량 | 수준 | 1 | 1980Q1–2025Q4 | 0 | 2,326.267 |
| 36 | 차분 | 36 | 1980Q1–2025Q4 | 0 | 2,178.489 |
| 62 | 차분 | 62 | 2000Q1–2025Q4 | 0 | 2,178.489 |
| 69 | 차분 | 69 | 2000Q1–2025Q4 | 0 | 2,178.489 |
3. 외생 6종 — 수준값과 VARX 투입 형태¶
외생은 BOKDPM 내생·관측 변수를 조합한 관측 가능한 계열이다. VARX에는 적분차수에 맞춰 수준·차분·로그차분 중 하나로 들어간다.
| 외생 | 구성 | 투입 형태 |
|---|---|---|
| 유가 | LOIL_us(100·ln 달러/배럴) |
1차 차분 |
| 세계수요 | Y_us(미국 산출갭) |
수준 |
| 실질금리 | CB_kor − 소비자물가 전년동기비 |
수준 |
| 실질환율 | LS_kor + 미국 CPI − 한국 CPI(100·ln) |
1차 차분 |
| 고용_취업자 | 경제활동인구 × (1 − 실업률/100), 천 명 | 100·로그차분 |
| 우리경기 | Y_kor(한국 산출갭) |
수준 |
수준값 파일에 원 변환(build_varx_exog.py와 같은 규칙)을 다시 적용해 과거값·시나리오 경로 두 파일과 같아지는지 확인한다.
eh = F.read_exog_hist(C.EXOG_HIST)
es = F.read_exog_scen(C.EXOG_SCEN)
lv = F.read_exog_levels(C.EXOG_LEVELS)
st = F.to_stationary(lv)
d_hist = (st[(st["시나리오"] == "S1") & (st["분기"] < "2026Q1")].set_index("분기")[F.EXOG] - eh).abs().max().max()
d_scen = max((g.set_index("분기")[F.EXOG] - es[s]).abs().max().max() for s, g in st[st["분기"] >= "2026Q1"].groupby("시나리오"))
print(f"과거값 {eh.shape} · 시나리오 {len(es)}개 × {es['S1'].shape} · 수준값 {lv.shape}")
print(f"재변환 최대 차이: 과거 {d_hist:.1e} · 시나리오 {d_scen:.1e}")
assert d_hist < 1e-4 and d_scen < 1e-4, "수준값과 정상형 파일의 변환 관계가 맞지 않는다"
pd.DataFrame({"2025Q4(과거 끝)": eh.loc["2025Q4"], "2035Q4(S1)": es["S1"].loc["2035Q4"], "변환": pd.Series(F.TF)})
과거값 (104, 6) · 시나리오 5개 × (40, 6) · 수준값 (720, 8) 재변환 최대 차이: 과거 0.0e+00 · 시나리오 0.0e+00
| 2025Q4(과거 끝) | 2035Q4(S1) | 변환 | |
|---|---|---|---|
| 유가 | -9.763 | 2.469 | diff |
| 세계수요 | 1.022 | -0.001 | level |
| 실질금리 | 0.939 | 2.781 | level |
| 실질환율 | 3.610 | -0.109 | diff |
| 고용_취업자 | 0.024 | 0.216 | dlog |
| 우리경기 | 0.102 | -0.074 | level |
4. 유가 원천 — LOIL_us와 FRED 두 계열¶
BOKDPM 자료 수집 문서(used_data_sources.json)는 미국 유가를 FRED POILWTIUSDM(WTI)로 적고, 관측자료 파일의 LOIL_us는
WTI 분기 평균의 100·ln과 정확히 같다(아래 첫 줄). 그런데 VARX 외생 유가의 과거 구간(외생 수준값의 2000Q1–2025Q4)은 BOKDPM 추정 결과의
평활값이며, 이 값은 WTI가 아니라 두바이유(POILDUBUSDM) 분기 평균의 100·ln과 일치한다(아래 둘째·셋째 줄).
BOKDPM 추정 당시 Dynare가 읽은 자료 파일에는 두바이유가 들어 있었고, 이후 자료 파일을 WTI로 다시 만들었으나 모형을 다시 추정하지 않은 결과로 판단된다.
이 차이가 전망에 주는 영향은 02_분석.ipynb 6절에서 계산한다.
wti_m, dub_m = F.read_fred_csv(C.WTI_CSV), F.read_fred_csv(C.DUB_CSV)
obs = pd.read_csv(C.BOKDPM_OBS)
chk = F.loil_observation_check(obs, wti_m)
s1 = lv[lv["시나리오"] == "S1"].set_index("분기")["유가"].reindex(F.SAMPLE)
wq = F.quarterly_100ln(wti_m, start="2000-01-01"); wq.index = wq.index.astype(str)
dq = F.quarterly_100ln(dub_m, start="2000-01-01"); dq.index = dq.index.astype(str)
src = pd.DataFrame([
{"비교": "BOKDPM 관측 LOIL_us − 100·ln(WTI)", "최대 |차이|": chk["차이"].abs().max()},
{"비교": "VARX 유가(평활, 과거) − 100·ln(두바이)", "최대 |차이|": (s1 - dq.reindex(F.SAMPLE)).abs().max()},
{"비교": "VARX 유가(평활, 과거) − 100·ln(WTI)", "최대 |차이|": (s1 - wq.reindex(F.SAMPLE)).abs().max()},
]).set_index("비교")
assert src.iloc[0, 0] < 1e-6 and src.iloc[1, 0] < 1e-3, "유가 원천 대조 결과가 예상과 다르다"
oilq = pd.DataFrame({"WTI_100ln": wq, "두바이_100ln": dq, "VARX유가_100ln": s1}).loc["2000Q1":]
oilq.to_csv(os.path.join(C.INTERIM, "oil_quarterly_100ln.csv"), encoding="utf-8-sig")
display(src.style.format("{:.6f}"))
oilq.loc[["2000Q1", "2008Q2", "2012Q1", "2020Q2", "2025Q4"]]
| 최대 |차이| | |
|---|---|
| 비교 | |
| BOKDPM 관측 LOIL_us − 100·ln(WTI) | 0.000000 |
| VARX 유가(평활, 과거) − 100·ln(두바이) | 0.000050 |
| VARX 유가(평활, 과거) − 100·ln(WTI) | 30.309548 |
| WTI_100ln | 두바이_100ln | VARX유가_100ln | |
|---|---|---|---|
| 2000Q1 | 336.129 | 319.426 | 319.426 |
| 2008Q2 | 482.011 | 476.330 | 476.330 |
| 2012Q1 | 463.339 | 475.324 | 475.324 |
| 2020Q2 | 333.305 | 349.514 | 349.514 |
| 2025Q4 | 408.842 | 415.253 | 415.253 |
5. FRED 재수집과 대조(ONLINE=1)¶
ONLINE=1이면 FRED에서 두 계열을 다시 받아 동결본과 겹치는 달의 값을 대조하고, 원 시산 표본(1999-10–2026-07)으로 로그차분 상관을 다시 계산한다.
FRED 키는 환경변수 API_KEY_FRED에서만 읽으며, 키가 없으면 공개 CSV 주소를 쓴다. 기본 실행(ONLINE=0)은 이 절을 건너뛴다.
FRED가 과거 값을 개정하면 차이가 나타날 수 있으며, 이때도 검증은 동결본 기준으로 진행한다.
if C.ONLINE:
rows, fetched = [], {}
for sid, frozen in [(C.WTI_SERIES, wti_m), (C.DUB_SERIES, dub_m)]:
s = F.fetch_fred(sid, C.API_KEY_FRED or None)
fetched[sid] = s
s.rename(sid).to_frame().to_csv(os.path.join(C.INTERIM, f"fred_{sid}_online.csv"), encoding="utf-8-sig")
ov = s.index.intersection(frozen.index)
rows.append({"계열": sid, "받은 기간": f"{s.index.min():%Y-%m}–{s.index.max():%Y-%m}", "겹친 달": len(ov),
"새 달": int((s.index > frozen.index.max()).sum()), "겹친 달 최대 |차이|": float((s[ov] - frozen[ov]).abs().max())})
online = pd.DataFrame(rows).set_index("계열")
r_on = F.oil_benchmark(fetched[C.WTI_SERIES], fetched[C.DUB_SERIES], end=C.FROZEN_FRED_END)["corr"]
r_fz = F.oil_benchmark(wti_m, dub_m)["corr"]
print(f"로그차분 상관(1999-10–2026-07): 재수집 {r_on:.4f} · 동결본 {r_fz:.4f}")
print("겹친 달 값이 모두 같다." if online["겹친 달 최대 |차이|"].max() < 1e-8 else "주의: FRED가 과거 값을 개정했다(검증은 동결본 기준).")
display(online)
else:
print("ONLINE=0: FRED 재수집을 건너뛰고 동결본만 사용한다.")
ONLINE=0: FRED 재수집을 건너뛰고 동결본만 사용한다.
6. 산출물¶
다음 노트북(02_분석.ipynb)은 data/inputs/의 동결 입력을 직접 읽는다. 이 노트북의 산출물은 점검 기록이다.
for f in sorted(os.listdir(C.INTERIM)):
p = os.path.join(C.INTERIM, f)
print(f"{f:<34} {os.path.getsize(p):>9,d} bytes")
input_inventory.csv 1,138 bytes oil_quarterly_100ln.csv 5,771 bytes