在线广告中,点击率(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 未知分类变量
  • 数据文件已被分为三部分,训练集ctr_train.csv、训练集标签ctr_labels.csv和测试集ctr_test.csv
  • 基于训练集数据建立并训练CTR预估模型,输出测试集上的预测概率。
  • 本项目选择使用测试集的二分类交叉熵(对数损失)作为评价指标(详见sklearn.metrics.log_loss)。
In [1]:
#忽略警告
import warnings
warnings.filterwarnings('ignore')
In [2]:
#导入相关库
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

1. 数据读取

In [3]:
#读取数据,在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')
In [4]:
ctrdata_x.head()
Out[4]:
id hour C1 banner_pos site_id site_domain site_category app_id app_domain app_category ... device_type device_conn_type C14 C15 C16 C17 C18 C19 C20 C21
0 1.042149e+19 14102100 1005 0 6256f5b4 28f93029 f028772b ecad2386 7801e8d9 07d7df22 ... 1.0 0.0 16859.0 320.0 50.0 1887.0 3.0 39.0 -1.0 23.0
1 1.023817e+19 14102100 1002 0 2c4ed2f7 c4e18dd6 50e219e0 ecad2386 7801e8d9 07d7df22 ... 0.0 0.0 21699.0 320.0 50.0 2497.0 3.0 43.0 100151.0 42.0
2 1.589431e+19 14102100 1005 1 856e6d3f 58a89a43 f028772b ecad2386 7801e8d9 07d7df22 ... 1.0 0.0 19771.0 320.0 50.0 2227.0 0.0 687.0 100077.0 48.0
3 1.596550e+19 14102100 1005 0 d9750ee7 98572c79 f028772b ecad2386 7801e8d9 07d7df22 ... 1.0 0.0 15701.0 320.0 50.0 1722.0 0.0 35.0 100084.0 79.0
4 1.173352e+19 14102100 1005 0 d6137915 bb1ef334 f028772b ecad2386 7801e8d9 07d7df22 ... 1.0 0.0 16920.0 320.0 50.0 1899.0 0.0 431.0 100077.0 117.0

5 rows × 23 columns

可以看到,数据有23个特征列。

In [5]:
ctrdata_y.head(8)
Out[5]:
click
0 0
1 0
2 0
3 0
4 0
5 0
6 0
7 1

2. 数据说明与预处理

查看数据集基本信息

In [6]:
ctrdata_y.shape
Out[6]:
(40000, 1)
In [7]:
#数据集基本信息
ctrdata_x.info()
ctrdata_y.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40000 entries, 0 to 39999
Data columns (total 23 columns):
id                  40000 non-null float64
hour                40000 non-null int64
C1                  40000 non-null int64
banner_pos          40000 non-null int64
site_id             40000 non-null object
site_domain         40000 non-null object
site_category       40000 non-null object
app_id              40000 non-null object
app_domain          40000 non-null object
app_category        40000 non-null object
device_id           40000 non-null object
device_ip           40000 non-null object
device_model        40000 non-null object
device_type         40000 non-null float64
device_conn_type    40000 non-null float64
C14                 40000 non-null float64
C15                 40000 non-null float64
C16                 40000 non-null float64
C17                 40000 non-null float64
C18                 40000 non-null float64
C19                 40000 non-null float64
C20                 40000 non-null float64
C21                 40000 non-null float64
dtypes: float64(11), int64(3), object(9)
memory usage: 7.0+ MB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40000 entries, 0 to 39999
Data columns (total 1 columns):
click    40000 non-null int64
dtypes: int64(1)
memory usage: 312.6 KB

特征数据与标签数据均为40000条,且均不存在缺失值,但数据中的site_idsite_domainsite_categoryapp_idapp_domainapp_categorydevice_iddevice_ipdevice_model为字符型特征,我们考虑将其转化为数值型特征。

In [8]:
# 选出需要数值编码的特征
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)
Out[8]:
id hour C1 banner_pos site_id site_domain site_category app_id app_domain app_category ... device_type device_conn_type C14 C15 C16 C17 C18 C19 C20 C21
0 1.042149e+19 14102100 1005 0 260 92 14 489 19 0 ... 1.0 0.0 16859.0 320.0 50.0 1887.0 3.0 39.0 -1.0 23.0
1 1.023817e+19 14102100 1002 0 101 426 6 489 19 0 ... 0.0 0.0 21699.0 320.0 50.0 2497.0 3.0 43.0 100151.0 42.0
2 1.589431e+19 14102100 1005 1 337 179 14 489 19 0 ... 1.0 0.0 19771.0 320.0 50.0 2227.0 0.0 687.0 100077.0 48.0
3 1.596550e+19 14102100 1005 0 542 334 14 489 19 0 ... 1.0 0.0 15701.0 320.0 50.0 1722.0 0.0 35.0 100084.0 79.0
4 1.173352e+19 14102100 1005 0 530 413 14 489 19 0 ... 1.0 0.0 16920.0 320.0 50.0 1899.0 0.0 431.0 100077.0 117.0
5 1.182645e+19 14102100 1005 0 218 318 14 489 19 0 ... 1.0 0.0 20596.0 320.0 50.0 2161.0 0.0 35.0 -1.0 157.0
6 1.470678e+19 14102100 1005 0 339 426 6 464 5 9 ... 1.0 0.0 6560.0 320.0 50.0 571.0 2.0 39.0 -1.0 32.0
7 1.060172e+19 14102100 1005 0 382 254 4 489 19 0 ... 1.0 2.0 17653.0 300.0 250.0 1994.0 2.0 39.0 -1.0 33.0
8 1.241599e+19 14102100 1005 0 72 529 2 489 19 0 ... 1.0 0.0 15704.0 320.0 50.0 1722.0 0.0 35.0 100083.0 79.0
9 1.003899e+19 14102100 1005 0 72 529 2 489 19 0 ... 1.0 0.0 15705.0 320.0 50.0 1722.0 0.0 35.0 -1.0 79.0

10 rows × 23 columns

可以看到,字符型特征均被转化为数值型特征。

In [9]:
# 查看‘hour’的取值
print(ctrdata_x['hour'].value_counts())
14102100    40000
Name: hour, dtype: int64

可以看出hour只有一种取值,对分类无影响,故删去该特征,同时,id对分类也无影响,同样删去。

In [10]:
# 删除'id','hour'列
ctrdata_x = ctrdata_x.drop(['id','hour'],axis=1)
In [8]:
ctrdata_x.head(3)
Out[8]:
C1 banner_pos site_id site_domain site_category app_id app_domain app_category device_id device_ip ... device_type device_conn_type C14 C15 C16 C17 C18 C19 C20 C21
0 1005 0 260 92 14 489 19 0 2391 19903 ... 1.0 0.0 16859.0 320.0 50.0 1887.0 3.0 39.0 -1.0 23.0
1 1002 0 101 426 6 489 19 0 886 3238 ... 0.0 0.0 21699.0 320.0 50.0 2497.0 3.0 43.0 100151.0 42.0
2 1005 1 337 179 14 489 19 0 2391 3423 ... 1.0 0.0 19771.0 320.0 50.0 2227.0 0.0 687.0 100077.0 48.0

3 rows × 21 columns

特征数据中还剩21个特征列

In [11]:
# 查看数据处理以后的维度

ctrdata_x.shape
Out[11]:
(40000, 21)

3. 探索性分析

由于数据特征较多,我们需要探究一下各特征之间的关系,这将有助于后面建立合理有效的模型。从以下几个方面入手:

  • 采用直方图观察未知分类变量的分布情况;
  • 采用直方图观察标签的分布情况;
  • 采用直方图分析点击与某些特征之间的关系;
  • 使用特征的相关系数图分析各个特征之间的相关性。

首先使用直方图观察未知分类变量C1,C14,C15,C16,C17,C18,C19,C20,C21的分布情况。

In [12]:
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
Out[12]:
<function matplotlib.pyplot.show>

可以看到,在各未知分类变量的取值中,均存在个别值取值极多和个别值取值极少的情况,特征的取值分布存在不平衡现象。

查看标签分布情况:

In [13]:
#绘制条形图

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的数据的五倍,数据集存在类别不平衡现象。

我们再看看部分特征与是否点击之间的关系:

In [14]:
#首先将ctrdata_x与ctrdata_y拼接成一张表

ctrdata = pd.merge(ctrdata_x,ctrdata_y,on=ctrdata_x.index.values,how='left')
ctrdata.head(8)
Out[14]:
key_0 C1 banner_pos site_id site_domain site_category app_id app_domain app_category device_id ... device_conn_type C14 C15 C16 C17 C18 C19 C20 C21 click
0 0 1005 0 260 92 14 489 19 0 2391 ... 0.0 16859.0 320.0 50.0 1887.0 3.0 39.0 -1.0 23.0 0
1 1 1002 0 101 426 6 489 19 0 886 ... 0.0 21699.0 320.0 50.0 2497.0 3.0 43.0 100151.0 42.0 0
2 2 1005 1 337 179 14 489 19 0 2391 ... 0.0 19771.0 320.0 50.0 2227.0 0.0 687.0 100077.0 48.0 0
3 3 1005 0 542 334 14 489 19 0 2391 ... 0.0 15701.0 320.0 50.0 1722.0 0.0 35.0 100084.0 79.0 0
4 4 1005 0 530 413 14 489 19 0 2391 ... 0.0 16920.0 320.0 50.0 1899.0 0.0 431.0 100077.0 117.0 0
5 5 1005 0 218 318 14 489 19 0 2391 ... 0.0 20596.0 320.0 50.0 2161.0 0.0 35.0 -1.0 157.0 0
6 6 1005 0 339 426 6 464 5 9 2391 ... 0.0 6560.0 320.0 50.0 571.0 2.0 39.0 -1.0 32.0 0
7 7 1005 0 382 254 4 489 19 0 2391 ... 2.0 17653.0 300.0 250.0 1994.0 2.0 39.0 -1.0 33.0 1

8 rows × 23 columns

合并后,多了一列索引,将其删去。

In [15]:
ctrdata = ctrdata.drop(['key_0'],axis=1)
ctrdata.head(8)
Out[15]:
C1 banner_pos site_id site_domain site_category app_id app_domain app_category device_id device_ip ... device_conn_type C14 C15 C16 C17 C18 C19 C20 C21 click
0 1005 0 260 92 14 489 19 0 2391 19903 ... 0.0 16859.0 320.0 50.0 1887.0 3.0 39.0 -1.0 23.0 0
1 1002 0 101 426 6 489 19 0 886 3238 ... 0.0 21699.0 320.0 50.0 2497.0 3.0 43.0 100151.0 42.0 0
2 1005 1 337 179 14 489 19 0 2391 3423 ... 0.0 19771.0 320.0 50.0 2227.0 0.0 687.0 100077.0 48.0 0
3 1005 0 542 334 14 489 19 0 2391 18911 ... 0.0 15701.0 320.0 50.0 1722.0 0.0 35.0 100084.0 79.0 0
4 1005 0 530 413 14 489 19 0 2391 10483 ... 0.0 16920.0 320.0 50.0 1899.0 0.0 431.0 100077.0 117.0 0
5 1005 0 218 318 14 489 19 0 2391 12 ... 0.0 20596.0 320.0 50.0 2161.0 0.0 35.0 -1.0 157.0 0
6 1005 0 339 426 6 464 5 9 2391 5092 ... 0.0 6560.0 320.0 50.0 571.0 2.0 39.0 -1.0 32.0 0
7 1005 0 382 254 4 489 19 0 2391 11919 ... 2.0 17653.0 300.0 250.0 1994.0 2.0 39.0 -1.0 33.0 1

8 rows × 22 columns

In [16]:
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时,点击率也高于取其它值。

特征间的相关性:

In [17]:
# 特征相关系数图

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()

从特征相关系数图中可以看到,正相关系数越高,颜色越深;负相关程度越高,颜色越浅。由图可知,C14C17相关系数达到0.98,故C14C17存在很强的正相关性;C1device_type的相关系数达到0.97,正相关性非常高。所以,我们考虑将C14device_type特征列删去。

In [18]:
ctrdata_x = ctrdata_x.drop(['C14','device_type'],axis=1)
ctrdata_x.head(8)
Out[18]:
C1 banner_pos site_id site_domain site_category app_id app_domain app_category device_id device_ip device_model device_conn_type C15 C16 C17 C18 C19 C20 C21
0 1005 0 260 92 14 489 19 0 2391 19903 719 0.0 320.0 50.0 1887.0 3.0 39.0 -1.0 23.0
1 1002 0 101 426 6 489 19 0 886 3238 1891 0.0 320.0 50.0 2497.0 3.0 43.0 100151.0 42.0
2 1005 1 337 179 14 489 19 0 2391 3423 1659 0.0 320.0 50.0 2227.0 0.0 687.0 100077.0 48.0
3 1005 0 542 334 14 489 19 0 2391 18911 431 0.0 320.0 50.0 1722.0 0.0 35.0 100084.0 79.0
4 1005 0 530 413 14 489 19 0 2391 10483 1659 0.0 320.0 50.0 1899.0 0.0 431.0 100077.0 117.0
5 1005 0 218 318 14 489 19 0 2391 12 315 0.0 320.0 50.0 2161.0 0.0 35.0 -1.0 157.0
6 1005 0 339 426 6 464 5 9 2391 5092 1225 0.0 320.0 50.0 571.0 2.0 39.0 -1.0 32.0
7 1005 0 382 254 4 489 19 0 2391 11919 712 2.0 300.0 250.0 1994.0 2.0 39.0 -1.0 33.0
In [19]:
ctrdata_x.shape
Out[19]:
(40000, 19)

剩下19个特征。

4. 模型建立

判断点击与否是一个二分类问题,我们使用逻辑回归、决策树这两个单一模型以及使用随机森林、AdaBoost这两种集成模型进行建模。首先,我们使用train_test_split方法将数据集按照训练集80%和测试集20%的比例划分,模型的评价指标采用二分类交叉熵。

In [20]:
# 划分训练集与测试集

X_trainset,X_testset,y_trainset,y_testset = train_test_split(ctrdata_x,ctrdata_y,test_size=0.2,random_state=2)

4.1 逻辑回归

逻辑回归是一种广义线性回归,在线性回归基础上,使用Logistic函数将连续型的输出映射到(0,1)之间,用以解决分类问题,而模型的输出可以解释为样本属于正类的概率。

In [21]:
from sklearn.linear_model import LogisticRegression 

LR1 = LogisticRegression(random_state=10)  #模型构建

LR1.fit(X_trainset,y_trainset)             #模型训练
Out[21]:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=10, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)
In [23]:
prob_LR1 = LR1.predict_proba(X_testset)

# #模型预测
# y_LRpred1 = LR1.predict(X_testset)

print('LR1模型二分类交叉熵损失:',round(log_loss(y_testset,prob_LR1),3))
LR1模型二分类交叉熵损失: 0.437

可以看到,逻辑回归的交叉熵损失为0.437,但由于数据集存在类别不平衡问题,下面看看设置类别权重后的交叉熵损失。

In [20]:
prob_LR1
Out[20]:
array([[0.87160088, 0.12839912],
       [0.87063003, 0.12936997],
       [0.86399356, 0.13600644],
       ...,
       [0.90843304, 0.09156696],
       [0.8048136 , 0.1951864 ],
       [0.83788607, 0.16211393]])

数组第一项为预测为‘0’的概率,第二项为预测为‘1’的概率。

In [25]:
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))
LR2模型二分类交叉熵损失: 0.652

可以看到,设置类别权重后,交叉熵损失反而增大,效果不如未设置类别权重的逻辑回归模型。

4.2 决策树

决策树的建模过程主要包括:特征选择、树的生成和剪枝。在特征选择的过程中,我们可以选择使用信息增益、信息增益比和基尼系数三种方法来进行特征选择,每次选择出对训练数据最有分类能力的特征。

In [26]:
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))
DT1模型二分类交叉熵损失: 0.422

可以看到,不考虑类别权重问题时,决策树的交叉熵损失为0.422,与不考虑类别权重的逻辑回归模型性能接近。

In [27]:
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))
DT2模型二分类交叉熵损失: 0.63

设置类别权重为'balanced'后,决策树的交叉熵损失为0.63,高于不考虑类别权重的决策树模型,性能不如DT1模型,但好于LR2模型。

4.3 随机森林

随机森林是一种集成学习方法,通过使用随机的方式从数据中抽取样本和特征,训练多个不同的决策树,形成“森林”。每个树都给出自己的分类意见,称为“投票”。在分类问题下,选择选票最多的分类作为待测样本的预测类别;在回归问题下则使用平均值。在Python中使用sklearn.ensemble的RandomForestClassifier构建分类模型,主要参数如下:

参数 说明
n_estimators 基决策树的个数,默认为10
criterion 最佳划分的评价标准,默认为"gini",可选"entropy"
max_features 建立每棵树所使用的特征个数
bootstrap 样本的抽样方式,默认为"True"(有放回)
obb_score 是否使用袋外样本估计泛化能力,默认为False
class_weight 设置分类权重,默认为"None"
In [28]:
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))
C:\ProgramData\Anaconda3\lib\site-packages\sklearn\ensemble\weight_boosting.py:29: DeprecationWarning: numpy.core.umath_tests is an internal NumPy module and should not be imported. It will be removed in a future NumPy release.
  from numpy.core.umath_tests import inner1d
RFC的交叉熵损失: 1.939

可以看到,集成模型交叉熵损失达到1.939,效果不是很好。下面对随机森林模型进行网格搜索参数调优。

In [29]:
# 网格搜索参数调优
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)
Out[29]:
GridSearchCV(cv=5, error_score='raise',
       estimator=RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,
            oob_score=False, random_state=10, verbose=0, warm_start=False),
       fit_params=None, iid=True, n_jobs=1,
       param_grid={'n_estimators': [10, 100, 300, 500], 'max_features': array([ 2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18]), 'class_weight': ['balanced', None]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='neg_log_loss', verbose=0)
In [30]:
# 返回最优参数

grid_search.best_params_
Out[30]:
{'class_weight': None, 'max_features': 17, 'n_estimators': 500}

可以看到,基决策树数为500,不设置分类权重,建立每棵树使用17个特征时,随机森林效果最好。

In [31]:
# 得到预测概率

prob_RF = grid_search.predict_proba(X_testset)
In [32]:
# 输出交叉熵损失

print('model_RF的交叉熵损失:',round(log_loss(y_testset,prob_RF),3))
model_RF的交叉熵损失: 0.57
In [33]:
# 特征重要性评估

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')
Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x2c11b5c3f28>

由上图可以看出,C21 特征对分类特别重要。但该图表明,只用 C21 即可进行分类,我们需要结合其它模型对这一结果进行分析。

参数调优后,随机森林模型的交叉熵损失达到 0.57,较调参前大幅降低,但模型效果仍低于单一模型决策树。

4.4 AdaBoost

AdaBoost是Boosting的典型代表,其利用同一训练样本的不同加权版本,训练一组弱分类器,然后把这些弱分类器以加权的形式集成起来,形成一个强分类器,AdaBoostClassifier的主要参数如下:

参数 说明
base_estimator 基学习器的设置,默认是决策树
n_estimators 基学习器的个数,默认是50
learning_rate 学习率,默认50
algorithm 二分类推广到多分类使用的算法,默认为"SAMME.R",可选"SAMME"
In [34]:
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))
model_Ada的交叉熵损失: 0.63

基学习器选用决策树,模型的交叉熵损失函数为 0.58 ,下面进行网格搜索调参。

In [35]:
# 网格搜索调参

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)
Out[35]:
GridSearchCV(cv=5, error_score='raise',
       estimator=AdaBoostClassifier(algorithm='SAMME', base_estimator=None, learning_rate=1.0,
          n_estimators=50, random_state=10),
       fit_params=None, iid=True, n_jobs=1,
       param_grid={'n_estimators': [1, 5, 10, 20, 100, 200], 'learning_rate': array([0.001  , 0.05644, 0.11189, 0.16733, 0.22278, 0.27822, 0.33367,
       0.38911, 0.44456, 0.5    ])},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring='neg_log_loss', verbose=0)
In [37]:
# 返回最优参数

grid_search.best_params_
Out[37]:
{'learning_rate': 0.001, 'n_estimators': 1}

学习率设为0.001,基分类器个数为1时,模型最优。

In [40]:
# 得到预测概率

prob_Ada = grid_search.predict_proba(X_testset)
In [41]:
# 输出交叉熵损失

print('model_Ada的交叉熵损失:',round(log_loss(y_testset,prob_Ada),3))
model_Ada的交叉熵损失: 0.527

调参后的交叉熵损失为 0.527 ,模型性能仍不如单一决策树模型。

In [42]:
# 特征重要性评估

plt.figure(figsize=(15,6))

pd.Series(model_Ada.feature_importances_,index=X_trainset.columns).sort_values().plot(kind='barh')
Out[42]:
<matplotlib.axes._subplots.AxesSubplot at 0x2c11ab12128>

由AdaBoost模型的特征重要性评估图可知,C21 特征对分类的重要性最高。我们可以删去某些不重要的特征,达到降低数据维度的目的。

4.5 特征工程

4.5.1 数据二值化

我们首先删去特征重要性不太高的一些特征:app_id, banner_pos, device_ip, device_id, device_model, device_conn_type, C18, C1,然后再对一些特征列做二值化处理。

In [43]:
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)
Out[43]:
site_id site_domain site_category app_domain app_category C15 C16 C17 C19 C20 C21
0 260 92 14 19 0 320.0 50.0 1887.0 39.0 -1.0 23.0
1 101 426 6 19 0 320.0 50.0 2497.0 43.0 100151.0 42.0
2 337 179 14 19 0 320.0 50.0 2227.0 687.0 100077.0 48.0
3 542 334 14 19 0 320.0 50.0 1722.0 35.0 100084.0 79.0
4 530 413 14 19 0 320.0 50.0 1899.0 431.0 100077.0 117.0
5 218 318 14 19 0 320.0 50.0 2161.0 35.0 -1.0 157.0
6 339 426 6 5 9 320.0 50.0 571.0 39.0 -1.0 32.0
7 382 254 4 19 0 300.0 250.0 1994.0 39.0 -1.0 33.0

然后我们查看一些特征的取值情况。

In [44]:
# 查看 '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())
1722.0    15327
2161.0     5415
2333.0     4321
2480.0     1467
2493.0     1211
1993.0      926
2227.0      840
2374.0      747
1994.0      739
2253.0      565
1800.0      509
1899.0      423
2043.0      341
2264.0      340
1887.0      335
2446.0      310
423.0       304
2371.0      291
2351.0      284
1863.0      251
576.0       249
1934.0      211
2162.0      210
571.0       206
2331.0      183
2307.0      163
2372.0      157
1895.0      153
2449.0      147
2496.0      141
          ...  
1447.0        8
2304.0        8
2292.0        8
1769.0        7
2154.0        6
2225.0        6
1698.0        6
2284.0        5
1884.0        5
394.0         5
178.0         5
1932.0        4
2279.0        3
2425.0        3
2478.0        3
2483.0        3
1255.0        2
1685.0        2
2476.0        2
937.0         2
2278.0        2
2346.0        2
827.0         2
1528.0        2
2438.0        2
1507.0        1
686.0         1
2397.0        1
2250.0        1
153.0         1
Name: C17, Length: 125, dtype: int64
320.0    38013
300.0     1631
216.0      350
728.0        5
120.0        1
Name: C15, dtype: int64
50.0     38215
250.0     1419
36.0       350
480.0       10
90.0         5
20.0         1
Name: C16, dtype: int64
35.0      22335
39.0       8801
297.0      1587
687.0      1054
163.0       954
1063.0      926
167.0       825
303.0       669
427.0       442
431.0       439
171.0       328
551.0       291
47.0        195
161.0       159
813.0       157
681.0       153
175.0       116
1319.0      104
1711.0       85
291.0        69
43.0         68
943.0        43
1835.0       36
169.0        27
1315.0       27
41.0         26
1451.0       23
801.0        13
683.0        10
559.0         8
673.0         7
679.0         6
423.0         5
1327.0        5
33.0          3
425.0         2
1071.0        2
Name: C19, dtype: int64
-1.0         24834
 100084.0     4591
 100111.0     1559
 100083.0     1535
 100077.0      617
 100081.0      541
 100075.0      522
 100148.0      424
 100000.0      401
 100034.0      375
 100228.0      313
 100074.0      312
 100079.0      226
 100105.0      219
 100210.0      192
 100217.0      183
 100020.0      171
 100130.0      165
 100028.0      153
 100050.0      139
 100131.0      129
 100128.0      113
 100046.0      110
 100156.0      103
 100233.0      100
 100193.0       92
 100191.0       91
 100119.0       90
 100215.0       83
 100188.0       82
             ...  
 100168.0        4
 100141.0        4
 100165.0        4
 100025.0        3
 100053.0        3
 100124.0        3
 100097.0        3
 100163.0        3
 100095.0        3
 100029.0        3
 100150.0        3
 100112.0        2
 100138.0        2
 100099.0        2
 100051.0        2
 100177.0        2
 100040.0        2
 100061.0        2
 100133.0        2
 100152.0        1
 100090.0        1
 100185.0        1
 100091.0        1
 100213.0        1
 100071.0        1
 100143.0        1
 100004.0        1
 100043.0        1
 100056.0        1
 100055.0        1
Name: C20, Length: 129, dtype: int64
79.0     15327
157.0     9736
23.0      2814
61.0      2685
33.0      2025
117.0     1634
32.0      1331
48.0      1155
52.0       565
46.0       448
156.0      410
16.0       280
91.0       278
13.0       249
15.0       218
95.0       201
71.0       187
101.0      160
42.0        69
93.0        64
82.0        47
100.0       43
94.0        24
111.0       20
116.0        9
68.0         9
70.0         8
85.0         3
20.0         1
Name: C21, dtype: int64

C15, C16, C17, C19, C20, C21均出现某一取值的个数极高的情况,故我们考虑对这些特征进行二值化处理。首先,我们需要对这些取值数量极多的数据做一些替换,将取值替换为一个足够大的数,便于后面批量二值化。

In [45]:
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)
Out[45]:
site_id site_domain site_category app_domain app_category C15 C16 C17 C19 C20 C21
0 260 92 14 19 0 1000000.0 1000000.0 1887.0 39.0 1000000.0 23.0
1 101 426 6 19 0 1000000.0 1000000.0 2497.0 43.0 100151.0 42.0
2 337 179 14 19 0 1000000.0 1000000.0 2227.0 687.0 100077.0 48.0
3 542 334 14 19 0 1000000.0 1000000.0 1000000.0 1000000.0 100084.0 1000000.0
4 530 413 14 19 0 1000000.0 1000000.0 1899.0 431.0 100077.0 117.0
5 218 318 14 19 0 1000000.0 1000000.0 2161.0 1000000.0 1000000.0 157.0
6 339 426 6 5 9 1000000.0 1000000.0 571.0 39.0 1000000.0 32.0
7 382 254 4 19 0 300.0 250.0 1994.0 39.0 1000000.0 33.0
8 72 529 2 19 0 1000000.0 1000000.0 1000000.0 1000000.0 100083.0 1000000.0
9 72 529 2 19 0 1000000.0 1000000.0 1000000.0 1000000.0 1000000.0 1000000.0
In [46]:
# 再次查看 '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())
1000000.0    15327
2161.0        5415
2333.0        4321
2480.0        1467
2493.0        1211
1993.0         926
2227.0         840
2374.0         747
1994.0         739
2253.0         565
1800.0         509
1899.0         423
2043.0         341
2264.0         340
1887.0         335
2446.0         310
423.0          304
2371.0         291
2351.0         284
1863.0         251
576.0          249
1934.0         211
2162.0         210
571.0          206
2331.0         183
2307.0         163
2372.0         157
1895.0         153
2449.0         147
2496.0         141
             ...  
1447.0           8
2292.0           8
2304.0           8
1769.0           7
1698.0           6
2225.0           6
2154.0           6
2284.0           5
394.0            5
1884.0           5
178.0            5
1932.0           4
2478.0           3
2483.0           3
2425.0           3
2279.0           3
937.0            2
1528.0           2
2438.0           2
1685.0           2
2476.0           2
1255.0           2
2346.0           2
2278.0           2
827.0            2
153.0            1
1507.0           1
2250.0           1
686.0            1
2397.0           1
Name: C17, Length: 125, dtype: int64
1000000.0    38013
300.0         1631
216.0          350
728.0            5
120.0            1
Name: C15, dtype: int64
1000000.0    38215
250.0         1419
36.0           350
480.0           10
90.0             5
20.0             1
Name: C16, dtype: int64
1000000.0    22335
39.0          8801
297.0         1587
687.0         1054
163.0          954
1063.0         926
167.0          825
303.0          669
427.0          442
431.0          439
171.0          328
551.0          291
47.0           195
161.0          159
813.0          157
681.0          153
175.0          116
1319.0         104
1711.0          85
291.0           69
43.0            68
943.0           43
1835.0          36
169.0           27
1315.0          27
41.0            26
1451.0          23
801.0           13
683.0           10
559.0            8
673.0            7
679.0            6
423.0            5
1327.0           5
33.0             3
425.0            2
1071.0           2
Name: C19, dtype: int64
1000000.0    24834
100084.0      4591
100111.0      1559
100083.0      1535
100077.0       617
100081.0       541
100075.0       522
100148.0       424
100000.0       401
100034.0       375
100228.0       313
100074.0       312
100079.0       226
100105.0       219
100210.0       192
100217.0       183
100020.0       171
100130.0       165
100028.0       153
100050.0       139
100131.0       129
100128.0       113
100046.0       110
100156.0       103
100233.0       100
100193.0        92
100191.0        91
100119.0        90
100215.0        83
100188.0        82
             ...  
100165.0         4
100026.0         4
100168.0         4
100029.0         3
100163.0         3
100025.0         3
100150.0         3
100053.0         3
100097.0         3
100095.0         3
100124.0         3
100040.0         2
100138.0         2
100133.0         2
100051.0         2
100099.0         2
100061.0         2
100112.0         2
100177.0         2
100090.0         1
100152.0         1
100004.0         1
100091.0         1
100071.0         1
100185.0         1
100043.0         1
100143.0         1
100056.0         1
100055.0         1
100213.0         1
Name: C20, Length: 129, dtype: int64
1000000.0    15327
157.0         9736
23.0          2814
61.0          2685
33.0          2025
117.0         1634
32.0          1331
48.0          1155
52.0           565
46.0           448
156.0          410
16.0           280
91.0           278
13.0           249
15.0           218
95.0           201
71.0           187
101.0          160
42.0            69
93.0            64
82.0            47
100.0           43
94.0            24
111.0           20
116.0            9
68.0             9
70.0             8
85.0             3
20.0             1
Name: C21, dtype: int64

经过比较,数据替换无错误。下面我们对数据进行二值化处理。

In [47]:
# 二值化处理

from sklearn.preprocessing import Binarizer
In [50]:
# 阈值设为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()
Out[50]:
site_id site_domain site_category app_domain app_category C15 C16 C17 C19 C20 C21
0 260 92 14 19 0 0.0 0.0 0.0 0.0 0.0 0.0
1 101 426 6 19 0 0.0 0.0 0.0 0.0 0.0 0.0
2 337 179 14 19 0 0.0 0.0 0.0 0.0 0.0 0.0
3 542 334 14 19 0 0.0 0.0 0.0 0.0 0.0 0.0
4 530 413 14 19 0 0.0 0.0 0.0 0.0 0.0 0.0

查看前五行,C15, C16, C17, C19, C20, C21特征已完成二值化。

In [51]:
ctrdata_y_new = ctrdata_y
In [52]:
# 划分训练集与测试集

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)

4.5.2 决策树在二值化后的数据上的表现

In [54]:
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

可以看到DT1_2模型的交叉熵损失为 0.433,与处理前的交叉熵损失 0.422相比 ,模型性能并无较大区别。

4.5.3 逻辑回归在二值化后的数据上的表现

In [55]:
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))
LR1_2模型二分类交叉熵损失: 0.46

与二值化前的0.439相比,模型性能略有提升,说明数据处理有助于提升模型性能。

4.5.4 随机森林在二值化后的数据上的表现

In [56]:
# 网格搜索参数调优
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))
model_RF的交叉熵损失: 0.469

可以看到,在二值化后的数据上训练的随机森林模型的性能明显好于之前的随机森林模型(0.57),处理后的数据更适合随机森林模型。

In [57]:
# 返回最优参数

grid_search_RF.best_params_
Out[57]:
{'class_weight': None, 'max_features': 1, 'n_estimators': 500}

不设置权重,每次建树时使用一个特征,基决策树为500,模型最优。

4.5.5 AdaBoost在二值化后的数据上的表现

In [59]:
# 网格搜索调参
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))
model_Ada的交叉熵损失: 0.531

二值化前AdaBoost交叉熵损失:0.527

AdaBoost在二值化后的数据上表现不如在未进行二值化数据上训练的AdaBoost模型。

In [60]:
# 返回最优参数

grid_search_Ada.best_params_
Out[60]:
{'learning_rate': 0.001, 'n_estimators': 1}

4.6 VotingClassifier

VotingClassifier组合不同的基分类器,最终通过多数投票(硬投票)或概率平均(软投票)的方式来预测样本类别。其主要参数如下:

参数 说明
estimators 基分类器设置,需传入一个元组列表
voting 投票方式,硬投票或者软投票,默认为"hard",软投票为"soft"
weights 基分类器的权重,需传入一个列表对象
In [61]:
from sklearn.ensemble import VotingClassifier

4.6.1 决策树与逻辑回归投票表决器

In [62]:
# 决策树与逻辑回归投票表决器

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模型二分类交叉熵损失: 0.425
In [63]:
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_single模型二分类交叉熵损失: 0.437

4.6.2 集成模型投票表决器

In [65]:
# 集成模型的软投票

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_ensemble模型二分类交叉熵损失: 0.45
In [68]:
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_ensemble2模型二分类交叉熵损失: 0.457
In [70]:
# 集成模型的加权软投票(加权重)

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_wt模型二分类交叉熵损失: 0.444
In [72]:
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))
model_vote_ensemble_wt2模型二分类交叉熵损失: 0.428

可以看出,VotingClassifier在处理后的数据上的表现要好于在处理前的数据上的表现,但模型提升效果不明显。对分类器加权重时,决策树的权重越高,投票器的性能越好,说明相对于其他模型,决策树更适合这个数据集。

5. 总结

本案例基于点击率数据进行分析,通过数据预处理、探索性分析、模型建立以及特征工程这几个方面对数据进行分析。在模型建立的过程中,我们使用逻辑回归和决策树两种单一模型与随机森林、AdaBoost两种集成模型分析,通过交叉熵损失比较,效果最好的模型为单一模型决策树和决策树与逻辑回归的加权软投票器,其交叉熵损失分别为0.422、0.425。同时,我们发现决策树较适合于该数据集。在对数据特征重要性的分析中我们发现,未知分类变量C17,C15,C16,C19,C20,C21对是否点击起着重要作用,尤其是特征C21,对结果影响最大,因此,在对点击进行预测时,可重点参考这些未知分类变量特征。

本案例由数据酷客创造营学员张宁宁撰写。