
简介面向计算机、数据科学等专业正在准备毕业设计或课程设计的学生提供一套基于Python的协同过滤商品推荐系统完整项目。资源涵盖推荐算法核心源码、用户行为数据、数据库脚本、毕业论文文档以及演示视频能够帮助学习者快速理解并复现商品推荐场景下的协同过滤实现流程也支持在此基础上进行二次开发。压缩包共712个文件以py后端逻辑、vue前端页面、js交互脚本、css样式、sql建表脚本、mp4演示视频等类型为主包体约30.94MB文件结构清晰便于按模块检索。目前已有147人学习/下载。内置安装与运行批处理脚本配合数据库文件和论文说明可缩短环境搭建时间演示视频直观展示推荐效果适合作为毕设项目、课程设计或实战练习的参考范本。1. 协同过滤推荐系统的源码包里最值钱的是哪一层一个基于 python 的协同过滤商品推荐系统真正值得拆开看的不是登录页和商品列表而是从评分数据到 Top-N 推荐列表这一整条链路。这份源码包同时给出数据库脚本、Python 推荐引擎、Vue 前端骨架和说明文档恰好覆盖毕业设计里最容易被追问的三个问题相似度怎么算、推荐结果怎么生成、用户行为数据怎么和商品表打通。适合两类人一类是拿它当毕业设计或课程设计需要快速跑通且能讲清楚原理另一类是工作后想补推荐系统基础拿一套真实项目做二次开发的工程师。运行门槛不算高但前提是能把 python 环境、数据库连接和前端构建脚本理顺。下面按拆解顺序讲每个环节都留了可以直接搬到项目里的代码和参数。2. 协同过滤选型基于用户的 UserCF 和基于物品的 ItemCF 怎么取舍协同过滤是整个推荐系统的核心它不关心商品本身的文本或图片只看用户的历史行为。拿到一份 python 推荐系统源码先识别它用的是 UserCF 还是 ItemCF比直接改代码更重要因为这两个算法决定了相似度矩阵怎么建、推荐结果在什么场景下才可信。2.1 用户-物品评分矩阵是协同过滤的入口协同过滤在数学上先把用户行为变成矩阵行是用户列是商品格子是评分或点击次数。这个矩阵通常非常稀疏因为一个用户只对极少数商品产生过行为。物品数量上千时评分矩阵的稀疏度常常超过 90%这也是推荐系统区别于普通 CRUD 应用的地方。大多数毕设项目会直接用user_id item_id score三张基础表去构造这个矩阵。评分的含义可以很宽可以是用户主动打的 1~5 分也可以是把加购算 2 分、购买算 5 分、点击算 0.5 分得到的综合得分。关键点是评分不能只有唯一来源否则后面做协同过滤时无法区分“用户喜欢”和“用户只是看过”。从源码角度看用户-物品矩阵是为了后续计算相似度服务的。矩阵里缺失值不能随便填 0因为“没看过”和“打了 0 分”在语义上完全不同。常见做法是保留 NaN 占位在算相似度时只取两个用户或两个物品都有评分的维度这部分处理直接影响最终推荐质量。2.2 UserCF 与 ItemCF 的适用场景对比对比维度UserCF 基于用户的协同过滤ItemCF 基于物品的协同过滤相似度主体用户与用户之间的行为相似物品与物品之间的共现相似计算规模用户量增长后矩阵膨胀快物品量相对稳定时更可控推荐解释类似“和你相似的人还买了”类似“浏览过该商品的人还看了”实时性用户新行为后需要重算用户相似度物品相似度变化慢可离线更新冷启动新用户无历史行为几乎不可用新物品无共现数据也不可用典型场景新闻、短视频兴趣变化快电商、图书、电影物品属性稳定在商品推荐系统里ItemCF 通常是更稳妥的起点。电商商品数量相对用户数量小而且商品之间具有明显的“搭配、替代、同品牌”关系这类关系稳定且可解释。UserCF 更偏向发现群体热度容易出现“相似用户里有人买了但目标用户实际不感兴趣”的偏差。选择算法时还要看数据量级。如果用户表只有几千行商品只有几百行两个算法都能跑但 ItemCF 的相似度矩阵可以在离线批量更新线上只做查表性能上更友好。以下代码展示了最基础的相似度计算这是两个算法的公共底座。2.3 相似度计算余弦相似度和皮尔逊相关系数import numpy as np def cosine_similarity(vec_a, vec_b): 计算两个评分向量的余弦相似度输入为包含 NaN 的 numpy 数组 # 只保留两个向量都有评分的维度 common ~np.isnan(vec_a) ~np.isnan(vec_b) if np.sum(common) 0: return 0.0 # 缺失位置用 0 填充保证向量长度一致 a np.nan_to_num(vec_a) b np.nan_to_num(vec_b) dot np.dot(a, b) norm_a np.linalg.norm(a) norm_b np.linalg.norm(b) if norm_a 0 or norm_b 0: return 0.0 return float(dot / (norm_a * norm_b))参数说明vec_a和vec_b是一维 numpy 数组下标对应物品或用户 ID 映射后的索引。common用于过滤两方都没有评分的维度避免把大量空白当成相似证据。nan_to_num把 NaN 转成 0只影响范数计算不影响点积的公共部分。皮尔逊相关系数是余弦相似度的改进版它对每个向量先减去均值消除用户打分区间的差异。比如一个用户习惯打 4~5 分另一个习惯打 2~3 分用原始评分算余弦相似度可能很低但减去各自均值后能看出他们偏好模式接近。实际工程里评分矩阵不是稠密的所以皮尔逊公式里要同样处理缺失值。def pearson_similarity(vec_a, vec_b): 皮尔逊相关系数评分先中心化再计算 common ~np.isnan(vec_a) ~np.isnan(vec_b) if np.sum(common) 2: return 0.0 a_mean np.nanmean(vec_a) b_mean np.nanmean(vec_b) # 只计算共同评分位置的偏差 diff_a vec_a[common] - a_mean diff_b vec_b[common] - b_mean denom np.linalg.norm(diff_a) * np.linalg.norm(diff_b) if denom 0: return 0.0 return float(np.dot(diff_a, diff_b) / denom)这里的参数逻辑和余弦版本一致多出来的a_mean和b_mean分别代表用户或物品的平均评分。需要用np.nanmean而不是np.mean否则 NaN 会扩散到整个均值计算里。common维度少于 2 时直接返回 0因为两个数据点算出来的相关系数没有统计意义。2.4 为什么毕设和课程设计大多选 ItemCF毕设场景里最怕的是“讲不清楚”。ItemCF 的推荐结果能直接映射到一句人话“你买过的商品的相似商品”这句话放进论文、答辩 PPT、项目展示里都容易理解。UserCF 的推荐是“相似用户喜欢的商品”需要额外解释用户聚类开发量差不多但演示效果不如 ItemCF 直观。从源码结构看ItemCF 的相似度矩阵可以提前算好存在数据库或 pickle 文件里用户请求推荐时不需要实时跑全量计算。这也是商品推荐系统首选 ItemCF 的工程原因。在线推荐只需要两步找到用户评分过的商品再把这些商品的 Top-K 相似商品汇总排序计算量可控。如果项目里数据量很小只有几十个用户和几十个商品UserCF 和 ItemCF 的差异并不明显。更关键的是把评分数据组织好让相似度计算能落到具体的表字段上。这就是下一章要处理的数据库设计。3. 推荐系统数据库设计与 Python 数据层实现协同过滤算法本身不复杂复杂的是数据层。推荐系统项目的数据库设计决定了算法能否高效拿到训练数据、离线任务能否一键跑完、线上接口能不能在几百毫秒内返回结果。很多源码跑不起来问题不是 python 代码报错而是表结构不一致或者连接参数写死。3.1 核心表用户、商品、评分、行为日志一个商品推荐系统最少需要四张核心表同时承担业务展示和算法数据来源两个职责。表名关键字段作用userid, username, password, created_at用户登录与基础信息itemid, title, price, category, image_url商品展示与属性ratingid, user_id, item_id, score, timestamp显式评分协同过滤主数据behavior_logid, user_id, item_id, action, timestamp点击、收藏、加购、购买行为这里 rating 和 behavior_log 容易混淆。rating 是用户主动给出的分数数据稀疏但信号强behavior_log 是从系统日志里采集的隐式反馈数据量大但噪声多。在源码的演示阶段通常只用 rating 表生成推荐behavior_log 表只在论文中体现“未来可融合行为数据”。数据库建表时还要考虑索引。协同过滤最常见的查询是“查某个用户的所有评分”和“查某个物品的所有评分”这两类查询分别对应(user_id)和(item_id)索引。很多初版代码只建了主键导致数据到十万行时相似度矩阵计算变得很慢。3.2 MySQL 建表脚本及关键索引设计CREATE DATABASE IF NOT EXISTS recommend_system DEFAULT CHARSET utf8mb4; USE recommend_system; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB; CREATE TABLE item ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, category VARCHAR(64) DEFAULT NULL, image_url VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB; CREATE TABLE rating ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, item_id INT NOT NULL, score TINYINT NOT NULL DEFAULT 5 COMMENT 评分1-5, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_item_id (item_id), UNIQUE KEY uk_user_item (user_id, item_id) ) ENGINEInnoDB;这段 SQL 看起来和普通业务表差别不大但rating表里的UNIQUE KEY uk_user_item是推荐系统最关键的约束。它保证同一个用户对同一个商品只有一条评分记录后续做矩阵转换时不会因为重复评分导致相似度算错。TINYINT类型足够存 1~5 分比INT省空间索引查询也更快。user表单独设置UNIQUE KEY是为了前端登录组件能直接用用户名做唯一判断。item表的 category 索引主要给后台管理页面用算法层一般不会直接用分类字段。3.3 使用 PyMySQL 封装数据访问层源码里的 python 后端和数据库之间最常见的通信方式是 PyMySQL连接参数一般集中在配置文件中。下面这段是数据访问层的最小封装既能让推荐引擎读到训练数据也能让 Web 接口正常返回商品信息。import pymysql class Database: def __init__(self, host127.0.0.1, port3306, userroot, password123456, databaserecommend_system): self.conn pymysql.connect( hosthost, portport, useruser, passwordpassword, databasedatabase, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def fetch_all_ratings(self): 读取全部评分数据用于离线计算相似度矩阵 sql SELECT user_id, item_id, score FROM rating with self.conn.cursor() as cursor: cursor.execute(sql) rows cursor.fetchall() return rows def close(self): self.conn.close()cursorclassDictCursor让查询结果变成字典列表字段名可以直接用row[user_id]访问避免和 SQL 列名混淆。参数说明host和port默认指向本地 MySQLpassword需要和本机安装的数据库密码一致否则fetch_all_ratings()会抛出Access denied错误。实际项目里不建议把数据库连接放在推荐计算循环里反复开关而应该在服务启动时初始化一个连接池。这里只演示最直接的读写方式方便在 jupyter notebook 或命令行里快速验证。从源码运行角度看先执行建表 SQL再插入少量测试评分然后调用这个封装类读数据协同过滤算法就能跑起来了。3.4 冷启动阶段如何准备初始测试数据没有数据的推荐系统无法验证算法正确性。项目第一次启动时数据库里通常只有几张空表。常见做法是写一个生成模拟评分的脚本随机给用户和商品建立评分关系数据量控制在几百条以内先用小数据验证算法能出结果再导入完整的公开数据集。import random import pymysql def seed_ratings(user_count20, item_count50, rating_count300): 生成模拟评分数据避免冷启动 connection pymysql.connect( host127.0.0.1, userroot, password123456, databaserecommend_system, charsetutf8mb4 ) sql INSERT INTO rating (user_id, item_id, score) VALUES (%s, %s, %s) data [] for _ in range(rating_count): user_id random.randint(1, user_count) item_id random.randint(1, item_count) score random.choice([3, 4, 5, 4, 5, 2]) data.append((user_id, item_id, score)) with connection.cursor() as cursor: cursor.executemany(sql, data) connection.commit() connection.close() print(finserted {len(data)} ratings) seed_ratings()这里random.choice([3,4,5,4,5,2])让高分出现概率更高贴近真实电商场景。executemany批量插入 300 条记录只需要一次提交比逐条execute快得多。注意UNIQUE KEY uk_user_item存在时如果随机组合重复插入会报错正式脚本里可以加ON DUPLICATE KEY UPDATE或者插入前先查重。4. 推荐引擎核心相似度矩阵、Top-N 列表与 Web API数据层准备好之后推荐引擎进入实战阶段。Chapter 2 的原理要落到三个具体产出物上物品相似度矩阵、用户推荐列表、HTTP 接口。这一层是论文工作量最集中的地方也是答辩时最容易出彩的部分。4.1 构建物品相似度矩阵基于物品的协同过滤第一步是计算所有物品之间的相似度。通常不实时计算而是离线跑脚本把结果保存成item_similarity.json或直接放回 MySQL。下面是核心计算逻辑。import json from collections import defaultdict def build_item_similarity(ratings, top_k10): 输入 ratings: 从数据库读到的评分记录列表 返回 item_similarity: {item_id: [(similar_item_id, score), ...]} # 1. 构建用户-物品的评分映射 user_items defaultdict(dict) for r in ratings: user_items[r[user_id]][r[item_id]] r[score] # 2. 统计物品被哪些用户评分过 item_users defaultdict(set) for r in ratings: item_users[r[item_id]].add(r[user_id]) # 3. 计算两两物品的共现评分 sim_count defaultdict(float) sim_sum defaultdict(float) for item_i, users_i in item_users.items(): for item_j, users_j in item_users.items(): if item_i item_j: continue common_users users_i users_j if len(common_users) 2: continue # 点积累加表示共同评分强度 dot sum( user_items[u][item_i] * user_items[u][item_j] for u in common_users ) # 简化处理用共同用户数量作为相似度 sim_sum[(item_i, item_j)] dot sim_count[(item_i, item_j)] len(common_users) # 4. 生成每个物品的 top_k 相似列表 item_similarity defaultdict(list) for (item_i, item_j), score in sim_sum.items(): # 归一化除以共同评分数量的开方模拟余弦效果 normalized score / (sim_count[(item_i, item_j)] ** 0.5) item_similarity[item_i].append((item_j, normalized)) item_similarity[item_j].append((item_i, normalized)) # 5. 截取 top_k result {} for item_id, sim_list in item_similarity.items(): sim_list.sort(keylambda x: x[1], reverseTrue) result[item_id] sim_list[:top_k] return result if __name__ __main__: # ratings 来自 Database.fetch_all_ratings() sim build_item_similarity(ratings, top_k10) with open(item_similarity.json, w, encodingutf-8) as f: json.dump(sim, f, ensure_asciiFalse)这段代码把协同过滤训练过程完整走了一遍。common_users是两个物品都评分过的用户集合这是共现矩阵的基础。normalized的除法逻辑参考了余弦相似度的分母思想但没有完整计算向量范数属于工程上的简化适合小数据集和毕设演示。第3步的嵌套循环时间复杂度是 O(N^2)物品数量超过一万时跑起来很慢。所以真实项目里会先用top_k限制每个物品只保留和它共现次数最高的前几十个物品或者在 SQL 里先过滤掉被评分数过少的冷门物品。4.2 给用户生成 Top-N 推荐列表物品相似度矩阵生成后用户的推荐列表计算就变成查表累加。对用户评分过的每个商品找出它的相似商品再用用户评分作为权重加权排序。def recommend_for_user(user_id, user_ratings, item_sim, top_n20): user_ratings: {item_id: score}当前用户的评分记录 item_sim: 4.1 节生成的相似度字典 # 候选商品及累加得分 scores defaultdict(float) # 记录候选商品的相似度来源数量可用于惩罚热门商品 for rated_item, rating_score in user_ratings.items(): if rated_item not in item_sim: continue for similar_item, similarity in item_sim[rated_item]: if similar_item in user_ratings: continue # 排除已买过/已评分的商品 scores[similar_item] rating_score * similarity # 按得分降序返回 top_n ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [item_id for item_id, _ in ranked[:top_n]]参数说明rating_score是用户对已购买商品的评分评分越高它带动的相似商品得分越高。similarity是物品之间的相似度两者相乘相当于把用户偏好投射到相似商品上。if similar_item in user_ratings是必须有的过滤逻辑否则推荐结果会包含用户已经购买过的商品演示效果会大打折扣。这个函数可以封装成 Web 接口。线上调用时item_sim直接从 JSON 文件加载不需要每次请求都重算相似度矩阵接口响应时间主要取决于当前用户的评分数量而不是全局物品数量。4.3 暴露 /api/recommend 接口推荐引擎要接入前端需要一个轻量 HTTP 服务。Flask 是最适合这种情况的选择路由简单参数处理直观。from flask import Flask, jsonify, request from model.recommend import recommend_for_user app Flask(__name__) SIM_DICT load_similarity_from_json(item_similarity.json) app.route(/api/recommend, methods[GET]) def recommend(): user_id int(request.args.get(user_id, 0)) top_n int(request.args.get(top_n, 20)) user_ratings load_user_ratings_from_db(user_id) if not user_ratings: return jsonify({code: 200, data: [], msg: user has no rating}) result recommend_for_user(user_id, user_ratings, SIM_DICT, top_n) # 补充商品详情方便前端直接渲染 items get_item_detail(result) return jsonify({code: 200, data: items})接口设计里的top_n参数控制推荐数量前端“加载更多”功能可以复用它。load_user_ratings_from_db每次请求都查一次数据库压力不大时可接受。如果并发高可以把用户最近评分缓存到 Redis但毕设项目不需要这一步。这个接口的返回结构是{code, data, msg}其中code200表示成功msg用于携带错误提示。前端拿到data后直接渲染商品卡片。接口返回的商品详情需要再查一次item表否则前端只能拿到 ID展示不出商品名和价格。4.4 引入时间衰减和隐式反馈基础协同过滤把历史评分当成同等重要但电商场景里用户三个月前的行为可能已经失效。常见的改进方式是给评分加时间衰减权重。from datetime import datetime import math def time_decay_score(score, timestamp, half_life_days30): 评分随时间的衰减半衰期默认 30 天 days (datetime.now() - timestamp).days decay math.pow(0.5, days / half_life_days) return score * decay这里half_life_days30表示 30 天前的评分权重降到一半。decay用指数衰减函数控制不会出现评分直接归零的断层。在 4.1 节构建用户评分映射时就可以将原始评分替代为time_decay_score的返回值这样相似度矩阵和推荐列表都会优先反映近期偏好。隐式反馈的处理方式不同。如果数据源是点击流日志没有明确评分可以按行为类型加权点击 1 分、收藏 3 分、加购 4 分、购买 5 分然后组装成和 rating 表结构一样的数据。这样推荐算法本身不用改只是把评分来源从一张表换成多张表。5. 从运行验证到二次开发怎么把推荐结果调到能演示源码能跑通只是第一步要让推荐结果看起来“聪明”还需要一套验证和调整方法。这里的思路也适用于课程设计答辩和面试项目复盘。5.1 用离线评分数据验证推荐命中率启动推荐服务之前先写一个简单的离线验证脚本把测试集中的每个用户取出一半评分作为历史预测另一半评分统计推荐列表里包含多少真实商品。这是最基础的命中率验证。python evaluate.py --history_ratio 0.5 --top_n 10history_ratio控制训练和测试的切分比例top_n控制推荐列表长度。评估脚本输出precision10和recall10两个指标。如果一个用户历史有 8 条评分推荐列表里的商品有一半是测试集真实出现过的precision10 就是 0.5。一般电商场景的基线在 0.05~0.2 之间如果低于这个值优先检查评分数据是不是太稀疏或者相似度矩阵里热门商品占比过高。5.2 Vue 前端如何对接推荐接口源码文件里有.vue.bak后缀的组件备份说明前端基于 Vue 组件化的方式组织页面。前端对接推荐接口的关键是理解接口返回结构下面是最常见的调用写法。export function fetchRecommend(userId, topN) { return axios.get(/api/recommend, { params: { user_id: userId, top_n: topN } }).then(res { if (res.data.code 200) { return res.data.data; } throw new Error(res.data.msg); }); }这里的 Axios 实例通常会在main.js里配置baseURL指向后端的 Flask 服务地址。组件里收到数据后渲染商品卡片列表。如果推荐列表为空前端要有一个兜底展示比如显示“暂无推荐看看热门商品”不能让页面白屏。很多毕设演示翻车就翻在这里。5.3 二次开发方向混合推荐和多样化排序推荐的常见问题是结果太集中同一个分类的商品占满整个列表。应对方法是在推荐排序里加入一个轻度惩罚项让同分类商品不要连续出现或者引入随机因子打破热门商品的垄断。下面是推荐列表重排的示例逻辑。def diversify(ranked_items, limit10): 简单多样化的重排同分类最多出现 limit 次 seen defaultdict(int) result [] for item_id in ranked_items: cat item_category_map[item_id] if seen[cat] limit: result.append(item_id) seen[cat] 1 if len(result) top_n: break return result二次开发最常见的扩展点有两个一是把user_item_score替换为“点击 收藏 加购 购买”的加权融合分二是引入商品内容特征做混合推荐用协同过滤的结果做候选池再用分类、品牌、价格区间做精排。这两个方向都只需要修改数据输入和重排函数核心的recommend_for_user函数不需要推翻重写。这套源码的价值在于它不是停留在论文里的伪代码而是把协同过滤、数据库、Web 接口、前端展示串成了一条完整链路。拿到手之后先在小数据集上跑通再看相似度矩阵里的异常值基本就能定位出数据或参数的问题。本文还有配套的精品资源点击获取