상관계수를 예측 정확도로 쓰면 안 되는 이유

상관성(Correlation)과 일치성(Agreement)의 결정적 차이

피어슨 상관계수가 높다고 해서 예측이 정확한 것은 아니다. 상관계수는 두 변수의 선형적 패턴이 일치하는지를 측정할 뿐, 실제 값과 예측 값의 절대적 일치를 보장하지 않는다. 본 문서는 상관성과 일치성의 수학적 차이, 상관계수가 정확도 지표로 부적절한 통계적 근거, 그리고 MSE, CCC 등 올바른 예측 평가 지표를 다룬다.

Statistics
Model Evaluation
Data Science
저자

Kwangmin Kim

공개

2026년 03월 16일

1 문제 제기

예측 모델의 성능을 평가할 때 피어슨 상관계수(\(r\))만 사용하는 것은 통계적으로 불충분한 분석이다. 피어슨 상관계수는 두 변수 사이의 ’상관성(Correlation)’을 측정할 뿐, 실제 값과 예측 값 사이의 ’일치성(Agreement)’을 보장하지 않기 때문이다.

이 주장은 종종 “상관계수가 높으니 예측이 정확하다”는 형태로 나타난다. 이는 ’필요조건’을 ’충분조건’으로 오인한 통계적 오류이다. 정확한 예측이라면 상관계수가 높아야 하는 것은 맞지만, 상관계수가 높다고 해서 반드시 정확한 예측인 것은 아니다.

2 수학적 반례: 상관계수의 맹점

2.1 상수 편향 (Constant Bias)

실제값이 \(y = [1, 2, 3]\)일 때 두 가지 예측 모델을 비교하자:

  • 모델 A (완벽한 예측): \(\hat{y} = [1, 2, 3]\)
  • 모델 B (상수 편향): \(\hat{y} = [11, 12, 13]\)
지표 모델 A 모델 B
피어슨 \(r\) 1.0 1.0
\(MSE\) 0 100
\(MAE\) 0 10

모델 B는 실제값보다 항상 10만큼 크게 예측하고 있음에도 불구하고, 실제값과 변화의 방향 및 비율이 완벽하게 직선 관계를 이루기 때문에 \(r = 1.0\)이 나온다.

2.2 스케일 편향 (Scale Bias)

  • 모델 C (2배 과대 예측): \(\hat{y} = [2, 4, 6]\)
지표 모델 A 모델 C
피어슨 \(r\) 1.0 1.0
\(MSE\) 0 4.67

모델 C도 \(r = 1.0\)이다. 모든 값을 2배로 과대 예측하지만 피어슨 상관계수는 이를 감지하지 못한다.

3 수학적 근거: 왜 감지하지 못하는가

3.1 선형 변환의 불변성 (Invariance under Linear Transformation)

피어슨 상관계수의 정의상, 예측값에 임의의 양수 상수 \(a(>0)\)를 곱하거나 \(b\)를 더해도 상관계수는 변하지 않는다:

\[Cor(y, \hat{y}) = Cor(y, a\hat{y} + b), \quad a > 0\]

이는 피어슨 상관계수가 평균(\(\bar{y}\))으로부터의 편차를 표준편차(\(s\))로 나누어 정규화하기 때문이다. 정규화 과정에서 값의 절대적 크기 차이(Scale)와 평행 이동(Shift) 정보가 모두 소실된다.

중요

만약 실제 매출이 100억인데 모델이 항상 200억으로 예측(\(a=2\))하거나, 항상 110억으로 예측(\(b=10\))하더라도 상관계수는 \(1.0\)이 나온다. 이를 “정확도가 높다”고 표현하는 것은 비즈니스 의사결정에서 치명적인 오판이다.

3.2 MSE의 분해식

\(MSE\)와 상관계수의 관계를 수학적으로 분해하면 다음과 같다:

\[MSE = E[(y - \hat{y})^2] = (\mu_y - \mu_{\hat{y}})^2 + \sigma_y^2 + \sigma_{\hat{y}}^2 - 2\sigma_y \sigma_{\hat{y}} \rho_{y\hat{y}}\]

이 식에서:

  • \((\mu_y - \mu_{\hat{y}})^2\): 평균의 차이 (상수 편향)
  • \(\sigma_y^2 + \sigma_{\hat{y}}^2 - 2\sigma_y \sigma_{\hat{y}} \rho\): 분산 불일치와 상관 항

상관계수(\(\rho\))가 1이라 하더라도 앞의 항들(평균의 차이, 분산의 차이)이 존재하면 \(MSE\)는 무한히 커질 수 있다. 이것이 상관계수만으로 정확도를 판단할 수 없는 수학적 이유이다.

4 올바른 예측 평가 지표

4.1 1. 오차 기반 지표 (절대적 정확도)

지표 수식 특징
MSE \(\frac{1}{n}\sum(\hat{y}_i - y_i)^2\) 큰 오차에 높은 패널티, 스케일 의존적
RMSE \(\sqrt{MSE}\) MSE의 제곱근, 원래 단위로 해석 가능
MAE \(\frac{1}{n}\sum|\hat{y}_i - y_i|\) 이상치에 강건, 중앙값 최적해
MAPE \(\frac{100}{n}\sum\left|\frac{y_i - \hat{y}_i}{y_i}\right|\) 스케일 독립적, \(y_i = 0\) 근처에서 불안정

\(MSE\) vs \(MAE\) 선택 기준:

  • 큰 오차가 치명적인 도메인(금융, 의료): \(MSE\) (큰 오차에 제곱 패널티)
  • 이상치가 많은 데이터: \(MAE\) (이상치에 강건)
  • 비즈니스 보고용: \(MAPE\) (비율로 직관적 해석 가능)

4.2 2. 결정계수 (\(R^2\), 설명력)

\[R^2 = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2} = 1 - \frac{SS_{res}}{SS_{tot}}\]

  • \(R^2 = 1\): 완벽한 예측
  • \(R^2 = 0\): 단순 평균 예측과 동일한 수준
  • \(R^2 < 0\): 평균 예측보다 못함
경고

단순 선형 회귀에서는 \(R^2 = r^2\)이 성립하지만, 다중 회귀나 비선형 모델에서는 \(R^2 \neq r^2\)이다. 또한 \(R^2\)는 데이터의 분산을 얼마나 설명하는지를 나타낼 뿐, 실제 오차의 크기를 직접적으로 나타내지는 않는다.

4.3 3. 일치상관계수 (CCC, Concordance Correlation Coefficient)

상관계수에 ‘정확도(Accuracy)’ 항을 추가하여, 데이터가 \(y = \hat{y}\) 선에서 벗어날수록 수치를 페널티로 깎는 지표이다.

\[CCC = \frac{2\rho\sigma_y\sigma_{\hat{y}}}{\sigma_y^2 + \sigma_{\hat{y}}^2 + (\mu_y - \mu_{\hat{y}})^2}\]

이를 분해하면:

\[CCC = \underbrace{\rho}_{\text{정밀도 (Precision)}} \times \underbrace{C_b}_{\text{정확도 (Accuracy)}}\]

여기서 \(C_b = \frac{2\sigma_y\sigma_{\hat{y}}}{\sigma_y^2 + \sigma_{\hat{y}}^2 + (\mu_y - \mu_{\hat{y}})^2}\)이다.

시나리오 \(r\) \(C_b\) \(CCC\)
완벽한 예측 1.0 1.0 1.0
상수 편향 (+10) 1.0 0.02 0.02
스케일 편향 (x2) 1.0 0.80 0.80
무작위 예측 0.0 - 0.0

CCC는 상관계수가 높더라도 편향이 있으면 수치를 깎기 때문에, 진정한 의미의 일치성을 측정한다.

import numpy as np

def concordance_correlation_coefficient(y_true, y_pred):
    """일치상관계수(CCC) 계산"""
    mean_true = np.mean(y_true)
    mean_pred = np.mean(y_pred)
    var_true = np.var(y_true)
    var_pred = np.var(y_pred)
    covariance = np.mean((y_true - mean_true) * (y_pred - mean_pred))

    numerator = 2 * covariance
    denominator = var_true + var_pred + (mean_true - mean_pred) ** 2

    return numerator / denominator

# 반례 시연
y_true = np.array([1, 2, 3, 4, 5])
y_perfect = np.array([1, 2, 3, 4, 5])      # 완벽한 예측
y_shifted = np.array([11, 12, 13, 14, 15])  # 상수 편향
y_scaled = np.array([2, 4, 6, 8, 10])       # 스케일 편향

print(f"완벽 예측 - r: {np.corrcoef(y_true, y_perfect)[0,1]:.3f}, "
      f"CCC: {concordance_correlation_coefficient(y_true, y_perfect):.3f}")
print(f"상수 편향 - r: {np.corrcoef(y_true, y_shifted)[0,1]:.3f}, "
      f"CCC: {concordance_correlation_coefficient(y_true, y_shifted):.3f}")
print(f"스케일 편향 - r: {np.corrcoef(y_true, y_scaled)[0,1]:.3f}, "
      f"CCC: {concordance_correlation_coefficient(y_true, y_scaled):.3f}")

4.4 4. 잔차 분석 (Residual Analysis)

수치 지표만으로는 오차의 패턴을 파악할 수 없다. 잔차(\(e_i = y_i - \hat{y}_i\))를 시각화하여 다음을 점검해야 한다:

  • 잔차 vs 예측값 산점도: 잔차가 무작위로 분포하는지 확인. 패턴이 보이면 모델에 시스템적 편향이 있음
  • 잔차의 정규성: Q-Q plot으로 잔차가 정규분포를 따르는지 확인
  • 등분산성: 예측값이 커질수록 잔차가 커지는 이분산(Heteroscedasticity) 여부 확인

5 지표의 올바른 조합

예측 모델의 성능을 종합적으로 평가하려면 여러 지표를 역할별로 분리하여 사용해야 한다:

역할 추천 지표 확인 사항
절대적 오차 RMSE, MAE 예측값이 실제값과 얼마나 가까운가
설명력 \(R^2\) 모델이 변동성의 몇 %를 설명하는가
일치성 CCC 예측값이 실제값과 체계적으로 일치하는가
방향성 피어슨 \(r\) (보조) 모델이 경향성이라도 파악하고 있는가
오차 패턴 잔차 분석 시스템적 편향이 있는가
중요

데이터 과학자는 \(L_2\) Loss(\(MSE\))를 최소화하는 방향으로 모델을 튜닝하지, 상관계수를 높이는 방향으로 튜닝하지 않는다. 상관계수는 모델이 데이터의 “순서(Ranking)”나 “흐름”을 파악하고 있는지 확인하는 보조 지표일 뿐이다.

6 실무에서의 논증 전략

“상관계수가 높으니 정확도가 높다”는 주장을 반박해야 하는 상황에서, 다음과 같은 논증이 효과적이다:

  1. 반례 제시: 상수 편향 모델(\(\hat{y} = y + 100\))은 \(r = 1.0\)이지만 \(RMSE = 100\)이다. “상관계수는 1인데 오차가 수조 원이 넘는 모델을 신뢰할 수 있는가?”
  2. 시각화: 실제값 vs 예측값 산점도에 \(y = \hat{y}\) 대각선을 그리고, 데이터가 대각선에서 벗어나 있음을 보여준다.
  3. CCC 활용: 동일한 데이터에 대해 \(r\)\(CCC\)를 함께 계산하여, \(r\)은 높지만 \(CCC\)는 낮은 경우를 실증한다.
import matplotlib.pyplot as plt
import numpy as np

y_true = np.array([10, 20, 30, 40, 50])
y_biased = y_true + 15  # 상수 편향

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# 산점도
axes[0].scatter(y_true, y_biased, s=100)
axes[0].plot([0, 60], [0, 60], 'r--', label='y = ŷ (완벽한 예측)')
axes[0].set_xlabel('실제값')
axes[0].set_ylabel('예측값')
axes[0].set_title(f'r = {np.corrcoef(y_true, y_biased)[0,1]:.2f}')
axes[0].legend()

# 잔차
residuals = y_biased - y_true
axes[1].bar(range(len(residuals)), residuals)
axes[1].axhline(y=0, color='r', linestyle='--')
axes[1].set_xlabel('관측치')
axes[1].set_ylabel('잔차 (예측 - 실제)')
axes[1].set_title(f'RMSE = {np.sqrt(np.mean(residuals**2)):.1f}')

plt.tight_layout()
plt.show()

Subscribe

Enjoy this blog? Get notified of new posts by email: