在线广告中,点击率(CTR)是评估广告效果的重要指标,随着机器学习技术的不断发展,通过机器学习方法构建自动广告点击预测系统也变得越来越普及。我们收集了 Avazu 公司一段时间内的广告点击数据,利用机器学习方法训练CTR预估模型,完成相应的数据分析报告。
数据文件ctr.csv包含Avazu公司一段时间内的广告点击数据,字段及说明如下:
字段 | 说明 |
---|---|
id | 用户ID |
click | 建模目标,是否点击,1为点击,0为未点击 |
C1 | 匿名分类变量 |
banner_pos | 网页上广告位置 |
site_id | 站点ID |
site_domain | 站点区域 |
site_category | 站点类别 |
app_id | 用户APPID |
app_domain | 用户APP区域 |
app_category | 用户APP类别 |
device_id | 设备ID |
device_ip | 设备IP |
device_model | 设备模型 |
device_type | 设备类型 |
device_conn_type | 设备接入类型 |
C14-C21 | 未知分类变量 |
#忽略警告
import warnings
warnings.filterwarnings('ignore')
#导入相关库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
import matplotlib
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss
#读取数据,在ctr_label.csv中,标签数据无表头,故在读取时为其加上。
ctrdata_x = pd.read_csv('./ctr_train.csv')
ctrdata_y = pd.read_csv('./ctr_labels.csv',header=None,names=['click']) #ctr_label无列索引,设置列索引‘click’
ctrdata_test = pd.read_csv('./ctr_test.csv')
ctrdata_x.head()
可以看到,数据有23个特征列。
ctrdata_y.head(8)
查看数据集基本信息
ctrdata_y.shape
#数据集基本信息
ctrdata_x.info()
ctrdata_y.info()
特征数据与标签数据均为40000条,且均不存在缺失值,但数据中的site_id
、site_domain
、site_category
、app_id
、app_domain
、app_category
、device_id
、device_ip
、device_model
为字符型特征,我们考虑将其转化为数值型特征。
# 选出需要数值编码的特征
str_columns = ctrdata_x.dtypes[ctrdata_x.dtypes == 'object'].index
# LabelEncoder
for col in str_columns:
ctrdata_x[col] = LabelEncoder().fit_transform(ctrdata_x[col])
ctrdata_x.head(10)
可以看到,字符型特征均被转化为数值型特征。
# 查看‘hour’的取值
print(ctrdata_x['hour'].value_counts())
可以看出hour
只有一种取值,对分类无影响,故删去该特征,同时,id
对分类也无影响,同样删去。
# 删除'id','hour'列
ctrdata_x = ctrdata_x.drop(['id','hour'],axis=1)
ctrdata_x.head(3)
特征数据中还剩21个特征列
# 查看数据处理以后的维度
ctrdata_x.shape
由于数据特征较多,我们需要探究一下各特征之间的关系,这将有助于后面建立合理有效的模型。从以下几个方面入手:
首先使用直方图观察未知分类变量C1
,C14
,C15
,C16
,C17
,C18
,C19
,C20
,C21
的分布情况。
col = ctrdata_x.columns[[0,13,14,15,16,17,18,19,20]].tolist()
fig,ax = plt.subplots(3,3,figsize=(15,10))
for i in range(0,len(col)):
plt.subplot("33"+str(i+1))
plt.hist(ctrdata_x[col[i]])
plt.xlabel(col[i],fontsize=15)
plt.ylabel("Count",fontsize=13)
plt.tight_layout()
plt.show
可以看到,在各未知分类变量的取值中,均存在个别值取值极多和个别值取值极少的情况,特征的取值分布存在不平衡现象。
查看标签分布情况:
#绘制条形图
plt.figure(figsize=(2,6))
plot = sns.countplot(x='click',data=ctrdata_y,palette='coolwarm',order=[0,1])
plot.set(xlabel='Class',ylabel='Count',title='click distribution')
plt.show()
可以看到,标签为0的数据接近于标签为1的数据的五倍,数据集存在类别不平衡现象。
我们再看看部分特征与是否点击之间的关系:
#首先将ctrdata_x与ctrdata_y拼接成一张表
ctrdata = pd.merge(ctrdata_x,ctrdata_y,on=ctrdata_x.index.values,how='left')
ctrdata.head(8)
合并后,多了一列索引,将其删去。
ctrdata = ctrdata.drop(['key_0'],axis=1)
ctrdata.head(8)
matplotlib.rcParams['font.sans-serif']=['SimHei'] #解决中文乱码问题
fig,[ax1,ax2] = plt.subplots(1,2,figsize=(15,8))
sns.countplot(x='C16',hue='click',data=ctrdata,palette="Set2",ax=ax1)
sns.countplot(x='C18',hue='click',data=ctrdata,palette="Set2",ax=ax2)
ax1.set_xlabel('C16',fontsize=15)
ax2.set_xlabel('C18',fontsize=15)
ax1.set_title('C16与点击情况的分布',fontsize=15)
ax2.set_title('C18与点击情况的分布',fontsize=15)
ax1.legend(['不点击','点击'],fontsize=15)
ax2.legend(['不点击','点击'],fontsize=15)
plt.show()
由上图可知,C16
取值分布存在不平衡问题,在C16
取值为250.0时,相较于取其它值,点击率高很多;C18
亦是如此,在取值为2.0时,点击率也高于取其它值。
特征间的相关性:
# 特征相关系数图
matplotlib.rcParams['axes.unicode_minus']=False #解决不显示负号问题
fig,ax = plt.subplots(figsize=(15,15))
corr = ctrdata_x.corr()
sns.heatmap(corr,annot = True,vmax=1,square=True,cmap='Reds')
plt.xlabel('Feature',fontsize=15)
plt.ylabel('Feature',fontsize=15)
plt.title('The correlation of features',fontsize=15)
plt.show()
从特征相关系数图中可以看到,正相关系数越高,颜色越深;负相关程度越高,颜色越浅。由图可知,C14
与C17
相关系数达到0.98,故C14
与C17
存在很强的正相关性;C1
与device_type
的相关系数达到0.97,正相关性非常高。所以,我们考虑将C14
和device_type
特征列删去。
ctrdata_x = ctrdata_x.drop(['C14','device_type'],axis=1)
ctrdata_x.head(8)
ctrdata_x.shape
剩下19个特征。
判断点击与否是一个二分类问题,我们使用逻辑回归、决策树这两个单一模型以及使用随机森林、AdaBoost这两种集成模型进行建模。首先,我们使用train_test_split
方法将数据集按照训练集80%和测试集20%的比例划分,模型的评价指标采用二分类交叉熵。
# 划分训练集与测试集
X_trainset,X_testset,y_trainset,y_testset = train_test_split(ctrdata_x,ctrdata_y,test_size=0.2,random_state=2)
逻辑回归是一种广义线性回归,在线性回归基础上,使用Logistic函数将连续型的输出映射到(0,1)之间,用以解决分类问题,而模型的输出可以解释为样本属于正类的概率。
from sklearn.linear_model import LogisticRegression
LR1 = LogisticRegression(random_state=10) #模型构建
LR1.fit(X_trainset,y_trainset) #模型训练
prob_LR1 = LR1.predict_proba(X_testset)
# #模型预测
# y_LRpred1 = LR1.predict(X_testset)
print('LR1模型二分类交叉熵损失:',round(log_loss(y_testset,prob_LR1),3))
可以看到,逻辑回归的交叉熵损失为0.437,但由于数据集存在类别不平衡问题,下面看看设置类别权重后的交叉熵损失。
prob_LR1
数组第一项为预测为‘0’的概率,第二项为预测为‘1’的概率。
LR2 = LogisticRegression(random_state=10,class_weight='balanced') #模型构建
LR2.fit(X_trainset,y_trainset) #模型训练
prob_LR2 = LR2.predict_proba(X_testset)
# #模型预测
# y_LRpred1 = LR1.predict(X_testset)
print('LR2模型二分类交叉熵损失:',round(log_loss(y_testset,prob_LR2),3))
可以看到,设置类别权重后,交叉熵损失反而增大,效果不如未设置类别权重的逻辑回归模型。
决策树的建模过程主要包括:特征选择、树的生成和剪枝。在特征选择的过程中,我们可以选择使用信息增益、信息增益比和基尼系数三种方法来进行特征选择,每次选择出对训练数据最有分类能力的特征。
from sklearn.tree import DecisionTreeClassifier
DT1 = DecisionTreeClassifier(criterion='gini',max_depth=5,random_state=10) #模型构建
DT1.fit(X_trainset,y_trainset) #模型训练
prob_DT1 = DT1.predict_proba(X_testset)
print('DT1模型二分类交叉熵损失:',round(log_loss(y_testset,prob_DT1),3))
可以看到,不考虑类别权重问题时,决策树的交叉熵损失为0.422,与不考虑类别权重的逻辑回归模型性能接近。
DT2 = DecisionTreeClassifier(criterion='gini',max_depth=5,class_weight='balanced',random_state=10) #模型构建
DT2.fit(X_trainset,y_trainset) #模型训练
prob_DT2 = DT2.predict_proba(X_testset)
print('DT2模型二分类交叉熵损失:',round(log_loss(y_testset,prob_DT2),3))
设置类别权重为'balanced'后,决策树的交叉熵损失为0.63,高于不考虑类别权重的决策树模型,性能不如DT1模型,但好于LR2模型。
随机森林是一种集成学习方法,通过使用随机的方式从数据中抽取样本和特征,训练多个不同的决策树,形成“森林”。每个树都给出自己的分类意见,称为“投票”。在分类问题下,选择选票最多的分类作为待测样本的预测类别;在回归问题下则使用平均值。在Python中使用sklearn.ensemble的RandomForestClassifier
构建分类模型,主要参数如下:
参数 | 说明 |
---|---|
n_estimators | 基决策树的个数,默认为10 |
criterion | 最佳划分的评价标准,默认为"gini",可选"entropy" |
max_features | 建立每棵树所使用的特征个数 |
bootstrap | 样本的抽样方式,默认为"True"(有放回) |
obb_score | 是否使用袋外样本估计泛化能力,默认为False |
class_weight | 设置分类权重,默认为"None" |
from sklearn.ensemble import RandomForestClassifier
RFC = RandomForestClassifier(n_estimators=10,class_weight=None,random_state=10)
RFC.fit(X_trainset,y_trainset)
probsRFC = RFC.predict_proba(X_testset)
print('RFC的交叉熵损失:',round(log_loss(y_testset,probsRFC),3))
可以看到,集成模型交叉熵损失达到1.939,效果不是很好。下面对随机森林模型进行网格搜索参数调优。
# 网格搜索参数调优
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
grid_n = [10,100,300,500]
grid_fea = np.arange(2,19)
grid_weight = ["balanced",None]
model_RF = RandomForestClassifier(random_state=10)
grid_search = GridSearchCV(estimator=model_RF,
param_grid={'n_estimators':grid_n,'max_features':grid_fea,'class_weight':grid_weight},
cv=5,scoring='neg_log_loss')
grid_search.fit(X_trainset,y_trainset)
# 返回最优参数
grid_search.best_params_
可以看到,基决策树数为500,不设置分类权重,建立每棵树使用17个特征时,随机森林效果最好。
# 得到预测概率
prob_RF = grid_search.predict_proba(X_testset)
# 输出交叉熵损失
print('model_RF的交叉熵损失:',round(log_loss(y_testset,prob_RF),3))
# 特征重要性评估
best_RF = grid_search.best_estimator_
best_RF.fit(X_trainset,y_trainset)
plt.figure(figsize=(15,6))
pd.Series(best_RF.feature_importances_,index=X_trainset.columns).sort_values().plot(kind='barh')
由上图可以看出,C21 特征对分类特别重要。但该图表明,只用 C21 即可进行分类,我们需要结合其它模型对这一结果进行分析。
参数调优后,随机森林模型的交叉熵损失达到 0.57,较调参前大幅降低,但模型效果仍低于单一模型决策树。
AdaBoost是Boosting的典型代表,其利用同一训练样本的不同加权版本,训练一组弱分类器,然后把这些弱分类器以加权的形式集成起来,形成一个强分类器,AdaBoostClassifier
的主要参数如下:
参数 | 说明 |
---|---|
base_estimator | 基学习器的设置,默认是决策树 |
n_estimators | 基学习器的个数,默认是50 |
learning_rate | 学习率,默认50 |
algorithm | 二分类推广到多分类使用的算法,默认为"SAMME.R",可选"SAMME" |
from sklearn.ensemble import AdaBoostClassifier
model_Ada = AdaBoostClassifier(random_state=10,algorithm='SAMME')
model_Ada.fit(X_trainset,y_trainset)
prob_Ada = model_Ada.predict_proba(X_testset)
print('model_Ada的交叉熵损失:',round(log_loss(y_testset,prob_Ada),2))
基学习器选用决策树,模型的交叉熵损失函数为 0.58 ,下面进行网格搜索调参。
# 网格搜索调参
grid_n = [1, 5, 10, 20, 100, 200]
grid_rate = np.linspace(0.001, 0.5, 10) # [0.001,0.5] 内等间距返回 10 个数
grid_search = GridSearchCV(estimator=model_Ada,
param_grid={'n_estimators':grid_n, 'learning_rate':grid_rate},
cv=5, scoring='neg_log_loss')
grid_search.fit(X_trainset, y_trainset)
# 返回最优参数
grid_search.best_params_
学习率设为0.001,基分类器个数为1时,模型最优。
# 得到预测概率
prob_Ada = grid_search.predict_proba(X_testset)
# 输出交叉熵损失
print('model_Ada的交叉熵损失:',round(log_loss(y_testset,prob_Ada),3))
调参后的交叉熵损失为 0.527 ,模型性能仍不如单一决策树模型。
# 特征重要性评估
plt.figure(figsize=(15,6))
pd.Series(model_Ada.feature_importances_,index=X_trainset.columns).sort_values().plot(kind='barh')
由AdaBoost模型的特征重要性评估图可知,C21 特征对分类的重要性最高。我们可以删去某些不重要的特征,达到降低数据维度的目的。
我们首先删去特征重要性不太高的一些特征:app_id
, banner_pos
, device_ip
, device_id
, device_model
, device_conn_type
, C18
, C1
,然后再对一些特征列做二值化处理。
ctrdata_x = ctrdata_x.drop(['C1','C18','app_id','device_conn_type','device_ip','device_id','device_model','banner_pos'],axis=1)
ctrdata_x.head(8)
然后我们查看一些特征的取值情况。
# 查看 'C15', 'C16', 'C17', 'C19', 'C20', 'C21'特征的取值
C_list = ['C17','C15','C16','C19','C20','C21']
for C_feature in C_list:
print(ctrdata_x[C_feature].value_counts())
C15
, C16
, C17
, C19
, C20
, C21
均出现某一取值的个数极高的情况,故我们考虑对这些特征进行二值化处理。首先,我们需要对这些取值数量极多的数据做一些替换,将取值替换为一个足够大的数,便于后面批量二值化。
ctrdata_x['C15'] = ctrdata_x['C15'].replace(320.0,1000000)
ctrdata_x['C16'] = ctrdata_x['C16'].replace(50.0,1000000)
ctrdata_x['C17'] = ctrdata_x['C17'].replace(1722.0,1000000)
ctrdata_x['C19'] = ctrdata_x['C19'].replace(35.0,1000000)
ctrdata_x['C20'] = ctrdata_x['C20'].replace(-1.0,1000000)
ctrdata_x['C21'] = ctrdata_x['C21'].replace(79.0,1000000)
ctrdata_x.head(10)
# 再次查看 'C15', 'C16', 'C17', 'C19', 'C20', 'C21'特征的取值
C_list = ['C17','C15','C16','C19','C20','C21']
for C_feature in C_list:
print(ctrdata_x[C_feature].value_counts())
经过比较,数据替换无错误。下面我们对数据进行二值化处理。
# 二值化处理
from sklearn.preprocessing import Binarizer
# 阈值设为999999,大于该值映射为 1,小于该值映射为 0。
scaler = Binarizer(threshold=999999)
col_list = ["C15","C16","C17","C19","C20","C21"]
ctrdata_x_new = ctrdata_x
for col in col_list:
ctrdata_x_bin = pd.DataFrame(scaler.fit_transform(ctrdata_x[[col]]),columns = [col+"_bin"])
ctrdata_x_new[col] = ctrdata_x_bin
ctrdata_x_new.head()
查看前五行,C15
, C16
, C17
, C19
, C20
, C21
特征已完成二值化。
ctrdata_y_new = ctrdata_y
# 划分训练集与测试集
X_trainset_new,X_testset_new,y_trainset_new,y_testset_new = train_test_split(ctrdata_x_new,ctrdata_y_new,test_size=0.2,random_state=2)
from sklearn.tree import DecisionTreeClassifier
DT1_2 = DecisionTreeClassifier(criterion='gini',max_depth=5,random_state=10) #模型构建
DT1_2.fit(X_trainset_new,y_trainset_new) #模型训练
prob_DT1_2 = DT1_2.predict_proba(X_testset_new)[:,1]
print('DT1_2模型二分类交叉熵损失:',round(log_loss(y_testset_new,prob_DT1_2),3))
可以看到DT1_2模型的交叉熵损失为 0.433,与处理前的交叉熵损失 0.422相比 ,模型性能并无较大区别。
from sklearn.linear_model import LogisticRegression
LR1_2 = LogisticRegression(random_state=10) #模型构建
LR1_2.fit(X_trainset_new,y_trainset_new) #模型训练
prob_LR1_2 = LR1_2.predict_proba(X_testset_new)
print('LR1_2模型二分类交叉熵损失:',round(log_loss(y_testset_new,prob_LR1_2),3))
与二值化前的0.439相比,模型性能略有提升,说明数据处理有助于提升模型性能。
# 网格搜索参数调优
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
grid_n = [10,100,300,500]
grid_fea = np.arange(1,5)
grid_weight = ["balanced",None]
model_RF = RandomForestClassifier(random_state=10)
grid_search_RF = GridSearchCV(estimator=model_RF,
param_grid={'n_estimators':grid_n,'max_features':grid_fea,'class_weight':grid_weight},
cv=5,scoring='neg_log_loss')
grid_search_RF.fit(X_trainset_new,y_trainset_new)
# best_RF = grid_search_RF.best_estimator_
# best_RF.fit(X_trainset_new,y_trainset_new)
# 得到预测概率
prob_RF = grid_search_RF.predict_proba(X_testset_new)
# 输出交叉熵损失
print('model_RF的交叉熵损失:',round(log_loss(y_testset_new,prob_RF),3))
可以看到,在二值化后的数据上训练的随机森林模型的性能明显好于之前的随机森林模型(0.57),处理后的数据更适合随机森林模型。
# 返回最优参数
grid_search_RF.best_params_
不设置权重,每次建树时使用一个特征,基决策树为500,模型最优。
# 网格搜索调参
from sklearn.ensemble import AdaBoostClassifier
model_Ada = AdaBoostClassifier(random_state=10,algorithm='SAMME')
grid_n = [1, 5, 10, 20, 100, 200]
grid_rate = np.linspace(0.001, 0.5, 10) # [0.001,0.5] 内等间距返回 10 个数
grid_search_Ada = GridSearchCV(estimator=model_Ada,
param_grid={'n_estimators':grid_n, 'learning_rate':grid_rate},
cv=5, scoring='neg_log_loss')
grid_search_Ada.fit(X_trainset_new, y_trainset_new)
best_Ada = grid_search_Ada.best_estimator_
best_Ada.fit(X_trainset_new,y_trainset_new)
# 得到预测概率
prob_Ada = best_Ada.predict_proba(X_testset_new)
# 输出交叉熵损失
print('model_Ada的交叉熵损失:',round(log_loss(y_testset_new,prob_Ada),3))
二值化前AdaBoost交叉熵损失:0.527
AdaBoost在二值化后的数据上表现不如在未进行二值化数据上训练的AdaBoost模型。
# 返回最优参数
grid_search_Ada.best_params_
VotingClassifier组合不同的基分类器,最终通过多数投票(硬投票)或概率平均(软投票)的方式来预测样本类别。其主要参数如下:
参数 | 说明 |
---|---|
estimators | 基分类器设置,需传入一个元组列表 |
voting | 投票方式,硬投票或者软投票,默认为"hard",软投票为"soft" |
weights | 基分类器的权重,需传入一个列表对象 |
from sklearn.ensemble import VotingClassifier
# 决策树与逻辑回归投票表决器
model_vote_single = VotingClassifier(estimators=[('DT',DT1),('LR',LR1)],voting='soft')
model_vote_single.fit(X_trainset,y_trainset)
prob_single = model_vote_single.predict_proba(X_testset)
print('model_vote_single模型二分类交叉熵损失:',round(log_loss(y_testset,prob_single),3))
model_vote_single.fit(X_trainset_new,y_trainset_new)
prob_single_new = model_vote_single.predict_proba(X_testset_new)
print('model_vote_single模型二分类交叉熵损失:',round(log_loss(y_testset_new,prob_single_new),3))
# 集成模型的软投票
model_vote_ensemble = VotingClassifier(estimators=[('RF',best_RF),('Ada',best_Ada)],voting='soft')
model_vote_ensemble.fit(X_trainset,y_trainset)
prob_ensm = model_vote_ensemble.predict_proba(X_testset)
print('model_vote_ensemble模型二分类交叉熵损失:',round(log_loss(y_testset,prob_ensm),2))
model_vote_ensemble2 = VotingClassifier(estimators=[('RF',best_RF),('Ada',best_Ada)],voting='soft')
model_vote_ensemble2.fit(X_trainset_new,y_trainset_new)
prob_ensm_new = model_vote_ensemble2.predict_proba(X_testset_new)
print('model_vote_ensemble2模型二分类交叉熵损失:',round(log_loss(y_testset_new,prob_ensm_new),3))
# 集成模型的加权软投票(加权重)
model_vote_ensemble_wt = VotingClassifier(estimators=[('RF',best_RF),('Ada',best_Ada)],voting='soft',weights=[0.7,0.1])
model_vote_ensemble_wt.fit(X_trainset,y_trainset)
prob_ensm = model_vote_ensemble_wt.predict_proba(X_testset)
print('model_vote_ensemble_wt模型二分类交叉熵损失:',round(log_loss(y_testset,prob_ensm),3))
model_vote_ensemble_wt2 = VotingClassifier(estimators=[('RF',best_RF),('Ada',best_Ada)],voting='soft',weights=[0.7,0.1])
model_vote_ensemble_wt2.fit(X_trainset_new,y_trainset_new)
prob_ensm_new = model_vote_ensemble_wt2.predict_proba(X_testset_new)
print('model_vote_ensemble_wt2模型二分类交叉熵损失:',round(log_loss(y_testset_new,prob_ensm_new),3))
可以看出,VotingClassifier在处理后的数据上的表现要好于在处理前的数据上的表现,但模型提升效果不明显。对分类器加权重时,决策树的权重越高,投票器的性能越好,说明相对于其他模型,决策树更适合这个数据集。
本案例基于点击率数据进行分析,通过数据预处理、探索性分析、模型建立以及特征工程这几个方面对数据进行分析。在模型建立的过程中,我们使用逻辑回归和决策树两种单一模型与随机森林、AdaBoost两种集成模型分析,通过交叉熵损失比较,效果最好的模型为单一模型决策树和决策树与逻辑回归的加权软投票器,其交叉熵损失分别为0.422、0.425。同时,我们发现决策树较适合于该数据集。在对数据特征重要性的分析中我们发现,未知分类变量C17
,C15
,C16
,C19
,C20
,C21
对是否点击起着重要作用,尤其是特征C21
,对结果影响最大,因此,在对点击进行预测时,可重点参考这些未知分类变量特征。