前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python手写了 35 种可解释的特征工程方法

Python手写了 35 种可解释的特征工程方法

作者头像
Sam Gor
发布2020-09-14 14:43:01
1.2K0
发布2020-09-14 14:43:01
举报
文章被收录于专栏:SAMshareSAMshareSAMshare

背景

特征的挖掘,是一个 算法工程师 or 数据挖掘工程师,最最最基本的能力。实际业务中,许多数时候数据源和建模目标都是确定的,这时候特征工程几乎就决定了最终模型的业务效果。即使是表示学习横行的当下,在风控和推荐系统中依然大量的使用着手工的特征进行建模。本文将介绍机器学习中的2大类特征深入挖掘方法(特征聚合&特征交叉),以及其中35种特征衍生方案。希望能为对此处经验较少的读者提供一些帮助。

通过一个简单的小漫画,来看看机器学习&数据挖掘中的特征工程。

目录

背景 特征生成 特征聚合 特征交叉 总结 书籍推荐

一、特征生成

特征生成,即基础的特征构造。通常只需从平台数据库或数据仓库中,通过SQL(结构化查询语言)语句,根据确定下来的样本标识直接提取即可。然而直接用通过简单匹配得到的变量进行建模,其效果通常较差。在数据源与数据质量确定的情况下,特征工程将极大程度上决定评分卡模型的效果,因此特征工程是建模人员的核心能力之一。特征工程包括特征生成与特征变换。

本文主要介绍其中的特征生成。即如何深入挖掘特征的信息。

用于预测的特征的原始变量,必须是在模型开发样本和将来模型实施时均可观察到的信息。特征的预测能力主要来源于它们与目标变量的相关性(Correlation)和逻辑因果关系。传统的特征提炼主要靠建模人员的直觉、长期经验的积累和数据挖掘技术的应用。此外,还有一些通过表示学习自动抽取关键特征的技术,如神经网络等模型。本文不介绍这些内容,感兴趣的读者可以参考小白书《智能风控:原理、算法与工程实践》中的案例(如FM特征二阶交叉、LSTM序列挖掘、GBDT+LR特征局部交叉等)。

特征工程=特征提取+特征衍生+特征编码+特征筛选

这篇文章不会去介绍的内容包括:数据源分类、频次统计、时长统计、归一化、one-hot编码、WOE编码、过滤式、嵌入式、包裹式 特征筛选等内容。感兴趣的朋友可以参考一下书中的内容。小黑书对这一块做了比较详细地介绍,也确定了基本的流程与架构。

本文的主要的目的是,为 对如何深入挖掘变量感到迷茫的读者,提供一个技术框架与思考方向。

二、特征聚合

接下来开始介绍第一种特征挖掘的方法,叫作特征聚合,即将每个样本的变量通过各种运算,将单个特征的多个时间节点取值进行聚合的操作。特征聚合是传统评分卡建模的主要特征构造方法。本节为读者介绍业内实用效果较好的35种基于时间序列进行特征聚合的方法。

首先要提到的是,为了方便读者使用,我将本文用到的所有聚合函数写成了一个多进程版本的衍生函数,如果读者有需要可以跳转到代码块自取。他的调用方法非常简单。如下:

#读取数据
data = pd.read_excel('textdata.xlsx')
#指定参与衍生的变量名
FEATURE_LIST = ['ft','gt']
#指定聚合月份
P_LIST = [3,6]
#调用变量衍生函数
gen = feature_generation(data, FEATURE_LIST, P_LIST)
df = gen.fit_generate()

读到这里可能有的同学一头雾水。接下来详细的剖析一下这35种特征衍生方案。

举一个简单的例子,现在计算每个用户额度使用率,记为特征ft ,按照时间轴以月份p为切片展开,得到申请前30天内的额度使用率,申请前30天至60天内的额度使用率,申请前60天至90天内的额度使用率,…,申请前330天至360天内的额度使用率,于是得到相当于一个用户的12个特征,如图所示。

可以根据这个时间序列进行基于经验的人工特征衍生,例如设计一个函数,计算最近p个月特征值大于0的月份数。

1)计算最近p个月特征inv大于0的月份数。

def Num(inv, p):      
  df = data.loc[:,inv+'1':inv+str(p)]      
  auto_value = np.where(df>0,1,0).sum(axis=1)      
  return inv+'_num'+str(p), auto_value

之所以要用p和inv来代替月份和特征名,是因为在工业界通常都是对高维特征进行批量处理。所有设计的函数最好要有足够高的灵活性,能兼容特征和月份的灵活指定。对于函数Num来说,传入不同的inv取值,会对不同的特征进行计算,而指定不同的p值,就会对不同的月份做聚合。因此只需要遍历每一个inv和每一种p的取值,就可以衍生出更深层次的特征。

最下面有统一的代码,不过为了帮助大家掌握规律,又举了3个其中的例子。

2)计算最近p个月特征inv等于0的月份数。

def Nmz(inv, p):  
    df = data.loc[:,inv+'1':inv+str(p)]  
    auto_value = np.where(df==0,1,0).sum(axis=1)  
    return inv+'_nmz'+str(p), auto_value

3)求最近p个月特征inv大于0的月份数是否大于等于1。

def Evr(inv, p):  
    df = data.loc[:,inv+'1':inv+str(p)]  
    Arr = np.where(df>0,1,0).sum(axis=1)  
    auto_value = np.where(arr,1,0)  
    return inv+'_evr'+str(p), auto_value

4)计算最近p个月特征inv的均值。

def Avg(inv, p):  
    df = data.loc[:,inv+'1':inv+str(p)]  
    auto_value = np.nanmean(df, axis=1)  
    return inv+'_avg'+str(p), auto_value

...

等等。

我们一共有35种特征聚合的方法。在书中有详细的介绍。这篇文章为了节约篇幅,具体的解释和python代码,可以参考下面这个封装好的批量调用函数 feature_generation() 。原谅我不能提供企业级的分布式脚本,不过里面的每个函数都写了备注。比心❤。

虽然花了很多功夫打磨这个函数,但您其实在实际工作中是否使用了这个函数,我认为并没那么重要。关键是要知道哪些变量衍生是有意义的。在实际业务或者比赛中,知道如何进行特征聚合。或者对手工特征工程究竟有哪些思路有一个好的认识,我觉得都是更有价值的。

下面奉上此次的多进程(可单进程)版本变量衍生函数。友情提醒,服务器核数少于35个或数据量不大不要开多进程。不然进程开销远大于变量衍生的计算过程。

import pandas as pd
import numpy as np
import multiprocessing
import warnings
warnings.filterwarnings('ignore')
pd.set_option('mode.chained_assignment', None)

# 首先执行下面的全部函数
class feature_generation(object):

    def __init__(self, data, feature_list, p_list, core_num=1):
        self.data = data  # 包含基础变量的数据
        self.feature_list = feature_list  # 变量名前缀
        self.p_list = p_list  # 变量名前缀
        self.df = pd.DataFrame([])  # 用于收集最终变量的数据框
        self.func_list = ['Num', 'Nmz', 'Evr', 'Avg', 'Tot', 't2T', 'Max', 'Min', 'Msg', 'Msz',
                          'Cav', 'Cmn', 'Std', 'Cva', 'Cmm', 'Cnm', 'Cxm', 'Cxp', 'Ran', 'Nci', 'Ncd',
                          'Ncn', 'Pdn', 'Cmx', 'Cmp', 'Cnp', 'Msx', 'Trm', 'Bup', 'Mai', 'Mad',
                          'Rpp', 'Dpp', 'Mpp', 'Npp']
        self.core_num = core_num  # 35个函数对应35个核

    def fit_generate(self):
        """
        通过循环变量名inv和月份p,
        实现全部变量的衍生
        """
        for self.inv in self.feature_list:
            for self.p in self.p_list:
                var_df = self.generate(self.inv, self.p)
                self.df = pd.concat([self.df, var_df], axis=1)
        return self.df

    def generate(self, inv, p):
        """
        多进程,衍生变量主函数
        """
        var_df = pd.DataFrame([])
        pool = multiprocessing.Pool(self.core_num)
        results = [pool.apply_async(self.auto_var, [func]) for func in self.func_list]
        pool.close()
        pool.join()
        for i in range(len(results)):
            try:
                columns, value = results[i].get()
                var_df[columns] = value
            except:
                continue
        return var_df

    # 定义批量调用双参数的函数,具体函数请往下面看。
    def auto_var(self, func):
        if func == 'Num':
            try:
                return self.Num(self.inv, self.p)
            except:
                print("Num PARSE ERROR", self.inv, self.p)
        elif func == 'Nmz':
            try:
                return self.Nmz(self.inv, self.p)
            except:
                print("Nmz PARSE ERROR", self.inv, self.p)

        elif func == 'Evr':
            try:
                return self.Evr(self.inv, self.p)
            except:
                print("Evr PARSE ERROR", self.inv, self.p)
        elif func == 'Avg':
            try:
                return self.Avg(self.inv, self.p)
            except:
                print("Avg PARSE ERROR", self.inv, self.p)
        elif func == 'Tot':
            try:
                return self.Tot(self.inv, self.p)
            except:
                print("Tot PARSE ERROR", self.inv, self.p)
        elif func == 'Tot2T':
            try:
                return self.Tot2T(self.inv, self.p)
            except:
                print("Tot2T PARSE ERROR", self.inv, self.p)
        elif func == 'Max':
            try:
                return self.Max(self.inv, self.p)
            except:
                print("Tot PARSE ERROR", self.inv, self.p)
        elif func == 'Min':
            try:
                return self.Min(self.inv, self.p)
            except:
                print("Min PARSE ERROR", self.inv, self.p)
        elif func == 'Msg':
            try:
                return self.Msg(self.inv, self.p)
            except:
                print("Msg PARSE ERROR", self.inv, self.p)
        elif func == 'Msz':
            try:
                return self.Msz(self.inv, self.p)
            except:
                print("Msz PARSE ERROR", self.inv, self.p)
        elif func == 'Cav':
            try:
                return self.Cav(self.inv, self.p)
            except:
                print("Cav PARSE ERROR", self.inv, self.p)
        elif func == 'Cmn':
            try:
                return self.Cmn(self.inv, self.p)
            except:
                print("Cmn PARSE ERROR", self.inv, self.p)
        elif func == 'Std':
            try:
                return self.Std(self.inv, self.p)
            except:
                print("Std PARSE ERROR", self.inv, self.p)
        elif func == 'Cva':
            try:
                return self.Cva(self.inv, self.p)
            except:
                print("Cva PARSE ERROR", self.inv, self.p)
        elif func == 'Cmm':
            try:
                return self.Cmm(self.inv, self.p)
            except:
                print("Cmm PARSE ERROR", self.inv, self.p)
        elif func == 'Cnm':
            try:
                return self.Cnm(self.inv, self.p)
            except:
                print("Cnm PARSE ERROR", self.inv, self.p)
        elif func == 'Cxm':
            try:
                return self.Cxm(self.inv, self.p)
            except:
                print("Cxm PARSE ERROR", self.inv, self.p)
        elif func == 'Cxp':
            try:
                return self.Cxp(self.inv, self.p)
            except:
                print("Cxp PARSE ERROR", self.inv, self.p)
        elif func == 'Ran':
            try:
                return self.Ran(self.inv, self.p)
            except:
                print("Ran PARSE ERROR", self.inv, self.p)
        elif func == 'Nci':
            try:
                return self.Nci(self.inv, self.p)
            except:
                print("Nci PARSE ERROR", self.inv, self.p)
        elif func == 'Pdn':
            try:
                return self.Pdn(self.inv, self.p)
            except:
                print("Pdn PARSE ERROR", self.inv, self.p)
        elif func == 'Cmx':
            try:
                return self.Cmx(self.inv, self.p)
            except:
                print("Cmx PARSE ERROR", self.inv, self.p)
        elif func == 'Cmp':
            try:
                return self.Cmp(self.inv, self.p)
            except:
                print("Cmp PARSE ERROR", self.inv, self.p)
        elif func == 'Cnp':
            try:
                return self.Cnp(self.inv, self.p)
            except:
                print("Cnp PARSE ERROR", self.inv, self.p)
        elif func == 'Msx':
            try:
                return self.Msx(self.inv, self.p)
            except:
                print("Msx PARSE ERROR", self.inv, self.p)
        elif func == 'Trm':
            try:
                return self.Trm(self.inv, self.p)
            except:
                print("Trm PARSE ERROR", self.inv, self.p)
        elif func == 'Bup':
            try:
                return self.Bup(self.inv, self.p)
            except:
                print("Bup PARSE ERROR", self.inv, self.p)
        elif func == 'Ncd':
            try:
                return self.Ncd(self.inv, self.p)
            except:
                print("Ncd PARSE ERROR", self.inv, self.p)
        elif func == 'Ncn':
            try:
                return self.Ncn(self.inv, self.p)
            except:
                print("Ncn PARSE ERROR", self.inv, self.p)
        elif func == 'Mai':
            try:
                return self.Mai(self.inv, self.p)
            except:
                print("Mai PARSE ERROR", self.inv, self.p)
        elif func == 'Mad':
            try:
                return self.Mad(self.inv, self.p)
            except:
                print("Mad PARSE ERROR", self.inv, self.p)
        elif func == 'Rpp':
            try:
                return self.Rpp(self.inv, self.p)
            except:
                print("Rpp PARSE ERROR", self.inv, self.p)
        elif func == 'Dpp':
            try:
                return self.Dpp(self.inv, self.p)
            except:
                print("Dpp PARSE ERROR", self.inv, self.p)
        elif func == 'Mpp':
            try:
                return self.Mpp(self.inv, self.p)
            except:
                print("Mpp PARSE ERROR", self.inv, self.p)
        elif func == 'Npp':
            try:
                return self.Npp(self.inv, self.p)
            except:
                print("Npp PARSE ERROR", self.inv, self.p)

    """
    35个衍生函数,inv为变量名,p为月份(时间切片)
    """

    #计算最近p个月特征inv大于0的月份数。
    def Num(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.where(df > 0, 1, 0).sum(axis=1)
        return inv + '_num' + str(p), auto_value

    #计算最近p个月特征inv等于0的月份数。
    def Nmz(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.where(df == 0, 1, 0).sum(axis=1)
        return inv + '_nmz' + str(p), auto_value

    #求最近p个月特征inv大于0的月份数是否大于等于1。
    def Evr(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        arr = np.where(df > 0, 1, 0).sum(axis=1)
        auto_value = np.where(arr, 1, 0)
        return inv + '_evr' + str(p), auto_value

    #计算最近p个月特征inv的均值。
    def Avg(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.nanmean(df, axis=1)
        return inv + '_avg' + str(p), auto_value

    #计算最近p个月特征inv的和。
    def Tot(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.nansum(df, axis=1)
        return inv + '_tot' + str(p), auto_value

    #最近(2, p+1)个月,特征inv的和。
    def Tot2T(self, inv, p):
        df = self.data.loc[:, inv + '2':inv + str(p + 1)].values
        auto_value = df.sum(1)
        return inv + '_tot2t' + str(p), auto_value

    #计算最近p个月特征inv的最大值。
    def Max(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = np.nanmax(df, axis=1)
        return inv + '_max' + str(p), auto_value

    #计算最近p个月特征inv的最小值。
    def Min(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.nanmin(df, axis=1)
        return inv + '_min' + str(p), auto_value

    #计算最近p个月,最近一次特征inv大于0到现在的月份数。
    def Msg(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        df_value = np.where(df > 0, 1, 0)
        auto_value = []
        for i in range(len(df_value)):
            row_value = df_value[i, :]
        if row_value.max() <= 0:
            indexs = '0'
            auto_value.append(indexs)
        else:
            indexs = 1
        for j in row_value:
            if j > 0:
                break
        indexs += 1
        auto_value.append(indexs)
        return inv + '_msg' + str(p), auto_value

    #计算最近p个月,最近一次特征inv等于0到现在的月份数。
    def Msz(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        df_value = np.where(df == 0, 1, 0)
        auto_value = []
        for i in range(len(df_value)):
            row_value = df_value[i, :]
        if row_value.max() <= 0:
            indexs = '0'
            auto_value.append(indexs)
        else:
            indexs = 1
        for j in row_value:
            if j > 0:
                break
        indexs += 1
        auto_value.append(indexs)
        return inv + '_msz' + str(p), auto_value

    #计算当月inv/(最近p个月inv的均值)。
    def Cav(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = df[inv + '1'] / (np.nanmean(df, axis=1) + 1e-10)
        return inv + '_cav' + str(p), auto_value

    #计算当月inv/(最近p个月inv的最小值)。
    def Cmn(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = df[inv + '1'] / (np.nanmin(df, axis=1) + 1e-10)
        return inv + '_cmn' + str(p), auto_value

    #计算最近p个月,每两个月间inv的增长量的最大值。
    def Mai(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        value_lst = []
        for k in range(len(df_value) - 1):
            minus = df_value[k] - df_value[k + 1]
        value_lst.append(minus)
        auto_value.append(np.nanmax(value_lst))
        return inv + '_mai' + str(p), auto_value

    #计算最近p个月,每两个月间inv的减少量的最大值。
    def Mad(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        value_lst = []
        for k in range(len(df_value) - 1):
            minus = df_value[k + 1] - df_value[k]
        value_lst.append(minus)
        auto_value.append(np.nanmax(value_lst))
        return inv + '_mad' + str(p), auto_value

    #计算最近p个月特征inv的方差。
    def Std(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.nanvar(df, axis=1)
        return inv + '_std' + str(p), auto_value

    #计算最近p个月特征inv的变异系数。
    def Cva(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = np.nanmean(df, axis=1) / (np.nanvar(df, axis=1) + 1e-10)
        return inv + '_cva' + str(p), auto_value

    #计算(当月inv)-(最近p个月inv的均值)。
    def Cmm(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = df[inv + '1'] - np.nanmean(df, axis=1)
        return inv + '_cmm' + str(p), auto_value

    #计算(当月inv)-(最近p个月inv的最小值)。
    def Cnm(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = df[inv + '1'] - np.nanmin(df, axis=1)
        return inv + '_cnm' + str(p), auto_value

    #计算(当月inv)-(最近p个月inv的最大值)。
    def Cxm(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = df[inv + '1'] - np.nanmax(df, axis=1)
        return inv + '_cxm' + str(p), auto_value

    #计算((当月inv)-(最近p个月inv的最大值))/(最近p个月inv的最大值)。
    def Cxp(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        temp = np.nanmin(df, axis=1)
        auto_value = (df[inv + '1'] - temp) / (temp + 1e-10)
        return inv + '_cxp' + str(p), auto_value

    #计算最近p个月inv的极差。
    def Ran(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = np.nanmax(df, axis=1) - np.nanmin(df, axis=1)
        return inv + '_ran' + str(p), auto_value

    #计算最近p个月中,后一个月inv相比前一个月inv增长的月份数。
    def Nci(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        value_lst = []
        for k in range(len(df_value) - 1):
            minus = df_value[k] - df_value[k + 1]
        value_lst.append(minus)
        value_ng = np.where(np.array(value_lst) > 0, 1, 0).sum()
        auto_value.append(np.nanmax(value_ng))
        return inv + '_nci' + str(p), auto_value

    #计算最近p个月中,后一个月inv相比于前一个月inv减少的月份数。
    def Ncd(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        value_lst = []
        for k in range(len(df_value) - 1):
            minus = df_value[k] - df_value[k + 1]
        value_lst.append(minus)
        value_ng = np.where(np.array(value_lst) < 0, 1, 0).sum()
        auto_value.append(np.nanmax(value_ng))
        return inv + '_ncd' + str(p), auto_value

    #计算最近p个月中,相邻月份inv 相等的月份数。
    def Ncn(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        value_lst = []
        for k in range(len(df_value) - 1):
            minus = df_value[k] - df_value[k + 1]
        value_lst.append(minus)
        value_ng = np.where(np.array(value_lst) == 0, 1, 0).sum()
        auto_value.append(np.nanmax(value_ng))
        return inv + '_ncn' + str(p), auto_value

    #如果最近p个月中,inv按照月份严格递增,则返回1,否则返回0。
    def Bup(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        index = 0
        for k in range(len(df_value) - 1):
            if df_value[k] > df_value[k + 1]:
                break
        index = + 1
        if index == p:
            value = 1
        else:
            value = 0
        auto_value.append(value)
        return inv + '_bup' + str(p), auto_value

    #果最近p个月中,inv按照月份严格递减,则返回1,否则返回0。
    def Pdn(self, inv, p):
        arr = self.data.loc[:, inv + '1':inv + str(p)].values
        auto_value = []
        for i in range(len(arr)):
            df_value = arr[i, :]
        index = 0
        for k in range(len(df_value) - 1):
            if df_value[k + 1] > df_value[k]:
                break
        index = + 1
        if index == p:
            value = 1
        else:
            value = 0
        auto_value.append(value)
        return inv + '_pdn' + str(p), auto_value

    #计算最近p个月inv的修剪均值。
    def Trm(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = []
        for i in range(len(df)):
            trm_mean = list(df.loc[i, :])
        trm_mean.remove(np.nanmax(trm_mean))
        trm_mean.remove(np.nanmin(trm_mean))
        temp = np.nanmean(trm_mean)
        auto_value.append(temp)
        return inv + '_trm' + str(p), auto_value

    #计算当月inv/最近p个月的inv中的最大值。
    def Cmx(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = (df[inv + '1'] - np.nanmax(df, axis=1)) / (np.nanmax(df, axis=1) + 1e-10)
        return inv + '_cmx' + str(p), auto_value

    #计算(当月inv-最近p个月的inv均值)/inv均值。
    def Cmp(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = (df[inv + '1'] - np.nanmean(df, axis=1)) / (np.nanmean(df, axis=1) + 1e-10)
        return inv + '_cmp' + str(p), auto_value

    #计算(当月inv-最近p个月的inv最小值)/inv最小值。
    def Cnp(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        auto_value = (df[inv + '1'] - np.nanmin(df, axis=1)) / (np.nanmin(df, axis=1) + 1e-10)
        return inv + '_cnp' + str(p), auto_value

    #计算最近p个月取最大值的月份距现在的月份数。
    def Msx(self, inv, p):
        df = self.data.loc[:, inv + '1':inv + str(p)]
        df['_max'] = np.nanmax(df, axis=1)
        for i in range(1, p + 1):
            df[inv + str(i)] = list(df[inv + str(i)] == df['_max'])
        del df['_max']
        df_value = np.where(df == True, 1, 0)
        auto_value = []
        for i in range(len(df_value)):
            row_value = df_value[i, :]
        indexs = 1
        for j in row_value:
            if j == 1:
                break
        indexs += 1
        auto_value.append(indexs)
        return inv + '_msx' + str(p), auto_value

    #计算 (最近p个月的均值)/(最近p~2p个月的inv均值) 。
    def Rpp(self, inv, p):
        df1 = self.data.loc[:, inv + '1':inv + str(p)].values
        value1 = np.nanmean(df1, axis=1)
        df2 = self.data.loc[:, inv + str(p):inv + str(2 * p)].values
        value2 = np.nanmean(df2, axis=1)
        auto_value = value1 / (value2 + 1e-10)
        return inv + '_rpp' + str(p), auto_value

    #计算(最近p个月的均值)-(最近p~2p个月的inv均值)。
    def Dpp(self, inv, p):
        df1 = self.data.loc[:, inv + '1':inv + str(p)].values
        value1 = np.nanmean(df1, axis=1)
        df2 = self.data.loc[:, inv + str(p):inv + str(2 * p)].values
        value2 = np.nanmean(df2, axis=1)
        auto_value = value1 - value2
        return inv + '_dpp' + str(p), auto_value

    #计算(最近p个月的inv最大值)/(最近p~2p个月的inv最大值)。
    def Mpp(self, inv, p):
        df1 = self.data.loc[:, inv + '1':inv + str(p)].values
        value1 = np.nanmax(df1, axis=1)
        df2 = self.data.loc[:, inv + str(p):inv + str(2 * p)].values
        value2 = np.nanmax(df2, axis=1)
        auto_value = value1 / (value2 + 1e-10)
        return inv + '_mpp' + str(p), auto_value

    #计算(最近p个月的inv最小值)/(最近p~2p个月的inv最小值)。
    def Npp(self, inv, p):
        df1 = self.data.loc[:, inv + '1':inv + str(p)].values
        value1 = np.nanmin(df1, axis=1)
        df2 = self.data.loc[:, inv + str(p):inv + str(2 * p)].values
        value2 = np.nanmin(df2, axis=1)
        auto_value = value1 / (value2 + 1e-10)
        return inv + '_npp' + str(p), auto_value

需要注意,通过这种无差别聚合方法进行聚合得到的结果,通常具有较高的共线性,其所具备的信息量并无明显增加,反而会为广义线性模型带来干扰,影响模型的鲁棒性和稳定性。评分卡模型通常对于模型的稳定性要求远高于其性能。因此通常时间窗口为1年的场景下,p值会通过先验知识,人为选择3、6、12等,而不是遍历全部取值1~12。并在后续建模中,根据变量显著性、共线性等指标进行相应的特征选择。减少变量存储与数据开销。

此外,由于部分函数逻辑对p有要求(比如修剪均值需要至少p为3才能计算),所以使用了try...except结构。月份也可以换成天或者年,切片越细变量越多,但稳定性可能下降。

有的同学可能对DFS算法有所了解。并且也知道有一个开源工具叫做Featuretools,可以从理论上实现特征的无差别自动挖掘。但是在实际业务中却很少有平台真的去使用它。其原因有二:一是生产效率问题,且特征有大量信息杂糅,对变量存储和模型部署都是一种负担;二是究竟从哪些角度做特征聚合,还是由人来决定的,缺少经验指导仍然不能找到正确的挖掘方向。 而上述内容的意义,是直接给出那些经过时间沉淀后被证实好用的衍生逻辑。并且通过多进程的逻辑精准快速的完成该过程。并且同时让读者触摸人工可解释特征工程的内核。

另外提供一种便于实际落地的方案。在hive中进行特征开发,只构造基础表。可以在每次离线建模过程中衍生特征,同时根据相关性和目标相关性,在衍生过程中筛选特征,最终入模特征通过python脚本自动生成其变量生成的hql脚本。直接部署上线。从而绕过hive中撰写UDF衍生变量后存储空间过大的问题。这在合理组织表结构后是完全可以实现的。

三、特征组合

特征组合(Feature combination),又叫特征交叉(Feature crossing),指通过不同特征之间基于常识、经验、数据挖掘技术进行分段组合实现特征构造,产生包含更多信息的新特征。如将{工作日,休息日},{上午,下午}两组特征维度进行组合,可以得到四个特征维度,其交叉逻辑如表所示。

除此之外,可以通过决策树模型,基于特定指标,贪心地搜索最优的特征组合形式。本节以CART回归树为例,使用一个书中的外卖平台骑手贷的例子进行演示。数据字典如图所示。

import pandas as pd  
import numpy as np  
import os  
os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
data = pd.read_excel('./data/_data_for_tree.xlsx')  
x = data.drop('bad_ind',axis=1).copy()  
y = data.bad_ind.copy()  
from sklearn import tree  
Dtree = tree.DecisionTreeRegressor(max_depth=2, min_samples_leaf=500,
                                          min_samples_split=5000)  
dtree = dtree.fit(x,y)  
import pydotplus   
from IPython.display import Image  
from sklearn.externals.six import StringIO  
with open("dt.dot", "w") as f:  
    tree.export_graphviz(dtree, out_file=f)  
dot_data = StringIO()  
tree.export_graphviz(dtree, out_file=dot_data,  
                     feature_names=x.columns,  
                     class_names=['bad_ind'],  
                     filled=True, rounded=True,  
                     special_characters=True)  
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())   
Image(graph.create_png()) 

运行结果如下所示。

CART回归树的节点预测属性value表示当前子群中目标变量的均值。而当前标签为0和1的时候,目标变量的均值等价于标签为1的样本占当前子群样本的比例。

按照决策树结果,对本例子进行新特征构造。

x['n1'] = x.apply(lambda x:1 if x.amount_tot>9614.5 and coupon_amount_cnt<=6.0 else 0)  
x['n2'] = x.apply(lambda x:1 if x.amount_tot>9614.5 and coupon_amount_cnt>6.0 else 0)  

利用决策树实现特征的自动组合,可以有效减少建模人员的工作难度。由于LR模型缺乏非线性学习能力,因此常需要和决策树模型结合,人工构造相应特征。这个过程可以更好的利用变量的局部性质,而不是在lr中那种只能利用变量的全局性质。这也是为什么XGBoost&LightGBM&CatBoost等树模型经常有远超线性模型表现的原因之一。另一部分原因主要是来自于集成模型的偏差优化。但是经过试验可以发现,使用lr作为元模型做集成,很多时候效果也是不如决策树做元模型的。

然而特征之间的组合并非任何时候都会取得好的结果。通常在建立线性评分卡模型时,建模人员会同时使用树模型进行训练并对比评分卡与树模型的结果。若两者结果相近,通常代表特征之间的组合对模型的提升较为有限。

四、总结

本文为读者介绍了2类特征挖掘方法。其中包括35种特征聚合的方案,以及如何通过树模型提供特征交叉的指导方向。其实细心的读者可能还会在其中发现许多小的知识点。依旧是那句话,也许不对,也许没用。不过还是希望读完这篇文章对您有所帮助。感谢阅读。

- 完 -

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 SAMshare 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 目录
    • 一、特征生成
      • 二、特征聚合
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档