
简介一套基于Python实现的用户画像生成系统完整源码面向数据分析、产品运营及后端开发者覆盖数据收集与预处理、特征工程、用户聚类、特征权重计算、画像构建和可视化展示等完整环节解决从原始用户数据到可交互画像平台的工程化落地问题。压缩包共149个文件包含69个Python脚本、53个pyc编译文件以及HTML/CSS/JS前端页面、CSV示例数据和字体图表资源整体仅2.45MB结构清晰便于直接部署或二次开发。目前已有352人学习下载适合具备基础Python与机器学习知识、希望快速搭建用户画像系统的读者。源码中提供了pandas数据清洗、sklearn特征处理与聚类、Flask风格服务接口等可复用代码还附带可运行的Web页面和示例数据帮助读者深入理解从行为数据到用户标签的完整链路并可在此基础上扩展推荐系统或精准运营工具。1. 用户画像系统不是报表先想清楚它解决什么问题运营要“把高价值用户拉出来”你从订单表里 group by 了一晚上第二天交张 Excel运营说“我要的不是这个”。确实手工拼表只能回答“谁买了”回答不了“谁快流失了、谁喜欢什么、该给谁推什么”。基于 python 实现用户画像生成系统本质是把“用户做过什么”的行为数据加工成“用户是谁”的标签化表达。这套系统能解决三件事圈人人群筛选、特征供给推荐/风控、业务监控分层运营。适合正在做精细化运营的团队、数据工程师和想转行数据开发的后端。下面从头到尾拆一个最小可落地的方案从标签设计讲到接口上线。2. 标签体系怎么设计从原始行为到可计算标签的映射规则2.1 标签分层的常见做法事实标签、规则标签、模型标签做画像第一个翻车点是上来就写代码没先定标签口径。标签不是拍脑袋起的名字它得能被一段代码稳定地算出来而且要能被业务解释。业内普遍把标签拆成三层每一层的计算方式和维护成本都不一样。标签类型计算方式更新频率典型示例事实标签直接从业务表取字段不加工低频日更/周更性别、注册时间、注册渠道规则标签按业务口径写判断逻辑日更为主活跃度分层、RFM 分层、高价值用户模型标签用机器学习预测周更/月更流失概率、品类偏好、付费意愿我一般建议先做事实标签和规则标签模型标签放到后面再上。原因很直接模型标签有准确率问题预测错了会直接影响运营动作上线后需要人持续盯着团队没有数据算法经验时很容易把整个画像项目的口碑带崩。事实和规则标签哪怕口径写错也容易人工发现并修正。三层标签之间的关系是层层递进。事实标签是最底层的“原料”规则标签是对原料的加工解释模型标签则是在事实和规则基础上做预测。设计标签体系时先把这三层画在一张表里标清楚每个标签的数据来源、计算逻辑、更新频率后面写代码只是把这张表翻译成程序。2.2 用 Python 定义标签口径一个可配置的标签字典标签口径必须可配置、可审计。常见做法是用一个 Python 字典保存所有标签的定义代码只负责执行不负责写死规则。这么做的收益在三个月后才会显现业务要调“高价值用户”的金额门槛你只改一行配置而不是从一堆 SQL 脚本里翻。# 画像标签定义一个字典就是一个标签库的“元数据” TAG_DEFINITIONS { user_id: {type: dimension, source: dim_user, desc: 用户唯一标识}, register_date: {type: fact, source: dim_user, expr: register_date, desc: 注册日期}, active_level: { type: rule, source: dws_user_behavior_daily, logic: bucket, params: {column: active_days_30, buckets: [0, 1, 8, 15, inf], labels: [流失, 低活, 中活, 高活]}, desc: 近30天活跃天数分层 }, is_high_value: { type: rule, source: dws_user_behavior_daily, logic: expr, params: {formula: total_amount_30 1000 and order_count_30 3}, desc: 高价值用户 } }这个字典是整个画像系统的“宪法”。后续所有代码都只认这个字典不再散落硬编码。type 字段区分 fact 和 rulefact 标签直接取字段rule 标签按 logic 计算。source 指明数据来自哪张表或哪个数仓层方便排查上游问题。改口径只改字典不改加工逻辑这是配置化最核心的价值。有了配置字典还需要一个解释器把字典里的 rule 定义翻译成 DataFrame 的操作import pandas as pd def compute_rule_tag(df: pd.DataFrame, config: dict) - pd.Series: params config[params] if config[logic] bucket: col params[column] bins [float(b) if b ! inf else float(inf) for b in params[buckets]] return pd.cut(df[col], binsbins, labelsparams[labels], rightFalse) if config[logic] expr: return df.eval(params[formula]).astype(int) raise ValueError(funknown logic: {config[logic]}) # 示例计算活跃分层 feature_df pd.DataFrame({active_days_30: [0, 3, 12, 20, 30]}) tag_series compute_rule_tag(feature_df, TAG_DEFINITIONS[active_level]) print(tag_series.tolist()) # [流失, 低活, 低活, 中活, 高活]逻辑说明bucket 模式用 pd.cut 对连续值分箱buckets 列表里的每个数字是左闭右开的边界最后一个 inf 是正无穷。expr 模式用 DataFrame.eval 解析公式字符串注意公式里引用的列必须真实存在于传入的 df 中。参数说明labels 个数必须等于 buckets 长度减一否则 pd.cut 直接报错。这个解释器可以复用新增规则类型时只需要在这里加一个分支。2.3 用户唯一标识ID 打通的三种常见方案标签定义好了第一关是“谁是同一个用户”。不打通 ID画像就是散沙。同一个用户在 Web 端有 cookie_id在 App 端有设备 ID登录后有 user_id如果不用一套主键串起来会出现一个人被算成三个人的情况。第一类方案以注册用户的 user_id 为主键。这是最简单的做法适合有强登录体系的产品。游客行为可以挂到匿名 ID 上用户登录后通过 mapping 表把游客期间的行为归属到 user_id。第二类方案用设备指纹识别游客通过浏览器或 App 上报的设备特征生成一个稳定 ID。这个方案听着高端但真实场景里误判率不低设备型号相同的两台手机会被指纹算法算成同一个所以我不建议新团队一上来就搞设备指纹。第三类方案建统一 ID 映射表把 user_id、手机号、邮箱、设备 ID 全部收进一张表每天增量更新。中小团队最稳妥的组合是方案一加方案三。ID 映射表是画像系统的地基每天必须有调度任务维护-- 每日增量更新 id_mapping以 user_id 为主键来源 ID 取最近一次出现时间 insert overwrite table dim_id_mapping select user_id, max_by(device_id, last_seen) as device_id, max_by(email, last_seen) as email, max(last_seen) as last_seen from ( select user_id, device_id, email, last_seen from ods_user_login_log where dt ${bizdate} ) t group by user_id;逻辑说明max_by 是取分组内某个字段最大值对应的另一个字段这里用 max_by(device_id, last_seen) 表示取该用户最近一次登录用的设备 ID。如果数仓引擎不支持 max_by可以改用 row_number() over(partition by user_id order by last_seen desc) 取第一条。参数说明${bizdate} 是调度日期变量每天跑一次只处理当天增量存量数据通过插入覆盖的方式整体刷新保证映射表里每个 user_id 只有一行。3. 用 Python 搭画像加工链路数据清洗、特征聚合与标签计算3.1 最小可运行的画像计算流程从行为日志到特征宽表画像加工链路本质是“日志 - 宽表 - 标签”三步。宽表是中间产物一行一个用户一列一个特征。有了宽表所有规则标签只需要在列上做判断不需要再回原始日志里反复 join。先写一个最小流程处理一份行为日志import pandas as pd # 1. 读原始行为日志解析时间字段 logs pd.read_csv(behavior_logs.csv, parse_dates[event_time]) logs logs[[user_id, event_time, event_id, order_amount]].dropna(subset[user_id]) # 2. 按用户聚合产出特征宽表 features logs.groupby(user_id).agg( total_visits(event_id, count), last_visit_time(event_time, max), total_amount(order_amount, sum), order_count(order_amount, lambda x: (x 0).sum()) ).reset_index() # 3. 计算最近访问天数Recency today pd.Timestamp.now().normalize() features[recency_days] (today - features[last_visit_time].dt.normalize()).dt.days # 4. 落 parquet给后续标签计算用 features.to_parquet(features_daily.parquet)逻辑说明groupby.agg 里每个字段名就是宽表列名。last_visit_time 取 max 表示“最近一次访问时间”recency_days 用今天减去最近访问时间得到距今天数。order_count 的 lambda 统计的是订单金额大于 0 的记录数注意 order_amount 为空时要先 dropna 或 fillna否则空值会被当成 0 计入。参数说明parse_dates 参数指定时间列pandas 会自动转成 datetime 类型。dropna(subset[user_id]) 用来剔除 user_id 为空的行这类噪声日志在真实数据里很常见。落 parquet 而不是 csv是因为宽表列多时 parquet 体积更小读取速度也快得多。3.2 用户活跃度与价值分层RFM 的 Python 实现宽表出来后业务最常用的 RFM 分层就能算了。RFM 是 Recency最近一次消费距今、Frequency消费频率、Monetary消费金额的缩写运营团队靠它把用户分成高价值、流失、潜力等群体。这里给出一个简化的二值化版本def rfm_segment(df, r_th30, f_th5, m_th500): df[r_score] (df[recency_days] r_th).astype(int) df[f_score] (df[order_count] f_th).astype(int) df[m_score] (df[total_amount] m_th).astype(int) # 组合成三位码111高价值活跃000沉默流失 level_map { 111: 高价值活跃, 110: 高频低额, 101: 低频高额, 100: 新客潜力, 000: 沉默流失 } df[rfm_level] df.apply( lambda r: level_map.get(r[r_score] * 100 r[f_score] * 10 r[m_score], 其他), axis1 ) return df features rfm_segment(features) print(features[rfm_level].value_counts())逻辑说明这里把 R、F、M 各压成 0/1 二值比经典五分制好解释。r_score1 表示近 30 天内来过f_score1 表示下单不少于 5 次m_score1 表示累计消费不低于 500。三位码拼成 key从 level_map 里取分层名。参数说明r_th、f_th、m_th 三个阈值怎么定小规模场景先用分位数试探比如 r_th 取 recency_days 的 33% 分位f_th 取 order_count 的中位数有业务经验就直接给业务值。阈值一定要写进 TAG_DEFINITIONS 配置里否则下次调口径又是一次代码改动。3.3 画像结果的存储选型MySQL / ClickHouse / Redis 怎么选画像算完之后要存起来供查询。存储选型直接决定后面的查询体验和维护成本。以我接手的项目经验三者的边界大致如下。存储适合规模查询特点维护成本MySQL用户量百万级以下标签数少于 200灵活 SQLjoin 多了会慢低需要建索引ClickHouse千万级用户宽表列多列式存储标签圈选秒级中需要懂分区键Redis在线实时场景内存查询快按 key 取整行标签中内存贵我的选型习惯是离线分析和人群圈选放 ClickHouse线上接口用 MySQL 或 Redis。对于一套刚起步的画像系统用户量在百万到千万之间时MySQL 单表加索引完全扛得住日常查询。只有标签列多、运营频繁做“圈选人群”这种按多个标签条件过滤的查询时ClickHouse 的列式存储优势才明显。Redis 这个后悔药留着后面实时标签多了再上。有些团队一开始就上 Hadoop 全家桶结果标签只有几十个数据量百万级三台机器跑得比单机 MySQL 还慢。画像系统前期不要追求架构大一套 MySQL 加 Redis 的组合能撑到千万级用户的前期运营。4. 画像系统常见的翻车现场五个高频踩坑记录下面五条是每套画像系统上线头一个月大概率遇到的血泪经验。每条都是“现象 - 原因 - 解决”的结构遇到同类问题可以直接照着排查。4.1 现象同一用户出现了两个画像 ID有时候画像表里同一个用户出现两行一行 user_id 有值一行只有 cookie_id。原因多半是没做 ID 打通或者 id_mapping 表只做了存量数据当天的增量登录记录没有并进去。解决建统一 id_mapping 表每天调度先更新增量映射再跑画像加工。排查问题时先看 mapping 表的最后更新时间而不是去怀疑标签代码。很多团队在这里浪费一整天最后发现只是映射任务挂了三天没人注意到。4.2 现象标签跑出来全是默认值画像结果里“活跃分层”全是“流失”“高价值用户”全是 0。原因很可能是上游表字段改名或者空值被统一填成 0导致“近 30 天消费金额”全为 0分层自然全落到“流失”。解决空值处理别一上来就 fillna(0)。先保留 null统计每列 null 占比超过 5% 就告警。对业务核心字段用 -1 占位让“缺失”和“真实 0”在标签值上区分开。排查时可以写一条 SQL 快速定位select count(*) as total_rows, sum(case when total_amount_30 is null then 1 else 0 end) as null_amount_rows from dws_user_profile_daily where dt ${bizdate};逻辑说明这条 SQL 统计当天画像表里 total_amount_30 为空的行数。如果 null_amount_rows 占比异常高说明上游加工链路断了不是标签逻辑的问题。参数说明dt 是分区字段画像表建议按天分区排查问题时可以对比前后两天的空值占比变化。4.3 现象画像表越来越大查询越来越慢每天全量快照一个用户一行一个月存 30 份查询时没指定分区全表扫描当然慢。原因是没有做分区和生命周期管理。解决画像表按日期分区只保留最近 7 天快照历史快照归档到冷存储。查询时必须强制带 dt 条件。这个约束要在数仓建模时定好规范否则后期改会动到所有下游任务。4.4 现象规则改了旧数据却没重算业务把“高价值用户”的门槛从 1000 改成 800改完第二天一查历史日期还是按老口径算的。原因标签配置改了但调度任务只跑增量历史分区没回刷导致新旧口径的数据混在一起。解决每次口径变更给标签定义加 version 字段变更时强制全量回刷最近 90 天。上线前先跑一个采样用户的 diff 脚本对比新旧标签差异率。简化的回刷逻辑可以这样写# tag config 里带版本号回刷时按版本筛选数据 TAG_DEFINITIONS[is_high_value][version] v2_20240601 # 全量重算今天之前 N 天的画像分区 for day_offset in range(90): dt (pd.Timestamp.now() - pd.Timedelta(daysday_offset)).strftime(%Y-%m-%d) recalc_profile_partition(dt, versionv2_20240601)逻辑说明recalc_profile_partition 是一个模拟函数实际项目中对应重跑某天分区的加工任务。核心思想是版本号加回刷窗口两个缺一不可。参数说明回刷窗口取 90 天是业务常用的周期超过 90 天一般会被新数据稀释如果标签用于长期价值评估窗口要相应拉长到 180 天或 365 天。4.5 现象画像可视化页面和计算结果对不上运营在页面上看到的标签占比和数仓里跑出来的分布对不上。原因前端读的是 Redis 缓存离线跑的是数仓表缓存过期时间没对齐或者缓存更新任务失败但没告警。解决所有标签输出统一带 generated_at 字段可视化页只认同一数据源。缓存设置差异化过期策略活跃类标签 20 分钟过期价值类标签一天过期。这个设计能避免“页面显示高价值用户 4000 人SQL 查出来 6000 人”这种最尴尬的场景。注意4.2 和 4.4 最隐蔽前者是数据质量问题后者是流程问题都容易甩锅给算法。真遇到这两条先把上游数据核对清楚再动代码。5. 画像服务化与可视化用 FastAPI 暴露标签查询接口5.1 画像查询接口的最小实现数据躺在仓库里不算系统。要让运营能查、能圈人得有一个接口。一个可运行的画像系统源码至少包含标签配置、加工脚本、查询接口、监控脚本四部分。查询接口常见做法是用 FastAPI 包一层把每天加工的 parquet 文件加载进内存按 user_id 直接查from fastapi import FastAPI import pandas as pd app FastAPI() # 进程启动时加载当天快照后续查询只读内存 df pd.read_parquet(features_daily.parquet).set_index(user_id) app.get(/user/{user_id}/profile) def get_profile(user_id: str, tags: str None): if user_id not in df.index: return {code: 404, msg: user not found} row df.loc[user_id] if tags: tag_list [t.strip() for t in tags.split(,)] row row[tag_list] return {code: 0, data: row.to_dict(), generated_at: 当日快照}逻辑说明启动时把 parquet 读进 DataFrame 并 set_index(user_id)查询就是 index 查找百万级用户完全扛得住。tags 参数支持按需返回字段避免一次性把所有标签都传回去。参数说明tags 用逗号分隔接口内部做 split 和 strip这样运营在页面上可以勾选要看的标签组。生产环境建议把接口拆成“基础属性”“消费行为”“预测标签”三组响应体更干净也能避免大字段拖慢网络。5.2 标签可视化一个够用的极简前端页面不要一上来就搭 BI 大屏。真实运营需要的是两个能力输入用户 ID 看标签按标签条件筛选用户。第一个用 HTML 表格就够第二个让后端接口支持 filter 参数app.get(/users) def list_users(active_level: str None, rfm_level: str None, limit: int 50): mask pd.Series(True, indexdf.index) if active_level: mask (df[active_level] active_level) if rfm_level: mask (df[rfm_level] rfm_level) matched df[mask] return {code: 0, data: matched.head(limit).reset_index().to_dict(records)}逻辑说明用布尔 mask 逐条件叠加筛选避免链式赋值。head(limit) 限制返回条数防止运营一次拉出全量数据把接口打爆。参数说明limit 默认 50前端页面上做一个下拉框让运营选 50/100/200。接口就绪后运营在浏览器里输入“高价值活跃 高活”就能拿到一批 user_id复制到 CRM 系统里建人群包。先把这两个接口做扎实再谈可视化图表。5.3 标签更新策略全量重算还是增量更新画像系统的更新策略直接影响成本和数据新鲜度。三种方式各有边界。策略适用标签优点缺点全量日更事实标签、大部分规则标签逻辑简单口径好控制用户量大时跑批耗资源增量更新近期行为类标签如近 30 天活跃只处理当天活跃用户成本低需要处理删数和回刷场景实时更新在线实时标签如 30 分钟内活跃数据新支持实时场景Redis 成本高代码复杂我的做法是核心规则标签全量日更睡前跑批第二天早上看结果。实时标签抽离出来Redis 里只放最近 N 天活跃的用户不碰历史全量。增量更新看着省资源但每次都要处理用户删除、标签回刷、口径变更流程复杂度会上一个台阶。新团队起步阶段全量日更是最稳的选择数据量到千万级再考虑增量。6. 画像质量验证的三个土办法上线前先过自己这关6.1 抽样人工核对抽 50 个用户看常识别只信测试集直接抽 50 个 user_id打印标签和原始行为。这个步骤土但有效率极高。我的习惯是固定随机种子保证每次抽查的是同一批人方便对比口径变更前后的差异sample features.sample(50, random_state42) for uid, row in sample.iterrows(): print(uid, row[rfm_level], row[active_level], row[total_amount])逻辑说明sample 的 random_state 固定为 42同一份数据每次抽到的用户一样。打印出来后对照订单表手工核三个点最近购买日期是否和 recency_days 一致、累计金额是否对得上、活跃分层是否符合直观。看到可疑的再去订单表核一遍基本能拦住八成的数据质量问题。6.2 分布稳定性监控标签占比突变就报警上线之后第二件事是看分布。把每天的标签占比画出来没有业务动作却跳了 10 个点就该查上游。常见做法是落一张每日标签分布表对比相邻两天的占比差select rfm_level, count(*) as cnt, count(*) * 1.0 / sum(count(*)) over() as ratio from dws_user_profile_daily where dt ${bizdate} group by rfm_level;逻辑说明ratio 是当天某标签的占比对比前一天的 ratio超过阈值比如绝对值 5%就告警。大多数跳变来自上游日志缺失不是用户真的变了。参数说明告警阈值别设太低画像标签日常波动一般在 1% 到 3% 之间5% 是一个既能暴露问题又不会天天误报的平衡点。6.3 用画像做一次小规模 AB 验证收尾画像系统上线有争议时最有力的证明是一次小规模实验。拿“高价值活跃”标签圈一批用户发不同的优惠券策略对照组用随机用户对比转化率。如果差异显著说明标签真的圈住了该圈的人也能反推标签口径是否合理。这个方法不仅验证画像质量还让业务方直观看到画像的价值。最后说个习惯我现在每次改完标签口径都会先用 6.1 抽一轮再发上线。那些看着“差不多”的规则跑出来的结果往往差得离谱。画像系统不是一次性项目它需要日常盯着分布和数据新鲜度。希望这篇基于 python 实现用户画像生成系统的落地笔记能帮你少走几趟弯路。本文还有配套的精品资源点击获取