RFM,是一種經典的用戶分類、價值分析模型:
R,Rencency,即每個客戶有多少天沒回購了,可以理解為最近一次購買到現在隔了多少天。
F,Frequency,是每個客戶購買了多少次。
M,Monetary,代表每個客戶平均購買金額,也可以是累計購買金額。
這三個維度,是RFM模型的精髓所在,幫助我們把混雜一體的客戶數據分成標准的8類,然后根據每一類用戶人數占比、金額貢獻等不同的特征,進行人、貨、場三重匹配的精細化運營。
用Python建立RFM模型,整體建模思路分為五步,分別是數據概覽、數據清洗、維度打分、分值計算和客戶分層。
一:數據概覽
開發環境:jupyter Notebook, python 3.6
import pandas as pd import numpy as np import os os.chdir('F:\\50mat') df = pd.read_excel('PYTHON-RFM實戰數據.xlsx') df.head()
# 打印結果
品牌名稱 買家昵稱 付款日期 訂單狀態 實付金額 郵費 省份 城市 購買數量 0 一只阿木木 棒西瓜皮的店 2019-04-18 11:05:26 交易成功 210 0 北京 北京市 1 1 一只阿木木 8fiona_c8 2019-04-18 11:08:03 交易成功 53 0 上海 上海市 4 2 一只阿木木 3t_1479778131547_04 2019-04-18 11:13:01 交易成功 169 0 上海 上海市 1 3 一只阿木木 0kexintiantian20 2019-04-18 11:13:19 付款以后用戶退款成功,交易自動關閉 107 0 北京 北京市 1 4 一只阿木木 ysxxgx 2019-04-18 11:18:07 付款以后用戶退款成功,交易自動關閉 254 0 江蘇省 蘇州市 2
在訂單狀態中,交易成功、用戶退款導致交易關閉的,那還包括其他狀態嗎?退款訂單對於我們模型價值不大,需要在后續清洗中剔除。
df['訂單狀態'].unique()
# 打印結果
array(['交易成功', '付款以后用戶退款成功,交易自動關閉', '訂單狀態'], dtype=object)
再觀察數據的類型和缺失情況:
df.info()
# 打印結果
<class 'pandas.core.frame.DataFrame'> RangeIndex: 922658 entries, 0 to 922657 Data columns (total 9 columns): 品牌名稱 922658 non-null object 買家昵稱 922658 non-null object 付款日期 922658 non-null object 訂單狀態 922658 non-null object 實付金額 922658 non-null object 郵費 922658 non-null object 省份 922658 non-null object 城市 922626 non-null object 購買數量 922658 non-null object dtypes: object(9) memory usage: 31.7+ MB
數據類型方面,付款日期是時間格式,實付金額、郵費和購買數量是數值型,其他均為字符串類型。
二:數據清洗
2.1 剔除退款
在觀察階段,我們明確了第一個清洗的目標,就是剔除退款數據:
df = df.loc[df['訂單狀態'] == '交易成功',:] print('剔除退款后還剩:%d行' % len(df))
打印結果
剔除退款后還剩:889372行
2.2 關鍵字段提取
剔除之后,覺得我們訂單的字段還是有點多,而RFM模型只需要買家昵稱,付款時間和實付金額這3個關鍵字段,所以提取之:
df = df[['買家昵稱','付款日期','實付金額']] df.head()
# 打印結果
買家昵稱 付款日期 實付金額 0 棒西瓜皮的店 2019-04-18 11:05:26 210 1 8fiona_c8 2019-04-18 11:08:03 53 2 3t_1479778131547_04 2019-04-18 11:13:01 169 7 2jill27 2019-01-01 10:00:11 121 8 yjessieni 2019-01-01 10:00:14 211
2.3 關鍵字段構造
R 值構造 。
R值,即每個用戶最后一次購買時間距今多少天。
- 如果用戶只下單過一次,用現在的日期減去付款日期即可;
- 若是用戶多次下單,需先篩選出這個用戶最后一次付款的時間,再用今天減去它。
距離今天越近,時間也就越“大”,
pd.to_datetime('2019-11-11') > pd.to_datetime('2019-1-1')
# 打印結果 True
用戶最近一次付款時間,只需要按買家昵稱分組,再選取付款日期的最大值即可:
r = df.groupby('買家昵稱')['付款日期'].max().reset_index() r.head()
#打印結果
買家昵稱 付款日期 0 .blue_ram 2019-02-04 17:49:34.000 1 .christiny 2019-01-29 14:17:15.000 2 .willn1 2019-01-11 03:46:18.000 3 .托托m 2019-01-11 02:26:33.000 4 0000妮 2019-06-28 16:53:26.458
用今天減去每位用戶最近一次付款時間,就得到R值了,測試樣本數據是1月至6月上半年的數據,所以我們把“2019-7-1”當作“今天”:
r['R'] = (pd.to_datetime('2019-7-1') - r['付款日期']).dt.days r = r[['買家昵稱','R']] r.head()
#打印結果
買家昵稱 R 0 .blue_ram 146 1 .christiny 152 2 .willn1 170 3 .托托m 170 4 0000妮 2
F 值構造
F值,即每個用戶累計購買頻次。
我們明確“單個用戶一天內多次下單行為看作整體一次”,引入一個精確到天的日期標簽,依照“買家昵稱”和“日期標簽”進行分組,把每個用戶一天內的多次下單行為合並
統計購買次數:
# F 值構造 # 引入日期標簽 df['日期標簽'] = df['付款日期'].astype(str).str[:10] #把單個用戶一天內訂單合並 dup_f = df.groupby(['買家昵稱','日期標簽'])['付款日期'].count().reset_index() #對合並后的用戶統計頻次 f = dup_f.groupby('買家昵稱')['付款日期'].count().reset_index() f.columns = ['買家昵稱','F'] f.head()
# 打印結果
買家昵稱 F 0 .blue_ram 1 1 .christiny 1 2 .willn1 1 3 .托托m 1 4 0000妮 1
M 值構造
客戶平均購買金額,需要得到每個用戶總金額,再用總金額除以購買頻次即可。
# M 值構造 sum_m = df.groupby('買家昵稱')['實付金額'].sum().reset_index() sum_m.columns = ['買家昵稱','總支付金額'] com_m = pd.merge(sum_m,f,left_on = '買家昵稱',right_on = '買家昵稱',how = 'inner') #計算用戶平均支付金額 com_m['M'] = com_m['總支付金額'] / com_m['F'] com_m.head()
# 打印結果
買家昵稱 總支付金額 F M 0 .blue_ram 1568 1 1568.0 1 .christiny 5856 1 5856.0 2 .willn1 1088 1 1088.0 3 .托托m 1184 1 1184.0 4 0000妮 5248 1 5248.0
R F M 三值合並
# R F M 值構造並合並 rfm = pd.merge(r,com_m,left_on = '買家昵稱',right_on = '買家昵稱',how = 'inner') rfm = rfm[['買家昵稱','R','F','M']] rfm.head()
# 打印結果
買家昵稱 R F M 0 .blue_ram 146 1 1568.0 1 .christiny 152 1 5856.0 2 .willn1 170 1 1088.0 3 .托托m 170 1 1184.0 4 0000妮 2 1 5248.0
三:維度打分
此部分不涉及代碼。
維度確認的核心是分值確定,按照設定的標准,我們給每個消費者的R/F/M值打分,分值的大小取決於我們的偏好,即我們越喜歡的行為,打的分數就越高:
- R值 用戶有多少天沒來下單,所以R越大,用戶流失的可能性越大,分值越小。
- F值 用戶購買頻次,數值越大,得分越高
- M值 用戶平均支付金額,數值越大,得分越高
RFM模型中打分一般采取5分制,有兩種比較常見的方式,
- 一種是按照數據的分位數來打分
- 一種是依據數據和業務的理解進行分值的划分
這里使用的是第二種,即提前制定好不同數值對應的分值,加深對數據的理解。
R值根據行業經驗,設置為30天一個跨度,區間左閉右開:
R 分 | R 值 |
---|---|
1 | [120, +%) |
2 | [90, 120) |
3 | [60, 90) |
4 | [30, 60) |
5 | [0, 30) |
F值 F值和購買頻次掛鈎,每多一次購買,分值就多加一分:
F 分 | F 值 |
---|---|
1 | 1 |
2 | 2 |
3 | 3 |
4 | 4 |
5 | [5, +%) |
M值 先對M值做個簡單的區間統計,然后分組,這里我們按照1600元的一個區間來進行划分:
M 分 | M 值 |
---|---|
1 | [0, 1600) |
2 | [1600, 3200) |
3 | [3200, 4800) |
4 | [4800, 6400) |
5 | [6400, +%) |
我們確定了一個打分框架,每一位用戶的每個指標,都有了與之對應的分值。
四:分值計算
第一次計算
R值:
# 4 R 值計算 rfm['R-SCORE'] = pd.cut(rfm['R'],bins = [0,30,60,90,120,1000000],labels = [5,4,3,2,1],right = False).astype(float) rfm.head()
# 打印結果
買家昵稱 R F M R-SCORE 0 .blue_ram 146 1 1568.0 1.0 1 .christiny 152 1 5856.0 1.0 2 .willn1 170 1 1088.0 1.0 3 .托托m 170 1 1184.0 1.0 4 0000妮 2 1 5248.0 5.0
F M 值計算
# F M 值計算 rfm['F-SCORE'] = pd.cut(rfm['F'],bins = [1,2,3,4,5,1000000],labels = [1,2,3,4,5],right = False).astype(float) rfm['M-SCORE'] = pd.cut(rfm['M'],bins = [0,1600,3200,4800,6400,10000000],labels = [1,2,3,4,5],right = False).astype(float) rfm.head()
# 打印結果
買家昵稱 R F M F-SCORE M-SCORE 0 .blue_ram 146 1 1568.0 1.0 1.0 1 .christiny 152 1 5856.0 1.0 4.0 2 .willn1 170 1 1088.0 1.0 1.0 3 .托托m 170 1 1184.0 1.0 1.0 4 0000妮 2 1 5248.0 1.0 4.0
第二次計算
過多的分類和不分類本質是一樣的。所以,我們通過判斷每個客戶的R、F、M值是否大於平均值,來簡化分類結果。
因為每個客戶和平均值對比后的R、F、M,只有0和1(0表示小於平均值,1表示大於平均值)兩種結果,整體組合下來共有8個分組,是比較合理的一個情況。我們來判斷用戶的每個分值是否大於平均值:
# 第二次計算 rfm['R是否大於均值'] = (rfm['R-SCORE'] > rfm['R-SCORE'].mean()) * 1 rfm['F是否大於均值'] = (rfm['F-SCORE'] > rfm['F-SCORE'].mean()) * 1 rfm['M是否大於均值'] = (rfm['M-SCORE'] > rfm['M-SCORE'].mean()) * 1 rfm.head()
# 打印結果
買家昵稱 R F M R-SCORE F-SCORE M-SCORE R是否大於均值 F是否大於均值 M是否大於均值 0 .blue_ram 146 1 1568.0 1.0 1.0 1.0 0 0 0 1 .christiny 152 1 5856.0 1.0 1.0 4.0 0 0 1 2 .willn1 170 1 1088.0 1.0 1.0 1.0 0 0 0 3 .托托m 170 1 1184.0 1.0 1.0 1.0 0 0 0 4 0000妮 2 1 5248.0 5.0 1.0 4.0 1 0 1
代碼為什么 * 1,這是由於Python中判斷后返回的結果是True和False,對應着數值1和0,只要把這個布爾結果乘上1,True就變成了1,False變成了0,處理之后更加易讀。
知識點:
Pandas的cut函數:
- 第一個參數傳入要切分的數據列。
- bins參數代表我們按照什么區間進行分組,上面我們已經確定了R值按照30天的間隔進行分組,輸入[0,30,60,90,120,1000000]即可,最后一個數值設置非常大,是為了給分組一個容錯空間,允許出現極端大的值。
- right表示了右側區間是開還是閉,即包不包括右邊的數值,如果設置成False,就代表[0,30),包含左側的分組數據而不含右側,若設置為True,則是[0,30],首尾都包含。
- labels和bins切分的數組前后呼應,什么意思呢?bins設置了6個數值,共切分了5個分組,labels則分別給每個分組打標簽,0-30是5分,30-60是4分,依此類推。
五:客戶分層
清洗完之后我們確定了打分邏輯,然后分別計算每個用戶的R、F、M分值(SCORE),隨后,用分值和對應的平均值進行對比,得到了是否大於均值的三列結果。那么客戶分層如何處理呢。
R 大於均值 | F 大於均值 | M 大於均值 | 一般分類 | RFM 分類 | 釋義 |
---|---|---|---|---|---|
1 | 1 | 1 | 重要價值客戶 | 重要價值客戶 | 近購、高頻、高消費 |
1 | 1 | 0 | 重要潛力客戶 | 消費潛力客戶 | 近購、高頻、低消費 |
1 | 0 | 1 | 重要深耕客戶 | 頻次深耕客戶 | 近購、低頻、高消費 |
1 | 0 | 0 | 新客戶 | 新客戶 | 近購、低頻、低消費 |
0 | 1 | 1 | 重要喚回客戶 | 重要價值流失預警客戶 | 近未購、高頻、高消費 |
0 | 1 | 0 | 一般客戶 | 一般客戶 | 近未購、高頻、低消費 |
0 | 0 | 1 | 重要挽回客戶 | 高消費喚回客戶 | 近未購、低頻、高消費 |
0 | 0 | 0 | 流失客戶 | 流失客戶 | 近未購、低頻、低消費 |
潛力是針對消費(平均支付金額),深耕是為了提升消費頻次,以及重要喚回客戶其實和重要價值客戶非常相似,只是最近沒有回購了而已,應該做流失預警等等。
5.1 構建合並指標
先引入一個人群數值的輔助列,把之前判斷的R\F\M是否大於均值的三個值給串聯起來:
# 5 客戶分層,構建合並指標 rfm['人群數值'] = (rfm['R是否大於均值'] * 100) + (rfm['F是否大於均值'] * 10) + (rfm['M是否大於均值'] * 1) rfm.head()
# 打印結果
買家昵稱 R F M R-SCORE F-SCORE M-SCORE R是否大於均值 F是否大於均值 M是否大於均值 人群數值 0 .blue_ram 146 1 1568.0 1.0 1.0 1.0 0 0 0 0 1 .christiny 152 1 5856.0 1.0 1.0 4.0 0 0 1 1 2 .willn1 170 1 1088.0 1.0 1.0 1.0 0 0 0 0 3 .托托m 170 1 1184.0 1.0 1.0 1.0 0 0 0 0 4 0000妮 2 1 5248.0 5.0 1.0 4.0 1 0 1 101
人群數值是數值類型,所以位於前面的0就自動略過,比如1代表着“001”的高消費喚回客戶人群,10對應着“010”的一般客戶。
5.2 基於指標給客戶打標簽
為了得到最終人群標簽,再定義一個判斷函數,通過判斷人群數值的值,來返回對應的分類標簽:
#判斷R/F/M是否大於均值 def transform_label(x): if x == 111: label = '重要價值客戶' elif x == 110: label = '消費潛力客戶' elif x == 101: label = '頻次深耕客戶' elif x == 100: label = '新客戶' elif x == 11: label = '重要價值流失預警客戶' elif x == 10: label = '一般客戶' elif x == 1: label = '高消費喚回客戶' elif x == 0: label = '流失客戶' return label
5.3 標簽應用
rfm['人群類型'] = rfm['人群數值'].apply(transform_label) rfm.head()
# 打印結果
買家昵稱 R F M R-SCORE F-SCORE M-SCORE R是否大於均值 F是否大於均值 M是否大於均值 人群數值 人群類型 0 .blue_ram 146 1 1568.0 1.0 1.0 1.0 0 0 0 0 流失客戶 1 .christiny 152 1 5856.0 1.0 1.0 4.0 0 0 1 1 高消費喚回客戶 2 .willn1 170 1 1088.0 1.0 1.0 1.0 0 0 0 0 流失客戶 3 .托托m 170 1 1184.0 1.0 1.0 1.0 0 0 0 0 流失客戶 4 0000妮 2 1 5248.0 5.0 1.0 4.0 1 0 1 101 頻次深耕客戶
RFM 建模,每一位客戶都有了屬於自己的RFM標簽。
六:RFM 模型結果探索性分析
切模型結果最終都要服務於業務,所以我們基於現有模型結果做一些拓展、探索性分析。
6.1 人數統計
# 6.1 人數分析 count = rfm['人群類型'].value_counts().reset_index() count.columns = ['客戶類型','人數'] count['人數占比'] = count['人數'] / count['人數'].sum() count
# 打印結果
客戶類型 人數 人數占比 0 高消費喚回客戶 7338 0.288670 1 流失客戶 6680 0.262785 2 頻次深耕客戶 5427 0.213493 3 新客戶 4224 0.166168 4 重要價值客戶 756 0.029740 5 消費潛力客戶 450 0.017703 6 重要價值流失預警客戶 360 0.014162 7 一般客戶 185 0.007278
6.2 金額統計
# 6.2 金額分析 rfm['購買總金額'] = rfm['F'] * rfm['M'] mon = rfm.groupby('人群類型')['購買總金額'].sum().reset_index() mon.columns = ['客戶類型','消費金額'] mon['金額占比'] = mon['消費金額'] / mon['消費金額'].sum() mon
# 打印結果
客戶類型 消費金額 金額占比 0 一般客戶 825696.0 0.007349 1 新客戶 8667808.0 0.077143 2 流失客戶 14227744.0 0.126625 3 消費潛力客戶 2050506.0 0.018249 4 重要價值客戶 8615698.0 0.076679 5 重要價值流失預警客戶 3732550.0 0.033219 6 頻次深耕客戶 31420996.0 0.279644 7 高消費喚回客戶 42819846.0 0.381092
6.3 客戶類型
result = pd.merge(count,mon,left_on = '客戶類型',right_on = '客戶類型') result
# 打印結果
客戶類型 人數 人數占比 消費金額 金額占比 0 高消費喚回客戶 7338 0.288670 42819846.0 0.381092 1 流失客戶 6680 0.262785 14227744.0 0.126625 2 頻次深耕客戶 5427 0.213493 31420996.0 0.279644 3 新客戶 4224 0.166168 8667808.0 0.077143 4 重要價值客戶 756 0.029740 8615698.0 0.076679 5 消費潛力客戶 450 0.017703 2050506.0 0.018249 6 重要價值流失預警客戶 360 0.014162 3732550.0 0.033219 7 一般客戶 185 0.007278 825696.0 0.007349
七:模型封裝 ENTER
模型封裝,一個回車就能返回結果
import pandas as pd import numpy as np import os os.chdir('F:\\50mat') #輸入源數據文件名 def get_rfm(name = 'PYTHON-RFM實戰數據.xlsx'): # 數據概覽 df = pd.read_excel(name) # 數據清洗 df = df.loc[df['訂單狀態'] == '交易成功',:] print('剔除退款后還剩:%d行' % len(df)) df = df[['買家昵稱','付款日期','實付金額']] # 構造 R 值,Recency 即每個用戶最后一次購買時間距今多少天。 r = df.groupby('買家昵稱')['付款日期'].max().reset_index() r['R'] = (pd.to_datetime('2019-7-1') - r['付款日期']).dt.days r = r[['買家昵稱','R']] # 構造 F 值,Frequency 即每個用戶累計購買頻次。 #引入日期標簽輔助列 df['日期標簽'] = df['付款日期'].astype(str).str[:10] #把單個用戶一天內訂單合並 dup_f = df.groupby(['買家昵稱','日期標簽'])['付款日期'].count().reset_index() #對合並后的用戶統計頻次 f = dup_f.groupby('買家昵稱')['付款日期'].count().reset_index() f.columns = ['買家昵稱','F'] # M 值構造,Monetary 客戶平均購買金額 sum_m = df.groupby('買家昵稱')['實付金額'].sum().reset_index() sum_m.columns = ['買家昵稱','總支付金額'] com_m = pd.merge(sum_m,f,left_on = '買家昵稱',right_on = '買家昵稱',how = 'inner') #計算用戶平均支付金額 com_m['M'] = com_m['總支付金額'] / com_m['F'] rfm = pd.merge(r,com_m,left_on = '買家昵稱',right_on = '買家昵稱',how = 'inner') rfm = rfm[['買家昵稱','R','F','M']] rfm['R-SCORE'] = pd.cut(rfm['R'],bins = [0,30,60,90,120,1000000],labels = [5,4,3,2,1],right = False).astype(float) rfm['F-SCORE'] = pd.cut(rfm['F'],bins = [1,2,3,4,5,1000000],labels = [1,2,3,4,5],right = False).astype(float) rfm['M-SCORE'] = pd.cut(rfm['M'],bins = [0,1600,3200,4800,6400,10000000],labels = [1,2,3,4,5],right = False).astype(float) rfm['R是否大於均值'] = (rfm['R-SCORE'] > rfm['R-SCORE'].mean()) * 1 rfm['F是否大於均值'] = (rfm['F-SCORE'] > rfm['F-SCORE'].mean()) * 1 rfm['M是否大於均值'] = (rfm['M-SCORE'] > rfm['M-SCORE'].mean()) * 1 rfm['人群數值'] = (rfm['R是否大於均值'] * 100) + (rfm['F是否大於均值'] * 10) + (rfm['M是否大於均值'] * 1) rfm['人群類型'] = rfm['人群數值'].apply(transform_label) count = rfm['人群類型'].value_counts().reset_index() count.columns = ['客戶類型','人數'] count['人數占比'] = count['人數'] / count['人數'].sum() rfm['購買總金額'] = rfm['F'] * rfm['M'] mon = rfm.groupby('人群類型')['購買總金額'].sum().reset_index() mon.columns = ['客戶類型','消費金額'] mon['金額占比'] = mon['消費金額'] / mon['消費金額'].sum() result = pd.merge(count,mon,left_on = '客戶類型',right_on = '客戶類型') return result #判斷R/F/M是否大於均值 def transform_label(x): if x == 111: label = '重要價值客戶' elif x == 110: label = '消費潛力客戶' elif x == 101: label = '頻次深耕客戶' elif x == 100: label = '新客戶' elif x == 11: label = '重要價值流失預警客戶' elif x == 10: label = '一般客戶' elif x == 1: label = '高消費喚回客戶' elif x == 0: label = '流失客戶' return label res = get_rfm(name = 'PYTHON-RFM實戰數據.xlsx') res
# 打印結果
剔除退款后還剩:889372行 客戶類型 人數 人數占比 消費金額 金額占比 0 高消費喚回客戶 7338 0.288670 42819846.0 0.381092 1 流失客戶 6680 0.262785 14227744.0 0.126625 2 頻次深耕客戶 5427 0.213493 31420996.0 0.279644 3 新客戶 4224 0.166168 8667808.0 0.077143 4 重要價值客戶 756 0.029740 8615698.0 0.076679 5 消費潛力客戶 450 0.017703 2050506.0 0.018249 6 重要價值流失預警客戶 360 0.014162 3732550.0 0.033219 7 一般客戶 185 0.007278 825696.0 0.007349
by:一只阿木木