《Python金融大数据风控建模实战》 第7章 变量选择

  • 本章引言
  • Python代码实现及注释


本章引言

变量选择常见的方法有过滤法、包装法、嵌入法,并且在上述方法中又有单变量选择、多变量选择、有监督选择、无监督选择。在实际应用中,单纯从数据挖掘的角度进行变量选择是不够的,还要结合业务理解对选择后的变量进行回测,以符合业务解释。

Python代码实现及注释

# 第7章:变量选择

'''
在变量分箱的基础上进行变量编码,然后进行变量编码,然后进行变量选择,变量选择程序主要采用scikit-learn包中的feature_selection
模块来完成,实践中常用的函数为:
category_continue_separation:                区分离散变量与连续变量
sklearn.feature_selection.VarianceThreshold:  方差筛选函数
sklearn.feature_selection.SelectKBest:        单变量筛选函数
Pandas.DataFrame.corr:                        计算相关系数矩阵
sklearn.feature_selection.RFECV:              递归消除变量筛选函数
sklearn.feature_selection.SelectFromModel:    嵌入法变量选择函数
feature_selector:                             一个集成的变量选择函数
程序运行逻辑:数据读取-->划分数据集与测试集-->区分离散变量与连续变量-->变量分箱-->分箱后进行WOE编码-->多种变量选择方法测试
'''

import os
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
import variable_bin_methods as varbin_meth
import variable_encode as var_encode
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use(arg='Qt5Agg')
matplotlib.rcParams['font.sans-serif']=['SimHei']   
matplotlib.rcParams['axes.unicode_minus']=False 
from sklearn.linear_model import LogisticRegression 
from sklearn.feature_selection import VarianceThreshold
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.feature_selection import RFECV
from sklearn.svm import SVR
from sklearn.feature_selection import SelectFromModel

'''
Seaborn是matplotlib库的扩展,主要专注于统计学的分析
Seaborn背后有调色板
import seaborn as sns
sns.set_style(‘darkgrid’)     设置一些背景
sns.load_dataset(‘tips’)      加载数据 seaborn datasets
'''
import seaborn as sns

from sklearn.tree import DecisionTreeClassifier
from feature_selector import FeatureSelector
import warnings
warnings.filterwarnings("ignore") ##忽略警告
##数据读取
def data_read(data_path,file_name):
    df = pd.read_csv( os.path.join(data_path, file_name), delim_whitespace = True, header = None )
    ##变量重命名
    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两部分,训练集用于得到编码函数,验证集用已知的编码规则对验证集编码
    data_train, data_test = train_test_split(df, test_size=0.2, random_state=0,stratify=df.target)
    return data_train, data_test
##离散变量与连续变量区分   
def category_continue_separation(df,feature_names):
    categorical_var = []
    numerical_var = []
    if 'target' in feature_names:
        feature_names.remove('target')
    ##先判断类型,如果是int或float就直接作为连续变量
    numerical_var = list(df[feature_names].select_dtypes(include=['int','float','int32','float32','int64','float64']).columns.values)
    categorical_var = [x for x in feature_names if x not in numerical_var]
    return categorical_var,numerical_var
if __name__ == '__main__':
    path = 'D:\\code\\chapter7'
    data_path = os.path.join(path ,'data')
    file_name = 'german.csv'
    ##读取数据
    data_train, data_test = data_read(data_path,file_name)
    sum(data_train.target ==0)
    data_train.target.sum()
    ##区分离散变量与连续变量
    feature_names = list(data_train.columns)
    feature_names.remove('target')
    categorical_var,numerical_var = category_continue_separation(data_train,feature_names)
    for s in set(numerical_var):
        print('变量'+s+'可能取值'+str(len(data_train[s].unique())))
        ## 如果变量的值小于等于10,直接放在类型变量中,从数字类型中删去
        if len(data_train[s].unique())<=10:
            categorical_var.append(s)
            numerical_var.remove(s)
            ##同时将后加的数值变量转为字符串,注意空值不能变成字符串
            index_1 = data_train[s].isnull()
            if sum(index_1) > 0:
                data_train.loc[~index_1,s] = data_train.loc[~index_1,s].astype('str')
            else:
                data_train[s] = data_train[s].astype('str')
            index_2 = data_test[s].isnull()
            if sum(index_2) > 0:
                data_test.loc[~index_2,s] = data_test.loc[~index_2,s].astype('str')
            else:
                data_test[s] = data_test[s].astype('str')

    ###连续变量分箱
    dict_cont_bin = {}
    for i in numerical_var:
        print(i)

        '''
        def cont_var_bin(x, y, method, mmin=5, mmax=10, bin_rate=0.01, stop_limit=0.1, bin_min_num=20):
        """
        ##连续变量分箱函数
        ##参数
        x:输入分箱数据,pandas series
        y:标签变量
        method:分箱方法选择,1:chi-merge , 2:IV值, 3:基尼系数分箱
        mmin:最小分箱数,当分箱初始化后如果初始化箱数小于等mmin,则mmin=2,即最少分2箱,
             如果分两箱也无法满足箱内最小样本数限制而分1箱,则变量删除
        mmax:最大分箱数,当分箱初始化后如果初始化箱数小于等于mmax,则mmax等于初始化箱数-1
        bin_rate:等距初始化分箱参数,分箱数为1/bin_rate,分箱间隔在数据中的最小值与最大值将等间隔取值
        stop_limit:分箱earlystopping机制,如果已经没有明显增益即停止分箱
        bin_min_num:每组最小样本数
        ##返回值
        return data , gain_value_save0 ,gain_rate_save0
        分箱结果:pandas dataframe
        """
        '''
        dict_cont_bin[i],gain_value_save , gain_rate_save = varbin_meth.cont_var_bin(data_train[i], data_train.target, method=2, mmin=3, mmax=12,
                                     bin_rate=0.01, stop_limit=0.05, bin_min_num=20)

    ###离散变量分箱
    dict_disc_bin = {}
    del_key = []
    for i in categorical_var:

        '''
        """
        离散变量分箱方法,如果变量过于稀疏最好先编码在按连续变量分箱
        ##参数:
        x:输入分箱数据,pandas series
        y:标签变量
        method:分箱方法选择,1:chi-merge , 2:IV值, 3:信息熵
        mmin:最小分箱数,当分箱初始化后如果初始化箱数小于等mmin,则mmin=2,即最少分2箱,
             如果分两厢也无法满足箱内最小样本数限制而分1箱,则变量删除
        mmax:最大分箱数,当分箱初始化后如果初始化箱数小于等于mmax,则mmax等于初始化箱数-1
        stop_limit:分箱earlystopping机制,如果已经没有明显增益即停止分箱
        bin_min_num:每组最小样本数
        ##返回值
        return temp_cont.sort_values(by='bin') , gain_value_save0 , gain_rate_save0,del_key
        分箱结果:pandas dataframe
        """
        '''
        dict_disc_bin[i],gain_value_save , gain_rate_save ,del_key_1 = varbin_meth.disc_var_bin(data_train[i], data_train.target, method=2, mmin=3,
                                     mmax=8, stop_limit=0.05, bin_min_num=20)
        if len(del_key_1)>0 :
            del_key.extend(del_key_1)
    ###删除分箱数只有1个的变量
    if len(del_key) > 0:
        for j in del_key:
            del dict_disc_bin[j]
    
    ##训练数据分箱
    ##连续变量分箱映射
    df_cont_bin_train = pd.DataFrame()
    for i in dict_cont_bin.keys():
        df_cont_bin_train = pd.concat([ df_cont_bin_train , varbin_meth.cont_var_bin_map(data_train[i], dict_cont_bin[i]) ], axis = 1)
    ##离散变量分箱映射
#    ss = data_train[list( dict_disc_bin.keys())]
    df_disc_bin_train = pd.DataFrame()
    for i in dict_disc_bin.keys():
        df_disc_bin_train = pd.concat([ df_disc_bin_train , varbin_meth.disc_var_bin_map(data_train[i], dict_disc_bin[i]) ], axis = 1)

  
    ##测试数据分箱
    ##连续变量分箱映射
    df_cont_bin_test = pd.DataFrame()
    for i in dict_cont_bin.keys():
        df_cont_bin_test = pd.concat([ df_cont_bin_test , varbin_meth.cont_var_bin_map(data_test[i], dict_cont_bin[i]) ], axis = 1)
    ##离散变量分箱映射
#    ss = data_test[list( dict_disc_bin.keys())]
    df_disc_bin_test = pd.DataFrame()
    for i in dict_disc_bin.keys():
        df_disc_bin_test = pd.concat([ df_disc_bin_test , varbin_meth.disc_var_bin_map(data_test[i], dict_disc_bin[i]) ], axis = 1)
    
    ###组成分箱后的训练集与测试集
    df_disc_bin_train['target'] = data_train.target
    data_train_bin = pd.concat([df_cont_bin_train,df_disc_bin_train],axis=1)
    df_disc_bin_test['target'] = data_test.target
    data_test_bin = pd.concat([df_cont_bin_test,df_disc_bin_test],axis=1)

    data_train_bin.reset_index(inplace=True,drop=True)
    data_test_bin.reset_index(inplace=True,drop=True)
    
    ###WOE编码
    var_all_bin = list(data_train_bin.columns)
    var_all_bin.remove('target')
    ##训练集WOE编码
    df_train_woe, dict_woe_map, dict_iv_values ,var_woe_name = var_encode.woe_encode(data_train_bin,data_path,
                                 var_all_bin, data_train_bin.target,'dict_woe_map', flag='train')
    
    
    ##测试集WOE编码
    df_test_woe, var_woe_name = var_encode.woe_encode(data_test_bin,data_path,var_all_bin, data_test_bin.target, 'dict_woe_map',flag='test')
    y = np.array(data_train_bin.target)
    
    ###过滤法特征选择
    ##方差筛选
    df_train_woe = df_train_woe[var_woe_name]
    len_1 = df_train_woe.shape[1]
    
    '''
	scikit-learn的feature_selection特征选择模块中的VarianceThreshold方法实现了方差极小的变量筛选方法,函数原型如下:
	class sklearn.feature_selection.VarianceThreshold(threshold=0.0)
	threshold:浮点数,用于指定变量方差的最小阈值,方差小于阈值的变量会被剔除,默认为0
	主要方法:
	fit(self,X[,y]):  用于模型训练,在训练集上训练模型
	transform(self,X):用fit方法得到的模型对原始数据进行方差极小变量剔除,这个过程可以通过fit_transform函数将训练和转化的过程集成在一起完成
	get_support(self[,indices]):得到被保留的变量的索引,以便确定哪些变量被删除
	'''
    select_var = VarianceThreshold(threshold=0.01)
    
    select_var_model = select_var.fit(df_train_woe)
    df_1 = pd.DataFrame(select_var_model.transform(df_train_woe))
    ##保留的索引
    save_index = select_var.get_support(True)
    var_columns = [list(df_train_woe.columns)[x] for x in save_index]
    df_1.columns = var_columns
    ##删除变量的方差
    select_var.variances_[[x for x in range(len_1) if x not in save_index]]
    [list(df_train_woe.columns)[x] for x in range(len_1) if x not in save_index]
    [select_var.variances_[x] for x in range(len_1) if x not in save_index]

    ##单变量筛选
    select_uinvar = SelectKBest(score_func= f_classif, k=15)
    '''
    scikit-learn中提供了多种变量选择方法,如SelectKBest、SelectPercentile及GenericUnivariateSelect等方法,通过计算特征的统计指标完成变量筛选。SelectKBest提供了通过指标排序,给出得分最高的K个变量作为变量选择的结果。其原型如下:
    class sklearn.feature_selection.SelectKBest(score_func=<function f_classif>, k=10)
    主要参数说明:
    	k:整数或字符串,用于指定选择得分最高的前百分之几的特征。如果为字符串all,则保留所有特征。
    	score_func:用于计算输入变量与目标变量之间统计变量的函数,返回数组(scores,pvalues)或单个得分数组。
    常用的指标有:chi2卡方统计分量用于分类任务;
                 f_classif方差分析用于分类任务
                 f_regression计算特征与目标变量之间的F值,即用线性回归分析方法来计算统计指标,用于回归问题
    主要属性说明:
    	scores:用于查询变量选择时每个变量的得分
    	pvalues_:用于查询变量得分的p值   
	'''
	
    select_uinvar_model = select_uinvar.fit(df_train_woe,y)
    df_1 = select_uinvar_model.transform(df_train_woe)
    
    ##看得分
    len_1 = len(select_uinvar_model.scores_)
    var_name = [str(x).split('_BIN_woe')[0] for x in list(df_train_woe.columns)]
    ##
    plt.figure(figsize=(10,6))
    fontsize_1 = 14
    
    '''
	plt.bar()
		x:表示x坐标,数据类型为int或float类型,刻度自适应调整;也可传dataframe的	object,x轴上等间距排列;
		height:表示柱状图的高度,也就是y坐标值,数据类型为int或float类型;
		width:表示柱状图的宽度,取值在0~1之间,默认为0.8;
		bottom:柱状图的起始位置,也就是y轴的起始坐标;
		align:柱状图的中心位置,默认"center"居中,可设置为"lege"边缘;
		color:柱状图颜色;
		edgecolor:边框颜色;
		linewidth:边框宽度;
		tick_label:下标标签;
		log:柱状图y周使用科学计算方法,bool类型;
		orientation:柱状图是竖直还是水平,竖直:“vertical”,水平条:“horizontal”;
	'''
    plt.barh(np.arange(0,len_1),select_uinvar_model.scores_,color = 'c',tick_label=var_name)
    
    '''
    用matplotlib画二维图像时,默认情况下的横坐标和纵坐标显示的值有时达不到自己的需求,需要借助xticks()和yticks()分别对横坐标x-axis和纵坐标y-axis进行设置。
    '''
    plt.xticks( fontsize=fontsize_1)
    plt.yticks( fontsize=fontsize_1)
    
    plt.xlabel('得分',fontsize=fontsize_1)
    plt.show()
    
    ##分析变量相关性
    ##计算相关矩阵
    correlations = abs(df_train_woe.corr())
    ##相关性绘图
    fig = plt.figure(figsize=(10,6)) 
    fontsize_1 = 10
    sns.heatmap(correlations,cmap=plt.cm.Greys, linewidths=0.05,vmax=1, vmin=0 ,annot=True,annot_kws={'size':6,'weight':'bold'})
    plt.xticks(np.arange(20)+0.5,var_name,fontsize=fontsize_1,rotation=20)
    plt.yticks(np.arange(20)+0.5,var_name,fontsize=fontsize_1) 
    plt.title('相关性分析')
#    plt.xlabel('得分',fontsize=fontsize_1)
    plt.show()

    ##包装法变量选择:递归消除法 
    ##给定学习器
    estimator = SVR(kernel="linear")
    ##递归消除法 
	
	'''
	scikit-learn的feature_selection特征选择模块中并没有实现逐步回归stepwise regression变量选择,但是提供了后向搜索方法的代码实现:
	递归消除方法(RFE)与带交叉验证的递归消除方法(RFECV)。RFE方法通过自定义的学习器,开始在全部空间变量中训练学习器,得到每个变量的权重后,剔除当前权重最小的那个变量,
	再重新训练学习器,直至变量数满足预定设置值为止。与RFE相比,RFECV不需要预先设定选择的特征数量,通过交叉验证方法得到一个最优的变量个数。其原型如下:
	class sklearn.feature_selection.RFECV(estimator, step = 1, min_features_to_select = 1, cv = 'warn', scoring = None, verbose = 0, n_jobs = None)
	主要参数说明:
		estimator: 学习器,可自行选择,学习器必须具有coef_属性或feature_importance_属性。
		step:整数或浮点数,用于指定每次迭代过程(变量选择过程)要剔除的变量数。如果为大于1的整数,则每次剔除这个数量的变量;如果为浮点数,则按变量比例剔除变量
		cv:指定交叉验证的次数,默认为3折交叉验证,也可以是交叉验证生成器。
		scoring:用于指定学习器的性能评估指标,如果为None,则使用estimator.score()方法评估学习器性能
	主要属性说明:
		n_feature_:用于查询变量选择后保留的特征数
		support_:查询哪个变量被保留,返回一个数组,如果为True表示该变量被保留,反之表示该变量被删除
	'''
    select_rfecv = RFECV(estimator, step=1, cv=3)
    
    select_rfecv_model = select_rfecv.fit(df_train_woe, y)
    df_1 = pd.DataFrame(select_rfecv_model.transform(df_train_woe))
    ##查看结果
    select_rfecv_model.support_ 
    select_rfecv_model.n_features_
    select_rfecv_model.ranking_
    
    ###嵌入法变量选择
    ##选择学习器
    lr = LogisticRegression(C=0.1, penalty='l2')
    ##嵌入法变量选择
	
	'''
	scikit-learn的feature_selection特征选择模块中SelectFromModel方法实现了嵌入法变量选择,其原型如下:
	class sklearn.feature_selection.SelectFromModel(estimaor,threshold=None,prefit = False,norm_order = 1,max_features=None)
	主要参数说明:
		estimator: 学习器,可自行选择,与递归消除方法中对学习器的要求一样,学习器必须具有coef_属性或feature_importance_属性。变量选择过程通过这些权重或重要性进行变量剔除。
		threshold:字符串或浮点数,用于指定每次迭代过程(变量选择过程)要剔除的变量数。如果为浮点数,则小于阈值的变量被剔除。如果是字符串则提供一种启发式的变量剔除方法。如为mean,则以特征重要性的均值作为阈值,小于均值的变量被剔除,同理还有median等。
		prefit:布尔类型,指定是否为预训练的模型,如果模型已经训练好,则将prefit置为True。
		max_features:整数或None,变量选择的最大变量数,与threshold参数类似,如果已经设定了threshold,则该参数可忽略。如果threshold=inf,则该参数起作用。
	主要属性说明:
		threshold_:用于查询变量选择的阈值
		get_support:查看选择了哪些变量
	'''
    select_lr = SelectFromModel(lr, prefit=False,threshold ='mean')
    
    select_lr_model = select_lr.fit(df_train_woe, y)
    df_1 = pd.DataFrame(select_lr_model.transform(df_train_woe))
    ##查看结果
    select_lr_model.threshold_   
    select_lr_model.get_support(True)
    
    ##基学习器选择预训练的决策树来进行变量选择
    ##先训练决策树
    cart_model = DecisionTreeClassifier(criterion='gini',max_depth = 3).fit(df_train_woe, y)
    cart_model.feature_importances_  
    ##用预训练模型进行变量选择
    select_dt_model = SelectFromModel(cart_model, prefit=True)
    df_1 = pd.DataFrame(select_dt_model.transform(df_train_woe))
    ##查看结果
    select_dt_model.get_support(True)
    
    ###集成学习下的变量选择lightgbm     
    fs = FeatureSelector(data = df_train_woe, labels = y)
    ##设置筛选参数
    '''
    剔除缺失值比例大于0.9,相关性大于0.6,方差为0,变量重要性为0或较低的变量,保留累计重要性达到90%的变量。
    corr_matrix:查看变量的相关矩阵
    feature_importances:查看变量重要性的累计贡献率
    record_low_importance:查看低重要性的变量
    '''
    fs.identify_all(selection_params = {'missing_threshold': 0.9, 
                                         'correlation_threshold': 0.6, 
                                         'task': 'classification', 
                                         'eval_metric': 'binary_error',
                                         'max_depth':2,
                                         'cumulative_importance': 0.90})
                                         
    df_train_woe = fs.remove(methods = 'all')
    ##查看结果
    fs.feature_importances
    fs.corr_matrix
    fs.record_low_importance