
简介一套基于Python实现机器学习算法的电影推荐系统与票房预测系统完整项目适合计算机相关专业毕业设计、课程设计或期末大作业参考也面向需要实战练习的机器学习学习者。项目以TMDB与MovieLens电影数据为基础围绕电影特征可视化分析、票房回归预测、多策略个性化推荐展开融合协同过滤、内容推荐、关键词相似度及多种集成算法覆盖数据分析到模型构建的完整流程。压缩包共60个文件约30.94MB包含16个Python源码、16个CSV数据集、2个TXT说明、2个Markdown笔记、1份PDF报告以及23张可视化分析图片目录按预测、推荐、个人推荐、集成推荐等模块组织便于理解与二次开发。已有260人学习下载项目附带数据文档与实验分析报告可帮助读者快速复现实验、理解算法细节是含金量较高的实战参考资料。1. 基于 Python 的电影推荐与票房预测两个机器学习任务怎么分工很多人看到“电影推荐系统 票房预测系统”的标题第一反应是要搭两套独立的机器学习算法流程。实际拆解要清醒得多推荐系统训练的是用户与电影的交互矩阵输出的是偏好排序票房预测训练的是电影属性向量输出的是连续票房数值。两个任务的输入形态、损失函数和评估指标完全不同但共享同一套 Python 技术栈pandas 清洗数据scipy 构造稀疏矩阵sklearn 建模joblib 持久化Flask 暴露接口。真正值得学的不是两个模型本身而是同一个工程骨架里排序任务和回归任务各自怎么设计特征、选评价指标、做模型交付。下面按“数据准备 → 推荐建模 → 票房回归 → 系统整合”的路线展开每一步都给出可以直接跑的代码和参数边界。2. 数据准备与特征工程推荐与票房预测的公共底座2.1 三张核心表怎么读、怎么对齐拿到完整数据包之后先浏览文件目录。通常会有评分表、电影信息表、票房表三份主数据另外是说明文档和代码。评分表格式一般为 CSV包含 userId、movieId、rating、timestamp 四个字段电影信息表包含 movieId、title、genres票房表需要包含 movieId、budget、release_date、total_gross 等字段。用 pandas 一次读入先确认类型import pandas as pd ratings pd.read_csv(data/ratings.csv) movies pd.read_csv(data/movies.csv) boxoffice pd.read_csv(data/boxoffice.csv) print(ratings.dtypes) print(ratings.head(3)) print(boxoffice.columns.tolist())运行后先看 ratings 里的 rating 是 int 还是 floattimestamp 是 Unix 秒还是可读日期再看 boxoffice 的字段是否带有多余空格或大小写不一致。最常踩的坑是 movieId 在 ratings 里是 int64、在 boxoffice 里是 float64直接 merge 时连接键被自动升级成 float导致匹配率大幅下降。统一转成字符串就能规避for df in (ratings, movies, boxoffice): df[movieId] df[movieId].astype(str)这段代码把三个表的连接键全部统一成字符串字符串连接键不会发生隐式类型升级比靠 pandas 自动对齐更可控。三张表的用途和规模如下表表关键字段服务对象常见规模ratings.csvuserId, movieId, rating, timestamp推荐系统十万行以上movies.csvmovieId, title, genres两个系统共用数千行boxoffice.csvmovieId, budget, release_date, total_gross票房预测数百到数千行2.2 去重、缺失值处理和评分分布检查评分表最常见的脏数据是同一用户对同一部电影出现多条评分可能来自重复上报或多端登录。处理方式是先按时间排序再保留每组最后一条ratings ratings.sort_values(timestamp).drop_duplicates( subset[userId, movieId], keeplast )drop_duplicates 的 keeplast 必须配合 sort_values 才有意义时间升序排好后每组最后一行就是最新一次评分。处理完后必须再验证一次唯一性可以用ratings.duplicated(subset[userId, movieId]).sum()检查否则后续按时间切分训练集和测试集时同一条行为会同时出现在两侧造成数据泄漏。票房表的缺失值集中在 budget 和 total_gross 两列。直接 dropna 会丢掉大量训练样本常见做法是分类型取中位数填充median_by_genre boxoffice.groupby(genre_main)[total_gross].transform(median) boxoffice[total_gross] boxoffice[total_gross].fillna(median_by_genre) boxoffice[budget] boxoffice[budget].fillna(boxoffice[budget].median())先按影片主类型分组取中位数比全局中位数保留了更多上下文。但要注意如果某个类型的样本量少于 20 部分组中位数波动大稳健做法是先统计类型频次低频类型统一回退到全局中位数。最后查看评分分布这是一步容易被跳过的检查print(ratings[rating].describe(percentiles[0.25, 0.5, 0.75, 0.9]))机器学习模型对评分偏移很敏感。如果 75 分位就是满分或接近满分说明高分片占绝对主导此时用 RMSE 评价推荐效果会失真应该把排序指标 PrecisionK 作为主导评估维度。2.3 构造推荐系统的稀疏评分矩阵推荐系统需要把评分数据变成“用户 × 电影”二维矩阵行是用户编号列是电影编号值是评分。真实数据集中这个矩阵非常稀疏直接用 pandas 的 pivot_table 会生成大量填充 NaN 的稠密结构内存消耗极大。正确做法是用 scipy.sparse 构造 CSR 矩阵from scipy.sparse import csr_matrix import numpy as np user_codes ratings[userId].astype(category) movie_codes ratings[movieId].astype(category) rows user_codes.cat.codes.to_numpy() cols movie_codes.cat.codes.to_numpy() values ratings[rating].to_numpy() ratings_sparse csr_matrix((values, (rows, cols)), dtypenp.float32) print(矩阵维度:, ratings_sparse.shape) print(非零元素:, ratings_sparse.nnz) print(稠密度: {:.2f}%.format(100 * ratings_sparse.nnz / (ratings_sparse.shape[0] * ratings_sparse.shape[1])))astype(category) 会把 userId 和 movieId 映射成 0 到 N-1 的整数编号再通过 cat.codes 取出编码数组这是构造稀疏矩阵最省内存的路径。nnz 是非零元素数稠密度低于 1% 属于正常如果超过 5%基本可以判断是前面的去重没做干净回头检查重复记录。稠密度过高时协同过滤的区分度会下降因为几乎所有用户都看过几乎所有电影。2.4 票房特征工程档期、类型、主创留一均值票房预测的特征分三层电影自身属性、档期特征、主创市场号召力。核心代码boxoffice[release_date] pd.to_datetime(boxoffice[release_date]) boxoffice[year] boxoffice[release_date].dt.year boxoffice[month] boxoffice[release_date].dt.month boxoffice[is_hot_period] boxoffice[month].isin([2, 6, 7, 8, 12]).astype(int) for g in [Action, Adventure, Sci-Fi, Comedy, Drama]: boxoffice[fgenre_{g}] boxoffice[genres].fillna().str.contains(g, regexFalse).astype(int)直接放 month 作为连续特征模型会学到“6 月好、9 月差”这种不稳定规律。is_hot_period 把 2、6、7、8、12 这五个月定义为热门档期对应春节档、暑期档和贺岁档把市场节奏压缩成两个取值对回归模型更友好。主创特征的构造最容易引入数据泄漏。导演历史票房均值如果直接用包含当前影片的全量数据计算等于把答案提前给了模型。正确做法是只用上映日期早于当前影片的数据取累计均值boxoffice boxoffice.sort_values(release_date) grouped boxoffice.groupby(director)[total_gross] boxoffice[director_avg] grouped.expanding(min_periods1).mean() \ .reset_index(level0, dropTrue) boxoffice[director_avg] boxoffice[director_avg].fillna( boxoffice[total_gross].median() )expanding().mean() 生成“当前行之前所有行”的累计均值不包含当前影片自身因此不会泄漏未来信息。新导演首次出现时累计均值是 NaN用全局中位数兜底。特征构造完成后推荐系统拿到的是 ratings_sparse 稀疏矩阵和 user_codes、movie_codes 两个编码器票房预测拿到的是特征矩阵 X 和标签 ytotal_gross两条数据流到这里正式分开。3. 电影推荐系统实现从物品协同过滤到 SVD 矩阵分解3.1 协同过滤为什么是默认起点这个阶段可用的数据里最丰富的是用户行为而不是电影内容因此基于内容的推荐不是首选。协同过滤的核心假设是“相似的人或相似的电影会带来相似偏好”它不需要内容标注直接吃评分矩阵就能产生推荐结果。在机器学习算法选型里它不是最炫的但绝对是行为数据下的默认起点。几种方案的对比方法输入优点短板基于内容的推荐电影元数据物品冷启动友好多样性差容易同质化基于用户的协同过滤评分矩阵直觉清晰实现简单用户量大时实时计算压力大基于物品的协同过滤评分矩阵相似度可离线预计算长尾物品推荐效果偏弱矩阵分解 SVD评分矩阵隐因子表达能力强可解释性弱仍无法解决冷启动在中小规模数据集万级用户、千级电影上先实现基于物品的协同过滤再加矩阵分解对比精度是最稳的推进路径。3.2 物品相似度矩阵的构建与内存边界计算物品与物品之间的余弦相似度一行代码就能完成但参数的取舍会影响内存from sklearn.metrics.pairwise import cosine_similarity import numpy as np item_sim cosine_similarity(ratings_sparse.T, dense_outputTrue) np.fill_diagonal(item_sim, 0) print(item_sim.shape, item_sim.dtype)参数说明ratings_sparse 的行是用户、列是电影转置后得到“电影 × 用户”矩阵每一行可以看作一部电影的评分向量。dense_outputTrue 会生成稠密矩阵电影数量超过 1 万部时10000×10000 的 float64 矩阵约 800MB内存直接吃紧。此时把参数改成 dense_outputFalse 得到稀疏矩阵只保留相似度非零的项推荐阶段的计算速度会更快。fill_diagonal 把自身相似度置零否则在后续加权累加时用户已看过的电影会按照自身评分被重复加权。3.3 基于物品相似度的 TopN 推荐生成推荐生成的逻辑是对每个用户用其所有已评分电影的分值乘以相似度矩阵加权累加得到对未看过的电影的预测分def recommend_item_based(user_idx, ratings_sparse, item_sim, top_n10): user_vec ratings_sparse.getrow(user_idx).toarray().ravel() rated_mask user_vec 0 scores user_vec item_sim scores[rated_mask] -np.inf top_indices np.argsort(scores)[::-1][:top_n] return top_indicesscores 的语义是对每个候选电影累加当前用户所有已评分电影与该候选电影的相似度并按用户评分加权。用户打过分且与候选电影相似度高的电影会推高该候选的分数。rated_mask 把已看过的电影全部屏蔽避免推荐列表里出现用户已经消费过的内容这在离线评估时也能杜绝“命中已看影片”的假阳性。参数 top_n 控制推荐列表长度离线评估时一般取 10 或 20线上取 5 到 10 更符合展示位设计。3.4 SVD 矩阵分解隐因子与预测矩阵物品相似度矩阵只能捕捉二阶关联无法挖掘更深层的用户偏好结构。矩阵分解把“用户 × 电影”评分矩阵分解成用户隐因子矩阵和电影隐因子矩阵两者的内积就是预测评分from sklearn.decomposition import TruncatedSVD n_factors 50 svd TruncatedSVD(n_componentsn_factors, random_state42) user_factors svd.fit_transform(ratings_sparse) item_factors svd.components_.T pred_ratings user_factors item_factors.T print(user_factors:, user_factors.shape) print(item_factors:, item_factors.shape) print(pred_ratings:, pred_ratings.shape)TruncatedSVD 直接接受稀疏矩阵输入不需要先 toarray()这是它在这个场景下比 PCA 更合适的原因。n_components50 是通用起步值在数万用户、数千电影的数据规模上20 到 100 之间需要验证集上的表现来决定。隐因子太少会欠拟合太多会把噪声也学进来。调试时从 50 开始每次加 20观察 RMSE 不再下降就停下来。3.5 离线评估RMSE 与 PrecisionK 都要看离线评估先切分训练集和测试集。推荐系统不能用随机抽样因为随机抽样会把同一用户对同一部电影的后续评分也放进训练集造成时间泄漏。常见做法是按时间切分split_time ratings[timestamp].quantile(0.8) train_df ratings[ratings[timestamp] split_time] test_df ratings[ratings[timestamp] split_time] test_user_idx test_df[userId].map(user_codes.cat.categories.get_loc).values test_item_idx test_df[movieId].map(movie_codes.cat.categories.get_loc).values preds pred_ratings[test_user_idx, test_item_idx] rmse np.sqrt(np.mean((preds - test_df[rating].values) ** 2)) print(RMSE:, rmse)用 pred_ratings 取出测试集中每个评分对应的预测值计算 RMSE这是评分预测的全局指标。但 RMSE 只能反映“预测分和真实分差多少”不能反映推荐列表的命中质量。评分偏斜明显时两个模型的 RMSE 可能接近但推荐排序效果差异很大所以还要看排序指标def precision_at_k(pred_scores, test_items, k10): top_k np.argsort(pred_scores)[::-1][:k] hits len(set(top_k) set(test_items)) return hits / k参数说明pred_scores 是用户对所有电影的预测评分数组test_items 是测试集中该用户实际交互过的电影下标列表。PrecisionK 计算前 K 个推荐位中有多少比例命中了用户真实交互过的物品。K 取 5 时更严格取 20 时更宽松。评估时对测试集中所有用户取平均同时报告 RMSE 和 PrecisionK才是完整的离线结论。4. 票房预测系统实现回归建模、对数变换与关键参数4.1 回归还是分类票房预测的建模方式选择票房预测的本质是回归问题目标是连续的票房金额。把票房分档做成机器学习分类器也能跑通比如分成“票房过亿”和“票房不过亿”的二分类但产出粒度太粗业务上想知道的是“预计能收多少钱”而不是“能不能过亿”。回归模型直接输出连续金额参考价值更高。回归里有个经典处理前提票房分布极度偏态头部大片和尾部小片相差三个数量级。直接拿原始票房做回归模型会把重心放在高频的小额影片上忽略头部大片的拉动效应。所以先把目标做对数变换再用回归模型拟合。4.2 特征矩阵与对数变换从 2.4 节构造好的 boxoffice 表里选出特征列拼成特征矩阵feature_cols [ budget, is_hot_period, year, genre_Action, genre_Adventure, genre_Sci-Fi, genre_Comedy, genre_Drama, director_avg ] X boxoffice[feature_cols].fillna(0).values y boxoffice[total_gross].values y_log np.log1p(y)log1p 即 log(1x)避免对零值取对数时出现负无穷。模型输出的预测值在对数空间最终要用 np.expm1 还原成票房金额。这么做的本质是把原本右偏的长尾分布拉近正态回归模型拟合的是“增长模式”而不是“绝对值大小”。如果跳过这一步直接建模线性回归的系数会被几个头部大片带偏岭回归和随机森林也会在极端值上浪费大量拟合能力。4.3 三种回归模型的横评先从最简单的模型做起用同一份验证集做横向对比from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split X_train, X_val, y_log_train, y_log_val train_test_split( X, y_log, test_size0.2, random_state42 ) models { Linear: LinearRegression(), Ridge: Ridge(alpha1.0), RandomForest: RandomForestRegressor( n_estimators300, max_depth12, random_state42 ) } for name, model in models.items(): model.fit(X_train, y_log_train) val_pred model.predict(X_val) rmse mean_squared_error(y_log_val, val_pred, squaredFalse) print(f{name}: {rmse:.4f})train_test_split 固定 random_state 保证三个模型看到完全相同的训练集和验证集对比才是公平的。线性回归作为基线如果它的 RMSE 和其他模型差距不大说明特征与票房之间基本是线性关系岭回归的 alpha1.0 是保守起点特征共线性明显时可加大到 10.0随机森林 n_estimators300 已经够用再多只增加训练时间不提升精度max_depth12 控制单棵树复杂度防止过拟合。验证集上输出的 RMSE 在对数空间只有相对比较的意义。要获得业务可读的误差把预测值和真实值都还原到原始量级计算相对误差val_pred_money np.expm1(rf.predict(X_val)) val_true_money np.expm1(y_log_val) relative_error np.abs(val_pred_money - val_true_money) / np.maximum(val_true_money, 1e6) print(中位相对误差:, np.median(relative_error))分母加 1e6 是为了防止真实票房接近零时除法爆炸。中位相对误差比均值更稳健因为少数极端影片会把均值拉高。4.4 XGBoost 参数的具体含义和设置习惯XGBoost 是表格数据回归里最常用的机器学习算法它本身就是梯度提升树的一种工程实现。针对中小规模数据集XGBoost 不需要跑大规模网格搜索把下面五个参数定好就能收敛得很好import xgboost as xgb dtrain xgb.DMatrix(X_train, labely_log_train) dval xgb.DMatrix(X_val, labely_log_val) params { objective: reg:squarederror, eta: 0.05, max_depth: 6, min_child_weight: 5, subsample: 0.8, colsample_bytree: 0.8, eval_metric: rmse } bst xgb.train(params, dtrain, num_boost_round1000, evals[(dval, val)], early_stopping_rounds30, verbose_eval50)XGBoost 在梯度提升框架下逐轮拟合残差参数的直接影响是模型复杂度与鲁棒性之间的平衡具体设置如下参数含义典型范围这里取值的理由eta学习率0.01~0.10.05 控制每轮步长降低过拟合风险max_depth单棵树最大深度3~106 在特征数不多的场景下足够min_child_weight叶子节点最小样本权重和1~105 防止叶子节点分裂过细subsample行抽样比例0.5~1.00.8 让每棵树看到不同样本增强泛化colsample_bytree列抽样比例0.5~1.00.8 降低特征间相关性的影响early_stopping_rounds30 表示连续 30 轮验证集 RMSE 不再下降就提前终止这样不需要手动猜测 num_boost_round 的合适值。产生最优模型的迭代轮数会留在 bst.best_iteration 里线上使用时注意截断到这个轮数而不是使用训练停止时的完整模型。4.5 误差分布分析看模型在哪个票房区间失效模型训完不能只看一个 RMSE 总分。按真实票房分桶统计每个区间内的中位相对误差能直接定位模型的失效区间df_eval pd.DataFrame({ true_money: val_true_money, pred_money: val_pred_money }) df_eval[bucket] pd.cut( df_eval[true_money], bins[0, 1e7, 5e7, 1e8, 1e9, np.inf] ) def median_rel_error(group): return np.median(np.abs(group[pred_money] - group[true_money]) / group[true_money]) print(df_eval.groupby(bucket, observedTrue).apply(median_rel_error))如果小额票房段误差率显著高于大额段说明模型对小成本电影的学习不足特征里缺少表现宣发强度或排片规模的变量如果大额段误差高则可能头部影片样本量太少XGBoost 拟合不了这种极端分布。误差分析的结论直接决定下一步是补特征、调参数还是扩充数据而不是盲目继续调模型。5. 两个系统整合落地joblib 持久化、统一接口与冷启动降级5.1 用 joblib 持久化推荐模型和票房模型模型训练完不能只留在内存里项目交付时源码、模型文件和数据要能独立搬运。joblib 对 numpy/scipy 对象的序列化效率比 pickle 高推荐系统的稀疏矩阵和 TruncatedSVD 模型都用 joblib 保存import joblib joblib.dump(svd, models/svd_model.joblib) joblib.dump(bst, models/xgb_gross.joblib) joblib.dump(user_codes.cat.categories, models/user_cat.joblib) joblib.dump(movie_codes.cat.categories, models/movie_cat.joblib) svd joblib.load(models/svd_model.joblib) bst joblib.load(models/xgb_gross.joblib)dump 和 load 配对使用加载后直接调用 predict 或 transform。注意两点保存的必须是 fit 之后的对象不是初始化后的空模型如果部署环境里的 sklearn 版本和训练环境不一致joblib 文件可能加载失败交付源码时把 requirements.txt 一并给到是最稳的做法。5.2 一个 Flask 接口同时暴露推荐和票房预测两个系统打包成同一个 Web 服务两个路由就能解决from flask import Flask, request, jsonify import numpy as np import xgboost as xgb app Flask(__name__) app.route(/api/recommend/int:user_idx, methods[GET]) def recommend(user_idx): top_indices recommend_item_based(user_idx, ratings_sparse, item_sim, top_n10) return jsonify({movie_ids: [int(i) for i in top_indices]}) app.route(/api/predict/int:movie_idx, methods[GET]) def predict(movie_idx): features fetch_features(movie_idx) pred_log bst.predict(xgb.DMatrix([features]))[0] return jsonify({predicted_gross: float(np.expm1(pred_log))}) if __name__ __main__: app.run(host0.0.0.0, port8000)推荐路由返回电影编号列表预测路由返回还原到原始量级的预估票房。xgb.DMatrix 的输入必须是二维所以特征向量要包一层列表 [features]。fetch_features 从 boxoffice 表按 movie_idx 取特征列的顺序必须与训练时 feature_cols 完全一致这个函数是线上最容易出错的环节训练代码和接口代码要共用同一个特征构造模块。5.3 冷启动降级与上线前验证清单冷启动是推荐系统的固有缺口新用户没有评分行为协同过滤无法生成个性化结果新电影没有评分相似度矩阵对它的建边几乎为空。常见降级方案是直接返回全站热门电影代码只需把 item_sim 换成一个预先算好的全局评分均值向量保证接口不会返回空列表。更细一点的做法是按类型做热门榜让冷启动推荐保留一点内容偏好。上线前过一遍验证清单第一特征构造流程与训练时一致尤其是导演累计均值这类时序特征线上实现稍有偏差预测结果就会系统性偏移第二model 文件能成功加载推荐接口的相似度矩阵不为空票房接口的 predict 结果在合理量级第三拿 20 部历史电影对拍真实票房和预测票房落在同一量级第四推荐接口对相似度矩阵只做只读计算不会有并发写问题。把数据清洗和特征构造的每一步都做成独立可复用的函数推荐和票房预测各写各的模型但底层共用同一个数据抽取模块这一条做到位后面迭代数据或增量更新时能省掉一大半排查时间。本文还有配套的精品资源点击获取