紧接上次的分析初探,进行进一步特征工程的详细分析。
1.数据准备
1.1导入工具包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import lightgbm as lgb
import gc
%matplotlib inline
1.2导入数据
path = '/home/WLY/learn/Kaggle_example_learn/Instacart/Data/'
aisles = pd.read_csv(path + 'aisles.csv')
departments = pd.read_csv(path + 'departments.csv')
products = pd.read_csv(path + 'products.csv')
orders = pd.read_csv(path + 'orders.csv')
order_products__train = pd.read_csv(path + 'order_products__train.csv')
order_products__prior = pd.read_csv(path + 'order_products__prior.csv')
2.特征工程
特征工程在很多比赛中的作用十分重要,有句话说的是:
数据和特征决定机器学习的上限,而算法只是用来逼近这个上限
接下来以推荐问题为例,介绍基本的推荐算法的特征工程
将该赛题的特征工程粗略得分为几个模块:
- 用户特征
- 产品特征
- 用户产品特征
2.1用户特征
新建一个DataFrame存储用户特征
user_fea=pd.DataFrame()
新增一列用户id,得到用户id的取值情况
user_fea['user_id']=orders['user_id'].unique()
将用户id的取值排序
user_fea=user_fea[['user_id']].sort_values('user_id')
2.1.1用户购物总次数
将orders数据集按照用户id分组,统计order_id的次数,即得到用户购物的总次数
user_fea['user_orderid_count']=orders.groupby('user_id')['order_id'].count().values
2.1.2用户购物频繁度
根据orders数据集中的days_since_prior_order用户本单购物与上一次的时间间隔来得出以下情况:
1.若用户购买的平均相隔天数少,说明用户是爱频繁购物的,则有可能会不断地消费某些产品
2.若用户距离上次购物的最大天数都很小,则证明用户非常爱购物
3.若用户距离上次购物的时间的方差较小,说明用户购物具有较强的周期性
4.用户距离上次购买产品的众数较小,证明用户爱购物
user_fea['user_days_since_prior_order_mean']=orders.groupby('user_id')['days_since_prior_order'].mean().values
user_fea['user_days_since_prior_order_max'] = orders.groupby('user_id')['days_since_prior_order'].max().values
user_fea['user_days_since_prior_order_std'] = orders.groupby('user_id')['days_since_prior_order'].std().values
user_fea['user_days_since_prior_order_mode'] = orders.groupby('user_id')['days_since_prior_order'].apply(lambda x: x.mode()[0]).values
user_fea.head()
2.1.3用户时间喜好
根据orders数据集中的order_dow和order_hour_of_day用户购买的星期几和时间点得出以下情形:
根据用户购买的星期数和时间点的众数,可能得知用户的一些生活习惯等
user_fea['user_order_dow_mode'] = orders.groupby('user_id')['order_dow'].apply(lambda x: x.mode()[0]).values
user_fea['user_order_hour_of_day_mode'] = orders.groupby('user_id')['order_hour_of_day'].apply(lambda x: x.mode()[0]).values
补充一个二阶的用户时间喜好特征,用来表示用户喜欢在每周晚上几点开始购物?对于该特征还有些疑问。。。
orders['dow_hour'] = orders['order_dow'].values * 25 + orders['order_hour_of_day'].values
user_fea['user_dow_hour_mode'] = orders.groupby('user_id')['dow_hour'].apply(lambda x: x.mode()[0]).values
user_fea.head()
2.1.4用户商品喜好特征
注意此处以priors数据集进行商品特征的提取,为什么不结合train一起呢?
原因是这样数据量更大,主要考虑到test中并无类似的特征,而且test用户并没有出现在train中,
因而这样提取特征是有问题的,因为训练集中的特征来源于prior和train,但test只来源于prior,可能会有所偏差
思考:
将order_product__prior_和orders拼接,根据拼接的数据集得到以下的情形:
用户购买不同产品的数量,即产品的种类
用户购买最多的三种产品,若用户只购买了两类产品,则最后一种产品用-1表示
用户购买所有产品的数量
用户平均每一单购买产品的数量
order_product_prior_=order_products__prior.merge(orders,on='order_id',how='left')
将prior_数据集按照用户id分组,统计不同产品的数量,即产品的种类数
user_fea['user_product_nunique']=order_product_prior_.groupby('user_id')['product_id'].nunique().sort_index().values
定义函数来计算众数,最多的三种产品情况
def mode_N(x,i):
m = x.value_counts().index
if len(m) > i:
return m[i]
return -1
user_fea['user_product_mode'] = order_product_prior_.groupby('user_id')['product_id'].apply(lambda x: x.mode()[0]).sort_index().values
user_fea['user_product_mode_1'] = order_product_prior_.groupby('user_id')['product_id'].apply(lambda x: mode_N(x,1)).sort_index().values
user_fea['user_product_mode_2'] = order_product_prior_.groupby('user_id')['product_id'].apply(lambda x: mode_N(x,2)).sort_index().values
根据prior_数据集按照用户id分组,统计产品id的数量,即得到用户购买所有产品的总数量
user_fea['user_product_count']=order_product_prior_.groupby('user_id')['product_id'].count().sort_index().values
用户购买产品的总数量/用户购物的总次数,即为用户平均每一单购买产品的数量
user_fea['user_product_orderid_ratio'] = user_fea['user_product_count'] / user_fea['user_orderid_count']
user_fea.head()
2.1.5用户购物疯狂性特征
将数据集按照用户id和订单id分组,得到每个订单id添加到购物篮顺序的最大值
1.用户订单中商品的最多数
2.用户每次平均购买的商品量
3.用户购买商品的1/4,3/4位数
tmp=order_product_prior_.groupby(['user_id','order_id'])['add_to_cart_order'].max().reset_index()
根据用户id将新构建的表分组,得到添加到购物篮顺序的最大值,即可得到用户所有订单中购买商品的最多数
user_fea['user_add_to_cart_order_max'] = tmp.groupby('user_id')['add_to_cart_order'].max().sort_index().values
根据用户id将新构建的表分组,得到添加到购物篮顺序的平均值,即可得到用户每次平均购买的商品数量
user_fea['user_add_to_cart_order_mean'] = tmp.groupby('user_id')['add_to_cart_order'].mean().sort_index().values
根据用户id将新构建的表分组,得到添加到购物篮顺序的1/4,3/4位数,即可得到用户购买商品数量的1/4和3/4位数
user_fea['user_add_to_cart_order_quantile_25'] = tmp.groupby('user_id')['add_to_cart_order'].quantile().sort_index().values
user_fea['user_add_to_cart_order_quantile_75'] = tmp.groupby('user_id')['add_to_cart_order'].quantile(0.75).sort_index().values
释放内存
del tmp
gc.collect()
2.1.6用户购买商品的复购率
根据用户id分组得到复购情况的和与均值
user_fea['user_reordered_sum'] = order_product_prior_.groupby('user_id')['reordered'].sum().sort_index().values
user_fea['user_reordered_mean'] = order_product_prior_.groupby('user_id')['reordered'].mean().sort_index().values
2.2基于产品的特征
新建一个DataFrame存储产品特征
product_fea=pd.DataFrame()
新增加一列产品id,得到产品id的不同取值情况并排序
product_fea['product_id']=order_product_prior_['product_id'].unique()
product_fea= product_fea.sort_values('product_id')
2.2.1产品的热度
1.产品被买多少次
2.产品被多少不同用户所购买
3.产品被添加到购物篮顺序的均值&标准差
将order_product_prior_数据集按照产品id分组,统计用户个数,得到产品被购买次数
product_fea['product_count'] = order_product_prior_.groupby('product_id')['user_id'].count().sort_index().values
将数据集按照产品id分组,统计不同用户的个数,得到产品被多少不同用户所购买
product_fea['product_order_nunqiue'] = order_product_prior_.groupby('product_id')['order_id'].nunique().sort_index().values
将数据集按照产品id分组,统计添加到购物篮顺序的均值和标准差
product_fea['product_add_to_cart_order_mean'] = order_product_prior_.groupby('product_id')['add_to_cart_order'].mean().sort_index().values
product_fea['product_add_to_cart_order_std'] = order_product_prior_.groupby('product_id')['add_to_cart_order'].std().sort_index().values
product_fea.head()
2.2.2产品受欢迎的时间段
1.产品被购买最多的两天
2.产品被购买最多的两个小时段
3.产品被购买距离上次购买最多的两个时间段
4.产品距离上次被购买的均值和标准差
将数据集按照产品id分组,统计order_dow的众数,得到产品被购买最多的两天
product_fea['product_dow_mode'] = order_product_prior_.groupby('product_id')['order_dow'].apply(lambda x: x.mode()[0]).sort_index().values
product_fea['product_dow_mode_1'] = order_product_prior_.groupby('product_id')['order_dow'].apply(lambda x: mode_N(x,1)).sort_index().values
将数据集按照产品id分组,统计order_hour_of_day的众数,得到产品被购买最多的两个小时段
product_fea['product_hour_of_day_mode'] = order_product_prior_.groupby('product_id')['order_hour_of_day'].apply(lambda x: x.mode()[0]).sort_index().values
product_fea['product_hour_of_day_mode_1'] = order_product_prior_.groupby('product_id')['order_hour_of_day'].apply(lambda x: mode_N(x,1)).sort_index().values
将数据集按照产品id分组,统计days_since_prior_order的众数,得到产品被购买距离上次购买最多的两个时间段
product_fea['product_days_since_prior_order_mode'] = order_product_prior_.groupby('product_id')['days_since_prior_order'].apply(lambda x: mode_N(x,0)).sort_index().values
product_fea['product_days_since_prior_order_mode_1'] = order_product_prior_.groupby('product_id')['days_since_prior_order'].apply(lambda x: mode_N(x,1)).sort_index().values
用户在每周晚上几点购物的众数,即构建的二阶特征的众数
product_fea['product_dow_hour_mode'] = order_product_prior_.groupby('product_id')['dow_hour'].apply(lambda x: mode_N(x,0)).sort_index().values
product_fea['product_dow_hour_mode_1'] = order_product_prior_.groupby('product_id')['dow_hour'].apply(lambda x: mode_N(x,1)).sort_index().values
将数据集按照产品id分组,统计days_since_prior_order的均值和方差,产品距离上次被购买的均值和方差
product_fea['product_days_since_prior_order_mean'] = order_product_prior_.groupby('product_id')['days_since_prior_order'].mean().sort_index().values
product_fea['product_days_since_prior_order_std'] = order_product_prior_.groupby('product_id')['days_since_prior_order'].std().sort_index().values
product_fea()
2.2.3产品的被重购率
将数据集根据产品id分组得到产品复购率的均值、标准差以及和
product_fea['product_reordered_mean'] = order_product_prior_.groupby('product_id')['reordered'].mean().sort_index().values
product_fea['product_reordered_std'] = order_product_prior_.groupby('product_id')['reordered'].std().sort_index().values
product_fea['product_reordered_sum'] = order_product_prior_.groupby('product_id')['reordered'].sum().sort_index().values
2.3基于用户+产品的特征
在order_product_prior_数据集新增一类user_product,将产品id和用户id联合起来
order_product_prior_['user_product'] = order_product_prior_['user_id'].values * 10**5 + order_product_prior_['product_id'].values
新建一个DataFrame存储用户+产品特征
userXproduct_fea=pd.DataFrame()
新增一列用户_产品,得到用户_产品的取值情况并排序
userXproduct_fea['user_product'] = order_product_prior_['user_product'].unique()
userXproduct_fea = userXproduct_fea[['user_product']].sort_values('user_product')
此处简单分析几个用户和产品的交叉特征
1.同一用户对某一个产品的重构率;
2.同一用户对某一个产品的加入篮子的顺序的和以及均值;
3.同一用户购买某一个产品的次数
将数据集按照user_product分组,统计复购率的和,即同一用户对某个产品的复购率
userXproduct_fea['user_product_reordered_sum'] = order_product_prior_.groupby('user_product')['reordered'].sum().sort_index().values
将数据集按照user_product分组,统计添加到购物篮顺序的和与均值,即同一用户对某个产品添加到购物篮顺序的和与均值
userXproduct_fea['user_product_add_to_cart_order_sum'] = order_product_prior_.groupby('user_product')['add_to_cart_order'].sum().sort_index().values
userXproduct_fea['user_product_add_to_cart_order_mean'] = order_product_prior_.groupby('user_product')['add_to_cart_order'].mean().sort_index().values
将数据集按照user_product分组,统计订单数的个数,即统计同一用户对某个产品的购买次数
userXproduct_fea['user_product_order_nunique'] = order_product_prior_.groupby('user_product')['order_id'].nunique().sort_index().values
将数据集按照user_product分组,统计订单购买顺序的最大值,即同一用户最后一次购买某产品的顺序
userXproduct_fea['user_product_last_order_num'] = order_product_prior_.groupby('user_product')['order_number'].max().sort_index().values
userXproduct_fea.head()
3.构建训练集和验证集
此处构建训练集和验证集的情况可参考上一篇文章初探里面的过程
3.1训练集和验证集
orders_prior_data = orders.loc[orders.eval_set == 'prior']
orders_train_data = orders.loc[orders.eval_set == 'train']
orders_test_data = orders.loc[orders.eval_set == 'test' ]
priors = order_products__prior.merge(orders_prior_data, on =['order_id'], how='left')
trains = order_products__train.merge(orders_train_data, on =['order_id'], how='left')
user_product = order_product_prior_[['user_id','product_id']].copy()
user_product['user_X_product'] = user_product['user_id'].values* 10**5 + user_product['product_id'].values
train_user_X_product = trains['user_id'].values* 10**5 + trains['product_id'].values
user_product = user_product.drop_duplicates(subset=['user_X_product'], keep = 'last')
test_user = orders_test_data['user_id']
train_user = orders_train_data['user_id']
user_product['label'] = 0
train_data = user_product.loc[user_product.user_id.isin(train_user)]
train_data.loc[train_data.user_X_product.isin(train_user_X_product), 'label'] = 1
train_data = train_data.merge(orders_train_data,on ='user_id', how='left')
test_data = user_product.loc[user_product.user_id.isin(test_user)]
test_data = test_data.merge(orders_test_data,on ='user_id', how='left')
3.2特征拼接
train_data = train_data.merge(user_fea, on='user_id', how='left')
train_data = train_data.merge(product_fea, on='product_id', how='left')
train_data = train_data.merge(userXproduct_fea, left_on='user_X_product', right_on='user_product', how='left')
train_data = train_data.merge(products, on='product_id', how= 'left')
有时候列数过多会显示为...下面代码的目的是显示所有行
pd.set_option('display.max_columns',50)
train_data.head()
test_data = test_data.merge(user_fea, on='user_id', how='left')
test_data = test_data.merge(product_fea, on='product_id', how='left')
test_data = test_data.merge(userXproduct_fea, left_on='user_X_product', right_on='user_product', how='left')
test_data = test_data.merge(products, on='product_id', how= 'left')
test_data.head()
1.同一用户购买某一个产品的次数/用户购物总次数,得到同一用户购买某一个产品的百分数
2.用户购物总次数-同一用户最后一次购买某产品的顺序,得到用户最后一次购买该产品距离现在的时间,
也就是假如用户购买了10次,最后一次购买某产品是在第4次,则证明用户购买该产品距离现在已经有6次了,
证明很有可能会再次购买
train_data['user_product_count_percent'] = train_data['user_product_order_nunique'] * 1.0 / train_data['user_orderid_count']
train_data['user_product_last_to_now'] = train_data['user_orderid_count'] - train_data['user_product_last_order_num']
test_data['user_product_count_percent'] = test_data['user_product_order_nunique'] * 1.0 / test_data['user_orderid_count']
test_data['user_product_last_to_now'] = test_data['user_orderid_count'] - test_data['user_product_last_order_num']
3.3确定特征和标签
fea_not_need = ['user_id','user_X_product','order_id','eval_set','product_name','user_product_last_order_num','label']
feature_cols = [col for col in train_data.columns if col not in fea_not_need]
label_cols = 'label'
3.4划分训练集和验证集
def validation_sample(order_ids, frac = 0.2):
import random
sample_number = int(frac * len(order_ids))
sample_val_order = random.sample( order_ids , sample_number)
sample_train_order = list(set(order_ids) - set(sample_val_order))
return sample_train_order,sample_val_order
sample_train_order,sample_val_order = validation_sample(list(train_data['order_id'].unique()))
train = train_data.loc[train_data.order_id.isin(sample_train_order)]
val = train_data.loc[train_data.order_id.isin(sample_val_order)]
3.5模型验证
此处是近似的验证,仅作为参考,注意:此处的F1不是Mean F1 Score
import lightgbm as lgb
from sklearn.metrics import f1_score
def lgb_f1_score(y_hat, data):
y_true = data.get_label()
y_hat = np.round(y_hat >= 0.2) # scikits f1 doesn't like probabilities
return 'f1', f1_score(y_true, y_hat), True
d_train = lgb.Dataset(train[feature_cols], label=train[label_cols].values)
d_val = lgb.Dataset(val[feature_cols], label=val[label_cols].values)
params = {
'task': 'train',
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': {'binary_logloss'},
'num_leaves': 2 ** 5,
'max_depth': 10,
'learning_rate':0.5,
'feature_fraction': 0.9,
'bagging_fraction': 0.9,
'bagging_freq': 5
}
ROUNDS = 200
print('light GBM train :-)')
bst = lgb.train(params, d_train, ROUNDS, valid_sets=[d_train,d_val], feval =lgb_f1_score,verbose_eval=10)
del d_train
3.6模型训练
import lightgbm as lgb
d_train = lgb.Dataset(train_data[feature_cols], label=train_data[label_cols].values)
params = {
'task': 'train',
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': {'binary_logloss'},
'num_leaves': 255,
'max_depth': 10,
# 'feature_fraction': 0.9,
# 'bagging_fraction': 0.95,
'bagging_freq': 5
}
ROUNDS = 120
print('light GBM train :-)')
bst = lgb.train(params, d_train, ROUNDS, valid_sets=[d_train], feval =lgb_f1_score,verbose_eval=10)
3.7模型预测
pred = bst.predict(test_data[feature_cols])
test_data['pred'] = pred
test_data['product_id'] = test_data['product_id'].astype(str)
order_product = {}
for order_id, val, product_id in test_data[['order_id','pred','product_id']].values:
if order_id in order_product.keys():
if val >= 0.21:
if order_product[order_id] == '':
order_product[order_id] = str(product_id )
else:
order_product[order_id] += ' ' + str(product_id )
else:
order_product[order_id] = ''
if val >= 0.21:
order_product[order_id] = str(product_id )
sub = pd.DataFrame.from_dict(order_product, orient='index')
sub.reset_index(inplace=True)
sub.columns = ['order_id', 'products']
sub.loc[sub.products =='', 'products'] = 'None'
sub.to_csv('simple_fe_0.21.csv',index = None)
4.总结
1.本次分析从用户、商品和用户商品的角度构建了三组特征。相比初探的结果要好。
2.常见的推荐比赛的特征工程大致可以分为全局特征和局部特征,局部特征在推荐系统中最常见的就是对近期的用户、商品信息进行分析来提取用户近期的爱好等。而全局的特征则是从全局的角度提取特征。
2.下一篇将总结推荐比赛的一种常用特征工程框架。