모집단이 어떤 분포를 따르든, 표본평균은 표본 크기가 커질수록 정규분포에 가까워진다. 아래 앱에서 슬라이더를 움직이며 이 수렴을 직접 확인해 보세요.
중심극한정리(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(μ, σ²/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 으로 나란히 비교합니다.