数据竞赛/第二届易观算法大赛 Top2 方案分享

数据竞赛/第二届易观算法大赛 Top2 方案分享

赛题描述

本文介绍第二届易观算法大赛——根据用户手机 APP 的使用情况,预测用户的性别和年龄,这是比赛地址。这虽然是一年多前的比赛,其中的数据处理和特征工程等思路依然值得学习。

这次大赛的要求根据用户手机数据、和手机上的应用数据等,训练模型预测用户的性别和年龄。

在上一篇文章中,分享了一个 baseline,把每个 APP 当作一个词,使用 TF-IDF 计算权重作为特征,进行训练。在测试集上得到了 logloss 为 2.73161 的分数。但是没有利用其他的信息,本文主要利用了 APP 的使用数据、APP 的类别数据、以及设备本身的型号数据等提取特征,主要特征提取思路如下图所示。


在模型方面,采用了机器学习以及神经网络两种方法。

  • 其中机器学习方法中用到的特征有 APP 使用行为特征、APP 类别特征、手机属性特征、以及 APP 的 TF_IDF 特征,方法使用了 LogisticRegression、SGDClassifier、PassiveAggressiveClassifier、RidgeClassifier、BernoulliNB、MultinomialNB、LinearSVC 对 APP 的 Tf-IDF 做预训练,最终使用了 XGB 预测性别以及年龄。在测试集上取得了 2.59489 的分数。

  • 神经网络用到的特征有 APP 使用行为特征、APP 类别特征、手机属性特征、以及 APP 的 Word2Vec 向量。模型结构如下:


其中Other feature代表APP 使用行为特征、APP 类别特征以及手机属性特征拼接的特征向量。

数据表

该数据包含了 6 个表。 - deviceid_packages.tsv:设备数据。包括每个设备上的应用安装列表,设备和应用名都进行了 hash 处理。

- deviceid_package_start_close.tsv:每个设备上各个应用的打开、关闭行为数据。第三、第四列是带毫秒的时间戳,分别表示应用打开时间和关闭时间。

- deviceid_brand.tsv:机型数据:每个设备的品牌和型号。

- package_label.tsv:APP数据,每个应用的类别信息。

- deviceid_train.tsv:训练数据:每个设备对应的性别、年龄段。


- deviceid_test.tsv:测试数据,只包含设备号

一个设备 ID 会有唯一的性别和年龄段。性别有1、2两种可能值,分别代表男和女。年龄段有 0 到 10 十一种可能,分别代表不同的年龄段,且数值越大相应的年龄越大。一个设备只属于一个唯一的类别(性别+年龄段),共有 22 个类别。

因此该问题可以看作22 分类问题。 也可以分成两个问题的组合来看:一个是性别的 2 分类的问题,一个是年龄的 11 分类问题,按照两种策略分类好之后,再把结果组合成 22 分类问题。

在上一篇 baseline 中,我们把题目建模为 22 分类的问题。

在这一篇中,我们按照第二种方式来建模,即分成两个问题的组合来看:一个是性别的 2 分类的问题,一个是年龄的 11 分类问题,按照两种策略分类好之后,再把结果组合成 22 分类问题。

预测结果的 csv 文件为每一种类别的概率值,格式按照以下示例,1-0代表男性,第 0 个年龄段。从第二行开始,每一行概率之和应为 1

1
2
3
DeviceID, 1-0,1-1,1-2,…,1-9,1-10,2-0,2-1,2-2, …,2-9,2-10

1111111, 0.05,0.05,0.05,…,0.05,0.05,0.05,0.05,0.05,…,0.05,0.05

数据处理

主要包括读取数据,并进行可视化。

  1. 首先导入包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pandas as pd
%matplotlib inline
import os
from sklearn.feature_extraction.text import TfidfVectorizer, TfidfTransformer, CountVectorizer
import gc
from tqdm import tqdm
import pickle
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.model_selection import train_test_split
import lightgbm as lgb
from sklearn.preprocessing import LabelEncoder

import pandas as pd
import seaborn as sns
import numpy as np
from tqdm import tqdm
from sklearn.metrics import accuracy_score
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import time

from sklearn import preprocessing
from sklearn.feature_extraction.text import TfidfVectorizer

from scipy.sparse import hstack, vstack
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_val_score
# from skopt.space import Integer, Categorical, Real, Log10
# from skopt.utils import use_named_args
# from skopt import gp_minimize
from gensim.models import Word2Vec, FastText
import gensim
import re
  1. 列出所有文件
1
2
3
4
# 数据都放在 ./Demo 文件夹中
file_lists=os.listdir('./Demo/')
for file in file_lists:
print(file)


3. 读取数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 读取 设备信息,包括品牌和型号
deviced_brand=pd.read_csv('./Demo/deviceid_brand.tsv',sep='\t', names=['device_id','brand','model'])
# 读取 app 信息,包括 app 所属的类别
package_label=pd.read_csv('./Demo/package_label.tsv',sep='\t',names=['app','class1','class2'])
# 读取训练数据集
deviceid_train=pd.read_csv('./Demo/deviceid_train.tsv',sep='\t',names=['device_id','sex','age'])
# 读取测试数据集
deviceid_test=pd.read_csv('./Demo/deviceid_test.tsv',sep='\t',names=['device_id'])
# 读取 app 数据
app_category=pd.read_csv('./Demo/package_label.tsv',sep='\t', names=['app', 'category', 'app_name'])
# 读取设备安装的 app 数据
deviceid_packages=pd.read_csv('./Demo/deviceid_packages.tsv',sep='\t', names=['device_id','apps'])
# 读取 APP 使用情况
package_time=pd.read_csv('./Demo/deviceid_package_start_close.tsv',sep='\t',names=['device_id','app','start','close'])

用户 APP 使用行为特征构造

从上面数据说明中我们可以知道能使用的数据只有设备的上的安装的 APP,以及对应的打开和关闭时间。应该利用 APP 的数据来构造一系列特征。

每个设备上的每个 APP 的时间特征分析

  1. 首先统计每个设备的每个 APP 的使用时长
1
2
3
4
5
6
7
# 计算 app 使用时长,单位为秒
package_time['period']=(package_time['close']-package_time['start'])/1000
# 把 app 的开始使用时间戳转换为 pd.datetime 数据类型
package_time['start']=pd.to_datetime(package_time['start'], unit='ms')
# 删除 app 使用结束的时间戳
del package_time['close']
gc.collect()

分别提取 app 开始使用的小时,日期,和星期几。

1
2
3
package_time['hour']=package_time['start'].dt.hour
package_time['date']=package_time['start'].dt.date
package_time['dayofweek']=package_time['start'].dt.dayofweek

package_time表如下:


2. 分别统计每个设备在每天、每小时、每周以及每个 APP 的使用时间

1
2
3
4
5
6
7
8
#计算每个设备的每天使用时间
dtime=package_time.groupby(['device_id','date'])['period'].agg('sum')
#计算每个设备的每小时使用时间
qtime=package_time.groupby(['device_id','hour'])['period'].agg('sum')
#计算每个设备的每周使用时间
wtime=package_time.groupby(['device_id','dayofweek'])['period'].agg('sum')
#计算每个设备上每个 app 的使用时间
atime=package_time.groupby(['device_id','app'])['period'].agg('sum')

d_time表如下,qtimewtime以及atime表类似,故不展示。


3. 统计每个设备每天使用的 app 数量

1
2
3
4
5
6
# 计算每个设备每天使用的 app 数量
# 首先根据 ['device_id', 'date'] 分组,然后将每个设备每天的所有 app 用空格连接起来
dapp=package_time[['device_id', 'date', 'app']].drop_duplicates().groupby(['device_id', 'date'])['app'].agg(' '.join)
dapp = dapp.reset_index()
# 根据空格分隔 app,并计算数量
dapp['app_nums']=dapp['app'].apply(lambda x: x.split(' ')).apply(len)

dapp表如下:


4. 计算每个设备使用 APP 数量的方差、平均值、最大值

agg() 中可以传入多个函数对 groupby 对象的操作函数。这里传入 dict,其中 key 作为 index,value 是执行的聚合函数。

1
2
3
4
5
# 计算每个设备使用 APP 数量的方差、平均值、最大值。 agg() 中可以传入多个函数对 groupby 对象的操作函数
dapp_stat = dapp.groupby('device_id')['app_nums'].agg(
{'std': 'std', 'mean': 'mean', 'max': 'max'})
dapp_stat = dapp_stat.reset_index()
dapp_stat.columns = ['device_id', 'app_num_std', 'app_num_mean', 'app_num_max']

dapp_stat表如下:


5. 计算每个设备使用时间的和、方差、平均值、最大值。

代码和上面类似。

1
2
3
4
5
6
dtime = dtime.reset_index()
# 和上面的操作类似,计算每个设备使用时间的和、方差、平均值、最大值
dtime_stat = dtime.groupby(['device_id'])['period'].agg(
{'sum': 'sum', 'mean': 'mean', 'std': 'std', 'max': 'max'}).reset_index()
dtime_stat.columns = ['device_id', 'date_sum',
'date_mean', 'date_std', 'date_max']

dtime_stat表如下:


6. 计算每个设备在每个小时的开始使用时间

通过透视来转置每个设备的每个小时的使用时间。变换之前的数据每个设备有 24 行,每行表示设备每个小时的使用时间。变换之后的数据有 24 列,每一列表示设备每个小时的使用时间。

1
2
3
4
5
6
7
8
# 通过透视把每个设备的每个小时开始使用时间的转置。变换之前的数据每个设备有 24 行,每行表示设备每个小时的使用时间。
# 变换之后的数据有 24 列,每一列表示设备每个小时的使用时间
qtime = qtime.reset_index()
ftime = qtime.pivot(index='device_id', columns='hour',
values='period').fillna(0)
# 设置列名
ftime.columns = ['h%s' % i for i in range(24)]
ftime.reset_index(inplace=True)

ftime_stat表如下:


7. 计算每个设备在每个周几的使用时间

通过透视转置每个设备的每个周几的使用时间。变换之前的数据每个设备有 7 行,每行表示设备每个周几的使用时间。变换之后的数据有 7 列,每一列表示设备每个周几的使用时间

1
2
3
4
5
6
7
wtime = wtime.reset_index()
# 通过透视把每个设备的每个周几的使用时间的转置。变换之前的数据每个设备有 7 行,每行表示设备每个周几的使用时间。
# 变换之后的数据有 7 列,每一列表示设备每个周几的使用时间
weektime = wtime.pivot(
index='device_id', columns='dayofweek', values='period').fillna(0)
weektime.columns = ['w0', 'w1', 'w2', 'w3', 'w4', 'w5', 'w6']
weektime.reset_index(inplace=True)

weektime表如下:


8. 找出每个设备上使用时间最多的 APP

找出每个设备上使用时间最多的 app 的行索引。其中idmax() 的作用和argmax()的作用 类似,都是找出最大值所在的索引。

1
2
3
4
# 找出每个设备上使用时间最多的 app 的行索引
atime = atime.reset_index()
app = atime.groupby(['device_id'])['period'].idxmax()
app.head()


9. 汇总用户 APP 使用时间的特征数据

把处理的中间表全部汇总连接,得到用户行为数据表。

1
2
3
4
5
6
7
8
9
10
把表连接起来,得到用户行为数据表
# dapp_stat 是每个设备每天使用 APP 数量的方差、平均值、最大值
# dtime_stat 是每个设备使用时间的和、方差、平均值、最大值
user = pd.merge(dapp_stat, dtime_stat, on='device_id', how='left')
# ftime 是每个设备的每个小时的使用时间
user = pd.merge(user, ftime, on='device_id', how='left')
# weektime 是每个设备的每个周几的使用时间
user = pd.merge(user, weektime, on='device_id', how='left')
# atime.iloc[app] 表示每个设备使用时间最多的app 的名字和时间
user = pd.merge(user, atime.iloc[app], on='device_id', how='left')

user表如下:


# APP 类别特征构造

  1. 计算每种类别的数量,并且添加编号 idx,结果中类别名作为索引。
1
2
3
4
# 计算每种类别的数量,并且添加编号 idx,作用类似于 LabelEncoder
cat_enc = pd.DataFrame(app_category['category'].value_counts())
cat_enc['idx']=range(cat_enc.shape[0])
cat_enc.head()


2. 使用 map 函数,将每个类别对应的编号映射到原来的 app 表中

1
2
3
# 使用 map 函数,将每个类别对应的编号映射到原来的 app 表中
app_category['cat_enc'] = app_category['category'].map(cat_enc['idx'])
app_category.head()


3. 计算每个设备在每个APP类别分别使用了多少个 APP

将 app 所在的列设置为索引,方便下一步操作。

1
2
# 将 app 所在的列设置为索引
app_category.set_index(['app'], inplace=True)

将每个设备的每个 APP 的使用时间的表添加编号映射,因为编号总共有 0-44,如果某个 APP 没有对应编号,则填入 45。

1
2
# 将每个设备的每个 APP 的使用时间的表添加编号映射,如果没有编号,则填入 45
atime['app_cat_enc'] = atime['app'].map(app_category['cat_enc']).fillna(45)

atime表如下:


计算每个设备在每个APP类别分别使用了多少个 APP

1
2
# 计算每个设备在每个APP类别分别使用了多少个 APP
cat_num = atime.groupby(['device_id', 'app_cat_enc'])['app'].agg('count').reset_index()

cat_num表如下:


4. 计算每个设备在每个APP类别分别使用了多长时间。

1
2
3
# 计算每个设备在每个APP类别分别使用了多长时间
cat_time = atime.groupby(['device_id', 'app_cat_enc'])[
'period'].agg('sum').reset_index()

通过透视来转置每个设备在每个APP类别分别使用了多少个 APP。变换之后的数据有 46 列,每一列表示某个列别的 APP 个数。

1
2
3
4
# 通过透视来转置每个设备在每个APP类别分别使用了多少个 APP。变换之后的数据有 46 列,每一列表示某个列别的 APP 个数。
app_cat_num = cat_num.pivot(index='device_id', columns='app_cat_enc', values='app').fillna(0)
app_cat_num.columns = ['cat%s' % i for i in range(46)]
app_cat_num.head()


通过透视来转置每个设备在每个APP类别分别使用了多长时间。变换之后的数据有 46 列,每一列表示设备在某个列别的使用时长。

1
2
3
4
# 通过透视来转置每个设备在每个APP类别分别使用了多长时间。变换之后的数据有 46 列,每一列表示设备在某个列别的使用时长。
app_cat_time=cat_time.pivot(index='device_id', values='period', columns='app_cat_enc').fillna(0)
app_cat_time.columns = ['time%s' % i for i in range(46)]
app_cat_time.head()


5. 合并数据

合并上面的两张表,并保存到user_behavior.csv

1
2
3
4
# 合并上面的两张表,并保存到 user_behavior.csv
user = pd.merge(user, app_cat_num, on='device_id', how='left')
user = pd.merge(user, app_cat_time, on='device_id', how='left')
user.to_csv('Demo/user_behavior.csv', index=False)

减少内存占用

在我的尝试中,接下来的代码出现了内存不足的Memory Error错误。因此在这里做一些处理来减少内存占用。首先查看内存使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pandas as pd
from sys import getsizeof
def get_memory(threshold=1048576):
'''查看变量占用内存情况

:param threshold: 仅显示内存数值大于等于threshold的变量, 默认为 1MB=1048576B
'''
memory_df=pd.DataFrame(columns=['name', 'memory', 'convert_memory'])
i=0
for key in list(globals().keys()):
memory = eval("getsizeof({})".format(key))
if memory<threshold:
continue
if(memory>1073741824):# GB
unit='GB'
convert_memory=round(memory/1073741824)
elif(memory>1048576):# MB
unit='MB'
convert_memory=round(memory/1048576)
elif(memory>1024):# KB
unit='KB'
convert_memory=round(memory/1024)
else:
unit='B'
convert_memory = memory
memory_df.loc[i]=[key, memory, str(convert_memory)+unit]
i=i+1
# 按照内存占用大小降序排序
memory_df.sort_values("memory",inplace=True,ascending=False)
return memory_df

memory_df = get_memory()
memory_df

输出如下:


可以看到package_time的大小为 9 GB,因为下面还要用到这个表,对这个表的内存占用进行优化,下面是定义减少DataFrame内存占用的函数。原理是在int 中,就有intcintpint8int16int32int64等多种类型,越往后能表示的数据范围就越大,同时内存占用也随之增大。但很多时候,某个字段中的数据达不到这个范围,使用大的类型来表示是浪费的。比如一个字段类型是int64,但是这个字段中的数据都比较小,范围在-30000 to 30000之间,那么这时只需要使用int32来表示即可。float类型同理,下面的函数就是针对int类型和float类型进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def reduce_mem(df):
start_mem = df.memory_usage().sum() / 1024 ** 2
for col in df.columns:
col_type = df[col].dtypes
# 如果当前列的类型不是 object,剩下就是 int,int32, float,float32 等
if col_type != object:
# 取出这一列的最小值
c_min = df[col].min()
# 取出这一列的最大值
c_max = df[col].max()
# 如果类型是 int 开头的
if str(col_type)[:3] == 'int':
# 如果最大值和最小值都在 int8 的范围内,则转为 int8
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
# 如果最大值和最小值都在 int16 的范围内,则转为 int16
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
# 如果最大值和最小值都在 int32 的范围内,则转为 int32
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
# 如果最大值和最小值都在 int64 的范围内,则转为 int64
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
df[col] = df[col].astype(np.int64)
elif str(col_type)[:3] == 'float':# 如果类型是 float 开头的
# 如果最大值和最小值都在 float16 的范围内,则转为 float16
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
df[col] = df[col].astype(np.float16)
# 如果最大值和最小值都在 float32 的范围内,则转为 float32
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
else:
df[col] = df[col].astype(np.float64)
end_mem = df.memory_usage().sum() / (1024 ** 2) # 计算用了多少 M
print('{:.2f} Mb, {:.2f} Mb ({:.2f} %)'.format(start_mem, end_mem, 100 * (start_mem - end_mem) / start_mem))
gc.collect()
return df

然后调用该函数:

1
package_time = reduce_mem(package_time)

对于下面用不到的变量,可以进行删除:

1
2
3
4
5
6
# 删除变量,回收内存
del dapp
del dtime
del qtime
del dtime_stat
gc.collect()

手机属性特征构造

  1. 计算每个设备在前 100 个 APP的使用时间

计算每个 APP 使用的时长总和,并取出使用时间最长的前 100 个 APP 的名称

1
2
3
# 计算每个 APP 使用的时长总和,并取出使用时间最长的前 100 个 APP 的名称
app_use_time = package_time.groupby(['app'])['period'].agg('sum').reset_index()
app_use_top100 = app_use_time.sort_values(by='period', ascending=False)[:100]['app']

把 app 列设置为索引,取出使用时间最长的前 100 个

1
2
# 把 app 列设置为索引,取出使用时间最长的前 100 个
use_time_top100_statis=atime.set_index('app').loc[list(app_use_top100)].reset_index()

通过透视来转置每个设备在前 100 个 APP的使用时间。变换之后的数据有 100 列,每一列表示设备在这个 APP 使用的时间。

1
2
3
4
# 通过透视来转置每个设备在前 100 个 APP的使用时间。变换之后的数据有 100 列,每一列表示设备在这个 APP 使用的时间。
top100_statis = use_time_top100_statis.pivot(
index='device_id', columns='app', values='period').reset_index().fillna(0)
top100_statis.shape


2. 将品牌和型号进行拼接,合并到原数据表,进行独热编码

将品牌名去掉空格:如果品牌名有空格,则取前面的作为新品牌名。然后把品牌和型号拼接起来,作为新的字段

1
2
3
4
# 如果品牌名有空格,则取前面的作为新品牌名
deviced_brand.brand=deviced_brand.brand.astype(str).apply(lambda x:x.split(' ')[0].upper())
# 把品牌和型号拼接起来,作为新的字段
deviced_brand['ph_ver'] = deviced_brand['brand'] + '_' + deviced_brand['model']

统计品牌_型号的数量,并和原表拼接起来

1
2
3
4
5
6
# 统计品牌_型号的数量
ph_ver = brand['ph_ver'].value_counts()
ph_ver_cnt = pd.DataFrame(ph_ver).reset_index()
ph_ver_cnt.columns = ['ph_ver', 'ph_ver_cnt']
# 和原表拼接起来
deviced_brand = pd.merge(left=deviced_brand, right=ph_ver_cnt, on='ph_ver')

现在deviced_brand表如下:


然后针对长尾分布做处理,去掉数量小于 100 次的品牌_型号,因为这些数据有可能是噪声。

1
2
3
# 针对长尾分布做的一点处理
mask = (deviced_brand.ph_ver_cnt < 100)
deviced_brand.loc[mask, 'ph_ver'] = 'other'

将手机属性数据和训练集、测试集合并

1
2
3
4
5
# 将手机属性数据和训练集、测试集合并
deviceid_train = pd.merge(deviced_brand[['device_id', 'ph_ver']],
deviceid_train, on='device_id', how='right')
deviceid_test = pd.merge(deviced_brand[['device_id', 'ph_ver']],
deviceid_test, on='device_id', how='right')

ph_ver进行编码。构造标签,并且对标签也进行编码

1
2
3
4
5
6
7
8
9
10
11
# 对 ph_ver 进行 label encoder
deviceid_train['ph_ver'] = deviceid_train['ph_ver'].astype(str)
deviceid_test['ph_ver'] = deviceid_test['ph_ver'].astype(str)
ph_ver_le = preprocessing.LabelEncoder()
deviceid_train['ph_ver'] = ph_ver_le.fit_transform(deviceid_train['ph_ver'])
deviceid_test['ph_ver'] = ph_ver_le.transform(deviceid_test['ph_ver'])

# 构造标签,并对标签进行 label encoder
deviceid_train['label'] = deviceid_train['sex'].astype(str) + '-' + deviceid_train['age'].astype(str)
label_le = preprocessing.LabelEncoder()
deviceid_train['label'] = label_le.fit_transform(deviceid_train['label'])

把测试集的标签进行处理,合并训练集和测试集

1
2
3
4
5
# 把测试集的标签进行处理,合并训练集和测试集
deviceid_test['sex'] = -1
deviceid_test['age'] = -1
deviceid_test['label'] = -1
data = pd.concat([deviceid_train, deviceid_test], ignore_index=True)

将 ph_ver 进行独热编码

1
2
3
4
5
6
7
# 将 ph_ver 进行独热编码
ph_ver_dummy = pd.get_dummies(data['ph_ver'])
ph_ver_dummy.columns = ['ph_ver_%s' %i
for i in range(ph_ver_dummy.shape[1])]
data = pd.concat([data, ph_ver_dummy], axis=1)
del data['ph_ver']
data.head()


3. 统计每个 APP 的使用次数,和原来的 APP 使用表合并

统计每个app的总使用次数

1
2
3
4
# 统计每个app的总使用次数
app_num = package_time['app'].value_counts().reset_index()
app_num.columns = ['app', 'app_num']
app_num.head()


将 APP 的使用次数和原表合并,并处理掉长尾分布

1
2
3
4
# 将 APP 的使用次数和原表合并
package_time = pd.merge(left=package_time, right=app_num, on='app')
# 同样的,针对长尾分布做些处理(尝试过不做处理,或换其他阈值,这个100的阈值的准确率最高)
package_time.loc[package_time.app_num < 100, 'app'] = 'other'
  1. 统计每台设备的 APP数量,和原表合并

统计每台设备的 APP 数量

1
2
3
4
5
6
7
8
9
10
# 统计每台设备的app数量
df_app = package_time[['device_id', 'app']]
apps = df_app.drop_duplicates().groupby(['device_id'])[
'app'].apply(' '.join).reset_index()
apps['app_length'] = apps['app'].apply(lambda x: len(x.split(' ')))

# 这是另一种统计每台设备的app数量的方法
# df_app = package_time[['device_id', 'app']]
# apps = df_app.drop_duplicates().groupby(['device_id'])[
# 'app'].agg('count').reset_index()

apps表如下:


分别和训练集、测试集合并

1
2
3
# 分别和训练集、测试集合并
train = pd.merge(train, apps, on='device_id', how='left')
test = pd.merge(test, apps, on='device_id', how='left')
  1. 合并数据,保存特征文件

将用户行为数据和训练集、测试集合并

1
2
3
4
# 将用户行为数据和训练集、测试集合并
del user['app']
deviceid_train = pd.merge(deviceid_train, user, on='device_id', how='left')
deviceid_test = pd.merge(deviceid_test, user, on='device_id', how='left')

将每个设备在前 100 个 APP的使用时间的表和训练集、测试集合并

1
2
3
4
5
6
7
# 将每个设备在前 100 个 APP的使用时间的表和训练集、测试集合并
top100_statis.columns = ['device_id'] + ['top100_statis_' + str(i) for i in range(0, 100)]
deviceid_train = pd.merge(deviceid_train, top100_statis, on='device_id', how='left')
deviceid_test = pd.merge(deviceid_test, top100_statis, on='device_id', how='left')
# 把处理后的训练集、测试集保存到 csv 中
deviceid_train.to_csv("./Demo/train_statistic_feat.csv", index=False)
deviceid_test.to_csv("./Demo/test_statistic_feat.csv", index=False)

使用 TF-IDF 来处理 APP 列表

每个设备安装的 APP,使用空格分割

1
2
3
4
5
6
7
# 每个设备安装的 APP,使用空格分割
def get_str(df):
res=""
for i in df.split(','):
res+=i+" "
return res
deviceid_packages["str_app"]=deviceid_packages['apps'].apply(lambda x:get_str(x))

计算每个 APP 的 tf-idf

1
2
3
4
5
6
7
8
# 计算每个 APP 的 tf-idf
tfidf = TfidfVectorizer()
train_str_app=pd.merge(deviceid_train[['device_id']],deviceid_packages[["device_id",'str_app']],on="device_id",how="left")
test_str_app=pd.merge(deviceid_test[['device_id']],deviceid_packages[["device_id",'str_app']],on="device_id",how="left")
cntTf=tfidf.fit_transform(deviceid_packages['str_app'])
# 将训练集和测试集的 app 列表转换为 tf-idf。使用 .tocsr() 转换为稀疏矩阵,节省内存。
train_app = tfidf.transform(list(train_str_app['str_app'])).tocsr()
test_app = tfidf.transform(list(test_str_app['str_app'])).tocsr()

取出训练集和测试集的设备 id

1
2
3
# 取出训练集和测试集的设备 id
all_id=pd.concat([deviceid_train[["device_id"]],deviceid_test[['device_id']]])
all_id.index=range(len(all_id))

机器学习方法建模

使用 APP 的 TF-IDF 来预测性别

  1. 只使用 APP 的 TF-IDF 来预测性别,作为预训练数据

这里使用 7 种方法来预测性别,数据使用 app 的 tf-idf。7 种方法分别是 LogisticRegression、SGDClassifier、PassiveAggressiveClassifier、RidgeClassifier、BernoulliNB、MultinomialNB、LinearSVC。把预测的结果保存到tfidf_classfiy.csv中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
from sklearn.linear_model import LogisticRegression
from sklearn.linear_model import SGDClassifier
from sklearn.linear_model import PassiveAggressiveClassifier
from sklearn.linear_model import RidgeClassifier
from sklearn.naive_bayes import BernoulliNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC

from sklearn.metrics import mean_squared_error

# 使用 app 的tfidf 来预测性别,分别使用 7 种方法
df_stack = pd.DataFrame()
n_folds = 5
# 预测性别,由于原表是从 1 开始,因此-1,变为从 0 开始
sex = deviceid_train['sex']-1
print('lr stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))
sex_va = 0
kv=StratifiedKFold(n_splits=n_folds, random_state=1017)
# LogisticRegression
for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
clf = LogisticRegression(random_state=1017, C=8)
clf.fit(train_app[tr], sex[tr])
# 由于标签为 0 和 1,因此取第一列作为标签
sex_va = clf.predict_proba(train_app[va])[:,1]
sex_te=clf.predict_proba(test_app)[:,1]
print('得分' + str(mean_squared_error(sex[va], clf.predict(train_app[va]))))
stack_train[va,0]=sex_va
stack_test[:,0]+=sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['pack_tfidf_lr_classfiy_{}'.format(sex)] = stack[:, 0]

print('SGDClassifier stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))
# SGDClassifier
for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
sgd = SGDClassifier(random_state=1017, loss='log')
sgd.fit(train_app[tr], sex[tr])
sex_va = sgd.predict_proba(train_app[va])[:,1]
sex_te = sgd.predict_proba(test_app)[:,1]
print('得分' + str(mean_squared_error(sex[va], sgd.predict(train_app[va]))))
stack_train[va,0] = sex_va
stack_test[:,0]+= sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['tfidf_sgd_classfiy_{}'.format(sex)] = stack[:, 0]


print('PassiveAggressiveClassifier stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))
# PassiveAggressiveClassifier
for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
pac = PassiveAggressiveClassifier(random_state=1017)
pac.fit(train_app[tr], sex[tr])
sex_va = pac._predict_proba_lr(train_app[va])[:,1]
sex_te = pac._predict_proba_lr(test_app)[:,1]
print(sex_va)
print('得分' + str(mean_squared_error(sex[va], pac.predict(train_app[va]))))
stack_train[va,0] += sex_va
stack_test[:,0] += sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['tfidf_pac_classfiy_{}'.format(sex)] = stack[:, 0]


# RidgeClassifier
print('RidgeClassfiy stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))
for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
ridge = RidgeClassifier(random_state=1017)
ridge.fit(train_app[tr], sex[tr])
sex_va = ridge._predict_proba_lr(train_app[va])[:,1]
sex_te = ridge._predict_proba_lr(test_app)[:,1]
print(sex_va)
print('得分' + str(mean_squared_error(sex[va], ridge.predict(train_app[va]))))
stack_train[va,0] += sex_va
stack_test[:,0] += sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['tfidf_ridge_classfiy_{}'.format(sex)] = stack[:, 0]


# BernoulliNB
print('BernoulliNB stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))

for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
bnb = BernoulliNB()
bnb.fit(train_app[tr], sex[tr])
sex_va = bnb.predict_proba(train_app[va])[:,1]
sex_te = bnb.predict_proba(test_app)[:,1]
print(sex_va)
print('得分' + str(mean_squared_error(sex[va], bnb.predict(train_app[va]))))
stack_train[va,0] += sex_va
stack_test[:,0] += sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['tfidf_bnb_classfiy_{}'.format(sex)] = stack[:, 0]


# MultinomialNB
print('MultinomialNB stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))

for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
mnb = MultinomialNB()
mnb.fit(train_app[tr], sex[tr])

sex_va = mnb.predict_proba(train_app[va])[:,1]
sex_te = mnb.predict_proba(test_app)[:,1]
print(sex_va)
print('得分' + str(mean_squared_error(sex[va], mnb.predict(train_app[va]))))
stack_train[va,0] += sex_va
stack_test[:,0] += sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['tfidf_mnb_classfiy_{}'.format(sex)] = stack[:, 0]


# LinearSVC
print('LinearSVC stacking')
stack_train = np.zeros((len(deviceid_train), 1))
stack_test = np.zeros((len(deviceid_test), 1))
for i, (tr,va) in enumerate(kv.split(train_app,sex)):
print('stack:%d/%d' % ((i + 1), n_folds))
lsvc = LinearSVC(random_state=1017)
lsvc.fit(train_app[tr], sex[tr])
sex_va = lsvc._predict_proba_lr(train_app[va])[:,1]
sex_te = lsvc._predict_proba_lr(test_app)[:,1]
print(sex_va)
print('得分' + str(mean_squared_error(sex[va], lsvc.predict(train_app[va]))))
stack_train[va,0] += sex_va
stack_test[:,0] += sex_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])
df_stack['tfidf_lsvc_classfiy_{}'.format(sex)] = stack[:, 0]
# 添加设备 id 列
df_stack['device_id']=all_id
# 保存到 csv 文件中
df_stack.to_csv('./Demo/tfidf_classfiy.csv', index=None, encoding='utf8')
  1. 把预测结果作为特征,用于训练 xgb 模型

然后把上面 7 种方法预测的结果作为特征,和训练集、测试集合并。

1
2
3
# 将性别预测结果和训练集、测试集合并
train_data = pd.merge(deviceid_train,tfidf_feat,on="device_id",how="left")
test_data = pd.merge(deviceid_test,tfidf_feat,on="device_id",how="left")

再次构造性别预测训练集

1
2
3
4
# 再次构造性别预测训练集
features = [x for x in train_data.columns if x not in ['device_id', 'sex',"age","label","app"]]
X=train_data[features]
Y = train_data['sex'] - 1

使用 xgb 预测最终的性别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import lightgbm as lgb
import xgboost as xgb
from sklearn.metrics import auc, log_loss, roc_auc_score,f1_score,recall_score,precision_score
from sklearn.model_selection import StratifiedKFold
kf = StratifiedKFold( n_splits=5, shuffle=True, random_state=1024)

params={
'booster':'gbtree',

'objective': 'binary:logistic',
# 'is_unbalance':'True',
# 'scale_pos_weight': 1500.0/13458.0,
'eval_metric': "logloss",

'gamma':0.2,#0.2 is ok
'max_depth':6,
# 'lambda':20,
# "alpha":5,
'subsample':0.7,
'colsample_bytree':0.4 ,
# 'min_child_weight':2.5,
'eta': 0.01,
# 'learning_rate':0.01,
"silent":1,
'seed':1024,
'nthread':12,

}
num_round = 3500
early_stopping_rounds = 100

auc = []

test_sex = np.zeros((len(deviceid_test), ))
pred_sex=np.zeros((len(deviceid_train),))
for i, (train_index,valid_index) in enumerate(kv.split(X,Y)):
tr_x = X.loc[train_index,:]
tr_y = Y[train_index]
valid_x = X.loc[valid_index,:]
valid_y = Y[valid_index]
d_tr = xgb.DMatrix(tr_x, label=tr_y)
d_valid = xgb.DMatrix(valid_x, label=valid_y)
watchlist = [(d_tr,'train'),(d_valid,'val')]
model = xgb.train(params, d_tr, num_boost_round=5500,
evals=watchlist,verbose_eval=200,
early_stopping_rounds=100)
valid_pred = model.predict(d_valid)
pred_sex[valid_index] =valid_pred
a = log_loss(valid_y, valid_pred)
test_sex += model.predict(xgb.DMatrix(test_data[features]))/5
print ("idx: ", i)
print (" loss: %.5f" % a)
# print " gini: %.5f" % g
auc.append(a)
print ("mean")
print ("auc: %s" % (sum(auc) / 5.0))

拆分训练集、测试集的性别预测结果

1
2
3
4
5
6
# 拆分训练集、测试集的性别预测结果
train_sex = pd.DataFrame(pred_sex, columns=['sex2'])
test_sex = pd.DataFrame(test_sex, columns=['sex2'])
sex=pd.concat([train_sex,test_sex])
sex['sex1'] = 1-sex['sex2']
gc.collect()

使用 APP 的 TF-IDF 来预测年龄

  1. 只使用 APP 的 TF-IDF 来预测年龄,作为预训练数据

接着使用 APP 的 tf-idf 来预测年龄,这里也使用 7 种方法来预测年龄,数据使用 app 的 tf-idf。7 种方法分别是 LogisticRegression、SGDClassifier、PassiveAggressiveClassifier、RidgeClassifier、BernoulliNB、MultinomialNB、LinearSVC。把预测的结果保存到pack_tfidf_age.csv中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
df_stack = pd.DataFrame()
df_stack['device_id']=all_id['device_id']
score = deviceid_train['age']

########################### lr(LogisticRegression) ################################
print('lr stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
clf = LogisticRegression(random_state=1017, C=8)
clf.fit(train_app[tr], score[tr])
score_va = clf.predict_proba(train_app[va])

score_te = clf.predict_proba(test_app)
print('得分' + str(mean_squared_error(score[va], clf.predict(train_app[va]))))
stack_train[va] = score_va
stack_test+= score_te
stack_test /= n_folds

stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_lr_classfiy_{}'.format(i)] = stack[:, i]


########################### SGD(随机梯度下降) ################################
print('sgd stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
sgd = SGDClassifier(random_state=1017, loss='log')
sgd.fit(train_app[tr], score[tr])
score_va = sgd.predict_proba(train_app[va])
score_te = sgd.predict_proba(test_app)
print('得分' + str(mean_squared_error(score[va], sgd.predict(train_app[va]))))
stack_train[va] = score_va
stack_test+= score_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_sgd_classfiy_{}'.format(i)] = stack[:, i]


########################### pac(PassiveAggressiveClassifier) ################################
print('PAC stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
pac = PassiveAggressiveClassifier(random_state=1017)
pac.fit(train_app[tr], score[tr])
score_va = pac._predict_proba_lr(train_app[va])
score_te = pac._predict_proba_lr(test_app)
print(score_va)
print('得分' + str(mean_squared_error(score[va], pac.predict(train_app[va]))))
stack_train[va] += score_va
stack_test += score_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_pac_classfiy_{}'.format(i)] = stack[:, i]



########################### ridge(RidgeClassfiy) ################################
print('RidgeClassfiy stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
ridge = RidgeClassifier(random_state=1017)
ridge.fit(train_app[tr], score[tr])
score_va = ridge._predict_proba_lr(train_app[va])
score_te = ridge._predict_proba_lr(test_app)
print(score_va)
print('得分' + str(mean_squared_error(score[va], ridge.predict(train_app[va]))))
stack_train[va] += score_va
stack_test += score_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_ridge_classfiy_{}'.format(i)] = stack[:, i]



########################### bnb(BernoulliNB) ################################
print('BernoulliNB stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
bnb = BernoulliNB()
bnb.fit(train_app[tr], score[tr])
score_va = bnb.predict_proba(train_app[va])
score_te = bnb.predict_proba(test_app)
print(score_va)
print('得分' + str(mean_squared_error(score[va], bnb.predict(train_app[va]))))
stack_train[va] += score_va
stack_test += score_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_bnb_classfiy_{}'.format(i)] = stack[:, i]

########################### mnb(MultinomialNB) ################################
print('MultinomialNB stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
mnb = MultinomialNB()
mnb.fit(train_app[tr], score[tr])
score_va = mnb.predict_proba(train_app[va])
score_te = mnb.predict_proba(test_app)
print(score_va)
print('得分' + str(mean_squared_error(score[va], mnb.predict(train_app[va]))))
stack_train[va] += score_va
stack_test += score_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_mnb_classfiy_{}'.format(i)] = stack[:, i]


############################ Linersvc(LinerSVC) ################################
print('LinerSVC stacking')
stack_train = np.zeros((len(deviceid_train), 11))
stack_test = np.zeros((len(deviceid_test), 11))
score_va = 0

for i, (tr,va) in enumerate(kv.split(train_app,score)):
print('stack:%d/%d' % ((i + 1), n_folds))
lsvc = LinearSVC(random_state=1017)
lsvc.fit(train_app[tr], score[tr])
score_va = lsvc._predict_proba_lr(train_app[va])
score_te = lsvc._predict_proba_lr(test_app)
print(score_va)
print('得分' + str(mean_squared_error(score[va], lsvc.predict(train_app[va]))))
stack_train[va] += score_va
stack_test += score_te
stack_test /= n_folds
stack = np.vstack([stack_train, stack_test])

for i in range(stack.shape[1]):
df_stack['pack_tfidf_lsvc_classfiy_{}'.format(i)] = stack[:, i]

df_stack.to_csv('./Demo/pack_tfidf_age.csv', index=None, encoding='utf8')
  1. 把预测结果作为特征,用于训练 xgb 模型

拆分训练集、测试集年龄与预测的结果,并且重新构造训练集、测试集

1
2
3
4
5
6
7
8
9
# 拆分训练集、测试集年龄与预测的结果
age=pd.read_csv("./Demo/pack_tfidf_age.csv")
train_data = pd.merge(deviceid_train,age,on="device_id",how="left")
test_data = pd.merge(deviceid_test,age,on="device_id",how="left")
features = [x for x in train_data.columns if x not in ['device_id',"age","sex","label","app"]]
X=train_data[features]
Y = train_data['age']
del package_time
gc.collect()

使用 xgb 预测最终的年龄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 使用 xgb 预测最终的年龄
params={
'booster':'gbtree',
'objective': 'multi:softprob',
# 'is_unbalance':'True',
# 'scale_pos_weight': 1500.0/13458.0,
'eval_metric': "mlogloss",
'num_class':11,
'gamma':0.1,#0.2 is ok
'max_depth':6,
# 'lambda':20,
# "alpha":5,
'subsample':0.7,
'colsample_bytree':0.4 ,
# 'min_child_weight':2.5,
'eta': 0.01,
# 'learning_rate':0.01,
"silent":1,
'seed':1024,
'nthread':12,
}

auc = []

test_age = np.zeros((len(deviceid_test),11 ))
pred_age=np.zeros((len(deviceid_train),11))
for i, (train_index,valid_index) in enumerate(kv.split(X,Y)):
tr_x = X.loc[train_index,:]
tr_y = Y[train_index]
valid_x = X.loc[valid_index,:]
valid_y = Y[valid_index]
d_tr = xgb.DMatrix(tr_x, label=tr_y)
d_valid = xgb.DMatrix(valid_x, label=valid_y)
watchlist = [(d_tr,'train'),(d_valid,'val')]
model = xgb.train(params, d_tr, num_boost_round=5500,
evals=watchlist,verbose_eval=200,
early_stopping_rounds=100)
pred = model.predict(d_valid)
pred_age[valid_index] =pred
a = log_loss(valid_y, pred)
test_age += model.predict(xgb.DMatrix(test_data[features]))/5
print ("idx: ", i)
print (" loss: %.5f" % a)
# print " gini: %.5f" % g
auc.append(a)
print ("mean")
print ("auc: %s" % (sum(auc) / 5.0))

合并性别和年龄的预测结果

根据性别和年龄的预测结果,计算各个[性别-年龄]的概率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 合并训练集、测试集的预测年龄
age=np.vstack((pred_age,test_age))
age = pd.DataFrame(age)

age.index=range(len(age))
sex.index=range(len(sex))

# 计算每个[性别-年龄]的概率
# age1 表示性别为 0 的各年龄层分布概率
# age2 表示性别为 1 的各年龄层分布概率
sex_age1=age.copy()
sex_age2=age.copy()
for i in range(11):
sex_age1[i]=sex['sex1']*age[i]
sex_age2[i]=sex['sex2']*age[i]

添加设备 ID 列,并取出测试集的结果,保存到 csv 中。

1
2
3
4
5
6
7
8
9
10
# 加上设备 id 列 
final_pred = pd.concat([sex_age1,sex_age2],1)
all_id.columns= ['DeviceID']
final=pd.concat([all_id,final_pred],1)
final.columns = ['DeviceID', '1-0', '1-1', '1-2', '1-3', '1-4', '1-5', '1-6',
'1-7','1-8', '1-9', '1-10', '2-0', '2-1', '2-2', '2-3', '2-4',
'2-5', '2-6', '2-7', '2-8', '2-9', '2-10']

# 取出测试集的预测结果,保存到 csv 中
final[50000:].to_csv('./Demo/xgb_feat_chizhu.csv', index=False)

最后删除一些变量,回收内存

1
2
3
4
5
# 删除一些变量,回收内存
del deviceid_train
del deviceid_test
del user
gc.collect()

提交后获得了 2.59489 的成绩,比 baseline 降低了 0.5%。

神经网络建模

这部分的用户特征数据使用上面得到的数据,在 APP 的数据方面,不再使用 TF-IDF 特征,而是使用 Word2Vec 得到每个 APP 对应的词向量。每个设备有两组特征:用户行为特征和 APP 特征,把这两组特征输入到网络中训练,分别预测性别和年龄。

构造 APP 的词向量

  1. 数据读取

    首先读取之前处理好的用户行为数据,以及原始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 读取之前处理好的特征
behave_train = pd.read_csv('./Demo/train_statistic_feat.csv')
behave_test = pd.read_csv('./Demo/test_statistic_feat.csv')

# # 设置列不限制数量
# pd.set_option('display.max_columns',None)
# # 设置行不限制数量
# pd.set_option('display.max_rows',None)
# # 由于 behave_train.columns 显示不全,因此转换为 np.array 再显示
# # print(np.array(behave_train.columns))
# # behave_train['app']
# # 由于这一列是字符串,故删除
# del behave_train['app']

behave_train.drop(['sex', 'age', 'label', 'app'], 1, inplace=True)
behave_test.drop(['sex', 'age', 'label', 'app'], 1, inplace=True)

# 读取 设备信息,包括品牌和型号
brand=pd.read_csv('./Demo/deviceid_brand.tsv',sep='\t', names=['device_id','brand','model'])
# 读取训练数据集
train=pd.read_csv('./Demo/deviceid_train.tsv',sep='\t',names=['device_id','sex','age'])
# 读取测试数据集
test=pd.read_csv('./Demo/deviceid_test.tsv',sep='\t',names=['device_id'])
# 读取 APP 使用情况
# 读取设备安装的 app 数据
packages=pd.read_csv('./Demo/deviceid_packages.tsv',sep='\t', names=['device_id','apps'])
  1. 拼接品牌和型号,和测试集、训练集合并
1
2
3
4
5
6
# 拼接品牌和型号,和测试集、训练集合并
brand['phone_version'] = brand['brand'] + ' ' + brand['model']
train = pd.merge(brand[['device_id', 'phone_version']],
train, on='device_id', how='right')
test = pd.merge(brand[['device_id', 'phone_version']],
test, on='device_id', how='right')

把用户行为数据合并到训练集、测试集

1
2
3
# 把用户行为数据合并到训练集、测试集
train = pd.merge(train, behave_train, on='device_id', how='left')
test = pd.merge(test, behave_test, on='device_id', how='left')
  1. 把 APP 列表转换为 list 分词
1
2
3
4
5
6
7
8
# 处理app,为下面分词做准备
gc.collect()
packages['app_lenghth'] = packages['apps'].apply(
lambda x: x.split(',')).apply(lambda x: len(x))
packages['app_list'] = packages['apps'].apply(lambda x: x.split(','))
# 把app 数据合并到训练集、测试集,下面转换为词索引
train = pd.merge(train, packages, on='device_id', how='left')
test = pd.merge(test, packages, on='device_id', how='left')

packages表如下所示:


4. 得到词向量

创建 Word2Vec,训练 APP 的词向量

1
2
3
4
# 创建 Word2Vec,训练 APP 的词向量
embed_size = 128
fastmodel = Word2Vec(list(packages['app_list']), size=embed_size, window=4, min_count=3, negative=2,
sg=1, sample=0.002, hs=1, workers=4)

将词向量转换为 DataFrame,每行对应一个 APP 的词向量

1
2
3
4
5
6
# 将词向量转换为 DataFrame
# 每行对应一个 APP 的词向量
embedding_fast=pd.DataFrame([fastmodel[word] for word in fastmodel.wv.vocab])
embedding_fast['app'] = list(fastmodel.wv.vocab)
embedding_fast.columns = ["fdim_%s" % str(i) for i in range(embed_size)]+['app']
embedding_fast.head()

embedding_fast表如下所示:


5. 对设备的 APP 进行处理。将每个设备安装的 APP 转换为向量对应的索引,方便后面在 Embedding 层中根据索引取出向量

导入keras的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sklearn.feature_extraction.text import TfidfVectorizer
from scipy.sparse import hstack
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_val_score
from gensim.models import FastText, Word2Vec
import re
from keras.layers import *
from keras.models import *
from keras.preprocessing.text import Tokenizer, text_to_word_sequence
from keras.preprocessing.sequence import pad_sequences
from keras.preprocessing import text, sequence
from keras.callbacks import *
from keras.layers.advanced_activations import LeakyReLU, PReLU
import keras.backend as K
from keras.optimizers import *
from keras.utils import to_categorical
from keras.utils import multi_gpu_model
from keras import optimizers

抽取出 APP 列表,填充使得长度均为 50,然后用 texts_to_sequences 将 APP 转换为数字

1
2
3
4
5
6
7
8
9
10
11
# 抽取出 APP 列表,填充使得长度均为 50
tokenizer = Tokenizer(lower=False, char_level=False, split=',')

tokenizer.fit_on_texts(list(packages['apps']))
# 使用 texts_to_sequences 将 APP 转换为数字
X_seq = tokenizer.texts_to_sequences(train['apps'])
X_test_seq = tokenizer.texts_to_sequences(test['apps'])

maxlen = 50
X_app = pad_sequences(X_seq, maxlen=maxlen, value=0)
X_app_test = pad_sequences(X_test_seq, maxlen=maxlen, value=0)

查看某个设备安装的 APP

1
2
# 查看某个设备安装的 APP
X_app[0]


查看共有多少不同的 APP

1
2
# 总共有 35000 个不同 APP
len(tokenizer.word_index)


构建性别标签

1
2
# 构建性别标签
Y_sex = train['sex']-1

根据 tokenizer 的索引,创建词向量矩阵。用于后面的 Embedding 层。总共有 35000 个不同 APP,由于 tokenizer 的索引从 1 开始,因此为 35001

1
2
3
4
5
6
7
8
# 根据 tokenizer 的索引,创建词向量矩阵。用于后面的 Embedding 层
# 总共有 35000 个不同 APP,由于 tokenizer 的索引从 1 开始,因此为 35001
max_feaures = 35001
embedding_matrix = np.zeros((max_feaures, embed_size))
for word in tokenizer.word_index:
if word not in fastmodel.wv.vocab:
continue
embedding_matrix[tokenizer.word_index[word]]= fastmodel[word]
  1. 取出用户特征数据

分别对训练集、测试集的用户特征数据做过滤,根据训练集、测试集的设备 id 过滤

1
2
3
4
5
# 分别对训练集、测试集的用户行为数据做过滤,根据训练集、测试集的设备 id 过滤
behave_train = pd.merge(train[['device_id']],
behave_train, on='device_id', how="left")
behave_test = pd.merge(test[['device_id']],
behave_test, on='device_id', how="left")

取出数值矩阵,去掉第一列:设备 ID

1
2
3
# 取出数值矩阵,去掉第一列:设备 ID
X_behave = behave_train.iloc[:, 1:].values
X_behave_test = behave_test.iloc[:, 1:].values

使用神经网络预测性别

定义预测性别的模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 定义预测性别的模型
def model_conv1D(embedding_matrix):
K.clear_session()
# The embedding layer containing the word vectors
# embedding 层是根据词取出对应的词向量
emb_layer = Embedding(
input_dim=embedding_matrix.shape[0],# 有多少个词
output_dim=embedding_matrix.shape[1], # 词向量的维度
weights=[embedding_matrix], # 词向量矩阵
input_length=maxlen,# 最多有 50 个词
trainable=False
)
# units=128 就是输出层的维度,return_sequences=True 表示中间的状态也保留
# input(batch_size, num_words, dim), output(batch_size, num_words, units*2)
lstm_layer = Bidirectional(GRU(128, recurrent_dropout=0.15, dropout=0.15, return_sequences=True))
# 1D convolutions that can iterate over the word vectors

# output (batch_size, num_words-kernel_size+1, filters)
conv1 = Conv1D(filters=128, kernel_size=1,
padding='same', activation='relu',)

# Define inputs
seq = Input(shape=(maxlen,))
# Run inputs through embedding
emb = emb_layer(seq)

lstm = lstm_layer(emb)
# Run through CONV + GAP layers
conv1a = conv1(lstm)
# output (batch_size, 1, channels(units)) (128,128)
gap1a = GlobalAveragePooling1D()(conv1a)
# output (batch_size, 1, channels(units)) (128,128)
gmp1a = GlobalMaxPool1D()(conv1a)

# 385 对应于 X_behave 的维度
hin = Input(shape=(385, ))
htime = Dense(64, activation='relu')(hin) # (128, 64)
merge1 = concatenate([gap1a, gmp1a, htime]) # (128, 320) 128+128+64=320

# The MLP that determines the outcome
x = Dropout(0.3)(merge1)
x = BatchNormalization()(x)
x = Dense(200, activation='relu',)(x)
x = Dropout(0.25)(x)
x = BatchNormalization()(x)
x = Dense(200, activation='relu',)(x)
x = Dropout(0.25)(x)
x = BatchNormalization()(x)
x = Dense(200, activation='relu',)(x)
x = Dropout(0.25)(x)
x = BatchNormalization()(x)
pred = Dense(1, activation='sigmoid')(x)

# model = Model(inputs=[seq1, seq2, magic_input, distance_input], outputs=pred)
model = Model(inputs=[seq, hin], outputs=pred)

model.compile(loss='binary_crossentropy',
optimizer=Adam())
# model.summary()
return model
  1. 训练模型,预测性别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 训练模型,预测性别
kfold = StratifiedKFold(n_splits=5, random_state=20, shuffle=True)

text_sex = np.zeros((test.shape[0], ))
train_sex = np.zeros((train.shape[0], 1))
score = []

for i, (train_index, valid_index) in enumerate(kfold.split(X_app, Y_sex)):
print("FOLD | ", i+1)
filepath = "model/sex_weights_best_%d.h5" % i
checkpoint = ModelCheckpoint(
filepath, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
reduce_lr = ReduceLROnPlateau(
monitor='val_loss', factor=0.8, patience=2, min_lr=0.0001, verbose=0)
earlystopping = EarlyStopping(
monitor='val_loss', min_delta=0.0001, patience=6, verbose=1, mode='auto')
callbacks = [checkpoint, reduce_lr, earlystopping]

model_sex = model_conv1D(embedding_matrix)
X_app_train = X_app[train_index]
x_app_valid = X_app[valid_index]
x_behave_train = X_behave[train_index]
x_behave_valid = X_behave[valid_index]
y_train = Y_sex[train_index]
y_valid = Y_sex[valid_index]
hist = model_sex.fit([X_app_train, x_behave_train], y_train, batch_size=128, epochs=50, validation_data=([x_app_valid, x_behave_valid], y_valid),
callbacks=callbacks, verbose=1, shuffle=True)
if os.path.exists(filepath):
model_sex.load_weights(filepath)
else:
model_sex.save_weights(filepath)
text_sex += np.squeeze(model_sex.predict([X_app_test, X_behave_test]))/kfold.n_splits
train_sex[valid_index] = model_sex.predict([x_app_valid, x_behave_valid])
score.append(np.min(hist.history['val_loss']))
print('log loss:', np.mean(score))

处理预测结果,预测结果是性别为 1 的概率,根据性别为 1 的概率计算性别为 0 的概率

1
2
3
# 处理预测结果,预测结果是性别为 1 的概率,根据性别为 1 的概率计算性别为 0 的概率
text_sex = pd.DataFrame(text_sex, columns=['sex2'])
text_sex['sex1'] = 1-text_sex['sex2']

使用神经网络预测年龄

定义预测年龄的模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 定义预测年龄的模型
def model_age_conv(embedding_matrix):

# The embedding layer containing the word vectors
K.clear_session()
emb_layer = Embedding(
input_dim=embedding_matrix.shape[0],
output_dim=embedding_matrix.shape[1],
weights=[embedding_matrix],
input_length=maxlen,
trainable=False
)
lstm_layer = Bidirectional(
GRU(128, recurrent_dropout=0.15, dropout=0.15, return_sequences=True))

# 1D convolutions that can iterate over the word vectors
conv1 = Conv1D(filters=128, kernel_size=1,
padding='same', activation='relu',)
conv2 = Conv1D(filters=64, kernel_size=2,
padding='same', activation='relu', )
conv3 = Conv1D(filters=64, kernel_size=3,
padding='same', activation='relu',)
conv5 = Conv1D(filters=32, kernel_size=5,
padding='same', activation='relu',)

# Define inputs
seq = Input(shape=(maxlen,))

# Run inputs through embedding
emb = emb_layer(seq)

lstm = lstm_layer(emb)
# Run through CONV + GAP layers
conv1a = conv1(lstm)
gap1a = GlobalAveragePooling1D()(conv1a)
gmp1a = GlobalMaxPool1D()(conv1a)

conv2a = conv2(lstm)
gap2a = GlobalAveragePooling1D()(conv2a)
gmp2a = GlobalMaxPool1D()(conv2a)

conv3a = conv3(lstm)
gap3a = GlobalAveragePooling1D()(conv3a)
gmp3a = GlobalMaxPooling1D()(conv3a)

conv5a = conv5(lstm)
gap5a = GlobalAveragePooling1D()(conv5a)
gmp5a = GlobalMaxPooling1D()(conv5a)

# 385 对应于 X_behave 的维度
hin = Input(shape=(385, ))
htime = Dense(64, activation='relu')(hin)
merge1 = concatenate([gap1a, gmp1a, htime])

# merge1 = concatenate([gap1a, gap2a, gap3a, gap5a])

# The MLP that determines the outcome
x = Dropout(0.3)(merge1)
x = BatchNormalization()(x)
x = Dense(200, activation='relu',)(x)
x = Dropout(0.22)(x)
x = BatchNormalization()(x)
x = Dense(200, activation='relu',)(x)
x = Dropout(0.22)(x)
x = BatchNormalization()(x)
x = Dense(200, activation='relu',)(x)
x = Dropout(0.22)(x)
x = BatchNormalization()(x)
pred = Dense(11, activation='softmax')(x)

model = Model(inputs=[seq, hin], outputs=pred)
model.compile(loss='categorical_crossentropy',
optimizer=Adam())
# model.summary()
return model

训练模型,预测年龄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 训练模型,预测年龄
test_age = np.zeros((X_app_test.shape[0], 11))
train_age = np.zeros((X_app.shape[0], 11))
Y_age = to_categorical(train['age'])

score = []
for i, (train_index, valid_index) in enumerate(kfold.split(X_app, train['age'])):

print("FOLD | ", i+1)

filepath2 = "model/age_weights_best_%d.h5" % i
checkpoint2 = ModelCheckpoint(
filepath2, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
reduce_lr2 = ReduceLROnPlateau(
monitor='val_loss', factor=0.8, patience=2, min_lr=0.0001, verbose=1)
earlystopping2 = EarlyStopping(
monitor='val_loss', min_delta=0.0001, patience=8, verbose=1, mode='auto')
callbacks2 = [checkpoint2, reduce_lr2, earlystopping2]

model_age = model_age_conv(embedding_matrix)

X_app_train = X_app[train_index]
x_app_valid = X_app[valid_index]
x_behave_train = X_behave[train_index]
x_behave_valid = X_behave[valid_index]
y_train = Y_age[train_index]
y_valid = Y_age[valid_index]


hist = model_age.fit([X_app_train, x_behave_train], y_train, batch_size=128, epochs=50, validation_data=([x_app_valid, x_behave_valid], y_valid),
callbacks=callbacks2, verbose=1, shuffle=True)

if os.path.exists(filepath2):
model_age.load_weights(filepath2)
else:
model_age.save_weights(filepath2)
train_age[valid_index] = model_age.predict([x_app_valid, x_behave_valid])
test_age += model_age.predict([X_app_test, X_behave_test])/kfold.n_splits
score.append(np.min(hist.history['val_loss']))
print('log loss:', np.mean(score))

合并性别和年龄的预测结果

根据性别和年龄的预测结果,计算各个[性别-年龄]组合的概率,作为与预测集的结果,保存到 csv 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 根据性别和年龄的预测结果,计算各个[性别-年龄]组合的概率,作为与预测集的结果,保存到 csv 中
test_age = pd.DataFrame(test_age)
test_age1 = test_age
test_age2 = test_age
for i in range(11):
test_age1[i] = text_sex['sex1']*test_age[i]
test_age2[i] = text_sex['sex2']*test_age[i]
id_list=test[['device_id']]
id_list.columns = ['DeviceID']
final = pd.concat([id_list,test_age1, test_age2], 1)
final.columns = ['DeviceID', '1-0', '1-1', '1-2', '1-3', '1-4', '1-5', '1-6',
'1-7', '1-8', '1-9', '1-10', '2-0', '2-1', '2-2', '2-3', '2-4',
'2-5', '2-6', '2-7', '2-8', '2-9', '2-10']
final.to_csv('./Demo/nn_pred.csv', index=False)

最后提交结果,得到了 logloss 为 2.73161 的分数。

总结

在这篇文章中,使用了两个方案。第一个方案是使用机器学习的方法,第二方案是使用神经网络的方法。

首先挖掘了每个设备的特征,主要包括每个设备使用 APP 的时间上的特征,APP 使用频次上的特征,APP 的类别特征,设备本身的型号特征。

在第一个方案中,针对 APP 本身,使用了 TF-DF 来构造特征,使用了多种机器学习方法进行预训练,然后把训练结果作为特征,加入到之前的数据,最后使用 xgb 训练。

在第二个方案中,针对 APP 本身,使用了 Word2Vec 来构造特征,然后使用神经网络训练。
代码链接:https://github.com/xiechuanyu/data_competition


如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。

我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学


评论