☰
基于Python的电影推荐系统毕业设计:ItemCF算法、数据库设计与Flask实现
2026/10/12 11:59:31 网站建设 项目流程

简介:这是一套基于Python的电影推荐系统毕业设计完整源码与数据库,面向计算机、通信、人工智能、自动化等专业的学生与教师,可用于毕业设计、期末课程设计或课程大作业,也适合作为小白入门与进阶学习的实战项目。压缩包共726个文件,约19.54MB,其中39个py文件承载推荐算法与后端逻辑,41个vue与164个js、53个css构成前端界面,另有sql数据库脚本、html页面、图片与字体等静态资源,并附bat启动脚本与md说明,目录结构清晰,便于按模块阅读与二次开发。项目经过调试测试,可正常运行,答辩评审分达98分,已有239人学习下载。读者可从中获取完整的推荐系统实现方案、数据库设计、前后端交互逻辑与工程化目录组织方式,并在此基础上修改调整,实现个性化推荐等不同功能,具备较高的学习借鉴价值。

1. 从一份能跑起来的电影推荐系统说起:毕业设计到底该交什么

很多同学做毕业设计,选题定了「基于 Python 的电影推荐系统设计与实现」,结果卡在第一步:到底交什么才算完整。我见过太多人把 MovieLens 数据集下载下来,跑通一个协同过滤的 demo,截图几张评分预测的表格就交差了,答辩时被老师一句「你的数据库表结构呢」问得哑口无言。这个标题背后其实包含四样东西:一套能跑的 Python 推荐算法、一个存电影和用户行为的数据库、一份可复现的源码工程,以及一套能自圆其说的设计文档。它解决的不是「推荐准不准」这种工业级问题,而是「你能不能独立完成一个数据闭环系统」的验证。适合谁?适合计算机相关专业、需要在一个学期内交付可演示系统的本科生,也适合想拿一个完整项目练手 Python 后端与数据处理的新手。下面我按自己带过几届毕设的经验,把这条路拆开讲清楚。

2. 推荐算法选型:协同过滤、内容推荐还是混着来

2.1 三种主流方案在毕设场景下的取舍

毕业设计的时间通常只有 8 到 12 周,算法选型的第一原则是「能讲清楚原理,且代码量可控」。基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)是最常见的选择,因为它们只需要用户-物品评分矩阵,不需要电影文本特征。ItemCF 在电影场景下通常比 UserCF 更稳,原因是电影数量相对固定,物品相似度矩阵可以离线算好,线上响应快,答辩时也容易解释「为什么给你推荐这部」。

内容推荐(Content-Based)需要电影的类型、导演、演员等元数据,MovieLens 的 movies.csv 里只有 genres 字段,信息量偏少,单独用效果一般。混合推荐听起来高级,但如果你把协同过滤和内容推荐简单加权,答辩老师很可能追问「权重怎么定的」,这时候没有实验对比就会很被动。

我的建议是:主算法用 ItemCF,辅以基于流行度的冷启动兜底。这样既有协同过滤的核心逻辑,又能解释新用户第一次进来看到什么。

2.2 ItemCF 的核心计算与 Python 实现

ItemCF 的核心是算物品之间的相似度。常用余弦相似度,公式是两列评分向量的点积除以模长乘积。下面这段代码用 pandas 和 numpy 实现,输入是用户-电影评分表,输出是电影相似度矩阵。

import numpy as np import pandas as pd def item_similarity(ratings: pd.DataFrame): """ ratings: 包含 user_id, movie_id, rating 三列 返回: 电影-电影余弦相似度 DataFrame """ # 构建用户-电影评分矩阵,缺失值填 0 matrix = ratings.pivot_table( index='user_id', columns='movie_id', values='rating' ).fillna(0) # 转置成电影-用户矩阵,每行是一部电影的评分向量 item_matrix = matrix.T.values # 计算模长 norms = np.linalg.norm(item_matrix, axis=1, keepdims=True) # 防止除零 norms[norms == 0] = 1e-9 # 归一化后点积即余弦相似度 normalized = item_matrix / norms sim = np.dot(normalized, normalized.T) # 对角线置零,避免自己和自己最相似 np.fill_diagonal(sim, 0) return pd.DataFrame(sim, index=matrix.columns, columns=matrix.columns)

逻辑说明:pivot_table把长表转成宽表,这是协同过滤的标准输入格式。fillna(0)表示未评分,注意这里用 0 而不是均值,是为了让余弦相似度只受共同评分影响。np.fill_diagonal那一步很关键,否则推荐结果永远是电影自己。

参数说明:rating列如果是 1 到 5 分,可以直接用;如果要做归一化,可以改成(rating - 用户均分),这样能消除用户打分偏严或偏松的影响,但代码会多几行,毕设里看时间取舍。

2.3 生成推荐列表与冷启动兜底

有了相似度矩阵,给用户推荐就是:找到用户看过的电影,取这些电影的相似电影,按相似度加权求和排序,去掉已看过的。

def recommend_for_user(user_id, ratings, sim_df, top_n=10): """给指定用户生成 top_n 推荐""" user_ratings = ratings[ratings['user_id'] == user_id] if user_ratings.empty: # 冷启动:返回评分人数最多的电影 popular = ratings.groupby('movie_id')['rating'].count() return popular.sort_values(ascending=False).head(top_n).index.tolist() seen = set(user_ratings['movie_id']) scores = {} for _, row in user_ratings.iterrows(): mid, r = row['movie_id'], row['rating'] if mid not in sim_df.columns: continue similar = sim_df[mid].drop(labels=list(seen), errors='ignore') for other_mid, s in similar.items(): scores[other_mid] = scores.get(other_mid, 0) + s * r ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [mid for mid, _ in ranked[:top_n]]

逻辑说明:加权求和时用用户原始评分r作为权重,评分高的电影对推荐影响更大。drop(labels=list(seen))去掉已看,避免推荐重复。

参数说明:top_n一般取 10 到 20,毕设演示取 10 足够。冷启动分支返回流行电影,这是最常见的兜底策略,答辩时能体现你考虑了新用户场景。

3. 数据库设计:从 ER 图到能跑 SQL 的表结构

3.1 电影推荐系统需要哪几张表

毕设的数据库不用太复杂,但必须完整。核心四张表:用户表user、电影表movie、评分表rating、推荐结果表recommendation。评分表是算法输入,推荐结果表是算法输出,这样整个数据流是闭环的。

表名关键字段说明
useruser_id, username, password_hash, create_time存登录信息
moviemovie_id, title, genres, release_year存电影元数据
ratinguser_id, movie_id, rating, timestamp存用户评分,联合主键
recommendationuser_id, movie_id, score, gen_time存离线算好的推荐

评分表用(user_id, movie_id)做联合主键,天然防止同一用户对同一电影重复评分。推荐结果表可以加一个gen_time,方便演示「重新计算推荐」的功能。

3.2 建表 SQL 与索引优化

下面这段 SQL 在 MySQL 8.0 和 SQLite 上都能跑,注意 SQLite 不支持AUTO_INCREMENT,要改成AUTOINCREMENT。

CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE movie ( movie_id INT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(100), release_year INT ); CREATE TABLE rating ( user_id INT NOT NULL, movie_id INT NOT NULL, rating FLOAT NOT NULL, timestamp INT, PRIMARY KEY (user_id, movie_id), INDEX idx_movie (movie_id) ); CREATE TABLE recommendation ( user_id INT NOT NULL, movie_id INT NOT NULL, score FLOAT NOT NULL, gen_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, movie_id) );

逻辑说明:movie_id在评分表里建索引,因为 ItemCF 算相似度时要频繁按电影聚合。rating用 FLOAT 而不是 INT,是为了兼容 MovieLens 的 0.5 分制。

参数说明:VARCHAR(128)存密码哈希,如果用 bcrypt 够用;timestamp用 INT 存 Unix 时间戳,和 MovieLens 原始数据一致,导入时不用转换。

3.3 从 CSV 导入 MovieLens 数据的脚本

MovieLens 的 ratings.csv 格式是userId,movieId,rating,timestamp,写个脚本批量插入,比手动敲 SQL 靠谱。

import pandas as pd import sqlite3 conn = sqlite3.connect('movie_recommend.db') ratings = pd.read_csv('ratings.csv') # 列名映射到数据库字段 ratings = ratings.rename(columns={ 'userId': 'user_id', 'movieId': 'movie_id' }) ratings.to_sql('rating', conn, if_exists='append', index=False) conn.close()

逻辑说明:to_sql是 pandas 自带的批量写入,比逐条 INSERT 快很多。if_exists='append'表示追加,第一次跑之前记得先建表。

参数说明:如果数据量超过百万行,to_sql会慢,可以加chunksize=10000分批写。SQLite 单文件数据库适合毕设演示,换成 MySQL 只需改连接字符串。

4. 避坑与排查:毕设答辩前最容易翻车的五个地方

4.1 现象:推荐结果全是同一部电影

原因:相似度矩阵对角线没置零,或者推荐时没去掉已看过的电影,导致某部热门电影因为评分人数多,加权后分数永远最高。

解决:检查np.fill_diagonal(sim, 0)是否执行,推荐函数里drop(labels=list(seen))是否生效。另外可以在排序前对分数做一次归一化,除以该电影被推荐次数。

4.2 现象:数据库导入中文电影名变成乱码

原因:CSV 文件编码是 UTF-8,但 SQLite 或 MySQL 连接时没指定编码,或者建表时字符集用了 latin1。

解决:MySQL 建库时指定CHARACTER SET utf8mb4,Python 连接时加charset='utf8mb4'。SQLite 默认 UTF-8,一般不会出问题,但如果用 pandas 读 CSV,加encoding='utf-8'。

4.3 现象:答辩时老师问「新用户怎么办」答不上来

原因:只做了协同过滤,没考虑冷启动。新用户没有评分记录,ItemCF 无法计算。

解决:在推荐函数里加分支,新用户返回流行度最高的电影。更进一步,可以在用户注册时让用户勾选几个喜欢的类型,用内容推荐做初始推荐,这样答辩时能多讲一层。

4.4 现象:源码在别人电脑上跑不起来

原因:依赖没写清楚,或者用了绝对路径读数据文件。

解决:在项目根目录放requirements.txt,列出 pandas、numpy、flask 等依赖及版本。数据文件路径用os.path.join(os.path.dirname(__file__), 'data', 'ratings.csv'),不要写D:\毕设\data\ratings.csv。

4.5 现象:推荐结果每次刷新都不一样

原因:相似度计算时用了随机初始化,或者推荐排序时分数相同的电影顺序不稳定。

解决:ItemCF 本身是确定性的,检查有没有引入随机数。排序时如果分数相同,加一个次级排序键,比如按电影 ID 升序,保证结果可复现。

5. 把系统跑起来:Flask 接口与前端展示的最小闭环

5.1 用 Flask 暴露推荐接口

毕设演示需要一个能点击的界面,最省事的方案是 Flask 加几个 HTML 页面。下面是一个推荐接口,接收 user_id,返回 JSON 格式的推荐列表。

from flask import Flask, request, jsonify import pandas as pd import numpy as np app = Flask(__name__) # 启动时加载数据和相似度矩阵,避免每次请求重算 ratings = pd.read_csv('data/ratings.csv') sim_df = item_similarity(ratings) @app.route('/recommend') def recommend(): user_id = int(request.args.get('user_id', 1)) movies = recommend_for_user(user_id, ratings, sim_df, top_n=10) return jsonify({'user_id': user_id, 'movies': movies}) if __name__ == '__main__': app.run(debug=True, port=5000)

逻辑说明:item_similarity和recommend_for_user是前面定义的函数,放在同一个文件或 import 进来。启动时算一次相似度,请求时只做查表和排序,响应能在毫秒级。

参数说明:debug=True只在开发时用,答辩演示可以关掉。port=5000是 Flask 默认端口,如果被占用改成 5001。

5.2 前端页面与数据库查询的衔接

前端用一个简单的 HTML 表格展示推荐结果,通过 fetch 调用接口。数据库里的recommendation表可以存离线算好的结果,页面加载时直接查表,这样即使算法部分出问题,演示也不会崩。

fetch('/recommend?user_id=1') .then(res => res.json()) .then(data => { const list = document.getElementById('movie-list'); data.movies.forEach(mid => { const li = document.createElement('li'); li.textContent = '电影 ID: ' + mid; list.appendChild(li); }); });

逻辑说明:fetch是浏览器原生 API,不需要额外引入 jQuery。拿到 movie_id 后可以再查电影表拿标题,这里为了简洁只显示 ID。

参数说明:user_id可以从登录 session 里取,演示时写死 1 也行。如果要显示电影名,在接口里 join 一下 movie 表返回 title 字段。

6. 让推荐结果经得起追问:离线评估与参数调优的一个实用技巧

答辩时老师最可能问的是「你怎么知道推荐得准」。工业界用 A/B 测试,毕设没这个条件,但你可以做离线评估。把评分数据按时间切分,前 80% 做训练,后 20% 做测试,对每个用户预测他实际看过的电影,算命中率(Hit Rate)和 NDCG。这两个指标不用手写,scikit-learn没有现成的,但逻辑不复杂:对每个测试用户,取推荐列表 top 10,看有多少落在测试集里,命中数除以测试集电影数就是 Hit Rate。

我一般会调的一个参数是相似度矩阵的邻居数量 K。ItemCF 里不是所有相似电影都参与推荐,只取最相似的 K 部。K 太小,推荐多样性差;K 太大,噪声多。在 MovieLens 100K 上,K 取 20 到 50 之间通常 Hit Rate 最高。你可以写个循环,K 从 10 到 100 步长 10,跑一遍看指标曲线,把这张图放进论文里,比任何文字都有说服力。

def evaluate_hit_rate(ratings, sim_df, K=30, top_n=10): """简单离线评估:按时间切分,算 Hit Rate""" ratings = ratings.sort_values('timestamp') split = int(len(ratings) * 0.8) train, test = ratings.iloc[:split], ratings.iloc[split:] hits, total = 0, 0 for uid in test['user_id'].unique(): recs = recommend_for_user(uid, train, sim_df, top_n) actual = set(test[test['user_id'] == uid]['movie_id']) hits += len(set(recs) & actual) total += len(actual) return hits / total if total else 0

逻辑说明:按时间切分比随机切分更接近真实场景,因为推荐系统是用过去预测未来。total累加的是测试集里所有用户实际看过的电影数,hits是推荐命中的数量。

参数说明:K在recommend_for_user里可以通过similar.head(K)控制,这里为了简洁没展开。top_n取 10 是常见评估口径,和答辩演示保持一致。

最后说个血泪经验:毕设代码一定要在答辩前一周完整跑一遍,从建库、导数据、算相似度到启动服务,每一步都截图存档。我见过太多人算法调通了但数据库连不上,或者换了台电脑路径全错。把requirements.txt、建表 SQL、数据导入脚本放在一个setup文件夹里,写个README说明执行顺序,这比多写两百行算法代码更能让你顺利通过。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询