노트북은 예측 → 계산 → 대조 세 부분으로 고정한다. 새 개념은 도입하지 않는다. 본문에서 이미 유도한 것을 수치로 확인할 뿐이다.
1. 예측 (코드를 쓰기 전에)¶
아래 칸을 먼저 채운다. 계산하지 않는다.
계산 계획: 가계별 해는 (eq-w23-8)·(eq-w23-9)·(eq-w23-10)의 경우 나누기를 코드로 그대로 쌓는다. 검증용으로 몇 가계는 격자 위에서 를 직접 최대화한다. 집계 성향은 모든 가계에 를 주고 중앙차분한다. 기간은 부터 를 격자 위에서 — 그리드 최대화, 선형 보간. 불확실성은 (eq-w23-14)를 에 대한 이분법(200회)으로 푼다.
가계 을 , , 으로 뽑고 , 로 각자 풀면, 묶인 비율은 이고, 균일 이전 에 대한 집계 성향은 , 이며, 쐐기 는 묶인 비율과 소수 셋째 자리까지 일치한다.
예측: ____
기간 로그 문제를 역진귀납(W12의 벨만 연산자를 번 적용)으로 풀면 1기 성향은 에서 — (eq-w23-13)의 닫힌형과 소수 셋째 자리까지 맞고, 을 1.02로 바꿔도 변하지 않는다.
예측: ____
한도 없는 세계에서 : 에서 은 로 오르지만 는 10-3 이내에서 0이고, (eq-w23-12)로 역산한 , 는 로 떨어진다.
예측: ____
2. 계산¶
패키지 호출로 답을 내지 않는다. 경우 나누기·격자 최대화·역진귀납·이분법을 차례로 직접 쌓는다.
# 도구와 한국어 글꼴, 그리고 두 기간 모형의 파라미터를 쌓는다
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
_cands = ['Apple SD Gothic Neo', 'AppleGothic', 'NanumGothic', 'Noto Sans CJK KR', 'Malgun Gothic']
_have = {f.name for f in font_manager.fontManager.ttflist}
for _f in _cands:
if _f in _have:
plt.rcParams['font.family'] = _f
break
plt.rcParams['axes.unicode_minus'] = False
BETA, R, Y1, BBAR = 0.96, 1.04, 1.0, 0.0
def solve2p(y1, y2, bbar, beta=BETA, r=R):
"""(eq-w23-8)·(eq-w23-9)·(eq-w23-10)의 경우 나누기를 식 그대로 쌓는다"""
c1u = (y1 + y2 / r) / (1.0 + beta) # 묶이지 않은 해
bound = (y1 - c1u) < -bbar # 묶임 조건 s^u < -b̄
c1 = np.where(bound, y1 + bbar, c1u)
s = np.where(bound, -bbar, y1 - c1u)
c2 = y2 + r * s
mu = np.where(bound, 1.0 / c1 - beta * r / c2, 0.0)
return c1, c2, s, mu, bound, c1u
THR = R * (BETA * Y1 + (1 + BETA) * BBAR)
print('글꼴:', plt.rcParams['font.family'][0])
print(f'묶임 문턱 y2 > R[βy1+(1+β)b̄] = {THR:.4f} (y1={Y1}, b̄={BBAR})')
print(f'대표적 개인의 성향 1/(1+β) = {1 / (1 + BETA):.4f}')글꼴: Apple SD Gothic Neo
묶임 문턱 y2 > R[βy1+(1+β)b̄] = 0.9984 (y1=1.0, b̄=0.0)
대표적 개인의 성향 1/(1+β) = 0.5102
예측 1. 두 기간 해를 (eq-w23-8)·(eq-w23-9)·(eq-w23-10) 그대로 쌓고, 같은 답이 격자 최대화에서도 나오는지 본 뒤, 가계 1만을 뽑아 집계 성향을 중앙차분한다.
# 검증 — 몇 가계는 s 격자 위에서 u(y1-s)+βu(y2+Rs)를 직접 최대화해 같은 답인지 본다
sg = np.linspace(-BBAR, Y1 - 1e-7, 400001)
print(f"{'y2':>6}{'c1u':>9}{'c1':>9}{'c2':>9}{'μ':>9}{'R̃':>10}"
f"{'격자 s':>11}{'|차|':>10} 묶임")
for yy2 in (0.9, 1.2, 1.5, 2.0):
c1, c2, s, mu, bd, c1u = solve2p(Y1, yy2, BBAR)
s_grid = sg[np.argmax(np.log(Y1 - sg) + BETA * np.log(yy2 + R * sg))]
rt = float(c2) / (BETA * float(c1)) # 잠재 이자율 R̃ = c2/(βc1)
print(f'{yy2:6.1f}{float(c1u):9.3f}{float(c1):9.3f}'
f'{float(c2):9.3f}{float(mu):9.3f}{rt:10.4f}{s_grid:11.5f}'
f'{abs(s_grid - float(s)):10.1e} {"예" if bd else "아니오"}')
print('격자 최대화와 닫힌형이 같은 s를 준다 — 경우 나누기가 맞다') y2 c1u c1 c2 μ R̃ 격자 s |차| 묶임
0.9 0.952 0.952 0.950 0.000 1.0400 0.04827 6.6e-07 아니오
1.2 1.099 1.000 1.200 0.168 1.2500 0.00000 0.0e+00 예
1.5 1.246 1.000 1.500 0.334 1.5625 0.00000 0.0e+00 예
2.0 1.491 1.000 2.000 0.501 2.0833 0.00000 0.0e+00 예
격자 최대화와 닫힌형이 같은 s를 준다 — 경우 나누기가 맞다
# 예측 1 — 가계 1만을 뽑아 각자 풀고, 모두에게 ±Δ를 주어 집계 성향을 중앙차분한다
rng = np.random.default_rng(2023)
N, DELTA = 10000, 1e-3
y2 = rng.uniform(0.6, 2.0, N)
bound = solve2p(Y1, y2, BBAR)[4]
frac = float(bound.mean())
def C1bar(y1v, y2v):
"""모든 가계에 같은 소득을 주었을 때의 집계 1기 소비"""
return float(solve2p(y1v, y2v, BBAR)[0].mean())
kap1 = (C1bar(Y1 + DELTA, y2) - C1bar(Y1 - DELTA, y2)) / (2 * DELTA)
kap2 = (C1bar(Y1, y2 + DELTA) - C1bar(Y1, y2 - DELTA)) / (2 * DELTA)
wedge = kap1 - R * kap2
print(f'묶인 가계 {int(bound.sum())} / {N} 비율 {frac:.4f} (모집단 값 {(2.0 - THR) / 1.4:.4f})')
print(f'κ1 = {kap1:.4f} κ2 = {kap2:.4f} Rκ2 = {R * kap2:.4f}')
print(f'쐐기 κ1-Rκ2 = {wedge:.4f} 묶인 비율 = {frac:.4f} 차 = {wedge - frac:+.2e}')
print(f'역산 (eq-w23-12): ω̂ = {wedge:.4f} β̂ = {(1 - kap1) / (R * kap2):.4f} (참값 β = {BETA})')
print(f'대표적 개인이라면 κ1 = {1 / (1 + BETA):.4f} — 수준 차 '
f'{kap1 - 1 / (1 + BETA):.4f}는 전부 분포에서 온다')묶인 가계 7162 / 10000 비율 0.7162 (모집단 값 0.7154)
κ1 = 0.8610 κ2 = 0.1392 Rκ2 = 0.1448
쐐기 κ1-Rκ2 = 0.7162 묶인 비율 = 0.7162 차 = +1.85e-05
역산 (eq-w23-12): ω̂ = 0.7162 β̂ = 0.9600 (참값 β = 0.96)
대표적 개인이라면 κ1 = 0.5102 — 수준 차 0.3508는 전부 분포에서 온다
# 소비함수의 꺾임과 집계 성향을 그린다
gy2 = np.linspace(0.6, 2.0, 561)
cc, ccu = solve2p(Y1, gy2, BBAR)[0], (Y1 + gy2 / R) / (1 + BETA)
fig, (axL, axR) = plt.subplots(1, 2, figsize=(9.8, 3.8))
axT = axL.twinx()
axT.hist(y2, bins=45, color='#e8e8e8', zorder=0)
axT.set_ylabel('가계 수')
axL.set_zorder(axT.get_zorder() + 1)
axL.patch.set_visible(False)
axL.plot(gy2, ccu, ls='--', lw=1.2, color='#3b6ea5', label=r'원하던 $c_1^{u}$')
axL.plot(gy2, cc, lw=1.9, color='#c0392b', label=r'실현 $c_1=\min(c_1^{u},\,y_1+\bar b)$')
axL.axvline(THR, ls=':', color='k', lw=1.1)
axL.text(THR + 0.03, 0.78, f'문턱 {THR:.4f}', fontsize=9)
axL.set_xlabel('2기 소득 $y_2$ [화폐]')
axL.set_ylabel('1기 소비 $c_1$ [화폐]')
axL.set_ylim(0.75, 1.55)
axL.legend(loc='upper left', fontsize=8)
axL.set_title(f'표본에서 묶인 비율 {frac:.3f}')
vals = [kap1, R * kap2, wedge, frac]
axR.bar(range(4), vals, 0.55, color=['#3b6ea5', '#d98c3f', '#c0392b', '#7f8c8d'])
for i, v in enumerate(vals):
axR.text(i, v + 0.02, f'{v:.3f}', ha='center', fontsize=9)
axR.set_xticks(range(4))
axR.set_xticklabels([r'$\kappa_1$', r'$R\kappa_2$', '쐐기', '묶인 비율'])
axR.set_ylim(0, 1.05)
axR.set_title('쐐기가 묶인 비율과 만난다')
plt.tight_layout()
plt.show()
예측 2. 지평을 되살린다. 에서 시작해 를 격자 위에서 역진하고, 1기 정책의 기울기를 (eq-w23-13)의 닫힌형과 겹친다. 자원은 이므로 은 정책의 에 대한 기울기다.
# 예측 2 — W 격자 위의 벨만 연산자를 쌓는다
LNW = np.linspace(np.log(0.02), np.log(200.0), 161) # 자원 W의 로그 격자
TH = np.linspace(1e-4, 1 - 1e-4, 4001) # 소비 비율 c/W 격자
def Vinterp(x, vg):
"""lnW 격자 위 선형 보간 — 격자 밖은 끝 기울기로 잇는다"""
v = np.interp(x, LNW, vg)
s0 = (vg[1] - vg[0]) / (LNW[1] - LNW[0])
s1 = (vg[-1] - vg[-2]) / (LNW[-1] - LNW[-2])
v = np.where(x < LNW[0], vg[0] + s0 * (x - LNW[0]), v)
return np.where(x > LNW[-1], vg[-1] + s1 * (x - LNW[-1]), v)
def backward(T, r):
"""V_T(W)=ln W 에서 시작해 벨만 연산자를 T-2번 적용하고 V_2를 돌려준다"""
V, W = LNW.copy(), np.exp(LNW)[:, None]
for _ in range(T - 2):
val = np.log(TH[None, :] * W) + BETA * Vinterp(np.log(r * W * (1 - TH[None, :])), V)
V = val.max(axis=1)
return V
def policy1(W, V2, r, rounds=4):
"""1기 정책 — c/W 격자를 네 번 조여 최대점을 찾는다"""
lo, hi = 1e-7, 1 - 1e-7
for _ in range(rounds):
th = np.linspace(lo, hi, 1001)
val = np.log(th * W) + BETA * Vinterp(np.log(r * W * (1 - th)), V2)
j, d = int(np.argmax(val)), (hi - lo) / 1000.0
lo, hi = max(th[j] - d, 1e-9), min(th[j] + d, 1 - 1e-9)
return 0.5 * (lo + hi) * W
print(f'격자 {LNW.size} × {TH.size}, 조이기 {4}회 — 보간은 선형, 최대화는 격자')격자 161 × 4001, 조이기 4회 — 보간은 선형, 최대화는 격자
# 역진귀납의 1기 성향을 (eq-w23-13)의 닫힌형과 겹친다
HH = 1e-3
bi = {}
print(f"{'R':>6}{'T':>5}{'W0':>9}{'역진귀납':>12}{'닫힌형':>11}{'차':>11}")
for r in (1.04, 1.02):
for T in (2, 10, 40):
V2 = backward(T, r)
W0 = sum(r ** -k for k in range(T)) # y_t=1 (t=1..T)의 현재가치
bi[(r, T)] = (policy1(W0 + HH, V2, r) - policy1(W0 - HH, V2, r)) / (2 * HH)
cf = (1 - BETA) / (1 - BETA ** T)
print(f'{r:6.2f}{T:5d}{W0:9.3f}{bi[(r, T)]:12.4f}{cf:11.4f}{bi[(r, T)] - cf:+11.1e}')
print('R 1.04→1.02의 차: ' + ', '.join(
f'T={T}: {bi[(1.02, T)] - bi[(1.04, T)]:+.1e}' for T in (2, 10, 40)))
print('묶인 가계의 성향은 지평과 무관하게 1 — 순위는 살고 수준은 살지 않는다') R T W0 역진귀납 닫힌형 차
1.04 2 1.962 0.5102 0.5102 -3.9e-07
1.04 10 8.435 0.1193 0.1193 -1.3e-05
1.04 40 20.584 0.0497 0.0497 -5.8e-06
1.02 2 1.980 0.5102 0.5102 +1.4e-06
1.02 10 9.162 0.1194 0.1193 +7.5e-06
1.02 40 27.903 0.0497 0.0497 -7.8e-07
R 1.04→1.02의 차: T=2: +1.8e-06, T=10: +2.0e-05, T=40: +5.1e-06
묶인 가계의 성향은 지평과 무관하게 1 — 순위는 살고 수준은 살지 않는다
# 지평에 대한 성향 곡선을 그린다
Ts = np.arange(2, 51)
fig, ax = plt.subplots(figsize=(6.2, 3.9))
ax.plot(Ts, (1 - BETA) / (1 - BETA ** Ts), color='#3b6ea5',
label=r'닫힌형 $(1-\beta)/(1-\beta^{T})$')
ax.axhline(1 - BETA, ls=':', color='#3b6ea5', lw=1.1)
ax.text(33, 1 - BETA + 0.03, rf'$1-\beta={1 - BETA:.2f}$', fontsize=9, color='#3b6ea5')
ax.axhline(1.0, color='#c0392b', lw=1.6)
ax.text(27, 1.03, '묶인 가계: 1 (지평과 무관)', fontsize=9, color='#c0392b', ha='center')
for T in (2, 10, 40):
ax.plot([T], [bi[(1.04, T)]], 'o', color='k', ms=5, zorder=3)
ax.annotate(f'$T={T}$: {bi[(1.04, T)]:.3f}', xy=(T, bi[(1.04, T)]),
xytext=(T + 2, bi[(1.04, T)] + 0.08), fontsize=9)
ax.set_xlabel('지평 $T$ (기간 수)')
ax.set_ylabel(r'1기 성향 $\partial c_1/\partial y_1$')
ax.set_xlim(0, 54)
ax.set_ylim(-0.03, 1.14)
ax.legend(loc='center right', fontsize=8)
ax.set_title('역진귀납 점이 닫힌형 곡선 위에 앉는다')
plt.tight_layout()
plt.show()
예측 3. 확실성(A2)을 뺀다. 한도를 없애고 , 등확률로 두어 (eq-w23-14)를 에 대한 이분법으로 푼다. 수준 과 쐐기 가 서로 다르게 움직이는지 본다.
# 예측 3 — 확실성을 뺀다. (eq-w23-14)를 c1에 대한 이분법 200회로 푼다
def c1_unc(y1, yb2, eps, beta=BETA, r=R, iters=200):
"""1/c1 = βR·E[1/(ȳ2+η+R(y1-c1))], η=±ε 등확률, 한도 없음"""
lo, hi = 1e-9, y1 + (yb2 - eps) / r - 1e-9 # 나쁜 상태에서도 c2>0
for _ in range(iters):
mid = 0.5 * (lo + hi)
c2 = yb2 + np.array([-eps, eps]) + r * (y1 - mid)
if 1.0 / mid - beta * r * np.mean(1.0 / c2) > 0:
lo = mid # 좌변이 크다 → c1을 키운다
else:
hi = mid
return 0.5 * (lo + hi)
YB2 = 1.5
print(f'ε=0 이분법 해 c1 = {c1_unc(Y1, YB2, 0.0):.8f}')
print(f'확실성 닫힌형 (eq-w23-8) = {(Y1 + YB2 / R) / (1 + BETA):.8f}')ε=0 이분법 해 c1 = 1.24607535
확실성 닫힌형 (eq-w23-8) = 1.24607535
# ε 격자에서 집계 성향과 역산 모수를 쌓는다 (한도가 없으므로 ω=0인 세계다)
HU, unc = 1e-4, []
print(f"{'ε':>6}{'c1':>10}{'κ1':>10}{'Rκ2':>10}{'쐐기':>13}{'ω̂':>10}{'β̂':>10}")
for e in (0.0, 0.25, 0.5, 0.75, 1.0):
c = c1_unc(Y1, YB2, e)
a1 = (c1_unc(Y1 + HU, YB2, e) - c1_unc(Y1 - HU, YB2, e)) / (2 * HU)
a2 = (c1_unc(Y1, YB2 + HU, e) - c1_unc(Y1, YB2 - HU, e)) / (2 * HU)
unc.append((e, c, a1, a2))
print(f'{e:6.2f}{c:10.4f}{a1:10.4f}{R * a2:10.4f}{a1 - R * a2:+13.1e}'
f'{a1 - R * a2:10.4f}{(1 - a1) / (R * a2):10.4f}')
print('κ1은 오르고 쐐기는 0에 붙어 있다 — 신중성은 참을성 부족으로 오독되되 제약으로는 아니다') ε c1 κ1 Rκ2 쐐기 ω̂ β̂
0.00 1.2461 0.5102 0.5102 -2.3e-12 -0.0000 0.9600
0.25 1.2219 0.5197 0.5197 +1.2e-12 0.0000 0.9241
0.50 1.1545 0.5427 0.5427 +1.3e-12 0.0000 0.8426
0.75 1.0548 0.5695 0.5695 +3.2e-12 0.0000 0.7558
1.00 0.9334 0.5943 0.5943 +3.6e-12 0.0000 0.6827
κ1은 오르고 쐐기는 0에 붙어 있다 — 신중성은 참을성 부족으로 오독되되 제약으로는 아니다
# 모멘트 평면에 허용 삼각형·신중성 궤적·표본 점을 그린다
fig, ax = plt.subplots(figsize=(5.8, 5.6))
ax.fill([0, 0.5, 1], [1, 0.5, 1], color='#ededed', zorder=0)
xx = np.linspace(0, 1, 201)
for om in (0.0, 0.25, 0.5, 0.75):
keep = (om + xx <= 1 + 1e-9) & (om + xx >= 1 - xx - 1e-9)
ax.plot(xx[keep], (om + xx)[keep], color='#9aa0a6', lw=0.9, zorder=1)
ax.text(1 - om, 1.015, rf'$\omega={om}$', fontsize=8, color='#6b6b6b',
ha='right' if om == 0.0 else 'center')
ax.plot([0.10, 0.5], [0.10, 0.5], ls='--', lw=0.8, color='#9aa0a6', zorder=1)
u = np.array([[R * a2, a1] for _, _, a1, a2 in unc])
ax.plot(u[:, 0], u[:, 1], '-o', color='#d98c3f', ms=4, lw=2.2, zorder=2,
label=r'신중성 궤적 ($\varepsilon=0\to1$)')
ax.text(0.58, 0.42, rf'$\varepsilon=0$: $\kappa_1$={unc[0][2]:.3f}, '
rf'$\hat\beta$={(1 - unc[0][2]) / (R * unc[0][3]):.2f}' + '\n'
+ rf'$\varepsilon=1$: $\kappa_1$={unc[-1][2]:.3f}, '
rf'$\hat\beta$={(1 - unc[-1][2]) / (R * unc[-1][3]):.2f}',
fontsize=8.5, color='#d98c3f', va='top')
ax.plot([R * kap2], [kap1], 'o', color='#c0392b', ms=7, zorder=3, label='표본 1만 가계')
ax.annotate('', xy=(R * kap2, R * kap2), xytext=(R * kap2, kap1),
arrowprops=dict(arrowstyle='<->', color='#c0392b', lw=1.0))
ax.text(R * kap2 + 0.02, 0.5 * (kap1 + R * kap2), f'쐐기 = {wedge:.3f}',
fontsize=9, color='#c0392b', va='center')
ax.set_xlabel(r'$R\kappa_2$ — 예고소득 반응 $\times R$')
ax.set_ylabel(r'$\kappa_1$ — 현재소득 반응')
ax.set_xlim(0, 1.06)
ax.set_ylim(0.10, 1.10)
ax.set_aspect('equal')
ax.legend(loc='lower right', fontsize=8)
plt.tight_layout()
plt.show()
# 예측 항목 세 개에 대응하는 수치를 한자리에 모은다
print(f'[예측 1] 묶인 비율 {frac:.4f} (예측 0.715) · κ1 = {kap1:.4f} (0.861) · '
f'κ2 = {kap2:.4f} (0.140)')
print(f' 쐐기 {wedge:.4f} vs 묶인 비율 {frac:.4f} — 차 {wedge - frac:+.1e}, '
f'β̂ = {(1 - kap1) / (R * kap2):.4f}')
print('[예측 2] 역진귀납 T=2,10,40: ' + ', '.join(f'{bi[(1.04, T)]:.3f}' for T in (2, 10, 40))
+ ' (예측 0.510, 0.119, 0.050)')
print(f' 닫힌형과의 최대 차 '
f'{max(abs(bi[(1.04, T)] - (1 - BETA) / (1 - BETA ** T)) for T in (2, 10, 40)):.1e} · '
f'R=1.02와의 최대 차 {max(abs(bi[(1.02, T)] - bi[(1.04, T)]) for T in (2, 10, 40)):.1e}')
print(f'[예측 3] κ1 {unc[0][2]:.3f} → {unc[-1][2]:.3f} (예측 0.510 → 0.594) · '
f'최대 |쐐기| = {max(abs(a1 - R * a2) for _, _, a1, a2 in unc):.1e}')
print(f' ω̂ = {unc[-1][2] - R * unc[-1][3]:+.4f} (예측 0) · '
f'β̂ {(1 - unc[0][2]) / (R * unc[0][3]):.2f} → '
f'{(1 - unc[-1][2]) / (R * unc[-1][3]):.2f} (예측 0.96 → 0.68)')[예측 1] 묶인 비율 0.7162 (예측 0.715) · κ1 = 0.8610 (0.861) · κ2 = 0.1392 (0.140)
쐐기 0.7162 vs 묶인 비율 0.7162 — 차 +1.9e-05, β̂ = 0.9600
[예측 2] 역진귀납 T=2,10,40: 0.510, 0.119, 0.050 (예측 0.510, 0.119, 0.050)
닫힌형과의 최대 차 1.3e-05 · R=1.02와의 최대 차 2.0e-05
[예측 3] κ1 0.510 → 0.594 (예측 0.510 → 0.594) · 최대 |쐐기| = 3.6e-12
ω̂ = +0.0000 (예측 0) · β̂ 0.96 → 0.68 (예측 0.96 → 0.68)
3. 대조¶
예측과 계산이 어긋난 지점을 적는다. 어느 쪽이 틀렸는지 판정한다.
| 예측 | 결과 | 어긋남 | 원인 |
|---|---|---|---|
본문 확인: 어긋났으면 (eq-w23-9)·(eq-w23-12)·(eq-w23-13)로 돌아간다.