风控建模二、特征工程---风控

本节主要将风控中比较常见的特征工程。

目录

目录

一、分箱

1.1 Best-KS

1.2 卡方分箱法(ChiMerge)

二、WOE和IV

2.1 两种woe处理分类问题。

三、共线性

  3.1 相关系数 COR: 

 3.2 方差膨胀系数 VIF

四、PSI

参考文献



一、分箱

分箱是将连续变量离散化,将多状态的离散变量合并成少状态。

分箱的重要性:避免特征中无意义的波动对评分带来的波动(稳定性),避免极端值的影响(健壮性)。

分箱的优势:可以将缺失作为独立的一个箱带入模型中,将所有的变量变换到相似的尺度。

分箱的限制:计算量大,分箱后需要编码。

分箱常用方法:

有监督: Best-KS, ChiMerge

无监督:等频,等距,聚类。

1.1 Best-KS

原理:让分箱后组别的分布的差异最大化。

对于连续变量

   1. 排序 x={x1,x2,x3,x4...}

   2. 计算每一点的KS值。

   3. 选取最大的KS对应的特征值xm,将x分为{xi <= xm}与{xi > xm}两步,对于每一个部分,重复2-3,直到满足终止条件之一。

终止条件:

    1. 下一步分箱后,最小的箱的占比低于设定的阈值。

    2.下一步分箱后,该箱对应的y类别全部为0或1

    3. 下一步分箱后,bad rate 不单调。

对于离散度很高的变量

   1.编码

   2.依据连续变量的方式进行分享。

 在风控中,KS常用于评估模型区分度。区分度越大,说明模型的风险排序能力(ranking ability)越强。KS统计量是基于经验累积分布函数(Empirical Cumulative Distribution Function,ECDF)
建立的,一般定义为:

Ks = max\{\left | cum(bad\_rate) - cum(good\_rate)\right |\}

cum是累加函数。

import math

def sloveKS(self, model, X, Y, Weight):  
    Y_predict = [s[1] for s in model.predict_proba(X)]  
    nrows = X.shape[0]  
    #还原权重  
    lis = [(Y_predict[i], Y.values[i], Weight[i]) for i in range(nrows)]
    #按照预测概率倒序排列  
    ks_lis = sorted(lis, key=lambda x: x[0], reverse=True)        
    KS = list()  
    bad = sum([w for (p, y, w) in ks_lis if y > 0.5])  
    good = sum([w for (p, y, w) in ks_lis if y <= 0.5])  
    bad_cnt, good_cnt = 0, 0  
    for (p, y, w) in ks_lis:  
        if y > 0.5:  
            #1*w 即加权样本个数  
            bad_cnt += w                
        else:  
            #1*w 即加权样本个数  
            good_cnt += w               
        ks = math.fabs((bad_cnt/bad)-(good_cnt/good))  
        KS.append(ks)  
    return max(KS) 

Ks值对模型的评价不受正负样本不均衡问题的干扰,但仅限于模型评价。

1.2 卡方分箱法(ChiMerge)

自底向上(基于合并)的数据离散化方法。依赖于卡方检验,将最小卡方值得相邻区间进行合并,直到满足停止准则。

与Best-Ks相比,ChiMerge可以应用于Multi-Class的情况。

ChiMerge法采取自底向上不断合并的方法完成分箱操作。

在每一步的合并过程中,依靠最小的卡方值来寻找最优的合并项。

其核心思想是,如果某两个区间可以被合并,那么这两个区间的坏样本需要有最接近的分布,进而意味着两个区间的卡方值是最小的。

于是ChiMerge的步骤如下:

1. 将数值变量A排序后分成区间较多的若干组,设为A_1,A_2,…,A_n

2. 计算A_1与A_2合并后的卡方值,A_2与A_3合并后的卡方值,直至A_n−1与A_n合并后的卡方值

3. 找出上一步所有合并后的卡方值中最小的一个,假设为A_i−1与A_i,将其合并形成新的A_i−1

4. 不断重复2和3,直至满足终止条件

通用的ChiMerge的终止条件是:

1. 某次合并后,最小的卡方值的p值超过0.9(或0.95,0.99等),

2. 某侧合并后,总的未合并的区间数达到指定的数目(例如5,10,15等)   

\chi ^{2}=\sum_{i=1}^{m}\sum_{j=1}^{k}\frac{\left ( A_{ij}-B_{ij} \right )^{2}}{E_{ij}}

m:该因素取值个数;      k:类别数

A_ij:因素i组中,k类别的观察频数

E_ij:原假设下A_ij的期望。

当样本总量比较大时,χ2统计量近似服从(m-1)(k-1)个自由度的卡方分布。

例子:

分箱注意点

1. 连续变量(箱数5~10之间)

    分箱后bad_rate不满足单调性,仍需要合并。直到bad_rate满足单调性,箱数最少为2.

2. 类别性变量 

    类别较少,则不需要分箱。

    有几个类bad为0,需要合并。

3. ChiMerge算法,建议使用0.90,0.95,0.99置信度,最大区间数10~15之间。

二、WOE和IV

\begin{aligned} & P_{good_{i}} = \frac{good_{i}}{good_{T}} \\ & P_{bad_{i}} = \frac{bad_{i}}{bad_{T}} \\ & Woe_{i} = ln(\frac{P_{good_{i}}}{P_{bad_{i}}}) = ln(\frac{bad_{i}}{good_{i}}-\frac{bad_{T}}{good_{T}})\\ & IV_{i} = (P_{good_{i}} - P_{bad_{i}}) \times Woe_{i} \\ & IV = \sum_{i=1}^{n}IV_{i} \end{aligned}

p_{good_{i}}是这个组中正例样本占整个样本中正例样本的比例,p_{bad_{i}}是这个组中负例样本占整个样本中负例样本的比例,good_{i}是这个组中正例样本的数量,bad_{i}是这个组中负例样本的数量,good_{T}是整个样本中所有正例样本的数量,bad_{T}是整个样本中所有负例样本的数量。

    WOE(Weight of Evidence)常用于特征变换,IV(Information Value)则用来衡量特征的预测能力。

   WOE可以这么理解,表示的是当前这个组中响应的客户和未响应客户的比值,和所有样本中这个比值的差异。每个分箱里的坏好比(Odds)相对于总体的坏好比之间的差异性。WOE越大,这种差异越大,这个分组里的样本响应的可能性就越大(这组数据分到正例的概率越大),WOE越小,差异越小,这个分组里的样本响应的可能性就越小。

    IV:  在筛选变量的时候, 我们需要一些具体的量化指标来衡量每自变量的预测能力,并根据这些量化指标的大小,来确定哪些变量进入模型。IV就是这样一种指标,他可以用来衡量自变量(特征)的预测能力

  • 我们假设在一个分类问题中,目标变量的类别有两类:Y1,Y2。对于一个待预测的样本A,要判断A属于Y1还是Y2,我们是需要一定的信息,假设这个信息总量是I,而这些所需要的信息,就蕴含在所有的待预测样本的特征C1,C2,C3,……,Cn中,那么,对于其中的一个特征Ci来说,其蕴含的信息越多,那么它对于判断A属于Y1还是Y2的贡献就越大,Ci的信息价值就越大,Ci的IV就越大,它就越应该进入到入模变量列表中。

下面例子是利用决策树的信息增益最大化思想来实现变量的最优分箱。

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

data = pd.read_csv('woe_data/cs-training.csv')
print(data.shape)
data.head()

def optimal_binning_boundary(x: pd.Series, y: pd.Series, nan: float = -999.) -> list:
    '''
        利用决策树获得最优分箱的边界值列表
    '''
    boundary = []  # 待return的分箱边界值列表
    
    x = x.fillna(nan).values  # 填充缺失值
    y = y.values
    
    clf = DecisionTreeClassifier(criterion='entropy',    #“信息熵”最小化准则划分
                                 max_leaf_nodes=6,       # 最大叶子节点数
                                 min_samples_leaf=0.05)  # 叶子节点样本数量最小占比

    clf.fit(x.reshape(-1, 1), y)  # 训练决策树
    
    n_nodes = clf.tree_.node_count
    children_left = clf.tree_.children_left
    children_right = clf.tree_.children_right
    threshold = clf.tree_.threshold
    print(children_left)
    print(children_right)
    print(threshold)
    for i in range(n_nodes):
        if children_left[i] != children_right[i]:  # 获得决策树节点上的划分边界值
            boundary.append(threshold[i])

    boundary.sort()

    min_x = x.min()
    max_x = x.max() + 0.1  # +0.1是为了考虑后续groupby操作时,能包含特征最大值的样本
    boundary = [min_x] + boundary + [max_x]

    return boundary

optimal_binning_boundary(x=data['RevolvingUtilizationOfUnsecuredLines'],
                         y=data['SeriousDlqin2yrs'])

def feature_woe_iv(x: pd.Series, y: pd.Series, nan: float = -999.) -> pd.DataFrame:
    '''
        计算变量各个分箱的WOE、IV值,返回一个DataFrame
    '''
    x = x.fillna(nan)
    boundary = optimal_binning_boundary(x, y, nan)        # 获得最优分箱边界值列表
    df = pd.concat([x, y], axis=1)                        # 合并x、y为一个DataFrame,方便后续计算
    df.columns = ['x', 'y']                               # 特征变量、目标变量字段的重命名
    df['bins'] = pd.cut(x=x, bins=boundary, right=False)  # 获得每个x值所在的分箱区间
    
    grouped = df.groupby('bins')['y']                     # 统计各分箱区间的好、坏、总客户数量
    result_df = grouped.agg([('good',  lambda y: (y == 0).sum()), 
                             ('bad',   lambda y: (y == 1).sum()),
                             ('total', 'count')])

    result_df['good_pct'] = result_df['good'] / result_df['good'].sum()       # 好客户占比
    result_df['bad_pct'] = result_df['bad'] / result_df['bad'].sum()          # 坏客户占比
    result_df['total_pct'] = result_df['total'] / result_df['total'].sum()    # 总客户占比

    result_df['bad_rate'] = result_df['bad'] / result_df['total']             # 坏比率
    
    result_df['woe'] = np.log(result_df['good_pct'] / result_df['bad_pct'])              # WOE
    result_df['iv'] = (result_df['good_pct'] - result_df['bad_pct']) * result_df['woe']  # IV
    
    print(f"该变量IV = {result_df['iv'].sum()}")
    
    return result_df

feature_woe_iv(x=data['RevolvingUtilizationOfUnsecuredLines'], 
               y=data['SeriousDlqin2yrs'])

     变量RevolvingUtilizationOfUnsecuredLines,分箱WOE趋势单调,bad_rate风险排序性较好,IV值>1.0则说明该变量预测能力很强。 

2.1 两种woe处理分类问题。

一、分箱的原则是负样本占比差异最大化。通常分箱的总数一般在5-10箱,每一箱之间的负样本占比差值应该尽可能大。

分箱具体分三个步骤

(1)先按照每个类型的badrate大小,从小到大排序;

(2)计算与后一箱的badrate差异,将差异最小的合并;

(3)如果某一箱的数量占比不超过5%,或者总箱数超过10箱,就需要继续合并,如果是第一箱或者最后一箱不满足,就与相邻的合并;如果是中间的箱不满足,就与badrate差异最小的合并。

#!/usr/bin/env python
# coding: utf-8

# In[5]:


# 数据来源: https://www.kaggle.com/c/home-credit-default-risk/data.

# In[2]:


import pandas as pd
df_cat = pd.read_csv("./application_train.csv",
                     engine='c')

# In[4]:


variable_cat = "NAME_INCOME_TYPE"
x_cat = df_cat[variable_cat].values
y_cat = df_cat.TARGET.values

# In[5]:


df_cat[variable_cat].value_counts()

# In[9]:


df_cat.shape[0]*0.8

# In[15]:


data_dict = {}
data_dict['dev'] = df_cat.iloc[:184506,:][['NAME_INCOME_TYPE','TARGET']]
data_dict['val'] = df_cat.iloc[184506:246008,:][['NAME_INCOME_TYPE','TARGET']]
data_dict['off'] = df_cat.iloc[246008:,:][['NAME_INCOME_TYPE','TARGET']]

# In[27]:


import math  
#离散型变量 WOE编码  
class charWoe(object):  
    def __init__(self, datasets, dep, weight, vars):  
                #数据集字典,{'dev':训练集,'val':测试集,'off':跨时间验证集}  
        self.datasets = datasets 
        self.devf = datasets.get("dev", "") #训练集  
        self.valf = datasets.get("val", "") #测试集  
        self.offf = datasets.get("off", "") #跨时间验证集  
        self.dep = dep #标签  
        self.weight = weight #样本权重  
        self.vars = vars #参与建模的特征名  
        self.nrows, self.ncols = self.devf.shape #样本数,特征数  
  
    def char_woe(self):  
        #得到每一类样本的个数,且加入平滑项使得bad和good都不为0  
        dic = dict(self.devf.groupby([self.dep]).size())  
        good  = dic.get(0, 0) + 1e-10
        bad =  dic.get(1, 0) + 1e-10  
        #对每一个特征进行遍历。  
        for col in self.vars:  
            #得到每一个特征值对应的样本数。  
            data = dict(self.devf[[col, self.dep]].groupby(
                                                  [col, self.dep]).size())  
            ''' 
            当前特征取值超过100个的时候,跳过当前取值。 
            因为取值过多时,WOE分箱的效率较低,建议对特征进行截断。 
            出现频率过低的特征值统一赋值,放入同一箱内。 
            '''  
            if len(data) > 100:  
                print(col, "contains too many different values...")
                continue  
            #打印取值个数  
            print(col, len(data))  
            dic = dict()  
            #k是特征名和特征取值的组合,v是样本数  
            for (k, v) in data.items():  
                #value为特征名,dp为特征取值  
                value, dp = k  
                #如果找不到key设置为一个空字典  
                dic.setdefault(value, {})   
                #字典中嵌套字典  
                dic[value][int(dp)] = v  
            for (k, v) in dic.items():  
                dic[k] = {str(int(k1)): v1 for (k1, v1) in v.items()}  
                dic[k]["cnt"] = sum(v.values())  
                bad_rate = round(v.get("1", 0)/ dic[k]["cnt"], 5)  
                dic[k]["bad_rate"] = bad_rate  
            #利用定义的函数进行合并。  
            dic = self.combine_box_char(dic)  
            #对每个特征计算WOE值和IV值  
            for (k, v) in dic.items():  
                a = v.get("0", 1) / good + 1e-10  
                b = v.get("1", 1) / bad + 1e-10  
                dic[k]["Good"] = v.get("0", 0)  
                dic[k]["Bad"] = v.get("1", 0)  
                dic[k]["woe"] = round(math.log(a / b), 5)
                dic[k]["iv"] = (a - b) * dic[k]["woe"]
            ''' 
            按照分箱后的点进行分割, 
            计算得到每一个特征值的WOE值, 
            将原始特征名加上'_woe'后缀,并赋予WOE值。 
            '''  
            for (klis, v) in dic.items():  
                for k in klis.split(","):  
                    #训练集进行替换  
                    self.devf.loc[self.devf[col]==k,
                                                    "%s_woe" % col] = v["woe"]
                    #测试集进行替换  
                    if not isinstance(self.valf, str):  
                        self.valf.loc[self.valf[col]==k,
                                                     "%s_woe" % col] = v["woe"]
                    #跨时间验证集进行替换  
                    if not isinstance(self.offf, str):  
                        self.offf.loc[self.offf[col]==k,                     
                                                     "%s_woe" % col] = v["woe"]
        #返回新的字典,其中包含三个数据集。  
        return {"dev": self.devf, "val": self.valf, "off": self.offf},dic
  
    def combine_box_char(self, dic):  
        ''' 
        实施两种分箱策略。 
        1.不同箱之间负样本占比差异最大化。 
        2.每一箱的样本量不能过少。 
        '''  
        #首先合并至10箱以内。按照每一箱负样本占比差异最大化原则进行分箱。  
        while len(dic) >= 10:  
            #k是特征值,v["bad_rate"]是特征值对应的负样本占比
            bad_rate_dic = {k: v["bad_rate"] 
                                             for (k, v) in dic.items()}  
            #按照负样本占比排序。因为离散型变量 是无序的,
                        #可以直接写成负样本占比递增的形式。  
            bad_rate_sorted = sorted(bad_rate_dic.items(),
                                                         key=lambda x: x[1])
            #计算每两箱之间的负样本占比差值。
                        #准备将差值最小的两箱进行合并。  
            bad_rate = [bad_rate_sorted[i+1][1]-
                                      bad_rate_sorted[i][1] for i in 
                                      range(len(bad_rate_sorted)-1)]
            min_rate_index = bad_rate.index(min(bad_rate))  
            #k1和k2是差值最小的两箱的key.  
            k1, k2 = bad_rate_sorted[min_rate_index][0],\
                                     bad_rate_sorted[min_rate_index+1][0]  
            #得到重新划分后的字典,箱的个数比之前少一。  
            dic["%s,%s" % (k1, k2)] = dict()  
            dic["%s,%s" % (k1, k2)]["0"] = dic[k1].get("0", 0)\
                                                            + dic[k2].get("0", 0)
            dic["%s,%s" % (k1, k2)]["1"] = dic[k1].get("1", 0) \
                                                            + dic[k2].get("1", 0)
            dic["%s,%s" % (k1, k2)]["cnt"] = dic[k1]["cnt"]\
                                                              + dic[k2]["cnt"]
            dic["%s,%s" % (k1, k2)]["bad_rate"] = round(
                                    dic["%s,%s" % (k1, k2)]["1"] / 
                                    dic["%s,%s" % (k1, k2)]["cnt"],5)  
            #删除旧的key。  
            del dic[k1], dic[k2]  
        ''' 
        结束循环后,箱的个数应该少于10。 
        下面实施第二种分箱策略。 
        将样本数量少的箱合并至其他箱中,以保证每一箱的样本数量不要太少。 
        '''  
        #记录当前样本最少的箱的个数。      
        min_cnt = min([v["cnt"] for v in dic.values()])  
        #当样本数量小于总样本的5%或者总箱的个数大于5的时候,对箱进行合并  
        while min_cnt < self.nrows * 0.05 and len(dic) > 5:  
            min_key = [k for (k, v) in dic.items() 
                                     if v["cnt"] == min_cnt][0]  
            bad_rate_dic = {k: v["bad_rate"] 
                                          for (k, v) in dic.items()}  
            bad_rate_sorted = sorted(bad_rate_dic.items(),
                                              key=lambda x: x[1])  
            keys = [k[0] for k in bad_rate_sorted]  
            min_index = keys.index(min_key)  
            ''''' 
            同样想保持合并后箱之间的负样本占比差异最大化。 
            由于箱的位置不同,按照三种不同情况进行分类讨论。 
            '''  
            #如果是第一箱,和第二项合并  
            if min_index == 0:  
                k1, k2 = keys[:2]  
            #如果是最后一箱,和倒数第二箱合并  
            elif min_index == len(dic) - 1:  
                k1, k2 = keys[-2:]  
            #如果是中间箱,和bad_rate值相差最小的箱合并  
            else:  
                bef_bad_rate = dic[min_key]["bad_rate"]\
                                             -dic[keys[min_index - 1]]["bad_rate"]
                aft_bad_rate = dic[keys[min_index+1]]["bad_rate"] - dic[min_key]["bad_rate"]
                if bef_bad_rate < aft_bad_rate:  
                    k1, k2 = keys[min_index - 1], min_key
                else:  
                    k1, k2 = min_key, keys[min_index + 1]
            #得到重新划分后的字典,箱的个数比之前少一。  
            dic["%s,%s" % (k1, k2)] = dict()  
            dic["%s,%s" % (k1, k2)]["0"] = dic[k1].get("0", 0) \
                                                             + dic[k2].get("0", 0)
            dic["%s,%s" % (k1, k2)]["1"] = dic[k1].get("1", 0)\
                                                             + dic[k2].get("1", 0)
            dic["%s,%s" % (k1, k2)]["cnt"] = dic[k1]["cnt"]\
                                                                  +dic[k2]["cnt"]
            dic["%s,%s" % (k1, k2)]["bad_rate"] = round(
                                                dic["%s,%s" % (k1, k2)]["1"] / 
                                                dic["%s,%s" % (k1, k2)]["cnt"],5)
            #删除旧的key。  
            del dic[k1], dic[k2]  
            #当前最小的箱的样本个数  
            min_cnt = min([v["cnt"] for v in dic.values()])  
        return dic  



# In[28]:


dep = 'TARGET'
weight = 0
vars = ['NAME_INCOME_TYPE']
charWoe_obj = charWoe(data_dict,dep, weight,vars)

result, woe_dict = charWoe_obj.char_woe()

result['dev']

# In[31]:


ddf = pd.DataFrame(woe_dict)
ddf

# In[34]:


new_ddf = ddf.transpose()
new_ddf

# In[35]:


new_ddf['iv'].sum()

# In[ ]:



输出:

二、 调用库  Tutorial: optimal binning with binary target — optbinning 0.15.0 documentation

Tutorial: optimal binning with binary target — optbinning 0.15.0 documentation

三、共线性

  3.1 相关系数 COR: 

    在做很多基于空间划分思想的模型的时候,我们必须关注变量之间的相关性。单独看两个变量的时候我们会使用皮尔逊相关系数。

df_train.corr()

or

import seaborn as sns
sns.set(color_codes=True)
np.random.seed(sum(map(ord, "distributions")))
sns.pairplot(df_train)#对角线上是单维度分布

 3.2 方差膨胀系数 VIF

  在多元回归中,我们可以通过计算方差膨胀系数VIF来检验回归模型是否存在严重的多重共线性问题。定义:

VIF=\frac{1}{1-R^{2}}

   其中,R^{2}为自变量  对其余自变量作回归分析的负相关系数。方差膨胀系数是容忍度1-R2的倒数。 方差膨胀系数VIF越大,说明自变量之间存在共线性的可能性越大。一般来讲,如果方差膨胀因子超过10,则回归模型存在严重的多重共线性。又根据Hair(1995)的共线性诊断标准,当自变量的容忍度大于0.1,方差膨胀系数小于10的范围是可以接受的,表明白变量之间没有共线性问题存在。

比如:

from statsmodels.stats.outliers_influence import variance_inflation_factor
import numpy as np

data = [[1,2,3,4,5],
        [2,4,6,8,9],
        [1,1,1,1,1],
       [2,4,6,4,7]]
X = np.array(data).T

variance_inflation_factor(X,0)

四、PSI

     稳定性主要考虑  群体稳定性指标(population stability index)

PSI = \sum{(dev\_ratio-val\_ratio)*{\ln(\frac{dev\_ratio}{val\_ratio})}}

def var_PSI(dev_data, val_data):
    dev_cnt, val_cnt = sum(dev_data), sum(val_data)
    if dev_cnt * val_cnt == 0:
        return None
    PSI = 0
    for i in range(len(dev_data)):
        dev_ratio = dev_data[i] / dev_cnt
        val_ratio = val_data[i] / val_cnt + 1e-10
        psi = (dev_ratio - val_ratio) * math.log(dev_ratio/val_ratio)
        PSI += psi
    return PSI

下面将举一个例子,使用ks和poi进行特征筛选。

import math

def sloveKS(self, model, X, Y, Weight):  
    Y_predict = [s[1] for s in model.predict_proba(X)]  
    nrows = X.shape[0]  
    #还原权重  
    lis = [(Y_predict[i], Y.values[i], Weight[i]) for i in range(nrows)]
    #按照预测概率倒序排列  
    ks_lis = sorted(lis, key=lambda x: x[0], reverse=True)        
    KS = list()  
    bad = sum([w for (p, y, w) in ks_lis if y > 0.5])  
    good = sum([w for (p, y, w) in ks_lis if y <= 0.5])  
    bad_cnt, good_cnt = 0, 0  
    for (p, y, w) in ks_lis:  
        if y > 0.5:  
            #1*w 即加权样本个数  
            bad_cnt += w                
        else:  
            #1*w 即加权样本个数  
            good_cnt += w               
        ks = math.fabs((bad_cnt/bad)-(good_cnt/good))  
        KS.append(ks)  
    return max(KS) 

def slovePSI(self, model, dev_x, val_x):  
    dev_predict_y = [s[1] for s in model.predict_proba(dev_x)]  
    dev_nrows = dev_x.shape[0]  
    dev_predict_y.sort()  
    #等频分箱成10份  
    cutpoint = [-100] + [dev_predict_y[int(dev_nrows/10*i)] 
                         for i in range(1, 10)] + [100]  
    cutpoint = list(set(cutpoint))  
    cutpoint.sort()
    val_predict_y = [s[1] for s in list(model.predict_proba(val_x))]  
    val_nrows = val_x.shape[0]  
    PSI = 0  
    #每一箱之间分别计算PSI  
    for i in range(len(cutpoint)-1):  
        start_point, end_point = cutpoint[i], cutpoint[i+1]  
        dev_cnt = [p for p in dev_predict_y 
                                 if start_point <= p < end_point]  
        dev_ratio = len(dev_cnt) / dev_nrows + 1e-10  
        val_cnt = [p for p in val_predict_y 
                                 if start_point <= p < end_point]  
        val_ratio = len(val_cnt) / val_nrows + 1e-10  
        psi = (dev_ratio - val_ratio) * math.log(dev_ratio/val_ratio)
        PSI += psi  
    return PSI  

import xgboost as xgb  
from xgboost import plot_importance  
  
class xgBoost(object):  
    def __init__(self, datasets, uid, dep, weight, 
                                  var_names, params, max_del_var_nums=0):
        self.datasets = datasets  
        #样本唯一标识,不参与建模  
        self.uid = uid       
        #二分类标签  
        self.dep = dep     
        #样本权重  
        self.weight = weight      
        #特征列表  
        self.var_names = var_names    
        #参数字典,未指定字段使用默认值  
        self.params = params     
        #单次迭代最多删除特征的个数  
        self.max_del_var_nums = max_del_var_nums    
        self.row_num = 0  
        self.col_num = 0  
  
    def training(self, min_score=0.0001, modelfile="", output_scores=list()):  
        lis = self.var_names[:]  
        dev_data = self.datasets.get("dev", "")  #训练集  
        val_data = self.datasets.get("val", "")  #测试集  
        off_data = self.datasets.get("off", "")  #跨时间验证集
                #从字典中查找参数值,没有则使用第二项作为默认值  
        model = xgb.XGBClassifier(
                           learning_rate=self.params.get("learning_rate", 0.1),
              n_estimators=self.params.get("n_estimators", 100),  
              max_depth=self.params.get("max_depth", 3),  
              min_child_weight=self.params.get("min_child_weight", 1),subsample=self.params.get("subsample", 1),  
              objective=self.params.get("objective", 
                                                             "binary:logistic"),
              nthread=self.params.get("nthread", 10),  
              scale_pos_weight=self.params.get("scale_pos_weight", 1),
              random_state=0,  
              n_jobs=self.params.get("n_jobs", 10),  
              reg_lambda=self.params.get("reg_lambda", 1),  
              missing=self.params.get("missing", None) )  
        while len(lis) > 0:   
            #模型训练  
            model.fit(X=dev_data[self.var_names], y=dev_data[self.dep])  
            #得到特征重要性  
            scores = model.feature_importances_     
            #清空字典  
            lis.clear()      
            ''' 
            当特征重要性小于预设值时, 
            将特征放入待删除列表。 
            当列表长度超过预设最大值时,跳出循环。 
            即一次只删除限定个数的特征。 
            '''  
            for (idx, var_name) in enumerate(self.var_names):  
                #小于特征重要性预设值则放入列表  
                if scores[idx] < min_score:    
                    lis.append(var_name)  
                #达到预设单次最大特征删除个数则停止本次循环  
                if len(lis) >= self.max_del_var_nums:     
                    break  
            #训练集KS  
            devks = self.sloveKS(model, dev_data[self.var_names],
                                       dev_data[self.dep], dev_data[self.weight])
            #初始化ks值和PSI  
            valks, offks, valpsi, offpsi = 0.0, 0.0, 0.0, 0.0 
            #测试集KS和PSI  
            if not isinstance(val_data, str):  
                valks = self.sloveKS(model,
                                                      val_data[self.var_names], 
                                                      val_data[self.dep], 
                                                      val_data[self.weight])  
                valpsi = self.slovePSI(model,
                                                        dev_data[self.var_names],
                                                        val_data[self.var_names])
            #跨时间验证集KS和PSI  
            if not isinstance(off_data, str):  
                offks = self.sloveKS(model,
                                                  off_data[self.var_names],
                                                  off_data[self.dep],
                                                  off_data[self.weight])  
                offpsi = self.slovePSI(model,
                                                     dev_data[self.var_names],
                                                     off_data[self.var_names])  
            #将三个数据集的KS和PSI放入字典  
            dic = {"devks": float(devks), 
                                 "valks": float(valks),
                                  "offks": offks,  
                 "valpsi": float(valpsi),
                                  "offpsi": offpsi}  
            print("del var: ", len(self.var_names), 
                                       "-->", len(self.var_names) - len(lis),
                                       "ks: ", dic, ",".join(lis))
            self.var_names = [var_name for var_name in self.var_names if var_name not in lis]
        plot_importance(model)  
        #重新训练,准备进入下一循环  
        model = xgb.XGBClassifier(
                             learning_rate=self.params.get("learning_rate", 0.1),
               n_estimators=self.params.get("n_estimators", 100),
                 max_depth=self.params.get("max_depth", 3),  
                 min_child_weight=self.params.get("min_child_weight",1),
               subsample=self.params.get("subsample", 1),  
               objective=self.params.get("objective", 
                                                        "binary:logistic"),  
               nthread=self.params.get("nthread", 10),  
               scale_pos_weight=self.params.get("scale_pos_weight",1),
               random_state=0,  
               n_jobs=self.params.get("n_jobs", 10),  
               reg_lambda=self.params.get("reg_lambda", 1),  
               missing=self.params.get("missing", None))  

参考文献

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值