03 결과 도출 — 3장 표·그림, 이전 판 대비 변화, 검증¶

02_분석.ipynb의 산출물로 3장의 표와 그림을 만들고, 이전 판(2026-10-08 패키지가 재현한 공표 산출물, data/reference_prev/)과 비교한다. 마지막 셀은 계산 결과를 정비판 기록 산출물(data/reference/, 2026-10 정비 실행에서 동결)과 config.PUBLISHED의 핵심 수치에 대조한다. 허용오차는 기록 파일의 반올림 자릿수(소수 셋째·넷째·여섯째 자리)에 맞춘다.

In [1]:
import os, time
import numpy as np, pandas as pd
import config as C
import bokdpm_macro as B
pd.set_option("display.float_format", lambda v: f"{v:,.4f}")
pd.set_option("display.width", 160)
env = C.setup()
import matplotlib.pyplot as plt
import koreanize_matplotlib  # noqa: F401  (한글 글꼴)
post = pd.read_csv(os.path.join(C.OUTPUT, "posterior_summary.csv"), encoding="utf-8-sig")
ann = pd.read_csv(os.path.join(C.OUTPUT, "baseline_annual.csv"), index_col=0, encoding="utf-8-sig")
ann_prev = pd.read_csv(C.REF_PREV["baseline_annual"], index_col=0, encoding="utf-8-sig")
fc = B.read_exact(os.path.join(C.OUTPUT, "baseline_forecast_70.csv"), index_col=0)
fc_prev = B.read_exact(C.REF_PREV["forecast_70"], index_col=0)
sm = B.read_exact(os.path.join(C.INTERIM, "bokdpm_smoothed_2000_2025.csv") if C.FULL else C.SMOOTHED, index_col=0)
mac = pd.read_csv(os.path.join(C.OUTPUT, "dsge_macro_paths.csv"), encoding="utf-8-sig")
ex = pd.read_csv(os.path.join(C.OUTPUT, "dsge_exog_paths.csv"), encoding="utf-8-sig")
lev = pd.read_csv(os.path.join(C.OUTPUT, "exog_levels_2000_2035.csv"), encoding="utf-8-sig")
lev_prev = B.read_levels_xlsx(C.REF_PREV["exog_levels_xlsx"])
hist = pd.read_csv(os.path.join(C.OUTPUT, "exog_hist_v2.csv"), encoding="utf-8-sig")
scen = pd.read_csv(os.path.join(C.OUTPUT, "exog_scen_v2.csv"), encoding="utf-8-sig")
reach = pd.read_csv(os.path.join(C.OUTPUT, "lever_reach.csv"), index_col=0, encoding="utf-8-sig")
pop = pd.read_csv(C.POP, encoding="utf-8-sig")
lab = pd.read_csv(C.LABOR, index_col=0, encoding="utf-8-sig")
CLR = {"S1": "#333333", "S2": "#c0392b", "S3": "#1f4e79", "S4": "#2e7d32", "S5": "#b8892f"}
Python 3.12.10 · pandas 2.2.3 · numpy 2.4.6 · scipy 1.18.0 · matplotlib 3.11.0
  실행 방식   ONLINE=0 (ECOS·FRED 재수집) · FULL=0 (Dynare 재추정)
  ECOS 키     없음 · FRED 키 없음  (ONLINE=0이면 쓰지 않는다)
  Octave      있음 · Dynare 있음  (FULL=0이면 쓰지 않는다)
  BOKDPM_data_2025.m               있음      23,440 bytes
  bokdpm_forecast_2026_2035.csv    있음      56,135 bytes
  bokdpm_posterior_draws.csv.gz    있음   5,426,716 bytes
  bokdpm_posterior_mode.csv        있음       2,909 bytes
  bokdpm_smoothed_2000_2025.csv    있음     142,828 bytes
  cpi_kor_1999_2000.csv            있음         146 bytes
  ecos_labor_force.csv             있음       2,620 bytes
  keis_labor_supply_2024_2034.csv  있음         353 bytes
  kosis_생산연령인구.csv                 있음         735 bytes

표 0. 정상상태 정비 — 이전 값·새 값·근거¶

In [2]:
t0 = pd.read_csv(os.path.join(C.OUTPUT, "steady_state_revision.csv"), index_col=0, encoding="utf-8-sig")
t0.to_csv(os.path.join(C.OUTPUT, "table0_steady_state.csv"), encoding="utf-8-sig")
t0
Out[2]:
의미 이전 값 새 값 근거
파라미터
growth_kor_ss 한국 잠재성장률(%) 4.2925 1.8000 한국은행(2024.12) 「우리 경제의 잠재성장률과 향후 전망」: 2024–2026...
pietar_kor_ss 한국 물가목표(CPI, %) 3.0000 2.0000 한국은행 물가안정목표 2%(2016년 이후)
def_kor_ss GDP 디플레이터 상승률(%) 2.1943 2.0000 물가목표와 같게(최근 10년 평균 2.08%)
rr_kor_bar_ss 한국 중립 실질 정책금리(%) 1.5000 0.5500 한국은행(2024.11) BOK 경제연구 「한국의 중립금리 추정」: 명목 1.8–3...
tprem_kor_ss CD(91일) − 콜금리(%p) 0.3900 0.1800 최근 10년 평균
tprem_tb_kor_ss 국고채(3년) − 콜금리(%p) 0.7645 0.3000 최근 10년 평균
tprem_cb_kor_ss 회사채(3년, AA−) − 국고채(3년)(%p) 0.7419 0.6700 최근 10년 평균
house_kor_ss 주택가격 상승률(%) 4.2000 2.3300 최근 10년 평균
ge_kor_ss 통합재정 총지출 증가율(%) 7.5885 6.5700 최근 10년 평균
growth_us_ss 미국 잠재성장률(%) 2.3000 1.8000 미 의회예산처(CBO, 2025.1) 잠재GDP 증가율 2027–2035년 1.8%...
pietar_us_ss 미국 물가목표(CPI, %) 2.4982 2.3000 CBO 소비자물가(CPI-U) 장기 전망 2.3%(PCE 목표 2%에 CPI 괴리를...
rr_us_bar_ss 미국 중립 실질 정책금리(%) 2.0000 0.9000 FOMC 경제전망(2026.9) 장기 연방기금금리 중간값 3.2% − 미국 CPI ...
oil_g_us_ss 유가 상승률(달러 기준, %) 9.8763 2.3000 장기 실질유가 불변(= 미국 CPI 목표 2.3%)

표 1. 재추정 행태계수 20개 — 사전분포와 사후분포¶

In [3]:
t1 = post[post["구분"] == "parameters"].set_index("파라미터").drop(columns="구분")
t1.insert(0, "의미", [B.PARAM_LABEL.get(n, "") for n in t1.index])
t1["이전 판 사후평균"] = pd.read_csv(C.REF_PREV["estimation_results"]).set_index("파라미터")["사후평균"].reindex(t1.index)
t1.to_csv(os.path.join(C.OUTPUT, "table1_posterior_params.csv"), encoding="utf-8-sig")
t1.style.format({"사전평균": "{:.2f}", "사전표준편차": "{:.2f}", "사후평균": "{:.3f}", "표준편차": "{:.3f}",
                 "HPD_low": "{:.3f}", "HPD_high": "{:.3f}", "이전 판 사후평균": "{:.3f}"})
Out[3]:
  의미 사전분포 사전평균 사전표준편차 사후평균 표준편차 HPD_low HPD_high 이전 판 사후평균
파라미터                  
gamma_kor1 한국 테일러 준칙 — 금리 평활 beta 0.50 0.15 0.841 0.021 0.808 0.876 0.883
gamma_kor3 한국 테일러 준칙 — 산출갭 반응 gamma 0.50 0.20 0.466 0.118 0.269 0.654 0.574
beta_kor1 한국 IS — 산출갭 지속성 beta 0.50 0.15 0.211 0.059 0.113 0.306 0.203
beta_kor2 한국 IS — 산출갭 기대(선행항) beta 0.10 0.07 0.021 0.015 0.001 0.043 0.019
beta_kor3 한국 IS — 실질 회사채금리갭 반응 gamma 0.10 0.05 0.081 0.041 0.018 0.142 0.086
lambda_kor1 한국 필립스 — 기대물가 가중 beta 0.55 0.15 0.306 0.114 0.128 0.493 0.321
lambda_kor2 한국 필립스 — 산출갭 기울기 gamma 0.15 0.08 0.083 0.041 0.021 0.143 0.092
lambda_kor3 한국 필립스 — 물가목표 가중 beta 0.65 0.15 0.766 0.028 0.725 0.813 0.753
alpha_kor1 한국 오쿤 — 실업갭 지속성 beta 0.80 0.10 0.579 0.097 0.421 0.738 0.611
alpha_kor2 한국 오쿤 — 산출갭 계수 gamma 0.30 0.12 0.070 0.015 0.046 0.094 0.072
gamma_us1 미국 테일러 준칙 — 금리 평활 beta 0.50 0.15 0.887 0.017 0.860 0.917 0.916
gamma_us2 미국 테일러 준칙 — 기대물가 반응 gamma 1.50 0.25 1.721 0.273 1.263 2.153 1.677
gamma_us3 미국 테일러 준칙 — 산출갭 반응 gamma 0.20 0.10 0.462 0.154 0.208 0.707 0.349
beta_us1 미국 IS — 산출갭 지속성 beta 0.70 0.12 0.452 0.058 0.359 0.547 0.446
beta_us2 미국 IS — 산출갭 기대(선행항) beta 0.15 0.08 0.039 0.021 0.006 0.070 0.041
lambda_us1 미국 필립스 — 기대물가 가중 beta 0.45 0.15 0.378 0.098 0.219 0.543 0.340
lambda_us2 미국 필립스 — 산출갭 기울기 gamma 0.05 0.03 0.090 0.024 0.049 0.128 0.077
lambda_us3 미국 필립스 — 물가목표 가중 beta 0.50 0.15 0.431 0.037 0.368 0.490 0.439
rho_oil1 유가 상승률 — 정상상태 회귀 가중 beta 0.90 0.05 0.880 0.049 0.798 0.958 0.881
gamma_kor2 한국 테일러 준칙 — 기대물가 반응 gamma 1.50 0.10 1.539 0.101 1.368 1.702 1.595

표 2. 구조충격 표준편차 34개 — 사전평균과 사후분포¶

In [4]:
t2 = post[post["구분"] == "shocks_std"].set_index("파라미터")[["사전평균", "사후평균", "표준편차", "HPD_low", "HPD_high"]]
t2.to_csv(os.path.join(C.OUTPUT, "table2_posterior_shocks.csv"), encoding="utf-8-sig")
t2.style.format("{:.3f}")
Out[4]:
  사전평균 사후평균 표준편차 HPD_low HPD_high
파라미터          
RES_RR_kor_BAR 0.200 1.142 0.157 0.892 1.400
RES_RS_kor 0.300 0.117 0.022 0.083 0.151
RES_UNR_kor_GAP 0.200 0.138 0.020 0.104 0.172
RES_UNR_kor_BAR 0.100 0.083 0.031 0.036 0.130
RES_UNR_G_kor 0.100 0.056 0.014 0.034 0.076
RES_Y_kor 0.500 1.835 0.143 1.582 2.050
RES_LGDP_kor_BAR 0.400 0.059 0.005 0.051 0.068
RES_G_kor 0.200 0.245 0.021 0.210 0.278
RES_BLT_kor 10.000 2.447 0.192 2.150 2.764
RES_BLT_kor_BAR 5.000 3.716 1.314 1.730 5.563
RES_PIE_kor 0.300 0.447 0.031 0.394 0.495
RES_PIE_E_kor4 0.300 0.210 0.081 0.101 0.334
RES_CA_kor 2.000 4.252 0.399 3.592 4.862
RES_LS_kor_BAR 1.000 1.691 0.878 0.353 2.973
RES_S_G_kor 0.500 1.212 1.045 0.145 2.940
RES_S_kor 2.000 3.114 0.679 1.972 4.203
RES_DEF_kor 0.300 0.706 0.052 0.618 0.789
RES_RR_us_BAR 0.200 0.221 0.139 0.065 0.413
RES_RS_us 0.300 0.396 0.028 0.349 0.442
RES_Y_us 0.500 1.602 0.121 1.410 1.806
RES_LGDP_us_BAR 0.400 0.056 0.005 0.048 0.063
RES_G_us 0.200 0.167 0.016 0.142 0.193
RES_BLT_us 10.000 2.529 0.271 2.071 2.974
RES_BLT_us_BAR 5.000 5.010 1.189 3.051 6.898
RES_PIE_us 0.300 0.336 0.023 0.298 0.373
RES_CD_kor 0.300 0.508 0.037 0.447 0.568
RES_RCD_kor_BAR 0.200 0.204 0.118 0.065 0.390
RES_HOUSE_kor 1.000 1.279 0.087 1.144 1.432
RES_OIL_us 5.000 15.193 1.039 13.526 16.889
RES_RCB_kor_BAR 0.200 0.166 0.073 0.061 0.268
RES_GE_kor 10.000 12.115 0.776 10.817 13.339
RES_TB_kor 0.300 0.347 0.025 0.308 0.386
RES_RTB_kor_BAR 0.200 0.428 0.527 0.053 1.500
RES_RES_CB_kor 0.300 0.574 0.041 0.508 0.639

표 3. 기준선 전망 — 연간 요약(2024–2035), 정비판과 이전 판¶

2024–2025년은 평활값(관측변수는 실측치와 같다), 2026년 이후는 사후최빈값 모수의 무충격 전망이다. 정비판은 잠재성장률 1.8%, 물가목표 2%, 실질유가 불변으로 수렴하고, 이전 판은 2000년대 정상상태(4.29%, 3.0%, 연 9.88%)로 수렴한다.

In [5]:
ann.to_csv(os.path.join(C.OUTPUT, "table3_baseline_annual.csv"), encoding="utf-8-sig")
cols = [c for c in ann.columns if c != "구분"]
t3 = pd.concat({"정비판": ann[cols], "이전 판": ann_prev[cols]}, axis=1)
t3.to_csv(os.path.join(C.OUTPUT, "table3b_baseline_vs_prev.csv"), encoding="utf-8-sig")
t3.style.format("{:.2f}")
Out[5]:
  정비판 이전 판
  실질GDP 성장률(%) CPI 상승률(%) 콜금리(%) 실업률(%) 산출갭(%) 두바이유(달러/배럴) 미국 산출갭(%) 실질GDP 성장률(%) CPI 상승률(%) 콜금리(%) 실업률(%) 산출갭(%) 두바이유(달러/배럴) 미국 산출갭(%)
연도                            
2024 1.56 2.32 3.40 2.77 -0.01 80.99 1.32 1.56 2.32 3.40 2.77 -0.01 80.99 1.32
2025 1.83 2.12 2.60 2.78 -0.31 69.39 1.00 1.83 2.12 2.60 2.78 -0.31 69.39 1.00
2026 2.19 1.72 2.42 2.86 -0.19 63.86 1.18 2.39 2.15 2.78 2.89 -0.37 66.78 0.99
2027 1.73 1.79 2.24 2.91 -0.45 65.62 0.56 3.15 2.71 3.24 2.91 -0.33 74.00 0.69
2028 2.24 1.87 2.27 2.87 -0.15 67.24 -0.03 3.92 2.94 3.71 2.84 0.03 81.85 0.10
2029 1.90 1.92 2.30 2.86 -0.14 68.78 -0.08 3.80 3.01 4.01 2.83 -0.01 90.36 0.00
2030 1.89 1.92 2.33 2.85 -0.11 70.36 -0.05 4.00 3.00 4.17 2.84 -0.03 99.74 -0.00
2031 1.86 1.93 2.35 2.85 -0.10 71.98 -0.03 4.13 2.99 4.25 2.84 -0.05 110.10 -0.00
2032 1.84 1.93 2.36 2.85 -0.09 73.65 -0.01 4.22 2.98 4.30 2.84 -0.06 121.52 -0.00
2033 1.83 1.93 2.37 2.85 -0.09 75.36 -0.01 4.28 2.97 4.32 2.84 -0.07 134.14 -0.00
2034 1.83 1.93 2.38 2.85 -0.09 77.11 -0.00 4.31 2.97 4.33 2.84 -0.07 148.06 -0.00
2035 1.82 1.93 2.38 2.85 -0.09 78.90 -0.00 4.34 2.97 4.33 2.85 -0.07 163.43 -0.00

그림 1. 기준선 전망 — 주요 변수(2015–2035, 분기), 정비판과 이전 판¶

In [6]:
q, qp = pd.concat([sm, fc]), pd.concat([sm, fc_prev])
t = pd.PeriodIndex(q.index, freq="Q").to_timestamp(how="end")
spec = [("실질GDP 성장률(전년동기, %)", lambda d: d["LGDP_kor"].diff(4)), ("CPI 상승률(전년동기, %)", lambda d: d["LCPI_kor"].diff(4)),
        ("콜금리(%)", lambda d: d["RS_kor"]), ("실질금리(회사채 − CPI, %)", lambda d: d["CB_kor"] - d["LCPI_kor"].diff(4)),
        ("산출갭(%)", lambda d: d["Y_kor"]), ("두바이유(달러/배럴)", lambda d: np.exp(d["LOIL_us"] / 100))]
fig, axes = plt.subplots(2, 3, figsize=(13, 6.4))
sel = t >= pd.Timestamp("2015-01-01")
for ax, (title, f) in zip(axes.ravel(), spec):
    ax.plot(t[sel], f(qp).values[sel], color="#b0b0b0", lw=1.4, ls="--", label="이전 판")
    ax.plot(t[sel], f(q).values[sel], color="#1f4e79", lw=1.7, label="정비판")
    ax.axvline(pd.Timestamp("2025-12-31"), color="grey", ls=":", lw=1)
    ax.set_title(title, loc="left", fontsize=11); ax.grid(alpha=.3)
axes[0, 0].legend(frameon=False, fontsize=9)
fig.suptitle("기준선 전망(점선 오른쪽이 2026Q1 이후 전망)", x=0.01, ha="left", fontweight="bold")
fig.tight_layout(); fig.savefig(os.path.join(C.OUTPUT, "fig1_baseline.png"), dpi=150)
C:\Users\meals\AppData\Local\Temp\ipykernel_18692\2431658101.py:15: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) NanumGothic.
  fig.tight_layout(); fig.savefig(os.path.join(C.OUTPUT, "fig1_baseline.png"), dpi=150)
C:\Users\meals\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\events.py:96: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) NanumGothic.
  func(*args, **kwargs)
C:\Users\meals\AppData\Local\Programs\Python\Python312\Lib\site-packages\IPython\core\pylabtools.py:170: UserWarning: Glyph 8722 (\N{MINUS SIGN}) missing from font(s) NanumGothic.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image

표 4. 시나리오 설계 레버 — 인구 가정과 감쇠 충격(최고치, 감쇠 분기)¶

In [7]:
t4 = pd.read_csv(os.path.join(C.OUTPUT, "scenario_levers.csv"), index_col=0, encoding="utf-8-sig")
t4.to_csv(os.path.join(C.OUTPUT, "table4_scenario_levers.csv"), encoding="utf-8-sig")
t4
Out[7]:
인구(생산연령인구 추계) 미국수요 yus 유가 oil 교역비용 tcost 내수 dshk
시나리오
S1 기준 중위 — — — —
S2 공급제약+충격 저위 -1.2 (12분기) -8.0 (8분기) +1.5 (12분기) -1.0 (12분기)
S3 제약완화 고위 +0.6 (12분기) -3.0 (8분기) -1.0 (12분기) +0.5 (12분기)
S4 기술도약 중위 +0.8 (20분기) — — +0.6 (28분기)
S5 지정학 중위 -0.6 (10분기) +15.0 (8분기) +2.0 (10분기) -0.8 (10분기)

그림 2. 거시 블록 경로 — 산출갭·실질금리·실질환율갭·실업갭(2026–2035)¶

In [8]:
tt = pd.PeriodIndex(B.quarters("2026Q1", B.H), freq="Q").to_timestamp(how="end")
fig, axes = plt.subplots(2, 2, figsize=(12, 6.6))
for ax, (c, title) in zip(axes.ravel(), [("산출갭Y", "산출갭(%)"), ("실질금리RR", "실질금리(%)"),
                                          ("실질환율갭S", "실질환율갭(%)"), ("실업갭UNR", "실업갭(%p)")]):
    for s in B.SCENARIOS:
        ax.plot(tt, mac[mac["시나리오"] == s][c].values, color=CLR[s], lw=1.6, label=f"{s} {B.SCN_KO[s]}")
    ax.axhline(0, color="grey", lw=.6); ax.set_title(title, loc="left"); ax.grid(alpha=.3)
axes[0, 0].legend(frameon=False, fontsize=9)
fig.tight_layout(); fig.savefig(os.path.join(C.OUTPUT, "fig2_macro_paths.png"), dpi=150)
No description has been provided for this image

표 5. 산업 외생 6종 — 2035년 4분기 수준(정비판, 괄호 안 이전 판 S1)¶

유가는 두바이유 가격(달러/배럴)으로, 실질환율은 로그 수준(100·ln)으로 표시한다. 정비판에서 취업자 수는 인구 레버에 따라 S2·S3가 S1과 영구적으로 다르고, 유가는 유가 증가율 편차를 누적하므로 수준 차이가 남는다. 나머지 네 외생은 충격이 감쇠하여 2035년에 시나리오 간 차이가 거의 사라진다.

In [9]:
def lvl35(d):
    x = d[d["분기"] == "2035Q4"].set_index("시나리오")[B.EXOG_COLS].copy()
    x["유가"] = np.exp(x["유가"] / 100)
    return x.rename(columns={"유가": "두바이유(달러/배럴)", "세계수요": "세계수요(%)", "실질금리": "실질금리(%)",
                             "실질환율": "실질환율(100·ln)", "고용_취업자": "취업자 수(천 명)", "우리경기": "국내 경기(%)"})
t5 = lvl35(lev)
t5.loc["이전 판 S1"] = lvl35(lev_prev).loc["S1"]
t5.index = [f"{s} {B.SCN_KO[s]}" if s in B.SCN_KO else s for s in t5.index]
t5.to_csv(os.path.join(C.OUTPUT, "table5_exog_2035.csv"), encoding="utf-8-sig")
t5.style.format("{:,.2f}")
Out[9]:
  두바이유(달러/배럴) 세계수요(%) 실질금리(%) 실질환율(100·ln) 취업자 수(천 명) 국내 경기(%)
S1 기준 79.59 -0.00 1.31 833.46 28,762.75 -0.09
S2 공급제약+충격 66.48 -0.00 1.31 833.46 28,267.54 -0.09
S3 제약완화 74.39 -0.00 1.31 833.46 29,249.70 -0.09
S4 기술도약 79.59 -0.00 1.31 833.46 28,767.78 -0.09
S5 지정학 111.53 -0.00 1.31 833.46 28,762.45 -0.09
이전 판 S1 169.53 -0.00 2.78 824.93 31,464.06 -0.07

그림 3. 산업 외생 6종 수준 경로(2000–2035, S1–S5와 이전 판 S1)¶

In [10]:
ta = pd.PeriodIndex(B.quarters("2000Q1", 144), freq="Q").to_timestamp(how="end")
fig, axes = plt.subplots(2, 3, figsize=(14, 7))
for ax, c in zip(axes.ravel(), B.EXOG_COLS):
    yp = lev_prev[lev_prev["시나리오"] == "S1"][c].values
    ax.plot(ta[104:], yp[104:], color="#b0b0b0", lw=1.3, ls="--", label="이전 판 S1")
    for s in B.SCENARIOS:
        y = lev[lev["시나리오"] == s][c].values
        ax.plot(ta[104:], y[104:], color=CLR[s], lw=1.8 if s == "S1" else 1.2, label=f"{s} {B.SCN_KO[s]}")
    ax.plot(ta[:105], lev[lev["시나리오"] == "S1"][c].values[:105], color="#777777", lw=1.1)
    ax.axvline(pd.Timestamp("2025-12-31"), color="grey", ls=":", lw=1)
    ax.set_title(f"{B.EXOG_LABEL[c]} — {B.EXOG_DEF[c][1]} 투입", loc="left", fontsize=11); ax.grid(alpha=.3)
axes[0, 0].legend(frameon=False, fontsize=8)
fig.suptitle("산업 외생 6종(수준): 회색 실선 = 과거 공통, 색 = 시나리오(2026Q1 이후), 회색 점선 = 이전 판 S1", x=0.01, ha="left", fontweight="bold")
fig.tight_layout(); fig.savefig(os.path.join(C.OUTPUT, "fig3_exog_levels.png"), dpi=150)
No description has been provided for this image

설계 일관성 점검 — 레버가 외생 6종에 전달되는가¶

이전 판에서 확인한 세 가지 차이(인구 레버 미전달, 교역비용 레버 미전달, S3 세계수요 누락)가 정비판에서 해소되었는지 확인한다.

  1. 인구 레버. 외생 취업자 수의 2035Q4 시나리오 차이가 인구 레버의 크기와 같은 방향·크기여야 한다(첫 표).
  2. S3의 미국수요 레버. 세계수요 외생에 더한 편차가 모든 시나리오에서 설계 레버 yus와 같아야 한다(둘째 표).
  3. 교역비용 레버. 레버를 껐을 때 외생 6종이 달라져야 한다(셋째 표의 tcost 행).
In [11]:
pt = B.population_transmission(pop, ex, lev, lev_prev)
display(pt.style.format("{:+.3f}", subset=pd.IndexSlice[:, [c for c in pt.columns if c != "인구 가정"]]))
dc = B.design_consistency()
assert dc["일치"].all()
display(dc)
reach.rename(columns=B.EXOG_LABEL).style.set_caption("레버를 끌 때 외생 6종의 최대 변화폭(2026–2035)").format("{:.3f}")
  인구 가정 생산연령인구 2035, 중위 대비(%) 인구 레버 2035Q4(100·ln) 거시 블록 10년 누적 고용, S1 대비(%p) 외생 취업자 2035Q4, S1 대비(%) 이전 판 외생 취업자 2035Q4, S1 대비(%)
시나리오            
S1 기준 중위 +0.000 +0.000 +0.000 +0.000 +0.000
S2 공급제약+충격 저위 -2.067 -1.735 -1.756 -1.722 -0.002
S3 제약완화 고위 +2.052 +1.678 +1.661 +1.693 +0.001
S4 기술도약 중위 +0.000 +0.000 -0.002 +0.017 +0.017
S5 지정학 중위 +0.000 +0.000 -0.018 -0.001 -0.001
레버 yus 2026Q1 세계수요 편차 2026Q1 이전 판 편차 2026Q1 40분기 최대 차이 일치
시나리오
S1 기준 0.0000 0.0000 0.0000 0.0000 True
S2 공급제약+충격 -1.2000 -1.2000 -1.2000 0.0000 True
S3 제약완화 0.6000 0.6000 0.0000 0.0000 True
S4 기술도약 0.8000 0.8000 0.8000 0.0000 True
S5 지정학 -0.6000 -0.6000 -0.6000 0.0000 True
Out[11]:
레버를 끌 때 외생 6종의 최대 변화폭(2026–2035)
  유가 세계수요 실질금리 실질환율 취업자 수 국내 경기
끈 레버            
인구 0.000 0.000 0.000 0.000 494.622 0.000
yus 0.000 1.200 0.959 2.503 138.569 0.539
oil 33.750 0.000 0.366 0.862 50.137 0.216
tcost 0.000 0.000 1.145 2.902 162.872 0.663
dshk 0.000 0.000 2.664 6.954 384.914 1.496

이전 판 대비 변화 — 외생 6종(투입형)¶

산업모형에 들어가는 투입형 외생을 이전 판과 비교한다. 과거 구간에서는 실질금리 한 열만 달라지고(수준 → 1차 차분), 나머지 다섯 외생은 같아야 한다(관측자료가 같다. 평활값의 수치 오차 때문에 소수 넷째 자리 한 단위까지는 같은 값으로 본다). 미래 구간은 기준선·레버·노동공급이 모두 바뀌었다.

In [12]:
hp = pd.read_csv(C.REF_PREV["exog_hist_v2"], encoding="utf-8-sig"); spv = pd.read_csv(C.REF_PREV["exog_scen_v2"], encoding="utf-8-sig")
ch = B.compare_versions(hist, hp, ["분기"], B.EXOG_COLS)
# 소수 넷째 자리 반올림 값이고 평활값이 수치 오차(10⁻⁹) 수준에서 다를 수 있으므로 마지막 자리 한 단위(10⁻⁴)까지 같은 값으로 본다
assert (ch.drop(index="실질금리")["최대 절대차"] <= 1.0001e-4).all(), "실질금리 밖의 과거 외생이 이전 판과 다르다"
display(ch.rename(index=B.EXOG_LABEL).style.set_caption("과거 2000Q1–2025Q4(투입형)").format({c: "{:.4f}" for c in ["최대 절대차", "평균 절대차", "새 값", "이전 값"]}))
s1n, s1p = scen[scen["시나리오"] == "S1"], spv[spv["시나리오"] == "S1"]
cmp = pd.DataFrame({"정비판 S1 40분기 평균": s1n[B.EXOG_COLS].mean(), "이전 판 S1 40분기 평균": s1p[B.EXOG_COLS].mean()})
cmp.index = [f"{B.EXOG_LABEL[c]}({B.EXOG_DEF[c][1]})" for c in cmp.index]
cmp.to_csv(os.path.join(C.OUTPUT, "table6_exog_vs_prev.csv"), encoding="utf-8-sig")
cmp.style.format("{:.3f}")
과거 2000Q1–2025Q4(투입형)
  최대 절대차 평균 절대차 최대차 위치 새 값 이전 값
항목          
유가 0.0001 0.0000 2023Q1 -4.9567 -4.9568
세계수요 0.0000 0.0000 2000Q1 1.6680 1.6680
실질금리 8.2946 2.0087 2000Q3 -2.1900 6.1046
실질환율 0.0000 0.0000 2000Q1 0.8177 0.8177
취업자 수 0.0000 0.0000 2000Q1 0.8011 0.8011
국내 경기 0.0000 0.0000 2000Q1 -0.8543 -0.8543
Out[12]:
  정비판 S1 40분기 평균 이전 판 S1 40분기 평균
유가(1차 차분) 0.561 2.451
세계수요(수준) 0.153 0.177
실질금리(1차 차분) 0.009 2.350
실질환율(1차 차분) 0.104 -0.109
취업자 수(로그 차분(×100)) -0.003 0.221
국내 경기(수준) -0.150 -0.103

검증 — 정비판 기록 산출물과 핵심 수치¶

계산 결과가 2026-10 정비 실행에서 동결한 기록 산출물(data/reference/)과 반올림 범위 안에서 같아야 통과한다. 표 단위 비교는 최대 절대차를 계산 열에 적고 기록 열을 0으로 둔다. 허용오차는 사후분포 요약·기준선 전망 5×10⁻⁷(소수 여섯째 자리), 거시 블록 경로 5×10⁻⁴(셋째 자리), 외생 6종 5×10⁻⁵(넷째 자리), 핵심 수치는 표기 자릿수의 절반이다.

In [13]:
P = C.PUBLISHED
checks = []
def add(item, calc, pub, tol):
    checks.append({"항목": item, "계산": calc, "기록": pub, "허용오차": tol})

# (1) 입력·모형
obs = B.read_dynare_data(C.DATAFILE)
add("관측자료 SHA-256(앞 12자리)", B.sha256(C.DATAFILE)[:12], P["관측자료_SHA256"][:12], "일치")
add("관측자료 분기 수", obs.shape[0], P["관측자료_분기"], 0)
add("관측변수 수", obs.shape[1], P["관측변수"], 0)
add("평활값 − 관측자료(최대절대차)", float(max((sm[v] - obs[v]).abs().max() for v in B.VAROBS)), 0.0, 1e-6)
mod = B.parse_mod(C.MODFILE)
add("방정식 수", mod["방정식"], P["방정식"], 0)
add("구조충격 수", mod["외생충격"], P["구조충격"], 0)
add("추정 행태계수 수", int((mod["사전분포"]["구분"] == "parameters").sum()), P["추정계수"], 0)
ss = B.calibrated_values(C.MODFILE, [r[0] for r in B.SS_REVISION])
for n, _, new, _, _ in B.SS_REVISION:
    add(f"정상상태 {n}", ss[n], new, 1e-9)

# (2) 사후분포 요약 54개 × 4통계
pub = pd.read_csv(C.REF["estimation_results"], encoding="utf-8-sig")
m = post.merge(pub, on=["구분", "파라미터"], suffixes=("", "_기록"), how="outer", indicator=True)
assert (m["_merge"] == "both").all() and len(m) == 54
for c in ["사후평균", "표준편차", "HPD_low", "HPD_high"]:
    add(f"사후 {c} 54개(최대절대차)", float((m[c] - m[c + "_기록"]).abs().max()), 0.0, 5e-7)
pm = post.set_index("파라미터")["사후평균"]
for k in ["gamma_kor1", "gamma_kor2", "gamma_kor3", "beta_kor1", "lambda_kor3", "alpha_kor2", "rho_oil1", "RES_OIL_us", "RES_Y_kor"]:
    add(f"사후평균 {k}", pm[k], P[k], 5e-7)

# (3) 기준선 전망과 연간 요약
bf = pd.read_csv(C.REF["baseline_forecast"], encoding="utf-8-sig")
add("기준선 전망 11변수 × 40분기(최대절대차)", float(max(np.abs(fc[c].values - bf[c].values).max() for c in bf.columns[1:])), 0.0, 5e-7)
for y in (2030, 2035):
    add(f"기준선 실질GDP 성장률 {y}(%)", ann.loc[y, "실질GDP 성장률(%)"], P[f"GDP성장_{y}"], 0.005)
    add(f"기준선 CPI 상승률 {y}(%)", ann.loc[y, "CPI 상승률(%)"], P[f"CPI_{y}"], 0.005)
add("기준선 두바이유 2035(달러, 연평균)", ann.loc[2035, "두바이유(달러/배럴)"], P["두바이유_2035"], 0.05)

# (4) 거시 블록
rmac = pd.read_csv(C.REF["dsge_macro_paths"], encoding="utf-8-sig"); rex = pd.read_csv(C.REF["dsge_exog_paths"], encoding="utf-8-sig")
add("거시경로 6열 × 200행(최대절대차)", float(B.max_abs_diff(mac, rmac, ["시나리오", "분기"], list(rmac.columns[2:])).max()), 0.0, 5e-4)
add("거시 블록 성장경로 5열 × 200행(최대절대차)", float(B.max_abs_diff(ex, rex, ["시나리오", "분기"], list(rex.columns[2:])).max()), 0.0, 5e-4)
cum = ex.groupby("시나리오")["lab_g"].sum()
for s in B.SCENARIOS:
    add(f"10년 누적 고용 증가율 {s}(%)", cum[s], P[f"누적고용_{s}"], 0.005)
add("S4 2030Q1 산출갭(%)", mac[(mac["시나리오"] == "S4") & (mac["분기"] == "2030Q1")]["산출갭Y"].iloc[0], P["산출갭2030_S4"], 0.005)

# (5) 수준 외생 — 외생별, 5개 시나리오 × 144분기
rlev = pd.read_csv(C.REF["exog_levels"], encoding="utf-8-sig")
dl = B.max_abs_diff(lev, rlev, ["시나리오", "분기"], B.EXOG_COLS)
for c in B.EXOG_COLS:
    add(f"수준 외생 {B.EXOG_LABEL[c]} S1–S5(최대절대차)", float(dl[c]), 0.0, 5e-5)
emp = lev[(lev["시나리오"] == "S1") & (lev["분기"] == "2025Q4")]["고용_취업자"].iloc[0]
add("취업자 수 구성값 2025Q4(천 명)", emp, P["고용구성_2025Q4"], 0.5)
add("관측 취업자 2025Q4(천 명)", float(lab.loc["2025Q4", "취업자"]), P["관측취업자_2025Q4"], 0.5)
e35 = lev[lev["분기"] == "2035Q4"].set_index("시나리오")
add("취업자 수 2035Q4 S1(천 명)", e35.loc["S1", "고용_취업자"], P["취업자_2035_S1"], 0.5)
add("취업자 수 2035Q4 S2/S1 − 1(%)", 100 * (e35.loc["S2", "고용_취업자"] / e35.loc["S1", "고용_취업자"] - 1), P["취업자_2035_S2대비"], 0.005)
add("두바이유 2035Q4 S1(달러)", float(np.exp(e35.loc["S1", "유가"] / 100)), P["유가_2035Q4_S1"], 0.05)
add("실질금리 2035Q4 S1(%)", e35.loc["S1", "실질금리"], P["실질금리_2035Q4_S1"], 0.005)

# (6) 산업모형 투입 외생 — 과거(외생별), 미래(시나리오 × 외생별)
rh = pd.read_csv(C.REF["exog_hist_v2"], encoding="utf-8-sig"); rs = pd.read_csv(C.REF["exog_scen_v2"], encoding="utf-8-sig")
dh = B.max_abs_diff(hist, rh, ["분기"], B.EXOG_COLS)
for c in B.EXOG_COLS:
    add(f"exog_hist_v2 {B.EXOG_LABEL[c]}(104분기)", float(dh[c]), 0.0, 5e-5)
for s in B.SCENARIOS:
    ds = B.max_abs_diff(scen[scen["시나리오"] == s], rs[rs["시나리오"] == s], ["시나리오", "분기"], B.EXOG_COLS)
    for c in B.EXOG_COLS:
        add(f"exog_scen_v2 {s} {B.EXOG_LABEL[c]}(40분기)", float(ds[c]), 0.0, 5e-5)

# (7) 레버 전달
for lever in reach.index:
    add(f"레버 전달 {lever}(최대 변화폭 > 0)", bool((reach.loc[lever] > 0).any()), True, "일치")

def passed(r):
    if r["허용오차"] == "일치":
        return r["계산"] == r["기록"]
    return abs(float(r["계산"]) - float(r["기록"])) <= float(r["허용오차"])
res = pd.DataFrame(checks)
res["통과"] = res.apply(passed, axis=1)
res.to_csv(os.path.join(C.OUTPUT, "verification.csv"), index=False, encoding="utf-8-sig")
show = res.copy()
for c in ["계산", "기록", "허용오차"]:
    show[c] = show[c].map(lambda v: v if isinstance(v, (str, bool)) else f"{v:.6g}")
with pd.option_context("display.max_rows", 200):
    display(show)
assert res["통과"].all(), "기록 산출물과 계산 결과가 다르다 — 위 표에서 통과=False 항목을 확인한다"
print(f"검증 통과: {len(res)}개 항목에서 정상상태·사후분포 요약·기준선·거시경로·외생 6종(S1–S5)·레버 전달을 재현했다.")
same = [f for f in ("exog_hist_v2.csv", "exog_scen_v2.csv", "dsge_macro_paths.csv", "dsge_exog_paths.csv", "exog_levels_2000_2035.csv")
        if B.sha256(os.path.join(C.OUTPUT, f)) == B.sha256(os.path.join(C.REFERENCE, f))]
print(f"참고(통과 조건 아님): 파일 단위로도 기록본과 바이트가 같은 산출물 {len(same)}/5개 — {', '.join(same)}")
항목 계산 기록 허용오차 통과
0 관측자료 SHA-256(앞 12자리) 79c7586814e9 79c7586814e9 일치 True
1 관측자료 분기 수 104 104 0 True
2 관측변수 수 20 20 0 True
3 평활값 − 관측자료(최대절대차) 2.88287e-09 0 1e-06 True
4 방정식 수 70 70 0 True
5 구조충격 수 34 34 0 True
6 추정 행태계수 수 20 20 0 True
7 정상상태 growth_kor_ss 1.8 1.8 1e-09 True
8 정상상태 pietar_kor_ss 2 2 1e-09 True
9 정상상태 def_kor_ss 2 2 1e-09 True
10 정상상태 rr_kor_bar_ss 0.55 0.55 1e-09 True
11 정상상태 tprem_kor_ss 0.18 0.18 1e-09 True
12 정상상태 tprem_tb_kor_ss 0.3 0.3 1e-09 True
13 정상상태 tprem_cb_kor_ss 0.67 0.67 1e-09 True
14 정상상태 house_kor_ss 2.33 2.33 1e-09 True
15 정상상태 ge_kor_ss 6.57 6.57 1e-09 True
16 정상상태 growth_us_ss 1.8 1.8 1e-09 True
17 정상상태 pietar_us_ss 2.3 2.3 1e-09 True
18 정상상태 rr_us_bar_ss 0.9 0.9 1e-09 True
19 정상상태 oil_g_us_ss 2.3 2.3 1e-09 True
20 사후 사후평균 54개(최대절대차) 0 0 5e-07 True
21 사후 표준편차 54개(최대절대차) 0 0 5e-07 True
22 사후 HPD_low 54개(최대절대차) 0 0 5e-07 True
23 사후 HPD_high 54개(최대절대차) 0 0 5e-07 True
24 사후평균 gamma_kor1 0.841458 0.841458 5e-07 True
25 사후평균 gamma_kor2 1.53946 1.53946 5e-07 True
26 사후평균 gamma_kor3 0.466217 0.466217 5e-07 True
27 사후평균 beta_kor1 0.211223 0.211223 5e-07 True
28 사후평균 lambda_kor3 0.765561 0.765561 5e-07 True
29 사후평균 alpha_kor2 0.0702645 0.070265 5e-07 True
30 사후평균 rho_oil1 0.879517 0.879517 5e-07 True
31 사후평균 RES_OIL_us 15.1934 15.1934 5e-07 True
32 사후평균 RES_Y_kor 1.83497 1.83497 5e-07 True
33 기준선 전망 11변수 × 40분기(최대절대차) 4.96344e-07 0 5e-07 True
34 기준선 실질GDP 성장률 2030(%) 1.89091 1.89 0.005 True
35 기준선 CPI 상승률 2030(%) 1.92217 1.92 0.005 True
36 기준선 실질GDP 성장률 2035(%) 1.82258 1.82 0.005 True
37 기준선 CPI 상승률 2035(%) 1.9342 1.93 0.005 True
38 기준선 두바이유 2035(달러, 연평균) 78.9038 78.9 0.05 True
39 거시경로 6열 × 200행(최대절대차) 0 0 0.0005 True
40 거시 블록 성장경로 5열 × 200행(최대절대차) 0 0 0.0005 True
41 10년 누적 고용 증가율 S1(%) -0.169 -0.17 0.005 True
42 10년 누적 고용 증가율 S2(%) -1.925 -1.93 0.005 True
43 10년 누적 고용 증가율 S3(%) 1.492 1.49 0.005 True
44 10년 누적 고용 증가율 S4(%) -0.171 -0.17 0.005 True
45 10년 누적 고용 증가율 S5(%) -0.187 -0.19 0.005 True
46 S4 2030Q1 산출갭(%) 0.463 0.46 0.005 True
47 수준 외생 유가 S1–S5(최대절대차) 0 0 5e-05 True
48 수준 외생 세계수요 S1–S5(최대절대차) 0 0 5e-05 True
49 수준 외생 실질금리 S1–S5(최대절대차) 0 0 5e-05 True
50 수준 외생 실질환율 S1–S5(최대절대차) 0 0 5e-05 True
51 수준 외생 취업자 수 S1–S5(최대절대차) 0 0 5e-05 True
52 수준 외생 국내 경기 S1–S5(최대절대차) 0 0 5e-05 True
53 취업자 수 구성값 2025Q4(천 명) 28800.8 28801 0.5 True
54 관측 취업자 2025Q4(천 명) 28792 28792 0.5 True
55 취업자 수 2035Q4 S1(천 명) 28762.7 28762.7 0.5 True
56 취업자 수 2035Q4 S2/S1 − 1(%) -1.72172 -1.72 0.005 True
57 두바이유 2035Q4 S1(달러) 79.5853 79.6 0.05 True
58 실질금리 2035Q4 S1(%) 1.3132 1.31 0.005 True
59 exog_hist_v2 유가(104분기) 0 0 5e-05 True
60 exog_hist_v2 세계수요(104분기) 0 0 5e-05 True
61 exog_hist_v2 실질금리(104분기) 0 0 5e-05 True
62 exog_hist_v2 실질환율(104분기) 0 0 5e-05 True
63 exog_hist_v2 취업자 수(104분기) 0 0 5e-05 True
64 exog_hist_v2 국내 경기(104분기) 0 0 5e-05 True
65 exog_scen_v2 S1 유가(40분기) 0 0 5e-05 True
66 exog_scen_v2 S1 세계수요(40분기) 0 0 5e-05 True
67 exog_scen_v2 S1 실질금리(40분기) 0 0 5e-05 True
68 exog_scen_v2 S1 실질환율(40분기) 0 0 5e-05 True
69 exog_scen_v2 S1 취업자 수(40분기) 0 0 5e-05 True
70 exog_scen_v2 S1 국내 경기(40분기) 0 0 5e-05 True
71 exog_scen_v2 S2 유가(40분기) 0 0 5e-05 True
72 exog_scen_v2 S2 세계수요(40분기) 0 0 5e-05 True
73 exog_scen_v2 S2 실질금리(40분기) 0 0 5e-05 True
74 exog_scen_v2 S2 실질환율(40분기) 0 0 5e-05 True
75 exog_scen_v2 S2 취업자 수(40분기) 0 0 5e-05 True
76 exog_scen_v2 S2 국내 경기(40분기) 0 0 5e-05 True
77 exog_scen_v2 S3 유가(40분기) 0 0 5e-05 True
78 exog_scen_v2 S3 세계수요(40분기) 0 0 5e-05 True
79 exog_scen_v2 S3 실질금리(40분기) 0 0 5e-05 True
80 exog_scen_v2 S3 실질환율(40분기) 0 0 5e-05 True
81 exog_scen_v2 S3 취업자 수(40분기) 0 0 5e-05 True
82 exog_scen_v2 S3 국내 경기(40분기) 0 0 5e-05 True
83 exog_scen_v2 S4 유가(40분기) 0 0 5e-05 True
84 exog_scen_v2 S4 세계수요(40분기) 0 0 5e-05 True
85 exog_scen_v2 S4 실질금리(40분기) 0 0 5e-05 True
86 exog_scen_v2 S4 실질환율(40분기) 0 0 5e-05 True
87 exog_scen_v2 S4 취업자 수(40분기) 0 0 5e-05 True
88 exog_scen_v2 S4 국내 경기(40분기) 0 0 5e-05 True
89 exog_scen_v2 S5 유가(40분기) 0 0 5e-05 True
90 exog_scen_v2 S5 세계수요(40분기) 0 0 5e-05 True
91 exog_scen_v2 S5 실질금리(40분기) 0 0 5e-05 True
92 exog_scen_v2 S5 실질환율(40분기) 0 0 5e-05 True
93 exog_scen_v2 S5 취업자 수(40분기) 0 0 5e-05 True
94 exog_scen_v2 S5 국내 경기(40분기) 0 0 5e-05 True
95 레버 전달 인구(최대 변화폭 > 0) True True 일치 True
96 레버 전달 yus(최대 변화폭 > 0) True True 일치 True
97 레버 전달 oil(최대 변화폭 > 0) True True 일치 True
98 레버 전달 tcost(최대 변화폭 > 0) True True 일치 True
99 레버 전달 dshk(최대 변화폭 > 0) True True 일치 True
검증 통과: 100개 항목에서 정상상태·사후분포 요약·기준선·거시경로·외생 6종(S1–S5)·레버 전달을 재현했다.
참고(통과 조건 아님): 파일 단위로도 기록본과 바이트가 같은 산출물 5/5개 — exog_hist_v2.csv, exog_scen_v2.csv, dsge_macro_paths.csv, dsge_exog_paths.csv, exog_levels_2000_2035.csv