Android进阶系列之第三方库知识点整理。
知识点总结,整理也是学习的过程,如有错误,欢迎批评指出。
上一篇:Rxjava2(一)、基础概念及使用
直接开整,上一篇基础概念里面说了,
1、Observable 和 Observer
能够发射0或n个数据,并以成功或错误事件终止,在第一篇中已经举例说明了,这里就不再详细说明。
2、Flowable 和 Subscriber
能够发射0或n个数据,并以成功或错误事件终止。 支持背压,可以控制数据源发射的速度。
我们看到
2.1、什么是背压
背压是一种现象,简单来说就是在异步操作中,上游发送数据速度快于下游处理数据的速度,下游来不及处理,Buffer 溢出,导致事件阻塞,从而引起的各种问题,比如事件丢失,OOM等。
在
A:我们上游模拟循环发送数据。
B:线程切换,异步操作。
C:下游每隔一秒获取数据。
我们
看日志,打印结果停留在了13就没有继续打印了?同时可以看到程序已经崩了,是因为在
可以看到内存一直在往上飙,针对背压这种现象,
下面由浅入深,慢慢揭开
我们先用
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> emitter.onNext("事件一");<br> LogUtil.d(TAG + "--subscribe 发送事件一");<br> emitter.onNext("事件二");<br> LogUtil.d(TAG + "--subscribe 发送事件二");<br> emitter.onNext("事件三");<br> LogUtil.d(TAG + "--subscribe 发送事件三");<br> emitter.onNext("事件四");<br> LogUtil.d(TAG + "--subscribe 发送事件四");<br> emitter.onComplete();<br> LogUtil.d(TAG + "--subscribe 发送完成");<br> }<br> }, BackpressureStrategy.ERROR) // 这里需要传入背压策略,跟线程池里面饱和策略类似,当缓存区存满时候采取的处理策略<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread()) // 线程切换,异步操作<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> // 决定观察者能接收多少个事件,多余事件放入缓存区<br> // Flowable 默认缓存区大小为128,即最大能存放128个事件<br> s.request(3);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t.getLocalizedMessage());<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
可以看到
Flowable 创建和Observable 基本差不多,只是在create 方法中多传入BackpressureStrategy.ERROR 这么一个背压策略,这个后面会详讲。
在
onSubscribe 的回调中,参数变成了Subscription ,我们可以通过这个参数,让观察者自己设置要接收多少个事件,如果发送的事件大于观察者设置接收的事件,多余事件将会存入Flowable 缓存区中。
Flowable 缓存区队列大小只能存放128个事件,如果超过,就会报异常。
结果:
发送四个事件,观察者通过
Subscription.request(3) 设置只接收三个事件,所以下游只接收三个,剩下一个放入Flowable 缓存区中。
如果我们观察者不设置
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br><br> for (int x = 0; x <= 10; x++) {<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> emitter.onNext(x + "事件");<br> }<br> }<br> }, BackpressureStrategy.ERROR) <br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> subscription = s;<br> // s.request(3); 这里不指定观察者接收事件个数<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t.getLocalizedMessage());<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
动态获取
1 | findViewById(R.id.bt_get_event).setOnClickListener(new View.OnClickListener() {<br> @Override<br> public void onClick(View v) {<br> if (subscription != null) {<br> LogUtil.d(TAG + "--onClick");<br> subscription.request(4);<br> }<br> }<br> });<br> |
可以看到我们观察者一开始并没有指定接收多少个事件,而是通过外接点击事件,来动态设置接收事件个数,我们看结果,当点击触发后,我们收到了最先存入队列的四个事件。
结果:
2.2、背压策略
我们前面提到,
整理如下:
策略 | 作用 |
---|---|
MISSING | 当缓存区大小存满(128),被观察者仍然继续发送下一个事件时,抛出异常 |
ERROR | 当缓存区大小存满(128)(默认缓存区大小128),被观察者仍然继续发送下一个事件时,直接抛出异常 |
BUFFER | 当缓存区大小存满(128),被观察者仍然继续发送下一个事件时,缓存区大小设置无限大, 即被观察者可无限发送事件,但实际上是存放在缓存区 |
DROP | 当缓存区大小存满,被观察者仍然继续发送下一个事件时, 超过缓存区大小(128)的事件会被全部丢弃 |
LATEST | 当缓存区大小存满,被观察者仍然继续发送下一个事件时,只保存最新/最后发送的事件, 其他超过缓存区大小(128)的事件会被全部丢弃 |
2.2.1、MISSING
当缓存区大小存满(128),被观察者仍然继续发送下一个事件时,抛出异常
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // 发送129个事件,模拟超出缓存区<br> for (int x = 0; x < 129; x++) {<br> emitter.onNext(x + "事件");<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> }<br> }<br> }, BackpressureStrategy.MISSING) // 使用BackpressureStrategy.MISSING背压策略<br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(Integer.MAX_VALUE);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
我们使用BackpressureStrategy.MISSING背压策略,观察者接收request(Integer.MAX_VALUE),此值也为推荐值。
结果:
我们看到,当发送了128个事件后,再发送第129个事件时候,抛了
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // ******* 发送128个事件 ********<br> for (int x = 0; x < 128; x++) {<br> emitter.onNext(x + "事件");<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> }<br> }<br> }, BackpressureStrategy.MISSING)<br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(Integer.MAX_VALUE);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
就是在上面demo的基础上,改了发送的事件个数,上游发送128个事件,刚好为缓存区大小,并不抛异常。
结果:
我们看到程序没有抛异常,并且正常打印了缓存区中的128个数据(从0开始),可以印证两点
1、缓存区大小确实为128
2、先存入缓存区后再获取(如果异常,
onNext 直接不调用)
2.2.2、ERROR
当缓存区大小存满(128)(默认缓存区大小128),被观察者仍然继续发送下一个事件时,直接抛出异常
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // 发送129个事件,模拟超出缓存区<br> for (int x = 0; x < 129; x++) {<br> emitter.onNext(x + "事件");<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> }<br> }<br> }, BackpressureStrategy.ERROR)<br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(Integer.MAX_VALUE);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
使用 BackpressureStrategy.ERROR 背压策略
结果:
跟Missing一样,直接抛了
2.2.3、BUFFER
当缓存区大小存满(128),被观察者仍然继续发送下一个事件时,缓存区大小设置无限大, 即被观察者可无限发送事件,但实际上是存放在缓存区。
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // 发送129个事件,模拟超出缓存区<br> for (int x = 0; x < 129; x++) {<br> emitter.onNext(x + "事件");<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> }<br> }<br> }, BackpressureStrategy.BUFFER)<br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(Integer.MAX_VALUE);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
使用 BackpressureStrategy.BUFFER 背压策略
更改缓存区大小,不做限制。
结果:
可以看到,我们发送的129个事件全部发送且接收到了。
2.2.4、DROP
当缓存区大小存满,被观察者仍然继续发送下一个事件时, 超过缓存区大小(128)的事件会被全部丢弃
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // 发送129个事件,模拟超出缓存区<br> for (int x = 0; x < 129; x++) {<br> emitter.onNext(x + "事件");<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> }<br> }<br> }, BackpressureStrategy.DROP)<br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(Integer.MAX_VALUE);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
使用 BackpressureStrategy.DROP 背压策略
丢掉大于缓存区的事件。
结果:
结果很明了,并没有抛异常同时也正常打印了,但是超过缓存区的那个事件被抛弃,并没有获取到。
2.2.5、LATEST
当缓存区大小存满,被观察者仍然继续发送下一个事件时,只保存最新/最后发送的事件, 其他超过缓存区大小(128)的事件会被全部丢弃
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // 发送150个事件<br> for (int x = 0; x < 150; x++) {<br> emitter.onNext(x + "事件");<br> LogUtil.d(TAG + "--subscribe 发送了" + x + "个事件");<br> }<br> }<br> }, BackpressureStrategy.LATEST)<br> // 线程切换,异步操作<br> .subscribeOn(Schedulers.io())<br> .observeOn(AndroidSchedulers.mainThread())<br> .subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(Integer.MAX_VALUE);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
使用 BackpressureStrategy.LATEST 背压策略
发送了150个事件
当超出128时,会保存最新的一个事件,即会接收129个事件。
结果:
我们可以看到,观察者端接收到129个数据,分别为缓存区内数据,加上最新/最后一条数据,中间数据均被丢弃。
2.3、同步情况下`Flowable`
前面说过,背压前提是异步操作下,在同步下,我们并不会有背压一说,因为在同一个线程,发送数据后总是要等下游处理了才会发送第二条数据,不会存在缓冲区,如下:
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> LogUtil.d(TAG + "--subscribe 发送事件一");<br> emitter.onNext("事件一");<br> LogUtil.d(TAG + "--subscribe 发送事件二");<br> emitter.onNext("事件二");<br> LogUtil.d(TAG + "--subscribe 发送事件三");<br> emitter.onNext("事件三");<br> LogUtil.d(TAG + "--subscribe 发送完成");<br> emitter.onComplete();<br> }<br> }, BackpressureStrategy.ERROR).subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(3);<br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
结果:
可以看到,事件都是顺序执行,发送一条接收一条,然后再执行下一条。
但是,我们可能会遇到这个一个情况,当上游发送了四条数据,但是下游只接收三条?我们改一下demo如下:
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> LogUtil.d(TAG + "--subscribe 发送事件一");<br> emitter.onNext("事件一");<br> LogUtil.d(TAG + "--subscribe 发送事件二");<br> emitter.onNext("事件二");<br> LogUtil.d(TAG + "--subscribe 发送事件三");<br> emitter.onNext("事件三");<br> LogUtil.d(TAG + "--subscribe 发送事件四");<br> emitter.onNext("事件四");<br> LogUtil.d(TAG + "--subscribe 发送完成");<br> emitter.onComplete();<br> }<br> }, BackpressureStrategy.ERROR).subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> s.request(3);<br><br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.d(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
可以看到,被观察者发送了四个事件,但是观察者只接收了三条。
结果:
可以看到,同样抛了
这里可以使用BUFFER的背压策略来处理,但是我们为了说明观察者反向控制被观察者,我们采用如下方案:
1 | Flowable.create(new FlowableOnSubscribe<String>() {<br> @Override<br> public void subscribe(FlowableEmitter<String> emitter) throws Exception {<br> // 通过emitter.requested()获取观察者设置的接收的事件数目<br> long requested = emitter.requested();<br> LogUtil.d(TAG + "--subscribe 观察者设置接收的事件数目:" + requested);<br><br> for (int x = 0; x < requested; x++) {<br> LogUtil.d(TAG + "--subscribe 发送事件" + x);<br> emitter.onNext("发送事件" + x);<br> }<br> LogUtil.d(TAG + "--subscribe 发送完成");<br> emitter.onComplete();<br> }<br> }, BackpressureStrategy.BUFFER).subscribe(new Subscriber<String>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> LogUtil.d(TAG + "--onSubscribe");<br> // 设置观察者接收事件数目为3<br> s.request(3);<br><br> }<br><br> @Override<br> public void onNext(String s) {<br> LogUtil.d(TAG + "--onNext 接收到:" + s);<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.e(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.d(TAG + "--onComplete");<br> }<br> });<br> |
我们在
subscribe 中通过emitter.requested() 获取观察者中设置的接收事件数目,来动态的发送数据,这样就避免了上下游数据不同步问题。
结果:
2.4、使用操作符时背压处理
我们前面都是通过create来创建
onBackpressureBuffer() onBackpressureDrop() onBackpressureLatest()
三种方式来指定相应的背压策略。
1 | Flowable.interval(1, TimeUnit.MILLISECONDS)<br> .observeOn(Schedulers.io())<br> .subscribe(new Subscriber<Long>() {<br> @Override<br> public void onSubscribe(Subscription s) {<br> Log.d(TAG, "onSubscribe");<br> subscription = s;<br> s.request(Long.MAX_VALUE); //默认可以接收Long.MAX_VALUE个事件<br> }<br><br> @Override<br> public void onNext(Long aLong) {<br> LogUtil.i(TAG + "--onNext aLong=" + aLong);<br> try {<br> // 延时一秒接收<br> Thread.sleep(1000);<br> } catch (InterruptedException e) {<br> e.printStackTrace();<br> }<br> }<br><br> @Override<br> public void onError(Throwable t) {<br> LogUtil.e(TAG + "--onError error=" + t);<br> }<br><br> @Override<br> public void onComplete() {<br> LogUtil.i(TAG + "--onComplete");<br> }<br> });<br> |
这里我们通过
结果:
我们可以通过
结果:
当我们指定背压策略为BUFFER后,可以看到并没有异常抛出,程序一直在打印输出。
3、Single和SingleObserver
只发射单个数据或错误事件。
1 | Single.create(new SingleOnSubscribe<String>() {<br> @Override<br> public void subscribe(SingleEmitter<String> emitter) throws Exception {<br> // 只能发送onSuccess或者onError,发射多条数据,只接受第一条<br> emitter.onSuccess("Success");<br> emitter.onError(new NullPointerException(""));<br> }<br> }).subscribe(new SingleObserver<String>() {<br> @Override<br> public void onSubscribe(Disposable d) {<br> LogUtil.d(TAG + "--onSubscribe");<br> }<br><br> @Override<br> public void onSuccess(String s) {<br> LogUtil.d(TAG + "--onSuccess s=" + s);<br> }<br><br> @Override<br> public void onError(Throwable e) {<br> LogUtil.e(TAG + "--onError error=" + e.getMessage());<br> }<br> });<br> |
SingleEmitter 发射器只能发送一条onSuccess 或者onError 数据,如果发射器发射多条数据,观察者只能接收到第一条数据。
结果:
4、Completable和CompletableObserver
不发射数据,只处理 onComplete 和 onError 事件。
image-20191230175048374
方法
onComplete 与onError 只可调用一个,同时调用,第一个生效。
5、Maybe和MaybeObserver
能够发射0或者1个数据,要么成功,要么失败。有点类似于Optional。
onSuccess 方法一次订阅只能发送一次。
方法
onComplete 与onError 只可调用一个,同时调用,第一个生效。
{"s":1,"m":"ok","d":{"entryViewId":"5e08c3516fb9a016561cbb32","entryId":"5e08c351f265da33f2165d1f","content":"
在用户关系管理中,常会遇到些直击灵魂的问题:
-
这批用户到底价值几何?
-
为什么要用这种措施去干预用户,而不是另一种方式。
-
为什么干预这类用户,而不去干预另一类,他们的划分标准是什么。
有这些问题,实质是因为对客户价值不够了解,缺乏行之有效的划分方式。
用户精细化运营价值巨大
随着人口红利的消失,增长逐渐见顶,急需在现有用户池做学问。过去粗放式的买量策略已经不再生效,一是买量成本逐渐高企,二是买量带来的用户忠诚度极低。对现有客户群体的划分和互相倒流,成为重中之重。行业中的黑话“洗用户”,即是讲的这一策略。
对于如何划分用户,不用的职能会有不同的看法。产品有产品的看法,可能基于某项功能偏好;运营有运营的看法,是各种活动玩法的定义;甚至领导还有他的一套看法。但是,无论怎么切入,商业的核心拿捏住,才会八九不离十。
什么是商业的本质:商业的本质是获利。因此,我们从用户的货币价值切入,评估和划分用户的生命周期。
用户生命周期价值,这并不是学界的新鲜产物,该理论在上世纪80年代就已经提出。但对于互联网,网上可搜寻到的资料少之又少。可能的原因有两个:一是互联网在过去20年快速爆发,风口上躺着也能赚钱;二是各家的策略内部不统一,无法形成统一的口径。
但这些都不是不去应用他的理由,反而说明其中价值巨大。这里,我们剥离开复杂的商业逻辑,仅从交易入手,分析用户的生命周期价值,以及用户所处的状态。
用户生命周期价值(CLV)
随着精细化运营的铺开,过去粗放式的、买量用户已经不再买账。每个用户所能接受的最低服务各不相同。如何根据用户价值,进行资源的有效利用。最大化杠杆的使用,成为企业生死的关键。
过去,没有统一的理论出现在互联网应用或是游戏中。但是,运用跨学科的思维,就可以发现:市场营销领域已进行过研究,并给出了精度极高、可解释性强的模型方法。
这种方法,就叫做用户生命周期价值,英文名称 Customer Life Time Value,简称 CLV 或者 LTV。
CLV 是什么
用户生命周期,是一种刻画用户的方法。一般用来解决两类问题:
-
用户还有多少价值、用以衡量投入产出比
-
在干预用户后,根据用户生命周期价值的变化,优化资源的投放。
即用户管理的两个核心问题:用户所具备的价值以及策略的有效性。
需要注意的是,CLV 的产品形态要求非合约。合约在国内最有代表的是合约手机。一般互联网产品,合约形态较为少见。
CLV 的用户群体需已经产生交易,未付费用户不纳入考量。当然,概念迁移,将付费换成活跃或内容消费,该模型也能处理。
CLV 回答哪些问题
用户活跃还是流失,用户还有多少付费潜力,用户在未来某段时间会否再次购买。这三个问题,是用户生命周期价值能够回答的。
如何在自家产品中引入 CLV
应用场景
-
判断用户所处生命周期阶段
-
预测用户指定周期内购买概率
-
预测用户的生命周期价值
-
通过历史付费数据,预测未来付费
活跃与流失的定义
定义:
用户有交互为活跃
用户一段时间不交互,即为流失
lifetims 工具包引入
安装 python 的工具包:
1 | pip install lifetimes |
CLV 数据挖掘
用户生命周期判定,需要三个指标
-
frequency 用户登录的频率,这里为周期内的天数
-
recency 用户的最大周期,即第一次活跃到最后一次活跃
-
T 用户所处阶段,第一次活跃到观察周期结束
对于付费预测,还需要用户的平均付费金额。
数据获取
从数据库获取
1 2 3 4 5 6 7 8 | SELECT customer_id, COUNT(distinct date(transaction_at)) - 1 as frequency, datediff('day', MIN(transaction_at), MAX(transaction_at)) as recency, AVG(total_price) as monetary_value, datediff('day', CURRENT_DATE, MIN(transaction_at)) as T FROM orders GROUP BY customer_idpython 处理 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | from lifetimes.datasets import load_transaction_data from lifetimes.utils import summary_data_from_transaction_data transaction_data = load_transaction_data() print(transaction_data.head()) """ date id 0 2014-03-08 00:00:00 0 1 2014-05-21 00:00:00 1 2 2014-03-14 00:00:00 2 3 2014-04-09 00:00:00 2 4 2014-05-21 00:00:00 2 """ summary = summary_data_from_transaction_data(transaction_data, 'id', 'date', observation_period_end='2014-12-31') print(summary.head()) """ frequency recency T id 0 0.0 0.0 298.0 1 0.0 0.0 224.0 2 6.0 142.0 292.0 3 0.0 0.0 147.0 4 2.0 9.0 183.0 """ bgf.fit(summary['frequency'], summary['recency'], summary['T']) # <lifetimes.BetaGeoFitter: fitted with 5000 subjects, a: 1.85, alpha: 1.86, b: 3.18, r: 0.16> from lifetimes.datasets import load_cdnow_summary data = load_cdnow_summary(index_col=[0]) print(data.head()) """ frequency recency T ID 1 2 30.43 38.86 2 1 1.71 38.86 3 0 0.00 38.86 4 0 0.00 38.86 5 0 0.00 38.86 """BG/NBD 模型 |
BG/NBD 是一个经典模型改进型,详细的数学论证参见:A Note on Deriving the Pareto/NBD Model
and Related Expressions
该模型有如下假设:
通过模型拟合,得到4个参数。
1 2 3 4 5 6 7 8 9 10 | from lifetimes import BetaGeoFitter # similar API to scikit-learn and lifelines. bgf = BetaGeoFitter(penalizer_coef=0.0) bgf.fit(data['frequency'], data['recency'], data['T']) print(bgf) """ <lifetimes.BetaGeoFitter: fitted with 2357 subjects, a: 0.79, alpha: 4.41, b: 2.43, r: 0.24> """ bgf.summary |
效果可视化
1 2 3 | from lifetimes.plotting import plot_probability_alive_matrix plot_probability_alive_matrix(bgf) |
右下角为最佳客户,交易频率高。交易跨度大;右上的客户短时多次交易,极可能已流失。
预测单个用户的购买行为
1 2 3 4 5 | t = 10 #predict purchases in 10 periods individual = summary.iloc[20] # The below function is an alias to `bfg.conditional_expected_number_of_purchases_up_to_time` bgf.predict(t, individual['frequency'], individual['recency'], individual['T']) # 0.0576511 |
生命周期价值预测
在预测价值时,需要第四个参数:用户交易的次均金额。
该模型有个重要前提:购买频次和购买金额无相关性。具体可参考 The Gamma-Gamma Model of Monetary
Value
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from lifetimes.datasets import load_cdnow_summary_data_with_monetary_value summary_with_money_value = load_cdnow_summary_data_with_monetary_value() summary_with_money_value.head() returning_customers_summary = summary_with_money_value[summary_with_money_value['frequency']>0] print(returning_customers_summary.head()) """ frequency recency T monetary_value customer_id 1 2 30.43 38.86 22.35 2 1 1.71 38.86 11.77 6 7 29.43 38.86 73.74 7 1 5.00 38.86 11.77 9 2 35.71 38.86 25.55 """ |
相关性检验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | returning_customers_summary[['monetary_value', 'frequency']].corr() """ monetary_value frequency monetary_value 1.000000 0.113884 frequency 0.113884 1.000000 """ from lifetimes import GammaGammaFitter ggf = GammaGammaFitter(penalizer_coef = 0) ggf.fit(returning_customers_summary['frequency'], returning_customers_summary['monetary_value']) print(ggf) """ <lifetimes.GammaGammaFitter: fitted with 946 subjects, p: 6.25, q: 3.74, v: 15.45> """ |
次均估计
1 2 3 4 5 6 7 8 9 | print(ggf.conditional_expected_average_profit( summary_with_money_value['frequency'], summary_with_money_value['monetary_value'] ).head(3)) """ customer_id 1 24.658619 2 18.911489 3 35.170981 |
总价值估计
最后,使用 DCF 现金流折现,得到用户总体价值的当下估值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # refit the BG model to the summary_with_money_value dataset bgf.fit(summary_with_money_value['frequency'], summary_with_money_value['recency'], summary_with_money_value['T']) print(ggf.customer_lifetime_value( bgf, #the model to use to predict the number of future transactions summary_with_money_value['frequency'], summary_with_money_value['recency'], summary_with_money_value['T'], summary_with_money_value['monetary_value'], time=12, # months discount_rate=0.01 # monthly discount rate ~ 12.7% annually ).head(3)) """ customer_id 1 140.096211 2 18.943467 3 38.180574 Name: clv, dtype: float64 """ |
总结
用户生命周期价值模型,不同于其它模型。该模型对每个用户单独建模,而不是硬性的按流失天数划分,有极强的灵活性。在得到用户生命周期阶段、以及用户的生命周期价值,下一步就是具体应用了。
落地场景多种多样,但要推动上下游,仍需要足够信服的理由。这里给到的建议是,去模拟历史的数据表现,用数据说明效果。
喜欢本文的朋友,别忘了点赞 ??、喜欢 ? +关注 ??哦,您的小小举动,是对作者最大的支持~??