
一、案例背景
在產品迭代過程中,通常需要根據用戶的屬性進行歸類,也就是通過分析數據,對用戶進行歸類,以便於在推送及轉化過程中獲得更大的收益。
本案例是基於某互聯網公司的實際用戶購票數據為研究對象,對用戶購票的時間,購買的金額進行了采集,每個用戶用手機號來區別唯一性。數據分析人員根據用戶購買的時間和金額,通過建立RFM模型,來計算出用戶最近最近一次購買的打分,用戶購買頻率的打分,用戶購買金額的打分,然后根據三個分數進行一個加權打分,和綜合打分。業務人員可以根據用戶的打分情況,對不同的用戶進行個性化營銷和精准營銷,例如給不同的用戶推送定制的營銷短信,不同優惠額度的打折券等等。
通過RFM方法,可以根據用戶的屬性數據分析,對用戶進行了歸類。在推送、轉化等很多過程中,可以更加精准化,不至於出現用戶反感的情景,更重要的是,對產品轉化等商業價值也有很大的幫助。
二、RFM概念
RFM模型是衡量客戶價值和客戶創利能力的重要工具和手段。在眾多的客戶關系管理(CRM)的分析模式中,RFM模型是被廣泛提到的。該機械模型通過一個客戶的近期購買行為、購買的總體頻率以及花了多少錢3項指標來描述該客戶的價值狀況。
RFM分析 就是根據客戶活躍程度和交易金額的貢獻,進行客戶價值細分的一種方法。其中:
R(Recency):客戶最近一次交易時間的間隔。R值越大,表示客戶交易發生的日期越久,反之則表示客戶交易發生的日期越近。
F(Frequency):客戶在最近一段時間內交易的次數。F值越大,表示客戶交易越頻繁,反之則表示客戶交易不夠活躍。
M(Monetary):客戶在最近一段時間內交易的金額。M值越大,表示客戶價值越高,反之則表示客戶價值越低。

R打分:基於最近一次交易日期計算的得分,距離當前日期越近,得分越高。例如5分制。
F打分:基於交易頻率計算的得分,交易頻率越高,得分越高。如5分制。
M打分:基於交易金額計算的得分,交易金額越高,得分越高。如5分制。
RFM總分值:RFM=Rx100+Fx10+Mx1
RFM分析的主要作用:
-
識別優質客戶。可以指定個性化的溝通和營銷服務,為更多的營銷決策提供有力支持。
-
能夠衡量客戶價值和客戶利潤創收能力。
三、代碼實現
3.1、引包
首先我們引入需要用的包,數據分析常用的numpy包,pandas包,等。
import time import numpy as np import pandas as pd import mysql.connector
3.2、讀取數據
接下來我們開始用pd.read_csv方法讀取用戶的數據
print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+':讀取數據...')
config = {
'host' : '127.0.0.1',
'user' : 'root',
'password' : 'test123',
'port' : 3306,
'database' : 'user',
'charset' : 'gb2312'
}
cnn = mysql.connector.connect(**config) # 建立MySQL連接
cursor = cnn.cursor() # 獲得游標
sql = "SELECT phoneNo AS PHONENO,create_date AS ORDERDATE,order_no AS ORDERNO,ROUND(pay_amount/100,2) AS PAYAMOUNT " \
"FROM user.`event_record_order`" # SQL語句
raw_data = pd.read_sql(sql,cnn,index_col='PHONENO')
cursor.close() # 關閉游標
cnn.close() # 關閉連接
print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+':讀取數據完畢!')
print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+':開始建立RFM模型...')
介紹一下config 里的參數信息:host是數據庫的ip信息,本案例用的是本地數據庫,實際部署生產服務器時,改成生產的ip地址即可。user 是數據庫的用戶名,password是密碼,port是數據庫的端口號,database是連接的數據庫名 (schema),charset是字符集編碼。
購票時間(ORDERDATE),訂單號(ORDERID)是object類型,訂單金額(AMOUNTINFO)是浮點類型。index_col指定了數據中用戶的唯一性用 USERID來表示。
time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())打印了當前的系統時間,用來記錄日志信息。
3.3、數據審查
print('Data Overview :')
print(raw_data.head(4)) #打印原始數據前4條
print('-' * 30)
print('Data DESC:')
print(raw_data.describe()) #打印原始數據基本描述性信息
我們用raw_data.head(n)來指定取出數據的前幾條,'-'*30是用來輸出打印分隔線,下文再出現時不再重復解釋,用raw_data.describe()來獲得數據的基本描述性信息。輸出結果:
Data Overview: ORDERDATE ORDERNO PAYAMOUNT PHONENO 135****0930 2019-10-02 13:37:36 01201910021336227979 7.0 183****1153 2019-09-30 06:22:29 0120190930062149F9AF 4.5 150****6073 2019-10-30 18:21:45 01201910301821065CFD 2.0 173****7295 2019-10-21 15:13:23 01201910211512498153 7.0 ------------------------------ Data DESC: PAYAMOUNT count 96323.000000 mean 4.212409 std 3.049499 min 0.000000 25% 2.600000 50% 3.600000 75% 5.000000 max 80.000000
我們看到結果中的 count表示總共的記錄條數,mean表示了均值,std表示標准差,min表示最小值,25%表示下四分位,也叫第一四分位,50%表示中位值,也叫第二四分位,75%表示上四分位,也叫第三四分位。
na_cols = raw_data.isnull().any(axis=0) #查看每一列是否具有缺失值
print('NA Cols:')
print(na_cols)
print('-' * 30)
na_lines = raw_data.isnull().any(axis=1) #查看每一行是否具有缺失值
print('NA Records:')
print('Total number of NA lines is :{0}'.format(na_lines.sum())) #查看具有缺失值的行總記錄數
print(raw_data[na_lines]) #只查看具有缺失值的行信息
我們用raw_data.isnull()來判斷是否有缺失值,其中參數axis=0表示的是列,axis=1表示的是行,用:{0}'.format()的方式在字符串中傳入參數。輸出結果:
NA Cols: ORDERDATE False ORDERNO False PAYAMOUNT False dtype: bool ------------------------------ NA Records: Total number of NA lines is :0 Empty DataFrame Columns: [ORDERDATE, ORDERNO, PAYAMOUNT] Index: []
通過結果可以看到,實際的交易用戶數據還是比較完整的,沒有缺失數據的情況,可能這批數據被技術人員采集過來已經處理過了,不討論了。如果數據有缺失的情況怎么辦?那就要對缺失的數據進行一個預處理。
3.4、數據預處理
數據預處理,包括數據異常,格式轉換,單位轉化(如果有單位不統一的情況)等。
我們先來看異常值處理:
sales_data = raw_data.dropna() #丟棄帶有缺失值的行記錄
sales_data = sales_data[sales_data['PAYAMOUNT'] > 1]
這里,我用代碼去除了小於1元的訂單,正常出行連1塊錢都不用,那應該是測試數據了,現在誰出門做個公交還不得1元起步。對於用戶有缺失值的記錄進行了丟棄,當然也可以用其他的方法,例如平均值補全法。
然后看日期格式轉換:
sales_data['ORDERDATE'] = pd.to_datetime(sales_data['ORDERDATE'])
print('Raw Dtype:')
print(sales_data.dtypes)
用pd.to_datetime()方法對用戶的訂單日期進行了格式化轉換。輸出結果:
Raw Dtype: ORDERDATE datetime64[ns] ORDERNO object PAYAMOUNT float64 dtype: object
最后看數據轉換:
recency_value = sales_data['ORDERDATE'].groupby(sales_data.index).max() #計算原始最近一次購買時間
frequency_value = sales_data['ORDERDATE'].groupby(sales_data.index).count() #計算原始訂單數
monetray_value = sales_data['PAYAMOUNT'].groupby(sales_data.index).sum() #計算原始訂單總金額
這里根據訂單日期的聚合運算得到了用戶的最近一次購買時間,用戶總的購買數,和購買金額,max()得到了購買時間,count()得到了購買數量,sum()得到了購買金額。
3.5、計算RFM得分
得到了最近的購買時間,購買數,和購買金額,下面就可以開始計算RFM得分了。
deadline_date = pd.datetime(2019,11,15)
r_interval = (deadline_date - recency_value).dt.days
r_score = pd.cut(r_interval,5,labels=[5,4,3,2,1])
f_score = pd.cut(frequency_value,5,labels=[1,2,3,4,5])
m_score = pd.cut(monetray_value,5,labels=[1,2,3,4,5])
我們又把客戶分成五等分,這個五等分分析相當於是一個“忠誠度的階梯”(loyalty ladder),如購買一次的客戶為新客戶,購買兩次的客戶為潛力客戶,購買三次的客戶為老客戶,購買四次的客戶為成熟客戶,購買五次及以上則為忠實客戶。其訣竅在於讓消費者一直順着階梯往上爬,把銷售想象成是要將兩次購買的顧客往上推成三次購買的顧客,把一次購買者變成兩次的。
我們用deadline_date來表示分析的截止日期,那么統計用戶的時間范圍就是從數據中最早開始的購買時間到deadline_date。
用pandas.series.dt.days可以對操作后的datatime直接進行取數。pandas.cut用來把一組數據分割成離散的區間。
簡單介紹一下pandas.cut的用法:
pandas.cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_lowest=False, duplicates='raise')
- x:被切分的類數組(array-like)數據,必須是1維的(不能用DataFrame);
- bins:bins是被切割后的區間(或者叫“桶”、“箱”、“面元”),有3中形式:一個int型的標量、標量序列(數組)或者pandas.IntervalIndex 。
- 一個int型的標量,當bins為一個int型的標量時,代表將x平分成bins份。x的范圍在每側擴展0.1%,以包括x的最大值和最小值。
- 標量序列,標量序列定義了被分割后每一個bin的區間邊緣,此時x沒有擴展。
- pandas.IntervalIndex,定義要使用的精確區間。
- right:bool型參數,默認為True,表示是否包含區間右部。比如如果bins=[1,2,3],right=True,則區間為(1,2],(2,3];right=False,則區間為(1,2),(2,3)。
- labels:給分割后的bins打標簽,比如把年齡x分割成年齡段bins后,可以給年齡段打上諸如青年、中年的標簽。labels的長度必須和划分后的區間長度相等,比如bins=[1,2,3],划分后有2個區間(1,2],(2,3],則labels的長度必須為2。如果指定labels=False,則返回x中的數據在第幾個bin中(從0開始)。
- retbins:bool型的參數,表示是否將分割后的bins返回,當bins為一個int型的標量時比較有用,這樣可以得到划分后的區間,默認為False。
- precision:保留區間小數點的位數,默認為3.
- include_lowest:bool型的參數,表示區間的左邊是開還是閉的,默認為false,也就是不包含區間左部(閉)。
- duplicates:是否允許重復區間。有兩種選擇:raise:不允許,drop:允許。
重點理解我標粗的幾個參數,其他參數有需要用到時查閱。
RFM數據合並
rfm_list = [r_score,f_score,m_score] #將r、f、m三個維度組成列表 rfm_cols = ['r_score','f_score','m_score'] #設置r、f、m 三個維度列名 rfm_pd = pd.DataFrame(np.array(rfm_list).transpose(),dtype=np.int32,columns=rfm_cols,index=frequency_value.index) #建立r、f、m數據框
我們把RFM的數據進行了合並,首先是將r、f、m三個維度組成一個列表,然后取了三個列名,把數據,列名組裝成一個數據框DataFrame.
print('RFM Score Overview:')
print(rfm_pd.head(4))
輸出結果:
RFM Score Overview:
r_score f_score m_score
PHONENO
13001055088 4 1 1
13001061903 4 1 1
13001066446 5 1 1
13001123218 4 1 1
rfm_pd['rfm_wscore'] = rfm_pd['r_score'] * 0.6 + rfm_pd['f_score'] * 0.3 + rfm_pd['m_score'] * 0.1 rfm_pd_tmp = rfm_pd.copy() rfm_pd_tmp['r_score'] = rfm_pd_tmp['r_score'].astype('str') rfm_pd_tmp['f_score'] = rfm_pd_tmp['f_score'].astype('str') rfm_pd_tmp['m_score'] = rfm_pd_tmp['m_score'].astype('str') rfm_pd['rfm_comb'] = rfm_pd_tmp['r_score'].str.cat(rfm_pd_tmp['f_score']).str.cat(rfm_pd_tmp['m_score'])
理論上,上一次消費時間越近的顧客應該是比較好的顧客,對提供即時的商品或是服務也最有可能會有反應。營銷人員若想業績有所成長,只能靠偷取競爭對手的市場占有率,而如果要密切地注意消費者的購買行為,那么最近的一次消費就是營銷人員第一個要利用的工具。歷史顯示,如果我們能讓消費者購買,他們就會持續購買。這也就是為什么,0至3個月的顧客收到營銷人員的溝通信息多於3至6個月的顧客。
這里,對RFM進行了加權打分,R占60%,F占30%,M占10%,當然也可以根據業務的實際情況進行相應的權重調整。綜合打分是根據RFM=R100+F10+M*1。
3.6、保存結果
print('Final RFM Score Overview:')
print(rfm_pd.head(4))
print('-'*30)
print('Final RFM Score DESC:')
print(rfm_pd.describe())
rfm_pd.to_csv('sales_rfm_score.csv')
輸出結果:
Final RFM Score Overview: r_score f_score m_score rfm_wscore rfm_comb PHONENO 13001055088 4 1 1 2.8 411 13001061903 4 1 1 2.8 411 13001066446 5 1 1 3.4 511 13001123218 4 1 1 2.8 411 ------------------------------ Final RFM Score DESC: r_score f_score m_score rfm_wscore count 53064.000000 53064.000000 53064.000000 53064.000000 mean 3.732172 1.006407 1.002148 2.641441 std 0.944452 0.113022 0.055212 0.570417 min 1.000000 1.000000 1.000000 1.000000 25% 3.000000 1.000000 1.000000 2.200000 50% 4.000000 1.000000 1.000000 2.800000 75% 5.000000 1.000000 1.000000 3.400000
3.7、寫入數據庫
建立數據庫連接
table_name = 'sale_rfm_score' #數據框基本信息 config = { 'host' : '172.0.0.1', 'user' : 'root', 'password' : 'test123', 'port' : 3306, 'database' : 'skpda', 'charset' : 'gb2312' } con = mysql.connector.connect(**config) cursor = con.cursor() cursor.execute("show tables") # table_object = cursor.fetchall() # 通過fetchall方法獲得所有數據 table_list = [] # 創建庫列表 for t in table_object: # 循環讀出所有庫 table_list.append(t[0]) # 每個每個庫追加到列表 if not table_name in table_list: # 如果目標表沒有創建 cursor.execute(''' CREATE TABLE %s ( phone_no VARCHAR(20), r_score int(2), f_score int(2), m_score int(2), rfm_wscore DECIMAL(10,2), rfm_comb VARCHAR(10), create_date VARCHAR(20) )ENGINE=InnoDB DEFAULT CHARSET=gb2312 ''' % table_name) # 創建新表 print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+ ':開始清除 table {0}的歷史數據...'.format(table_name)) # 輸出開始清歷史數據的提示信息 delete_sql = 'truncate table {0}'.format(table_name) cursor.execute(delete_sql) print(time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+ ':清除 table {0}的歷史數據完畢!'.format(table_name)) # 輸出清除歷史數據完畢的提示信息
連接的參數不再介紹,上文已經介紹過。通過fetchall方法獲得所有數據,讀出所有的表,如果沒有表則創建。用cursor.execute先執行truncate語句,把表中的信息先清除,然后重新寫入數據。
將數據寫入數據庫
phone_no = rfm_pd.index # 索引列
rfm_wscore = rfm_pd['rfm_wscore'] #RFM 加權得分列
rfm_comb = rfm_pd['rfm_comb'] #RFM組合得分列
timestamp = time.strftime('%Y-%m-%d',time.localtime(time.time())) # 寫庫日期
print('開始寫入數據庫表 {0}'.format(table_name)) # 輸出開始寫庫的提示信息
for i in range(rfm_pd.shape[0]):
insert_sql = "INSERT INTO `%s` VALUES ('%s',%s,%s,%s,%s,%s,'%s')" % \
(table_name, phone_no[i], r_score.iloc[i], f_score.iloc[i], m_score.iloc[i], rfm_wscore.iloc[i],
rfm_comb.iloc[i], timestamp) # 寫庫SQL依據
cursor.execute(insert_sql)
con.commit()
cursor.close()
con.close()
print('寫入數據庫結束,總記錄條數為: %d' %(i+1))
先從數據集合 rfm_pd (rfm_pd 是一個DataFrame)中獲取到rfm的每個字段, ’....{0}'.format(table_name)表示的是在字符串中拼接參數,{0}代表一個字符串占位符。
四、案例結果分析
根據RFM模型的建立,我們在數據庫里生成了數據。

然后前段工程師根據數據庫里的數據得到了用戶RFM的價值打分頁面,如圖(后台展示頁面)。
運營人員根據頁面的打分情況來衡量客戶價值和客戶創利能力,了解客戶差異。將客戶分別按照R、F、M參數分組后,假設某個客戶同時屬於R5、F4、M3三個組,則可以得到該客戶的RFM代碼543。同理,我們可以推測,有一些客戶剛剛成功交易、且交易頻率高、總采購金額大,其RFM代碼是555,還有一些客戶的RFM代碼是554、545……每一個RFM代碼都對應着一小組客戶,開展市場營銷活動的時候可以從中挑選出若干組進行。

用戶是根據RFM的打分倒序排列,可以直接找到重點客戶的信息,點開手機號,查看客戶的詳細信息(這一步由前端開發人員實現),針對重點客戶展開各種個性化營銷。

RFM三個指標每個維度再細分出5份,這樣就能夠細分出5x5x5=125類用戶,再根據每類用戶精准營銷……顯然125類用戶已超出普通人腦的計算范疇了,更別說針對125類用戶量體定制營銷策略。實際運用上,我們只需要把每個維度做一次兩分即可,這樣在3個維度上我們依然得到了8組用戶。
這樣,就可以得到以下解讀(編號次序RFM,1代表高,0代表低)
重要價值客戶(111):最近消費時間近、消費頻次和消費金額都很高,必須是VIP啊!
重要保持客戶(011):最近消費時間較遠,但消費頻次和金額都很高,說明這是個一段時間沒來的忠誠客戶,我們需要主動和他保持聯系。
重要發展客戶(101):最近消費時間較近、消費金額高,但頻次不高,忠誠度不高,很有潛力的用戶,必須重點發展。
重要挽留客戶(001):最近消費時間較遠、消費頻次不高,但消費金額高的用戶,可能是將要流失或者已經要流失的用戶,應當基於挽留措施。
案例結論:
- 表現處於一般水平以上的用戶的比例太小,低於1%(R、F、M三個維度得分均在3以上的用戶數),VIP客戶太少。
- 會員中99%以上的客戶消費狀態都不容樂觀,主要體現在消費頻率低R、消費總金額低M。這可能跟公司的地鐵出行的業務有關系,公司的業務分布在全國中小城市,大部分用戶都是使用一次的用戶。
- 低價值客戶有262個,占總比例的 0.4%,運營人員可以導出下載這批用戶。