콘텐츠로 이동

효과크기와 Cramér의 V

개요

카이제곱 검정통계량은 통계적으로 유의한 연관이 있는지는 알려주지만 그 연관이 얼마나 강한지는 알려주지 않는다. 표본이 크면 독립으로부터의 사소한 이탈만으로도 매우 유의한 p-값이 나올 수 있다. 효과크기 측도는 표본크기와 무관하게 연관의 크기를 재어 이 한계를 보완한다.

Cramér의 V

Cramér의 V는 카이제곱 검정에서 가장 널리 쓰이는 효과크기 측도이다. 다음으로 정의한다:

\[ V = \sqrt{\frac{\chi^2}{n \cdot (q - 1)}} \]

여기서

  • \(\chi^2\)는 카이제곱 검정통계량,
  • \(n\)은 전체 표본크기,
  • \(q = \min(r, c)\)는 행의 수 \(r\)과 열의 수 \(c\) 중 작은 값

이다.

성질

  • \(V\)의 범위는 0부터 1까지이다.
  • \(V = 0\)은 연관이 없음(완전한 독립)을 뜻한다.
  • \(V = 1\)은 완전한 연관을 뜻한다.
  • \(V\)는 대칭이다. 어느 변수를 행에 두고 어느 변수를 열에 두는지에 의존하지 않는다.

해석 지침

Cramér의 V 해석
0.00 – 0.10 무시할 만함
0.10 – 0.30 작음
0.30 – 0.50 중간
0.50 이상 큼

이 문턱들은 대략적이며 맥락에 따라 달라진다. 어떤 분야에서는 "작은" 효과크기도 실질적으로 의미가 있을 수 있다.

특수한 경우: 2×2 표

\(2 \times 2\) 표에서는 \(q - 1 = 1\)이므로 Cramér의 V가 다음으로 단순해진다:

\[ V = \sqrt{\frac{\chi^2}{n}} = |\phi| \]

여기서 \(\phi\)는 파이 계수로, \(2 \times 2\) 표에서 연관을 재는 또 다른 흔한 측도이다.

보기 1. Cramer의 V로 효과크기 재기. 성별 × 주로 쓰는 손 표(\(3\times2\), \(n = 2{,}237\))를 쓴다. 이 표의 \(\chi^2 = 11.8061\) 은 \(p = 0.0027\) 로 강하게 유의하다.

(1) \(p_{ij} = O_{ij}/n\) 이라 두면

\[ \chi^2 = n\left(\sum_{i,j}\frac{p_{ij}^2}{p_{i\cdot}\,p_{\cdot j}} - 1\right) \]

임을 보이시오.

(2) (1)로부터 \(\chi^2 \le n\bigl(\min(r,c) - 1\bigr)\) 임을 보이고, 따라서 \(0 \le V \le 1\) 임을 보이시오. 등호가 성립하는 표는 어떤 표인가.

(3) 이 표의 \(V\) 를 구해 해석 지침에 비추어 판정하시오. 같은 \(n\) 에서 \(V\) 가 얼마를 넘어야 유의해지는가. 거꾸로 이 \(V\) 가 유의해지려면 \(n\) 이 얼마여야 하는가.

(4) 코드로 확인하시오.

풀이

(1) 비율로 다시 쓰기. 어긋남 제곱을 전개하는 대신 익숙한 변형을 쓴다.

\[ \chi^2 = \sum_{i,j}\frac{(O_{ij}-E_{ij})^2}{E_{ij}} = \sum_{i,j}\frac{O_{ij}^2}{E_{ij}} - 2\sum_{i,j}O_{ij} + \sum_{i,j}E_{ij} = \sum_{i,j}\frac{O_{ij}^2}{E_{ij}} - n \]

마지막 등식은 \(\sum O_{ij} = \sum E_{ij} = n\) 을 썼다. 이제 \(O_{ij} = np_{ij}\), \(E_{ij} = R_iC_j/n = n\,p_{i\cdot}p_{\cdot j}\) 를 넣으면

\[ \sum_{i,j}\frac{O_{ij}^2}{E_{ij}} = \sum_{i,j}\frac{n^2p_{ij}^2}{n\,p_{i\cdot}p_{\cdot j}} = n\sum_{i,j}\frac{p_{ij}^2}{p_{i\cdot}p_{\cdot j}} \]

이므로 주장한 식이다. \(\square\) \(\chi^2/n\) 은 비율만으로 적힌다. 도수를 전부 \(k\) 배 해도 \(p_{ij}\) 는 그대로이므로 \(\chi^2\) 은 정확히 \(k\) 배가 된다. 효과크기를 \(n\) 으로 나누어 정의해야 하는 까닭이 이 한 줄에 다 있다.

(2) 상한. \(S = \sum_{i,j} p_{ij}^2/(p_{i\cdot}p_{\cdot j})\) 를 위에서 누른다. 열 \(j\) 를 고정하고 행에 대해 먼저 더한다. \(p_{ij} \le p_{i\cdot}\) 이므로 \(p_{ij}/p_{i\cdot} \le 1\) 이고

\[ \sum_i \frac{p_{ij}^2}{p_{i\cdot}} = \sum_i p_{ij}\cdot\frac{p_{ij}}{p_{i\cdot}} \le \sum_i p_{ij} = p_{\cdot j} \]

다. 이것을 \(p_{\cdot j}\) 로 나누어 열에 대해 더하면

\[ S = \sum_j \frac{1}{p_{\cdot j}}\sum_i \frac{p_{ij}^2}{p_{i\cdot}} \le \sum_{j=1}^c \frac{p_{\cdot j}}{p_{\cdot j}} = c \]

이므로 \(\chi^2 = n(S-1) \le n(c-1)\) 이다. 행과 열의 역할을 바꾸어도 같은 계산이므로 \(\chi^2 \le n(r-1)\) 이고, 합치면

\[ \chi^2 \le n\bigl(\min(r,c)-1\bigr) = n(q-1) \]

이다. 따라서 \(V^2 = \chi^2/\{n(q-1)\} \in [0,1]\) 이고 \(0 \le V \le 1\) 이다. \(\square\)

등호는 언제인가. 위 부등식에서 \(p_{ij}/p_{i\cdot} \le 1\) 이 등호가 되려면, \(p_{ij} > 0\) 인 모든 칸에서 \(p_{ij} = p_{i\cdot}\) 이어야 한다. 곧 행마다 도수가 한 칸에만 몰려 있어야 한다(\(c \le r\) 일 때). 행을 알면 열이 결정되는 표, 곧 완전한 예측이다. 예를 들어

\[ \begin{pmatrix} 50 & 0 \\ 0 & 50 \\ 30 & 0 \end{pmatrix} \quad\Longrightarrow\quad \chi^2 = 130 = n(q-1), \quad V = 1 \]

이다(\(n = 130\), \(q = 2\)).

(3) 이 표의 \(V\). \(q - 1 = \min(3,2) - 1 = 1\) 이므로

\[ V = \sqrt{\frac{11.8061}{2237 \times 1}} = \sqrt{0.0052777} = 0.072648 \]

해석 지침의 첫 칸 "무시할 만함"(\(0.00 \sim 0.10\)) 에 들어간다. "작음" 의 경계인 \(0.10\) 에도 못 미친다.

유의해지는 문턱을 \(V\) 로 적어 보면 사정이 분명해진다. \(\chi^2 = n(q-1)V^2\) 이므로 \(\chi^2 \ge \chi^2_{2,\,0.05} = 5.99146\) 은

\[ V \ge \sqrt{\frac{5.99146}{2237}} = 0.051753 \]

과 같은 말이다. \(n = 2{,}237\) 에서는 \(V\) 가 \(0.0518\) 만 넘으면 유의해진다. 관측된 \(0.0726\) 은 그 문턱의 \(1.4\) 배일 뿐이다.

거꾸로 \(V = 0.072648\) 을 고정하고 \(n\) 을 묻는다.

\[ n \ge \frac{5.99146}{0.072648^2} = \frac{5.99146}{0.0052777} = 1135.25 \]

곧 같은 비율로 \(1{,}136\) 명만 모았어도 유의했을 것이다. 실제 표본은 그 두 배다. 반대로 \(n = 300\) 이었다면 \(\chi^2 = 300 \times 0.0052777 = 1.583\) 으로 \(p = 0.45\), 전혀 유의하지 않다.

같은 자료가 유의하기도 하고 아니기도 하다. 바뀐 것은 \(n\) 뿐이다. \(V\) 는 그동안 \(0.0726\) 에 그대로 있다. 참고로 \(H_0\) 이 참일 때에도 \(E[\chi^2] = \text{df} = 2\) 이므로 \(V\) 는 \(\sqrt{2/2237} = 0.0299\) 쯤 나온다. 관측된 \(0.0726\) 은 그 \(2.4\) 배다. 0 은 아니지만 크지도 않다 는 것이 이 수들이 말하는 전부다.

(4) 수치적으로.

import numpy as np
from scipy import stats

def cramers_v(observed):
    """분할표의 Cramér V를 계산한다.

    V는 카이제곱을 표본크기로 나눠 정규화한 것이라 0과 1 사이에 놓인다.
    카이제곱 통계량 자체는 n에 비례해 커지므로 연구 사이 비교에 쓸 수 없지만
    V는 쓸 수 있다는 것이 요점이다.
    """
    chi2, p_value, df, expected = stats.chi2_contingency(observed)
    n = observed.sum()
    # q는 행 수와 열 수 중 작은 것에서 1을 뺀 값이다.
    # 카이제곱의 최댓값이 n*q라서 이것으로 나누면 상한이 1이 된다.
    q = min(observed.shape) - 1
    v = np.sqrt(chi2 / (n * q))
    return v, chi2, p_value

# 보기: 성별과 손잡이
observed = np.array([[934, 1070], [113, 92], [20, 8]])
v, chi2, p_value = cramers_v(observed)

print(f"Chi-square statistic: {chi2:.4f}")
print(f"p-value: {p_value:.4f}")
print(f"Cramér's V: {v:.4f}")

# (1) 비율로 적은 항등식
n = observed.sum()
P = observed / n
pr, pc = P.sum(1, keepdims=True), P.sum(0, keepdims=True)
print(f"\n항등식  n(S-1) = {n * ((P**2 / (pr * pc)).sum() - 1):.6f}"
      f"   정의대로 = {chi2:.6f}")

# (2) 상한. 완전히 예측되는 표에서 등호가 성립한다.
perfect = np.array([[50, 0], [0, 50], [30, 0]])
c2p = stats.chi2_contingency(perfect)[0]
print(f"완전 예측 표: chi2 = {c2p:.1f}  n(q-1) = {perfect.sum() * 1}"
      f"  V = {np.sqrt(c2p / perfect.sum()):.1f}")

# (3) 유의 문턱을 V 로, 그리고 n 으로
crit = stats.chi2.ppf(0.95, 2)
print(f"\nn = {n:.0f} 에서 유의해지는 V 문턱 {np.sqrt(crit / n):.6f}")
print(f"V = {v:.6f} 가 유의해지는 n   {crit / v**2:.2f}")
print(f"H0 아래 V 의 대략적 크기 sqrt(df/n) = {np.sqrt(2 / n):.6f}")
for m in [300, 1136, 2237, 20000]:
    x = m * v**2
    print(f"  n = {m:6d}:  chi2 = {x:8.3f}   p = {stats.chi2(2).sf(x):.3g}"
          f"   V = {v:.4f}")

출력:

Chi-square statistic: 11.8061
p-value: 0.0027
Cramér's V: 0.0726

항등식  n(S-1) = 11.806135   정의대로 = 11.806135
완전 예측 표: chi2 = 130.0  n(q-1) = 130  V = 1.0

n = 2237 에서 유의해지는 V 문턱 0.051753
V = 0.072648 가 유의해지는 n   1135.25
H0 아래 V 의 대략적 크기 sqrt(df/n) = 0.029901
  n =    300:  chi2 =    1.583   p = 0.453   V = 0.0726
  n =   1136:  chi2 =    5.995   p = 0.0499   V = 0.0726
  n =   2237:  chi2 =   11.806   p = 0.00273   V = 0.0726
  n =  20000:  chi2 =  105.553   p = 1.2e-23   V = 0.0726

(1)의 항등식이 여섯째 자리까지 맞는다. (2)의 상한도 완전히 예측되는 표에서 chi2 = 130.0 과 n(q-1) = 130 이 같아 등호가 실제로 달성됨을 보인다.

(3)의 두 문턱 0.051753 과 1135.25 도 손계산 그대로다. 마지막 표가 요점을 한눈에 보인다. \(V\) 열은 네 줄 모두 \(0.0726\) 으로 똑같은데 p-값은 \(0.45\) 에서 \(1.2\times10^{-23}\) 까지 간다.

\(p = 0.0027\) 로 강하게 기각되지만 Cramér의 V는 \(0.073\) 에 불과하다. Cohen의 기준으로 "작음"인 \(0.10\) 에도 못 미친다.

\(n = 2237\) 이라 아주 약한 연관도 통계적으로는 또렷하게 잡히기 때문이다. p-값은 효과의 크기가 아니라 증거의 강도를 잰다. 표가 큰 자료에서 카이제곱 검정 결과만 보고하고 효과크기를 빼면 독자가 오해하기 쉽다.

표본을 키우면 무엇이 변하고 무엇이 변하지 않는가

칸 비율을 그대로 둔 채 표본크기만 바꾸면 카이제곱 통계량은 직선으로 커지지만 Cramér의 V는 수평선으로 일정하다

위 손잡이 자료의 여섯 칸 비율을 그대로 유지한 채 표본크기 \(n\)만 바꿔 보았다. 왼쪽 (가)에서 카이제곱 통계량은 완벽한 직선이다. 비율이 같으면 \(O_{ij}\)와 \(E_{ij}\)가 모두 \(n\)에 비례하므로 \((O-E)^2/E\)가 정확히 \(n\)에 비례하기 때문이다. 같은 이야기를 식으로 쓰면

\[ \chi^2 = n \cdot (q - 1) \cdot V^2 \]

인데, \(V\)와 \(q\)가 고정이면 \(\chi^2\)은 \(n\)의 일차함수다. 연관의 강도가 하나도 변하지 않았는데 통계량은 얼마든지 커진다.

그래서 유의성은 표본크기만으로 살 수 있다. 같은 비율이라도 \(n = 300\)이면 \(\chi^2 = 1.58\), \(p = 0.45\)로 전혀 유의하지 않다. 초록 점선이 가리키는 \(n = 1135\)를 넘는 순간 통계량이 임계값 5.991을 넘어서고, 실제 자료인 \(n = 2237\)에서는 \(\chi^2 = 11.81\), \(p = 0.0027\)이 된다. 같은 비율을 \(n = 20000\)까지 키우면 \(\chi^2 = 105.6\), \(p \approx 1.2 \times 10^{-23}\)이다. 자료의 성격은 한 번도 바뀌지 않았고 표본크기만 바뀌었다.

오른쪽 (나)가 대조군이다. Cramér의 V는 \(n\)이 얼마이든 0.0726에 붙어 있는 수평선이다. 정의에서 \(n\)으로 나누었으니 당연한 일이지만, 그 당연함이 바로 효과크기를 쓰는 이유다. 0.0726은 "무시할 만함" 띠 안에 있으며 "작음"의 경계인 0.10에도 못 미친다. \(p = 0.0027\)이라는 강한 유의성과 이 수평선은 모순이 아니다. p-값은 "연관이 없다고 보기 어렵다"를 말하고, V는 "있긴 한데 아주 약하다"를 말한다. 서로 다른 질문에 대한 서로 다른 답이다.

논문이나 보고서를 읽을 때 \(\chi^2\) 값과 \(n\)이 같이 적혀 있으면 위 식을 뒤집어 \(V = \sqrt{\chi^2 / \{n(q-1)\}}\)로 효과크기를 직접 복원할 수 있다. 큰 \(\chi^2\) 값에 감탄하기 전에 \(n\)을 먼저 확인하는 습관이 필요하다.

효과크기를 언제 쓰는가

  • 특히 표본이 클 때에는 카이제곱 검정 결과와 함께 항상 효과크기를 보고하라.
  • 서로 다른 연구나 자료집합 사이에서 연관의 강도를 비교할 때 효과크기를 쓴다.
  • 통계적으로 유의하더라도 Cramér의 V가 아주 작으면 실질적으로는 중요하지 않을 수 있다.
  • 작은 표본에서 유의하지 않으나 Cramér의 V가 중간 정도라면 더 큰 표본으로 추가 조사를 해볼 만하다.

연습문제

연습문제 1. \(n = 500\)인 \(3 \times 2\) 표에 대한 카이제곱 독립성 검정에서 \(\chi^2 = 12.5\)를 얻었다. Cramér의 V를 계산하고 효과크기를 분류하라.

풀이

\(3 \times 2\) 표에서 \(\min(r-1, c-1) = \min(2, 1) = 1\)이다.

\[ V = \sqrt{\frac{\chi^2}{n \cdot \min(r-1, c-1)}} = \sqrt{\frac{12.5}{500 \times 1}} = \sqrt{0.025} \approx 0.158 \]

\(df^* = 1\)에 대한 Cohen의 기준(작음 \(\approx 0.10\), 중간 \(\approx 0.30\), 큼 \(\approx 0.50\))에 비추면 \(V = 0.158\)은 작음에서 중간 사이의 효과크기이다.

연습문제 2. 두 연구가 성별과 투표 성향의 연관에 대해 같은 가설을 검정했다. 연구 A(\(n = 100\))는 \(\chi^2 = 4.0\) (\(p = 0.046\))을, 연구 B(\(n = 10{,}000\))도 \(\chi^2 = 4.0\) (\(p = 0.046\))을 얻었다. 두 연구의 Cramér의 V를 계산하라. 무엇을 알 수 있는가?

풀이

둘 다 \(2 \times 2\) 표이므로 \(\min(r-1, c-1) = 1\)이다.

연구 A: \(V = \sqrt{4.0 / (100 \times 1)} = \sqrt{0.04} = 0.20\) (작음에서 중간 사이의 효과).

연구 B: \(V = \sqrt{4.0 / (10{,}000 \times 1)} = \sqrt{0.0004} = 0.02\) (무시할 만한 효과).

\(\chi^2\) 값과 p-값이 같은데도 효과크기는 극적으로 다르다. 연구 A는 의미 있는 연관을 찾았고, 연구 B는 통계적으로 유의하지만 실질적으로는 무시할 만한 연관을 찾았다. 유의성 검정과 함께 효과크기를 항상 보고해야 하는 이유를 보여준다.

연습문제 3. 서로 다른 연구 사이에서 연관의 강도를 비교할 때 원래의 카이제곱 통계량보다 Cramér의 V가 선호되는 이유를 설명하라.

풀이

카이제곱 통계량은 표본크기에 비례한다: \(\chi^2 \approx n \cdot V^2\). 연관의 강도가 그대로여도 \(n\)을 두 배로 하면 \(\chi^2\)가 대략 두 배가 된다. 그래서 표본크기가 다른 연구들을 \(\chi^2\)로 비교하는 것은 적절하지 않다.

Cramér의 V는 \(n\)과 표의 차원으로 정규화하여 표본크기에 의존하지 않는 \([0, 1]\) 값을 준다. 100명 연구에서의 \(V = 0.30\)은 10,000명 연구에서의 \(V = 0.30\)과 같은 강도의 연관을 나타내므로 연구 사이의 비교가 의미를 갖는다.

연습문제 4. \(2 \times 2\) 표에서 Cramér의 V가 파이 계수의 절댓값 \(|\phi|\)와 같음을 보여라.

풀이

\(2 \times 2\) 표에서는 \(\min(r-1, c-1) = \min(1, 1) = 1\)이다. 따라서

\[ V = \sqrt{\frac{\chi^2}{n \cdot 1}} = \sqrt{\frac{\chi^2}{n}} \]

이다. 파이 계수는 다음으로 정의된다:

\[ \phi = \sqrt{\frac{\chi^2}{n}} \]

(\(2 \times 2\) 표에서는 부호 규약이 따로 있다). \(V\)는 제곱근을 취하므로 항상 음이 아니며, 따라서 \(V = |\phi|\)이다. \(2 \times 2\) 표에서 Cramér의 V와 파이 계수의 절댓값은 동일한 측도이다.

연습문제 5. 두 변수가 완전히 독립일 때 크라메르의 \(V\)는 0이어야 한다. 표본이 작으면 실제로 어떤 값이 나오는지 확인하고, 보정 방법을 적용하라.

풀이

\(V\)는 항상 0 이상이다. 제곱근을 취하기 때문이다. 따라서 참값이 0이어도 추정값은 반드시 양수이고, 이는 곧 위로 편향됨을 뜻한다.

import numpy as np
from scipy import stats

rng = np.random.default_rng(909)

def cramers_v(obs):
    chi2 = stats.chi2_contingency(obs, correction=False)[0]
    n, q = obs.sum(), min(obs.shape) - 1
    return np.sqrt(chi2 / (n * q))

def cramers_v_corrected(obs):
    """베르그스마(2013)의 편향보정."""
    chi2 = stats.chi2_contingency(obs, correction=False)[0]
    n, (r, c) = obs.sum(), obs.shape
    phi2 = max(0.0, chi2 / n - (r - 1) * (c - 1) / (n - 1))
    r_t = r - (r - 1)**2 / (n - 1)
    c_t = c - (c - 1)**2 / (n - 1)
    return np.sqrt(phi2 / min(r_t - 1, c_t - 1))

print(f"{'표':>7s} {'n':>5s} {'V 평균':>9s} {'V 중앙값':>10s} {'보정 V 평균':>12s}")
layouts = [((2, 2), np.array([.5, .5]), np.array([.5, .5])),
           ((3, 3), np.ones(3) / 3, np.ones(3) / 3),
           ((4, 5), np.ones(4) / 4, np.ones(5) / 5)]
for shape, p_row, p_col in layouts:
    prob = np.outer(p_row, p_col).ravel()      # 완전 독립: 참 V = 0
    for n in [30, 100, 500]:
        vs, vc = [], []
        for _ in range(2_000):
            obs = rng.multinomial(n, prob).reshape(shape).astype(float)
            if (obs.sum(0) == 0).any() or (obs.sum(1) == 0).any():
                continue
            vs.append(cramers_v(obs))
            vc.append(cramers_v_corrected(obs))
        print(f"{str(shape):>7s} {n:5d} {np.mean(vs):9.4f} "
              f"{np.median(vs):10.4f} {np.mean(vc):12.4f}")
      표     n      V 평균      V 중앙값      보정 V 평균
 (2, 2)    30    0.1457     0.1336       0.0624
 (2, 2)   100    0.0814     0.0705       0.0351
 (2, 2)   500    0.0360     0.0313       0.0153
 (3, 3)    30    0.2487     0.2424       0.0810
 (3, 3)   100    0.1355     0.1323       0.0449
 (3, 3)   500    0.0595     0.0580       0.0188
 (4, 5)    30    0.3639     0.3617       0.0887
 (4, 5)   100    0.1975     0.1971       0.0495
 (4, 5)   500    0.0878     0.0874       0.0217

편향이 심각하다. \(4\times5\) 표에 \(n=30\)이면 두 변수가 완전히 독립인데도 \(V\)의 평균이 0.364다. 본문의 표에서는 "중간" 효과에 해당한다.

상황 참 \(V\) \(V\)의 평균 본문 기준 해석
\(2\times2\), \(n=30\) 0 0.146 작음
\(3\times3\), \(n=30\) 0 0.249 작음~중간
\(4\times5\), \(n=30\) 0 0.364 중간

표가 클수록, \(n\)이 작을수록 편향이 커진다. \(E[\chi^2]=\text{df}\)이므로

\[ E[V^2]\approx\frac{\text{df}}{n(q-1)}=\frac{(r-1)(c-1)}{n\,(q-1)} \]

이고, 이것이 자유도에 비례해 커진다.

베르그스마의 보정이 크게 개선한다. \(4\times5\)·\(n=30\)에서 0.364 → 0.089다. 아이디어는 \(\phi^2=\chi^2/n\)에서 기댓값 \(\text{df}/(n-1)\)을 빼는 것이다.

\[ \tilde\phi^2=\max\Bigl(0,\ \phi^2-\frac{(r-1)(c-1)}{n-1}\Bigr) \]

그래도 완전하지는 않다. \(n=30\)에서 0.089가 남는다. \(\max(0,\cdot)\)로 잘라 내기 때문에 여전히 위로 치우친다.

실무 지침 넷.

  1. \(n\)이 작으면 \(V\)를 액면 그대로 읽지 않는다. 특히 표가 클 때.
  2. 보정된 \(V\)를 쓴다. R의 rcompanion::cramerV(..., bias.correct=TRUE), Python의 dython 등이 지원한다.
  3. 신뢰구간을 함께 본다(다음 문제).
  4. 독립일 때의 기댓값을 계산해 견준다. 위 식 \(\sqrt{\text{df}/(n(q-1))}\)가 그 눈금이다.

연습문제 6. 크라메르 \(V\)의 신뢰구간을 비중심 카이제곱 분포를 뒤집어 구하고, 보기 1의 자료에 적용하라.

풀이

원리. \(\chi^2\sim\chi'^2_{\text{df}}(\lambda)\)이고 \(\lambda=n\,w^2=n(q-1)V^2\)이다. 관측된 \(\chi^2_{\text{obs}}\)에 대해

\[ P\bigl(\chi'^2_{\text{df}}(\lambda_L)>\chi^2_{\text{obs}}\bigr)=\frac\alpha2, \qquad P\bigl(\chi'^2_{\text{df}}(\lambda_U)<\chi^2_{\text{obs}}\bigr)=\frac\alpha2 \]

를 만족하는 \(\lambda_L,\lambda_U\)를 찾아 \(V\)로 환산한다. 이것이 검정을 뒤집어 만든 구간이다.

import numpy as np
from scipy import stats
from scipy.optimize import brentq

def ncp_ci(chi2, df, alpha=0.05, big=1e6):
    """비중심 모수 λ 의 신뢰구간.
    sf(chi2; df, λ) 는 λ 에 대해 증가하고 cdf 는 감소한다."""
    lo = (brentq(lambda l: stats.ncx2.sf(chi2, df, l) - alpha / 2, 0.0, big)
          if stats.ncx2.sf(chi2, df, 0.0) < alpha / 2 else 0.0)
    hi = (brentq(lambda l: stats.ncx2.cdf(chi2, df, l) - alpha / 2, 0.0, big)
          if stats.ncx2.cdf(chi2, df, 0.0) > alpha / 2 else 0.0)
    return lo, hi

def v_with_ci(obs, alpha=0.05):
    chi2, p, df, _ = stats.chi2_contingency(obs, correction=False)
    n, q = obs.sum(), min(obs.shape) - 1
    lo, hi = ncp_ci(chi2, df, alpha)
    return (np.sqrt(chi2 / (n * q)),
            np.sqrt(lo / (n * q)), np.sqrt(hi / (n * q)), chi2, p, df)

tables = [np.array([[934, 1070], [113, 92], [20, 8]], float),
          np.array([[12, 8], [6, 14]], float),
          np.array([[40, 10], [10, 40]], float)]
for obs in tables:
    v, lo, hi, chi2, p, df = v_with_ci(obs)
    print(f"{obs.astype(int).tolist()}")
    print(f"   n={obs.sum():.0f}  χ²={chi2:.4f}  df={df}  p={p:.4f}")
    print(f"   V = {v:.4f}   95% CI ({lo:.4f}, {hi:.4f})")
[[934, 1070], [113, 92], [20, 8]]
   n=2237  χ²=11.8061  df=2  p=0.0027
   V = 0.0726   95% CI (0.0261, 0.1116)
[[12, 8], [6, 14]]
   n=40  χ²=3.6364  df=1  p=0.0565
   V = 0.3015   95% CI (0.0000, 0.6114)
[[40, 10], [10, 40]]
   n=100  χ²=36.0000  df=1  p=0.0000
   V = 0.6000   95% CI (0.4040, 0.7960)

보기 1의 \(V=0.073\)은 \((0.026,\ 0.112)\)다. 0을 포함하지 않아(\(p=0.0027\)과 일관) 연관이 있기는 하지만, 상한조차 0.112로 "작음"의 문턱을 겨우 넘는다. "무시할 만한 연관"이라는 결론이 구간으로도 뒷받침된다.

둘째 표가 교훈적이다. \(V=0.302\)로 본문 기준의 "중간"인데, 구간이 \((0,\ 0.611)\)로 거의 전 범위다. \(n=40\)으로는 아무것도 결론지을 수 없다. 점추정만 보고 "중간 효과를 발견했다"고 쓰면 안 된다.

포함률 확인.

rng = np.random.default_rng(55)
P = np.array([[.35, .15], [.15, .35]])
p_row, p_col = P.sum(1), P.sum(0)
v_true = np.sqrt((((P - np.outer(p_row, p_col))**2)
                  / np.outer(p_row, p_col)).sum())
print(f"참 V = {v_true:.4f}")
for n in [50, 200, 1000]:
    cov = ok = 0
    for _ in range(2_000):
        obs = rng.multinomial(n, P.ravel()).reshape(2, 2).astype(float)
        if (obs.sum(0) == 0).any() or (obs.sum(1) == 0).any():
            continue
        ok += 1
        _, lo, hi, *_ = v_with_ci(obs)
        cov += lo <= v_true <= hi
    print(f"  n={n:4d}: 포함률 {cov / ok:.4f}")
참 V = 0.4000
  n=  50: 포함률 0.9560
  n= 200: 포함률 0.9680
  n=1000: 포함률 0.9695

포함률이 0.956~0.970으로 약간 보수적이다. 비중심 카이제곱 근사와 이산성 때문인데, 과대 포함은 과소 포함보다 훨씬 안전하므로 실무에서 쓸 만하다.

붓스트랩도 대안이다. 다만 표본을 재추출하면 주변합이 달라지므로, 어떤 재추출 방식을 쓸지(전체 다항, 행 고정 등)를 설계에 맞춰 정해야 한다.

연습문제 7. \(V\)가 같아도 표의 구조는 전혀 다를 수 있다. 그런 예를 만들고, 구조를 보려면 무엇을 봐야 하는지 설명하라.

풀이
import numpy as np
from scipy import stats

def report(obs, label):
    chi2, p, df, exp = stats.chi2_contingency(obs, correction=False)
    n = obs.sum()
    v = np.sqrt(chi2 / (n * (min(obs.shape) - 1)))
    p_row, p_col = obs.sum(1) / n, obs.sum(0) / n
    # 조정 표준화 잔차: H0 아래 근사적으로 N(0,1)
    adj = (obs - exp) / np.sqrt(exp * np.outer(1 - p_row, 1 - p_col))
    print(f"{label}   n={n:.0f}  χ²={chi2:.3f}  df={df}  "
          f"p={p:.4f}  V={v:.4f}")
    print(f"  조정 표준화 잔차\n{np.round(adj, 2)}")
    print(f"  |잔차| > 2 인 칸 {int((np.abs(adj) > 2).sum())}개, "
          f"최대 {np.abs(adj).max():.2f}\n")

concentrated = np.array([[102, 45, 45], [45, 45, 45], [45, 45, 45]], float)
diffuse = np.array([[64, 43, 43], [43, 64, 43], [43, 43, 64]], float)
report(concentrated, "집중형 (한 칸만 큼)")
report(diffuse, "분산형 (대각선 전체)")
집중형 (한 칸만 큼)   n=462  χ²=18.097  df=4  p=0.0012  V=0.1399
  조정 표준화 잔차
[[ 4.25 -2.3  -2.3 ]
 [-2.3   1.25  1.25]
 [-2.3   1.25  1.25]]
  |잔차| > 2 인 칸 5개, 최대 4.25

분산형 (대각선 전체)   n=450  χ²=17.640  df=4  p=0.0015  V=0.1400
  조정 표준화 잔차
[[ 2.97 -1.48 -1.48]
 [-1.48  2.97 -1.48]
 [-1.48 -1.48  2.97]]
  |잔차| > 2 인 칸 3개, 최대 2.97

\(V\)가 0.1399와 0.1400으로 사실상 같다. \(p\) 값도 0.0012와 0.0015로 비슷하다. 그런데 이야기가 전혀 다르다.

집중형 분산형
무엇이 일어났나 (1,1) 칸 하나가 크게 큼 대각선 전체가 고르게 큼
최대 잔차 4.24 2.97
해석 특정 조합의 과잉 두 변수의 일치 경향
후속 조치 그 칸을 조사 순서형 모형·일치도 지표

\(V\)는 이탈의 "총량"만 잰다. 어디서, 어떤 모양으로 이탈했는지는 말해 주지 않는다.

구조를 보려면 조정 표준화 잔차를 본다.

\[ d_{ij}=\frac{O_{ij}-E_{ij}}{\sqrt{E_{ij}(1-p_{i\cdot})(1-p_{\cdot j})}} \]

\(H_0\) 아래에서 근사적으로 \(N(0,1)\)이므로 \(|d_{ij}|>2\)면 눈에 띄는 칸이다. 분모의 \((1-p_{i\cdot})(1-p_{\cdot j})\) 보정이 없는 단순 잔차 \((O-E)/\sqrt{E}\)는 분산이 1보다 작아 이탈을 과소평가한다.

보고 절차 넷.

  1. \(\chi^2\)과 \(p\) — 전체적으로 이탈이 있는가
  2. \(V\)와 그 구간 — 얼마나 큰가
  3. 조정 잔차의 표 — 어디서 이탈했는가
  4. 잔차 여러 개를 해석할 때는 다중비교 보정 — \(r\times c\)개의 칸을 보므로 본페로니 기준 \(|d|>z_{1-0.025/(rc)}\)를 쓰는 것이 안전하다. \(3\times3\)이면 \(|d|>2.77\)이다.

마지막 항목을 적용하면 집중형에서는 \(|d|=4.24\)인 칸 하나만 살아남고, 분산형에서는 \(2.97>2.77\)인 대각선 세 칸이 살아남는다. 보정 후에도 두 표의 이야기가 다르다.

모자이크 그림을 그리면 이 구조가 한눈에 보인다. 칸의 넓이를 도수에, 색을 잔차에 대응시키는 그림이다.

연습문제 8. 크라메르 \(V\) 말고도 여러 연관 측도가 있다. 주요한 것들을 한 자료에 모두 적용해 비교하고, 언제 무엇을 쓸지 정리하라.

풀이
import numpy as np
from scipy import stats

def measures(obs, label):
    obs = obs.astype(float)
    n = obs.sum()
    chi2, p, df, _ = stats.chi2_contingency(obs, correction=False)
    r, c = obs.shape
    v = np.sqrt(chi2 / (n * (min(r, c) - 1)))
    w = np.sqrt(chi2 / n)                                  # 코헨 w
    cc = np.sqrt(chi2 / (chi2 + n))                        # 분할계수
    t = np.sqrt((chi2 / n) / np.sqrt((r - 1) * (c - 1)))   # 추프로프 T

    P = obs / n
    p_row, p_col = P.sum(1), P.sum(0)
    h_row = -(p_row * np.log(p_row)).sum()
    h_col = -(p_col * np.log(p_col)).sum()
    h_joint = -(P[P > 0] * np.log(P[P > 0])).sum()
    u = 2 * (h_row + h_col - h_joint) / (h_row + h_col)     # 불확실성 계수

    print(f"{label}   n={n:.0f}  χ²={chi2:.3f}  p={p:.4f}")
    print(f"  V {v:.4f} | 코헨 w {w:.4f} | 분할계수 C {cc:.4f} | "
          f"추프로프 T {t:.4f} | 불확실성 U {u:.4f}")
    if obs.shape == (2, 2):
        a, b, c_, d = obs.ravel()
        print(f"  오즈비 {(a * d) / (b * c_):.4f} | "
              f"위험비 {(a / (a + b)) / (c_ / (c_ + d)):.4f} | "
              f"위험차 {a / (a + b) - c_ / (c_ + d):+.4f}")

measures(np.array([[40, 10], [10, 40]]), "강한 연관 (2×2)")
measures(np.array([[55, 45], [45, 55]]), "약한 연관 (2×2)")
measures(np.array([[934, 1070], [113, 92], [20, 8]]), "보기 1 (3×2)")
강한 연관 (2×2)   n=100  χ²=36.000  p=0.0000
  V 0.6000 | 코헨 w 0.6000 | 분할계수 C 0.5145 | 추프로프 T 0.6000 | 불확실성 U 0.2781
  오즈비 16.0000 | 위험비 4.0000 | 위험차 +0.6000
약한 연관 (2×2)   n=200  χ²=2.000  p=0.1573
  V 0.1000 | 코헨 w 0.1000 | 분할계수 C 0.0995 | 추프로프 T 0.1000 | 불확실성 U 0.0072
  오즈비 1.4938 | 위험비 1.2222 | 위험차 +0.1000
보기 1 (3×2)   n=2237  χ²=11.806  p=0.0027
  V 0.0726 | 코헨 w 0.0726 | 분할계수 C 0.0725 | 추프로프 T 0.0611 | 불확실성 U 0.0050

\(2\times2\)에서 \(V\), \(w\), \(T\)가 모두 같다. 세 정의가 \(q-1=1\), \(\sqrt{(r-1)(c-1)}=1\)일 때 일치하기 때문이다.

분할계수 \(C\)는 1에 도달하지 못한다. 강한 연관(완전에 가까운 2×2)에서도 0.515다. \(C=\sqrt{\chi^2/(\chi^2+n)}\)의 상한이 \(\sqrt{(q-1)/q}\)이기 때문이다. \(2\times2\)에서는 0.707이 최대다. 표 크기가 다르면 비교할 수 없다는 뜻이라, 오늘날에는 권하지 않는다.

불확실성 계수 \(U\)는 훨씬 작게 나온다(0.278 대 \(V=0.600\)). 정보이론 기반이라 척도가 다르다. "한 변수를 알면 다른 변수의 불확실성이 몇 % 줄어드는가"를 재므로, \(V\)와 직접 비교하면 안 된다.

선택 기준.

상황 측도
일반적인 \(r\times c\) 연관의 강도 크라메르 \(V\)
검정력·표본크기 계산 코헨 \(w\)
\(2\times2\), 의학·역학 오즈비, 위험비, 위험차
예측 관점("얼마나 알려주는가") 불확실성 계수, 굿맨·크루스칼 \(\lambda\)
두 변수가 순서형 켄달 \(\tau_b\), 감마, 스피어만
두 평가자의 일치도 코헨 \(\kappa\)

중요한 구분 셋.

1 — 대칭 대 비대칭. \(V\)는 대칭이라 "어느 쪽이 원인인가"를 말하지 않는다. 굿맨·크루스칼 \(\lambda\)는 비대칭이라 방향을 지정한다.

2 — 순서 정보의 활용. 두 변수가 순서형(예: 교육수준 × 소득구간)인데 \(V\)를 쓰면 순서 정보를 버리는 셈이다. \(\tau_b\)나 선형 추세 검정이 훨씬 강력하다.

3 — \(2\times2\)에서는 오즈비가 더 유익하다. "약한 연관"의 \(V=0.10\)은 해석하기 어렵지만, 오즈비 1.49와 위험차 \(+0.10\)은 바로 와닿는다. \(V\)는 여러 표를 비교할 때, 비 지표는 하나의 표를 해석할 때 유용하다.

연습문제 9. 효과크기를 이용해 표본크기를 설계하라. 탐지하려는 \(V\)와 표의 크기에 따라 필요한 \(n\)이 어떻게 달라지는가?

풀이

연결고리. 비중심 모수가

\[ \lambda=n\,w^2=n\,(q-1)\,V^2,\qquad q=\min(r,c) \]

이므로, 목표 검정력을 주면 \(n\)을 풀 수 있다.

import numpy as np
from scipy import stats

def n_for_v(v, r, c, power=0.80, alpha=0.05):
    df, q = (r - 1) * (c - 1), min(r, c) - 1
    crit = stats.chi2.ppf(1 - alpha, df)
    for n in range(10, 200_000):
        if stats.ncx2.sf(crit, df, n * v * v * q) >= power:
            return n

header = " ".join(f"{'V=' + str(v):>8s}" for v in [0.1, 0.2, 0.3, 0.5])
print(f"{'표 크기':>8s} {'df':>4s} {header}")
for r, c in [(2, 2), (2, 3), (3, 3), (4, 5), (5, 5)]:
    row = " ".join(f"{n_for_v(v, r, c):8d}" for v in [0.1, 0.2, 0.3, 0.5])
    print(f"{f'{r}×{c}':>8s} {(r - 1) * (c - 1):4d} {row}")

print("\n보기 1 (3×2, V=0.0726, n=2237) 에 대해")
for v, label in [(0.05, "무시할 만함"), (0.10, "작음"), (0.30, "중간")]:
    print(f"  참 V={v} ({label}) 를 80% 로 탐지하려면 n = {n_for_v(v, 3, 2)}")
    표 크기   df    V=0.1    V=0.2    V=0.3    V=0.5
     2×2    1      785      197       88       32
     2×3    2      964      241      108       39
     3×3    4      597      150       67       24
     4×5   12      578      145       65       24
     5×5   16      482      121       54       20

보기 1 (3×2, V=0.0726, n=2237) 에 대해
  참 V=0.05 (무시할 만함) 를 80% 로 탐지하려면 n = 3854
  참 V=0.1 (작음) 를 80% 로 탐지하려면 n = 964
  참 V=0.3 (중간) 를 80% 로 탐지하려면 n = 108

\(n\)이 \(V^2\)에 반비례한다. \(V\)가 절반이면 표본은 4배다. \(2\times2\)에서 \(V=0.1\)은 785명, \(V=0.2\)는 197명이다.

표가 커지면 필요한 \(n\)이 오히려 줄어든다(2×3의 964에서 5×5의 482로). 직관에 반하지만 이유가 있다. \(V\)를 고정하면 \(\lambda=n(q-1)V^2\)에서 \(q-1\)이 커져 비중심 모수가 커진다. 자유도가 늘어 임계값도 오르지만, 그 효과보다 \(\lambda\)의 증가가 더 크다.

이것을 "큰 표가 유리하다"로 읽으면 안 된다. 같은 \(V\)라도 표 크기가 다르면 가리키는 연관의 실질적 내용이 다르다. 범주를 잘게 나눠 \(V\)를 유지할 수 있다는 보장이 없다.

보기 1의 설계를 되돌아보면. \(n=2237\)은 \(V=0.10\)을 잡기에 충분하고도 남는다(964면 된다). 표본이 지나치게 컸던 셈이고, 그래서 \(V=0.073\)이라는 무시할 만한 연관이 \(p=0.0027\)로 나왔다.

설계 단계에서 물어야 할 것.

  1. 실질적으로 의미 있는 최소 \(V\)는 얼마인가? 통계가 아니라 분야가 답한다.
  2. 그 \(V\)를 80%로 탐지할 \(n\)은? 위 표로 계산한다.
  3. 그보다 훨씬 큰 \(n\)이 이미 있다면, 검정보다 구간 추정을 주 결과로 삼는다.
  4. \(n\)을 못 늘린다면, 탐지 가능한 최소 \(V\)를 미리 밝히고 결과를 해석한다.

연습문제 10. 분할표 분석의 보고 양식을 정리하라.

풀이

보고할 것.

항목 이유
원 도수표 재분석의 근거. 백분율만으로는 부족
행·열 백분율 해석의 편의
\(\chi^2\), df, \(p\) 관행
\(V\)와 그 신뢰구간 크기와 정밀도
기대도수의 최솟값 근사의 타당성
조정 표준화 잔차 어디서 이탈했는가
결측·무응답의 처리 투명성

문장 예시.

성별과 손잡이의 연관을 2,237명에서 조사했다. 카이제곱 독립성 검정 결과 \(\chi^2(2)=11.81\), \(p=0.003\)으로 독립성을 기각했다. 그러나 효과크기는 크라메르 \(V=0.073\)(95% CI 0.026~0.112)로 무시할 만한 수준이었다. 모든 기대도수가 5 이상이었다. 조정 표준화 잔차는 왼손잡이·양손잡이 범주에서 가장 컸으나, 도수가 작아 해석에 주의가 필요하다.

자주 하는 실수 여섯.

  1. \(p\)만 보고하고 \(V\)를 빠뜨린다. \(n\)이 크면 \(p\)는 거의 언제나 작다.
  2. \(V\)의 점추정만 보고한다. \(n=40\)에서 \(V=0.30\)의 구간은 \((0,\ 0.61)\)이다(연습문제 6).
  3. 소표본 \(V\)를 액면대로 읽는다. 독립이어도 \(4\times5\)·\(n=30\)이면 평균 0.36이다(연습문제 5).
  4. \(V\)만 보고 구조를 안다고 생각한다. 잔차를 봐야 한다(연습문제 7).
  5. 순서형 변수에 \(V\)를 쓴다. 순서 정보를 버리는 것이다.
  6. 백분율만 싣고 원 도수를 생략한다. 독자가 검증할 수 없다.

\(V\)를 해석하는 세 가지 기준.

기준 방법
관례 코헨의 0.1/0.3/0.5 — 가장 약한 근거
분야 비교 같은 분야 다른 연구의 \(V\)와 견줌
귀무 기댓값 독립일 때의 \(E[V]\approx\sqrt{\text{df}/(n(q-1))}\)와 견줌

셋째 기준이 가장 유용하다. 보기 1이면 \(\sqrt{2/(2237\times1)}=0.030\)인데 관측된 \(V=0.073\)이므로 그 2.4배다. 코헨의 표에 기대는 것보다 이런 비교가 정직하다.

마지막으로 — 유의성과 중요성을 분리해 쓰는 습관. "통계적으로 유의했으나 효과크기는 무시할 만했다"라는 문장을 쓸 수 있어야 한다. 이 둘이 다르다는 것을 인정하는 것이 분할표 분석에서 가장 중요한 태도다.


정리하며

\(\chi^2\) 은 유의성을 말하고 크라메르의 \(V\) 는 크기를 말한다.

\[ V=\sqrt{\frac{\chi^2}{n\,(q-1)}}, \qquad q=\min(r,c) \]
  • \(n\) 으로 나누는 것이 요점이다. \(\chi^2\) 은 표본이 커지면 함께 커지지만 \(V\) 는 그렇지 않다. 표본크기와 무관한 연관의 강도를 준다.
  • \(V\in[0,1]\) 이라 비교가 가능하다. \(0\) 이면 완전 독립, \(1\) 이면 완전 연관이며, 표의 크기가 달라도 견줄 수 있다.
  • \(2\times2\) 에서는 \(\phi\) 계수와 같다.
  • 큰 표본에서 특히 필요하다. \(n\) 이 수만이면 사소한 이탈도 \(p<0.001\) 을 내므로, 유의성만 보고하면 오해를 부른다. \(V\) 가 \(0.05\) 라면 통계적으로 유의해도 실질적으로는 무시할 만하다.
  • 해석 기준은 분야마다 다르다. 코헨의 관례가 있지만 절대적이지 않으며, 같은 분야의 다른 연구와 견주는 것이 더 유용하다.

다음 절 Fisher의 정확검정으로 10장을 마무리한다.