浅析特征数据离散化的几种方法

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

什么是离散化?

离散化就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:

  • 原数据为:1,999,100000,15;处理之后为:1,3,4,2;

  • 原数据为:{100,200},{20,50000},{1,400}; 处理之后为:{3,4},{2,6},{1,5};

数据离散化是指将连续的数据进行分段,使其变为一段段离散化的区间。离散化过程也被表述成分箱(Binning)的过程。特征离散化常应用于逻辑回归和金融领域的评分卡中,同时在规则提取,特征分类中也有对应的应用价值。

特征离散化(分箱)可以从不同的角度来进行划分。当分箱方法使用了目标y的信息,那么该分箱方法就属于有监督的分箱方法,反之为无监督的分箱方法。

本文将介绍特征离散化常见的几种方法:

  1. 距离分箱-无监督
  2. 等频分箱-无监督
  3. 利用聚类分箱-无监督
  4. 信息熵分箱-有监督
  5. 基于决策树分箱-有监督
  6. 卡方分箱-有监督

数据离散化的意义

模型中,同一线性系数应该对所有可能的计数值起作用。过大的计数值对无监督学习方法也会造成破坏,比如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)
复制代码

运行结果如下:

image.png

# 查看数据的统计摘要,如均值,最大值,最小值等
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 = m a x m i n n w = \frac {max- min}{n}

这样就将原始数据划分成了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'])
复制代码

image.png

等距分箱计算简单,但是当数值方差较大时,即数据离散程度很大,那么很可能出现没有任何数据的分箱,这个问题可以通过自适应数据分布的分箱方法(等频分箱)来避免。

自定义距离分箱

下面使用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'])
复制代码

image.png

sns.countplot(df['amount_split'])
复制代码

image.png

指数性区间分箱

当数值横跨多个数量级时,最好按照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)
复制代码

image.png

每个区间分别是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)
复制代码

image.png

本来是要分成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)
复制代码

image.png

上述的等宽和等频分箱容易出现的问题是每箱中信息量变化不大。例如,等宽分箱不太适合分布不均匀的数据集、离群值;等频方法不太适合特定的值占比过多的数据集,如长尾分布。

利用聚类离散化

聚类分析是一种流行的离散化方法。通过聚类算法(K-Means算法)将连续属性值进行聚类,处理聚类之后的到的k个簇。聚类考虑数据属性值的分布以及数据点的邻近性,因此可以产生高质量的离散化结果。

import numpy as np
import pandas as pd
from sklearn.cluster import KMeans

small_counts = np.random.randint(0, 10000, 20)
df = pd.DataFrame()
df['amount'] = small_counts

data_reshape = small_counts.reshape((data.shape[0],1))
model_kmeans = KMeans(n_clusters=4,random_state=0)
kmeans_result = model_kmeans.fit_predict(data_reshape)
df['amount_cut'] = kmeans_result
df.sort_values(by=['amount_cut'])
复制代码

运行结果:

amount amount_cut
9 9449 0
16 8283 0
15 7755 0
14 8226 0
13 9257 0
12 7068 0
7 7658 0
19 8728 0
4 2017 1
3 2956 1
2 3310 1
10 4500 2
11 4029 2
5 4936 2
17 5337 2
6 3971 2
8 432 3
18 208 3
1 1023 3
0 327 3

信息熵分箱

上面介绍的距离分箱和等频分箱方法对建模的优化有限。如果分箱后箱内样本对y的区分度好,那么这是一个好的分箱。通过信息论理论,我们可知,信息熵衡量了这种区分能力。当特征按照某个分隔点划分为上下两部分后能达到最大的信息增益,那么这就是一个好的分隔点。由上可知,信息熵分箱是有监督的分箱方法。其实,决策树的节点分裂原理也是基于信息熵。

它基本思想是离散后输入变量对输出变量的解释能力变强,则这种离散是有用的,否则是没有意义的。

它是利用信息增益最大化的方法寻找连续变量的最优切点,当切点确定后,将连续变量一分为二,分为两部分数据集,在这两部分数据集中用同样的方法循环切分,直到信息增益的值小于停止标准为止。

关于熵、信息、信息熵的说明:

熵的概念最早起源于物理学,用于度量一个热力学系统的无序程度。在信息论里面,熵是对不确定性的测量。

信息是用来消除随机不确定性的东西。

信息熵则是用来解决信息的度量问题。从公式上来看,它其实是一个随机变量信息量的数学期望。

基于决策树分箱

由于决策树的结点选择和划分也是根据信息熵来计算的,因此我们其实可以利用决策树算法来进行特征选择。

决策树分箱的原理就是用想要离散化的变量(单变量)用树模型拟合目标变量,例如,直接使用sklearn提供的决策树(是用cart决策树实现的),然后将内部节点的阈值作为分箱的切点。

这里以乳腺癌数据为例,首先取其中mean radius字段,和target字段来拟合一棵决策树。

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

# bc = load_breast_cancer()
# df = pd.DataFrame.from_records(data=bc.data, columns=bc.feature_names)
# df['target'] = bc.target
print(df.sample(5)[["mean radius","target"]])

print("-----------------")

dt = DecisionTreeClassifier(criterion='entropy', max_depth=3) # 树最大深度为3
dt.fit(df['mean radius'].values.reshape(-1, 1), df['target'])

print(dt.tree_.feature) # 存放每个节点所采用的特征
print(dt.tree_.threshold) # 存放每个节点特征的阈值
print(dt.tree_.children_left) # 存放经过特征和阈值分裂之后的左孩子
print(dt.tree_.children_right) # 存放经过特征和阈值分裂之后右孩子

print("-----------------")
# 取出这课决策树的所有叶节点的分割点的阈值
print(np.where(dt.tree_.children_right > -1))
qtsr = dt.tree_.threshold[np.where(dt.tree_.children_right > -1)]
print(qtsr)

print(np.where(dt.tree_.children_left > -1))
qts = dt.tree_.threshold[np.where(dt.tree_.children_left > -1)]
print(qts)

print("-----------------")

qts = np.sort(qts)
res = [np.round(x, 3) for x in qts.tolist()]

print(res)
复制代码

运行结果:

     mean radius  target
556        10.16       1
254        19.45       0
117        14.87       0
65         14.78       0
568         7.76       1
-----------------
[ 0  0  0 -2 -2  0 -2 -2  0  0 -2 -2 -2]
[15.04500008 13.09499979 10.94499969 -2.         -2.         13.70499992
 -2.         -2.         17.88000011 17.80000019 -2.         -2.
 -2.        ]
[ 1  2  3 -1 -1  6 -1 -1  9 10 -1 -1 -1]
[ 8  5  4 -1 -1  7 -1 -1 12 11 -1 -1 -1]
-----------------
(array([0, 1, 2, 5, 8, 9]),)
[15.04500008 13.09499979 10.94499969 13.70499992 17.88000011 17.80000019]
(array([0, 1, 2, 5, 8, 9]),)
[15.04500008 13.09499979 10.94499969 13.70499992 17.88000011 17.80000019]
-----------------
[10.945, 13.095, 13.705, 15.045, 17.8, 17.88]
复制代码

注意:这里只给出了6个点,但是相当于分了7个箱子。

可视化:

我们将这7个箱子分别设为a-g,我们可以将划分后的效果绘制出来:

radius = df['mean radius'].values.tolist()

r = []
for i in radius:
    if i < res[0]:
        r.append('a')
    elif i >= res[-1]:
        r.append('g')
    else:
        for j in range(0, 5):
            if i > res[j] and i <= res[j+1]:
                r.append(chr(98+j))
                break
sns.countplot(r)
复制代码

image.png

卡方分箱(ChiMerge)

卡方分箱是自底向上的(即基于合并的)数据离散化方法。它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。

关于卡方检验说明:

卡方检验可以用来评估两个分布的相似性,因此可以将这个特性用到数据分箱的过程中。

它主要包括两个阶段:初始化阶段和自底向上的合并阶段

1、初始化阶段:

首先按照属性值的大小进行排序(对于非连续特征,需要先做数值转换,然后排序),然后每个属性值单独作为一组。

2、合并阶段:

(1)对每一对相邻的组,计算卡方值。

(2)根据计算的卡方值,对其中最小的一对邻组合并为一组。

(3)不断重复(1),(2)直到计算出的卡方值都不低于事先设定的阈值或者分组数达到一定的条件(如,最小分组数5,最大分组数8)。

简单来讲,卡方分箱就是不断的计算相邻区间的卡方值(卡方值越小表示分布越相似),将分布相似的区间(卡方值最小的)进行合并,直到相邻区间的分布不同,达到一个理想的分箱结果。理想的分箱是在同一个区间内标签的分布是相同的。

参考文档

猜你喜欢

转载自juejin.im/post/7030937879660462110