Kaggle Click-Through Rate Prediction 点击率预测/CTR预估

前言

以下代码的github链接为:Kaggle-Click-Through-Rate-Prediction

点击率(Click through rate)预估用来判断一条广告被用户点击的概率,对每次广告的点击做出预测,把用户最有可能点击的广告找出来,是广告技术最重要的算法之一。
在这里插入图片描述
目前,点击率一般都小于1%
常用的CTR预估算法有FM, FFM, DeepFM

数据集

数据来源于kaggle赛题 Avazu:Click-Through Rate Prediction
提供了11天的Avazu数据来构建和测试预测模型:

  • 任务输入:数据集文件:train.csv 和 test.csv。
  • 评估指标:二分类任务的交叉熵损失函数
  • 任务输出:给出预测集id所对应的CTR预估值,形如:
id,点击
60000000,0.384 
63895816,0.5919 
759281658,0.1934 
895936184,0.9572 
...
  • 原始数据规模
train - 训练集。按时间顺序排列的10天点击数据。 40428967个样本  约4千万 
test  - 测试集。用于测试模型预测的1天广告。 4577464个样本  约4.6百万   

0.探索性数据分析

探索性数据分析(Exploratory Data Analysis,EDA)

首先导入需要的包

import pandas as pd
import numpy as  np
from matplotlib import pyplot as plt
import seaborn as sns
import json
import datetime
from sklearn import preprocessing
sns.set()

使用pandas读入数据(由于数据量较大,先读入前100W行),查看前几行的原数据:

train = pd.read_csv('input/train.csv', nrows = 1000000)
train.head()

在这里插入图片描述
在这里插入图片描述

train.columns
Index(['id', 'click', 'hour', 'C1', 'banner_pos', 'site_id', 'site_domain',
           'site_category', 'app_id', 'app_domain', 'app_category', 'device_id',
           'device_ip', 'device_model', 'device_type', 'device_conn_type', 'C14',
           'C15', 'C16', 'C17', 'C18', 'C19', 'C20', 'C21'],
           dtype='object')

我们看到,总共有23列,其中click字段表示的是该用户是否点击,其余都是用户的个人信息,包括:

id: 用户ID号
click: 0/1 表示未点击/点击
hour: 格式为YYMMDDHH,因此14091123表示2014年9月11日UTC时间23:00。
C1: 匿名分类变量
banner_pos: int型,网页上的广告位置,离散特征0,1,2,3...
site_id:Site ID
site_domain:Site领域
site_category: 网站类别
app_id: string型,用户APP的ID
app_domain
app_category
device_id: 设备编号
device_ip
device_model
device_type: 设备类型
device_conn_type:Device接入类型
C14-C21 -- anonymized categorical variables

下面查看数据的描述,包括每个属性的类型

train.info()
id                  1000000 non-null float64
click               1000000 non-null int64
hour                1000000 non-null int64
C1                  1000000 non-null int64
banner_pos          1000000 non-null int64
site_id             1000000 non-null object
site_domain         1000000 non-null object
site_category       1000000 non-null object
app_id              1000000 non-null object
app_domain          1000000 non-null object
app_category        1000000 non-null object
device_id           1000000 non-null object
device_ip           1000000 non-null object
device_model        1000000 non-null object
device_type         1000000 non-null int64
device_conn_type    1000000 non-null int64
C14                 1000000 non-null int64
C15                 1000000 non-null int64
C16                 1000000 non-null int64
C17                 1000000 non-null int64
C18                 1000000 non-null int64
C19                 1000000 non-null int64
C20                 1000000 non-null int64
C21                 1000000 non-null int64

数据类型有 int float 及object类型。

train.isnull().any()  #查看缺失值情况
id                  False
click               False
hour                False
C1                  False
banner_pos          False
site_id             False
site_domain         False
site_category       False
app_id              False
app_domain          False
app_category        False
device_id           False
device_ip           False
device_model        False
device_type         False
device_conn_type    False
C14                 False
C15                 False
C16                 False
C17                 False
C18                 False
C19                 False
C20                 False
C21                 False

没有数据缺失的情况。

扫描二维码关注公众号,回复: 11495751 查看本文章

我们用下列的方法,得到数值型数据的一些分布:

train.describe()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们从上面看到更进一步的什么信息呢?
click字段告诉我们,大概16.0219%的人点击了广告 等…

各属性分布

sns.countplot(train['click'])
plt.show()

在这里插入图片描述
看一下“点击位置”的分布:

sns.countplot(train['banner_pos']) #广告位置
plt.show()

在这里插入图片描述
多数用户点击的广告,在0、1位置。

sns.countplot(train['site_category']) #广告类型

在这里插入图片描述
第四类网站点击数较多。

下面看一下 'site_category’属性值的统计分布:

from collections import Counter
counts = Counter(train['site_category'])
Counter({'50e219e0': 360056, '28905ebd': 262519, 'f028772b': 260471, '3e814130': 75062, 'f66779e6': 8842, '76b2941d': 7880, '335d28a8': 7251, '70fb0e29': 4961, '0569f928': 4505, '75fa27f6': 3958, '72722551': 2705, 'c0dd3be3': 552, 'dedf689d': 508, 'a818d37a': 374, 'e787de0e': 281, 'bcf865d9': 25, '8fd0aea4': 22, '42a36e14': 13, '5378d028': 10, '9ccfa2ea': 4, '110ab22d': 1})

‘50e219e0’: 360056, ‘28905ebd’: 262519, ‘f028772b’: 260471, ‘3e814130’: 75062
这四个类别的广告,点击数较多。

属性与点击结果的关联统计
设备类型与点击

sns.countplot(x = "device_type", hue = "click", data = train)

在这里插入图片描述
四类中,“1”类设备点击数最高,“0”类设备“点击数/总数”最高。

广告类型与点击

fig = plt.figure()
fig.set(alpha=0.2)  # 设定图表颜色alpha参数

click_0 = train.site_category[train.click == 0].value_counts()
click_1 = train.site_category[train.click == 1].value_counts()

df=pd.DataFrame({'click':click_0, 'click_1':click_1})
df.plot(kind='bar', stacked=True)

plt.xlabel("site_category")
plt.ylabel("count")
plt.show()

在这里插入图片描述
‘28905ebd’类广告,点击数与“点击数/总数”最高。
注意以下四个属性:
site_id、site_domain、app_id、device_model
以site_id为例

train.site_id.value_counts()
85f751fd    332893
1fbe01fe    232096
d9750ee7     38603
e151e245     32359
5b08c53b     16455
543a539e     16355
5b4d2eda     16064
856e6d3f     13231
43d6df75      9924
5ee41ff2      9011
f282ab5a      7526
6399eda6      7437
17caea14      6249
6256f5b4      6109
0a742914      5894
030440fe      5682
bb4524e7      5094
78d60190      4908
84c7ba46      4832
d6137915      4804
5114c672      4791
c21df24f      4791
b7e9786d      4503
57fe1b20      4383
e8f79e60      4111
83a0ad1a      3997
9b971c93      3908
fe8cc448      3784
93eaba74      3733
cd58172f      3704
             ...  
4461dbba         1
ffad33d9         1
ba7311c2         1
f203fb1d         1
040a1ec2         1
b0386b86         1
feb07b6f         1
8a3ef90f         1
e0dfd16b         1
c36224b6         1
80c247a5         1
897302f4         1
69170ade         1
4f9550e2         1
74fd3234         1
e3088707         1
881897fe         1
ba85e0e9         1
41fb5282         1
acaf2eb1         1
c2c7dac1         1
dde8c24f         1
8602e748         1
a7041082         1
40752d0c         1
a4ae03ab         1
f4c83917         1
6594e957         1
e3beeed7         1
46e56230         1

counts最高为332893 但还有大量 频率低于20的,这样的类别,需要后续处理。

train.device_id.value_counts()
a99f214a    840265
c357dbff      1310
936e92fb       482
31da1bd0       292
787d2bb0       274
b09da1c4       267
e8440dbf       246
d857ffbb       214
f0b5276b       195
dcefb131       155
f765372d       135
7166f9fd       133
79b5916c       130
045d057f       121
afeffc18       119
c810e66f       118
c07b6494       117
332d28f2       116
e09ded8f       115
a167aa83       113
8bd5456a       113
3c19ea8f       110
9d7b8b52       109
09ecc3b0       108
6e30eb86       106
da3579c6       104
0cc497c8       101
5140d8aa       100
104811b5        99
0b68504c        98
             ...  
1cb404c8         1
8d62c261         1
e3f3490a         1
277045bf         1
5b54f2b5         1
f328857e         1
45e2734a         1
76ec6353         1
31ff0264         1
37472f99         1
6694c8f3         1
1a39aac8         1
edd3b9ce         1
1d3c6e01         1
52722463         1
5579014f         1
43bcc40c         1
029324ae         1
0ca1c8b4         1
12c0a9bc         1
6ff6cd93         1
11142cba         1
63ac5ad3         1
771e1ed6         1
e7fe4714         1
a6a90563         1
fcd8fec8         1
98a087df         1
197b8098         1
38cedf7c         1

a99f214a 840265
这一类别出现的频率非常高,可以将这一特征转为二值型
是a99f214a 类 为0
不是a99f214a 类 为1

特征间相关性分析

我们挑选一些主要的特征,生成特征之间的关联图,查看特征与特征之间的相关性:

for column in ['C1', 'site_id', 'site_domain',
       'site_category', 'app_id', 'app_domain', 'app_category', 'device_id',
       'device_ip', 'device_model']:
    enc = LabelEncoder()
    train[column] = enc.fit_transform(train[column])
    
sub_train = train.iloc[:, 4:]
plt.figure(figsize=(20, 18))
sns.heatmap(sub_train.corr().abs(), annot=True)
plot.show()

在这里插入图片描述
C14 与 C17线性相关性非常强。

1.特征工程

由于train.csv是按照时间排序的,只读取前100w条记录,可能会有采样偏差的风险。
应将用户分成均匀的子分组,称为分层,从每个分层去取合适数量的实例,以保证训练集对总体有代表性。
这里,我们对label分成取样,对click = 0 与click = 1的用户都抽取0.05的样本,减小数据集,方便后序操作。

from sklearn.model_selection import StratifiedShuffleSplit
train_all = pd.read_csv('input/train.csv')

split = StratifiedShuffleSplit(n_splits=1, train_size=0.05, random_state=42)
for train_index, test_index in split.split(train_all, train_all["click"]):
    strat_train_set = train_all.loc[train_index]
    strat_train_set.to_csv("train_sample.csv", header = True)

读入分层采样后的样例数据,将分层采样和原数据的样本比较:

train = pd.read_csv("train_sample.csv")
train_all = pd.read_csv('input/train.csv')
train.click.value_counts()
train_all.click.value_counts()
0      1678195    #占比0.83019
1       343253    #占比0.16981

0      33563901   #占比0.83019
1       6865066   #占比0.16981

接下来,在样例数据“train_sample.csv”上进行数据预处理及特征工程

现在来看下每个属性和click的关联度(标准相关系数(standard correlation coefficient,也称作皮尔逊相关系数)):

corr_matrix = train.corr()
corr_matrix["click"].sort_values(ascending = False)

在这里插入图片描述
在上述特征中,C16与click相关性最大。

在进行特征工程的时候,我们不仅需要对训练数据进行处理,还需要同时将测试数据同训练数据一起处理,使得二者具有相同的数据类型和数据分布。为了方便起见,现将二者合并。

tr_csv_path = 'train_sample.csv'
ts_csv_path = 'test.csv'

data_type = {'id': 'U', 'hour': 'U', 'device_type':'U', 'C1':'U', 'C15':'U', 'C16':'U'}

train = pd.read_csv(tr_csv_path, dtype=data_type, index_col='id')
test  = pd.read_csv(ts_csv_path, dtype=data_type, index_col='id')
test.insert(0, 'click', 0)

tr_ts = pd.concat([test, train], copy=False)

统计类别型

Scikit-learn要求数据都是数字型numeric,所以我们要将一些非数字型的原始数据转换为数字型numeric。

所以下面对数据的转换进行介绍,以在进行特征工程的时候使用。

所有的数据可以分为两类:

1.定量(Quantitative)变量可以以某种方式排序(例如后面的构建的新特征,用户规模——‘app_id_users’)。
2.定性(Qualitative)变量描述了物体的某一(不能被数学表示的)方面,是无序的。

之前在EDA中介绍过,site_id、site_domain、app_id、device_model 四个特征存在很多低频率类别。

1) site_id 处理频率低于20
将site_id转为二值型
site_id 出现>20 为 0
site_id 出现<=20 为 1

site_id_count = tr_ts.site_id.value_counts()
site_id_category={}
site_id_category[0] = site_id_count.loc[site_id_count>20].index.values
site_id_category[1] = site_id_count.loc[site_id_count<=20].index.values

site_id_C_type_dict = {}
for key, values in site_id_category.items():
    for item in values:
        site_id_C_type_dict[str(item)] = key

json.dump(site_id_C_type_dict, open("output/site_id_C_type_dict.json", "w"))

2) site_domain 处理频率低于20
方式与site_id类似

site_domain_count = tr_ts.site_domain.value_counts()
site_domain_category={}
site_domain_category[0] = site_domain_count.loc[site_domain_count>20].index.values
site_domain_category[1] = site_domain_count.loc[site_domain_count<=20].index.values

site_domain_C_type_dict = {}
for key, values in site_domain_category.items():
    for item in values:
        site_domain_C_type_dict[str(item)] = key

json.dump(site_domain_C_type_dict, open("output/site_domain_C_type_dict.json", "w"))

3) app_id 处理频率低于20

app_id_count = tr_ts.app_id.value_counts()
app_id_category={}
app_id_category[0] = app_id_count.loc[app_id_count>20].index.values
app_id_category[1] = app_id_count.loc[app_id_count<=20].index.values

app_id_C_type_dict = {}
for key, values in app_id_category.items():
    for item in values:
        app_id_C_type_dict[str(item)] = key

json.dump(app_id_C_type_dict, open("output/app_id_C_type_dict.json", "w"))

4) device_model 处理频率低于200

device_model_count = tr_ts.device_model.value_counts()
device_model_category={}
device_model_category[0] = device_model_count.loc[device_model_count>200].index.values
device_model_category[1] = device_model_count.loc[device_model_count<=200].index.values

device_model_C_type_dict = {}
for key, values in device_model_category.items():
    for item in values:
        device_model_C_type_dict[str(item)] = key

json.dump(device_model_C_type_dict, open("output/device_model_C_type_dict.json", "w"))

处理类别型数据

我们看到hour这个属性,hour: 格式为YYMMDDHH,因此14091123表示2014年9月11日UTC时间23:00。
我们可以将其构建 ‘时间’,与‘日期’两个新属性。(考虑到广告点击可能与时间段有关)

train['day']=np.round(train.hour % 10000 / 100)
train['hour1'] = np.round(train.hour % 100)
train['day'].value_counts()
print("-" * 40)
train['hour1'].value_counts()

结果如下:
在这里插入图片描述
看上去每天数据量级大致相当。
在这里插入图片描述
将日期与时间属性,可视化:

sns.countplot(x="hour1", hue="click",data=train)
sns.countplot(x="day", hue="click",data=train)
plt.show()

在这里插入图片描述
在这里插入图片描述
点击率随时间与日期变化相对平稳。

tr_ts['day'] = tr_ts['hour'].apply(lambda x: x[-4:-2])
tr_ts['hour'] = tr_ts['hour'].apply(lambda x: x[-2:])

tr_ts['is_device'] = tr_ts['device_id'].apply(lambda x: 0 if x=='a99f214a' else 1)  #详见探索性数据分析部分


app_id_C_type_dict = json.load(open("output/app_id_C_type_dict.json", "r"))
site_id_C_type_dict = json.load(open("output/site_id_C_type_dict.json", "r"))
site_domain_C_type_dict = json.load(open("output/site_domain_C_type_dict.json", "r"))
device_model_C_type_dict = json.load(open("output/device_model_C_type_dict.json", "r"))

tr_ts['C_app_id'] = tr_ts["app_id"].apply(lambda x: x if app_id_C_type_dict.get(x)==0 else "other_app_id")
tr_ts['C_site_id'] = tr_ts['site_id'].apply(lambda x: x if site_id_C_type_dict.get(x)==0 else "other_site_id")
tr_ts['C_site_domain'] = tr_ts['site_domain'].apply(lambda x: x if site_domain_C_type_dict.get(x)==0 else "other_site_domain")
tr_ts['C_device_model'] = tr_ts['device_model'].apply(lambda x: x if device_model_C_type_dict.get(x)==0 else "other_device_model")

特征组合

sns.countplot(x="C15", hue="click",data=train)
sns.countplot(x="C16", hue="click",data=train)
plt.show()
train['C15'].value_counts()
print("-" * 40)
train['C16'].value_counts()

在这里插入图片描述
对比一下,二者分布类似,猜测,二者可能组合起来可能代表一个用户标识,或者一种设备标识,因此将二者合为一列。

tr_ts["C_pix"] = tr_ts["C15"] + '&' + tr_ts["C16"]
tr_ts["C_device_type_1"] = tr_ts["device_type"] + '&' + tr_ts["C1"]

tr_ts.drop(['device_id', "device_type", 'app_id', 'site_id', 'site_domain', 'device_model',"C1", "C17", 'C15', 'C16'], axis=1, inplace=True)

处理类别特征

app_category 与 广告类别site_category是类别型变量,对其进行 dummy编码

lenc = preprocessing.LabelEncoder()
C_fields = [ 'hour', 'banner_pos', 'site_category', 'app_domain', 'app_category',
            'device_conn_type', 'C14', 'C18', 'C19', 'C20','C21', 'is_device', 'C_app_id', 'C_site_id', 
            'C_site_domain', 'C_device_model', 'C_pix', 'C_device_type_1']
for f, column in enumerate(C_fields):
    print("convert " + column + "...")
    tr_ts[column] = lenc.fit_transform(tr_ts[column])


dummies_site_category = pd.get_dummies(tr_ts['site_category'], prefix = 'site_category')
dummies_app_category = pd.get_dummies(tr_ts['app_category'], prefix = 'app_category')



tr_ts_new = pd.concat([tr_ts, dummies_site_category, dummies_app_category], axis=1)
tr_ts_new.drop(['site_category', 'app_category'], axis = 1, inplace=True)

构建新特征

考虑到用户点击广告,可能与广告对应的用户规模,以及广告投放的APP对应的用户规模有关。
我们新添加了两个特征,分别对应于某类广告的受众规模,以及APP的规模。

site_id_users = defaultdict(set)
app_id_users = defaultdict(set)
tr_path = 'input/train_FE.csv'
with open(tr_path, 'r') as csv_file:
    for i, row in enumerate(csv.DictReader(csv_file), start=1):
        site_id_users[row['C_site_id']].add(row['device_ip'])
        app_id_users[row['C_app_id']].add(row['device_ip'])



ts_path = 'data/train_FE.csv'
with open(ts_path, 'r') as csv_file:
    for i, row in enumerate(csv.DictReader(csv_file), start=1):
        site_id_users[row['C_site_id']].add(row['device_ip'])  #广告对应设备id
        app_id_users[row['C_app_id']].add(row['device_ip'])    #app对应设备id




app_id_dict = pd.Series()
site_id_dict = pd.Series()

for item in app_id_users:
    app_id_dict[item] = int(np.log10(len(app_id_users[item])))


for item in site_id_users:
    site_id_dict[item] = int(np.log10(len(site_id_users[item])))

app_id_dict = app_id_dict.sort_values(ascending=False)
site_id_dict = site_id_dict.sort_values(ascending=False)

app_id_users = app_id_dict.to_dict()
site_id_users = site_id_dict.to_dict()


ts_csv_path = 'data/test_FE.csv'
tr_csv_path = 'data/train_FE.csv'

test  = pd.read_csv(ts_csv_path, dtype={'id': 'U'}, index_col='id')
train = pd.read_csv(tr_csv_path, dtype={'id': 'U'}, index_col='id')
tr_ts = pd.concat([test, train], copy=False)


tr_ts['app_id_users'] = tr_ts.C_app_id.apply(lambda x: app_id_users[str(x)] if str(x) in app_id_users else 0)
tr_ts['site_id_users'] = tr_ts.C_site_id.apply(lambda x: site_id_users[str(x)] if str(x) in site_id_users else 0)

数据归一化

各属性值之间scale差距太大,将对收敛速度造成很大的影响!甚至不收敛!

import sklearn.preprocessing as preprocessing
scaler = preprocessing.StandardScaler()
age_scale_param = scaler.fit(tr_ts[['C14','C18','C19','C20','C21']])
tr_ts[['C14','C18','C19','C20','C21']] = age_scale_param.transform(tr_ts[['C14','C18','C19','C20','C21']])

将训练集与测试集分开

tr_ts.iloc[:test.shape[0],].to_csv('data/ts_FE.csv')
tr_ts.iloc[test.shape[0]:,].to_csv('data/tr_FE.csv')

特征选择

进过上述步骤,来看一下数据的维度

train = pd.read_csv('input/tr_FE.csv')
print(train.shape)
(2021448, 78)

如果在进行特征工程的过程中,产生了大量的特征,而特征与特征之间会存在一定的相关性。太多的特征一方面会影响模型训练的速度,另一方面也可能会使得模型过拟合。所以在特征太多的情况下,我们可以利用不同的模型对特征进行筛选,选取出我们想要的前n个特征。

这里选择了XGboost对特征进行筛选,选出最重要的30个特征。

1) n_estimators参数调优

import pandas as pd
import numpy as np
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
from sklearn.metrics import accuracy_score #评估指标
from sklearn.metrics import roc_auc_score
from sklearn.metrics import log_loss
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

if __name__ == '__main__':



    train = pd.read_csv('data/tr_FE.csv')

	
    y_train = train.click
    X_train = train.drop(['click', 'device_ip', 'Unnamed: 0'], axis = 1)

    #X_test = loadTestData(testFilePath)

    cv_params = {'n_estimators': [400, 500, 600, 700, 800]}
    other_params = 
    {'learning_rate': 0.1, 
    'n_estimators': 500, 
    'max_depth': 5, 
    'min_child_weight': 1, 
    'seed': 0,
    'subsample': 0.8, 
    'objective' : 'binary:logistic',
    'colsample_bytree': 0.8, 
    'gamma': 0,  
    }
    

    model = xgb.XGBClassifier(**other_params)
    optimized_GBM = GridSearchCV(estimator=model, param_grid=cv_params, scoring='neg_log_loss', cv=5, verbose=1, n_jobs=4)
    optimized_GBM.fit(X_train, y_train)
    evalute_result = optimized_GBM.grid_scores_

    print('每轮迭代运行结果:{0}'.format(evalute_result))
    print('参数的最佳取值:{0}'.format(optimized_GBM.best_params_))
    print('最佳模型得分:{0}'.format(optimized_GBM.best_score_))

在这里插入图片描述
从输出结果可以看出,在学习速率为0.1时,理想的决策树数目是400。但是,我们还不能认为这是最终的结果,由于设置的间隔太大,所以,我又测试了一组参数,这次粒度小一些:

 cv_params = {'n_estimators': [350, 375, 400, 425, 450]}

在这里插入图片描述
…可以继续细粒度得搜索。

2) max_depth 和 min_child_weight 参数调优

我们先对这两个参数调优,是因为它们对最终结果有很大的影响。首先,我们先大范围地粗调参数,然后再小范围地微调。

cv_params = { 'max_depth':range(3, 10, 2), 'min_child_weight':range(1, 6, 2)}


3) 接着我们就开始调试参数:gamma:

cv_params = {'gamma': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]}

4) 接着是subsample以及colsample_bytree:

cv_params = {
 'subsample':[i/10.0 for i in range(6,10)],
 'colsample_bytree':[i/10.0 for i in range(6,10)]
}

5) 正则化参数调优
应用正则化来降低过拟合。

cv_params = {'reg_alpha':[1e-5, 1e-2, 0.1, 1, 100]} 

6) 降低学习速率

cv_params = {'learning_rate': [0.01, 0.05, 0.07, 0.1, 0.2]}

最终模型参数

other_params = {
    'learning_rate': 0.1,
    'n_estimators': 400,
    'max_depth': 10,
    'min_child_weight': 6,
    'seed': 0,
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'objective':'binary:logistic',
    'reg_alpha': 0,
    'reg_lambda': 1}

选择特征如下。
在这里插入图片描述

model = xgb.XGBRegressor(n_estimators=350, max_depth=10, objective='binary:logistic', min_child_weight=50,
                         subsample=0.8, gamma=0, learning_rate=0.2, colsample_bytree=0.5, seed=27)

model.fit(X_train, y_train)
# y_test = model.predict(X_test)
plot_importance(model, importance_type="gain")

features = X_train.columns
feature_importance_values = model.feature_importances_

feature_importances = pd.DataFrame({'feature': list(features), 'importance': feature_importance_values})

feature_importances.sort_values('importance', inplace=True, ascending=False)
print(feature_importances)

# print(model.get_booster().get_fscore())

print(model.get_booster().get_score(importance_type="gain"))


feature_importances.to_csv('feature.csv')

在这里插入图片描述

建模

GBDT

首先我们步长(learning rate)迭代次数(n_estimators) 入手。一般来说,开始选择一个较小的步长来网格搜索最好的迭代次数。这里,我们将步长初始值设置为0.1。对于迭代次数进行网格搜索。
找到了一个合适的迭代次数,现在我们开始对决策树进行调参。首先我们对决策树最大深度max_depth和内部节点再划分所需最小样本数min_samples_split进行网格搜索。

……
经过与一系列调参,得到我们所有调优的参数结果,下面就做预测取结果。

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier  #分类器
from sklearn.metrics import accuracy_score #评估指标
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV     #网格搜索

import matplotlib.pylab as plt

features = pd.read_csv('feature.csv')
x_columns = features.head(30)['feature'].tolist() #特征重要性排序中,选择前30的特征



train = pd.read_csv('data/tr_FE.csv')


y_train = train.click
X_train = train[x_columns]


X = pd.read_csv('data/ts_FE.csv', dtype = {'id':'U'})
id_ = X.id
X_ = X[x_columns]


gbm = GradientBoostingClassifier(max_depth=15, learning_rate=0.02, n_estimators=1200, min_samples_leaf=60, 
               min_samples_split =1200, max_features=4, subsample=1.0, random_state=10)

gbm.fit(X_train, y_train)
y_pred = gbm.predict(X_)
y_predprob = gbm.predict_proba(X_)[:,1]

y_out = pd.DataFrame({'id': list(id_), 'click': y_predprob})
y_out.to_csv('y_out1.csv', index = False, sep = ',')  #保存预测值

提交
在这里插入图片描述

模型融合

常见的模型融合方法有:Bagging、Boosting、Stacking、Blending。
这里选择了linear stacking

Blending

在这里插入图片描述
这里我们使用了两层的模型融合,Level 1使用了:RandomForest、AdaBoost、GBDT一共3个模型,Level 2使用了linear_model使用第一层预测的结果作为特征对最终的结果进行预测。

Blending相较于Stacking来说要简单一些,其流程大致分为以下几步:

  1. 将数据划分为训练集和测试集(test_set),其中训练集需要再次划分为训练集(train_set)和验证集(val_set);
  2. 创建第一层的多个模型,这些模型可以使同质的也可以是异质的;
  3. 使用train_set训练步骤2中的多个模型,然后用训练好的模型预测val_set和test_set得到val_predict,test_predict1;
  4. 创建第二层的模型,使用val_predict作为训练集训练第二层的模型;
  5. 使用第二层训练好的模型对第二层测试集test_predict1进行预测,该结果为整个测试集的结果。

Satcking

相比 Blending,Stacking 能更好地利用训练数据。以 5-Fold Stacking 为例,它的基本原理如图所示:
在这里插入图片描述
整个过程很像 Cross Validation。首先将训练数据分为 5 份,接下来一共 5 个迭代,每次迭代时,将 4 份数据作为 Training Set 对每个 Base Model 进行训练,然后在剩下一份 Hold-out Set 上进行预测。同时也要将其在测试数据上的预测保存下来。这样,每个 Base Model 在每次迭代时会对训练数据的其中 1 份做出预测,对测试数据的全部做出预测。5 个迭代都完成以后我们就获得了一个 #训练数据行数 x #Base Model 数量 的矩阵,这个矩阵接下来就作为第二层的 Model 的训练数据。当第二层的 Model 训练完以后,将之前保存的 Base Model 对测试数据的预测(因为每个 Base Model 被训练了 5 次,对测试数据的全体做了 5 次预测,所以对这 5 次求一个平均值,从而得到一个形状与第二层训练数据相同的矩阵)拿出来让它进行预测,就得到最后的输出。

from sklearn.ensemble import GradientBoostingClassifier  # 分类器
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.model_selection import KFold
from sklearn.linear_model import RidgeCV
import pandas as pd
import numpy as np

INITIAL_PARAMS = {
    'RF:': {
        'n_estimators': 400, 'n_jobs': -1, 'criterion': 'entropy',
        'min_samples_leaf': 3, 'bootstrap': False,
        'max_depth': 12, 'min_samples_split': 6, 'max_features': 0.14357
    },

    'AdaBoost:': {
        'n_estimators': 500, 'learning_rate': 0.01
    },

    'GBDT:': {
        'max_depth': 15, 'learning_rate': .02, 'n_estimators': 1200,
        'min_samples_leaf': 60, 'min_samples_split': 1200,
        'max_features': 4, 'subsample': 1.0, 'random_state': 10
    },

}

MODEL_NAME = 'stack_ensemble'

if __name__ == '__main__':

    NFOLDS = 5  # set folds for out-of-fold prediction
    SEED = 0  # for reproducibility
    LM_CV_NUM = 100

    skf = KFold(n_splits=NFOLDS, random_state=SEED, shuffle=False)

    clfs = [
        RandomForestClassifier().set_params(**INITIAL_PARAMS.get("RF:", {})),
        AdaBoostClassifier().set_params(**INITIAL_PARAMS.get("AdaBoost:", {})),
        GradientBoostingClassifier().set_params(**INITIAL_PARAMS.get("GBDT:", {})),
    ]

    print("Creating train and test sets for stacking.")

    dataset_stack_train = np.zeros((X.shape[0], len(clfs)))
    dataset_stack_test = np.zeros((X_submission.shape[0], len(clfs)))

    for j, clf in enumerate(clfs):
        print(j, clf)
        dataset_stack_test_j = np.zeros((X_submission.shape[0], NFOLDS))
        for i, (train_index, test_index) in enumerate(skf.split(X)):
            # 训练集 与 验证集
            print("Fold", i)
            X_train = X.iloc[train_index]
            y_train = y.iloc[train_index]
            X_test = X.iloc[test_index]

            clf.fit(X_train, y_train)
            y_submission = clf.predict_proba(X_test)[:, 1]

            dataset_stack_train[test_index, j] = y_submission  # 第j个基学习器的预测值
            dataset_stack_test_j[:, i] = clf.predict_proba(X_submission)[:, 1]
        dataset_stack_test[:, j] = dataset_stack_test_j.mean(1)

    print("stacking.")

    clf = Ridge(alpha=0.5)

    clf.fit(dataset_stack_train, y)

    y_submission = clf.predict(dataset_stack_test)  # for RidgeCV

    y_submission = (y_submission - y_submission.min()) / (y_submission.max() - y_submission.min())

    y_out = pd.DataFrame({'id': list(id_), 'click': y_submission})
    y_out.to_csv('data.csv', index=False, sep=',')  # 保存预测值

提交
在这里插入图片描述

看上去结果好了一些

FM和FFM模型是最近几年提出的模型,凭借其在数据量比较大并且特征稀疏的情况下,仍然能够得到优秀的性能和效果的特性,屡次在各大公司举办的CTR预估比赛中获得不错的战绩。
在计算广告领域,点击率CTR(click-through rate)和转化率CVR(conversion rate)是衡量广告流量的两个关键指标。准确的估计CTR、CVR对于提高流量的价值,增加广告收入有重要的指导作用。预估CTR/CVR,业界常用的方法有人工特征工程 + LR(Logistic Regression)GBDT(Gradient Boosting Decision Tree) + LRFM(Factorization Machine)FFM(Field-aware Factorization Machine) 模型。在这些模型中,FM和FFM近年来表现突出,分别在由Criteo和Avazu举办的CTR预测竞赛中夺得冠军。

方法二:FM

FM主要目标是:FM旨在解决稀疏数据下的特征组合问题

例如 categorical类型的特征,需要经过独热编码 (One-Hot Encoding)转换成数值型特征,编码之后,大部分数据特征是比较稀疏的。One-Hot编码的另一个特点就是导致特征空间大

在一般的线性模型中,是各个特征独立考虑的,没有考虑到特征与特征之间的相互关系。但实际上,某些特征经过关联之后,与label之间的相关性就会提高。
一般的线性模型为:
在这里插入图片描述
多项式模型是包含特征组合的最直观的模型,在多项式模型中,特征 x i x_{i} x j x_{j} 的组合采用 x i x j x_{i}x_{j} 表示,即 x i x_{i} x j x_{j} 都非零时,组合特征 x i x j x_{i}x_{j} 才有意义。
二阶多项式模型为:
在这里插入图片描述
n n 代表样本的特征数量, x i x_{i} 是第 i i 个特征的值, w w 是模型参数。
特征组合部分的参数有 n ( n 1 ) 2 \frac{n(n-1)}{2} 个, 时间复杂度将大大增加

由于样本数据本来就比较稀疏, x i x_{i} x j x_{j} 都非零的样本将会非常少。训练样本的不足,很容易导致参数不准确,最终将严重影响模型的性能

参照矩阵分解的思路,可以将所有的二次项参数 w i j w_{ij} 可以组成一个对称阵 W W ,矩阵可分解为 W = V T V W = V^{T}V w i j = < v i , v j > w_{ij} = <v_{i}, v_{j}> ,这就是FM的核心思想。 v i v_{i} 的第 i i 维特征的隐向量,长度为 k ( k n ) k(k\ll n) ,包含 k k 个描述特征的因子。
二项式的参数由 n ( n 1 ) 2 \frac{n(n-1)}{2} 个降到 k n kn 个。

dataframe转化为libsvm

xLearn 是一款高性能的,易用的,并且可扩展的机器学习算法库,xLearn 支持简单易用的 Python 接口。支持FM,FFM。

对于FM 算法而言,我们的输入数据格式必须是libsvm.
首先将dataframe转化为libsvm

import pandas as pd
from sklearn.datasets import dump_svmlight_file

df = pd.read_csv("data/tr_FE.csv")      # 第一个字段为target
features = pd.read_csv('feature.csv')
x_columns = features.head(30)['feature'].tolist() #特征重要性排序中,选择前30的特征

y = df.click     # y为数据的label值
dummy = pd.get_dummies(df[x_columns])
mat = dummy.as_matrix()
dump_svmlight_file(mat, y, 'svm_output.libsvm', zero_based=False)

通过 Validation Dataset (验证集) 来进行超参数调优,最终模型如下:

xfm = xl.create_fm()
# xfm.setTrain("train.libsvm")
# param = {'task':'binary', 'lr':0.0001, 'lambda':0.01, 'k':8, 'epoch':150}
# xfm.fit(param, 'model.out')
# xfm.setTXTModel("model.txt")

xfm.setSigmoid()
xfm.setTest("test.libsvm")
xfm.predict('model.out', "output.txt")

train = pd.read_csv('data/test.csv', dtype={'id': 'U'})
click = pd.read_csv('output.txt', names=['click'])
out = pd.DataFrame({'id': train['id'], 'click': click['click']})
out.to_csv('Y_out.csv', index=False, sep=',')

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/u014128608/article/details/93393175