ML 모델: 왜 당신의 예측은 좋은가... 그렇지 않을 때까지
Source: Dev.to

데이터 사이언티스트의 실제 업무에 대한 기술적 몰입: 피처 엔지니어링, 파이프라인 및 비즈니스 메트릭.
제가 여러분에게 전하고 싶은 것은 업계에서 매일 겪는 현실입니다: Jupyter Notebook에서 model.fit(X, y)를 작성하는 것과 경영진 회의에서 실제 신뢰를 얻을 수 있는 머신러닝 시스템을 구축하는 것 사이에는 압도적인 차이가 있습니다.
대부분의 튜토리얼은 보스턴 주택 가격을 예측하거나 아이리스 꽃을 분류하는 방법을 가르칩니다. 이는 학술적인 연습에 불과합니다. 하지만 실제 세계에서는 데이터가 더럽고, 요구사항이 변하며, 프로덕션에 배포된 모델은 조용히 실패합니다.
오늘은 “Hello World” 를 제쳐두고, 오늘만 훈련한다면 내일 모델이 왜 실패할지, 그리고 Scikit‑Learn 을 취미가 아니라 전문 데이터 엔지니어처럼 사용하는 방법을 살펴보겠습니다.
Source: …
1️⃣ 현실적인 시나리오
우리가 부에노스아이레스에 있는 부동산 회사의 데이터 팀이라고 가정해 보겠습니다. 우리는 아파트의 판매 가격(precio_final)을 예측하는 모델을 요청받았습니다.
| Variable | Tipo | Comentario |
|---|---|---|
m2_totales | Numérico | 실제 면적. |
barrio | Categórico | 팔레르모, 레코레타 등. |
antiguedad | Numérico (años) | |
tiene_pileta | Binario (0/1) | |
fecha_venta | Timestamp | 예제에서는 사용되지 않지만 일반적입니다. |
1.1 함정: 유튜브에서 보는 모델
기본적인 작업—Pandas를 불러오고, 결측치를 정리하고, LinearRegression이나 빠른 RandomForest를 훈련시키는—만 하면 흔히 겪는 문제에 부딪히게 됩니다:
- 새 데이터가 스케일링되지 않으면 모델이 엉망이 됩니다.
barrio에 One‑Hot Encoding을 적용하지 않으면 모델이 깨집니다.- 모델이 “쓰레기”(데이터 누수)를 학습하면 개발 단계에서는 완벽해 보이지만 실제에서는 손해를 보게 됩니다.
2️⃣ 전문가 솔루션: 파이프라인 및 전처리
Python Baires에서는 성공의 핵심이 알고리즘(모두 Random Forest 사용)이 아니라 데이터를 정리하고 준비하는 **파이프라인 (Pipeline)**이라고 가르칩니다. 우리는 Scikit‑Learn의 ColumnTransformer와 Pipeline을 사용해 혼란을 자동화할 것입니다.
2.1 더러운 데이터 생성 (시뮬레이션)
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
# --- 1. SIMULACIÓN DE DATOS SUCIOS ---
np.random.seed(42)
n_muestras = 1000
data = {
'm2_totales': np.random.normal(70, 30, n_muestras).astype(float),
'barrio': np.random.choice(['Palermo', 'Recoleta', 'Caballito', 'Almagro'], n_muestras),
'antiguedad': np.random.randint(0, 60, n_muestras).astype(float),
'tiene_pileta': np.random.choice([0, 1], n_muestras, p=[0.7, 0.3]),
}
df = pd.DataFrame(data)
# Introducimos errores humanos (nulos, outliers)
df.loc[5:15, 'm2_totales'] = np.nan
df.loc[20:25, 'antiguedad'] = np.nan
df.loc[30, 'barrio'] = 'sin dato' # Categoría sucia
df.loc[35, 'antiguedad'] = 150 # Outlier imposible
# Generamos el target (y) añadiendo ruido a una operación lógica
df['precio_final'] = (
df['m2_totales'].fillna(0) * 2500 +
df['antiguedad'].fillna(0) * -100 +
df['tiene_pileta'] * 15000 +
np.random.normal(0, 20000, n_muestras)
)
# --- 2. SEPARACIÓN Y VALIDACIÓN INICIAL ---
df = df.dropna(subset=['precio_final'])
X = df.drop('precio_final', axis=1)
y = df['precio_final']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("Datos listos. Observa la columna 'barrio' y los NaN en m2:")
print(X_train.head())
3️⃣ 워크플로우 설계 (Workflow)
DataFrame을 수동으로 정리하는 대신, 전체 로직을 캡슐화하는 객체를 생성합니다. 우리의 워크플로우는 세 가지 작업을 수행해야 합니다:
- 수치값 결측치 대체 (예:
m2_totales의 NaN을 중앙값으로 채우기). - 수치값 스케일링 (표준화하여
antiguedad가 크기 때문에 지배하지 않도록). - 범주형 처리 (
barrio를 더미 변수로 변환).
# 1. Definimos qué columnas son numéricas y cuáles categóricas
numeric_features = ['m2_totales', 'antiguedad', 'tiene_pileta']
categorical_features = ['barrio']
# 2. Creamos los "tubos" de procesamiento
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(handle_unknown='ignore'))
])
# 3. Juntamos todo en un ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
]
)
# 4. Construimos el Pipeline final (preprocesador + modelo)
full_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('model', RandomForestRegressor(
n_estimators=200,
random_state=42,
n_jobs=-1
))
])
# 5. Entrenamos
full_pipeline.fit(X_train, y_train)
# 6. Predicción y métricas
y_pred = full_pipeline.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
rmse = mean_squared_error(y_test, y_pred, squared=False)
r2 = r2_score(y_test, y_pred)
print(f"MAE : ${mae:,.0f}")
print(f"RMSE: ${rmse:,.0f}")
print(f"R² : {r2:.3f}")
3.1 우리가 측정하는 것
| 지표 | 비즈니스에서 중요한 이유 |
|---|---|
| MAE (Mean Absolute Error) | 평균적으로 몇 페소(아르헨티나 페소) 정도 오차가 나는지 알려줍니다. 경영진에게 전달하기 쉽습니다. |
| RMSE (Root Mean Squared Error) | 큰 오류에 대해 강하게 패널티를 부여합니다; 이상치가 큰 비용을 초래할 때 유용합니다. |
| R² | 설명된 분산 비율을 나타내며; 모델을 빠르게 비교하는 데 사용됩니다. |
4️⃣ 파이프라인을 프로덕션에 배포하기
joblib또는pickle로 직렬화하기.- 모델과 파이프라인을 버전 관리하기 (예: DVC 또는 MLflow).
- API 생성 (FastAPI, Flask) – 필드가 포함된 JSON을 받아 예측을 반환.
- 데이터 드리프트와 메트릭 저하를 모니터링하고 필요 시 재학습하기.
import joblib
# Guardar
joblib.dump(full_pipeline, 'pipeline_rf.pkl')
# Cargar en producción
pipeline_prod = joblib.load('pipeline_rf.pkl')
# Ejemplo de predicción en producción
sample = pd.DataFrame([{
'm2_totales': 85,
'barrio': 'Palermo',
'antiguedad': 10,
'tiene_pileta': 1
}])
print(pipeline_prod.predict(sample))
5️⃣ 결론
- 아니오 “훈련하고 끝”만으로는 충분하지 않다.
- 파이프라인은 재현성을 보장하고, 데이터 누수를 방지하며, 프로덕션 전환을 간소화한다.
- 비즈니스 지표(예: 페소 단위 MAE)는 경영진을 설득할 수 있는 공통 언어이다.
모델이 다음 날에도 살아남게 하려면 Feature Engineering, 파이프라인, 그리고 비즈니스에 맞춘 지표에 시간을 투자하라. 이것이 전문 데이터 사이언티스트와 노트북 애호가를 구분하는 점이다!
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
import pandas as pd
# Supongamos que ya tienes X_train, X_test, y_train, y_test definidos
# y que las columnas categóricas son 'barrio' y la numérica es 'm2_totales'
# 1️⃣ Pre‑procesamiento de columnas
preprocess = ColumnTransformer(
transformers=[
('cat', OneHotEncoder(handle_unknown='ignore'), ['barrio']),
('num', StandardScaler(), ['m2_totales'])
]
)
# 2️⃣ Pipeline completa (pre‑procesamiento + modelo)
full_pipeline = Pipeline(steps=[
('preprocess', preprocess),
('regressor', RandomForestRegressor(
n_estimators=100,
random_state=42,
n_jobs=-1
))
])
# Entrenamos (esto transforma y entrena en un solo paso)
full_pipeline.fit(X_train, y_train)
# Predicciones
y_pred = full_pipeline.predict(X_test)
비즈니스를 속이지 마세요: 올바른 평가
기본 튜토리얼에서는 R2만 알려줄 것입니다. 산업 현장에서는 돈에 대한 오류가 중요합니다.
- MAE (Mean Absolute Error, 평균 절대 오차): “평균적으로 우리 모델은 X 페소만큼 틀립니다.”
- RMSE (Root Mean Squared Error, 평균 제곱근 오차): 큰 오류에 훨씬 더 큰 패널티를 부여합니다 (희귀한 경우에 모델이 터무니없이 크게 틀리는 것을 원하지 않을 때 필수).
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
print("\n--- REPORTE DE NEGOCIO ---")
print(f"R2 Score: {r2:.3f} (Explica el {r2*100:.1f}% de la varianza del precio)")
print(f"MAE: ${mae:,.0f} (Error promedio absoluto)")
print(f"RMSE: ${rmse:,.0f} (Desviación del error)")
# Demostración de uso final
print("\n--- PREDICCIÓN DE EJEMPLO ---")
ejemplo = pd.DataFrame({
'm2_totales': [85.0],
'barrio': ['Palermo'],
'antiguedad': [10.0],
'tiene_pileta': [1]
})
precio_estimado = full_pipeline.predict(ejemplo)[0]
print(f"Un depto en Palermo de 85 m² con pileta se estima en: ${precio_estimado:,.0f}")
이것이 당신의 상사에게 의미하는 바는?
- MAE가 15 000 페소라면, 평균적으로 모델이 그 금액만큼 틀린다는 의미입니다.
- RMSE가 50 000이라면, 오류가 훨씬 더 크게 나타나는 경우가 있음을 나타냅니다 (아마도 모델이 제대로 학습하지 못한 고급 아파트일 수 있습니다).
지식 격차 (그리고 왜 이 글을 읽고 있는가)
우리가 방금 본 것은 프로덕션에 투입되는 모델에 대한 최소 품질 기준입니다. 하지만…
- 하이퍼파라미터 최적화를 다루지 않았습니다: (GridSearch, RandomizedSearch) 모델을 튜닝하기 위해.
- 배포(Deployment)를 다루지 않았습니다: (FastAPI나 Flask로 REST API로 노출해서 부동산 업체가 웹에서 사용할 수 있게 하려면 어떻게 할까요?).
- 딥러닝을 다루지 않았습니다: (문제가 선형 회귀보다 훨씬 복잡한 패턴을 가질 때).
이것이 파이썬 주니어 개발자와 시니어 데이터 사이언티스트를 구분하는 차이점입니다.
다음 단계
블로그에서 코드를 복사하는 것은 쉽습니다. 데이터셋 크기를 바꾸거나 다른 출처의 새로운 데이터가 들어올 때 그 코드가 왜 실패하는지 이해하는 것이 여러분의 경력을 정의합니다.
“스크립트를 고치는 파이썬 소년”에서 벗어나 기업을 변화시키는 AI 솔루션을 설계하고 싶다면, Python Baires에서 현지 시장에서 가장 완벽한 머신러닝 과정을 제공합니다.
코딩을 배우는 것이 전부가 아닙니다. 문제 해결을 배우는 것입니다.
전체 프로그램을 확인하고 자리를 예약하세요: