왜도와 첨도로 읽는 데이터 분포의 비대칭과 꼬리
왜도와 첨도로 데이터 분포의 비대칭과 꼬리 두께를 진단하고, 변환·이상치 탐지·모델 가정 검증에 활용하는 방법
2026-08-14 · 최초 발행 2024-04-29
평균과 분산 뒤에 남는 분포의 모양
데이터가 같은 평균과 분산을 가져도 한쪽으로 길게 늘어진 꼬리나 극단값의 집중 정도는 다를 수 있다. 왜도(skewness)는 비대칭의 크기와 방향을, 첨도(kurtosis)는 꼬리 두께와 중심 집중도를 설명하는 데 쓰인다.
오른쪽 꼬리가 더 길면 왜도는 양(+)이고, 왼쪽 꼬리가 우세하면 음(−)이다. 모멘트 기반 왜도는 표준화된 3차 중심 모멘트로 정의한다.
첨도는 흔히 분포가 “뾰족한지”를 말하는 지표로 소개되지만, 꼬리 두께로 해석하는 편이 정확하다. Fisher 정의의 초과첨도(excess kurtosis)는 정규분포에서 0이며, 양(+)이면 두터운 꼬리(heavy tail), 음(−)이면 얇은 꼬리(light tail)를 뜻한다.
실무에서는 모수 대신 표본 추정치를 사용하므로 편향 보정이 필요하다. Pearson 첨도는 Fisher 초과첨도에 3을 더한 값이므로, 어느 정의를 썼는지 명확히 적어야 한다. 다봉(multi-modal) 분포, 혼합분포, 상수열에서는 이 지표만으로 분포를 해석하기 어렵다.
계산값을 해석하기 전에 맞춰 둘 기준
왜도는 표준화된 3차 중심 모멘트의 표본 추정치를 사용하며, 편향 보정에는 bias=False를 권장한다. 첨도는 정규분포 기준선이 0인 Fisher 초과첨도를 기본으로 두고, Pearson 첨도를 쓸 때는 정규분포 기준선이 3이라는 점을 함께 표시한다.
모멘트 기반 계산에는 scipy.stats.skew, kurtosis를 쓸 수 있다. 이상치 민감도를 낮추려면 사분위수 Q1, Q2, Q3를 이용한 Bowley 왜도와 옥타일 기반 Moors 첨도를 대안으로 검토한다.
해석 기준은 단독 판정 규칙이 아니라 후속 분석을 위한 신호로 다루는 편이 낫다.
- 왜도 절대값 |γ1|이 <0.5이면 대칭에 근접, 0.5~1이면 중간 비대칭, >1이면 강한 비대칭으로 본다.
- 초과첨도 κ가 −1~1이면 보통 꼬리, >1이면 두터운 꼬리(heavy-tailed), <−1이면 얇은 꼬리로 해석한다.
- 히스토그램과 QQ-플롯을 함께 확인해야 수치가 가리는 형태를 보완할 수 있다.
변환과 검증은 분포 진단 뒤에 이어진다
결측 처리, 이상치 플래그, 스케일 표준화를 먼저 정리한다. 오른쪽 꼬리가 긴 양의 왜도에는 로그나 Box-Cox 변환을, 음의 왜도에는 반사+변환 또는 Yeo-Johnson을 후보로 둘 수 있다. 표본 크기의 적정성은 n≥30을 권장하며, 계절성이나 그룹 간 이질성이 있으면 분포를 분리해서 살펴본다.
형태 확인에는 히스토그램, KDE, QQ-플롯, 상자그림을 사용한다. Jarque–Bera와 D’Agostino K^2 같은 정규성 검정은 보조 판단 수단이 된다. 모델을 만든 뒤에는 잔차에도 같은 절차를 적용해 가정 검증을 이어간다.
분석 대상에 따라 달라지는 활용 방식
선형/일반화 선형 모델에서는 가정 적합성을 확보하고, 트리 모델에서는 분해능을 높이는 것이 목적이다. 분포를 진단한 뒤 변환 후보를 탐색하고, 교차검증으로 성능과 왜도·첨도를 다시 평가한다.
제조 공정 SPC에서는 공정능력지수(Cp/Cpk)를 산정하기 전 정상성 가정을 검토한다. 배치와 라인별 분포를 나눈 뒤 왜도·첨도 기반으로 이상 배치를 격리하고 공정 설정을 보정한다.
금융 리스크와 이상치 탐지에서는 수익률의 꼬리 리스크 및 급등락 민감도를 진단한다. 롤링 윈도우로 초과첨도를 모니터링하고, 임계를 넘으면 VaR/CVaR를 보수화한 뒤 경보를 발신한다.
ARPU·세션시간처럼 꼬리가 두꺼운 서비스 지표는 비교 방식 자체를 점검해야 한다. 중앙값이나 절단평균 같은 강건 통계량을 사용하고, 변환 후 효과 크기를 추정한 다음 부트스트랩 신뢰구간을 산출한다.
이 과정을 통해 정규성 가정 위반과 잔차 구조를 줄이고, 이상치 영향과 과적합 위험을 완화할 수 있다. 꼬리 리스크를 반영한 의사결정과 경보 정확도 개선에도 연결된다. 임계값과 절차를 표준 운영으로 관리하면 재현성을 확보하고, 분포 왜형을 교정한 공정 변동성 해석도 쉬워진다.
입력부터 리포팅까지의 판단 흐름
모멘트 기반 지표와 강건 지표의 차이
| 지표 | 정의 요지 | 정규분포 기준선 | 이상치 민감도 | 표본 크기 영향 | 해석 용이성 | 주요 용도 |
|---|---|---|---|---|---|---|
| Moment 왜도(γ1) | 표준화 3차 중심 모멘트 | 0 | 높음 | 중간 | 높음 | 일반적 비대칭 진단 |
| Bowley 왜도 | 사분위 기반(Q1,Q2,Q3) | 0 | 낮음 | 낮음 | 중간 | 강건 비대칭 진단 |
| 초과첨도(κ, Fisher) | 표준화 4차 중심 모멘트−3 | 0 | 매우 높음 | 중간 | 중간 | 꼬리 두께 판단 |
| Pearson 첨도 | 초과첨도+3 | 3 | 매우 높음 | 중간 | 낮음 | 레거시 문헌 호환 |
| Moors 첨도 | 옥타일 기반 | 0 | 낮음 | 낮음 | 중간 | 강건 꼬리 판단 |
계산·해석·변환을 연결하는 코드
실행 환경은 Python 3.10+, numpy 1.26+, pandas 2.x, scipy 1.11+, scikit-learn 1.3+이며, 입력은 결측을 NaN으로 표기한 1차원 수치 시리즈다.
# env: python>=3.10, numpy>=1.26, pandas>=2.0, scipy>=1.11, scikit-learn>=1.3
import numpy as np
import pandas as pd
from scipy import stats
from sklearn.preprocessing import PowerTransformer
def bowley_skew(x: np.ndarray) -> float:
q1, q2, q3 = np.quantile(x, [0.25, 0.5, 0.75])
denom = (q3 - q1)
if denom == 0:
return np.nan
return ((q3 + q1 - 2*q2) / denom)
def moors_kurtosis(x: np.ndarray) -> float:
# Octiles: P12.5, 25, 37.5, 62.5, 75, 87.5
o = np.quantile(x, [0.125, 0.25, 0.375, 0.625, 0.75, 0.875])
o1, o2, o3, o5, o6, o7 = o # note: indices mapped by order
# Moors: ( (O7 - O5) + (O3 - O1) ) / (O6 - O2)
denom = (o6 - o2)
if denom == 0:
return np.nan
return ((o7 - o5) + (o3 - o1)) / denom
def moment_stats(x: np.ndarray):
x = np.asarray(x, dtype=float)
x = x[np.isfinite(x)]
n = x.size
if n < 8:
raise ValueError("표본 수 부족: n>=8 권장")
if np.allclose(np.var(x), 0):
raise ValueError("상수열 입력: 분산=0")
# moment-based with bias correction
skew = stats.skew(x, bias=False) # sample skewness
kurt_excess = stats.kurtosis(x, fisher=True, bias=False) # excess kurtosis
return skew, kurt_excess
def interpret(skew, kurt_excess):
# thresholds per guidance
if np.isnan(skew) or np.isnan(kurt_excess):
return "해석 불가: NaN 포함 또는 분산=0"
if abs(skew) <= 0.5:
skew_msg = "대칭에 근접"
elif abs(skew) <= 1.0:
skew_msg = "중간 비대칭"
else:
skew_msg = "강한 비대칭"
skew_dir = "오른쪽 왜곡(+)" if skew > 0 else ("왼쪽 왜곡(-)" if skew < 0 else "대칭")
if -1 <= kurt_excess <= 1:
kurt_msg = "보통 꼬리"
elif kurt_excess > 1:
kurt_msg = "두터운 꼬리(heavy-tailed)"
else:
kurt_msg = "얇은 꼬리(light-tailed)"
return f"{skew_msg}, {skew_dir}; {kurt_msg}, 초과첨도={kurt_excess:.3f}"
def transform_if_needed(x: np.ndarray, method: str = "auto", random_state: int = 42):
x = np.asarray(x, dtype=float).reshape(-1, 1)
if method == "auto":
# Use Yeo-Johnson to handle zero/negative
pt = PowerTransformer(method="yeo-johnson", standardize=True)
elif method == "box-cox":
if np.any(x <= 0):
raise ValueError("Box-Cox 변환은 양의 값만 허용")
pt = PowerTransformer(method="box-cox", standardize=True)
else:
raise ValueError("지원하지 않는 method")
x_t = pt.fit_transform(x).ravel()
return x_t, pt
# Demo
rng = np.random.default_rng(0)
# mixture with right tail
x = np.concatenate([rng.normal(0, 1, 1000), rng.lognormal(0, 0.8, 200)])
skew, kex = moment_stats(x)
print("moment 왜도, 초과첨도:", skew, kex)
print("Bowley 왜도:", bowley_skew(x))
print("Moors 첨도:", moors_kurtosis(x))
print("해석:", interpret(skew, kex))
# 변환 후 재평가
x_t, pt = transform_if_needed(x, method="auto")
skew_t, kex_t = moment_stats(x_t)
print("변환 후 왜도, 초과첨도:", skew_t, kex_t)
print("해석(변환 후):", interpret(skew_t, kex_t))
결측·이상치 처리 정책과 트리밍 규칙은 버전 관리가 필요하다. 변환 파라미터(λ 등)와 해석 임계값은 구성관리(Configuration as Code) 대상으로 두는 편이 좋다. 재현성을 위해 입력 해시, 코드 커밋 해시, 난수 시드를 기록한다.