기술 통계 (Descriptive Statistics)¶
데이터의 특성을 요약하고 시각화하는 방법. 분석의 첫 단계로, 데이터의 분포와 패턴을 파악함. 모델링 전 반드시 수행해야 하는 필수 과정.
왜 기술 통계가 중요한가¶
기술 통계를 건너뛰고 바로 모델링에 들어가면:
- 이상치가 모델을 망칠 수 있다
- 변수 간 관계를 놓칠 수 있다
- 분포 가정이 맞지 않는 모델을 선택할 수 있다
- 데이터 품질 문제를 발견하지 못함
EDA(탐색적 데이터 분석)의 핵심: "데이터가 어떻게 생겼는지" 먼저 파악하라.
중심 경향치 (Measures of Central Tendency)¶
데이터가 어디에 집중되어 있는지를 나타내는 지표.
평균 (Mean)¶
모든 값의 합을 개수로 나눈 값.
직관적 해석: 모든 데이터를 하나의 값으로 "균등하게 재분배"하면 얼마가 될까?
import numpy as np
data = [10, 20, 30, 40, 50]
mean = np.mean(data) # 30.0
# 가중 평균
weights = [0.1, 0.2, 0.3, 0.2, 0.2]
weighted_mean = np.average(data, weights=weights) # 32.0
특성: - 이상치에 민감 - 합을 보존 (평균 * n = 총합) - 연속적, 미분 가능 (최적화에 유리)
실무 활용: - 손실 함수의 기본 (MSE는 평균 제곱 오차) - 배치 정규화에서 배치 평균 계산 - 임베딩 벡터의 평균 (문서 임베딩)
중앙값 (Median)¶
정렬했을 때 중앙에 위치하는 값.
data = [10, 20, 30, 40, 1000] # 이상치 포함
print(np.mean(data)) # 220.0 (이상치 영향)
print(np.median(data)) # 30.0 (robust)
직관적 해석: 절반은 이 값보다 작고, 절반은 이 값보다 큼.
특성: - 이상치에 강건 (robust) - 순위 기반 통계량 - 미분 불가능
실무 활용: - 소득, 주택 가격 등 왜곡된 분포의 대표값 - MAE(중앙값 절대 오차)와 연결 - A/B 테스트에서 이상치 영향 줄이기
최빈값 (Mode)¶
가장 자주 나타나는 값.
from scipy import stats
data = [1, 2, 2, 3, 3, 3, 4]
mode = stats.mode(data, keepdims=True)
print(mode.mode[0]) # 3
print(mode.count[0]) # 3
실무 활용: - 범주형 데이터의 대표값 - 다봉(multimodal) 분포 탐지 - 클러스터링 결과 해석
중심 경향치 비교¶
| 지표 | 장점 | 단점 | 사용 상황 |
|---|---|---|---|
| 평균 | 계산 용이, 수학적 성질 좋음 | 이상치 민감 | 정규 분포 데이터 |
| 중앙값 | 이상치 강건 | 정보 손실 | 왜도가 큰 데이터 |
| 최빈값 | 범주형에 적합 | 유일하지 않을 수 있음 | 범주형 데이터 |
왼쪽 꼬리 분포 (Left-skewed): Mean < Median < Mode
정규 분포 (Symmetric): Mean = Median = Mode
오른쪽 꼬리 분포 (Right-skewed): Mode < Median < Mean
절사 평균 (Trimmed Mean)¶
극단값을 제외한 평균. 평균과 중앙값의 중간.
from scipy.stats import trim_mean
data = [10, 20, 30, 40, 1000]
# 양쪽 10%씩 제외
trimmed = trim_mean(data, 0.1)
print(trimmed) # 이상치 영향 감소
산포도 (Measures of Dispersion)¶
데이터가 얼마나 퍼져 있는지를 나타내는 지표.
범위 (Range)¶
한계: 이상치 하나에 완전히 좌우됨.
분산 (Variance)¶
평균으로부터 편차 제곱의 평균.
직관적 해석: 데이터가 평균에서 얼마나 떨어져 있는지의 "평균적인 정도"를 제곱으로 측정.
data = [10, 20, 30, 40, 50]
# 모분산 (N으로 나눔)
pop_var = np.var(data, ddof=0) # 200.0
# 표본분산 (n-1로 나눔, 불편 추정량)
sample_var = np.var(data, ddof=1) # 250.0
왜 n-1로 나누는가? (자유도)
- 표본에서 평균을 먼저 계산하면, 마지막 값은 나머지로 결정됨
- 실제로 "자유롭게 변할 수 있는" 값은 n-1개
- n으로 나누면 분산을 체계적으로 과소추정 (biased)
- n-1로 나누면 불편 추정량(unbiased estimator)이 됨
# 증명: 표본분산의 기댓값
np.random.seed(42)
true_var = 1.0
estimates_n = []
estimates_n1 = []
for _ in range(10000):
sample = np.random.normal(0, 1, 10)
estimates_n.append(np.var(sample, ddof=0))
estimates_n1.append(np.var(sample, ddof=1))
print(f"n으로 나눔: {np.mean(estimates_n):.4f}") # ~0.9 (과소추정)
print(f"n-1로 나눔: {np.mean(estimates_n1):.4f}") # ~1.0 (불편)
표준편차 (Standard Deviation)¶
분산의 제곱근. 원래 단위와 동일.
직관적 해석: 데이터가 평균에서 "평균적으로" 얼마나 떨어져 있는가.
변동 계수 (Coefficient of Variation)¶
평균 대비 표준편차 비율. 서로 다른 단위의 변동성 비교에 유용.
# 키(cm)와 몸무게(kg)의 변동성 비교
heights = np.array([170, 175, 180, 165, 172]) # cm
weights = np.array([65, 70, 75, 60, 68]) # kg
cv_height = np.std(heights) / np.mean(heights) * 100
cv_weight = np.std(weights) / np.mean(weights) * 100
print(f"키 CV: {cv_height:.2f}%")
print(f"몸무게 CV: {cv_weight:.2f}%")
# 단위와 무관하게 변동성 비교 가능
사분위수와 IQR (Quartiles and Interquartile Range)¶
데이터를 4등분하는 값.
data = np.random.randn(1000)
q1, q2, q3 = np.percentile(data, [25, 50, 75])
# IQR (Interquartile Range)
iqr = q3 - q1
# 이상치 탐지 기준 (Tukey's fences)
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr
outliers = data[(data < lower_bound) | (data > upper_bound)]
print(f"IQR: {iqr:.4f}")
print(f"이상치 개수: {len(outliers)}")
1.5 * IQR 규칙의 유래: - 정규분포에서 이 범위는 약 99.3%의 데이터를 포함 - John Tukey가 경험적으로 제안 - 3 * IQR은 "극단적 이상치" 기준
MAD (Median Absolute Deviation)¶
중앙값에서의 절대 편차의 중앙값. 이상치에 매우 강건.
from scipy.stats import median_abs_deviation
data = [10, 20, 30, 40, 1000] # 이상치 포함
mad = median_abs_deviation(data)
print(f"MAD: {mad}")
# 정규분포 가정 시 표준편차 추정
# σ ≈ 1.4826 * MAD
robust_std = 1.4826 * mad
분포 형태 (Shape of Distribution)¶
왜도 (Skewness)¶
분포의 비대칭 정도.
직관적 해석: 3차 모멘트 → 비대칭성 측정. 꼬리가 어느 쪽으로 늘어져 있는가?
from scipy.stats import skew
data_right = np.random.exponential(1, 10000) # 오른쪽 꼬리
data_normal = np.random.normal(0, 1, 10000) # 대칭
data_left = -np.random.exponential(1, 10000) # 왼쪽 꼬리
print(f"오른쪽 꼬리: {skew(data_right):.3f}") # > 0 (양의 왜도)
print(f"대칭: {skew(data_normal):.3f}") # ≈ 0
print(f"왼쪽 꼬리: {skew(data_left):.3f}") # < 0 (음의 왜도)
| 값 | 해석 |
|---|---|
| Skew ≈ 0 | 대칭 |
| Skew > 0 | 오른쪽 꼬리 (양의 왜도) - 소득, 주택가격 |
| Skew < 0 | 왼쪽 꼬리 (음의 왜도) - 시험 점수(만점 근처) |
ML에서의 의미: - 왜도가 큰 타겟 변수 → 로그 변환 고려 - 왜도가 큰 특성 → 스케일링 방법 선택에 영향
첨도 (Kurtosis)¶
분포의 꼬리 두께(극단값 빈도).
직관적 해석: 4차 모멘트 → 극단값의 발생 빈도. 정규분포보다 꼬리가 두꺼운가?
from scipy.stats import kurtosis
data_normal = np.random.normal(0, 1, 10000)
data_uniform = np.random.uniform(-1, 1, 10000)
data_laplace = np.random.laplace(0, 1, 10000)
data_t = np.random.standard_t(3, 10000) # 두꺼운 꼬리
# Fisher's definition (정규분포 = 0)
print(f"정규분포: {kurtosis(data_normal):.3f}") # ≈ 0
print(f"균등분포: {kurtosis(data_uniform):.3f}") # < 0 (얇은 꼬리)
print(f"라플라스: {kurtosis(data_laplace):.3f}") # > 0 (두꺼운 꼬리)
print(f"t분포(df=3): {kurtosis(data_t):.3f}") # >> 0 (매우 두꺼운 꼬리)
| 값 | 해석 | 예시 |
|---|---|---|
| Kurtosis ≈ 0 | 정규분포와 유사 (mesokurtic) | 정규분포 |
| Kurtosis > 0 | 두꺼운 꼬리 (leptokurtic) | 금융 수익률 |
| Kurtosis < 0 | 얇은 꼬리 (platykurtic) | 균등분포 |
ML에서의 의미: - 첨도 > 3: 극단값이 자주 발생 → 이상치에 강건한 손실 함수 사용 - 금융 데이터는 대부분 첨도가 높음 (fat tails)
공분산과 상관관계¶
두 변수 간의 관계를 나타내는 지표.
공분산 (Covariance)¶
두 변수가 함께 변하는 정도.
직관적 해석: X가 평균보다 클 때 Y도 평균보다 크면 양의 공분산.
x = np.array([1, 2, 3, 4, 5])
y = np.array([2, 4, 5, 4, 5])
cov_matrix = np.cov(x, y)
print(cov_matrix)
# [[2.5 1.5]
# [1.5 1.3]]
한계: 단위에 의존하여 해석 어려움.
피어슨 상관계수 (Pearson Correlation)¶
공분산을 표준화한 값. -1 ~ 1 범위.
from scipy.stats import pearsonr
correlation, p_value = pearsonr(x, y)
print(f"상관계수: {correlation:.3f}, p-value: {p_value:.3f}")
# NumPy
corr_matrix = np.corrcoef(x, y)
| 값 | 해석 |
|---|---|
| r = 1 | 완전 양의 선형 관계 |
| r = 0.7~1 | 강한 양의 상관 |
| r = 0.3~0.7 | 중간 양의 상관 |
| r = -0.3~0.3 | 약한 상관 (거의 무관) |
| r = -1 | 완전 음의 선형 관계 |
스피어만 상관계수 (Spearman Correlation)¶
순위 기반 상관계수. 비선형 단조 관계 탐지.
피어슨 vs 스피어만: - 피어슨: 선형 관계만 탐지 - 스피어만: 단조 관계 탐지 (비선형도 OK)
# 비선형 단조 관계 예시
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
y = np.exp(x) # 지수 관계 (비선형이지만 단조)
print(f"Pearson: {pearsonr(x, y)[0]:.3f}") # 0.8~0.9 (과소평가)
print(f"Spearman: {spearmanr(x, y)[0]:.3f}") # 1.0 (완벽한 단조 관계)
켄달 타우 (Kendall's Tau)¶
순위 일치도 기반. 소표본에서 더 robust.
상관관계 주의사항¶
- 교란 변수: 아이스크림 판매량 ↔ 익사 사고 (여름이라는 교란 변수)
- 역인과: A와 B가 상관 있어도 A→B인지 B→A인지 알 수 없음
- 비선형 관계: 상관계수 0이어도 강한 비선형 관계 가능
# 상관계수 0이지만 완벽한 관계
x = np.linspace(-1, 1, 100)
y = x ** 2 # 포물선
print(f"Pearson: {pearsonr(x, y)[0]:.3f}") # ≈ 0
# 하지만 y = f(x)로 완벽히 결정됨!
데이터 시각화¶
히스토그램 (Histogram)¶
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde
data = np.random.normal(0, 1, 1000)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 히스토그램
axes[0].hist(data, bins=30, edgecolor='black', density=True)
axes[0].set_xlabel('Value')
axes[0].set_ylabel('Density')
axes[0].set_title('Histogram')
# KDE (Kernel Density Estimation)
kde = gaussian_kde(data)
x = np.linspace(-4, 4, 100)
axes[1].plot(x, kde(x))
axes[1].fill_between(x, kde(x), alpha=0.3)
axes[1].set_title('KDE')
plt.tight_layout()
bins 개수 선택:
- Sturges: bins = 1 + log2(n)
- Freedman-Diaconis: bins = (max - min) / (2 * IQR * n^(-1/3))
- Scott: bins = (max - min) / (3.5 * std * n^(-1/3))
박스플롯 (Box Plot)¶
data_groups = [np.random.normal(0, 1, 100),
np.random.normal(1, 1.5, 100),
np.random.normal(-0.5, 0.5, 100)]
fig, ax = plt.subplots(figsize=(8, 5))
bp = ax.boxplot(data_groups, labels=['A', 'B', 'C'])
ax.set_ylabel('Value')
ax.set_title('Box Plot')
# 구성 요소:
# - 박스: Q1 ~ Q3 (IQR)
# - 중앙선: 중앙값
# - 수염: 1.5*IQR 범위 내 최대/최소
# - 점: 이상치
바이올린 플롯 (Violin Plot)¶
박스플롯 + KDE. 분포 형태까지 시각화.
import seaborn as sns
data = {
'Group': ['A']*100 + ['B']*100 + ['C']*100,
'Value': np.concatenate([
np.random.normal(0, 1, 100),
np.random.exponential(1, 100), # 비대칭
np.random.bivariate_normal(0, 1, 100) # 이봉
])
}
df = pd.DataFrame(data)
sns.violinplot(x='Group', y='Value', data=df)
산점도 행렬 (Scatter Matrix)¶
import pandas as pd
import seaborn as sns
df = pd.DataFrame({
'A': np.random.randn(100),
'B': np.random.randn(100),
'C': np.random.randn(100)
})
# Seaborn pairplot
sns.pairplot(df, diag_kind='kde')
plt.suptitle('Scatter Matrix', y=1.02)
상관 히트맵¶
corr_matrix = df.corr()
plt.figure(figsize=(8, 6))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0,
vmin=-1, vmax=1, square=True)
plt.title('Correlation Heatmap')
흔한 실수와 오해¶
1. 평균만 보고 판단¶
# Anscombe's quartet: 평균, 분산, 상관계수가 같지만 완전히 다른 데이터
from sklearn.datasets import load_iris
# 항상 시각화와 함께!
# 평균 50, 표준편차 10인 데이터가 두 개 있어도:
# - 하나는 정규분포
# - 하나는 이봉분포
# 완전히 다른 데이터임
해결: 항상 히스토그램/박스플롯으로 분포를 확인하라.
2. 상관관계를 인과관계로 해석¶
# 나쁜 예:
# "교육 수준과 소득이 상관 있음 → 교육이 소득을 높임"
#
# 가능한 설명:
# - 교육 → 소득 (인과)
# - 소득 → 교육 (부유한 집안이 교육 투자)
# - 제3 변수 (능력, 사회적 배경)
해결: 인과 추론은 실험(RCT) 또는 인과 추론 기법으로.
3. 이상치를 무조건 제거¶
# 이상치가 발생한 이유를 먼저 파악:
# 1. 입력 오류 → 수정 또는 제거
# 2. 측정 오류 → 제거 고려
# 3. 실제 극단값 → 제거하면 정보 손실!
# 금융에서 극단값을 제거하면 리스크를 과소평가
해결: 이상치의 원인을 파악하고, 분석 목적에 맞게 처리.
4. 표본 크기 무시¶
# r = 0.9, n = 5 vs r = 0.3, n = 10000
# 후자가 훨씬 신뢰할 수 있음
# 항상 신뢰구간과 함께 보고
from scipy.stats import pearsonr
x = np.random.randn(5)
y = x + np.random.randn(5) * 0.5
r, p = pearsonr(x, y)
print(f"r={r:.3f}, p={p:.3f}") # n이 작으면 p-value가 큼
5. 분산 vs 표준편차 혼동¶
# 분산: 제곱 단위 (해석 어려움)
# 표준편차: 원래 단위 (직관적)
# "키의 분산이 25cm²" → 의미 파악 어려움
# "키의 표준편차가 5cm" → 평균에서 약 5cm 떨어져 있음
6. 정규분포 가정¶
# 많은 통계 기법이 정규분포를 가정하지만,
# 실제 데이터는 대부분 정규분포가 아님
# 소득, 대기 시간, 클릭 수 → 오른쪽 꼬리
# 시험 점수 (만점 근처) → 왼쪽 꼬리
# 사용자 리뷰 → 양극화 (J-shape)
# 해결: 정규성 검정 + 적절한 변환
from scipy.stats import shapiro
data = np.random.exponential(1, 100)
stat, p_value = shapiro(data)
print(f"Shapiro-Wilk p-value: {p_value:.4f}") # p < 0.05면 정규 아님
LLM/ML에서의 활용¶
임베딩 분석¶
# 임베딩 벡터 통계
embeddings = np.random.randn(1000, 768) # 1000개 문서, 768차원
# 각 차원별 통계
means = embeddings.mean(axis=0)
stds = embeddings.std(axis=0)
# 차원별 분포 확인
print(f"평균의 범위: [{means.min():.3f}, {means.max():.3f}]")
print(f"표준편차의 범위: [{stds.min():.3f}, {stds.max():.3f}]")
# 임베딩 간 유사도 분포
from sklearn.metrics.pairwise import cosine_similarity
sample = embeddings[:100]
sim_matrix = cosine_similarity(sample)
upper_triangle = sim_matrix[np.triu_indices(100, k=1)]
print(f"평균 유사도: {upper_triangle.mean():.3f}")
print(f"유사도 표준편차: {upper_triangle.std():.3f}")
손실 함수 분석¶
from scipy.stats import skew
# 학습 손실 통계
losses = np.random.exponential(0.5, 1000) # 예시 손실값
print(f"평균 손실: {np.mean(losses):.4f}")
print(f"중앙값 손실: {np.median(losses):.4f}")
print(f"손실 표준편차: {np.std(losses):.4f}")
print(f"왜도: {skew(losses):.4f}")
# 손실이 오른쪽 꼬리 분포면 일부 샘플에서 높은 손실
# -> 어려운 샘플 식별에 활용
특성 선택¶
import pandas as pd
# 분산이 낮은 특성 제거
def low_variance_filter(df, threshold=0.01):
"""분산이 threshold 미만인 컬럼 식별"""
variances = df.var()
low_var_cols = variances[variances < threshold].index.tolist()
return low_var_cols
# 상관관계가 높은 특성 쌍 식별
def high_corr_pairs(df, threshold=0.9):
"""상관계수가 threshold 이상인 쌍 식별"""
corr_matrix = df.corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
pairs = [(col, row) for col in upper.columns for row in upper.index
if upper.loc[row, col] > threshold]
return pairs
요약 통계 자동화¶
import pandas as pd
from scipy.stats import skew, kurtosis
def comprehensive_summary(data, name="Data"):
"""포괄적 데이터 요약"""
df = pd.Series(data)
stats = {
'N': len(data),
'Mean': df.mean(),
'Std': df.std(),
'Min': df.min(),
'Q1 (25%)': df.quantile(0.25),
'Median': df.quantile(0.5),
'Q3 (75%)': df.quantile(0.75),
'Max': df.max(),
'IQR': df.quantile(0.75) - df.quantile(0.25),
'Skewness': skew(data),
'Kurtosis': kurtosis(data),
'CV (%)': (df.std() / df.mean()) * 100 if df.mean() != 0 else np.nan
}
print(f"\n{'='*40}")
print(f" {name}")
print(f"{'='*40}")
for k, v in stats.items():
if isinstance(v, float):
print(f"{k:12}: {v:>12.4f}")
else:
print(f"{k:12}: {v:>12}")
return stats
# 사용
comprehensive_summary(np.random.normal(0, 1, 1000), "Normal Distribution")
참고 자료¶
- NumPy Statistics
- SciPy Stats
- Pandas describe()
- Seaborn Tutorial
- Tukey, J. W. (1977). Exploratory Data Analysis
- Same Stats, Different Graphs (Anscombe's Quartet)