확률변수의 평균과 표준편차
개념
어떤 시행의 결과에 따라 값이 정해지는 변수를 확률변수 \(X\)라 하고, \(X\)가 가질 수 있는 각 값에 확률을 대응시킨 것을 확률분포라고 합니다. 확률변수 \(X\)의 기댓값(평균)은 각 값에 그 확률을 곱해 모두 더한 값입니다.
\[E(X) = x_1 p_1 + x_2 p_2 + \cdots + x_n p_n\]
복권 한 장의 “평균 당첨금”이 바로 기댓값입니다. 아래 앱에서 복권 시나리오를 골라 기댓값과 복권 가격을 비교해 보세요.
기댓값이 분포의 “중심”이라면, 값들이 그 중심에서 얼마나 흩어져 있는지를 재는 것이 분산 \(V(X)\)와 표준편차 \(\sigma(X)\)입니다. 분산은 편차 제곱의 기댓값이고, 평균을 \(m = E(X)\)라 할 때 계산은 \(E(X^2)\)에서 평균의 제곱을 빼는 편이 간단합니다.
\[V(X) = E\big((X-m)^2\big) = E(X^2) - \{E(X)\}^2, \qquad \sigma(X) = \sqrt{V(X)}\]
같은 기댓값을 가져도 분산이 크면 값이 넓게 흩어지고, 작으면 평균 가까이 몰립니다. 시행을 많이 반복하면 관측값들의 평균은 \(E(X)\)에, 관측값들의 표준편차는 \(\sigma(X)\)에 가까워집니다. 아래 앱에서 시나리오와 시행 횟수를 바꿔 가며 이론값(기댓값·표준편차)과 실제 표본에서 계산한 값이 가까워지는 모습을 확인해 보세요.
만지며 배우기
#| '!! 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 로만 그린다
import random
from shiny import App, reactive, render, ui
ACCENT = "#2563eb"
INK = "#111827"
MUTED = "#6b7280"
GRID = "#e5e7eb"
SCENARIOS = {
"die": {"name": "주사위 한 개의 눈",
"x": [1, 2, 3, 4, 5, 6],
"p": [1/6] * 6},
"coins": {"name": "동전 두 개의 앞면 수",
"x": [0, 1, 2],
"p": [0.25, 0.5, 0.25]},
"lotto": {"name": "복권 한 장의 당첨금(원)",
"x": [0, 1000, 10000, 1000000],
"p": [0.899, 0.09, 0.01, 0.001]},
}
def dist_svg(xs, probs, freqs):
W, H, ml, mr, mt, mb = 470, 320, 44, 12, 28, 40
pw, ph = W - ml - mr, H - mt - mb
nb = len(xs)
gw = pw / nb
ymax = max(max(probs), max(freqs), 1e-4)
grid = ""
for t in range(5):
v = ymax * t / 4
yy = mt + ph - (v / ymax) * ph
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:.2f}</text>')
bars = ""
bw = gw * 0.30
for i in range(nb):
gx = ml + i * gw
hT = probs[i] / ymax * ph
hE = freqs[i] / ymax * ph
xT, xE = gx + gw * 0.18, gx + gw * 0.52
bars += (f'<rect x="{xT:.1f}" y="{mt+ph-hT:.1f}" width="{bw:.1f}" '
f'height="{hT:.1f}" fill="none" stroke="{INK}" stroke-width="1.2"/>'
f'<rect x="{xE:.1f}" y="{mt+ph-hE:.1f}" width="{bw:.1f}" '
f'height="{hE:.1f}" fill="{ACCENT}" opacity="0.85"/>'
f'<text x="{gx+gw/2:.1f}" y="{mt+ph+13:.1f}" font-size="8.5" '
f'fill="{MUTED}" text-anchor="middle">{xs[i]:,}</text>')
leg = (f'<rect x="{ml}" y="4" width="12" height="12" fill="none" '
f'stroke="{INK}" stroke-width="1.2"/>'
f'<text x="{ml+16}" y="14" font-size="10" fill="{MUTED}">P(X=x)</text>'
f'<rect x="{ml+90}" y="4" width="12" height="12" fill="{ACCENT}" opacity="0.85"/>'
f'<text x="{ml+106}" y="14" font-size="10" fill="{MUTED}">상대도수</text>')
return (f'<svg viewBox="0 0 {W} {H}" style="width:100%;height:auto;'
f'font-family:-apple-system,sans-serif;">{grid}{bars}{leg}'
f'<text x="{ml+pw/2:.0f}" y="{H-4}" font-size="10" fill="{MUTED}" '
f'text-anchor="middle">X 의 값</text></svg>')
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_select(
"scenario", "확률변수 X",
{k: v["name"] for k, v in SCENARIOS.items()},
),
ui.input_slider("n", "시행 횟수 n", min=10, max=5000, value=100, step=10),
ui.input_action_button("redraw", "다시 추첨"),
width=240,
),
ui.card(
ui.card_header("확률분포(테두리) vs 실제 시행의 상대도수(파랑)"),
ui.output_ui("dist_plot"),
),
ui.card(ui.output_ui("readout")),
fillable=True,
)
def server(input, output, session):
@reactive.calc
def draws():
input.redraw() # 버튼을 누르면 새로 추첨
sc = SCENARIOS[input.scenario()]
return random.choices(sc["x"], weights=sc["p"], k=input.n())
@render.ui
def dist_plot():
sc = SCENARIOS[input.scenario()]
vals = draws()
n = len(vals)
freq = [vals.count(v) / n for v in sc["x"]]
return ui.HTML(dist_svg(sc["x"], sc["p"], freq))
@render.ui
def readout():
sc = SCENARIOS[input.scenario()]
xs, ps = sc["x"], sc["p"]
e = sum(x * p for x, p in zip(xs, ps))
ex2 = sum(x * x * p for x, p in zip(xs, ps))
var = ex2 - e * e
sd = var ** 0.5
vals = draws()
n = len(vals)
m = sum(vals) / n
ssd = (sum((v - m) ** 2 for v in vals) / n) ** 0.5
line = (f"이론값 — 기댓값 E(X) = {e:,.1f} · 표준편차 σ(X) = {sd:,.1f}<br>"
f"표본({input.n():,}번) — 표본평균 = {m:,.1f} · 표본표준편차 = {ssd:,.1f}")
extra = ""
if input.scenario() == "lotto":
extra = (" — 복권 기댓값은 약 1,190원이지만 표준편차는 3만 원이 넘습니다. "
"드문 고액 당첨 때문에 흩어짐이 매우 큰 분포입니다.")
return ui.HTML(f'<p class="hs-readline">{line}{extra}</p>')
app = App(app_ui, server)
앱의 핵심 계산은 이 몇 줄이 전부입니다.
x = [0, 1000, 10000, 1000000] # 복권 당첨금
p = [0.899, 0.09, 0.01, 0.001] # 각 확률
E = sum(xi * pi for xi, pi in zip(x, p)) # 기댓값 E(X) = 1190.0
EX2 = sum(xi * xi * pi for xi, pi in zip(x, p)) # E(X²)
V = EX2 - E ** 2 # 분산 V(X)
print(E, V, V ** 0.5) # 기댓값, 분산, 표준편차스스로 확인
표본평균이 기댓값 \(E(X)\)에 점점 가까워집니다. 시행 횟수가 적을 때는 추첨할 때마다 표본평균이 크게 출렁이지만, 횟수가 커지면 안정됩니다. (대수의 법칙 — 04단원 표본평균의 분포에서 다시 만납니다.)
당첨금 1,000,000원의 확률이 0.001이라 100번 추첨에는 대개 한 번도 나오지 않다가, 어쩌다 한 번 나오면 표본평균이 크게 뜁니다. 드문 값이 평균에 미치는 영향을 눈으로 확인해 보세요. 이 “잘 튀는 정도”를 수로 나타낸 것이 바로 큰 표준편차입니다.
\(E(X) = 3.5\), \(E(X^2) = \dfrac{1+4+9+16+25+36}{6} = \dfrac{91}{6} \approx 15.17\)이므로 \(V(X) = 15.17 - 3.5^2 = 2.92\), \(\sigma(X) = \sqrt{2.92} \approx 1.71\)입니다. 분산은 “편차 제곱의 평균”이므로, \(E(X^2)\)에서 평균의 제곱 \(\{E(X)\}^2\)을 빼는 공식이 계산에 편리합니다. 앱에서 주사위를 골라 이론값과 표본표준편차를 비교해 보세요.