Django协同过滤推荐系统实战:冷启动+实时评分+后台管理
简介:这是一套基于协同过滤算法的电影推荐系统毕业设计实现,面向Python与Web开发初学者及高校计算机专业学生,解决个性化推荐系统从理论到工程落地的学习痛点。资源完整复现了基于用户和基于物品的双路协同过滤逻辑,采用Django 2.2.1框架构建前后端,后端支持MySQL与SQLite双数据库,前端集成Bootstrap 3响应式界面,并预置MovieLens公开数据集及配套爬虫脚本。压缩包共83个文件,含28个核心Python模块(如userCF、itemCF、数据填充与推荐计算脚本)、16个HTML模板页、4个CSV原始数据文件、1个PDF论文、1个SQLite3数据库及详细README与requirements依赖说明,整体大小8.41MB。已有10608人学习下载,读者可直接运行调试、查阅完整技术文档、复用爬虫与数据初始化脚本,并参考附带的毕业论文与目录结构组织方式,快速掌握推荐系统工程化开发全流程。
1. 这不是又一个“Hello World”推荐系统:它真能跑通冷启动+实时评分+后台管理闭环,专治毕设答辩前一周还在调不通 MySQL 连接的焦虑
你手头那份写着“基于协同过滤的电影推荐系统”的毕设文档,是不是已经卡在第三章——数据库建表失败、Django 启动报django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module、用户评分提交后推荐结果永远是《阿凡达》?别慌。这不是理论空谈,而是一套完整落地过、可直接 clone、改完 settings.py 就能跑通的 Django 2.2.1 + Python 3.7 + MySQL 实战工程。它不依赖任何云服务或第三方 API,所有推荐逻辑写在recommend/views.py里,用纯 SQL + Pandas 做用户-物品矩阵构建,用余弦相似度实现基于用户的协同过滤(User-Based CF),并内置管理员后台——你能看到每个用户的历史评分、系统生成的 Top-N 推荐列表、甚至手动触发“重新计算相似度矩阵”按钮。适合正在赶毕设 deadline 的本科生,也适合想搞懂“协同过滤怎么从公式变成可调试代码”的转行者。它不炫技,但每一步都踩在真实部署的坑上:MySQL 字符集乱码、Django 2.2.1 对 MySQL 5.7+ 的驱动兼容性、评分表外键约束导致的 INSERT 失败……这些,我都替你翻过车。
2. 从零搭起推荐骨架:Python 环境、Django 工程结构与 MySQL 数据库三件套实操
2.1 Python 3.7 环境隔离与依赖精准安装:为什么不用 conda 而坚持 venv?
很多同学用pip install django全局安装,结果发现 Django 版本冲突、MySQL 驱动报错。协同过滤对环境版本极其敏感:Django 2.2.1 明确要求mysqlclient>=1.3.13,<2.0,而PyMySQL在该版本下无法正确处理DateTimeField的时区解析。我坚持用venv(不是 conda)的原因很实际:毕业答辩现场演示时,评审老师可能用的是 Windows 10 + 默认 CMD,venv创建的Scripts/activate.bat兼容性远高于 conda 的 shell 脚本。
# 在项目根目录执行(注意:不要用管理员权限!) python -m venv venv_rec venv_rec\Scripts\activate.bat # Windows # venv_rec/bin/activate # macOS/Linux # 安装指定版本(关键!) pip install --upgrade pip pip install django==2.2.1 pip install mysqlclient==1.4.6 # 注意:不是最新版!1.4.6 是 Django 2.2.1 最稳的搭档 pip install pandas==0.24.2 numpy==1.16.4 scikit-learn==0.20.3提示:
mysqlclient==1.4.6必须用源码编译安装。Windows 下需提前装好 Visual Studio Build Tools(2015 或 2017 版本),否则会报Microsoft Visual C++ 14.0 is required。Mac 用户用brew install mysql-client再pip install mysqlclient;Linux 用户先sudo apt-get install python3-dev default-libmysqlclient-dev。
2.2 Django 2.2.1 工程初始化:四步完成核心配置
Django 2.2.1 的settings.py有两处必须改,否则连数据库都连不上:
INSTALLED_APPS加入自定义 app
项目含movies(电影模型)、ratings(评分记录)、recommend(推荐逻辑)三个核心 app,必须显式注册:
# settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'movies', # ← 新增 'ratings', # ← 新增 'recommend', # ← 新增 ]DATABASES配置必须用mysqlclient驱动
不要抄网上教程用pymysql,Django 2.2.1 会报DatabaseWrapper错误:
# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', # ← 引擎固定 'NAME': 'movie_recommender', # ← 数据库名(需提前创建) 'USER': 'root', # ← MySQL 用户名 'PASSWORD': 'your_password', # ← 密码(明文,毕设可接受) 'HOST': '127.0.0.1', # ← 本地连接 'PORT': '3306', # ← 默认端口 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'charset': 'utf8mb4', # ← 关键!支持 emoji 和生僻字 }, 'TEST': { 'CHARSET': 'utf8mb4', 'COLLATION': 'utf8mb4_unicode_ci', } } }参数说明:
charset: 'utf8mb4'解决中文电影名存入乱码;init_command强制严格模式,避免INSERT时因字段为空被静默截断;TEST配置确保单元测试也能用相同字符集。
2.3 MySQL 数据库建模:五张表支撑协同过滤全流程
协同过滤不是只存用户和电影,它需要行为日志、关系映射、缓存中间结果。本系统共 5 张表,全部用 Django ORM 自动生成(python manage.py makemigrations && python manage.py migrate),但你必须理解每张表的物理意义:
| 表名 | 字段(关键) | 作用 | 协同过滤中角色 |
|---|---|---|---|
movies_movie | id,title,genres,year | 电影元数据 | 物品集合(Item Set) |
auth_user | id,username,email | Django 自带用户表 | 用户集合(User Set) |
ratings_rating | user_id,movie_id,rating,timestamp | 用户对电影的 1-5 分评分 | 核心评分矩阵(Ru,i) |
recommend_usersimilarity | user1_id,user2_id,similarity | 用户间余弦相似度(预计算) | 相似度缓存表,加速推荐 |
recommend_recommendation | user_id,movie_id,score,rank | 每个用户的 Top-10 推荐结果 | 推荐结果持久化,供前端调用 |
注意:
ratings_rating表的user_id和movie_id是外键,指向auth_user.id和movies_movie.id。Django 迁移时会自动加ON DELETE CASCADE,但务必在 MySQL 中确认外键已启用(SHOW VARIABLES LIKE 'foreign_key_checks';应为ON)。
3. 协同过滤核心实现:从评分矩阵到 Top-N 推荐的四步推导与代码落地
3.1 构建用户-物品评分矩阵:Pandas DataFrame 是最稳的起点
协同过滤的第一步,是把ratings_rating表里的稀疏数据转成稠密矩阵。很多人直接用scipy.sparse,但在 Django 视图里频繁构造稀疏矩阵容易内存溢出。我的做法是:用 Pandas 读取全量评分,用pivot_table生成 DataFrame,再转为 numpy 数组——既保留可读性,又便于后续计算。
# recommend/utils.py import pandas as pd import numpy as np from ratings.models import Rating def build_user_item_matrix(): """ 返回 (user_ids, movie_ids, matrix) 三元组 matrix[i][j] 表示 user_ids[i] 对 movie_ids[j] 的评分(NaN 表示未评) """ # 1. 从数据库读取所有评分(仅需三列) qs = Rating.objects.values('user_id', 'movie_id', 'rating') df = pd.DataFrame(list(qs)) # 2. 生成唯一用户ID和电影ID列表(保证索引连续) user_ids = sorted(df['user_id'].unique()) movie_ids = sorted(df['movie_id'].unique()) # 3. pivot_table 构建矩阵(自动填充 NaN) matrix = df.pivot_table( index='user_id', columns='movie_id', values='rating', aggfunc='first' # 防止同一用户对同一电影多次评分取平均 ) # 4. 重索引,确保所有用户/电影都在行列中(补 NaN) matrix = matrix.reindex(index=user_ids, columns=movie_ids) return user_ids, movie_ids, matrix.values # 返回 numpy.ndarray逻辑说明:
pivot_table比groupby+unstack更稳定;aggfunc='first'防止历史数据中存在重复评分导致聚合错误;reindex确保矩阵行列严格对应user_ids/movie_ids列表,这是后续用np.nanmean计算用户均值的前提。
3.2 用户相似度计算:余弦相似度 + 均值中心化,拒绝 raw cosine 玄学
原始余弦相似度(Raw Cosine)对用户评分尺度敏感——一个习惯打 4-5 分的用户和一个只打 1-2 分的用户,相似度会虚高。本系统采用 Pearson 相关系数变体:先中心化(减去用户均分),再算余弦。代码封装在recommend/algorithms.py:
# recommend/algorithms.py from scipy.spatial.distance import cosine import numpy as np def calculate_user_similarity(matrix, user_ids): """ matrix: (n_users, n_movies) numpy array, 缺失值为 np.nan 返回相似度矩阵 sim_matrix[i][j] = user_ids[i] 与 user_ids[j] 的相似度 """ n_users = len(user_ids) sim_matrix = np.zeros((n_users, n_users)) # 预计算每个用户的均分(跳过 NaN) user_means = np.nanmean(matrix, axis=1) for i in range(n_users): # 获取用户 i 的中心化向量(只取非 NaN 位置) vec_i = matrix[i] - user_means[i] mask_i = ~np.isnan(vec_i) # True 表示该电影有评分 for j in range(i+1, n_users): vec_j = matrix[j] - user_means[j] mask_j = ~np.isnan(vec_j) mask = mask_i & mask_j # 两人共同评分的电影 if mask.sum() < 5: # 共同评分少于 5 部,视为无相似性 sim = 0.0 else: # 只在共同评分位置上计算余弦 valid_i = vec_i[mask] valid_j = vec_j[mask] sim = 1 - cosine(valid_i, valid_j) # cosine 返回距离,1-距离=相似度 sim_matrix[i][j] = sim sim_matrix[j][i] = sim # 对称 return sim_matrix参数说明:
mask.sum() < 5是硬性阈值——协同过滤中,两个用户至少共同评过 5 部电影,相似度才有统计意义;cosine函数来自scipy,比手写更鲁棒;1 - cosine(...)直接得到 [0,1] 区间相似度。
3.3 Top-N 推荐生成:加权平均预测 + 排序截断,拒绝“猜你喜欢”黑匣子
推荐不是找相似用户然后随机选电影,而是对目标用户未评分的每一部电影,预测其评分,再按预测分排序。预测公式为:
$$ \hat{r}{u,i} = \bar{r}u + \frac{\sum{v \in N(u)} sim(u,v) \cdot (r{v,i} - \bar{r}v)}{\sum{v \in N(u)} |sim(u,v)|} $$
其中 $N(u)$ 是用户 $u$ 的 Top-K 相似用户(K=20)。代码实现:
# recommend/views.py def get_top_n_recommendations(user_id, n=10): """ 为 user_id 生成 Top-N 推荐 返回 [(movie_id, predicted_rating, rank), ...] """ from recommend.algorithms import calculate_user_similarity from movies.models import Movie # 1. 获取全量矩阵和 ID 映射 user_ids, movie_ids, matrix = build_user_item_matrix() try: u_idx = user_ids.index(user_id) except ValueError: return [] # 用户不存在 # 2. 计算相似度矩阵(生产环境应缓存!) sim_matrix = calculate_user_similarity(matrix, user_ids) # 3. 找出 Top-20 相似用户(排除自己) sim_scores = sim_matrix[u_idx] top_sim_indices = np.argsort(sim_scores)[::-1][1:21] # 跳过第0位(自己) # 4. 预测未评分电影的分数 predictions = [] user_mean = np.nanmean(matrix[u_idx]) for m_idx, movie_id in enumerate(movie_ids): if not np.isnan(matrix[u_idx][m_idx]): # 已评分,跳过 continue # 计算分子分母 numerator = 0.0 denominator = 0.0 for v_idx in top_sim_indices: r_vi = matrix[v_idx][m_idx] if np.isnan(r_vi): continue v_mean = np.nanmean(matrix[v_idx]) numerator += sim_scores[v_idx] * (r_vi - v_mean) denominator += abs(sim_scores[v_idx]) if denominator == 0: pred = user_mean # 无相似用户贡献,回退到用户均分 else: pred = user_mean + numerator / denominator predictions.append((movie_id, pred)) # 5. 按预测分降序,取 Top-N predictions.sort(key=lambda x: x[1], reverse=True) top_n = predictions[:n] # 6. 补充电影标题(供前端显示) movie_titles = {m.id: m.title for m in Movie.objects.filter(id__in=[x[0] for x in top_n])} result = [] for i, (mid, score) in enumerate(top_n): result.append({ 'movie_id': mid, 'title': movie_titles.get(mid, '未知电影'), 'predicted_rating': round(score, 2), 'rank': i+1 }) return result关键点:
numerator / denominator是加权平均的核心;denominator == 0的兜底逻辑防止除零;Movie.objects.filter用id__in批量查询,比循环查单个快 10 倍以上。
4. 避坑指南:Django + MySQL 协同过滤项目里,90% 的翻车都发生在这五个地方
4.1 现象:python manage.py migrate报错BLOB/TEXT column used in key specification without a key length
原因:MySQL 5.7+ 默认innodb_large_prefix=OFF,而 Django 2.2.1 的auth_user.username字段是VARCHAR(150),建索引时超出限制。
解决:登录 MySQL 执行
SET GLOBAL innodb_file_format=Barracuda; SET GLOBAL innodb_file_per_table=ON; SET GLOBAL innodb_large_prefix=ON; ALTER TABLE auth_user MODIFY username VARCHAR(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后删掉migrations/下除__init__.py外所有文件,重新makemigrations。
4.2 现象:Django 后台能登录,但ratings_rating表里user_id总是 0
原因:前端表单提交时,user_id字段没传,Django ORM 默认用0填充外键。
解决:在ratings/forms.py中强制绑定当前用户:
class RatingForm(forms.ModelForm): class Meta: model = Rating fields = ['movie', 'rating'] # 不包含 user 字段! def save(self, commit=True, user=None): instance = super().save(commit=False) if user: instance.user = user # 从视图传入 request.user if commit: instance.save() return instance视图中调用:form.save(user=request.user)。
4.3 现象:get_top_n_recommendations()执行超时(>30秒),Apache 直接 504
原因:每次请求都重新计算相似度矩阵,而 1000 用户 × 1000 用户的矩阵计算需 O(N²×M) 时间。
解决:必须加缓存。在settings.py中配置 Redis(或 Django 自带的文件缓存):
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/var/tmp/django_cache', # Linux;Windows 用 'C:/tmp/django_cache' 'TIMEOUT': 3600, # 缓存 1 小时 } }然后在get_top_n_recommendations开头加:
cache_key = f'user_sim_matrix_{len(user_ids)}' sim_matrix = cache.get(cache_key) if sim_matrix is None: sim_matrix = calculate_user_similarity(matrix, user_ids) cache.set(cache_key, sim_matrix, 3600)4.4 现象:中文电影名在 Django Admin 中显示为????
原因:MySQL 数据库、表、字段三级字符集不一致。CREATE DATABASE时没指定CHARSET=utf8mb4。
解决:
-- 删除旧库(谨慎!先备份) DROP DATABASE movie_recommender; -- 重建时指定字符集 CREATE DATABASE movie_recommender CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 然后重新 migrate python manage.py migrate4.5 现象:mysqlclient安装成功,但python manage.py dbshell报Can't connect to local MySQL server
原因:MySQL 服务未启动,或my.ini中bind-address=127.0.0.1被注释导致监听0.0.0.0,而防火墙阻止了 3306 端口。
解决:
- Windows:打开「服务」→ 找到
MySQL→ 右键「启动」 - 检查
my.ini:确保bind-address=127.0.0.1且未被#注释 - 命令行验证:
mysql -u root -p能登录即服务正常
5. 生产就绪技巧:让毕设系统扛住答辩演示流量,同时保住你的 GPA
5.1 推荐结果预计算 + 定时任务:告别“点击推荐按钮后转圈 10 秒”
答辩现场最怕什么?评委说“点一下推荐看看”,然后你盯着加载动画心跳加速。协同过滤的预测计算必须离线化。Django 自带django-crontab可实现定时任务,每天凌晨 2 点批量更新所有活跃用户的 Top-10 推荐:
# 安装 pip install django-crontab # settings.py INSTALLED_APPS += ['django_crontab'] CRONJOBS = [ ('0 2 * * *', 'recommend.cron.update_all_recommendations'), # 每天2点执行 ]# recommend/cron.py from django.contrib.auth.models import User from recommend.views import get_top_n_recommendations from recommend.models import Recommendation def update_all_recommendations(): # 只更新近 30 天有评分的用户(减少计算量) recent_user_ids = set( r.user_id for r in Rating.objects.filter( timestamp__gte=timezone.now() - timedelta(days=30) ).values_list('user_id', flat=True) ) for uid in recent_user_ids: recs = get_top_n_recommendations(uid, n=10) # 清空旧推荐,写入新结果 Recommendation.objects.filter(user_id=uid).delete() Recommendation.objects.bulk_create([ Recommendation(user_id=uid, movie_id=r['movie_id'], score=r['predicted_rating'], rank=r['rank']) for r in recs ])技巧:
bulk_create比循环save()快 50 倍;timedelta(days=30)过滤掉沉寂用户,让计算量从 1000 降到 200;Recommendation模型需加unique_together = ('user_id', 'movie_id')防止重复插入。
5.2 Django Admin 深度定制:让答辩老师一眼看懂你的推荐逻辑
默认 Admin 只能 CRUD,但你可以把它变成“推荐系统监控面板”。在recommend/admin.py中:
from django.contrib import admin from .models import UserSimilarity, Recommendation @admin.register(UserSimilarity) class UserSimilarityAdmin(admin.ModelAdmin): list_display = ('user1', 'user2', 'similarity', 'updated_at') list_filter = ('updated_at',) search_fields = ('user1__username', 'user2__username') ordering = ('-similarity',) def user1(self, obj): return obj.user1.username def user2(self, obj): return obj.user2.username @admin.register(Recommendation) class RecommendationAdmin(admin.ModelAdmin): list_display = ('user', 'movie_title', 'score', 'rank', 'created_at') list_filter = ('rank', 'created_at') search_fields = ('user__username', 'movie__title') ordering = ('user', 'rank') def movie_title(self, obj): return obj.movie.title[:30] + '...' if len(obj.movie.title) > 30 else obj.movie.title效果:评委点进
/admin/recommend/recommendation/,能看到所有用户的推荐列表,按rank筛选“Top-1”、“Top-3”,还能搜用户名快速定位——这比口头解释“我们用了协同过滤”有力得多。
5.3 最小化部署 checklist:三步打包,答辩前 1 小时搞定
别等到答辩前夜才部署。按这个顺序走,1 小时内必通:
数据库导出/导入(避免现场建表)
# 导出(开发机) mysqldump -u root -p --databases movie_recommender > movie_rec.sql # 导入(答辩机,先创建空库) mysql -u root -p movie_recommender < movie_rec.sql静态文件收集(Django 2.2.1 必做)
python manage.py collectstatic --noinput # 确保 settings.py 中 STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')用
runserver替代 Gunicorn(答辩机无需高并发)# 修改 settings.py DEBUG = True # 答辩现场开 DEBUG,方便看错误页 ALLOWED_HOSTS = ['*'] # 临时允许所有 host # 启动 python manage.py runserver 0.0.0.0:8000然后用手机访问
http://[答辩机IP]:8000,确保首页、登录、推荐页全通。
从那以后我每次做毕设,都强制走一遍「导出 SQL → 新建虚拟机 → 导入 → collectstatic → runserver」全流程,哪怕只是本地测试。因为答辩现场的网络、系统环境、甚至 USB 插口松动,都可能让你的 demo 在最后一分钟崩掉。这份协同过滤系统不是炫技的玩具,它是你交出去的代码答卷——而答卷的及格线,从来不是“能跑”,而是“在任何一台 Windows 10 笔记本上,插上 U 盘,双击 start.bat,就能让评委点头说‘嗯,逻辑是通的’”。
希望帮到你。
本文还有配套的精品资源,点击获取