회귀 분석: 상관, 인과, 그리고 최소제곱

두 변수의 관계를 숫자로 표현하는 법을 익혀요. 상관계수와 그 한계, 그래프를 안 그리면 속는 앤스컴의 네 데이터, 상관과 인과를 가르는 교란 변수, 최소제곱 회귀선과 정규 방정식, 다중 회귀, 결론이 뒤집히는 심슨의 역설, 칭찬이 역효과처럼 보이는 평균으로의 회귀까지 다뤄요. 머신러닝의 첫 모델이 바로 여기서 나와요.

중급강의 6 / 8약 13분

"공부를 1시간 더 하면 점수가 몇 점 오를까?", "집이 10평 넓으면 값이 얼마나 비쌀까?", "광고비를 늘리면 매출이 얼마나 늘까?" 이렇게 한 변수가 다른 변수와 어떻게 함께 움직이는지를 숫자로 표현하는 도구가 회귀 분석이에요.

회귀는 머신러닝의 출발점이기도 해요. 머신러닝 트랙에서 처음 만나는 모델이 선형 회귀이고, 신경망은 선형 회귀 비슷한 계산을 여러 층으로 쌓은 것이거든요. 오늘 배우는 것이 그대로 AI의 첫 벽돌이 돼요.

상관계수: 함께 움직이는 정도

학생 8명의 공부 시간과 시험 점수예요.

공부 시간12345678
점수5255616470717881

공부 시간이 길수록 점수가 높아요. 이 "함께 움직이는 정도"를 하나의 숫자로 나타낸 것이 상관계수 rr이에요.

r=cov(X,Y)σX σYr = \frac{\text{cov}(X, Y)}{\sigma_X \, \sigma_Y}

기댓값과 분산 강의에서 배운 공분산을 두 표준편차로 나눠서, 단위와 상관없이 -1에서 1 사이의 값이 되게 만든 거예요.

  • rr이 1에 가까우면: 한쪽이 클수록 다른 쪽도 커요.
  • rr이 -1에 가까우면: 한쪽이 클수록 다른 쪽은 작아요.
  • rr이 0에 가까우면: 직선 관계가 없어요.
import numpy as np

hours = np.array([1, 2, 3, 4, 5, 6, 7, 8])
score = np.array([52, 55, 61, 64, 70, 71, 78, 81])
print(round(np.corrcoef(hours, score)[0, 1], 3))   # 0.995

상관계수 0이 "관계없음"은 아니다

"직선 관계"라는 단서가 중요해요. y=x2y = x^2은 xx가 정해지면 yy가 완벽하게 정해지는 관계인데도, xx가 -3에서 3까지 고르게 있으면 상관계수는 정확히 0이에요.

x = np.arange(-3, 4)
print(np.corrcoef(x, x ** 2)[0, 1])   # 0.0

왼쪽에서는 내려가고 오른쪽에서는 올라가서, 직선으로 보면 상쇄돼 버리거든요. 상관계수는 직선 관계의 세기만 재요.

숫자만 보면 속는다: 앤스컴의 네 데이터

1973년 통계학자 앤스컴은 일부러 만든 네 개의 데이터를 발표했어요. 네 데이터는 이런 숫자들이 모두 같아요.

값
xx의 평균9.0
yy의 평균7.5
yy의 분산4.12
상관계수0.816
회귀선y=3.00+0.500xy = 3.00 + 0.500x

그런데 그래프를 그리면 완전히 달라요. 첫째는 평범한 직선 관계, 둘째는 깔끔한 곡선, 셋째는 완벽한 직선에 이상치 하나, 넷째는 xx가 거의 다 8인데 점 하나가 멀리 떨어져 회귀선을 혼자 결정해요. 요약 숫자가 같아도 데이터의 이야기는 전혀 달라요. 분석 전에 반드시 그래프부터 그리세요. 실습에 데이터를 넣어 뒀으니 직접 그려 보세요.

상관관계는 인과관계가 아니다

아이스크림이 많이 팔리는 날 물놀이 사고도 많아요. 그럼 아이스크림이 사고를 부를까요? 기온이 높으면 둘 다 늘어날 뿐이에요. 이렇게 두 변수에 동시에 영향을 주는 숨은 변수를 교란 변수라고 해요.

1년치 데이터를 시뮬레이션해 볼게요. 사고는 오직 기온 때문에만 늘어나게 만들었어요.

rng = np.random.default_rng(3)
temp = rng.uniform(0, 35, 365)                          # 1년 동안의 기온
ice = 20 + 3 * temp + rng.normal(0, 10, 365)            # 아이스크림 판매량
drown = 0.5 + 0.1 * temp + rng.normal(0, 0.8, 365)      # 물놀이 사고
print(round(np.corrcoef(ice, drown)[0, 1], 2))          # 0.75

아이스크림과 사고의 상관계수가 0.75나 돼요. 인과 관계가 전혀 없는데도요. 상관관계가 생기는 이유는 크게 네 가지예요.

  1. A가 B의 원인: 진짜 인과
  2. B가 A의 원인: 거꾸로 된 인과. "병원에 자주 가는 사람이 더 아프다"는 병원이 병을 만들어서가 아니에요.
  3. C가 둘 다의 원인: 교란 변수. 기온, 나이, 소득이 흔한 교란 변수예요.
  4. 우연: 변수를 수백 개 뒤지면 아무 관계 없는 것끼리도 우연히 높은 상관이 나와요. 가설 검정 강의의 "여러 번 검정하기"와 같은 문제예요.

인과를 확인하는 가장 확실한 방법은 무작위 실험이에요. 사람들을 무작위로 두 그룹에 나누면 교란 변수가 양쪽에 고르게 퍼지니까, 남는 차이는 처치 때문이라고 말할 수 있어요. A/B 테스트가 강력한 이유예요.

최소제곱 회귀선

이제 관계를 직선으로 표현해 볼게요.

y^=a+bx\hat{y} = a + b x

직선이 예측한 값과 실제 값의 차이 ei=yi−y^ie_i = y_i - \hat{y}_i를 잔차라고 해요. 가장 좋은 직선은 잔차를 제곱해서 더한 값이 가장 작은 직선이에요. 그래서 최소제곱법이에요.

min⁡a,b∑i(yi−(a+bxi))2\min_{a, b} \sum_i \left(y_i - (a + b x_i)\right)^2

미적분 트랙에서 배운 대로 aa와 bb로 각각 편미분해서 0으로 놓고 풀면 답이 깔끔하게 나와요.

b=∑(xi−xˉ)(yi−yˉ)∑(xi−xˉ)2=rsysx,a=yˉ−bxˉb = \frac{\sum (x_i - \bar{x})(y_i - \bar{y})}{\sum (x_i - \bar{x})^2} = r \frac{s_y}{s_x}, \qquad a = \bar{y} - b \bar{x}

두 번째 식은 회귀선이 언제나 평균점 (xˉ,yˉ)(\bar{x}, \bar{y})을 지난다는 뜻이에요.

b = ((hours - hours.mean()) * (score - score.mean())).sum() / ((hours - hours.mean()) ** 2).sum()
a = score.mean() - b * hours.mean()
print(round(b, 2), round(a, 2))   # 4.21 47.54

pred = a + b * hours
r2 = 1 - ((score - pred) ** 2).sum() / ((score - score.mean()) ** 2).sum()
print(round(r2, 3))   # 0.989

"공부를 1시간 더 하면 점수가 약 4.2점 오른다"는 관계가 나왔어요. 공부를 안 해도 47.5점에서 시작한다는 뜻의 절편도 함께요.

결정계수 R2R^2

R2R^2은 점수의 전체 변동 중에서 직선이 설명하는 비율이에요. 0.989면 점수 차이의 98.9%를 공부 시간으로 설명한다는 뜻이에요. 단순 회귀에서는 R2=r2R^2 = r^2이에요. 이 예시는 일부러 깔끔하게 만든 데이터라 높게 나왔고, 실제 사람에 대한 데이터에서는 0.3만 나와도 꽤 의미 있는 경우가 많아요.

왜 하필 제곱일까

절댓값을 더해도 될 텐데 왜 제곱일까요? 이유는 세 가지예요.

  1. 미분이 깔끔해서 공식으로 답이 나와요.
  2. 큰 오차를 더 무겁게 벌줘요. 10만큼 틀린 것이 1만큼 틀린 것보다 100배 나쁘게 쳐져요.
  3. 오차가 정규 분포를 따른다고 가정하면, 최소제곱이 그 데이터가 나올 확률을 가장 크게 만드는 답(최대 가능도 추정)과 같아요.

대신 2번 때문에 이상치에 약해요. 앤스컴의 넷째 데이터처럼 점 하나가 직선을 끌고 갈 수 있어요.

행렬로 보면: 정규 방정식

선형대수 트랙에서 배운 형태로 쓰면 모든 회귀가 한 줄이 돼요. 각 행이 학생 한 명이고, 첫 열은 절편을 위한 1이에요.

X=[1112⋮⋮18],β=[ab]X = \begin{bmatrix} 1 & 1 \\ 1 & 2 \\ \vdots & \vdots \\ 1 & 8 \end{bmatrix}, \qquad \boldsymbol{\beta} = \begin{bmatrix} a \\ b \end{bmatrix}

Xβ=yX\boldsymbol{\beta} = \mathbf{y}는 식이 8개, 미지수가 2개라 정확한 해가 없어요. 대신 잔차의 제곱합을 최소로 만드는 해가 정규 방정식으로 나와요.

β=(XTX)−1XTy\boldsymbol{\beta} = (X^T X)^{-1} X^T \mathbf{y}

기하학적으로는 y\mathbf{y}를 XX의 열들이 만드는 공간에 정사영한 거예요. 실제로는 역행렬을 직접 구하지 않고, 수치적으로 안정한 방법(내부적으로 SVD를 쓰는)을 써요.

X = np.column_stack([np.ones(len(hours)), hours])   # 첫 열은 절편용 1
beta, *_ = np.linalg.lstsq(X, score, rcond=None)
print(beta.round(2))   # [47.54  4.21]

공식으로 구한 값과 같아요. 이 형태의 장점은 변수가 몇 개로 늘어나도 그대로 쓸 수 있다는 거예요.

다중 회귀: 다른 조건을 고정하고 보기

변수가 여러 개면 XX에 열을 더하기만 하면 돼요. 그리고 각 계수의 뜻이 바뀌어요. "다른 변수들이 같을 때" 그 변수가 한 단위 늘면 결과가 얼마나 변하는가예요.

아이스크림 예시로 돌아가 볼게요.

X1 = np.column_stack([np.ones(365), ice])
X2 = np.column_stack([np.ones(365), ice, temp])
print(np.linalg.lstsq(X1, drown, rcond=None)[0].round(4))   # [-0.1614  0.0325]
print(np.linalg.lstsq(X2, drown, rcond=None)[0].round(4))   # [0.3333 0.0007 0.1051]

아이스크림만 넣으면 "100개 더 팔리면 사고가 3.25건 늘어난다"는 엉뚱한 결론이 나와요. 기온을 함께 넣으면 아이스크림의 계수는 0.0007로 사실상 0이 되고, 기온의 계수가 0.105로 진짜 관계(우리가 만든 0.1)를 거의 정확히 찾아내요. 기온이 같은 날끼리 비교하면 아이스크림은 사고와 상관이 없다는 뜻이에요.

다만 다중 회귀는 데이터에 있는 교란 변수만 통제해요. 측정하지 않은 교란 변수는 여전히 결론을 속일 수 있어요. 그래서 관찰 데이터의 회귀 결과로 인과를 주장할 때는 늘 조심해야 해요.

심슨의 역설

1986년 신장 결석 치료법 두 가지를 비교한 실제 연구 데이터예요.

치료 A치료 B
작은 결석93% (81/87)87% (234/270)
큰 결석73% (192/263)69% (55/80)
전체78% (273/350)83% (289/350)

작은 결석에서도 A가 낫고, 큰 결석에서도 A가 나아요. 그런데 합치면 B가 나아 보여요. 어떻게 이럴 수 있을까요?

A는 주로 치료가 어려운 큰 결석에 쓰였고(263명), B는 주로 쉬운 작은 결석에 쓰였기 때문이에요(270명). 결석 크기가 치료법 선택과 성공률 모두에 영향을 준 교란 변수였던 거예요. 이 경우 믿어야 할 것은 크기별로 나눈 결과, 즉 A가 낫다는 결론이에요.

어느 쪽을 믿을지는 숫자가 아니라 인과 구조가 정해요. "무엇이 무엇에 영향을 주는가"를 먼저 생각해야 같은 데이터에서 올바른 결론을 고를 수 있어요.

평균으로의 회귀

"회귀"라는 이름은 19세기 골턴의 연구에서 왔어요. 키가 아주 큰 부모의 자녀는 대체로 부모보다 덜 크고, 아주 작은 부모의 자녀는 부모보다 덜 작았어요. 극단적인 값이 다음에는 평균 쪽으로 돌아오는 현상이에요.

시험 점수가 "실력 + 그날의 운"으로 정해진다고 해 볼게요.

rng = np.random.default_rng(10)
skill = rng.normal(70, 8, 10_000)              # 진짜 실력
test1 = skill + rng.normal(0, 8, 10_000)       # 1차 시험 = 실력 + 그날의 운
test2 = skill + rng.normal(0, 8, 10_000)       # 2차 시험 = 실력 + 새로운 운
top = test1 >= np.percentile(test1, 90)
bottom = test1 <= np.percentile(test1, 10)
print(test1[top].mean().round(1), test2[top].mean().round(1))         # 89.9 79.5
print(test1[bottom].mean().round(1), test2[bottom].mean().round(1))   # 49.8 59.9

1차 시험 상위 10%는 2차에서 평균 10점 떨어지고, 하위 10%는 10점 올라요. 아무도 아무것도 하지 않았는데요. 1차에서 극단적이었던 사람들은 실력도 좋았지만 운도 좋았던 사람들이 섞여 있고, 그 운은 2차에서 다시 오지 않기 때문이에요.

이걸 모르면 엉뚱한 결론을 내려요.

  • 비행 교관들이 "잘했을 때 칭찬하면 다음엔 못하고, 못했을 때 혼내면 다음엔 잘한다"고 믿었던 사례가 유명해요. 칭찬이나 꾸중과 상관없이 평균으로 돌아온 것뿐이었어요.
  • 증상이 가장 심할 때 병원에 가니, 무엇을 해도 그 뒤에는 대체로 나아져요. 효과 없는 치료도 효과 있어 보이는 이유예요. 그래서 진짜 효과를 보려면 대조군이 필요해요.
  • 작년에 성적이 가장 나빴던 지점에 새 제도를 도입하면, 제도가 효과가 없어도 다음 해 성적은 오르기 쉬워요.

결과의 불확실성 읽기

회귀 계수도 표본에서 추정한 값이라 표준오차와 신뢰 구간이 있어요.

from scipy import stats

res = stats.linregress(hours, score)
t = stats.t.ppf(0.975, len(hours) - 2)
print(round(res.slope, 2), round(res.stderr, 3))   # 4.21 0.179
print(round(res.slope - t * res.stderr, 2), round(res.slope + t * res.stderr, 2))   # 3.78 4.65
print(round(a + b * 40))   # 216

기울기의 95% 신뢰 구간은 3.78에서 4.65예요. 그리고 마지막 줄은 외삽의 위험을 보여 줘요. 40시간 공부하면 216점이라는 불가능한 예측이 나와요. 회귀선은 데이터가 있는 범위(1에서 8시간) 안에서만 믿을 수 있어요. 실제로는 점수가 100점에서 멈추고, 공부 시간이 너무 길면 오히려 피곤해서 떨어질 수도 있죠.

잔차를 그려 보기

회귀선을 구한 뒤에는 잔차를 xx에 대해 그려 보세요. 잔차가 0 주위에 아무 패턴 없이 흩어져 있어야 좋은 모델이에요. 잔차가 U자 모양이면 관계가 곡선이라는 뜻이고, 오른쪽으로 갈수록 퍼지면 오차의 크기가 일정하지 않다는 뜻이에요.

AI와의 연결

  • 선형 회귀는 활성화 함수가 없는 뉴런 하나예요. 입력에 가중치를 곱해 더하고 편향을 더하는 것, 정확히 a+bxa + bx의 확장이에요.
  • 최소제곱의 목표 함수는 딥러닝의 평균 제곱 오차(MSE) 손실과 같아요. 정규 방정식 대신 경사 하강법으로 풀어도 같은 답에 도착하고, 데이터가 수억 개일 때는 경사 하강법만 현실적이에요.
  • 결과가 "합격/불합격"처럼 두 가지라면, 직선의 출력을 시그모이드 함수에 넣어 확률로 바꾼 로지스틱 회귀를 써요. 분류 모델의 기본형이에요.
  • 교란 변수와 인과의 문제는 AI에서도 그대로 나타나요. 폐렴 환자의 사망 위험을 예측하는 모델이 "천식 환자는 위험이 낮다"고 배운 유명한 사례가 있어요. 천식 환자는 바로 집중 치료를 받아서 결과가 좋았던 것이었죠. 모델은 상관을 배울 뿐, 인과를 알지 못해요.

실습

  1. 앤스컴의 네 데이터를 직접 그려 보세요. 각각의 회귀선과 상관계수를 구해서 같은지 확인하세요.
x = [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5]
y1 = [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68]
y2 = [9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74]
y3 = [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73]
x4 = [8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8]
y4 = [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89]
  1. 공부 시간 데이터에 "20시간 공부하고 60점"인 학생을 하나 추가하면 기울기가 얼마나 변하는지 보세요. 이상치 하나의 힘을 느낄 수 있어요.
  2. 평균으로의 회귀 시뮬레이션에서 운의 크기(표준편차 8)를 2로 줄이면 상위 10%의 점수 하락이 어떻게 변하나요? 운이 클수록 회귀가 커지는 이유를 설명해 보세요.
  3. 아이스크림 시뮬레이션에서 기온 없이 "요일" 같은 상관없는 변수를 넣으면 아이스크림 계수가 줄어드나요?

자주 하는 실수

  • 상관을 인과로 읽기. 교란 변수, 거꾸로 된 인과, 우연을 먼저 의심하세요.
  • 그래프 없이 숫자만 보기. 앤스컴의 네 데이터를 떠올리세요.
  • 데이터 범위 밖으로 외삽하기. 직선은 영원히 이어지지 않아요.
  • 평균으로의 회귀를 효과로 착각하기. 극단값을 골라 개입했다면 대조군과 비교해야 해요.
  • 합친 데이터만 보기. 심슨의 역설처럼 나눠 보면 결론이 뒤집힐 수 있어요.

점검 문제

  1. 상관계수가 0이면 두 변수는 아무 관계가 없나요?
  2. 회귀선이 반드시 지나는 점은?
  3. r=0.6r = 0.6인 단순 회귀의 R2R^2은?
  4. 다중 회귀에서 어떤 변수의 계수는 어떻게 해석하나요?
  5. 지난달 매출이 가장 낮았던 매장 10곳에 새 교육을 했더니 이번 달 매출이 올랐어요. 교육 효과라고 결론 내리기 전에 무엇을 의심해야 하나요?

정답

  1. 아니에요. 직선 관계가 없다는 뜻일 뿐, y=x2y = x^2처럼 곡선 관계는 있을 수 있어요.
  2. 평균점 (xˉ,yˉ)(\bar{x}, \bar{y})이에요.
  3. 0.62=0.360.6^2 = 0.36이에요. 변동의 36%를 설명해요.
  4. 다른 변수들이 같다고 할 때, 그 변수가 한 단위 늘면 결과가 계수만큼 변한다는 뜻이에요.
  5. 평균으로의 회귀예요. 가장 낮았던 매장은 운이 나빴던 경우가 섞여 있어서 교육 없이도 오르기 쉬워요. 교육하지 않은 비슷한 매장(대조군)과 비교해야 해요.

다음 강의

다음 강의에서는 확률을 "새 증거가 들어올 때마다 갱신하는 믿음"으로 보는 베이즈 추론을 배워요. 양성 판정 문제를 베이즈 정리로 다시 풀고, 스팸 필터와 동전의 편향 추정, 그리고 딥러닝의 정규화가 사실 베이즈의 사전 분포라는 연결까지 확인해요.