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

连续值特征构建

约 11 分钟

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

大家好,欢迎回到小象的特征工程课程。在上一章我们学习了数据与特征预处理,掌握了处理缺失值和异常值的基本方法。本章我们将进入特征工程的核心环节——特征构建。本节我们聚焦于连续值特征的构建方法,学习如何将原始的数值型数据转化为对模型更友好、更有效的特征形式。

💡 核心导读

  • 数值型数据的分类:理解连续数据与离散数据的本质区别,这是选择正确处理方法的前提。
  • 幅度缩放:学习 Min-Max 缩放和标准化(Z-score)两种核心方法,理解它们在不同模型(如逻辑回归 vs 树模型)中的应用场景。
  • 离散化(分箱):掌握将连续值分段处理的技巧,包括等距切分和等频切分,理解其引入非线性表达能力、缓解异常值影响的原理。
  • 组合与多项式特征:了解如何通过特征间的四则运算或逻辑组合,构建更具信息量的高阶特征。

数值型数据的分类与理解

在进行特征构建之前,我们首先要正确识别数据的类型。数值型数据并不等同于连续值特征,它内部还有重要的区分:

  • 连续数据:代表对象可测量的、在某个区间内可以取任意值的属性。其取值是通过测量或计量得到的。例如:零件的规格尺寸、人的身高体重、房间的长宽等。
  • 离散数据:指其取值是不连续的分离值,数据只能在一些特定的点(通常是整数)取值,一般通过计数方法获得。例如:企业职工人数、设备台数、年龄(通常按整数统计)等。

这种区分之所以重要,是因为针对真正的连续值数据,我们有一系列特定的特征工程方法(如幅度缩放、离散化),而离散的数值型数据(如类别编号、计数)则可能更适合采用后续章节会讲到的类别型编码或直接使用。

幅度缩放:统一特征的量纲

幅度缩放是连续值特征工程中最基础也最常用的操作之一。它的核心目的是将不同特征的数值调整到同一数量级。

为什么需要这样做?让我们考虑一个经典的例子:构建一个预测个人信用的模型,特征包含“年龄”(范围0-100)和“年收入”(范围0-1,000,000)。如果我们使用逻辑回归、支持向量机或神经网络这类计算型模型,模型内部会进行加权求和(如 WX+bWX + b)。此时,收入特征由于其数值巨大,会主导整个计算过程,导致年龄特征的作用被严重削弱,即使年龄可能是一个重要的预测因子。

幅度缩放通过数学变换,将所有特征的值映射到一个大致相当的范围内,从而消除量纲带来的不公平影响。

重要提示:幅度缩放主要对计算型模型(逻辑回归、SVM、神经网络、K近邻、主成分分析等)至关重要。对于树模型(决策树、随机森林、XGBoost、LightGBM等),由于其分裂节点时只关注特征值的排序而非绝对大小,因此通常不需要进行幅度缩放处理。

最大最小缩放 (Min-Max Scaling)

最大最小缩放是最直观的缩放方法,它将原始数据线性变换到 [0, 1] 区间内。其公式为:

xscaled=x−xmin⁡xmax⁡−xmin⁡x_{\text{scaled}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}}

其中,xx 是原始值,xmin⁡x_{\min} 和 xmax⁡x_{\max} 分别是该特征在整个数据集中的最小值和最大值。

处理后,原数据中的最小值变为0,最大值变为1,其余值均匀分布在0到1之间。

import pandas as pd
from sklearn.preprocessing import MinMaxScaler

# 构造示例数据:年龄和年收入
data = pd.DataFrame({
    'age': [25, 30, 35, 40, 45, 50],
    'annual_income': [150000, 180000, 220000, 300000, 350000, 420000]
})
print("原始数据:")
print(data)
print("\n数据统计描述(注意量纲差异):")
print(data.describe())

# 应用最大最小缩放
minmax_scaler = MinMaxScaler()
scaled_data = minmax_scaler.fit_transform(data)
scaled_df = pd.DataFrame(scaled_data, columns=['age_scaled', 'income_scaled'])
print("\n经过最大最小缩放后的数据(0-1范围):")
print(scaled_df)
原始数据:
   age  annual_income
0   25         150000
1   30         180000
2   35         220000
3   40         300000
4   45         350000
5   50         420000

数据统计描述(注意量纲差异):
             age  annual_income
count   6.000000       6.000000
mean   37.500000  270000.000000
std     9.354143  104690.018626
min    25.000000  150000.000000
25%    31.250000  190000.000000
50%    37.500000  260000.000000
75%    43.750000  337500.000000
max    50.000000  420000.000000

经过最大最小缩放后的数据(0-1范围):
   age_scaled  income_scaled
0         0.0       0.000000
1         0.2       0.111111
2         0.4       0.259259
3         0.6       0.555556
4         0.8       0.740741
5         1.0       1.000000

可以看到,年龄和收入都被压缩到了[0,1]区间,现在它们处于相同的数量级,可以在计算型模型中平等地贡献信息。

标准化 (Standardization / Z-score Scaling)

另一种常见的缩放方法是标准化,也称为Z-score标准化。它基于数据的均值(μ\mu)和标准差(σ\sigma)进行调整,其公式为:

xstandardized=x−μσx_{\text{standardized}} = \frac{x - \mu}{\sigma}

标准化处理后,数据的均值变为0,标准差变为1。如果原始数据大致服从正态分布,那么标准化后的数据会近似服从标准正态分布 N(0,1)N(0,1)。与Min-Max缩放不同,标准化不将数据限制在固定区间(如[0,1]),因此对数据中存在极大或极小值(异常值)的情况相对更鲁棒。

import pandas as pd
from sklearn.preprocessing import StandardScaler

# 使用同样的示例数据
data = pd.DataFrame({
    'age': [25, 30, 35, 40, 45, 50],
    'annual_income': [150000, 180000, 220000, 300000, 350000, 420000]
})

# 应用标准化
standard_scaler = StandardScaler()
standardized_data = standard_scaler.fit_transform(data)
standardized_df = pd.DataFrame(standardized_data, columns=['age_standardized', 'income_standardized'])
print("经过标准化(Z-score)后的数据:")
print(standardized_df)
print("\n标准化后数据的均值和标准差:")
print(f"年龄均值: {standardized_df['age_standardized'].mean():.6f}, 标准差: {standardized_df['age_standardized'].std():.6f}")
print(f"收入均值: {standardized_df['income_standardized'].mean():.6f}, 标准差: {standardized_df['income_standardized'].std():.6f}")
经过标准化(Z-score)后的数据:
   age_standardized  income_standardized
0          -1.46385            -1.255644
1          -0.87831            -0.941733
2          -0.29277            -0.523185
3           0.29277             0.313911
4           0.87831             0.837096
5           1.46385             1.569555

标准化后数据的均值和标准差:
年龄均值: 0.000000, 标准差: 1.095445
收入均值: -0.000000, 标准差: 1.095445

💡 口径说明:这里打印出的标准差 1.095445 来自 pandas 的 .std()(样本标准差,ddof=1),对 6 个样本即 6/5≈1.0954\sqrt{6/5}\approx 1.0954;而 StandardScaler 内部按总体标准差(ddof=0)缩放,缩放后数据的总体标准差为 1.0。两数不同是自由度口径差异,不是计算错误。

如何选择?

  • Min-Max缩放:当你需要将数据严格限制在一个区间(如后续的神经网络激活函数需要0-1输入),或者数据分布范围已知且稳定时使用。
  • 标准化:当数据分布近似正态,或者数据中存在异常值,你希望缩放方法对异常值不那么敏感时使用。这也是许多机器学习算法默认推荐的预处理步骤。

离散化:将连续值分段处理

离散化,也称为分箱或分桶,是另一种处理连续值特征的重要技术。它指的是将连续属性的值域划分为若干个区间(箱),然后将每个原始值映射到它所归属的区间。

为什么要离散化?

  1. 引入非线性关系:许多模型(如逻辑回归)本质上是线性的,它们假设特征与目标之间存在单调的线性或近似线性关系。但现实中,关系往往是非线性的。例如,在“是否让座”的场景中,对年龄很小(儿童)和年龄很大(老人)的乘客,让座的可能性都高,而对中青年乘客让座的可能性低。这种“两头高,中间低”的关系无法用一个简单的线性系数表达。通过将年龄离散化成“儿童”、“青年”、“中年”、“老年”等箱,模型可以为每个箱学习一个独立的系数,从而捕捉这种非线性模式。
  2. 增强模型鲁棒性:离散化能够减少异常值和数据噪声的影响。一个异常的年龄值(如200岁)会被归入“老年”箱,而不会对“青年”箱的系数估计产生直接影响。
  3. 适应某些模型要求:有些算法(如贝叶斯网络、Apriori关联规则)本身只能处理类别型变量。

离散化的方法

主要的离散化策略有两种:

  • 等距切分:按照值域的宽度均匀划分。例如,将年龄范围0-100岁,每20岁切一段,得到[0,20), [20,40), [40,60), [60,80), [80,100]五个箱。这种方法简单,但可能因为数据分布不均匀,导致某些箱内样本数极少,而另一些箱内样本数极多。
  • 等频切分:按照样本数量均匀划分,使得每个箱内包含大致相同数量的样本。例如,将样本按年龄排序后,平均分成5份,每份占20%的样本。这种方法能保证数据分布均匀,但箱的边界值可能不是整齐的数字。

离散化后,我们需要对每个箱进行编码,常用的编码方式是独热编码,将一个连续特征转化为多个二值特征。

import pandas as pd
from sklearn.preprocessing import KBinsDiscretizer

# 构造示例数据,模拟让座场景:年龄和是否让座(1是,0否)
# 注意:这里构造数据体现非线性关系:低龄和高龄让座率高。
data = pd.DataFrame({
    'age': [8, 12, 25, 30, 35, 48, 62, 71, 85, 90],
    'give_seat': [1, 1, 0, 0, 0, 0, 1, 1, 1, 1]
})
print("原始数据(年龄与让座标签):")
print(data)

# 1. 等距切分(分成4个箱)
discretizer_eqw = KBinsDiscretizer(n_bins=4, encode='onehot-dense', strategy='uniform')
age_binned_eqw = discretizer_eqw.fit_transform(data[['age']])
bin_edges_eqw = discretizer_eqw.bin_edges_[0]
print(f"\n等距切分边界: {bin_edges_eqw}")
print("等距切分后的One-hot编码(每行代表一个样本,每列代表一个年龄分段):")
print(age_binned_eqw)

# 2. 等频切分(分成4个箱)
discretizer_eqf = KBinsDiscretizer(n_bins=4, encode='onehot-dense', strategy='quantile')
age_binned_eqf = discretizer_eqf.fit_transform(data[['age']])
bin_edges_eqf = discretizer_eqf.bin_edges_[0]
print(f"\n等频切分边界: {bin_edges_eqf}")
print("等频切分后的One-hot编码:")
print(age_binned_eqf)
原始数据(年龄与让座标签):
   age  give_seat
0    8          1
1   12          1
2   25          0
3   30          0
4   35          0
5   48          0
6   62          1
7   71          1
8   85          1
9   90          1

等距切分边界: [ 8.  28.5 49.  69.5 90. ]
等距切分后的One-hot编码(每行代表一个样本,每列代表一个年龄分段):
[[1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 1. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]
 [0. 0. 0. 1.]
 [0. 0. 0. 1.]]

等频切分边界: [ 8.  25.  41.5 71.  90. ]
等频切分后的One-hot编码:
[[1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 1. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]
 [0. 0. 0. 1.]
 [0. 0. 0. 1.]]

观察结果:

  • 等距切分:边界是 8, 28.5, 49, 69.5, 90。样本分布不均匀:第一个箱(828.5)有 3 个样本,第三个箱(4969.5)只有 1 个样本(62 岁),最后一个箱(69.5~90)有 3 个样本。
  • 等频切分:边界是 8, 25, 41.5, 71, 90。目标是每个箱约 2.5 个样本。可以看到,边界值不再是整齐的间隔,但每个箱的样本数更均衡(分别为 2、3、2、3 个)。

组合特征与多项式特征

除了对单个特征进行变换,我们还可以通过组合多个原始特征来构建新的、可能更具预测力的特征。

  • 组合特征:通常指对几个条件做“且”的逻辑组合,在类别型特征中更常见(例如“性别=女且城市=北京”)。
  • 多项式特征:针对连续值特征,通过四则运算(加、减、乘、除、平方、开方等)构建新的特征。例如,从“身高”和“体重”可以衍生出“身体质量指数(BMI) = 体重/身高^2”。乘法或除法交互项可以帮助线性模型捕捉特征间的交互效应。
import pandas as pd

# 示例:手动构建组合特征(收入年龄比)
data = pd.DataFrame({
    'age': [25, 30, 35, 40, 45],
    'annual_income': [150000, 180000, 220000, 300000, 350000]
})
# 构建新特征:单位年龄收入(假设业务上认为这可能与消费能力相关)
data['income_per_age'] = data['annual_income'] / data['age']
# 构建交互项:年龄与收入的乘积(有时可代表累积资源)
data['age_times_income'] = data['age'] * data['annual_income']
print("添加组合特征后的数据:")
print(data)
添加组合特征后的数据:
   age  annual_income  income_per_age  age_times_income
0   25         150000     6000.000000           3750000
1   30         180000     6000.000000           5400000
2   35         220000     6285.714286           7700000
3   40         300000     7500.000000          12000000
4   45         350000     7777.777778          15750000

sklearn 提供了 PolynomialFeatures 工具来自动生成多项式特征,它可以生成特征的所有多项式组合(如 x1,x2,x12,x22,x1x2x_1, x_2, x_1^2, x_2^2, x_1x_2),在需要探索复杂特征关系时非常有用。

📝 动手练一练

  1. 概念辨析:假设你正在处理一个电商数据集,其中包含“用户浏览商品时长(秒)”和“用户历史订单数”两个特征。从连续值特征工程的角度,你会对它们分别采用哪种处理方式(幅度缩放、离散化、直接使用)?请说明理由。
👉 点击查看参考答案
  • 用户浏览商品时长(秒):这是一个典型的连续值测量数据。建议先进行幅度缩放(如标准化),因为其取值可能从几秒到几小时,量纲跨度大,尤其在使用逻辑回归等模型时。如果业务上认为浏览时长与购买意愿存在非单调关系(例如,太短可能没兴趣,太长可能只是闲逛,中等时长最可能购买),也可以考虑离散化(分箱)来捕捉这种非线性。
  • 用户历史订单数:这是一个离散的计数数据。虽然它是数值,但取值是非负整数。对于较小的取值范围,可以将其视为类别型特征并进行编码(如序号编码或独热编码),特别是当订单数较少(如0,1,2,3+)且不同订单数代表显著不同的用户阶段时。如果取值范围很大,也可以将其作为连续值进行幅度缩放。通常不建议对计数数据做离散化,因为它本身已经是离散的。
  1. 动手实践:使用上面“年龄与收入”的示例数据,尝试用 sklearn 的 PolynomialFeatures (degree=2) 生成多项式特征。观察生成的新特征包含了哪些项?(提示:需要 from sklearn.preprocessing import PolynomialFeatures)
👉 点击查看参考答案
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures

data = pd.DataFrame({
    'age': [25, 30, 35],
    'annual_income': [150000, 180000, 220000]
})

poly = PolynomialFeatures(degree=2, include_bias=False)
poly_features = poly.fit_transform(data)
poly_feature_names = poly.get_feature_names_out(['age', 'annual_income'])

poly_df = pd.DataFrame(poly_features, columns=poly_feature_names)
print("原始特征:")
print(data)
print("\n二次多项式特征(包含交互项和平方项):")
print(poly_df)
原始特征:
   age  annual_income
0   25         150000
1   30         180000
2   35         220000

二次多项式特征(包含交互项和平方项):
    age  annual_income   age^2  age annual_income  annual_income^2
0  25.0       150000.0   625.0          3750000.0     2.250000e+10
1  30.0       180000.0   900.0          5400000.0     3.240000e+10
2  35.0       220000.0  1225.0          7700000.0     4.840000e+10

运行后你将看到,新特征包括:['age', 'annual_income', 'age^2', 'age annual_income', 'annual_income^2']。这允许模型拟合年龄和收入之间更复杂的曲面关系,而不仅仅是线性平面。

本章小结

本节我们深入学习了连续值特征构建的三大核心武器:幅度缩放、离散化和组合/多项式特征。记住,特征工程没有一成不变的公式,核心在于理解数据背后的业务逻辑以及你所选用模型的特点。

📋 行动清单

  • 审视你的数据:打开一个项目中的数据集,区分出真正的连续值特征和离散的数值特征。
  • 尝试缩放:对连续值特征分别用 MinMaxScaler 和 StandardScaler 进行变换,观察数据分布的变化。
  • 实践分箱:选择一个连续特征,用 KBinsDiscretizer 分别尝试等距和等频分箱,并思考哪种方式更符合该特征的业务含义。

—— 小象教研组