查看全课大纲(第 16 / 35 节)

特征变换

约 5 分钟

小象实战讲义 · 特征工程精讲与实践

在特征工程中,我们不仅会从原始数据中构造新特征,还会对已有特征的分布进行调整,这就是特征变换。本节将聚焦于对连续值特征进行数学变换,以调整其分布形态,使其更符合模型的假设或提升模型的性能。学完本节,你将掌握对数变换、幂变换、Box-Cox变换等多种常用特征变换方法,并理解它们适用的场景。

💡 核心导读

  • 目的:通过数学变换调整连续特征的分布,使其更接近正态分布或减小极端值的影响,从而提升模型效果。
  • 常用方法:对数变换、平方根变换、指数变换、Box-Cox变换、分位数变换等。
  • 适用场景:处理长尾分布、幅度差异巨大的数据,或在回归问题中对标签进行变换。
  • 核心工具:主要依赖 numpy 和 sklearn.preprocessing 中的 PowerTransformer、QuantileTransformer。
  • 实践要点:变换后需观察分布变化,理解不同变换对数据“压缩”或“拉伸”的效果。

特征变换概述

特征变换(Feature Transformation)是指对连续值字段进行数学上的调整或校正,以改变其数据分布。这种变换的目的通常是为了:

  1. 减小数据间的幅度差异:当特征中存在极大值和极小值,且分布极不均匀(长尾分布)时,模型可能被少数极端值过度影响。通过变换可以压缩这种差异。
  2. 使数据分布更接近正态分布:许多统计方法和机器学习模型(如线性回归)在数据服从或近似正态分布时表现更好。
  3. 稳定方差:对于方差与均值相关的数据,变换可以使其方差更稳定。

与之前介绍的幅度缩放(如标准化、归一化)不同,缩放主要改变数据的尺度和中心位置,但不改变原始数据的相对顺序和分布形状。而特征变换则会改变数据的分布形态。

常用特征变换方法

我们将通过一个简化的泰坦尼克数据集船票价格(Fare)字段来演示各种变换。为了确保代码的独立性和可复现性,我们直接在代码块内构造一个微型数据集。

import pandas as pd
import numpy as np

# 构造一个微型数据集,模拟泰坦尼克号船票价格的长尾分布
# 包含几个较小的值和几个较大的值
data_sample = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 512.33]  # 包含一个极端大值512.33
})

print("原始数据:")
print(data_sample[['PassengerId', 'Fare']])
print("\n原始Fare字段描述统计:")
print(data_sample['Fare'].describe())
原始数据:
   PassengerId    Fare
0            1    7.25
1            2   71.28
2            3    7.92
3            4   53.10
4            5    8.05
5            6  512.33

原始Fare字段描述统计:
count      6.000000
mean     109.988333
std      198.986416
min        7.250000
25%        7.952500
50%       30.575000
75%       66.735000
max      512.330000
Name: Fare, dtype: float64

可以看到,Fare 字段的最小值为7.25,最大值高达512.33,标准差很大,是一个典型的具有较大幅度差异的字段。

对数变换 (Log Transformation)

对数变换是处理正偏态(右偏)长尾数据最常用的方法之一。其公式为:x_new = log(x + 1)。加1是为了防止对0取对数。

import pandas as pd
import numpy as np

# 构造微型数据集
data_sample = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 512.33]
})

# 对数变换演示
data_log = data_sample.copy()
# 应用对数变换,加1防止对0取对数
data_log['Fare_log'] = np.log(data_log['Fare'] + 1)
print("对数变换后数据:")
print(data_log[['Fare', 'Fare_log']])
print("\n变换后描述统计:")
print(data_log['Fare_log'].describe())
对数变换后数据:
     Fare  Fare_log
0    7.25  2.110213
1   71.28  4.280547
2    7.92  2.188296
3   53.10  3.990834
4    8.05  2.202765
5  512.33  6.240919

变换后描述统计:
count    6.000000
mean     3.502262
std      1.655154
min      2.110213
25%      2.191913
50%      3.096799
75%      4.208119
max      6.240919
Name: Fare_log, dtype: float64

观察结果:原始数据中最大值 512.33 约为最小值 7.25 的 71 倍;对数变换后,最大值 6.24 仅为最小值 2.11 的约 3 倍。对数变换显著压缩了极大值与其余值之间的绝对差距,使数据分布更加紧凑。

幂变换 (Power Transformation)

幂变换通过对数据施加指数运算来调整分布,包括平方根变换和更一般的指数变换。公式为:x_new = x^α。

  • 平方根变换 (α=0.5):x_new = sqrt(x),效果与对数变换类似,但压缩力度稍弱。
  • 指数变换:α取其他值,如0.2(相当于开5次方根,压缩),或2(平方,扩大差异)。
  • 倒数变换:x_new = 1/x,同样用于压缩幅度差异,但实际项目中使用较少(原课程也仅作提及)。
import pandas as pd
import numpy as np

# 构造微型数据集
data_sample = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 512.33]
})

# 平方根变换和指数变换演示
data_power = data_sample.copy()
data_power['Fare_sqrt'] = np.sqrt(data_power['Fare'])          # 平方根变换 (α=0.5)
data_power['Fare_exp_0.2'] = data_power['Fare'] ** 0.2        # 指数变换 (α=0.2,相当于开5次方)
data_power['Fare_square'] = data_power['Fare'] ** 2           # 平方变换 (α=2,扩大差异)

print("幂变换结果对比:")
print(data_power[['Fare', 'Fare_sqrt', 'Fare_exp_0.2', 'Fare_square']])
幂变换结果对比:
     Fare  Fare_sqrt  Fare_exp_0.2  Fare_square
0    7.25   2.692582      1.486167      52.5625
1   71.28   8.442748      2.347435    5080.8384
2    7.92   2.814249      1.512673      62.7264
3   53.10   7.286975      2.213191    2819.6100
4    8.05   2.837252      1.517606      64.8025
5  512.33  22.634708      3.482651  262482.0289

可以看到:

  • Fare_sqrt 和 Fare_exp_0.2 都压缩了数据,极端值512.33的影响被减弱。
  • Fare_square 则相反,放大了数据间的差异,最大值变得极其巨大。

Box-Cox变换与分位数变换

对于更复杂的分布调整,我们可以使用sklearn提供的自动化变换工具。

Box-Cox变换:要求输入数据必须为正数。它寻找一个最优的λ参数,使得变换后的数据尽可能接近正态分布。其公式为: 当 λ ≠ 0 时, y = (x^λ - 1) / λ 当 λ = 0 时, y = ln(x)

分位数变换:将原始数据的分布映射到指定的目标分布(如标准正态分布)。它基于分位数进行匹配,不依赖于特定的参数族,能更好地将任何分布转换为目标分布。

import pandas as pd
import numpy as np
from sklearn.preprocessing import PowerTransformer, QuantileTransformer

# 构造微型数据集(确保数据为正)
data_sample = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 512.33]
})

data_advanced = data_sample.copy()

# Box-Cox变换
pt = PowerTransformer(method='box-cox') # method='box-cox' 要求数据为正
pt.fit(data_advanced[['Fare']])
data_advanced['Fare_boxcox'] = pt.transform(data_advanced[['Fare']])

# 分位数变换(映射到标准正态分布)
qt = QuantileTransformer(output_distribution='normal', random_state=42)
qt.fit(data_advanced[['Fare']])
data_advanced['Fare_quantile'] = qt.transform(data_advanced[['Fare']])

print("高级变换结果:")
print(data_advanced[['Fare', 'Fare_boxcox', 'Fare_quantile']])
print("\nBox-Cox变换后描述统计:")
print(data_advanced['Fare_boxcox'].describe())
print("\n分位数变换后描述统计:")
print(data_advanced['Fare_quantile'].describe())
高级变换结果:
     Fare  Fare_boxcox  Fare_quantile
0    7.25    -1.024254      -5.199338
1   71.28     0.748686       0.841621
2    7.92    -0.929755      -0.841621
3   53.10     0.584545       0.253347
4    8.05    -0.912631      -0.253347
5  512.33     1.533408       5.199338

Box-Cox变换后描述统计:
count    6.000000e+00
mean    -1.110223e-16
std      1.095445e+00
min     -1.024254e+00
25%     -9.254740e-01
50%     -1.640427e-01
75%      7.076508e-01
max      1.533408e+00
Name: Fare_boxcox, dtype: float64

分位数变换后描述统计:
count    6.000000e+00
mean     1.630755e-11
std      3.335004e+00
min     -5.199338e+00
25%     -6.945527e-01
50%      1.665335e-16
75%      6.945527e-01
max      5.199338e+00
Name: Fare_quantile, dtype: float64

两种变换都试图将数据标准化(均值为0),并调整分布。Box-Cox 变换自带标准化,其输出的总体标准差(ddof=0)恰为 1——上面用 pandas 的 describe 查看时显示 1.095,是因为 pandas 默认按样本标准差(ddof=1)计算。分位数变换后数据被严格映射到标准正态分布的分位数上。注意,由于我们样本量极小,变换效果主要是演示API用法,在实际大数据集上,这些变换能更有效地将长尾分布“拉”向正态分布。

变换方法的选择与应用场景

  • 对数变换:简单有效,是处理正偏态、长尾分布的首选尝试方法。也常用于回归问题的标签变换。
  • 平方根变换:效果略弱于对数变换,适用于轻度偏态的数据。
  • Box-Cox变换:更通用和自动化,能自动寻找最优变换参数,但要求数据严格为正。
  • 分位数变换:最强大,可以将任何分布转换为目标分布,且对异常值不敏感,但计算成本相对较高。

重要提示:所有变换都应在训练集上拟合(fit),然后用相同的转换器去转换(transform)验证集和测试集,以确保数据分布的一致性,避免数据泄露。

📝 动手练一练

  1. 概念辨析:特征幅度缩放(如StandardScaler)与本节介绍的特征变换(如Log Transformation)最核心的区别是什么?请从数据分布形态改变的角度简要说明。
👉 点击查看参考答案

特征幅度缩放(如标准化、归一化)主要通过线性变换改变数据的尺度和中心位置(例如,标准化使均值为0,标准差为1),但不改变原始数据的分布形状。数据点之间的相对顺序和分布形态(如偏度、峰度)保持不变。

而特征变换(如对数变换、Box-Cox变换)通过非线性数学函数对数据进行映射,其核心目的是改变数据的分布形态,例如将长尾分布调整为更接近正态分布,或压缩极端值带来的幅度差异。变换后,数据点的相对关系(顺序可能不变,但间距)和整体的分布形状都发生了改变。

  1. 动手实践:假设你有一个代表网站用户浏览时长的特征 view_time,其部分取值为 [2, 15, 3, 600, 5, 1800](单位:秒)。其中包含少数极长的会话(如1800秒)。请编写代码,分别使用对数变换和平方根变换处理该数据,并观察变换后最大值与最小值的比值变化,说明哪种变换在此场景下压缩幅度差异的效果更明显。
👉 点击查看参考答案
import numpy as np
import pandas as pd

# 原始数据
view_time = pd.Series([2, 15, 3, 600, 5, 1800])
print("原始数据:", view_time.values)
print("原始极值比 (max/min):", view_time.max() / view_time.min())

# 对数变换 (加1防止对0取对数)
view_time_log = np.log(view_time + 1)
print("\n对数变换后:", np.round(view_time_log, 2))
print("对数变换后极值比:", np.round(view_time_log.max() / view_time_log.min(), 2))

# 平方根变换
view_time_sqrt = np.sqrt(view_time)
print("\n平方根变换后:", np.round(view_time_sqrt, 2))
print("平方根变换后极值比:", np.round(view_time_sqrt.max() / view_time_sqrt.min(), 2))
原始数据: [   2   15    3  600    5 1800]
原始极值比 (max/min): 900.0

对数变换后: 0    1.10
1    2.77
2    1.39
3    6.40
4    1.79
5    7.50
dtype: float64
对数变换后极值比: 6.82

平方根变换后: 0     1.41
1     3.87
2     1.73
3    24.49
4     2.24
5    42.43
dtype: float64
平方根变换后极值比: 30.0

分析:原始数据极值比高达900倍。经过对数变换,极值比降至约6.8倍;经过平方根变换,极值比降至约30倍。对数变换在此场景下压缩幅度差异的效果更为显著,更有效地减弱了极端长尾值(1800秒)的影响。

本章小结

本节我们深入学习了特征变换,这是处理连续值特征、优化其分布以适配模型的关键技术。核心在于理解不同非线性变换如何影响数据的分布形态,特别是对长尾、偏态数据的校正作用。

📋 行动清单

  • 掌握核心方法:理解对数变换、Box-Cox变换、分位数变换的原理、公式及适用场景。
  • 学会观察与选择:面对一个新的连续特征,先观察其分布(如使用直方图),若发现长尾、偏态或幅度差异巨大,则考虑应用特征变换。
  • 正确工程实践:记住在sklearn的Pipeline或工作流中,变换器必须在训练集上fit,再用于转换所有数据集,严防数据泄露。

—— 小象教研组