简介:基于Python实现用户画像系统的完整源码,面向数据挖掘、个性化推荐与精准营销方向的开发者,解决从原始行为数据到可视化用户画像的落地问题。压缩包共149个文件、仅2.45MB,69个py源码覆盖数据预处理、特征工程、聚类分析、画像构建等核心环节,53个pyc文件便于直接调用,另含5个html及配套css/js前端资源,可展示登录、推荐与图表页面,csv样例数据等辅助文件也一并包含。已有352人学习下载。系统采用pandas、sklearn、Flask等库,按数据清洗、LabelEncoder/OneHotEncoder编码、KMeans聚类、TF-IDF权重计算、画像存储到Web服务集成的流程组织,层次清晰,阅读源码即可掌握各模块设计思路;配合可视化页面与示例数据,可直接运行观察效果,适合想快速搭建用户画像系统的初中级Python开发者。
1. 从规则到模型,用户画像生成的工程化路径
当业务方拿着“沉睡用户召回率低、新客转化路径看不懂”这类问题找到数据团队时,最终都会落到同一个需求上:把散落在各系统的用户行为数据,加工成一套可查询、可计算、可推送的标签集合,也就是用户画像。这个标题里的核心词是“基于Python实现用户画像生成系统源码”,它背后对应的是一个典型的横向项目:数据接入、标签加工、画像存储、查询服务。适合谁?适合已经跑通埋点和数仓基础表、但还没有独立画像服务的团队;也适合想从零搭建一套轻量级画像中台、又不想直接引入重型商用产品的开发者。
Python在这个场景下的优势不在计算引擎本身,而在它能把清洗、规则计算、模型推理、API暴露这条链路用一套语言串起来。实际落地时计算层可以是Pandas或PySpark,存储层用MySQL或ClickHouse,接口层用FastAPI。源码的意义也在于此:不是给你一份跑完就完的脚本,而是一个能拆解、能加标签、能换存储的骨架。下面顺着一条典型的搭建路径拆开讲,每一步都给到可执行的代码和参数。
2. 数据接入与标签体系建设:先定维度,再写代码
2.1 画像系统的数据源分类
用户画像的输入数据大体分三类:用户属性数据、用户行为数据、业务交易数据。属性数据来自注册信息或CRM,属于低频静态数据;行为数据来自埋点日志,属于高频事件流;交易数据则决定RFM模型中的金额和频次字段。工程上需要注意,这三类数据的时效性完全不同,不能在同一层处理。
常见图景是:属性数据在MySQL里,埋点日志在HDFS或对象存储上,交易数据在数仓分层表中。画像系统的第一个任务不是写标签,而是把这些源头统一成一张“用户特征宽表”。这个步骤决定了后续所有标签计算的正确性,做得粗糙,后面全是脏数据。
2.2 用Pandas做用户行为特征聚合
在数据量不大(百万级用户以内)或离线批处理场景下,Pandas足够应付行为特征的加工。下面代码演示如何从用户行为日志中提取“最近30天活跃天数”和“平均浏览时长”。
import pandas as pd # 读取用户行为日志,假设包含user_id、event_type、event_time、duration df = pd.read_csv('user_behavior.log', sep='\t', parse_dates=['event_time']) # 筛选最近30天数据 cutoff_date = df['event_time'].max() - pd.Timedelta(days=30) recent = df[df['event_time'] > cutoff_date] # 按用户统计活跃天数和平均时长 user_features = recent.groupby('user_id').agg( active_days=('event_time', lambda x: x.dt.date.nunique()), avg_duration=('duration', 'mean') ).reset_index() # 合并用户属性数据 user_profile = pd.read_sql('SELECT user_id, gender, age, city FROM user_base', engine) result = pd.merge(user_profile, user_features, on='user_id', how='left')这段代码的核心逻辑是先做时间窗口过滤,再做分组聚合。nunique用于统计去重后的活跃天数,避免同一用户当天多次行为被重复计数。how='left'保证属性表全量保留,即使某用户没有行为记录,画像表中也会留下空特征行,便于后续处理缺失值。
参数调整的关键点:时间窗口的长度(30天、90天、180天)会直接影响画像的时效性和业务解读口径。窗口太长,短期行为变化被平滑掉;窗口太短,低频但高价值的用户容易被误判为不活跃。
2.3 标签体系的分层设计
标签体系通常分为三层:事实标签、规则标签、模型标签。
- 事实标签:直接来源于行为数据的统计结果,如“最近登录时间”“30天订单数”。
- 规则标签:基于事实标签叠加业务规则生成,如“高活跃用户”对应“近30天活跃天数>15”。
- 模型标签:通过聚类、分类或打分模型产出,如“价值分”“流失概率”。
规则标签的生成在Python中通常是一组字典映射或函数集合。下面是一个典型的规则标签计算示例。
def gen_rule_tags(row): tag_set = set() if row['active_days'] >= 15: tag_set.add('高活跃') elif row['active_days'] >= 5: tag_set.add('中活跃') else: tag_set.add('低活跃') return tag_set result['rule_tags'] = result.apply(gen_rule_tags, axis=1)注意,这种逐行apply处理在百万级数据上会很慢,实际项目里建议用np.select向量化实现,速度可以提升数十倍。
import numpy as np conditions = [result['active_days'] >= 15, result['active_days'] >= 5] choices = ['高活跃', '中活跃'] result['active_level'] = np.select(conditions, choices, default='低活跃')2.4 标签存储选型与表结构设计
标签数量少(几十个)时,可以直接在MySQL里建一张宽表,每一列是一个标签。标签数量多(数百个)且需要频繁迭代时,推荐用“用户+标签名+标签值”的三列结构,行式存储换成列式存储,方便后续加标签而不用改表结构。
这是一个常见的使用方式:
CREATE TABLE user_tags ( user_id STRING, tag_name STRING, tag_value STRING, update_time DATETIME, PRIMARY KEY (user_id, tag_name) ) ENGINE=InnoDB;改成三列结构后,查询逻辑从“查一行取多列”变成“按用户聚合多行”,查询模式变了。如果查询频率高,可以再用Redis做缓存层,把热门用户的全量标签提前灌入Hash结构中。
3. 画像核心引擎:规则解析、模型推理与批流一体化
3.1 规则引擎的代码实现思路
规则不能每次硬改硬发布,否则运营提一个“近7天加购3次”的标签,开发就得改一次代码。工程上要把规则配置化,JSON或YAML描述规则,Python动态读取并执行。
一个简单的规则配置如下。
{ "rule_name": "高意向用户", "logic": "AND", "conditions": [ {"field": "cart_count_7d", "op": ">=", "value": 3}, {"field": "visit_days", "op": ">=", "value": 5} ] }对应解释器:
import json import operator ops = { '>=': operator.ge, '<=': operator.le, '==': operator.eq, '>': operator.gt, '<': operator.lt, } def eval_rule(row, rule): results = [] for cond in rule['conditions']: val = row[cond['field']] op = ops[cond['op']] results.append(op(val, cond['value'])) if rule['logic'] == 'AND': return all(results) else: return any(results) with open('rules.json') as f: rules = json.load(f) result['pred_label'] = result.apply(lambda r: eval_rule(r, rules), axis=1)这段代码的核心是把规则与逻辑解耦。新增标签时只需修改JSON配置,无需改动Python代码。注意row[cond['field']]要求字段名完全匹配特征宽表的列名,因此字段命名规范在画像体系里是硬约束,上线前需要做一遍配置合法性的校验。
3.2 基于评分卡的价值分模型
规则解决的是确定性判断,但用户价值本身是连续性变量,更适合用打分模型表达。经典的做法是构造评分卡模型,将RFM(Recency, Frequency, Monetary)特征映射为用户价值分。
下面代码演示一个基于百分位映射的评分方式。
# 假设facts包含recency, frequency, monetary三列 facts['r_score'] = pd.qcut(facts['recency'], 5, labels=[5, 4, 3, 2, 1]) facts['f_score'] = pd.qcut(facts['frequency'], 5, labels=[1, 2, 3, 4, 5]) facts['m_score'] = pd.qcut(facts['monetary'], 5, labels=[1, 2, 3, 4, 5]) facts['value_score'] = facts['r_score'].astype(int) * 0.3 + \ facts['f_score'].astype(int) * 0.4 + \ facts['m_score'].astype(int) * 0.3权重参数0.3/0.4/0.3代表团队对行为频次的偏重高于消费金额,这个权重可以根据业务方的主观经验或回归模型校准。pd.qcut按分位数均匀切分,避免极端值拉偏打分区间。
版本落地时要注意,qcut的区间数量(这里用5)在样本量变化后会抖动。建议每隔一段时间用全量数据重新计算分位点,然后固化成映射配置,而不是每次都实时算,否则同一用户在不同日期的评分可能出现跳跃,影响下游策略稳定性。
3.3 画像结果落库的写优化
画像计算完成后,把结果写入存储是瓶颈环节。按用户维度逐行写入MySQL的INSERT会非常慢,常见做法是批量提交。
from sqlalchemy import create_engine from sqlalchemy.dialects.mysql import insert engine = create_engine('mysql+pymysql://user:pass@host:3306/db') rows = result.to_dict('records') # 分批写入,每批5000行 batch_size = 5000 for i in range(0, len(rows), batch_size): batch = rows[i:i + batch_size] stmt = insert(user_tags_table).values(batch) upsert = stmt.on_duplicate_key_update( tag_value=stmt.inserted.tag_value, update_time=stmt.inserted.update_time ) engine.execute(upsert)on_duplicate_key_update解决的是“同一用户重复计算”时的覆盖更新问题。如果不做这个处理,第一次跑完后第二次全量计算会因主键冲突报错,或者直接跳过导致数据不更新。
批量大小(batch_size)不是越大越好,MySQL的max_allowed_packet默认值通常是64MB,批次过大会触发连接中断,实际项目中建议按500~2000行或每个批次不超过1MB来压测。
3.4 批流一体化处理的框架选择
离线画像更新频率通常是T+1,但推荐系统、广告投放要求小时级甚至分钟级标签。一个现实的方案是用MaxCompute或Spark做日级离线计算,而核心高频标签用Flink或Spark Streaming实时计算,最终在Redis中拼接。
这里有一个实际经验:初期不要为了“实时”而设计实时画像,因为实时标签的成本远高于离线条数。建议从增量ETL开始,用Python脚本监听日志文件或消息队列,只对当天新增用户或当天活跃用户做局部更新,降低全量计算的频率。
4. 画像查询服务:从离线表到在线API的完整闭环
4.1 用Redis缓存画像快照
画像服务的调用方是推荐系统和营销后台,QPS往往在几百到几千。直接查MySQL或ClickHouse在峰值时会打爆数据库,因此在存储之前先加一层缓存。Redis的Hash结构是最贴合画像场景的数据模型:key存用户ID,field存标签名,value存标签值。
import redis r = redis.Redis(host='localhost', port=6379, db=0) # 批量写入画像数据 for user_id, row in result.iterrows(): mapping = {'active_level': row['active_level'], 'value_score': row['value_score']} r.hset(f'profile:{user_id}', mapping=mapping) # 读取画像 user_tags = r.hgetall('profile:12345')hset批量写入时要注意,单条Hash的field数量不要超过100个,否则单次请求包体过大,序列化效率下降。超过100个标签时,建议分成多个key,比如profile:12345:base和profile:12345:behavior。
4.2 用FastAPI暴露画像查询接口
在对外提供服务之前,先确定好接口协议。常见的设计有两类:一是按用户ID查单人的全量标签;二是按标签值查用户列表,后者一般交给搜索引擎,不在API层实现。下面给出前者的实现示例。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class UserProfileResponse(BaseModel): user_id: str tags: dict updated_at: str @app.get("/api/user/{user_id}/profile", response_model=UserProfileResponse) def get_profile(user_id: str): tags = r.hgetall(f'profile:{user_id}') if not tags: raise HTTPException(status_code=404, detail='user profile not found') return { "user_id": user_id, "tags": tags, "updated_at": r.hget(f'profile:{user_id}', 'update_time') }这是一个可直接复用的骨架-只需调整Redis连接配置和返回字段。注意接口要增加超时控制和降级逻辑,比如Redis挂了的时候回源到MySQL或返回空标签,避免拖垮主链路。
4.3 实时标签的流式拼接
当业务需要“用户刚完成一次加购就立刻打上购物车意向标签”时,离线计算无法满足时效性要求。一个轻量级的替代方案是:在nginx层或消息队列中捕获行为日志,Python消费者实时更新指定用户的特定标签。
import json from kafka import KafkaConsumer consumer = KafkaConsumer( 'user_events', bootstrap_servers='localhost:9092', group_id='profile-updater' ) for msg in consumer: event = json.loads(msg.value) user_id = event['user_id'] # 仅更新与本次事件相关的标签 if event['event_type'] == 'add_cart': r.hincrby(f'profile:{user_id}', 'cart_count_7d', 1)这里用hincrby而不是先读后写,是为了保证原子性。需要注意的是,实时更新只覆盖少量高频标签,全量画像的刷新仍然依赖离线任务,两套逻辑必须共用同一套标签编码体系,否则会出现同样的业务含义在不同链路产生不同标签值的情况。
5. 画像质量验证与参数调优:覆盖率和准确率是两条腿
5.1 覆盖率校验的落地脚本
画像系统上线之后,首先要回答的问题是“这4000万用户里,打了标签的人占比多少”。覆盖率低说明特征宽表有大量空值,可能的原因是行为日志缺失、用户属性信息未补齐、窗口期设置不合理。
import pandas as pd profile = pd.read_sql('SELECT user_id, active_level, value_score, cart_count_7d FROM user_tags', engine) total_users = 40000000 tagged_users = profile['user_id'].nunique() coverage_rate = tagged_users / total_users print(f'画像覆盖率: {coverage_rate:.2%}') # 单标签覆盖率 for col in ['active_level', 'value_score', 'cart_count_7d']: non_null = profile[col].notna().sum() print(f'{col} 覆盖率: {non_null / total_users:.2%}')空值过滤用notna,但这里要区分“空值”和“0值”。cart_count_7d为0的场景表示用户近7天没有加购,这是有效信息;而NaN表示数据缺失。两者在标签体系中的含义完全不同,调优时需要分开处理。
5.2 画像准确率抽检的抽样样本
准确率无法全量验证,实践中是从已打标签的用户中随机抽取一定数量(如200-500人)做人工核对。核对方式为:查看原始行为日志,判断标签值与事实是否一致。下面脚本生成抽检样本。
sample_users = profile.sample(n=300, random_state=42)[['user_id', 'active_level']] sample_users.to_csv('sample_check.csv', index=False)抽检样本应该覆盖高、中、低活跃三类用户,不能只抽高活跃部分。random_state固定随机种子是为了保证后续重跑样本一致,便于复现问题。
5.3 特征计算的性能调优
当用户量达到亿级时,纯Pandas计算会出现内存不足或耗时过长。调优有几个方向:一是向量化替换apply;二是用PySpark替换Pandas;三是优化窗口期字段索引。
# 为event_time加索引,过滤速度提升明显 df.set_index('event_time', inplace=True) mask = df.index > cutoff_date recent = df.loc[mask]加索引后做切片而不是布尔掩码遍历,这一步在千万级数据行上能缩短2到3倍耗时。另一个参数调整点是read_csv时指定usecols只读需要的列,减少IO和内存占用。
5.4 缓存淘汰策略的参数调整
Redis存画像时,随着用户规模增长内存会缓慢上涨。需要设置过期时间,画像类的数据适合用EXPIRE命令加24-72小时过期时间。
r.expire(f'profile:{user_id}', 86400) # 24小时过期超过百亿Key时,Redis集群的分片数量与内存规格、带宽限制都要重新评估,这部分参数不单是代码问题,还涉及Redis集群的容量规划。建议用Redis的INFO memory监控used_memory曲线,当占比超过80%时,考虑增加分片或调大maxmemory-policy为allkeys-lru让冷用户数据自动淘汰。
画像系统稳定运行后,更高的形态是把准确率、覆盖率、时效性做成每日监控看板,并把标签变更记录沉淀成元数据表,方便后续排查某个策略为什么命中用户数变了。项目源码里这部分可以单独做成一个模块,只是别塞在主流程里,否则每次跑批都变慢。
本文还有配套的精品资源,点击获取