第02章:数据准备——模型质量的真正决定因素
第02章:数据准备——模型质量的真正决定因素
“Garbage in, garbage out. 95%的ML项目失败不是因为算法选错了,而是因为数据处理得太草率。数据工程才是ML工程师最核心的技能。”
一、为什么数据比算法更重要
一个常见的误区是:花80%的时间选择和调优算法,花20%的时间处理数据。
现实恰好相反。工业界的经验规律:
- 数据质量提升10%,通常比算法升级带来更多收益
- 大多数ML问题,用标准算法+好数据,效果优于复杂算法+差数据
- 数据问题通常是沉默的——模型会"学习",只是学到了错误的东西
本章学习完整的数据准备流程:探索 → 清洗 → 转换 → 划分。
二、探索性数据分析(EDA)
在动手处理之前,先理解你的数据。EDA是数据科学中最有价值也最被忽视的步骤。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 加载示例数据集(房价预测)
df = pd.read_csv("housing.csv")
# === 第一步:基本情况 ===
print("数据形状:", df.shape)
print("\n数据类型:\n", df.dtypes)
print("\n基本统计:\n", df.describe())
# === 第二步:缺失值分析 ===
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(2)
missing_report = pd.DataFrame({
"缺失数量": missing,
"缺失比例(%)": missing_pct
}).query("缺失数量 > 0").sort_values("缺失比例(%)", ascending=False)
print("\n缺失值报告:\n", missing_report)
# === 第三步:目标变量分布 ===
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
df["price"].hist(bins=50)
plt.title("价格分布(原始)")
plt.subplot(1, 2, 2)
np.log1p(df["price"]).hist(bins=50)
plt.title("价格分布(对数变换后)")
plt.tight_layout()
plt.savefig("price_distribution.png")
# === 第四步:特征与目标的相关性 ===
numeric_cols = df.select_dtypes(include=[np.number]).columns
correlation = df[numeric_cols].corr()["price"].sort_values(ascending=False)
print("\n与价格的相关性:\n", correlation)
# === 第五步:检测异常值 ===
def find_outliers(series: pd.Series, n_std: float = 3) -> pd.Series:
"""使用标准差方法找异常值"""
mean = series.mean()
std = series.std()
return (series < mean - n_std * std) | (series > mean + n_std * std)
for col in ["price", "area", "rooms"]:
if col in df.columns:
outlier_count = find_outliers(df[col]).sum()
print(f"{col}: {outlier_count} 个异常值")
三、缺失值处理
缺失值没有"正确答案"——策略取决于缺失原因和业务含义。
from sklearn.impute import SimpleImputer, KNNImputer
# 策略一:删除(缺失比例 > 50% 时考虑)
df_cleaned = df.dropna(subset=["critical_column"]) # 删除关键列为空的行
df_cleaned = df.drop(columns=["mostly_empty_column"]) # 删除基本是空的列
# 策略二:简单填充
# 数值型:用均值或中位数
imputer_mean = SimpleImputer(strategy="mean")
imputer_median = SimpleImputer(strategy="median")
# 分类型:用众数
imputer_mode = SimpleImputer(strategy="most_frequent")
# 策略三:KNN填充(更精准,但更慢)
knn_imputer = KNNImputer(n_neighbors=5)
df_imputed = pd.DataFrame(
knn_imputer.fit_transform(df[numeric_cols]),
columns=numeric_cols
)
# 策略四:添加"是否缺失"标记列(有时缺失本身就是信息)
df["age_is_missing"] = df["age"].isnull().astype(int)
df["age_filled"] = df["age"].fillna(df["age"].median())
# === 重要原则:在训练集上fit,在测试集上transform ===
# 错误做法(数据泄露):
# imputer.fit_transform(all_data) # 测试集影响了填充值
# 正确做法:
imputer.fit(X_train) # 只用训练集学习统计量
X_train_imputed = imputer.transform(X_train)
X_test_imputed = imputer.transform(X_test) # 用训练集的统计量
四、数据集划分与数据泄露
**数据泄露(Data Leakage)**是ML项目中最常见也最隐蔽的错误,会导致模型在测试集上表现优异,但在生产中完全失效。
from sklearn.model_selection import train_test_split
# === 基本划分 ===
X = df.drop("target", axis=1)
y = df["target"]
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, # 80%训练,20%测试
random_state=42, # 固定随机种子,确保可复现
stratify=y # 分层采样,确保各类别比例一致
)
# === 时间序列数据:不能随机划分!===
df = df.sort_values("date")
split_point = int(len(df) * 0.8)
train_df = df.iloc[:split_point] # 时间上靠前的用于训练
test_df = df.iloc[split_point:] # 时间上靠后的用于测试
# === 常见数据泄露场景 ===
# ❌ 泄露1:在全数据集上做归一化
scaler.fit_transform(X) # 测试集的均值/方差"泄露"到了训练
# ✅ 正确:只在训练集fit
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# ❌ 泄露2:使用未来信息作为特征
# 例如:预测用户是否流失,用了"本月最后一天的登录次数"
# 如果预测是在月中进行的,这个特征在预测时根本不存在
# ❌ 泄露3:目标编码时使用了测试集
# 用整体均值做目标编码时,测试集的标签污染了特征
五、使用sklearn Pipeline防止泄露
Pipeline是防止数据泄露的最可靠工具,也是生产代码的最佳实践:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
# 定义数值和分类列
numeric_features = ["age", "income", "years_experience"]
categorical_features = ["city", "job_type", "education"]
# 数值特征处理流水线:填充 → 标准化
numeric_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
])
# 分类特征处理流水线:填充 → 独热编码
categorical_transformer = Pipeline(steps=[
("imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
# 合并特征处理
preprocessor = ColumnTransformer(transformers=[
("num", numeric_transformer, numeric_features),
("cat", categorical_transformer, categorical_features)
])
# 完整Pipeline:预处理 + 模型
full_pipeline = Pipeline(steps=[
("preprocessor", preprocessor),
("classifier", RandomForestClassifier(n_estimators=100, random_state=42))
])
# 训练
full_pipeline.fit(X_train, y_train)
# 评估(Pipeline自动对X_test做同样的预处理)
score = full_pipeline.score(X_test, y_test)
print(f"准确率: {score:.4f}")
# 保存Pipeline(包含所有预处理参数)
import joblib
joblib.dump(full_pipeline, "model_pipeline.pkl")
# 加载并预测
loaded_pipeline = joblib.load("model_pipeline.pkl")
new_predictions = loaded_pipeline.predict(new_data)
本章小结
- 数据质量是ML项目成败的第一决定因素,投入数据处理的时间永远是值得的。
- EDA(探索性数据分析)必须先于模型训练:了解分布、缺失、异常、相关性。
- 缺失值处理的策略取决于缺失原因,没有万能方案,但必须在训练集上fit才能用于测试集。
- 数据泄露是ML中最隐蔽的错误,症状是测试集分高但生产失效——根治方案是sklearn Pipeline。
- 时间序列数据必须按时间顺序划分,不能随机划分,否则会产生未来信息泄露。
核心行动建议: 今天找一个Kaggle数据集(推荐Titanic或House Prices),做完整的EDA——输出缺失值报告、分布图、相关性矩阵。这个探索过程至少要花1小时,不要跳过。
本章提示词模板
EDA分析报告生成
我有一个机器学习数据集,需要做EDA分析。
数据集描述:[描述数据集,包括字段名、目标变量、行数]
请帮我生成完整的EDA分析代码,包括:
1. 基本信息(形状、数据类型、统计摘要)
2. 缺失值分析(数量、比例、可视化)
3. 目标变量分布(直方图、是否需要对数变换)
4. 特征相关性热力图
5. 针对目标变量的箱线图(数值特征)和柱状图(分类特征)
6. 异常值检测
使用pandas, seaborn, matplotlib,代码要可直接运行。
→ 继续阅读:第03章——scikit-learn实战:从线性回归到随机森林