【机器学习】数据预处理
Scikit-learn
数据预处理
数据预处理概念
为什么要做数据预处理?
机器学习模型的本质是 “从数据中学习规律”,但模型对 “数据格式” 和 “数据质量” 有严格要求,就像厨师做菜需要新鲜、干净的食材,否则再好的厨艺(算法)也做不出好菜(好模型)。
核心原因可以总结为一句话:
模型无法直接处理 “原始、混乱、不规范” 的数据,必须通过预处理让数据符合模型的 “输入标准”,同时提升数据质量,才能让模型学到真正的规律。
原始数据为什么 “不能直接用”?
-
数据缺失
现实数据中,缺失值非常常见(比如用户没填年龄、传感器故障导致数据丢失)。
大部分模型(如逻辑回归、SVM)无法处理
NaN(缺失值),直接输入会报错;即使部分模型(如决策树)能兼容,缺失值也会干扰模型对规律的判断。例:如果 “年龄” 特征有 30% 的缺失,模型可能无法正确学习 “年龄与购买意愿” 的关系。
-
特征量纲不统一
不同特征的单位和数值范围可能差异极大。
比如 “身高(厘米)” 的取值可能是 150-190,“体重(公斤)” 是 40-100,“年收入(万元)” 是 10-1000。
许多模型(如 KNN、SVM、线性回归)基于 “距离” 或 “权重” 计算,量纲大的特征(如年收入)会 “淹没” 量纲小的特征(如身高),导致模型误以为 “年收入” 更重要,而忽略其他特征的真实影响。
-
存在类别型数据(非数值)
模型只能理解数字,无法直接处理字符串(如 “性别:男 / 女”“颜色:红 / 绿 / 蓝”)。
若不转换,模型会把 “男”“女” 当成无意义的符号,无法学习其与目标的关系。
-
存在冗余或低信息特征
原始数据可能包含大量 “无用特征”(如方差接近 0 的特征,即几乎所有样本取值相同)。
这些特征不仅增加计算量,还可能引入噪声,导致模型过拟合(把噪声当成规律)。
预处理的核心目标:让数据 “适配模型” 并 “提升质量”
预处理不是 “无意义的操作”,而是有明确目标的:
- 兼容性:把数据转换成模型能接受的格式(如填补缺失值、编码类别特征);
- 公平性:消除量纲差异,让每个特征对模型的影响 “公平可比”(如标准化 / 归一化);
- 有效性:剔除噪声和冗余,保留关键信息,让模型更专注于真正的规律(如特征选择)。
假设我们用 “身高(米)” 和 “体重(公斤)” 预测 “是否患高血压”,原始数据如下:
| 身高(米) | 体重(公斤) | 患高血压(0/1) |
|---|---|---|
| 1.75 | 70 | 0 |
| 1.80 | 90 | 1 |
| 1.65 | 85 | 1 |
如果不做预处理,直接用 KNN 模型:
- 身高的数值范围是 1.65-1.80(差异 0.15),体重是 70-90(差异 20);
- KNN 计算距离时,体重的差异会主导结果,导致模型主要依赖体重判断,忽略身高的影响。
用标准化处理后(均值 0,方差 1):
| 身高(标准化) | 体重(标准化) | 患高血压(0/1) |
|---|---|---|
| 0.27 | -1.22 | 0 |
| 1.09 | 1.22 | 1 |
| -1.36 | 0.41 | 1 |
此时两个特征的数值范围相近,KNN 能同时考虑两者的影响,预测更准确。
数据预处理的主要任务
预处理的目标是 “把原始数据变成模型可用的优质数据”,围绕这个目标,有五大核心任务,每个任务都有明确的解决工具和作用:
| 核心任务 | 对应 sklearn 模块 / 类 | 作用(为什么要做) |
|---|---|---|
| 缺失值处理 | sklearn.impute.SimpleImputer |
填补数据中的缺失值(如 NaN),避免模型因无法识别缺失值而报错或预测失真。 |
| 特征缩放 | StandardScaler、MinMaxScaler |
统一不同特征的量纲(如厘米、公斤、万元),避免数值范围大的特征 “淹没” 小特征的影响。 |
| 编码处理 | LabelEncoder、OneHotEncoder |
将字符串类型的类别特征(如 “男 / 女”“红 / 绿 / 蓝”)转换为数字,让模型能够理解和学习。 |
| 特征选择 | VarianceThreshold、SelectKBest |
剔除低信息量或冗余的特征(如几乎所有样本取值相同的特征),减少噪声,提升模型效率。 |
| 流水线组合 | Pipeline、ColumnTransformer |
将上述预处理步骤和模型训练串联成自动化流程,避免手动操作的繁琐和 “数据泄漏” 风险。 |
-
缺失值处理:数据的 “补漏”
现实世界的数据几乎不可能完美无缺 —— 用户可能漏填信息、设备可能故障、数据传输可能丢失。如果直接忽略缺失值(删除样本),可能导致数据量减少、信息丢失(尤其当缺失比例高时);如果保留缺失值,大部分模型(如线性回归、SVM)会直接报错。
SimpleImputer的作用就是用合理的策略(如均值、中位数、众数)填补缺失值,让数据 “完整可用”。 -
特征缩放:特征的 “公平竞争”
不同特征的数值范围可能天差地别:比如 “年龄” 是 0-120,“年收入” 是 0-1000 万,“用户活跃度” 是 0-10。
对于依赖 “距离计算”(如 KNN、SVM)或 “权重更新”(如线性回归、神经网络)的模型来说,数值大的特征会被赋予更高的 “权重”,但这并不代表它更重要(比如 “年收入” 的数值大,不代表它比 “年龄” 对预测的影响更大)。
缩放的目的是让所有特征 “站在同一起跑线”,确保模型能公平地学习每个特征的真实影响。
-
编码处理:类别特征的 “翻译”
模型本质是数学计算,只能理解数字,无法直接处理字符串。比如 “性别” 字段的 “男” 和 “女”,模型无法直接学习它们与 “是否购买” 的关系,必须转换成数字(如 0 和 1)。
但编码不是简单的 “替换”:
- 目标变量(y,如 “是否购买”)可以用
LabelEncoder直接转成 0/1/2; - 特征变量(X,如 “颜色”)若用简单数字编码(红 = 0,绿 = 1,蓝 = 2),模型可能误以为 “红 < 绿 < 蓝”(存在顺序关系),而实际颜色间没有顺序,此时需要
OneHotEncoder转换成 “哑变量”(如红 =[1,0,0],绿 =[0,1,0]),避免引入虚假的顺序关系。
- 目标变量(y,如 “是否购买”)可以用
-
特征选择:数据的 “瘦身”
特征不是越多越好。有些特征可能几乎没有信息量(如 “用户 ID”—— 每个样本取值都不同,或 “性别”——99% 都是女性),这些特征不仅增加模型计算量,还可能干扰模型对关键特征的学习(过拟合)。
特征选择的作用是 “去粗取精”:
VarianceThreshold剔除方差过小的特征(几乎无变化,信息量低);SelectKBest基于统计指标(如相关性、卡方检验)筛选与目标变量最相关的特征。
-
流水线组合:流程的 “自动化”
实际项目中,预处理步骤往往不止一个(比如先填缺失值,再标准化,再编码),如果手动分步处理,不仅繁琐,还容易出现 “数据泄漏”—— 比如用整个数据集的均值填补训练集的缺失值,相当于让模型提前 “偷看” 了测试集的信息,导致评估结果失真。
Pipeline能将所有步骤 “串起来”,确保:- 所有预处理操作只基于训练集计算(如用训练集均值填补训练集和测试集的缺失值);
- 一键完成 “预处理 + 模型训练”,代码更简洁,可复用性更强。
任务优先级:先解决 “致命问题”,再优化 “细节”
处理时注意优先级:
- 先处理缺失值(否则后续步骤无法进行,模型可能直接报错);
- 再处理编码(将非数值特征转成数字,确保模型能识别);
- 然后做特征缩放(针对需要的模型,如 KNN、SVM);
- 最后做特征选择(剔除冗余,简化模型);
- 全程用流水线组合(避免数据泄漏,提高效率)。
缺失值处理
什么是缺失值?
缺失值指数据集中的 “空值” 或 “未记录值”,在 Python 中通常用 np.nan(Not a Number)表示。例如:
- 问卷调查中用户未填写的 “年龄”;
- 传感器故障导致某一时刻的 “温度” 数据缺失;
- 数据库同步错误导致的 “交易金额” 为空。
为什么必须处理缺失值?
- 模型兼容性:大多数机器学习模型(如逻辑回归、SVM、KNN)无法直接处理
np.nan,输入含缺失值的数据会直接报错; - 信息完整性:直接删除含缺失值的样本可能导致数据量大幅减少,丢失重要信息(尤其当缺失比例较高时)。
SimpleImputer 的核心功能
SimpleImputer 提供了多种填补缺失值的策略,能自动识别并填补数据中的 np.nan,默认以 “列” 为单位处理(因为不同特征的缺失值需要用各自的统计量填补)。
用 SimpleImputer 填补缺失值
-
导入工具和数据
import numpy as np from sklearn.impute import SimpleImputer # 导入缺失值处理工具 # 定义含缺失值的样本(3个样本,3个特征) X = np.array([ [1, 2, np.nan], # 第3个特征缺失 [3, np.nan, 1], # 第2个特征缺失 [5, 6, 2] # 无缺失 ]) print("原始数据:") print(X)原始数据输出:
原始数据: [[ 1. 2. nan] [ 3. nan 1.] [ 5. 6. 2.]] -
选择填补策略并初始化
SimpleImputerSimpleImputer的核心参数是strategy(填补策略),常用选项:strategy='mean':用该列的平均值填补(默认,适用于数值型特征);strategy='median':用该列的中位数填补(适用于含 outliers 离群值的数值特征);strategy='most_frequent':用该列的众数填补(适用于类别型特征,如 “性别”);strategy='constant':用指定的常数填补(需配合fill_value参数,如fill_value=0)。
这里先用 “平均值” 填补:
# 初始化填补器,策略为“平均值” imputer = SimpleImputer(strategy='mean') -
拟合(计算填补值)并转换(填补缺失值)
SimpleImputer遵循 sklearn 的统一接口:fit(X):根据非缺失数据计算每列的填补值(如平均值);transform(X):用计算好的填补值替换X中的缺失值;fit_transform(X):等价于先fit再transform(更高效)。
# 拟合并转换数据(一步完成) X_filled = imputer.fit_transform(X) print("填补后的数据:") print(X_filled)填补后输出:
填补后的数据: [[1. 2. 1.5] [3. 4. 1. ] [5. 6. 2. ]] -
理解填补逻辑
填补是以 “列” 为单位的:
- 第 1 列:
[1, 3, 5]→ 无缺失值 → 直接保留; - 第 2 列:
[2, np.nan, 6]→ 非缺失值为 2 和 6 → 平均值 = (2+6)/2 = 4 → 缺失值用 4 填补; - 第 3 列:
[np.nan, 1, 2]→ 非缺失值为 1 和 2 → 平均值 = (1+2)/2 = 1.5 → 缺失值用 1.5 填补。
不同策略的对比实践
换用 “中位数” 和 “常数” 策略,看看结果差异:
-
用中位数填补(适合含离群值的场景):
imputer_median = SimpleImputer(strategy='median') X_filled_median = imputer_median.fit_transform(X) print("中位数填补:") print(X_filled_median)输出(第 2 列中位数是 (2+6)/2=4,第 3 列中位数是 (1+2)/2=1.5,结果同上,因为此例无离群值):
[[1. 2. 1.5] [3. 4. 1. ] [5. 6. 2. ]] -
用常数 0 填补:
imputer_constant = SimpleImputer(strategy='constant', fill_value=0) X_filled_constant = imputer_constant.fit_transform(X) print("常数0填补:") print(X_filled_constant)输出:
[[1. 2. 0. ] [3. 0. 1. ] [5. 6. 2. ]]
- 第 1 列:
注意事项
-
只能处理数值型缺失值:
SimpleImputer默认处理np.nan,如果缺失值用其他符号表示(如 “?”“未知”),需先手动替换为np.nan; -
区分训练集和测试集:必须用训练集的统计量填补测试集的缺失值(避免数据泄漏)。例如:
# 正确做法:用训练集拟合,分别转换训练集和测试集 imputer.fit(X_train) # 基于X_train计算填补值 X_train_filled = imputer.transform(X_train) X_test_filled = imputer.transform(X_test) # 用X_train的统计量填补X_test -
类别型特征建议用众数:类别特征(如 “职业”“学历”)的缺失值,用
strategy='most_frequent'(众数)更合理(不能用均值 / 中位数)。
特征缩放
为什么需要特征缩放?
现实世界的特征往往具有不同的 “量纲”(单位)和 “数值范围”:
- 比如 “年龄” 可能是 0-120(单位:岁),“收入” 可能是 0-1000 万(单位:元),“用户活跃度” 可能是 0-10(无单位);
- 这些差异会导致数值大的特征(如收入)在模型计算中 “权重过高”,掩盖数值小的特征(如年龄)的真实影响,即使后者对预测更重要。
特征缩放的作用是:将不同特征的数值映射到相似的范围,消除量纲差异,让模型能公平地学习每个特征的影响。
两种核心缩放方法及适用场景
sklearn 提供了多种缩放工具,最常用的是以下两种:
| 方法 | 核心逻辑(数学公式) | 目标范围 | 适用场景 |
|---|---|---|---|
| 标准化(StandardScaler) | X s c a l e d = X − μ σ X_{scaled}={{X−μ}\over σ} Xscaled=σX−μ,其中 μ μ μ 是均值, σ σ σ 是标准差 | 均值 = 0,方差 = 1 | 大多数机器学习模型(如 SVM、逻辑回归、KNN、KMeans),尤其对异常值较稳健 |
| 归一化(MinMaxScaler) | X s c a l e d = X − X m i n X m a x − X m i n X_{scaled}={X−X_{min}\over X_{max}−X_{min}} Xscaled=Xmax−XminX−Xmin | 默认 [0, 1] | 神经网络(输入通常要求在 0-1 或 - 1-1 之间)、对特征范围有严格要求的场景 |
标准化(StandardScaler)—— 让特征 “分布标准化”
标准化的核心是将特征转换为 “均值为 0,标准差为 1” 的分布,保留原始数据的分布形状(如正态分布),但消除量纲影响。
-
准备数据
import numpy as np from sklearn.preprocessing import StandardScaler # 定义原始数据(2个特征,3个样本) # 特征1:[1, 2, 4](范围较小) # 特征2:[2, 3, 5](范围与特征1类似,但整体偏移) X = np.array([[1, 2], [2, 3], [4, 5]]) print("原始数据:") print(X)原始数据输出:
原始数据: [[1 2] [2 3] [4 5]] -
初始化标准化器并转换
# 初始化标准化器 scaler = StandardScaler() # 拟合并转换数据(计算每个特征的均值和标准差,再应用公式) X_scaled = scaler.fit_transform(X) print("标准化后的数据:") print(X_scaled)标准化后输出:
标准化后的数据: [[-1.22474487 -1.22474487] [-0. -0. ] [ 1.22474487 1.22474487]] -
拆解标准化逻辑
标准化以 “列” 为单位计算,我们逐特征验证:
- 特征 1(第一列):
[1, 2, 4]- 均值 μ = ( 1 + 2 + 4 ) / 3 = 7 / 3 ≈ 2.333 μ = (1+2+4)/3 = 7/3 ≈ 2.333 μ=(1+2+4)/3=7/3≈2.333
- 标准差 σ = ( 1 − 2.333 ) 2 + ( 2 − 2.333 ) 2 + ( 4 − 2.333 ) 2 3 − 1 ≈ 1.527 σ = \sqrt{{(1−2.333)^2+(2−2.333)^2+(4−2.333)^2}\over 3−1} ≈ 1.527 σ=3−1(1−2.333)2+(2−2.333)2+(4−2.333)2≈1.527
- 标准化后:
- ( 1 − 2.333 ) / 1.527 ≈ − 0.877 ≈ − 1.2247 (1 - 2.333)/1.527 ≈ -0.877 ≈ -1.2247 (1−2.333)/1.527≈−0.877≈−1.2247(注:实际计算用样本标准差,分母为 n − 1 n-1 n−1)
- ( 2 − 2.333 ) / 1.527 ≈ − 0.218 ≈ 0 (2 - 2.333)/1.527 ≈ -0.218 ≈ 0 (2−2.333)/1.527≈−0.218≈0
- ( 4 − 2.333 ) / 1.527 ≈ 1.091 ≈ 1.2247 (4 - 2.333)/1.527 ≈ 1.091 ≈ 1.2247 (4−2.333)/1.527≈1.091≈1.2247
- 特征 2(第二列):
[2, 3, 5]- 计算逻辑同上,结果与特征 1 完全一致(因为两列数据的相对分布相同)。
- 特征 1(第一列):
归一化(MinMaxScaler)—— 让特征 “范围归一化”
归一化的核心是将特征压缩到指定范围(默认 [0, 1]),直接改变数据的分布范围,但可能放大异常值的影响(如果存在离群值)。
-
用同样的数据初始化归一化器
from sklearn.preprocessing import MinMaxScaler # 初始化归一化器(默认范围 [0, 1],可通过 feature_range 参数修改,如 feature_range=(-1, 1)) scaler = MinMaxScaler() # 拟合并转换数据 X_scaled = scaler.fit_transform(X) print("归一化后的数据:") print(X_scaled)归一化后输出:
归一化后的数据: [[0. 0. ] [0.33333333 0.33333333] [1. 1. ]] -
拆解归一化逻辑
同样以 “列” 为单位:
- 特征 1(第一列):
[1, 2, 4]- 最小值 X m i n = 1 X_{min} = 1 Xmin=1,最大值 X m a x = 4 X_{max} = 4 Xmax=4
- 归一化后:
- ( 1 − 1 ) / ( 4 − 1 ) = 0 / 3 = 0 (1 - 1)/(4 - 1) = 0/3 = 0 (1−1)/(4−1)=0/3=0
- ( 2 − 1 ) / ( 4 − 1 ) = 1 / 3 ≈ 0.333 (2 - 1)/(4 - 1) = 1/3 ≈ 0.333 (2−1)/(4−1)=1/3≈0.333
- ( 4 − 1 ) / ( 4 − 1 ) = 3 / 3 = 1 (4 - 1)/(4 - 1) = 3/3 = 1 (4−1)/(4−1)=3/3=1
- 特征 2(第二列):
[2, 3, 5]- 最小值 = 2,最大值 = 5
- 归一化后:
- ( 2 − 2 ) / ( 5 − 2 ) = 0 → 0 (2-2)/(5-2)=0 \to 0 (2−2)/(5−2)=0→0
- ( 3 − 2 ) / ( 5 − 2 ) = 1 / 3 ≈ 0.333 (3-2)/(5-2)=1/3≈0.333 (3−2)/(5−2)=1/3≈0.333
- ( 5 − 2 ) / ( 5 − 2 ) = 3 / 3 = 1 (5-2)/(5-2)=3/3=1 (5−2)/(5−2)=3/3=1
- 特征 1(第一列):
两种方法的核心区别
-
对异常值的敏感度:
-
标准化:受异常值影响较小(因为用均值和标准差,而标准差对异常值的敏感度低于极差);
-
归一化:受异常值影响较大(因为直接依赖最大值和最小值,若存在极端值,大部分数据会被压缩到很小的范围)。
例:若特征 1 有一个异常值 100,则归一化后其他值会接近 0,而标准化影响较小。
-
-
适用模型:
- 标准化:优先用于基于距离的模型(KNN、SVM、KMeans)和线性模型(逻辑回归、线性回归);
- 归一化:优先用于神经网络(输入层通常要求 0-1 范围)、决策树之外的模型(决策树不依赖特征范围,可跳过缩放)。
-
保留信息:
- 标准化保留特征的 “分布形状”(如正态分布的特征标准化后仍是正态分布);
- 归一化改变分布形状,但强制特征范围统一,适合对范围有严格要求的场景。
注意事项
-
缩放只针对特征(X),不针对标签(y):标签的缩放会改变预测目标的物理意义(如预测房价时,y 的单位是 “万元”,缩放后失去意义)。
-
用训练集的统计量缩放测试集:与缺失值处理同理,必须避免数据泄漏:
scaler.fit(X_train) # 用训练集计算均值/标准差/最大最小值 X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test) # 用训练集的统计量缩放测试集 -
不是所有模型都需要缩放:
- 需要缩放:KNN、SVM、逻辑回归、线性回归、神经网络、KMeans;
- 不需要缩放:决策树、随机森林、梯度提升树(基于特征分裂,与数值范围无关)。
编码处理(类别型特征)
什么是类别型特征?
类别型特征指取值为 “离散类别” 的特征,通常是字符串或有限的整数,可分为:
- 名义型:类别间无顺序关系(如 “颜色:红 / 绿 / 蓝”“性别:男 / 女”);
- 有序型:类别间有明确顺序(如 “学历:高中 / 本科 / 硕士 / 博士”“评分:1 星 / 2 星 / 3 星”)。
模型无法直接理解这些文字,必须通过编码转换为数字,但不同类型的类别特征需要不同的编码方式。
两种核心编码工具及适用场景
| 工具 | 功能 | 适用对象 | 优缺点 |
|---|---|---|---|
LabelEncoder |
将类别映射为 0,1,2,… 整数 | 目标变量(y)或有序特征 | 优点:简单直观,压缩维度;缺点:会引入虚假顺序关系(不适合名义特征) |
OneHotEncoder |
将每个类别转换为 “哑变量”(0/1 向量) | 特征变量(X)中的名义特征 | 优点:无顺序偏差,适合名义特征;缺点:高基数特征(类别多)会导致维度爆炸 |
标签编码(LabelEncoder)—— 适合目标变量和有序特征
LabelEncoder 的核心是 “一对一映射”:将每个唯一类别对应一个整数(如 “男→1”“女→0”),适合两类场景:
- 目标变量(y):无论是否有序,都可以用(因为模型关注的是 “预测值与真实值的差异”,而非类别间的数学关系);
- 有序特征(如 “学历”):编码后的整数能保留顺序信息(如 1<2<3 对应高中 < 本科 < 硕士)。
-
处理目标变量(y)
from sklearn.preprocessing import LabelEncoder # 原始目标变量(字符串类型) y = ['male', 'female', 'female', 'male'] print("原始目标变量:", y) # 初始化标签编码器 encoder = LabelEncoder() # 拟合并转换(学习类别→整数的映射,再转换) y_encoded = encoder.fit_transform(y) print("编码后目标变量:", y_encoded) # 输出:[1 0 0 1] print("类别映射关系:", encoder.classes_) # 输出:['female' 'male'](0对应female,1对应male)逻辑解析:
encoder.fit(y)会自动识别唯一类别['female', 'male'],并按字母顺序映射为 0 和 1;transform(y)将每个元素替换为对应的整数,结果为[1, 0, 0, 1]。
-
处理有序特征(如 “评分”)
# 有序特征(有明确顺序) X_ordered = ['low', 'medium', 'high', 'medium', 'low'] print("原始有序特征:", X_ordered) # 编码 encoder_ordered = LabelEncoder() X_ordered_encoded = encoder_ordered.fit_transform(X_ordered) print("编码后有序特征:", X_ordered_encoded) # 输出:[1 2 0 2 1](注意:映射顺序按字母,low→1,medium→2,high→0) print("类别映射:", encoder_ordered.classes_) # 输出:['high' 'low' 'medium']注意:
LabelEncoder按 “字母 / 数字顺序” 映射,若想保留自定义顺序(如 low=0, medium=1, high=2),需手动映射(LabelEncoder不支持自定义顺序)。
独热编码(OneHotEncoder)—— 适合名义特征
名义特征(如 “颜色”“职业”)的类别间无顺序,若用 LabelEncoder 编码(如红 = 0,绿 = 1,蓝 = 2),模型会误以为 “红 < 绿 < 蓝”(存在数值关系),这是错误的。
OneHotEncoder 的解决思路是:为每个类别创建一个 “哑变量”(0/1),用向量表示类别,如:
- 红 → [1, 0, 0]
- 绿 → [0, 1, 0]
- 蓝 → [0, 0, 1]
-
处理名义特征(如 “颜色”)
from sklearn.preprocessing import OneHotEncoder import numpy as np # 原始名义特征(2D数组,因为sklearn要求特征必须是二维的) X_nominal = np.array([['red'], ['green'], ['blue'], ['red']]) print("原始名义特征:") print(X_nominal)原始特征输出:
原始名义特征: [['red'] ['green'] ['blue'] ['red']] -
初始化独热编码器并转换
# 初始化独热编码器(sparse_output=False 表示输出稠密矩阵,默认是稀疏矩阵) encoder = OneHotEncoder(sparse_output=False) # 拟合并转换 X_encoded = encoder.fit_transform(X_nominal) print("独热编码后特征:") print(X_encoded) print("类别映射关系:", encoder.categories_) # 输出:[['blue' 'green' 'red']](每个类别对应一列)编码后输出:
独热编码后特征: [[0. 0. 1.] # red 对应第三列(索引2),故第三列=1 [0. 1. 0.] # green 对应第二列(索引1),故第二列=1 [1. 0. 0.] # blue 对应第一列(索引0),故第一列=1 [0. 0. 1.]] # red 再次出现 -
理解独热编码的维度变化
- 原始特征:1 个特征,3 个类别 → 编码后:3 个特征(每个类别对应 1 列);
- 维度变化公式:若某特征有
n个唯一类别,独热编码后会生成n个新特征(“维度扩展”)。
两种编码的核心区别
-
适用对象严格区分:
LabelEncoder→ 只能用于目标变量(y)或有序特征(X),绝对不能用于名义特征(X)(会引入虚假顺序);OneHotEncoder→ 只能用于特征变量(X)中的名义特征,不能用于目标变量(y)(多分类时 y 的独热编码需用OneHotEncoder的姐妹工具LabelBinarizer,但 sklearn 模型通常支持原始整数标签)。
-
独热编码的 “维度爆炸” 问题:
若特征类别过多(如 “城市” 有 100 个类别),独热编码会生成 100 个新特征,导致维度骤增(“维度灾难”),此时可改用:
- 目标编码(Target Encoding,用类别对应的目标均值编码);
- 频数编码(用类别出现的频率编码)。
-
输入格式要求:
OneHotEncoder要求输入是二维数组(即使只有 1 个特征),而LabelEncoder可以处理一维数组(适合 y)。
特征选择
为什么需要特征选择?
不是特征越多,模型效果越好。原始数据中可能存在:
- 低信息特征:如 “用户 ID”(每个样本取值唯一,无规律)、“性别”(99% 都是女性,几乎无区分度);
- 冗余特征:如 “身高(厘米)” 和 “身高(米)”(高度相关,信息重复);
- 噪声特征:与目标变量无关的特征(如 “姓名” 与 “是否购买商品” 无关)。
这些特征会:
- 增加模型计算量(拖慢训练速度);
- 干扰模型对关键特征的学习(导致过拟合);
- 降低模型可解释性(特征太多难以分析)。
特征选择的作用是:保留 “有用” 特征,剔除 “无用” 特征,在减少计算成本的同时提升模型性能。
VarianceThreshold:基于方差的过滤法
VarianceThreshold 是最简单的特征选择工具,核心逻辑是:方差越小的特征,信息量越少(取值几乎不变),可以直接剔除。
- 原理:计算每个特征的方差,保留方差大于指定阈值的特征;
- 适用场景:快速剔除 “常量特征”(方差 = 0,所有样本取值相同)或 “近常量特征”(方差接近 0)。
用 VarianceThreshold 过滤低方差特征
-
准备数据(含低方差特征)
import numpy as np from sklearn.feature_selection import VarianceThreshold # 原始特征数据(4个特征,3个样本) # 特征0:[0,0,0] → 方差=0(常量特征,无信息) # 特征1:[2,1,1] → 方差较小(近常量) # 特征2:[0,4,1] → 方差较大(有信息) # 特征3:[3,3,3] → 方差=0(常量特征) X = np.array([ [0, 2, 0, 3], [0, 1, 4, 3], [0, 1, 1, 3] ]) print("原始特征数据:") print(X)原始数据输出:
原始特征数据: [[0 2 0 3] [0 1 4 3] [0 1 1 3]] -
计算各特征的方差(手动验证)
方差公式: V a r ( X ) = 1 n − 1 ∑ i = 1 n ( X i − X ˉ ) 2 Var(X) = \frac{1}{n-1} \sum_{i=1}^{n} (X_i - \bar{X})^2 Var(X)=n−11i=1∑n(Xi−Xˉ)2(样本方差,分母为 n − 1 n-1 n−1)
- 特征 0: [ 0 , 0 , 0 ] → 均值 = 0 → 方差 = 0 [0,0,0] → 均值 = 0 → 方差 = 0 [0,0,0]→均值=0→方差=0;
- 特征 1: [ 2 , 1 , 1 ] → 均值 = 4 / 3 ≈ 1.333 → 方差 = ( 2 − 1.333 ) 2 + ( 1 − 1.333 ) 2 + ( 1 − 1.333 ) 2 2 ≈ 0.333 [2,1,1] → 均值 = 4/3 ≈1.333 → 方差 = {(2−1.333)^2+(1−1.333)^2+(1−1.333)^2 \over 2} ≈ 0.333 [2,1,1]→均值=4/3≈1.333→方差=2(2−1.333)2+(1−1.333)2+(1−1.333)2≈0.333;
- 特征 2: [ 0 , 4 , 1 ] → 均值 = 5 / 3 ≈ 1.666 → 方差 ≈ ( 0 − 1.666 ) 2 + ( 4 − 1.666 ) 2 + ( 1 − 1.666 ) 2 2 ≈ 4.333 [0,4,1] → 均值 = 5/3≈1.666 → 方差≈{(0−1.666)^2+(4−1.666)^2+(1−1.666)^2 \over 2}≈4.333 [0,4,1]→均值=5/3≈1.666→方差≈2(0−1.666)2+(4−1.666)2+(1−1.666)2≈4.333;
- 特征 3: [ 3 , 3 , 3 ] → 均值 = 3 → 方差 = 0 [3,3,3] → 均值 = 3 → 方差 = 0 [3,3,3]→均值=3→方差=0。
-
用
VarianceThreshold筛选特征# 初始化选择器,设置方差阈值(保留方差>0.2的特征) sel = VarianceThreshold(threshold=0.2) # 拟合并转换(计算方差,保留符合条件的特征) X_selected = sel.fit_transform(X) print("筛选后的特征数据:") print(X_selected) print("被保留的特征索引:", sel.get_support(indices=True)) # 输出保留的特征在原始数据中的索引输出结果:
筛选后的特征数据: [[2 0] [1 4] [1 1]] 被保留的特征索引: [1 2] -
结果解析
- 阈值
threshold=0.2表示:保留方差 > 0.2 的特征; - 特征 0(方差 = 0)、特征 3(方差 = 0):方差≤0.2 → 被剔除;
- 特征 1(方差≈0.333)、特征 2(方差≈4.333):方差 > 0.2 → 被保留;
- 筛选后的数据只保留了特征 1 和特征 2,维度从 4 降为 2(“瘦身” 成功)。
- 阈值
其他常用特征选择工具
VarianceThreshold 是 “无监督选择”(不依赖目标变量 y),适合快速过滤低信息特征。实际中还有 “有监督选择”(结合目标变量 y 的相关性):
-
SelectKBest:-
原理:根据指定的统计指标(如相关性、卡方检验),选择得分最高的前 K 个特征;
-
示例:选择与目标变量相关性最高的 2 个特征:
from sklearn.feature_selection import SelectKBest, f_regression # f_regression用于回归任务 # 假设y是目标变量,选择前2个最佳特征 selector = SelectKBest(score_func=f_regression, k=2) X_selected = selector.fit_transform(X, y)
-
-
模型自带的特征重要性:
- 如随机森林的
feature_importances_、线性模型的系数绝对值,可根据重要性排序筛选特征。
- 如随机森林的
注意事项
- 阈值选择需谨慎:
VarianceThreshold的默认阈值是 0(只剔除方差 = 0 的常量特征);- 若数据已标准化(方差 = 1),阈值设置需结合业务(如保留方差 > 0.5 的特征)。
- 先预处理,后选择特征:
- 特征选择应在 “缺失值处理、编码、缩放” 之后(如方差受量纲影响,需先标准化再计算);
- 例:若特征单位是 “万元”,缩放为 “元” 后方差会变大,可能导致选择结果不同。
- 避免过度筛选:
- 特征太少可能导致模型 “欠拟合”(丢失关键信息),需通过交叉验证判断最佳特征数量。
Pipeline 流水线
为什么需要 Pipeline?
手动处理预处理步骤(如先填补缺失值、再标准化、最后训练模型)会遇到两个关键问题:
- 步骤繁琐:实际项目中预处理可能有 5-10 步,手动调用
fit_transform容易出错,且代码冗余; - 数据泄漏(Data Leakage):最致命的问题!如果用整个数据集的统计量(如均值、标准差)预处理训练集和测试集,相当于让模型提前 “偷看” 了测试集信息,导致评估结果失真(看似效果好,实际泛化能力差)。
Pipeline 的作用是:将所有预处理步骤和模型 “串联” 成一个整体,确保所有操作只基于训练集,自动完成 “拟合 - 转换 - 预测” 全流程,从根本上避免数据泄漏。
Pipeline 的核心逻辑
Pipeline 本质是一个 “步骤容器”,按顺序存储预处理工具和模型,调用时:
- 对训练集:按顺序执行
fit_transform(每个步骤用训练集拟合,再转换训练集); - 对测试集:按顺序执行
transform(每个步骤用训练集的统计量转换测试集,不重新拟合); - 最终用最后一步的模型进行预测。
用 Pipeline 串联 “缺失值处理→标准化→逻辑回归”
以一个含缺失值的数据集为例,演示 Pipeline 的完整用法。
-
准备数据和工具
import numpy as np from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer # 缺失值处理 from sklearn.preprocessing import StandardScaler # 标准化 from sklearn.linear_model import LogisticRegression # 模型 from sklearn.model_selection import train_test_split # 划分数据集 # 生成含缺失值的样本(特征X,标签y) X = np.array([ [1, np.nan, 3], [np.nan, 2, 4], [3, 4, np.nan], [5, 6, 7] ]) y = np.array([0, 0, 1, 1]) # 二分类标签 # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42) -
定义 Pipeline 步骤
Pipeline 的步骤需用列表表示,每个元素是
(步骤名称, 工具对象)的元组,顺序必须是 “预处理步骤在前,模型在后”。# 定义流水线:缺失值填补 → 标准化 → 逻辑回归 pipe = Pipeline([ ('imputer', SimpleImputer(strategy='mean')), # 步骤1:用均值填补缺失值 ('scaler', StandardScaler()), # 步骤2:标准化 ('model', LogisticRegression()) # 步骤3:逻辑回归模型 ]) -
用 Pipeline 拟合和评估
Pipeline 遵循 sklearn 的统一接口,直接调用
fit和score即可:# 拟合流水线(自动完成:训练集拟合→转换→模型训练) pipe.fit(X_train, y_train) # 评估模型(自动完成:测试集转换→模型预测→计算分数) accuracy = pipe.score(X_test, y_test) print(f"模型准确率:{accuracy:.2f}")输出:
模型准确率:1.0
Pipeline 如何避免数据泄漏?
我手动拆解 Pipeline 对训练集和测试集的处理逻辑:
- 对训练集(X_train):
imputer.fit_transform(X_train):用训练集的均值填补训练集缺失值;scaler.fit_transform(填补后的数据):用训练集的均值和标准差标准化;model.fit(标准化后的数据, y_train):用处理后的训练集训练模型。
- 对测试集(X_test):
imputer.transform(X_test):用训练集的均值填补测试集缺失值(而非测试集自身的均值);scaler.transform(填补后的数据):用训练集的均值和标准差标准化测试集;model.predict(标准化后的数据):用训练好的模型预测。
这种 “训练集拟合,测试集仅转换” 的逻辑,从根本上避免了测试集信息泄漏到训练过程中。
用 ColumnTransformer 处理不同类型的特征
实际数据中,不同特征可能需要不同的预处理(如数值特征用标准化,类别特征用独热编码),ColumnTransformer 可与 Pipeline 配合,实现 “按列定制预处理”。
示例:混合特征的处理
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
# 混合特征数据(第一列是数值特征,第二列是类别特征)
X_mixed = np.array([
[1, 'red'],
[3, 'blue'],
[2, 'red'],
[5, 'green']
])
# 定义不同列的预处理方式
# 数值特征(第0列):填补缺失值→标准化
# 类别特征(第1列):独热编码
preprocessor = ColumnTransformer(
transformers=[
('num', Pipeline([ # 数值特征的流水线
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler())
]), [0]), # 应用于第0列
('cat', OneHotEncoder(), [1]) # 类别特征:独热编码,应用于第1列
])
# 最终流水线:预处理→模型
final_pipe = Pipeline([
('preprocessor', preprocessor),
('model', LogisticRegression())
])
# 拟合(自动按列处理不同特征)
final_pipe.fit(X_mixed, y)
Pipeline 的核心优势
- 避免数据泄漏:严格区分训练集和测试集的处理逻辑;
- 代码简洁:用一个对象替代多个预处理步骤和模型,减少代码量;
- 支持交叉验证和网格搜索:可直接传入
cross_val_score或GridSearchCV,实现自动化调参; - 可复用性强:训练好的 Pipeline 可直接保存,部署时无需重复编写预处理逻辑。
注意事项
-
步骤顺序不可乱:必须按 “数据加载→预处理→模型” 的顺序排列,例如 “标准化” 不能放在 “缺失值处理” 之前(因为缺失值会导致标准化失败);
-
最后一步必须是模型:Pipeline 的
fit方法会调用最后一步的fit,而模型是唯一需要y进行训练的步骤; -
保存和加载 Pipeline:用
joblib保存整个 Pipeline(包含预处理和模型),加载后可直接用于预测:import joblib joblib.dump(pipe, 'my_pipeline.pkl') # 保存 loaded_pipe = joblib.load('my_pipeline.pkl') # 加载 loaded_pipe.predict(X_new) # 直接预测新数据
总结
核心预处理工具总结表
为了方便记忆,我们把第二模块的核心工具、用途和关键参数整理成表格,一目了然:
| 预处理任务 | 对应工具类 | 核心用途 | 关键参数 / 注意事项 |
|---|---|---|---|
| 缺失值处理 | SimpleImputer |
填补 np.nan 缺失值 |
strategy:‘mean’(均值)、‘median’(中位数)、‘most_frequent’(众数)、‘constant’(常数);需用训练集统计量填补测试集 |
| 标准化 | StandardScaler |
将特征转换为均值 = 0、方差 = 1 | 适合大多数模型(SVM、KNN、线性模型);对异常值较稳健 |
| 归一化 | MinMaxScaler |
将特征缩放到 [0,1] 或指定范围 | feature_range:指定缩放范围(如 (-1,1));适合神经网络,对异常值敏感 |
| 标签编码 | LabelEncoder |
将类别转换为 0,1,2… 整数 | 仅用于目标变量(y)或有序特征;不适合名义特征(会引入虚假顺序) |
| 独热编码 | OneHotEncoder |
将名义特征转换为哑变量(0/1 向量) | sparse_output=False:输出稠密矩阵;高基数特征慎用(避免维度爆炸) |
| 低方差特征过滤 | VarianceThreshold |
剔除方差小于阈值的特征 | threshold:方差阈值(默认 0,剔除常量特征);需先处理量纲(如标准化) |
| 流水线组合 | Pipeline |
串联预处理步骤和模型,避免数据泄漏 | 步骤顺序:预处理在前,模型在后;支持交叉验证和网格搜索 |
| 多类型特征处理 | ColumnTransformer |
对不同列应用不同预处理 | 与 Pipeline 配合使用,按列定制处理逻辑(如数值列标准化,类别列独热编码) |
实战小练习:泰坦尼克号数据预处理
泰坦尼克号数据集是经典的分类任务(预测乘客是否幸存),包含数值特征(如年龄、票价)、类别特征(如性别、船舱等级),且存在缺失值,非常适合练习完整预处理流程。
任务目标:
对泰坦尼克号数据中的指定列做预处理,并用 Pipeline 串联步骤,训练逻辑回归模型预测 “Survived”。
具体步骤:
- 加载数据(
titanic.csv,可从 Kaggle 下载); - 选择特征列:
Age(年龄)、Sex(性别)、Fare(票价),目标列:Survived; - 预处理:
Age:用平均值填补缺失值;Sex:用独热编码转换;Fare:用标准化处理;
- 用
ColumnTransformer按列定制预处理,再用Pipeline串联预处理和逻辑回归模型; - 划分训练集和测试集,训练模型并评估准确率。
参考代码(数据集在绑定资源中):
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
# 1. 加载数据
titanic = pd.read_csv('titanic.csv')
X = titanic[['Age', 'Sex', 'Fare']] # 特征
y = titanic['Survived'] # 目标变量
# 2. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 3. 定义不同特征的预处理方式
# 数值特征:Age(含缺失值)、Fare(无缺失值)
numeric_features = ['Age', 'Fare']
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='mean')), # Age用均值填补
('scaler', StandardScaler()) # 标准化Fare和填补后的Age
])
# 类别特征:Sex(名义特征)
categorical_features = ['Sex']
categorical_transformer = OneHotEncoder() # 独热编码
# 4. 用ColumnTransformer组合不同预处理
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# 5. 定义完整流水线:预处理 + 逻辑回归
model = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', LogisticRegression())
])
# 6. 训练模型并评估
model.fit(X_train, y_train)
print(f"测试集准确率:{model.score(X_test, y_test):.2f}") # 通常在0.75-0.85之间
代码解析:
ColumnTransformer分别处理数值特征和类别特征:- 数值特征(
Age、Fare):先填补Age的缺失值,再对两列做标准化; - 类别特征(
Sex):直接独热编码(将 “male”“female” 转换为两列 0/1 向量);
- 数值特征(
Pipeline确保所有预处理仅基于训练集,避免数据泄漏;
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)