산점도와 상관관계
개념
키와 몸무게처럼 짝을 이룬 두 변량을 좌표평면 위의 점으로 나타낸 그래프가 산점도입니다. 한 변량이 커질 때 다른 변량도 커지는 경향이 있으면 양의 상관관계, 반대로 작아지는 경향이 있으면 음의 상관관계가 있다고 합니다.
\[-1 \;\le\; r \;\le\; 1\]
상관관계가 있다고 해서 인과관계가 있는 것은 아닙니다. 아이스크림 판매량과 물놀이 사고는 함께 늘지만, 원인은 둘 다 “더운 날씨”입니다.
상관관계의 방향과 강도를 하나의 수로 나타낸 것이 상관계수 \(r\)입니다. \(r\)이 1에 가까울수록 점들이 오른쪽 위로 향하는 직선에 가깝게 모이고, \(-1\)에 가까울수록 오른쪽 아래로 향하는 직선에 모이며, 0 근처이면 뚜렷한 직선 경향이 없습니다. 아래 앱에서 \(r\)을 움직여 산점도의 모양이 어떻게 변하는지 관찰해 보세요.
만지며 배우기
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 640
# matplotlib·numpy 없이 stdlib + 인라인 SVG 로만 그린다
import random
from math import sqrt
from shiny import App, reactive, render, ui
ACCENT = "#2563eb"
INK = "#111827"
MUTED = "#6b7280"
GRID = "#e5e7eb"
LO, HI = -3.5, 3.5
def fit(x, y):
# 표본상관계수 r, 최소제곱 추세선(기울기·절편)을 stdlib 로 계산
n = len(x)
mx, my = sum(x) / n, sum(y) / n
sxy = sum((a - mx) * (b - my) for a, b in zip(x, y))
sxx = sum((a - mx) ** 2 for a in x)
syy = sum((b - my) ** 2 for b in y)
if sxx == 0 or syy == 0:
return 0.0, 0.0, my
return sxy / sqrt(sxx * syy), sxy / sxx, my - (sxy / sxx) * mx
def scatter_svg(x, y, trend, slope, intercept):
W, H, ml, mr, mt, mb = 440, 340, 26, 14, 14, 28
pw, ph = W - ml - mr, H - mt - mb
def X(v):
return ml + ((v - LO) / (HI - LO)) * pw
def Y(v):
return mt + ph - ((v - LO) / (HI - LO)) * ph
axes = (f'<line x1="{X(LO):.1f}" y1="{Y(0):.1f}" x2="{X(HI):.1f}" y2="{Y(0):.1f}" stroke="{GRID}"/>'
f'<line x1="{X(0):.1f}" y1="{Y(LO):.1f}" x2="{X(0):.1f}" y2="{Y(HI):.1f}" stroke="{GRID}"/>')
dots = "".join(
f'<circle cx="{X(a):.1f}" cy="{Y(b):.1f}" r="3.4" fill="{ACCENT}" opacity="0.7"/>'
for a, b in zip(x, y) if LO <= a <= HI and LO <= b <= HI
)
line = ""
if trend:
line = (f'<line x1="{X(LO):.1f}" y1="{Y(slope*LO+intercept):.1f}" '
f'x2="{X(HI):.1f}" y2="{Y(slope*HI+intercept):.1f}" '
f'stroke="{INK}" stroke-width="1.6"/>')
return (
f'<svg viewBox="0 0 {W} {H}" style="width:100%;height:auto;'
'font-family:-apple-system,sans-serif;">'
f'{axes}{line}{dots}'
f'<text x="{X(HI)-4:.0f}" y="{Y(0)-6:.0f}" font-size="11" fill="{MUTED}" '
'text-anchor="end">x</text>'
f'<text x="{X(0)+8:.0f}" y="{mt+10:.0f}" font-size="11" fill="{MUTED}">y</text>'
'</svg>'
)
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_slider("r", "상관계수 r", min=-1.0, max=1.0, value=0.7, step=0.05),
ui.input_slider("n", "점의 개수 n", min=20, max=200, value=60, step=5),
ui.input_checkbox("trend", "추세선 보기", False),
ui.input_action_button("regen", "다시 생성"),
width=240,
),
ui.card(
ui.card_header("산점도 — r이 점들의 모양을 정합니다"),
ui.output_ui("cloud"),
),
ui.card(ui.output_ui("readout")),
fillable=True,
)
def server(input, output, session):
@reactive.calc
def points():
input.regen() # 버튼을 누르면 새로 생성
r, n = input.r(), input.n()
x = [random.gauss(0, 1) for _ in range(n)]
e = [random.gauss(0, 1) for _ in range(n)]
y = [r * a + sqrt(max(0.0, 1 - r * r)) * ei for a, ei in zip(x, e)]
return x, y
@render.ui
def cloud():
x, y = points()
rs, slope, intercept = fit(x, y)
return ui.HTML(scatter_svg(x, y, input.trend(), slope, intercept))
@render.ui
def readout():
x, y = points()
rs, _, _ = fit(x, y)
if abs(rs) < 0.1:
desc = "상관관계가 거의 없습니다"
else:
direction = "양" if rs > 0 else "음"
if abs(rs) >= 0.7:
strength = "강한"
elif abs(rs) >= 0.3:
strength = "어느 정도의"
else:
strength = "약한"
desc = f"{strength} {direction}의 상관관계가 보입니다"
return ui.HTML(
'<p class="hs-readline">'
f"설정한 r = {input.r():.2f} · 이 표본에서 계산한 상관계수 = {rs:.3f} — {desc}. "
"점의 개수 n이 작을수록 두 값의 차이가 커지고, "
"'다시 생성'을 누를 때마다 표본상관계수가 조금씩 달라집니다.</p>"
)
app = App(app_ui, server)
상관계수가 \(r\)인 자료를 만들고 확인하는 핵심 코드입니다.
import random
from math import sqrt
r, n = 0.7, 60
x = [random.gauss(0, 1) for _ in range(n)]
e = [random.gauss(0, 1) for _ in range(n)]
y = [r * a + sqrt(1 - r * r) * ei for a, ei in zip(x, e)]
# 표본상관계수 (피어슨) — numpy 없이
mx, my = sum(x) / n, sum(y) / n
sxy = sum((a - mx) * (b - my) for a, b in zip(x, y))
sxx = sum((a - mx) ** 2 for a in x)
syy = sum((b - my) ** 2 for b in y)
print(sxy / sqrt(sxx * syy)) # r 에 가까움
print(sxy / sxx, my - sxy / sxx * mx) # 추세선 기울기·절편스스로 확인
힌트문제 1 — r = 1(또는 −1)로 맞추면 점들은 어떤 모양이 되는가?
모든 점이 하나의 직선 위에 완벽하게 늘어섭니다. \(r = 1\)이면 오른쪽 위로, \(r = -1\)이면 오른쪽 아래로 향하는 직선입니다. 상관계수의 절댓값은 “점들이 직선에 얼마나 가깝게 모이는가”를 재는 수임을 확인해 보세요.
힌트문제 2 — r = 0.3, n = 20으로 두고 ’다시 생성’을 여러 번 누르면 계산된 상관계수는 얼마나 출렁이는가?
표본이 작으면 계산된 상관계수가 0 근처부터 0.6 이상까지 크게 출렁입니다. 심지어 부호가 뒤집히기도 합니다. 적은 자료에서 관찰한 상관관계를 성급하게 믿으면 안 되는 이유입니다. n을 200으로 키워 비교해 보세요.