协同过滤图书推荐系统实战:Django+MySQL构建基于商品的推荐引擎
简介基于Python Django框架的图书推荐系统完整项目后端采用基于物品的协同过滤算法Item-based CF依托豆瓣图书与评分数据集结合Mysql存储实现从用户登录、书目管理到推荐结果展示的完整流程。项目包含源码、数据集和说明文档适合计算机相关专业学生用于毕业设计、课程设计或项目立项演示也适合希望上手推荐系统与Web开发的初学者。资源包共52个文件以20个Python源码文件为核心涵盖模型、视图、URL路由及协同过滤算法实现另有pyc编译文件、Django配置文件、前端HTML/CSS模板和README说明整体压缩包约797KB体积小、结构清晰便于快速部署与二次开发。压缩包内还包含登录、首页、推荐结果展示等页面模板以及Mysql相关配置。当前已有78人学习下载代码经运行验证、测试通过可放心使用也可在此基础之上扩展新功能或调整算法逻辑进一步提升学习价值。1. 基于商品的协同过滤图书推荐系统不写神经网络照样做出推荐拿到这份源码的第一感觉是「干净」一个 Django 工程、一份豆瓣图书评分数据集、两个协同过滤算法文件加起来代码量不大却能把“用户登录 → 提交评分 → 生成推荐列表”完整跑通。它用的不是深度学习正是推荐系统入行必学的基于商品的协同过滤Item-based Collaborative Filtering。用户给哪几本书打过分系统就去找与这些书“口味相似”的其他书按预测分从高到低排出 Top-N。全流程不依赖图书标题、作者、分类等文本特征只靠评分共现关系逻辑简单但效果不差。如果你正在做推荐系统课设或毕设或者想搞清楚“物品相似度到底怎么算”这套源码值得下下来亲手跑一遍。2. 先把评分数据理顺uid_score_bid 文件的读取、矩阵构建与稀疏度评估这个资源里真正起决定作用的文件只有一个uid_score_bid。它没有扩展名用文本编辑器打开后每一行就是一条「谁给哪本书打了多少分」的记录。字段顺序在源码里是用户 IDuid、评分score、图书 IDbid中间用逗号分隔。整份数据集本质上就是一张用户–物品评分表协同过滤算法所有计算都从它出发。2.1 uid_score_bid 文件到底存了什么import pandas as pd # 文件没有表头必须手动指定三列名称 ratings pd.read_csv(data/uid_score_bid.csv, names[uid, score, bid]) print(评分记录总数:, len(ratings)) print(参与评分的用户数:, ratings[uid].nunique()) print(被评分的图书数:, ratings[bid].nunique()) print(ratings.head(10))names参数把三列分别命名为uid、score、bidnunique()统计去重后的用户数和图书数这两个数字直接决定后面相似度矩阵的规模。项目里大概是不到一千个用户、数千本图书、上万条评分记录规模不大但足够把一条 Item-based 推荐链路完整跑通。再看一眼评分分布# 豆瓣评分以 1~5 的整数为主统计每个分值出现的次数 print(ratings[score].value_counts().sort_index())输出结果里 4 分和 5 分占了大头2 分以下记录很少。这是豆瓣用户真实打分习惯——不喜欢的书往往直接不打分。这个分布会影响后续推荐权重既然整体评分偏高单纯给高分书找相似书意义有限更好的做法是额外统计每个用户自己的评分均值把「用户给分高」和「用户真的喜欢」区分开后面讲排序时会再提到。2.2 从 CSV 到两种内存结构正排表和倒排表协同过滤算法的输入不是 DataFrame而是嵌套字典。常见做法是构造两个结构正排表user_items[uid][bid] score供推荐阶段遍历某个用户评过哪些书倒排表item_users[bid][uid] score供相似度阶段统计每个物品被哪些用户评过。from collections import defaultdict # 正排表用户 - {图书: 评分} user_items defaultdict(dict) for uid, bid, score in ratings.itertuples(indexFalse): user_items[uid][bid] score # 倒排表图书 - {用户: 评分} item_users defaultdict(dict) for uid, bid, score in ratings.itertuples(indexFalse): item_users[bid][uid] score # 简单验证用户 1 应该至少有一条评分记录 assert len(user_items[1]) 0, 用户 1 没有评分记录 print(用户 1 评分过的图书:, list(user_items[1].keys())[:5])这里用itertuples而不是iterrows是因为前者在遍历 DataFrame 时快一个量级处理几万条记录几乎无感。defaultdict(dict)省去了每一层的判空逻辑。多花这几行代码把结构转好后面算法部分就不再依赖 Pandas读取原始文件的次数也变成一次——这也正是项目里算法模块的典型做法启动时全部加载进内存。2.3 稀疏度决定了算法效果的下限n_users ratings[uid].nunique() n_items ratings[bid].nunique() sparsity 1 - len(ratings) / (n_users * n_items) print(f稀疏度: {sparsity:.4f})算出来的值通常落在 0.9x说明评分矩阵非常稀疏——大量用户只评过几本书物品之间缺乏共同评分用户。稀疏度直接影响两个决策第一相似度计算要不要设置“共现次数下界”比如两个物品至少被 3 个用户同时评过才保留相似度否则算出来的相似度纯属噪音第二用户至少评过多少本书才触发协同过滤否则推荐结果全凭一两条评分得出来可信度极低。3. 物品相似度是怎么算出来的余弦公式、共现矩阵与 Top-N 推荐生成理解了数据结构接下来就是核心逻辑。基于商品的协同过滤分两步先离线计算所有物品之间的相似度再在线根据用户已评分的物品生成推荐列表。3.1 余弦相似度为什么是 Item-based 的默认选择物品 i 和 j 之间的相似度数学上定义为sim(i, j) |N(i) ∩ N(j)| / sqrt(|N(i)| * |N(j)|)其中 N(i) 是给物品 i 评过分的用户集合。这个公式只看「谁评过」不关心评分具体是多少天然规避了用户打分尺度不同带来的偏差。相比皮尔逊相关系数余弦相似度在稀疏数据上更稳定——两个物品共同评分用户往往只有个位数皮尔逊在这种样本量下极易算出接近 ±1 的极端值参考价值很低。等项目数据量涨上去再替换成皮尔逊或调整余弦相似度也不迟这是推荐系统最常见的演进路径。3.2 共现矩阵的核心实现import math from collections import defaultdict def build_item_similarity(user_items): 输入正排表 user_items: {uid: {bid: score}} 输出物品相似度字典: {(item_a, item_b): sim_score} # 1. 倒排表物品 - 评过分的用户集合 item_users defaultdict(set) for uid, items in user_items.items(): for bid in items: item_users[bid].add(uid) # 2. 共现矩阵统计两个物品被同一用户评分的次数 cooccur defaultdict(int) for uid, items in user_items.items(): bids list(items.keys()) for i in range(len(bids)): for j in range(i 1, len(bids)): a, b bids[i], bids[j] cooccur[(a, b)] 1 cooccur[(b, a)] 1 # 3. 代入余弦公式计算相似度 item_sim {} for (a, b), cnt in cooccur.items(): item_sim[(a, b)] cnt / math.sqrt(len(item_users[a]) * len(item_users[b])) return item_sim第一步构建倒排表把「物品有哪些用户评过」做成集合后面算 |N(i)| 直接取集合长度即可。第二步是性能关键如果对全量物品两两组合计算复杂度是 O(items²)数据稍大就扛不住改成遍历每个用户已评分的物品组合实际只统计共现过的物品对内存和耗时都小很多。第三步的余弦公式里cnt就是共现次数len(item_users[a])和len(item_users[b])分别是两个物品各自的评分用户数。3.3 推荐排序加权求和而不是只看相似度相似度算完怎么给用户生成推荐列表基本逻辑是用户给某本书打了 5 分那就要优先推荐与这本书高度相似的书如果用户只给另一本书打了 2 分即使它的相似书很多也不应该占太高的推荐权重。def recommend(user_id, user_items, item_sim, top_n10): 给指定用户生成 Top-N 推荐返回图书 ID 列表 rated set(user_items[user_id].keys()) scores defaultdict(float) sim_sum defaultdict(float) # 遍历用户已评分的每本书累加相似物品的加权得分 for bid, rating in user_items[user_id].items(): for (a, b), sim in item_sim.items(): if a bid and b not in rated: scores[b] sim * rating sim_sum[b] sim # 用加权平均值排序避免热门书靠累加得分霸榜 ranked [] for bid in scores: if sim_sum[bid] 0: continue ranked.append((scores[bid] / sim_sum[bid], bid)) ranked.sort(reverseTrue) return [bid for _, bid in ranked[:top_n]]rated集合用来排除用户已经读过的书这是协同过滤的基本约束。scores[b]累加的是相似度乘以用户评分sim_sum[b]累加的是相似度本身两者相除得到的是一个归一化预测分。如果不做这一步归一化候选物品的原始得分会被相似度总和带偏——那些和很多本书都有弱相似度的热门书累加得分必然虚高这就是为什么单纯的加权求和会推出一堆豆瓣高分书。归一化之后排序结果的解释性也更强预测分接近用户自己的历史平均评分水平。3.4 和基于用户的协同过滤对比项目里同时存在basedUserCF.py这给了对照实验的机会。两种算法本质都是找“相似对象”关键区别在相似对象是谁维度UserCF基于用户ItemCF基于物品相似对象与你评分习惯相似的用户与你读过书相似的图书适合场景新闻、短视频用户兴趣变化快图书、电商物品相对稳定冷启动难度新用户无行为时更难推荐新用户只要评过一两本书即可推荐可解释性解释起来绕「因为你喜欢 A所以推荐相似的 B」图书这种物品特征稳定的场景ItemCF 天然更合适这也是这套源码把基于物品作为核心方案的原因。basedUserCF.py的存在更多是用于课设对比写论文或答辩时可以放一组实验两种算法在同一数据集上的推荐效果差异。4. 把算法搬上 Django 和 MySQL模型建模、数据导入与视图集成算法代码只是推荐系统的内核完整系统还要能被人通过网页用起来。这一章梳理 Django 工程结构以及怎么把离线算法和 Web 层连接起来。4.1 Django 工程文件结构与职责划分打开工程包典型 Django 项目结构清晰可见项目配置目录bookRecommend/里是settings.py、urls.py、wsgi.pylogin/应用处理登录注册主应用里放着models.py、views.py、basedUserCF.py等核心模块。这种划分很常规但有一点值得留意算法模块和 Web 逻辑没有强耦合views.py负责从数据库取数据、调用算法、把结果交给模板渲染算法计算细节封装在独立文件中。# 切换到工程根目录先跑迁移再启动服务 python manage.py makemigrations python manage.py migrate python manage.py runserver 0.0.0.0:8000makemigrations会根据 models 定义生成数据库迁移脚本migrate把表结构同步到 MySQL。runserver是 Django 自带开发服务器本地调试够用真要上线后面要换 gunicorn 或 uwsgi。4.2 评分表建模与 MySQL 导入数据文件uid_score_bid最终要落到 MySQL 里ORM 模型定义很直接# bookRecommend/models.py from django.db import models class Rating(models.Model): uid models.IntegerField(db_indexTrue) bid models.IntegerField(db_indexTrue) score models.FloatField() class Meta: db_table uid_score_bid unique_together (uid, bid)db_indexTrue给 uid 和 bid 各建索引推荐查询时按用户 ID 取评分记录会快很多。unique_together保证同一用户对同一本书只会有一条评分避免重复数据污染算法。db_table指明表名方便和 CSV 文件名对应上。MySQL 连接配置放在settings.pyDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: bookrecommend, USER: root, PASSWORD: 你的数据库密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }如果手上已经有整理好的 CSV可以直接用 SQL 批量导入不必写 Python 脚本逐行 insertLOAD DATA LOCAL INFILE uid_score_bid.csv INTO TABLE uid_score_bid FIELDS TERMINATED BY , LINES TERMINATED BY \n (uid, score, bid);注意列顺序要和 CSV 文件一致uid、score、bid千万别按 DataFrame 里看到的顺序想当然。用LOAD DATA导入百万行数据也是秒级完成比 ORM 批量创建快得多。4.3 在视图层把推荐结果渲染成网页算法模块跑完如何把推荐结果展示到网页上核心在views.pyfrom django.shortcuts import render from .models import Rating from .recommend import recommend def see_recommendations(request): # 从 URL 参数读取当前用户 ID默认给用户 1 演示 uid int(request.GET.get(uid, 1)) # 从数据库加载该用户的评分记录 rows Rating.objects.filter(uiduid) if not rows.exists(): return render(request, see.html, {error: 该用户还没有评分记录先去 index 页评几本书吧}) # 组装成正排表送给算法模块 user_items {uid: {r.bid: r.score for r in rows}} # 正式环境建议把相似度矩阵做成启动时缓存不要每次请求都重算 item_sim build_item_similarity(all_user_items) rec_bids recommend(uid, user_items, item_sim, top_n15) books BookInfo.objects.filter(bid__inrec_bids) return render(request, see.html, {books: books})filter(uiduid)返回的是 QuerySet按用户 ID 取评分记录如果 QuerySet 为空说明该用户冷启动直接返回提示页。build_item_similarity这里有个性能优化点每次请求都重建相似度矩阵会拖垮页面响应。常见做法是启动时算好存内存或者用 Redis 做缓存这套源码里由于数据量小直接每次重建也能接受。模板see.html里的渲染逻辑很简单{% for book in books %}循环输出书籍封面和书名即可。登录页面login.html提交表单后重定向到推荐页整个流程就闭环了。5. 协同过滤落地常见问题排查冷启动、数据稀疏、热门偏差与数据库坑推荐系统的坑不在算法公式里而在真实运行环境中。这几条踩坑记录是跑这套源码时最容易撞上的按「现象 → 原因 → 解决」逐个说清楚。5.1 新用户冷启动推荐列表永远是空的现象一个新注册用户没评过任何书打开推荐页直接空白评了 1 本书推荐列表只有零星几条还都是豆瓣上随便哪本都在推荐的热门书。原因Item-based 协同过滤的输入是评分记录没有评分就没有共现关系算法无从下手。解决加一层兜底逻辑——用户评分数量小于 3 条时直接返回全局热门图书榜。热门榜由整个数据集评分数最多的前 10 本书构成不涉及个性化计算。等用户评到 5 本以上再切换到协同过滤。这套源码的默认页面是从 index 页引导用户先评分但新用户直接访问推荐页时依然会触发空列表这个边界值得处理。5.2 评分太稀疏相似度矩阵全是噪音现象推荐结果出现明显不相关的书比如用户只读过 Python 编程书推荐列表里混进来一本情感小说。原因稀疏矩阵下两本书的共现次数只有 1 或 2余弦相似度算出来虚高相似关系完全不具备统计意义。解决在build_item_similarity里加一个共现次数下界阈值比如if cnt 3: continue过滤掉低置信度的相似关系。调阈值后矩阵会稀疏很多但剩下的每条相似度才勉强算“有点可信”。5.3 热门书霸榜推荐的多样性失效现象每次推荐列表里都是《小王子》《活着》这类高分热门书用户根本没有被个性化的感觉。原因余弦相似度对热门物品有天然偏向。热门书出现在大量用户的评分列表里共现次数远超小众书即使归一化之后它们的相似度整体仍然偏高。解决在排序阶段对热门物品做降权常见公式是把相似度乘以1 / log(1 该物品的评分人数)热度越高的书降权越狠。改完后推荐列表会冒出一些“不完美但更对口”的书这也是推荐效果看起来更像推荐的关键一步。5.4 MySQL 8 连不上认证插件不兼容现象执行python manage.py migrate时报错Authentication plugin caching_sha2_password cannot be loaded。原因MySQL 8 默认的认证插件是caching_sha2_password而 Django 连接 MySQL 的客户端库版本较老时不支持这个插件。解决把数据库账号的认证方式改为旧版兼容模式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;改完后重启 MySQL 服务再跑migrate。如果用的是 PyMySQL可以在项目__init__.py里加一行pymysql.install_as_MySQLdb()效果相同。5.5 Python 环境报错 No module named MySQLdb现象本地 Python 3.10 环境运行时ModuleNotFoundError: No module named MySQLdb。原因Django 原生依赖mysqlclient而它在 Windows 上安装经常因为没有预编译的 wheel 包失败。解决先试pip install mysqlclient装不上就改用 PyMySQLpip install PyMySQL然后在 Django 项目__init__.py中加入import pymysql pymysql.install_as_MySQLdb()这一步做完Django 的mysqldb调用会自动转给 PyMySQL不需要改settings.py。6. 推荐结果怎么验证和调参离线评估与三处最值得动的参数推荐系统做完不是能跑就行得知道推荐得准不准。最轻量有效的评估方法是留出法把用户的历史评分随机切成训练集和测试集用训练集生成推荐看推荐结果里有多少本书出现在测试集里。def evaluate(user_items, item_sim, users, test_ratio0.2, top_n10): 用留出法估算推荐准确率命中数 / 测试集评分数 total_hit 0 total_test 0 for uid in users: rated list(user_items[uid].keys()) if len(rated) 5: continue test_count max(1, int(len(rated) * test_ratio)) test_items set(rated[:test_count]) train_items {bid: user_items[uid][bid] for bid in rated[test_count:]} # 注意这里用的是完整 user_items实际应该重新训练数据量小这个近似也能看趋势 recs set(recommend(uid, {uid: train_items}, item_sim, top_n)) total_hit len(recs test_items) total_test len(test_items) return total_hit / total_test if total_test else 0test_ratio0.2表示留出 20% 的评分作为验证集recs test_items计算推荐命中数最后返回的是召回率——也就是测试集里的书有多少比例出现在了推荐列表中。这个数字一般不会太高0.05 到 0.15 之间都算正常毕竟每本书只有几条评分能推荐中已经很难得。参数调整主要看三处。第一处是top_n推荐列表从 5 条改到 20 条命中率会上升但用户感知的精准度会下降实际系统中通常 10 到 15 条比较均衡。第二处是共现次数下界从 0 调到 5相似度矩阵噪音减少推荐结果会更纯净但覆盖率会下降常见做法是在 2 到 5 之间试几组值挑命中率最高的。第三处是热门降权系数上面提到的1 / log(1 热度)中把 log 换成平方根可以调节降权力度数值越大越偏向小众书。那次调完参数后我把这套源码改成了自己的一个习惯模板每次拿到推荐系统数据集先花五分钟统计稀疏度和评分分布再决定要不要设置共现下界、要不要降权热门物品。数据说话比凭感觉调参数靠谱得多。希望你也能在这份源码上跑出自己的推荐列表希望这些调试路径能让你少绕几个弯。本文还有配套的精品资源点击获取