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

In [1]:
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에 적힌 해시와 파일 해시가 모두 같아야 다음 단계로 넘어간다.

In [2]:
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
Out[2]:
파일 크기(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분류 일부를 분화한 것이다).

In [3]:
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
Out[3]:
명세 산업 수 전체 기간 표본 결측 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와 같은 규칙)을 다시 적용해 과거값·시나리오 경로 두 파일과 같아지는지 확인한다.

In [4]:
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
Out[4]:
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절에서 계산한다.

In [5]:
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
Out[5]:
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가 과거 값을 개정하면 차이가 나타날 수 있으며, 이때도 검증은 동결본 기준으로 진행한다.

In [6]:
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/의 동결 입력을 직접 읽는다. 이 노트북의 산출물은 점검 기록이다.

In [7]:
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