Python用户画像实战:基于Jupyter Notebook的RFM聚类分群源码解析
简介这份资源是一套基于Jupyter Notebook的Python用户画像构建源码面向数据分析初学者与从事用户研究的从业者帮助其掌握从原始数据到画像输出的完整流程。压缩包共20个文件约118.46MB以13个CSV数据文件为核心载体配合4个ipynb笔记本承载数据处理与建模代码另有1个py脚本封装可复用函数、1个png图像展示关键结果、1个txt说明文档结构清晰便于按模块学习。内容覆盖用户行为分析、特征提取与可视化呈现涉及Pandas数据处理及Matplotlib、Seaborn图表绘制等常用技能并包含泰坦尼克号数据挖掘等实践案例。目前已有125人学习下载。读者可借此获得一套可直接运行的画像构建范例理解数据清洗、整合与特征工程的组织方式并参考其分析思路迁移到市场分析、产品设计或公共服务评估等场景。1. 从一份 Jupyter Notebook 源码说起用户画像到底在算什么电商运营丢过来一份订单表问“能不能把用户分个层我想知道哪些人值得发券”。这个场景里用户画像不是画一个人长什么样而是把散落在订单、行为、属性表里的数据压缩成几个能驱动决策的标签高价值、流失预警、价格敏感、复购潜力。基于 Jupyter Notebook 的 Python 用户画像构建源码分享核心价值就在于把“数据清洗 → 特征计算 → 聚类分群 → 标签落库”这条链路用可交互、可复现的 Notebook 串起来而不是丢一个跑不通的脚本。它适合两类人一类是刚学完 pandas 想找个完整项目练手的 Python 入门者另一类是手里有业务数据、需要快速验证分群逻辑的数据分析从业者。Notebook 的好处是每一步都能看到中间结果调参不用反复重启进程这对画像这种需要反复试阈值的工作特别友好。2. 环境与数据准备把 Jupyter Notebook 跑起来并喂进第一份数据2.1 用 Miniconda 装出一套不打架的 Python 环境很多人卡在第一步不是代码写错而是环境里包版本互相冲突。我一般不会在系统 Python 上直接装而是用 Miniconda 建独立环境。Miniconda 安装后如何使用 Jupyter Notebook顺序是这样的先装 Miniconda再建环境再装包最后启动。下面这套命令在 Windows、macOS、Linux 上基本一致只是路径写法略有差别。# 创建名为 user_profile 的独立环境指定 Python 3.10 conda create -n user_profile python3.10 -y # 激活环境 conda activate user_profile # 安装画像常用四件套数据处理、数值计算、绘图、Notebook conda install pandas numpy matplotlib scikit-learn jupyter -y # 启动 Notebook浏览器会自动打开 jupyter notebook逻辑说明conda create -n的-n后面跟环境名python3.10是版本约束写死版本是为了避免不同机器上 pandas 行为不一致。jupyter notebook启动后默认监听本地 8888 端口如果提示端口占用加--port 8889换一个。参数上-y表示自动确认适合脚本化操作手动操作时可以去掉看清楚它要装哪些包。提示如果启动时显示找不到指定的程序八成是 conda 环境没激活就敲了 jupyter先conda activate user_profile再启动。2.2 画像数据的最小字段集与读入方式用户画像不需要一开始就上大数据平台一张订单明细表加一张用户属性表就能跑通最小闭环。订单表至少要有用户 ID、订单时间、订单金额、商品类目属性表要有用户 ID、注册时间、城市、渠道。下面用 pandas 读入并做第一次体检。import pandas as pd import numpy as np # 读入订单表和用户属性表注意编码中文 CSV 常见 gbk orders pd.read_csv(orders.csv, encodinggbk, parse_dates[order_time]) users pd.read_csv(users.csv, encodinggbk, parse_dates[register_time]) # 第一次体检看形状、看缺失、看时间范围 print(订单表形状:, orders.shape) print(属性表形状:, users.shape) print(缺失值:\n, orders.isnull().sum()) print(订单时间范围:, orders[order_time].min(), 到, orders[order_time].max()) # 把订单金额转成数值异常字符变 NaN 再填 0 orders[amount] pd.to_numeric(orders[amount], errorscoerce).fillna(0)逻辑说明parse_dates让 pandas 直接把时间列解析成 datetime后面算最近一次购买间隔才不用再转。errorscoerce是关键参数遇到“100”这种带符号的脏数据会变成 NaN而不是直接报错中断。fillna(0)是保守处理金额缺失按 0 算避免影响求和但如果缺失比例超过 30%就要回头找数据源不能硬填。2.3 用 RFM 把原始表压成用户级特征RFM 是画像里最稳的起点R 是最近一次购买距今天数F 是购买频次M 是累计金额。这三个指标几乎不依赖业务理解先算出来后面聚类才有输入。# 设定一个观察基准日通常取数据里最大日期加一天 snapshot_date orders[order_time].max() pd.Timedelta(days1) # 按用户聚合算出 R、F、M rfm orders.groupby(user_id).agg( last_order(order_time, max), frequency(order_id, nunique), monetary(amount, sum) ).reset_index() # R 值基准日减去最近一次购买日期 rfm[recency] (snapshot_date - rfm[last_order]).dt.days # 只保留建模需要的三列 rfm rfm[[user_id, recency, frequency, monetary]] print(rfm.describe())逻辑说明groupby(user_id)把订单粒度压成用户粒度nunique统计订单号去重数量避免同一订单多行被重复计数。snapshot_date用最大日期加一天是为了让最近购买的用户的 R 值为 1 而不是 0方便后续做对数变换。describe()看分布如果 monetary 的最大值和均值差两个数量级说明有极端值后面要处理。3. 特征工程与聚类把 RFM 变成可解释的用户分群3.1 标准化之前先处理偏态否则聚类会被大额用户带偏RFM 三个指标量纲不同R 是几十到几百天M 可能是几块到几万块。直接丢进 KMeans距离会被 M 主导。常见做法是先做对数变换压偏态再标准化。from sklearn.preprocessing import StandardScaler # 对 frequency 和 monetary 做 log1p 变换recency 本身偏态不严重可不动 rfm[frequency_log] np.log1p(rfm[frequency]) rfm[monetary_log] np.log1p(rfm[monetary]) # 选取三列做标准化 features rfm[[recency, frequency_log, monetary_log]].copy() scaler StandardScaler() features_scaled scaler.fit_transform(features) print(标准化后均值:, features_scaled.mean(axis0).round(3)) print(标准化后标准差:, features_scaled.std(axis0).round(3))逻辑说明log1p是log(1x)专门处理有零值的情况frequency 可能为 0 的用户不会报错。StandardScaler做的是减均值除标准差让三列在同一尺度。参数上fit_transform只在训练集上用如果后面有新用户要预测必须用同一个 scaler 的transform不能重新 fit否则新旧用户不在同一空间。3.2 用肘部法则和轮廓系数定 K别拍脑袋选 4 或 5分几群是画像里最容易被问的问题。我的习惯是跑 K 从 2 到 8同时看惯性和轮廓系数两个指标交叉验证。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score inertias [] silhouettes [] K_range range(2, 9) for k in K_range: kmeans KMeans(n_clustersk, random_state42, n_init10) labels kmeans.fit_predict(features_scaled) inertias.append(kmeans.inertia_) silhouettes.append(silhouette_score(features_scaled, labels)) # 打印结果人工看拐点 for k, inertia, sil in zip(K_range, inertias, silhouettes): print(fK{k}, 惯性{inertia:.1f}, 轮廓系数{sil:.3f})逻辑说明random_state42固定随机种子保证每次跑结果一致方便复现。n_init10表示用 10 组不同初始中心跑取最优避免陷入局部最优。惯性下降变缓的拐点就是候选 K轮廓系数越接近 1 越好但实际业务里经常选轮廓系数不是最高、但分群可解释性最好的那个 K。3.3 给每个簇打上业务标签画像才算落地聚类出来的 0、1、2 没有意义必须回头算每个簇的 RFM 均值再翻译成人话。# 用选定的 K 重新训练这里假设选 K4 kmeans KMeans(n_clusters4, random_state42, n_init10) rfm[cluster] kmeans.fit_predict(features_scaled) # 看每个簇的 RFM 均值 cluster_summary rfm.groupby(cluster).agg( recency_mean(recency, mean), frequency_mean(frequency, mean), monetary_mean(monetary, mean), user_count(user_id, count) ).round(1) print(cluster_summary) # 按业务规则打标签R 低 F 高 M 高 高价值R 高 F 低 流失预警 def label_cluster(row): if row[recency_mean] 30 and row[monetary_mean] 1000: return 高价值活跃 elif row[recency_mean] 90 and row[frequency_mean] 2: return 流失预警 elif row[monetary_mean] 200: return 低价值长尾 else: return 潜力复购 cluster_summary[label] cluster_summary.apply(label_cluster, axis1) print(cluster_summary[[label, user_count]])逻辑说明groupby(cluster).agg把每个簇的均值算出来user_count看各群人数是否均衡如果某一群只有几个人说明 K 选大了或者有离群点。label_cluster里的阈值 30、90、1000、200 是示例实际要按业务客单价和复购周期调整不能照搬。打完标签后把user_id和label两列导出成 CSV就能给运营用了。4. 避坑与排查画像源码跑不通时先看这五处4.1 中文列名或编码导致读文件直接报错现象pd.read_csv报UnicodeDecodeError或者列名变成乱码。原因CSV 是 GBK 编码pandas 默认 UTF-8。解决加encodinggbk如果还不行试encodinggb18030它兼容范围更广。列名乱码的话读入后用df.columns df.columns.str.encode(latin1).str.decode(gbk)修一次。4.2 时间列没解析算 R 值时直接抛类型错误现象snapshot_date - rfm[last_order]报TypeError。原因order_time还是字符串不是 datetime。解决读文件时加parse_dates[order_time]或者读入后pd.to_datetime(df[order_time], errorscoerce)。注意errorscoerce会把解析失败的行变成 NaT之后要检查有多少 NaT比例高说明时间格式不统一。4.3 聚类结果每次跑都不一样没法复现现象同样的数据两次运行分群结果不同。原因KMeans 初始中心随机没固定种子。解决KMeans(n_clustersk, random_state42, n_init10)random_state固定随机性n_init提高找到更优解的概率。如果换了 scikit-learn 版本结果仍有差异把版本号记在 Notebook 第一格方便回溯。4.4 极端值把标准化拉偏小群被吞掉现象某个用户一年买了 50 万聚类后他自己一群其他人挤在一起。原因monetary 极端值在标准化后仍然很大。解决先做log1p再标准化如果还有极端值用分位数截断比如把 monetary 超过 99% 分位的值压到 99% 分位。截断前先确认这些极端值是不是真实业务用户别把大客户误伤。4.5 新用户预测时重新 fit 标准化器标签全乱现象训练时用fit_transform预测新用户时又fit_transform一次结果新用户分群和训练时对不上。原因两次 fit 的均值和标准差不同。解决把训练时的 scaler 用joblib.dump存下来预测时joblib.load再transform。Notebook 里可以把 scaler 和 kmeans 一起存下次直接加载。5. 把 Notebook 变成可交付的画像工具导出、调度与增量更新Notebook 适合探索但运营不会每天打开 Notebook 点运行。我的做法是探索阶段在 Notebook 里调通稳定后把核心逻辑抽成profile.py用jupyter nbconvert --to script导出再整理。增量更新时不要每次全量重跑而是把历史用户的标签存成一张表新数据只算新增用户和需要更新的用户。下面这段是导出标签并做增量合并的最小写法。import joblib # 保存模型和标准化器供下次增量预测使用 joblib.dump(scaler, scaler.pkl) joblib.dump(kmeans, kmeans.pkl) # 导出用户标签 rfm[[user_id, cluster]].to_csv(user_cluster.csv, indexFalse, encodingutf-8-sig) # 增量读入新订单用同一套 scaler 和 kmeans 预测 new_orders pd.read_csv(new_orders.csv, encodinggbk, parse_dates[order_time]) new_rfm new_orders.groupby(user_id).agg( recency(order_time, lambda x: (snapshot_date - x.max()).days), frequency(order_id, nunique), monetary(amount, sum) ).reset_index() new_rfm[frequency_log] np.log1p(new_rfm[frequency]) new_rfm[monetary_log] np.log1p(new_rfm[monetary]) new_scaled scaler.transform(new_rfm[[recency, frequency_log, monetary_log]]) new_rfm[cluster] kmeans.predict(new_scaled)逻辑说明joblib.dump把 scaler 和 kmeans 序列化utf-8-sig带 BOMExcel 打开中文不乱码。增量预测时transform和predict分开调用保证新用户落在训练时的同一空间。lambda x里算 recency 用的是同一个snapshot_date如果跨月更新snapshot_date 要跟着数据窗口滚动否则 R 值会整体偏大。验证方法上我习惯抽 20 个用户手工核对看他的订单时间、金额、频次和模型给的标签是否一致。如果高价值群里混进了只买过一次的用户回头检查 monetary 的 log 变换是不是没做或者标准化器加载错了。这套源码分享的价值不在代码多长而在每一步都能在 Notebook 里看到输入输出改一个参数立刻知道影响哪群人。我自己踩过最深的坑是拿训练集重新 fit 标准化器去预测标签全乱排查了一下午才发现是这行。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →