1 Stratified Analysis & Nesting
1.1 세 가지 분석 전략
같은 데이터에 대해 그룹별 효과를 파악하는 방법이 세 가지 있다.
전략 1: 전체 모델 (하나의 회귀선)
lm(Y ~ X + Group)
→ 그룹 간 절편 차이만 허용, 기울기는 동일 가정
전략 2: 교호작용 모델
lm(Y ~ X * Group)
→ 그룹마다 다른 절편 + 다른 기울기
→ 교호작용 계수로 그룹 간 기울기 차이 직접 검정 가능
전략 3: 층화 분석 (각 그룹별 별도 모델)
for each group: lm(Y ~ X, data=group_data)
→ 그룹별 계수를 독립적으로 추정
→ 그룹 간 차이에 대한 공식 검정 없음
| 전체 모델 | 교호작용 | 층화 분석 | |
|---|---|---|---|
| 그룹별 기울기 | ❌ 동일 | ✅ | ✅ |
| 그룹 간 차이 검정 | △ (절편만) | ✅ (공식 검정) | ❌ (비공식) |
| 해석 용이성 | 높음 | 중간 | 높음 |
| 파라미터 수 | 적음 | 중간 | 많음 |
| 소규모 그룹 안정성 | 높음 | 중간 | 낮음 |
1.2 교호작용 모델
1.2.1 기본 형태
library(tidyverse)
# NYC Airbnb: borough마다 stars 효과가 다른가?
fit_interaction <- lm(
price ~ stars * borough + room_type,
data = nyc_airbnb
)
fit_interaction |>
broom::tidy() |>
filter(str_detect(term, "stars")) |>
select(term, estimate, std.error, p.value)term estimate std.error p.value
stars 27.1 4.0 <0.001 ← Manhattan에서 stars 효과
stars:boroughBrooklyn -6.4 5.1 0.21 ← Brooklyn vs Manhattan 차이
stars:boroughQueens -17.5 7.3 0.02 *
stars:boroughBronx -22.7 16.3 0.16
해석:
- Manhattan(기준): stars 1점 증가 → 가격 $27.1 상승
- Brooklyn: $27.1 - $6.4 = $20.7 상승 (차이 p=0.21, 유의하지 않음)
- Queens: $27.1 - $17.5 = $9.6 상승 (차이 p=0.02, 유의)
1.2.2 Python에서 교호작용
import statsmodels.formula.api as smf
# * 는 주효과 + 교호작용 모두 포함
model_int = smf.ols(
"price ~ stars * C(borough) + C(room_type)",
data=df
).fit()
# 교호작용 계수만 추출
import pandas as pd
coef_df = pd.DataFrame({
"coef": model_int.params,
"se": model_int.bse,
"p": model_int.pvalues
})
interaction_terms = coef_df[coef_df.index.str.contains("stars:")]
print(interaction_terms.round(3))1.3 층화 분석: nest() + map() 패턴 (R)
1.3.1 핵심 아이디어
# 전통적 방법: 직접 필터링 (반복 코드)
lm(price ~ stars + room_type, data=filter(nyc_airbnb, borough=="Manhattan"))
lm(price ~ stars + room_type, data=filter(nyc_airbnb, borough=="Brooklyn"))
# ...
# nest() + map() 패턴: 깔끔한 함수형 프로그래밍
nyc_airbnb |>
nest(data = -borough) |> # borough별 데이터 중첩
mutate(
models = map(data, \(df) lm(price ~ stars + room_type, data=df)),
results = map(models, broom::tidy) # 각 모델 결과 정리
) |>
select(-data, -models) |>
unnest(results)1.3.2 완전한 예시
library(tidyverse)
# Step 1: 그룹별 중첩
nested_data <- nyc_airbnb |>
nest(data = -borough)
print(nested_data)# A tibble: 4 × 2
borough data
<fct> <list>
1 Manhattan <tibble [21,447 × 4]>
2 Brooklyn <tibble [16,810 × 4]>
3 Queens <tibble [5,821 × 4]>
4 Bronx <tibble [1,190 × 4]>
# Step 2: 각 그룹에 모델 적합
results <- nyc_airbnb |>
nest(data = -borough) |>
mutate(
# 각 데이터셋에 lm 적합
models = map(data, \(df) lm(price ~ stars + room_type, data=df)),
# broom::tidy로 결과 정리
tidy_results = map(models, broom::tidy),
# broom::glance로 모델 적합도
glance_results = map(models, broom::glance)
)
# Step 3: 결과 추출
coef_df <- results |>
select(borough, tidy_results) |>
unnest(tidy_results)
print(coef_df |> filter(term == "stars"))borough term estimate std.error statistic p.value
Manhattan stars 21.9 2.7 8.1 <0.001
Brooklyn stars 20.3 2.5 8.1 <0.001
Queens stars 9.5 3.9 2.4 0.016
Bronx stars 4.4 15.0 0.3 0.775
1.3.3 층화 결과 시각화
# stars 효과의 borough별 분포
coef_df |>
filter(term == "stars") |>
ggplot(aes(x = borough, y = estimate, color = borough)) +
geom_point(size=3) +
geom_errorbar(aes(ymin = estimate - 1.96*std.error,
ymax = estimate + 1.96*std.error),
width=0.2) +
geom_hline(yintercept=0, linestyle='dashed') +
labs(x="Borough", y="stars 계수 (95% CI)",
title="Borough별 별점 효과") +
theme_minimal()시각화 결과:
stars 효과
$30 │ •
│ ─ •
$20 │ ─ ─ •
│ ─
$10 │ ─
│ •
$0 │ •
│ ─────────── (Bronx: 효과 없음, CI 큼)
└──────────────────────────
Manhattan Brooklyn Queens Bronx
1.3.4 room_type 효과의 동네별 이질성
# Manhattan 내 동네별 room_type 효과
manhattan_results <- nyc_airbnb |>
filter(borough == "Manhattan") |>
nest(data = -neighborhood) |>
mutate(
models = map(data, \(df) {
# 너무 적은 데이터는 건너뜀
if(nrow(df) < 10) return(NULL)
lm(price ~ stars + room_type, data=df)
}),
results = map(models, \(m) {
if(is.null(m)) return(tibble())
broom::tidy(m)
})
) |>
select(neighborhood, results) |>
unnest(results)
# room_type 효과 동네별 시각화
manhattan_results |>
filter(str_detect(term, "room_type")) |>
ggplot(aes(x = neighborhood, y = estimate)) +
geom_point(alpha=0.7) +
facet_wrap(~term, nrow=2) +
theme(axis.text.x = element_text(angle=80, hjust=1)) +
labs(title="Manhattan 동네별 room_type 효과")결과 패턴:
- Private room 할인폭: 동네마다 -$80 ~ -$160 범위
- Shared room 할인폭: 동네마다 -$100 ~ -$200 범위
- 할인폭의 이질성이 크다 → 동네 랜덤 효과가 필요함을 시사
1.4 Python에서 Stratified Analysis
import pandas as pd
import statsmodels.formula.api as smf
def stratified_regression(df, group_col, formula, min_obs=10):
"""
그룹별 별도 회귀 분석 (R의 nest+map 패턴)
"""
results = []
for group, group_df in df.groupby(group_col):
if len(group_df) < min_obs:
continue
try:
model = smf.ols(formula, data=group_df).fit()
tidy = model.params.reset_index()
tidy.columns = ["term", "estimate"]
tidy["std_error"] = model.bse.values
tidy["p_value"] = model.pvalues.values
tidy["group"] = group
tidy["n"] = len(group_df)
results.append(tidy)
except Exception as e:
print(f"{group}: 모델 적합 실패 ({e})")
return pd.concat(results, ignore_index=True)
# 실행
strat_results = stratified_regression(
df=nyc_airbnb,
group_col="borough",
formula="price ~ stars + C(room_type)"
)
# stars 효과만 추출
stars_effect = strat_results[strat_results["term"] == "stars"]
print(stars_effect[["group", "estimate", "std_error", "p_value", "n"]]) group estimate std_error p_value n
Manhattan 21.9 2.7 <0.001 21447
Brooklyn 20.3 2.5 <0.001 16810
Queens 9.5 3.9 0.016 5821
Bronx 4.4 15.0 0.775 1190
import matplotlib.pyplot as plt
# 계수 + 신뢰구간 시각화
fig, ax = plt.subplots(figsize=(8, 5))
boroughs = stars_effect["group"]
estimates = stars_effect["estimate"]
ci = 1.96 * stars_effect["std_error"]
ax.errorbar(
x=range(len(boroughs)),
y=estimates,
yerr=ci,
fmt='o', capsize=5, capthick=2, markersize=8
)
ax.axhline(0, color='red', linestyle='--', alpha=0.5)
ax.set_xticks(range(len(boroughs)))
ax.set_xticklabels(boroughs)
ax.set_ylabel("stars 계수 (95% CI)")
ax.set_title("Borough별 별점 효과")1.5 층화 분석 → Mixed Model: 자연스러운 확장
층화 분석의 한계:
- 그룹 간 차이 공식 검정 불가 (교호작용 모델이 필요)
- 소규모 그룹 불안정 (Bronx: n=1190, 동네별은 더 작음)
- 정보 공유 없음 — 각 그룹이 독립적으로 추정, 다른 그룹 정보 활용 못함
Mixed Model의 해결:
# 층화 분석: 동네별 별도 모델
# → 56개 동네 × 3개 계수 = 168개 파라미터
# → 소규모 동네에서 불안정
# Mixed Model: 랜덤 기울기로 정보 공유
# → 전체 데이터에서 "평균 패턴" 학습
# → 각 동네는 평균에서의 편차만 추정 (Shrinkage)
# → 소규모 동네는 전체 평균 쪽으로 수축 (안정적)
lmer(
price ~ stars + room_type + (1 + room_type | neighborhood),
data = manhattan_airbnb
)Shrinkage 효과 시각화:
library(lme4)
library(broom.mixed)
# Mixed Model
mm_fit <- lmer(
price ~ stars + room_type + (1 + room_type | neighborhood),
data = manhattan_airbnb
)
# 층화 추정 vs Mixed Model 랜덤 효과 비교
strat_est <- manhattan_results |>
filter(term == "room_typePrivate room") |>
select(neighborhood, strat_estimate = estimate)
mm_re <- ranef(mm_fit)$neighborhood |>
rownames_to_column("neighborhood") |>
select(neighborhood, mm_re = `room_typePrivate room`)
comparison <- left_join(strat_est, mm_re, by="neighborhood")
# 시각화
ggplot(comparison, aes(x=strat_estimate, y=mm_re)) +
geom_point(alpha=0.6) +
geom_abline(slope=1, intercept=0, linestyle='dashed', color='red') +
geom_smooth(method='lm', se=FALSE) +
labs(x="층화 분석 추정치",
y="Mixed Model 랜덤 효과",
title="Shrinkage 효과: 층화 분석 vs Mixed Model")Shrinkage 효과 해석:
- 대각선(빨간 점선): 두 추정이 완전히 동일한 경우
- 실제 점들: 대각선 근처이지만 0(전체 평균) 쪽으로 당겨짐
- 관측치 많은 동네: 층화 ≈ MM (충분한 정보)
- 관측치 적은 동네: MM이 전체 평균 쪽으로 수축 (안정적)
1.6 언제 무엇을 쓰는가
목표: 그룹 간 효과 차이가 존재하는가?
→ 교호작용 모델 + LRT
목표: 각 그룹의 효과 크기를 개별 추정
→ 층화 분석 (충분한 표본) 또는 Mixed Model 랜덤 기울기
목표: 소규모 그룹이 많음 + 안정적 추정
→ Mixed Model (Shrinkage 덕분에 안정)
목표: 탐색적 분석 (어떤 그룹이 다른가 시각화)
→ 층화 분석 → 시각화 → 검정은 교호작용 모델로
실무 권장 순서:
1. 전체 모델 (주효과)
2. 층화 분석 시각화 (탐색)
3. 교호작용 모델 + LRT (공식 검정)
4. Mixed Model 랜덤 기울기 (소규모 그룹 or 많은 그룹)