什么是离散化?
离散化就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:
- 原数据为:1,999,100000,15;处理之后为:1,3,4,2;
- 原数据为:{100,200},{20,50000},{1,400}; 处理之后为:{3,4},{2,6},{1,5};
数据离散化是指将连续的数据进行分段,使其变为一段段离散化的区间。离散化过程也被表述成分箱(Binning)的过程。特征离散化常应用于逻辑回归和金融领域的评分卡中,同时在规则提取,特征分类中也有对应的应用价值。
特征离散化(分箱)可以从不同的角度来进行划分。当分箱方法使用了目标y的信息,那么该分箱方法就属于有监督的分箱方法,反之为无监督的分箱方法。
本文将介绍特征离散化常见的几种方法:
- 距离分箱-无监督
- 等频分箱-无监督
- 利用聚类分箱-无监督
- 信息熵分箱-有监督
- 基于决策树分箱-有监督
- 卡方分箱-有监督
数据离散化的意义
模型中,同一线性系数应该对所有可能的计数值起作用。过大的计数值对无监督学习方法也会造成破坏,比如k-均值聚类,它使用欧氏距离作为相似度函数来测量数据点之间的相似度。数据向量某个元素中过大的计数值对相似度的影响会远超其他元素,从而破坏整体的相似度测量。
离散化除了一些计算方面等等好处,还可以引入非线性特性,模型会更稳定。具体的优点如下:
1. 算法需要
比如决策树、朴素贝叶斯等算法,都是基于离散型的数据展开的。如果要使用该类算法,必须使用离散型的数据进行。有效的离散化能减小算法的时间和空间开销,提高系统对样本的分类聚类能力和抗噪声能力。
2. 离散化的特征相对于连续型特征更易理解,更接近知识层面的表达
比如,工资收入,月薪2000和月薪20000,从连续型特征来看高低薪的差异还要通过数值层面才能理解,但将其转换为离散型数据(底薪、高薪),则可以更加直观的表达出了我们心中所想的高薪和底薪。
3. 可以有效的克服数据中隐藏的缺陷,使模型结果更加稳定,增强模型鲁棒性
4. 提升模型的泛化能力
5. 扩展数据在不同各类型算法中的应用范围
6. 增加非线性表达能力
连续特征不同区间对模型贡献或者重要程度不一样时,分箱后不同的权重能直接体现这种差异,离散化后的特征再进行特征 交叉衍生能力会进一步加强。
当然特征离散化也有其缺点,具体如下:
1. 分箱操作必定会导致一定程度的信息损失
2. 增加流程:建模过程中加入了额外的的离散化步骤
3. 影响模型稳定性
当一个特征值处于分箱点的边缘时,此时微小的偏差会造成该特征值的归属从一箱跃迁到另外一箱,影响模型的稳定性。
准备数据
加载乳腺癌数据集,并打印原始特征分布。
import seaborn as sns import pandas as pd from sklearn.datasets import load_breast_cancer # 加载乳腺癌数据集 bc = load_breast_cancer() df = pd.DataFrame.from_records(data=bc.data, columns=bc.feature_names) df['target'] = bc.target sns.distplot(df['mean radius'], kde=False) 复制代码
运行结果如下:
# 查看数据的统计摘要,如均值,最大值,最小值等 df.describe()[['mean radius','target']] 复制代码
运行结果如下:
mean radius | target | |
count | 569.000000 | 569.000000 |
mean | 14.127292 | 0.627417 |
std | 3.524049 | 0.483918 |
min | 6.981000 | 0.000000 |
25% | 11.700000 | 0.000000 |
50% | 13.370000 | 1.000000 |
75% | 15.780000 | 1.000000 |
max | 28.110000 | 1.000000 |
距离分箱(距离区间法)
距离分箱可使用等距区间或自定义区间的方式对数据进行离散化,分段可以是线性的,也可以是指数性的。该方法(尤其是等距区间)可以较好地保持数据原有的分布。
等距分箱
等距分箱指的是每个分隔点或者划分点的距离一样,即等宽。在实践中一般指定分隔的箱数,等分计算后得到每个分隔点。例如,将数据序列分为n份,则分隔点的宽度计算公式为:
w=max−minnw = \frac {max- min}{n}w=nmax−min
这样就将原始数据划分成了n个等宽的子区间,一般情况下,分箱后每个箱内的样本数量是不一致的。
下面使用pandas中的cut函数来实现等宽分箱,代码如下:
import pprint # 等宽分箱(Equal-Width Binning) value, cutoff = pd.cut(df['mean radius'], bins=4, retbins=True, precision=2) radius = df['mean radius'] print(radius.head()) print("-------------------") print(value.head()) print("-------------------") pprint.pprint(cutoff) 复制代码
运行结果如下:
0 17.99 1 20.57 2 19.69 3 11.42 4 20.29 Name: mean radius, dtype: float64 ------------------- 0 (17.55, 22.83] 1 (17.55, 22.83] 2 (17.55, 22.83] 3 (6.96, 12.26] 4 (17.55, 22.83] Name: mean radius, dtype: category Categories (4, interval[float64]): [(6.96, 12.26] < (12.26, 17.55] < (17.55, 22.83] < (22.83, 28.11]] ------------------- array([ 6.959871, 12.26325 , 17.5455 , 22.82775 , 28.11 ]) 复制代码
可视化:
df1 = value.to_frame() df1.columns = ['bins'] sns.countplot(df1['bins']) 复制代码
等距分箱计算简单,但是当数值方差较大时,即数据离散程度很大,那么很可能出现没有任何数据的分箱,这个问题可以通过自适应数据分布的分箱方法(等频分箱)来避免。
自定义距离分箱
下面使用numpy的digitize函数和pandas的cut函数这两种方式来实现自定义距离分箱。
import numpy as np import pandas as pd small_counts = np.random.randint(0, 10000, 20) df = pd.DataFrame() df['amount'] = small_counts # 注意:bins数据是有要求的,bins内的数据一定要是降序或者升序的数据,不能是一堆无序数据。 bins=[0,200,1000,5000,10000] # 方法一 df['amount_cut']=pd.cut(df['amount'], bins) # 方法二 indices=np.digitize(df['amount'],bins) # 返回值为每个值所属区间的索引。 print(indices) df['amount_split']=[str(bins[i-1])+"~"+str(bins[i]) for i in indices] print(df.head()) 复制代码
运行结果如下:
[2 4 4 2 3 3 4 3 1 4 3 4 4 4 4 3 3 4 3 4] amount amount_cut amount_split 0 296 (200, 1000] 200~1000 1 9938 (5000, 10000] 5000~10000 2 5442 (5000, 10000] 5000~10000 3 240 (200, 1000] 200~1000 4 3811 (1000, 5000] 1000~5000 复制代码
可视化:
sns.countplot(df['amount_cut']) 复制代码
sns.countplot(df['amount_split']) 复制代码
指数性区间分箱
当数值横跨多个数量级时,最好按照10的幂(或任何常数的幂)来进行分组:0~9、10~99、100~999、1000~9999。
import pprint large_counts = np.array([296, 8286, 64011, 80, 3, 725, 867, 2215, 7689, 11495, 91897, 44, 28, 7971,926, 122, 22222]) print(large_counts.tolist()) bins=np.floor(np.log10(large_counts)) # 取对数之后再向下取整 print(bins) pprint.pprint(np.array(['['+str(10**i)+','+str(10**(i+1))+')' for i in bins])) 复制代码
运行结果如下:
[296, 8286, 64011, 80, 3, 725, 867, 2215, 7689, 11495, 91897, 44, 28, 7971, 926, 122, 22222] [2. 3. 4. 1. 0. 2. 2. 3. 3. 4. 4. 1. 1. 3. 2. 2. 4.] array(['[100.0,1000.0)', '[1000.0,10000.0)', '[10000.0,100000.0)', '[10.0,100.0)', '[1.0,10.0)', '[100.0,1000.0)', '[100.0,1000.0)', '[1000.0,10000.0)', '[1000.0,10000.0)', '[10000.0,100000.0)', '[10000.0,100000.0)', '[10.0,100.0)', '[10.0,100.0)', '[1000.0,10000.0)', '[100.0,1000.0)', '[100.0,1000.0)', '[10000.0,100000.0)'], dtype='<U18') 复制代码
等频分箱(分位数法)
顾名思义,等频分箱理论上分隔后的每个箱内得到数据量大小一致,但是当某个值出现次数较多时,会出现等分边界是同一个值,导致同一数值分到不同的箱内,这是不正确的。
具体的实现可以去除分界处的重复值,但这也导致每箱的数量不一致。
具体代码如下:
s1 = pd.Series([1,2,3,4,5,6]) value, cutoff = pd.qcut(s1, 3, retbins=True) print(value) print("------------------") print(cutoff) 复制代码
运行结果:
0 (0.999, 2.667] 1 (0.999, 2.667] 2 (2.667, 4.333] 3 (2.667, 4.333] 4 (4.333, 6.0] 5 (4.333, 6.0] dtype: category Categories (3, interval[float64]): [(0.999, 2.667] < (2.667, 4.333] < (4.333, 6.0]] ------------------ [1. 2.66666667 4.33333333 6. ] 复制代码
可视化:
sns.countplot(value) 复制代码
每个区间分别是2个数,这没有问题,但是如果某个数字出现的次数较多,则可能出现下面的情况:
s1 = pd.Series([1,2,3,4,5,6,6,6,6]) value, cutoff = pd.qcut(s1, 3, duplicates='drop', retbins=True) 复制代码
运行结果:
0 (0.999, 3.667] 1 (0.999, 3.667] 2 (0.999, 3.667] 3 (3.667, 6.0] 4 (3.667, 6.0] 5 (3.667, 6.0] 6 (3.667, 6.0] 7 (3.667, 6.0] 8 (3.667, 6.0] dtype: category Categories (2, interval[float64]): [(0.999, 3.667] < (3.667, 6.0]] ------------------ [1. 3.66666667 6. ] 复制代码
可视化:
sns.countplot(value) 复制代码
本来是要分成3个箱子的,但是由于出现同一个数值被分到了不同的箱子里,因此被合并了,所以最后只有2个箱子。
同样,我们对乳腺癌数据进行等频分箱,该数据分布正常,等频分箱后每箱数量基本一致。
value, cutoff = pd.qcut(df['mean radius'], 4, duplicates='drop', retbins=True) print(value) print("------------------") print(cutoff) 复制代码
结果如下:
0 (15.78, 28.11] 1 (15.78, 28.11] 2 (15.78, 28.11] 3 (6.9799999999999995, 11.7] 4 (15.78, 28.11] ... 564 (15.78, 28.11] 565 (15.78, 28.11] 566 (15.78, 28.11] 567 (15.78, 28.11] 568 (6.9799999999999995, 11.7] Name: mean radius, Length: 569, dtype: category Categories (4, interval[float64]): [(6.9799999999999995, 11.7] < (11.7, 13.37] < (13.37, 15.78] < (15.78, 28.11]] ------------------ [ 6.981 11.7 13.37 15.78 28.11 ] 复制代码
可视化:
sns.countplot(value) 复制代码
上述的等宽和等频分箱容易出现的问题是每箱中信息量变化不大。例如,等宽分箱不太适合分布不均匀的数据集、离群值;等频方法不太适合特定的值占比过多的数据集,如长尾分布。