kaggle竞赛-Instacart Market Basket Analysis(推荐)-特征工程

紧接上次的分析初探,进行进一步特征工程的详细分析。

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.下一篇将总结推荐比赛的一种常用特征工程框架。

kaggle竞赛-Instacart Market Basket Analysis(推荐)-特征工程kaggle竞赛-Instacart Market Basket Analysis(推荐)-特征工程 银河系少女 发布了9 篇原创文章 · 获赞 8 · 访问量 2230 私信 关注
上一篇:希尔伯特频谱算法Hilbert-Huang spectral analysis(matlab代码)


下一篇:特征工程-EDA(Exploratory Data Analysis)