26~29편에서 결측치를 채우고, 이상치를 걸러내고, 상관관계와 통계적 유의성을 확인했다. 이 모든 과정은 사실 "모델을 만들기 전에 반드시 거쳐야 하는 단계"이기도 하다. 이번 편에서는 이 흐름 전체를 하나의 재현 가능한 파이프라인으로 묶는다.
한 줄 정의
CRISP-DM 은 분석 문제를 업무 이해부터 배포까지 6단계로 나누는 방법론이고, sklearn Pipeline 은 전처리와 모델을 하나의 객체로 묶어 그 흐름을 코드로 재현 가능하게 만드는 도구다.
CRISP-DM — 기술이 아니라 문제 해결 중심
단계 | 하는 일 |
|---|---|
업무 이해 | 무엇을 예측·설명해야 하는지 문제를 정의한다 |
데이터 이해 | EDA로 데이터의 분포·결측·이상치를 파악한다 (26·28·29편) |
데이터 준비 | 결측 처리, 이상치 제거, 인코딩·스케일링으로 모델 입력을 만든다 |
모델링 | 문제에 맞는 알고리즘을 선택하고 학습한다 |
평가 | 성능 지표로 목표를 달성했는지 확인한다 |
배포 | 서비스에 적용하고 이후 재현·모니터링이 가능하게 만든다 |
이 6단계는 순서대로 한 번만 도는 것이 아니라, 평가 결과가 나쁘면 데이터 준비나 업무 이해로 되돌아가는 반복 과정이다. 실무에서는 데이터 준비 단계가 전체 시간의 대부분을 차지한다고 흔히 이야기한다 — 26~29편에서 다룬 내용이 바로 이 단계다. 알고리즘을 잘 고르는 것보다 데이터를 제대로 이해하는 것이 먼저다.
예제: 주문 데이터로 매출 예측하기
지역·카테고리·방문 횟수·할인율로 주문 금액을 예측하는 회귀 문제를 CRISP-DM 흐름으로 풀어본다. visits 열에는 결측치가 10건 섞여 있다.
region,category,visits,discount_rate,amount
대구,식품,2.0,0.112,6307.0
인천,식품,5.0,0.155,5043.0
서울,가전,3.0,0.203,16727.0
인천,가전,14.0,0.116,21252.0
...(총 500행, visits 10곳 결측)
왜 전처리를 따로 떼어 하면 위험한가
수치 열은 스케일을 맞추고, 범주 열은 원-핫 인코딩해야 한다. 이 전처리를 학습 데이터와 테스트 데이터를 나누기 전에 전체 데이터에 적용하면, 테스트 데이터의 정보(평균·분산 같은 통계량)가 학습 과정에 스며든다. 이를 데이터 유출(data leakage) 이라고 부르며, 실제 배포 환경보다 성능이 좋게 나오는 착시를 만든다. StandardScaler 는 fit() 시점에 평균·표준편차를 계산해 저장하므로, 이 계산은 반드시 학습 데이터에만 해야 한다.
Pipeline 은 이 순서를 강제한다. model.fit(X_train, y_train) 한 번이면 내부의 모든 전처리 단계가 학습 데이터에만 fit 되고, 이후 predict() 는 그때 저장된 통계량으로 transform 만 수행한다.
ColumnTransformer로 열마다 다른 전처리 적용하기
수치 열과 범주 열은 필요한 처리가 다르다. ColumnTransformer 로 열 이름별로 처리 방법을 나눠 지정한다.
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import Ridge
num_cols = ["visits", "discount_rate"]
cat_cols = ["region", "category"]
num_pipe = Pipeline([
("impute", SimpleImputer(strategy="median")), # 결측치를 중앙값으로
("scale", StandardScaler()),
])
preproc = ColumnTransformer([
("num", num_pipe, num_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
])
수치 열은 결측 보완(SimpleImputer) 뒤에 스케일링을, 범주 열은 원-핫 인코딩을 거친다. 전처리 안에 또 다른 Pipeline(num_pipe)을 넣을 수 있다 — 이렇게 중첩해서 필요한 단계를 얼마든지 쌓을 수 있다.
Pipeline 조립 — fit·predict·score
from sklearn.model_selection import train_test_split
X = df[num_cols + cat_cols]
y = df["amount"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = Pipeline([
("prep", preproc),
("reg", Ridge(alpha=1.0)),
])
model.fit(X_train, y_train)
r2 = model.score(X_test, y_test)
print(f"R2: {r2:.3f}")
R2: 0.894
전처리 → 모델 전체가 model 이라는 객체 하나다. fit 한 번으로 결측 보완·스케일링·인코딩·회귀 학습이 순서대로 실행되고, score 는 테스트 데이터에 같은 전처리(이번에는 transform 만)를 적용한 뒤 결정계수(R²)를 계산한다.
joblib으로 저장하고 다시 불러오기
배포 환경에서는 학습에 썼던 것과 완전히 같은 전처리가 적용돼야 한다. Pipeline 전체를 joblib 으로 저장하면 전처리 통계량(평균·표준편차·인코딩 범주)까지 그대로 보존된다.
import joblib
joblib.dump(model, "model.pkl")
loaded = joblib.load("model.pkl")
print((loaded.predict(X_test) == model.predict(X_test)).all())
True
저장 전과 로딩 후의 예측이 완전히 같다. 전처리 단계와 모델을 따로 저장했다면, 배포 시점에 그 둘을 정확히 같은 순서·같은 설정으로 다시 조립해야 하는 부담이 남는다. Pipeline은 그 조립 순서 자체를 파일 하나에 담는다.
교차검증으로 안정성 확인하기
학습·테스트를 한 번만 나눠 본 점수는 그 한 번의 분할에 운이 좋았을 수도 있다. cross_val_score 에 Pipeline 객체를 그대로 넘기면, 각 폴드마다 전처리까지 새로 fit 하며 여러 번 나눠 평가한다.
from sklearn.model_selection import cross_val_score, KFold
cv = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=cv, scoring="r2")
print(scores.round(3), scores.mean().round(3))
[0.894 0.928 0.928 0.898 0.898] 평균: 0.909
다섯 폴드의 점수가 0.894~0.928 사이로 고르다. 한 번의 분할로 얻은 0.894가 우연히 낮게 나온 값이 아니라는 근거다. Pipeline을 쓰지 않고 전처리를 미리 전체 데이터에 적용해뒀다면, 이 교차검증 자체가 데이터 유출을 포함한 채로 진행돼 점수를 신뢰할 수 없다.
장점과 한계
구분 | 내용 |
|---|---|
장점 | 학습·평가·배포에서 전처리 순서가 항상 같음을 보장하고, 교차검증·그리드서치 같은 sklearn 도구에 모델 하나처럼 바로 넘길 수 있다. |
한계 | 중간 단계의 결과를 확인하려면 |
자주 발생하는 문제
분할 전에 전체 데이터로 스케일러를 미리 fit한다. scaler.fit(X) 를 train_test_split 보다 먼저 실행하면, 테스트 데이터의 평균·분산이 이미 스케일링 기준에 반영된 뒤다. Pipeline을 학습·평가 전체 흐름에 넣고 fit 을 항상 학습 데이터에서만 호출하는 습관으로 막는다.
handle_unknown 기본값을 그대로 둔다. OneHotEncoder 는 학습 때 못 본 범주 값을 만나면 기본적으로 오류를 낸다.
enc = OneHotEncoder() # handle_unknown 기본값: "error"
enc.fit(train) # 학습에는 서울·부산·대구만 있었다
enc.transform(test) # 테스트에 "광주"가 새로 등장
ValueError: Found unknown categories ['광주'] in column 0 during transform
실제 서비스에서는 학습 때 없던 범주가 언젠가 들어온다. handle_unknown="ignore" 로 두면 새 범주를 모두 0인 벡터로 처리하고 예측을 계속 진행한다.
모델 성능만 보고 CRISP-DM의 앞 단계를 건너뛴다. R²가 낮게 나오면 알고리즘을 바꾸기 전에 데이터 이해·준비 단계로 돌아가는 것이 먼저다. 결측·이상치를 놓쳤거나, 애초에 지금 가진 열만으로는 설명이 안 되는 문제일 수 있다.
관련 문서
이 문서의 모델은 26·29편에서 다룬 결측치 처리·상관관계 분석을 전제로 한다. 통계 없이 모델을 만들면 왜 결과를 해석하기 어려운지는 기술통계와 가설검정 기초 를, 원본 데이터 정제 방법은 Pandas로 데이터 탐색·정제·집계하기 를 먼저 본다.
참고 자료
scikit-learn 공식 문서 — Pipelines and composite estimators (2026-08-10 확인)
scikit-learn 공식 문서 — ColumnTransformer (2026-08-10 확인)
joblib 공식 문서 — Persistence (2026-08-10 확인)
댓글 0
댓글을 불러오는 중…