基于RFM与K-means的电信用户画像可视化系统:Django全链路实战

发布时间:2026/10/10 0:54:56
基于RFM与K-means的电信用户画像可视化系统:Django全链路实战 简介这份资源面向具备Python基础、希望入门用户画像与数据可视化实战的学习者围绕RFM模型构建一套可运行的Web分析系统。内容先用Pandas清洗交易数据并计算R、F、M三项指标再借助K-means聚类完成用户分群与价值分级最后通过Django搭建界面结合Matplotlib、Seaborn等库将分析结果以图表形式呈现帮助理解用户行为并支撑营销决策。压缩包共43个文件约14.72MB以py脚本、csv与tsv数据表、html模板为主另含ipynb分析笔记、sqlite3数据库及环境安装说明覆盖从数据处理、建模到Web展示的完整链路。目前已有1565人学习下载。读者可据此掌握RFM指标计算、聚类分群、Django视图与模板组织及可视化落地方法并参考目录结构快速复现项目、排查环境配置问题。1. 从一份电信用户 CSV 说起这套 RFM 画像系统到底能跑出什么手里拿到一份几十万行的电信用户行为明细业务方张口就要“把高价值用户筛出来做个能点的页面看分布”。这时候从零写 Django 配置、再手撸聚类和图表少说两天。我拆的这个基于 RFM 的用户画像可视化系统就是冲着这个场景来的它把数据处理、K-means 分群、Django 页面展示串成了一条完整链路压缩包里既有可直接跑的工程也有 Jupyter Notebook 记录建模过程还有电信、短信、App 三路 RFM 归一表。适合谁一是想拿一个完整 Django 机器学习项目练手的人二是需要快速搭用户分层看板的从业者。它不解决“数据从哪来”但解决“数据到手后怎么变成能看的画像”。2. 拆包先看目录Django 工程与数据文件的对应关系2.1 工程骨架与启动入口拿到压缩包先别急着装依赖把目录结构过一遍能省很多事。根目录下manage.py是 Django 的标准入口app210406是主应用目录里面settings.py、urls.py、views.py、wsgi.py、asgi.py一应俱全templates下放着user_class.html、start.html、base.html、user_base.html、error.html这几个页面。db.sqlite3是自带的 SQLite 库说明这个项目没打算让你先配 MySQL 才能跑。data目录是数据核心pythonProject里放着电信用户画像.ipynb也就是建模笔记。环境安装.docx和环境启动操作.txt是作者留的说明先扫一眼版本要求能避开后面一半的依赖坑。2.2 三路 RFM 数据表的分工data目录里的文件不是随便堆的它按数据源分了三路tele数据rfm数据表.csv和tele数据rfm数据归一表.csv对应通话行为短信数据rfm数据表.csv和短信数据rfm数据归一表.csv对应短信行为app数据rfm数据表.csv和app数据rfm数据归一表.csv对应 App 使用行为。归一表是在原始 RFM 指标上做了标准化方便直接喂给聚类。另外还有ods_user_call_detail_dt_201612.tsv、ods_user_sms_detail_dt_201612.tsv、ods_user_app_detail_dt_201612.tsv这些 ODS 层明细以及dim_call_flag.tsv、dim_sms_flag.tsv、dim_app_flag.tsv、dw_user_brand_sex.tsv这些维度表。电信用户画像.csv是最终汇总结果。理解这个分层后面改数据源或者换业务口径时才知道该动哪张表。2.3 环境安装与依赖确认环境安装.docx里通常会写 Python 版本和 pip 依赖但文档容易过期我一般直接看代码里的 import 反推。这个项目核心依赖是 Django、pandas、numpy、scikit-learn可视化部分可能用到 matplotlib 或 seaborn。先建虚拟环境再装别污染全局。下面这段是常见的依赖安装命令版本号按你本地 Python 版本微调Python 3.8 到 3.10 一般都能跑。# 创建虚拟环境避免和系统包冲突 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境macOS / Linux source venv/bin/activate # 安装核心依赖Django 版本按文档要求选 pip install django pandas numpy scikit-learn matplotlib seaborn # 如果文档指定了版本按文档锁版本例如 # pip install django3.2 pandas1.3.5 scikit-learn1.0.2逻辑说明虚拟环境是为了隔离Django 大版本之间urls.py写法有差异装错版本会在启动时直接报ImportError或ImproperlyConfigured。参数上django3.2是 LTS 版本兼容性最好如果你本地是 Python 3.11pandas 建议 1.5 以上。装完用pip list核对一遍别等跑起来才发现少包。3. 从 ODS 明细到 RFM 归一表数据处理链路怎么复现3.1 读取明细与 RFM 指标计算打开电信用户画像.ipynb前半部分基本是数据清洗和 RFM 计算。ODS 明细是 TSV 格式用pandas.read_csv时sep\t不能漏。R、F、M 三个指标的计算口径要按业务定通话场景里 R 是最近一次通话距统计日期的天数F 是统计周期内通话次数M 是通话时长或费用。下面这段是常见的计算骨架字段名按你实际表头改。import pandas as pd import numpy as np # 读取通话明细TSV 必须指定分隔符 call_detail pd.read_csv(data/ods_user_call_detail_dt_201612.tsv, sep\t) # 确认关键字段存在字段名按实际表头调整 # 假设字段为 user_id, call_time, call_duration call_detail[call_time] pd.to_datetime(call_detail[call_time]) # 设定统计基准日通常取明细中最大日期或业务指定日期 snapshot_date call_detail[call_time].max() pd.Timedelta(days1) # 按用户聚合计算 RFM rfm call_detail.groupby(user_id).agg( R(call_time, lambda x: (snapshot_date - x.max()).days), # 最近一次通话距今天数 F(call_time, count), # 通话频次 M(call_duration, sum) # 通话总时长 ).reset_index() print(rfm.head()) print(rfm.describe())逻辑说明snapshot_date取最大日期加一天是为了让最近一次通话的 R 值最小为 1避免出现 0 导致后续归一化异常。groupby后三个聚合分别对应 R、F、Mlambda里用x.max()取最近时间。参数上如果业务要求按自然月统计就把snapshot_date固定为月初。跑完先看describe()如果 F 或 M 出现极端大值说明有异常账号得先处理再归一化。3.2 归一化与 K-means 分群原始 RFM 三个指标量纲不同R 是几十到几百天M 可能是几万秒直接聚类会被大数值主导。所以项目里单独出了归一表常见做法是 Min-Max 或 Z-Score。电信数据里我一般用 Min-Max把每个指标压到 0 到 1 之间保留分布形状。归一之后用 K-means 聚成 3 到 5 类按业务需要定。下面这段是归一化加聚类的完整流程。from sklearn.preprocessing import MinMaxScaler from sklearn.cluster import KMeans # 对 R、F、M 三列做 Min-Max 归一化 scaler MinMaxScaler() rfm_scaled scaler.fit_transform(rfm[[R, F, M]]) # 注意R 是越小越好归一后需要反转让高分代表高价值 rfm_scaled[:, 0] 1 - rfm_scaled[:, 0] # 构建归一化后的 DataFrame方便后续合并 rfm_norm pd.DataFrame(rfm_scaled, columns[R_norm, F_norm, M_norm]) rfm_norm[user_id] rfm[user_id].values # K-means 聚类n_clusters 按业务分层数定常见 3 到 5 kmeans KMeans(n_clusters4, random_state42, n_init10) rfm_norm[cluster] kmeans.fit_predict(rfm_scaled) # 查看每个簇的中心判断哪一类是高价值 print(rfm_norm.groupby(cluster)[[R_norm, F_norm, M_norm]].mean()) # 导出归一表供 Django 读取 rfm_norm.to_csv(data/tele数据rfm数据归一表.csv, indexFalse)逻辑说明MinMaxScaler把三列压到 [0,1]但 R 的语义是“越小越近越好”所以归一后要1 -反转否则聚类会把最近消费的用户分到低分簇。n_init10是让 K-means 多跑几次初始中心避免陷入局部最优这个参数在 sklearn 新版本里必须显式写不然会有警告。random_state42保证每次跑结果一致方便复现。聚类完看簇中心均值R_norm 高、F_norm 高、M_norm 高的那一类就是高价值用户。这一步跑通归一表就是 Django 页面的数据源。3.3 三路数据合并与画像字段扩展电信项目通常不只通话一路短信和 App 行为也要并进来。三路归一表按user_id合并再和dw_user_brand_sex.tsv这种维度表 join就能在画像里带上品牌、性别等属性。合并时注意用howouter还是howinner如果只分析三路都活跃的用户用 inner如果要看全量用户用 outer 并填充缺失值。下面这段是合并骨架。# 读取三路归一表 tele_norm pd.read_csv(data/tele数据rfm数据归一表.csv) sms_norm pd.read_csv(data/短信数据rfm数据归一表.csv) app_norm pd.read_csv(data/app数据rfm数据归一表.csv) # 按 user_id 外连接合并保留全量用户 user_profile tele_norm.merge(sms_norm, onuser_id, howouter, suffixes(_tele, _sms)) user_profile user_profile.merge(app_norm, onuser_id, howouter) user_profile user_profile.rename(columns{cluster: cluster_app}) # 合并维度表补充品牌和性别 dim_user pd.read_csv(data/dw_user_brand_sex.tsv, sep\t) user_profile user_profile.merge(dim_user, onuser_id, howleft) # 缺失值填充聚类结果用 -1 表示未分群 user_profile user_profile.fillna({cluster_tele: -1, cluster_sms: -1, cluster_app: -1}) # 导出最终画像表 user_profile.to_csv(data/电信用户画像.csv, indexFalse) print(user_profile.shape)逻辑说明suffixes参数解决三路表同名列冲突合并后列名带_tele、_sms后缀便于区分。fillna把没参与某路行为的用户聚类标为 -1页面展示时可以单独归为“沉默用户”。howleft合并维度表保证用户不丢。跑完看shape行数应该和全量用户数一致如果少了说明 join 键有重复或类型不一致检查user_id是不是被读成了 float。4. Django 页面搭建把聚类结果变成能点的画像看板4.1 视图层读取数据与分群逻辑Django 部分的核心在app210406/views.py。它要做的事很直接读电信用户画像.csv按聚类簇统计人数和占比把结果传给模板渲染。下面这段是常见的视图写法用 pandas 读 CSV 再转成字典列表避免在模板里做复杂计算。import os import pandas as pd from django.shortcuts import render from django.conf import settings def user_class(request): # 拼接数据文件绝对路径避免相对路径在不同启动目录下失效 data_path os.path.join(settings.BASE_DIR, data, 电信用户画像.csv) df pd.read_csv(data_path) # 按通话聚类簇统计人数cluster_tele 为 -1 的归为未分群 cluster_counts df[cluster_tele].value_counts().sort_index() cluster_labels { 0: 低价值, 1: 中价值, 2: 高价值, 3: 潜力用户, -1: 未分群 } # 构造模板需要的上下文 context { total_users: len(df), cluster_data: [ {label: cluster_labels.get(int(k), f簇{k}), count: int(v)} for k, v in cluster_counts.items() ], } return render(request, user_class.html, context)逻辑说明settings.BASE_DIR是 Django 项目根目录用它拼路径比写死相对路径稳换机器不用改。value_counts().sort_index()保证簇顺序固定模板渲染时颜色不会乱。cluster_labels是业务映射把 0 到 3 翻译成中文标签页面可读性直接上来。参数上如果 CSV 很大每次请求都读会慢常见做法是启动时读一次缓存到内存或者用 Django 的缓存框架。这个项目数据量不大直接读没问题。4.2 模板渲染与图表接入templates/user_class.html负责展示。Django 模板本身不能画图常见做法有两种一是后端用 matplotlib 生成图片转 base64 塞进模板二是前端用 ECharts 或 Chart.js后端只传 JSON。这个项目里static目录有css和img说明作者可能用了静态图片或前端图表库。下面这段是 ECharts 接入的骨架数据从视图传过来。!-- user_class.html 片段引入 ECharts 并渲染柱状图 -- div idclusterChart stylewidth: 100%; height: 400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script // 从 Django 模板变量拿到聚类数据safe 过滤器避免转义 var clusterData {{ cluster_data|safe }}; var chart echarts.init(document.getElementById(clusterChart)); var option { title: { text: 用户价值分群分布 }, tooltip: {}, xAxis: { type: category, data: clusterData.map(function(d) { return d.label; }) }, yAxis: { type: value }, series: [{ type: bar, data: clusterData.map(function(d) { return d.count; }) }] }; chart.setOption(option); /script逻辑说明{{ cluster_data|safe }}把 Python 列表转成 JS 能解析的 JSON不加safe会被 HTML 转义成字符串。ECharts 的xAxis.data和series.data分别取标签和人数结构对上了图就出来。参数上height: 400px按页面布局调太小图会挤。如果不想依赖 CDN把 echarts.min.js 下到static目录用{% static %}引用内网环境也能跑。4.3 路由配置与启动验证urls.py把视图和路径绑起来settings.py里要确认INSTALLED_APPS包含app210406TEMPLATES的DIRS指向templates目录。下面这段是路由配置和启动命令。# app210406/urls.py from django.urls import path from . import views urlpatterns [ path(, views.start, namestart), # 首页 path(user_class/, views.user_class, nameuser_class), # 画像页 ]# 在 manage.py 同级目录执行 python manage.py migrate # 初始化 SQLite 库 python manage.py runserver 0.0.0.0:8000逻辑说明migrate是必须的哪怕项目用 CSV 不用模型Django 自带的 session、admin 表也要建不跑会报no such table。runserver 0.0.0.0:8000让局域网内其他机器也能访问方便演示。启动后浏览器开http://127.0.0.1:8000/user_class/能看到柱状图就说明链路通了。如果报TemplateDoesNotExist检查TEMPLATES的DIRS是不是[os.path.join(BASE_DIR, templates)]。5. 避坑与排查这套工程跑不起来时先看这几条5.1 现象启动报ModuleNotFoundError: No module named django原因虚拟环境没激活或者依赖装到了全局 Python 里。解决先which pythonWindows 用where python确认当前解释器路径在 venv 下再pip list看 Django 在不在。不在就重新pip install django。如果用了 PyCharm检查项目解释器有没有指向 venv这个坑我见过太多次代码没问题纯粹是解释器选错。5.2 现象页面能开但图表空白控制台报cluster_data is not defined原因视图没把cluster_data传进模板或者模板里{{ cluster_data|safe }}写成了{{ cluster_data }}被转义。解决先在视图里print(context)确认数据有值再检查模板变量名拼写。如果数据是空的多半是 CSV 路径不对pd.read_csv抛了异常但被吞了。把data_path打印出来确认文件真实存在。5.3 现象K-means 每次跑出来的簇编号不一样页面标签对不上原因K-means 初始中心随机random_state没固定或者固定了但n_init没设导致多次初始化结果不同。解决KMeans(n_clusters4, random_state42, n_init10)三个参数一起写。另外簇编号本身没有语义0 不一定就是低价值得看簇中心均值再映射标签。我一般把映射关系写死在视图里跑完聚类先人工确认一遍再上线。5.4 现象合并三路数据后用户数变多原因某路归一表里user_id有重复merge 时产生笛卡尔积。解决合并前先df.drop_duplicates(subsetuser_id)或者groupby(user_id).first()去重。电信数据里一个用户多条记录很常见归一表如果没按用户聚合就导出合并必炸。检查方法很简单print(df[user_id].nunique())和len(df)对比不相等就有重复。5.5 现象中文列名或文件名在 Windows 下读取报编码错误原因CSV 存的是 GBKpandas 默认按 UTF-8 读。解决pd.read_csv(path, encodinggbk)或者先用记事本另存为 UTF-8。文件名带中文在 Linux 服务器上一般没事Windows 下如果路径含中文且用了os.path.join确认 Python 版本在 3.6 以上低版本对中文路径支持差。这个坑不常遇到但遇到一次能查半天。6. 进阶玩法把静态画像改成可筛选的交互看板基础链路跑通后这套系统还能往上加东西。最实用的一步是让页面支持按品牌、性别筛选而不是只看全量分布。做法是在视图里接收 GET 参数用 pandas 过滤后再统计。下面这段是筛选逻辑的骨架。def user_class(request): data_path os.path.join(settings.BASE_DIR, data, 电信用户画像.csv) df pd.read_csv(data_path) # 接收前端筛选参数默认不筛选 brand request.GET.get(brand, ) sex request.GET.get(sex, ) if brand: df df[df[brand] brand] if sex: df df[df[sex] sex] cluster_counts df[cluster_tele].value_counts().sort_index() # 后续 context 构造同上逻辑说明request.GET.get拿 URL 查询参数?brand某品牌sex男就能过滤。参数为空时不过滤保证默认展示全量。这个改动很小但页面从“只能看”变成“能查”业务方接受度高很多。再进一步可以把聚类数做成可调前端传n_clusters后端重新跑 K-means但每次请求都聚类会慢常见做法是预计算几套结果缓存起来。另一个方向是验证聚类质量。K-means 的簇数不是拍脑袋定的可以用肘部法看 SSE 拐点或者用轮廓系数评估。下面这段是肘部法的快速实现。from sklearn.cluster import KMeans import matplotlib.pyplot as plt sse [] k_range range(2, 9) for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) km.fit(rfm_scaled) sse.append(km.inertia_) # inertia_ 即簇内平方和 plt.plot(k_range, sse, markero) plt.xlabel(簇数 k) plt.ylabel(SSE) plt.title(肘部法确定最佳簇数) plt.show()逻辑说明inertia_是每个点到其簇中心的距离平方和k 增大 SSE 必然下降拐点处再增加 k 收益变小那个位置就是相对合理的簇数。参数上k_range从 2 到 8 足够覆盖常见分层。跑完看图如果曲线平滑没有明显拐点说明数据本身分群特征弱这时候硬分 4 类意义不大得回头检查 RFM 指标口径是不是有问题。我自己的习惯是每次换数据源或者改 RFM 计算口径都先把归一化和聚类单独跑一遍确认簇中心均值符合业务直觉再往 Django 里灌。有一次直接拿未去重的归一表合并页面人数翻了一倍查了半天才发现是user_id重复。从那以后我每次合并前都强制走一遍nunique校验这个习惯帮我省了不少后悔药。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询