중심극한정리

Central Limit Theorem

추론
중심극한정리
정규분포
지수분포
균등분포
저자

이광춘

공개

2026-07-06

모집단이 어떤 분포를 따르든, 표본평균은 표본 크기가 커질수록 정규분포에 가까워진다. 아래 앱에서 슬라이더를 움직이며 이 수렴을 직접 확인해 보세요.

중심극한정리(CLT)는 통계학에서 가장 중요한 정리 중 하나입니다. 정규·지수·균등 등 서로 다른 모집단에서 뽑은 표본평균이 모두 정규분포 \(N(\mu,\ \sigma^2/n)\) 로 수렴합니다. 표준오차는 \(\sigma/\sqrt{n}\) 이므로 \(n\) 이 커질수록 분포가 좁아집니다.

sample_mean() 은 표본평균을 n 번 반복 추출하고, clt_simulation() 은 표본 크기 목록마다 이를 계산해 하나의 데이터프레임으로 묶습니다. Run Code 를 눌러 실행해 보세요.

#| label: shinylive-infer-clt
#| viewerWidth: 900
#| viewerHeight: 650
#| standalone: true

library(shiny)
library(bslib)
library(bsicons)
library(ggplot2)
library(showtext)
# 한글 세리프(가능하면). 실패 시 showtext 기본 폰트로 폴백(한글 렌더 유지).
tryCatch(sysfonts::font_add_google("Nanum Myeongjo", "kr"), error = function(e) NULL)
showtext_auto()

# ── Tufte 팔레트 ────────────────────────────────────────────
bit_cream <- "#fffff8"; bit_ink <- "#111111"; bit_rust <- "#8a1500"
bit_muted <- "#4a4a44"; bit_grid <- "#e8e6dc"; bit_axis <- "#8a8578"

# ── Tufte 미니멀 ggplot 테마 ────────────────────────────────
theme_tufte_bit <- function(base_size = 13) {
  theme_minimal(base_size = base_size) +
    theme(
      text             = element_text(family = "kr", colour = bit_ink),
      plot.title       = element_text(family = "kr", size = base_size, hjust = 0),
      plot.background  = element_rect(fill = bit_cream, colour = NA),
      panel.background = element_rect(fill = bit_cream, colour = NA),
      panel.grid.major = element_line(colour = bit_grid, linewidth = 0.3),
      panel.grid.minor = element_blank(),
      axis.line        = element_line(colour = bit_axis, linewidth = 0.3),
      axis.ticks       = element_line(colour = bit_axis, linewidth = 0.3),
      legend.position  = "none"
    )
}

# ── Tufte 앱 테마(크림 배경·세리프·뮤트) ────────────────────
bit_theme <- bs_theme(
  version   = 5,
  bg        = bit_cream,
  fg        = bit_ink,
  primary   = bit_rust,
  base_font = font_collection("Palatino Linotype", "Palatino", "Georgia",
                              "Times New Roman", "serif"),
  "card-border-color" = "#dcd8c8",
  "card-cap-bg"       = bit_cream,
  "card-box-shadow"   = "none"
)

ui <- page_sidebar(
  title = "중심극한정리",
  theme = bit_theme,
  sidebar = sidebar(
    width = 280,
    selectInput("distribution", "분포 선택",
                c("정규 분포" = "norm", "지수 분포" = "exp", "균등 분포" = "unif")),
    sliderInput("n", "표본 크기 (n)", min = 10, max = 100, value = 30),
    sliderInput("num_samples", "표본 수", min = 100, max = 10000, value = 1000),
    hr(),
    p(class = "text-muted", style = "font-size:.85rem; line-height:1.5;",
      "슬라이더를 움직여 표본평균의 분포가 정규분포 ",
      HTML("N(&mu;, &sigma;&sup2;/n)"),
      "에 가까워지는 과정을 관찰하세요.")
  ),
  card(
    card_header(
      "모집단 분포와 표본평균의 분포",
      tooltip(
        bs_icon("info-circle"),
        "왼쪽은 표본을 추출하는 원래 분포(PDF), 오른쪽은 표본평균의 분포입니다. n이 커질수록 오른쪽이 종 모양(정규)에 가까워집니다."
      )
    ),
    div(class = "text-muted", style = "font-size:.9rem; padding:.25rem .25rem .5rem;",
        textOutput("stats", inline = TRUE)),
    layout_columns(
      col_widths = c(6, 6),
      plotOutput("distPlot", height = "320px"),
      plotOutput("cltPlot",  height = "320px")
    )
  )
)

server <- function(input, output) {

  sim <- reactive({
    n <- input$n; num_samples <- input$num_samples; distribution <- input$distribution
    if (distribution == "norm") {
      samples <- matrix(rnorm(n * num_samples), nrow = n); mu <- 0;   sigma <- 1
    } else if (distribution == "exp") {
      samples <- matrix(rexp(n * num_samples, rate = 1), nrow = n); mu <- 1; sigma <- 1
    } else {
      samples <- matrix(runif(n * num_samples), nrow = n); mu <- 0.5; sigma <- sqrt(1/12)
    }
    list(means = colMeans(samples), mu = mu, sigma = sigma, n = n)
  })

  # 계산된 통계량을 절제된 한 줄로(Tufte — 큰 값상자 대신 인라인 수치)
  output$stats <- renderText({
    s <- sim()
    sprintf("표본평균의 평균 = %.3f    ·    표준오차(σ/√n) = %.3f",
            mean(s$means), s$sigma / sqrt(s$n))
  })

  output$distPlot <- renderPlot({
    d <- input$distribution
    if (d == "norm") { x <- seq(-3, 3, length.out = 100); y <- dnorm(x); ttl <- "정규 분포의 PDF" }
    else if (d == "exp") { x <- seq(0, 5, length.out = 100); y <- dexp(x, rate = 1); ttl <- "지수 분포의 PDF" }
    else { x <- seq(-0.1, 1.1, length.out = 100); y <- dunif(x, 0, 1); ttl <- "균등 분포의 PDF" }
    ggplot() +
      geom_line(aes(x, y), colour = bit_ink, linewidth = 0.6) +
      labs(title = ttl, x = "x 값", y = "밀도") +
      theme_tufte_bit()
  }, bg = bit_cream)

  output$cltPlot <- renderPlot({
    s <- sim()
    x_seq <- seq(min(s$means), max(s$means), length.out = 100)
    y <- dnorm(x_seq, mean = s$mu, sd = s$sigma / sqrt(s$n))
    ggplot() +
      geom_histogram(aes(x = s$means, y = after_stat(density)),
                     bins = 30, fill = bit_muted, alpha = 0.35, colour = NA) +
      geom_line(aes(x = x_seq, y = y), colour = bit_rust, linewidth = 0.7) +
      labs(title = "표본평균의 분포", x = "표본 평균", y = "밀도") +
      theme_tufte_bit()
  }, bg = bit_cream)
}

shinyApp(ui = ui, server = server)

정규·균등·지수 세 모집단에서 표본을 반복 추출해 표본 크기별 표본평균 분포를 facet_wrap 으로 나란히 비교합니다.

라이센스