표본추출

단원 04 통계적 추정 · 「확률과 통계」 · 1차시(45분)

개념

조사 대상 전체(모집단)를 빠짐없이 조사하는 것이 전수조사, 그 일부(표본)만 뽑아 조사하는 것이 표본조사입니다. 전교생 400명의 평균 키를 알고 싶을 때, 표본 \(n\)명을 잘 뽑으면 표본평균 \(\bar{x}\)로 모평균 \(m\)을 추정할 수 있습니다.

\[\text{모집단(크기 } N=400\text{)} \;\longrightarrow\; \text{표본(크기 } n\text{)} \;\longrightarrow\; \bar{x} \approx m\]

급식 만족도 조사를 한 반에서만 하면 안 되는 이유 — 그 반의 특성(급식실과의 거리, 학년 분위기 등)이 결과를 치우치게 하기 때문입니다. 표본은 모집단 전체를 골고루 대표해야 합니다.

표본을 뽑는 대표적인 방법은 세 가지입니다. 단순임의추출은 제비뽑기처럼 모두에게 같은 확률을 주고, 계통추출은 명부에서 일정한 간격으로 뽑으며, 층화추출은 남/녀처럼 집단을 층으로 나눈 뒤 각 층에서 비례하여 뽑습니다. 아래 앱에서 방법을 바꿔 가며 어떤 점들이 뽑히는지, 표본평균이 모평균에서 얼마나 벗어나는지 관찰해 보세요.

만지며 배우기

#| '!! 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"

# 모집단: 전교생 400명의 키 (고정된 시드 → 항상 같은 학교)
random.seed(42)
def _clip(v, lo, hi):
    return max(lo, min(hi, v))
male = [_clip(random.gauss(173, 6), 145, 195) for _ in range(200)]    # 앞 200명: 남
female = [_clip(random.gauss(161, 5), 145, 195) for _ in range(200)]  # 뒤 200명: 여
POP = male + female
POP_MEAN = sum(POP) / len(POP)

METHOD_NOTE = {
    "srs": "단순임의추출은 모두에게 같은 확률을 주는 제비뽑기입니다. 어디에도 치우치지 않지만, 우연히 한쪽 층이 많이 뽑힐 수도 있습니다.",
    "sys": "계통추출은 명부에서 일정 간격으로 뽑아 빠르고 간편합니다. 다만 명부에 규칙적인 배열이 있으면 치우칠 수 있습니다.",
    "strat": "층화추출은 남녀 비율(200:200)을 표본에서도 그대로 유지하므로, 남녀 키 차이 때문에 생기는 치우침이 적습니다.",
}

def grid_svg(idx):
    idxs = set(idx)
    cell, ox, oy = 16, 26, 8
    body = ""
    for i in range(400):
        c, r = i % 20, i // 20
        x, y = ox + c * cell, oy + r * cell
        if i in idxs:
            body += f'<circle cx="{x}" cy="{y}" r="4.5" fill="{ACCENT}"/>'
        else:
            body += f'<circle cx="{x}" cy="{y}" r="2.4" fill="#d1d5db"/>'
    ydiv = oy + 10 * cell - cell / 2
    body += (f'<line x1="{ox-6}" y1="{ydiv}" x2="{ox+19*cell+6}" y2="{ydiv}" '
             f'stroke="#9ca3af" stroke-width="0.8" stroke-dasharray="2 3"/>'
             f'<text x="12" y="{oy+4.5*cell:.0f}" font-size="12" fill="{MUTED}" '
             f'text-anchor="middle">남</text>'
             f'<text x="12" y="{oy+14.5*cell:.0f}" font-size="12" fill="{MUTED}" '
             f'text-anchor="middle">여</text>')
    W, H = ox + 20 * cell + 6, oy + 20 * cell
    return (f'<svg viewBox="0 0 {W} {H}" style="width:100%;max-height:340px;'
            f'height:auto;font-family:-apple-system,sans-serif;">{body}</svg>')

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_select(
            "method", "추출 방법",
            {"srs": "단순임의추출", "sys": "계통추출", "strat": "층화추출(남/녀)"},
        ),
        ui.input_slider("n", "표본 크기 n", min=10, max=100, value=40, step=10),
        ui.input_action_button("redraw", "다시 추출"),
        width=240,
    ),
    ui.card(
        ui.card_header("전교생 400명(회색) 중 이번에 뽑힌 표본(파랑)"),
        ui.output_ui("grid_plot"),
    ),
    ui.card(ui.output_ui("readout")),
    fillable=True,
)

def server(input, output, session):
    @reactive.calc
    def sample_idx():
        input.redraw()  # 버튼을 누르면 새로 추출
        n = input.n()
        method = input.method()
        if method == "srs":
            return random.sample(range(400), n)
        if method == "sys":
            k = 400 // n
            start = random.randrange(k)
            return list(range(start, 400, k))[:n]
        # 층화추출: 남녀에서 절반씩
        half = n // 2
        idx_m = random.sample(range(200), half)
        idx_f = [200 + i for i in random.sample(range(200), n - half)]
        return idx_m + idx_f

    @render.ui
    def grid_plot():
        return ui.HTML(grid_svg(sample_idx()))

    @render.ui
    def readout():
        idx = sample_idx()
        xbar = sum(POP[i] for i in idx) / len(idx)
        return ui.HTML(
            f'<p class="hs-readline">'
            f"모평균 m = {POP_MEAN:.2f} cm · 이번 표본평균 x̄ = {xbar:.2f} cm · "
            f"차이 = {abs(POP_MEAN - xbar):.2f} cm. "
            f"{METHOD_NOTE[input.method()]}</p>"
        )

app = App(app_ui, server)

세 가지 추출 방법의 핵심은 인덱스를 고르는 방식의 차이입니다.

import random

srs = random.sample(range(400), 40)              # 단순임의추출

k = 400 // 40                                     # 계통추출: 간격 10
sys_ = list(range(random.randrange(k), 400, k))

boys = random.sample(range(200), 20)             # 층화추출: 남 20 + 여 20
girls = [200 + i for i in random.sample(range(200), 20)]
strat = boys + girls

스스로 확인

어떤 400명 중 누가 뽑히느냐가 매번 달라지기 때문입니다. 표본평균 \(\bar{x}\)는 표본에 따라 값이 변하는 확률변수입니다 — 이 사실이 다음 모듈 “표본평균의 분포”의 출발점입니다.

단순임의추출은 우연히 남학생(또는 여학생)만 많이 뽑힐 수 있고, 남녀 키 차이가 약 12 cm이므로 표본평균이 크게 치우칩니다. 층화추출은 남녀를 10명씩 강제로 나누어 뽑아 이런 치우침을 원천 차단합니다. 앱에서 두 방법의 “차이” 값을 여러 번 비교해 보세요.