手写协同过滤电影推荐系统:Python原生实现与毕设落地指南
简介本资源是一套高分98分Python毕业设计项目面向计算机及相关专业本科生、毕设学生及推荐系统初学者提供基于协同过滤算法的电影推荐系统完整实现方案。项目涵盖从数据预处理、相似度计算、用户/物品协同过滤到前端展示的全流程配套论文与详细说明文档可直接用于毕业设计、课程设计或期末大作业。压缩包共2000个文件大小28.04MB主体为1159个Python源码文件含核心推荐逻辑与Web接口、148张界面截图与图表jpg、124个HTML前端页面、63对国际化语言文件po/mo、7个CSV数据集样本及PDF论文等结构清晰、模块分明便于理解算法原理与工程落地细节。目前已有147人学习下载资源经严格调试确保环境兼容、运行稳定并附带Bootstrap等主流前端样式支持显著降低部署门槛与二次开发成本。1. 为什么用协同过滤做电影推荐不是直接调个 API 就完事很多同学拿到“基于协同过滤的电影推荐系统Python实现”这个毕业设计题目时第一反应是网上不是有现成的surprise库、lightfm或者implicit吗pip install 一行搞定加几行代码跑出 RMSE 就能交差。但现实是——答辩老师点开你的 Jupyter Notebook问一句“你这个 user-item 矩阵稀疏度 98.7%冷启动用户怎么处理相似度用的是余弦还是皮尔逊为什么不用 Jaccard邻居数 k20 是怎么验证出来的”——当场卡壳。协同过滤不是黑盒它是一套可解释、可调试、可落地的推荐逻辑链从原始评分数据出发经矩阵构建、相似度计算、邻居筛选、加权预测最终生成带置信度的推荐列表。本项目不依赖任何云服务或第三方推荐平台全部用原生 Pythonpandas numpy scikit-learn 核心模块手写关键流程覆盖内存型 User-Based 和 Item-Based 两种实现支持离线训练、实时预测、Top-N 推荐结果导出并附完整论文写作框架含算法推导、评估指标公式、消融实验设计。适合需要真正理解推荐底层逻辑、能讲清每一步数学含义、且需通过中期检查与答辩质询的本科毕设场景。2. 构建可复现的协同过滤基线从原始数据到稠密用户-物品矩阵协同过滤效果好坏第一步不取决于算法多炫而在于输入矩阵是否真实反映用户偏好。MovieLens 数据集ml-latest-small是公认的教学基准但它原始格式是三元组user_id, movie_id, rating直接用于相似度计算会因稀疏性导致邻居失效。我们必须完成三项不可跳过的预处理ID 映射标准化、隐式反馈清洗、矩阵稠密化控制。2.1 下载并解析 MovieLens 数据不依赖网络请求使用urllib.request直接下载压缩包并解压避免因网络波动中断导致数据不一致import urllib.request import zipfile import os import pandas as pd # 下载 ml-latest-small.zip官方稳定链接 url https://files.grouplens.org/datasets/movielens/ml-latest-small.zip zip_path ml-latest-small.zip urllib.request.urlretrieve(url, zip_path) # 解压并读取核心文件 with zipfile.ZipFile(zip_path, r) as z: z.extractall() ratings_df pd.read_csv(ml-latest-small/ratings.csv) movies_df pd.read_csv(ml-latest-small/movies.csv)注意不要用pandas.read_csv(https://...)直接读远程 CSV部分学校内网会拦截外部域名本地解压后读取确保环境隔离、路径可控。2.2 构建双映射字典与规整化 ID 空间原始ratings.csv中 user_id 和 movie_id 是字符串型整数如1但存在大量未评分用户/电影。我们需构建连续整数 ID 空间使矩阵索引对齐# 提取所有出现过的 user_id 和 movie_id去重排序 unique_users sorted(ratings_df[userId].unique()) unique_movies sorted(ratings_df[movieId].unique()) # 创建映射原始ID → 连续整数索引0-based user_to_idx {uid: idx for idx, uid in enumerate(unique_users)} movie_to_idx {mid: idx for idx, mid in enumerate(unique_movies)} # 反向映射后续结果解释用 idx_to_user {v: k for k, v in user_to_idx.items()} idx_to_movie {v: k for k, v in movie_to_idx.items()} # 添加映射列 ratings_df[user_idx] ratings_df[userId].map(user_to_idx) ratings_df[movie_idx] ratings_df[movieId].map(movie_to_idx)2.3 构造用户-物品评分矩阵scipy.sparse.csr_matrix使用稀疏矩阵而非numpy.ndarray既节省内存MovieLens-small 约 10 万条记录稠密矩阵达 610×9724≈6000 万元素又加速后续相似度计算from scipy.sparse import csr_matrix import numpy as np # 过滤掉映射失败的行极少数异常值 valid_ratings ratings_df.dropna(subset[user_idx, movie_idx]) # 构造 CSR 矩阵行user_idx列movie_idx值rating user_item_matrix csr_matrix( (valid_ratings[rating].values, (valid_ratings[user_idx].values, valid_ratings[movie_idx].values)), shape(len(unique_users), len(unique_movies)) ) print(f用户数: {user_item_matrix.shape[0]}, 电影数: {user_item_matrix.shape[1]}) print(f稀疏度: {1 - user_item_matrix.nnz / (user_item_matrix.shape[0] * user_item_matrix.shape[1]):.3%})参数含义典型值ml-latest-smallshape[0]唯一用户总数610shape[1]唯一电影总数9724nnz非零评分数量100836稀疏度未评分比例98.3%提示稀疏度 95% 是协同过滤的常态。后续所有相似度计算必须基于非零行/列子集否则余弦相似度会因大量零值被拉低失真。3. 手写 User-Based 与 Item-Based 协同过滤核心逻辑协同过滤本质是“物以类聚人以群分”。User-Based 找相似用户做加权平均Item-Based 找相似电影做迁移推荐。二者数学形式对称但工程实现差异显著User-Based 需缓存用户相似度矩阵O(U²)Item-Based 缓存物品相似度矩阵O(I²)。因 I9724 ≫ U610本项目优先实现 User-Based再扩展 Item-Based 作为对比模块。3.1 User-Based 协同过滤逐行计算用户相似度与预测评分核心步骤① 对每个活跃用户 u提取其评分向量② 计算 u 与其他所有用户的皮尔逊相关系数③ 取 top-k 最相似用户加权平均其对目标物品 i 的评分。from sklearn.metrics.pairwise import pairwise_distances import numpy as np def compute_user_similarity(matrix, metricpearson): 计算用户相似度矩阵仅上三角节省内存 # 转为 dense 并填充均值皮尔逊要求中心化 dense_mat matrix.toarray() user_means np.nanmean(dense_mat, axis1, keepdimsTrue) dense_centered np.where(dense_mat ! 0, dense_mat - user_means, 0) # 使用 sklearn 计算余弦相似度等价于皮尔逊在中心化后 similarities 1 - pairwise_distances(dense_centered, metriccosine) np.fill_diagonal(similarities, 0) # 自相似置0避免自推荐 return similarities # 计算相似度耗时约 2~3 秒610x610 矩阵 user_sim compute_user_similarity(user_item_matrix)3.1.1 预测单个用户对单个物品的评分def predict_rating_user_based(user_idx, item_idx, matrix, sim_matrix, k20): 预测用户 user_idx 对物品 item_idx 的评分 if matrix[user_idx, item_idx] ! 0: return matrix[user_idx, item_idx] # 已评分直接返回 # 获取该用户所有已评分物品 user_ratings matrix[user_idx].toarray().flatten() rated_items np.where(user_ratings ! 0)[0] # 若用户未评任何分无法预测 if len(rated_items) 0: return np.nan # 获取所有其他用户的相似度排除自身 sim_scores sim_matrix[user_idx].copy() sim_scores[user_idx] -1 # 屏蔽自己 # 取 top-k 相似用户需确保他们评过分 top_k_indices np.argsort(sim_scores)[-k:][::-1] neighbors [] for idx in top_k_indices: if matrix[idx, item_idx] ! 0: # 邻居必须评过该物品 neighbors.append((idx, sim_scores[idx])) if not neighbors: return np.nan # 加权平均Σ(sim * rating) / Σ|sim| weighted_sum sum(sim * matrix[nbr, item_idx] for nbr, sim in neighbors) sim_sum sum(abs(sim) for _, sim in neighbors) return weighted_sum / sim_sum if sim_sum ! 0 else np.nan # 示例预测用户0对电影5的评分 pred predict_rating_user_based(0, 5, user_item_matrix, user_sim, k20) print(f用户0对电影5预测评分: {pred:.2f})参数说明k20不是固定值。实际毕设中需在验证集上遍历 k∈[5,50]绘制 MAE 曲线选择拐点。过小k5导致邻居噪声大过大k50引入不相关用户降低精度。3.2 Item-Based 协同过滤转置矩阵 物品相似度复用Item-Based 的关键优化在于物品相似度矩阵可离线预计算并复用预测时只需查表。由于物品数远大于用户数我们改用更鲁棒的调整余弦相似度Adjusted Cosine消除用户评分偏差def compute_item_similarity_adj_cosine(matrix): 计算物品相似度调整余弦 dense_mat matrix.toarray() item_means np.nanmean(dense_mat, axis0, keepdimsTrue) # 每列均值 dense_centered np.where(dense_mat ! 0, dense_mat - item_means, 0) # 转置后计算物品间余弦即原矩阵列间相似度 item_sim 1 - pairwise_distances(dense_centered.T, metriccosine) np.fill_diagonal(item_sim, 0) return item_sim item_sim compute_item_similarity_adj_cosine(user_item_matrix) # 形状 (9724, 9724)3.2.1 Item-Based 预测用用户历史行为“迁移”到相似物品def predict_rating_item_based(user_idx, item_idx, matrix, item_sim, k20): Item-Based 预测找与 item_idx 相似的物品看用户是否评过分 if matrix[user_idx, item_idx] ! 0: return matrix[user_idx, item_idx] # 获取用户所有已评分物品 user_ratings matrix[user_idx].toarray().flatten() rated_items np.where(user_ratings ! 0)[0] if len(rated_items) 0: return np.nan # 找与 item_idx 最相似的 k 个已评分物品 item_sim_for_target item_sim[item_idx] # 过滤出用户评过分的相似物品 candidate_items [i for i in rated_items if item_sim_for_target[i] 0] if not candidate_items: return np.nan # 按相似度排序取 top-k candidate_sim item_sim_for_target[candidate_items] top_k_indices np.argsort(candidate_sim)[-k:][::-1] neighbors [(candidate_items[i], candidate_sim[i]) for i in top_k_indices] # 加权平均Σ(sim * user_rating_on_neighbor) weighted_sum sum(sim * user_ratings[nbr] for nbr, sim in neighbors) sim_sum sum(abs(sim) for _, sim in neighbors) return weighted_sum / sim_sum if sim_sum ! 0 else np.nan关键区别User-Based 是“找相似的人”Item-Based 是“找相似的电影”。后者对新用户更友好只要评过1部电影就能推荐前者对新电影更友好只要被10人评过就能被推荐。毕设论文中必须对比二者在冷启动场景下的 MAE/RMSE 差异。4. 完整推荐流程封装与 Top-N 推荐生成毕业设计不能只停留在单点预测必须输出可交付的推荐列表。我们封装一个Recommender类支持① 指定用户生成 Top-10 未评分电影② 支持 User/Item 两种策略切换③ 返回带预测分、电影名、年份的结构化结果。4.1 推荐器类定义与初始化class MovieRecommender: def __init__(self, ratings_df, movies_df, user_to_idx, movie_to_idx, user_item_matrix, user_simNone, item_simNone): self.ratings_df ratings_df self.movies_df movies_df self.user_to_idx user_to_idx self.movie_to_idx movie_to_idx self.user_item_matrix user_item_matrix self.user_sim user_sim self.item_sim item_sim def get_top_n_recommendations(self, user_id, n10, strategyuser): 生成指定用户的 Top-N 推荐 if user_id not in self.user_to_idx: raise ValueError(fUser {user_id} not found in training data) user_idx self.user_to_idx[user_id] predictions [] # 遍历所有电影跳过用户已评分的 for movie_id in self.movie_to_idx: movie_idx self.movie_to_idx[movie_id] if self.user_item_matrix[user_idx, movie_idx] ! 0: continue if strategy user: pred predict_rating_user_based( user_idx, movie_idx, self.user_item_matrix, self.user_sim, k20 ) else: # item-based pred predict_rating_item_based( user_idx, movie_idx, self.user_item_matrix, self.item_sim, k20 ) if not np.isnan(pred): predictions.append((movie_id, pred)) # 按预测分降序取 top-n predictions.sort(keylambda x: x[1], reverseTrue) top_n predictions[:n] # 关联电影信息名称、年份 result [] for movie_id, score in top_n: movie_info self.movies_df[self.movies_df[movieId] movie_id] if not movie_info.empty: title movie_info.iloc[0][title] year title[-5:-1] if title.endswith()) and title[-5:-1].isdigit() else Unknown result.append({ movie_id: movie_id, title: title, year: year, predicted_rating: round(score, 2) }) return result # 初始化推荐器传入预计算的相似度矩阵 recommender MovieRecommender( ratings_df, movies_df, user_to_idx, movie_to_idx, user_item_matrix, user_sim, item_sim )4.2 生成可读推荐报告并导出 CSV# 为用户1生成推荐 recs recommender.get_top_n_recommendations(user_id1, n10, strategyuser) df_recs pd.DataFrame(recs) print(df_recs[[title, year, predicted_rating]]) # 导出为 CSV答辩演示必备 df_recs.to_csv(user1_recommendations_user_based.csv, indexFalse, encodingutf-8-sig) # 对比两种策略效果 recs_item recommender.get_top_n_recommendations(user_id1, n10, strategyitem) df_item pd.DataFrame(recs_item) print(\nItem-Based 推荐:) print(df_item[[title, year, predicted_rating]])titleyearpredicted_ratingFather of the Bride Part II (1995)19954.82While You Were Sleeping (1995)19954.76Sudden Death (1995)19954.69.........提示导出 CSV 时用encodingutf-8-sig防止 Excel 打开乱码utf-8-sig会在文件头添加 BOM兼容 Windows 默认编码。5. 毕设必过技巧评估指标计算、参数调优与论文图表生成答辩老师最关注三点① 你是否真的跑通了② 效果到底好不好③ 你有没有思考过为什么好/不好本章提供一套可直接粘贴进论文的量化验证方案。5.1 严格划分训练集/测试集并计算 RMSE/MAE必须用时间感知切分按 timestamp 列而非随机打乱否则泄露未来信息# 按时间切分前80%为训练后20%为测试 ratings_df_sorted ratings_df.sort_values(timestamp) split_point int(0.8 * len(ratings_df_sorted)) train_df ratings_df_sorted.iloc[:split_point] test_df ratings_df_sorted.iloc[split_point:] # 重建训练矩阵测试数据不参与训练 train_matrix build_sparse_matrix(train_df, user_to_idx, movie_to_idx) # 对测试集每条记录做预测 test_predictions [] for _, row in test_df.iterrows(): uid, mid, true_rating row[userId], row[movieId], row[rating] if uid in user_to_idx and mid in movie_to_idx: pred predict_rating_user_based( user_to_idx[uid], movie_to_idx[mid], train_matrix, user_sim, k20 ) if not np.isnan(pred): test_predictions.append((true_rating, pred)) # 计算 RMSE 和 MAE y_true np.array([p[0] for p in test_predictions]) y_pred np.array([p[1] for p in test_predictions]) rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) mae np.mean(np.abs(y_true - y_pred)) print(fTest RMSE: {rmse:.4f}, MAE: {mae:.4f})5.2 绘制 k 值调优曲线论文核心图表import matplotlib.pyplot as plt k_values range(5, 51, 5) rmse_list [] for k in k_values: preds_k [] for _, row in test_df.head(500).iterrows(): # 取子集加速 uid, mid, true row[userId], row[movieId], row[rating] if uid in user_to_idx and mid in movie_to_idx: pred predict_rating_user_based( user_to_idx[uid], movie_to_idx[mid], train_matrix, user_sim, kk ) if not np.isnan(pred): preds_k.append((true, pred)) if preds_k: y_t np.array([p[0] for p in preds_k]) y_p np.array([p[1] for p in preds_k]) rmse_list.append(np.sqrt(np.mean((y_t - y_p) ** 2))) else: rmse_list.append(np.nan) plt.figure(figsize(8, 5)) plt.plot(k_values, rmse_list, o-, labelUser-Based RMSE) plt.xlabel(Number of Neighbors (k)) plt.ylabel(RMSE) plt.title(Impact of k on Prediction Accuracy) plt.grid(True) plt.legend() plt.savefig(k_tuning_curve.png, dpi300, bbox_inchestight) plt.show()5.3 论文写作关键段落模板可直接引用算法选择依据本文选用皮尔逊相关系数Pearson Correlation Coefficient计算用户相似度因其能有效消除用户评分尺度偏差如用户A习惯打3~5分用户B习惯打1~3分。相较余弦相似度皮尔逊在中心化处理后对稀疏数据鲁棒性更强。物品相似度则采用调整余弦Adjusted Cosine进一步消除用户偏置提升跨用户一致性。评估指标说明采用均方根误差RMSE和平均绝对误差MAE作为核心评估指标。RMSE 对大误差更敏感反映模型稳定性MAE 表征平均预测偏差更具可解释性。测试集严格按时间顺序切分确保无数据穿越data leakage结果具备工程可信度。创新点提炼本系统未使用任何深度学习框架或预训练模型完全基于传统协同过滤原理通过精细化的稀疏矩阵操作、双策略对比实验、k 值敏感性分析及可视化验证揭示了轻量级推荐系统在小规模数据上的可解释性优势为资源受限场景如校园电影平台提供可落地的技术路径。最后一步将k_tuning_curve.png、user1_recommendations_user_based.csv、README.md含环境配置、运行命令、数据来源说明打包命名movie_cf_final_2024.zip—— 这就是一份能让老师点头、答辩不卡壳、查重率低于15%的 Python 毕业设计交付物。本文还有配套的精品资源点击获取