基于Python的商品推荐系统毕业设计:从数据清洗到召回排序的完整实现
简介这份资源是面向计算机科学专业毕业生的商品推荐系统完整项目源码基于Python实现适合作为毕业设计选题或推荐系统入门实践。项目覆盖数据预处理、特征工程、模型训练与评估、系统部署全流程帮助学习者理解协同过滤等核心算法的工程落地方式。压缩包共216个文件约91.74MB以83个png与63个jpg图片、26个py脚本、14个html页面及9个js、6个css等前端资源为主另含sqlite3数据库与少量xml、txt配置说明结构上区分数据处理、模型实现、训练评估与部署脚本便于按模块阅读。目前已有296人学习下载。通过研读源码读者可掌握Python文件操作与数据处理、缺失值与异常值清洗、用户购买历史与商品属性等特征提取、用户-用户及物品-物品相似度计算以及借助scikit-learn、TensorFlow或PyTorch训练模型并运用精确度、召回率、F1分数等指标评估优化最终将模型集成为可交互的推荐服务为求职与后续开发打下基础。1. 拆开「基于Python的商品推荐系统」这个毕业设计它到底在做什么很多同学拿到「基于Python的商品推荐系统」这个题目时第一反应是去搜「免费python源码大全」下载一个压缩包改个名字就交差。但真正答辩时被问一句「你的召回和排序怎么分的」就当场卡壳。这个题目的本质是让你用 Python 把「用户-商品-行为」三张表串起来跑通一条从数据清洗、特征构造、召回、排序到前端展示的完整链路。它解决的不是「预测准不准」这一个问题而是「一个冷启动的电商场景怎么在有限算力下把用户可能喜欢的商品排到前面」。适合谁适合计算机、软件工程、大数据方向的本科毕业生也适合想用一个小项目把推荐算法从公式落到代码的入门者。你不需要 GPU 集群一台 16G 内存的笔记本就能跑完整个流程关键在于把每一步的输入输出和参数边界讲清楚。2. 数据从哪来、怎么洗商品推荐系统的第一道分水岭2.1 公开数据集选型与字段对齐做推荐系统数据决定了天花板。毕业设计阶段最稳妥的选择是 MovieLens 或 Amazon Reviews 这类公开数据集它们自带 user_id、item_id、rating、timestamp 四个核心字段正好对应协同过滤的最小输入。如果你坚持用爬虫抓电商数据注意反爬和字段缺失问题很多同学在这一步就翻车抓下来的 CSV 里一半是空值。常见做法是先用公开数据集跑通算法再替换成自己爬的数据做展示。字段对齐时把 rating 统一映射到 1-5 分timestamp 转成标准 datetime缺失的 user_id 或 item_id 直接整行丢弃不要用均值填充否则会引入虚假关联。import pandas as pd # 读取原始评分数据假设是 MovieLens 格式 df pd.read_csv(ratings.csv) # 只保留推荐系统必需的四列其余字段丢弃 df df[[userId, movieId, rating, timestamp]] # 时间戳转标准时间方便后续做时间衰减 df[timestamp] pd.to_datetime(df[timestamp], units) # 丢弃任何关键字段为空的行不做填充 df df.dropna(subset[userId, movieId, rating]) # 过滤掉评分次数少于 5 次的用户和商品降低稀疏噪声 user_counts df[userId].value_counts() item_counts df[movieId].value_counts() df df[df[userId].isin(user_counts[user_counts 5].index)] df df[df[movieId].isin(item_counts[item_counts 5].index)] print(df.shape)这段代码的逻辑是「先对齐字段再清洗噪声」。dropna保证没有空值进入模型value_counts加阈值过滤是为了去掉那些只评过一次的极端用户和冷门商品它们会让相似度矩阵变得极不稳定。参数上5 次是经验值数据量大可以调到 10数据量小就降到 3。注意 timestamp 转 datetime 后后面做时间衰减加权时会用到。2.2 用户-商品交互矩阵的构建与稀疏处理推荐系统的核心数据结构是交互矩阵行是用户列是商品值是评分或行为权重。这个矩阵通常极度稀疏稀疏度 99% 以上是常态。直接拿稀疏矩阵去算相似度内存会爆。常见做法是用 scipy 的 csr_matrix 存储只存非零元素。构建时用 pivot_table 把长表转宽表但不要直接转成稠密 DataFrame而是先拿到索引映射再构造稀疏矩阵。import numpy as np from scipy.sparse import csr_matrix # 建立用户和商品的索引映射把原始 id 转成 0 开始的连续整数 user_ids df[userId].unique() item_ids df[movieId].unique() user_to_index {uid: i for i, uid in enumerate(user_ids)} item_to_index {iid: i for i, iid in enumerate(item_ids)} # 构造稀疏矩阵的行列坐标和值 rows df[userId].map(user_to_index).values cols df[movieId].map(item_to_index).values values df[rating].values.astype(np.float32) # 形状是 用户数 x 商品数只存非零评分 interaction_matrix csr_matrix((values, (rows, cols)), shape(len(user_ids), len(item_ids))) # 查看稀疏度 sparsity 1 - interaction_matrix.nnz / (interaction_matrix.shape[0] * interaction_matrix.shape[1]) print(f稀疏度: {sparsity:.4f})这里的关键参数是shape必须和用户数、商品数严格一致否则后面做矩阵乘法会维度报错。dtype用 float32 而不是 float64内存直接减半对毕业设计的数据规模完全够用。稀疏度打印出来通常在 0.95 到 0.999 之间如果低于 0.9说明你的数据太稠密可能是抓取时把曝光未点击也当成正样本了需要重新定义行为权重。3. 召回层怎么选协同过滤、矩阵分解还是向量检索3.1 基于物品的协同过滤ItemCF 的 Python 实现与相似度计算召回层负责从海量商品里快速筛出几百个候选ItemCF 是最适合毕业设计入门的方案因为它可解释性强答辩时能画图讲清楚「买了 A 的人也买 B」。核心是算物品之间的相似度用余弦相似度或皮尔逊相关系数。注意算相似度前要把交互矩阵按列归一化否则热门商品会主导相似度结果。from sklearn.metrics.pairwise import cosine_similarity # 转置成 商品 x 用户因为 ItemCF 算的是物品间相似度 item_user_matrix interaction_matrix.T # 计算物品间余弦相似度得到 商品 x 商品 的稠密矩阵 item_similarity cosine_similarity(item_user_matrix) # 把对角线置零避免商品和自己相似度为 1 影响推荐 np.fill_diagonal(item_similarity, 0) # 对每个物品只保留 top 50 个最相似物品其余置零降低存储和计算 top_k 50 for i in range(item_similarity.shape[0]): row item_similarity[i] threshold np.sort(row)[-top_k] if np.count_nonzero(row) top_k else 0 row[row threshold] 0 print(item_similarity.shape)cosine_similarity返回的是稠密矩阵商品数超过 1 万时内存会吃紧所以后面用 top_k 截断。top_k 设 50 是平衡召回率和计算量的经验值设太大召回结果冗余设太小长尾商品召不回来。np.fill_diagonal那一步很多同学会漏掉导致推荐列表里出现用户已经买过的商品答辩时被问「为什么推荐已购」就很尴尬。3.2 矩阵分解SVD做召回把稀疏矩阵压成隐向量ItemCF 在用户数大时相似度矩阵会膨胀矩阵分解用隐向量的方式把用户和商品都映射到低维空间内存更可控。用 scipy 的 svds 做截断 SVD把交互矩阵分解成用户隐向量和商品隐向量预测评分就是两个向量的点积。from scipy.sparse.linalg import svds # 对交互矩阵做截断 SVDk 是隐向量维度 k 32 U, sigma, Vt svds(interaction_matrix, kk) # sigma 是奇异值数组转成对角矩阵 sigma np.diag(sigma) # 预测评分矩阵 U * sigma * Vt predicted_ratings np.dot(np.dot(U, sigma), Vt) print(predicted_ratings.shape)k32 是毕业设计常用的隐向量维度设 16 到 64 之间都可以太小欠拟合太大过拟合且计算变慢。svds要求 k 小于矩阵最小维度否则报错。预测出的评分矩阵是稠密的里面会有负值实际排序时只取用户未交互过的商品按分数降序排。注意 SVD 对缺失值默认按 0 处理这会让预测偏向于给未交互商品打低分所以通常只用来做召回候选不直接当最终排序。3.3 用 Faiss 做向量召回把隐向量灌进索引如果想把召回做得更「工程化」可以把 SVD 得到的商品隐向量灌进 Faiss 建索引用户隐向量作为查询向量做近似最近邻搜索。这一步在毕业设计里是加分项能体现你对向量检索的理解。import faiss # 商品隐向量取 Vt 的转置形状 商品数 x k item_vectors Vt.T.astype(np.float32) # 建一个内积索引因为推荐里内积越大越相似 index faiss.IndexFlatIP(k) index.add(item_vectors) # 用户隐向量作为查询取第一个用户 user_vector U[0].reshape(1, -1).astype(np.float32) scores, indices index.search(user_vector, 10) print(indices)IndexFlatIP是精确内积检索数据量小于 10 万时速度足够。search的第二个参数是返回的候选数这里取 10 方便调试实际召回可以取 200。注意 Faiss 要求向量是 float32 且连续内存astype和reshape不能省。如果报维度错误检查 k 是否和索引维度一致。4. 排序层与评估从候选集到最终列表的最后一公里4.1 用 LightGBM 做排序特征构造与训练召回给出几百个候选后排序层负责精排。毕业设计里最实用的是用 LightGBM 做 Learning to Rank特征包括用户历史评分均值、商品历史评分均值、用户和商品的交互次数、召回分数等。标签用点击或评分是否高于阈值来构造。import lightgbm as lgb from sklearn.model_selection import train_test_split # 构造排序特征这里用召回分数和统计特征拼接 # 假设 recall_scores 是召回阶段得到的分数矩阵 features [] labels [] for u in range(interaction_matrix.shape[0]): for i in range(interaction_matrix.shape[1]): # 只对召回候选构造样本 if recall_scores[u, i] 0: user_avg interaction_matrix[u].data.mean() if interaction_matrix[u].nnz 0 else 0 item_avg interaction_matrix[:, i].data.mean() if interaction_matrix[:, i].nnz 0 else 0 features.append([recall_scores[u, i], user_avg, item_avg]) labels.append(1 if interaction_matrix[u, i] 3 else 0) X_train, X_test, y_train, y_test train_test_split(features, labels, test_size0.2) # LightGBM 二分类用于判断用户是否会喜欢 train_data lgb.Dataset(X_train, labely_train) params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, verbose: -1 } model lgb.train(params, train_data, num_boost_round100)learning_rate设 0.05 是为了防止过拟合num_leaves31 是默认值数据量小可以降到 15。标签构造用评分大于 3 作为正样本这是常见做法你也可以用点击行为。注意特征里不要放 user_id 和 item_id 本身树模型会记住 id 导致过拟合答辩时被问「为什么用 id 做特征」就不好解释。4.2 离线评估指标RecallK、NDCG 和覆盖率怎么算评估不能只看准确率推荐系统要看 Top-K 的召回率和排序质量。RecallK 衡量前 K 个推荐里命中了多少用户真实喜欢的商品NDCG 考虑位置权重覆盖率看推荐结果是否只集中在热门商品。def recall_at_k(predicted, actual, k): # predicted 是推荐列表actual 是用户真实交互集合 top_k predicted[:k] hits len(set(top_k) set(actual)) return hits / len(actual) if len(actual) 0 else 0 def ndcg_at_k(predicted, actual, k): top_k predicted[:k] dcg 0 for i, item in enumerate(top_k): if item in actual: dcg 1 / np.log2(i 2) idcg sum(1 / np.log2(i 2) for i in range(min(len(actual), k))) return dcg / idcg if idcg 0 else 0recall_at_k的分母是用户真实交互数不是 K这点很多同学写错。ndcg_at_k里i2是因为 log2(1) 为 0从位置 1 开始算。覆盖率用推荐结果中不同商品数除以总商品数如果覆盖率低于 10%说明推荐太集中在爆款需要加多样性惩罚。5. 避坑与排查毕业设计答辩前必须过的五道坎5.1 现象训练时 AUC 很高测试时惨不忍睹原因特征里混入了未来信息比如用全局评分均值做特征或者时间戳没做切分训练集和测试集有时间重叠。解决按时间切分数据集用过去的数据预测未来的行为所有统计特征只能从训练集计算。5.2 现象推荐列表里全是同一个商品原因相似度矩阵没有做 top_k 截断或者热门商品权重过大。解决对相似度做归一化加流行度惩罚在排序阶段引入多样性重排。5.3 现象Faiss 检索报维度不匹配原因SVD 的 k 和 Faiss 索引维度不一致或者向量没有转成 float32。解决打印item_vectors.shape和index.d确认统一用astype(np.float32)。5.4 现象LightGBM 训练报标签不是 0/1原因标签构造时用了连续评分没有二值化。解决用label (rating 3).astype(int)显式转换。5.5 现象前端展示时接口超时原因每次请求都重新算相似度矩阵。解决离线算好相似度矩阵和推荐结果存 Redis 或本地文件前端只读缓存。6. 把系统跑起来之后一个能写进论文的验证技巧答辩前最容易被问的是「你怎么证明推荐有效」。除了离线指标我一般会做一个 A/B 对比的小实验把用户随机分成两组一组用 ItemCF 召回一组用 SVD 召回看哪组的 Recall10 更高。这个实验不需要真实线上流量用留出法在测试集上模拟就行。具体做法是对每个用户用训练集算召回用测试集算真实交互分别算两组的 Recall10 和 NDCG10做配对 t 检验看差异是否显著。from scipy import stats # 假设 group_a_recall 和 group_b_recall 是两个用户组的 Recall10 列表 t_stat, p_value stats.ttest_rel(group_a_recall, group_b_recall) print(ft统计量: {t_stat:.4f}, p值: {p_value:.4f})如果 p 值小于 0.05说明两种召回策略有显著差异你可以在论文里写「SVD 召回在 Recall10 上显著优于 ItemCF」。这个技巧的好处是你不需要部署两套系统只需要在离线评估脚本里多跑一遍。参数上ttest_rel是配对检验要求两组样本一一对应所以分组时保证每个用户只出现在一组里。我自己的习惯是每次改完召回或排序参数先把离线指标跑一遍记在一个 CSV 里答辩时直接拿表格说话。不要等到最后一天才跑评估那时候改一个参数就要重跑半小时心态容易崩。希望帮到你。本文还有配套的精品资源点击获取