今天还是讲一下金融风控的相关知识,上一次我们有讲到,如果我们需要计算变量的IV值,从而判断变量的预测能力强弱,是需要对变量进行离散化的,也就是分箱处理。那么,今天就来给大家解释一下其中一种分箱方式 —— 卡方分箱处理。
了解卡方分箱,首先需要了解下卡方分布。卡方分布(chi-square distribution, χ2-distribution)是概率统计里常用的一种概率分布,也是统计推断里应用最广泛的概率分布之一,在假设检验与置信区间的计算中经常能见到卡方分布的身影,其定义如下:
若k个相互独立的随机变量, , , ..., 满足标准正态分布 N(0,1),那么这k个随机变量的平方和 就是服从自由度为k的卡方分布了,一般记作:~
其概率密度函数如下所示(图片来自百度百科):
卡方检测是以卡方分布为基础的一种假设检验方法,主要是用于检验分类变量之间的独立性情况。它的基本思想就是根据样本数据推断总体分布与期望分布之间是否存在显著性差异,或者说两个分类变量之间是否相互独立(or是否相关)。
一般的情况下我们会把原假设设置为:观察频数与期望频数之间没有差异,也就是说两个分类变量之间是相互独立不相关的。
实际的应用中我们假设原假设成立,然后计算出卡方值,从而来决策是否需要拒绝原假设,卡方值的计算公式如下:
其中,A为实际频数,E为期望频数,卡方值就是计算实际与期望之间的差异程度大小的量化指标。上面公式结果服从卡方分布,然后我们根据卡方分布、卡方统计量以及自由度,就可以查出p值,如果p值很小,代表观察值与期望值偏离程度很大,那么就需要拒绝原假设,也就是说两个分类变量之间有相关性。
这个概念貌似在大一的时候就有接触过了,可以知道横轴是分位数,纵轴是自由度,然后类似于Python的loc方法,定位到的值就是卡方值了。(比如,要找分位数位0.9,自由度为8的值,查表可知为3.489539
如果想要在Python里生成卡方分布表,可以尝试下面的代码:
# 用Python生成卡方分布临界值表
import numpy as np
import pandas as pd
from scipy.stats import chi2
# chi square distribution
percents = [0.995, 0.990, 0.975, 0.950, 0.900, 0.100, 0.050, 0.025, 0.010, 0.005]
df = pd.DataFrame(np.array([chi2.isf(percents, df=i) for i in range(1, 10)]), columns=percents, index=list(range(1,10)))
df
我们有一组数据,是某种病的患者使用了A和B两种不同方案的治疗,所得到的治疗结果,如下表所示,问A、B两种疗法是否有明显差异?
组别 | 有效 | 无效 | 合计 | 有效率% |
---|---|---|---|---|
A组 | 19 | 24 | 43 | 44.2% |
B组 | 34 | 10 | 44 | 77.3% |
合计 | 53 | 34 | 87 | 60.9% |
这道题其实就是套公式,从上面我了解到要计算卡方值可以有这个公式:
所以,我们先算出每个格子的期望频数:
示例1:A组有效的期望频数:43*53/87=26.20
期望频数 | 有效 | 无效 | 合计 | 有效率% |
---|---|---|---|---|
A组 | 26.20 | 16.80 | 43 | 60.9% |
B组 | 26.80 | 17.20 | 44 | 60.9% |
合计 | 53 | 34 | 87 | 60.9% |
先建立原假设:A、B两种疗法没有区别。
然后就套入上面的公式:(A为实际频数,E为期望频数)
因为我们选择了其中一个方案,另外一个方案就明确了,所以自由度是1,因此可以查表,自由度为1的,而且卡方值为10.01的分位数是多少了~
查表自由度为1,p=0.05的卡方值为3.841,而此例卡方值10.01>3.841,因此 p < 0.05,说明原假设在0.05的显著性水平下是可以拒绝的,也就是说原假设不成立,也就是两种治疗方案有区别。
上面讲了这么多基础的知识,就是为了引出这一节的内容—— ChiMerge分箱算法。
ChiMerge卡方分箱算法由Kerber于1992提出。它主要包括两个阶段:初始化阶段和自底向上的合并阶段。
首先按照属性值的大小进行排序(对于非连续特征,需要先做数值转换,比如转为坏人率,然后排序),然后每个属性值单独作为一组。
(1)对每一对相邻的组,计算卡方值。
(2)根据计算的卡方值,对其中最小的一对邻组合并为一组。
(3)不断重复(1)和(2)直到计算出的卡方值都不低于事先设定的阈值,或者分组数达到一定的条件(如最小分组数5,最大分组数8)。
值得注意的是,阿Sam之前发现有的实现方法在合并阶段,计算的并非相邻组的卡方值(只考虑在此两组内的样本,并计算期望频数),因为他们用整体样本来计算此相邻两组的期望频数。
了解了原理之后,那么Python如何实现呢?请看下面的代码:
import numpy as np
from scipy.stats import chi
import pandas as pd
from pandas import DataFrame,Series
import scipy
def chi3(arr):
'''
计算卡方值
arr:频数统计表,二维numpy数组。
'''
assert(arr.ndim==2)
#计算每行总频数
R_N = arr.sum(axis=1)
#每列总频数
C_N = arr.sum(axis=0)
#总频数
N = arr.sum()
# 计算期望频数 C_i * R_j / N。
E = np.ones(arr.shape)* C_N / N
E = (E.T * R_N).T
square = (arr-E)**2 / E
#期望频数为0时,做除数没有意义,不计入卡方值
square[E==0] = 0
#卡方值
v = square.sum()
return v
def chiMerge(df,col,target,max_groups=None,threshold=None):
'''
卡方分箱
df: pandas dataframe数据集
col: 需要分箱的变量名(数值型)
target: 类标签
max_groups: 最大分组数。
threshold: 卡方阈值,如果未指定max_groups,默认使用置信度95%设置threshold。
return: 包括各组的起始值的列表.
'''
freq_tab = pd.crosstab(df[col],df[target])
#转成numpy数组用于计算。
freq = freq_tab.values
#初始分组切分点,每个变量值都是切分点。每组中只包含一个变量值.
#分组区间是左闭右开的,如cutoffs = [1,2,3],则表示区间 [1,2) , [2,3) ,[3,3+)。
cutoffs = freq_tab.index.values
#如果没有指定最大分组
if max_groups is None:
#如果没有指定卡方阈值,就以95%的置信度(自由度为类数目-1)设定阈值。
if threshold is None:
#类数目
cls_num = freq.shape[-1]
threshold = chi2.isf(0.05,df= cls_num - 1)
while True:
minvalue = None
minidx = None
#从第1组开始,依次取两组计算卡方值,并判断是否小于当前最小的卡方
for i in range(len(freq) - 1):
v = chi3(freq[i:i+2])
if minvalue is None or (minvalue > v): #小于当前最小卡方,更新最小值
minvalue = v
minidx = i
#如果最小卡方值小于阈值,则合并最小卡方值的相邻两组,并继续循环
if (max_groups is not None and max_groupsor (threshold is not None and minvalue #minidx后一行合并到minidx
tmp = freq[minidx] + freq[minidx+1]
freq[minidx] = tmp
#删除minidx后一行
freq = np.delete(freq,minidx+1,0)
#删除对应的切分点
cutoffs = np.delete(cutoffs,minidx+1,0)
else: #最小卡方值不小于阈值,停止合并。
break
return cutoffs
def value2group(x,cutoffs):
'''
将变量的值转换成相应的组。
x: 需要转换到分组的值
cutoffs: 各组的起始值。
return: x对应的组,如group1。从group1开始。
'''
#切分点从小到大排序。
cutoffs = sorted(cutoffs)
num_groups = len(cutoffs)
#异常情况:小于第一组的起始值。这里直接放到第一组。
#异常值建议在分组之前先处理妥善。
if x 0]:
return 'group1'
for i in range(1,num_groups):
if cutoffs[i-1] <= x return 'group{}'.format(i)
#最后一组,也可能会包括一些非常大的异常值。
return 'group{}'.format(num_groups)
def calWOE(df ,var ,target):
'''
计算WOE编码
param df:数据集pandas.dataframe
param var:已分组的列名,无缺失值
param target:响应变量(0,1)
return:编码字典
'''
eps = 0.000001 #避免除以0
gbi = pd.crosstab(df[var],df[target]) + eps
gb = df[target].value_counts() + eps
gbri = gbi/gb
gbri['woe'] = np.log(gbri[1]/gbri[0])
return gbri['woe'].to_dict()
def calIV(df,var,target):
'''
计算IV值
param df:数据集pandas.dataframe
param var:已分组的列名,无缺失值
param target:响应变量(0,1)
return:IV值
'''
eps = 0.000001 #避免除以0
gbi = pd.crosstab(df[var],df[target]) + eps
gb = df[target].value_counts() + eps
gbri = gbi/gb
gbri['woe'] = np.log(gbri[1]/gbri[0])
gbri['iv'] = (gbri[1] - gbri[0])*gbri['woe']
return gbri['iv'].sum()
cutoffs = chiMerge(data,'MAX_AMOUNT','target',max_groups=8)
'''
PS:绝大数情况下,会将缺失值NaN归类到最后一组,如果不想这么简单粗暴的,需要在最开始的时候对缺失值进行填充。
'''
data['MAX_AMOUNT_chigroup'] = data['MAX_AMOUNT'].apply(value2group,args=(cutoffs,))
r = data.loc[:,['MAX_、AMOUNT','MAX_AMOUNT_chigroup','target']]
woe_map = calWOE(data,'MAX_AMOUNT_chigroup','target')
iv = calIV(data,'MAX_AMOUNT_chigroup','target')
iv
[1] Python评分卡建模—卡方分箱(1)
[2] Python评分卡建模—卡方分箱(2)之代码实现
[3] python评分卡建模—实现WOE编码及IV值计算
往 期 锦 囊
? GitHub传送门
https://github.com/Pysamlam/Tips-of-Feature-engineering
原创不易,如果觉得这种学习方式有用,希望可以帮忙随手转发or点下“在看”,这是对我的极大鼓励!阿里嘎多!?