社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Python数据分析与数据化运营:会员数据化运营6-案例:基于RFM的用户价值度分析

Python爱好者社区 • 7 年前 • 1360 次点击  

作者:宋天龙(TonySong),资深大数据技术专家,历任软通动力集团大数据研究院数据总监、Webtrekk(德国最大的网站数据分析服务提供商)中国区技术和咨询负责人、国美在线大数据中心经理。


本文来自《Python数据分析与数据化运营》配套书籍第5章节内容,机械工业出版社华章授权发布,未经允许,禁止转载!此书包含 50个数据工作流知识点,14个数据分析和挖掘主题,8个综合性运营案例。涵盖了会员、商品、流量、内容4大数据化运营主题,360°把脉运营问题并贴合数据场景落地。


课程学习链接:网站数据分析场景和方法——效果预测、结论定义、数据探究和业务执行https://edu.hellobi.com/course/221


前文传送门:

Python数据分析与数据化运营:会员数据化运营1-概述与关键指标 

Python数据分析与数据化运营:会员数据化运营2-应用场景与分析模型

  Python数据分析与数据化运营:会员数据化运营3-分析小技巧

Python数据分析与数据化运营:会员数据化运营4-“大实话”

Python数据分析与数据化运营:会员数据化运营5-案例:基于AdaBoost的营销响应预测


5.7.1 案例背景

用户价值细分是了解用户价值度的重要途径,而销售型公司中对于订单交易尤为关注,因此基于订单交易的价值度模型将更适合运营需求。

对于用户价值度模型而言,由于用户的状态是动态变化的,因此一般需要定期更新,业务方的主要需求是至少每周更新一次。由于要兼顾历史状态变化,因此在每次更新时都需要保存历史数据,不同时间点下的数据将通过日期区分。

每次模型结果的数据,一部分是要给运营直接做分析,一部分是要回吐到数据库中,作为其他数据建模的基本数据维度,因此数据的输出需要有本地文件和写数据库两种方式。

本节案例的输入源数据sales.csv和源代码chapter5_code1.py位于“附件-chapter5”中,默认工作目录为“附件-chapter5”(如果不是,请cd切换到该目录下,否则会报“IOError: File sales.csv does not exist”)。程序输出RFM得分数据写入本地文件sales_rfm_score.csvMySQL数据库的目标数据表(sales_rfm_score)。

5.7.2 案例主要应用技术

本案例没有使用现有成熟模型包,而是通过Python代码手动实现RFM模型,主要用到的库包括time、numpy、pandas和mysql.connector。有关RFM模型更多内容,请查看“5.4.3 会员价值度模型

5.7.3 案例数据

案例数据是某企业2016年的部分抽样数据,数据来源于销售系统,主要是用户订单记录。以下是数据概况:

  • 特征变量数:4

  • 数据记录数:86135

  • 是否有NA值:有

  • 是否有异常值:有

以下是本数据集的4个特征变量,包括:

  • USERID:用户ID,每个用户的ID唯一,由纯数字组成。

  • ORDERDATE:订单日期,格式为YYYY-MM-DD,例如2016-01-01

  • ORDERID:订单ID,每个订单的ID唯一,由纯数字组成。

  • AMOUNTINFO:订单金额,浮点型数据。


5.7.4 案例过程

步骤1 导入用到的库

import time  # 导入时间库
import numpy as np  # 导入numpy库
import pandas as pd  # 导入pandas库
import mysql.connector  # 导入mysql连接库

该案例用到了四个库:time、numpy、pandas、mysql.connector。

  • time:用来记录插入数据库时的当前时间

  • numpy:用来做基本数据处理等

  • pandas:有关日期转换、数据格式化处理、主要RFM计算过程等

  • mysql.connector:数据库连接工具,读写MySQL数据库

步骤2 读取原始数据

dtypes = {'ORDERDATE': object, 'ORDERID': object, 'AMOUNTINFO': np.float32}  # 设置每列数据类型
raw_data = pd.read_csv('sales.csv', dtype=dtypes, index_col='USERID')  # 读取数据文件

dtypes定义的字典用于使用pd.read_csv读取数据时对数据框数据类型的自定义,而非系统默认类型。本案例中将ORDERDATE(订单时间)和ORDERID(订单ID)都设置为object(字符串对象),AMOUNTINFO(订单金额)设置为浮点型。

使用pd.read_csv读取数据文件过程中,默认的csv以逗号作为分隔符,因此无需指定分隔符;设置参数dtype为上述自定义的类型字典,同时指定列USERID(用户ID)为索引列,代替默认的数值索引。

步骤3数据审查和校验,主要包括数据概览、缺失值审查等。

# 数据概览
print ('Data Overview:')
print (raw_data.head(4))  # 打印原始数据前4条
print ('-' * 30)
print ('Data DESC:')
print (raw_data.describe())  # 打印原始数据基本描述性信息
print ('-' * 60)

在数据概览部分,通过数据框的head方法输出前4条数据,通过数据框的 describe方法输出其基本描述性统计信息。返回结果如下:

Data Overview:
         ORDERDATE     ORDERID  AMOUNTINFO
USERID                                    
142074  2016-01-01  4196439032      9399.0
56927   2016-01-01  4198324983      8799.0
87058   2016-01-01  4191287379      6899.0
136104  2016-01-01  4198508313      5999.0
------------------------------
Data DESC:
         AMOUNTINFO
count  86127.000000
mean     744.705261
std     1425.211182
min        0.500000
25%       13.000000
50%       59.000000
75%      629.000000
max    30999.000000
------------------------------------------------------------

如何通过上述结果得到有效信息?

Data Overview部分,主要查看不同数据列的数据格式,尤其是有特定转换操作之后是否符合源数据文件的格式或得到目标转换要求;另外,数据的长度、组成规律、类型等是否与真实数据一致。

Data DESC部分主要分析数值型字段(本案例中是AMOUNTINFO)的数值分布规律,包括记录数、极值、标准差、分位数结果等,可用于数据集的适用模型、极值的处理等后续计算的辅助判断依据。

在本案例中,发现数据的极值相差非常大,并且标准差也很大,说明数据波动非常明显。另外,最大值和最小值似乎有些奇怪:最大值竟然有超过30000元、最小值却只有0.5元,这两种状态都非常异常。经过与业务方沟通后确认,最大值的订单金额有效,为某客户一次性购买多个大家电商品;而订单金额为0.5元的订单都属于促销优惠券生成的订单,这些订单用来为用户消费时提供优惠券,没有实际意义,因此这些数据需要去掉。除了这些0.5元的订单,所有低于1元的订单均有这个问题。

# 缺失值审查
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 Recors:')
print ('Total number of NA lines is: {0}'.format(na_lines.sum()))  # 查看具有缺失值的行总记录数
print (raw_data[na_lines])  # 只查看具有缺失值的行信息
print ('-' * 60)

缺失值对于后续的计算会产生重大影响,因此这里需要确认数据中是否含有缺失数据。先通过raw_data.isnull().any(axis=0)来判断所有列是否含有缺失信息,其中的isnull用来查看是否有缺失值,any用来判断数据记录中的任何一个位置出现缺失值都会计算在内,参数axis=0以列为基础做查看。打印结果如下:

NA Cols:
ORDERDATE      True
ORDERID       False
AMOUNTINFO     True

返回结果显示了ODERDATEAMOUNTINFO都有缺失。下一步结合每一行看下到底有多少条数据出现缺失。这次使用的方法跟判断列缺失一致,仅仅是axis设为1用来表示按行查看。在na_lines中,结果由TrueFalse组成,因此可以直接做数值计算,这里使用sum函数统计一共有多少行为含有NA,含有NA的总记录数和详细行记录打印结果如下:

NA Recors:
Total number of NA lines is: 10
         ORDERDATE     ORDERID  AMOUNTINFO
USERID                                    
75849   2016-01-01  4197103430         NaN
103714         NaN  4136159682       189.0
155209  2016-01-01  4177940815         NaN
139877         NaN  4111956196         6.3
54599   2016-01-01  4119525205         NaN
65456   2016-01-02  4195643356         NaN
122134  2016-09-21  3826649773         NaN
116995  2016-10-24  3981569421         NaN
98888   2016-12-06  3814398698         NaN
145951  2016-12-29  4139830098         NaN

由返回结果可知,一共有10条数据含有NA值,这些数据在整体样本集中占比非常小,因此这里可以直接删除。

步骤4 数据预处理准备工作,包括数据异常、格式转换和处理




    
# 异常值处理
sales_data = raw_data.dropna()  # 丢弃带有缺失值的行记录
sales_data = sales_data[sales_data['AMOUNTINFO'] > 1]  # 丢弃订单金额<=1的记录

对于异常值的处理,直接使用数据框的dropna方法删除;对于订单金额<=1的数据,我们也直接丢弃,只选择订单金额>1的数据记录。、

# 日期格式转换
sales_data['ORDERDATE'] = pd.to_datetime(sales_data['ORDERDATE'], format='%Y-%m-%d')  # 将字符串转换为日期格式
print ('Raw Dtypes:')
print (sales_data.dtypes)  # 打印输出数据框所有列的数据类型
print ('-' * 60)

日期转换的目的是实现基于时间间隔的计算,这样才能算出R距离指定日期的天数。这一操作没有在读取数据转换(在读取时的具体转换方法请参考“4.6.4 代码实操:Python时间序列分析部分),这里使用pd.to_datetime方法将ORDERDATE列转换为pandas的日期类型(pd.datetime类型),format参数以原始数据字符串的格式来写,只有格式对应上才能实现解析。解析完成后,使用数据框的dtypes打印输出Dtypes类型如下:

Raw Dtypes:
ORDERDATE     datetime64[ns]
ORDERID               object
AMOUNTINFO           float32
dtype: object

接下来需要分别计算R、F、M三个原始变量的数值,主要使用的方式是数据框的groupby方法。

# 数据转换
recency_value = sales_data['ORDERDATE'].groupby(sales_data.index).max()  # 计算原始最近一次订单时间
frequency_value = sales_data['ORDERDATE'].groupby(sales_data.index).count()  # 计算原始订单频率
monetary_value = sales_data['AMOUNTINFO'].groupby(sales_data.index).sum()  # 计算原始订单总金额

这三行代码都是以原始数据框的索引为主键(以用户ID作为汇总维度)分别对ORDERDATE求最大值、对ORDERDATE做计数统计、对AMOUNTINFO求和,得分R、F、M三个指标的原始值。

步骤5 计算RFM得分

# 分别计算R、F、M得分
deadline_date = pd.datetime(2017, 01, 01)  # 指定一个时间节点,用于计算其他时间与该时间的距离
r_interval = (deadline_date - recency_value).dt.days  # 计算R间隔
r_score = pd.cut(r_interval, 5, labels=[5, 4, 3, 2, 1])  # 计算R得分
f_score = pd.cut(frequency_value, 5, labels=[1, 2, 3, 4, 5])  # 计算F得分
m_score = pd.cut(monetary_value, 5, labels=[1, 2, 3, 4, 5])  # 计算M得分分

首先指定一个时间节点,用于计算其他时间与该时间的距离,这是计算R的基础。这里定义了2017-01-01,通过数据框相减得到时间间隔天数对象,并对该对象使用dt.days方法获得天的数值。

下面对得到的R、F、M三个变量值使用分位数法做区间划分,这里使用了pd.cut方法,默认设置为5份,同时通过labels标签指定区间标志。主要注意的是对R(最近购买时间)而言,数值越大意味着离指定日期越远,因此其区间划分后的值应该越小,所以该标签列表顺序与其他两个相反。

提示 dt是pandas中Series时间序列datetime类属性的访问对象,除了代码中用到的days(天)以外,还包括:datedayofweekdayofyeardays_in_month freqhourmicrosecondminutemonthquartersecondtime、tz、weekweekdayweekday_nameweekofyearyear等。这些是提取Series时间数据的常用方法。

在得到RFM各自得分后,将三维维度数据合并为一个数据框,便于在一起做展示以及后续的RFM总得分计算。

# R、F、M数据合并
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数据框
print ('RFM Score Overview:')
print (rfm_pd.head(4))
print ('-' * 60)

先建立RFM三个维度的值列表和名称列表,用于生成数据框时指定数据和标签。然后使用pd.DataFrame建立数据框。使用np.array将R、F、M生成的值列表转换为矩阵,此时的矩阵形状是(3, 59676),不符合我们需要的三列的需求,因此使用transpose方法对矩阵做转置处理,该方法也可以简写为T(注意大小写),即np.array(rfm_list).transpose()等价于np.array(rfm_list).T;由于R、F、M的值域是[1,5]的整数,因此创建数据框时指定数据类型为整数型,通过dtype=np.int32实现,该方法也可以写成dtype='int32',二者是等价的;然后设置列名并指定索引列为用户ID,由于R、F、M三个Series的索引都相同,因此这里随便指定为frequency_value.index。最后打印前4条结果如下:

RFM Score Overview:
        r_score  f_score  m_score
USERID                           
51220         4        1        1
51221         2        1        1
51224         3        1        1
51225         4        1        1
-----------------------------------------------------------

完成R、F、M数据框创建后,可以基于该数据框计算RFM总得分,这里使用两种方法:基于三个维度加权的RFM总得分以及基于组成的总得分。

# 计算RFM总得分
# 方法一:加权得分
rfm_pd['rfm_wscore'] = rfm_pd['r_score'] * 0.6 + rfm_pd['f_score'] * 0.3 + rfm_pd['m_score'] * 0.1
# 方法二:RFM组合
rfm_pd_tmp = rfm_pd.copy()
rfm_pd_tmp['r_score'] = rfm_pd_tmp['r_score'].astype('string')
rfm_pd_tmp['f_score'] = rfm_pd_tmp['f_score'].astype('string')
rfm_pd_tmp['m_score'] = rfm_pd_tmp['m_score'].astype('string')
rfm_pd['rfm_comb'] = rfm_pd_tmp['r_score'].str.cat(rfm_pd_tmp['f_score']).str.cat(rfm_pd_tmp['m_score'])

在方法一中,直接取出数据框的三列(R、F、M)通过乘以特定权重值得到总得分。由于业务方更关注活跃度,认为访问的邻近度最重要,因此R的权重比较高,设置为0.6;其次是访问频率F设置为0.3;订单金额M则设置为0.1。然后基于不同的列直接做加权相加,而无需通过循环读出各个元素再做计算。

提示 在设定权重值时需要与业务部门沟通,主要看业务关注哪个方面,再设置对应维度的值。而对于权重值的分配一般情况下总权重的和为1,这样划分更清晰并便于解释和管理。

在方法二中,我们要将R、F、M三个值组合,需要用到字符串的组合。由于原始数据框中为了做加权计算而设置为数值型,因此这里需要转换为字符串型。为了不影响原始数据,通过copy方法得到一份副本,然后将副本的三列使用astype方法将数值型转换为字符串型。

提示 设置数据类型时一般有两种思路:一种是在创建数据框时通过dtype指定(原始数据框就是这种方法),第二种是在创建好的数据框中使用astype方法将特定栏位转换为目标类型。

在最后的合并过程中,使用了pandas的字符串处理库str中的cat方法做字符串合并,该方法可以将右侧的数据合并到左侧,在连续使用两个str.cat方法后得到总的R、F、M字符串组合。

相关知识点:使用str.cat方法将字符串合并

关于字符串的合并,之前在不同章节中主要用到了四种方法:

  • 使用+(加号)组合字符串:例如输入X = 'a'+'b'能得到X的值是'ab'

  • 使用%占位符做字符串组合,主要用在变量输出上,例如输入X = 'I am %s'%'Tony',能得到X的值是'I am Tony'

  • 使用.join方法将多个可迭代的对象合并,例如输入X = ' '.join(['I','am','tony']),得到X的值是'I am tony'

  • 使用.format做占位符将多个字符串合并,跟%用法类似但更强大,主要用在变量输出上,例如输入:X = 'I am {1} and {0} years old'.format(30,'Tony'),得到X的值是'I am Tony and 30 years old'

本节使用了pandas自带的字符串组合方法,该放在在str库中,它用于字符串对象合并。该方法语法如下:cat(self, others=None, sep=None, na_rep=None),参数:

  • others:要合并的另外一个对象(右侧对象),如果为空则将左侧对象组合

  • sep:合并的分隔符,默认为空,可自定义,例如','、';'等na_rep:如果遇到NA(缺失值)时如果处理,默认为忽略

  • 需要注意的是:该方法用于对Series做组合,而不能是数据框,适用于一维数据或字符串。

示例:

将左侧对象组合

输入:pd.Series(['a','b','c']).str.cat(sep=';')

输出:'a;b;c'

将左侧对象和右侧对象组合

输入:pd.Series(['a', 'b', 'c']).str.cat(['A', 'B', 'C'], sep=';')

输出:

0    a;A
1    b;B
2    c;C

步骤6 打印输出和保存结果,该步骤中我们将数据打印出来,并分别保存为本地csv数据文件以及将数据存入MySQL数据库。

经过上述计算过程,已经得到R、F、M结果,这里我们将其打印出来。




    
# 打印结果
print ('Final RFM Scores Overview:')
print (rfm_pd.head(4))  # 打印数据前4项结果
print ('-' * 30)
print ('Final RFM Scores DESC:')
print (rfm_pd.describe())

使用head方法指定打印前4条结果以及RFM得分描述性统计结果如下:

Final RFM Scores Overview:
        r_score  f_score  m_score  rfm_wscore rfm_comb
USERID                                                
51220         4        1        1         2.8      411
51221         2        1        1         1.6      211
51224         3        1        1         2.2      311
51225         4        1        1         2.8      411
------------------------------
Final RFM Scores DESC:
            r_score       f_score       m_score    rfm_wscore
count  59676.000000  59676.000000  59676.000000  59676.000000
mean       3.299970      1.013439      1.000134      2.384027
std        1.402166      0.116017      0.018307      0.845380
min        1.000000      1.000000      1.000000      1.000000
25%        2.000000      1.000000      1.000000      1.600000
50%        3.000000      1.000000      1.000000      2.200000
75%        5.000000      1.000000      1.000000      3.400000
max        5.000000      5.000000      5.000000      5.000000

该过程主要验证数据输出是否符合预期,主要是RFM的组合和加权计算;使用描述性统计简单看下数值型得分的区间分布(重点是极值)是否在预期的[1,5]之间。分析发现上述数据没问题。

注意 如果原始数据集中有NA值而没有经过处理,那么在使用分位数方法时会默认忽视NA的分位,即值为NA的数据点仍然是NA。这会导致在做后期加权得分和组合时都会遇到问题。通过描述性统计也能发现该问题是否存在。

接着将RFM得分文件保存到本地,便于业务做进一步分析应用。

  • # 保存RFM得分到本地文件

  • rfm_pd.to_csv('sales_rfm_score.csv')  # 保存数据为csv

直接使用数据框对象的to_csv方法将数据保存为csv格式的数据文件。除了保存为csv格式的文件,数据框对象还可以支持保存为excel等文件。另外也支持sql、json、stata、pickle、sparse、hdf、html等对象的输出。保存到本地csv文件的部分数据截图如下:


5-4本地csv文件结果

由于RFM结果要作为其他模型结果的输入维度,因此需要将RFM得分写入数据框。这里使用的是MySQL官方连接程序实现。

  • # 设置要写库的数据库连接信息

  • table_name = 'sales_rfm_score'  # 要写库的表名

  • # 数据库基本信息

  • config = {'host': '127.0.0.1',  # 默认127.0.0.1

  •           'user': 'root',  # 用户名

  •           'password': '123456',  # 密码

  •           'port': 3306,  # 端口,默认为3306

  •           'database': 'python_data',  # 数据库名称

  •           'charset': 'gb2312'  # 字符编码

  •           }

首先建立要写入数据库的基本信息,通过table_name = 'sales_rfm_score'定义要写入的表的名称,该变量会在后续表识别和写入时用到。config定义了一个用于写入数据库的详细配置信息定义,包括主机host、用户user、密码password、端口port、要写入的数据表所处的数据库名database、字符集charset 。这些信息在“2.2.3 从关系型数据库MySQL读取运营数据”中已经详细介绍过,在此不再赘述。

配置信息定义完成后,下面建立数据库连接。

con = mysql.connector.connect(**config)  # 建立mysql连接
cursor = con.cursor()  # 获得游标****

使用mysql.connector.connect方法连接数据库,后续的获得游标和写库都需要该有效连接;使用连接的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 (

  •     userid               VARCHAR(20),

  •     r_score               int(2),

  •     f_score              int(2),

  •     m_score              int(2),

  •     rfm_wscore              DECIMAL(10,2),

  •     rfm_comb              VARCHAR(10),

  •     insert_date              VARCHAR(20)

  •     )ENGINE=InnoDB DEFAULT CHARSET=gb2312

  •     ''' % table_name)  # 创建新表

使用游标的execute方法执行一个sql语句,由于在config中我们定义了数据库名,因此这里直接使用"show tables"来显示当前数据库下所有的表列表,而不是先使用use [数据库],然后再"show tables"

游标执行后获得的对象是没有数据的,使用fetchall方法抓取全部数据并返回,但返回的是一个数据列表对象[(u'order',), (u'test',)],列表中的每个元素都是一个元组,而数据表名处于元组的第一个值。因此接着通过一个循环将对象读取出来,得到数据表列表table_list

使用if条件语句判断要写入的数据表table_name是否在数据表列表中,如果该表不存在则新建。这里仍然使用游标的execute方法执行一段SQL语句,只是由于创建表的SQL语句较长,使用'''来表示多个段落的字符串。

相关知识点:Python的字符串标识符

Python的字符串标识符有三种,分别是'(单引号)、"(双引号)、'''(三个连续单引号)、"""(三个连续双引号)。这四种表示符号(注意都是英文状态下)在大多数场景下的作用是相同的,但是都要求成对出现。例如:'ABC'等价于"ABC",也等价于'''ABC'''

但在某些场景下他们的用途是有区分的,主要体现在两个方面:

  • 当字符串中出现子集字符串时,需要使用不同类型的字符串做区分,例如'I said:"You can go!"',这时由于在外层字符串使用了'(单引号),在内层就不能再使用相同的表示方法了,所以使用双引号。

  • 当字符串非常长(通常超过1行)、或者字符串步长但是需要分段显示时,就需要用到'''(连续三个单引号)或"""(三个连续双引号)来表示,上述代码就是这种应用。除此以外,这种长段落还经常用到字典定义、格式化段落文本输出、复杂功能的SQL语句(例如SQL嵌套、关联查询)等。

经过上述步骤,能保证要写入的数据表已经存在,并且跟要写入的数据类型和格式相匹配。接下来开始写库操作。

# 将数据写入数据库
user_id = 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 ('Begin to insert data into table {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, user_id[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)  # 执行SQL语句,execute函数里面要用双引号
    con.commit()  # 提交命令

写库时用到了7列数据,分别是useridr_scoref_scorem_scorerfm_wscorerfm_combinsert_date。其中:

  • userid可以从数据框的索引中取出,用于标识不同的用户,使用数据框的index获得索引列用户ID数据框;

  • r_scoref_scorem_score三列值在分别计算R、F、M得分时已经产生;

  • rfm_wscorerfm_comb可以分别使用数据框读取列名以获得RFM加权得分和组合得分数据;

  • insert_date用来记录每次写库时的日期,该日期可以作为变化状态的判断依据,可应用于分析不同时间和周期下用户的活跃度变化,例如5.5.3 借助动态数据流关注会员状态的轮转”中的那种应用。而在跟其他模型做数据集成应用时,可根据实际情况取需要的时间对应的状态,例如取最新状态可取insert_date中的最大值;

在获取insert_date写库日期时使用了3个时间函数:

  • time.time():获取当前系统时间下相对于Epoch时间(1970-01-01 00:00:00 UTC),以浮点型数据的形式表示,例如1495770037.572

  • time.localtime():将时间转换为本地时间,是一个包含了年、月、日、小时、分钟、秒、一周中第几天、一年中第几天等数据的元组。例如将上述时间转换结果为time.struct_time(tm_year=2017, tm_mon=5, tm_mday=26, tm_hour=11, tm_min=40, tm_sec=37, tm_wday=4, tm_yday=146, tm_isdst=0)

  • time.strftime():将不同形式的时间转换为特定格式和时间表示方法,该方法在本书中多次用到,例如将上述结果转换为'2017-05-26'

经过上述功能转换后的日期是YYYY-MM-DD的格式,例如2017-05-26。接下来的写库操作利用for循环,根据数据框的记录做读出索引值,然后使用游标的执行方法运行SQL;由于我们要做批量写入操作,因此需要使用on.commit方法提交命令。

写入操作完成后,关闭游标和数据库连接。

cursor.close()  # 关闭游标
con.close()  # 关闭数据库连接
print ('Finish inserting, total records is: %d' % (i + 1))  # 打印写库结果

得到总的写入数据库记录数如下:

Finish inserting, total records is: 59676

可使用Navicat打开对应的数据库表,查看结果是否符合预期,以及总记录数是否一致。

 

5.7.5 案例数据结论

由于在RFM划分时,将区间划分为5份,因此可以将这5份区间分别定义了:高、中、一般、差和非常差5个级别,分别对应到R、F、M中的5/4/3/2/1

基于RFM得分业务方得到这样的结论:

  • 公司的会员中99%以上的客户消费状态都不容乐观,主要体现在消费频率低R、消费总金额低M——经过分析,这里主要由于其中有一个用户(ID74270)消费金额非常高,导致做5分位时收到最大值的影响,区间向大值域区偏移。

  • 公司中有一些典型客户的整个贡献特征明显,重点是RFM得分为555的用户(ID为74270),该用户不仅影响了订单金额高,而且其频率和购买新鲜度和消费频率都非常高,应该引起会员管理部门的重点关注。

  • 本周表现处于一般水平以上的用户的比例(R、F、M三个维度得分均在3以上的用户数)相对上周环比增长了1.3%。这种良好趋势体现了活跃度的提升。

  • 本周低价值(R、F、M得分为111以上)用户名单中,新增了1221个新用户,这些新用户的列表已经被取出。


5.7.6 案例应用和部署

针对上述得到的分析结论,会员部门采取了以下措施:

  • ID74270的用户加入大客户名单,并实现重点关怀和管理

  • 上周的会员工作中有关低价值群体的用户并未减少,反而新增了一些,需要会员部门重点关注和处理。目前最主要的还是通过会员渠道拉动会员再次访问网站并订单,防止客户的流失和沉默,而订单的金额是次要因素。

录入数据库的RFM得分数据已经应用到其他数据模型中,成为建模输入的关键维度特征之一。同时,该模型已经作为定时任何每周一早晨上班前运行一次。

5.7.7 案例注意点

本案例中有以下几个点需要读者重点关注:

对于R、F、M区间的划分是一个离散化的过程,具体需要划分为几个区间需要跟业务方确认。本案例是划分为5个区间,实际上一般不会比这个区间更多,3~5是比较好的选择。

R、F、M的组合加权中关于权重的确认,不同时期的业务关注点可能不同,因此权重值可能是需要调整的;另外,权重值的打分一般使用专家打分法,即让业务来做权重打分,如果没有业务打分,那么大多数情况下三者的权重从大到小依次是:R>F>M

本案例中有一个ID74270的用户,由于其各方面特征非常显著,因此整个RFM得分的划分区间都受其影响。这种极值的影响(包括案例开始时极小值的处理)需要跟业务部门沟通确认才能进行处理,此次的极大值的出现是业务认可的有效数据,因此没有去除;如果有其他极值可能需要特别处理,否则会影响区间划分。本例中虽然影响划分,但恰好发现了该用户的价值。

虽然订单数据库中的数据质量相对较高,但可能由于采集问题、数据库同步、ETL、查询、误操作等问题还是会导致NA值的出现,NA值的处理非常重要。

R、F、M三个维度的处理包括计算、离散化、组合、转换之前都需要注意其数据类型和格式,尤其是有关时间项的转换操作提前完成。

5.7.8 案例引申思考

利用RFM模型划分用户群体并作价值度分析是统计分析非常基础且有效的方法。该模型几乎用不到任何专业的统计分析和挖掘知识,只需要具有基本的数据清洗、处理和转换技能即可完成,因此几乎是各个企业都会用到的模型。更重要的是,该模型原理简单,业务方在理解和应用起来非常容易入手,大大提高了部署落地的可能性。

对于本案例的实施,读者还可以从以下几方面做引申思考:

  • 问题 按照 R、F、M每个区间划分为5份的原则,实际上出来的总得分可能性是5*5*5=125种,为什么结果中只有14种组合得分?

  • 问题二 如果不同周期下RFM的权重会改变,其运营结果是否具有可比性和连续性?

  • 问题三 RFM模型可以作为模型分析方法,也可以作为数据预处理方法,基于不同的维度通过计算组合得分或加权得分的方式获得新的数据,这是一种数据降维的有效方法。使用组合得分方法和加权得分方法得到的两种降维方法在后续应用中有哪些不同?

笔者关于引申思考的想法

  • 问题 在案例注意点中也提到了,由于受到ID74270的用户行为影响,其极大值导致整个区间划分非常大,很多区间内没有数据。以F值为例,该用户的F原始值为14(一年中有14个订单),其他用户最高的只有6个订单,因此在做区间划分时从7-13内没有一个用户符合条件,也就不会产生该得分区间的用户统计量。

  • 问题二 权重的变化影响的仅仅是加权得分项,而不影响组合得分项,因此其他建模应用中没有任何影响。在分析过程中,这种改变不可避免的会使加权得分产生规则发生变化,需要在业务做分析时加以提醒。

  • 问题三 首先需要明确的是RFM的两种总得分应该只选择其中一种加以应用,因为二者具有高度相关性。由于加权RFM得分是一个连续型变量,更适合直接参与到后续回归建模中;组合RFM得分是一个分类变量,更适合应用到分类建模中,同时需要注意的是该分类需要做标志转换,具体过程参见“3.2 将分类数据和顺序数据转换为标志变量

作者好书推荐:《Python数据分析与数据化运营》,小编看过,觉得很不错,难得的技术+业务的好书!



点击阅读原文,立即购买宋天龙老师Python数据分析与数据化运营秘籍


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/d7ulE9j8Ii
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/12753