【數(shù)分基本功】 兩種不同的用戶活躍度,留存率居然完全一致!
大家好,我是“蔣點(diǎn)數(shù)分”,多年以來(lái)一直從事數(shù)據(jù)分析工作。從今天開(kāi)始,與大家持續(xù)分享關(guān)于數(shù)據(jù)分析的學(xué)習(xí)內(nèi)容。
本文是第 4 篇,也是【數(shù)分基本功】系列的第 1 篇。該系列會(huì)講一些數(shù)據(jù)分析的基本問(wèn)題,必要時(shí)增加拓展和深入。對(duì) SQL 感興趣的同學(xué),可以看看我的【SQL 周周練】系列(已發(fā)布 3 篇),保證都是有挑戰(zhàn)性有意思的 SQL 題目。后續(xù)創(chuàng)作的內(nèi)容,初步規(guī)劃的方向包括:
后續(xù)內(nèi)容規(guī)劃
1.利用 Streamlit 實(shí)現(xiàn) Hive 元數(shù)據(jù)展示
、SQL 編輯器
、 結(jié)合Docker 沙箱實(shí)現(xiàn)數(shù)據(jù)分析 Agent
2.時(shí)間序列異常識(shí)別、異動(dòng)歸因算法
3.留存率擬合、預(yù)測(cè)、建模
4.學(xué)習(xí) AB 實(shí)驗(yàn)
、復(fù)雜實(shí)驗(yàn)設(shè)計(jì)等
5.自動(dòng)化機(jī)器學(xué)習(xí)
、自動(dòng)化特征工程
6.因果推斷
學(xué)習(xí)
7. ……
歡迎關(guān)注,一起學(xué)習(xí)。
留存率的不足
一、留存率的基本概念
1. 留存率如何定義與計(jì)算
這里談?wù)勱P(guān)于留存率的定義,如果你已經(jīng)非常熟悉,可以跳過(guò)(直接要看留存率曲線的缺陷,請(qǐng)?zhí)D(zhuǎn)到第三節(jié))
留存率曲線是用的“某一日留存”,這個(gè)“留存”是根據(jù)你想要分析的內(nèi)容來(lái)定的。如果用戶某一日做出了我們想要的行為,那就可以認(rèn)定用戶該日留存。這就是留存率的分子,而分母就是第一天滿足我們要求的用戶數(shù)或設(shè)備數(shù)。
比如大家經(jīng)常關(guān)心的日活躍用戶 DAU
,這個(gè)活躍的標(biāo)準(zhǔn)一般比較低,你打開(kāi)/進(jìn)入/登錄等,只要你進(jìn)來(lái)了,就算上你這個(gè)用戶或設(shè)備。實(shí)際執(zhí)行的時(shí)候,必然牽扯到啟動(dòng)接口上報(bào)(冷啟動(dòng)、熱啟動(dòng)、切桌面、殺后臺(tái)......),也有人會(huì)結(jié)合啟動(dòng)接口和幾個(gè)覆蓋面廣的埋點(diǎn)來(lái)統(tǒng)計(jì),避免遺漏數(shù)據(jù)。
如果你關(guān)心的是某個(gè)功能或者某個(gè)業(yè)務(wù),你還可以將統(tǒng)計(jì)條件設(shè)置為用戶要使用 App 的某個(gè)功能或者進(jìn)入某個(gè)業(yè)務(wù)的頁(yè)面,這也是一種留存。
本文談的是最常見(jiàn)的留存,每日新增用戶 DNU
后續(xù)的留存。用戶從某一天首次進(jìn)入這個(gè) App,并且被我們統(tǒng)計(jì)上了(設(shè)備維度或賬號(hào)維度);留存率曲線用的是“第 x 日留存”,比如“第 7 日留存”,也就是第 7 天用戶還來(lái)這個(gè) App,才算數(shù)。否則用戶就是第 3~6 天,每天都來(lái),那也跟“第 7 日留存”無(wú)關(guān)。
2. 為什么用“第 7 日留存”而不是“7 日內(nèi)留存”
為什么有些人更愿意選擇“7 日內(nèi)留存”?我一直在互聯(lián)網(wǎng)工作,有些部門扛著這些指標(biāo)。甚至我第一次用 SQL 求留存率的時(shí)候,我也覺(jué)得——“哎,要是用戶第 3~6 天來(lái)了,第 7 天沒(méi)有來(lái),這個(gè)第 7 日留存率的指標(biāo)不把人家算上,不是太可惜了?!?/p>
但如果計(jì)算留存用了“7 日內(nèi)留存”,那么“30 天”、“365 天“ 怎么辦?比如用戶第二天出現(xiàn)了一次,但是后面一年都不再出現(xiàn)了,“365 天”的留存我們也把這個(gè)用戶算入。周期越長(zhǎng),這個(gè)指標(biāo)就越虛增,這怎么行。
3. 留存率用來(lái)分析做什么
最關(guān)鍵的,“留存率曲線”后面都跟著幾個(gè)話題:第一是 DAU 預(yù)測(cè),DAU 就需要當(dāng)天的“點(diǎn)”數(shù)據(jù),來(lái)個(gè)“7 日內(nèi)”這咋辦。第二就是 LT 用戶生命周期,它其實(shí)就是留存曲線下面的面積/積分(說(shuō)累加也行,感覺(jué)積分拽一點(diǎn)),同樣要求留存率是“點(diǎn)”計(jì)算。第三就是 LTV 用戶生命周期價(jià)值:不限制天數(shù),就是用戶全生命周期價(jià)值;限制天數(shù),就是某個(gè)時(shí)段內(nèi)的用戶生命周期價(jià)值。LT
都依賴于點(diǎn)計(jì)算,而 LTV
更是如此了。
我們本質(zhì)上更關(guān)注那三個(gè)指標(biāo)(DAU
、LT
、LTV
;特別是 LTV
和 CAC
的比值,可以視為投放推廣、營(yíng)銷運(yùn)營(yíng)活動(dòng)的 ROI
),這就反向限制了新增用戶留存率的計(jì)算邏輯。
二、留存率曲線的擬合方法
1. 到底是用指數(shù)函數(shù)還是冪函數(shù)
指數(shù)函數(shù)的表達(dá)式為
冪函數(shù)的表達(dá)式為
這兩個(gè)函數(shù)的差異在于:衰減的速度不一樣。把留存率 retention rate
簡(jiǎn)寫(xiě)為 ,指數(shù)函數(shù)是
;冪函數(shù)是
。咱不用微分也不用差分,就直接來(lái)個(gè)
和
,代入公式除除看:
可以看出指數(shù)函數(shù)的“衰減”,即后面某一天 相對(duì)于前面某一天
的比值,與
的取值無(wú)關(guān),只和日期的間隔
有關(guān)。
這也就意味著。第 2 天到第 8 天流失的比例,和第 302 天到第 308 天流失的比例是一樣的。直覺(jué)上懷疑,因?yàn)楹笳呷俣嗵爝€來(lái)了;如果不是因?yàn)槔嫌脩粽倩氐惹闆r,那么這個(gè)用戶感覺(jué)挺穩(wěn)定的(也需要看您公司的業(yè)務(wù)類型)如果是低頻業(yè)務(wù):C 端用戶搬家、汽車保養(yǎng)、旅游、買車買房,那這種流失情況有可能出現(xiàn)。對(duì)于中高頻的 App 應(yīng)該不會(huì)如此。
再來(lái)看看冪函數(shù):
可以看出冪函數(shù)的“衰減”,與日期間隔 和起始的天數(shù)
都有關(guān);如果
越大,這個(gè)衰減越小。越往后流失速度越慢,這個(gè)感覺(jué)好一些。
2. 用 Python 來(lái)驗(yàn)證擬合效果
最核心的函數(shù)是 scipy
庫(kù)的 optimize
下面的 curve_fit
;具體計(jì)算原理,感興趣的同學(xué)請(qǐng)自行搜索。
a.我們先定義指數(shù)函數(shù)和冪函數(shù)的 Python 函數(shù),然后使用 curve_fit
來(lái)獲取參數(shù),scipy
的文檔鏈接,我已經(jīng)在代碼中給出。因?yàn)槲沂掷锶狈?shí)際可靠的留存率數(shù)據(jù),我們就用 “40-20-10” 來(lái)擬合,都說(shuō)它 Facebook/Meta 給的留存率標(biāo)準(zhǔn) —— 次日留存 40%,七日留存 20%,30 日留存 10%。
請(qǐng)注意:實(shí)際擬合留存率時(shí),有很多細(xì)節(jié)需要考慮。包括長(zhǎng)期擬合的情況,以及喂給模型多少天的數(shù)據(jù),這部分細(xì)節(jié)可以見(jiàn)我給出的參考鏈接 2 。
import numpy as np from scipy.optimize import curve_fit # 定義指數(shù)函數(shù)形式留存率函數(shù) def exponential_ret_rate_func(t, a, b): return a * np.exp(-b * t) # 定義冪函數(shù)形式留存率函數(shù) def power_ret_rate_func(t, a, b): return a * np.power(t, -b) # facebook 提出那個(gè) 40-20-10 留存率 days = [2, 7, 30] actual_ret_rate = [0.4, 0.2, 0.1] # 不加范圍,會(huì)提示 warning;雖然不影響結(jié)果 # https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html#scipy.optimize.curve_fit exp_ret_arg, _ = curve_fit( exponential_ret_rate_func, days, actual_ret_rate, bounds=([-np.inf, 0], [np.inf, np.inf]), ) # 冪函數(shù)參數(shù) pow_ret_arg, _ = curve_fit(power_ret_rate_func, days, actual_ret_rate) all_days = np.arange(2, 31) exp_ret_rate_arr = exponential_ret_rate_func(all_days, *exp_ret_arg) pow_ret_rate_arr = power_ret_rate_func(all_days, *pow_ret_arg)
b.采用 RMSE
來(lái)對(duì)比一下,擬合的結(jié)果與我們給出的三個(gè)留存率的差異:
# 求求擬合的函數(shù),與之前給出的 40-20-10 留存率差異 # 取 RMSE,np.array(days) - 2 注意起始點(diǎn)的序號(hào)映射關(guān)系 rmse_exponential = np.sqrt(np.mean((exp_ret_rate_arr[np.array(days) - 2] - actual_ret_rate) ** 2)) rmse_power = np.sqrt(np.mean((pow_ret_rate_arr[np.array(days) - 2] - actual_ret_rate) ** 2)) print(f"指數(shù)函數(shù)擬合的 RMSE:{rmse_exponential:.4f}") print(f"冪函數(shù)擬合的 RMSE:{rmse_power:.4f}")
輸出的結(jié)果為:指數(shù)函數(shù)擬合的 RMSE:0.0477冪函數(shù)擬合的 RMSE:0.0043(冪函數(shù)比指數(shù)函數(shù)好一個(gè)量級(jí))
c.用 pyvchart
將兩條留存率曲線繪制出來(lái),它是字節(jié)跳動(dòng)開(kāi)源的 vchart
的 Python 包。你也可以使用 pyecharts
來(lái)繪制,我一般更喜歡這種動(dòng)態(tài)圖表
from pyvchart import render_chart retent_data = [ {"days": int(d), "retent_rate": float(round(r, 4)), "retent_func_type": "指數(shù)函數(shù)"} for d, r in zip(all_days, exp_ret_rate_arr) ] retent_data.extend([ { "days": int(d), "retent_rate": float(round(r, 4)), "retent_func_type": "冪函數(shù)", } for d, r in zip(all_days, pow_ret_rate_arr) ]) spec = { "type": "line", "data": [{"id": "lineData", "values": retent_data}], "xField": "days", "yField": "retent_rate", "seriesField": "retent_func_type", "title": {"visible": True, "text": "指數(shù)函數(shù)和冪函數(shù)擬合留存率效果"}, } # 在 jupyter 環(huán)境,使用 display 顯示 display(render_chart(spec))
看一下擬合的曲線,的確是冪函數(shù)形式更加適合。后續(xù)我們采用冪函數(shù)
d.我們來(lái)繪制出一張?zhí)貏e經(jīng)典的圖,每日的 DAU
其實(shí)是由歷史上每一天的新增用戶的每日留存構(gòu)成的。這里為了代碼模擬簡(jiǎn)化,假定從 2025-05-01 開(kāi)始每天的新增用戶都是 10000,留存率曲線每一天都用上面擬合的冪函數(shù)留存率。
pow_ret_people_num = np.round(10000 * pow_ret_rate_arr) import datetime start_date = datetime.date(2025, 5, 1) all_days_formatted = [start_date+datetime.timedelta(days=int(d)-1) for d in all_days] dau_detail = [] dau_list = [] for idx, d in enumerate(all_days_formatted): dau = 0 d = d.strftime('%y-%m-%d') for i in range(idx+1): dau += int(pow_ret_people_num[idx-i]) dau_detail.append({ 'date': d, 'group': int(all_days[i]), 'people_num': int(pow_ret_people_num[idx-i]) }) dau_list.append({'date': d, 'dau_num': dau}) spec = { "type": "line", "data": [{"id": "dau_detail_data", "values": dau_detail}, {"id": "dau_data", "values": dau_list}], "series": [ { "type": "area", "dataId": "dau_detail_data", "xField": "date", "yField": "people_num", "seriesField": "group", "stack": True, "point": {"visible": False}, }, { "type": "line", "dataId": "dau_data", "xField": "date", "yField": "dau_num", } ], "title": {"visible": True, "text": "DAU 是由歷史的每日新增用戶每一日留存構(gòu)成的"}, } # 在 jupyter 環(huán)境,使用 display 顯示 display(render_chart(spec))
可能很多同學(xué)覺(jué)得這圖太亂了,但是我想這幅圖表達(dá)的意思還是很清楚的。而且這幅圖有實(shí)際意義的,比如分析時(shí),可以把新老用戶拆開(kāi)。針對(duì)老用戶,不一定按照新增日期;完全可以根據(jù)其他不會(huì)改變的維度值,并且把這些維度值歸為少數(shù)幾類。這樣再畫(huà)出這幅圖,用來(lái)分析老用戶留存,這就是一個(gè)不錯(cuò)的展示工具。
三、留存率曲線的不足
我用上面的同樣的一條冪函數(shù)留存率,繪制出兩種不一樣的用戶活躍情況(用 50 個(gè)用戶做可視化模擬)。a.第一種用戶活躍情況,注意數(shù)據(jù)集結(jié)果寫(xiě)入剪貼板(方便我貼到 WPS 中,我使用的 Ubuntu 沒(méi)有微軟 Office)。要分開(kāi)運(yùn)行,后面代碼也有寫(xiě)入剪貼板
import pandas as pd user_retention_num = np.round(50*exp_ret_rate_arr,0) first_user_retention_user_tag = np.zeros((29,50)) for i, num in enumerate(user_retention_num): first_user_retention_user_tag[i][:int(num)] = 1 # to_clioboard 函數(shù)是寫(xiě)入剪貼板,注意要分開(kāi)運(yùn)行 # 后續(xù)的寫(xiě)入剪貼板會(huì)覆蓋這部分 pd.DataFrame(first_user_retention_user_tag.T).to_clipboard(index=False, header=False)
第一種活躍情況,看上去是非常極端。
b.第二種用戶活躍情況
np.random.seed(2025) second_user_retention_user_tag = np.zeros((29,50)) for i, num in enumerate(user_retention_num): idx = np.random.choice(50, size=int(num), replace=False) second_user_retention_user_tag[i][idx] = 1 df = pd.DataFrame(second_user_retention_user_tag.T) df.to_clipboard(index=False, header=False)
參考這兩張圖,大家應(yīng)該都能看出來(lái)問(wèn)題??赡苡腥擞X(jué)得第二張圖有點(diǎn)亂,是我故意混淆大家的視覺(jué)吧。那么我借鑒 RFM
的思路,根據(jù)用戶最后一次活躍距今多少天以及總活躍天數(shù)來(lái)排序。
c.實(shí)現(xiàn)根據(jù)用戶最后一次活躍距今多少天以及總活躍天數(shù)來(lái)排序的邏輯:
df_sort = pd.DataFrame() df_sort['last_retent'] = df.apply(lambda row: row[row==1].index[-1] if any(row) else -1, axis=1) df_sort['retent_days'] = df.apply(lambda row: sum(row), axis=1) sort_order = df_sort.sort_values(by=['last_retent', 'retent_days'], ascending=[False, False]).index df.loc[sort_order,:].to_clipboard(index=False, header=False)
此處再對(duì)比,應(yīng)該明顯多了。如果我們把最后一次活躍。當(dāng)成一種“包絡(luò)線”來(lái)看,后者的情況要比第一種極端情況好得多。但是兩者的留存率曲線是一樣。圖片表格最上面的紅色數(shù)字,就是公式,對(duì)表格里整列的 1 求和;可以佐證每日留存用戶數(shù)是一樣的。
也就是說(shuō)留存率一致其實(shí)只能說(shuō)明每日新增的用戶后續(xù)“活躍的人*天”是一致的,用戶的活躍分布,甚至是真正留下的用戶數(shù)量并不一致。第一種活躍情況,現(xiàn)實(shí)情況不會(huì)這么極端,但是我最開(kāi)始用 SQL 計(jì)算留存率時(shí),的確隱隱感覺(jué)一種不對(duì)勁。
一般情況下,不需要什么調(diào)整。如果需要新的指標(biāo)輔助,可以增加新增用戶平均活躍天數(shù)或流失用戶比例等。
四、參考資料
本文關(guān)于指數(shù)函數(shù)和冪函數(shù)的啟發(fā)來(lái)自于青十五1.青十五——《LTV預(yù)估與留存曲線擬合:指數(shù)函數(shù)還是冪函數(shù)?》
該文章提到了擬合留存率的一些細(xì)節(jié)2.黎湘艷——《Python數(shù)據(jù)分析實(shí)戰(zhàn)(四):收入、活躍預(yù)測(cè)》
<hr>
??????我現(xiàn)在正在求職數(shù)據(jù)類工作(主要是數(shù)據(jù)分析或數(shù)據(jù)科學(xué));如果您有合適的機(jī)會(huì),即時(shí)到崗,不限城市。