California Housing
S3 파이프라인 + EBM + DoWhy
소득 수준이 집값에 미치는 인과적 효과는 얼마인가?
Part 2 — California Housing S3 + EBM + DoWhy
S3 파이프라인으로 실험을 버전 관리하고, EBM 회귀 + DoWhy 인과추론으로 집값의 원인을 찾습니다.
이 세션의 목적
오전 Wine 실습 결과물이 S3 output에 잘 저장됐는지 확인하고, Housing 데이터를 S3 data 버킷에 올립니다. conf yml 3종(env / meta / model)을 직접 작성해 실험 설정을 코드와 분리합니다. 이 패턴을 익히면 팀원 간 실험 재현이 가능해지고, 편의점 데이터 스프린트에서 동일한 구조를 그대로 쓸 수 있습니다.
conf yml 3종 작성
| 파일 | 역할 | 주요 내용 |
|---|---|---|
env.yml | 환경 설정 | region, env, S3 버킷명, user_id, log_level |
meta.yml | 실험 메타데이터 | project명, experiment명, 설명, author, tags |
model.yml | 모델 설정 | 모델 타입, 하이퍼파라미터, 피처 목록, treatment 변수 |
📋 env.yml
region: ap-northeast-2
env: dev
conf_bucket: gs-retail-awesome-conf-{region}
data_bucket: gs-retail-awesome-data-{region}
output_bucket: gs-retail-awesome-output-{region}
user_id: your_id # ← 변경
log_level: INFO
📋 model.yml
model_type: EBM_Regressor
framework: interpretML
hyperparameters:
max_bins: 256
interactions: 5
outer_bags: 8
random_state: 42
features:
- MedInc
- HouseAge
- AveRooms
- AveBedrms
- Population
- AveOccup
- Latitude
- Longitude
target: MedHouseVal
treatment: MedInc # DoWhy용
📋 복사해서 붙여넣기
import yaml, boto3, s3fs
with open("conf/env.yml") as f: ENV = yaml.safe_load(f)
with open("conf/meta.yml") as f: META = yaml.safe_load(f)
with open("conf/model.yml") as f: MODEL = yaml.safe_load(f)
USER_ID = ENV['user_id']
PROJECT = META['project']
EXPERIMENT = META['experiment']
REGION = ENV['region']
CONF_PATH = f"s3://gs-retail-awesome-conf-{REGION}/dev/{USER_ID}/{PROJECT}/{EXPERIMENT}"
DATA_PATH = f"s3://gs-retail-awesome-data-{REGION}/dev/{USER_ID}/{PROJECT}/v1/data"
OUTPUT_PATH = f"s3://gs-retail-awesome-output-{REGION}/dev/{USER_ID}/{PROJECT}/{EXPERIMENT}"
s3 = boto3.client('s3')
CONF_BUCKET = f"gs-retail-awesome-conf-{REGION}"
for fname in ["env.yml", "meta.yml", "model.yml"]:
s3.upload_file(f"conf/{fname}", CONF_BUCKET,
f"dev/{USER_ID}/{PROJECT}/{EXPERIMENT}/{fname}")
print(f" ✓ {fname} → conf 버킷")
회귀 EBM — 분류와의 차이
Wine에서는 ExplainableBoostingClassifier(분류)를 썼습니다. Housing에서는 ExplainableBoostingRegressor(회귀)를 씁니다. Shape function의 y축이 "클래스 확률 기여도"에서 "$단위 기여도"로 바뀝니다. "이 피처가 1단위 증가할 때 집값이 평균 얼마나 변하는가"를 구간별로 읽을 수 있습니다.
📋 복사해서 붙여넣기
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from interpret.glassbox import ExplainableBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# ── 데이터 로드
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = pd.Series(housing.target, name="MedHouseVal")
# ── 70/15/15 분리
X_train, X_tmp, y_train, y_tmp = train_test_split(X, y, test_size=0.3, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_tmp, y_tmp, test_size=0.5, random_state=42)
# ── EBM 회귀 학습
ebm_reg = ExplainableBoostingRegressor(
max_bins=256, interactions=5, outer_bags=8, random_state=42)
ebm_reg.fit(X_train, y_train)
# ── 성능 평가
val_pred = ebm_reg.predict(X_val)
val_rmse = np.sqrt(mean_squared_error(y_val, val_pred))
val_r2 = r2_score(y_val, val_pred)
print(f"Val RMSE: {val_rmse:.4f} (~${val_rmse*100:.0f}K)")
print(f"Val R² : {val_r2:.4f}")
| Feature | EBM Importance | 역할 태그 | DoWhy 처리 |
|---|---|---|---|
| Latitude | ~0.95 (1위) | Confounder | 교란 변수로 통제 |
| Longitude | ~0.83 (2위) | Confounder | 교란 변수로 통제 |
| MedInc | ~0.45 (3위) | Treatment (처치) | 인과 효과 추정 대상 |
| AveOccup | ~0.23 | Confounder | 교란 변수로 통제 |
| AveRooms | ~0.10 | Secondary | 보조 맥락 변수 |
왜 DoWhy가 필요한가?
EBM에서 Latitude(위도)가 1위, MedInc(소득)이 3위로 나왔습니다. 그렇다고 "위도가 집값의 주요 원인"이라고 말할 수 없습니다. 위도는 LA권/SF권처럼 지역 클러스터를 나타내는 교란 변수입니다. DoWhy는 이 교란 변수를 명시적으로 통제하고, "소득 수준만 바꾸면 집값이 실제로 얼마나 달라지는가"를 추정합니다.
📋 복사해서 붙여넣기
import dowhy
from dowhy import CausalModel
# ── 인과 DAG 정의 (GML 형식)
# MedInc(처치) → MedHouseVal(결과)
# Latitude, Longitude, AveRooms, AveOccup → MedInc AND MedHouseVal (교란)
causal_graph = """
digraph {
MedInc -> MedHouseVal;
Latitude -> MedInc; Latitude -> MedHouseVal;
Longitude -> MedInc; Longitude -> MedHouseVal;
AveRooms -> MedInc; AveRooms -> MedHouseVal;
AveOccup -> MedInc; AveOccup -> MedHouseVal;
HouseAge -> MedHouseVal;
}
"""
model = CausalModel(
data=pd.concat([X_train, y_train], axis=1),
treatment="MedInc",
outcome="MedHouseVal",
graph=causal_graph
)
print("✓ 인과 모델 생성 완료")
📋 복사해서 붙여넣기
# ── STEP 1: Identify (인과 효과 식별)
identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
print(identified_estimand)
# ── STEP 2: Estimate (ATE 추정)
estimate = model.estimate_effect(
identified_estimand,
method_name="backdoor.linear_regression",
target_units="ate"
)
print(f"\nATE (MedInc → MedHouseVal): {estimate.value:.4f}")
print(f"해석: MedInc $10K 증가 시 집값 평균 ${estimate.value * 100000:.0f} 변화")
# ── STEP 3: Refute (인과 효과 검증)
refute_random = model.refute_estimate(
identified_estimand, estimate,
method_name="random_common_cause"
)
print(refute_random)
refute_placebo = model.refute_estimate(
identified_estimand, estimate,
method_name="placebo_treatment_refuter",
placebo_type="permute"
)
print(refute_placebo)
Primary / Secondary Index 선정
DoWhy 결과를 바탕으로 편의점 데이터 스프린트에서 쓸 index 체계를 정의합니다.
| 구분 | 피처 | 근거 | 편의점 대응 |
|---|---|---|---|
| Primary Index | MedInc | DoWhy ATE +$35,390 — 인과 효과 확인 | area_income_level, foot_traffic |
| Secondary Index | HouseAge | ATE +$550 — 미미하지만 양(+). 감가 없음 확인 | store_age, location_type |
| Confounder | Latitude, Longitude | EBM importance 높지만 인과 경로 아님. 통제 필요 | 입지 유형, 경쟁점 수 |
| Confounder | AveOccup | 과밀 거주 효과. 처치·결과 양쪽에 영향 | 요일, 공휴일 |