모비율의 추정
개념
찬성 비율, 지지율처럼 모집단에서 어떤 성질을 가진 비율 \(p\)(모비율)를 추정할 때는, \(n\)명을 조사해 얻은 표본비율 \(\hat{p}\)을 사용합니다. \(n\)이 충분히 크면 모비율 \(p\)의 신뢰구간은 다음과 같습니다.
\[\hat{p} - z\sqrt{\frac{\hat{p}(1-\hat{p})}{n}} \;\le\; p \;\le\; \hat{p} + z\sqrt{\frac{\hat{p}(1-\hat{p})}{n}} \qquad (z = 1.96,\ 2.576)\]
여기서 익힌 오차범위 읽기는 다음 모듈(05 여론조사 읽기)에서 실제 뉴스 기사 해석으로 이어집니다.
\(\pm\) 뒤의 값 \(z\sqrt{\hat{p}(1-\hat{p})/n}\)이 뉴스에서 말하는 오차범위입니다. “지지율 52%, 오차범위 ±3.1%p”라면 진짜 지지율은 48.9%~55.1% 어디쯤이라는 뜻이므로, 구간이 50%를 걸치면 누가 앞선다고 단정할 수 없습니다. 아래 앱에서 \(n\)과 \(\hat{p}\)을 움직여 오차범위가 어떻게 변하는지 확인해 보세요.
만지며 배우기
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 620
# matplotlib·numpy 없이 stdlib + 인라인 SVG 로만 그린다
from math import sqrt
from shiny import App, render, ui
ACCENT = "#2563eb"
INK = "#111827"
MUTED = "#6b7280"
Z = {"95": 1.96, "99": 2.576}
def ci_svg(phat, lo, hi, margin):
W, H, ml, mr, mt, mb = 470, 220, 16, 16, 44, 34
pw, ph = W - ml - mr, H - mt - mb
xlo = max(0.0, phat - 5 * margin)
xhi = min(1.0, phat + 5 * margin)
if xhi <= xlo:
xhi = xlo + 0.01
def X(v):
return ml + ((v - xlo) / (xhi - xlo)) * pw
cy = mt + ph / 2
body = (f'<line x1="{X(lo):.1f}" y1="{cy:.1f}" x2="{X(hi):.1f}" y2="{cy:.1f}" '
f'stroke="{ACCENT}" stroke-width="7" opacity="0.85" stroke-linecap="round"/>'
f'<circle cx="{X(phat):.1f}" cy="{cy:.1f}" r="6" fill="{ACCENT}"/>'
f'<text x="{X(phat):.1f}" y="{cy-16:.1f}" font-size="11" fill="{INK}" '
f'text-anchor="middle">p̂ = {phat*100:.0f}%</text>')
if xlo <= 0.5 <= xhi:
body += (f'<line x1="{X(0.5):.1f}" y1="{mt}" x2="{X(0.5):.1f}" y2="{mt+ph}" '
f'stroke="{INK}" stroke-width="1" stroke-dasharray="5 3"/>'
f'<text x="{X(0.5):.1f}" y="{mt-4:.0f}" font-size="10" fill="{INK}" '
f'text-anchor="middle">50%</text>')
ticks = ""
for i in range(5):
v = xlo + (xhi - xlo) * i / 4
ticks += (f'<text x="{X(v):.1f}" y="{mt+ph+16:.0f}" font-size="9" '
f'fill="{MUTED}" text-anchor="middle">{v*100:.0f}%</text>')
return (f'<svg viewBox="0 0 {W} {H}" style="width:100%;height:auto;'
f'font-family:-apple-system,sans-serif;">'
f'<line x1="{ml}" y1="{mt+ph:.0f}" x2="{W-mr}" y2="{mt+ph:.0f}" stroke="#9ca3af"/>'
f'{body}{ticks}'
f'<text x="{W-mr:.0f}" y="{H-6}" font-size="10" fill="{MUTED}" '
f'text-anchor="end">p</text></svg>')
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_slider("n", "응답자 수 n", min=100, max=3000, value=1000, step=100),
ui.input_slider("phat", "표본비율 p̂", min=0.10, max=0.90, value=0.52, step=0.01),
ui.input_select("level", "신뢰수준", {"95": "95%", "99": "99%"}),
width=240,
),
ui.card(
ui.card_header("모비율 p의 신뢰구간 — 점선은 50% 기준선"),
ui.output_ui("ci_plot"),
),
ui.card(ui.output_ui("readout")),
fillable=True,
)
def server(input, output, session):
@render.ui
def ci_plot():
n, phat = input.n(), input.phat()
margin = Z[input.level()] * sqrt(phat * (1 - phat) / n)
return ui.HTML(ci_svg(phat, phat - margin, phat + margin, margin))
@render.ui
def readout():
n, phat = input.n(), input.phat()
level = input.level()
z = Z[level]
margin = z * sqrt(phat * (1 - phat) / n)
lo, hi = phat - margin, phat + margin
if lo <= 0.5 <= hi:
verdict = ("구간이 50%를 걸치므로 절반보다 많다(우세)고 "
"단정할 수 없습니다.")
elif lo > 0.5:
verdict = "구간 전체가 50%보다 위에 있어 절반을 넘는다고 볼 수 있습니다."
else:
verdict = "구간 전체가 50%보다 아래에 있어 절반에 못 미친다고 볼 수 있습니다."
return ui.HTML(
f'<p class="hs-readline">'
f"표본비율 {phat * 100:.1f}%, {level}% 신뢰구간 "
f"[{lo * 100:.1f}%, {hi * 100:.1f}%] — 오차범위 ±{margin * 100:.1f}%p. "
f"n을 4배로 늘리면 오차범위는 절반이 됩니다. {verdict}</p>"
)
app = App(app_ui, server)
여론조사 기사의 오차범위는 이 한 줄 계산에서 나옵니다.
from math import sqrt
n, phat, z = 1000, 0.52, 1.96 # 응답자 1000명, 표본비율 52%, 95%
margin = z * sqrt(phat * (1 - phat) / n) # 오차범위 ≈ 0.031 (±3.1%p)
lo, hi = phat - margin, phat + margin
print(f"{lo:.3f} ~ {hi:.3f}") # 0.489 ~ 0.551 → 50%를 걸친다스스로 확인
오차범위가 약 \(\pm 3.1\)%p라 구간 \([48.9\%, 55.1\%]\)이 50%를 걸치기 때문입니다. 앱에서 \(n\)을 키워 보면 \(n = 2500\) 근처부터 구간의 아래끝이 50%를 넘어섭니다(\(\pm 2.0\)%p 이하). 오차범위는 \(\sqrt{n}\)에 반비례하므로 \(n\)을 4배로 하면 절반이 됩니다.
\(\hat{p} = 0.5\)에서 가장 큽니다. \(\hat{p}(1-\hat{p})\)이 \(0.5 \times 0.5 = 0.25\)로 최대가 되기 때문입니다. 그래서 여론조사 기관은 안전하게 \(\hat{p} = 0.5\)를 가정한 최대 오차범위를 함께 표기하곤 합니다.