尧图精选

用Python实现用户画像系统:标签体系、RFM分层与工程实践

🕒 发布时间:2026/10/2 1:10:38 📁 来源:尧图网络
简介基于Python的用户画像生成系统源码以数据驱动方式构建用户特征模型适合数据分析、推荐系统及精细化运营相关开发者学习参考。资源包含完整项目代码与Web展示页面覆盖数据收集预处理、特征工程、用户行为分析、聚类、特征权重计算、画像生成与可视化等模块结合pandas、sklearn、Flask等常用技术栈可直接用于二次开发或毕设演示。压缩包共149个文件以Python脚本69个py为主另有53个pyc编译文件及5个HTML页面、CSS/JS前端资源、CSV数据文件和图表与字体素材整体体积仅2.45MB轻量易部署。目前已有352人学习下载。通过阅读源码可以掌握用户画像系统的完整工程结构、数据清洗与编码处理思路、KMeans等聚类方法的实际应用以及利用Flask将画像结果以可视化界面呈现的集成方法对理解从原始数据到可交互画像服务的全链路具有直接帮助。1. 用户画像不是玄学一个 Python 原型能解决什么做用户运营的同学常被问“你的用户长什么样”如果你的回答还是一张按性别年龄切好的报表那叫人口统计不叫用户画像。真正能指导选品、推荐和活动策略的画像最少要回答三个问题用户是谁、用户做过什么、用户接下来可能做什么。这套基于 Python 实现的用户画像生成系统就是用订单数据和埋点行为数据把这三个问题转化成一套可查询、可更新、可校验的标签结构。它能解决的最实际问题是把散落在订单表、商品表、访问日志里的用户痕迹加工成“近 90 天高活跃、偏好数码配件、价格敏感度高、处于挽留期”这样的结构化标签并输出成 JSON、Excel 或接口供业务调用。适合的人群很明确准备做精细化运营但没有现成画像平台的中小团队以及想搞懂画像系统内部逻辑的数据开发初学者。我给你的不是一份“跑完就删”的脚本而是一个能在单机复现、可以对着改数据源和标签口径的最小实现。下面这套方案我把它拆成标签设计、特征加工、价值计算、工程避坑和上线验证五个部分全程只用 pandas 和轻量 API数据量在百万级以内不需要上集群。2. 画像标签体系怎么设计先拆维度再写代码2.1 为什么标签体系要先于代码大多数用户画像项目做砸不是代码写不出来而是标签口径没定。同一个“高活跃”运营按登录天数定义算法按行为事件数定义BI 按停留时长定义三张表对不上最后谁也信不过谁。所以实现用户画像生成系统的第一步不是写 Python而是拉业务方把标签字典定下来。我做这套系统时的经验是先把用户维度拆成两类事实标签和推断标签。事实标签来自订单和注册资料比如性别、年龄段、城市等级这类标签不需要模型直接 SQL 或 pandas 就能取出推断标签来自行为统计比如活跃度分层、品类偏好、消费能力这类标签需要设定规则或简单模型是画像系统的核心价值所在。2.2 画像标签字典一份可以直接抄的表下面这份五维标签字典是我在电商场景下沉淀的通用模板。维度分为基础属性、活跃度、消费价值、品类偏好和渠道偏好每个维度都有对应的标签示例、数据来源、更新频率和计算口径。换到内容平台或 SaaS 系统只需要替换“品类”为内容分类或功能模块其它结构可以直接复用。2.3 画像存储结构设计从宽表到 JSON标签计算完成后要同时产出面向分析用的宽表以及面向在线查询用的 JSON 文档。宽表给运营跑 SQL 用JSON 给推荐服务或客服工作台用。两者必须同源否则就是两张皮。下面这段代码是画像结构定义的核心逻辑我一般会把“标签字典”和“画像输出结构”写成配置而不是散落在业务代码里# 画像输出结构定义按维度聚合标签保证前端/接口展示结构统一 profile_schema { user_id: , # 用户标识 basic: {gender: , age_band: , city_level: }, active: {active_level: , last_active_days: 0}, value: {value_segment: , rfm_code: }, preference: {top_categories: [], price_sensitivity: }, channels: {first_channel: , recent_channel: }, } def empty_profile(user_id: str) - dict: 生成一个空画像模板后续逐维度回填 import copy p copy.deepcopy(profile_schema) p[user_id] user_id return p这段代码没有什么高深算法但它定了画像的“容器”。后续所有维度标签都是往这个空模板里填值保证每个用户输出的 JSON 都是同一把尺子。参数说明age_band在写入前要做分箱比如 18-24 映射为 young_adultcity_level来自城市表维护的一线城市、二线城市等枚举top_categories最好限制 3 到 5 个太多会稀释偏好表达。这里有一个关键决策画像要不要把全部历史行为算进去我见过一开始按全量数据打标签结果系统越跑越慢最后不得不加数仓调度任务的做法。正确姿势是按时间窗计算因为用户的偏好和价值是漂移的。我的习惯是活跃度按近 30 天偏好按近 90 天。这个时间窗口要在第 3 章的特征加工和标签字典里同时生效两边口径一致才不会出结果矛盾。3. 行为特征加工从订单和埋点日志里抽特征3.1 数据源长什么样先看字段再谈加工画像系统的输入通常有两大类订单表和埋点行为日志。订单表结构大致是 order_id、user_id、product_id、category_id、pay_amount、order_time、status埋点日志大致是 user_id、behavior_typeclick/fav/cart/pay、item_id、category_id、event_time。动手前先摸清两张表的主键粒度订单表一行是一笔订单日志表一行是一次行为后者比前者大一到两个数量级。我踩过的第一个大坑是 user_id 在两张表里的类型不一致订单表里是整数日志表里是字符串join 后直接少掉一批用户。所以第一步要做 ID 归一化。import pandas as pd # 订单表读取与字段规范统一 user_id 类型过滤无效订单 orders pd.read_csv(orders.csv, parse_dates[order_time]) orders[user_id] orders[user_id].astype(str).str.strip() orders orders[orders[status].isin([paid, completed])] # 剔除退款/未支付 orders[category_id] orders[category_id].astype(str) # 行为日志读取与字段规范同名字段不能直接 join先做类型对齐 logs pd.read_csv(events.csv, parse_dates[event_time], nrows2000000) logs[user_id] logs[user_id].astype(str).str.strip() logs[behavior_type] logs[behavior_type].str.lower() logs[category_id] logs[category_id].astype(str) print(f订单用户数: {orders[user_id].nunique()}, 日志用户数: {logs[user_id].nunique()})这段处理的逻辑先把 user_id 统一转成字符串并去空格避免“10086”和 10086 两张表对不上再过滤掉未付款和退款订单退款单对消费能力标签是强干扰行为日志加 nrows 限制是为了在原型阶段快速验证调通后再全量读入。参数说明parse_dates指定时间列能省掉后面 panda 的 to_datetime 转换astype(str)一定要在 strip 之前做否则整数类型的列没有 strip 方法。3.2 构造用户维度的特征扩展表原始表都是行为粒度不能直接打标签需要按 user_id 做 groupby 聚合成“用户特征宽表”。这一步是画像系统的核心加工环节所有后续的 RFM 和价值分层都以这张表为基础。# 聚合用户维度特征近30天活跃天数、近90天行为分布、首次/最近行为时间 from datetime import timedelta now pd.Timestamp(2024-06-30) # 按数据截止时间计算不用当天避免重跑结果漂移 # 近30天活跃按日期去重统计 logs[event_date] logs[event_time].dt.date recent30 logs[logs[event_time] now - timedelta(days30)] active_days ( recent30.groupby(user_id)[event_date] .nunique() .rename(active_days_30) ) # 近90天行为类型计数四类行为各一列 recent90 logs[logs[event_time] now - timedelta(days90)] behavior_pivot ( recent90.groupby([user_id, behavior_type]) .size() .unstack(fill_value0) .rename(columns{click: click_cnt, fav: fav_cnt, cart: cart_cnt, pay: pay_cnt}) )这段代码的逻辑先把行为时间切成近 30 天和近 90 天两个窗口用 event_date 去重算活跃天数用 behavior_type 透视得到行为计数矩阵。参数说明里有一个非常重要的小细节基准时间now不要写成pd.Timestamp.today()否则定时重跑同一批数据会产生不同的“近 30 天”导致画像每天都不一样。我通常的做法是取数据文件的最大时间戳或者由调度任务传入数据日期参数。3.3 订单侧特征消费金额与品类结构订单侧主要产出消费能力和品类偏好两类特征。消费能力不能只看一笔最大订单要用近 90 天的累计支付金额加购买次数品类偏好不能只看购买次数要把浏览、收藏、加购行为也纳入才能识别“想买还没买”的潜在偏好。# 订单侧聚合金额、频次、品类购买结构 recent_orders orders[orders[order_time] now - timedelta(days90)] order_stats ( recent_orders.groupby(user_id) .agg( total_amount(pay_amount, sum), order_cnt(pay_amount, size), avg_amount(pay_amount, mean), max_amount(pay_amount, max), ) ) # 品类偏好用购买次数代表偏好强度取每个用户的前3个品类 cat_stats ( recent_orders.groupby([user_id, category_id]) .size() .rename(cat_cnt) .reset_index() ) # 对每个用户排序取 TOP3 cat_stats[rank] cat_stats.groupby(user_id)[cat_cnt].rank(dense, ascendingFalse) top_cats cat_stats[cat_stats[rank] 3].sort_values([user_id, rank])参数说明rank(dense)和rank(first, ascendingFalse)的区别是dense 在计数并列时会保留同一名次避免前 3 个品类因为并列被挤掉业务上属于可以接受的取舍。品类表字段是 category_id最终输出标签时建议把 ID 和品类名做映射运营同学看“digital_accessories”比看“C01”直观得多。4. 标签计算与画像落库RFM 分层到接口输出4.1 RFM 计算用分位数切分不拍脑袋定阈值用户价值分层我用 RFM 模型的三维框架R 表示最近一次购买距今天数F 表示近 90 天购买次数M 表示近 90 天累计金额。阈值不拍脑袋而是用数据自身分位数。常见做法是先按 R、F、M 分别打 1 到 4 分再组合成价值分组。这里先算 R再合并之前得到的特征表计算 RFM 的总分用四分位数来划分用户价值的层级。# R 值计算最近一次购买距离基准日期的天数 last_buy orders.groupby(user_id)[order_time].max().apply( lambda x: (now - x).days ).rename(recency) # 合并成 RFM 宽表 rfm pd.DataFrame({ recency: last_buy, frequency: order_stats[order_cnt], monetary: order_stats[total_amount], }) # 分位数打分越小越好recency越高越好frequency/monetary rfm[r_score] pd.qcut(rfm[recency], 4, labels[4, 3, 2, 1]).astype(int) rfm[f_score] pd.qcut(rfm[frequency].rank(methodfirst), 4, labels[1, 2, 3, 4]).astype(int) rfm[m_score] pd.qcut(rfm[monetary].rank(methodfirst), 4, labels[1, 2, 3, 4]).astype(int) # 简化分组RFM_Score 高于中位数 3 分且金额不低 - 重要价值用户 rfm[value_segment] 一般用户 rfm.loc[(rfm[r_score] 3) (rfm[f_score] 3) (rfm[m_score] 3), value_segment] 重要价值 rfm.loc[(rfm[r_score] 3) (rfm[f_score] 3) (rfm[m_score] 3), value_segment] 重点发展 rfm.loc[(rfm[r_score] 3) (rfm[f_score] 3) (rfm[m_score] 3), value_segment] 重点保持 rfm.loc[(rfm[r_score] 3) (rfm[f_score] 3) (rfm[m_score] 3), value_segment] 重点挽留这里有一个容易翻车的细节pd.qcut 在数据分布不均匀时会产生重复边界导致某一段是空桶直接报错。frequency和monetary这类幂律分布数据直接切分位数会不稳妥。我一般先对列做rank(methodfirst)再 qcut相当于把并列值打散保证每个分位桶里都有样本。参数说明labels[4,3,2,1]的顺序要和 qcut 默认左开右闭的区间对应如果标反了你会得到“最近购买的用户 R 分反而最低”的诡异结论。4.2 行为权重矩阵让偏好识别代替关键词统计仅仅按购买次数取前 3 品类会把“收藏了很多却没买”的潜在偏好漏掉。所以品类偏好标签我一般用行为加权评分四类行为赋值分别计算购买、加购、收藏、点击的权重再按总分排序得到更接近真实意图的偏好排序。分类别打分的公式也简单weighted_score 点击权重乘点击次数 收藏权重乘收藏次数 加购权重乘加购次数 购买权重乘购买次数。# 品类偏好按行为类型加权统计 # 权重赋值逻辑购买行为最能代表已转化偏好点击仅代表路过 behavior_weight {click: 1, fav: 2, cart: 3, pay: 5} log_weight recent90.copy() log_weight[weight] log_weight[behavior_type].map(behavior_weight) log_weight[weighted_score] log_weight[weight] * 1 # 每行是一条行为 # 按用户品类汇总加权得分 cat_pref ( log_weight.groupby([user_id, category_id])[weighted_score] .sum() .rename(pref_score) .reset_index() ) cat_pref[rank] cat_pref.groupby(user_id)[pref_score].rank(dense, ascendingFalse) cat_pref_top3 cat_pref[cat_pref[rank] 3]权重参数这里的取值其实就是业务策略没有绝对正确。把购买设为 5 是为了让它主导排序但如果你发现“收藏强但购买弱”的用户被压得太狠可以把收藏提到 3、购买降为 4。这个权重表应该设计成配置项方便按月调整不建议写死在业务代码里。4.3 生成宽表和画像 JSON给运营和接口各一份交付物所有特征加工完成后需要做一次全维度合并生成最终的画像宽表并同步导出画像 JSON 文件。宽表是给数据分析师做透视用的JSON 是给业务接口用的。我一般会把画像生成做成一个函数输入是订单和日志输出是画像表和 JSON 目录。# 合并全维度特征生成用户画像宽表 profile_df active_days.to_frame().join(order_stats, howleft).join(rfm, howleft) profile_df profile_df.join(cat_pref_top3.groupby(user_id)[category_id].apply(list).rename(top_cats)) # 填充缺失值新手最容易忽略的一步 profile_df[[total_amount, order_cnt, avg_amount, max_amount]] ( profile_df[[total_amount, order_cnt, avg_amount, max_amount]].fillna(0) ) profile_df[recency] profile_df[recency].fillna(999) # 无购买记录用户 # 导出宽表供 SQL/Excel 分析 profile_df.to_csv(user_profile_wide_table.csv, indexTrue) # 导出单用户 JSON 供接口使用 import json profile_df.reset_index(inplaceTrue) profiles {} for _, row in profile_df.iterrows(): p empty_profile(str(row[user_id])) p[active][active_level] high if row[active_days_30] 10 else mid p[value][value_segment] row[value_segment] p[preference][top_categories] row[top_cats] if isinstance(row[top_cats], list) else [] profiles[p[user_id]] p with open(user_profiles.json, w, encodingutf-8) as f: json.dump(profiles, f, ensure_asciiFalse, indent1)参数说明recency 填充 999 表示“从未购买”后续会有规则把这类用户另归一组活跃度阈值我在这里写的是 10 天但真实项目里应该看过用户分布再定比如用 pandas 的 describe 看 30 天活跃天数的分位数取 P50 和 P75 作为中高活跃两个切点。JSON 输出用ensure_asciiFalse是为了保留中文标签原名否则写入文件后全变成 \uXXXX 转义序列业务方拿到根本没法读。5. 用户画像搭建避坑5 个高频翻车点与排查命令5.1 订单里藏看单退款单没过滤消费能力标签整体失真现象画像系统刚上线运营发现一批消费金额极高的“高价值用户”定向发券后一个月内大量退货。原因我只按状态包含 paid 就统计了金额没排除掉后来的“refunded”状态订单导致同一批货既计入正向金额、又没有在退款后扣减。解决在订单聚合前增加过滤条件并且按订单状态做二次校验。实际情况里很多团队直接用订单表的 status 字段再做一层 groupby 核对才能发现金额异常偏大。# 排查思路比较过滤前后总金额差距 orders[status].value_counts() refunded orders[orders[status] refunded] print(f退款订单金额占比: {refunded[pay_amount].sum() / orders[pay_amount].sum():.2%})排查命令的逻辑先看状态枚举里到底有多少个值再单独统计退款金额占比。如果这个占比超过 5%说明你的画像消费金额整体被抬高需要回溯清洗。5.2 时间窗口写死当天每天重跑同一用户画像结果漂来漂去现象画像系统每周重跑一次但同一用户本周画像和上周完全不一致。原因代码里直接用了pd.Timestamp.now()取当天做窗口边界周与周之间“近 30 天”不是一个固定区间新数据也没统一补到同一分区。解决所有行为窗口统一使用数据批次日期参数由调度任务传入跑批和补数不混用同一个时间基准。这个问题的核心是不要用脚本运行时的当前时间作为业务统计时间。真实项目里应该把biz_date写成函数参数命令行加--biz_date2024-06-30每次跑批只要使用同一个参数结果就可复现。5.3 新用户全部掉进“无价值”桶冷启动规则缺失现象上线两周内注册的新用户因为没有任何购买记录被统一分到“一般用户”运营无法对新人做差异化触达。原因RFM 只按购买行为打分新用户天然缺失 M 和 F。解决增加冷启动修正规则——注册小于 14 天且有过浏览行为的用户单独标记为 new_user有收藏或加购行为的标记为 potential_user不参与 RFM 分层。“冷启动”这个坑不只是电商内容产品的用户画像同样会对新注册无行为的用户直接打上低活跃标签。我的习惯是把“无行为”和“新注册”明确区分开前者可能流失后者还在观察期策略上完全不同。5.4 pandas 内存爆掉两千万行日志按用户全表笛卡尔 join现象脚本跑在 16G 内存的机器上处理 2000 万行日志时进程直接 OOM。原因行为日志做 groupby 后没有及时释放中间表后续 join 又把多张大 DataFrame 全部压在内存里。解决先按 user_id 聚合压缩行数再 join 订单侧特征中间变量用 del 释放数据量上亿时改用 dask.dataframe 或按日期分片处理。这个在本地用 Python 做原型时尤其常见。我一般会监控脚本内存占用优先调整数据处理顺序再考虑换工具。遇到日志行数极大时可以先用 pyarrow 的列式格式存中间结果比 CSV 快一个量级。5.5 画像标签更新策略混乱全量每天重算凌晨任务跑不完现象日活用户 1000 万的系统画像任务每 40 分钟一个批次全天都在跑数据业务方仍然投诉画像滞后。原因所有标签都按同一频率全量重算高频标签和低频标签没有区分任务。解决把标签按时效性分组活跃度标签每小时增量计算消费价值标签每日全量计算品类偏好每 6 小时计算一次。否则等到画像就算出来了运营看的还是昨天的活动数据。我在用户画像系统里遇到的最常见问题就是调度频率没设计好——没有必要把全部标签都做成实时计算。通过 MySQL 存储最新标签版本用版本号控制读取后端服务按需读取可以避免标签值更新导致接口返回不一致。具体做法是把画像 JSON 写入存储后加一个 updated_at 全局版本字段业务侧请求时带版本号避免用户端加载到一半出现新旧标签的混合结果。6. 画像上线前怎么验证覆盖率和标签纠偏的实战手段画像系统上线前必须做覆盖率和准确性两层验证。覆盖率指的是画像中有标签的用户占全部注册用户的比例通常基础属性覆盖率低于 95% 就需要排查数据接入问题行为标签覆盖率则允许明显分层比如近 90 天有行为的用户覆盖 60%完全无行为用户标记为 cold。准确性验证的做法是从画像结果中随机抽取 100 到 200 个用户去原始日志核对标签与真实行为是否匹配比如“偏好配件类”用户近 90 天订单中配件订单是否占比领先。这是一个抽样核对脚本用来随机核验画像标签的准确率。# 从画像中抽样核对标签一致性以 top_categories 品类偏好为例 import random sample_profiles random.sample(list(profiles.items()), min(len(profiles), 200)) hit_cnt 0 miss_examples [] for user_id, prof in sample_profiles: top_cats prof[preference][top_categories] if not top_cats: continue user_orders recent_orders[recent_orders[user_id] user_id] actual_top user_orders[category_id].value_counts().index[0] if len(user_orders) else None if actual_top in top_cats: hit_cnt 1 else: miss_examples.append((user_id, top_cats, actual_top)) print(f抽查标签一致率: {hit_cnt / len([p for p in sample_profiles if p[preference][top_categories]]):.1%}) if miss_examples: print(不一致示例个数:, len(miss_examples))参数说明抽查数量 200 是因为一致性比率在正负 5% 的置信区间内基本够了如果目标精度更高就抽 500。miss 不一定代表代码错可能是订单数据里 latest 聚合的品类和第二大品类差距很小标签本身在边界上模糊这种允许存在。做完验证并且效果稳定后可以把每个用户画像的版本号作为后缀写入 JSON 文件名或记录在数据库的 profile_version 字段里。线上排查问题时直接根据版本号找到对应的画像生成日志。手动纠偏的情况很常见运营实际确认了某用户是“品牌偏好型”但系统因为历史行为不够而打了“价格敏感”。这时可以通过一张人工标签覆盖表来修正把人工标签作为最高优先级合并到画像中等系统行为数据积累后再自动判定能否覆盖。最后把三大习惯和落版号建议总结一下希望你能照着搭一套自己的画像系统。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →