十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python构建用户画像系统:从特征工程到Flask API落地

Python构建用户画像系统:从特征工程到Flask API落地 简介这是一份基于Python实现的用户画像生成系统源码适合数据运营、推荐系统开发人员及Python进阶学习者参考。资源围绕数据收集与预处理、特征工程、用户行为分析、聚类、特征权重计算、画像构建、可视化展示及Web系统集成等完整流程展开包含大量可直接运行的Python脚本、前端页面文件和示例数据便于对照实际业务理解并复用。压缩包共149个文件以py源码和pyc编译文件为主辅以HTML/CSS/JS页面、csv数据、png图片与字体资源整体大小2.45MB结构清晰。已有352人学习下载。借助该资源可快速搭建推送、营销场景下的基础用户画像工具并对特征标准化、KMeans聚类、TF-IDF权重计算等核心环节形成直观的工程认知。1. 用户画像不只是给用户贴标签而是一条从 CSV 到 API 的数据流水线业务方说给我一份高价值用户画像很多人的第一反应是去数据库跑一条 SELECT把用户分层贴好标签交差。现实是一套能支撑个性化推荐和精细化运营的用户画像系统要经历数据清洗、特征编码、聚类分群、权重计算、存储和 Web 接口落地这些环节任何一环断了下游都拿不到可用结果。这套基于 Python 实现的用户画像生成系统输入是 data.csv 和 newdata.csv 两份用户行为数据输出是 chart.html 可视化报表、带登录的 login.html 和推荐页 recommend.html 组成的可演示 Web 应用。它适合正在搭推荐系统、用户运营后台的数据工程师也适合需要快速做一个可扩展画像原型的开发者。下面从特征工程开始拆这条流水线。2. 数据清洗与特征工程把 data.csv 转成特征矩阵的自动化步骤用户画像的第一道坎是数据质量不是算法。实际拿到的 data.csv 通常混合了数值、类别和文本字段常见结构包括 user_id、age、gender、register_days、monthly_spend、browse_history 等。用 pandas 探查时我一般先跑df.head()和df.info()看字段类型和空值率再决定清洗策略。特征工程的本质就是把这种异构数据统一成一张特征矩阵后续 KMeans 聚类和权重计算都依赖这张矩阵矩阵质量直接决定画像的可用性。2.1 缺失值、去重与类型修正拿到 data.csv 后第一步处理缺失值和类型问题。age 和 monthly_spend 这类数值字段出现空值我一般用中位数填充而不是均值因为用户消费分布是长尾的均值容易被高客单价用户拉偏gender 这类类别字段空值则单独填充为 unknown把缺失本身保留成一种信息。至于摘要里提到的dropna()在这个阶段并不适合直接全行删除某一列缺失不代表这个用户没有画像价值按列填充比按行删除保留的用户信息多得多。import pandas as pd df pd.read_csv(data.csv) print(df.info()) # 数值字段用中位数填充避免长尾分布被均值带偏 num_cols [age, register_days, monthly_spend] for col in num_cols: df[col] pd.to_numeric(df[col], errorscoerce) # 非法值转 NaN df[col] df[col].fillna(df[col].median()) # 类别字段保留 unknown 占位 df[gender] df[gender].fillna(unknown) # user_id 去重同一用户保留最近一条行为记录 df df.drop_duplicates(subset[user_id], keeplast)这里pd.to_numeric(..., errorscoerce)会把 NA、abc 这类脏数据强制转成 NaN再统一填充比直接报错中断稳健得多。drop_duplicates的keeplast表示重复记录保留最后写入的一条适合行为日志按时间追加的场景。清洗阶段还有个常用操作是按用户聚合行为频率比如df.groupby(user_id).size()统计每个用户产生的行为条数这个值本身就可以作为活跃度特征进入后面的画像字段。字段原始类型清洗动作说明age数值中位数填充抗噪性强于均值register_days数值转数值后中位数填充注册时长缺失可作未知处理monthly_spend数值中位数填充消费长尾分布均值不可靠gender类别填 unknown保留缺失信息避免引入偏差browse_history文本下游 TF-IDF本轮只做空值兜底2.2 类别编码与数值标准化清洗完就要做编码和标准化。gender 这种无序类别用LabelEncoder会给类别强加大小关系比如 male 被编成 1、female 被编成 2聚类算法会把 2 当成更大的数值参与距离计算这是错误的。对无序类别应该用 OneHotEncoder。为控制特征维度这里把 gender 展开成 is_male、is_female、gender_unknown 三个 0/1 列。数值特征则用 StandardScaler 做标准化让 age 和 monthly_spend 不再因为量纲不同而主导距离计算。from sklearn.preprocessing import OneHotEncoder, StandardScaler import pandas as pd # OneHotEncoder 处理类别列 encoder OneHotEncoder(sparse_outputFalse, handle_unknownignore) gender_encoded encoder.fit_transform(df[[gender]]) # 把编码结果拼回 DataFrame丢弃原始 gender 列 gender_df pd.DataFrame( gender_encoded, columnsencoder.get_feature_names_out([gender]) ) df pd.concat([df.drop(columns[gender]), gender_df], axis1) # 对连续特征做标准化消除量纲影响 scaler StandardScaler() scaled scaler.fit_transform(df[[age, register_days, monthly_spend]]) df[[age_scaled, register_days_scaled, monthly_spend_scaled]] scaledOneHotEncoder 的handle_unknownignore表示 future 数据里出现训练集没见过的类别时编码结果全 0 而不是直接报错这个参数在增量更新 newdata.csv 时非常关键。StandardScaler 计算的是 (x - mean) / std让不同量纲的特征落入同一尺度。sparse_output参数在 sklearn 1.2 之前叫sparse升级版本后改名旧代码迁移时注意兼容。2.3 多表合并与增量拼接工程里的数据不只一份。newdata.csv 是新增的用户行为日志与 data.csv 存在部分重复用户。合并时我用pd.merge按 user_id 对齐两张表的特征列再用pd.concat把新用户记录追加到主表后面形成带全量特征的训练矩阵。这一步的细节会直接影响最后一章要讲的增量更新策略。newdf pd.read_csv(newdata.csv) # 按索引对齐两张表重复字段用后缀区分 merged pd.merge( df.drop(columns[user_id]), newdf, left_indexTrue, right_indexTrue, howinner, suffixes(_old, _new) )howinner只保留两张表都有的 user_id适合确认新增数据与历史数据的对齐情况suffixes参数解决两表相同字段名的冲突。这一步做完数据集才真正变成一张可以喂给聚类模型的特征矩阵。数据质量检查里有一个经验值清洗前后用户量差异超过 30% 时要回头确认是不是清洗逻辑误删了有效用户而不是直接信任代码结果。3. KMeans 聚类与特征权重让用户分群可解释特征矩阵准备好后核心问题变成哪些用户应该被归为一类。这套系统用的是 KMeans而不是 DBSCAN 或层次聚类。选型理由很简单业务侧需要稳定、可复述的分群结果KMeans 每次跑完输出的是确定的分群中心和类别标签运营可以直接拿去配置人群包。层次聚类在 Python 里也能实现但数据量到十万级以后凝聚层次聚类的算法复杂度会明显拖慢训练节奏。DBSCAN 不需要预先指定簇数但密度阈值对行为数据这种高维稀疏矩阵极难调稳线上跑起来容易出现大量点被标记为噪声。3.1 KMeans 参数选择与分群逻辑KMeans 需要指定的核心参数是 n_clusters、init、n_init 和 random_state。n_clusters 决定分几群最终通过肘部法则和轮廓系数共同确定而不是拍脑袋定init 默认 k-means用距离平方加权初始化中心点收敛更快n_init 建议保留默认的 10跑多组初始中心取最优避免每次结果波动太大。random_state 固定后同样的输入跑出来的结果逐字节一致这在做线上画像更新时非常关键不然每天用户的 cluster 编号都在跳运营配置的人群包会全部错乱。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score feature_cols [ age_scaled, register_days_scaled, monthly_spend_scaled, is_male, is_female, gender_unknown ] X df[feature_cols].values # 固定随机种子保证画像可复现 kmeans KMeans(n_clusters4, initk-means, n_init10, random_state42) df[cluster] kmeans.fit_predict(X) score silhouette_score(X, df[cluster]) print(f轮廓系数: {score:.3f})KMeans 对初始中心敏感n_init10 表示用 10 组不同初始中心分别运行只保留聚类误差最小的一组结果。random_state 不设的话每次跑出来的 cluster 编号都是乱序的你会遇到上周的 1 群用户这周变成了 3 群这种无法解释的画像漂移。提示cluster 编号只在同样的特征列顺序和 random_state 下有意义。任何一列特征位置调整都会导致编号整体漂移特征列顺序要固化成配置项不要手写三遍。参数取值作用说明n_clusters4肘部法确定分群数决定画像粒度initk-means中心点初始化策略加速收敛n_init10多组初始中心取最优缓解局部最优random_state42固定种子保证画像可复现max_iter300默认单次迭代上限防止不收敛3.2 簇数怎么定肘部法则与轮廓系数组合判断簇数选择没有绝对标准我一般把 2 到 8 的 K 值全部跑一遍画出 inertia 的肘部图和 silhouette_score 曲线取拐点明显且轮廓系数较高的那个 K。这里给出一个简化版本直接循环计算results [] for k in range(2, 9): km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X) results.append({ k: k, inertia: km.inertia_, silhouette: silhouette_score(X, labels) })inertia_是所有样本到所属簇中心的距离平方和K 增大时必然下降下降趋势突然变缓的那个点就是肘部。轮廓系数接近 1 表示类内紧凑、类间分离低于 0.25 说明当前特征对分群没什么区分度。这时候不要急着调 K先回去重新做特征工程考虑加入 browse_history 的 TF-IDF 特征。这里膝盖的位置通常不会是一个明确的点而是一个区间业务上宁可多分几群也不要少分运营更愿意合并人群包而不是拆不开一个过粗的群体。3.3 特征权重从 TF-IDF 到模型权重摘要提到可以用 TF-IDF 给特征加权。具体落到这份数据browse_history 是文本列需要先用分词切出候选词再用 TfidfVectorizer 把每个用户的行为文本转成向量按词频逆文档频率给词加权。画像标签里权重最高的几个词直接成为用户偏好关键词会被下游推荐逻辑直接读取。更细的行为关联可以跑 Apriori 挖掘频繁项集但那条线和 KMeans 的画像生成主线关系不大一般单独建任务。from sklearn.feature_extraction.text import TfidfVectorizer # 把浏览历史转成空格分隔的文本交给 TfidfVectorizer 统计 tfidf TfidfVectorizer(max_features50, stop_wordsenglish, token_patternr(?u)\b\w\b) tfidf_matrix tfidf.fit_transform(df[browse_history].fillna())max_features50限制词典大小防止画像标签被长尾词刷屏中文场景需要换成 jieba 分词英文场景直接按空格切。注意 TF-IDF 在这里的作用是给文本特征降维和赋权而不是生成聚类主特征。文本向量和数值特征量纲差异太大混进同一矩阵会让 KMeans 完全被文本维度主导我一般把文本权重单独存为标签不参与聚类距离计算。4. 画像落盘与 chart.html 可视化JSON 存储和图表挂载分群完成之后画像数据不能停在 DataFrame 里业务系统要能访问。我通常会做两层输出第一层用 json.dump 把画像按 user_id 落盘成结构化文件第二层用 matplotlib 和 seaborn 生成图表base64 编码后嵌进项目的 chart.html。项目里的 bootstrap.css 负责页面样式glyphicons-halflings-regular.eot 提供图表页图标字形整体就是一个可以直接打开的静态可视化页面不需要额外启动服务也能看。4.1 画像 JSON 结构设计画像文件的关键是分层组织不要平铺一百个特征列。我的习惯是 basic属性信息、behavior行为偏好、cluster分群结果三层结构。这样推荐服务拿到 user_id 后可以直接读取 behavior 里的关键词做倒排匹配不需要重新计算特征。import json profile { user_id: U1001, basic: { age: 28, gender: male }, behavior: { monthly_spend: 1260.50, register_days: 365, keywords: [数码, 户外, 健身] }, cluster: 2 } with open(profiles/U1001.json, w, encodingutf-8) as f: json.dump(profile, f, ensure_asciiFalse, indent2)ensure_asciiFalse保证中文关键词不被转义成 \uXXXX 序列indent2 方便人工排查。按 user_id 分文件存储的好处是更新单个画像时不用重写整个库代价是文件数量多百万级用户时建议换成 SQLite 或列式存储。这套源码配套的 chart.html 和 recommend.html 读取的就是这种结构化 JSON字段名一改前端逻辑也跟着要动所以字段命名要在项目启动前定死。4.2 matplotlib 与 seaborn 出图base64 嵌入 HTMLchart.html 里展示的画像图我用 seaborn 的 countplot 画人群分布用 heatmap 画行为特征相关性。matplotlib 生成的 PNG 以 base64 字符串直接写在 img 标签的 src 里避免引入静态资源服务器。中文字体是常见坑Linux 上默认没有中文字体时图内中文会全部变成方块需要先指定系统已安装的中文字体。import matplotlib import matplotlib.pyplot as plt import seaborn as sns import base64 from io import BytesIO # 指定中文字体避免图内中文显示为方块 matplotlib.rcParams[font.sans-serif] [WenQuanYi Micro Hei, SimHei] matplotlib.rcParams[axes.unicode_minus] False fig, ax plt.subplots(1, 2, figsize(12, 4)) sns.countplot(datadf, xcluster, axax[0]) sns.heatmap(df[[monthly_spend_scaled, register_days_scaled]].corr(), axax[1]) buf BytesIO() fig.savefig(buf, formatpng, dpi100) img_base64 base64.b64encode(buf.getvalue()).decode() html fimg srcdata:image/png;base64,{img_base64} / with open(chart.html, w, encodingutf-8) as f: f.write(f!DOCTYPE htmlhtmlheadlink relstylesheet hrefbootstrap.min.css/headbody{html}/body/html)dpi100 控制图片清晰度与文件大小的平衡base64 会让数据膨胀约 1.33 倍图表多了页面会明显变慢。直接内嵌是为了让 chart.html 可以单文件传给业务方不需要额外部署静态资源目录。如果图表规模上来应该把图片落到 independent 目录HTML 里只保留 img src 路径。图表类型展示内容适用场景countplot各聚类人群数量看整体人群分布heatmap行为特征相关性找特征冗余barplot聚类特征均值对比群组差异wordcloud偏好关键词运营快速理解群体4.3 Bootstrap 与页面样式挂载项目文件里的 bootstrap.css、bootstrap-theme.css 和 glyphicons-halflings-regular.eot 是前端部分的资源依赖。chart.html 里通过link relstylesheet hrefbootstrap.min.css引入样式glyphicons eot 字体文件给页面图标提供字形支持。我一般把可视化结果塞进 Bootstrap 的卡片容器和其他页面共用一套样式体系这样 login.html 和 recommend.html 即使由不同开发者维护视觉上也是一致的。实际操作中别漏了meta charsetutf-8否则图表页中文标题在部分浏览器里会乱码。5. Flask API 与 login.html / recommend.html 的前后端串联画像落盘不是终点运营平台需要查询入口。这套系统用 Flask 提供 REST 接口对外暴露用户画像查询能力。login.html 负责登录态签发recommend.html 负责展示基于画像的推荐结果。做接口设计时我坚持一个原则返回给前端的 JSON 只包含业务要用的字段不要把整个画像对象直接丢出去否则后端结构一变前端就要跟着改。5.1 /profile/user_id 查询接口与缓存最核心的接口是画像查询。用 user_id 作为路由参数先从内存缓存查没有再从 profiles 目录读 JSON 文件。为了避免业务方高频查询把磁盘 IO 打满我加了简单的 TTL 缓存过期时间兜底 300 秒。from flask import Flask, jsonify import os, time, json app Flask(__name__) cache {} app.route(/profile/user_id) def get_profile(user_id): # TTL 缓存300 秒内同一个 user_id 直接返回缓存 hit cache.get(user_id) if hit and time.time() - hit[ts] 300: return jsonify(hit[data]) path fprofiles/{user_id}.json if not os.path.exists(path): return jsonify({error: user not found}), 404 with open(path, encodingutf-8) as f: data json.load(f) cache[user_id] {data: data, ts: time.time()} return jsonify(data)user_id 直接拼进文件路径虽然是内网系统也应该加os.path.basename白名单校验防止路径穿越。TTL 缓存是开发期最简单有效的方案数据量上来后换成 Redis缓存键直接沿用 user_id过期时间从 300 秒调成业务可接受的分钟级。5.2 登录态签发与推荐页跳转login.html 是登录页提交的用户名密码校验通过后Flask 返回一个带用户标识的跳转地址前端拿到 uid 参数请求画像接口。推荐逻辑可以很朴素同一个 cluster 的用户把该簇内消费金额最高的 top N 商品作为推荐列表。这种做法是协同过滤最朴素的变体画像准确率不算高但是一天之内就能跑通全链路。from flask import request, redirect app.route(/login, methods[POST]) def login(): form request.form # 内部系统做轻量校验正式环境换成 JWT 或 session if form.get(username) admin and form.get(password) 123456: return redirect(f/recommend.html?uid{form.get(uid)}) return login failed, 401redirect把登录成功后的用户带到 recommend.html 并携带 uid 参数。生产环境密码不能硬编码需要改成从环境变量读取接口统一走 HTTPS。GET 参数在 URL 里明文可见涉及用户身份时尽量用 POST 加 token 方案。接口方法入参返回/loginPOSTusername, password, uid302 跳转 recommend.html/profile/user_idGET路径参数画像 JSON/recommend/user_idGET路径参数推荐商品列表6. 一个落地技巧用 newdata.csv 做画像增量更新最后的技巧是画像系统上线后一定会碰到的画像不能每次都全量重算。数据量到几十万用户后全量重跑 KMeans 和 TF-IDF 的耗时会让画像更新变成负担。常见做法是以 data.csv 建模用 newdata.csv 做增量更新把新用户合并进已有画像。先按 user_id 判断新数据用户是否已在画像库中。老用户只更新行为字段不动 cluster 标签新用户先临时挂到距离最近的聚类中心进入下一个日更周期再归入正式分群。行为特征更新时用指数加权让旧数据的影响随时间衰减不然用户消费习惯已经变了画像还停留在三个月前的状态。import pandas as pd import numpy as np newdf pd.read_csv(newdata.csv) old pd.read_csv(data.csv) existing_uids set(old[user_id]) decay 0.9 # 旧特征权重越小衰减越快 for _, row in newdf.iterrows(): if row[user_id] in existing_uids: # 老用户按月均消费做指数加权保留历史累积信息 old_spend old.loc[old[user_id] row[user_id], monthly_spend].mean() new_spend decay * old_spend (1 - decay) * row[monthly_spend] old.loc[old[user_id] row[user_id], monthly_spend] new_spend else: # 新用户先按最近距离挂到已有聚类中心 old pd.concat([old, row.to_frame().T], ignore_indexTrue) feat row[feature_cols].values.reshape(1, -1) old.loc[len(old) - 1, cluster] kmeans.predict(feat)[0]decay 取 0.9 表示历史数据占 90% 权重适合 monthly_spend 这类稳定性高的特征点击频率这类短期波动大的特征可以降到 0.7 左右。新用户直接调用kmeans.predict拿到最近的簇中心编号避免重训模型。注意如果新用户特征明显脱离已有簇predict 也会硬分配一个最近的簇需要设置离群距离阈值超过阈值的用户先挂进待观察分组等积累了足够行为再分群。增量脚本用 crontab 或 APScheduler 每天固定时间执行老用户占比超过 70% 后定期用全量数据重训一次模型。本文还有配套的精品资源点击获取
返回列表