第二届易观算法大赛 baseline 分享

张贤 2020年04月16日 278次浏览

赛题描述

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

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

数据表

该数据包含了 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 分类问题。

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

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

下面先看一个 baseline,直接做 22 分类。

baseline

数据处理

首先导入包

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%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

列出所有文件

# 数据都放在 ./Demo 文件夹中
file_lists=os.listdir('./Demo/')
for file in file_lists:
    print(file)

读取数据集

# 读取 设备信息,包括品牌和型号
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 数据
_package_label=pd.read_csv('./Demo/package_label.tsv',sep='\t')
# 读取设备安装的 app 数据
deviceid_packages=pd.read_csv('./Demo/deviceid_packages.tsv',sep='\t', names=['device_id','apps'])

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

# 读取每个设备安装了哪些app,原始数据每个设备的 app 之间以逗号分隔,处理后转换为 list
deviceid_packages['apps']=deviceid_packages['apps'].apply(lambda x:x.split(','))
deviceid_packages['app_nums']=deviceid_packages['apps'].apply(lambda x:len(x))
deviceid_packages.head()

把训练集和测试集合并到一起处理

# 把训练集和测试集合并到一起处理
deviceid_all=pd.concat([deviceid_train,deviceid_test])

特征工程

使用 TF-IDF 处理 APP 列表。关于 CountVectorizer 和 TfidfTransformer 的说明可以查看我的这篇文章

# 把每个设备的 app 列表转换为字符串,以空格分隔
apps=deviceid_packages['apps'].apply(lambda x:' '.join(x)).tolist()
vectorizer=CountVectorizer()
transformer=TfidfTransformer()
# 原来的 app 列表 转换为计数的稀疏矩阵。
cntTf = vectorizer.fit_transform(apps)
# 得到 tf-idf 矩阵
tfidf=transformer.fit_transform(cntTf)
# 得到所有的 APP 列表,相当于词典
word=vectorizer.get_feature_names()

其中 tfidf 和 cntTf 是计数的稀疏矩阵,维度为 (72727, 35000),wrod 的长度为 35000,可以看出总共有 35000 个不同的 APP。

接下来计算 每一行 的 tf-idf 的权重的和,也就是每个设备安装的所有 APP 的对应的 tf-idf 的权重的和。一开始尝试直接使用np.sum(tfidf.toarray(),axis=1)来计算,但是在我的电脑上会报MemoryError错误,尝试了很多办法都没有解决,因此使用 for 循环逐行计算。

deviceid_packages['tfidf_sum']=0
#计算 每一行 的 tf-idf 的权重的和
for i in range(tfidf.shape[0]):
    deviceid_packages.loc[i,'tfidf_sum']=np.sum(tfidf[i].toarray())

接下来使用 LatentDirichletAllocation 对文档进行主题分类。这是一种无监督学习算法。简单理解就是先假定一个主题数目 n_components,然后就可以把每一篇文档都归类到其中一个主题。

# 这里设置主题数量为 5
lda = LatentDirichletAllocation(n_components=5,learning_offset=50.,random_state=666)
# 输入是计数的稀疏矩阵
docres=lda.fit_transform(cntTf)

输出的 docres 维度是 (72727, 5),共有 72727 个设备,每行表示每个设备安装的 APP 对应 5 个主题的概率。下面把学习到的每个设备安装的 APP 主题分布信息添加到原有数据中。

deviceid_packages=pd.concat([deviceid_packages,pd.DataFrame(docres)],axis=1)
deviceid_packages=deviceid_packages.drop('apps',axis=1)
data_all=pd.merge(deviceid_all,deviceid_packages,on='device_id',how='left')

然后把 age 字段和 sex 字段拼接起来,作为label。需要先把 sex 字段和 age 字段的数据转换为 str 类型才能拼接。

# 把 int 转换为 str 
# 把 float 转换为 str 
def transfer(x):
    if np.isnan(x):
        return 'nan'
    else:
        # x 是 float 类型,有小数点。
        # 所以这里需要先转换为 int,再转换为 str
        return str(int(x))
# 把 sex 字段由 int 类型转换为 str   
data_all['sex']=data_all['sex'].apply(transfer)
# 把 age 字段由 int 类型转换为 str   
data_all['age']=data_all['age'].apply(transfer)
# 把 age 字段和 sex 字段拼接起来,作为label   
data_all['sex_age']=data_all['sex']+'-'+data_all['age']

#### 建模

然后拆分训练集和测试集,使用 LGB 训练,并得到测试集的结果。

# 先把`nan`和'nan-nan'字符串转换为 np.NaN,方便判断拆分训练集和测试集
data_all=data_all.replace({'nan':np.NaN,'nan-nan':np.NaN})
data_all=data_all.drop(['sex','age','device_id'],axis=1)
train=data_all[data_all['sex_age'].notnull()]
test=data_all[data_all['sex_age'].isnull()]

# 得到 训练集的特征
X=train.drop(['sex_age'],axis=1)
# 得到 训练集的标签
Y=train['sex_age']
le=LabelEncoder()
# LGB 不支持 str 类型的label,需要转换为数字
Y=le.fit_transform(Y)
# 划分训练集和测试集
X_train,X_test, y_train, y_test =train_test_split(X,Y,test_size=0.3, random_state=666)
# 设置 LGB 训练集
lgb_train=lgb.Dataset(X_train,label=y_train)
# 设置 LGB 验证集
lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)
# 设置参数
params = {
    'boosting_type': 'gbdt',
    'max_depth':3,
    'metric': {'multi_logloss'},
    'num_class':22,
    'objective':'multiclass',
    'random_state':666,
}

# 开始训练模型
gbm = lgb.train(params,
lgb_train,
num_boost_round=1000,
valid_sets=lgb_eval,
early_stopping_rounds=300)

# 预测得到测试集的结果
pre_x=test.drop(['sex_age'],axis=1)
pred_y=gbm.predict(pre_x.values,num_iteration=gbm.best_iteration)

# 根据 LabelEndcoder 中的属性设置列名
result=pd.DataFrame(pred_y,columns=le.classes_)
# 添加 DeviceID 列
result['DeviceID']=deviceid_test['device_id'].values

# 验证 列名是否正确
result=result[['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 文件中
result.to_csv('baseline.csv',index=False)

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

总结

这个 baseline 只是用到的设备对应的 APP 来预测性别和年龄,就已经能获得比较好的结果。没有用到每个设备上 APP 的使用情况的数据。下一篇文章会提供一个更加全面利用数据的方案,可以获得更高的分数。


代码链接:https://github.com/xiechuanyu/data_competition