风控建模九:一些特征工程方法及自动化工具小结

建模工作中我们常常面临的一大问题就是,如何在有限的数据下,通过一些特征工程方法,挖掘出数据中更多的信息,来达到提升模型效果的目的。所以本篇文章就简单整理了一些特征工程的思路、方法和一些现成的特征工程python工具包,供大家一起探讨。

一、方法篇

我们下面所做的特征工程工作都是基于这样一种基本思想,即数据孤立地呈现是意义不大的,当我们忽略数字背后的时空背景时,数字背后更丰富的信息就无法表达出来。不同数据之间应该能够通过某种特定方式的有机组合、互相提供场景而衍生出新的有效信息,这是我们特征工程工作基本的出发点。

1、交叉组合,打标有风险区分力的特定人群

举例来看,比如婚姻状态这样一个变量,在我们的业务场景下看到的结果通常是离异的客群比已婚的客群的坏账率稍微高一些,但也没有明显差异。这样的结果能说明婚姻状态这个变量对风险没有区分度,对提升模型效果没有贡献吗?实际并非如此,当我们把年龄和婚姻状态两个变量组合分析后发现,30岁以上离异的客群和已婚的客群坏账率并没有什么差别,但30岁以下就离异的客群坏账率却明显高于其他婚姻状态。这就是一个比较生动的赋予数据场景的例子。当我们单独看婚姻状态这个数据时,我们能获取到的信息非常有限,在风险区分度上也看不到明显效果,但当我们给婚姻状态赋予了年龄这个场景后,我们就进一步获取到了更深层次的信息,30岁不到、离异,仅仅通过两个维度的数据组合,我们就看到了一个似乎不靠谱、不慎重、不负责的形象,而这种组合圈定的人群在风险表现上自然也更高。
基于这样的想法,我们设计的第一种特征工程方法就是所有变量分箱,遍历交叉,找到所有交叉后有明显风险区分力的组合方式,并将这些组合圈定的人群以独热编码的方式打标,作为新的衍生变量。代码如下:

from itertools import product
import multiprocessing
from tqdm import tqdm

def var_cross(df, res_col, target = 'if_overdue_30', bad_thresh=0.2, good_thresh=0.05, num_rate=0.02, lift_rate=1.5):
    
    res = pd.DataFrame()
    df_res = pd.DataFrame()
    for group_col in res_col:

        group_df = df[list(group_col)+[target]].copy()
        for num_col in group_df.select_dtypes(include=[np.number]):
            if group_df[num_col].value_counts().shape[0]>=10:
                group_df[num_col] = pd.qcut(group_df[num_col],10,duplicates='drop').astype(str)
        
        tmp = group_df.groupby(list(group_col) if isinstance(group_col,tuple) else group_col)[target].agg({'count','mean'})
        for item in tmp.iterrows():
            if (item[1]['count']>df.shape[0]*num_rate)&((item[1]['mean']>bad_thresh)|(item[1]['mean']<good_thresh)):

                if len(group_col)==2:
                    br1 = group_df.loc[group_df[group_col[0]]==item[0][0],target].mean()
                    br2 = group_df.loc[group_df[group_col[1]]==item[0][1],target].mean()
                    if ( (item[1]['mean']>=br1*lift_rate)&(item[1]['mean']>=br2*lift_rate) )|( (item[1]['mean']*lift_rate<=br1)&(item[1]['mean']*lift_rate<=br2) ):
                        col_name = 'CROSS_'+str(group_col[0])+str(item[0][0])+str(group_col[1])+str(item[0][1])
                        col_name = col_name.replace(', ','_').replace('(','').replace(']','')
                        group_df[col_name]=0
                        group_df.loc[(group_df[group_col[0]]==item[0][0])&(group_df[group_col[1]]==item[0][1]),col_name]=1
                        df_tmp = pd.DataFrame({'var':str(group_col),'bin':str(item[0]),'count':item[1]['count'],
                                       'bad_rate':item[1]['mean'],'var_bad_rate':str([br1,br2])},index=[0])
                        res = pd.concat([res,df_tmp])
                        df_res = pd.concat([df_res,group_df[col_name]],axis=1)
                        
                elif len(group_col)==3:
                    br1 = group_df.loc[group_df[group_col[0]]==item[0][0],target].mean()
                    br2 = group_df.loc[group_df[group_col[1]]==item[0][1],target].mean()
                    br3 = group_df.loc[group_df[group_col[2]]==item[0][2],target].mean()
                    if ( (item[1]['mean']>=br1*lift_rate)&(item[1]['mean']>=br2*lift_rate)&(item[1]['mean']>=br3*lift_rate) )|( (item[1]['mean']*lift_rate<=br1)&(item[1]['mean']*lift_rate<=br2)&(item[1]['mean']*lift_rate<=br3) ):
                        col_name = 'CROSS_'+str(group_col[0])+str(item[0][0])+str(group_col[1])+str(item[0][1])+str(group_col[2])+str(item[0][2])
                        col_name = col_name.replace(', ','_').replace('(','').replace(']','')
                        group_df[col_name]=0
                        group_df.loc[(group_df[group_col[0]]==item[0][0])&(group_df[group_col[1]]==item[0][1])&(group_df[group_col[2]]==item[0][2]),col_name]=1                    
                        df_tmp = pd.DataFrame({'var':str(group_col),'bin':str(item[0]),'count':item[1]['count'],
                                       'bad_rate':item[1]['mean'],'var_bad_rate':str([br1,br2,br3])},index=[0])
                        res = pd.concat([res,df_tmp])
                        df_res = pd.concat([df_res,group_df[col_name]],axis=1)

    return df_res, res

具体解释一下,函数var_cross的整体功能就是实现变量的分箱交叉,并保留有效组合。其中第一个参数df就是包含所有变量的数据集;第二个参数res_col存放的是我们需要进行交叉组合的变量对,比如我们有三个变量V1,V2,V3,要两两交叉组合,那res_col就是[(V1,V2), (V1,v3), (V2,V3)];第三个参数指定哪一列是目标y,比如我们这里用的是否逾期30天;后面四个参数是我们对组合的限制条件,我们期望找到有明显风险区分力的组合,那区分力的定义就是这个组合圈定的人群要么很好,要么很坏,即坏账率要不然高于某个阈值,要不然低于某个阈值,同时,为了防止单变量某个分箱本身就很有区分力,我们还加入了一个限定条件,即组合后,坏账率要有一定幅度的提升才保留。比如学历变量,初中以下学历本身坏账可能就有20%,那我们限定某个变量与学历初中以下这一箱组合后,坏账要达到30%才得以保留。另外,组合圈定的人群也要保证一定的人群覆盖,比如至少保证2%的人在这个组合里面。由此,第四个参数指定了交叉后坏账率要高过的阈值,这里指定为20%;第五个参数指定了坏账要低于的阈值;第六个参数限定了交叉后的最小人数比例;第七个变量指定了交叉后提升的倍数。
最后,这个函数返回了两个数据集,第一个df_res即包含所有新衍生变量的数据集。第二个res包含了交叉组合的详细信息,如下所示:

第一列表示哪两个变量交叉,这里是年龄和性别;第二列表示每个变量分箱取值是多少,这里年龄是18-28岁,性别是女,第三列表示交叉组合所覆盖的人群数,这里是1170;第四列表示交叉组合覆盖人群的坏账率,3%;最后一列呈现的是这两个变量在其各自的分箱里面坏账率是多少,用于对比着看交叉组合后坏账率的提升度,这里年龄18-28岁人群坏账率4.37%,性别女性的客户坏账率5.41%。如此会生成一列变量,变量中18-28岁女性客群会打标为1,其它为0,衍生变量的变量名包含两个交叉组合的变量名和分箱取值:CROSS_AGE18.0_28.0SEX女。
当我们的数据维度较多,量级较大时,这种遍历交叉组合就会非常费时,当有一个好的服务器的时候,我们可以通过多线程去解决计算耗时的问题,这里同时贴出多线程执行上述函数的代码:

def var_cross_multi(df, col:list, target = 'if_overdue_30', bad_thresh=0.15, good_thresh=0.05, num_rate=0.02, lift_rate=1.5):

    if len(col)==1:
        res_col = col[0]
    else:
        res_col = [list(x) for x in product(*col) if len(list(x))==len(set(x))]
        res_col = set([tuple(set(sorted(x))) for x in res_col])

	##这里指定要用几个核来跑,根据自己的机器情况调整
    jobn = 60
    
    row_s = pd.Series(range(0, len(res_col)), index=res_col)
    jobn = min(jobn, len(row_s.index))
    row_cut = pd.qcut(row_s, jobn, labels=range(0, jobn))
    data_list = []
    for i in range(0, jobn):
        data_list.append(list(row_cut[row_cut == i].index))
    
    mp = multiprocessing.Pool(jobn)
    mplist = []
    for i in range(0, jobn):
        mplist.append(
            mp.apply_async(
                func=var_cross,
                kwds={'df':df,'res_col':data_list[i],'target':target, 'bad_thresh':bad_thresh, 'good_thresh':good_thresh, 'num_rate':num_rate, 'lift_rate':lift_rate}))
    mp.close()
    mp.join()

    res = pd.DataFrame()
    df_res_list = []
    for result in tqdm(mplist):
        part_res = result.get()
        if len(part_res)>1:
            res = pd.concat([res,part_res[1]])
            df_res_list.append(part_res[0])
            
    df_res = pd.concat(df_res_list,axis=1)

    print('FINISH!!')
    return df_res,res

这里值得一提的是,函数的第二个参数col,是直接传入需要进行组合的变量列表,比如我们指定var_list1 = [V1,V2,V3],var_list2=[V4,V5,V6]想要让着两个列表中的变量进行组合交叉,那我们就将[var_list1, var_list2]传给col就可以了,函数会自动生成出所有不重复的两两组合对,传给var_cross函数。如果想要三个变量组合,只需要把[var_list1, var_list2, var_list3]传给col就可以了,目前这个函数的功能只支持最多三个变量的组合。

2、数值变量场景化分段比较信息

有些数值型变量,放在整个群体下去比较是不公允的。比如收入。5000元的月收入是高是低呢?放在不同的环境下则有不同的含义,如果是在北京月入5000,那可想而知,生活质量一定不高,甚至捉襟见肘;如果是在鹤岗月入5000,生活滋润不说,搞不好房产也有两三套了。所以对于类似收入这样的变量,最好能够区分出不同的环境,在同一环境下做相对公平的比较排序。实现相对公平比较的方式有两种,一是借助外部的信息知识,比如我们可以到国家统计局找到每个城市居民的平均月收入,并将我们要分析的客户月收入减去其所在城市的平均月收入,得到“与所在城市平均收入差值”这样一个变量;二是直接用我们的客户样本做每个城市的收入均值统计,以这个均值来衍生收入差异变量。这样做也有道理,因为借贷客群本身就是一类较为特殊的人群,同一城市同属性的人群比较理论上更为公平,当然,当我们样本不充足的时候,这样做的稳定性会差一些。
基于这样的想法,我们给出的第二种特征衍生方法就是把类似收入这类的变量放到不同场景下,如不同城市、不同学历、不同职业、不同年龄段等等,去和该场景下的均值、最大值、最小值做比较,同时在该场景下进行排序,代码如下:

def scene_feature_engineer(df,id_key,scene_features,value_features):
    '''
    create features such as: difference between personal salary and mean/max/min salary within a specific group
    '''
    
    for col in value_features:
        if df[col].dtypes=='object':
            raise ValueError('{} is not number value'.format(col))
    
    columns = list(set(scene_features+value_features))+[id_key]
    tmp = df[columns].copy()
    for gp_feature in scene_features:
        if (tmp[gp_feature].dtypes != object)&(tmp[gp_feature].value_counts().shape[0]>8):
            tmp[gp_feature+'_bin'] = pd.qcut(tmp[gp_feature],8,duplicates='drop').astype(str)
        else:
            tmp[gp_feature+'_bin'] = tmp[gp_feature]
            
        for v_feature in value_features:
            if gp_feature == v_feature:
                continue
            
            gp_data = tmp.groupby(gp_feature+'_bin')[v_feature].agg({'mean','max','min'}).reset_index()
            tmp = tmp.merge(gp_data, on=gp_feature+'_bin',how='left')
            for agg in ['mean','max','min']:
                tmp['GP_FEATURE_gpby_'+gp_feature+'_cntby_'+v_feature+'_'+ agg] = tmp[v_feature] - tmp[agg]

            tmp.drop(['mean','max','min'],axis=1,inplace=True)
            
            for i,gp_part in tmp.groupby(gp_feature+'_bin'):
                gp_part['rankpercent'] = gp_part[v_feature].rank(pct=True)
                tmp.loc[gp_part.index,'GP_RANK_gpby_'+gp_feature+'_sortby_'+v_feature] = gp_part['rankpercent']
            
    return tmp[[id_key]+[x for x in tmp if (x.startswith('GP_FEATURE'))|(x.startswith('GP_RANK'))]]

其中第一个参数df是包含所有变量的数据集;参数id_key指定哪一类是主键,比如身份证号,方便变量衍生完成后和原数据集做匹配;参数scene_features是一个列表,放置所有可作为场景的变量,比如城市、省份、职业、年龄等;参数value_features也是一个列表,方式所有要做不同场景化比较的变量,比如收入、负债、消费额等等。
函数返回的是一个数据集,包含主键和所有衍生变量,以GP_FEATURE开头的变量反映的是与不同场景下的均值、最大值、最小值差异;以GP_RANK开头的变量反映的是在不同场景下排序的分位数。

3、场景变量分割,找出隐藏区分度

我们再一次拿出这个经典的表格去说明问题:
在这里插入图片描述
住房状态这个变量整体来看,区分度非常一般,租房、自有住房、与父母同住三个住房状态的人群坏账率差别不明显。但当我们用年龄这个变量以30岁为界做一个分割后发现,30岁以下人群里面,三个住房状态的坏账率差异就显现出来了。如果用IV值来衡量区分度,那整体计算住房状态这个变量的IV值一定没有用年龄30岁分割后,只计算30岁以下人群住房状态IV值高。说明当我们用其他变量对某些变量做分割后,会挖掘出更多潜藏的信息。据此,我们提出的第三种特征工程方法就是通过交叉分割,挖掘出这些潜藏信息。
具体方法如下图所示,我们以两个变量A、B为例,首先遍历变量A的每一个分箱,计算每一个分箱下B变量的IV值,和变量B的整体IV值相比较。若分箱交叉部分变量B的IV值高于B整体的IV值,则将这部分B的变量取值单独保留,作为一个新的衍生变量(比如变量A为年龄,B为住房状态,分别计算30岁以下,30-40岁,40岁以上客群的住房状态IV值,如果有高于整个客群住房状态IV值的,则保留下取值作为新的衍生变量)。然后反之,遍历B的每个分箱,计算A的IV值。最后遍历A和B的每种分箱的两两组合。具体代码如下:
在这里插入图片描述

from itertools import product
import multiprocessing
import numpy as np
from tqdm import tqdm
import warnings
warnings.filterwarnings('ignore')

def find_bins_cutpoint(data,col):
    if if_continue_value(data,col):
        _,bins = pd.qcut(data[col],5,duplicates='drop',retbins=True)
        bins = zip([-np.inf]+list(bins),list(bins)+[np.inf])
    else:
        bins = data[col].value_counts().index.tolist()
    return bins


def find_bins_dataframe(data,col_bin,bins,col_value):
    if isinstance(bins,tuple):
        return data.loc[(data[col_bin]>bins[0])&(data[col_bin]<=bins[1]),col_value]
    else:
        return data.loc[data[col_bin]==bins,col_value]


def create_new_df_vars(data,col_bin,bins,col_value,new_var_name):
    data[new_var_name]=np.nan
    if isinstance(bins,tuple):
        data.loc[(data[col_bin]>bins[0])&(data[col_bin]<=bins[1]),new_var_name] = data.loc[(data[col_bin]>bins[0])&(data[col_bin]<=bins[1]),col_value]
    else:
        data.loc[data[col_bin]==bins,new_var_name] = data.loc[data[col_bin]==bins,col_value]
        
    return data


def iv_value_compare(y,col_list,lift_rate=1.2,iv_threshold=0.02,num_threshold=0.03):
    
    res = pd.DataFrame()
    res_df = pd.DataFrame()
    
    for col_1,col_2 in col_list:
        
        if (not if_continue_value(df,col_1))&(not if_continue_value(df,col_2)):
            continue
            
        data = df[[col_1,col_2,y]].copy()

        base_iv_value1 = calculate_iv(data,col_1,y)
        base_iv_value2 = calculate_iv(data,col_2,y)

        bins1 = find_bins_cutpoint(data,col_1)
        bins2 = find_bins_cutpoint(data,col_2)
        for col,bins,base_iv in [(col_1,bins1,base_iv_value2),(col_2,bins2,base_iv_value1)]:
            for b in bins:
                df_bin = find_bins_dataframe(data,col,b,[col_1,col_2,y])
                if df_bin.shape[0]<df.shape[0]*num_threshold:
                    continue
                bin_iv_value = calculate_iv(df_bin,[x for x in [col_1,col_2] if x!=col][0],y)
                if (bin_iv_value>=base_iv*lift_rate)&(bin_iv_value>iv_threshold):
                    res_tmp = pd.DataFrame({'total_iv_value':base_iv,'col1_col2':str([col_1,col_2]),
                                        'col':col,'bin':str(b),'bin_iv_value':bin_iv_value,'bin_num':df_bin.shape[0]},index=[0])
                    res = pd.concat([res,res_tmp])
                    var_name = 'CROSS_IV_'+col_1+col_2+col+'_'+str(b)
                    var_name = var_name.replace(', ','_').replace('(','').replace(']','').replace(')','')
                    df_tmp = create_new_df_vars(data.fillna(-9999),col,b,[x for x in [col_1,col_2] if x!=col][0],var_name)
                    res_df = pd.concat([res_df,df_tmp[[var_name]]],axis=1)

        for b1 in bins1:
            df_bin = find_bins_dataframe(data,col_1,b1,[col_1,col_2,y])
            for b2 in bins2:
                df_bin = find_bins_dataframe(df_bin,col_2,b2,[col_1,col_2,y])
                if df_bin.shape[0]<df.shape[0]*num_threshold:
                    continue
                for col,base_iv in [(col_1,base_iv_value1),(col_2,base_iv_value2)]:
                    bin_iv = calculate_iv(df_bin,col,y)
                    if (bin_iv>=base_iv*lift_rate)&(bin_iv>iv_threshold):
                        res_tmp = pd.DataFrame({'total_iv_value':base_iv,'col1_col2':str([col_1,col_2]),
                                            'col':col,'bin':str([b1,b2]),'bin_iv_value':bin_iv,'bin_num':df_bin.shape[0]},index=[0])
                        res = pd.concat([res,res_tmp])
                        var_name = 'CROSS_IV_'+col_1+col_2+str(b1)+str(b2)
                        var_name = var_name.replace(', ','_').replace('(','').replace(']','').replace(')','')
                        df_tmp = create_new_df_vars(data.fillna(-9999),col_1,b1,col,var_name+'tmp')
                        df_tmp = create_new_df_vars(df_tmp,col_2,b2,var_name+'tmp',var_name)
                        res_df = pd.concat([res_df,df_tmp[[var_name]]],axis=1)

    return res_df,res

其中iv_value_compare函数为实现此功能的主函数,其它都是辅助性的功能小函数。其中参数y指定了目标值列;参数col_list放置需要进行交叉的变量对,比如我们想要V1,V2,V3三个变量两两交叉,只需要把列表[(V1,V2),(V1,V3),(V2,V3)]传给col_list;参数lift_rate指定了分箱交叉后IV值要比整体IV值提升多少倍,才可以衍生变量,这里设置的是1.2倍提升;参数iv_threshhold设定了IV值的最小阈值,这里指定了变量分箱交叉后的IV值要至少超过0.02才进行新变量的衍生,参数num_threshhold指定了变量分箱分箱的最小覆盖客群数,这里指定了分箱后覆盖人数至少要达到总体客群的3%才可以。最后函数返回两个数据集,数据集res记录了所有变量交叉的详细信息,数据集res_df包含所有新的衍生变量,如下图所示:

res中第一列显示了哪两个变量交叉,这里是收入和公司类型;第二、三列表示用哪个变量的哪个分箱去做切割,这里是用公司类型为外资企业去切割收入收入变量;第四列显示切割后这一段收入变量的IV值;最后一列显示分箱中的客户数。这样生成的新变量会将公司类型为外资企业的收入原始值保存下来,其它位置填空,变量名为CROSS_IV_INCOME_COMPANY_TYPE_COMPANY_TYPE外资企业。
同样,样本较大时,对所有变量的遍历交叉计算耗时较长,这里同样给出多线程处理的代码:

def iv_value_compare_multi(col,lift_rate=1.0,iv_threshold=0.02,num_threshold=0.03):

    if len(col)==1:
        res_col = col[0]
    else:
        res_col = [list(x) for x in product(*col) if len(list(x))==len(set(x))]
        res_col = set([tuple(set(sorted(x))) for x in res_col])
    
    ##依据自己机器的核数进行更改
    jobn = 60
    
    row_s = pd.Series(range(0, len(res_col)), index=res_col)
    jobn = min(jobn, len(row_s.index))
    row_cut = pd.qcut(row_s, jobn, labels=range(0, jobn))
    data_list = []
    for i in range(0, jobn):
        data_list.append(list(row_cut[row_cut == i].index))
    
    print(jobn)
    mp = multiprocessing.Pool(jobn)
    mplist = []
    for i in range(0, jobn):
        mplist.append(
            mp.apply_async(
                func=iv_value_compare,
                kwds={'y':'if_overdue_90','col_list':data_list[i],'lift_rate':lift_rate,'iv_threshold':iv_threshold,'num_threshold':num_threshold}))
    mp.close()
    mp.join()

    res = pd.DataFrame()
    df_res_list = []
    for result in tqdm(mplist):
        part_res = result.get()
        if len(part_res)>1:
            res = pd.concat([res,part_res[1]])
            df_res_list.append(part_res[0])
            
    df_res = pd.concat(df_res_list,axis=1)

    print('FINISH!!!!')
    return df_res,res

同方法一一样,如果我们指定var_list1 = [V1,V2,V3],var_list2=[V4,V5,V6],想要让着两个列表中的变量进行组合交叉,那我们就将[var_list1, var_list2]传给参数col就可以了。
最后需要说明的是,以上提到的特征工程方法也只是根据一些常识逻辑和业务逻辑而进行的探索,在不同样本上试验,效果参差,但都不显著。有时候我们认为机器学习模型仅仅是对数字的简单拟合,忽略了数字时空背景也许是机器学习模型的一大缺陷,但试验结果并不能对这个论点提供有力支撑。或许是我们还没有找到正确赋予数字时空背景的方法,也或许,是我们小看了机器学习模型,对其了解还不够深入,很多特征工程无疑过分的妈宝行为。机器学习模型仅仅是数字拟合听上去确实简单粗暴,但这也许就是理想理论与实际情况之间博弈平衡的最优解。虽然我们有了大数据这个有力的手段去全面了解客户,但再大的数据也是有限的数据,有限的数据就脱离不了概率论的视角,因为总有我们看不到的一面隐藏在大数据的帷幕之后,两个在大数据上表现得完全一致的人,背后的本质也许就截然对立,所以只凭大数据,我们就只能以概率论的手段去认知。既然脱离不了概率论,那机器学习模型拟合一个样本概率的全局最优解也成了突破不了的唯一答案。在不增加新信息的情况下,模型效果很难有大的提升。这些特征工程并非提升模型效果的灵丹妙药,如果我们奔着模型效果显著提升这个目的,那以上所述的价值也只能是前车之鉴,避免大家重蹈覆辙;或者为大家提供一些灵感,以期能够找到更有效的特征衍生方法。

二、工具篇

1、Featuretools

Featuretools是一个实现特征工程自动化的python框架。主要功能就是根据时间型的和关系型的数据集,自动生成包含一系列有意义变量的特征矩阵。它的优势就在于标准化封装了从原始数据,到衍生特征的构造过程,大大节省了特征工程时间,更便于做自动化机器学习。
具体如何应用,通过它官网的案例一眼就可以看明白:
在这里插入图片描述
它应对的场景大致如下,比如一个交易场景,第一个custumer的数据集存了每个客户的基本信息,第二个session的数据集存了每个交易场景的信息,一个客户一般对应多个交易场景,如手机上交易、电脑上交易,和平板上交易。第三个transaction数据集存了每笔交易信息,一个客户在一个场景下可能有多笔交易,信息就都存在这里。
在这里插入图片描述
指定好参数后,只需要简单一个语句的调用,就可以自动生成各种聚合类变量和时间类变量,从变量名中也能很清楚地看到衍生了哪些变量,比如聚合类变量有:一个客户进行交易的场景次数、最常用的设备类型、使用过的设备类型数、交易笔数等等;时间类变量有:申请年、月、日、时段、最近一笔用款距今时间等。这是Featuretools的一个基本应用,它还有两个稍微进阶一些的高级功能,我们也来简单看一下:

1.1 深度特征合成

深度特征合成和深度学习没有什么关系,简单来说就是加工变量时,关联层级加深,一个例子就可以简单说明:当我们指定了深度层级为2度时,它就会衍生出类似这样的特征:
在这里插入图片描述
首先先计算每个交易场景下所有交易的金额总和,然后对所有场景下的金额总和求平均值。这就是深度为2的含义,即关联了两个层级去做聚合特征衍生。

1.2 时间序列处理

进行时间序列预测一个非常重要的点就是做好时间切分,明确区分出训练集和预测集。featuretools在做时间类变量衍生的时候就可以自动只计算切分点之前的信息,比如我们要预测一个客户是否会在1月1号4点后的这一天花费500块,我们在衍生变量进行预测的时候,就要严格把握,不能将1月1号4点之后的信息计算进来。当我们指定了cutoff_time参数为1月1日4点之后,featuretools会自动帮我们剔除4点后的信息。
在这里插入图片描述
除此之外,我们还可以自定义一个时间窗口,每条记录都按统一的时间窗口做变量计算。还可以指定每个时间发生的开始和结束时间,并计算和指定时间窗口有重叠的事件有多少等等。
这里我们只是简要概述了Featuretools的应用场景和主要功能,具体的实现方法和更细节的功能都可以在官网上找到详细论述。先掌握一个框架,需要具体应用时再细细研究,提高认知效率。

2、Autofeat

Autofeat库封装了自动化特征衍生和自动化特征筛选两个功能,它要解决的问题是这样的:复杂的机器学习模型如深度神经网络等在业务应用中有着固有缺陷,一是超参太多难调优;二是数据量要求大难满足;三是难以向非统计人员解释,所以线性回归往往是业务应用首选,但其区分精度往往低于神经网络,主要是因为神经网络能够从原始变量中学习到非线性的表达性特征(expressive representations)。基于此,autofeat主要功能就是通过乘方、对数、加减乘除运算衍生出非线性特征,并自动筛选出有价值的特征。所以,autofeat的特征衍生功能可以看做是对featuretools功能的互补,也可以看做是对featuretools的功能进阶,featuretools实现从原始数据到基本变量的衍生,autofeat完成从基本变量到非线性变量的衍生和筛选。

2.1 autofeat的变量衍生逻辑

autofeat的变量衍生逻辑就是“非线性转化”——“两两交叉组合”这两个步骤的不断交替迭代。具体而言,“非线性转化”指的是对变量进行 l o g ( x ) log(x) log(x) x \sqrt x x 1 / x 1/x 1/x x 2 x^2 x2 x 3 x^3 x3 ∣ x ∣ \left | x \right | x e x p ( x ) exp(x) exp(x) 2 x 2^x 2x s i n ( x ) sin(x) sin(x) c o s ( x ) cos(x) cos(x)等方式的运算;“两两交叉组合”是指对两个变量之间进行加减乘等运算。这种方式衍生的变量数会呈指数级增长,以3个原始变量为例,第一步会生成20个左右的变量,第二步会生成750个变量左右,第三步就会扩展到4000个变量左右,因而对内存的要求比较大。所以衍生变量前可以先对样本进行采样,通常执行2-3步就足够应用。

2.2 autofeat的变量选择逻辑

第一步,在庞大的衍生变量池中去掉和原始变量高相关的变量。
第二步,选出贡献最大的一组变量A:用所有变量训练一个L1正则化的逻辑回归模型,选出系数绝对值最大的一组变量。
第三步,进一步选出有效变量:把剩下的变量均分n组,每一组变量分别和A放在一起训练一个模型,并根据系数大小选择出有效变量。
第四步,选出的所有候选变量放在一起训练一个模型并决定最终要保留的变量。
第五步,以上2-4步在多个数据子集上面执行,选出多组候选变量。
第六步,多组候选变量放在一起,去掉高相关变量并训练一个模型,通过系数大小决定最终保留变量。

值得一提的是,变量选择之所以要按以上方式一组一组的选,而非单独计算每个变量的贡献(如IV值)进行选择,主要是因为这里衍生的变量多互为相关,而非相互独立,这就导致有些单变量可能存在冗余信息,而有些单变量可能自身效果不明显,但和其他一些变量一起入模型则显现出效果。
这一套方法最终带来多少模型效果提升呢?也不必抱有很大的预期,总的来说效果有所提升,但和机器学习模型相比还有差距,可以看下他们的试验结果,如下图(其中RR代表原始变量训练的岭回归结果,SVR是支持向量机回归结果,RF是随机森林结果,AFR123分别代表一度、两度、三度衍生后的autofeat结果)。只能说时间和计算内存都够用的情况下可以试一试。
在这里插入图片描述

2.3 autofeat使用方法

autofeat支持pip安装,并提供了和sklearn同样方式的调用接口:

# 初始化模型
model = AutoFeatRegressor()
# 训练模型,并得到一个带有原始变量和所有衍生变量的数据集df
df = model.fit_transform(X, y)
# 预测新的样本集
y_pred = model.predict(X_test)
# 也可以通过调用transform在一个新的样本集上得到所有衍生变量
df_test = model.transform(X_test)

#另外也可以单独使用2.2中提到的特征选择方法
FeatureSelector

最后附上论文原文链接供大家学习。

3、Tsfresh

tsfresh主要功能是可以将时间序列中的各种信号提取为特征。它可以在这样的场景中去应用:当我们判断一个客户是否有逾期风险时,我们不应该仅仅通过其申请时点的特征状态去判断,也应该通过其特征的历史变化去判断,比如常见的趋势、历史最大异常值等等。而tsfresh这个包就可以帮助我们在时间序列中将各种信号都提取出来,包括绝对能量(时间序列中所有样本点的平方加和)、各种差分、近似熵、自回归系数、自相关性、各种窗口中的各种统计值、小波分析系数等等,每一个变量的时间序列都能提取出700多个变量,可以在时间变化维度上有效扩充特征维度。
简单使用方法如下:

df数据格式要求:
id	time	value1	value2
101	04-15	1		3
101	04-17	2		4
101	04-12	2		1
102	03-12	6		2
102	03-11	8		2
102	03-20	2		1

from tsfresh import extract_features
##ts_features是一个以id为index,包含所有时间序列衍生变量的dataframe
ts_features = extract_features(df, column_id='id', column_sort='time')

其它具体细节可以参考官网详细介绍。

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. 营销获客 2. 贷前风控 2.1 贷前审查 2.2 反欺诈 2.3 风控策略 2.4 风控建模 2.5 数据管理 风控总监训练营 ......................................................................................................792 4 节课玩转信用评分卡模型....................................................................................792 如何搭建虚拟信用卡风控体系 ...............................................................................792 风控大牛手把手教你搭建企业级信用评分模型.....................................................792 2 大维度全面ᨀ升催收效率....................................................................................792 3 堂课,从 0-1 掌握基于数据驱动的风险定价核心...............................................792 如何打造现金贷产品的风控体系?........................................................................792 解密 P2P 网贷备案——专家教你如何正确应对备案..............................................793 区块链的前世今生及其应用 ...................................................................................793 区块链热潮下不可不知的法律风险:法律专家权威解读区块链、代币等案例与法律 分析 .........................................................................................................................793 牌照决定生死,现金贷及 P2P 如何拿牌?............................................................793
《Python金融大数据风控建模实战:基于机器学习》是一本介绍如何使用Python进行金融大数据风险控制建模的实践指南。本书主要包括以下内容。 首先,本书详细介绍了使用Python进行金融大数据处理的基础知识。读者将了解如何使用Python进行数据清洗、特征工程以及数据可视化等操作。这些基础知识对于建立可靠的金融风险模型至关重要。 其次,本书介绍了机器学习在金融风控建模中的应用。读者将学习常用的机器学习算法,包括逻辑回归、决策树、随机森林等。同时,本书还介绍了如何使用交叉验证和网格搜索等技术来选择最佳的模型参数。 另外,本书还提供了一些实际案例,介绍了如何使用Python进行金融大数据风控建模的实战经验。这些案例包括信用评级、欺诈检测等实际应用场景,读者可以通过实际案例来学习如何将机器学习算法应用于真实的金融风控问题。 最后,本书还介绍了一些工具和库,如pandas、numpy和scikit-learn等,这些工具和库能够帮助读者更高效地使用Python进行金融大数据风控建模。 总的来说,《Python金融大数据风控建模实战:基于机器学习》是一本非常实用的书籍,对于想要学习如何使用Python进行金融大数据风控建模的读者来说,具有很高的参考价值。通过阅读本书,读者可以了解到如何使用机器学习技术来解决金融风险问题,了解如何应用Python工具和库进行数据处理和模型建立,并通过实际案例来提高实践能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值