简介:基于RFM模型的用户画像可视化系统完整代码,面向正在学习Python数据分析、机器学习与Django Web开发的读者,旨在帮助理解以消费行为数据刻画用户价值,并搭建可交互的可视化后台。资源围绕R(最近消费时间)、F(消费频率)、M(消费金额)三个核心维度,展示从Pandas清洗数据、计算RFM指标、K-means聚类分群,到利用Django框架实现页面展示的完整链路,并包含电信、短信、App等多源用户行为数据表。压缩包共43个文件,涵盖7个Python源码、csv/tsv数据表、5个HTML页面模板、Jupyter Notebook分析脚本,以及环境安装文档与启动说明,整体大小14.72MB。已有1565人学习下载。通过实际代码,读者可直观掌握RFM用户分层与可视化的落地方法,理解数据归一化、聚类建模和Web渲染的关键步骤,并可作为课程设计或推荐系统项目的参考范例。配套的目录结构清晰,Python源码、数据、模板和文档分区存放,便于按模块阅读和二次开发,能够快速复现一个可运行的用户画像系统。
1. 基于RFM的用户画像可视化系统:这不是画几张图,是把用户分层变成可运营的决策依据
RFM这个词在用户增长和数据分析岗的JD里几乎快写烂了,但真正把RFM落地成一套可视化系统的人并不多。多数团队做到最后就是pandas里算三个字段,然后丢给BI工具画三个柱状图,老板看完说一句“哦”就结束了。这不算用户画像系统,这叫数据报表。
基于RFM的用户画像可视化系统,核心价值是把Recency(最近一次消费时间间隔)、Frequency(消费频次)、Monetary(消费金额)三个维度的计算结果,转成业务方能直接理解、直接做运营动作的标签体系和数据看板。它不是算完即止,而是要让运营打开一个页面就能回答三个问题:哪些用户该召回、哪些用户该发券、哪些用户已经处在流失边缘。这篇文章会按真实项目里从取数、清洗、计算到可视化落地的完整路径来写,每一步都给出可抄作业的代码、参数和踩坑记录。适合手里有订单表但没有完整画像系统、正打算从零搭建的读者。
2. RFM口径定义与评分标准:先把三个维度定义到不会吵架的程度
2.1 为什么多数团队的RFM计算从一开始就是错的
很多项目翻车不在技术,在于口径没定清楚就开写代码。Recency到底算最近一次消费距今多少天,还是按月份算第几个自然月没消费?Frequency是算累计订单数,还是算活跃月份数?Monetary是算实付金额还是算包含退款前的原始金额?这些差异会导致同一个用户在不同人手里算出完全不同的分层结果。
我一般建议先在项目文档里把口径锁定成一张表,而不是上来就写SQL。实际操作中,Recency统一用「当前日期减去该用户最近一次有效订单的付款时间,向下取整到天」;Frequency统一用「统计周期内该用户的有效订单数」;Monetary统一用「统计周期内该用户的有效订单实付金额之和」。这里的重点在「有效订单」的定义,通常要排除已退款、已取消、测试单这三种状态。如果业务方对有效订单有不同意见,比如部分退款算不算有效,要在这张表里先说明白,否则后面代码怎么改都会有人不满意。
2.2 RFM评分的分位数切分:五档评分与节点选择
RFM算出来是三个连续值,但画像系统里展现给运营看的应该是评分而不是原始值。常见的做法是把每个维度按分位数切成1到5分,5分最好,1分最差。这里的关键节点是分位数怎么取。
一种做法是等宽切分,比如金额最大值的五分之一作为一个档。这在数据分布均匀时没问题,但电商和本地生活订单数据基本都呈长尾分布,头部用户贡献绝大部分金额,等宽切分会让大量用户落在1分档,评分完全失去区分度。更推荐等频切分,也就是按20%、40%、60%、80%分位点切分。这样每个评分档的用户数量基本一致,后续分层才不会出现某个人群占了全体用户80%的极端情况。
如果订单表的数据量足够大,也可以直接用pd.qcut而不手工传分位点,但要注意处理重复值。同一个分位点上出现大量相同值时,pd.qcut会直接报错,需要先做去重或用rank方法规避。这个坑在电商大促场景尤其常见,满减活动会让大量用户实付金额恰好等于同一个门槛值。
2.3 用代码固化口径:从订单明细表到RFM特征表的可复现脚本
下面给一段我常用的口径固化代码。这段代码的输入是一张订单明细表,字段包含用户ID、订单状态、付款时间、实付金额,输出是每个用户一行、包含r_score、f_score、m_score三列评分数据的RFM特征表。
import pandas as pd import numpy as np # 读取订单明细表 df = pd.read_csv('orders.csv', parse_dates=['pay_time']) # 第一步:只保留有效订单,这是全项目最关键的一步 valid_status = ['paid', 'completed', 'partial_refund'] df = df[df['order_status'].isin(valid_status)].copy() # 第二步:在统计周期上取数,避免跨年数据污染 # cutoff_date 是项目设定的统计截止日期 cutoff_date = pd.Timestamp('2025-06-30') start_date = cutoff_date - pd.DateOffset(months=6) df = df[(df['pay_time'] >= start_date) & (df['pay_time'] <= cutoff_date)].copy() # 第三步:三个RFM基础特征 rfm = df.groupby('user_id').agg( recency=('pay_time', lambda x: (cutoff_date - x.max()).days), frequency=('order_id', 'count'), monetary=('pay_amount', 'sum') ).reset_index() # 第四步:五档评分,用rank(method='first')避开分位点重合问题 rfm['r_score'] = pd.qcut(rfm['recency'], 5, labels=[5, 4, 3, 2, 1]).astype(int) rfm['f_score'] = pd.qcut(rfm['frequency'].rank(method='first'), 5, labels=[1, 2, 3, 4, 5]).astype(int) rfm['m_score'] = pd.qcut(rfm['monetary'].rank(method='first'), 5, labels=[1, 2, 3, 4, 5]).astype(int) rfm.to_csv('rfm_features.csv', index=False)这段代码有几个参数要重点说明。
cutoff_date是统计截止日,直接影响recency的值,上线后如果发现所有用户的recency整体偏小,多半是截止日设成了今天而订单数据还包含未来时间,这属于脏数据问题。
第四步里r_score的分箱逻辑是反过来的,因为recency越大表示用户越久没来,所以1分的箱对应最新用户,5分的箱对应最久未产生消费的用户。这一点新手极容易搞反,建议在代码边上注释清楚。
rank(method='first')的作用是给相同分位值赋不同序号,配合qcut可以避免大量重复值导致的分箱崩溃。如果业务上要求相同金额的用户必须得到相同评分,则需要改用随机分组或按其他业务字段二次排序,但这属于产品取舍,代码层面rank(method='first')只是保证可运行。
3. 从订单表到RFM花名册:全量用户的特征计算与画像标签生成
3.1 用户完整生命周期的特征记录生成
RFM特征表算出来后,下一步不是直接画图,而是要让这张表具备「可查询、可筛选、可分层」的画像属性。这一步要解决两个问题:一是把评分组合映射成人话,二是把用户的邮箱、手机号、注册时间、会员等级等基础属性关联进来,形成完整的用户花名册。
先说评分组合映射。三位评分r_score、f_score、m_score可以组成3位数字串,共5^3=125种组合。业务上看不了125种,所以要归纳成传统RFM模型里的8类人群。比如r_score=5且f_score=5且m_score=5的组合叫「重要价值用户」,r_score=1且f_score=4且m_score=5的组合叫「重要唤回用户」。这里要注意评分组合与8类人群的对应规则没有绝对公式,不同行业有不同定义,只要在项目文档里固定下来就行。
# 读取上一步生成的RFM特征表 rfm = pd.read_csv('rfm_features.csv') # 定义8类人群的评分区间映射 # 区间写的是闭区间,业务方可直接看懂 def map_user_segment(row): r, f, m = row['r_score'], row['f_score'], row['m_score'] if f >= 4 and m >= 4 and r >= 4: return '重要价值用户' if f >= 4 and m >= 4 and 2 <= r <= 3: return '重要保持用户' if f >= 4 and m >= 4 and r <= 1: return '重要唤回用户' if f >= 4 and m <= 3 and r >= 4: return '一般价值用户' if f >= 4 and m <= 3 and 2 <= r <= 3: return '一般保持用户' if f >= 4 and m <= 3 and r <= 1: return '一般唤回用户' if f <= 3 and m >= 4: return '潜力用户' if f <= 3 and m <= 3 and r >= 4: return '新用户' return '流失用户' rfm['segment'] = rfm.apply(map_user_segment, axis=1) # 关联用户基础属性表,补齐画像信息 user_info = pd.read_csv('user_info.csv') rfm = rfm.merge(user_info, on='user_id', how='left') # 输出到数据库或CSV rfm.to_csv('rfm_user_profile.csv', index=False)这段代码里的map_user_segment函数是纯规则映射,业务方如果觉得某类人群的阈值不合理,可以直接改区间。需要注意的是f>=4这个阈值,它意味着一个用户只要下单频率在前40%就算高频,而不是非得一个月买很多次。这套规则适合低频高客单场景,比如家居、教育、二手车。如果是外卖或网约车这种高频低客单场景,「消费频次高」的阈值要重新定义,否则8类人群里一半以上都会挤进「重要价值用户」,分群就没有意义了。
3.2 画像标签体系设计:除了RFM还要有哪些标签
一个完整的用户画像可视化系统,RFM模型提供的是用户价值分层,但运营点开页面后还想知道这个用户是新是旧、在哪个城市、最近一次消费买了什么品类。这些信息不来自RFM计算,而来自基础标签。
我自己的习惯是把标签按性质分三组。第一组是静态属性标签,包括性别、年龄段、城市等级、注册渠道,直接在user_info表里取;第二组是消费行为标签,包括近30天订单数、累计消费金额、客单价、常用支付方式,从orders表里汇总;第三组是RFM价值标签,就是我们刚算到的r/f/m评分和用户分群。可视化系统展示的每张卡片、每个筛选器背后,都要能落到这三组标签的某个字段上,而不是只挂一个用户分群。
3.3 特征表的验证:在可视化之前先过三关
可视化之前先验证特征表,否则图上出现数据异常时根本分不清是计算逻辑错还是图表配置错。我每次都会先跑三个检查。
第一关是完整性检查,用户数是否与原订单表的去重用户数一致,有订单但没关联上user_info的用户是否有兜底处理。第二关是分布合理性检查,打印每个用户分群的人数占比。正常情况下,「重要价值用户」占比应在5%到15%之间。如果超过20%,要么是评分阈值太松,要么是数据集中在高频高额区,要回头调整分位数切分方式。第三关是抽样验证,随机抽5个用户,手动把他们的订单明细拉出来复核一遍recency和monetary是否和计算一致。这三关跑完没异常,才能进可视化环节。
4. 可视化系统怎么落地:交互式看板的核心配置与参数调优
4.1 选型:为什么推荐用Plotly而不是Excel或ECharts
到可视化阶段,常见的选择是Excel数据透视表、BI工具或Plotly。Excel做不了动态下钻,BI工具适合固定报表但改交互逻辑成本高,Plotly是Python生态里做交互式可视化最顺手的方案。关键优势在于,Plotly的图表本身就是Altair和Bokeh之外少数能直接在Python里生成HTML交互图表、随后嵌到Web服务里的方案,不依赖前端团队的排期。
同时,整个RFM计算管线已经写在Python里了,用Plotly可以直接复用同一个DataFrame,不会出现「分析师在Python算完数据,再手工导到BI工具」的分裂流程。前端展示以下三种典型图就可以了:人群占比饼图、RFM三维评分分布散点图、用户明细表。
4.2 核心代码块:RFM三维散点图与分群着色
import plotly.express as px # 从特征表读取数据 rfm_profile = pd.read_csv('rfm_user_profile.csv') # 关键参数:size控制点大小,color按人群着色,hover_data展示明细 fig = px.scatter_3d( rfm_profile, x='recency', y='frequency', z='monetary', color='segment', size='monetary', size_max=10, hover_data=['user_id', 'r_score', 'f_score', 'm_score', 'order_count_30d'], title='用户RFM三维分布', labels={'recency': '最近消费间隔(天)', 'frequency': '消费次数', 'monetary': '累计金额(元)'} ) fig.update_layout( legend_title_text='用户人群', scene=dict( xaxis=dict(title='最近消费间隔(天)'), yaxis=dict(title='消费次数'), zaxis=dict(title='累计金额(元)') ), height=600 ) fig.write_html('rfm_3d.html')这里最值得调的是figure的height参数,默认值800在某些内网浏览器里会出现滚动条,页面整体观感很差。size_max也建议控制在8到12之间,数值过大会让头部用户的点连成一片,立体图完全看不出密度分布。
实际运行时如果发现散点图太稠密,可以在读入后先做一次采样。比如用户数超过3万时,用rfm_profile.sample(n=3000, random_state=42)抽样展示,否则浏览器渲染几万个点会明显卡顿。但要注意,抽样只用于散点图,不要影响后面画占比图和数据表的行数。
4.3 图表之外的交互筛选:数据表与图表的联动
可视化系统不只是几张静态图。运营用这个系统时的典型操作是:先看饼图发现「重要唤回用户」占18%,觉得这部分人有召回价值,然后点一下饼图里对应的人群,页面下方的用户明细表就只显示这一群人,并且可以直接导出成CSV去发短信或做定向活动。
这种联动在Plotly里用subplot的click事件实现。最常见的落地方式是做一个简单的Dash应用,把图和数据表放在同一个页面里。如果不想引入Dash,也可以退一步,把图表做成多个HTML文件回传给运营,由运营自己开多个页面交叉筛选,但体验差不少。
参数上需要关注的是数据表的排序逻辑。默认按user_id排序,运营根本没法用。强烈建议明细表默认按monetary降序排列,让运营第一眼看到的就是钱。这也是「用户画像可视化系统」和「数据报表」的一个分水岭——报表罗列数据,系统组织决策顺序。
5. 自建RFM可视化系统的避坑手册:5个典型翻车现场与解法
5.1 Frequency被退款单污染,高频人群虚高
现象:分群结果里「重要价值用户」占比接近30%,明显偏离业务感知。
原因:订单表里的order_status没过滤退款和取消状态。一个反复下单又反复取消的用户,在frequency上被计算为多次消费,但实际一分钱贡献都没有。
解决:在口径固化阶段就严格过滤有效状态。尤其是部分退款订单,到底算有效还是算无效必须在代码里写死。我的建议是退款金额小于订单金额20%的算有效,大于等于20%的整单剔除。这条规则需要业务方签字确认,否则后期有人质疑数据时没有依据。
5.2 Recency截止日选择不当,用户被误判为流失
现象:某个月的分群结果中「流失用户」激增,但业务方确认并没有大量用户离开。
原因:统计截止日用了当月1日,而订单表里还有截止日当月的后续订单,数据没更新到最新状态。所有用户最近一次消费时间相对截止日都变长,导致recency整体抬高。
解决:统一用etl任务跑批时刻作为cutoff_date,并在特征表里增加一个running_date字段,标注这张表是什么时候算的。可视化系统页面上也要显示这个日期,否则运营拿着上周的数据做本周的召回动作,人群早就变了。
5.3 分位数切分在订单量少的月份整体失效
现象:新用户和老用户的f_score大量集中在同一档,新用户人群几乎消失。
原因:某个月份有效订单总量本来就不高,比如春节月份,所有人都只下了1单或2单,frequency的分布极度集中。这时按分位数切5档就是硬切,结果就是天平的每个盘子上几乎一样重。
解决:低频月份不要用纯分位数,改用「观察窗口内是否达到N单」的绝对标准。常见做法是先把f_score改成0/1二值化,Frequency≥2单算1分,然后重新映射到5档。该方案需要额外写规则,但数据分布复杂时比机械分位数靠谱得多。
5.4 百分比堆积图里小人群被视觉吞掉
现象:饼图或堆积条形图里,「重要保持用户」只占3%,在图上几乎看不见,运营误以为这类人群不存在。
解决:可视化层面不要只依赖饼图,加一个表格展示占比数据。用户在这种场景下第一时间看数字,而不是看图。具体到Plotly实现,可以这样写:
seg_summary = rfm_profile.groupby('segment').agg( user_count=('user_id', 'count'), avg_recency=('recency', 'mean'), avg_frequency=('frequency', 'mean'), avg_monetary=('monetary', 'mean') ).reset_index() seg_summary['pct'] = (seg_summary['user_count'] / seg_summary['user_count'].sum() * 100).round(2) # Plotly表格展示,列顺序按业务决策顺序排列 from plotly.graph_objects import Figure, Table fig_tbl = Figure(Table( header=dict(values=['用户人群', '人数', '占比%', '平均最近消费间隔', '平均消费频次', '平均消费金额']), cells=dict(values=[ seg_summary['segment'], seg_summary['user_count'], seg_summary['pct'], seg_summary['avg_recency'].round(1), seg_summary['avg_frequency'].round(1), seg_summary['avg_monetary'].round(0) ]) )) fig_tbl.write_html('segment_summary_table.html')这段代码的逻辑很简单,但作用很关键。它把分群结果从「看图讲故事」变成了「看表做决策」,尤其当某一类人群占比特别小或特别大时,表格仍然能给出精确数字,饼图则做不到。
5.5 历史回溯对比时,RFM分群结果对不上账
现象:月初算的分群结果和月底复盘时重算的分群结果,同一批用户的人群体征变化很大,运营质问数据口径不稳定。
原因:底层订单表里补充录入了历史订单,或者退款处理发生了延迟。比如用户在月初下单,月底退款,这把重算时frequency和monetary都改掉了。
解决:给每个用户存一份历史快照表,用batch_id或calc_date字段标识是哪一天算的结果。可视化系统默认展示最新快照,但支持运营选择历史日期回看。这个习惯一旦养成,业务方对数据系统的信任度会大幅提升。
6. 进阶玩法:从静态RFM报表走向网约车场景的运营指标可视化
6.1 把RFM模型迁移到网约车订单数据的关键改造
网格车订单数据和常规电商订单对RFM的含义有显著差异。网约车的消费频次高、单均金额低、消费间隔短,用户可能一天内下单多次。如果原封不动照搬电商里的RFM规则,Frequency会普遍高得离谱,Recency会有大量用户在0到1之间,分群依然分不出层次。
实际做网约车订单数据处理时,我的建议是把时间窗口压缩为两周,将frequency替换为「活跃天数」而不是「订单数」。用户一天打5次车和打1次车,在消费频次上的业务含义完全不同,但对平台来说都是当天活跃用户。用活跃天数做Frequency评分更贴近「用户对这个平台的依赖程度」这个语义。
Monetary维度建议用「近两周内平均每单金额」替代「总金额」,这样能把高频低价用户和高频高价用户区分开,防止快车用户和专车用户被混为一谈。经过这三处改造后,网约车订单数据在RFM模型下的分群结果才有运营对照价值。
6.2 验证分群效果的技巧:RFM迁移矩阵
分群模型建好后,每周重算一次RFM,并输出用户在这两周之间的人群迁移矩阵。迁移矩阵能回答一个关键问题:上周的「重要价值用户」这周还在吗,还是掉到了「一般保持用户」?
在运营视角下,迁移矩阵比静态分布图更有意义,因为它是可以闭环验证ROI的。比如针对「重要唤回用户」发了折扣券,下周这批用户有多少比例晋升为「重要价值用户」,这个数字直接决定活动预算要不要继续投入。操作上也很简单,用上一版的rfm_user_profile.csv和本周的rfm_user_profile.csv在user_id上做merge,然后交叉统计segment的流转关系。
6.3 可视化系统上线后我的习惯
系统不是交付完就跑。我一般会在上线后的前两周,每周固定跑一次分群结果和运营手动标记的名单做比对,确认模型分层没有离谱偏差。然后每个月滚一次分位点阈值,因为用户结构会随时间变化,年初和年尾的消费分布完全不是一回事。
以上这套下来,系统才算是真正在业务里站住了脚。项目运行时踩过的坑都在前面几章里,这篇笔记的价值在于让读者少走我走过的弯路。希望帮到你。
本文还有配套的精品资源,点击获取