商业数据分析案例:使用Python做用户群组与留存分析

  • A+
所属分类:数据分析
摘要

本文从群组分析的基本介绍和它被广泛运用在互联网企业与成长性商业中的原因谈起,接着对一个标准的消费数据集对该分析方法进行一次实例运用。

一、什么是群组分析(Cohort Analysis)?

在群组分析中,一个群组表示某一类具有特定相同特征的用户群体,这些特征可以是用户注册日期、用户第一次购买行为发生的月份、用户的生日抑或是用户的购买途径等等。群组分析能够通过对这些群体进行实时的追踪,帮助我们发现趋势、了解用户的重复行为(包括购买、访问、花费金额等等),并对用户与收入的留存情况进行监控。

通常情况下,群组的构建基于用户在平台上进行的第一次“行为”,而如何评判用户是否进行了“行为”则取决于你的商业核心指标体系。例如,对于Uber和Lyft来说,用户的行为可能是运用它们的app预定下一段旅程;对于GrubHub,行为是点几盘菜;而在AirBnB看来,关键行为则或许是预定一个房间。

对于上述的企业来说,购买或消费永远是第一位的,不论是旅游还是点菜——企业的收入永远与用户的购买行为息息相关。

而对于其他的商业形态,购买行为也许并不是其商业模型中的核心环节,有些企业更倾向于关注用户对平台的“访问”行为。其中典型的例子就是Facebook和Twitter——你每天都登陆它们的网站吗?又或是在它们的网站上进行了其他行为,比如点了个赞?

在构建群组分析模型之前,首先需要思考一下我们所追踪的行为事件与互动之间的关系,以及它们对于整个商业模式的联系,这尤为重要。

二、群组分析的商业价值

当你想要分析一下现有商业模式的生命力或“用户粘性”——用户的忠诚度时,群组分析便能大显身手了。对于许多企业来说,用户粘性是至关重要的,因为留住一个现有的用户永远比去寻找一个新的用户要廉价与容易得多。对于初创企业来说,用户粘性也是评价产品市场契合程度的一个关键指标。

另外,我们的产品将会随着时间而演变,新的功能被不断加入,旧的功能被替换移除,产品本身的设计也将不断改变。通过观察每个群组在不同时间点上的表现,我们能够更好地理解产品每一次换代升级对于用户行为的影响。

同时群组分析也是可视化企业用户留存/流失率的好方法,这有助于我们更清晰地了解用户的活跃周期。

三、实际案例

接下来我们使用 Python 中的 Pandas 等相关工具来对一个实际范例数据进行分析。

# 导入所需要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

%matplotlib inline

pd.set_option('max_columns', 500)
mpl.rcParams['lines.linewidth'] = 2

第一步,读取范例数据

df = pd.read_excel('relay-foods.xlsx', sheetname=1)
df.head(1000)
OrderId OrderDate UserId TotalCharges CommonId PupId PickupDate
0 262 2009-01-11 47 50.67 TRQKD 2 2009-01-12
1 278 2009-01-20 47 26.60 4HH2S 3 2009-01-20
2 294 2009-02-03 47 38.71 3TRDC 2 2009-02-04
3 301 2009-02-06 47 53.38 NGAZJ 2 2009-02-09
4 302 2009-02-06 47 14.28 FFYHD 2 2009-02-09

1. 首先基于 OrderDate 数据构造一个时间段数据列

因为我们想做的是月度的群组分析,所以需要我们着重观察的是月度合计的用户行为。在这种情况下,我们并不需要粒度化的OrderDate数据,只要以月为粒度即可。

df['OrderPeriod'] = df.OrderDate.apply(lambda x: x.strftime('%Y-%m'))
df.head(1000)
OrderId OrderDate UserId TotalCharges CommonId PupId PickupDate OrderPeriod
0 262 2009-01-11 47 50.67 TRQKD 2 2009-01-12 2009-01
1 278 2009-01-20 47 26.60 4HH2S 3 2009-01-20 2009-01
2 294 2009-02-03 47 38.71 3TRDC 2 2009-02-04 2009-02
3 301 2009-02-06 47 53.38 NGAZJ 2 2009-02-09 2009-02
4 302 2009-02-06 47 14.28 FFYHD 2 2009-02-09 2009-02

2.确定用户的群组(基于用户首次订购)

添加新的一列,并将列名更改为CohortGroup,以表示用户首次购买行为发生的年和月。

df.set_index('UserId', inplace=True)

df['CohortGroup'] = df.groupby(level=0)['OrderDate'].min().apply(lambda x: x.strftime('%Y-%m'))
df.reset_index(inplace=True)
df.head(1000)
UserId OrderId OrderDate TotalCharges CommonId PupId PickupDate OrderPeriod CohortGroup
0 47 262 2009-01-11 50.67 TRQKD 2 2009-01-12 2009-01 2009-01
1 47 278 2009-01-20 26.60 4HH2S 3 2009-01-20 2009-01 2009-01
2 47 294 2009-02-03 38.71 3TRDC 2 2009-02-04 2009-02 2009-01
3 47 301 2009-02-06 53.38 NGAZJ 2 2009-02-09 2009-02 2009-01
4 47 302 2009-02-06 14.28 FFYHD 2 2009-02-09 2009-02 2009-01

3. 按照群组和行为时间汇总数据

为了考察月度群组,我们需要对一个月(OrderPeriod)之中每个CohortGroup的用户数、订购数和花费金额进行加总。

grouped = df.groupby(['CohortGroup', 'OrderPeriod'])

# 按照群组和订单行为时间对订单量、用户数、以及总金额进行汇总
cohorts = grouped.agg({'UserId': pd.Series.nunique,
                       'OrderId': pd.Series.nunique,
                       'TotalCharges': np.sum})

# 给数据列重命名,使含义更清晰
cohorts.rename(columns={'UserId': 'TotalUsers',
                        'OrderId': 'TotalOrders'}, inplace=True)
cohorts.head(1000)
TotalOrders TotalUsers TotalCharges
CohortGroup OrderPeriod
2009-01 2009-01 30 22 1850.255
2009-02 25 8 1351.065
2009-03 26 10 1357.360
2009-04 28 9 1604.500
2009-05 26 10 1575.625

4. 标注群组与群组时段

接下来,我们想看看每个群组在发生首次购买之后的几个月中的表现,这要求我们对每个群发生购买之后的每个月进行标记。例如,CohortPeriod = 1表示第一个月,CohortPeriod = 2表示第二个月,以此类推。

这么一来,我们就拥有了在不同的用户活跃阶段对群组进行对比的能力。

def cohort_period(df):
    """
    创建一个名为 `CohortPeriod` 的数据列,代表购买时间与第一次购买时间之间相差的期数
    
    举个例子
    ---------
    比如你想获得到与第一次购买相差3个月的每个用户信息:
        df.sort(['UserId', 'OrderTime', inplace=True)
        df = df.groupby('UserId').apply(cohort_period)
        df[df.CohortPeriod == 3]
    """
    df['CohortPeriod'] = np.arange(len(df)) + 1
    return df

cohorts = cohorts.groupby(level=0).apply(cohort_period)
cohorts.head(1000)
TotalOrders TotalUsers TotalCharges CohortPeriod
CohortGroup OrderPeriod
2009-01 2009-01 30 22 1850.255 1
2009-02 25 8 1351.065 2
2009-03 26 10 1357.360 3
2009-04 28 9 1604.500 4
2009-05 26 10 1575.625 5

5. 检查

接下来我们检查一下流程是否正确。我们从初始的数据框中抽取一些用于测试的数据,看看他们对应的相关属性在新的群组数据框中能否像我们预期的一样正常可用,如果我们之前没有出错的话,这里应该不会有什么意外发生。

x = df[(df.CohortGroup == '2009-01') & (df.OrderPeriod == '2009-01')]
y = cohorts.ix[('2009-01', '2009-01')]

assert(x['UserId'].nunique() == y['TotalUsers'])
assert(x['TotalCharges'].sum().round(2) == y['TotalCharges'].round(2))
assert(x['OrderId'].nunique() == y['TotalOrders'])

x = df[(df.CohortGroup == '2009-01') & (df.OrderPeriod == '2009-09')]
y = cohorts.ix[('2009-01', '2009-09')]

assert(x['UserId'].nunique() == y['TotalUsers'])
assert(x['TotalCharges'].sum().round(2) == y['TotalCharges'].round(2))
assert(x['OrderId'].nunique() == y['TotalOrders'])

x = df[(df.CohortGroup == '2009-05') & (df.OrderPeriod == '2009-09')]
y = cohorts.ix[('2009-05', '2009-09')]

assert(x['UserId'].nunique() == y['TotalUsers'])
assert(x['TotalCharges'].sum().round(2) == y['TotalCharges'].round(2))
assert(x['OrderId'].nunique() == y['TotalOrders'])

6.用户群组与用户留存分析

接下来,我们想要看看随着时间的推移,每个群组中用户数量的变化。需要注意的是,我们关注的是百分比的变化,而非数值的绝对变化。

为了达成目标,我们首先需要构建一个包含了每个断代群以及其规模的pandas时间序列。

# 对数据 dataframe 重新构造索引
cohorts.reset_index(inplace=True)
cohorts.set_index(['CohortGroup', 'CohortPeriod'], inplace=True)

# 创建一个数据列保存每个群组的用户数量
cohort_group_size = cohorts['TotalUsers'].groupby(level=0).first()
cohort_group_size.head()
CohortGroup
2009-01    22
2009-02    15
2009-03    13
2009-04    39
2009-05    50
Name: TotalUsers, dtype: int64

现在我们需要做的是在上文中cohort_group_size的基础上进一步划分TotalUsers的值。由于数据框操作的实现都是基于对目标对象的索引,我们将对群组数据框使用出栈的方式来构建分析所需的矩阵。在该矩阵中,每列表示一个CohortGroup,每行则表示其对应的各个CohortPeriod。

为了呈现所谓的出栈方法,我们可以看看矩阵构建过程中最先得到的五个TotalUsers值:

cohorts['TotalUsers'].head()
CohortGroup  CohortPeriod
2009-01      1               22
             2                8
             3               10
             4                9
             5               10
Name: TotalUsers, dtype: int64

整个过程进行完成之后,我们得到的矩阵如下所示:

cohorts['TotalUsers'].unstack(0).head(1000)
CohortGroup 2009-01 2009-02 2009-03 2009-04 2009-05 2009-06 2009-07 2009-08 2009-09 2009-10 2009-11 2009-12 2010-01 2010-02 2010-03
CohortPeriod
1 22 15 13 39 50 32 50 31 37 54 130 65 95 100 24
2 8 3 4 13 13 15 23 11 15 17 32 17 50 19 NaN
3 10 5 5 10 12 9 13 9 14 12 26 18 26 NaN NaN
4 9 1 4 13 5 6 10 7 8 13 29 7 NaN NaN NaN
5 10 4 1 6 4 7 11 6 13 13 13 NaN NaN NaN NaN

然后我们可以将矩阵中的每个元素都除以对应的cohort_group_size。

这样一来,我们便能得到所谓的客户留存率矩阵(user_retention),它显示了各个群组中在对应时间段上发生了购买行为的用户占比,例如在2009年3月份发生购买行为的用户中有38.4%在之后5月份(CohortPeriod=3)又进行了购买。

user_retention = cohorts['TotalUsers'].unstack(0).divide(cohort_group_size, axis=1)
user_retention.head(10)
CohortGroup 2009-01 2009-02 2009-03 2009-04 2009-05 2009-06 2009-07 2009-08 2009-09 2009-10 2009-11 2009-12 2010-01 2010-02 2010-03
CohortPeriod
1 1.000000 1.000000 1.000000 1.000000 1.00 1.00000 1.00 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.00 1
2 0.363636 0.200000 0.307692 0.333333 0.26 0.46875 0.46 0.354839 0.405405 0.314815 0.246154 0.261538 0.526316 0.19 NaN
3 0.454545 0.333333 0.384615 0.256410 0.24 0.28125 0.26 0.290323 0.378378 0.222222 0.200000 0.276923 0.273684 NaN NaN
4 0.409091 0.066667 0.307692 0.333333 0.10 0.18750 0.20 0.225806 0.216216 0.240741 0.223077 0.107692 NaN NaN NaN
5 0.454545 0.266667 0.076923 0.153846 0.08 0.21875 0.22 0.193548 0.351351 0.240741 0.100000 NaN NaN NaN NaN
6 0.363636 0.266667 0.153846 0.179487 0.12 0.15625 0.20 0.258065 0.243243 0.129630 NaN NaN NaN NaN NaN
7 0.363636 0.266667 0.153846 0.102564 0.06 0.09375 0.22 0.129032 0.216216 NaN NaN NaN NaN NaN NaN
8 0.318182 0.333333 0.230769 0.153846 0.10 0.09375 0.14 0.129032 NaN NaN NaN NaN NaN NaN NaN
9 0.318182 0.333333 0.153846 0.051282 0.10 0.31250 0.14 NaN NaN NaN NaN NaN NaN NaN NaN
10 0.318182 0.266667 0.076923 0.102564 0.08 0.09375 NaN NaN NaN NaN NaN NaN NaN NaN NaN

最后,我们可以运用绘图的方式对群组的时间变化趋势进行展示,以尝试去发现群组之间的行为差异与相似点。常见的两种图表形式为折线图和热图,下文中我们将分别对两种图表进行实现。

可以发现,在图表中每个群组在第一个时段的用户留存率都是100%——这是因为我们的同期群本身就是由用户的首次购买行为确定的。

user_retention[['2009-06', '2009-07', '2009-08']].plot(figsize=(10,5))
plt.title('User Retention')
plt.xticks(np.arange(1, 12.1, 1))
plt.xlim(1, 12)
plt.ylabel('% of Cohort Purchasing');
# 使用 seaborn 库我们可以很容易的绘制热力图

import seaborn as sns
sns.set(style='white')

plt.figure(figsize=(12, 8))
plt.title('User Retention')
sns.heatmap(user_retention.T, mask=user_retention.T.isnull(), annot=True, fmt='.0%');

和我们的直觉感受相似,我们可以从上面的图表中看到随着时间的流逝,购买的人越来越少。

然而,我们也可以看到,2009-01的群组最具代表性,我们可以通过它与其他群组的对比发现许多有针对性的问题——是什么其他属性导致了用户的留存与粘性?这些用户中的大多数是通过何种渠道获得的?是不是源于某个特定的营销活动?用户的注册是否都是为了从促销中获利?这些问题的答案将对企业未来的市场及产品策略作出建议。

总结

用户留存率仅仅是群组分析在商业中运用的一个例子而已——用户留存固然重要,但每个群组能给我们带来的收入在商业分析中也不容小觑。同样的,我们可以运用群组分析的方法来关注我们的收入留存(即每个群组第一月份中的收入在接下来一段时间中的回流百分比),为我们的市场决策添加更多助力。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: