基于BG/NBD概率模型的用户CLV预测

news2024/11/17 6:30:13

基于BG/NBD概率模型的用户CLV预测

小P:小H,我们最近想预测下用户的生命周期价值,有没有什么好的方法啊?

小H:简单啊, C L V = 用户每月平均花费 ∗ 用户平均寿命 CLV=用户每月平均花费*用户平均寿命 CLV=用户每月平均花费用户平均寿命。用户每月平均花费根据历史数据就能算出来;用户平均寿命可以根据流失率简单计算下 用户平均寿命 = 1 / 平均每月用户流失率 用户平均寿命=1/平均每月用户流失率 用户平均寿命=1/平均每月用户流失率,或者也可以用生存分析预测下用户平均寿命。

小P:额,你懂的模型那么多,就不能直接利用算法预测每个用户的CLV吗?

小H:这…,那好吧,有个BG/NBD概率模型可以依据用户的RFM进行预测

如果你想知道用户是不是流失了呢?还有多少付费潜力呢?在未来某段时间是否会再次购买呢?BG/NBD概率模型都可以解决。但是该模型不能预测周期性消费的客户,因为它只关注T时段内的交易。

该模型的假设前提比较强,但在日常消费中一般都符合,所以可以放心使用

  • 交易假设1:用户在活跃状态下,一个用户在时间段t内完成的交易数量服从均值为λt的泊松分布
  • 交易假设2:用户的交易率λ服从形状参数为r,逆尺度参数为α的gamma分布
  • 流失假设1:每个用户在交易j完成后流失的概率服从参数为p(流失率)的几何分布
  • 流失假设2:用户的流失率p服从形状参数为a,b的beta分布
  • 联合假设:每个用户的交易率λ和流失率p互相独立
  • 混合分布理解:指数分布与Gamma分布的混合分布为Pareto分布;而泊松分布与Gamma分布的混合分布为负二项分布

数据探索

# pip install lifetimes
import pandas as pd
import numpy as np
import warnings
import matplotlib.pyplot as plt
import seaborn as sns
import lifetimes
import toad 
from lifetimes.utils import *
from lifetimes import BetaGeoFitter
from lifetimes import GammaGammaFitter
from lifetimes.plotting import *

# 初始化设置
%matplotlib inline
pd.set_option('display.max_columns', None) # 显示所有列
sns.set(style="ticks")

以下数据如果有需要的同学可关注公众号HsuHeinrich,回复【数据挖掘-CLV预测】自动获取~

# 读取数据
raw_data = pd.read_excel('Online Retail.xlsx')
raw_data.head()

image-20230206153725730

主要字段含义:InvoiceNo:订单ID、StockCode:产品ID、Quantity:数量、UnitPrice:单价、CustomerID:客户ID

# 查看数据信息
toad.detector.detect(raw_data)

image-20230206153747727

# 数据处理
df=raw_data.copy()
# 剔除缺失的CustomerID
df.dropna(subset=['CustomerID'],inplace=True)
# 剔除Quantity<=0 or UnitPrice<=0的数据
df.query('Quantity > 0 & UnitPrice >0', inplace=True)
# 生成结果数据
raw_result = df.copy()
# 再次查看数据信息
toad.detector.detect(raw_result)

image-20230206153809170

特征工程

模型的主要输入参数为RF:T,因此需要构建出该输入数据

  • R:recency=客户最后一次购买商品和第一次购买商品的时间差
  • F:frequency=客户重复购买商品的期间数(模型中会减去1表示复购,即0表示1次购买,0次复购)
  • T=数据集中的最后一天与客户第一次购买商品的时间差
# 函数方式-通过lifetimes的summary_data_from_transaction_data
df_model=raw_result.copy()
# 生成价格数据
df_model['Sales']=df_model['Quantity']*df_model['UnitPrice']
# 生成观察日期
last_date=df_model.InvoiceDate.max()
df_temp = summary_data_from_transaction_data(df_model, 'CustomerID', 'InvoiceDate',
                         monetary_value_col='Sales',
                         observation_period_end=last_date)
df_temp.sort_values('monetary_value', ascending=True).head()
frequencyrecencyTmonetary_value
CustomerID
12346.00.00.0325.00.0
15130.00.00.0169.00.0
15127.00.00.065.00.0
17852.00.00.011.00.0
15120.00.00.0133.00.0
# 构造模型数据
df_model_finall=df_temp.copy()

当然也可以自己手动计算RF:T

# 构造模型输入数据RF:T
df_model=raw_result.copy()
# 生成价格数据
df_model['Sales']=df_model['Quantity']*df_model['UnitPrice']
# 生成观察日期
last_date=df_model.InvoiceDate.max()
# 手动计算
df_model.set_index('CustomerID',inplace=True)
df_temp2=df_model.groupby('CustomerID').agg(
 # 计算RF:T
 max_date=('InvoiceDate','max'),
 min_date=('InvoiceDate','min'),
 frequency=('InvoiceDate',lambda x: pd.Series(x.dt.to_period('D')).nunique()), # 按日计算频次
 recency=('InvoiceDate',lambda x: (max(x.dt.to_period('D')) - 
                                   min(x.dt.to_period('D')))/np.timedelta64(1, 'D')), # 按日计算最近时间差
 T=('InvoiceDate',lambda x: (pd.Timestamp(last_date).to_period('D')-
                             min(pd.Series(x.dt.to_period('D'))))/np.timedelta64(1,'D')), # 按日计算观察时间差
 Invoice_count=('InvoiceNo','nunique'), # 计算订单数
 Stock_count=('StockCode','count'), # 计算产品数
 Sales_sum=('Sales','sum'), # 计算销售额总额
)
df_temp2['Sales_mean']=df_temp2['Sales_sum']/df_temp2['frequency']
df_temp2.sort_values('Sales_mean', ascending=True).head()

image-20230206153831958

对比与life包的函数计算的结果,我们发线存在一些差异,这是因为函数只计算复购的情况。具体如下(其中复购日期为不包含首次购买日期)

frequencyrecencySales_meanT
人工计算购买日期按日去重末次与首次购买日期差(D)销售总额/frequency观察日与首次购买日期差(D)
函数计算复购日期按日去重末次与首次购买日期差(D)复购总额/frequency观察日与首次购买日期差(D)

lifetimes的summary_data_from_transaction_data函数也可以通过参数设置是否包含首次购买,还可以自定义计算周期

# summary_data_from_transaction_data可以通过参数设置日期差的方式,是否包含首次购买
df_model=df.copy()
# 生成价格数据
df_model['Sales']=df_model['Quantity']*df_model['UnitPrice']
# 生成观察日期
last_date=df_model.InvoiceDate.max()
df_temp3 = summary_data_from_transaction_data(df_model, 'CustomerID', 'InvoiceDate',
                      monetary_value_col='Sales',
                      observation_period_end=last_date,
                      freq='W',
                      include_first_transaction='True'
                      )
df_temp3.sort_values('frequency', ascending=True).head()
frequencyrecencyTmonetary_value
CustomerID
12346.01.00.046.077183.60
15243.01.00.010.0316.68
16400.01.00.013.0303.93
13927.01.00.010.0348.99
13926.01.00.03.0223.85

模型拟合

# 模型拟合
bgf = BetaGeoFitter(penalizer_coef=0)
bgf.fit(df_model_finall['frequency'], df_model_finall['recency'], df_model_finall['T'])
<lifetimes.BetaGeoFitter: fitted with 4338 subjects, a: 0.00, alpha: 68.91, b: 6.75, r: 0.83>

结果展示

用户预期交易热力图

# 用户预期交易热力图
fig = plt.figure(figsize=(12,8))
plot_frequency_recency_matrix(bgf, T=1, cmap='cool')
plt.show()

output_19_0

  • 潜在客户:右下角红色区域用户,这部分用户购买频次高,且距离上购买较久。因此在未来T=1(默认)期间预期购买数最多
  • 冷客户:右上角冷色区域用户,这部分用户在最近快速购买,因此在未来T=1(默认)期间预期购买数最少
  • 不确定客户(长尾客户):暖蓝色区域(20,250)附近,这部分客户不经常来,最近也没见过,因此不确定是否还会购买

用户留存概率热力图

# 用户留存概率热力图
fig = plt.figure(figsize=(12,8))
plot_probability_alive_matrix(bgf, cmap='cool')
plt.show()

output_21_0

  • 暖红色为大概率存活的用户
  • 冷蓝色为大概率流失的用户

预测下个时期的购买量

# 预测用户下个时期(t)的预期购买量
t = 30
df_model_finall['predicted_purchases'] = bgf.conditional_expected_number_of_purchases_up_to_time(t, 
                                  df_model_finall['frequency'], df_model_finall['recency'], df_model_finall['T'])
df_model_finall.sort_values(by='predicted_purchases', ascending=False).head()
frequencyrecencyTmonetary_valuepredicted_purchases
CustomerID
14911.0131.0372.0373.01093.6616798.948135
12748.0112.0373.0373.0301.0248217.658492
17841.0111.0372.0373.0364.4521627.590548
15311.089.0373.0373.0677.7294386.097251
14606.088.0372.0373.0135.8901146.029322

例如客户14911历史购买了131次,且最近一次购买在372天。在未来30天预期有8.9天的购买。

gamma-gamma模型估算客户终生价值

# 我们仅估算至少有一次重复购买的客户
df_gg_model=df_model_finall[df_model_finall['frequency']>0]
df_gg_model.shape
(2790, 5)
# 前提假设:购买频次和购买金额无相关性
df_gg_model[['monetary_value', 'frequency']].corr()
monetary_valuefrequency
monetary_value1.0000000.015906
frequency0.0159061.000000
# 模型拟合
ggf = GammaGammaFitter(penalizer_coef = 0)
ggf.fit(df_gg_model['frequency'],
        df_gg_model['monetary_value'])
<lifetimes.GammaGammaFitter: fitted with 2790 subjects, p: 2.10, q: 3.45, v: 485.89>
# 预测每笔交易的预期收益
ggf.conditional_expected_average_profit(
        df_gg_model['frequency'],
        df_gg_model['monetary_value'])
CustomerID
12347.0    569.978836
12348.0    333.784235
12352.0    376.175359
12356.0    324.039419
12358.0    539.907126
              ...    
18272.0    474.368524
18273.0    201.838133
18282.0    260.340479
18283.0    174.532812
18287.0    492.169257
Length: 2790, dtype: float64
# 预测用户未来的CLV
bgf.fit(df_gg_model['frequency'], df_gg_model['recency'], df_gg_model['T'])
ggf.customer_lifetime_value(
    bgf, # bgf预测用户未来的预期购买量
    df_gg_model['frequency'],
    df_gg_model['recency'],
    df_gg_model['T'],
    df_gg_model['monetary_value'],
    time=12, # 用户的预期寿命,以月为单位
    freq='D', # T的单位,默认为'D'
    discount_rate=0.01 # monthly discount rate ~ 12.7% annually
)
CustomerID
12347.0    3194.349694
12348.0    1175.905597
12352.0    2472.058198
12356.0     979.177238
12358.0    1940.851493
              ...     
18272.0    3111.075131
18273.0     723.867524
18282.0    1026.433547
18283.0    1966.557687
18287.0    2070.391699
Name: clv, Length: 2790, dtype: float64

模型评估

# 预期购买次数校验
plot_period_transactions(bgf)
plt.show()

output_32_0

预测与实际值接近,模型似乎不错

# 分数据集校准

# 设置校准结束时间,观察结束时间,对数据集划分
summary_cal_holdout = calibration_and_holdout_data(raw_result, 'CustomerID', 'InvoiceDate',
                                        calibration_period_end='2011-05-01',
                                        observation_period_end='2011-12-9' )  
# 拟合查看校准结果
bgf = BetaGeoFitter(penalizer_coef=0.1)
bgf.fit(summary_cal_holdout['frequency_cal'], summary_cal_holdout['recency_cal'], 
        summary_cal_holdout['T_cal'])
plot_calibration_purchases_vs_holdout_purchases(bgf, summary_cal_holdout)
plt.show()
/Users/heinrich/opt/anaconda3/lib/python3.8/site-packages/pandas/core/series.py:726: RuntimeWarning: invalid value encountered in sqrt
  result = getattr(ufunc, method)(*inputs, **kwargs)
/Users/heinrich/opt/anaconda3/lib/python3.8/site-packages/pandas/core/series.py:726: RuntimeWarning: invalid value encountered in log
  result = getattr(ufunc, method)(*inputs, **kwargs)

output_34_1

  • penalizer_coef=0时不收敛,更改为0.1。
  • 模型预测的效果在0-4次较为接近,在5、6购买预测存在低估情况

总结

这个模型实际只依赖RFT进行训练和预测,虽然大多数消费数据的概率分布服从假设,但是在使用时应该结合业务数据进行预测效果验证,毕竟和钱相关的任务都是很重要的,不可含糊~

共勉~

参考

  • 用户增长 - BG/NBD概率模型预测用户生命周期LTV
  • 如何计算用户生命周期价值(CLV)
  • 使用lifetimes进行客户终身价值(CLV)探索
  • 官方案例演示
  • lifetimes官方文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/636738.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Masked Autoencoders As Spatiotemporal Learners

Masked Autoencoders As Spatiotemporal Learners 文章目录 Masked Autoencoders As Spatiotemporal Learners一、文章背景二、文章变量1 mask sampling 方式2 Mask ratio3 其余的ablation studies 一、文章背景 用于视频中的时间信息学习。 基本思想是重构&#xff0c;使用的…

bilibili记录

霹雳吧啦Wz的个人空间-霹雳吧啦Wz个人主页-哔哩哔哩视频 目标检测篇github地址&#xff1b;GitHub - WZMIAOMIAO/deep-learning-for-image-processing: deep learning for image processing including classification and object-detection etc.

mapbox分屏地图同步缩放拖拽旋转

成果图 之前写过一版&#xff0c;后来又经过一些优化&#xff0c;形成了现在的最终版本&#xff0c;之前是二维的&#xff0c;现在是三维的也可以了&#xff0c;地址在这儿 https://blog.csdn.net/Sakura1998gis/article/details/113175905 实现 监听动作 // 拖拽同步map.on(d…

pm3包1.8版本发布----一个用于3组倾向性评分的R包

目前&#xff0c;本人写的第二个R包pm3包的1.8版本已经正式在CRAN上线&#xff0c;用于3组倾向评分匹配&#xff0c;只能3组不能多也不能少。 可以使用以下代码安装 install.packages("pm3")什么是倾向性评分匹配&#xff1f;倾向评分匹配&#xff08;Propensity Sc…

经常被问道的这些类,佬们能够吊打面试官嘛(适合秋招和小白系列)?

前言&#xff1a; 本篇文章主要讲解Java中的几个类常被问到的面试题相关知识。该专栏比较适合刚入坑Java的小白以及准备秋招的大佬阅读。 如果文章有什么需要改进的地方欢迎大佬提出&#xff0c;对大佬有帮助希望可以支持下哦~ 小威在此先感谢各位小伙伴儿了&#x1f601; 以…

宏病毒组研究大放异彩!| 凌恩生物1-5月高分宏病毒组文章大盘点!

凌恩生物现已在宏组学、基因组、表观遗传以及蛋白代谢等多组学及联合分析领域积累了深厚经验&#xff0c;打造出成熟的科研服务平台&#xff0c;以优质售前方案和优秀售后服务助力客户在Nature、Science、PNAS、ISME和MIcrobiome等高端国际期刊上发表了大量文章。 伴随着组学技…

【DevOps】Python+Golang(一)

Python is和的区别 is检查两个对象是否是同一个对象&#xff0c;即它们的内存地址是否相同。如果是同一个对象&#xff0c;则返回True&#xff0c;否则返回False。 检查两个对象是否相等&#xff0c;即它们的值是否相同。如果值相同&#xff0c;则返回True&#xff0c;否则返回…

Maven-基础

Maven Maven是专门用于管理和构建Java项目的工具&#xff0c;主要功能有&#xff1a; 提供了一套标准化的项目结构 Maven提供了一套标准化的项目结构&#xff0c;所有的IDE使用Maven构建的项目完全一样 提供了一套标准化的构建流程&#xff08;编译&#xff0c;测试&#xff0c…

jmeter性能测试进阶使用纪要

目录 目录 随机变量&#xff1a;实现注册手机号不重复分配 正则表达式&#xff1a;token等变量提取 HTTP header manager&#xff1a;token传参Authorization使用 后置BeanShell PostProcessor设置prev.setDataEncoding(“utf-8”)&#xff1a;响应中文乱码处理 同步定时…

与AI合作穿越剧 编剧徐婷:AI脑洞大,但无法替代人类的情感表达

热门喜剧秀《周六夜现场》本季提前结束&#xff0c;美剧《亿万》最新第七季的更新搁浅&#xff0c;漫威新电影《新刀锋战士》暂停拍摄……美国影视娱乐行业的编剧们以抵制AI为由的大罢工&#xff0c;开始影响诸多作品的产出&#xff0c;据说造成了100亿美元的损失。 这场罢工已…

深入理解Linux虚拟内存管理(八)

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核&#xff08;一&#xff09; 深入理解 Linux 内核&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;一&#xff09; Linux 设备驱动程序&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;三&#xf…

Hive企业级调优

Hive企业级调优 调优原则已经在MR优化阶段已经有核心描述,优化Hive可以按照MR的优化思路来执行 优化的主要考虑方面: 环境方面&#xff1a;服务器的配置、容器的配置、环境搭建具体软件配置参数&#xff1a;代码级别的优化 调优的主要原则: ​ 20/80原则非常重要,简单的说80…

骨传导蓝牙耳机排行榜10强,介绍几款不错的户外骨传导耳机

随着骨传导技术的不断发展&#xff0c;骨传导耳机的性能也得到了很大的提升&#xff0c;特别是在音质和佩戴舒适性上&#xff0c;都有了很大的提升。很多人在听音乐的时候&#xff0c;都会佩戴骨传导耳机&#xff0c;因为骨传导耳机具有开放双耳的特点&#xff0c;长时间佩戴也…

Android Jetpack Compose 中的Tabs(TabLayout)

Android Jetpack Compose 中的Tabs&#xff08;TabLayout&#xff09; 添加依赖 我们需要依赖于2个 accompanist组件&#xff0c;你可以从下面链接中获取最新版本https://github.com/google/accompanist/tree/main/pager#pager-composable-for-jetpack-compose def accompan…

探索LowLatency的HLS低延迟直播协议

HLS全称为HTTP Live Streaming&#xff0c;其中m3u8作为描述协议&#xff0c;指向一系列切片文件。支持多码流与自适应码率&#xff0c;支持广告无缝播放&#xff0c;支持CMAF协议的低延时直播&#xff0c;也支持CDN动态选择。 我们先看下HLS整体架构&#xff0c;由三部分构成…

莫顿曲线映射 一维到二维的变换 MD(莫顿)码 反向变换 线性四叉树

函数声明&#xff1a; #include <stdio.h> #include <math.h>#define MAXSIZE 200 #define N 8typedef struct //栈的存储结构 {int data[MAXSIZE];int MD[MAXSIZE];int top; }stack1;void stackinitiate(stack1 *s); //初始化栈 void push (s…

【已解决】c++ 读入灰度图进行dft变换报错

报错原因&#xff1a; 1、imread函数读入默认参数为1&#xff0c;即彩色三通道图像&#xff0c;而我们要指定参数为0&#xff0c;读入灰度图像 2、在进行傅里叶变换前要将图像数据类型转为CV_32F&#xff0c;因为默认灰度图像类型为CV_8U 正确代码&#xff1a; #include <…

Vue中如何进行滚动加载与无限滚动?

Vue中如何进行滚动加载与无限滚动&#xff1f; 随着Web应用程序的复杂性和数据量的增加&#xff0c;滚动加载和无限滚动成为了Web开发中常见的需求。在Vue中&#xff0c;我们可以使用一些插件和技术来实现这些功能。 本文将介绍Vue中如何进行滚动加载和无限滚动&#xff0c;包…

内核实现信号捕捉的过程,以及要用到的函数sigaction

1.信号捕捉过程 1.在执行主控制流程的某条指令时因为中断、异常或系统调用进入内核。 2.内核处理完异常准备回用户模式之前先处理当前进程中可以递送的信号。 3.do_signal(); 如果信号的处理动作为自定义的信号处理函数&#xff0c;则回到用户模式执行信号处理函数&#xff08…

prometheus监控应用数据(一)

prometheus监控应用数据(一) 以下代码实现均使用go语言,至于其他语言请参照其他语言的prometheus api文档 获取go package: prometheus: go get -u github.com/prometheus/client_golang/prometheus搭建程序基本骨架 IP地址暂定为: localhost启用prometheus的监控端口是2112以…