☰
基于用户画像的购物网站商品推荐系统:从画像构建到算法落地
2026/10/11 10:39:10 网站建设 项目流程

简介:这是一套面向毕业设计场景的推荐系统完整项目,适合计算机相关专业学生快速搭建基于用户画像的购物网站推荐功能。项目分为Java网站端与Python算法端,网站端基于SSM并支持升级为SpringBoot,算法端使用TF-IDF特征提取与Word2Vec文档转向量技术,实现了物品画像与用户画像的构建、权重计算和TOP-N标签选取。配套资料包含项目源码、文档说明、演示视频与论文,代码均经过运行测试,功能正常,遇到问题可私聊远程教学。资源包共1172个文件,约26.96MB,涵盖HTML/CSS/JS前端页面、JSP/Java后端逻辑、Python算法脚本、SQL数据库脚本、XML配置、设计文档及演示录屏等,目录结构清晰,便于对照学习。已有104人学习下载,适合需要完整方案参考、快速理解画像推荐流程并完成课设或毕设的开发者使用。

1. 基于用户画像的购物网站商品推荐系统:这个毕设题背后是一整套可落地的技术栈

“基于用户画像购物网站商品推荐系统”这个题目每年毕业季都会在技术社区里被翻出来,换的只是商品品类:电影、音乐、图书,背后是同一套技术栈——先用注册信息和行为日志刻画出用户标签向量,再对不同品类做差异化召回,最后统一返回一个 Top-N 列表。

它解决的核心问题是让购物网站从“所有人看到同一张首页”变成“每个用户看到自己偏好的内容”,同时也把冷启动、稀疏数据、热门偏置这三个推荐领域最典型的难题暴露出来,正好成为论文里最有对比价值的实验章节。适合打算用 Python 落地的本科生,也适合刚接触生产环境推荐系统的从业者。下面讲的是能直接复现的完整路径。

2. 用户画像与推荐算法选型:先定策略再写代码

2.1 三种推荐路径:基于内容、协同过滤、混合推荐的取舍

写代码之前先想清楚策略。标题里的“基于用户画像”暗示了两条路:一是直接用画像向量做基于内容的召回,二是把画像作为协同过滤的特征而非替代品。刚接触推荐系统的人最容易一上来就写 SVD,写到一半发现评分矩阵稀疏得没法看——多数购物网站里,超过八成的用户从未对任何商品评过分。

常见做法是先把三个算法的适用场景分清。基于内容的思路是用物品标签和用户画像向量的相似度找相似物品,优点是结果可解释(“因为你喜欢悬疑片”),缺点是只能推荐和过去相似的东西;UserCF 找“和我行为相似的用户”喜欢过的物品,对电影、音乐这类兴趣社区化明显的商品效果好;ItemCF 找“和我买过的物品相似”的物品,在图书这种长尾品类里更能挖出潜在兴趣。

策略冷启动表现稀疏数据多样性可解释性计算成本
基于内容中(需画像)较好较差强低
UserCF弱较差中中随用户数上升
ItemCF弱中较好中随物品数上升
混合推荐强(热榜兜底)好好中最高

实际项目里不赌单一算法。我一般用两路召回加一步融合:电影走 ItemCF 加内容补充,音乐走 UserCF,图书走内容标签为主、ItemCF 为辅,最后都汇到同一个 Top-N 接口里。融合不是平均分,而是给每路召回分配权重,这个权重就是后面要反复调的核心参数。

2.2 用户画像字段怎么设计:从注册信息到行为日志的画像维度

画像不是一张表,是分层的。第一层是静态画像:用户注册时填的性别、年龄、常驻城市,以及购物网站在注册页让你勾选的兴趣标签。电影、音乐、图书三个品类天然适合这种设计,注册页可以直接让用户选“悬疑 / 摇滚 / 科幻小说”之类的偏好。第二层是动态画像,从行为日志里算出来。对购物网站来说,标准的行为类型是浏览、收藏、加购、购买、评分;电影和图书评分多,音乐评分比例低但收藏和试听多。

第三层才是推荐真正消费的标签画像:把物品元数据转成标签,再按用户行为加权聚合成“用户—标签—权重”向量。字段设计要映射到品类,同一套用户表可以兼容三个品类:电影需要影片类型、导演、主演、上映年份;音乐需要歌手、风格、语种;图书需要作者、出版社、中图法分类。

品类关键画像维度行为信号标签来源
电影类型、导演、演员、年份浏览、评分、收藏类型 / 导演 / 主题词
音乐歌手、风格、语种试听、收藏、重复播放风格 / 歌手
图书作者、出版社、分类浏览、加购、购买分类 / 主题

这个设计直接决定了代码结构。画像计算程序读三类输入:用户表、物品表、行为日志,输出一张只有三列的中间表:user_id、tag、weight。推荐召回程序只认这张表,不关心 tag 是“悬疑”还是“摇滚”——这正是“基于用户画像”四个字落到工程里的形态。

3. 用 Python 搭建画像数据层:从行为日志到用户兴趣向量

3.1 行为数据建模与预处理:把原始日志整理成可计算的结构

拿到项目源码后,我一般先不碰算法文件,先看数据文件长什么样。常见结构是三张表:users(用户信息)、items(物品元数据)、behavior / ratings(行为日志)。推荐系统领域的公开数据集也基本是这个结构,MovieLens 是 ratings.csv 加 movies.csv,Last.fm 是 user-artist-tags,Book-Crossing 是评分加书目信息。数据到手第一步是统一时间戳、统一 ID 类型、把散表合并成一张行为日志大表。

import pandas as pd # 三张原始表:路径按实际工程修改 users = pd.read_csv("data/users.csv") # user_id, age, gender, interests items = pd.read_csv("data/items.csv") # item_id, cate, title, tags behav = pd.read_csv("data/behavior.csv") # user_id, item_id, action, timestamp # 时间戳若是 Unix 秒,转成 datetime;若是字符串,直接 to_datetime behav["timestamp"] = pd.to_datetime(behav["timestamp"], unit="s") # 行为表与物品表合并,后面画像计算要用物品上的标签字段 log = behav.merge(items, on="item_id", how="left") log = log.merge(users[["user_id", "age", "gender"]], on="user_id", how="left") # 丢弃标签缺失的行:没有标签的物品进不了画像计算 log = log.dropna(subset=["tags"]) log = log.sort_values("timestamp").reset_index(drop=True) print(log.shape) print(log["action"].value_counts())

这段代码把三张表合并成一行一行为的宽表,画像计算只消费这个大表。unit="s" 表示时间戳是 Unix 秒,如果数据给的是毫秒要改 unit="ms";how="left" 保证行为记录全保留,物品信息缺失用 NaN 标出来再统一 drop。action 字段要检查枚举值,常见的是 view / fav / cart / buy / rating,拼写不统一就在这里顺手归一化,比如把 VIEW、View、view 全部统一成小写。

3.2 行为打分与时间衰减:把“看过/收藏/购买”转成画像权重

画像计算的本质,是对每个用户把所有行为过的物品标签加权累加。三个问题要在这里解决:不同行为的价值怎么定、时间久远的行为要不要衰减、最后向量拿什么比较。常见做法是预设一张行为权重表,再加一个时间衰减系数。行为权重没有标准答案,我的初始值一般是浏览 0.2、收藏 0.4、加购 0.7、购买 1.0、评分按 rating/5 作为强度系数。时间衰减用 1 / (1 + alpha × 天数),alpha 默认 0.01,意思是 100 天前的行为权重折半,300 天前只剩约四分之一。

ACTION_WEIGHT = {"view": 0.2, "fav": 0.4, "cart": 0.7, "buy": 1.0} ALPHA = 0.01 # 时间衰减系数:越大,历史行为折旧越快 def build_user_profile(log, tag_col="tags"): now = log["timestamp"].max() user_profile = {} for row in log.itertuples(): # 评分行为按评分强度折算;普通行为按动作类型取权重 if row.action == "rating": base_weight = row.rating / 5.0 # 评分 1~5 归一化 else: base_weight = ACTION_WEIGHT.get(row.action, 0.1) days = max((now - row.timestamp).days, 0) decay = 1.0 / (1.0 + ALPHA * days) weight = base_weight * decay # 物品标签是逗号分隔的字符串,拆开后逐个累加到用户向量 for tag in str(getattr(row, tag_col)).split(","): tag = tag.strip() if not tag: continue user_profile.setdefault(row.user_id, {}) user_profile[row.user_id][tag] = user_profile[row.user_id].get(tag, 0) + weight # 归一化:每个用户的标签权重归一成概率分布,避免行为多的用户向量值虚高 user_vec = {} for uid, tag_counter in user_profile.items(): total = sum(tag_counter.values()) if total > 0: user_vec[uid] = {tag: w / total for tag, w in tag_counter.items()} return user_vec

这个函数返回“用户 → {标签: 权重}”的字典,归一化后每个用户向量和为 1,后面算余弦相似度或直接做内容召回都方便。性能上它用循环遍历,几万行行为日志没问题,几十万行以上建议换成 groupby 聚合或用 scipy 稀疏矩阵,毕业设计量级不需要上 Spark。ALPHA 调到 0.05 时 20 天前的行为就折半,适合新闻资讯这类时效强的场景;购物网站和电影这类慢消费品用 0.01 更稳。

提示:行为权重表别凭感觉乱填。先按默认值跑一遍推荐结果,再手动抽查 10 个用户的推荐理由是否合理,权重微调幅度控制在 0.1 以内。

画像算好后要落盘。常见做法是存成 CSV 或 parquet,也可以建一张 user_profile 表,字段是 user_id、tag、weight、update_date。每天定时任务重算一次全量画像,新行为做实时增量更新。这一步做完,后面所有推荐策略都只消费这张画像表,算法代码和画像逻辑彻底解耦。

4. 三类商品推荐落地:统一召回接口与差异化策略配置

4.1 统一推荐接口:一次召回、一次融合、一个 Top-N 列表

三个品类的画像结构完全一样,区别只在物品侧的标签来源和召回策略,所以接口层不要写三套,写一套。用 FastAPI 开一个接口,路径参数 cate 区分 movie / music / book。

from fastapi import FastAPI app = FastAPI() # 多路召回结果带权重合并,统一截断到 top_k def merge_recall(*recall_lists, top_k=50): merged = {} for lst, weight in recall_lists: for item_id, score in lst: merged[item_id] = merged.get(item_id, 0) + weight * score return sorted(merged.items(), key=lambda x: x[1], reverse=True)[:top_k] @app.get("/rec/{user_id}") def recommend(user_id: int, cate: str = "movie", top_k: int = 20): # 1. 读画像;没有画像就返回冷启动兜底热榜 profile = load_profile(user_id, cate) if not profile: return {"user_id": user_id, "cate": cate, "items": load_hot(cate, top_k)} # 2. 按品类走差异化召回 if cate == "movie": rec = merge_recall( (recall_itemcf(user_id, cate), 0.6), (recall_content(profile, cate), 0.4), top_k=top_k) elif cate == "music": rec = merge_recall( (recall_usercf(user_id, cate), 0.7), (recall_hot(cate), 0.3), top_k=top_k) else: rec = merge_recall( (recall_content(profile, cate), 0.6), (recall_itemcf(user_id, cate), 0.4), top_k=top_k) # 3. 过滤掉已购/已评分的物品,避免重复推荐 rec = filter_consumed(rec, user_id, cate) return {"user_id": user_id, "cate": cate, "items": [i for i, _ in rec]}

merge_recall 把多路召回结果加权合并,权重是策略配置,别写死在代码里。recommend 先查画像,没有画像直接走热榜兜底,这是冷启动第一道防线。电影权重 0.6/0.4 表示 ItemCF 为主、内容为辅;音乐 0.7/0.3 给 UserCF 更高权重,因为音乐推荐更看重群体口味。filter_consumed 必须在最后一步,否则用户第二天打开接口,看到的还是昨天买过的东西。

4.2 电影、音乐、图书的参数差异:item_cf 还是 user_cf,取决于消费习惯

差异不是随便拍的,背后是三类商品的消费习惯。电影趋近一次性消费,很少人反复看同一部电影,用 ItemCF 算“看过 A 的人也看 B”最稳;音乐是重度重复消费,今天听、明天还听同一首歌,UserCF 利用同好群体的播放序列更有效;图书是超长尾商品,评分矩阵极度稀疏,UserCF 容易把用户带到只看过两三本书的陌生人身上,所以图书必须以内容标签召回为主。

工程文件也按这个逻辑拆,代码和策略配置分开:

recsys/ ├── data/ # 原始数据与清洗脚本 ├── profile/ # 用户画像计算与增量更新 ├── rec/ # 召回、融合、过滤 ├── api/ # FastAPI 接口 └── eval/ # 离线评估脚本

这个目录结构不依赖任何特定框架,换一个数据集进来,只需要改 data 层的数据读取和字段映射,rec 和 api 不用动。协同过滤的相似度计算我在这里放一个极简版本方便看懂原理:ItemCF 的相似度是“物品—共现用户数”归一化,UserCF 是“用户—共同物品数”归一化。

def compute_item_sim(interaction): # interaction: 只有 user_id, item_id 两列的行为数据 item_users = interaction.groupby("item_id")["user_id"].apply(set) sim = {} for i, users_i in item_users.items(): for j, users_j in item_users.items(): if i >= j: continue # 共现用户越多,相似度越高;这里是未归一化版本,只展示核心思路 common = len(users_i & users_j) if common > 0: sim.setdefault(i, {})[j] = common sim.setdefault(j, {})[i] = common return sim

这个双重循环的复杂度是物品数的平方,物品超过几千就跑不动,所以我只用它讲原理,实际项目替换成“先按用户聚合出物品对,再统计共现次数”的写法。调参时可以在相似度计算里加时间窗口,只统计最近 90 天的行为,既能降计算量,又能让相似关系反映近期趋势。

5. 推荐系统踩坑与排查:5个血泪案例教你定位推荐翻车问题

5.1 评估指标虚高:把时间切分当成随机切分的后果

现象:训练集随机抽 80%,测试集用剩下 20%,离线 Precision@10 算出来 0.35,看着很漂亮,上线后用户反馈完全不对。

原因:随机切分造成了时间泄漏。用户的购买行为按时间累积,测试集里混着早期行为,模型在训练时已经“见过”这些行为对应的物品,评估结果天然虚高。

解决:一律按时间切分,以行为时间戳为准,最近 30 天当测试集,其余当训练集。评估脚本里加一个断言,断言训练集最大时间戳小于测试集最小时间戳。这是推荐系统评估的铁律,论文里必须写清楚。

5.2 新用户推荐为空:冷启动兜底策略缺失

现象:新注册用户访问推荐接口,返回的 items 是空数组,前端渲染出空白页,用户直接流失。

原因:画像计算只覆盖有行为记录的用户,新用户没有任何行为,build_user_profile 里查不到这个 user_id,推荐接口又没有准备兜底分支。

解决:注册时让用户勾选兴趣标签,把勾选结果写进静态画像;接口读画像为空时直接返回该品类热榜前 20 条。热榜本身要分品类,把电影热榜推给只看图书的用户,是另一种翻车。

5.3 标签“伪相似”:未归一化的文本标签污染相似度

现象:电影“科幻 动作”和“科幻,动作”被当成两个不同标签,明明是同一系列的作品,系统算出相似度为零。

原因:标签来自不同爬虫源或人工录入,分隔符和空格不统一,又没有做文本归一化,直接 split(",") 把空格也带进了标签集合。

解决:写一个统一的标签清洗函数,先把逗号和空格都替换成逗号,再小写化、strip 空白,最后按标签长度和停用词过滤一遍。推荐系统里这种纯文本脏数据造成的玄学问题,排查优先级要高于算法调参。

5.4 音乐推荐永远同一个歌单:缺乏多样性重排

现象:推荐列表前 20 条里有 17 首是同一个歌手的歌,用户听腻了,点踩率上升。

原因:UserCF 按相似用户聚合,相似用户口味高度一致,召回候选天然集中在少数热门歌手,没有做重排。

解决:召回后按歌手或风格做分组限制,每个歌手最多占推荐列表的 20%,或者用 MMR(最大边际相关)思想做一次多样性重排。不需要接复杂模型,按歌手分组轮盘抽样在毕业设计里完全够用。

5.5 演示数据太少,指标曲线抖动:报告均值而不是单次结果

现象:文件里只有几千条行为数据,评估跑出来的 Precision 一会儿 0.31 一会儿 0.12,画的柱状图高低乱跳,论文没法写。

原因:测试集太小,单个用户的活跃度又高,一个人跑偏就拉掉好几个点。

解决:换公开数据集,MovieLens、Last.fm 标签集、Book-Crossing 都是这个方向常用的公开数据;如果只能用本地数据,做 bootstrap 重采样,随机抽 10 次各算一遍指标,报告均值和标准差。论文里写“Precision@10 = 0.214 ± 0.035”比写单个数字有说服力得多。

6. 离线评估与论文指标:用对比实验证明你的推荐系统可用

6.1 三个必算指标:Precision@K、Recall@K、Coverage

答辩时老师最常问的一句话是“你怎么证明你的推荐比热门推荐好”。正确做法是固定一个评估脚本,跑三行输出。

def evaluate(train, test, recommend_fn, K=10): users = set(test["user_id"].unique()) hits = 0 for uid in users: rec = [i for i, _ in recommend_fn(uid)][:K] ground = set(test.loc[test["user_id"] == uid, "item_id"]) hits += len(set(rec) & ground) precision = hits / (len(users) * K) recall = hits / len(users) # 简化版:每个用户保底一个正例 return precision, recall

这个函数假设测试集里每个用户至少有一条正例,所以 recall 简化为命中用户数除以用户数。K 从 5、10、20 分别跑一遍画成折线图,能看出算法在不同截断长度下的表现差异。再加一个覆盖率指标:推荐列表里出现的物品数占全部物品数的比例,覆盖率低说明系统只推热门,没有真正利用画像。

6.2 演示视频与论文图表的录制技巧

演示视频不用追求花哨,一镜到底反而可信。我习惯的录制顺序是先打开冷启动页面,展示未登录状态下的热榜;再登录一个老用户账号,展示个性化推荐列表,强调“上次买的某某影响了这次的某某”;接着切一次品类,从电影切到图书,说明画像分层起了作用;最后跑一遍离线评估脚本,让 Precision 和 Recall 数字出现在屏幕上。

论文里的对比表放三列:热门推荐、单一协同过滤、混合推荐(即本项目),指标用 Precision@10 和 Coverage 两行。所有图表用评估脚本输出的真实数据画,别手填数字,答辩时经不起追问。

带过的项目里,凡是把评估脚本留到最后才写的,几乎都在答辩前熬夜补图;现在我拿到这类推荐系统源码,第一件事就是先跑评估脚本确认数字能复现,再动手改算法。这个习惯省下的时间远超过写脚本本身。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询