ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于RFM的用户画像可视化:Django+Python实战代码全解析

基于RFM的用户画像可视化:Django+Python实战代码全解析 简介一套基于RFM模型的用户画像可视化系统完整代码资源采用Python技术栈实现面向希望掌握用户价值分析、Web开发与数据可视化技能的开发者。项目围绕最近一次消费时间、消费频率和消费金额三个核心维度以电信、短信、App等多源业务数据为例包含数据清洗、RFM指标计算、归一化处理、K均值聚类分群等完整流程并通过Django框架搭建管理后台与展示页面利用图表直观呈现不同用户群体的特征帮助理解高价值、中价值与低价值用户的差异化运营策略。压缩包共43个文件约14.72MB内有多份交易与画像表格数据、Django项目源码、页面模板、环境安装与启动说明、Jupyter Notebook分析记录、数据库文件等覆盖数据处理、模型构建、Web界面集成、结果解读等完整环节。目前已有1565人学习浏览。借助其中代码与文档读者可快速复现RFM分析流程掌握Django项目结构学会对聚类结果进行可视化展示也可根据自身数据替换样例数据逐步完成从原始数据到用户画像看板的搭建适合用于课程设计、毕业设计或商业分析实战。1. 基于 RFM 的用户画像可视化一份能直接跑起来的 Django Python 实战代码做用户运营的同学对 RFM 不会陌生R 是最近一次消费距今多久F 是消费频次M 是消费金额。这三个维度一组合就能把用户切成高价值、流失风险、新客、沉睡客等不同群体。这份压缩包里的内容就是把这套 RFM 理论真正落地的完整代码——前端通过 Django 搭建展示界面后端用 Pandas 做数据清洗和指标计算再用 K-means 做用户分群。从原始数据到可视化图表整条链路都是通的不是那种只有几个 .ipynb 文件然后让你自己补全的半成品。适合谁来用如果你正在做用户增长、会员运营或者需要给学生讲 RFM 模型的实战案例这份代码可以直接改数据源后复用。压缩包里带了电信、短信、App 三套用户明细数据还有对应的 RFM 数据表和归一表这意味着你不需要自己造数据就能先跑通全流程。下面我把项目结构、RFM 计算逻辑、Django 配置和常见的坑逐个拆开讲。2. 读懂项目结构与 RFM 计算逻辑从原始数据到分群标签2.1 压缩包里的文件到底是怎么组织的解压后你会看到几个顶层目录我按实际用途重新梳理了一下目录/文件作用关键内容app210406/Django 项目主目录settings.py、urls.py、views.py、templates/data/原始数据和 RFM 计算结果电信、短信、App 三套明细数据及对应的 RFM 表、归一表pythonProject/Jupyter Notebook 分析脚本电信用户画像.ipynb环境安装.docx、环境启动操作.txt环境配置与启动说明依赖库版本、启动步骤manage.pyDjango 项目入口运行服务、迁移数据库templates下有user_class.html、base.html、user_base.html等页面模板static里放的是 CSS 和图片资源。db.sqlite3是 Django 自带的数据库文件说明这个项目已经把分析结果导入了本地库启动后直接能看到页面。如果你想把数据换成自己的需要同时改data目录下的 CSV 文件和数据库中的表结构。2.2 RFM 指标计算的三个核心步骤打开电信用户画像.ipynb你会发现整个 RFM 计算并不是简单地对三列数据求值而是有一套完整流程。第一步是数据清洗原始数据通常是行为日志级别的明细数据比如ods_user_call_detail_dt_201612.tsv这类文件包含了用户 ID、通话时间、通话时长等字段。常见做法是先用 Pandas 读取然后过滤掉空值和异常值。比如通话时长为 0 的记录在计算消费金额时就应该剔除import pandas as pd # 读取原始明细数据 call_data pd.read_csv(data/ods_user_call_detail_dt_201612.tsv, sep\t, encodingutf-8) # 查看字段和缺失值情况 print(call_data.info()) print(call_data.isnull().sum()) # 剔除关键字段为空的数据用户ID、通话时间、通话费用 call_data call_data.dropna(subset[user_id, call_time, call_fee]) # 剔除费用小于等于0的异常记录 call_data call_data[call_data[call_fee] 0] print(f清洗后剩余记录数: {len(call_data)})这段代码里sep\t是因为 tsv 文件用制表符分隔dropna(subset...)只对指定列做非空过滤不会误删整个 DataFrame 的其他字段。清洗之后你会发现原始数据量通常会减少 5%~10%这是正常现象。第二步是计算 R、F、M 三个维度。计算 R 时要先确定一个参考日期否则“最近一次消费距今多久”没有基准点import datetime # 以数据集中最大日期加1天作为参考点 ref_date call_data[call_time].max() datetime.timedelta(days1) # R值用户最近一次消费距参考日期的天数 rfm_data call_data.groupby(user_id).agg( R(call_time, lambda x: (ref_date - x.max()).days), F(user_id, count), # 统计每个用户的消费次数 M(call_fee, sum) # 统计每个用户的消费总额 ).reset_index() print(rfm_data.head()) print(rfm_data.describe())groupby(user_id)是按用户聚合agg函数里用字典指定各列的计算方式。R 值的 lambda 表达式核心是x.max()取该用户所有消费记录里最近的时间再用参考日期去减得到天数。F 和 M 分别是计数和求和。这里有同学会直接用df[recency] df.groupby(user_id)[call_time].transform(max)再算差值两种写法结果一样但agg写法更紧凑。第三步也是关键一步——RFM 归一化。原始 R、F、M 三个值的量纲完全不同R 是 1~100 天F 可能是 1~50 次M 可能是几千元。如果不做归一化直接喂给 K-meansM 的数值会把 R 和 F 的差异完全吞掉分群结果基本就变成“按花钱多少排序”了。项目里的归一表文件就是用标准化或 MinMaxScaler 处理后生成的。2.3 K-means 分群的参数选择为什么是 4~6 类而不是越多越好归一化之后进入分群环节。项目里用的是 K-means这是最简单也最直观的聚类算法。但第一次跑的同学通常会卡在两个点上K 值选多少、随机种子要不要设。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler import matplotlib.pyplot as plt # 读取归一化后的RFM数据 rfm_norm pd.read_csv(data/电信用户画像.csv) # 只取R、F、M三列作为聚类特征 features rfm_norm[[R, F, M]].values # 标准化处理消除量纲影响 scaler StandardScaler() features_scaled scaler.fit_transform(features) # 用肘部法确定K值 inertia_values [] for k in range(2, 11): km KMeans(n_clustersk, random_state42, n_init10) km.fit(features_scaled) inertia_values.append(km.inertia_) plt.plot(range(2, 11), inertia_values) plt.xlabel(K值) plt.ylabel(簇内平方和) plt.show() # 观察肘部转折点通常选4-6这里的n_init10是让算法跑 10 次初始化取最优结果避免陷入局部最优。random_state42是固定随机种子保证每次运行分群结果一致。如果你不设随机种子同一份数据每次跑出来的用户分群都会变后期做运营策略就完全没法对齐。分群完成后需要给每个类打标签。常见做法是看每一类的 R、F、M 均值与总体均值比较高于均值标记为 1低于标记为 0。例如某类用户 R 均值低、F 均值高、M 均值高那这一群就是“高频高价值用户”反之 R 均值高、F 均值低、M 均值低基本就是“沉睡流失用户”。这个逻辑决定了后面 Django 页面里展示的用户价值层级。3. Django 站点搭建与数据可视化把分析结果变成可点的页面3.1 项目配置的核心文件逐个拆开看app210406作为 Django 主目录里面的settings.py决定了整个站点怎么跑。你要关心的配置项有几个INSTALLED_APPS里是否注册了自定义应用DATABASES指向的数据库路径TEMPLATES模板目录配置以及STATIC_URL静态文件路径。# app210406/settings.py 关键配置 BASE_DIR Path(__file__).resolve().parent.parent SECRET_KEY django-insecure-xxxxxx # 部署时需要更换 DEBUG True # 生产环境必须改为False ALLOWED_HOSTS [*] # 学习环境可放开生产环境必须限定 DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], # 模板路径 APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.debug, django.template.context_processors.request, ], }, }, ] STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]ALLOWED_HOSTS [*]只适合本地调试一旦部署到服务器必须改成实际域名或 IP否则 Django 会拒绝请求并报Bad Request (400)错误。STATICFILES_DIRS是告诉 Django 去哪里找 CSS 和图片压缩包里的static/css和static/img就是被这个配置关联起来的。urls.py是整个站点的路由分发中心它决定了浏览器访问不同 URL 时由哪个视图函数处理。项目里通常有一个根路由和多个应用路由这里根路由直接指向视图函数# app210406/urls.py from django.contrib import admin from django.urls import path from . import views urlpatterns [ path(admin/, admin.site.urls), path(, views.start, namestart), # 启动页 path(user/, views.user_class, nameuser_class), # 用户分群结果页 path(error/, views.error, nameerror), # 异常提示页 ]这段配置里每条path对应一个 URL 匹配规则。第一个是空路径访问站点根地址时响应start视图user/路径对应分群展示页面error/是兜底页面当请求的数据不存在时跳转。3.2 视图函数如何读取 RFM 结果并渲染到页面视图层是整个 Django 项目的业务逻辑核心。一个合格的视图函数应该做三件事接收请求、读取数据、渲染模板。在这个项目中views.py里的user_class函数负责把数据库中的 RFM 分群结果查询出来打包传给模板展示import sqlite3 from django.shortcuts import render def user_class(request): # 连接本地SQLite数据库 conn sqlite3.connect(db.sqlite3) cursor conn.cursor() # 查询分群统计结果 cursor.execute( SELECT cluster_label, COUNT(*) as user_count, ROUND(AVG(r_value), 2) as avg_r, ROUND(AVG(f_value), 2) as avg_f, ROUND(AVG(m_value), 2) as avg_m FROM rfm_user_cluster GROUP BY cluster_label ORDER BY user_count DESC ) rows cursor.fetchall() conn.close() # 将查询结果组织为字典列表方便模板循环渲染 clusters [] for row in rows: clusters.append({ label: row[0], count: row[1], avg_r: row[2], avg_f: row[3], avg_m: row[4] }) return render(request, user_class.html, {clusters: clusters})注意这里用的是sqlite3模块直接查询数据库而不是 Django ORM。原因很简单项目的数据是 Pandas 处理完写入 SQLite 的用原生 SQL 查询更直接不用在models.py里重新定义一遍模型映射。如果你后续要改成 MySQL只需要把sqlite3.connect换成pymysql.connectSQL 语句基本可以复用。视图返回的clusters是一个列表每个元素是一个字典包含分群标签、用户数量、R/F/M 均值。模板页面用 Django 模板语法进行循环渲染这是user_class.html里的核心片段!-- templates/user_class.html -- table classtable table-bordered thead tr th用户分群/th th用户数量/th th平均R值/th th平均F值/th th平均M值/th /tr /thead tbody {% for item in clusters %} tr td{{ item.label }}/td td{{ item.count }}/td td{{ item.avg_r }}/td td{{ item.avg_f }}/td td{{ item.avg_m }}/td /tr {% endfor %} /tbody /table{% for item in clusters %}是 Django 模板引擎的循环语法等价于 Python 的for item in clusters。模板里拿到的变量就是视图函数上下文里传给render的字典键值。3.3 启动项目的完整命令序列代码都看懂了接下来就是把服务跑起来。压缩包里虽然带了环境安装.docx但安装依赖时大概率会遇到版本坑我一般会先创建一个干净的虚拟环境再安装避免污染系统 Python# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate # 安装核心依赖库 pip install django4.2 pandas scikit-learn matplotlib seaborn # 执行数据库迁移生成Django默认表 python manage.py makemigrations python manage.py migrate # 启动开发服务器 python manage.py runserver 127.0.0.1:8000启动成功后浏览器访问http://127.0.0.1:8000/会先看到start视图对应的入口页点击进入后才能访问user_class分群页。如果 8000 端口被占用替换成其他端口号即可比如python manage.py runserver 127.0.0.1:8080。Django 开发服务器的逻辑是每次请求时自动加载urls.py和视图函数所以你改完views.py或模板文件后不需要重启服务刷新页面就能看到效果。4. 可视化展示的三种方式静态图表、前端模板与交互式探索4.1 用 Matplotlib 和 Seaborn 生成静态分析图RFM 分群的结果如果不画图光看表格很难向业务同学解释清楚。项目里在 Jupyter Notebook 阶段已经把图表画好了生成图片后放在static/img目录下Django 页面直接引用。常见做法是生成四类图RFM 三维散点图、各分群用户数量柱状图、R/F/M 均值雷达图、以及用户价值分布饼图。import matplotlib.pyplot as plt import seaborn as sns from mpl_toolkits.mplot3d import Axes3D # 读取分群结果数据 df pd.read_csv(data/电信用户画像.csv) # 设置中文字体避免乱码 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False # 创建三维散点图按分群结果着色 fig plt.figure(figsize(10, 8)) ax fig.add_subplot(111, projection3d) scatter ax.scatter(df[R], df[F], df[M], cdf[cluster], cmapviridis, s30, alpha0.7) ax.set_xlabel(Recency (天)) ax.set_ylabel(Frequency (次)) ax.set_zlabel(Monetary (元)) plt.colorbar(scatter) plt.savefig(static/img/rfm_3d_scatter.png, dpi150, bbox_inchestight) plt.close()这段代码里plt.rcParams是 Matplotlib 的全局配置SimHei是 Windows 自带中文字体Mac 上改成PingFang SC。fig.add_subplot(projection3d)创建三维坐标系scatter函数里用c参数指定颜色映射。bbox_inchestight确保图片边缘不会裁掉标签。生成的图片保存在static/img/下Django 模板里用{% load static %}标签引用即可img src{% static img/rfm_3d_scatter.png %} altRFM三维散点图 classimg-responsive4.2 Django 模板中的数据处理与图表切换有了静态图片做底子页面还可以更进一步——在模板里直接对分群数据做二次加工。比如user_class.html里常见的一个操作把后端传来的clusters列表按用户数排序并加一个高亮逻辑让用户数最多的分群在表格里突出显示{% for item in clusters %} tr {% if forloop.first %}classhighlight-row{% endif %} td{{ item.label }}/td td{{ item.count }}/td td{{ item.avg_r }}/td td{{ item.avg_f }}/td td{{ item.avg_m }}/td /tr {% endfor %}{% if forloop.first %}是模板内置变量表示当前循环是否为第一轮。第一行默认是用户数最多的分组加上highlight-row的 CSS 类就能在视觉上突出主要人群。这种写法比在视图里嵌套一堆字典判断要清爽很多。4.3 让图表“动”起来引入 ECharts 做前端交互虽然项目本体用的是 Matplotlib 生成静态图但如果你想要更直观的交互体验完全可以在不改后端的前提下引入 ECharts。把分群数据通过 Django 模板变量渲染为 JavaScript 变量ECharts 就能直接消费!-- templates/user_class.html 中的 ECharts 柱状图 -- script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script div idcluster_bar stylewidth: 100%; height: 400px;/div script var clusterData [ {% for item in clusters %} { name: {{ item.label }}, value: {{ item.count }} }, {% endfor %} ]; var chart echarts.init(document.getElementById(cluster_bar)); chart.setOption({ title: { text: 用户分群数量分布 }, tooltip: {}, xAxis: { data: clusterData.map(function(d) { return d.name; }) }, yAxis: {}, series: [{ type: bar, data: clusterData.map(function(d) { return d.value; }) }] }); /script注意clusterData数组是在页面加载时由 Django 模板引擎渲染生成的后端 JSON 数据和前端 JavaScript 数据通过这一行{% for %}循环打通。ECharts 的 CDN 在某些内网环境加载不了项目没有外网依赖的话建议把echarts.min.js下载到static/js/目录下。5. 项目运行避坑手记环境、编码、数据路径三大翻车现场5.1 Django 版本过高导致启动报错现象按文档装完依赖执行python manage.py runserver报错TypeError: __init__() got an unexpected keyword argument providing_args。原因新版 Django 4.x 移除了某些旧 API压缩包里的代码可能基于 Django 2.2 或 3.x 编写。providing_args是 Django 3.x 中Signal的参数4.0 后已被删掉。解决查看项目里是否拼写为django3.2或django4.2。把requirements.txt里的 Django 版本改为代码生成时的匹配版本即可。如果项目用的 ORM 比较简单也可以不回退版本找到报错的第三方库通常是django-import-export或simpleui升级到支持新 Django 的版本。5.2 tsv 文件读进来全是一列现象用 Pandas 读取dim_call_flag.tsv时发现整张表只有一列正常应拆成多列。原因Pandas 默认分隔符是逗号tsv 文件是制表符分隔read_csv没加sep\t参数。解决# 读取tsv文件时建议统一使用下面的写法 import pandas as pd data pd.read_csv(data/dim_call_flag.tsv, sep\t, encodingutf-8) # 如果文件是GBK编码需要换成gbk否则中文会乱码 try: data pd.read_csv(data/电信用户画像.csv, sep,, encodingutf-8) except UnicodeDecodeError: data pd.read_csv(data/电信用户画像.csv, sep,, encodinggbk)这类编码坑在 Windows 上尤其常见。压缩包里 CSV 文件如果是从某个 Excel 另存来的大概率是 GBK 编码如果是 Linux 环境下生成的通常是 UTF-8。我一般会写一个 try-except 双重编码尝试一劳永逸。5.3 数据库表名对不上导致查询失败现象views.py里的 SQL 查询执行时报错Table rfm_user_cluster doesnt exist。原因数据库初始化时没有把 RFM 分析结果导入 SQLite或者表名拼写有出入。压缩包的db.sqlite3是在生成环境里建好的但如果你重新迁移过数据库原始表会被清空或重建。解决先看一下数据库实际有哪些表import sqlite3 conn sqlite3.connect(db.sqlite3) cursor conn.cursor() cursor.execute(SELECT name FROM sqlite_master WHERE typetable) print(cursor.fetchall()) conn.close()然后再执行数据导入import pandas as pd import sqlite3 # 读取RFM分群结果并写入SQLite df pd.read_csv(data/电信用户画像.csv) conn sqlite3.connect(db.sqlite3) df.to_sql(rfm_user_cluster, conn, if_existsreplace, indexFalse) conn.close()to_sql的if_existsreplace是覆盖写入如果表已存在且结构冲突也可以选择append追加。5.4 浏览器访问页面白屏或 404现象Django 服务启动成功但访问http://127.0.0.1:8000/返回 404 页面。原因多半是urls.py里的路由没匹配上。有的项目根路径不是而是index/访问根地址自然就 404 了。解决先尝试访问http://127.0.0.1:8000/user/看分群页是否存在。如果同样 404检查urlpatterns第一项path(, views.start)是否注释掉了。另一个常见原因是模板文件放错目录——Django 默认找templates目录但如果你把它放在了app210406/template少一个 s同样会报模板不存在。6. 把静态分群变成持续运营工具定时重算与前端联动技巧RFM 模型最大的价值不在于跑一次分群而是能让运营每周盯着变化。同一个用户上周还是高价值客户这周可能就滑到中价值区间了这个过程必须可视化出来才能及时发现。可以做一个简单的重算脚本把 Notebook 里的 RFM 计算逻辑封装成 Python 函数然后用 cron 或 Windows 任务计划定期执行。数据更新后 Django 页面查到的就是新分群结果不需要改视图代码。这里提供两个技巧第一个是让数据库表带上统计日期字段。每次重算时写入当天的数据日期页面查询时就只取最新日期同时保留历史数据供趋势对比。import datetime def recompute_rfm(data_path, output_path): # 计算RFM并分群 rfm_data compute_rfm(data_path) clustered kmeans_cluster(rfm_data) # 写入统计日期 clustered[stat_date] datetime.date.today().isoformat() clustered.to_sql(rfm_daily, conn, if_existsappend, indexFalse)第二个是给前端加一个日期筛选下拉框。模板里加一个select标签通过 GET 参数向视图传日期视图按日期过滤后返回对应数据。这一步会让“用户画像可视化”从一张静态报表变成一个动态分析工具运营同学自己就能对比不同时期的用户结构变化。实际做的时候最容易忽略的是 R 值的参考日期。很多人把整体数据集的最后一天写死在代码里结果每周重算时 R 值基准点没跟着变算出来的结果一次比一次离谱。从那以后我每次跑这个项目都会在重构脚本开头强制校验参考日期与数据最大日期的关系确认刷新逻辑没有问题再往下走。这个坑不踩一次很难记住希望帮到你。本文还有配套的精品资源点击获取
返回列表