本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:“机器学习数据挖掘案例和竞赛代码.zip”包含多个真实项目或竞赛中的完整源码,涵盖从数据预处理到模型部署的全流程。内容涉及数据清洗、特征工程、分类回归、聚类分析、模型评估与超参数调优等核心技术,采用Pandas、NumPy、Scikit-learn、XGBoost、LightGBM、Keras/TensorFlow等主流工具实现,并融合Matplotlib、Seaborn等可视化技术。适用于教学、研究及Kaggle类竞赛实战,帮助开发者系统掌握数据挖掘流程与高性能建模技巧。

机器学习项目中的数据预处理与特征工程实战

在真实的业务场景中,我们面对的数据往往不像教科书里那样“干净”。缺失值像幽灵一样潜伏在表格的角落,异常值如同离群的音符打乱模型节奏,而不同量纲的特征则像是用不同语言说话的人——如果不加以调和,整个系统就会陷入混乱。

这就像做一道复杂的菜:食材(数据)再新鲜,若不去皮、不切块、不调味(预处理),直接扔进锅里,结果只能是一锅无法下咽的混沌。所以啊,别急着炫耀你用了多么高级的模型,先问问自己:“我的数据,真的准备好了吗?” 🤔

缺失值?别慌!这里有你的补救指南 💡

让我们从最常见的问题开始: 缺失值 。现实世界中哪有完美的数据集呢?用户可能跳过问卷、传感器偶尔失灵、数据库迁移时出错……这些都会留下 NaN 的痕迹。

一个常见的误区是——看到缺失就删!兄弟,冷静点。删除样本固然简单粗暴有效,但代价可能是宝贵的业务信息流失。特别是当某个字段整体缺失率高达40%以上时,盲目删除会让你损失近一半的训练样本,这不是自废武功是什么?

那怎么办?聪明的做法是“化腐朽为神奇”——把缺失本身变成一种信号!

import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer

# 模拟真实情况下的部分缺失数据
data = pd.DataFrame({
    'age': [25, np.nan, 30, 28, np.nan],
    'income': [50000, 60000, np.nan, 70000, 55000],
    'city': ['北京', '上海', '广州', '深圳', '杭州']
})

# 标记哪些原本是缺失的(这个小动作超重要!)
data['age_missing'] = data['age'].isna().astype(int)
data['income_missing'] = data['income'].isna().astype(int)

# 使用均值插补数值型变量
imputer = SimpleImputer(strategy='mean')
data[['age', 'income']] = imputer.fit_transform(data[['age', 'income']])

看到了吗?我们不仅填补了空缺,还额外增加了两个布尔特征来记录“这里曾经缺失过”。有时候,“不知道”本身就是一种重要的知识。比如在信贷风控中,如果一个人拒绝提供收入信息,这本身就值得警惕 😏。

至于插补方法嘛,也不是一刀切:

  • 均值/中位数 :适合分布相对对称的连续变量;
  • 众数 :分类变量可以用;
  • KNN 插补 :考虑相似样本进行估计,更智能但也更耗资源;
  • 多重插补(Multiple Imputation) :统计学界的“高阶玩法”,通过模拟多个可能的值来反映不确定性,推荐给追求严谨的朋友。

✅ 小贴士:选择策略前一定要看数据分布!右偏严重的收入数据用均值插补?那你等于给每个穷人强行匹配了个富豪邻居……


异常值检测:不是所有偏离都是错误 🎯

接下来是另一个经典难题: 异常值 。它们到底是脏数据?还是隐藏的宝藏?

举个例子,在电商交易日志里,一笔金额为99999元的订单突然出现——是刷单作弊?还是真有个土豪一口气买了十台iPhone?如果你武断地把它当成噪声干掉,可能会误伤真正的高价值客户。

所以我们得学会区分对待。常用的方法有两个好帮手:

方法一:IQR 法则(四分位距)

这是一种基于排序的稳健方法,不受极端值影响太大。

Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]

它的逻辑很简单:大多数正常数据应该落在“箱子”内部,那些远远跑出去的就是可疑分子。

但注意哦,它假设数据大致呈对称分布。对于明显偏态的数据,比如App下载量或微博转发数,这种方法容易误判。

方法二:Z-Score(标准分数)

衡量某个点距离均值有多少个标准差。

from scipy import stats
z_scores = np.abs(stats.zscore(df['price']))
outliers_z = df[z_scores > 3]  # 通常认为超过3σ就算异常

不过它对异常值敏感,因为计算依赖于均值和标准差。如果你的数据本身就有一堆大尾巴,那这个“平均”就没啥参考意义了。

💡 所以我的建议是:
- 对正态性较好的数据 → Z-Score
- 对存在偏态或极端值的数据 → IQR 更稳妥
- 或者干脆两者结合,取交集或并集,灵活应对!

处理方式也多种多样:
- 删除:适用于明确错误(如年龄=-5)
- 盖帽法(capping):将上下边界外的值截断到临界点,保留结构又防爆
- 单独建模:把异常作为一个类别,专门分析其行为模式

记住一句话: 不要轻易定义什么是“异常”,而是去理解它为何存在。


特征缩放:让所有选手站在同一起跑线 ⚖️

当你把身高(cm)、体重(kg)、年收入(万元)丢进同一个模型时,有没有想过它们之间的“话语权”其实严重失衡?

想象一下:SVM 或 KNN 这类基于距离的算法,会把“年收入增加1万”看得比“身高长高1cm”重要得多——仅仅因为数值更大。这不是公平竞争,这是数字霸权!

所以我们需要 标准化 归一化 ,让每个特征都在同一尺度上发言。

Min-Max Scaling:压缩到 [0,1]

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)

优点是直观,适合神经网络输入层这类要求固定范围的场景。缺点也很明显:受异常值影响极大。一个百万年薪就能把所有人压成扁平饼。

Z-Score Normalization:变身标准正态分布

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_normalized = scaler.fit_transform(X)

它减去均值再除以标准差,使得新数据均值为0、方差为1。对异常值更鲁棒,尤其适合线性回归、逻辑回归等假设误差服从正态分布的模型。

⚠️ 关键提醒:无论哪种方法,都必须在训练集上 fit() ,然后应用到测试集!否则你就偷偷看了答案,属于作弊行为 ❌。

# 正确做法 ✅
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)  # 注意!不是 fit_transform!

# 错误示范 ❌
X_test_scaled = scaler.fit_transform(X_test)  # 测试集也能“学习”?不行!

类别变量编码:如何让模型听懂“城市名”这种话?🏙️

现在我们进入特征工程的核心战场: 类别变量处理

性别、城市、职业、商品品类……这些看似简单的字段,却是模型能否真正理解业务的关键。但机器不懂“北京”和“上海”有什么区别,它只认数字。

于是我们需要编码,而且不能随便编。

独热编码(One-Hot Encoding)—— 安全但占地

最基础的方式,就是给每个类别分配一个独立维度。

pd.get_dummies(data['city'], prefix='city')

输出如下:

   city_北京  city_上海  city_广州  city_深圳  city_杭州
0       1       0       0       0       0
1       0       1       0       0       0
...

好处是完全消除顺序假设,谁也不比谁高贵。但坏处也很致命:一旦遇到高基数特征(比如10万个用户ID),你的特征矩阵瞬间膨胀到内存爆炸💥。

解决办法之一是使用稀疏矩阵( sparse=True ),节省空间;另一个则是果断放弃独热,转向更聪明的编码方式。

标签编码(Label Encoding)—— 节省空间但暗藏陷阱

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
data['city_label'] = le.fit_transform(data['city'])

看起来很高效,但问题是:模型会以为“上海=1”比“北京=0”大,进而推导出“上海人更有钱”之类的荒谬结论。非树模型(如LR、SVM)根本扛不住这种误导!

所以在非树模型中,请远离标签编码!除非你能确定这些类别天然有序(比如学历:小学<中学<大学)。

高维救星:靶向编码(Target Encoding)🔥

这才是真正体现工程师智慧的地方。我们不再关心“城市”本身是什么,而是问:“在这个城市里,目标变量平均是多少?”

比如房价预测任务中,“朝阳区”的编码值就是该区域内房屋均价。这样既压缩了维度,又注入了强业务信号。

但是!⚠️ 直接用全局均值会导致严重过拟合,尤其是在小样本城市上(比如只有一个房子挂牌的郊区小镇)。

怎么办?加个“平滑”机制!

def target_encode_smooth(train_df, test_df, col, target, m=5):
    global_mean = train_df[target].mean()
    agg = train_df.groupby(col)[target].agg(['mean', 'count'])
    agg['smoothed'] = (agg['count'] * agg['mean'] + m * global_mean) / (agg['count'] + m)

    # 映射回原数据
    train_encoded = train_df[col].map(agg['smoothed'])
    test_encoded = test_df[col].map(agg['smoothed']).fillna(global_mean)

    return train_encoded, test_encoded

这里的 m 是平滑参数,控制局部均值与全局均值的权重比例。m越大,越偏向整体趋势,抗噪能力更强;m越小,越贴近本地特性,但风险更高。

m 值 倾向 抗噪 过拟合风险
0 完全依赖局部 极高
3 局部主导
10 接近全局

实践中可以通过交叉验证选最优 m,或者直接设置经验值 5~10 就能打遍天下。

🎯 决策流程图如下:

graph TD
    A[输入类别变量] --> B{基数高低?}
    B -- 高基数 --> C[优先考虑Target Encoding或Count Encoding]
    B -- 低基数 --> D{是否有自然顺序?}
    D -- 有序 --> E[使用Ordinal/Label Encoding]
    D -- 无序 --> F[使用One-Hot Encoding]
    C --> G[应用平滑Target Encoding防止过拟合]
    F --> H[可选drop='first'避免共线性]

数值变换:挖掘数据背后的非线性之美 🔍

原始数值常常不服管教,尤其是那些右偏严重的分布(想想工资、房价、点击量)。这时候我们需要数学魔法来驯服它们。

对数变换:压缩长尾,逼近正态

data['log_income'] = np.log(data['income'] + 1)  # +1防止log(0)

一句话效果:把指数级差异变成线性差异。原来相差百倍的富豪和平民,在 log 后可能只差几单位,模型更容易捕捉规律。

配合可视化一看便知:

import seaborn as sns
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, 2, figsize=(12, 4))
sns.histplot(data['income'], kde=True, ax=ax[0])
ax[0].set_title("原始收入分布(严重右偏)")
sns.histplot(data['log_income'], kde=True, ax=ax[1])
ax[1].set_title("对数变换后分布(接近正态)")
plt.show()

你会发现,原本拖着长长尾巴的柱状图,变得越来越像个钟形曲线了。

Box-Cox 变换:自动寻找最佳幂指数 🧪

如果说对数变换是手动挡,那 Box-Cox 就是自动挡。

from scipy.stats import boxcox

# 确保正值
data_positive = data['income'] + 1e-6
bc_data, best_lambda = boxcox(data_positive)

print(f"最优λ参数: {best_lambda:.3f}")

它会根据数据自动搜索最优的幂变换参数 λ,公式如下:

$$
y^{(\lambda)} =
\begin{cases}
\frac{y^\lambda - 1}{\lambda}, & \lambda \neq 0 \
\log(y), & \lambda = 0
\end{cases}
$$

当 λ≈0 时退化为对数变换;λ=1 表示无需变换;λ<0 可能用于左偏数据。

不过要注意:它只接受正值。负值或零需提前处理(如偏移、替换)。

还有一个更通用的版本叫 Yeo-Johnson 变换,连负数都能搞定,感兴趣可以试试 PowerTransformer


分箱技术:把连续变离散,也是一种智慧 🧱

你以为数字化就是越精细越好?错!有时候“模糊化”反而能让模型看得更清楚。

这就是 分箱 (Binning)的艺术。

等宽分箱 vs 等频分箱

# 等宽分箱:每段长度相等
data['age_bin_width'] = pd.cut(data['age'], bins=5, labels=['青年', '中青年', '中年', '中老年', '老年'])

# 等频分箱:每箱人数差不多
data['age_bin_freq'] = pd.qcut(data['age'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

前者简单直观,但容易被极端值带偏;后者保证分布均衡,更适合建模。

举个栗子🌰:如果你按年龄段划分营销策略,用等频分箱能确保每个群体规模相近,便于资源分配。

自适应分箱:KMeans 上场!

还可以用聚类来做密度感知的分箱:

from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters=3)
data['income_cluster'] = kmeans.fit_predict(data[['income']].values.reshape(-1,1))

这种方式能找到数据自然聚集的区域,特别适合未知分布形态的情况。

分箱类型 优点 缺点
等宽 直观易解释 易受极值影响
等频 分布均匀 边界可能不合理
KMeans 自适应密度 计算成本较高

选哪个?看你想要的是 可控性 还是 适应性


多项式特征:激发变量间的化学反应 💥

有些关系不是单个变量决定的,而是组合出来的。比如“教育程度 × 工作年限”可能比单独看任何一个更能预测薪资水平。

这就引出了 多项式特征生成

from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False)
X_poly = poly.fit_transform(X_numeric)

print("原始特征:", X_numeric.shape[1])
print("扩展后特征:", X_poly.shape[1])
print("新增特征名:", poly.get_feature_names_out())

你可以得到:
- 平方项:$x_1^2, x_2^2$
- 交互项:$x_1 x_2, x_1 x_3$

这对线性模型特别有用,相当于在不换模型的前提下提升了表达能力。

⚠️ 但要小心维度爆炸!10个原始特征 → degree=2 下会产生 $10 + 10 + C_{10}^2 = 65$ 个特征;degree=3 直接破百。建议配合 L1 正则化或 RFE 做后续筛选。


特征选择:别让模型在迷宫里瞎转 🧭

有了这么多特征,是不是越多越好?Too young too simple!

太多无关或冗余特征不仅拖慢训练速度,还会增加过拟合风险。我们需要一套科学的方法来“瘦身”。

过滤法(Filter Methods):快刀斩乱麻

基于统计指标独立评分,速度快,适合作为第一轮筛选。

连续 vs 连续:皮尔逊相关系数
corr_matrix = data.corr()['target'].abs().sort_values(ascending=False)
high_corr = corr_matrix[corr_matrix > 0.1]

保留相关性强的候选者。

分类 vs 分类:卡方检验
from sklearn.feature_selection import SelectKBest, chi2

selector = SelectKBest(score_func=chi2, k=10)
X_selected = selector.fit_transform(X_cat, y)

判断两个分类变量是否独立。p-value 小说明有关联。

连续 vs 分类:ANOVA F-test
from sklearn.feature_selection import f_classif

f_stats, p_vals = f_classif(X_cont, y)

看看不同类别下某连续特征的均值是否有显著差异。

整个流程可以用 mermaid 描述:

graph TD
    A[原始特征矩阵] --> B{特征类型判断}
    B -->|连续 vs 连续| C[计算Pearson相关系数]
    B -->|分类 vs 分类| D[执行卡方检验]
    B -->|连续 vs 分类| E[ANOVA F-test]
    C --> F[按得分排序]
    D --> F
    E --> F
    F --> G[选择Top-K特征]
    G --> H[输出精简特征集]

包裹法(Wrapper Methods):以终为始的贪婪搜索

代表人物: 递归特征消除(RFE)

思路很直接:训练模型 → 看谁贡献最小 → 删掉它 → 重复直到满意。

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression

estimator = LogisticRegression()
rfe = RFE(estimator, n_features_to_select=10, step=1)
X_rfe = rfe.fit_transform(X, y)

它考虑了特征之间的协同作用,选出的组合往往更优。缺点是计算开销大,毕竟要反复训练模型。

升级版: RFECV —— 加入交叉验证自动找最佳数量!

from sklearn.feature_selection import RFECV

rfecv = RFECV(estimator=LogisticRegression(), cv=5, scoring='accuracy')
rfecv.fit(X, y)

print(f"最优特征数: {rfecv.n_features_}")

plt.figure(figsize=(10, 6))
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_)
plt.axvline(x=rfecv.n_features_, color='r', linestyle='--')
plt.xlabel("Number of Features Selected")
plt.ylabel("Cross-Validation Score")
plt.title("RFECV Feature Selection")
plt.show()

红线告诉你:再多加特征也没用了,甚至可能下降。这就叫“见好就收”。

嵌入式方法(Embedded Methods):边训练边挑选

最高级的一种,特征选择过程内嵌在模型训练中。

Lasso 回归:L1 正则化的稀疏奇迹
from sklearn.linear_model import Lasso

lasso = Lasso(alpha=0.1)
lasso.fit(X_train, y_train)

print("非零系数数量:", np.sum(lasso.coef_ != 0))

由于 L1 正则项的几何特性,它会让一部分系数精确为 0,实现自动特征剔除。

你可以画个系数路径图看看不同 alpha 下的表现:

alphas = np.logspace(-4, 0, 50)
coefs = []

for a in alphas:
    lasso.set_params(alpha=a)
    lasso.fit(X_train, y_train)
    coefs.append(lasso.coef_)

plt.plot(alphas, coefs)
plt.xscale('log')
plt.xlabel('Alpha')
plt.ylabel('Coefficients')
plt.title('Lasso Coefficient Path')
plt.show()

随着 alpha 增大,越来越多特征被淘汰,直到只剩核心变量。

树模型的重要性评分:天生自带洞察力 🌲

随机森林、XGBoost 这些树模型会在训练过程中记录每个特征带来的信息增益。

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=100)
rf.fit(X_train, y_train)

importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]

# 输出前十
for i in range(10):
    print(f"{i+1}. {feature_names[indices[i]]}: {importances[indices[i]]:.4f}")

还能画图展示:

plt.figure(figsize=(10, 8))
plt.barh(range(10), importances[indices[:10]])
plt.yticks(range(10), [feature_names[i] for i in indices[:10]])
plt.gca().invert_yaxis()
plt.xlabel("Feature Importance")
plt.title("Top 10 Features by Random Forest")
plt.show()

这些重要性可以直接用来过滤低分选手,形成高效 pipeline。


实战整合:端到端特征工程流水线 🏭

最后,让我们把这些技巧串起来,打造一个完整的自动化流程。

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import RandomForestClassifier

# 定义列分组
numeric_features = ['age', 'income', 'price']
categorical_features = ['city', 'gender', 'category']

# 数值管道
numeric_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# 类别管道
categorical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
    ('onehot', OneHotEncoder(drop='first', handle_unknown='ignore'))
])

# 组合预处理器
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ])

# 构建完整 pipeline
pipeline = Pipeline([
    ('preprocess', preprocessor),
    ('classifier', RandomForestClassifier())
])

# 训练
pipeline.fit(X_train, y_train)

这样的设计不仅整洁,还能防止数据泄露,保证测试集完全独立。


结语:好的特征,胜过千行代码 🏆

写到这里,我想说的是:

“在机器学习的世界里,最贵的不是GPU,而是高质量的特征。”

你花一周时间调参,可能不如花一天好好思考“这个变量到底意味着什么”。

特征工程的本质,是对业务的理解、对数据的敬畏、对模型局限的认知。

它不是一个技术活,而是一种思维方式。

所以下次当你面对一堆原始数据时,不妨停下来问自己:

  • “这些缺失,真的是‘缺’吗?”
  • “那个异常,会不会是下一个机会?”
  • “我能从这个变量中学到更多吗?”

答案往往不在模型里,而在你心里 ❤️。

Keep learning, keep building, and keep asking why. 🚀

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:“机器学习数据挖掘案例和竞赛代码.zip”包含多个真实项目或竞赛中的完整源码,涵盖从数据预处理到模型部署的全流程。内容涉及数据清洗、特征工程、分类回归、聚类分析、模型评估与超参数调优等核心技术,采用Pandas、NumPy、Scikit-learn、XGBoost、LightGBM、Keras/TensorFlow等主流工具实现,并融合Matplotlib、Seaborn等可视化技术。适用于教学、研究及Kaggle类竞赛实战,帮助开发者系统掌握数据挖掘流程与高性能建模技巧。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐