天猫用户重复购买预测之数据分析

赛题理解

赛题链接
赛题背景
商家有时会在特定日期,例如Boxing-day,黑色星期五或是双十一(11月11日)开展大型促销活动或者发放优惠券以吸引消费者,然而很多被吸引来的买家都是一次性消费者,这些促销活动可能对销售业绩的增长并没有长远帮助,因此为解决这个问题,商家需要识别出哪类消费者可以转化为重复购买者。通过对这些潜在的忠诚客户进行定位,商家可以大大降低促销成本,提高投资回报率(Return on Investment, ROI)。众所周知的是,在线投放广告时精准定位客户是件比较难的事情,尤其是针对新消费者的定位。不过,利用天猫长期积累的用户行为日志,我们或许可以解决这个问题。
我们提供了一些商家信息,以及在“双十一”期间购买了对应产品的新消费者信息。你的任务是预测给定的商家中,哪些新消费者在未来会成为忠实客户,即需要预测这些新消费者在6个月内再次购买的概率。
数据说明
数据集主要包括在”双十一“之前和之后的六个月内,匿名用户购买的行为日志数据,用户的画像数据,以及相关的指示标签,标明其是否为重复购买用户。

  • 用户行为日志
字段名称描述
user_id购物者的唯一ID编码
item_id购物者的唯一ID编码
cat_id商品所属品类的唯一编码
merchant_id商家的唯一ID编码
brand_id商品品牌的唯一编码
time_tamp购买时间(格式:mmdd)
action_type包含{0, 1, 2, 3},0表示单击,1表示添加到购物车,2表示购买,3表示添加到收藏夹
  • 用户画像
字段名称描述
user_id购物者的唯一ID编码
age_range用户年龄范围。<18岁为1;[18,24]为2; [25,29]为3; [30,34]为4;[35,39]为5;[40,49]为6; > = 50时为7和8; 0和NULL表示未知
gender用户性别。0表示女性,1表示男性,2和NULL表示未知
  • 训练数据和测试数据
字段名称描述
user_id购物者的唯一ID编码
merchant_id商家的唯一ID编码
label包含{0, 1},1表示重复买家,0表示非重复买家。测试集这一部分需要预测,因此为空。

评价指标
A U C = ∑ i ∈ p o s i t i v e C l a s s r a n k i − M ( 1 + M ) 2 M ∗ N AUC = \cfrac{\sum_{i\in{positive Class}}rank_{i} - \cfrac{M(1+M)}{2}}{M*N} AUC=MNipositiveClassranki2M(1+M)
其中,M表示正样本个数,N表示负样本个数,AUC反映了模型对正负样本排序能力的强弱,对Score的大小和精度没有要求。
解题思路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-REekAfjv-1616405857760)(./imgs/解题思路.png)]

EDA

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import time
import scipy
import gc
from collections import Counter
import warnings
from matplotlib import rcParams

config = {
    "font.family":'Times New Roman',  # 设置字体类型
}

rcParams.update(config)
warnings.filterwarnings(action="ignore")

%matplotlib inline
# 数据存储情况
!tree data/data_format1/
[01;34mdata/data_format1/[00m
├── test_format1.csv
├── train_format1.csv
├── user_info_format1.csv
└── user_log_format1.csv

0 directories, 4 files
# 问题:如何优化读入数据的内存占用情况?

# 解释内存
def reduce_mem(df):
    """对于数值类型的数据进行内存节省"""
    
    starttime = time.time()
    numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
    start_mem = df.memory_usage().sum() / 1024**2  # 统计内存使用情况
    
    for col in df.columns:
        col_type = df[col].dtypes
        if col_type in numerics:
            c_min = df[col].min()
            c_max = df[col].max()
            if pd.isnull(c_min) or pd.isnull(c_max):
                continue
            if str(col_type)[:3] == 'int':
                # 装换数据类型
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float16)
                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
    print('-- Mem. usage decreased to {:5.2f} Mb ({:.1f}% reduction),time spend:{:2.2f} min'.format(end_mem,
                                                                                                           100*(start_mem-end_mem)/start_mem,
                                                                                                           (time.time()-starttime)/60))
    return df

训练集

train_data = reduce_mem(pd.read_csv("./data/data_format1/train_format1.csv"))
train_data
-- Mem. usage decreased to  1.74 Mb (70.8% reduction),time spend:0.00 min
user_idmerchant_idlabel
03417639060
1341761210
23417643561
33417622170
423078448180
............
26085935980743250
26086029452739710
2608612945271520
26086229452725370
26086322924741400

260864 rows × 3 columns

train_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 260864 entries, 0 to 260863
Data columns (total 3 columns):
 #   Column       Non-Null Count   Dtype
---  ------       --------------   -----
 0   user_id      260864 non-null  int32
 1   merchant_id  260864 non-null  int16
 2   label        260864 non-null  int8 
dtypes: int16(1), int32(1), int8(1)
memory usage: 1.7 MB
train_data.nunique()
user_id        212062
merchant_id      1993
label               2
dtype: int64

用户信息表

user_info = reduce_mem(pd.read_csv("./data/data_format1/user_info_format1.csv"))
user_info
-- Mem. usage decreased to  3.24 Mb (66.7% reduction),time spend:0.00 min
user_idage_rangegender
03765176.01.0
12345125.00.0
23445325.00.0
31861355.00.0
4302305.00.0
............
4241653958143.01.0
4241662459500.01.0
424167208016NaNNaN
4241682725356.01.0
424169180313.01.0

424170 rows × 3 columns

user_info.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 424170 entries, 0 to 424169
Data columns (total 3 columns):
 #   Column     Non-Null Count   Dtype  
---  ------     --------------   -----  
 0   user_id    424170 non-null  int32  
 1   age_range  421953 non-null  float16
 2   gender     417734 non-null  float16
dtypes: float16(2), int32(1)
memory usage: 3.2 MB
user_info.nunique()
user_id      424170
age_range         9
gender            3
dtype: int64

用户行为数据

# 问题:如何在pandas读取大批量的数据?

# 数据量过大,采用迭代方法
reader = pd.read_csv("./data/data_format1/user_log_format1.csv", iterator=True)
# try:
#     df = reader.get_chunk(100000)
# except StopIteration:
#     print("Iteration is stopped.")
loop = True
chunkSize = 100000
chunks = []

while loop:
    try:
        chunk = reader.get_chunk(chunkSize)
        chunks.append(chunk)
    except StopIteration:
        loop = False
        print("Iteration is stopped.")
        
df = pd.concat(chunks, ignore_index=True)
user_log = reduce_mem(df)
user_log
user_iditem_idcat_idseller_idbrand_idtime_stampaction_type
032886232329483328822660.08290
1328862844400127128822660.08290
2328862575153127128822660.08290
3328862996875127128822660.08290
43288621086186127112531049.08290
........................
5492532520801610766289813467996.011100
54925326208016105831389813467996.011100
549253272080164498148989837996.011100
5492532820801663485689813467996.011100
5492532920801627209489813467996.011110

54925330 rows × 7 columns

user_log["user_id"].nunique()
424170
user_log.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 54925330 entries, 0 to 54925329
Data columns (total 7 columns):
 #   Column       Dtype  
---  ------       -----  
 0   user_id      int32  
 1   item_id      int32  
 2   cat_id       int16  
 3   seller_id    int16  
 4   brand_id     float16
 5   time_stamp   int16  
 6   action_type  int8   
dtypes: float16(1), int16(3), int32(2), int8(1)
memory usage: 890.5 MB

测试集信息表

test_data = reduce_mem(pd.read_csv("./data/data_format1/test_format1.csv"))
test_data
-- Mem. usage decreased to  3.49 Mb (41.7% reduction),time spend:0.00 min
user_idmerchant_idprob
01639684605NaN
13605761581NaN
2986881964NaN
3986883645NaN
42952963361NaN
............
2614722284793111NaN
261473979192341NaN
261474979193971NaN
261475326393536NaN
261476326393319NaN

261477 rows × 3 columns

test_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 261477 entries, 0 to 261476
Data columns (total 3 columns):
 #   Column       Non-Null Count   Dtype  
---  ------       --------------   -----  
 0   user_id      261477 non-null  int32  
 1   merchant_id  261477 non-null  int16  
 2   prob         0 non-null       float64
dtypes: float64(1), int16(1), int32(1)
memory usage: 3.5 MB

缺失值

# user_log 用户日志表
Total = user_log.isnull().sum().sort_values(ascending=False)
percent = (user_log.isnull().sum()/user_log.isnull().count()).sort_values(ascending=False)*100
missing_data = pd.concat([Total, percent], axis=1, keys=["Total", "Percent"])
missing_data
TotalPercent
brand_id910150.165707
user_id00.000000
item_id00.000000
cat_id00.000000
seller_id00.000000
time_stamp00.000000
action_type00.000000
结论:用户日志表中的特征信息缺失比例较小,且只出现在品牌一栏中,可以删除。
# user_info 用户信息表
Total = user_info.isnull().sum().sort_values(ascending=False)
percent = (user_info.isnull().sum()/user_info.isnull().count()).sort_values(ascending=False)*100
missing_data = pd.concat([Total, percent], axis=1, keys=["Total", "Percent"])
missing_data
TotalPercent
gender64361.517316
age_range22170.522668
user_id00.000000
结论:用户信息表中,用户性别和年龄缺失,考虑此特征为类别特征,可以使用众数填补, 注意缺失值不止为NULL。

年龄缺失

# 年龄字段数据取值情况
user_info["age_range"].unique()
array([ 6.,  5.,  4.,  7.,  3.,  0.,  8.,  2., nan,  1.], dtype=float16)
# 年龄为零或为空为缺失值,缺失条目为95131条
user_info[(user_info["age_range"] == 0) | (user_info["age_range"].isna())].count()
user_id      95131
age_range    92914
gender       90664
dtype: int64
# 不同年龄段的缺失用户数量,不统计空值
user_info.groupby("age_range")[["user_id"]].count()
user_id
age_range
0.092914
1.024
2.052871
3.0111654
4.079991
5.040777
6.035464
7.06992
8.01266
### 性别缺失
# 查看性别取值范围
user_info["gender"].unique()
array([ 1.,  0.,  2., nan], dtype=float16)
# 统计性别缺失情况
user_info[(user_info["gender"].isna()) | (user_info["gender"] == 2)].count()
user_id      16862
age_range    14664
gender       10426
dtype: int64

统计用户只有一个缺失值

# 注:count只统计非Nan的值,因此在user_id的数量能够表示真实的缺失数量
user_info[(user_info["gender"].isna()) | (user_info["gender"] == 2) | (user_info["age_range"] == 0) | (user_info["age_range"].isna())].count()
user_id      106330
age_range    104113
gender        99894
dtype: int64

正负样本统计

label_gp = train_data.groupby("label")["user_id"].count()
label_gp
label
0    244912
1     15952
Name: user_id, dtype: int64
fig, ax = plt.subplots(1, 2, figsize=(12, 6))
train_data["label"].value_counts().plot(kind="pie"
                                        , ax=ax[0]
                                        , shadow=True
                                        , explode=[0, 0.1]
                                        , autopct="%1.1f%%"
                                       )

sns.countplot(x="label", data=train_data, ax=ax[1])

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OGTyJS3u-1616405857765)(./imgs/output_37_1.png)]

结论:样本极度不平衡,使用负采样或过采样技术调整数据不平衡的情况。
知识点: countplot

探索影响复购的各种因素

分析不同店铺与复购的关系

train_data_merchant = train_data.copy()
top5_idx = train_data_merchant.merchant_id.value_counts().head().index.tolist()
# 增加一列用于标记Top商户和非Top商户,便于统计其复购情况
train_data_merchant["Top5"] = train_data_merchant["merchant_id"].map(lambda x: 1 if x in top5_idx else 0)
train_data_merchant = train_data_merchant[train_data_merchant.Top5 == 1]

sns.countplot(x="merchant_id", hue="label", data=train_data_merchant)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bDAkJ4r3-1616405857767)(./imgs/output_41_1.png)]

结论:不同商铺复购情况大不相同,商铺复购率较低。

查看店铺的复购分布

merchant_repeat_buy = [rate for rate in train_data.groupby("merchant_id")["label"].mean() if rate <= 1 and rate >0]
plt.figure(figsize=(8, 4))

ax1 = plt.subplot(1, 2, 1)
sns.distplot(merchant_repeat_buy, fit=scipy.stats.norm)

ax2 = plt.subplot(1, 2, 2)
res = scipy.stats.probplot(merchant_repeat_buy, plot=plt)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ke0zp0Xj-1616405857770)(./imgs/output_44_0.png)]

结论:用户店铺复购率分布在0.15左右,用户复购率较低,且不同店铺具有不同的复购率。(数据分布属于右偏)

查看用户的复购分布

user_repeat_buy = [rate for rate in train_data.groupby("user_id")["label"].mean() if rate <= 1 and rate >0]

plt.figure(figsize=(8, 4))
ax1 = plt.subplot(1, 2, 1)
sns.distplot(user_repeat_buy, fit=scipy.stats.norm)

ax2 = plt.subplot(1, 2, 2)
res = scipy.stats.probplot(user_repeat_buy, plot=plt)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NtJWLIT2-1616405857772)(./imgs/output_47_0.png)]

结论:近六个月的用户主要集中在一次购买为主,较少出现重复购买的情况。

对用户性别的分析

train_data_user_info = train_data.merge(user_info, on=["user_id"], how="left")

plt.figure(figsize=(6, 4))
plt.title("Gender VS Label")
ax = sns.countplot(x="gender", hue="label", data=train_data_user_info)
for p in ax.patches:
    hight = p.get_height()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6udV8Zs-1616405857775)(./imgs/output_50_0.png)]

repeat_buy = [rate for rate in train_data_user_info.groupby("gender")["label"].mean() if rate <= 1 and rate >0]

plt.figure(figsize=(8, 4))
ax1 = plt.subplot(1, 2, 1)
sns.distplot(repeat_buy, fit=scipy.stats.norm)

ax2 = plt.subplot(1, 2, 2)
res = scipy.stats.probplot(repeat_buy, plot=plt)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFNRnzYp-1616405857776)(./imgs/output_51_0.png)]

结论:男女复购情况呈现差异性,女性购买情况远高于男性。

对用户年龄分析

plt.figure(figsize=(5, 4))
plt.title("Age VS Label")
res = sns.countplot(x="age_range", hue="label", data=train_data_user_info)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N2Glt6rs-1616405857777)(./imgs/output_54_0.png)]

repeat_buy = [rate for rate in train_data_user_info.groupby("age_range")["label"].mean() if rate <= 1 and rate >0]

plt.figure(figsize=(8, 4))
ax1 = plt.subplot(1, 2, 1)
sns.distplot(repeat_buy, fit=scipy.stats.norm)

ax2 = plt.subplot(1, 2, 2)
res = scipy.stats.probplot(repeat_buy, plot=plt)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8lsoUa8-1616405857778)(./imgs/output_55_0.png)]

结论:用户在不同的年龄阶段复购情况有差异,年龄重点分布在25~34之间的用户。

  • 17
    点赞
  • 137
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值