赛题描述
本文介绍第二届易观算法大赛——根据用户手机 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 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 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 的时间特征分析
首先统计每个设备的每个 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
表如下,qtime
、wtime
以及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 类别特征构造
计算每种类别的数量,并且添加编号 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
中,就有intc
、intp
、int8
、int16
、int32
、int64
等多种类型,越往后能表示的数据范围就越大,同时内存占用也随之增大。但很多时候,某个字段中的数据达不到这个范围,使用大的类型来表示是浪费的。比如一个字段类型是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()
手机属性特征构造
计算每个设备在前 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'
统计每台设备的 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 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 来预测性别
只使用 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')
把预测结果作为特征,用于训练 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 来预测年龄
只使用 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')
把预测结果作为特征,用于训练 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 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 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')
把 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]
取出用户特征数据
分别对训练集、测试集的用户特征数据做过滤,根据训练集、测试集的设备 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 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
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。
我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学 。