정규분포와 표준화
개념
키, 시험 점수처럼 평균 근처에 자료가 몰리고 양쪽으로 대칭인 분포를 정규분포 \(N(m,\,\sigma^2)\)로 나타냅니다. 곡선의 위치는 평균 \(m\)이, 퍼진 정도는 표준편차 \(\sigma\)가 정합니다. 곡선 아래 구간의 넓이가 곧 확률입니다.
\[Z = \frac{X - m}{\sigma} \;\sim\; N(0,\,1)\]
수능 표준점수가 바로 표준화입니다. 과목마다 \(m\)과 \(\sigma\)가 달라도 \(Z\)로 바꾸면 같은 자로 잴 수 있습니다.
어떤 정규분포든 표준화하면 표준정규분포 \(N(0, 1)\)이 되므로, 확률 계산은 표준정규분포표 하나로 충분합니다. 아래 앱에서 \(m\), \(\sigma\)를 움직여 곡선의 변화를 보고, 구간의 넓이가 표준화된 \(Z\) 구간의 넓이와 같음을 확인해 보세요.
만지며 배우기
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 660
# matplotlib·numpy 없이 stdlib + 인라인 SVG 로만 그린다
from math import erf, sqrt, exp, pi
from shiny import App, render, ui
ACCENT = "#2563eb"
INK = "#111827"
MUTED = "#6b7280"
def pdf(x, m, s):
return exp(-((x - m) ** 2) / (2 * s * s)) / (s * sqrt(2 * pi))
def cdf(x, m, s):
return 0.5 * (1 + erf((x - m) / (s * sqrt(2))))
def panel(m, s, a, b, title, xlab):
W, H, ml, mr, mt, mb = 240, 300, 12, 12, 26, 28
pw, ph = W - ml - mr, H - mt - mb
x0, x1 = m - 4 * s, m + 4 * s
def X(v):
return ml + ((v - x0) / (x1 - x0)) * pw
ymax = pdf(m, m, s) * 1.12
def Y(v):
return mt + ph - (v / ymax) * ph
def sample(u0, u1, k):
return [u0 + (u1 - u0) * i / k for i in range(k + 1)]
curve = ('M' + " L".join(f"{X(x):.1f},{Y(pdf(x,m,s)):.1f}"
for x in sample(x0, x1, 200)))
aa, bb = max(a, x0), min(b, x1)
fill = ""
if bb > aa:
seg = sample(aa, bb, 120)
d = (f"M{X(aa):.1f},{Y(0):.1f} "
+ " ".join(f"L{X(x):.1f},{Y(pdf(x,m,s)):.1f}" for x in seg)
+ f" L{X(bb):.1f},{Y(0):.1f} Z")
fill = f'<path d="{d}" fill="{ACCENT}" opacity="0.18"/>'
return (
f'<svg viewBox="0 0 {W} {H}" style="width:100%;height:auto;'
'font-family:-apple-system,sans-serif;">'
f'<line x1="{ml}" y1="{Y(0):.1f}" x2="{W-mr}" y2="{Y(0):.1f}" stroke="#9ca3af"/>'
f'<line x1="{X(m):.1f}" y1="{mt}" x2="{X(m):.1f}" y2="{Y(0):.1f}" '
f'stroke="#9ca3af" stroke-width="0.7" stroke-dasharray="2 3"/>'
f'{fill}<path d="{curve}" fill="none" stroke="{INK}" stroke-width="1.5"/>'
f'<text x="{W/2:.0f}" y="16" font-size="11" fill="{INK}" '
f'text-anchor="middle">{title}</text>'
f'<text x="{W-mr:.0f}" y="{Y(0)+16:.0f}" font-size="10" fill="{MUTED}" '
f'text-anchor="end">{xlab}</text>'
'</svg>'
)
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_slider("m", "평균 m", min=40, max=100, value=70, step=1),
ui.input_slider("s", "표준편차 σ", min=2, max=20, value=10, step=1),
ui.input_slider("ab", "구간 [a, b]", min=20, max=120, value=(65, 75), step=1),
ui.input_checkbox("show_z", "표준정규분포 Z 겹쳐 보기", False),
width=240,
),
ui.card(
ui.card_header("정규분포 곡선 — 색칠한 넓이가 곧 확률"),
ui.output_ui("curve"),
),
ui.card(ui.output_ui("readout")),
fillable=True,
)
def server(input, output, session):
@render.ui
def curve():
m, s = input.m(), input.s()
a, b = input.ab()
left = panel(m, s, a, b, f"N({m}, {s}²)", "x")
if input.show_z():
za, zb = (a - m) / s, (b - m) / s
right = panel(0, 1, za, zb, "N(0, 1)", "z")
return ui.HTML(
'<div style="display:flex; gap:10px;">'
f'<div style="flex:1;">{left}</div>'
f'<div style="flex:1;">{right}</div></div>'
)
return ui.HTML(left)
@render.ui
def readout():
m, s = input.m(), input.s()
a, b = input.ab()
p = cdf(b, m, s) - cdf(a, m, s)
za, zb = (a - m) / s, (b - m) / s
return ui.HTML(
f'<p class="hs-readline">'
f"P({a} ≤ X ≤ {b}) = {p:.4f} — 표준화하면 "
f"P({za:.2f} ≤ Z ≤ {zb:.2f})와 같습니다. "
f"m과 σ를 바꿔도 z가 같으면 확률(넓이)은 변하지 않습니다.</p>"
)
app = App(app_ui, server)
정규분포 확률은 표준화와 표준정규분포의 누적확률로 계산합니다.
from math import erf, sqrt
def cdf(z): # 표준정규분포 P(Z ≤ z)
return 0.5 * (1 + erf(z / sqrt(2)))
m, s = 70, 10 # X ~ N(70, 10²)
a, b = 65, 75
za, zb = (a - m) / s, (b - m) / s # 표준화
print(cdf(zb) - cdf(za)) # P(65 ≤ X ≤ 75) = P(-0.5 ≤ Z ≤ 0.5)스스로 확인
힌트문제 1 — σ를 두 배로 하면 곡선은 어떻게 변하는가?
곡선이 옆으로 두 배 퍼지면서 봉우리는 절반 높이로 낮아집니다. 전체 넓이가 항상 1이어야 하기 때문입니다. 같은 구간 \([a, b]\)의 확률(넓이)이 어떻게 변하는지도 함께 관찰해 보세요.
힌트문제 2 — m = 70, σ = 10일 때 P(60 ≤ X ≤ 80)은? 표준화하면 어떤 Z 구간인가?
\(Z = (X - 70)/10\)이므로 \(P(-1 \le Z \le 1) \approx 0.6827\) — “평균 ± 1σ 안에 약 68%”라는 정규분포의 기본 성질입니다. 앱에서 구간을 \([60, 80]\)으로 맞춰 확인해 보세요.