
第十五章 数据预处理
类型 | 特点 | 示例 |
|---|---|---|
数值型 | 连续/离散数字 | 年龄、价格、点击次数 |
类别型 | 有限个取值 | 性别(男/女)、城市 |
有序型 | 类别但有顺序 | 评分(1~5星)、教育程度 |
文本型 | 自由文本 | 评论、商品描述 |
时间型 | 日期/时间 | 注册时间、交易时间 |
import pandas as pd
df = pd.read_csv('data.csv')
print(df.info()) # 数据类型、非空数
print(df.describe()) # 数值特征统计(均值、std、min、max等)
print(df['category'].value_counts()) # 类别分布# 缺失值比例
print(df.isnull().mean() * 100)
# 异常值:用箱线图快速查看
import matplotlib.pyplot as plt
df.boxplot(column=['income', 'age'])
plt.show()# 数值特征:直方图看是否偏斜
df['income'].hist(bins=30)
plt.title("收入分布")
plt.show()
# 类别特征:条形图
df['city'].value_counts().plot(kind='bar')
plt.show()💡 关键:收入通常右偏 → 需要对数变换 某城市样本极少 → 可能需合并为“其他”
缺失类型 | 特征 | 处理建议 |
|---|---|---|
完全随机缺失(MCAR) | 与任何变量无关 | 可删除或填充 |
随机缺失(MAR) | 与其它变量有关 | 推荐模型填充 |
非随机缺失(MNAR) | 与自身值有关(如高收入不愿填) | 谨慎处理,可能需新增“是否缺失”标志 |
df.dropna() → 仅当缺失比例<5%df.drop(columns=['col']) → 当某列>50%缺失from sklearn.impute import SimpleImputer, KNNImputer
# 均值/中位数/众数填充
num_imputer = SimpleImputer(strategy='median') # 数值用中位数(抗异常值)
cat_imputer = SimpleImputer(strategy='most_frequent') # 类别用众数
# 插值填充(时间序列)
df['price'].interpolate(method='linear', inplace=True)
# KNN模型填充(利用相似样本)
knn_imputer = KNNImputer(n_neighbors=5)
X_filled = knn_imputer.fit_transform(X)⚠️ 注意:填充必须在训练集拟合,再应用于测试集,避免数据泄露!
方法 | 公式/规则 | 适用场景 |
|---|---|---|
Z-score | $|z| > 3$ | 数据近似正态分布 |
IQR | $Q1 - 1.5 \times IQR$ 或 $Q3 + 1.5 \times IQR$ | 任意分布,推荐! |
箱线图 | 可视化IQR结果 | 快速检查 |
# IQR方法
Q1 = df['income'].quantile(0.25)
Q3 = df['income'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常值
df['is_outlier'] = (df['income'] < lower_bound) | (df['income'] > upper_bound)方法 | 代码示例 | 适用场景 |
|---|---|---|
删除 |
| 异常值极少(<1%) |
修正 |
| 传感器噪声 |
隔离 | 新增二值特征 | 业务上异常值有意义(如高净值客户) |
💡 重要原则: 不要盲目删除异常值!先问业务:这是错误数据,还是真实但稀有的情况?
编码方式 | 适用场景 | 优缺点 |
|---|---|---|
标签编码(Label Encoding) | 树模型(决策树、XGBoost) | 简单,但引入虚假序关系 |
独热编码(One-Hot) | 线性模型、神经网络 | 无序关系,但维度爆炸 |
目标编码(Target Encoding) | 高基数类别(如城市ID) | 利用标签信息,但需防过拟合 |
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from category_encoders import TargetEncoder
# 标签编码(用于树模型)
le = LabelEncoder()
df['city_encoded'] = le.fit_transform(df['city'])
# 独热编码(用于线性模型)
ohe = OneHotEncoder(sparse_output=False)
city_ohe = ohe.fit_transform(df[['city']])
# 目标编码(需有标签y)
te = TargetEncoder()
df['city_target'] = te.fit_transform(df[['city']], y)# 等宽分箱
df['age_bin'] = pd.cut(df['age'], bins=5, labels=False)
# 等频分箱(每箱样本数相同)
df['income_qbin'] = pd.qcut(df['income'], q=4, labels=False)✅ 何时离散化?树模型:通常不需要 线性模型:可捕捉非线性关系
方法 | 公式 | 适用算法 |
|---|---|---|
标准化(Z-score) | $x' = \frac{x - \mu}{\sigma}$ | SVM、逻辑回归、PCA、K-Means、神经网络 |
归一化(Min-Max) | $x' = \frac{x - x{\min}}{x{\max} - x_{\min}}$ | 神经网络(尤其图像像素0~1) |
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 标准化(推荐默认)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 归一化
minmax = MinMaxScaler()
X_minmax = minmax.fit_transform(X)需要缩放 | 不需要缩放 |
|---|---|
SVM、逻辑回归、PCA、K-Means、KNN、神经网络 | 决策树、随机森林、XGBoost |
💡 口诀: “基于距离或梯度的算法要缩放,基于规则的不用”
方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
过滤式 | 单变量统计(方差、相关系数) | 快、简单 | 忽略特征交互 |
包裹式 | 用模型性能评价子集(如RFE) | 考虑交互,效果好 | 计算慢 |
嵌入式 | 模型训练中自动选(如Lasso) | 效率与效果平衡 | 依赖特定模型 |
from sklearn.feature_selection import VarianceThreshold, SelectKBest, f_classif, RFE
from sklearn.linear_model import LogisticRegression
# 过滤式:低方差特征删除
vt = VarianceThreshold(threshold=0.1)
X_vt = vt.fit_transform(X)
# 过滤式:基于统计检验
skb = SelectKBest(score_func=f_classif, k=10)
X_skb = skb.fit_transform(X, y)
# 包裹式:递归特征消除
estimator = LogisticRegression()
rfe = RFE(estimator, n_features_to_select=10)
X_rfe = rfe.fit_transform(X, y)
# 嵌入式:L1正则化(Lasso)
lasso = LogisticRegression(penalty='l1', solver='liblinear')
lasso.fit(X_scaled, y)
selected = lasso.coef_ != 0✅ 实践建议: 先用过滤式快速降维,再用嵌入式精细筛选。
模态 | 增强方法 |
|---|---|
图像 | 随机裁剪、颜色抖动、旋转、高斯模糊 |
文本 | 随机掩码、同义词替换、句子打乱 |
表格 | 随机掩码特征、加高斯噪声 |
# 图像增强示例(用于对比学习)
from torchvision import transforms
augment = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.2, 1.0)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(0.4, 0.4, 0.4, 0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]) # 错误做法:分别处理
X_labeled_scaled = scaler.fit_transform(X_labeled)
X_unlabeled_scaled = scaler.fit_transform(X_unlabeled) # ❌ 数据泄露!
# 正确做法:仅用标签数据拟合
scaler = StandardScaler().fit(X_labeled)
X_labeled_scaled = scaler.transform(X_labeled)
X_unlabeled_scaled = scaler.transform(X_unlabeled) # ✅import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# ========================
# 1. 加载数据
# ========================
# 模拟数据:user_id, age, income, city, clicks, purchase (0/1)
np.random.seed(42)
n_labeled = 200 # 少量标签
n_unlabeled = 2000 # 大量无标签
# 生成有标签数据
df_labeled = pd.DataFrame({
'age': np.random.randint(18, 70, n_labeled),
'income': np.random.exponential(50000, n_labeled),
'city': np.random.choice(['北京', '上海', '广州', '深圳', '其他'], n_labeled),
'clicks': np.random.poisson(10, n_labeled),
'purchase': np.random.binomial(1, 0.3, n_labeled)
})
# 生成无标签数据(同分布)
df_unlabeled = pd.DataFrame({
'age': np.random.randint(18, 70, n_unlabeled),
'income': np.random.exponential(50000, n_unlabeled),
'city': np.random.choice(['北京', '上海', '广州', '深圳', '其他'], n_unlabeled),
'clicks': np.random.poisson(10, n_unlabeled),
'purchase': -1 # 无标签标记为-1
})
# 合并(保留标识)
df_all = pd.concat([df_labeled, df_unlabeled], ignore_index=True)
df_all['is_labeled'] = df_all['purchase'] != -1
# ========================
# 2. EDA
# ========================
print("缺失值比例:\n", df_all.isnull().mean())
print("\n收入分布偏度:", df_all['income'].skew()) # 通常>1,右偏
# ========================
# 3. 缺失值处理(本例无缺失,模拟一个)
# ========================
# 假设income有10%缺失
df_all.loc[np.random.choice(df_all.index, size=int(0.1*len(df_all)), replace=False), 'income'] = np.nan
num_imputer = SimpleImputer(strategy='median')
df_all['income'] = num_imputer.fit_transform(df_all[['income']])
# ========================
# 4. 异常值处理
# ========================
Q1 = df_all['income'].quantile(0.25)
Q3 = df_all['income'].quantile(0.75)
IQR = Q3 - Q1
upper_bound = Q3 + 1.5 * IQR
df_all['income'] = np.clip(df_all['income'], None, upper_bound) # 修正上限
# ========================
# 5. 特征编码
# ========================
# 城市:目标编码(因有少量标签)
from category_encoders import TargetEncoder
te = TargetEncoder()
# 仅用有标签数据拟合
labeled_mask = df_all['is_labeled']
df_all['city_te'] = te.fit_transform(
df_all.loc[labeled_mask, ['city']],
df_all.loc[labeled_mask, 'purchase']
)
# 应用到全部数据
df_all['city_te'] = te.transform(df_all[['city']])['city']
# ========================
# 6. 特征缩放
# ========================
features = ['age', 'income', 'clicks', 'city_te']
scaler = StandardScaler()
# 仅用有标签数据拟合(避免数据泄露)
scaler.fit(df_all.loc[labeled_mask, features])
df_all[features] = scaler.transform(df_all[features])
# ========================
# 7. 特征选择(仅用有标签数据)
# ========================
X_labeled = df_all.loc[labeled_mask, features]
y_labeled = df_all.loc[labeled_mask, 'purchase']
selector = SelectKBest(score_func=f_classif, k=3)
X_selected = selector.fit_transform(X_labeled, y_labeled)
selected_features = np.array(features)[selector.get_support()]
print("选中的特征:", selected_features)
# ========================
# 8. 半监督训练:伪标签法
# ========================
# 步骤1: 用少量标签训练初始模型
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_labeled[selected_features], y_labeled)
# 步骤2: 预测无标签数据,取高置信度
X_unlabeled = df_all.loc[~labeled_mask, selected_features]
proba = rf.predict_proba(X_unlabeled)
pseudo_labels = rf.predict(X_unlabeled)
high_conf = proba.max(axis=1) > 0.8
# 步骤3: 构建增强训练集
X_train = pd.concat([
X_labeled[selected_features],
X_unlabeled[high_conf]
])
y_train = pd.concat([
y_labeled,
pd.Series(pseudo_labels[high_conf], index=X_unlabeled[high_conf].index)
])
# 步骤4: 重新训练
rf_final = RandomForestClassifier(n_estimators=100, random_state=42)
rf_final.fit(X_train, y_train)
# ========================
# 9. 评估(假设我们有隐藏测试集)
# ========================
# 模拟测试集
X_test = pd.DataFrame({
'age': np.random.randint(18, 70, 500),
'income': np.random.exponential(50000, 500),
'clicks': np.random.poisson(10, 500)
})
# 目标编码需用原te
X_test['city_te'] = te.transform(pd.DataFrame({'city': np.random.choice(['北京','上海','其他'], 500)}))['city']
X_test_scaled = scaler.transform(X_test[features])
X_test_final = pd.DataFrame(X_test_scaled, columns=features)[selected_features]
y_test = np.random.binomial(1, 0.3, 500) # 简化
acc = accuracy_score(y_test, rf_final.predict(X_test_final))
print(f"\n半监督模型准确率: {acc:.2%}")📊 典型收益:仅用200个标签:准确率 ~70% 加入伪标签后:准确率 ~78% 若有2000全标签:准确率 ~82% → 用10%标注达到95%性能!
步骤 | 关键问题 | 工具 |
|---|---|---|
EDA | 数据长什么样?缺失/异常多吗? |
|
缺失值 | 缺失机制是什么?如何填充? |
|
异常值 | 是噪声还是真实值?如何处理? | IQR, |
编码 | 类别特征怎么转数字? |
|
缩放 | 算法需要标准化吗? |
|
特征选择 | 哪些特征真正有用? |
|
半监督/自监督 | 无标签数据干净吗?增强够吗? | 数据增强、一致性检查 |
💡 黄金法则: “预处理管道必须在训练集上拟合,再应用于验证/测试/无标签数据” —— 防止数据泄露!
📘 延伸建议:
Feature-engine, tsfresh(时序)🌟 建议: 80%的机器学习工作是数据预处理,20%才是建模。 把数据弄干净,模型自然就强!
公众号:咚咚王
《Python编程:从入门到实践》
《利用Python进行数据分析》
《算法导论中文第三版》
《概率论与数理统计(第四版) (盛骤) 》
《程序员的数学》
《线性代数应该这样学第3版》
《微积分和数学分析引论》
《(西瓜书)周志华-机器学习》
《TensorFlow机器学习实战指南》
《Sklearn与TensorFlow机器学习实用指南》
《模式识别(第四版)》
《深度学习 deep learning》伊恩·古德费洛著 花书
《Python深度学习第二版(中文版)【纯文本】 (登封大数据 (Francois Choliet)) (Z-Library)》
《深入浅出神经网络与深度学习+(迈克尔·尼尔森(Michael+Nielsen)》
《自然语言处理综论 第2版》
《Natural-Language-Processing-with-PyTorch》
《计算机视觉-算法与应用(中文版)》
《Learning OpenCV 4》
《AIGC:智能创作时代》杜雨+&+张孜
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。