Scikit-learn

数据预处理

数据预处理概念

为什么要做数据预处理?

机器学习模型的本质是 “从数据中学习规律”,但模型对 “数据格式” 和 “数据质量” 有严格要求,就像厨师做菜需要新鲜、干净的食材,否则再好的厨艺(算法)也做不出好菜(好模型)。

核心原因可以总结为一句话:

模型无法直接处理 “原始、混乱、不规范” 的数据,必须通过预处理让数据符合模型的 “输入标准”,同时提升数据质量,才能让模型学到真正的规律。

原始数据为什么 “不能直接用”?

  1. 数据缺失

    现实数据中,缺失值非常常见(比如用户没填年龄、传感器故障导致数据丢失)。

    大部分模型(如逻辑回归、SVM)无法处理 NaN(缺失值),直接输入会报错;即使部分模型(如决策树)能兼容,缺失值也会干扰模型对规律的判断。

    例:如果 “年龄” 特征有 30% 的缺失,模型可能无法正确学习 “年龄与购买意愿” 的关系。

  2. 特征量纲不统一

    不同特征的单位和数值范围可能差异极大。

    比如 “身高(厘米)” 的取值可能是 150-190,“体重(公斤)” 是 40-100,“年收入(万元)” 是 10-1000。

    许多模型(如 KNN、SVM、线性回归)基于 “距离” 或 “权重” 计算,量纲大的特征(如年收入)会 “淹没” 量纲小的特征(如身高),导致模型误以为 “年收入” 更重要,而忽略其他特征的真实影响。

  3. 存在类别型数据(非数值)

    模型只能理解数字,无法直接处理字符串(如 “性别:男 / 女”“颜色:红 / 绿 / 蓝”)。

    若不转换,模型会把 “男”“女” 当成无意义的符号,无法学习其与目标的关系。

  4. 存在冗余或低信息特征

    原始数据可能包含大量 “无用特征”(如方差接近 0 的特征,即几乎所有样本取值相同)。

    这些特征不仅增加计算量,还可能引入噪声,导致模型过拟合(把噪声当成规律)。

预处理的核心目标:让数据 “适配模型” 并 “提升质量”

预处理不是 “无意义的操作”,而是有明确目标的:

  1. 兼容性:把数据转换成模型能接受的格式(如填补缺失值、编码类别特征);
  2. 公平性:消除量纲差异,让每个特征对模型的影响 “公平可比”(如标准化 / 归一化);
  3. 有效性:剔除噪声和冗余,保留关键信息,让模型更专注于真正的规律(如特征选择)。

假设我们用 “身高(米)” 和 “体重(公斤)” 预测 “是否患高血压”,原始数据如下:

身高(米) 体重(公斤) 患高血压(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),避免模型因无法识别缺失值而报错或预测失真。
特征缩放 StandardScalerMinMaxScaler 统一不同特征的量纲(如厘米、公斤、万元),避免数值范围大的特征 “淹没” 小特征的影响。
编码处理 LabelEncoderOneHotEncoder 将字符串类型的类别特征(如 “男 / 女”“红 / 绿 / 蓝”)转换为数字,让模型能够理解和学习。
特征选择 VarianceThresholdSelectKBest 剔除低信息量或冗余的特征(如几乎所有样本取值相同的特征),减少噪声,提升模型效率。
流水线组合 PipelineColumnTransformer 将上述预处理步骤和模型训练串联成自动化流程,避免手动操作的繁琐和 “数据泄漏” 风险。
  1. 缺失值处理:数据的 “补漏”

    现实世界的数据几乎不可能完美无缺 —— 用户可能漏填信息、设备可能故障、数据传输可能丢失。如果直接忽略缺失值(删除样本),可能导致数据量减少、信息丢失(尤其当缺失比例高时);如果保留缺失值,大部分模型(如线性回归、SVM)会直接报错。

    SimpleImputer 的作用就是用合理的策略(如均值、中位数、众数)填补缺失值,让数据 “完整可用”。

  2. 特征缩放:特征的 “公平竞争”

    不同特征的数值范围可能天差地别:比如 “年龄” 是 0-120,“年收入” 是 0-1000 万,“用户活跃度” 是 0-10。

    对于依赖 “距离计算”(如 KNN、SVM)或 “权重更新”(如线性回归、神经网络)的模型来说,数值大的特征会被赋予更高的 “权重”,但这并不代表它更重要(比如 “年收入” 的数值大,不代表它比 “年龄” 对预测的影响更大)。

    缩放的目的是让所有特征 “站在同一起跑线”,确保模型能公平地学习每个特征的真实影响。

  3. 编码处理:类别特征的 “翻译”

    模型本质是数学计算,只能理解数字,无法直接处理字符串。比如 “性别” 字段的 “男” 和 “女”,模型无法直接学习它们与 “是否购买” 的关系,必须转换成数字(如 0 和 1)。

    但编码不是简单的 “替换”:

    • 目标变量(y,如 “是否购买”)可以用 LabelEncoder 直接转成 0/1/2;
    • 特征变量(X,如 “颜色”)若用简单数字编码(红 = 0,绿 = 1,蓝 = 2),模型可能误以为 “红 < 绿 < 蓝”(存在顺序关系),而实际颜色间没有顺序,此时需要 OneHotEncoder 转换成 “哑变量”(如红 =[1,0,0],绿 =[0,1,0]),避免引入虚假的顺序关系。
  4. 特征选择:数据的 “瘦身”

    特征不是越多越好。有些特征可能几乎没有信息量(如 “用户 ID”—— 每个样本取值都不同,或 “性别”——99% 都是女性),这些特征不仅增加模型计算量,还可能干扰模型对关键特征的学习(过拟合)。

    特征选择的作用是 “去粗取精”:

    • VarianceThreshold 剔除方差过小的特征(几乎无变化,信息量低);
    • SelectKBest 基于统计指标(如相关性、卡方检验)筛选与目标变量最相关的特征。
  5. 流水线组合:流程的 “自动化”

    实际项目中,预处理步骤往往不止一个(比如先填缺失值,再标准化,再编码),如果手动分步处理,不仅繁琐,还容易出现 “数据泄漏”—— 比如用整个数据集的均值填补训练集的缺失值,相当于让模型提前 “偷看” 了测试集的信息,导致评估结果失真。

    Pipeline 能将所有步骤 “串起来”,确保:

    • 所有预处理操作只基于训练集计算(如用训练集均值填补训练集和测试集的缺失值);
    • 一键完成 “预处理 + 模型训练”,代码更简洁,可复用性更强。

任务优先级:先解决 “致命问题”,再优化 “细节”

处理时注意优先级:

  1. 先处理缺失值(否则后续步骤无法进行,模型可能直接报错);
  2. 再处理编码(将非数值特征转成数字,确保模型能识别);
  3. 然后做特征缩放(针对需要的模型,如 KNN、SVM);
  4. 最后做特征选择(剔除冗余,简化模型);
  5. 全程用流水线组合(避免数据泄漏,提高效率)。

缺失值处理

什么是缺失值?

缺失值指数据集中的 “空值” 或 “未记录值”,在 Python 中通常用 np.nan(Not a Number)表示。例如:

  • 问卷调查中用户未填写的 “年龄”;
  • 传感器故障导致某一时刻的 “温度” 数据缺失;
  • 数据库同步错误导致的 “交易金额” 为空。

为什么必须处理缺失值?

  • 模型兼容性:大多数机器学习模型(如逻辑回归、SVM、KNN)无法直接处理 np.nan,输入含缺失值的数据会直接报错;
  • 信息完整性:直接删除含缺失值的样本可能导致数据量大幅减少,丢失重要信息(尤其当缺失比例较高时)。

SimpleImputer 的核心功能

SimpleImputer 提供了多种填补缺失值的策略,能自动识别并填补数据中的 np.nan,默认以 “列” 为单位处理(因为不同特征的缺失值需要用各自的统计量填补)。

SimpleImputer 填补缺失值

  1. 导入工具和数据

    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.]]
    
  2. 选择填补策略并初始化 SimpleImputer

    SimpleImputer 的核心参数是 strategy(填补策略),常用选项:

    • strategy='mean':用该列的平均值填补(默认,适用于数值型特征);
    • strategy='median':用该列的中位数填补(适用于含 outliers 离群值的数值特征);
    • strategy='most_frequent':用该列的众数填补(适用于类别型特征,如 “性别”);
    • strategy='constant':用指定的常数填补(需配合 fill_value 参数,如 fill_value=0)。

    这里先用 “平均值” 填补:

    # 初始化填补器,策略为“平均值”
    imputer = SimpleImputer(strategy='mean')
    
  3. 拟合(计算填补值)并转换(填补缺失值)

    SimpleImputer 遵循 sklearn 的统一接口:

    • fit(X):根据非缺失数据计算每列的填补值(如平均值);
    • transform(X):用计算好的填补值替换 X 中的缺失值;
    • fit_transform(X):等价于先 fittransform(更高效)。
    # 拟合并转换数据(一步完成)
    X_filled = imputer.fit_transform(X)
    
    print("填补后的数据:")
    print(X_filled)
    

    填补后输出

    填补后的数据:
    [[1.  2.  1.5]
     [3.  4.  1. ]
     [5.  6.  2. ]]
    
  4. 理解填补逻辑

    填补是以 “列” 为单位的:

    • 第 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

  2. 区分训练集和测试集:必须用训练集的统计量填补测试集的缺失值(避免数据泄漏)。例如:

    # 正确做法:用训练集拟合,分别转换训练集和测试集
    imputer.fit(X_train)  # 基于X_train计算填补值
    X_train_filled = imputer.transform(X_train)
    X_test_filled = imputer.transform(X_test)  # 用X_train的统计量填补X_test
    
  3. 类别型特征建议用众数:类别特征(如 “职业”“学历”)的缺失值,用 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=XmaxXminXXmin 默认 [0, 1] 神经网络(输入通常要求在 0-1 或 - 1-1 之间)、对特征范围有严格要求的场景

标准化(StandardScaler)—— 让特征 “分布标准化”

标准化的核心是将特征转换为 “均值为 0,标准差为 1” 的分布,保留原始数据的分布形状(如正态分布),但消除量纲影响。

  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]]
    
  2. 初始化标准化器并转换

    # 初始化标准化器
    scaler = StandardScaler()
    
    # 拟合并转换数据(计算每个特征的均值和标准差,再应用公式)
    X_scaled = scaler.fit_transform(X)
    
    print("标准化后的数据:")
    print(X_scaled)
    

    标准化后输出

    标准化后的数据:
    [[-1.22474487 -1.22474487]
     [-0.         -0.        ]
     [ 1.22474487  1.22474487]]
    
  3. 拆解标准化逻辑

    标准化以 “列” 为单位计算,我们逐特征验证:

    • 特征 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/32.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 σ=31(12.333)2+(22.333)2+(42.333)2 1.527
      • 标准化后:
        • ( 1 − 2.333 ) / 1.527 ≈ − 0.877 ≈ − 1.2247 (1 - 2.333)/1.527 ≈ -0.877 ≈ -1.2247 (12.333)/1.5270.8771.2247(注:实际计算用样本标准差,分母为 n − 1 n-1 n1
        • ( 2 − 2.333 ) / 1.527 ≈ − 0.218 ≈ 0 (2 - 2.333)/1.527 ≈ -0.218 ≈ 0 (22.333)/1.5270.2180
        • ( 4 − 2.333 ) / 1.527 ≈ 1.091 ≈ 1.2247 (4 - 2.333)/1.527 ≈ 1.091 ≈ 1.2247 (42.333)/1.5271.0911.2247
    • 特征 2(第二列)[2, 3, 5]
      • 计算逻辑同上,结果与特征 1 完全一致(因为两列数据的相对分布相同)。

归一化(MinMaxScaler)—— 让特征 “范围归一化”

归一化的核心是将特征压缩到指定范围(默认 [0, 1]),直接改变数据的分布范围,但可能放大异常值的影响(如果存在离群值)。

  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.        ]]
    
  2. 拆解归一化逻辑

    同样以 “列” 为单位:

    • 特征 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 (11)/(41)=0/3=0
        • ( 2 − 1 ) / ( 4 − 1 ) = 1 / 3 ≈ 0.333 (2 - 1)/(4 - 1) = 1/3 ≈ 0.333 (21)/(41)=1/30.333
        • ( 4 − 1 ) / ( 4 − 1 ) = 3 / 3 = 1 (4 - 1)/(4 - 1) = 3/3 = 1 (41)/(41)=3/3=1
    • 特征 2(第二列)[2, 3, 5]
      • 最小值 = 2,最大值 = 5
      • 归一化后:
        • ( 2 − 2 ) / ( 5 − 2 ) = 0 → 0 (2-2)/(5-2)=0 \to 0 (22)/(52)=00
        • ( 3 − 2 ) / ( 5 − 2 ) = 1 / 3 ≈ 0.333 (3-2)/(5-2)=1/3≈0.333 (32)/(52)=1/30.333
        • ( 5 − 2 ) / ( 5 − 2 ) = 3 / 3 = 1 (5-2)/(5-2)=3/3=1 (52)/(52)=3/3=1

两种方法的核心区别

  1. 对异常值的敏感度

    • 标准化:受异常值影响较小(因为用均值和标准差,而标准差对异常值的敏感度低于极差);

    • 归一化:受异常值影响较大(因为直接依赖最大值和最小值,若存在极端值,大部分数据会被压缩到很小的范围)。

      例:若特征 1 有一个异常值 100,则归一化后其他值会接近 0,而标准化影响较小。

  2. 适用模型

    • 标准化:优先用于基于距离的模型(KNN、SVM、KMeans)和线性模型(逻辑回归、线性回归);
    • 归一化:优先用于神经网络(输入层通常要求 0-1 范围)、决策树之外的模型(决策树不依赖特征范围,可跳过缩放)。
  3. 保留信息

    • 标准化保留特征的 “分布形状”(如正态分布的特征标准化后仍是正态分布);
    • 归一化改变分布形状,但强制特征范围统一,适合对范围有严格要求的场景。

注意事项

  1. 缩放只针对特征(X),不针对标签(y):标签的缩放会改变预测目标的物理意义(如预测房价时,y 的单位是 “万元”,缩放后失去意义)。

  2. 用训练集的统计量缩放测试集:与缺失值处理同理,必须避免数据泄漏:

    scaler.fit(X_train)  # 用训练集计算均值/标准差/最大最小值
    X_train_scaled = scaler.transform(X_train)
    X_test_scaled = scaler.transform(X_test)  # 用训练集的统计量缩放测试集
    
  3. 不是所有模型都需要缩放

    • 需要缩放:KNN、SVM、逻辑回归、线性回归、神经网络、KMeans;
    • 不需要缩放:决策树、随机森林、梯度提升树(基于特征分裂,与数值范围无关)。

编码处理(类别型特征)

什么是类别型特征?

类别型特征指取值为 “离散类别” 的特征,通常是字符串或有限的整数,可分为:

  • 名义型:类别间无顺序关系(如 “颜色:红 / 绿 / 蓝”“性别:男 / 女”);
  • 有序型:类别间有明确顺序(如 “学历:高中 / 本科 / 硕士 / 博士”“评分:1 星 / 2 星 / 3 星”)。

模型无法直接理解这些文字,必须通过编码转换为数字,但不同类型的类别特征需要不同的编码方式。

两种核心编码工具及适用场景

工具 功能 适用对象 优缺点
LabelEncoder 将类别映射为 0,1,2,… 整数 目标变量(y)或有序特征 优点:简单直观,压缩维度;缺点:会引入虚假顺序关系(不适合名义特征)
OneHotEncoder 将每个类别转换为 “哑变量”(0/1 向量) 特征变量(X)中的名义特征 优点:无顺序偏差,适合名义特征;缺点:高基数特征(类别多)会导致维度爆炸

标签编码(LabelEncoder)—— 适合目标变量和有序特征

LabelEncoder 的核心是 “一对一映射”:将每个唯一类别对应一个整数(如 “男→1”“女→0”),适合两类场景:

  • 目标变量(y):无论是否有序,都可以用(因为模型关注的是 “预测值与真实值的差异”,而非类别间的数学关系);
  • 有序特征(如 “学历”):编码后的整数能保留顺序信息(如 1<2<3 对应高中 < 本科 < 硕士)。
  1. 处理目标变量(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]
  2. 处理有序特征(如 “评分”)

    # 有序特征(有明确顺序)
    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]
  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']]
    
  2. 初始化独热编码器并转换

    # 初始化独热编码器(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 再次出现
    
  3. 理解独热编码的维度变化

    • 原始特征:1 个特征,3 个类别 → 编码后:3 个特征(每个类别对应 1 列);
    • 维度变化公式:若某特征有 n 个唯一类别,独热编码后会生成 n 个新特征(“维度扩展”)。

两种编码的核心区别

  1. 适用对象严格区分

    • LabelEncoder → 只能用于目标变量(y)或有序特征(X),绝对不能用于名义特征(X)(会引入虚假顺序);
    • OneHotEncoder → 只能用于特征变量(X)中的名义特征,不能用于目标变量(y)(多分类时 y 的独热编码需用 OneHotEncoder 的姐妹工具 LabelBinarizer,但 sklearn 模型通常支持原始整数标签)。
  2. 独热编码的 “维度爆炸” 问题

    若特征类别过多(如 “城市” 有 100 个类别),独热编码会生成 100 个新特征,导致维度骤增(“维度灾难”),此时可改用:

    • 目标编码(Target Encoding,用类别对应的目标均值编码);
    • 频数编码(用类别出现的频率编码)。
  3. 输入格式要求

    • OneHotEncoder 要求输入是二维数组(即使只有 1 个特征),而 LabelEncoder 可以处理一维数组(适合 y)。

特征选择

为什么需要特征选择?

不是特征越多,模型效果越好。原始数据中可能存在:

  • 低信息特征:如 “用户 ID”(每个样本取值唯一,无规律)、“性别”(99% 都是女性,几乎无区分度);
  • 冗余特征:如 “身高(厘米)” 和 “身高(米)”(高度相关,信息重复);
  • 噪声特征:与目标变量无关的特征(如 “姓名” 与 “是否购买商品” 无关)。

这些特征会:

  • 增加模型计算量(拖慢训练速度);
  • 干扰模型对关键特征的学习(导致过拟合);
  • 降低模型可解释性(特征太多难以分析)。

特征选择的作用是:保留 “有用” 特征,剔除 “无用” 特征,在减少计算成本的同时提升模型性能

VarianceThreshold:基于方差的过滤法

VarianceThreshold 是最简单的特征选择工具,核心逻辑是:方差越小的特征,信息量越少(取值几乎不变),可以直接剔除

  • 原理:计算每个特征的方差,保留方差大于指定阈值的特征;
  • 适用场景:快速剔除 “常量特征”(方差 = 0,所有样本取值相同)或 “近常量特征”(方差接近 0)。

VarianceThreshold 过滤低方差特征

  1. 准备数据(含低方差特征)

    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]]
    
  2. 计算各特征的方差(手动验证)

    方差公式: 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)=n11i=1n(XiXˉ)2(样本方差,分母为 n − 1 n-1 n1

    • 特征 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/31.333方差=2(21.333)2+(11.333)2+(11.333)20.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/31.666方差2(01.666)2+(41.666)2+(11.666)24.333
    • 特征 3: [ 3 , 3 , 3 ] → 均值 = 3 → 方差 = 0 [3,3,3] → 均值 = 3 → 方差 = 0 [3,3,3]均值=3方差=0
  3. 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]
    
  4. 结果解析

    • 阈值 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 的相关性):

  1. 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)
      
  2. 模型自带的特征重要性

    • 如随机森林的 feature_importances_、线性模型的系数绝对值,可根据重要性排序筛选特征。

注意事项

  1. 阈值选择需谨慎
    • VarianceThreshold 的默认阈值是 0(只剔除方差 = 0 的常量特征);
    • 若数据已标准化(方差 = 1),阈值设置需结合业务(如保留方差 > 0.5 的特征)。
  2. 先预处理,后选择特征
    • 特征选择应在 “缺失值处理、编码、缩放” 之后(如方差受量纲影响,需先标准化再计算);
    • 例:若特征单位是 “万元”,缩放为 “元” 后方差会变大,可能导致选择结果不同。
  3. 避免过度筛选
    • 特征太少可能导致模型 “欠拟合”(丢失关键信息),需通过交叉验证判断最佳特征数量。

Pipeline 流水线

为什么需要 Pipeline?

手动处理预处理步骤(如先填补缺失值、再标准化、最后训练模型)会遇到两个关键问题:

  1. 步骤繁琐:实际项目中预处理可能有 5-10 步,手动调用 fit_transform 容易出错,且代码冗余;
  2. 数据泄漏(Data Leakage):最致命的问题!如果用整个数据集的统计量(如均值、标准差)预处理训练集和测试集,相当于让模型提前 “偷看” 了测试集信息,导致评估结果失真(看似效果好,实际泛化能力差)。

Pipeline 的作用是:将所有预处理步骤和模型 “串联” 成一个整体,确保所有操作只基于训练集,自动完成 “拟合 - 转换 - 预测” 全流程,从根本上避免数据泄漏

Pipeline 的核心逻辑

Pipeline 本质是一个 “步骤容器”,按顺序存储预处理工具和模型,调用时:

  • 对训练集:按顺序执行 fit_transform(每个步骤用训练集拟合,再转换训练集);
  • 对测试集:按顺序执行 transform(每个步骤用训练集的统计量转换测试集,不重新拟合);
  • 最终用最后一步的模型进行预测。

用 Pipeline 串联 “缺失值处理→标准化→逻辑回归”

以一个含缺失值的数据集为例,演示 Pipeline 的完整用法。

  1. 准备数据和工具

    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)
    
  2. 定义 Pipeline 步骤

    Pipeline 的步骤需用列表表示,每个元素是 (步骤名称, 工具对象) 的元组,顺序必须是 “预处理步骤在前,模型在后”。

    # 定义流水线:缺失值填补 → 标准化 → 逻辑回归
    pipe = Pipeline([
        ('imputer', SimpleImputer(strategy='mean')),  # 步骤1:用均值填补缺失值
        ('scaler', StandardScaler()),  # 步骤2:标准化
        ('model', LogisticRegression())  # 步骤3:逻辑回归模型
    ])
    
  3. 用 Pipeline 拟合和评估

    Pipeline 遵循 sklearn 的统一接口,直接调用 fitscore 即可:

    # 拟合流水线(自动完成:训练集拟合→转换→模型训练)
    pipe.fit(X_train, y_train)
    
    # 评估模型(自动完成:测试集转换→模型预测→计算分数)
    accuracy = pipe.score(X_test, y_test)
    print(f"模型准确率:{accuracy:.2f}")
    

    输出

    模型准确率:1.0
    

Pipeline 如何避免数据泄漏?

我手动拆解 Pipeline 对训练集和测试集的处理逻辑:

  1. 对训练集(X_train)
    • imputer.fit_transform(X_train):用训练集的均值填补训练集缺失值;
    • scaler.fit_transform(填补后的数据):用训练集的均值和标准差标准化;
    • model.fit(标准化后的数据, y_train):用处理后的训练集训练模型。
  2. 对测试集(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 的核心优势

  1. 避免数据泄漏:严格区分训练集和测试集的处理逻辑;
  2. 代码简洁:用一个对象替代多个预处理步骤和模型,减少代码量;
  3. 支持交叉验证和网格搜索:可直接传入 cross_val_scoreGridSearchCV,实现自动化调参;
  4. 可复用性强:训练好的 Pipeline 可直接保存,部署时无需重复编写预处理逻辑。

注意事项

  1. 步骤顺序不可乱:必须按 “数据加载→预处理→模型” 的顺序排列,例如 “标准化” 不能放在 “缺失值处理” 之前(因为缺失值会导致标准化失败);

  2. 最后一步必须是模型:Pipeline 的 fit 方法会调用最后一步的 fit,而模型是唯一需要 y 进行训练的步骤;

  3. 保存和加载 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”。

具体步骤

  1. 加载数据(titanic.csv,可从 Kaggle 下载);
  2. 选择特征列:Age(年龄)、Sex(性别)、Fare(票价),目标列:Survived
  3. 预处理:
    • Age:用平均值填补缺失值;
    • Sex:用独热编码转换;
    • Fare:用标准化处理;
  4. ColumnTransformer 按列定制预处理,再用 Pipeline 串联预处理和逻辑回归模型;
  5. 划分训练集和测试集,训练模型并评估准确率。

参考代码(数据集在绑定资源中):

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 分别处理数值特征和类别特征:
    • 数值特征(AgeFare):先填补 Age 的缺失值,再对两列做标准化;
    • 类别特征(Sex):直接独热编码(将 “male”“female” 转换为两列 0/1 向量);
  • Pipeline 确保所有预处理仅基于训练集,避免数据泄漏;
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐