15  범주형 변수 가설검정

범주형 변수는 관심 있는 특성이나 속성을 범주나 그룹으로 나누어 측정된 변수다. 예를 들어, 성별(남성/여성), 혈액형(A/B/O/AB), 정치 성향(진보/중도/보수) 등이 범주형 변수에 해당된다. 범주형 변수 가설검정은 이러한 범주형 변수들을 대상으로 가설을 검정하는 방법이다.

범주형 가설검정은 범주형 변수의 특성을 파악하는 데 필수적이다. 변수의 분포와 각 범주의 비율 또는 빈도 차이를 비교함으로써 변수의 전반적인 특성을 이해할 수 있다. 범주형 가설검정은 두 개 이상의 그룹 간 범주형 변수의 분포 차이를 검증함으로써 그룹 간의 특성 차이를 명확히 할 수 있으며, 두 범주형 변수 간의 연관성을 분석하여 변수들 사이의 상호 작용이나 연결 고리를 밝히는 데도 사용된다. 특히, 표본 데이터를 활용해 모집단의 범주형 변수 특성을 추론함으로써 정책 결정, 마케팅 전략 수립, 과학 연구 등 다양한 분야에서 의사 결정 과정에 근거를 제공한다.

범주형 가설검정은 여러 분야에서 중요한 역할을 한다. 의학 연구에서는 새로운 치료법의 효과를 평가하거나 질병과 위험 요인 간의 연관성을 분석하는 데 활용되고, 마케팅 조사 분야에서는 소비자의 선호도, 구매 행동, 광고 효과 등을 분석하여 마케팅 전략을 수립하는 데 사용된다. 사회 과학 연구에서는 인구 집단의 특성, 사회 현상, 여론 등을 분석하는 데 적용된다. 또한, 품질 관리 분야에서는 제품의 합격 또는 불합격 여부, 불량률 등을 분석하여 품질 관리 과정에도 활용된다.

15.1 데이터 구조

범주형 데이터를 표현하는 세 가지 방식—관측점 하나에 빈도수 하나인 경우 사례별(Case-by-Case), 빈도수(Frequency), 표(Table)—은 범주형 데이터 분석의 목적과 상황에 따라 다르게 사용된다. 각각의 형식은 범주형 데이터를 다루고 분석하는 방법에 서로 다른 장단점을 가지고 있다.

데이터 전처리나 기계학습에서는 개별 사례를 별도로 다룰 수 있는 사례별 방식이 유리하고, 전통적인 통계 검정에서는 빈도수 형태가 더 효과적이며, 표 형식은 데이터 구조를 명확하게 이해하고, 복잡한 데이터 세트 내의 패턴을 시각적으로 파악하는 데 유용하다.

그림 15.1: 표, 빈도수, 사례별 자료구조

사례별 범주형 자료는 각 관측치가 데이터셋에서 독립적인 행으로 생성된다. 특정 범주가 관측된 빈도만큼 그 범주를 반복하여 기록되어, 데이터를 개별적으로 처리할 때 유리하며, 데이터 전처리나 기계학습 모형에서 각 사례를 별도로 다루기 용이한 자료형이다.

빈도수 형태는 각 범주의 조합이 얼마나 자주 발생하는지를 직접적으로 보여주는 표를 사용한다. 통계적 분석에 매우 적합하며, 각 범주 조합의 빈도를 기반으로 한 계산을 용이하게 한다. 예를 들어, 카이제곱 검정과 같은 통계적 검정을 수행할 때 이 형식을 사용하여 각 범주의 기대 빈도와 실제 빈도를 비교한다. 데이터를 빠르게 요약하고 전체적인 분포를 이해하는 데도 도움을 준다.

표 형식은 데이터의 복잡한 구조를 시각적으로 파악하기 쉽게 만들며, 다변량 분석에서 변수 간의 관계를 빠르게 이해할 수 있게 도와준다. 요약 통계, 그래픽 표현, 상호작용 효과의 분석에 이르기까지 폭넓게 활용되고 있다.

datasets 내장 패키지에 포함된 HairEyeColor 데이터셋은 592명의 통계학과 학생들 머리색과 눈동자 색에 따라 남성과 여성의 빈도수를 포함한 3차원 배열로 구성되어 있다. HairEyeColor을 사례별, 빈도수 형식, 표 형식으로 변환하여 범주형 데이터 구조를 살펴보자.

#>      Hair   Eye  Sex
#> 1   Black Brown Male
#> 1.1 Black Brown Male
#> 1.2 Black Brown Male
#> 1.3 Black Brown Male
#> 1.4 Black Brown Male
#> 1.5 Black Brown Male
#>    Hair   Eye  Sex Freq
#> 1 Black Brown Male   32
#> 2 Brown Brown Male   53
#> 3   Red Brown Male   10
#> 4 Blond Brown Male    3
#> 5 Black  Blue Male   11
#> 6 Brown  Blue Male   50
#>        Eye
#> Hair    Brown Blue Hazel Green
#>   Black    32   11    10     3
#>   Brown    53   50    25    15
#>   Red      10   10     7     7
#>   Blond     3   30     5     8

15.2 수학적 정의

범주형 가설검정에는 주로 비율 검정(z-검정)과 카이제곱 검정이 포함된다. 비율 검정은 모집단 내의 특정 비율에 대한 가설을 검정하는 데 사용되며, 큰 표본 크기를 가진 상황에서 주로 적용된다. 표본비율 \[\hat{p}\]와 모비율 \[p_0\] 사이의 차이를 분석하며, 계산된 Z-값이 표준정규분포를 따른다고 가정한다. 비율 검정은 모집단 비율이 0.1에서 0.9 사이일 때 가장 유효하며, 두 처리 방법 간의 성공률 같은 비율을 비교할 때 유용하다.

반면, 카이제곱 검정은 두 범주형 변수 간의 독립성을 평가하는 데 사용된다. 관측된 빈도와 기대된 빈도 사이의 차이를 분석하여 두 변수 간의 상호 의존성을 검증한다. 카이제곱 검정은 각 셀의 기대도수가 일정 기준 이상일 때 적절하게 수행될 수 있으며, 두 범주형 변수 간의 관계를 분석할 때 주로 사용된다.

비율 검정과 카이제곱 검정의 선택은 데이터의 종류, 표본의 크기, 검정의 목적에 따라 달라진다. 비율 검정은 단일 범주형 변수의 비율을 다룰 때 강점을 보이며, 카이제곱 검정은 두 개 이상의 범주형 변수 간의 관계를 파악하는 데 더 적합하다. 작은 표본 크기나 기대 빈도가 낮은 경우에는 카이제곱 검정 대신 피셔의 정확 검정을 사용하여 정확한 결과를 확인할 수 있다.

15.2.1 비율 검정

비율 검정(z-검정)은 모집단의 비율에 대한 가설을 검정하는 방법으로표본의 크기가 충분히 크고(일반적으로 \(n \ge 30\)), 모집단 비율이 \(0.1 \le p \le 0.9\)인 경우에 사용된다.

\(p\)는 모비율, \(p_0\)는 귀무가설에서 주장하는 비율로 귀무가설과 대립가설은 다음과 같이 설정한다.

  • \(H_0: p = p_0\) (모비율은 \(p_0\)와 같다)
  • \(H_1: p \neq p_0\) (모비율은 \(p_0\)와 다르다) (양측 검정)
  • \(H_1: p < p_0\) (모비율은 \(p_0\)보다 작다) (단측 검정)
  • \(H_1: p > p_0\) (모비율은 \(p_0\)보다 크다) (단측 검정)

표본비율을 \(\hat{p} = \frac{X}{n}\)이라 할 때, 검정통계량 \(Z\)는 다음과 같이 계산하고 근사적으로 표준정규분포를 따른다.

\[Z = \frac{\hat{p} - p_0}{\sqrt{\frac{p_0(1-p_0)}{n}}}\]

15.2.2 \(\chi^2\) 검정

카이제곱 검정은 범주형 변수의 관측도수와 기대도수 간의 차이를 통해 범주형 변수 간 독립성을 검정하는 방법으로 기대도수가 5 이상인 셀이 전체 셀의 80% 이상이어야 하며, 기대도수가 1 이상인 셀이 전체 셀의 20% 이상이어야 한다는 조건이 충족되어야 의미있는 검정이 된다.

귀무가설과 대립가설은 다음과 같이 설정한다.

  • \(H_0\): 두 범주형 변수는 독립이다
  • \(H_1\): 두 범주형 변수는 독립이 아니다

\(O_{ij}\)\(i\)\(j\)열의 관측도수, \(E_{ij}\)\(i\)\(j\)열의 기대도수, \(r\)은 행의 개수, \(c\)는 열의 개수로 정의되며 검정통계량 \(\chi^2\)는 귀무가설 아래에서 자유도가 \((r-1)(c-1)\)인 카이제곱 분포를 따른다.

\[\chi^2 = \sum_{i=1}^{r}\sum_{j=1}^{c}\frac{(O_{ij}-E_{ij})^2}{E_{ij}}\]

비교 기준 비율 검정(z-검정) 카이제곱 검정
가설 설정 모집단의 비율에 대한 가설 검정 두 범주형 변수의 독립성에 대한 가설 검정
사용 조건 표본의 크기가 충분히 크고(n ≥ 30), 모집단의 비율이 0.1 ≤ p ≤ 0.9인 경우 기대도수가 5 이상 셀이 전체 셀의 80% 이상, 기대도수가 1 이상인셀이 전체 셀의 20% 이상
검정통계량 분포 검정통계량 Z는 근사적으로 표준정규분포 따름 검정통계량 \(\chi^2\)는 귀무가설 하에서 자유도가 \((r-1)(c-1)\)인 카이제곱 분포 따름
적용 상황 하나의 범주형 변수에 대해 모집단의 비율을 검정할 때 사용 (예: 불량품 비율 검정) 두 개 이상의 범주형 변수 간의 관계를 검정할 때 사용 (예: 성별과 정치 성향의 관계 검정)
사용 예시 두 비율이 주어졌을 때, 예를 들어 두 다른 처리법이나 조건에서의 성공 비율을 비교할 때 Z-검정 사용 두 개 이상의 범주형 변수 사이의 관계를 검증할 때, 예를 들어 성별과 선호 브랜드 간의 관계를 알아볼 때 사용
효과적인 상황 큰 표본 크기에서 비율 차이를 검정할 때 효과적 빈도수 데이터에 적합하며, 변수 간의 독립성을 평가할 수 있음
추가 사항 모분산이 알려져 있을 때 더 정확함 작은 표본 크기 또는 기대 빈도가 낮은 경우에는 피셔의 정확 검정(Fisher’s Exact Test)이 대안으로 사용될 수 있음
표 15.1: 비율 검정(z-검정)과 카이제곱 검정 주요 특징과 차이점 비교

15.3 교차분석표

교차분석표는 두 개 이상의 범주형 변수 간의 관계를 분석하는 데 사용되는 통계적 도구로, 각 변수의 범주별 도수(frequency)나 비율(proportion)을 나타내는 표이다. 연구나 실험에서 수집된 데이터의 복잡한 관계를 보여주며, 이를 통해 변수들 간의 상호 작용과 연관성을 파악할 수 있다. 다음은 R을 사용하여 두 개의 범주형 변수를 포함한 합성 데이터를 생성하고, 이를 바탕으로 교차분석표를 만드는 과정을 보여주는 예시 코드다.

#>   gender treatment outcome  group
#> 1   남성    대조군    실패 그룹 1
#> 2   여성    실험군    성공 그룹 1
#> 3   남성    실험군    실패 그룹 1
#> 4   여성    실험군    성공 그룹 1
#> 5   여성    대조군    성공 그룹 1
#> 6   남성    실험군    성공 그룹 1

이어지는 코드에서는 교차분석표를 작성하고, 비율 검정과 카이제곱 검정을 수행한다.

#>         
#>          성공 실패
#>   그룹 1  139   61
#>   그룹 2  158   92
#> 
#>  2-sample test for equality of proportions with continuity correction
#> 
#> data:  cross_table
#> X-squared = 1.6945, df = 1, p-value = 0.193
#> alternative hypothesis: two.sided
#> 95 percent confidence interval:
#>  -0.02893678  0.15493678
#> sample estimates:
#> prop 1 prop 2 
#>  0.695  0.632
#> 
#>  Pearson's Chi-squared test with Yates' continuity correction
#> 
#> data:  cross_table
#> X-squared = 1.6945, df = 1, p-value = 0.193

R에 내장된 prop.test, chisq.test 함수를 사용해서 범주형 데이터 비율을 검정하고, 두 변수 간의 독립성을 평가한다. 비율 검정은 모집단의 비율이나 두 모집단 간의 비율 차이를 추론하는 데 도움을 주며, 카이제곱 검정은 변수들 간의 독립성을 검증하는 데 유용하다.

15.3.1 모비율 검정

단일 모집단 비율 검정은 모집단의 비율이 특정 값과 같은지 확인하는 데 사용된다. 예를 들어, 어떤 제품의 불량률이 5%인지 검정하고자 할 때 사용할 수 있다.

귀무가설과 대립가설은 다음과 같이 설정한다:

  • \(H_0: p = p_0\) (모비율은 \(p_0\)와 같다)
  • \(H_1: p \neq p_0\) (모비율은 \(p_0\)와 다르다)

\(p\)는 모비율이고, \(p_0\)는 귀무가설에서 주장하는 비율이다.

\(\hat{p}\)는 표본비율, \(n\)은 표본크기, 검정통계량 \(Z\)는 다음과 같이 계산한다.

\[Z = \frac{\hat{p} - p_0}{\sqrt{p_0(1-p_0)/n}}\]

사례: 제품 불량률 검정

한 제조업체에서 생산되는 제품의 불량률이 5%인지 확인하고자 한다. 100개의 제품을 무작위로 추출하여 조사한 결과, 3개의 제품이 불량품으로 판명되었다. 유의수준 0.05에서 해당 제품의 불량률이 5%라고 할 수 있는지 검정하시오.

표본 크기 \(n = 100\), 성공 횟수 \(x = 3\), 귀무가설 하의 모비율 \(p_0 = 0.05\)를 가정할 때, 표본 비율은 \(\hat{p} = \frac{3}{100} = 0.03\) 이다. 따라서, 검정통계량 \(Z\)는 다음과 같이 계산된다.

\[Z = \frac{\hat{p} - p_0}{\sqrt{\frac{p_0(1-p_0)}{n}}} = \frac{0.03 - 0.05}{\sqrt{\frac{0.05(1-0.05)}{100}}} \approx -0.9177\]

R에서 내장된 prop.test() 함수를 사용하여 비율 검정을 수행할 수 있다.

15.3.2 두 모집단 비율 차이 검정

두 모집단 비율 차이 검정은 서로 독립인 두 모집단의 비율 차이를 비교하는 데 사용된다. 예를 들어, 두 가지 다른 광고 캠페인의 클릭률 차이를 비교할 때 사용할 수 있다.

귀무가설과 대립가설은 다음과 같이 설정한다.

  • \(H_0: p_1 = p_2\) (두 모집단의 비율은 같다)
  • \(H_1: p_1 \neq p_2\) (두 모집단의 비율은 다르다)

\(p_1\)\(p_2\)는 각 모집단의 비율이다.

\(\hat{p}_1\)\(\hat{p}_2\)는 각 표본의 비율, \(n_1\)\(n_2\)는 각 표본의 크기이고, \(\hat{p}\)는 두 표본을 합친 전체 표본의 비율이고 검정통계량 \(Z\)는 다음과 같이 계산한다.

\[Z = \frac{\hat{p}_1 - \hat{p}_2}{\sqrt{\hat{p}(1-\hat{p})(\frac{1}{n_1}+\frac{1}{n_2})}}\]

사례: A/B 테스트

한 회사에서 두 가지 다른 광고 캠페인 A와 B의 클릭률을 비교하고자 한다. 캠페인 A는 1,000명 중 100명이 클릭했고, 캠페인 B는 1,200명 중 144명이 클릭했다. 유의수준 0.05에서 두 캠페인의 클릭률에 유의한 차이가 있는지 검정해본다.

표본크기 \(n_1 = 1000\), \(n_2 = 1200\), 성공횟수 \(x_1 = 100\), \(x_2 = 144\)를 바탕으로 그룹 1과 2의 표본 비율을 다음과 같이 계산한다.

  • $ _1 = = = 0.1 $
  • $ _2 = = = 0.12 $

두 그룹을 합친 전체 표본의 비율은 다음과 같이 계산한다.

\[ \hat{p} = \frac{x_1 + x_2}{n_1 + n_2} = \frac{100 + 144}{1000 + 1200} = \frac{244}{2200} \approx 0.1109 \]

검정통계량 \(Z\) 값은 공식을 사용하여 계산한다.

\[ \begin{align*} Z &= \frac{\hat{p}_1 - \hat{p}_2}{\sqrt{\hat{p}(1-\hat{p})(\frac{1}{n_1}+\frac{1}{n_2})}} \\ &= \frac{0.1 - 0.12}{\sqrt{0.1109(1-0.1109)(\frac{1}{1000}+\frac{1}{1200})}} \\ &= \frac{-0.02}{\sqrt{0.1109 \times 0.8891 \times (\frac{1}{1000} + \frac{1}{1200})} \approx \sqrt{0.1109 \times 0.8891 \times 0.001833} \approx \sqrt{0.0001812} \approx 0.01346} \\ &= \frac{-0.02}{0.01346} \approx -1.486 \end{align*} \]

R에 내장된 prop.test() 함수를 사용하여 두 비율 차이 검정을 수행할 수 있다. prop.test() 함수는 내부적으로 귀무가설 하에서 두 비율이 같다고 가정하고, 전체 표본을 기반으로 통합 비율을 계산한 다음 관측된 빈도와 기대 빈도 간의 차이를 계산하고, 이를 기대 빈도로 나눈 값을 제곱한다. 이렇게 계산된 값은 자유도는 1(2x2 분할표에서는 (2-1)*(2-1) = 1)인 카이제곱 분포를 따른다. 이를 통해 p-값을 계산하고, 귀무가설을 검정한다.

비율 검정에 카이제곱 검정이 사용되는 이유는 이론적으로 표본 크기가 충분히 클 때(일반적으로 각 그룹의 기대도수가 5 이상), 비율의 차이는 정규 분포에 근사하고 정규 분포를 따르는 \(Z\) 통계량의 제곱은 카이제곱 분포에 근사하여 대표본에서는 \(Z\) 검정 대신 카이제곱 검정을 사용할 수 있다. 똑한 구현측면에서 보면 prop.test() 함수가 내부적으로 카이제곱 검정을 사용하면, 다른 유형의 범주형 데이터 분석과 일관성을 유지할 수 있어 사용자에게 편리하고 일관된 검정 인터페이스를 제공하게 된다. 따라서, 두 검정의 유사성, 대표본 근사의 적용 가능성, 편의성과 일관성, 해석의 용이성 이유로 인해, prop.test() 함수는 카이제곱 검정을 내부적으로 사용하여 비율 차이를 검정하는 것이 효과적이고 실용적인 선택이 된다.

prop.test() 함수가 카이제곱 검정을 사용하는 것이 그럼에도 불구하고 편하지 않은 경우 직접 z_test_proportions 함수와 같은 사용자 정의 함수를 직접 작성하여 검정할 수도 있다.

15.3.3 shiny 앱

#| label: shinylive-testing-prop
#| viewerHeight: 600
#| standalone: true
library(shiny)
library(ggplot2)

library(shiny)
library(ggplot2)

ui <- fluidPage(
  titlePanel("비율 검정 및 카이제곱 검정"),

  sidebarLayout(
    sidebarPanel(
      h4("그룹 1"),
      sliderInput("success1", "성공 횟수", min = 0, max = 100, value = 80),
      sliderInput("failure1", "실패 횟수", min = 0, max = 100, value = 20),
      hr(),
      h4("그룹 2"),
      sliderInput("success2", "성공 횟수", min = 0, max = 100, value = 60),
      sliderInput("failure2", "실패 횟수", min = 0, max = 100, value = 40),
      hr(),
      selectInput("test_type", "검정 유형", choices = c("z-검정", "카이제곱 검정")),
      selectInput("alpha", "유의 수준 (α)", choices = c(0.01, 0.05, 0.10), selected = 0.05),
      br(),
      h4("사용법:"),
      p("1. 각 그룹의 성공 횟수와 실패 횟수를 슬라이더로 조정하세요."),
      p("2. 원하는 검정 유형과 유의 수준을 선택하세요."),
      p("3. 결과, 교차분석표, 그래프가 오른쪽 탭에 표시됩니다.")
    ),

    mainPanel(
      tabsetPanel(
        tabPanel("검정 결과", verbatimTextOutput("result")),
        tabPanel("교차분석표", tableOutput("contingency_table")),
        tabPanel("막대 그래프", plotOutput("barplot")),
        tabPanel("분포 그래프", plotOutput("dist_plot"))
      )
    )
  )
)

server <- function(input, output) {

  contingency_table <- reactive({
    matrix(c(input$success1, input$success2, input$failure1, input$failure2),
           nrow = 2, byrow = FALSE,
           dimnames = list(c("성공", "실패"), c("그룹 1", "그룹 2")))
  })

  output$contingency_table <- renderTable({
    addmargins(contingency_table(), margin = c(1, 2), FUN = sum)
  }, rownames = TRUE, colnames = TRUE)

  test_result <- reactive({
    if (input$test_type == "z-검정") {
      x <- c(input$success1, input$success2)
      n <- c(input$success1 + input$failure1, input$success2 + input$failure2)
      prop.test(x, n)
    } else {
      chisq.test(contingency_table())
    }
  })

  output$result <- renderPrint({
    test_result()
  })

  output$barplot <- renderPlot({
    data <- as.data.frame.matrix(contingency_table())
    data$outcome <- row.names(data)
    data_long <- tidyr::gather(data, "group", "count", -outcome)

    ggplot(data_long, aes(x = group, y = count, fill = outcome)) +
      geom_bar(stat = "identity", position = "dodge") +
      labs(x = "그룹", y = "횟수", fill = "결과") +
      scale_fill_manual(values = c("성공" = "blue", "실패" = "red"))
  })

  output$dist_plot <- renderPlot({
    if (input$test_type == "z-검정") {

      test_stat <- test_result()$statistic
      p_value <- test_result()$p.value
      crit_val <- qnorm(1 - as.numeric(input$alpha) / 2)
      x <- seq(-5, 5, length.out = 1000)
      y <- dnorm(x)
      data_plot <- data.frame(x = x, y = y)

      ggplot(data_plot, aes(x = x, y = y)) +
        geom_line() +
        geom_vline(xintercept = test_stat, color = "blue", linetype = "dashed") +
        geom_vline(xintercept = c(-crit_val, crit_val), color = "red", linetype = "dashed") +
        annotate("text", x = test_stat, y = max(y) * 0.9,
                 label = paste0("검정 통계량 = ", round(test_stat, 2)), color = "blue", hjust = 0) +
        annotate("text", x = -crit_val, y = max(y) * 0.8,
                 label = paste0("임계값 = ", round(-crit_val, 2)), color = "red", hjust = 1.1) +
        annotate("text", x = crit_val, y = max(y) * 0.8,
                 label = paste0("임계값 = ", round(crit_val, 2)), color = "red", hjust = -0.1) +
        annotate("text", x = 0, y = max(y) * 0.6,
                 label = paste0("p-값 = ", round(p_value, 4)), hjust = 0.5) +
        labs(x = "z 통계량", y = "확률 밀도") +
        coord_cartesian(xlim = c(-5, 5), ylim = c(0, max(y) * 1.1)) +
        theme_minimal()
    } else {
      test_stat <- test_result()$statistic
      df <- test_result()$parameter
      p_value <- test_result()$p.value
      crit_val <- qchisq(1 - as.numeric(input$alpha), df)

      x <- seq(0, max(crit_val, test_stat) + 5, length.out = 1000)
      y <- dchisq(x, df)

      data_plot <- data.frame(x = x, y = y)

      ggplot(data_plot, aes(x = x, y = y)) +
        geom_line() +
        geom_vline(xintercept = test_stat, color = "blue", linetype = "dashed") +
        geom_vline(xintercept = crit_val, color = "red", linetype = "dashed") +
        geom_ribbon(data = subset(data_plot, x > crit_val),
                    aes(x = x, ymax = y), ymin = 0, fill = "red", alpha = 0.3) +
        annotate("text", x = test_stat + 0.1, y = (min(y) + 0.1) * 10, size = 7,
                 label = paste0("검정 통계량 = ", round(test_stat, 2)), color = "blue", hjust = 0) +
        annotate("text", x = crit_val + 0.1, y = (min(y) + 0.1) * 10, size = 7,
                 label = paste0("임계값 = ", round(crit_val, 2)), color = "red", hjust = 0) +
        annotate("text", x = max(crit_val, test_stat) * 0.5, y = (min(y) + 0.1) * 5, size = 7,
                 label = paste0("p-값 = ", round(p_value, 4)), hjust = 0) +
        labs(x = "카이제곱 통계량", y = "확률 밀도") +
        coord_cartesian(xlim = c(0, max(crit_val, test_stat) + 2)) +
        theme_minimal()
    }
  })
}

shinyApp(ui = ui, server = server)