简介一份基于Python构建用户画像生成系统的完整源码面向数据分析、产品运营及后端开发人员解决用户理解与精细化运营难题演示从用户行为数据采集清洗、特征工程、行为分析、聚类分群到画像构建与可视化的全流程实现。资源共149个文件压缩包2.45MB以69个py源码文件为主附带53个pyc编译文件及前端页面所需的html/css/js、字体图标等静态资源便于直接运行与二次开发。目前已有352人学习使用。代码涵盖pandas数据预处理、sklearn聚类与特征编码、TF-IDF权重计算、matplotlib/seaborn可视化并通过Flask提供查询接口适合入门数据挖掘与用户画像项目的开发者参考实践也可作为毕业设计或企业精细化运营系统的初始模板。1. 用户画像生成系统不只是算个标签而是一条完整的数据流水线做运营或产品的朋友应该都有过这种经历手里攒了几万行用户行为数据老板一句「做个用户画像」丢过来以为跑个groupby再画几张饼图就交差结果发现画像根本落不了地——既说不清楚每个用户到底长什么样也没法和推荐、营销打通。这套基于 Python 实现的用户画像生成系统源码核心价值在于它不是「画几个标签」的演示脚本而是一条从数据清洗、特征工程、行为分析、聚类分群到画像存储和 Web 展示的完整流水线。它自带data.csv和newdata.csv两份数据、login.html登录页、chart.html图表页、recommend.html推荐页跑通后你直接能拿到「用户 ID 特征向量 分群标签 特征权重」的画像结果适合正在做用户增长、个性化推荐或精细化运营的 Python 工程师和数据从业者。2. 数据预处理与特征工程从 data.csv 到能进模型的特征向量2.1 读取、清洗与融合pandas 三板斧这套源码的第一步是把原始行为数据洗干净。我拆完data.csv后发现它里面存的是典型的「宽表 长表混合」结构有用户基础属性列也有每次行为的明细记录。直接用read_csv()读进来会碰到两个最典型的问题一是缺省值散落在各列二是用户多次行为的记录没法直接参与建模。源码里用到的 pandas 三板斧——dropna()、groupby()、merge()——正好对应这两个问题。import pandas as pd # 读取原始数据 df_raw pd.read_csv(data.csv, encodingutf-8) # 去除关键字段缺失的行不对整表暴力删除 df_clean df_raw.dropna(subset[user_id, behavior_type]) # 对同一用户的行为记录做聚合得到用户级特征 user_behavior df_clean.groupby(user_id).agg( total_behaviors(behavior_type, count), unique_items(item_id, nunique), avg_rating(rating, mean) ).reset_index() # 与用户基础属性表融合 df_user pd.merge(user_behavior, df_clean[[user_id, age, gender, city]].drop_duplicates(), onuser_id, howleft)这段代码有三个地方值得细说。第一dropna()我特意加了subset参数只删除user_id和behavior_type缺失的行而不是对整个表做dropna()——因为用户画像场景里rating缺失很常见粗暴删除会让样本量骤减。第二groupby()聚合时用了三种不同的函数count统计行为总量nunique统计用户看过多少个不同的商品mean计算平均评分这三种特征分别对应活跃度、覆盖面和偏好强度是画像的基础维度。第三merge()的时候howleft保证行为数据为主表避免用户被基础属性表里的重复记录放大。这里还有个细节groupby().agg()之后reset_index()是必须的否则user_id会变成索引而不是列后续merge()会直接报 KeyError。我见过不少初学者在这一步翻车以为 pandas 自动处理了索引实际上groupby后的结果索引是分组键必须显式重置。2.2 类别编码与数值标准化不是所有数据都能直接喂给模型清洗完数据下一步是特征工程。这套源码里gender、city这类字段是字符串KMeans之类算法根本不认。sklearn 提供了三种常用处理手段LabelEncoder适合有序类别比如会员等级OneHotEncoder适合无序类别比如城市StandardScaler处理数值量纲差异。源码中默认的做法是把类别变量做OneHotEncoder数值变量做StandardScaler这个选型是对的。from sklearn.preprocessing import OneHotEncoder, StandardScaler import numpy as np # 类别特征独热编码 categorical_cols [gender, city] encoder OneHotEncoder(handle_unknownignore) encoded encoder.fit_transform(df_user[categorical_cols]).toarray() # 数值特征标准化 numeric_cols [age, total_behaviors, unique_items, avg_rating] scaler StandardScaler() scaled scaler.fit_transform(df_user[numeric_cols]) # 拼接成最终特征矩阵 X np.hstack([scaled, encoded]) print(f特征矩阵形状: {X.shape})关于OneHotEncoder有两个容易踩的坑。第一handle_unknownignore这个参数必须加——如果后续用newdata.csv做预测时出现了训练集里没见过的城市不设这个参数会直接抛异常设了之后会输出全 0 向量虽然不完美但至少不会让整个管线崩溃。第二.toarray()不能省OneHotEncoder默认返回稀疏矩阵直接np.hstack拼接会报维度不匹配的错误。数值特征这边StandardScaler的意义在于把年龄可能 20-60和总行为数可能几千拉到同一量纲否则聚类时年龄特征基本会被淹没KMenas 的距离计算会被大数值特征主导。还有一个常见误用是用LabelEncoder处理city。直接把城市编码成 0、1、2、3… 看起来没问题但会让 KMeans 误以为城市 3 和城市 4 的「距离」比城市 0 和城市 4 近形成无意义的相似度。城市是无序类别必须用独热编码。这条经验我是在做电商画像时付了学费才明白的——当时用 LabelEncoder 编码了一堆城市字段聚类结果里出现了一个「北京 上海」的诡异群体后来才发现是编码顺序导致的假距离。3. 用户行为分析与聚类分群从行为序列到可解释的用户群体3.1 时间序列与行为频次把浏览、购买、点击变成画像特征用户画像不能只靠静态属性行为数据才是灵魂。data.csv里每条记录带时间戳和behavior_type字段浏览、加购、购买、收藏等。源码中分析行为数据的方式分两层一层是统计频次用value_counts()看每个用户在不同行为类型上的分布另一层是分析时间规律用datetime模块提取用户活跃时间段、购买周期等特征。import datetime as dt # 解析时间戳 df_clean[timestamp] pd.to_datetime(df_clean[timestamp], units) # 提取时间特征 df_clean[hour] df_clean[timestamp].dt.hour df_clean[weekday] df_clean[timestamp].dt.weekday # 每个用户的行为类型分布 behavior_profile df_clean.groupby([user_id, behavior_type]).size().unstack(fill_value0) behavior_profile.columns [fbehavior_{col} for col in behavior_profile.columns] # 用户活跃时间段:统计用户行为集中在哪个时段 df_clean[time_slot] pd.cut(df_clean[hour], bins[0, 6, 12, 18, 24], labels[night, morning, afternoon, evening], rightFalse) slot_profile df_clean.groupby([user_id, time_slot]).size().unstack(fill_value0) # 关联规则挖掘:寻找行为之间的关联 from mlxtend.frequent_patterns import apriori, association_rules # 构造购物篮格式:每个用户购买的商品集合 basket df_clean[df_clean[behavior_type] buy].groupby( [user_id, item_id] ).size().unstack(fill_value0).astype(bool) frequent_itemsets apriori(basket, min_support0.05, use_colnamesTrue) rules association_rules(frequent_itemsets, metriclift, min_threshold1.2)时间特征这块有个设计细节值得学习pd.cut把 24 小时切成夜晚、上午、下午、晚间四个时段比直接用 hour 数值特征更稳健——因为凌晨 1 点和凌晨 3 点对用户画像来说语义相同但数值上距离却很远直接进模型会扭曲相似度。dt.weekday提取周几则用来区分工作日/周末活跃型用户这直接对应两类典型画像上班族通勤刷和周末集中购。关联规则挖掘我用的是mlxtend库它是apriori的主流实现。注意basket需要转成 bool 型因为apriori只关心某个商品是否被购买不关心买了几次。min_support0.05意味着至少 5% 的用户同时购买了两个商品组合低于这个阈值的组合因为样本太少没有统计意义metriclift和min_threshold1.2则要求规则的提升度大于 1.2即「买了 A 又买 B」的概率是随机情况下的 1.2 倍以上。如果拆包时发现mlxtend没装pip install mlxtend一条命令就解决。3.2 KMeans 与 DBSCAN 的选型不是聚类数越多越好也不是所有数据都适合 KMeans聚类是这套系统的核心环节。源码里同时给出了 KMeans 和 DBSCAN 两种算法的调用入口但默认走 KMeans。我的拆解结论是这个选择在大多数行为画像场景下是对的但必须知道为什么对。KMeans 的强项是快、可解释、结果稳定适合用户基础属性 行为特征构成的高维连续向量。但 KMeans 有两个硬伤第一需要预先指定 K 值第二对异常值敏感离群用户会把聚类中心拉偏。源码里通过肘部法则辅助选 K——画出不同 K 值下的 SSE簇内误差平方和找拐点。这个做法实操中能给出一个合理区间但拐点经常不清晰我一般会结合业务含义来定比如电商场景分 3-5 个群比较符合运营直觉。from sklearn.cluster import KMeans, DBSCAN from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt # 肘部法则确定最佳K值 sse [] silhouette_scores [] K_range range(2, 10) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X) sse.append(km.inertia_) silhouette_scores.append(silhouette_score(X, labels)) # 取 K4 进入正式聚类 final_k 4 kmeans KMeans(n_clustersfinal_k, random_state42, n_init10) df_user[cluster] kmeans.fit_predict(X) # 对比:DBSCAN处理环形分布和噪声 dbscan DBSCAN(eps1.5, min_samples5) df_user[cluster_dbscan] dbscan.fit_predict(X) # 输出每个群的中心特征,便于解释画像 cluster_centers pd.DataFrame( scaler.inverse_transform(kmeans.cluster_centers_[:, :len(numeric_cols)]), columnsnumeric_cols ) print(cluster_centers)这里有几个参数需要解释。n_init10表示 KMeans 用 10 组不同的初始中心跑 10 次选 SSE 最小的结果避免随机初始化导致的局部最优。random_state42保证可复现——如果你不设每次跑出来的分群结果可能不一样这在调参阶段是致命的。silhouette_score是轮廓系数取值范围 [-1, 1]越接近 1 表示簇内紧凑、簇间分离一般 0.25 以上就说明聚类结构可接受。DBSCAN 的eps1.5和min_samples5我拆的时候调了几次才稳定。eps是邻域半径决定了「距离多近算邻居」这个值对数据尺度极其敏感所以必须先做标准化再跑 DBSCAN。min_samples是核心点最少邻居数设太小会产生大量微簇设太大会把大多数人归为噪声点。DBSCAN 的优势是不用指定 K、能识别任意形状的簇、能自动标出异常用户——在用户行为画像里离群点往往对应刷单用户或爬虫账号有独特的业务价值。但它的劣势也很明显参数敏感eps稍微调大一点聚类结果天翻地覆。我的建议是正常业务用 KMeans 分群额外跑一次 DBSCAN 把噪声用户筛出来单独标记一举两得。4. 特征权重与画像构建从分组标签到可解释的用户模型4.1 TF-IDF 与模型特征重要性两种权重算法的适用边界用户分群只回答了「用户属于哪一类」但「为什么被分到这一类」需要特征权重来解释。源码提供了两种计算特征权重的方式TF-IDF 和基于模型的特征重要性。TF-IDF 原本是信息检索领域计算文本关键词权重的算法但用在画像场景里思路是一样的把每个用户的行为序列当成「文档」把行为特征当成「词」TF 衡量该特征在这个用户身上出现的频繁程度IDF 衡量该特征在所有用户中的稀缺程度。两者相乘既能突出个人特征又能抑制共性特征。from sklearn.feature_extraction.text import TfidfVectorizer # 将每个用户的行为序列构造成文本 df_user[behavior_text] df_user.apply( lambda row: .join( [fbeh_{col} for col in behavior_profile.columns if row[col] 0] ), axis1 ) # 计算TF-IDF权重 tfidf TfidfVectorizer(token_patternr\S) tfidf_matrix tfidf.fit_transform(df_user[behavior_text]) # 取出特征名和权重 feature_names tfidf.get_feature_names_out() user_weights tfidf_matrix.toarray() # 与模型特征重要性对比 from sklearn.ensemble import RandomForestClassifier # 用聚类结果做伪标签,训练随机森林获取特征重要性 y_cluster df_user[cluster] rf RandomForestClassifier(n_estimators100, random_state42) rf.fit(X, y_cluster) importance_df pd.DataFrame({ feature: numeric_cols list(encoder.get_feature_names_out(categorical_cols)), importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse)TF-IDF 做画像权重的优势是完全无监督不需要人工标注。它的局限性在于只能反映「行为组合的区分度」无法反映「行为与业务目标的关联度」——比如「浏览 100 个商品」这个特征在 TF-IDF 下权重可能很高但如果这些浏览没有转化为购买对营销价值其实有限。所以源码也提供了第二种方案用随机森林等模型跑特征重要性。做法是先把聚类结果当作「伪标签」训练一个分类模型然后从模型里读feature_importances_。这样选出来的特征是两个维度的折中既包含区分用户的信号也包含对预测分群有用的信息。实操里我一般两个都跑一遍如果两者选出的 top 特征差异很大说明特征工程环节可能有问题需要回头检查。4.2 画像存储用 json 和 pickle 输出可复用的用户画像画像算完之后必须存下来供上层 Web 应用实时调用。源码里同时用了json和pickle两种格式这个选择是合理的pickle适合保存 Python 对象本身比如训练好的模型、编码器、聚类器json适合保存面向业务方阅读的画像摘要。import json import pickle # 保存模型和预处理器,供后续使用 with open(models/encoder.pkl, wb) as f: pickle.dump(encoder, f) with open(models/scaler.pkl, wb) as f: pickle.dump(scaler, f) with open(models/kmeans.pkl, wb) as f: pickle.dump(kmeans, f) # 构建可读的用户画像JSON def build_user_profile(user_id, cluster_label, weights, top_n5): profile { user_id: user_id, cluster: int(cluster_label), top_features: sorted( zip(feature_names, weights), keylambda x: x[1], reverseTrue )[:top_n], timestamp: dt.datetime.now().isoformat() } return profile # 为部分用户生成画像 sample_users df_user[user_id].head(100).tolist() profiles [] for i, uid in enumerate(sample_users): profiles.append(build_user_profile(uid, df_user[cluster].iloc[i], user_weights[i])) # 写入json文件 with open(output/user_profiles.json, w, encodingutf-8) as f: json.dump(profiles, f, ensure_asciiFalse, indent2)pickle存模型有个重要细节必须把encoder、scaler、kmeans三个对象一起保存因为预测新用户时需要走完全相同的预处理流程。如果只保存 KMeans 模型而忘了保存scaler新数据进来你没有标准化工具维度都对不上。json.dump时ensure_asciiFalse必须加否则中文特征名会变成\uXXXX转义序列业务方拿到画像根本读不懂。indent2让 JSON 文件可读性更好方便直接用文本编辑器检查问题。这个画像 JSON 里包含了用户 ID、所属分群和 top 特征权重。如果你想看某用户「为什么被标记为高价值用户」查他的top_features一眼就明白。实际交付时我还会在 JSON 里加一个cluster_name字段把数字标签映射成业务名比如「高活跃高转化——核心用户」这个映射关系可以放在一个单独的配置字典里方便运营直接照着名单做策略。5. 避坑笔记聚类数量、数据泄露与新老数据不一致的四个教训5.1 现象聚类结果每次跑都不一样用 KMeans 跑同一份数据第一次聚类结果和第二次完全对不上用户分群标签差别很大。原因在于 KMeans 的初始中心点是随机选择的不同的初始点可能收敛到不同的局部最优解。解决方法是设置random_state42固定随机种子同时把n_init从默认的 10 调大源码里默认就是这个配置。还有一个补充手段先跑一次 KMeans把得到的聚类中心作为下一次的init参数传入这样后续每次结果都会保持一致。5.2 现象模型在训练集上效果很好但用 newdata.csv 预测时报错newdata.csv里的城市字段出现了data.csv里没有的值OneHotEncoder.transform()直接抛 ValueError。原因是没有设置handle_unknownignore导致编码器无法处理未见过的类别。解决方式是在训练时初始化编码器加上这个参数。更稳妥的做法是如果新数据里的类别值比较多可以考虑在特征工程阶段就把城市字段泛化成省份或区域层级降低类别空间的稀疏性。这个问题的根源是模型在生产环境里遇到的分布和训练集不同属于典型的「特征分布漂移」也是线上画像系统最容易翻车的地方。5.3 现象聚类完成后发现某个「高价值用户群」的特征全是缺失值填充的 0我在拆包时发现这个问题源码把datetime转换后的时间特征直接拼进了特征矩阵但因为部分用户的购买记录缺失pd.cut切片后产生大量 NaN这些 NaN 被StandardScaler当成缺失值传播最终聚类时 sklearn 直接报错。我没有直接删掉这些用户而是用fillna(0)填充缺失时间特征——对行为画像来说「没有购买记录」本身就是一种有效信息填 0 比删行更合理。另一个容易踩的坑是groupby().unstack(fill_value0)里fill_value0不能省否则behavior_profile里会出现 NaN 列拼特征矩阵时维度错乱。5.4 现象可视化图表里用户群特征分布和聚类结果对不上画chart.html时发现图表中「高活跃用户群」的平均行为数明显偏低和聚类中心的特征值矛盾。原因是图表代码用的是data.csv的原始数据而聚类用的是清洗、编码、标准化之后的数据两份数据的用户 ID 顺序在merge()之后被打乱了导致图表里张冠李戴。解决方式很简单所有可视化操作必须在df_user这个最终数据框上进行不要回头去用中间变量df_clean或原始df_raw的索引。我一般会在聚类完成后立刻把user_id和cluster的对应关系导出成一张表所有图表和报表都基于这张表生成避免索引错位的问题。5.5 现象json 文件里中文全部变成 \uXXXX 乱码业务方反馈用户画像文件里特征名全是转义字符无法阅读。原因是json.dump默认ensure_asciiTrue会把所有非 ASCII 字符转义。解决方式是在json.dump时显式设置ensure_asciiFalse。这个坑极其常见几乎所有 Python 工程师都遇到过。还有一个延伸问题读取这种 JSON 文件时如果文件编码不是 UTF-8json.load也会报 UnicodeDecodeError所以写入时最好以encodingutf-8显式打开文件读取时也要保持一致。6. 把画像变成能用的东西chart.html 与 recommend.html 的联动实现画像算完、存好最后一步是让非技术角色用起来。这套源码里login.html、chart.html、recommend.html三个页面构成一个完整的业务闭环。我拆包时的关注点是「怎么把 Python 端的画像结果渲染到浏览器里」。常见做法是用 Flask 做 Web 服务层把output/user_profiles.json读进内存通过 Jinja2 模板渲染chart.html同时提供一个/api/recommend接口给recommend.html调。下面这个示例展示了核心的联动逻辑。from flask import Flask, jsonify, render_template, request import json import pandas as pd app Flask(__name__) # 加载画像数据和聚类模型 with open(output/user_profiles.json, r, encodingutf-8) as f: profiles json.load(f) profiles_df pd.DataFrame(profiles) app.route(/) def login_page(): return render_template(login.html) app.route(/dashboard) def dashboard(): # 按分群汇总画像特征,供图表页面展示 cluster_summary profiles_df.groupby(cluster).size().reset_index( nameuser_count ).to_dict(orientrecords) return render_template(chart.html, clusterscluster_summary) app.route(/api/recommend) def recommend(): user_id request.args.get(user_id) profile profiles_df[profiles_df[user_id] int(user_id)] if profile.empty: return jsonify({error: user not found}), 404 # 取该用户所在分群的特征权重Top作为推荐依据 cluster_label profile.iloc[0][cluster] top_features profile.iloc[0][top_features] return jsonify({ user_id: user_id, cluster: cluster_label, feature_weights: top_features }) if __name__ __main__: app.run(debugTrue, port5000)在这个联动方案里/dashboard路由从profiles_df里按cluster聚合用户数量传给chart.html渲染群体分布图。/api/recommend则是画像落地的关键——它接收user_id先查画像库确认用户存在再取出该用户的top_features作为推荐依据。实际业务中你还可以在这个接口里加上「同分群用户最常购买的商品 TOP N」的逻辑那就是完整的协同过滤推荐了。这套系统的边界很清楚它把「用户分群 特征权重」这件最脏最累的活做完了但是推荐策略本身、前端页面的交互逻辑都需要你自己填充。如果你已经有自己的推荐算法画像结果可以作为特征输入给模型如果还没有直接从同分群用户的购买记录里统计高频商品来推荐是最轻量的起步方案。拆完这套源码还有一个很重要的体会用户画像系统的技术栈并不复杂pandas、sklearn、Flask 三板斧而已真正的复杂度在数据质量和业务理解上。从那以后我每次做画像系统都强制走一遍标准流程——先画数据字典、再定义分群语义、最后才碰聚类代码数据检查不过关坚决不进入建模环节。希望这篇拆解能帮你在自己的数据上少走几个来回。本文还有配套的精品资源点击获取
