이항분포

단원 03 확률분포 · 「확률과 통계」 · 1차시(45분)

개념

한 번의 시행에서 어떤 사건이 일어날 확률이 \(p\)로 일정할 때, 독립적으로 \(n\)번 반복한 시행에서 그 사건이 일어나는 횟수 \(X\)의 분포를 이항분포 \(B(n,\,p)\)라고 합니다.

\[P(X = k) = \binom{n}{k} p^k (1-p)^{n-k}, \qquad E(X) = np, \qquad \sigma = \sqrt{np(1-p)}\]

자유투 성공률이 \(p\)인 선수가 \(n\)번 던질 때의 성공 횟수, \(n\)문항 오지선다를 전부 찍었을 때의 정답 개수가 모두 이항분포입니다.

\(n\)이 충분히 크면 이항분포는 정규분포 \(N(np,\; np(1-p))\)에 가까워집니다. 아래 앱에서 \(n\)을 키우며 막대들이 종 모양 곡선에 붙는 모습을 확인해 보세요.

만지며 배우기

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 640

# matplotlib·numpy 없이 stdlib + 인라인 SVG 로만 그린다
from math import comb, sqrt, floor, ceil, exp, pi
from shiny import App, render, ui

ACCENT = "#2563eb"
INK = "#111827"
MUTED = "#6b7280"
GRID = "#e5e7eb"

def pmf(n, p):
    return [comb(n, k) * p**k * (1 - p)**(n - k) for k in range(n + 1)]

def pmf_svg(n, p, probs, mean, sd, approx):
    lo = max(0, floor(mean - 4 * sd))
    hi = min(n, ceil(mean + 4 * sd))
    W, H, ml, mr, mt, mb = 470, 320, 50, 12, 16, 34
    pw, ph = W - ml - mr, H - mt - mb
    span = max(1, hi - lo)
    def X(k):
        return ml + ((k - lo) / span) * pw
    window = probs[lo:hi + 1]
    ymax = max(window)
    peak = 1.0 / (sd * sqrt(2 * pi)) if sd > 0 else ymax
    if approx:
        ymax = max(ymax, peak)
    def Y(v):
        return mt + ph - (v / ymax) * ph
    grid = ""
    for t in range(5):
        v = ymax * t / 4
        yy = Y(v)
        grid += (f'<line x1="{ml}" y1="{yy:.1f}" x2="{W-mr}" y2="{yy:.1f}" stroke="{GRID}"/>'
                 f'<text x="{ml-6}" y="{yy+3:.1f}" font-size="9" fill="{MUTED}" '
                 f'text-anchor="end">{v:.3f}</text>')
    bw = pw / (span + 1) * 0.8
    bars = ""
    tick = max(1, span // 10)
    for k in range(lo, hi + 1):
        h = Y(0) - Y(probs[k])
        bars += (f'<rect x="{X(k)-bw/2:.1f}" y="{Y(probs[k]):.1f}" width="{bw:.1f}" '
                 f'height="{h:.1f}" fill="{ACCENT}" opacity="0.85"/>')
        if (k - lo) % tick == 0:
            bars += (f'<text x="{X(k):.1f}" y="{mt+ph+14:.1f}" font-size="9" '
                     f'fill="{MUTED}" text-anchor="middle">{k}</text>')
    mline = (f'<line x1="{X(mean):.1f}" y1="{mt}" x2="{X(mean):.1f}" y2="{mt+ph}" '
             f'stroke="{INK}" stroke-width="0.9" stroke-dasharray="2 3"/>'
             f'<text x="{X(mean)+3:.1f}" y="{mt+10}" font-size="9" fill="{INK}">E(X)=np</text>')
    curve = ""
    if approx and sd > 0:
        pts = []
        for i in range(121):
            xv = lo - 0.5 + (hi + 0.5 - (lo - 0.5)) * i / 120
            yv = exp(-((xv - mean) ** 2) / (2 * sd * sd)) / (sd * sqrt(2 * pi))
            pts.append((X(xv), Y(yv)))
        curve = ('<path d="M' + " L".join(f"{x:.1f},{y:.1f}" for x, y in pts)
                 + f'" fill="none" stroke="{INK}" stroke-width="1.5"/>')
    return (f'<svg viewBox="0 0 {W} {H}" style="width:100%;height:auto;'
            f'font-family:-apple-system,sans-serif;">{grid}{bars}{mline}{curve}'
            f'<text x="{ml+pw/2:.0f}" y="{H-4}" font-size="10" fill="{MUTED}" '
            f'text-anchor="middle">k</text>'
            f'<text x="6" y="{mt+8}" font-size="10" fill="{MUTED}">P(X=k)</text></svg>')

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_slider("n", "시행 횟수 n", min=1, max=80, value=20, step=1),
        ui.input_slider("p", "확률 p", min=0.05, max=0.95, value=0.5, step=0.05),
        ui.input_checkbox("approx", "정규분포 근사 곡선 겹쳐 보기", False),
        width=240,
    ),
    ui.card(
        ui.card_header("이항분포 B(n, p)"),
        ui.output_ui("pmf_plot"),
    ),
    ui.card(ui.output_ui("readout")),
    fillable=True,
)

def server(input, output, session):
    @render.ui
    def pmf_plot():
        n, p = input.n(), input.p()
        probs = pmf(n, p)
        mean, sd = n * p, sqrt(n * p * (1 - p))
        return ui.HTML(pmf_svg(n, p, probs, mean, sd, input.approx()))

    @render.ui
    def readout():
        n, p = input.n(), input.p()
        mean, sd = n * p, sqrt(n * p * (1 - p))
        probs = pmf(n, p)
        within = sum(probs[max(0, ceil(mean - sd)):floor(mean + sd) + 1])
        return ui.HTML(
            f'<p class="hs-readline">'
            f"E(X) = np = {mean:.1f} · σ = √np(1−p) = {sd:.2f} — "
            f"X가 평균 ± σ 안에 있을 확률은 약 {within:.2f}입니다. "
            f"n을 키우면 막대가 종 모양에 가까워집니다.</p>"
        )

app = App(app_ui, server)
from math import comb, sqrt

n, p = 20, 0.5

# P(X = k), k = 0..n
probs = [comb(n, k) * p**k * (1 - p)**(n - k) for k in range(n + 1)]

E = n * p                    # 기댓값
sd = sqrt(n * p * (1 - p))   # 표준편차
print(E, sd, sum(probs))     # 확률의 합은 항상 1

스스로 확인

\(p = 0.5\)이면 평균 \(np\)를 중심으로 좌우 대칭이고, \(p = 0.1\)이면 왼쪽으로 치우친 비대칭이 됩니다. 다만 \(n\)을 충분히 키우면 어느 쪽이든 다시 종 모양에 가까워집니다.

경험적으로 \(np \ge 5\), \(n(1-p) \ge 5\) 정도면 근사가 쓸 만합니다. \(p\)가 0이나 1에 가까울수록 더 큰 \(n\)이 필요합니다 — 앱에서 \(p = 0.05\)로 두고 \(n\)을 움직이며 확인해 보세요.