콘텐츠로 이동

9.7 가설검정

9.7 가설검정

통계 분석에서는 "적합"이 아니라 "판단"이 필요한 경우가 훨씬 더 많습니다. 표본에서 어떤 차이나 경향이 관찰되었을 때, 그것이 실제로 존재하는 차이인지 아니면 표본을 뽑는 과정에서 우연히 생긴 변동인지를 판단해야 하는 상황입니다. 이 절에서는 이런 판단을 체계적으로 내리는 절차인 가설검정(hypothesis test)과, R이 기본으로 제공하는 대표적인 가설검정 함수들을 다룹니다.

mtcars 데이터셋에는 1974년 미국 자동차 32종의 연비(mpg)와 변속기 종류(am: 0 = 자동, 1 = 수동)가 들어 있습니다. 두 그룹의 평균 연비를 구해 보면 수동변속기 쪽이 더 높게 나옵니다.

tapply(mtcars$mpg, mtcars$am, mean)
#>        0        1 
#> 17.14737 24.39231 

그런데 이 32종의 자동차는 당시 시판되던 자동차 전체(모집단)가 아니라 그중 일부를 뽑은 표본에 불과합니다. 표본을 다시 뽑았다면 두 그룹의 평균 차이(약 7.2)가 조금 더 크거나 작게 나왔을 수도 있고, 운이 나쁘면 부호가 바뀔 수도 있습니다. "표본에서 관찰된 7.2라는 차이가 모집단에도 실제로 존재하는 차이라고 믿을 만큼 충분히 큰가, 아니면 이 정도 차이는 우연히도 흔히 나타날 수 있는 수준인가"라는 질문에 답하는 도구가 바로 가설검정이며, 이 사례에는 아래에서 다룰 t.test()가 답을 줍니다.

R은 자료의 종류(연속형·범주형)와 비교 대상(평균·분산·비율·분포 모양·상관관계)에 따라 다음과 같은 가설검정 함수를 stats 패키지에 기본으로 제공합니다.

무엇을 비교·검정하는가 자료 형태 사용하는 함수
자료가 정규분포를 따르는가 연속형 1개 shapiro.test()
평균(1개 표본·2개 표본·대응표본) 연속형 t.test()
분산(2개 집단) 연속형, 2집단 var.test()
분산(3개 집단 이상) 연속형, 3집단 이상 bartlett.test()
평균·위치(정규성 가정 없이, 2개 표본·대응표본) 연속형(순위 기반) wilcox.test()
평균·위치(정규성 가정 없이, 3개 집단 이상) 연속형(순위 기반) kruskal.test()
비율(1개 또는 여러 집단) 범주형(이분형) prop.test()
이항 확률(정확검정) 범주형(이분형), 소표본 binom.test()
두 변수의 상관관계 연속형 2개 cor.test()
관측도수의 분포 적합도, 두 범주형 변수의 독립성 범주형 chisq.test()
두 범주형 변수의 독립성(정확검정, 소표본) 범주형, 소표본 fisher.test()
자료가 특정 분포를 따르는가, 두 표본이 같은 분포에서 왔는가 연속형 ks.test()

가설검정은 다음과 같은 공통 절차를 따릅니다.

  1. 귀무가설(H₀, null hypothesis)과 대립가설(H₁, alternative hypothesis)을 세웁니다. 귀무가설은 보통 "차이가 없다", "관계가 없다"처럼 기각의 대상이 되는 보수적인 주장이고, 대립가설은 연구자가 실제로 보이고 싶은 주장입니다.
  2. 표본으로부터 검정통계량(test statistic, 예: t값·F값·카이제곱값)을 계산합니다.
  3. 귀무가설이 참이라는 가정 아래에서, 지금 얻은 것만큼 극단적인 검정통계량이 나올 확률인 p값(p-value)을 구합니다.
  4. p값을 미리 정해 둔 유의수준(α, 보통 0.05)과 비교하여, p값이 α보다 작으면 귀무가설을 기각하고 대립가설을 채택합니다.

R의 검정 함수들은 이 절차를 하나의 함수 호출로 실행해 htest라는 공통 클래스의 객체를 반환합니다. 이 객체를 그냥 출력(print())하면 검정통계량·자유도·p값·신뢰구간·표본 추정치가 정해진 형식으로 요약되어 나타나며, $statistic·$p.value·$conf.int·$estimate처럼 필요한 값만 따로 꺼낼 수도 있습니다.

주의: p값은 "얼마나 중요한가"가 아니라 "얼마나 우연 같은가"를 나타냅니다. p값이 작다는 것은 "이 정도 차이가 순전히 우연으로 나타나기는 어렵다"는 뜻일 뿐, 그 차이가 실용적으로 큰 의미를 가진다는 뜻은 아닙니다. 표본 크기가 아주 크면 실질적으로 무의미할 만큼 작은 차이도 p값이 작게 나올 수 있습니다. 미국통계학회(ASA)는 2016년 성명을 통해 p값 하나만으로 결론을 내리는 관행에 우려를 표하고, 효과크기(effect size)와 신뢰구간을 함께 보고할 것을 권고한 바 있습니다. 이 절 마지막에서 효과크기를 함께 살펴보는 예시를 다룹니다.

가설검정을 실행하기 전에는 그 검정이 전제하는 조건(대표적으로 정규성)이 자료에서 성립하는지 먼저 점검하는 것이 순서입니다. 이런 이유로 이 절은 정규성 검정인 shapiro.test()부터 시작합니다.

shapiro.test()

shapiro.test(x)는 샤피로-윌크 검정(Shapiro-Wilk test)으로 표본 x가 정규분포에서 나왔다고 볼 수 있는지 검정합니다.

  • x : 검정할 수치형 벡터. 표본 크기가 3개 이상 5,000개 이하여야 합니다(그보다 크면 오류가 발생하며, 표본이 매우 클 때는 뒤(18장)에서 다룰 qqnorm() 같은 시각적 진단을 함께 참고하는 것이 좋습니다).

귀무가설은 "x는 정규분포에서 나왔다"이므로, p값이 작을수록(예: 0.05 미만) 정규분포라고 보기 어렵다는 뜻입니다. 이 방향이 직관과 반대라서 처음 배울 때 헷갈리기 쉬우니 주의해야 합니다.

shapiro.test(mtcars$mpg)
#> 
#>  Shapiro-Wilk normality test
#> 
#> data:  mtcars$mpg
#> W = 0.94756, p-value = 0.1229

p값이 0.1229로 0.05보다 크므로, mtcars$mpg가 정규분포를 따르지 않는다고 볼 근거는 충분하지 않습니다. 반면 지수분포처럼 애초에 정규분포와 모양이 다른 자료라면 p값이 매우 작게 나옵니다.

set.seed(1)
x <- rexp(50, rate = 0.2)   # 지수분포에서 뽑은 표본(정규분포와 모양이 다름)
shapiro.test(x)
#> 
#>  Shapiro-Wilk normality test
#> 
#> data:  x
#> W = 0.78898, p-value = 4.932e-07

💡 정규성은 왜 확인해야 할까요? 뒤에서 다룰 t.test()·var.test()·bartlett.test()·cor.test()(피어슨 방법)·aov()(9.3절) 등은 모두 자료가 정규분포에서 나왔다는 가정 위에서 검정통계량의 분포(t분포·F분포 등)를 유도합니다. 표본 크기가 크면 중심극한정리 덕분에 이 가정이 다소 어긋나도 크게 문제되지 않는 경우가 많지만, 표본이 작을 때는 정규성 위반이 결과를 왜곡할 수 있습니다. 정규성이 의심되면 wilcox.test()·kruskal.test()처럼 순위에 기반해 정규성을 가정하지 않는 비모수 검정(nonparametric test)을 대안으로 고려합니다. 이 절 곳곳에서 모수적 방법과 비모수적 방법을 짝지어 함께 소개합니다.

t.test()

t.test(x, y = NULL, alternative = c("two.sided", "less", "greater"), mu = 0, paired = FALSE, var.equal = FALSE, conf.level = 0.95, ...)는 t검정으로 하나 또는 두 집단의 평균이 특정 값(또는 서로)과 같은지 검정합니다. t.test(formula, data, ...) 형태로 반응변수 ~ 그룹변수(그룹변수는 정확히 두 수준을 가진 팩터) 수식을 넘길 수도 있습니다.

  • x, y : 비교할 표본. y를 생략하면 일표본 검정, 지정하면 이표본 검정입니다.
  • alternative : 대립가설의 방향. "two.sided"(기본값, 양측검정)는 "다르다", "less"는 "작다", "greater"는 "크다"를 검정합니다.
  • mu : 일표본 검정에서는 귀무가설의 기준 평균값이고, 이표본·대응표본 검정에서는 두 평균 차이의 기준값입니다(기본값 0, 즉 "차이가 없다").
  • paired : TRUE이면 같은 대상을 두 번 측정한 대응표본(paired sample) t검정을 실행합니다. 이 경우 x와 y의 길이가 같아야 하며, 같은 위치의 값이 같은 대상의 전·후 측정값 쌍이어야 합니다.
  • var.equal : 이표본 검정에서 두 모집단의 분산이 같다고 가정할지 여부입니다. FALSE(기본값)이면 분산이 다르다고 보고 Welch의 근사자유도를 사용하는 Welch t검정을, TRUE이면 두 표본을 합쳐 하나의 분산(합동분산, pooled variance)으로 추정하는 전통적인 Student t검정을 실행합니다.
  • conf.level : 신뢰구간의 신뢰수준(기본값 0.95, 즉 95%).

일표본 t검정

mtcars$mpg의 평균이 20이라고 할 수 있는지 검정합니다.

t.test(mtcars$mpg, mu = 20)
#> 
#>  One Sample t-test
#> 
#> data:  mtcars$mpg
#> t = 0.08506, df = 31, p-value = 0.9328
#> alternative hypothesis: true mean is not equal to 20
#> 95 percent confidence interval:
#>  17.91768 22.26357
#> sample estimates:
#> mean of x 
#>  20.09062 

p값이 0.9328로 매우 크므로, 표본평균(20.09)과 20의 차이는 우연한 표본 변동으로 충분히 설명됩니다. 95% 신뢰구간(17.92~22.26)이 20을 포함한다는 점에서도 같은 결론을 확인할 수 있습니다. "평균이 20보다 크다"만 확인하면 된다면 단측검정을 사용합니다.

t.test(mtcars$mpg, mu = 20, alternative = "greater")
#> 
#>  One Sample t-test
#> 
#> data:  mtcars$mpg
#> t = 0.08506, df = 31, p-value = 0.4664
#> alternative hypothesis: true mean is greater than 20
#> 95 percent confidence interval:
#>  18.28418      Inf
#> sample estimates:
#> mean of x 
#>  20.09062 

검정통계량(t = 0.08506)은 그대로지만, 단측으로 계산하면서 p값이 양측검정의 절반(0.9328 / 2 ≈ 0.4664)이 되고 신뢰구간의 한쪽 끝이 무한대(Inf)로 열린 것을 볼 수 있습니다.

이표본(독립표본) t검정

앞서 살펴본 변속기 종류(am)에 따른 연비(mpg) 차이를 검정합니다.

t.test(mpg ~ am, data = mtcars)
#> 
#>  Welch Two Sample t-test
#> 
#> data:  mpg by am
#> t = -3.7671, df = 18.332, p-value = 0.001374
#> alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
#> 95 percent confidence interval:
#>  -11.280194  -3.209684
#> sample estimates:
#> mean in group 0 mean in group 1 
#>        17.14737        24.39231 

var.equal을 지정하지 않았으므로 기본값인 Welch t검정이 실행되었고, 자유도(df = 18.332)가 정수가 아닌 것이 그 특징입니다. p값이 0.001374로 매우 작아, 두 변속기 그룹의 평균 연비 차이가 우연이라고 보기 어렵습니다. 두 그룹의 분산이 같다고 가정할 근거가 있다면(아래 var.test() 결과 참고) 다음과 같이 전통적인 Student t검정을 지정할 수도 있습니다.

t.test(mpg ~ am, data = mtcars, var.equal = TRUE)
#> 
#>  Two Sample t-test
#> 
#> data:  mpg by am
#> t = -4.1061, df = 30, p-value = 0.000285
#> alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
#> 95 percent confidence interval:
#>  -10.84837  -3.64151
#> sample estimates:
#> mean in group 0 mean in group 1 
#>        17.14737        24.39231 

자유도가 30(= 19 + 13 − 2)으로 정수가 되었고 p값도 더 작아졌습니다. 두 방법의 결론(유의함)은 같지만, var.equal 지정에 따라 자유도와 p값이 달라질 수 있다는 점에 유의해야 합니다. 실무에서는 분산이 같다는 확신이 없는 한 R의 기본값인 Welch t검정을 그대로 사용하는 것이 안전한 선택으로 권장됩니다.

대응표본 t검정

R에 내장된 sleep 데이터셋은 환자 10명에게 두 종류의 수면제를 각각 투여하고 수면시간 증가량(extra)을 기록한 고전적인 예제 자료입니다. 같은 환자가 두 약을 모두 투여받았으므로 대응표본입니다.

str(sleep)
#> 'data.frame':    20 obs. of  3 variables:
#>  $ extra: num  0.7 -1.6 -0.2 -1.2 -0.1 3.4 3.7 0.8 0 2 ...
#>  $ group: Factor w/ 2 levels "1","2": 1 1 1 1 1 1 1 1 1 1 ...
#>  $ ID   : Factor w/ 10 levels "1","2","3","4",..: 1 2 3 4 5 6 7 8 9 10 ...

sleep[sleep$ID %in% c(1, 5, 10),]
#>    extra group ID
#> 1    0.7     1  1
#> 5   -0.1     1  5
#> 10   2.0     1 10
#> 11   1.9     2  1
#> 15  -0.1     2  5
#> 20   3.4     2 10

sleep2 <- reshape(sleep, direction = "wide", idvar = "ID", timevar = "group")
sleep2
#>    ID extra.1 extra.2
#> 1   1     0.7     1.9
#> 2   2    -1.6     0.8
#> 3   3    -0.2     1.1
#> 4   4    -1.2     0.1
#> 5   5    -0.1    -0.1
#> 6   6     3.4     4.4
#> 7   7     3.7     5.5
#> 8   8     0.8     1.6
#> 9   9     0.0     4.6
#> 10 10     2.0     3.4

t.test(sleep2$extra.1, sleep2$extra.2, paired = TRUE)
#> 
#>  Paired t-test
#> 
#> data:  sleep2$extra.1 and sleep2$extra.2
#> t = -4.0621, df = 9, p-value = 0.002833
#> alternative hypothesis: true mean difference is not equal to 0
#> 95 percent confidence interval:
#>  -2.4598858 -0.7001142
#> sample estimates:
#> mean difference 
#>           -1.58 

자유도(df = 9 = 환자 수 10 − 1)가 독립표본이었다면(df = 18)의 절반 수준으로 줄어든 것이 눈에 띕니다. 대응표본 t검정은 내부적으로 "두 값의 차이"라는 새로운 변수 하나에 대한 일표본 t검정과 동일하게 계산되기 때문에, 환자 개인차가 만드는 분산을 제거하고 순수하게 약효 차이만 비교할 수 있어 표본 크기가 같아도 독립표본 검정보다 더 예민하게(작은 차이도 유의하게) 검정하는 경향이 있습니다.

주의: paired = TRUE로 지정하려면 x와 y의 길이가 같고, 같은 위치의 값끼리 짝이 맞아야 합니다. 짝이 어긋난 상태로 대응표본 검정을 실행해도 R은 오류 없이 계산을 마치지만 결과는 무의미해지므로, 데이터를 정렬한 뒤 검정을 실행하는 습관이 중요합니다. 또한, t.test()의 formula 방식에서는 paired = TRUE를 사용할 수 없습니다.

var.test()

var.test(x, y, ratio = 1, alternative = c("two.sided", "less", "greater"), conf.level = 0.95, ...)는 두 표본의 분산이 같은지를 F검정으로 비교합니다.

  • x, y : 분산을 비교할 두 표본(또는 t.test()처럼 formula로 지정 가능).
  • ratio : 귀무가설에서 두 모분산 비율의 기준값(기본값 1, 즉 "두 분산이 같다").
  • alternative, conf.level : t.test()와 동일한 역할입니다.

검정통계량은 두 표본분산의 비(F = 분산1/분산2)이며, F분포를 따릅니다. F검정은 정규성 가정에 특히 민감하다는 특징이 있어(자료가 정규분포에서 크게 벗어나면 결과를 신뢰하기 어려움), 실행 전 shapiro.test()로 정규성을 먼저 확인하는 것이 바람직합니다.

var.test(mpg ~ am, data = mtcars)
#> 
#>  F test to compare two variances
#> 
#> data:  mpg by am
#> F = 0.38656, num df = 18, denom df = 12, p-value = 0.06691
#> alternative hypothesis: true ratio of variances is not equal to 1
#> 95 percent confidence interval:
#>  0.1243721 1.0703429
#> sample estimates:
#> ratio of variances 
#>           0.3865615 

p값이 0.06691로 0.05보다는 크지만 그리 넉넉하지 않은 수준이어서, "두 그룹의 분산이 같다"는 가정을 완전히 안심하고 채택하기는 애매합니다. 이런 경계선 상황에서는 위 t.test() 절에서 설명한 것처럼 var.equal = TRUE로 확정 짓기보다, R의 기본값인 Welch t검정(등분산을 가정하지 않는 방법)을 그대로 쓰는 편이 안전합니다.

bartlett.test()

bartlett.test(x, g, ...)는 3개 이상의 집단에 대해 분산이 모두 같은지(등분산성, homogeneity of variance)를 검정합니다. bartlett.test(formula, data, ...) 형태로 반응변수 ~ 그룹변수 수식을 넘길 수도 있습니다.

  • x : 수치형 벡터(전체 관측값), 또는 그룹별 값을 담은 리스트.
  • g : x와 길이가 같은, 그룹을 나누는 팩터. x가 이미 그룹별 리스트라면 생략합니다.

PlantGrowth는 대조군(ctrl)과 두 처치군(trt1, trt2)의 식물 무게를 기록한 3그룹 자료입니다. 그룹별 분산을 먼저 살펴본 뒤 검정을 실행합니다.

tapply(PlantGrowth$weight, PlantGrowth$group, var)
#>      ctrl      trt1      trt2 
#> 0.3399956 0.6299211 0.1958711 

bartlett.test(weight ~ group, data = PlantGrowth)
#> 
#>  Bartlett test of homogeneity of variances
#> 
#> data:  weight by group
#> Bartlett's K-squared = 2.8786, df = 2, p-value = 0.2371

그룹별 분산이 0.34~0.63으로 얼핏 차이가 있어 보이지만, p값(0.2371)이 크게 나와 이 정도 차이는 우연한 표본 변동 범위 안에 있다고 볼 수 있습니다. 즉 세 그룹 모두 분산이 같다고 가정하고 분산분석(aov(), 9.3절)을 진행해도 무리가 없습니다.

주의: bartlett.test()는 var.test()와 마찬가지로 정규성에 민감합니다. 정규성이 의심스러운 자료에서 여러 그룹의 산포(散布)를 비교하려면, 정규성을 가정하지 않는 fligner.test()(Fligner-Killeen 검정, base stats 패키지에 함께 내장되어 있습니다)를 대안으로 고려할 수 있습니다.

fligner.test(weight ~ group, data = PlantGrowth)
#> 
#> Fligner-Killeen test of homogeneity of variances
#>
#> data:  weight by group
#> Fligner-Killeen:med chi-squared = 2.3499, df = 2, p-value = 0.3088

wilcox.test()

wilcox.test(x, y = NULL, alternative = c("two.sided", "less", "greater"), mu = 0, paired = FALSE, exact = NULL, correct = TRUE, conf.int = FALSE, conf.level = 0.95, ...)는 정규성을 가정하지 않고 순위(rank)만으로 두 집단(또는 하나의 집단과 기준값)의 위치(대체로 중위수)를 비교하는 윌콕슨 검정입니다. t.test()의 비모수 대응이라고 이해하면 됩니다.

  • x, y : 비교할 표본. paired = FALSE(기본값)일 때는 순위합검정(rank-sum test, Mann-Whitney U 검정과 동등)이, paired = TRUE일 때는 부호순위검정(signed-rank test)이 실행됩니다.
  • mu : 검정 기준값(일표본에서는 중위수의 기준, 이표본에서는 위치이동의 기준값. 기본값 0).
  • paired : TRUE이면 대응표본 검정.
  • exact : 정확분포로 p값을 계산할지 여부. NULL(기본값)이면 표본 크기와 동점(tie) 여부를 보고 자동으로 정합니다. 표본에 동점이 있으면 정확 p값을 계산할 수 없어 자동으로 정규근사로 전환되며, 이때 R은 경고 메시지를 출력합니다.
  • correct : 정규근사를 사용할 때 연속성 수정(continuity correction)을 적용할지 여부(기본값 TRUE).
  • conf.int : TRUE로 지정하면 위치모수(location shift)에 대한 신뢰구간도 함께 계산합니다(기본값 FALSE).

이표본(독립표본) 검정

wilcox.test(mpg ~ am, data = mtcars)
#> 
#>  Wilcoxon rank sum exact test
#> 
#> data:  mpg by am
#> W = 42, p-value = 0.001159
#> alternative hypothesis: true location shift is not equal to 0

앞서 t.test()로 얻은 결론(p = 0.001374)과 방향은 같지만, 순위만 사용하는 검정이라 값 자체는 t검정과 다르게 나옵니다.

대응표본 검정

sleep 자료를 wilcox.test()로도 검정할 수 있습니다.

wilcox.test(sleep2$extra.1, sleep2$extra.2, paired = TRUE)
#> 
#>  Wilcoxon signed rank exact test
#> 
#> data:  sleep2$extra.1 and sleep2$extra.2
#> V = 0, p-value = 0.003906
#> alternative hypothesis: true location shift is not equal to 0

일표본 검정

기준값(mu)과 비교하는 일표본 형태도 가능합니다.

d <- c(5.2, 6.1, 4.8, 7.3, 5.9, 6.5, 4.4, 5.0, 6.8, 5.5)
wilcox.test(d, mu = 5)
#> 
#>  Wilcoxon signed rank exact test
#> 
#> data:  d
#> V =  46.5, p-value = 0.04688
#> alternative hypothesis: true location is not equal to 5

kruskal.test()

kruskal.test(x, g, ...)는 3개 이상의 독립집단에 대해 분포(순위)의 위치가 모두 같은지를 검정하는 크루스칼-왈리스 검정입니다. kruskal.test(formula, data, ...) 형태도 지원합니다.

  • x : 수치형 벡터, 또는 그룹별 값을 담은 리스트.
  • g : 그룹을 나누는 팩터.
kruskal.test(weight ~ group, data = PlantGrowth)
#> 
#>  Kruskal-Wallis rank sum test
#> 
#> data:  weight by group
#> Kruskal-Wallis chi-squared = 7.9882, df = 2, p-value = 0.01842

p값이 0.01842로 0.05보다 작아, 세 그룹(ctrl, trt1, trt2) 사이에 순위 기준으로 유의한 차이가 있다고 판단할 수 있습니다. 다만 이 검정은 "적어도 한 그룹은 다르다"는 것만 알려줄 뿐 어느 그룹끼리 다른지는 알려주지 않으므로, 구체적으로 어느 쌍이 다른지 알고 싶다면 이 절 끝부분에서 다루는 사후검정(post-hoc test)을 함께 사용해야 합니다.

prop.test()

prop.test(x, n, p = NULL, alternative = c("two.sided", "less", "greater"), conf.level = 0.95, correct = TRUE)는 하나 또는 여러 집단의 비율(proportion)을 검정합니다.

  • x : 각 집단의 "성공"(관심 있는 사건) 횟수를 담은 정수 벡터, 또는 (성공, 실패) 2열로 이루어진 행렬.
  • n : 각 집단의 전체 시행 횟수를 담은 벡터(x와 길이가 같아야 함). x가 이미 행렬이면 지정하지 않습니다.
  • p : 귀무가설에서의 기준 비율(들). 생략하면 "여러 집단의 비율이 서로 같다"를 검정하고, 지정하면 "비율이 이 값과 같다"를 검정합니다.
  • correct : 이산적인 이항분포를 연속적인 카이제곱분포로 근사할 때 생기는 오차를 보정하는 Yates의 연속성 수정을 적용할지 여부입니다(기본값 TRUE).

Titanic 데이터셋으로 "성별에 따라 생존 비율이 달랐는가"를 검정해 보겠습니다. 먼저 성별 × 생존여부 2×2표를 만듭니다.

(tab <- apply(Titanic, c("Sex", "Survived"), sum))
#>         Survived
#> Sex        No Yes
#>   Male   1364 367
#>   Female  126 344

여기서 주의할 점이 있습니다. 이 표를 그대로 prop.test()에 넘기면, R은 첫 번째 열(여기서는 "No")을 "성공" 횟수로 간주합니다.

prop.test(tab)
#> 
#>  2-sample test for equality of proportions with continuity correction
#> 
#> data:  tab
#> X-squared = 454.5, df = 1, p-value < 2.2e-16
#> alternative hypothesis: two.sided
#> 95 percent confidence interval:
#>  0.4741109 0.5656866
#> sample estimates:
#>    prop 1    prop 2 
#> 0.7879838 0.2680851 

여기서 prop 1(0.7880)은 남성 승객 중 "사망(No)" 비율이지, 우리가 알고 싶은 "생존 비율"이 아닙니다. 생존 비율을 직접 비교하려면 x와 n을 명시적으로 지정하는 편이 안전합니다.

prop.test(x = tab[, "Yes"], n = rowSums(tab))
#> 
#>  2-sample test for equality of proportions with continuity correction
#> 
#> data:  tab[, "Yes"] out of rowSums(tab)
#> X-squared = 454.5, df = 1, p-value < 2.2e-16
#> alternative hypothesis: two.sided
#> 95 percent confidence interval:
#>  -0.5656866 -0.4741109
#> sample estimates:
#>    prop 1    prop 2 
#> 0.2120162 0.7319149 

검정통계량과 p값은 어느 쪽을 "성공"으로 두든 동일하지만(단순히 여사건 관계이므로), 이제 prop 1(남성 생존 비율 0.212)과 prop 2(여성 생존 비율 0.732)가 우리가 실제로 묻고 싶었던 값과 일치합니다. p값이 매우 작으므로(< 2.2e-16), 성별에 따른 생존 비율 차이는 우연이라고 보기 어렵습니다 — 잘 알려진 "여성과 어린이 먼저"라는 승선 원칙이 실제 자료에도 반영되어 있는 셈입니다.

주의: prop.test()는 내부적으로 카이제곱 근사를 사용하므로, 표 안의 기대도수(특히 "성공" 또는 "실패" 칸의 기대도수)가 5 미만으로 작을 때는 근사가 부정확해질 수 있습니다. 이런 경우 아래에서 다룰 binom.test()(1개 집단, 정확검정)나 fisher.test()(2개 이상 집단, 정확검정)를 사용하는 것이 더 안전합니다.

binom.test()

binom.test(x, n, p = 0.5, alternative = c("two.sided", "less", "greater"), conf.level = 0.95)는 이항분포로부터 정확한(exact) p값을 계산하는 이항검정입니다.

  • x : 성공 횟수.
  • n : 전체 시행 횟수.
  • p : 귀무가설에서의 기준 성공확률(기본값 0.5, 즉 "동전이 공정하다"는 가정에 해당).

prop.test()가 정규분포 근사를 쓰는 것과 달리, binom.test()는 이항분포 자체에서 p값을 직접 계산하므로 표본이 작을 때 특히 신뢰할 수 있습니다. 동전을 20번 던져 15번 앞면이 나왔을 때, 이 동전이 공정하다고 볼 수 있는지 검정해 보겠습니다.

binom.test(15, 20, p = 0.5)
#> 
#>  Exact binomial test
#> 
#> data:  15 and 20
#> number of successes = 15, number of trials = 20, p-value = 0.04139
#> alternative hypothesis: true probability of success is not equal to 0.5
#> 95 percent confidence interval:
#>  0.5089541 0.9134285
#> sample estimates:
#> probability of success 
#>                    0.75 

p값이 0.04139로 0.05보다 작아, 이 동전이 공정하다는 가정을 기각할 근거가 있습니다(다만 5%에 가까운 경계선 수준이므로 추가 실험으로 확인하는 것이 바람직합니다). "앞면이 더 잘 나온다"는 방향까지 미리 예상했다면 단측검정을 사용할 수 있습니다.

binom.test(15, 20, p = 0.5, alternative = "greater")
#> 
#>  Exact binomial test
#> 
#> data:  15 and 20
#> number of successes = 15, number of trials = 20, p-value = 0.02069
#> alternative hypothesis: true probability of success is greater than 0.5
#> 95 percent confidence interval:
#>  0.5444176 1.0000000
#> sample estimates:
#> probability of success 
#>                    0.75 

주의: 단측검정은 반드시 자료를 보기 전에 방향을 미리 정해 두었을 때만 정당화됩니다. 자료를 먼저 보고 "어차피 앞면이 더 많이 나왔으니 단측검정을 쓰자"는 식으로 사후에 방향을 정하면, p값이 부당하게 작아져 1종 오류(실제로는 차이가 없는데 있다고 잘못 판단하는 오류)를 범할 위험이 커집니다.

cor.test()

cor.test(x, y, alternative = c("two.sided", "less", "greater"), method = c("pearson", "kendall", "spearman"), exact = NULL, conf.level = 0.95, continuity = FALSE, ...)는 두 변수 사이의 상관관계가 통계적으로 유의한지 검정합니다.

  • x, y : 상관관계를 검정할 두 수치형 벡터.
  • method : 상관계수의 종류입니다.
  • "pearson"(기본값) : 두 변수의 선형 관계를 측정하는 피어슨 상관계수. 두 변수가 대략 정규분포를 따를 때 적합합니다.
  • "spearman" : 값 대신 순위를 사용해 상관관계를 측정하는 스피어만 순위상관계수. 반드시 직선일 필요는 없는 단조(monotonic) 관계를 잡아내며, 이상치의 영향을 덜 받습니다.
  • "kendall" : 모든 쌍을 비교해 순서가 일치하는 쌍과 불일치하는 쌍의 비율로 계산하는 켄달의 타우. 표본이 작을 때 스피어만보다 상대적으로 안정적이라고 알려져 있습니다.
  • exact : 정확 p값 계산 여부. 스피어만·켄달 방식에서 자료에 동점이 있으면 정확분포를 계산할 수 없어 자동으로 근사로 전환됩니다.
  • continuity : 스피어만·켄달의 근사계산에서 연속성 수정을 적용할지 여부.

cars 데이터(속도-제동거리)의 상관관계를 세 가지 방법으로 각각 검정해 비교해 보겠습니다.

cor.test(cars$speed, cars$dist)   # method 기본값 = "pearson"
#> 
#>  Pearson's product-moment correlation
#> 
#> data:  cars$speed and cars$dist
#> t = 9.464, df = 48, p-value = 1.49e-12
#> alternative hypothesis: true correlation is not equal to 0
#> 95 percent confidence interval:
#>  0.6816422 0.8862036
#> sample estimates:
#>       cor 
#> 0.8068949 

피어슨 상관계수는 약 0.81이고, p값이 극히 작아(1.49e-12) 속도와 제동거리 사이에 강한 양의 상관관계가 있다는 것이 통계적으로 뒷받침됩니다. 같은 자료를 스피어만·켄달 방법으로도 검정해 보면 방법에 따라 상관계수 값 자체는 조금씩 다르지만, 결론(유의한 양의 상관관계)은 일치합니다.

cor.test(cars$speed, cars$dist, method = "spearman")
#> 
#>  Spearman's rank correlation rho
#> 
#> data:  cars$speed and cars$dist
#> S = 3532.8, p-value = 8.825e-14
#> alternative hypothesis: true correlation is not equal to 0
#> sample estimates:
#>       rho 
#> 0.8303568 

cor.test(cars$speed, cars$dist, method = "kendall")
#> 
#>  Kendall's rank correlation tau
#> 
#> data:  cars$speed and cars$dist
#> z = 6.6655, p-value = 2.638e-11
#> alternative hypothesis: true correlation is not equal to 0
#> sample estimates:
#>       tau 
#> 0.6689901 

speed에 같은 값(동점)이 여러 개 있어서, 스피어만 방법에서는 R이 다음과 같은 경고를 출력합니다.

경고메시지(들): 
cor.test.default(cars$speed, cars$dist, method = "spearman")에서:
  tie가 있어 정확한 p값을 계산할 수 없습니다

이 역시 안내일 뿐, 결과 해석에는 문제가 없습니다.

💡 어떤 상관계수를 골라야 할까요? 산점도를 먼저 그려 보고, 관계가 대체로 직선에 가깝고 뚜렷한 이상치가 없다면 피어슨을, 관계는 있지만 곡선에 가깝거나 이상치가 있다면 스피어만을 우선 고려하는 것이 일반적인 실무 지침입니다. 9.4절에서 살펴본 것처럼 cars 데이터의 속도-제동거리 관계는 완전한 직선이 아니라 위로 살짝 휘어지는 곡선에 가까웠다는 점을 떠올려 보면, 이 자료에서 스피어만 상관계수(0.830)가 피어슨 상관계수(0.807)보다 근소하게 더 크게 나온 것도 자연스러운 결과입니다.

chisq.test()

chisq.test(x, y = NULL, correct = TRUE, p = rep(1/length(x), length(x)), rescale.p = FALSE, simulate.p.value = FALSE, B = 2000)는 범주형 자료에 대해 카이제곱 검정을 실행하는 함수로, 용법에 따라 적합도 검정과 독립성 검정 두 가지 역할을 모두 수행합니다.

  • x : 적합도 검정에서는 각 범주의 관측도수를 담은 벡터(또는 table() 결과), 독립성 검정에서는 행×열 분할표(matrix)입니다.
  • y : x가 아직 표로 정리되지 않은 팩터(벡터)일 때 함께 넘기는 두 번째 팩터입니다. x가 이미 분할표라면 사용하지 않습니다.
  • correct : 2×2 분할표에 대해 prop.test()와 같은 Yates의 연속성 수정을 적용할지 여부(기본값 TRUE).
  • p : 적합도 검정에서 각 범주에 대한 귀무가설 비율(기본값은 모든 범주가 동일한 비율이라는 뜻의 rep(1/length(x), length(x))).
  • rescale.p : p의 합이 1이 아닐 때 자동으로 재조정할지 여부.
  • simulate.p.value : TRUE로 지정하면 카이제곱분포를 이용한 근사 대신 몬테카를로 시뮬레이션으로 p값을 계산합니다. 기대도수가 작아 카이제곱 근사가 부정확할 때 유용합니다.
  • B : simulate.p.value = TRUE일 때의 시뮬레이션 반복 횟수(기본값 2000).

적합도 검정

주사위를 60번 던진 결과가 "모든 눈이 나올 확률이 같다"는 가정에 부합하는지 검정합니다. 아래 예제는 6이 잘 나오도록 편향된 주사위를 가정해 set.seed()로 만든 가상 자료입니다.

set.seed(42)
rolls <- sample(1:6, 60, replace = TRUE, prob = c(.1, .1, .1, .1, .1, .5))
(obs <- table(factor(rolls, levels = 1:6)))
#> 
#>  1  2  3  4  5  6 
#> 13  5  4  7  6 25 

chisq.test(obs)   # p를 지정하지 않으면 모든 범주가 동일 비율(1/6)이라고 가정
#> 
#>  Chi-squared test for given probabilities
#> 
#> data:  obs
#> X-squared = 32, df = 5, p-value = 5.941e-06

p값이 매우 작으므로(5.941e-06), 이 주사위의 눈이 균등하게 나온다는 가정은 기각됩니다. 관측도수(6이 25번)를 보면 실제로도 6 쪽으로 크게 치우쳐 있어 결과가 자연스럽게 이해됩니다.

독립성 검정

prop.test()에서 살펴본 Titanic의 성별 × 생존여부 표로, "성별과 생존 여부가 서로 독립적인가"(= 관계가 없는가)를 검정합니다.

chisq.test(tab)
#> 
#>  Pearson's Chi-squared test with Yates' continuity correction
#> 
#> data:  tab
#> X-squared = 454.5, df = 1, p-value < 2.2e-16

X-squared 값(454.5)과 p값이 앞서 prop.test()로 얻은 결과와 정확히 일치하는 것을 볼 수 있습니다. 2×2 표에서 두 방법은 사실상 같은 검정을 다른 이름으로 부르는 것이기 때문입니다. 귀무가설 하에서 "독립이라면 각 칸에 몇 명씩 있어야 하는가"를 나타내는 기대도수는 $expected로 확인할 수 있습니다.

chisq.test(tab)$expected
#>         Survived
#> Sex             No      Yes
#>   Male   1171.8264 559.1736
#>   Female  318.1736 151.8264

실제 관측도수(남성 생존 367명)가 기대도수(559명)보다 훨씬 적다는 데서, 남성의 생존 비율이 "독립"이라 가정했을 때보다 뚜렷하게 낮았음을 다시 확인할 수 있습니다.

주의: 카이제곱 검정은 기대도수가 5 미만인 칸이 많으면 근사가 부정확해집니다. 아래 fisher.test() 절의 예제로 이를 직접 확인해 보겠습니다.

fisher.test()

fisher.test(x, y = NULL, ..., or = 1, alternative = "two.sided", conf.int = TRUE, conf.level = 0.95, simulate.p.value = FALSE, B = 2000)는 분할표에 대해 카이제곱 근사를 쓰지 않고 초기하분포(hypergeometric distribution)로부터 정확한 p값을 계산하는 피셔의 정확검정(Fisher's exact test)입니다.

  • x : 분할표(행렬), 또는 y와 함께 넘기는 팩터.
  • or : 2×2 표에서 귀무가설의 오즈비(odds ratio) 기준값(기본값 1, 즉 "연관성이 없다").
  • simulate.p.value : 2×2보다 큰 표에서 정확검정 계산량이 지나치게 많을 때, 몬테카를로 근사로 대체할지 여부.

이 검정은 원래 1935년 로널드 피셔가 "홍차에 우유를 먼저 넣었는지 나중에 넣었는지 맛으로 구별할 수 있다"는 한 여성의 주장을 검증하기 위해 고안한 것으로 유명합니다. 이 발상을 본떠, 8잔 중 4잔은 우유를 먼저 넣고 4잔은 나중에 넣은 뒤 맞혀 보게 한 결과가 다음과 같다고 가정해 보겠습니다.

tea <- matrix(c(3, 1, 1, 3), nrow = 2,
              dimnames = list(예측 = c("우유먼저", "홍차먼저"),
                               실제 = c("우유먼저", "홍차먼저")))
tea
#>           실제
#> 예측     우유먼저 홍차먼저
#>   우유먼저        3        1
#>   홍차먼저        1        3

fisher.test(tea)
#> 
#>  Fisher's Exact Test for Count Data
#> 
#> data:  tea
#> p-value = 0.4857
#> alternative hypothesis: true odds ratio is not equal to 1
#> 95 percent confidence interval:
#>    0.2117329 621.9337505
#> sample estimates:
#> odds ratio 
#>   6.408309 

표본이 8개뿐이라 8잔 중 6잔을 맞힌 것(순전히 우연으로도 종종 나올 수 있는 수준)만으로는 p값이 0.4857로 크게 나와, "맛으로 구별하는 능력이 있다"고 결론짓기는 이릅니다. 같은 표를 chisq.test()로 검정하면 R이 다음과 같은 경고를 함께 출력합니다.

chisq.test(tea)
#> 
#>  Pearson's Chi-squared test with Yates' continuity correction
#> 
#> data:  tea
#> X-squared = 0.5, df = 1, p-value = 0.4795
#> Warning message:
#> In chisq.test(tea) : Chi-squared approximation may be incorrect

"카이제곱 근사가 부정확할 수 있다"는 이 경고가 바로 표본이 작을 때 chisq.test() 대신 fisher.test()를 써야 하는 이유입니다. 이 예제에서는 두 검정의 p값(0.4795 vs 0.4857)이 크게 다르지 않지만, 칸의 기대도수가 더 작아질수록 두 값의 차이는 커질 수 있습니다.

ks.test()

ks.test(x, y, ..., alternative = c("two.sided", "less", "greater"), exact = NULL, simulate.p.value = FALSE, B = 2000)는 콜모고로프-스미르노프 검정(Kolmogorov-Smirnov test)으로, 두 누적분포함수 사이의 최대 거리(D 통계량)를 이용해 분포의 모양을 비교합니다. y에 무엇을 넘기느냐에 따라 역할이 둘로 나뉩니다.

  • x : 검정할 표본.
  • y : 문자열로 분포함수 이름("pnorm", "pexp" 등, 9.1절 참고)을 지정하면 일표본 검정(x가 그 분포를 따르는지)이 되고, 다른 수치형 벡터를 지정하면 이표본 검정(x와 y가 같은 분포에서 나왔는지)이 됩니다.
  • ... : y가 분포함수 이름일 때, 그 분포의 모수(예: pnorm의 mean·sd)를 추가로 전달합니다.

일표본 검정(분포 적합도)

set.seed(1)
x <- rnorm(50, mean = 170, sd = 8)
ks.test(x, "pnorm", mean = mean(x), sd = sd(x))
#> 
#>  Exact one-sample Kolmogorov-Smirnov test
#> 
#> data:  x
#> D = 0.088539, p-value = 0.7954
#> alternative hypothesis: two-sided

p값이 커서, x가 (표본에서 추정한 평균·표준편차를 갖는) 정규분포를 따른다는 가정을 기각할 근거가 없습니다.

주의: 위 예제처럼 분포의 모수(mean, sd)를 같은 표본에서 추정해 검정에 사용하면, ks.test()가 원래 전제하는 "비교 대상 분포가 미리 완전히 정해져 있다"는 조건을 벗어나 p값이 이론적으로 정확하지 않게 됩니다. 정규성만 확인하는 것이 목적이라면 이런 문제가 없는 shapiro.test()를 사용하는 것이 더 적절합니다. ks.test()의 일표본 형태는 정규분포가 아닌 다른 특정 분포(지수분포, 균등분포 등)와의 적합도를 확인할 때, 또는 이표본 검정처럼 모수 추정 문제가 없는 상황에서 더 진가를 발휘합니다.

이표본 검정

두 변속기 그룹의 연비 분포가 같은 모양인지를, 평균만 비교하는 t검정과 달리 분포 전체의 모양(퍼짐, 치우침 등)까지 함께 비교해 봅니다.

ks.test(mtcars$mpg[mtcars$am == 0], mtcars$mpg[mtcars$am == 1])
#> 
#>  Exact two-sample Kolmogorov-Smirnov test
#> 
#> data:  mtcars$mpg[mtcars$am == 0] and mtcars$mpg[mtcars$am == 1]
#> D = 0.63563, p-value = 0.001909
#> alternative hypothesis: two-sided

p값이 0.001909로 작아, 두 그룹은 평균뿐 아니라 분포의 모양 자체가 다르다고 볼 수 있습니다. 이는 앞서 t.test()·wilcox.test()로 확인한 "평균(또는 위치)이 다르다"는 결론과 같은 방향을 가리키면서도, ks.test()는 평균 차이뿐 아니라 분산이나 분포 모양의 차이까지 함께 잡아낼 수 있다는 점에서 더 포괄적인 검정입니다. 다만 그만큼 "정확히 무엇이 다른가"에 대해서는 t검정보다 구체적인 답을 주지 못한다는 한계도 있습니다.


함수 무엇을 검정하는가 집단 수 정규성 가정 정규성이 어려울 때의 대안
t.test() 평균 1~2개(대응 포함) 필요 wilcox.test()
var.test() 분산 2개 필요(민감함) fligner.test()
bartlett.test() 분산 3개 이상 필요(민감함) fligner.test()
wilcox.test() 위치(순위 기반) 1~2개(대응 포함) 불필요 —
kruskal.test() 위치(순위 기반) 3개 이상 불필요 —
prop.test() 비율 1개 이상 해당 없음(대표본 근사) binom.test()·fisher.test()(소표본)
binom.test() 이항 확률 1개 해당 없음(정확검정) —
cor.test() 상관관계 변수 2개 피어슨만 필요 method = "spearman"·"kendall"
chisq.test() 적합도·독립성 범주 여러 개 해당 없음(대표본 근사) fisher.test()(소표본)
fisher.test() 독립성 범주 여러 개 해당 없음(정확검정) —
ks.test() 분포 전체의 모양 1~2개 불필요 —

💡 여러 번 검정할 때는 다중비교 보정을 고려하세요. 유의수준 α = 0.05는 "검정을 한 번 했을 때 우연히 유의하다고 잘못 판단할 확률이 5%"라는 뜻입니다. 그런데 같은 자료로 검정을 여러 번 반복하면(예: 세 그룹을 두 개씩 짝지어 t검정을 3번 실행) 적어도 하나는 우연히 유의하게 나올 확률이 5%보다 훨씬 커집니다. p.adjust(p, method = ...)는 이런 상황에서 여러 개의 p값을 한꺼번에 보정해 주는 함수입니다.

raw_p <- c(0.001, 0.012, 0.018, 0.03, 0.04, 0.05)
p.adjust(raw_p, method = "bonferroni")   # 검정 횟수만큼 곱해 보수적으로 보정
#> [1] 0.006 0.072 0.108 0.180 0.240 0.300
p.adjust(raw_p, method = "BH")           # Benjamini-Hochberg, 덜 보수적인 대안
#> [1] 0.006 0.036 0.036 0.045 0.048 0.050

kruskal.test()나 aov()(9.3절)처럼 "적어도 한 그룹은 다르다"까지만 알려주는 검정 다음에 "구체적으로 어느 그룹끼리 다른가"를 알고 싶다면, 짝별 비교와 다중비교 보정을 한 번에 처리해 주는 pairwise.t.test()·pairwise.wilcox.test()를 사용할 수 있습니다.

pairwise.t.test(PlantGrowth$weight, PlantGrowth$group)
#> 
#>  Pairwise comparisons using t tests with pooled SD 
#> 
#> data:  PlantGrowth$weight and PlantGrowth$group 
#> 
#>      ctrl  trt1 
#> trt1 0.194 -    
#> trt2 0.175 0.013
#> 
#> P value adjustment method: holm 

ctrl와 trt2 사이(p = 0.013)만 유의하고 나머지 조합은 유의하지 않다는 것을 한 번에 확인할 수 있습니다.

💡 p값과 함께 효과크기도 보고하는 것이 최근의 흐름입니다. 앞서 살펴본 mtcars의 변속기별 연비 차이(t검정 p = 0.001374)로 돌아가, "얼마나" 다른지를 나타내는 효과크기(effect size)를 코헨의 d(Cohen's d, 두 평균의 차이를 합동표준편차로 나눈 값)로 직접 계산해 볼 수 있습니다. 별도 패키지 없이도 다음과 같이 계산됩니다.

g0 <- mtcars$mpg[mtcars$am == 0]
g1 <- mtcars$mpg[mtcars$am == 1]
n0 <- length(g0); n1 <- length(g1)
sp <- sqrt(((n0 - 1) * var(g0) + (n1 - 1) * var(g1)) / (n0 + n1 - 2))  # 합동표준편차
(d <- (mean(g1) - mean(g0)) / sp)
#> [1] 1.477947

코헨의 경험칙(d = 0.2, 0.5, 0.8을 각각 작은·중간·큰 효과로 보는 기준)에 비추면, 1.48은 매우 큰 효과에 해당합니다. p값이 "차이가 우연이 아니라고 믿을 만한가"를 알려준다면, 효과크기는 "그 차이가 실질적으로 얼마나 큰가"를 알려준다는 점에서 서로 보완적입니다.