ML模型:为什么你的预测是好的……直到它不是

发布: (2025年12月28日 GMT+8 09:43)
10 min read
原文: Dev.to

Source: Dev.to

Imagen del artículo

对数据科学家真实工作的技术深入:特征工程、流水线和业务指标。

我来分享我们在行业中每天都经历的事情:在 Jupyter Notebook 中会写 model.fit(X, y) 与构建一个真正能在董事会议上赢得信任的机器学习系统之间存在巨大的差距。

大多数教程教你预测波士顿的房价或对 Iris 花卉进行分类。这些都是学术练习。但在真实世界中,数据往往脏乱,需求会变化,而你的模型在生产环境中会悄悄地失效。

今天我们将抛开 “Hello World”。我们将看看如果你只在今天训练,为什么你的模型明天会失效,以及如何像专业数据工程师而不是业余爱好者那样使用 Scikit‑Learn

1️⃣ 现实情境

想象我们是 布宜诺斯艾利斯一家房地产公司的数据团队。他们要求我们构建一个模型来估算公寓的成交价(precio_final)。

变量类型备注
m2_totales数值型实际面积。
barrio类别型Palermo、Recoleta 等。
antiguedad数值型(年)
tiene_pileta二元(0/1)
fecha_venta时间戳本例不使用,但在实际中很常见。

1.1 陷阱:我们在 YouTube 上看到的模型

如果我们只做最基础的操作——导入 Pandas、清理缺失值,然后快速训练一个 LinearRegressionRandomForest——会遇到常见问题:

  • 如果新数据进来时没有进行特征缩放,模型会失控。
  • 如果忘记对 barrio 进行 One‑Hot Encoding,模型会崩溃。
  • 如果模型学会预测“垃圾”(数据泄漏),在开发阶段看起来完美,但在真实环境中会亏钱。

2️⃣ 专业解决方案:管道与预处理

Python Baires 中,我们教导成功的关键不是算法(大家都用随机森林),而是 管道(Pipeline),它负责清洗和准备数据。我们将使用 Scikit‑Learn 的 ColumnTransformerPipeline 来自动化混乱。

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())

Source:

3️⃣ 设计工作流 (Workflow)

与其手动清洗 DataFrame,我们创建一个封装全部逻辑的对象。我们的工作流需要完成三件事:

  1. 数值特征插补(例如,用中位数填充 m2_totales 中的 NaN)。
  2. 数值特征缩放(标准化,使 antiguedad 不因量级过大而主导)。
  3. 类别特征处理(将 barrio 转换为哑变量)。
# 1. 定义哪些列是数值型,哪些是类别型
numeric_features = ['m2_totales', 'antiguedad', 'tiene_pileta']
categorical_features = ['barrio']

# 2. 创建“管道”进行处理
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. 将所有管道组合到 ColumnTransformer 中
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ]
)

# 4. 构建最终 Pipeline(预处理器 + 模型)
full_pipeline = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('model', RandomForestRegressor(
        n_estimators=200,
        random_state=42,
        n_jobs=-1
    ))
])

# 5. 训练
full_pipeline.fit(X_train, y_train)

# 6. 预测与评估
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(平均绝对误差)告诉我们平均误差是多少(以阿根廷比索计),便于向管理层说明。
RMSE(均方根误差)对大误差惩罚更重;当异常值代价高时尤为有用。
表示解释的方差比例,便于快速比较模型。

4️⃣ 将 Pipeline 部署到生产环境

  1. 序列化 使用 joblibpickle
  2. 版本管理 模型和管道(例如使用 DVC 或 MLflow)。
  3. 创建 API(FastAPI、Flask),接收包含字段的 JSON 并返回预测结果。
  4. 监控 数据漂移和指标退化;必要时重新训练。
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️⃣ 结论

  • 仅仅是“训练完就好”。
  • pipeline 确保可复现性,避免数据泄漏,并简化上线过程。
  • 业务指标(例如以比索计的 MAE)是说服管理层的通用语言。

如果你想让模型在第二天仍然有效,投入时间在 特征工程管道与业务对齐的指标 上。这就是专业数据科学家与笔记本爱好者的区别!

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(平均绝对误差): “平均而言,我们的模型在 X 比索上出错。”
  • RMSE(均方根误差): 对大误差的惩罚更高(如果你不想让模型在少数极端情况下出现巨大的错误,这一点至关重要)。
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 暴露,以便房地产公司在其网站上使用?)。
  • 我们没有看到深度学习:(当问题的模式远比线性回归更复杂时)。

这些差距将 Python 初级程序员高级数据科学家 区分开来。

下一步

复制博客上的代码很容易。理解为什么当你更改数据集大小或引入来自不同来源的新数据时,代码会出错……这才决定你的职业生涯。

如果你想摆脱“Python 小子”只会修复脚本的角色,开始设计能够改变企业的 AI 解决方案,在 Python Baires 我们提供本地市场最完整的机器学习课程。

这不仅是学习编码,而是学习解决问题。

查看完整课程并预定你的名额:

👉 https://www.python-baires.ar/Curso-ml.html

Back to Blog

相关文章

阅读更多 »

特征工程

什么是特征工程? - 特征只是数据的一列,例如年龄、薪水、购买次数。 - 特征工程指的是创建、修改或…

Train-Test Split 的终结

文章 URL: https://folio.benguzovsky.com/train-test 评论 URL: https://news.ycombinator.com/item?id=46149740 得分: 7 评论数: 1