☰
用户画像项目实战:RFM+K-Means聚类从数据清洗到标签全流程
2026/10/25 16:27:51 网站建设 项目流程

简介:一份面向人工智能学习者与数据挖掘初学者的CSDN用户画像构建项目实践源码包,覆盖从数据收集、预处理、特征工程到用户聚类、建模与画像生成的完整链路,旨在帮助读者理解如何从海量行为数据中提炼用户兴趣与需求。压缩包共17个文件,以9个Python脚本为核心,分别实现文本分词、Word2Vec词向量训练、模型训练与测试等环节;另含3个XML配置、2个pyc编译文件、2个TXT说明和1个IDEA工程文件,整体仅17KB,轻量便于快速阅读。目前已有112人学习下载。通过阅读源码与说明文档,可掌握文本切割、特征提取、用户聚类的工程实现思路,并借助内置工程配置直接运行;该画像方法可迁移至推荐系统、广告定向投放及产品优化等场景。适合有一定Python基础、希望以AI项目实战方式入门用户画像的读者进行研读和二次开发。

1. 人工智能项目实践里的用户画像:这份 CSDN 源码解决的是哪类问题

在人工智能项目实践里,用户画像是最容易被当成"玄学"的方向:一说起来就是标签、分群、精准营销,可真拿到一份真实用户数据时,很多人连从哪下手都不知道。这份在 CSDN 上被反复下载的《人工智能项目实践用户画像》源码,补上的正是这块空白——它不是 PPT 式演示,而是一条从原始数据到画像看板的数据分析项目实践链路,覆盖数据清洗、RFM 特征构造、K-Means 聚类分群、标签映射和结果解读。对赶人工智能大作业的学生、刚入行的数据分析开发者,它给了一个能直接抄作业的骨架:换数据、改参数,就能跑出自己的画像结果。这篇拆解只讲怎么落地、参数怎么调、坑在哪,不评价格值。

2. 数据预处理与特征工程:画像质量的七成由清洗决定

2.1 原始数据的字段结构与清洗思路

先说这份源码面对的数据长什么样。用户画像项目最常见的输入是两张表:用户基本信息表和交易订单表。订单表里通常有 user_id、order_time、order_amount、product_category 这些字段,有些场景还带 channel(来源渠道)和 device(设备类型)。源码自带的示例数据是模拟生成的,但字段命名风格和你从数仓里导出来的几乎一致,这是它比较讨喜的地方——不用花大量时间改列名,直接能跑。

拿到数据后第一件事不是建模,而是清洗。我按这个顺序走:先去重,再过滤异常金额,最后卡时间范围。常见做法是把这三步写成函数,换数据时只改路径和字段名,下面这段就是源码里清洗模块的骨架:

import pandas as pd df = pd.read_csv("user_orders.csv", parse_dates=["order_time"]) print(df.shape, df.isnull().sum()) # 订单表里去重:同一用户同一时间同一金额的重复记录 df = df.drop_duplicates(subset=["user_id", "order_time", "order_amount"]) # 去掉金额异常值:小于等于 0 的订单直接剔除 df = df[df["order_amount"] > 0] # 卡时间范围:下单时间晚于统计截止日期的属于脏数据 ref_date = pd.Timestamp("2023-12-31") df = df[df["order_time"] <= ref_date] print(f"清洗后剩余记录数: {len(df)}")

这里有几个参数值得说清楚。drop_duplicates 的 subset 用三个字段,是判断"重复订单"的最小组合;如果业务上有订单号,直接按 order_id 去重更可靠。ref_date 是画像统计的截止时间,它决定后面 R 值怎么算,一定要根据数据实际日期来改。源码里写死的是模拟数据生成日期,换真实数据不改这一行,后面 R 值全错。清洗之后再做一次朴素的质量检查:看每个 user_id 的订单数分布。如果大量用户只有一笔订单,这批"一次性用户"会独立聚成一类,这不是 bug,是数据本身的特征,你心里要有数。

2.2 构造 RFM 特征:别拿原始字段直接建模

用户画像最核心的三个特征是 RFM:R(Recency)是最近一次消费距统计日期的天数,F(Frequency)是统计周期内的消费次数,M(Monetary)是统计周期内的消费总金额。这套源码选 RFM 而不是把几十个原始字段直接丢进模型,原因很实际:维度少、可解释性强,聚类出来的每一类人群都能用"最近多久没来、来了几次、花了多少"讲清楚。这一点在课程答辩和项目汇报里几乎是决定性优势——评审只关心你能不能解释每个群是谁。

import pandas as pd ref_date = pd.Timestamp("2023-12-31") window_start = ref_date - pd.DateOffset(days=180) # 只取统计窗口内的订单,窗口外的不参与特征计算 df_window = df[df["order_time"] >= window_start] rfm = df_window.groupby("user_id").agg( recency=("order_time", lambda x: (ref_date - x.max()).days), frequency=("order_time", "count"), monetary=("order_amount", "sum") ).reset_index() # 频率字段加下限,避免后续聚类出现除零和空群 rfm["frequency"] = rfm["frequency"].clip(lower=1) print(rfm.head())

几个关键点。recency 用的是(ref_date - x.max()).days,也就是每个用户最后一笔订单距离统计日期的天数,值越小说明消费越近。frequency 直接用 count,这里是下单次数不是商品件数,源码没有区分这两个概念,要按件数算得自己改。window_start 把统计周期限定在 180 天,这是电商常客分析的常见做法,换成你业务的 90 天或一年,分群结果会明显不同,这个参数值得反复试。

提示:RFM 三列的命名在源码里统一是小写 recency、frequency、monetary,后面建模的特征选择和可视化都依赖这三个列名,自己加字段时别覆盖它们。

2.3 M 值长尾:要不要做截断与 log 变换

用户画像数据里最不缺的就是长尾,少数大客户贡献了大部分金额,M 值分布右偏严重。面对长尾,常见做法有三个:截断、log 变换、分箱。源码只用标准化,但标准化挡不住极端值——一个消费 50 万的用户经过 z-score 后仍然是大离群点,K-Means 会专门为它划出一个"一人群",这种群没有任何业务价值。

我一般会在标准化之前先看 M 值的分位数,如果 p99 是 p50 的几十倍,就先做一步截断,必要时再做 log 压缩:

import numpy as np m_p99 = rfm["monetary"].quantile(0.99) rfm["monetary_capped"] = rfm["monetary"].clip(upper=m_p99) # 截断后仍然偏态明显的话,用 log1p 压缩量级 rfm["monetary_log"] = np.log1p(rfm["monetary_capped"]) print(rfm[["monetary", "monetary_capped", "monetary_log"]].describe())

clip 的上限取 0.99 分位数是常见做法,不取最大值,因为 p100 往往是一个业务异常值,比如测试订单或集团采购。log1p 的好处是 x=0 时不报错,还能保持原始大小关系。要注意取舍:截断和 log 变换会改变 M 值的可解释性,聚类结果反标准化回去的是变换后的值,写给业务报告时要换算回原始金额。源码里没做这一层,真实数据我认为是必改项。

3. 建模核心:RFM + K-Means 分群与人群业务化解读

3.1 为什么选 RFM + K-Means,而不是规则标签

市面上用户画像的建模思路不少:有纯规则打标签的,有用 LDA 做兴趣主题的,也有用聚类做行为分群的。这套源码选 RFM + K-Means,我认为是权衡过的结果。RFM 把每个用户压缩成三个有明确业务含义的数字,K-Means 又是聚类算法里实现最简单、训练最快的一个,两者结合非常契合人工智能项目实践这个场景:代码量小、原理好讲、结果能画图。

相比之下,DBSCAN 能发现任意形状的簇,但 eps 和 min_samples 两个参数极难调,几万用户规模下很容易要么全是一类、要么全是噪声;层次聚类画出的树状图在大样本下根本没法看。所以无论是课程设计还是给业务方做第一版画像,K-Means 都是稳妥的起点。源码没有搞花活,用的就是 sklearn 的标准实现,这也是好事,换环境复现没有额外依赖。

3.2 标准化与最优 K 值选择:肘部法怎么读数

直接拿 R、F、M 三列丢进 K-Means 是大忌。M 值的量级通常是几千到几十万,R 值是 0 到 180 的整数,欧氏距离计算时,M 会把另外两个字段完全淹没。源码里聚类前用 StandardScaler 做了 z-score 标准化,顺序是:先标准化、再聚类、最后反标准化回去解读。如果你在 2.3 做了 log 变换,这一步把 monetary 换成 monetary_log 即可,下面按原始三列演示。

from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans import numpy as np # 只对建模字段标准化,user_id 不参与 features = rfm[["recency", "frequency", "monetary"]].copy() scaler = StandardScaler() X_scaled = scaler.fit_transform(features) # 用肘部法粗选 K:看簇内误差平方和的拐点 inertias = [] for k in range(2, 9): km = KMeans(n_clusters=k, n_init=10, random_state=42) km.fit(X_scaled) inertias.append(km.inertia_) print([round(v) for v in inertias]) # 按拐点确定 K,这里假设肘部在 4 K = 4 model = KMeans(n_clusters=K, n_init=10, random_state=42) labels = model.fit_predict(X_scaled) rfm["cluster"] = labels

n_init=10 的意思是 K-Means 用 10 组不同的初始质心各跑一遍,取结果最优的一组,能有效缓解随机初始化带来的不稳定。random_state=42 必须固定,不然每次跑出来的类标签顺序不一样,后面报告和可视化全乱。肘部法的读法是看 inertias 列表里下降速率明显放缓的那个 k:比如 2、3、4 下降很快,5、6、7 开始平缓,那拐点就是 4。嫌手动看拐点太主观,可以用轮廓系数做交叉验证,最后一章给具体做法。

注意:StandardScaler 要区分 fit 和 transform。训练时 fit_transform 得到均值和方差后,把 scaler 对象保存成文件;给新用户打标签时只能 transform,不能重新 fit,否则均值方差变了,聚类结果和训练时对不上。

3.3 分群解读:把聚类编号翻译成业务人群

聚类跑完只是拿到了 cluster 编号,这一步源码做了关键的事:按类聚合成每群的 RFM 均值,再把编号映射成业务人群名字。输出物从"用户 12345 属于 cluster 0"变成"用户 12345 属于高价值活跃用户",业务方才听得懂。

# 先把每个簇的均值画像算出来 profile = rfm.groupby("cluster")[["recency", "frequency", "monetary"]].mean() # 基于均值画像给每个簇命名,阈值可按业务调整 def name_cluster(row): if row["frequency"] >= 10 and row["monetary"] >= profile["monetary"].quantile(0.8): return "高价值活跃用户" elif row["recency"] <= 30 and row["frequency"] >= 5: return "近期活跃用户" elif row["recency"] >= 120: return "流失风险用户" else: return "普通用户" cluster_names = profile.apply(name_cluster, axis=1) rfm["user_type"] = rfm["cluster"].map(cluster_names)

我一般会先看 profile 的三列均值,确认每个群的特征是否清晰可区分。比如 cluster 0 的 recency 均值只有 12 天、frequency 8 次、monetary 五千块,这是典型的近期活跃用户;cluster 2 的 recency 超过 150 天,基本可以判定为流失风险。命名规则没有标准答案,源码给的是一种参考,完全可以根据业务改阈值。映射用 dict 加 map 实现,比逐行判断快得多,几万用户时性能差别肉眼可见。

4. 标签体系与可视化:把聚类编号翻译成业务能用的画像

4.1 标签映射规则与画像宽表输出

用户画像的最终交付物是标签,不是聚类编号。cluster=0 业务方听不懂,"高价值+活跃"一听就懂。源码的标签层是独立模块,做的事情是:把聚类编号映射成可配置的业务标签,同时把用户 ID、标签、RFM 原始值输出成一张画像宽表,方便后续导入 BI 报表或 CRM 系统。

# 标签配置与计算解耦,调整阈值不影响聚类结果 tag_config = { "high_value": {"frequency_min": 10, "monetary_quantile": 0.8}, "active": {"recency_max": 30, "frequency_min": 5}, "churn_risk": {"recency_min": 120} } m_high = rfm["monetary"].quantile(tag_config["high_value"]["monetary_quantile"]) def build_tags(row): tags = [] if row["frequency"] >= tag_config["high_value"]["frequency_min"] and row["monetary"] >= m_high: tags.append("高价值") if row["recency"] <= tag_config["active"]["recency_max"] and row["frequency"] >= tag_config["active"]["frequency_min"]: tags.append("活跃") if row["recency"] >= tag_config["churn_risk"]["recency_min"]: tags.append("流失风险") return "+".join(tags) if tags else "普通" rfm["tags"] = rfm.apply(build_tags, axis=1) rfm[["user_id", "recency", "frequency", "monetary", "cluster", "tags"]].to_csv( "user_profile_result.csv", index=False, encoding="utf-8-sig" )

这个设计有个好处:标签规则和聚类模型解耦。调整标签阈值(比如把"活跃"的 recency_max 从 30 改成 45)不需要重新聚类,只改配置再跑一遍 apply 就行。输出用 utf-8-sig 编码是给中文 Windows 用户准备的,Excel 直接打开 csv 不会乱码,这个细节在交付时很加分。标签设计上允许一个用户多个标签,用加号拼接,后续做人群圈选时可以直接按标签过滤。

4.2 可视化:人群占比与特征对比图

图不用多,三张就够:人群占比饼图、各群 RFM 均值条形图、标签交叉表。占比图讲清楚"谁是主力人群",条形图讲清楚"这群人在 RFM 上到底强在哪"。

import matplotlib.pyplot as plt # 人群占比饼图 share = rfm["user_type"].value_counts() plt.figure(figsize=(6, 6)) plt.pie(share, labels=share.index, autopct="%.1f%%") plt.title("用户画像人群占比") plt.savefig("user_profile_pie.png", dpi=150) # 各人群 RFM 均值对比条形图 fig, axes = plt.subplots(1, 3, figsize=(12, 4)) for ax, col in zip(axes, ["recency", "frequency", "monetary"]): profile[col].plot(kind="bar", ax=ax, title=col) ax.set_xticklabels(profile.index, rotation=0) plt.tight_layout() plt.savefig("user_profile_rfm.png", dpi=150)

给课程报告用 matplotlib 保存 png 就够了,给业务方看可以用 pyecharts 出交互图,或者把宽表导入 BI。下面这张表是一次聚类输出的典型画像 profile,也是答辩时最常被追问的一张表:

clusterrecency 均值(天)frequency 均值(次)monetary 均值(元)人群解读
01285200高价值活跃
11562800流失风险
24542100普通
381512000高价值核心

读表时注意,monetary 列如果是 log 变换后的值,均值要换算回原始金额再写报告,否则业务会觉得数字对不上。

5. 用户画像项目五个高频坑:现象、原因与解决办法

5.1 一人多账号导致画像严重失真

现象:聚类结果里突然冒出一个"每周消费几十次"的极端群,frequency 均值是其他群的 5 倍以上。

原因:同一个真实用户在多个账号下单,RFM 按 user_id 聚合时没有做账号合并。常见于支持匿名购买、游客下单的业务,一个用户被拆成两个人,画像自然失真,高价值用户还被低估。

解决:在清洗阶段用手机号、设备指纹或收货地址做用户归一化,把同一实体的多个 user_id 合并成统一用户 ID。源码没做这层是因为示例数据是干净的,换真实数据这一步不能省,否则后面所有分群都建立在一个错误的对象粒度上。

5.2 跳过标准化,聚类被消费金额一票否决

现象:分出来的几个群,区别几乎全在 monetary 上,recency 和 frequency 各群间没有显著差异,解读时只能讲"有钱没钱"。

原因:M 值量级远大于 R 和 F,欧氏距离被金额主导,K-Means 等于只按金额切了几刀,RFM 里另外两个维度白算了。

解决:聚类前必须做 StandardScaler 或 MinMaxScaler,而且要在所有特征构造完之后、划分样本之前完成。我见过有人把标准化做在 RFM 构造之前,结果也是错的,因为标准化会跟着原始字段一起被 groupby 聚合,语义完全变掉。

5.3 K 值随手填 5,人群拆得没法解释

现象:设置 K=5 后,有两个群的 profile 均值几乎一样,业务上完全区分不开。

原因:K 选大了。K-Means 在没有明显结构的地方硬切,把本来连续分布的用户群拦腰分成两段。

解决:先跑肘部法粗选,再用轮廓系数交叉验证。轮廓系数接近 1 说明聚类紧凑,接近 0 说明重叠严重,低于 0.25 就该考虑减小 K 或换特征。还有一个土办法:K 从小往大试,出现两个群 profile 高度相似,说明已经选大了。

5.4 R 值算成负数,出现"未来用户"

现象:部分用户 recency 为负,聚类解读完全错乱,流失风险用户变成"未来用户"。

原因:ref_date 设得比数据最新日期早,或者订单表里混入了未来时间戳的脏数据,两种情况都会让最后一个订单时间大于参考日期。

解决:清洗阶段先取 order_time 的最大值,ref_date 用df["order_time"].max()动态生成,或者至少保证 ref_date 大于等于数据最大日期。前面清洗代码里写死 2023-12-31 就是提醒:换数据必须先改这一行,这是源码里最容易踩的默认值陷阱。

5.5 随机种子没固定,同一份数据两次跑出两个版本

现象:两次运行聚类,cluster 编号对不上,用户归属看起来变了,报告里的数字和演示时对不上。

原因:K-Means 初始质心是随机的,不固定 random_state 每次结果都可能不同;更隐蔽的是 n_init 默认值随 sklearn 版本变化,老版本默认 10,新版本 n_init='auto',行为不一致。

解决:固定 random_state=42,显式写 n_init=10。如果要交付给业务方,把训练好的模型和 scaler 一起用 pickle 保存,新数据进来直接 transform 和 predict,保证线上和实验完全一致。这是人工智能项目实践里最容易被低估的工程习惯,却决定了项目能不能从 jupyter 笔记本走向稳定交付。

6. 进阶验证:轮廓系数与用户切分给画像上保险

6.1 轮廓系数给聚类结果打分

聚类不是跑完就完,得有个量化指标证明它没有乱分。轮廓系数综合了簇内紧凑度和簇间分离度,取值 -1 到 1。对 RFM 这种三维特征,系数在 0.5 以上说明分群结构清晰,0.25 到 0.5 可接受,低于 0.25 建议回到特征工程重新做。代码只有一行:

from sklearn.metrics import silhouette_score score = silhouette_score(X_scaled, labels) print(f"轮廓系数: {score:.3f}")

6.2 用户切分验证:训练集和验证集结构要一致

轮廓系数衡量的是整体质量,还差一步样本层面的稳定性验证。我的做法是按用户切出 70% 做聚类,剩下 30% 只预测不打标,对比两组的人群占比结构:

from sklearn.model_selection import train_test_split tr_idx, va_idx = train_test_split(rfm.index, test_size=0.3, random_state=42) model = KMeans(n_clusters=K, n_init=10, random_state=42).fit(X_scaled[tr_idx]) va_labels = model.predict(X_scaled[va_idx]) tr_dist = pd.Series(model.labels_).value_counts(normalize=True).sort_index() va_dist = pd.Series(va_labels).value_counts(normalize=True).sort_index() print(pd.DataFrame({"train": tr_dist, "val": va_dist}))

如果某个群在训练集占 20%、验证集只有 5%,说明这个群是靠少数样本撑起来的,模型对初始化和抽样太敏感,交付出去会翻车;反之两组占比接近,说明分群结构稳定,可以放心解读。从那以后我每次做用户画像都强制走一遍"轮廓系数 + 切分验证",这两个检查加起来不到十分钟,却能挡住绝大多数"答辩时换台机器结果就变了"的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询