Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

노트북은 예측 → 계산 → 대조 세 부분으로 고정한다. 새 개념은 도입하지 않는다. 본문에서 이미 유도한 것을 수치로 확인할 뿐이다.

1. 예측 (코드를 쓰기 전에)

코드를 쓰기 전에 아래 셋에 답한다. 계산하지 않는다.

1. 두 점 성장 (u,d)=(1.2,0.8)(u,d)=(1.2,0.8), 확률 각 12\tfrac12, Smin=1S_{\min}=1, N=20,000N=20{,}000 기업·T=2,000T=2{,}000기: 로그–로그 생존함수의 꼬리 기울기는 -1.00(Zipf — E[g]=0\mathbb{E}[g]=0이므로 정확히). d=0.75d=0.75로 바꾸면 약 -2(이분법 근 -1.975); 정규 공식 (eq-w19-11)은 1.908을 준다(3.4% 낮다). 벽에 붙어 있는(xt=0x_t=0) 기업 비율은 약 14%(첫 사례)·26%(둘째). 예측: “기울기 -1.00, -1.98; 벽 비율 14%, 26%”. 원인란에 적을 것: x[1,5]x\in[1,5] 구간 최소제곱은 유한 표본 때문에 이론값보다 5%까지 가파를 수 있다.

예측: ____

2. 벽을 떼면 Var[lnST]=TσΔ2\mathrm{Var}[\ln S_T]=T\sigma^{2}_\DeltaT=1,000T=1{,}000에서 41.1(첫 사례). 벽을 두면 Var[xt]\mathrm{Var}[x_t]T500T\approx500 이후 더 늘지 않고 약 1/ζ21/\zeta^{2} 근처 — 1.0(첫 사례), 0.26(둘째) — 에서 포화한다. 예측: “벽 없음은 TT에 선형, 벽 있음은 포화”. 원인란에 적을 것: 경계층 때문에 정확히 1/ζ21/\zeta^{2}은 아니다.

예측: ____

3. Simon t=105t=10^{5}: p=0.1p=0.1이면 기업 수 약 104, 규모 1인 비율 1/(2p)=0.5261/(2-p)=0.526, 로그–로그 국소 기울기 k=50k=50에서 -1.11. p=12p=\tfrac12: 규모 1 비율 0.667, 국소 기울기 k=5k=5에서 -1.83, k=50k=50에서 -1.98(순수 멱 -2는 점근; ζ=2\zeta=2Pr(Kk)=2/(k(k+1))\Pr(K\ge k)=2/(k(k+1)), dlnG/dlnk=(1+k/(k+1))d\ln G/d\ln k=-(1+k/(k+1))). tit/100t_i\approx t/100에 진입한 기업들의 평균 규모는 1000.963100^{0.9}\approx63(p=0.1p=0.110(p=12p=\tfrac12). 예측: “규모 1 비율 0.53·0.67, 기울기는 kk가 커질수록 ζ-\zeta로”. 원인란에 적을 것: 진입 시각별 평균 규모는 표본이 적어 seed마다 ±15%\pm15\% 흔들린다.

예측: ____

2. 계산

계산은 직접 쌓는다. 반사 무작위보행은 xmax(x+ε,0)x\leftarrow\max(x+\varepsilon,0) 한 줄로 돌리고, 경험적 생존함수는 정렬로, 고유값 식 M(θ)=1M(\theta)=1은 손으로 짠 이분법으로 푼다.

# 도구를 올리고 한국어 글꼴을 고른다.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.ticker import FuncFormatter, LogLocator, NullFormatter
from math import exp, lgamma, log

_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
plt.rcParams['mathtext.fontset'] = 'cm'

rng = np.random.default_rng(19)
print('글꼴:', plt.rcParams['font.family'][0])
글꼴: Apple SD Gothic Neo
# 두 점 성장의 적률생성함수와 로그 성장률의 두 적률을 쌓는다.
def M(theta, u, d):
    return 0.5 * (u ** theta + d ** theta)          # M(θ) = E[(1+g)^θ], 확률 각 1/2


def cumulants(u, d):
    e = np.array([log(u), log(d)])                  # ε = ln(1+g)
    return e.mean(), ((e - e.mean()) ** 2).mean()   # μ_Δ, σ²_Δ


# 로그축 눈금을 $10^{n}$ 으로 적는다 — 한국어 글꼴에 빼기표가 없어 생기는 깨짐을 피한다.
def pow10_axes(ax):
    fmt = FuncFormatter(lambda v, _: r'$10^{%d}$' % int(round(np.log10(v))))
    for axis in (ax.xaxis, ax.yaxis):
        axis.set_major_locator(LogLocator(base=10.0))
        axis.set_major_formatter(fmt)
        axis.set_minor_formatter(NullFormatter())


mu0, s20 = cumulants(1.2, 0.8)
print(f'(u, d) = (1.2, 0.8):  μ_Δ = {mu0:+.4f}  σ²_Δ = {s20:.4f}  M(1) = E[1+g] = {M(1.0, 1.2, 0.8):.4f}')
(u, d) = (1.2, 0.8):  μ_Δ = -0.0204  σ²_Δ = 0.0411  M(1) = E[1+g] = 1.0000
# (eq-w19-7)의 M(θ)=1 을 손으로 짠 이분법으로 푼다 — scipy.optimize 를 쓰지 않는다.
def root_bisect(u, d, lo=1e-6, hi=20.0, it=200):
    mu, _ = cumulants(u, d)
    if mu >= 0 or M(hi, u, d) <= 1:        # A4 위반이면 양의 근이 없다
        return None
    for _ in range(it):
        mid = 0.5 * (lo + hi)
        if M(mid, u, d) < 1:
            lo = mid
        else:
            hi = mid
    return 0.5 * (lo + hi)


# 네 파라미터에서 μ_Δ·σ²_Δ·E[g]·ζ·정규 공식 ζ_N 을 나란히 적는다.
PARS = [(1.2, 0.8), (1.2, 0.75), (1.2, 0.83), (1.2, 0.85)]
ZETA = {}
print(f"{'(u, d)':>12}{'mu_D':>9}{'s2_D':>9}{'E[g]':>9}{'zeta':>9}{'zeta_N':>9}{'M(1)':>9}")
for u, d in PARS:
    mu, s2 = cumulants(u, d)
    ZETA[(u, d)] = root_bisect(u, d)
    zs = '없음' if ZETA[(u, d)] is None else f'{ZETA[(u, d)]:.3f}'
    zn = '-' if mu >= 0 else f'{-2 * mu / s2:.3f}'
    print(f'({u}, {d})'.rjust(12) + f'{mu:+9.4f}{s2:9.4f}{0.5 * (u + d) - 1:+9.4f}'
          f'{zs:>9}{zn:>9}{M(1.0, u, d):9.4f}')
mu2, s22 = cumulants(1.2, 0.75)
print(f'정규 공식의 어긋남 (1.2, 0.75): ζ_N/ζ - 1 = '
      f'{(-2 * mu2 / s22) / ZETA[(1.2, 0.75)] - 1:+.2%}')
      (u, d)     mu_D     s2_D     E[g]     zeta   zeta_N     M(1)
  (1.2, 0.8)  -0.0204   0.0411  +0.0000    1.000    0.993   1.0000
 (1.2, 0.75)  -0.0527   0.0552  -0.0250    1.975    1.908   0.9750
 (1.2, 0.83)  -0.0020   0.0340  +0.0150    0.118    0.118   1.0150
 (1.2, 0.85)  +0.0099   0.0297  +0.0250       없음        -   1.0250
정규 공식의 어긋남 (1.2, 0.75): ζ_N/ζ - 1 = -3.40%

예측 1. 두 파라미터 세트의 반사 무작위보행을 돌려 로그–로그 생존함수의 꼬리 기울기와 벽 위의 비율을 잰다. 이분법 근 ζ\zeta · 정규 공식 ζN\zeta_N · 회귀 기울기 셋을 나란히 놓는다.

# 반사 무작위보행 x ← max(x+ε, 0) 과 벽 없는 y ← y+ε 를 같은 ε 로 함께 쌓는다.
N, T = 20000, 2000
SNAPS = (10, 100, 500, 1000, 2000)
TS = sorted(set(np.unique(np.round(np.logspace(0, np.log10(T), 40))).astype(int).tolist())
            | set(SNAPS))


def walk(u, d, n, horizon, ts):
    lu, ld = log(u), log(d)
    x = np.zeros(n)                        # 벽 있음 — (eq-w19-3)
    y = np.zeros(n)                        # 벽 없음 — (eq-w19-4)
    rec = {}
    for t in range(1, horizon + 1):
        eps = np.where(rng.random(n) < 0.5, lu, ld)
        y = y + eps
        x = np.maximum(x + eps, 0.0)
        if t in ts:
            rec[t] = (x.var(), y.var(), float((x == 0).mean()))
    return x, rec


X, REC = {}, {}
for u, d in [(1.2, 0.8), (1.2, 0.75)]:
    X[(u, d)], REC[(u, d)] = walk(u, d, N, T, set(TS))
    print(f'({u}, {d}) — 마지막 x 의 평균 {X[(u, d)].mean():.4f}, '
          f'최대 {X[(u, d)].max():.3f}, 벽 위 비율 {REC[(u, d)][T][2]:.3f}')
(1.2, 0.8) — 마지막 x 의 평균 0.9229, 최대 11.510, 벽 위 비율 0.138
(1.2, 0.75) — 마지막 x 의 평균 0.4352, 최대 4.590, 벽 위 비율 0.258
# 경험적 생존함수를 정렬로 만들고 x∈[1,5] 구간 최소제곱으로 꼬리 기울기를 잰다.
def survival(x):
    xs = np.sort(x)
    return xs, 1.0 - np.arange(x.size) / x.size


def tail_slope(xs, surv, lo=1.0, hi=5.0):
    m = (xs >= lo) & (xs <= hi) & (surv > 0)
    A = np.column_stack([np.ones(int(m.sum())), xs[m]])
    b = np.linalg.solve(A.T @ A, A.T @ np.log(surv[m]))    # 정규방정식을 직접 푼다
    return b[1], int(m.sum())


SLOPE, WALL = {}, {}
print(f"{'(u, d)':>12}{'-zeta':>9}{'회귀 기울기':>12}{'상대 어긋남':>13}{'벽 비율':>10}{'표본':>8}")
for u, d in [(1.2, 0.8), (1.2, 0.75)]:
    xs, surv = survival(X[(u, d)])
    b, n = tail_slope(xs, surv)
    z = ZETA[(u, d)]
    SLOPE[(u, d)], WALL[(u, d)] = b, REC[(u, d)][T][2]
    print(f'({u}, {d})'.rjust(12) + f'{-z:9.3f}{b:12.3f}{(-b - z) / z:+13.2%}'
          f'{WALL[(u, d)]:10.3f}{n:8d}')
      (u, d)    -zeta      회귀 기울기       상대 어긋남      벽 비율      표본
  (1.2, 0.8)   -1.000      -0.988       -1.24%     0.138    6647
 (1.2, 0.75)   -1.975      -1.949       -1.31%     0.258    2372
# 두 사례의 경험적 생존함수를 로그–로그로 그리고 기울기 -ζ 직선을 겹친다.
fig, ax = plt.subplots(figsize=(6.2, 4.0))
for (u, d), col in [((1.2, 0.8), 'C0'), ((1.2, 0.75), 'C3')]:
    xs, surv = survival(X[(u, d)])
    k = surv > 0
    s = np.exp(xs[k])
    z = ZETA[(u, d)]
    ax.plot(s, surv[k], color=col, lw=1.6,
            label=rf'$(u,d)=({u},{d})$, $\zeta={z:.3f}$')
    anchor = surv[np.searchsorted(xs, 1.0)]                # x=1 에서 경험값에 맞춘다
    line = np.array([np.e, s.max()])
    ax.plot(line, anchor * (line / np.e) ** (-z), color='k', ls=':', lw=1.1)
ax.set_xscale('log')
ax.set_yscale('log')
pow10_axes(ax)
ax.set_xlabel(r'규모 $s/S_{\min}$ (로그)')
ax.set_ylabel(r'$\Pr(S>s)$ (로그)')
ax.set_title(r'점선 기울기 $-\zeta$ 는 $\mathbb{E}[(1+g)^{\zeta}]=1$ 의 근', fontsize=10)
ax.legend(fontsize=9, loc='lower left')
fig.tight_layout()
plt.show()
<Figure size 620x400 with 1 Axes>

예측 2. 같은 난수로 벽 있음·없음 두 벌을 함께 쌓았으므로 T=10,100,500,1000,2000T=10,100,500,1000,2000의 분산을 두 벌에서 그대로 꺼내 비교한다.

# 벽 없음·있음 두 벌의 분산을 T = 10, 100, 500, 1000, 2000 에서 나란히 적는다.
print(' ' * 16 + ''.join(f'{t:>12}' for t in SNAPS))
for u, d in [(1.2, 0.8), (1.2, 0.75)]:
    mu, s2 = cumulants(u, d)
    z = ZETA[(u, d)]
    print(f'(u, d) = ({u}, {d})   σ²_Δ = {s2:.4f}   ζ = {z:.3f}   1/ζ² = {1 / z ** 2:.3f}')
    print('  벽 없음 Var[y]' + ''.join(f'{REC[(u, d)][t][1]:12.2f}' for t in SNAPS))
    print('  이론  T·σ²_Δ ' + ''.join(f'{t * s2:12.2f}' for t in SNAPS))
    print('  벽 있음 Var[x]' + ''.join(f'{REC[(u, d)][t][0]:12.3f}' for t in SNAPS))
    print('  벽 위 비율    ' + ''.join(f'{REC[(u, d)][t][2]:12.3f}' for t in SNAPS))
                          10         100         500        1000        2000
(u, d) = (1.2, 0.8)   σ²_Δ = 0.0411   ζ = 1.000   1/ζ² = 1.000
  벽 없음 Var[y]        0.41        4.09       20.51       40.56       82.01
  이론  T·σ²_Δ         0.41        4.11       20.55       41.10       82.20
  벽 있음 Var[x]       0.107       0.608       0.988       1.014       1.014
  벽 위 비율           0.251       0.148       0.140       0.137       0.138
(u, d) = (1.2, 0.75)   σ²_Δ = 0.0552   ζ = 1.975   1/ζ² = 0.256
  벽 없음 Var[y]        0.56        5.60       27.65       55.37      111.63
  이론  T·σ²_Δ         0.55        5.52       27.61       55.23      110.45
  벽 있음 Var[x]       0.101       0.251       0.252       0.259       0.258
  벽 위 비율           0.314       0.261       0.259       0.265       0.258
# 분산의 시간 경로를 로그–로그로 — 벽 없음은 기울기 1, 벽 있음은 포화.
fig, ax = plt.subplots(figsize=(6.2, 4.0))
for (u, d), col in [((1.2, 0.8), 'C0'), ((1.2, 0.75), 'C3')]:
    z = ZETA[(u, d)]
    ax.plot(TS, [REC[(u, d)][t][1] for t in TS], color=col, lw=1.4, ls='--',
            label=f'벽 없음 $({u},{d})$')
    ax.plot(TS, [REC[(u, d)][t][0] for t in TS], color=col, lw=1.8,
            label=f'벽 있음 $({u},{d})$')
    ax.axhline(1 / z ** 2, color=col, lw=0.9, ls=':')
ax.set_xscale('log')
ax.set_yscale('log')
pow10_axes(ax)
ax.set_xlabel('기간 $T$ (로그)')
ax.set_ylabel(r'$\mathrm{Var}$ (로그)')
ax.set_title(r'벽을 떼면 $T\sigma^{2}_{\Delta}$ 로 선형, 벽을 두면 $1/\zeta^{2}$ 근처에서 포화',
             fontsize=10)
ax.legend(fontsize=8, loc='upper left')
fig.tight_layout()
plt.show()
<Figure size 620x400 with 1 Axes>

예측 3. Simon 규칙을 단위 소유 배열로 구현한다. 길이 tt의 배열에서 균등 난수로 단위 하나를 뽑으면 그 단위의 주인이 뽑히는 확률이 규모에 비례하므로, 그것이 (A6)의 규모 비례 선택이다.

# Simon 규칙을 단위 소유 배열로 쌓는다 — 균등 난수로 단위를 뽑으면 규모 비례 선택이 된다.
def simon(p, t):
    owner = np.empty(t, dtype=np.int64)
    owner[0] = 0
    birth = [0]
    r, pick = rng.random(t), rng.random(t)
    for s in range(1, t):
        if r[s] < p:                       # 확률 p 로 규모 1 의 새 기업
            owner[s] = len(birth)
            birth.append(s)
        else:                              # 확률 1-p 로 뽑힌 단위의 주인에게
            owner[s] = owner[int(pick[s] * s)]
    return np.bincount(owner), np.array(birth)


TSIM = 100_000
SIZES, BIRTH, AGE = {}, {}, {}
for p in (0.1, 0.5):
    SIZES[p], BIRTH[p] = simon(p, TSIM)
    sz, bt = SIZES[p], BIRTH[p]
    m = (bt >= int(TSIM * 0.009)) & (bt < int(TSIM * 0.011))    # t_i ≈ t/100 에 진입
    AGE[p] = sz[m].mean()
    print(f'p = {p}:  기업 수 {sz.size} (평균장 p·t = {p * TSIM:.0f}) · '
          f'규모 1 비율 {np.mean(sz == 1):.3f} (이론 1/(2-p) = {1 / (2 - p):.3f})')
    print(f'          t_i≈t/100 진입 {int(m.sum())}개의 평균 규모 {AGE[p]:.1f} '
          f'(평균장 (t/t_i)^(1-p) = {100 ** (1 - p):.1f})')
p = 0.1:  기업 수 10217 (평균장 p·t = 10000) · 규모 1 비율 0.524 (이론 1/(2-p) = 0.526)
          t_i≈t/100 진입 20개의 평균 규모 58.7 (평균장 (t/t_i)^(1-p) = 63.1)
p = 0.5:  기업 수 50143 (평균장 p·t = 50000) · 규모 1 비율 0.668 (이론 1/(2-p) = 0.667)
          t_i≈t/100 진입 102개의 평균 규모 11.0 (평균장 (t/t_i)^(1-p) = 10.0)
# 정확식 Pr(K≥k) = ζB(k,ζ) 를 math.lgamma 로 만들고 국소 기울기를 잰다.
def yule_ccdf(k, zeta):
    return np.array([zeta * exp(lgamma(kk) + lgamma(zeta) - lgamma(kk + zeta))
                     for kk in np.atleast_1d(k)])


def ccdf_emp(sizes):
    cnt = np.bincount(sizes)[1:]
    return np.arange(1, cnt.size + 1), np.cumsum(cnt[::-1])[::-1] / sizes.size


CC, SL = {}, {}
print(f"{'p':>5}{'zeta':>8}{'k':>6}{'정확식 G':>11}{'경험 G':>11}{'국소 기울기':>13}{'순수 멱':>10}")
for p in (0.1, 0.5):
    z = 1.0 / (1.0 - p)
    CC[p] = ccdf_emp(SIZES[p])
    for k in (5, 50):
        h = 1e-4                           # d ln G / d ln k 를 중심차분으로
        g = yule_ccdf([k - h, k + h], z)
        SL[(p, k)] = k * (log(g[1]) - log(g[0])) / (2 * h)
        print(f'{p:5}{z:8.3f}{k:6d}{yule_ccdf(k, z)[0]:11.4f}{CC[p][1][k - 1]:11.4f}'
              f'{SL[(p, k)]:13.2f}{-z:10.2f}')
    p    zeta     k      정확식 G       경험 G       국소 기울기      순수 멱
  0.1   1.111     5     0.1739     0.1774        -1.10     -1.11
  0.1   1.111    50     0.0136     0.0144        -1.11     -1.11
  0.5   2.000     5     0.0667     0.0665        -1.83     -2.00
  0.5   2.000    50     0.0008     0.0007        -1.98     -2.00
# Simon 규모 분포를 로그–로그로 — 시뮬레이션 점, 정확식 실선, 평균장 순수 멱 점선.
fig, ax = plt.subplots(figsize=(6.2, 4.2))
for p, col in [(0.1, 'C0'), (0.5, 'C3')]:
    z = 1.0 / (1.0 - p)
    ks, tail = CC[p]
    idx = np.unique(np.round(np.logspace(0, np.log10(ks.max()), 50)).astype(int)) - 1
    idx = idx[(idx < ks.size) & (tail[np.minimum(idx, ks.size - 1)] >= 3.0 / SIZES[p].size)]
    ax.plot(ks[idx], tail[idx], 'o', ms=3.5, mfc='none', color=col, label=f'$p={p}$ 시뮬레이션')
    kk = np.arange(1, ks.max() + 1)
    ax.plot(kk, yule_ccdf(kk, z), color=col, lw=1.5,
            label=rf'$p={p}$ 정확식 $\zeta B(k,\zeta)$, $\zeta={z:.2f}$')
    ax.plot(kk, kk ** (-z), color='k', ls=':', lw=1.0)
ax.set_xscale('log')
ax.set_yscale('log')
pow10_axes(ax)
ax.set_xlabel('규모 $k$ (로그)')
ax.set_ylabel(r'$\Pr(K\geq k)$ (로그)')
ax.set_title(r'점선은 평균장 순수 멱 $k^{-\zeta}$ — 지수는 같고 작은 $k$ 의 형태가 다르다',
             fontsize=10)
ax.legend(fontsize=8, loc='lower left')
fig.tight_layout()
plt.show()
<Figure size 620x420 with 1 Axes>
# 대조 표에 옮길 수치를 예측 항목 순서로 모은다.
a, b = (1.2, 0.8), (1.2, 0.75)
print(f'예측 1 — 회귀 기울기 {SLOPE[a]:.3f} · {SLOPE[b]:.3f} '
      f'(이분법 근 {-ZETA[a]:.3f} · {-ZETA[b]:.3f}, 정규 공식 {2 * cumulants(*b)[0] / cumulants(*b)[1]:.3f}) / '
      f'벽 비율 {WALL[a]:.1%} · {WALL[b]:.1%}')
print(f'예측 2 — 벽 없음 Var[y] T=1000 {REC[a][1000][1]:.2f} (이론 {1000 * cumulants(*a)[1]:.2f}), '
      f'T=2000 {REC[a][2000][1]:.2f} / 벽 있음 포화 {REC[a][2000][0]:.3f} · {REC[b][2000][0]:.3f} '
      f'(1/ζ² {1 / ZETA[a] ** 2:.3f} · {1 / ZETA[b] ** 2:.3f})')
print(f'예측 3 — 규모 1 비율 {np.mean(SIZES[0.1] == 1):.3f} · {np.mean(SIZES[0.5] == 1):.3f} / '
      f'국소 기울기 p=0.1 k=50 {SL[(0.1, 50)]:.2f}, p=0.5 k=5 {SL[(0.5, 5)]:.2f}, '
      f'k=50 {SL[(0.5, 50)]:.2f} / t/100 진입 평균 규모 {AGE[0.1]:.1f} · {AGE[0.5]:.1f}')
예측 1 — 회귀 기울기 -0.988 · -1.949 (이분법 근 -1.000 · -1.975, 정규 공식 -1.908) / 벽 비율 13.8% · 25.8%
예측 2 — 벽 없음 Var[y] T=1000 40.56 (이론 41.10), T=2000 82.01 / 벽 있음 포화 1.014 · 0.258 (1/ζ² 1.000 · 0.256)
예측 3 — 규모 1 비율 0.524 · 0.668 / 국소 기울기 p=0.1 k=50 -1.11, p=0.5 k=5 -1.83, k=50 -1.98 / t/100 진입 평균 규모 58.7 · 11.0

3. 대조

예측과 계산이 어긋난 지점을 적는다. 어느 쪽이 틀렸는지 판정한다.

예측결과어긋남원인

본문 확인: 어긋났으면 1번은 (eq-w19-7)·(eq-w19-9)로, 2번은 (eq-w19-4)·(eq-w19-5)로, 3번은 (eq-w19-13)·(eq-w19-14)로 돌아간다.