浅谈各种连续变量分箱原理以及内涵(卡方,KS,IV值,树模型)


文章目录

  • 1.变量分箱对模型的好处
      • 1.降低异常值的影响,增加模型的稳定性
      • 2.缺失值作为特殊变量参与分箱,减少缺失值填补的不确定性(分箱还可以解决缺失值 )
      • 3.增加变量的可解释性
      • 4.增加变量的非线性
      • 5.增加模型的预测效果
  • 2.分箱的局限
      • 1.同一箱内的样本具有同质性
      • 2.需要专家经验支持
  • 3.变量分箱要注意的问题
      • 1.分箱结果不宜过多
      • 2.分箱结果不宜过少
      • 3.分箱后单调性的要求
  • 4.变量分箱的流程
      • 变量分箱的流程
  • 5.卡方分箱
  • 6.KS分箱
  • 7.混淆矩阵概念复习
  • 8.最优IV分箱
  • 9.基于树的最优分箱方法
  • 10.分箱框架源码(卡方、最优IV、信息增益)

什么是变量分箱:
  变量分箱(特征分箱)是一种特征工程方法.
变量分箱的目的:
  意在增强变量的可解释性与预测能力。

对于变量取值较稀疏的离散变量也应该进行分箱处理。

比如借款人的地址信息往往非常稀疏,通常先对地址信息处理到省或市,用每个省或市的坏样本比率进行数值化,将数值化后的变量作为连续变量进行分箱.

1.变量分箱对模型的好处

1.降低异常值的影响,增加模型的稳定性

1
 - 通过分箱 来降低噪声,使模型鲁棒性更好

2.缺失值作为特殊变量参与分箱,减少缺失值填补的不确定性(分箱还可以解决缺失值 )

1
2
3
- 通常的做法是,离散特征将缺失值转为字符串作为特殊字符即可,
- 连续特征将缺失值作为特殊值即可
 - 在后面的代码中连续值填充-777,离散值填充NA

3.增加变量的可解释性

1
 - 分箱的方法往往要配合变量编码使用,这就大大提高了变量的可解释性

4.增加变量的非线性

1
- 提高了模型的拟合能力

5.增加模型的预测效果

1
2
- 通常假设训练集和测试集满足同分布,分箱使连续变量离散化,更容易满足同分布的假设
- 即减少模型在训练集的表现和测试集的偏差

2.分箱的局限

1.同一箱内的样本具有同质性

1
2
3
4
5
6
    分箱的基本假设是分在一个箱内的样本(借款人)具有相同
    的风险等级,比如按年龄分箱的结果为{[18,25],[25,40],
    [40,55],[55,100]},也就是将年龄在 18~25 的借款人统一按照
    同一个数值变量来代替。对于树模型就减少了模型选择最优切分
    点的可选择范围,会对模型的预测能力产生影响,损失了模型的
    分辨能力

2.需要专家经验支持

3.变量分箱要注意的问题

分箱分的不好的话有些值的预测能力会忽略 会影响模型的预测能力 削弱模型的预测能力

1.分箱结果不宜过多

  • 分箱过多导致特征过于稀疏,编码后的特征维度快速增加,使特征更加稀疏,会降低模型的预测效果
  • 极端例子 一共100个样本 分了100个箱子 严重失衡

2.分箱结果不宜过少

  • 每个箱子默认是同质的即风险等级相同
  • 如果分箱过少则可能会造成模型辨识度过低
  • 例如,年龄分箱结果为{[18,50],[50,100]},认为18~50岁的借款人风险水平相同这是不符合业务解释的。

3.分箱后单调性的要求

4.变量分箱的流程

变量分箱的目的是增加变量的预测能力或减少变量的自身冗余。

当预测能力不再提升或冗余性不再降低时,则分箱完毕。

因此,分箱过程是一个优化过程,所有满足上述要求的指标都可以用于变量分箱,这个指标也可叫作目标函数,可以终止或改变分箱的限制就是优化过程的约束条件

在这里插入图片描述

在这里插入图片描述

变量分箱的流程

在这里插入图片描述

5.卡方分箱

基本思想:自底向上的分箱方法,相邻区间合并计算卡方值,卡方值越小说明两个区间的类分布越相似,合并两个区间

由设定的阈值决定(自由度、置信度),小于阈值就分箱

自底向上:由多至少逐层合并的过程
自顶向下是由少至多逐层切分的过程

1
2
3
4
5
数值特征离散化

特征之间强相关不好,但是某个特征和标签相关是好的

强相关:一个特征可以用另一个特征线性表示

  • 解释性强
  • 能解决多分类场景的分箱
  • 缺点是计算量大
    • 需要先对数值型变量离散化,然后迭代的计算卡方值

公式:
在这里插入图片描述
在这里插入图片描述
上述过程就是一个卡方检验的过程,因此,根据置信度和自由度可以计算出卡方检验的阈值,当计算的卡方值小于阈值,则认为相邻区间的类分布情况相似,可进行合并。其中自由度为类别个数减 1,即本例中的自由度为 1;置信度可以使用 0.9、0.95和 0.99。

6.KS分箱

Best-KS 分箱方法是一种自顶向下的分箱方法。与卡方分箱相比,Best-KS分箱方法只是目标函数采用了 KS 统计量,其余分箱步骤没有差别
注意:KS只能处理连续变量
可以用于模型对好坏样本的区分能力

基本思想:
  根据KS曲线,取TPR和FPR之间的最大差值,就是KS统计率,也就是KS分箱最优切分点的位置.
  
KS曲线:

  • 横轴就是认为设定的阈值,就是区分好坏样本的界限
  • 纵轴:一个是真正率TPR,一个是假正率FPR
    之间的差值一定程度反映模型对好坏样本的区分能力
  • 我们希望真正率高一点,假正率低一点(好样本多一点,坏样本少一点)
  • 真正率:正样本预测数 / 正样本实际数
    • TP /(TP + FN)
  • 假正率:被预测为正的负样本结果数 / 负样本实际数
    • FP /(FP + TN)

KS分箱过程也就是递归的找最优切分点的过程

KS值越大 模型的区分能力越强

在这里插入图片描述

7.混淆矩阵概念复习

在这里插入图片描述

  • 召回率,真正率(recall):TP/(TP+FN)
  • 准确率(accuracy):(TP+TN) / (TP+TN+FP+FN)
    • 预测正确的 / 总样本数
  • 精确率(precision):TP / (TP+FP)
    • 预测为1且正确 / 所有预测为1的样本数

8.最优IV分箱

最优 IV 分箱方法也是自顶向下的分箱方式,其目标函数为 IV 值

IV 值其本质是对称化的 K-L 距离,即在切分点处分裂得到的两部分数据中,选择好坏样本的分布差异最大点作为最优切分点。分箱结束后,计算每个箱内的 IV 值加和得到变量的 IV 值,可以用来刻画变量对目标值的预测能力。即变量的 IV 值越大,则对目标变量的区分能力越强,因此,IV 值还可以用来做变量选择

9.基于树的最优分箱方法

基于树的分箱方法借鉴了决策树 在树生成的过程中特征选择(最优分裂点) 的目标函数来完成变量分箱过程,可以理解为单变量的决策树模型。决策树采用自顶向下递归的方法进行树的生成,每个节点的选择目标是为了分类结果的纯度更高,也就是样本的分类效果更好。因此,不同的损失函数有不同的决策树,ID3采用信息增益方法,C4.5 采用信息增益比,CART 采用基尼系数(Gini)指标。

10.分箱框架源码(卡方、最优IV、信息增益)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# -*- coding: utf-8 -*-
import os
import pandas as pd
import numpy as np
import pickle
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
import warnings

warnings.filterwarnings("ignore")  ##忽略警告


# 注意sklearn版本要在v.20.0以上,不同版本函数的位置会不同。
def data_read(data_path, file_name):
    df = pd.read_csv(os.path.join(data_path, file_name), delim_whitespace=True, header=None, engine='python')
    # 变量重命名
    columns = ['status_account', 'duration', 'credit_history', 'purpose', 'amount',
               'svaing_account', 'present_emp', 'income_rate', 'personal_status',
               'other_debtors', 'residence_info', 'property', 'age',
               'inst_plans', 'housing', 'num_credits',
               'job', 'dependents', 'telephone', 'foreign_worker', 'target']
    df.columns = columns

    # 将标签变量由状态1,2转为0,1;0表示好用户,1表示坏用户
    df.target = df.target - 1

    # 数据分为data_train和 data_test两部分,训练集用于得到编码函数,验证集用已知的编码规则对验证集编码
    # x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
    # stratify: 依据标签y,按原数据y中各类比例,分配给train和test,使得train和test中各类数据的比例与原数据集一样
    data_train, data_test = train_test_split(df, test_size=0.2, random_state=0, stratify=df.target)
    return data_train, data_test


# one—hot编码
# df: 数据框  data_path_1:编码模型保存的位置  flag:数据集
def onehot_encode(df, data_path_1, flag='train'):
    # reset_index:重置索引, drop=True:不想保留原来的index
    df = df.reset_index(drop=True)

    # 判断数据集是否存在缺失值  如果是进行缺失值填补
    # df.isnull().any() 判断哪些列存在缺失值
    if sum(df.isnull().any()) > 0:
        # 数值型和字符串型特征拿出来
        numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        var_numerics = df.select_dtypes(include=numerics).columns
        var_str = [i for i in df.columns if i not in var_numerics]

        # 数据类型的缺失值用-77777填补
        if len(var_numerics) > 0:
            df.loc[:, var_numerics] = df[var_numerics].fillna(-7777)

        # 字符串类型的缺失值用NA填补
        if len(var_str) > 0:
            df.loc[:, var_str] = df[var_str].fillna('NA')

    # pickle.dump(obj, file, [,protocol])  序列化对象,将对象obj保存到文件file中去
    # 参数protocol是序列化模式,默认是0(ASCII协议,表示以文本的形式进行序列化)
    if flag == 'train':
        enc = OneHotEncoder(dtype='int').fit(df)
        # 保存编码模型
        with open(os.path.join(data_path_1, 'onehot.pkl'), 'wb') as save_model:
            pickle.dump(enc, save_model, 0)

        df_return = pd.DataFrame(enc.transform(df).toarray())
        df_return.columns = enc.get_feature_names(df.columns)

    elif flag == 'test':
        # 测试数据编码
        with open(os.path.join(data_path_1, 'onehot.pkl'), 'rb') as read_model:
            onehot_model = pickle.load(read_model)

        # 如果训练集无缺失值,测试集有缺失值则将该样本删除
        var_range = onehot_model.categories_  # 训练集one-hot编码后的类别种类
        var_name = df.columns
        del_index = []
        for i in range(len(var_range)):
            if 'NA' not in var_range[i] and 'NA' in df[var_name[i]].unique():
                index = np.where(df[var_name[i]] == 'NA')
                del_index.append(index)
            elif -7777 not in var_range[i] and -7777 in df[var_name[i]].unique():
                index = np.where(df[var_name[i]] == -7777)
                del_index.append(index)

        # 删除样本
        if len(del_index) > 0:
            del_index = np.unique(del_index)
            df = df.drop(del_index)
            print('训练集无缺失值,但测试集有缺失值,第{0}条样本被删除'.format(del_index))
        df_return = pd.DataFrame(onehot_model.transform(df).toarray())
        df_return.columns = onehot_model.get_feature_names(df.columns)

    elif flag == 'transform':
        # 编码数据值转化为原始变量
        with open(os.path.join(data_path_1, 'onehot.pkl'), 'rb') as read_model:
            onehot_model = pickle.load(read_model)

        # 逆变换
        df_return = pd.DataFrame(onehot_model.inverse_transform(df))
        df_return.columns = np.unique(['_'.join(i.rsplit('_')[:-1]) for i in df.columns])

    return df_return


# 标签编码
def label_encode(df, data_path_1, flag='train'):
    if flag == 'train':
        enc = LabelEncoder().fit(df)
        # 保存编码模型
        with open(os.path.join(data_path_1, 'labelcode.pkl'), 'wb') as save_model:
            pickle.dump(enc, save_model, 0)

        df_return = pd.DataFrame(enc.transform(df))
        df_return.name = df.name

    elif flag == 'test':
        # 测试数据编码
        with open(os.path.join(data_path_1, 'labelcode.pkl'), 'rb') as read_model:
            label_model = pickle.load(read_model)

        df_return = pd.DataFrame(label_model.transform(df))
        df_return.name = df.name

    elif flag == 'transform':
        # 编码数据值转化为原始变量
        with open(os.path.join(data_path_1, 'labelcode.pkl'), 'rb') as read_model:
            label_model = pickle.load(read_model)

        # 逆变换
        df_return = pd.DataFrame(label_model.inverse_transform(df))
    return df_return


# 自定义映射
def dict_encode(df, data_path_1):
    # 自定义映射
    embarked_mapping = {}
    embarked_mapping['status_account'] = {'NA': 1, 'A14': 2, 'A11': 3, 'A12': 4, 'A13': 5}
    embarked_mapping['svaing_account'] = {'NA': 1, 'A65': 1, 'A61': 3, 'A62': 5, 'A63': 6, 'A64': 8}
    embarked_mapping['present_emp'] = {'NA': 1, 'A71': 2, 'A72': 5, 'A73': 6, 'A74': 8, 'A75': 10}
    embarked_mapping['property'] = {'NA': 1, 'A124': 1, 'A123': 4, 'A122': 6, 'A121': 9}

    df = df.reset_index(drop=True)

    # 判断数据集是否存在缺失值
    if sum(df.isnull().any()) > 0:
        df = df.fillna('NA')

    # 字典映射
    var_dictEncode = []
    for i in df.columns:
        col = i + '_dictEncode'
        df[col] = df[i].map(embarked_mapping[i])
        var_dictEncode.append(col)
    return df[var_dictEncode]


# WOE编码
# 返回某个特征的woe映射后的df、woe字典、iv值
# x:特征   y:类别   target:正样本为1
def woe_cal_trans(x, y, target=1):
    # 计算总体的正负样本数
    p_total = sum(y == target)  # 正样本数
    n_total = len(x) - p_total  # 负样本数
    value_num = list(x.unique())  # 去重后的总数
    woe_map = {}
    iv_value = 0
    for i in value_num:  # 这个特征每种取值的woe值
        # 计算该变量取值箱内的正负样本总数
        y1 = y[np.where(x == i)[0]]
        p_num_1 = sum(y1 == target)
        n_num_1 = len(y1) - p_num_1
        # 计算占比
        # bad_1 = p_num_1 / p_total  # 坏样本分布率
        # good_1 = n_num_1 / n_total  # 好样本分布率
        good_1 = p_num_1 / p_total  # 坏样本分布率
        bad_1 = n_num_1 / n_total  # 好样本分布率
        if bad_1 == 0:
            bad_1 = 1e-5
        elif good_1 == 0:
            good_1 = 1e-5
        woe_map[i] = np.log(bad_1 / good_1)  # woe值
        iv_value += (bad_1 - good_1) * woe_map[i]  # iv值
    x_woe_trans = x.map(woe_map)
    x_woe_trans.name = x.name + "_woe"

    return x_woe_trans, woe_map, iv_value


# WOE编码映射
def woe_encode(df, data_path_1, varnames, y, filename, flag='train'):
    """
    Param:
    df: 待编码数据
    data_path_1 :存取文件路径
    varnames: 变量列表
    y:  目标变量
    filename:编码存取的文件名
    flag: 选择训练还是测试
    ---------------------------------------
    Return:
    df: 编码后的数据,包含了原始数据
    woe_maps: 编码字典
    iv_values: 每个变量的IV值
    var_woe_name: 每个特征拼接woe的列名
    """
    df = df.reset_index(drop=True)  # 重置索引,不保留原来的索引

    # 判断数据集是否存在缺失值
    if sum(df.isnull().any()) > 0:
        numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        var_numerics = df.select_dtypes(include=numerics).columns  # 数值型特征
        var_str = [i for i in df.columns if i not in var_numerics]  # 字符串型特征
        # 数据类型的缺失值用-77777填补
        if len(var_numerics) > 0:
            df.loc[:, var_numerics] = df[var_numerics].fillna(-7777)
        # 字符串类型的缺失值用NA填补
        if len(var_str) > 0:
            df.loc[:, var_str] = df[var_str].fillna('NA')

    if flag == 'train':
        iv_values = {}  # 保存每个特征的iv值
        woe_maps = {}  # 保存每个特征的woe值
        var_woe_name = []
        for var in varnames:  # 遍历每一个特征
            x = df[var]
            # 变量映射
            x_woe_trans, woe_map, info_value = woe_cal_trans(x, y)
            var_woe_name.append(x_woe_trans.name)
            df = pd.concat([df, x_woe_trans], axis=1)  # 按行拼接woe值
            woe_maps[var] = woe_map
            iv_values[var] = info_value

        # 保存woe映射字典
        with open(os.path.join(data_path_1, filename + '.pkl'), 'wb') as save_woe_dict:
            pickle.dump(woe_maps, save_woe_dict, 0)

        return df, woe_maps, iv_values, var_woe_name

    elif flag == 'test':
        # 测试数据编码
        with open(os.path.join(data_path_1, filename + '.pkl'), 'rb') as read_woe_dict:
            woe_dict = pickle.load(read_woe_dict)

        # 如果训练集无缺失值,测试集有缺失值则将该样本删除
        woe_dict.keys()
        del_index = []
        for key, value in woe_dict.items():
            if 'NA' not in value.keys() and 'NA' in df[key].unique():
                index = np.where(df[key] == 'NA')
                del_index.append(index)
            elif -7777 not in value.keys() and -7777 in df[key].unique():
                index = np.where(df[key] == -7777)
                del_index.append(index)
        # 删除样本
        if len(del_index) > 0:
            del_index = np.unique(del_index)
            df = df.drop(del_index)
            print('训练集无缺失值,但测试集有缺失值,该样本{0}删除'.format(del_index))

        # WOE编码映射
        var_woe_name = []
        for key, value in woe_dict.items():
            val_name = key + "_woe"
            df[val_name] = df[key].map(value)
            var_woe_name.append(val_name)

        return df, var_woe_name


if __name__ == '__main__':
    path = r'G:\A_实训前置1\python_workspace\finance_code\chapter5\'
    data_path = os.path.join(path, 'data')
    file_name = 'german.csv'
    # 读取数据
    data_train, data_test = data_read(data_path, file_name)
    # 不可排序变量
    var_no_order = ['credit_history', 'purpose', 'personal_status', 'other_debtors',
                    'inst_plans', 'housing', 'job', 'telephone', 'foreign_worker']

    # x_woe_trans, woe_map, iv_value = woe_cal_trans(data_train['job'], data_test['target'])
    # print(x_woe_trans)
    # print(woe_map)
    # print(iv_value)

    # one-hot编码
    # 训练数据编码
    data_train.credit_history[882] = np.nan
    data_train_encode = onehot_encode(data_train[var_no_order], data_path, flag='train')

    # 测试集数据编码
    data_test.credit_history[529] = np.nan
    data_test.purpose[355] = np.nan
    data_test_encode = onehot_encode(data_test[var_no_order], data_path, flag='test')

    # 查看编码逆变化后的原始变量名
    df_encoded = data_test_encode.loc[0:4]
    data_inverse = onehot_encode(df_encoded, data_path, flag='transform')
    print(data_inverse)

    # 哑变量编码
    data_train_dummies = pd.get_dummies(data_train[var_no_order])
    data_test_dummies = pd.get_dummies(data_test[var_no_order])
    print(data_train_dummies.columns)

    # 可排序变量
    # 注意,如果分类变量的标签为字符串,这是需要将字符串数值化才可以进行模型训练,标签编码其本质是为
    # 标签变量数值化而提出的方法,因此,其值支持单列数据的转化操作,并且转化后的结果是无序的。
    # 因此有序变量统一用字典映射的方式完成。
    var_order = ['status_account', 'svaing_account', 'present_emp', 'property']

    # 标签编码
    # 训练数据编码
    data_train_encode = label_encode(data_train[var_order[1]], data_path, flag='train')

    # 验证集数据编码
    data_test_encode = label_encode(data_test[var_order[1]], data_path, flag='test')

    # 查看编码你变化后的原始变量名
    # 后面再改一下
    df_encoded = data_test_encode
    data_inverse = label_encode(df_encoded, data_path, flag='transform')

    # 自定义映射
    # 训练数据编码
    data_train.credit_history[882] = np.nan
    data_train_encode = dict_encode(data_train[var_order], data_path)

    # 测试集数据编码
    data_test.status_account[529] = np.nan
    data_test_encode = dict_encode(data_test[var_order], data_path)
    print(data_test_encode)

    # WOE编码
    # 训练集WOE编码
    df_train_woe, dict_woe_map, dict_iv_values, var_woe_name = woe_encode(data_train, data_path, var_no_order,
                                                                          data_train.target, 'dict_woe_map',
                                                                          flag='train')
    print(df_train_woe, '\n')
    print(dict_woe_map, '\n')
    print(dict_iv_values, '\n')
    print(var_woe_name, '\n')

    # 测试集WOE编码
    df_test_woe, var_woe_name = woe_encode(data_test, data_path, var_no_order, data_train.target, 'dict_woe_map',
                                           flag='test')

    print(df_train_woe)