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

特征离散化

约 9 分钟

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

在特征工程中,我们常常会遇到连续值特征,如年龄、收入、商品价格等。直接使用这些连续值进行建模,有时并不能很好地捕捉数据中的非线性关系,或者模型容易受到极端值的干扰。本节我们将学习特征离散化(也称为分箱或分桶),这是一种将连续值特征转换为离散区间的技术,能够增强线性模型的非线性表达能力,并提升模型的健壮性。

💡 核心导读

  • 离散化的作用:将连续特征分段,赋予线性模型非线性能力,并增强模型抗干扰性。
  • 无监督离散化方法:掌握等距切分、等频切分和K-Means切分的原理与实现。
  • 有监督离散化方法:学习如何利用决策树和卡方检验(ChiMerge)进行更贴合标签分布的离散化。
  • 实践要点:理解离散化器的“先拟合后变换”流程,并学会将处理方式应用到新数据上。

离散化的作用与原理

特征离散化,顾名思义,是对连续值属性进行处理的一种方法。其核心思想是把连续的数值范围切分成若干个段(或称为桶、箱),然后将每个原始值归属到对应的段中。

这样做主要有两大好处:

  1. 引入非线性:对于逻辑回归等线性模型,离散化可以将一个连续特征转换为多个“开关”特征(例如,通过独热编码),使得模型能够分段拟合数据,从而具备一定的非线性表达能力。
  2. 增强健壮性:离散化之后,数据被归入不同的桶中。即使原始数据存在极端值或噪声,只要它们落入同一个桶,就会被赋予相同的离散值,这在一定程度上平滑了数据,提高了模型的抗干扰能力。

在工业实践中,离散化也常被称为分箱(Binning)或分桶。

无监督离散化方法

无监督离散化不依赖目标标签,仅根据特征自身的分布进行切分。Scikit-learn提供了KBinsDiscretizer工具来实现多种无监督离散化策略。

等距切分(Uniform)

等距切分按照相同的步长将数据范围均匀地切成n个桶。每个桶的宽度(区间长度)是相等的。

import pandas as pd
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer

# 构造一个简单的泰坦尼克票价数据样例
data = {'Fare': [0.0, 7.5, 8.7, 26.0, 512.0, 5.09, 1.8, 60.34, 27.57, 36.94]}
df = pd.DataFrame(data)

print("原始数据:")
print(df)

# 使用KBinsDiscretizer进行等距切分(3个桶)
enc_equal_width = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform')
enc_equal_width.fit(df[['Fare']])

print("\n等距切分的桶边界:", enc_equal_width.bin_edges_)

# 对数据进行变换
result_equal_width = enc_equal_width.transform(df[['Fare']])
df['Fare_equal_width'] = result_equal_width.astype(int)

print("\n等距离散化后的数据:")
print(df[['Fare', 'Fare_equal_width']])
原始数据:
     Fare
0    0.00
1    7.50
2    8.70
3   26.00
4  512.00
5    5.09
6    1.80
7   60.34
8   27.57
9   36.94

等距切分的桶边界: [array([  0.        , 170.66666667, 341.33333333, 512.        ])]

等距离散化后的数据:
     Fare  Fare_equal_width
0    0.00                 0
1    7.50                 0
2    8.70                 0
3   26.00                 0
4  512.00                 2
5    5.09                 0
6    1.80                 0
7   60.34                 0
8   27.57                 0
9   36.94                 0

可以看到,区间被均匀分为[0, 170.67), [170.67, 341.33), [341.33, 512]三段。大部分样本都落在了第一个桶(编号0)中,因为票价分布极度右偏。等距切分简单直接,但完全不看数据的实际分布。

等频切分(Quantile)

等频切分旨在让每个桶内包含大致相同数量的样本。它通过数据的分位数来确定切分点。

import pandas as pd
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer

# 构造一个简单的泰坦尼克票价数据样例
data = {'Fare': [0.0, 7.5, 8.7, 26.0, 512.0, 5.09, 1.8, 60.34, 27.57, 36.94]}
df = pd.DataFrame(data)

# 使用KBinsDiscretizer进行等频切分(3个桶)
enc_equal_freq = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='quantile')
enc_equal_freq.fit(df[['Fare']])

print("等频切分的桶边界:", enc_equal_freq.bin_edges_)

# 对数据进行变换
result_equal_freq = enc_equal_freq.transform(df[['Fare']])
df['Fare_equal_freq'] = result_equal_freq.astype(int)

print("\n等频离散化后的数据:")
print(df[['Fare', 'Fare_equal_freq']])
等频切分的桶边界: [array([  0.  ,   7.5 ,  27.57, 512.  ])]

等频离散化后的数据:
     Fare  Fare_equal_freq
0    0.00                0
1    7.50                1
2    8.70                1
3   26.00                1
4  512.00                2
5    5.09                0
6    1.80                0
7   60.34                2
8   27.57                2
9   36.94                2

桶边界为[0, 7.5), [7.5, 27.57), [27.57, 512]。每个桶大约包含3-4个样本,数量基本一致。注意,由于可能存在重复的取值,等频切分不一定能做到每个桶绝对平均。

K-Means切分

K-Means切分利用K-Means聚类算法对特征值进行聚类——聚类中心本身并不是桶的边界,相邻两个聚类中心的 midpoint(中点)才是分桶边界。它试图使桶内的样本距离中心尽可能近,桶间尽可能远。

import pandas as pd
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer

# 构造一个简单的泰坦尼克票价数据样例
data = {'Fare': [0.0, 7.5, 8.7, 26.0, 512.0, 5.09, 1.8, 60.34, 27.57, 36.94]}
df = pd.DataFrame(data)

# 使用KBinsDiscretizer进行K-Means切分(3个桶)
enc_kmeans = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='kmeans')
enc_kmeans.fit(df[['Fare']])

print("K-Means切分的桶边界:", enc_kmeans.bin_edges_)

# 对数据进行变换
result_kmeans = enc_kmeans.transform(df[['Fare']])
df['Fare_kmeans'] = result_kmeans.astype(int)

print("\nK-Means离散化后的数据:")
print(df[['Fare', 'Fare_kmeans']])
K-Means切分的桶边界: [array([  0.  ,  37.27, 286.17, 512.  ])]

K-Means离散化后的数据:
     Fare  Fare_kmeans
0    0.00            0
1    7.50            0
2    8.70            0
3   26.00            0
4  512.00            2
5    5.09            0
6    1.80            0
7   60.34            1
8   27.57            0
9   36.94            0

K-Means根据数据分布找到了三个聚类中心,并以此划分边界。它结合了数据分布和距离度量,是无监督方法中较为灵活的一种。

有监督离散化方法

有监督离散化利用目标标签的信息来寻找最优的切分点,使得离散化后的特征与标签的关系更强。

基于决策树的离散化

决策树天生就是通过选择特征的最佳切分点来进行分类的。我们可以训练一棵深度较浅的决策树(如深度为2或3),用连续特征预测目标标签。然后,将样本输入训练好的树,用其落入的叶节点的预测概率(或类别)作为离散化后的新特征值。

import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier

# 构造一个包含标签的小数据集
data = {
    'Fare': [0.0, 7.5, 8.7, 26.0, 512.0, 5.09, 1.8, 60.34, 27.57, 36.94],
    'Survived': [1, 0, 0, 0, 1, 1, 1, 0, 0, 0]
}
df_supervised = pd.DataFrame(data)
print("监督学习数据集:")
print(df_supervised)

# 使用决策树进行离散化(最大深度为2)
tree_model = DecisionTreeClassifier(max_depth=2, random_state=42)
tree_model.fit(df_supervised[['Fare']], df_supervised['Survived'])

# 使用predict_proba获取属于正类(Survived=1)的概率作为新特征
df_supervised['Fare_tree_discret'] = tree_model.predict_proba(df_supervised[['Fare']])[:, 1]

print("\n基于决策树离散化后的数据(前10行):")
print(df_supervised.head(10))

print("\n决策树离散化生成的特征唯一值:")
print(df_supervised['Fare_tree_discret'].unique())

# 查看根据决策树划分形成的近似分桶边界
bins = pd.concat([df_supervised.groupby(['Fare_tree_discret'])['Fare'].min(),
                  df_supervised.groupby(['Fare_tree_discret'])['Fare'].max()], axis=1)
bins.columns = ['min', 'max']
print("\n根据离散化结果推断的近似分桶边界:")
print(bins)
监督学习数据集:
     Fare  Survived
0    0.00         1
1    7.50         0
2    8.70         0
3   26.00         0
4  512.00         1
5    5.09         1
6    1.80         1
7   60.34         0
8   27.57         0
9   36.94         0

基于决策树离散化后的数据(前10行):
     Fare  Survived  Fare_tree_discret
0    0.00         1                1.0
1    7.50         0                0.0
2    8.70         0                0.0
3   26.00         0                0.0
4  512.00         1                1.0
5    5.09         1                1.0
6    1.80         1                1.0
7   60.34         0                0.0
8   27.57         0                0.0
9   36.94         0                0.0

决策树离散化生成的特征唯一值:
[1. 0.]

根据离散化结果推断的近似分桶边界:
                   min     max
Fare_tree_discret             
0.0                7.5   60.34
1.0                0.0  512.00

观察结果:决策树学到的并不是一个单调关系,而是一个「两端高、中间低」的分段模式——低票价(≤约7.5)与极高票价(512)的样本被归入生存概率为 1 的叶节点,而中等票价区间(7.5~60.34)的样本被归入生存概率为 0 的叶节点。这正是有监督离散化的价值所在:它自动找到了贴合标签分布的切分点;而等距、等频这类无监督方法对「标签与特征之间的非线性关系」是完全无感的。这也解释了为什么金融风控等场景更偏好有监督的分桶。

卡方合并(ChiMerge)

卡方合并是一种自底向上的监督离散化方法。它最初将每个唯一值视为一个区间,然后计算相邻区间的卡方统计量。卡方值衡量了两个区间内类别分布的相似性。如果卡方值小于某个阈值(如对应p=0.05,自由度为1的卡方值3.841),则认为两个区间分布相似,可以合并。该过程迭代进行,直到所有相邻区间的卡方值都超过阈值,或达到预设的桶数量。

这种方法在金融风控等领域应用广泛,因为它能自动合并分布相似的区间,找到有区分度的分桶。

# 卡方合并的实现相对复杂,通常需要自定义或使用第三方库。
# 其核心思想是:基于相邻区间类别分布的卡方检验,不断合并最相似的区间。
# 原课程中使用了封装好的 ChiMerge 工具类。
# 以下为使用逻辑的示意性描述(伪代码,不实际运行):
# 1. 初始化:将连续特征每个排序后的唯一值作为一个初始区间。
# 2. 计算相邻区间的卡方统计量。
# 3. 找到卡方值最小的一对相邻区间。
# 4. 如果最小卡方值小于阈值,则合并这对区间,并回到步骤2。
# 5. 否则,停止合并,得到最终的分桶方案。

离散化实践要点总结

  1. 先拟合后变换:所有离散化器(无论是KBinsDiscretizer还是自定义的监督离散化器)都需要先在训练集上fit,学习切分边界,然后在训练集和测试集上分别transform。绝对不能用测试集单独fit,这会导致数据泄露。
  2. 方法选择:
    • 无监督方法(等距、等频、K-Means):计算快,不依赖标签,适用于探索性分析或标签不可用时。
    • 有监督方法(决策树、ChiMerge):利用标签信息,离散化后的特征通常与目标关联更强,可能提升模型效果,但计算成本稍高,且需要防止过拟合。
  3. 参数调优:桶的数量(n_bins)、决策树的深度等是重要参数。可以通过交叉验证来评估不同离散化方式对最终模型性能的影响。
  4. 特征衍生:离散化本身产生的是离散的桶编号(如0,1,2)。我们通常需要进一步对这些编号进行独热编码或目标编码,才能将其输入到线性模型中,从而真正实现非线性表达。

📝 动手练一练

  1. 概念辨析:等距离散化与等频离散化在数据分布极度不平衡(如存在大量0值和少量极大值)时,产生的桶内样本分布会有何不同?哪种方法可能更适合后续建模?
👉 点击查看参考答案

等距离散化会按照数值范围均匀分桶。在数据极度不平衡时,可能导致绝大多数样本集中在第一个桶,而后面几个桶样本极少甚至为空,这会使离散化特征的信息量很低。等频离散化则保证每个桶的样本量大致相同,能更好地利用数据分布,将稀少的大值样本也单独成桶或合并到有样本的桶中,通常能保留更多信息,对后续建模更有利。

  1. 动手实践:使用上面提供的等距和等频离散化代码,尝试将桶数n_bins改为5,观察桶边界的变化。然后,假设有一个新样本Fare=200.0,请手动判断它分别会落入等距和等频的哪个桶中?
👉 点击查看参考答案
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer

# 使用同样的数据
sample_fare = [[200.0]]

# 等距5桶
enc_width_5 = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='uniform')
enc_width_5.fit([[0.0], [512.0]]) # 用最小最大值拟合
print("等距5桶边界:", enc_width_5.bin_edges_)
print("新样本 Fare=200.0 等距切分落入桶:", enc_width_5.transform(sample_fare).astype(int)[0][0])

# 等频5桶 (需要用到完整数据分布来拟合,这里用之前10个样本的数据分布来近似)
data_for_freq = np.array([0.0, 7.5, 8.7, 26.0, 512.0, 5.09, 1.8, 60.34, 27.57, 36.94]).reshape(-1,1)
enc_freq_5 = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile')
enc_freq_5.fit(data_for_freq)
print("\n等频5桶边界:", enc_freq_5.bin_edges_)
print("新样本 Fare=200.0 等频切分落入桶:", enc_freq_5.transform(sample_fare).astype(int)[0][0])
等距5桶边界: [array([  0. , 102.4, 204.8, 307.2, 409.6, 512. ])]
新样本 Fare=200.0 等距切分落入桶: 1

等频5桶边界: [array([  0.   ,   3.445,   8.1  ,  26.785,  48.64 , 512.   ])]
新样本 Fare=200.0 等频切分落入桶: 4

根据计算(或运行上述代码),在10个样本的分布下,等频切分的第5个桶边界约为(48.64, 512.0],200.0落在这个桶内(编号4)。等距切分下,200.0落在第二个桶[102.4, 204.8)内(编号1)。请注意,等频的边界严重依赖于拟合所用的数据分布。

本章小结

本节我们深入学习了特征工程中的一项关键技术——特征离散化。我们理解了它如何通过将连续值分段来增强模型能力,并掌握了从简单的无监督方法(等距、等频、K-Means)到更复杂的有监督方法(决策树、卡方合并)的完整工具箱。

📋 行动清单

  • 尝试对比:在同一个数据集上,对某个连续特征分别用等频和基于决策树的方法进行离散化,观察生成的新特征与标签的相关性有何不同。
  • 流程巩固:严格遵循“在训练集fit,在训练集和测试集分别transform”的流程,编写一个完整的离散化处理函数。
  • 思考延伸:离散化后的桶编号是顺序变量,直接输入线性模型是否合适?如果不合适,下一步应该做什么处理?(提示:回顾类别型特征编码)

—— 小象教研组