중심극한정리 문서의 그림 생성 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

# 중심극한정리 문서의 그림을 만든다: 22_clt_fig1.svg, 22_clt_fig2.svg
# 실행: ~/.venvs/vault-plots/bin/python 22_clt_plot.py  (matplotlib, numpy 필요)
import math
import os

import matplotlib
matplotlib.use("svg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import font_manager

HERE = os.path.dirname(os.path.abspath(__file__))
PREFIX = "22_clt"
INK = "#888888"                                         # 밝은 테마와 어두운 테마 모두에서 보이는 회색
C = ["#3b82c4", "#d9622b", "#3a9a5b", "#9b59b6", "#c49a1b"]
_fonts = {f.name for f in font_manager.fontManager.ttflist}
FONT = next((n for n in ["Apple SD Gothic Neo", "NanumGothic", "Noto Sans CJK KR"] if n in _fonts), "DejaVu Sans")
plt.rcParams.update({
    "font.family": FONT, "mathtext.fontset": "dejavusans",
    "font.size": 11, "axes.unicode_minus": False, "svg.fonttype": "path", "svg.hashsalt": PREFIX,
    "axes.edgecolor": INK, "axes.labelcolor": INK, "xtick.color": INK, "ytick.color": INK,
    "text.color": INK, "legend.frameon": False,
    "axes.facecolor": "none", "figure.facecolor": "none", "savefig.transparent": True,
    "axes.spines.top": False, "axes.spines.right": False,
})


def save(fig, k):
    fig.savefig(os.path.join(HERE, f"{PREFIX}_fig{k}.svg"), bbox_inches="tight", metadata={"Date": None})
    plt.close(fig)


def Phi(z):
    return 0.5 * (1 + math.erf(z / math.sqrt(2)))


def dice_sum_pmf(n):
    """주사위 n개 합의 정확한 분포 (합성곱 n번)."""
    p = np.array([1.0])
    die = np.ones(6) / 6
    for _ in range(n):
        p = np.convolve(p, die)
    return np.arange(n, 6 * n + 1), p


def npdf(z):
    return np.exp(-z * z / 2) / math.sqrt(2 * math.pi)


def max_cdf_gap(n):
    s, p = dice_sum_pmf(n)
    mu, sd = 3.5 * n, math.sqrt(35 / 12 * n)
    F = np.cumsum(p)
    return max(abs(F[i] - Phi((s[i] + 0.5 - mu) / sd)) for i in range(len(s)))   # 연속성 보정한 점에서 비교


# 그림 1: 주사위 n개 합을 표준화한 분포(막대 넓이 = 확률)와 표준정규 밀도
fig, axes = plt.subplots(1, 3, figsize=(6.5, 2.4), sharey=True)
z = np.linspace(-3.5, 3.5, 400)
for ax, n in zip(axes, [1, 2, 10]):
    s, p = dice_sum_pmf(n)
    sd = math.sqrt(35 / 12 * n)
    ax.bar((s - 3.5 * n) / sd, p * sd, width=0.85 / sd, color=C[0], alpha=0.75)
    ax.plot(z, npdf(z), color=C[1], lw=1.6)
    ax.set_title(f"주사위 {n}개", fontsize=10)
    ax.set_xticks([-3, 0, 3])
    ax.set_xlim(-3.5, 3.5)
axes[0].set_ylabel("밀도")
axes[1].set_xlabel("표준화한 합 $Z_n$")
fig.tight_layout()
save(fig, 1)

# 그림 2: 지수분포 n개 평균(감마분포)은 치우침이 천천히 사라진다
def gamma_std_pdf(zv, n):
    """Exp(1) n개 평균을 표준화한 Z의 밀도. 평균 = Gamma(n, n)."""
    x = 1 + zv / math.sqrt(n)
    out = np.zeros_like(zv)
    ok = x > 0
    logf = n * math.log(n) + (n - 1) * np.log(x[ok]) - n * x[ok] - math.lgamma(n)
    out[ok] = np.exp(logf) / math.sqrt(n)
    return out


def gamma_upper_tail(n, z):
    """P(Z > z)를 수치 적분으로."""
    g = np.linspace(z, 40, 400001)
    d = gamma_std_pdf(g, n)
    return float(np.sum((d[:-1] + d[1:]) / 2 * np.diff(g)))


fig, ax = plt.subplots(figsize=(6, 3.6))
z = np.linspace(-3.5, 4, 600)
ax.plot(z, npdf(z), color=INK, lw=2.6, label="표준정규")
for i, n in enumerate([1, 5, 50]):
    ax.plot(z, gamma_std_pdf(z, n), color=C[i], lw=1.6, label=f"지수분포 {n}개 평균")
ax.set_ylim(0, 0.62)
ax.set_xlabel("표준화한 평균")
ax.set_ylabel("밀도")
ax.legend(loc="upper right")
save(fig, 2)

if __name__ == "__main__":
    table = {1: 0.054, 2: 0.016, 10: 0.0026, 100: 0.0003}
    for n, v in table.items():
        assert abs(max_cdf_gap(n) - v) <= 0.5 * 10 ** (-len(str(v).split(".")[1])) + 1e-12, (n, max_cdf_gap(n))
    tails = [gamma_upper_tail(n, 2) for n in (5, 50, 500)]
    assert [round(t, 3) for t in tails] == [0.041, 0.030, 0.025]
    print("ALL CHECKS PASSED")