1 정의
1. 거리 척도 (Distance Metric) 선택
- 두 owner 의 "유사성" 정량화
2. 변수 정규화 (Rescaling)
- 단위 차이 균등화
3. 매칭 알고리즘 (Pair Matching)
- 비슷한 owner 묶기
각 단계의 결정이 stratum 의 품질 결정. 잘 형성된 stratum = 그룹별 perfect balance.
분석가의 자연스러운 직감: “그냥 비슷한 사람끼리 묶으면 끝.”
각 단계의 함정:
- 거리 없이: “비슷함” 의 정량화 없음 → 임의적 매칭
- Rescaling 없이: 큰 단위 변수가 거리 dominate → 잘못된 매칭
- 알고리즘 없이: 큰 데이터에서 unfeasible (수백만 가능 매칭)
세 단계 모두 통합되어야 정확한 stratum.
→ 각 단계가 다음 단계의 전제. Skip 불가.
2 거리 척도
2.1 Manhattan vs Euclidean
Manhattan (L1 norm):
\[ d_{AB} = \sum_{j} |x_{A,j} - x_{B,j}| \]
Euclidean (L2 norm):
\[ d_{AB} = \sqrt{\sum_{j} (x_{A,j} - x_{B,j})^2} \]
예 (2 변수, A = (0, 0), B = (3, 4)):
- Manhattan: |3-0| + |4-0| = 7
- Euclidean: √(9 + 16) = 5
→ 같은 두 점이 다른 거리.
비유: 도시 안에서 A 지점에서 B 지점 가는 방법.
- Manhattan: 격자형 도시 (NYC), 도로 따라 이동. “오른쪽 3 블럭, 위로 4 블럭” → 7 블럭.
- Euclidean: 직선 거리 (까마귀가 나는 방향). √(9 + 16) = 5.
일반적으로:
- Manhattan: outlier 영향 작음 (제곱 안 함)
- Euclidean: 표준 통계 가정에 부합
비즈니스 분석 default = Euclidean. 단, outlier 많으면 Manhattan 고려.
2.2 Mahalanobis 거리
Mahalanobis 거리:
\[ d_{AB} = \sqrt{(x_A - x_B)^T \Sigma^{-1} (x_A - x_B)} \]
여기서 \(\Sigma\) = 변수의 공분산 행렬.
특징:
- 변수 간 상관 자동 조정
- Rescaling 효과 자동 (분산 normalize)
- 통계학에서 표준 거리
비용: 공분산 행렬 계산 + 역행렬 (큰 데이터에서 계산 비용).
비유: 키와 몸무게 비교.
- 일반인: 키 170, 몸무게 70 (정상)
- 농구선수: 키 200, 몸무게 70 (가벼운 키)
- 여성: 키 170, 몸무게 50 (마름)
Euclidean (rescaled):
- 일반 vs 농구: 거리 큼 (키 차이)
- 일반 vs 여성: 거리 작음 (몸무게만 차이)
Mahalanobis:
- 키 200 + 몸무게 70 = “마름 (키 대비 가벼움)” 패턴
- 키 170 + 몸무게 50 = “정상 (키 대비 가벼움)” 패턴
- 두 경우 모두 “가벼움” 의 비슷함 인식 가능
→ Mahalanobis 는 “변수의 joint 분포” 를 고려. 더 정확하지만 복잡.
비즈니스 분석에서 default = Euclidean (단순). Mahalanobis 는 변수 간 강한 상관 + 정확도 우선 시.
2.3 Gower 거리
데이터가 numeric + categorical 혼합이면:
- Euclidean: one-hot 후 사용
- Gower: numeric/categorical 동시 처리
Gower 거리 (변수 j 의 거리):
- Numeric: \(|x_{A,j} - x_{B,j}| / \text{range}(x_j)\)
- Categorical: 0 (같음) 또는 1 (다름)
전체 거리:
\[ d_{AB} = \frac{1}{p} \sum_{j} d_j(A, B) \]
(p = 변수 수)
장점: One-hot 안 해도 됨. 단순.
단점: 모든 변수에 같은 weight. Euclidean 처럼 customize 어려움.
→ R 의 cluster::daisy() 가 Gower 지원. Python 은 별도 패키지 필요.
비즈니스 분석 default = Euclidean + one-hot. Gower 는 명시적 선택.
3 Rescaling
3.1 Min-Max vs Z-score
Min-Max (Range-based):
\[ x_{\text{scaled}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}} \]
- 출력 범위: [0, 1]
- 분포 형태 보존
- Outlier 에 민감 (max 가 outlier 면 다른 값들 모두 작아짐)
Z-score (Standard):
\[ x_{\text{scaled}} = \frac{x - \mu}{\sigma} \]
- 출력 범위: 약 [-3, 3] (정규 분포)
- 평균 0, 분산 1
- Outlier 영향 분산
선택 가이드:
| 상황 | 권장 |
|---|---|
| 분포가 정규에 가까움 | Z-score |
| Outlier 많음 | Z-score (또는 robust scaling) |
| 분포가 skewed | Min-Max (단, outlier 점검) |
| 모든 변수 [0, 1] 같은 단위 원함 | Min-Max |
| 통계학적 표준 | Z-score |
비즈니스 분석에서:
- Default = Min-Max (단순, 직관)
- Outlier 많으면 → Quantile rescaling 또는 winsorize 후 Min-Max
Buisson 의 default = Min-Max.
3.2 Quantile (Rank-Based) Rescaling
3.3 Outlier 처리
sq_ft 데이터: 460~1120 (정상)
+ 한 개의 outlier: 5,000 (10 배 큰 property)
Min-Max:
- 정상 값 범위가 [0, 0.13] 으로 압축
- 정상 owner 들 사이 거리가 매우 작아짐
- Outlier 가 “혼자 1.0”
대처:
- Winsorize: 99 percentile 로 cap (5,000 → 1,200)
- Log transform: \(\log(x)\) 후 rescale
- Quantile rescaling: rank 기반
Winsorize 코드:
df["sq_ft_winsor"] = df["sq_ft"].clip(
lower=df["sq_ft"].quantile(0.01),
upper=df["sq_ft"].quantile(0.99),
)이 처리로 outlier 영향 제거하되 정보 일부 유지.
4 Categorical 변수의 처리
4.1 One-Hot Encoding 의 변형
Property type: ("house", "townhouse", "apartment")
↓
type_house: 0/1
type_townhouse: 0/1
type_apartment: 0/1
Property A (house): (1, 0, 0) Property B (townhouse): (0, 1, 0) Property C (apartment): (0, 0, 1)
거리:
- A vs B (Euclidean): \(\sqrt{(1-0)^2 + (0-1)^2 + (0-0)^2} = \sqrt{2} \approx 1.414\)
- A vs C: \(\sqrt{2}\)
- B vs C: \(\sqrt{2}\)
→ 모든 다른 카테고리 쌍이 같은 거리.
흔한 실수: pandas 의 drop_first=True:
이 옵션이 첫 카테고리 (house) 를 drop. 결과:
type_townhouse: 0/1
type_apartment: 0/1
거리:
- House (0, 0) vs Townhouse (1, 0): √1 = 1
- House (0, 0) vs Apartment (0, 1): √1 = 1
- Townhouse (1, 0) vs Apartment (0, 1): √2
→ House 가 다른 두 type 과 거리 1, Townhouse 와 Apartment 사이 거리 √2 ≈ 1.41.
함의: House 가 reference 처럼 작용. Townhouse 와 Apartment 가 House 보다 서로 더 멀음 (artifact).
→ Stratification 에서는 drop_first=False 권장. 모든 카테고리 동등 처리.
drop_first=True 는 회귀 (multicollinearity 회피) 용. Stratification 과 다름.
4.2 Ordered Categorical
AirCnC 의 tier (1, 2, 3, descending):
- Tier 1 (top) > Tier 2 > Tier 3 (bottom)
One-hot 사용 시 ordering 무시:
- Tier 1 vs Tier 2 거리 = √2
- Tier 1 vs Tier 3 거리 = √2 (잘못! 2 가 더 가까울텐데)
해결 1: Numeric 처리
df["tier_num"] = df["tier"].map({1: 1, 2: 2, 3: 3})
df["tier_scaled"] = (df["tier_num"] - 1) / 2 # [0, 1]거리:
- Tier 1 (0) vs Tier 2 (0.5): 0.5
- Tier 1 (0) vs Tier 3 (1): 1.0
→ Tier 1 vs Tier 2 가 더 가까움 (직관 일치).
질문: “변수의 카테고리가 ordering 가능한가?”
Ordered:
- Tier (1, 2, 3)
- 등급 (A, B, C, D, F)
- Likert (매우 동의 ~ 매우 비동의)
- → Numeric 처리 (또는 ordered categorical with custom distance)
Unordered:
- 색깔 (red, blue, green)
- 국적 (US, JP, KR)
- Property type (house, townhouse, apartment) — 약간 ordering 가능 (size?)
- → One-hot
판단 어려운 경우:
- 거리 명세 가능 (예: “house ↔︎ townhouse 가 house ↔︎ apartment 보다 가깝다”)
- → Custom distance matrix 또는 ordering 정의
→ Domain 직관 + 데이터의 의미. 자동 결정 어려움.
4.3 Target Encoding
대안: 카테고리를 outcome 의 평균으로:
국가별 booking_amount 평균:
US: $80
JP: $120
KR: $95
각 owner 의 country 를 그 평균값으로 변환.
장점: Outcome 과 직접 연관 → distance 가 outcome 측면에서 의미 있음. 단점: Outcome 사용 → leakage 위험 (실험 후 분석 시 control vs treatment 비교가 영향)
비즈니스 분석에서 stratification 에는 권장 안 함 (outcome 사용으로 partial outcome leakage). 다른 ML 작업에서는 사용 가능.
→ Stratification default: one-hot 또는 ordered numeric. Target encoding 회피.
5 Pair Matching 알고리즘
5.1 Optimal Matching
Optimal matching = 모든 가능 매칭 중 최적 (전역 최소 거리).
알고리즘: Hungarian Algorithm (Munkres, 1957).
복잡도: O(N³).
5,000 owner: ~10^11 연산 → 며칠.
작은 N (< 500) 에서만 실용적.
Optimal 의 약속:
- 모든 stratum 의 거리 합이 최소
비용:
- N = 100: 0.01 초
- N = 1,000: 1 초
- N = 10,000: 17 분
- N = 100,000: 12 일
→ N > 1,000 에서 비실용적.
해결: Greedy approximation.
5.2 NaiveGreedy
1. 모든 pair 의 거리 계산 (O(N²))
2. 가장 가까운 pair 선택 → stratum 1
3. 사용된 owner 제외
4. 가장 가까운 다음 pair 선택 → stratum 2
5. 반복 until 모두 매칭
복잡도: O(N²) (거리 계산) + O(N² log N) (sorting).
5,000 owner: ~25M 연산 + sorting → 수 분.
전역 최적이 아님 — 첫 pair 가 좋아도 마지막 pair 가 매우 나쁠 수 있음.
예시:
4 owner: A, B, C, D
거리:
A-B: 1
A-C: 100
A-D: 100
B-C: 100
B-D: 100
C-D: 1
NaiveGreedy:
- 가장 가까운 pair: A-B (거리 1)
- 다음 pair: C-D (거리 1)
- 총 거리: 2
Optimal:
- A-B + C-D: 2 (같음)
이 경우는 OK. 그러나 다른 시나리오:
A-B: 1
A-C: 2
A-D: 100
B-C: 100
B-D: 100
C-D: 100
NaiveGreedy:
- A-B (거리 1)
- C-D (거리 100, 강제)
- 총 = 101
Optimal:
- A-C (거리 2) + B-D (거리 100)
- 또는 A-D + B-C
- 총 ≈ 102
거의 비슷. Greedy 가 일반적으로 충분.
5.3 OptGreedy
OptGreedy = “한 단계 더 보고 결정”.
1. 가장 가까운 pair X 선택
2. X 와 비슷한 다른 가까운 pair Y 가 있는가?
- X 가 사라지면 Y 의 distance 가 어떻게?
3. X 와 Y 를 함께 고려해 더 좋은 결정
복잡도: 약 N² log N.
정확도: NaiveGreedy 보다 약간 좋음, Optimal 보다 약간 나쁨.
5,000 owner: 수 분 (NaiveGreedy 와 비슷).
→ 분석가의 default: NaiveGreedy 또는 OptGreedy (실용적 trade-off).
5.4 R 의 block() vs Python 구현
Python 에는 직접 대응 패키지가 부족. 옵션:
- scikit-learn 의 NearestNeighbors: nearest pair 찾기
- networkx 의 max_weight_matching: graph 기반 optimal
- scipy 의 linear_sum_assignment: Hungarian
- 수동 구현: NaiveGreedy 직접
R 사용자가 stratification 에 더 편리. Python 사용자는 직접 구현 또는 R 호출.
→ Buisson 권장 (재방문): “Categorical 변수가 중요하면 R 사용. Python 은 numeric only 데이터에 OK.”
6 5,000 Owner 의 매칭
6.1 시간 측정
import time
import numpy as np
from scipy.spatial.distance import pdist, squareform
def time_matching(n_owners=5000, n_features=10, n_groups=3):
"""매칭 시간 측정."""
np.random.seed(42)
features = np.random.normal(0, 1, (n_owners, n_features))
start = time.time()
# 거리 계산
distances = squareform(pdist(features, metric="euclidean"))
elapsed_dist = time.time() - start
# 매칭 (단순화)
np.fill_diagonal(distances, np.inf)
used = np.zeros(n_owners, dtype=bool)
n_strata = n_owners // n_groups
for _ in range(n_strata):
masked = distances.copy()
masked[used, :] = np.inf
masked[:, used] = np.inf
i, j = np.unravel_index(np.argmin(masked), masked.shape)
if masked[i, j] == np.inf:
break
used[i] = used[j] = True
# 추가 stratum member (단순화)
avail = np.where(~used)[0]
if len(avail) > 0:
k = avail[0]
used[k] = True
elapsed_total = time.time() - start
return elapsed_dist, elapsed_total
dist_time, total_time = time_matching(5000)
print(f"거리 계산 시간: {dist_time:.2f} 초")
print(f"전체 매칭 시간: {total_time:.2f} 초")예상 (laptop):
- 거리 계산: 5 초
- 매칭: ~30 초
- 합계: ~35 초
5,000 owner 가 실용적 시간 안에 처리 가능.
거리 행렬 = 5,000 × 5,000 = 25M 엔트리.
각 엔트리 8 bytes (double) → 200 MB 메모리.
큰 데이터 (50,000+) 에서:
- 거리 행렬 = 50,000² × 8 = 20 GB → laptop 한계 초과
- 해결: chunked computation, sparse distance, 또는 approximate nearest neighbor
비즈니스 분석에서 N < 10,000 가 일반적 → 메모리 OK.
10,000+ 는 분산 처리 또는 sampling.
6.2 매칭 품질 평가
::: {.callout-note} ## Stratum 내 거리 통계
def evaluate_matching(strata, distances):
"""Stratum 내 거리의 분포 평가."""
intra_distances = []
for stratum in strata:
# Stratum 내 모든 pair 의 평균 거리
for i in range(len(stratum)):
for j in range(i + 1, len(stratum)):
intra_distances.append(distances[stratum[i], stratum[j]])
return {
"mean": np.mean(intra_distances),
"median": np.median(intra_distances),
"max": np.max(intra_distances),
"p95": np.percentile(intra_distances, 95),
}
# 비교: 단순 무작위 vs 층화
random_strata = np.random.permutation(5000).reshape(-1, 3) # 무작위 stratum
# 가정: stratified_strata 는 NaiveGreedy 결과
# 시뮬레이션 (간단화)
print("매칭 후 stratum 내 평균 거리:")
print(" 단순 무작위: 1.5 (예상)")
print(" 층화: 0.3 (예상)")
print("\n5 배 작은 거리 = stratum 안의 owner 가 매우 비슷")Stratum 내 평균 거리가:
- 작음 (0.3): owner 들이 서로 매우 비슷 → 그룹별 perfect balance
- 큼 (1.5): owner 들이 서로 다름 → balance 효과 약함
품질이 안 좋으면:
- 더 많은 변수 추가 (도메인 지식)
- 알고리즘 변경 (NaiveGreedy → OptGreedy)
- 더 많은 stratum (n_groups = 2 vs 3)
이 self-check 가 stratification 의 자기 검증.
7 응용 — 다양한 변수 형태
7.1 Numeric Only
5,000 owner, 5 numeric 변수.
처리:
- Min-Max rescale 또는 Z-score
- Euclidean 거리
- NaiveGreedy 매칭
가장 단순. R/Python 둘 다 쉬움.
7.2 Mixed (Numeric + Categorical)
5 numeric + 3 categorical (각 3~5 수준).
처리:
- Categorical: one-hot (drop_first=False)
- Numeric: rescale
- 결합 → Euclidean 거리 + NaiveGreedy
또는:
- Gower 거리 (categorical/numeric 동시 처리)
- 매칭 (Gower 호환 알고리즘)
R 의 cluster::daisy + blockTools 조합. Python 은 직접 구현.
7.3 Time Series Variables
5 변수가 시계열 (예: 6 개월 booking 추세):
처리 옵션:
- 요약 통계: 평균, 분산, trend (slope) 추출 → 5 numeric 변수로 환원
- DTW (Dynamic Time Warping) 거리: 시계열 직접 비교
- PCA: 주성분 추출 후 매칭
비즈니스 default = 요약 통계 (단순). DTW/PCA 는 정확도 우선 시.
8 코드 예시 — 종합
8.1 통합 함수
import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
from scipy.spatial.distance import pdist, squareform
def stratified_assignment(df, id_col, n_groups=3,
rescaling="minmax", distance="euclidean",
algorithm="naive_greedy", random_seed=42):
"""
완전한 층화 무작위 배정 파이프라인.
"""
np.random.seed(random_seed)
# 1. 변수 분리
ids = df[id_col].values
features_df = df.drop(columns=[id_col])
num_cols = features_df.select_dtypes(include=[np.number]).columns.tolist()
cat_cols = features_df.select_dtypes(include=["object", "category"]).columns.tolist()
# 2. Rescaling
if rescaling == "minmax":
scaler = MinMaxScaler()
elif rescaling == "zscore":
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
else:
raise ValueError(f"Unknown rescaling: {rescaling}")
if num_cols:
num_scaled = scaler.fit_transform(features_df[num_cols])
else:
num_scaled = np.empty((len(df), 0))
# 3. One-hot encoding
if cat_cols:
enc = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
cat_array = enc.fit_transform(features_df[cat_cols])
else:
cat_array = np.empty((len(df), 0))
# 4. 결합
features = np.concatenate([num_scaled, cat_array], axis=1)
# 5. 거리
if distance == "euclidean":
distances = squareform(pdist(features, metric="euclidean"))
elif distance == "manhattan":
distances = squareform(pdist(features, metric="cityblock"))
elif distance == "mahalanobis":
distances = squareform(pdist(features, metric="mahalanobis"))
else:
raise ValueError(f"Unknown distance: {distance}")
np.fill_diagonal(distances, np.inf)
# 6. 매칭 (NaiveGreedy 만 구현)
n = len(ids)
used = np.zeros(n, dtype=bool)
strata = []
while used.sum() + n_groups <= n:
masked = distances.copy()
masked[used, :] = np.inf
masked[:, used] = np.inf
i, j = np.unravel_index(np.argmin(masked), masked.shape)
if masked[i, j] == np.inf:
break
stratum = [i, j]
used[i] = used[j] = True
for _ in range(n_groups - 2):
avail = np.where(~used)[0]
if len(avail) == 0:
break
avg_dist = distances[stratum, :][:, avail].mean(axis=0)
next_idx = avail[np.argmin(avg_dist)]
stratum.append(next_idx)
used[next_idx] = True
if len(stratum) == n_groups:
strata.append(stratum)
# 7. Stratum 안에서 무작위 그룹 배정
group_labels = ["control"] + [f"treat{i+1}" for i in range(n_groups - 1)]
assignments = np.empty(n, dtype=object)
for stratum in strata:
groups = np.random.permutation(group_labels)
for k, idx in enumerate(stratum):
assignments[idx] = groups[k]
return pd.DataFrame({id_col: ids, "group": assignments})이 함수가 분석가에게 주는 것:
- 데이터 + 옵션 입력
- 7 단계 파이프라인 자동
- 결과 즉시 사용 가능
옵션:
- Rescaling: minmax / zscore
- Distance: euclidean / manhattan / mahalanobis
- Algorithm: naive_greedy (확장 가능)
→ 분석 파이프라인의 표준 도구. Production 사용 가능.
8.2 시뮬레이션 — Quality 비교
# 가상 AirCnC 데이터
np.random.seed(42)
n = 500
df = pd.DataFrame({
"ID": range(n),
"sq_ft": np.random.uniform(460, 1120, n),
"tier": np.random.choice([1, 2, 3], n),
"avg_review": np.random.uniform(0, 10, n),
"type": np.random.choice(["house", "townhouse", "apartment"], n),
})
# 1. 단순 무작위
np.random.seed(42)
df["random_group"] = np.random.choice(
["control", "treat1", "treat2"],
n,
)
# 2. 층화
strat = stratified_assignment(df, "ID", n_groups=3)
df_strat = df.merge(strat, on="ID")
# 그룹별 특성 비교
print("=== 단순 무작위 ===")
print(df.groupby("random_group").agg(
sq_ft_mean=("sq_ft", "mean"),
sq_ft_std=("sq_ft", "std"),
avg_review_mean=("avg_review", "mean"),
))
print("\n=== 층화 ===")
print(df_strat.groupby("group").agg(
sq_ft_mean=("sq_ft", "mean"),
sq_ft_std=("sq_ft", "std"),
avg_review_mean=("avg_review", "mean"),
))예상:
| sq_ft 평균 (단순) | sq_ft 평균 (층화) | |
|---|---|---|
| Control | 790 | 790 |
| Treat1 | 815 | 790 |
| Treat2 | 770 | 790 |
층화 후 그룹별 차이 거의 0. 단순 무작위에서 ±25 차이.
비즈니스 함의: 층화 후 효과 측정 정확. Sample size 줄이거나 power 향상 가능.
9 종합 — 분석가의 결정 트리
1. 표본 크기?
< 1,000 → 거의 항상 층화
> 10,000 → 단순 무작위 OK
2. Pre-experiment 데이터?
있음 → 층화
없음 → 단순
3. 변수 유형?
Numeric only → Euclidean + Min-Max
Mixed → One-hot + Euclidean (또는 Gower)
Time series → 요약 통계 추출 후 Euclidean
4. 알고리즘?
N < 500 → Optimal
N < 10,000 → NaiveGreedy 또는 OptGreedy
N > 10,000 → Sampling 또는 분산 처리
5. Quality 검증?
- Stratum 내 거리 분포 점검
- 그룹별 특성 비교
- 원하는 quality 안 나오면 → 변수 추가, 알고리즘 변경
이 결정 트리가 stratification 의 표준 절차.
10 관련 주제
10.1 Ch.9 의 형제 글
- E-BUI9-0 층화 무작위 배정 overview — Ch.9 전체 흐름
- E-BUI9-2 검정력 시뮬레이션 + ITT/CACE — 층화 후 power 분석
10.2 이전 챕터
- E-BUI8-2 무작위 배정 시점·수준 — Ch.8: 배정 level
10.3 후속 챕터
- E-BUI10-0 군집 무작위 배정 overview — Ch.10: 더 큰 단위 배정
10.4 카테고리 진입점
- Experimentation 학습 로드맵 — 11 Phase × 7 교재 매핑