devlog.

데이터 종류와 위치·변이 추정: 통계 기초 정리 1장

·8분 읽기

통계와 머신러닝을 공부하다 보면 가장 먼저 마주치는 것이 "이 데이터가 어떤 종류인가?"라는 질문입니다. 데이터 유형을 제대로 파악해야 적합한 분석 방법을 선택할 수 있습니다.

이 글은 Practical Statistics for Data Scientists 1장을 기반으로 정리했습니다.

1.1 정형화된 데이터의 요소#

유형설명예시
연속형 (continuous)일정 범위 안에 어떤 값이든 취할 수 있는 데이터키, 몸무게, 온도
이산 (discrete)횟수, 정수형 데이터방문자 수, 구매 횟수
범주형 (categorical)가능한 범주 안의 값만 취하는 데이터혈액형, 지역
이진 (binary)두 개의 값만 갖는 범주형 데이터합격/불합격, 0/1
순서형 (ordinal)값들 사이에 순위가 있는 범주형 데이터만족도(상/중/하), 학점

이 외에도 파일의 저장 구조에 따라 정형, 반정형, 비정형 데이터로 나누기도 합니다.

실무에서는? 머신러닝 모델에 데이터를 넣기 전에 유형을 파악하는 것이 가장 먼저 해야 할 일입니다. 범주형 변수는 원-핫 인코딩(one-hot encoding)이나 레이블 인코딩이 필요하고, 순서형 변수는 순서 정보를 보존하는 인코딩이 필요합니다. 잘못된 유형으로 모델을 학습시키면 결과가 왜곡됩니다.

import pandas as pd

df = pd.DataFrame({
    "키":        [165.3, 172.0, 180.5],   # 연속형
    "방문횟수":  [3, 7, 12],               # 이산
    "혈액형":    ["A", "B", "O"],          # 범주형
    "합격여부":  [True, False, True],       # 이진
    "만족도":    pd.Categorical(
                    ["상", "중", "하"],
                    categories=["하", "중", "상"],
                    ordered=True            # 순서형
                ),
})

print(df.dtypes)
# 키          float64
# 방문횟수      int64
# 혈액형        object
# 합격여부        bool
# 만족도      category

1.2 테이블 데이터#

머신러닝에서 가장 기본이 되는 데이터 구조는 테이블(행렬) 형태입니다.

  • 데이터 프레임 (data frame): 테이블 형태의 데이터 구조. 통계, ML에서 가장 기본이 되는 구조
  • 피처 (feature): 테이블의 열 (특징, 속성)
  • 레코드 (record): 테이블의 행 (사건, 사례, 관측값, 패턴, 샘플)
  • 결과 (outcome): 통계, ML 모델을 사용하여 도출하는 예측값 — 독립 변수들을 통해 종속변수를 도출
import pandas as pd

# 데이터 프레임 생성
df = pd.DataFrame({
    "나이":   [25, 32, 28, 45],   # 피처
    "소득":   [3000, 5200, 4100, 7800],
    "구매여부": [0, 1, 0, 1],     # 결과(레이블)
})

# 피처(X)와 결과(y) 분리
X = df[["나이", "소득"]]  # feature matrix
y = df["구매여부"]         # target vector

print(f"레코드 수: {len(df)}")        # 4
print(f"피처 수: {X.shape[1]}")       # 2

pandas 주요 특징#

pandas의 DataFrame 객체는 모든 행에 순차적으로 정수 인덱스를 자동 부여하며, 계층적(다중) 인덱스도 지원해 대규모 데이터를 효율적으로 처리할 수 있습니다.

실무에서는? 캐글(Kaggle) 등 데이터 분석 대회에서 주어지는 데이터는 대부분 이 테이블 구조입니다. 피처 엔지니어링(feature engineering)이란 기존 피처를 조합·변환해 모델 성능을 높이는 작업으로, 실무 데이터 분석의 핵심입니다.

# 기본 정수 인덱스
df = pd.DataFrame({"값": [10, 20, 30]})
print(df.index)  # RangeIndex(start=0, stop=3, step=1)

# 다중(계층적) 인덱스
arrays = [["서울", "서울", "부산"], ["강남", "마포", "해운대"]]
index = pd.MultiIndex.from_arrays(arrays, names=["시", "구"])
df_multi = pd.DataFrame({"인구": [56, 38, 42]}, index=index)
print(df_multi)

1.3 위치 추정#

데이터의 중심 경향성을 나타내는 대표값입니다.

지표설명특징
평균 (mean)모든 값의 합 / 개수특잇값에 민감
가중평균 (weighted mean)가중치를 곱한 합 / 가중치 합중요도 반영
중간값 (median)정렬 후 가운데 위치 값특잇값에 강건
가중 중간값 (weighted median)가중치 누적합의 중간에 위치하는 값-
절사평균 (trimmed mean)극단값 일부 제외 후 평균로버스트

로버스트(robust): 특잇값(outlier)에 민감하지 않은 성질. 중간값과 절사평균이 평균보다 로버스트합니다.

실무에서는? 연봉, 집값, 트래픽 데이터처럼 극단값이 흔한 도메인에서는 평균 대신 중앙값을 KPI로 씁니다. 예를 들어 "월간 활성 사용자의 평균 세션 시간"은 극소수 헤비유저 때문에 실제 체감과 다를 수 있어, 중앙값이나 절사평균을 함께 보고합니다.

import numpy as np
from scipy import stats

data = [10, 12, 11, 14, 13, 100]  # 100은 특잇값(outlier)

# 평균 — 특잇값에 크게 영향받음
print(f"평균: {np.mean(data):.1f}")           # 26.7

# 중간값 — 특잇값에 강건
print(f"중간값: {np.median(data):.1f}")        # 12.5

# 절사평균 — 상하 10% 제거 후 평균
print(f"절사평균: {stats.trim_mean(data, 0.1):.1f}")  # 12.0

# 가중평균 — 최신 데이터에 가중치 부여 예시
weights = [1, 1, 1, 2, 2, 3]
print(f"가중평균: {np.average(data, weights=weights):.1f}")

1.4 변이 추정#

데이터가 얼마나 퍼져 있는지(산포도)를 나타내는 지표입니다.

핵심 지표 정리#

편차 (deviation) 관측값과 기준값(주로 평균 또는 중간값) 사이의 차이입니다.

편차=xixˉ\text{편차} = x_i - \bar{x}

분산 (variance) 편차의 제곱을 평균한 값. 표본 분산은 n−1로 나눕니다(베셀 보정).

s2=1n1i=1n(xixˉ)2s^2 = \frac{1}{n-1} \sum_{i=1}^{n}(x_i - \bar{x})^2

표준편차 (standard deviation) 분산의 제곱근. 원래 단위와 같아 해석이 직관적입니다.

s=s2s = \sqrt{s^2}

평균절대편차 (MAD, Mean Absolute Deviation) 각 값이 평균에서 떨어진 거리의 평균. L1 노름 기반으로 이상값에 덜 민감합니다.

MAD=1ni=1nxixˉ\text{MAD} = \frac{1}{n}\sum_{i=1}^{n}|x_i - \bar{x}|

중위절대편차 (Median Absolute Deviation) 각 값이 중위값에서 떨어진 거리의 중위값. 가장 로버스트한 변이 지표입니다.

MADmed=median(ximedian(x))\text{MAD}_\text{med} = \text{median}(|x_i - \text{median}(x)|)

범위 (range)

Range=max(x)min(x)\text{Range} = \max(x) - \min(x)

사분위범위 (IQR, Interquartile Range) 중간 50% 데이터의 퍼짐 정도. 상자그림(box plot)의 기준이 됩니다.

IQR=Q3Q1\text{IQR} = Q_3 - Q_1

import numpy as np
import pandas as pd
from scipy import stats

data = np.array([2, 4, 4, 4, 5, 5, 7, 9, 100])  # 100은 이상값

# 기본 변이 지표
print(f"분산 (표본): {np.var(data, ddof=1):.2f}")
print(f"표준편차:    {np.std(data, ddof=1):.2f}")
print(f"범위:        {data.max() - data.min()}")

# pandas로 한번에
s = pd.Series(data)
print(s.describe())
# count     9.000000
# mean     16.667
# std      31.927
# min       2.000
# 25%       4.000   ← Q1
# 50%       5.000   ← 중위값(median)
# 75%       7.000   ← Q3
# max     100.000

# IQR
q1, q3 = s.quantile(0.25), s.quantile(0.75)
iqr = q3 - q1
print(f"IQR: {iqr}")          # 3.0

# 이상값 탐지 (IQR 기반)
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
outliers = s[(s < lower) | (s > upper)]
print(f"이상값: {outliers.tolist()}")   # [100]

실무에서는? 표준편차는 리스크 관리에서 핵심 지표입니다. 금융에서 자산의 변동성(volatility)을 수익률의 표준편차로 측정하고, A/B 테스트에서 효과 크기(effect size)를 표준편차로 정규화해 비교합니다. IQR은 이상값 탐지(anomaly detection)의 기준으로 자주 사용됩니다.

평균절대편차 vs 중위절대편차 비교#

from scipy.stats import median_abs_deviation

data_normal  = [2, 4, 4, 4, 5, 5, 7, 9]
data_outlier = [2, 4, 4, 4, 5, 5, 7, 9, 100]

for label, data in [("정상 데이터", data_normal), ("이상값 포함", data_outlier)]:
    arr = np.array(data)
    mad_mean   = np.mean(np.abs(arr - np.mean(arr)))
    mad_median = median_abs_deviation(arr)
    print(f"[{label}]")
    print(f"  평균절대편차(MAD):    {mad_mean:.2f}")
    print(f"  중위절대편차(MADmed): {mad_median:.2f}")
    print()

# [정상 데이터]
#   평균절대편차(MAD):    1.75
#   중위절대편차(MADmed): 1.00

# [이상값 포함]
#   평균절대편차(MAD):    17.28   ← 이상값에 크게 영향받음
#   중위절대편차(MADmed): 1.00    ← 이상값에 강건

순서통계량과 백분위수#

순서통계량 (order statistics): 데이터를 크기순으로 정렬했을 때 각 순서 위치의 값

백분위수 (percentile): 전체 데이터 중 P%가 해당 값 이하인 지점

data = [15, 20, 35, 40, 50, 55, 60, 75, 80, 100]

p25 = np.percentile(data, 25)   # Q1
p50 = np.percentile(data, 50)   # 중위값
p75 = np.percentile(data, 75)   # Q3
p90 = np.percentile(data, 90)   # 상위 10%

print(f"25번째 백분위수 (Q1): {p25}")  # 36.25
print(f"50번째 백분위수 (Q2): {p50}")  # 52.5
print(f"75번째 백분위수 (Q3): {p75}")  # 71.25
print(f"90번째 백분위수:      {p90}")  # 82.0

데이터 유형을 정확히 파악하고, 이상값 존재 여부에 따라 로버스트한 지표(중간값, 절사평균, 중위절대편차, IQR)를 선택하는 것이 좋은 데이터 분석의 출발점입니다.

통계 기초는 단순한 이론이 아닙니다. EDA → 피처 엔지니어링 → 모델 선택 → 결과 해석까지, 데이터 분석의 모든 단계에서 이 개념들이 반복적으로 등장합니다. 수식보다 "이 지표가 현실에서 무엇을 말해주는가"를 이해하는 것이 핵심입니다.

1.5 데이터 분포 탐색하기#

수치형 데이터를 요약하는 단일 지표(평균, 중앙값 등)만으로는 데이터의 전체적인 형태를 파악하기 어렵습니다. 시각화를 통해 분포를 직접 살펴보는 것이 중요합니다.

시각화설명특징
상자그림 (boxplot)Q1·중앙값·Q3·IQR·이상값을 한눈에 표현분포 요약, 이상값 탐지
도수분포도 (frequency table)구간별 데이터 빈도를 기록한 표수치로 분포 파악
히스토그램 (histogram)x축=구간, y축=빈도수인 막대 그래프분포 모양 직관적 확인
밀도 그림 (density plot)히스토그램을 부드러운 곡선으로 표현 (커널밀도추정, KDE)연속적 분포 표현

상자그림 vs 히스토그램: 상자그림은 여러 그룹을 나란히 비교할 때 유리하고, 히스토그램은 단일 변수의 분포 모양(정규분포, 편향 등)을 파악할 때 유리합니다.

실무에서는? EDA(탐색적 데이터 분석)의 첫 단계로 모든 수치형 변수에 히스토그램과 상자그림을 그려봅니다. 분포가 크게 치우쳐 있으면(skewed) 로그 변환(log transform)을 적용해 모델 성능을 개선할 수 있습니다. 밀도 그림은 두 그룹의 분포를 겹쳐 비교할 때 유용합니다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde

np.random.seed(42)
data = np.concatenate([np.random.normal(50, 10, 200),
                       np.random.normal(80, 5, 50)])   # 이봉 분포

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

# 상자그림
axes[0].boxplot(data, vert=True)
axes[0].set_title("상자그림 (Boxplot)")
axes[0].set_ylabel("값")

# 히스토그램
axes[1].hist(data, bins=20, edgecolor="white", color="steelblue")
axes[1].set_title("히스토그램 (Histogram)")
axes[1].set_xlabel("구간")
axes[1].set_ylabel("빈도")

# 밀도 그림 (KDE)
kde = gaussian_kde(data)
x = np.linspace(data.min(), data.max(), 300)
axes[2].plot(x, kde(x), color="steelblue", linewidth=2)
axes[2].fill_between(x, kde(x), alpha=0.3, color="steelblue")
axes[2].set_title("밀도 그림 (KDE)")
axes[2].set_xlabel("값")
axes[2].set_ylabel("밀도")

plt.tight_layout()
plt.show()

# 도수분포표
pd.cut(data, bins=6).value_counts().sort_index()

1.6 이진 데이터와 범주 데이터 탐색하기#

범주형 데이터는 수치 연산이 불가능하므로 빈도 기반의 요약 지표와 시각화를 사용합니다.

개념설명
최빈값 (mode)데이터에서 가장 자주 등장하는 범주 또는 값
기댓값 (expected value)각 범주의 값에 출현 확률을 곱해 합산한 가중 평균
막대도표 (bar chart)각 범주의 빈도수 또는 비율을 막대로 표현
파이차트 (pie chart)각 범주의 비율을 부채꼴로 표현 (범주가 적을 때 적합)

기댓값 예시: 주사위 눈의 기댓값 = 1×(1/6) + 2×(1/6) + ... + 6×(1/6) = 3.5

실무에서는? 추천 시스템에서 클릭률(CTR)과 전환율을 분석할 때 범주형 변수의 빈도 분포를 먼저 봅니다. 클래스 불균형(class imbalance) — 예를 들어 사기 탐지에서 사기 건수가 0.1% — 문제를 발견하는 것도 이 단계입니다. 이런 경우 정확도(accuracy)보다 F1-score나 AUC를 평가 지표로 써야 합니다.

import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

df = pd.DataFrame({
    "혈액형": ["A", "B", "O", "A", "AB", "O", "A", "B", "O", "A"],
    "합격여부": [1, 0, 1, 1, 0, 0, 1, 1, 0, 1],
})

# 최빈값
mode_result = stats.mode(df["혈액형"], keepdims=True)
print(f"최빈값: {mode_result.mode[0]}")   # A

# 빈도표
freq = df["혈액형"].value_counts()
print(freq)

# 기댓값 (합격여부: 0 또는 1)
p_pass = df["합격여부"].mean()  # 합격 확률
expected = 1 * p_pass + 0 * (1 - p_pass)
print(f"기댓값 (합격 확률): {expected:.2f}")

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

# 막대도표
freq.plot(kind="bar", ax=axes[0], color="steelblue", edgecolor="white")
axes[0].set_title("혈액형 막대도표")
axes[0].set_ylabel("빈도")
axes[0].tick_params(axis="x", rotation=0)

# 파이차트
freq.plot(kind="pie", ax=axes[1], autopct="%1.1f%%", startangle=90)
axes[1].set_title("혈액형 파이차트")
axes[1].set_ylabel("")

plt.tight_layout()
plt.show()

1.7 상관관계#

두 수치형 변수 사이의 선형 관계 강도와 방향을 수치로 나타냅니다.

피어슨 상관계수 (Pearson correlation coefficient)

r=i=1N(xixˉ)(yiyˉ)(N1)sxsyr = \frac{\sum_{i=1}^{N}(x_i - \bar{x})(y_i - \bar{y})}{(N-1)s_x s_y}

  • r=+1r = +1: 완전한 양의 선형 관계
  • r=0r = 0: 선형 관계 없음
  • r=1r = -1: 완전한 음의 선형 관계

주의: 상관관계는 인과관계를 의미하지 않습니다. 두 변수가 함께 움직인다고 해서 한쪽이 다른 쪽을 유발하는 것은 아닙니다. 아이스크림 판매량과 익사 사고 수가 양의 상관을 보이지만, 실제 원인은 둘 다 여름이라는 공통 원인(교란변수)입니다.

실무에서는? 머신러닝 피처 선택 단계에서 상관행렬로 다중공선성(multicollinearity)을 확인합니다. 상관계수가 0.9 이상인 피처 쌍은 중복 정보를 담고 있어 하나를 제거하거나 PCA로 차원을 축소합니다. 금융에서는 포트폴리오 구성 시 자산 간 상관관계를 낮춰 리스크를 분산합니다.

상관행렬 (correlation matrix): 여러 변수 간의 상관계수를 행·열로 정리한 표. 대각선은 항상 1(자기 자신과의 상관)입니다.

산점도 (scatterplot): x축과 y축이 서로 다른 두 변수를 나타내며, 점들의 패턴으로 관계를 시각적으로 확인합니다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.random.seed(0)
n = 100
df = pd.DataFrame({
    "공부시간": np.random.uniform(1, 10, n),
    "수면시간": np.random.uniform(4, 9, n),
})
df["시험점수"] = 50 + 5 * df["공부시간"] - 2 * df["수면시간"] + np.random.normal(0, 5, n)

# 상관계수
print("피어슨 상관계수:")
print(df.corr(numeric_only=True).round(2))
#              공부시간  수면시간  시험점수
# 공부시간     1.00   -0.03    0.82
# 수면시간    -0.03    1.00   -0.28
# 시험점수     0.82   -0.28    1.00

# 산점도 + 상관행렬 히트맵
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].scatter(df["공부시간"], df["시험점수"], alpha=0.6, color="steelblue")
axes[0].set_xlabel("공부시간")
axes[0].set_ylabel("시험점수")
axes[0].set_title("산점도: 공부시간 vs 시험점수")

corr = df.corr(numeric_only=True)
im = axes[1].imshow(corr, cmap="coolwarm", vmin=-1, vmax=1)
axes[1].set_xticks(range(len(corr.columns)))
axes[1].set_yticks(range(len(corr.columns)))
axes[1].set_xticklabels(corr.columns)
axes[1].set_yticklabels(corr.columns)
for i in range(len(corr)):
    for j in range(len(corr)):
        axes[1].text(j, i, f"{corr.iloc[i, j]:.2f}", ha="center", va="center")
axes[1].set_title("상관행렬")
plt.colorbar(im, ax=axes[1])

plt.tight_layout()
plt.show()

1.8 두 개 이상의 변수 탐색하기#

실제 데이터는 대부분 여러 변수가 복합적으로 얽혀 있습니다. 분석 목적에 따라 변수의 수를 달리해 접근합니다.

분석 유형설명예시
일변량분석 (Univariate)변수 하나의 분포·요약 통계 파악키의 평균, 히스토그램
이변량분석 (Bivariate)두 변수 간의 관계 탐색공부시간 vs 성적 산점도
다변량분석 (Multivariate)세 개 이상의 변수를 동시에 분석성별·나이·소득 간 관계

실무에서는? 데이터 분석 보고서는 대부분 이 세 가지 분석을 순서대로 진행합니다. 일변량으로 각 변수를 파악하고, 이변량으로 타겟 변수와의 관계를 확인한 뒤, 다변량으로 복합적인 패턴을 탐색합니다. 분할표는 A/B 테스트 결과를 성별·연령대별로 분석할 때 자주 사용되고, 바이올린 도표는 그룹 간 분포 차이를 직관적으로 비교할 때 상자그림보다 정보량이 많아 선호됩니다.

추가로 자주 사용하는 시각화:

시각화설명
분할표 (contingency table)두 범주형 변수의 빈도를 교차 집계한 표
육각형 구간 (hexagonal binning)점이 너무 많을 때 육각형 셀로 밀도를 표현
등고 도표 (contour plot)두 변수의 밀도를 등고선으로 표현
바이올린 도표 (violin plot)상자그림 + 밀도 추정을 결합한 도표
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.random.seed(42)
n = 300
df = pd.DataFrame({
    "그룹":   np.random.choice(["A", "B", "C"], n),
    "점수":   np.concatenate([
                  np.random.normal(60, 10, 100),
                  np.random.normal(75, 8, 100),
                  np.random.normal(55, 15, 100),
              ]),
    "변수X":  np.random.normal(0, 1, n),
    "변수Y":  np.random.normal(0, 1, n),
    "성별":   np.random.choice(["남", "여"], n),
    "합격":   np.random.choice(["합격", "불합격"], n),
})

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 바이올린 도표
groups = [df[df["그룹"] == g]["점수"].values for g in ["A", "B", "C"]]
axes[0, 0].violinplot(groups, positions=[1, 2, 3], showmedians=True)
axes[0, 0].set_xticks([1, 2, 3])
axes[0, 0].set_xticklabels(["A", "B", "C"])
axes[0, 0].set_title("바이올린 도표: 그룹별 점수 분포")

# 육각형 구간
hb = axes[0, 1].hexbin(df["변수X"], df["변수Y"], gridsize=20, cmap="Blues")
axes[0, 1].set_title("육각형 구간 (Hexbin)")
plt.colorbar(hb, ax=axes[0, 1], label="빈도")

# 등고 도표
from scipy.stats import gaussian_kde
xy = np.vstack([df["변수X"], df["변수Y"]])
kde = gaussian_kde(xy)
xi = np.linspace(-3, 3, 100)
yi = np.linspace(-3, 3, 100)
Xi, Yi = np.meshgrid(xi, yi)
Zi = kde(np.vstack([Xi.ravel(), Yi.ravel()])).reshape(Xi.shape)
axes[1, 0].contourf(Xi, Yi, Zi, levels=10, cmap="Blues")
axes[1, 0].set_title("등고 도표 (Contour Plot)")

# 분할표
ct = pd.crosstab(df["성별"], df["합격"])
print("분할표:\n", ct)
ct.plot(kind="bar", ax=axes[1, 1], color=["#e74c3c", "#3498db"], edgecolor="white")
axes[1, 1].set_title("분할표 시각화: 성별 × 합격 여부")
axes[1, 1].tick_params(axis="x", rotation=0)
axes[1, 1].legend(title="결과")

plt.tight_layout()
plt.show()

관련 포스트