【Kaggle】Elo 用户忠诚度预测

1,923 阅读22分钟

前言: 本项目来源于Kaggle平台的Elo Merchant Category Recommendation,参考了《机器学习算法竞赛实战》和一些热心网友分享的资料,如果有侵权联系我删除就好,做这个项目的想法是,将这个比赛当做自己入门机器学习的学习赛,并且看到掘金上好像没有这个比赛的相关信息,因此就整理好分享出来,感兴趣的同学可以一块学习一下机器学习竞赛的完整流程,所用到的参考资料已放于最后引用

建议使用32G内存及上的配置实现,数据集稍微有点大,若无条件可以到Ai Studio尝试复现哇,一键运行

一、赛题背景介绍

  • 巴西支付品牌Elo
    本次竞赛其实是由巴西最大的支付品牌之一的Elo和Kaggle合作举办的比赛,奖金和数据都由Elo公司提供。谈到支付品牌,国内用户首先会想到类似支付宝、PayPal这些带有浓烈互联网色彩的支付品牌,但是在巴西,线上支付更多是由本地银行主导,且线上支付的信贷产品也主要以信用卡为主。Elo就是这样的一家公司,在2011年由巴西三家主要银行合资创立,主要负责线上支付业务,并且以信用卡作为核心金融产品,目前已发放超过1.1亿张信用卡,是巴西最大的本地在线支付品牌之一。
4

并且,Elo不仅是支付入口,更是一个“o2o”平台,通过App,用户可以查阅本地餐饮旅馆电影旅游机票等各项服务,并支持信用卡在线支付。形象点理解,就好比把美团主页移到支付宝,并且支付宝没有花呗,取而代之的是自己发行的信用卡。或者更加形象的理解,就类似国内招行信用卡掌上生活的业务模式:

3
  • 业务目标:更好的进行本地服务推荐
    在官方给出的说明中,我们不难发现,Elo使用机器学习算法技术的核心目的,是为了更好的在App内为用户推荐当地吃穿住行的商家服务,包括热门餐厅展示、优惠折扣提醒等(非常类似上图掌上生活首页的推荐)。也就是说,其根本目的是为了推荐,或者说为每个用户进行更加个性化的推荐,也就是赛题标题中的所写的:Merchant Category Recommendation(商户类别推荐)。但是,需要注意的是,本次竞赛的建模目标却和推荐系统并不直接相关。赛题说明中,在介绍完业务目标之后,紧接着就介绍了本次赛题的目标:对用户的忠诚度评分进行预测。

  • 算法目标:用户忠诚度评分预测
    所谓用户忠诚度评分,通过后续查看Evaluation不难发现,其实就是对每个用户的评分进行预测,本质上是个回归问题。
    相信刚接触到本次赛题的小伙伴,一定会觉得赛题说明和建模目标有些“文不对题”,毕竟用户忠诚度评分貌似和个性化推荐并无直接关系,尤其此处用户忠诚度评分并不是针对某类商品或者某类商家的忠诚度评分。
    围绕这个问题,赛题说明给出了非常笼统的解释,只是简单说到通过对用户忠诚度评分,能够为其提供最相关的机会(serve the most relevant opportunities to individuals),或者可以理解成是用户较为中意的线下服务,并且帮助ELo节省活动成本,为用户提供更好的体验。其实也就等于是没有解释忠诚度评分和推荐系统到底是什么关系。Kaggle本赛题论坛上也有很多相关讨论帖,官方给出的解释大意是通过忠诚度评分给用户推荐商铺和商品,这个过程并不是一个传统的协同过滤或者推荐系统进行推荐的过程,无论如何,先做好忠诚度预测就好。

二、数据集简介

本次赛题数据较多、数据量也相对较大,部分数据甚至无法直接通过普通Excel直接打开。接下来我们快速了解每个数据集的基本含义:

image-20211021110745226

  总的来说,上述7个数据文件大概可以分为三类,其一是基本信息类数据集,包括Data_Dictionary和sample_submission。其中Data_Dictionary数据集是所有数据的数据字典,即包括了所有数据各字段的含义,而sample_submission则是提交结果时的范例数据。

  • Data Dictionary/Data_Dictionary:数据字典

  所有其他数据表中每个字段的含义,相当于是其他各数据表的说明书。数据字典包含多个sheet,每个sheet对应一个数据表的字段和解释:

image-20211020204816830

  最终建模结果提交格式,也就是以“一个id”+“对应预测结果”的格式进行提交。据此我们也能发现,实际上我们是需要预测每个card_id的用户忠诚度评分。

image-20211020211206067

RMSE的计算过程如下:

RMSE=1ni=1n(yiy^i)2RMSE= \sqrt{\frac{1}{n}\sum^n_{i=1}(y_i-\hat y_i)^2}

  然后就是完成比赛的必要数据,也就是train和test两个数据集。顾名思义,train数据集就是训练数据,test就是测试数据集,二者特征一致,极简情况下我们可以直接在train上训练模型,在test上进行预测。

  最后一类则是补充数据集,也就是 'historical_transactions.csv'、'new_merchant_transactions.csv'和'merchants.csv',其中前两个数据集记录了训练集和测试集信用卡的消费记录,而最后一个数据集则是前两个数据集中商铺信息(某特征)的进一步解释。在实际建模过程中,纳入更多数据进行规律挖掘,则有可能达到更好的效果。

三、数据探索与清洗

  在对train和test数据集完成探索性分析之后,接下来我们需要进一步围绕官方给出的商户数据与信用卡交易数据进行解读和分析,并对其进行数据清洗,从而为后续的特征工程和算法建模做准备。

  一般来说,在数据解读、数据探索和初步数据清洗都是同步进行的,都是前期非常重要的工作事项。其中,数据解读的目的是为了快速获取数据集的基本信息,通过比对官方给出的字段解释,快速了解数据集的字段含义,这对于许多复杂数据场景下的建模是非常有必要的。而数据探索,顾名思义,就是快速了解数据集的基本数据情况,主要工作包括数据正确性校验和数据质量分析,核心目的是为了能够快速了解各字段的基本情况,包括默认各字段的数据类型、数据集是否存在数据不一致的情况、数据集重复值情况、缺失值情况等,当然,通过一系列的数据探索,也能够快速加深对数据集的理解。当然,数据探索结束之后,就需要进行数据清洗了,所谓数据清洗,指的是在建模/特征工程之前进行的必要的调整,以确保后续操作可执行,包括数据字段类型调整、重复值处理、缺失值处理等等,当然,有些操作可能在后续会进行些许优化,比如数据清洗阶段我们可以先尝试进行较为简单的缺失值填补,在后续的建模过程中我们还可以根据实际建模结果来调整缺失值填补策略。

  我们也可将数据探索与数据清洗的过程总结如下:

image-20211022154015344

  我们将对商户数据、交易数据的三张表进行数据探索和数据清洗。

  由于该项工作较为繁琐,我们简单总结上述针对商户数据和交易数据的完整步骤如下:

商户数据merchants.csv

  • 划分连续字段和离散字段;
  • 对字符型离散字段进行字典排序编码;
  • 对缺失值处理,此处统一使用-1进行缺失值填充,本质上是一种标注;
  • 对连续性字段的无穷值进行处理,用该列的最大值进行替换;
  • 去除重复数据;

交易数据new_merchant_transactions.csv和historical_transactions.csv

  • 划分字段类型,分为离散字段、连续字段和时间字段;
  • 和商户数据的处理方法一样,对字符型离散字段进行字典排序,对缺失值进行统一填充;
  • 对新生成的Object字段进行字典排序编码;

3.1 创建清洗后数据

  结合训练集和测试集的清洗流程,我们可以在此统一执行所有数据的数据清洗工作,并将其最终保存为本地文件,方便后续特征工程及算法建模过程使用,其流程如下:

# 读取数据

import gc
import time
import numpy as np
import pandas as pd
from datetime import datetime
    
train = pd.read_csv('eloData/train.csv')
test =  pd.read_csv('eloData/test.csv')
merchant = pd.read_csv('eloData/merchants.csv')
new_transaction = pd.read_csv('eloData/new_merchant_transactions.csv')
history_transaction = pd.read_csv('eloData/historical_transactions.csv')
    
# 字典编码函数
def change_object_cols(se):
    value = se.unique().tolist()
    value.sort()
    return se.map(pd.Series(range(len(value)), index=value)).values

    
# 训练集/测试集的数据预处理

# 对首次活跃月份进行编码
se_map = change_object_cols(train['first_active_month'].append(test['first_active_month']).astype(str))
train['first_active_month'] = se_map[:train.shape[0]]
test['first_active_month'] = se_map[train.shape[0]:]
    
# 测试集/训练集导出与内存清理
!mkdir preprocess
train.to_csv("preprocess/train_pre.csv", index=False)
test.to_csv("preprocess/test_pre.csv", index=False)

del train
del test
gc.collect()
    
# 商户信息预处理

# 1、根据业务含义划分离散字段category_cols与连续字段numeric_cols。
category_cols = ['merchant_id', 'merchant_group_id', 'merchant_category_id',
       'subsector_id', 'category_1',
       'most_recent_sales_range', 'most_recent_purchases_range',
       'category_4', 'city_id', 'state_id', 'category_2']
numeric_cols = ['numerical_1', 'numerical_2',
     'avg_sales_lag3', 'avg_purchases_lag3', 'active_months_lag3',
       'avg_sales_lag6', 'avg_purchases_lag6', 'active_months_lag6',
       'avg_sales_lag12', 'avg_purchases_lag12', 'active_months_lag12']

# 2、对非数值型的离散字段进行字典排序编码。
for col in ['category_1', 'most_recent_sales_range', 'most_recent_purchases_range', 'category_4']:
    merchant[col] = change_object_cols(merchant[col])
    
# 3、为了能够更方便统计,进行缺失值的处理,对离散字段统一用-1进行填充。
merchant[category_cols] = merchant[category_cols].fillna(-1)


# 4、对离散型字段探查发现有正无穷值,这是特征提取以及模型所不能接受的,因此需要对无限值进行处理,此处采用最大值进行替换。
inf_cols = ['avg_purchases_lag3', 'avg_purchases_lag6', 'avg_purchases_lag12']
merchant[inf_cols] = merchant[inf_cols].replace(np.inf, merchant[inf_cols].replace(np.inf, -99).max().max())

# 5、平均值进行填充,后续有需要再进行优化处理。
for col in numeric_cols:
    merchant[col] = merchant[col].fillna(merchant[col].mean())
    
# 6、去除与transaction交易记录表格重复的列,以及merchant_id的重复记录。
duplicate_cols = ['merchant_id', 'merchant_category_id', 'subsector_id', 'category_1', 'city_id', 'state_id', 'category_2']
merchant = merchant.drop(duplicate_cols[1:], axis=1)
merchant = merchant.loc[merchant['merchant_id'].drop_duplicates().index.tolist()].reset_index(drop=True)

# 与处理完后先不着急导出或删除,后续需要和交易数据进行拼接。
    
# 交易数据预处理
    
# 1、为了统一处理,首先拼接new和history两张表格,后续可以month_lag>=0进行区分。
transaction = pd.concat([new_transaction, history_transaction], axis=0, ignore_index=True)
del new_transaction
del history_transaction
gc.collect()

# 2、同样划分离散字段、连续字段以及时间字段。
numeric_cols = [ 'installments', 'month_lag', 'purchase_amount']
category_cols = ['authorized_flag', 'card_id', 'city_id', 'category_1',
       'category_3', 'merchant_category_id', 'merchant_id', 'category_2', 'state_id',
       'subsector_id']
time_cols = ['purchase_date']

# 3、可仿照merchant的处理方式对字符型的离散特征进行字典序编码以及缺失值填充。
for col in ['authorized_flag', 'category_1', 'category_3']:
    transaction[col] = change_object_cols(transaction[col].fillna(-1).astype(str))
transaction[category_cols] = transaction[category_cols].fillna(-1)
transaction['category_2'] = transaction['category_2'].astype(int)

# 4、进行时间段的处理,简单起见进行月份、日期的星期数(工作日与周末)、以及
# 时间段(上午、下午、晚上、凌晨)的信息提取。
transaction['purchase_month'] = transaction['purchase_date'].apply(lambda x:'-'.join(x.split(' ')[0].split('-')[:2]))
transaction['purchase_hour_section'] = transaction['purchase_date'].apply(lambda x: x.split(' ')[1].split(':')[0]).astype(int)//6
transaction['purchase_day'] = transaction['purchase_date'].apply(lambda x: datetime.strptime(x.split(" ")[0], "%Y-%m-%d").weekday())//5                                                                    
del transaction['purchase_date']

# 5、对新生成的购买月份离散字段进行字典序编码。
transaction['purchase_month'] = change_object_cols(transaction['purchase_month'].fillna(-1).astype(str))

# 完成交易数据预处理后,即可进行交易数据和商铺数据的表格合并。
  • 表格合并

  在合并的过程中,有两种处理方案,其一是对缺失值进行-1填补,然后将所有离散型字段化为字符串类型(为了后续字典合并做准备),其二则是新增两列,分别是purchase_day_diff和purchase_month_diff,其数据为交易数据以card_id进行groupby并最终提取出purchase_day/month并进行差分的结果。

# 方案一代码

# 为了方便特征的统一计算将其merge合并,重新划分相应字段种类。
cols = ['merchant_id', 'most_recent_sales_range', 'most_recent_purchases_range', 'category_4']
transaction = pd.merge(transaction, merchant[cols], how='left', on='merchant_id')

numeric_cols = ['purchase_amount', 'installments']

category_cols = ['authorized_flag', 'city_id', 'category_1',
       'category_3', 'merchant_category_id','month_lag','most_recent_sales_range',
                 'most_recent_purchases_range', 'category_4',
                 'purchase_month', 'purchase_hour_section', 'purchase_day']

id_cols = ['card_id', 'merchant_id']

transaction[cols[1:]] = transaction[cols[1:]].fillna(-1).astype(int)
transaction[category_cols] =transaction[category_cols].fillna(-1).astype(str)

# 随后将其导出为transaction_d_pre.csv

transaction.to_csv("preprocess/transaction_d_pre.csv", index=False)

# 清空释放内存

del transaction
gc.collect()
# 方案二代码

merchant = pd.read_csv('eloData/merchants.csv')
new_transaction = pd.read_csv('eloData/new_merchant_transactions.csv')
history_transaction = pd.read_csv('eloData/historical_transactions.csv')

# 1、根据业务含义划分离散字段category_cols与连续字段numeric_cols。
category_cols = ['merchant_id', 'merchant_group_id', 'merchant_category_id',
       'subsector_id', 'category_1',
       'most_recent_sales_range', 'most_recent_purchases_range',
       'category_4', 'city_id', 'state_id', 'category_2']
numeric_cols = ['numerical_1', 'numerical_2',
     'avg_sales_lag3', 'avg_purchases_lag3', 'active_months_lag3',
       'avg_sales_lag6', 'avg_purchases_lag6', 'active_months_lag6',
       'avg_sales_lag12', 'avg_purchases_lag12', 'active_months_lag12']

# 2、对非数值型的离散字段进行字典排序编码。
for col in ['category_1', 'most_recent_sales_range', 'most_recent_purchases_range', 'category_4']:
    merchant[col] = change_object_cols(merchant[col])
    
# 3、为了能够更方便统计,进行缺失值的处理,对离散字段统一用-1进行填充。
merchant[category_cols] = merchant[category_cols].fillna(-1)


# 4、对离散型字段探查发现有正无穷值,这是特征提取以及模型所不能接受的,因此需要对无限值进行处理,此处采用最大值进行替换。
inf_cols = ['avg_purchases_lag3', 'avg_purchases_lag6', 'avg_purchases_lag12']
merchant[inf_cols] = merchant[inf_cols].replace(np.inf, merchant[inf_cols].replace(np.inf, -99).max().max())

# 5、平均值进行填充,后续有需要再进行优化处理。
for col in numeric_cols:
    merchant[col] = merchant[col].fillna(merchant[col].mean())
    
# 6、去除与transaction交易记录表格重复的列,以及merchant_id的重复记录。
duplicate_cols = ['merchant_id', 'merchant_category_id', 'subsector_id', 'category_1', 'city_id', 'state_id', 'category_2']
merchant = merchant.drop(duplicate_cols[1:], axis=1)
merchant = merchant.loc[merchant['merchant_id'].drop_duplicates().index.tolist()].reset_index(drop=True)
    
# 1、为了统一处理,首先拼接new和history两张表格,后续可以month_lag>=0进行区分。
transaction = pd.concat([new_transaction, history_transaction], axis=0, ignore_index=True)
del new_transaction
del history_transaction
gc.collect()

# 2、同样划分离散字段、连续字段以及时间字段。
numeric_cols = [ 'installments', 'month_lag', 'purchase_amount']
category_cols = ['authorized_flag', 'card_id', 'city_id', 'category_1',
       'category_3', 'merchant_category_id', 'merchant_id', 'category_2', 'state_id',
       'subsector_id']
time_cols = ['purchase_date']

# 3、可仿照merchant的处理方式对字符型的离散特征进行字典序编码以及缺失值填充。
for col in ['authorized_flag', 'category_1', 'category_3']:
    transaction[col] = change_object_cols(transaction[col].fillna(-1).astype(str))
transaction[category_cols] = transaction[category_cols].fillna(-1)
transaction['category_2'] = transaction['category_2'].astype(int)

# 4、进行时间段的处理,简单起见进行月份、日期的星期数(工作日与周末)、以及
# 时间段(上午、下午、晚上、凌晨)的信息提取。
transaction['purchase_month'] = transaction['purchase_date'].apply(lambda x:'-'.join(x.split(' ')[0].split('-')[:2]))
transaction['purchase_hour_section'] = transaction['purchase_date'].apply(lambda x: x.split(' ')[1].split(':')[0]).astype(int)//6
transaction['purchase_day'] = transaction['purchase_date'].apply(lambda x: datetime.strptime(x.split(" ")[0], "%Y-%m-%d").weekday())//5                                                                    
del transaction['purchase_date']


# 5、对新生成的购买月份离散字段进行字典序编码。
transaction['purchase_month'] = change_object_cols(transaction['purchase_month'].fillna(-1).astype(str))
    
cols = ['merchant_id', 'most_recent_sales_range', 'most_recent_purchases_range', 'category_4']
transaction = pd.merge(transaction, merchant[cols], how='left', on='merchant_id')

numeric_cols = ['purchase_amount', 'installments']

category_cols = ['authorized_flag', 'city_id', 'category_1',
       'category_3', 'merchant_category_id','month_lag','most_recent_sales_range',
                 'most_recent_purchases_range', 'category_4',
                 'purchase_month', 'purchase_hour_section', 'purchase_day']

id_cols = ['card_id', 'merchant_id']

transaction['purchase_day_diff'] = transaction.groupby("card_id")['purchase_day'].diff()
transaction['purchase_month_diff'] = transaction.groupby("card_id")['purchase_month'].diff()

# 导出
transaction.to_csv("preprocess/transaction_g_pre.csv", index=False)
del transaction
gc.collect()

四、特征工程与模型训练

  在经历了漫长的数据解读、探索与清洗之后,接下来,我们将进入到特征工程与算法建模的环节。并在本小节的结尾,得出最终的预测结果。

  在此前的内容中,我们最终得到了train.csv、test.csv和transaction.csv三张表。首先我们简单回顾下这三张数据表的构建过程,首先,目前得到的训练集和测试集都是由原始训练集/测试集将时间字段处理后得到:

image-20211023143401397

  而transaction数据集则相对复杂,该数据集是有一张商户数据merchants.csv和两张交易数据表处理后合并得到,该过程如下所示:

image-20211023144942013

接下来,我们就依据这三张表进行后续操作。

4.1 特征工程

  首先需要对得到的数据进一步进行特征工程处理。一般来说,对于已经清洗完的数据,特征工程部分核心需要考虑的问题就是特征创建(衍生)与特征筛选,也就是先尽可能创建/增加可能对模型结果有正面影响的特征,然后再对这些进行挑选,以保证模型运行稳定性及运行效率。当然,无论是特征衍生还是特征筛选,其实都有非常多的方法。此处为了保证方法具有通用性,此处列举两种特征衍生的方法,即创建通用组合特征与业务统计特征;并在特征创建完毕后,介绍一种基础而通用的特征筛选的方法:基于皮尔逊相关系数的Filter方法进行特征筛选。这些方法都是非常通用且有效的方法,不仅能够帮助本次建模取得较好的成果,并且也能广泛适用到其他各场景中。

4.1.1 通用组合特征创建

  首先是尝试创建一些通用组合特征。

  所谓通用组合特征,指的是通过统计不同离散特征在不同取值水平下、不同连续特征取值之和创建的特征,并根据card_id进行分组求和。具体创建过程我们可以如下简例来进行理解:

image-20211023153800138

通过该方法创建的数据集,不仅能够尽可能从更多维度表示每个card_id的消费情况,同时也能够顺利和训练集/测试集完成拼接,从而带入模型进行建模。相关过程我们可以借助Python中的字典对象类型来进行实现。

4.1.2 基于transaction数据集创建通用组合特征

  接下来,我们将上述过程应用于建模真实数据,即在此前已经清洗完的transaction数据集上来完成通用组合特征的创建工作。

此处需要注意的是,由于transaction数据集本身较大,尽管特征创建工作的求和部分会一定程度减少最终带入建模的数据体量,但操作transaction数据集本身就需要耗费大量的内容及一定的时间,如果要手动执行下述代码,建议至少配置V100 32G以上环境以满足内存需求

# 字段类型标注

# 标注离散字段or连续型字段
numeric_cols = ['purchase_amount', 'installments']

category_cols = ['authorized_flag', 'city_id', 'category_1',
       'category_3', 'merchant_category_id','month_lag','most_recent_sales_range',
                 'most_recent_purchases_range', 'category_4',
                 'purchase_month', 'purchase_hour_section', 'purchase_day']

id_cols = ['card_id', 'merchant_id']

# 特征创建

# 创建字典用于保存数据
features = {}
card_all = train['card_id'].append(test['card_id']).values.tolist()
for card in card_all:
    features[card] = {}
     
# 标记不同类型字段的索引
columns = transaction.columns.tolist()
idx = columns.index('card_id')
category_cols_index = [columns.index(col) for col in category_cols]
numeric_cols_index = [columns.index(col) for col in numeric_cols]

# 记录运行时间
s = time.time()
num = 0

# 执行循环,并在此过程中记录时间
for i in range(transaction.shape[0]):
    va = transaction.loc[i].values
    card = va[idx]
    for cate_ind in category_cols_index:
        for num_ind in numeric_cols_index:
            col_name = '&'.join([columns[cate_ind], str(va[cate_ind]), columns[num_ind]])
            features[card][col_name] = features[card].get(col_name, 0) + va[num_ind]
    num += 1
    if num%1000000==0:
        print(time.time()-s, "s")
del transaction
gc.collect()

  能够发现,整体运行所需时间较长。此外,此处需要注意的是,card_id的提取并不是从transaction从提取,而是从训练集和测试集中提取,主要是为了确保card_id的唯一性,毕竟transaction是为了train集服务的

  在提取完特征后,接下来即可将带有交易数据特征的合并入训练集和测试集了:

# 字典转dataframe
df = pd.DataFrame(features).T.reset_index()
del features
cols = df.columns.tolist()
df.columns = ['card_id'] + cols[1:]

# 生成训练集与测试集
train = pd.merge(train, df, how='left', on='card_id')
test =  pd.merge(test, df, how='left', on='card_id')
del df
train.to_csv("preprocess/train_dict.csv", index=False)
test.to_csv("preprocess/test_dict.csv", index=False)

gc.collect() 

  至此,我们就完成了从transaction中提取通用特征的过程。简单查看数据集基本情况:

image-20211023161451438

4.1.3 业务统计特征创建

  当然,除了通用组合特征外,我们还可以考虑从另一个角度进行特征提取,那就是先根据card_id来进行分组,然后统计不同字段再各组内的相关统计量,再将其作为特征,带入进行建模。其基本构造特征思路如下:

image-20211023162730619

该过程并不复杂,可以通过pandas中的groupby过程迅速实现。和此前特征构造的思路不同,通过该方法构造的特征,不会存在大量的缺失值,并且新增的列也将相对较少。

4.1.3 数据合并

  至此,我们即完成了从两个不同角度提取特征的相关工作。不过截至目前上述两套方案的特征仍然保存在不同数据文件中,我们需要对其进行合并,才能进一步带入进行建模,合并过程较为简单,只需要将train_dict(test_dict)与train_group(test_group)根据card_id进行横向拼接、然后剔除重复列即可。

4.2 随机森林模型预测

  • 特征选择

  由于此前创建了数千条特征,若带入全部特征进行建模,势必极大程度延长模型建模时间,并且带入太多无关特征对模型结果提升有限,因此此处我们借助皮尔逊相关系数,挑选和标签最相关的300个特征进行建模。当然此处300也可以自行调整。

    
# 提取特征名称
features = train.columns.tolist()
features.remove("card_id")
features.remove("target")
featureSelect = features[:]

# 计算相关系数
corr = []
for fea in featureSelect:
    corr.append(abs(train[[fea, 'target']].fillna(0).corr().values[0][1]))

# 取top300的特征进行建模,具体数量可选
se = pd.Series(corr, index=featureSelect).sort_values(ascending=False)
feature_select = ['card_id'] + se[:300].index.tolist()

# 输出结果
train = train[feature_select + ['target']]
test = test[feature_select]

  • 借助网格搜索进行参数调优

  接下来,我们将借助sklearn中基础调参工具—网格搜索(Gridsearch)进行参数搜索与调优。

然后根据网格搜索的要求,我们需要根据随机森林的参数情况,有针对性的创造一个参数空间,随机森林基本参数基本情况如下:

NameDescription
criterion规则评估指标或损失函数,默认基尼系数,可选信息熵
splitter树模型生长方式,默认以损失函数取值减少最快方式生长,可选随机根据某条件进行划分
max_depth树的最大生长深度,类似max_iter,即总共迭代几次
min_samples_split内部节点再划分所需最小样本数
min_samples_leaf叶节点包含最少样本数
min_weight_fraction_leaf叶节点所需最小权重和
max_features在进行切分时候最多带入多少个特征进行划分规则挑选
random_state随机数种子
max_leaf_nodes叶节点最大个数
min_impurity_decrease数据集再划分至少需要降低的损失值
min_impurity_split数据集再划分所需最低不纯度,将在0.25版本中移除
class_weight各类样本权重

其中我们挑选"n_estimators"、"min_samples_leaf"、"min_samples_split"、"max_depth"和"max_features"进行参数搜索:

  然后是关于网格搜索工具的选择。随着sklearn不断完善,有越来越多的网格搜索工具可供选择,但整体来看其实就是在效率和精度之间做权衡,有些网格搜索工具由于是全域枚举(如GridSearchCV),所以执行效率较慢、但结果精度有保障,而如果愿意牺牲精度换执行效率,则也有很多工具可以选择,如RandomizedSearchCV。当然,在最新的sklearn版本中,还出现了一种更高效的搜索策略——HalvingGridSearchCV,该方法先两两比对、然后逐层筛选的方法来进行参数筛选,并且同时支持HalvingGridSearchCV和HalvingRandomSearchCV。

  围绕本次竞赛的数据,在实际执行网格搜索的过程中,建议先使用RandomizedSearchCV确定大概范围,然后再使用GridSearchCV高精度搜索具体参数取值,此处我们在大致确定最优参数范围的前提下设置在一个相对较小的参数空间内来进行搜索:

    
from sklearn.metrics import mean_squared_error
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV
features = train.columns.tolist()
features.remove("card_id")
features.remove("target")


parameter_space = {
    "n_estimators": [79, 80, 81], 
    "min_samples_leaf": [29, 30, 31],
    "min_samples_split": [2, 3],
    "max_depth": [9, 10],
    "max_features": ["auto", 80]
}

# 然后构建随机森林评估器,并输入其他超参数取值

clf = RandomForestRegressor(
    criterion="mse",
    n_jobs=15,
    random_state=22)

# 开始网格搜索
# 该过程确实非常慢, 这里大概用了V100卡大概8个h左右,可以尝试切换别的方式进行参数搜索
grid = GridSearchCV(clf, parameter_space, cv=2, scoring="neg_mean_squared_error")
grid.fit(train[features].values, train['target'].values)

# 查看结果

print(grid.best_params_)
# 也可以直接调用最优参数组成的评估器
print(grid.best_estimator_)
# 也可以直接查看在训练集上的最终评分:
print(np.sqrt(-grid.best_score_))
grid.best_estimator_.predict(test[features])
# 然后将结果按照所需要提交的格式写入csv文档
test['target'] = grid.best_estimator_.predict(test[features])
test[['card_id', 'target']].to_csv("submission_randomforest.csv", index=False)

数据文档写入完毕后,接下来就可以直接在Kaggle上提交了。上传提交结果数据和下载数据过程类似,都可以直接利用网页功能实现,或者通过命令行的方式实现。

  • 结果提交

Kaggle竞赛主页 找到Late Submission进行结果提交,只需将结果文件在线提交即可:

6a480d241a9a072ca921feedc9356c2 image-20211023183654754

基本能达到一个baseline的效果

五、后续优化策略

  • 文本特征挖掘

  在特征处理的过程中,可以尝试使用NLP领域的TF-IDF进行词频统计,增加离散变量特征;

  • 更多衍生特征

  除了对离散变量进行词频统计外,我们还可以考虑构建更多特征,如全局card_id特征、最近两个月 card_id特征、二阶特征和补充特征等,来更深程度挖掘数据集信息;

  • 更多集成算法

  除了随机森林外,还有许多功能非常强大的集成模型,包括LightGBM、XGBoost等,都是可以尝试使用的算法;

  • 模型融合方法

  既然使用了多集成模型来进行建模,那么模型融合也势在必行。模型融合能够很好的综合各集成模型的输出结果,来做出最后更加综合的判断。当然模型融合可以考虑简单加权融合或者stacking融合方法;

  • 更加细致的数据处理

  除了技术手段外,我们可也可以围绕此前得出的业务分析结论,对数据集进行更加细致的处理,如此前标签中出现的异常值的处理、13家商户没有过去一段时间营销信息等,通过更加细致的处理,能够让模型达到更好的效果。

引用