콘텐츠로 이동
Data Prep
상세

기술 통계 (Descriptive Statistics)

데이터의 특성을 요약하고 시각화하는 방법. 분석의 첫 단계로, 데이터의 분포와 패턴을 파악함. 모델링 전 반드시 수행해야 하는 필수 과정.

왜 기술 통계가 중요한가

기술 통계를 건너뛰고 바로 모델링에 들어가면:

  • 이상치가 모델을 망칠 수 있다
  • 변수 간 관계를 놓칠 수 있다
  • 분포 가정이 맞지 않는 모델을 선택할 수 있다
  • 데이터 품질 문제를 발견하지 못함

EDA(탐색적 데이터 분석)의 핵심: "데이터가 어떻게 생겼는지" 먼저 파악하라.


중심 경향치 (Measures of Central Tendency)

데이터가 어디에 집중되어 있는지를 나타내는 지표.

평균 (Mean)

모든 값의 합을 개수로 나눈 값.

\[\bar{x} = \frac{1}{n}\sum_{i=1}^{n}x_i\]

직관적 해석: 모든 데이터를 하나의 값으로 "균등하게 재분배"하면 얼마가 될까?

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)

\[Range = max - min\]
data = [10, 20, 30, 40, 50]
range_val = np.ptp(data)  # peak to peak: 40

한계: 이상치 하나에 완전히 좌우됨.

분산 (Variance)

평균으로부터 편차 제곱의 평균.

\[\sigma^2 = \frac{1}{N}\sum_{i=1}^{N}(x_i - \mu)^2 \quad \text{(모분산)}\]
\[s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i - \bar{x})^2 \quad \text{(표본분산)}\]

직관적 해석: 데이터가 평균에서 얼마나 떨어져 있는지의 "평균적인 정도"를 제곱으로 측정.

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로 나누는가? (자유도)

  1. 표본에서 평균을 먼저 계산하면, 마지막 값은 나머지로 결정됨
  2. 실제로 "자유롭게 변할 수 있는" 값은 n-1개
  3. n으로 나누면 분산을 체계적으로 과소추정 (biased)
  4. 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)

분산의 제곱근. 원래 단위와 동일.

\[\sigma = \sqrt{\sigma^2}, \quad s = \sqrt{s^2}\]
std_pop = np.std(data, ddof=0)   # 14.14
std_sample = np.std(data, ddof=1)  # 15.81

직관적 해석: 데이터가 평균에서 "평균적으로" 얼마나 떨어져 있는가.

변동 계수 (Coefficient of Variation)

평균 대비 표준편차 비율. 서로 다른 단위의 변동성 비교에 유용.

\[CV = \frac{\sigma}{\mu} \times 100\%\]
# 키(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등분하는 값.

Q0 (0%):   최솟값
Q1 (25%):  1사분위수 (하위 25%)
Q2 (50%):  중앙값
Q3 (75%):  3사분위수 (상위 25%)
Q4 (100%): 최댓값
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)

분포의 비대칭 정도.

\[Skewness = \frac{E[(X-\mu)^3]}{\sigma^3}\]

직관적 해석: 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)

분포의 꼬리 두께(극단값 빈도).

\[Kurtosis = \frac{E[(X-\mu)^4]}{\sigma^4} - 3\]

직관적 해석: 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)

두 변수가 함께 변하는 정도.

\[Cov(X,Y) = E[(X-\mu_X)(Y-\mu_Y)]\]

직관적 해석: 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 범위.

\[r = \frac{Cov(X,Y)}{\sigma_X \sigma_Y}\]
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)

순위 기반 상관계수. 비선형 단조 관계 탐지.

from scipy.stats import spearmanr

rho, p_value = spearmanr(x, y)

피어슨 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.

from scipy.stats import kendalltau

tau, p_value = kendalltau(x, y)

상관관계 주의사항

상관관계 ≠ 인과관계
  • 교란 변수: 아이스크림 판매량 ↔ 익사 사고 (여름이라는 교란 변수)
  • 역인과: 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))

# bins='auto'는 위 규칙 중 적절한 것을 선택
plt.hist(data, bins='auto')

박스플롯 (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")

참고 자료