简介一份基于Python实现的用户画像生成系统源码包面向数据分析、用户增长、推荐系统方向的开发者与学习者。资源围绕用户画像构建全流程展开包含数据收集与预处理、特征工程、用户行为分析、聚类、特征权重计算、画像存储与可视化等核心模块并给出Web端展示页面可直接学习或二次开发。包体共149个文件压缩包2.45MB以69个py源码脚本和53个pyc编译文件为主辅以html/css/js前端页面、csv样例数据、图标字体及图片等覆盖后端处理逻辑、前端交互展示与测试数据目录结构清晰便于按模块对照学习。已有352人学习下载。资源不仅提供可直接运行的源码还包含Bootstrap样式页面、登录/推荐/图表展示等界面以及csv样例数据方便快速理解用户画像从数据到应用的落地过程。对于想掌握Python全栈数据应用或搭建画像系统的读者是份实用的参考工程。1. 用户画像生成系统先搞清楚它解决的是哪个问题用户画像这几年被讲得很玄学但拆开来看它就是一个从原始行为日志到结构化用户特征向量的加工流水线。这套基于 Python 实现的用户画像生成系统源码里带了两份 CSV 样本数据data.csv 和 newdata.csv还有 recommend.html、chart.html、login.html 三个页面和整套 Bootstrap 样式资源也就是说它不是只在命令行里跑脚本的玩具而是一套带 Web 展示层的完整工程。对做运营分析、推荐系统或刚接触数据挖掘的工程师来说这套系统的价值在于把数据清洗、特征工程、聚类分群、画像可视化全流程完整走了一遍你换一批数据就能复现出自己的画像。它要解决的核心问题很直接散落的行为日志怎么一步步变成业务人员能看懂的人。2. 数据预处理与特征工程从 data.csv 和 newdata.csv 到干净的特征矩阵画像第一步永远不是建模而是让数据变得可计算。拿到源码里的两份 CSV 之后我一般会先做一次五分钟的“体检”——看字段类型、缺失率、时间跨度再定清洗策略。这一步偷懒后面聚类出来的群体你根本没法和业务方解释。2.1 pandas 清洗主流程合并、去重、缺失值过滤两份 CSV 是最容易翻车的开始字段名不一致、时间格式不统一、同一用户在两份表里重复出现。常见做法是先打印结构再合并别上来就concat。import pandas as pd # 第一步读两份数据确认字段和类型 df1 pd.read_csv(data.csv, encodingutf-8) df2 pd.read_csv(newdata.csv, encodingutf-8) print(df1.columns.tolist()) print(df2.columns.tolist()) # 第二步纵向合并成一张全量明细表 df_all pd.concat([df1, df2], axis0, ignore_indexTrue) # 第三步用户主档去重——同一用户只保留一条画像基础记录 df_user df_all.drop_duplicates(subset[user_id]) # 第四步关键字段缺失的直接删掉别让空值混进聚类 df_user df_user.dropna(subset[user_id, gender, age_group])这里每个细节都有讲究。pd.concat比append更稳列数不一致时它能自动对齐ignore_indexTrue让合并后的行号重新从 0 开始避免后面groupby时索引混乱。drop_duplicates的subset只针对user_id去重如果放进整行同一用户只要有一条行为时间不同就会被当成两个人。dropna删的是主档字段行为明细字段别在这一步删——那些后面要统计缺了可以填充。行为明细和用户主档要分开处理因为一个用户对应几十条浏览记录。时间解析、行为编码这步做完才能进入特征聚合。# 行为明细单独取时间列转 datetime行为类型映射成数值 df_behavior df_all.dropna(subset[event_time, behavior_type]) df_behavior[event_time] pd.to_datetime(df_behavior[event_time], errorscoerce) df_behavior df_behavior.dropna(subset[event_time]) beh_map {view: 1, cart: 2, buy: 3, fav: 4} df_behavior[behavior_code] df_behavior[behavior_type].map(beh_map)errorscoerce的作用是把无法解析的时间变成NaT比抛异常温和后面统一过滤。行为类型映射成数值编码后做求和、均值这类聚合运算就顺手了可视化时也能按数值排序。这套源码里data.csv和newdata.csv的差异就在时间跨度上newdata 通常代表最近一批增量数据合并顺序会影响时间窗口的起点所以我在合并后总会再sort_values(event_time)一次保证后续窗口计算从旧到新。2.2 特征编码与标准化三类编码器和缩放器的选型逻辑用户画像的特征大体分三类名义型性别、城市、有序型消费等级、活跃度、数值型购买金额、浏览时长。处理方式完全不同这一块是很多人踩坑的重灾区——把性别LabelEncoder成 0/1 其实问题不大但城市这种高基数类别也按序号编码聚类时距离就全乱了。from sklearn.preprocessing import LabelEncoder, StandardScaler import pandas as pd # 名义型低基数特征OneHot 编码drop_first 去掉冗余列 df_user pd.get_dummies(df_user, columns[gender], drop_firstTrue) # 有序型特征LabelEncoder保留等级之间的大小关系 le LabelEncoder() df_user[activity_level_encoded] le.fit_transform(df_user[activity_level]) # 数值特征StandardScaler去均值除方差消除量纲影响 num_cols [total_amount, browse_count, avg_session_seconds] scaler StandardScaler() df_user[num_cols] scaler.fit_transform(df_user[num_cols]) # 高基数类别改用频次编码避免 OneHot 把特征矩阵撑爆 freq_map df_user[city].value_counts().to_dict() df_user[city_freq] df_user[city].map(freq_map)这段代码是我在实际项目里沉淀出的选型原则OneHotEncoder适合类别数少于 20 的字段比如性别activity_level这类有天然次序的字段用LabelEncoder因为它保留排序语义数值特征统一走StandardScaler因为 KMeans 基于欧氏距离量纲不一致会让浏览时长把消费金额完全压过去。城市这种高基数列直接转“该值出现的频次”既保留统计信息又只占一列聚类稳定性比 OneHot 要好得多。注意特征标准化必须在模型训练前统一fit。画像系统往往是离线全量建模没有 train/test 切分但在线服务如果要用同一套scaler务必用joblib把拟合好的对象保存下来别在新数据上重新fit否则特征分布一旦漂移聚类中心点全要重算。3. 用户行为分析与聚类分群KMeans 跑完不等于画像做完用户画像最核心的一步是把用户分群。但照着教程跑过一遍的人都会有同感数据丢进去n_clusters4出来四组人画像就“完成”了远没有。聚类之前的行为特征怎么提取决定了聚类结果能不能解释、能不能用。3.1 行为特征提取从事件明细到“一人一行”的宽表用户行为数据是典型的长表结构一个用户几十条记录必须压缩成宽表才能进模型。这一步的逻辑是每个用户算一组行为统计量包括频次、强度、活跃度和最近活跃时间。# 统计每个用户各类行为的次数行为明细转成宽表 beh_pivot df_behavior.groupby([user_id, behavior_code]).size().unstack(fill_value0) beh_pivot.columns [beh_view, beh_cart, beh_buy, beh_fav] # 行为活跃跨度最早行为到最晚行为的天数 df_behavior[event_date] df_behavior[event_time].dt.date active_span df_behavior.groupby(user_id)[event_date].apply( lambda x: (x.max() - x.min()).days ).rename(active_span_days) # 最近一次行为距今天数衡量用户活性 latest_date df_behavior[event_date].max() recency df_behavior.groupby(user_id)[event_date].max().apply( lambda x: (latest_date - x).days ).rename(recency_days) # 拼接到用户主档 df_user df_user.merge(beh_pivot, left_onuser_id, right_indexTrue, howleft) df_user df_user.merge(active_span, left_onuser_id, right_indexTrue, howleft) df_user df_user.merge(recency, left_onuser_id, right_indexTrue, howleft) df_user df_user.fillna(0)groupby加unstack是长表转宽表的常规操作fill_value0保证没有加购行为的用户不出现 NaN。active_span衡量的是活跃周期跨度越大说明用户黏性越强recency是“最近一次访问距今几天”这个指标对营销触达非常关键——近 7 天有行为的用户和近 90 天没出现的用户运营策略完全是两套。这里fillna(0)有个隐含前提没有行为记录的用户所有行为频次都该是 0而不是缺失这在后面聚类时才不会被当成异常点。关联规则挖掘比如 Apriori 算法在用户行为分析里也能派上用场用来找“看了 A 类商品的人还看了 B 类商品”的组合规则。但在画像系统里它更适合做推荐侧的辅助分析聚类之前先用频次统计和活跃跨度把行为表达清楚比直接上关联规则更稳。3.2 聚类算法选型与 K 值确定KMeans、DBSCAN、层次聚类怎么选sklearn提供了一堆聚类算法但这个场景下最常用的是 KMeans原因很实际数据量大时批处理友好结果可解释每个簇能输出中心点向量直接就能转成画像标签。但 KMeans 有前提——簇是凸的、密度接近的。如果你的用户行为呈明显的长尾分布DBSCAN 可能更合适。from sklearn.cluster import KMeans, DBSCAN from sklearn.metrics import silhouette_score from sklearn.preprocessing import StandardScaler import numpy as np # 只选行为相关的数值列均值填充后标准化 cluster_cols [beh_view, beh_cart, beh_buy, beh_fav, active_span_days, recency_days, total_amount] X df_user[cluster_cols].fillna(0).values X_scaled StandardScaler().fit_transform(X) # 用轮廓系数选 K不拍脑袋 scores [] k_range range(2, 9) for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) scores.append(silhouette_score(X_scaled, labels)) best_k k_range[int(np.argmax(scores))] print(fbest k {best_k}, silhouette {max(scores):.4f}) # 用最佳 K 训练最终模型 final_model KMeans(n_clustersbest_k, random_state42, n_init10) df_user[cluster] final_model.fit_predict(X_scaled)轮廓系数是最常用的 K 值选择方法范围在 [-1, 1]越大说明越紧凑。但这里有个坑轮廓系数在 K 偏大的时候会虚高不能唯它马首是瞻。我一般会同时看每个簇的人数占比如果某个 K 值下有个簇只有 3% 的用户那它大概率是离群点的集合没有运营价值。n_init10是为避免 KMeans 随机初始化落进局部最优参数越大越稳但训练时间也线性增长。三种算法的取舍直接看这张参数对比表算法适用场景关键参数主要限制KMeans样本量大、簇形状接近球形n_clusters、n_init对离群点敏感必须先标准化DBSCAN密度不均、含大量离群点eps、min_samples高维数据下距离度量容易失效层次聚类小样本万级以内、需要聚类树linkage、distance_threshold复杂度 O(n²)大数据跑不动DBSCAN 处理“大量沉默用户 少量活跃用户”的形态最合适。它的eps非常敏感太小全员离群点太大全并成一个簇。我常用的调试办法是画 K-距离图取拐点附近的eps值。层次聚类则适合样本量小、要给业务方讲清楚“分群依据”的场景树状图一眼能看到合并层次但这套用户画像系统动辄几十万用户层次聚类基本跑不动所以 KMeans 是默认主力。4. 特征权重与画像生成让聚类结果变成业务读得懂的人聚类只是打了组标签真正的画像还要能解释——你要告诉业务方“这个群体为什么是高价值用户”。特征权重就是回答这个问题的关键。4.1 特征权重计算TF-IDF 与随机森林特征重要性的双通道用户画像的特征权重我一般走两条路。如果特征里有文本内容比如用户搜索关键词、商品标题用 TF-IDF 衡量词的重要性如果是结构化特征就用随机森林或其他模型输出特征重要性两者结果合并起来交叉验证。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.ensemble import RandomForestClassifier import pandas as pd # 路径一文本行为特征用 TF-IDF docs df_user[search_keywords].fillna().tolist() vec TfidfVectorizer(max_features50) tfidf_matrix vec.fit_transform(docs) df_tfidf pd.DataFrame(tfidf_matrix.toarray(), columnsvec.get_feature_names_out()) # 路径二结构化特征用随机森林的特征重要性 rf RandomForestClassifier(n_estimators200, random_state42, max_depth8) rf.fit(X_scaled, df_user[cluster]) importance pd.Series(rf.feature_importances_, indexcluster_cols).sort_values(ascendingFalse) print(importance.head(10))TF-IDF 的直觉是一个词在某个用户的搜索记录里频繁出现但在全量用户里很少出现那这个词对区分这个用户的兴趣非常关键。max_features50限制词表大小否则上万个词的矩阵会稀疏到没法用。随机森林的特征重要性则回答“哪些行为字段对分群贡献最大”——如果beh_buy的贡献排第一说明购买行为是分群的主要驱动力如果recency_days排第一说明活跃度差异才是群与群之间最显著的差别。这两条路径的结果一旦矛盾比如 TF-IDF 显示某用户高频搜索“母婴”但随机森林却把他分进了“数码人群”那就要回头检查特征工程是否有字段泄漏或缺失。逻辑回归也能做特征权重而且它的系数带正负号能告诉你特征与目标比如高价值用户是正相关还是负相关比随机森林的feature_importances_多一层解释信息。缺点是对特征共线性敏感用之前先做相关性筛选。4.2 画像存储与可视化JSON 落地、matplotlib 出图、前端页面串联画像最终产物最好组织成“一个用户一个 JSON 对象”包含基本信息、行为特征和聚类标签。模型文件用joblib保存画像数据落地成 JSON这是可维护性最好的组合。源码里配套的chart.html、recommend.html页面就是这套 JSON 结构的展示端。import json import joblib import matplotlib.pyplot as plt import seaborn as sns # 保存聚类模型和标准化器在线服务需要复用 joblib.dump(final_model, kmeans_model.pkl) joblib.dump(scaler, scaler.pkl) # 每个用户输出一份画像 JSON profile {} for _, row in df_user.iterrows(): profile[str(row[user_id])] { cluster: int(row[cluster]), recency_days: int(row[recency_days]), active_span_days: int(row[active_span_days]), beh_view: int(row[beh_view]), beh_buy: int(row[beh_buy]), total_amount: round(float(row[total_amount]), 2) } with open(user_profiles.json, w, encodingutf-8) as f: json.dump(profile, f, ensure_asciiFalse, indent2) # 可视化簇内特征均值横向对比 cluster_summary df_user.groupby(cluster)[cluster_cols].mean() cluster_summary.T.plot(kindbar, figsize(12, 6)) plt.title(Cluster Feature Profile) plt.xticks(rotation45) plt.tight_layout() plt.savefig(cluster_profile.png, dpi150) # 可视化特征相关性热力图 plt.figure(figsize(10, 8)) sns.heatmap(df_user[cluster_cols].corr(), annotTrue, cmapcoolwarm, fmt.2f) plt.savefig(feature_corr.png, dpi150)chart.html在这里的价值是把上面的静态图变成可交互的页面。Bootstrap 已经把布局和样式搭好了剩下的工作就是写一个接口把画像 JSON 丢给前端前端用图表库读取数据渲染。login.html则负责访问鉴权——画像数据涉及用户隐私直接裸奔是不可接受的生产环境至少套一层登录验证。源码里这几个页面组合起来就是一个“登录 → 查看画像图表 → 查看推荐结果”的最小闭环。5. 用户画像系统避坑指南四个高频翻车点的现象、原因与解决画像系统从数据到上线坑比想象中多。下面四条是我在类似项目里反复踩过的按“现象 → 原因 → 解决”的格式写方便排查对照。5.1 现象聚类跑完80% 的用户被分到了同一个组原因通常是两个要么特征没做标准化高量纲字段完全主导了距离计算要么特征本身区分度不够。比如total_amount的量纲是万级browse_count只是个位数KMeans 算距离时基本只看金额其他特征形同虚设。解决先StandardScaler统一量纲再画轮廓系数曲线选 K。如果 K 在 2 到 9 的范围内轮廓系数都不到 0.3说明当前特征集合表达不了用户差异要回到特征工程加维度而不是硬调 K 值。另一个快速诊断方法打印各簇的中心点向量如果中心点之间只有一两个特征有差异其他都趋同说明有效特征太少了。5.2 现象新用户画像一片空白接口返回全是空字段原因很简单新用户在行为明细表里一条记录都没有groupby之后跟主档merge全成了 NaN。如果不处理聚类时会把这些空白用户塞到离原点最近的簇形成一个“垃圾簇”完全没有业务价值。解决行为特征列统一填充 0recency_days填一个大的默认值比如 999再建一个has_behavior标志列。我通常还会把无行为用户单独标记为cluster-1不参与常规运营推荐走冷启动策略——让推荐系统用内容热度或地域信息先试探。这一步不做线上接口给业务方返回一串空字段信任度直接归零。5.3 现象特征权重全为零TF-IDF 稀疏矩阵没有区分度有一次我跑完全流程发现 TF-IDF 矩阵全是零查了半天才找到原因搜索关键词列大面积缺失被fillna()变成了空字符串TF-IDF 对空串统计不出任何有价值的词频。加上当时还在用默认的stop_wordsenglish对中文文本完全没有过滤作用稀疏程度雪上加霜。解决缺失的文本样本要么丢弃要么把“无搜索行为”本身编码成一个 0/1 特征。中文场景下停用词表要换成中文的把“的、了、是、在”这类没有区分度的虚词滤掉。记住TF-IDF 的价值完全建立在有效文本分布上喂进去之前一定要确认非空文本占比过半数否则这个特征通道就是废的。5.4 现象离线 T1 画像和线上实时行为对不上推荐结果失真画像平台每天凌晨全量跑一次但用户当天上午买了商品下午推荐接口还在用昨天的画像推荐结果明显滞后。这是离线画像系统的通病现象就是“上午刚买的品类下午推荐里全是旧内容”。解决在离线全量画像之外加一条实时增量通道。增量数据先落到 Redis 之类的缓存里记录当天的recency_days重置和buy_count累加在线推荐时把增量特征拼接到离线特征上。注意这条通道只更新可增量计算的字段不做实时重聚类——聚类中心点每天都在线重算成本太高也没必要。业务上要接受这个边界画像分群是趋势判断实时行为是即时修正两者各司其职。6. 把画像接到业务里Flask 查询接口和一次人工验证抽检画像建完不落到接口上就只是个离线计算产物最后一步是搭一个查询服务。Flask 是最轻的选择源码里现有的login.html和chart.html正好能跟接口串起来。from flask import Flask, jsonify import json app Flask(__name__) # 启动时加载画像生产环境建议换成 Redis 缓存别每次请求都读文件 with open(user_profiles.json, r, encodingutf-8) as f: profiles json.load(f) app.route(/api/profile/user_id, methods[GET]) def get_profile(user_id): profile profiles.get(user_id) if not profile: return jsonify({error: user not found}), 404 cluster_tag {0: 高活跃高价值, 1: 潜力新客, 2: 沉默流失, 3: 价格敏感} profile[cluster_tag] cluster_tag.get(profile[cluster], 未知) return jsonify(profile) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)跑起来后请求/api/profile/1001就能看到某个用户的画像和分群标签。这个接口不单是给业务方喊数据用的它本身就是对画像系统的验证入口。我的习惯是每次上线或更新画像固定抽三个人验证——两个行为模式明确的老用户加上一个刚注册的新用户把接口返回的画像和业务直觉对一遍。有一次我就发现一个常买高端数码的用户被分进了“价格敏感”组查到最后是数据清洗时没排除退款记录导致total_amount算成负数聚类方向整体偏了。那种感觉就是及时踩住了刹车。从那以后我上线画像流程时强制要求抽检三个用户数值和直觉对不上就回头查特征工程哪怕多花半天也值得。这套检查法帮我挡过好几次翻车希望也能帮到你。本文还有配套的精品资源点击获取