简介:这份基于Python的电影数据可视化及票房影响因素分析与预测项目,主要面向计算机相关专业正在准备毕业设计的学生,以及需要项目实战练习的学习者,也可直接用于课程设计或期末大作业。项目包含全部源码,经过严格调试,下载即可运行。压缩包共包含38个文件,以Python脚本(6个.py)、Jupyter Notebook分析文档(3个.ipynb)、可视化结果图(24个.png)及SQL数据库脚本(douban.sql)为核心,另附README说明文档和结构预览图,整体大小约5.18MB。脚本覆盖电影基本信息获取、数据清洗入库、可视化展示与票房预测等环节,Notebook中则提供分步探索与建模过程。目前已有425人学习浏览,适合希望快速搭建完整数据分析和预测流程的开发者,可作为毕业设计直接引用。项目文档结构清晰,尤其适合在电影票房场景中学习Python数据可视化与回归预测技术。
1. 把电影票房影响因素分析做成毕设:这份 Python 源码包能直接跑通什么
如果你正在为毕业设计或期末大作业找一套能落地、能演示、还能写进论文里的完整项目,那份标题带“电影数据可视化及票房影响因素分析与预测”的 Python 源码包是个值得花时间拆开看的东西。它本质上是把豆瓣电影数据从 SQL 文件导入数据库,再用 Pandas 和 SQL 两条可视化链路做图表展示,接着对评分、类型、片长这些维度做票房影响因素分析,最后在 predict.ipynb 里跑票房预测。整套流程覆盖了“数据入库 → 可视化 → 分析 → 预测”,并且自带 README 文档和结果图,适合计算机相关专业做毕设,也适合想练手完整数据项目的学习者。不需要你从零搭环境,项目里 main.py、database.py、各 ipynb 文件的分工很清晰,跑通之后替换成自己的数据也能复用。
2. 先从数据说起:douban.sql 的表结构与入库前的清洗逻辑
2.1 库表设计:电影基本信息与票房流水怎么分开存
拆这个项目第一步是打开 douban.sql 看表结构。按我的经验,这类豆瓣电影数据一般不会只有一张表,常见设计是拆成几张职责不同的表:电影基本信息表存“片名、导演、主演、类型、片长、评分、评分人数”,电影详情表存“上映日期、制片地区、语言、剧情简介”,票房表单独存“上映日期、当日票房、累计票房、排片场次”。把票房单独拆出来是因为它有明显的时间序列属性,按天记录,分析的时候要和基本信息 JOIN,但存储上分开更合理。
CREATE TABLE IF NOT EXISTS movie_basic ( movie_id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, directors VARCHAR(255), actors VARCHAR(512), genres VARCHAR(255), duration INT, rating DECIMAL(3,1), rating_people INT ); CREATE TABLE IF NOT EXISTS box_office ( id INT PRIMARY KEY AUTO_INCREMENT, movie_id INT NOT NULL, date DATE NOT NULL, daily_box DECIMAL(12,2), cumulative_box DECIMAL(12,2), FOREIGN KEY (movie_id) REFERENCES movie_basic(movie_id) );这里要注意 genres 字段用的是 VARCHAR。豆瓣电影的类型经常是“剧情 / 喜剧 / 爱情”这种多值结构,用字符串存方便直接展示,但后面的分析如果要按类型统计,就得拆开处理。团队当时选择这种设计是最省事的,数据分析时再在 ipynb 里做字符串切割,而不是入库前拆成关联表,能减少很多逻辑。
2.2 database.py 的职责:连接、建表、幂等初始化
database.py 在这个项目里承担的不只是“连上数据库”这一件事。它会读取数据库配置,建立连接池或单次连接,然后执行 douban.sql 里的建表和初始化语句,确保每次启动项目时表结构都是完整的。这个文件最值得学习的地方是“幂等”处理——重复运行不会因为表已存在而报错。
import pymysql import sqlparse def init_database(config, sql_path="douban.sql"): conn = pymysql.connect( host=config["host"], user=config["user"], password=config["password"], charset="utf8mb4", autocommit=True ) cursor = conn.cursor() with open(sql_path, "r", encoding="utf-8") as f: sql_text = f.read() for statement in sqlparse.split(sql_text): statement = statement.strip() if not statement: continue cursor.execute(statement) cursor.close() conn.close() print("database initialized")这个实现里 pymysql.connect 用 utf8mb4 字符集很关键,豆瓣数据里有大量中文,某些描述里还有冷门字符,utf8 不够用,utf8mb4 才能避免导入时报 Illegal mix of collations 的错误。sqlparse.split 按分号拆分 SQL 语句逐条执行,比一次执行整个文件可控,哪条语句报错能精确定位。参数 autocommit=True 省去了手动提交的步骤,对毕设项目足够了。
2.3 入库前的清洗:空值、类型转换、日期归一化
豆瓣电影的原始数据质量参差不齐,清洗是这套流程里最费时间的一环。常见问题集中在三个地方:片长字段有的存“128分钟”有的存“128”,需要正则提取数字;评分字段部分缺失,需要决定是填充还是删除;上映日期格式不统一,有“2019-02-05”也有“2019年2月5日”,需要统一成 datetime 类型。
import pandas as pd import re def clean_movie_df(df): df = df.copy() # 片长: 提取数字, 无法解析的置为 NaN df["duration"] = df["duration"].apply( lambda x: int(re.search(r"\d+", str(x)).group()) if re.search(r"\d+", str(x)) else None ) # 上映日期: 统一成 datetime df["release_date"] = pd.to_datetime( df["release_date"].str.replace("年", "-").str.replace("月", "-").str.replace("日", ""), errors="coerce" ) # 评分空值: 用平均值填充, 并在表里做记录 rating_mean = df["rating"].mean() df["rating"] = df["rating"].fillna(rating_mean) return df这里把 release_date 先做字符串替换再 to_datetime,能兼容“2019-02-05”和“2019年2月5日”两种格式。errors="coerce" 会把解析失败的值转成 NaT,后续统一 dropna 处理。评分用均值填充是相对稳妥的策略,比直接删除整行保留更多样本。注意 project 里 movie_basic.py 和 movie_detail.py 都可能重复做清洗,很多同学会忽略幂等性,导致跑两次数据翻倍,这属于典型的坑,后面避坑章节会细说。
3. 可视化双链路:Pandas 直出图与 SQL 查询结果再绘图的取舍
3.1 visualization_pandas.ipynb:Pandas 直接画图的参数细节
这个 Notebook 的思路是把数据库表读成 DataFrame,然后直接调用 matplotlib 和 Pandas 的绘图接口出图。优点是链路短、调试方便,适合探索式分析,先在 Pandas 里做筛选、聚合,立刻看图调整参数。
import pandas as pd import matplotlib.pyplot as plt from database import load_dataframe df_basic = load_dataframe("movie_basic") df_box = load_dataframe("box_office") # 每部电影的总票房 df_total = df_box.groupby("movie_id")["daily_box"].sum().reset_index() df_merged = pd.merge(df_basic, df_total, on="movie_id") plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False # 评分与票房的关系散点图 fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(df_merged["rating"], df_merged["daily_box"], alpha=0.6) ax.set_xlabel("豆瓣评分") ax.set_ylabel("累计票房(元)") ax.set_title("电影评分与票房关系") plt.tight_layout() plt.show()这段代码有两个细节值得注意。第一是 plt.rcParams["font.sans-serif"]=["SimHei"],Windows 环境用黑体基本不会乱码,但 macOS 或 Linux 服务器上没有 SimHei 要改成系统中文字体名,或者用 rc 参数里缺省的回退方案。第二是先把 box_office 按 movie_id 聚合再 merge,如果在 merge 之后再 groupby 会有大量重复累计,逻辑上也没这么清晰。alpha=0.6 是处理散点重叠的常用手段,样本量几百时效果明显。
3.2 visualization_sql.ipynb:SQL 聚合之后再可视化的好处
另一条链路是直接在 SQL 里完成聚合统计,再把结果集传给绘图函数。这个思路的吸引力在于数据库擅长处理大表,把 join、group by、聚合函数压给 MySQL,Python 端只负责拿到已经压缩过的结果。
SELECT g.genre, COUNT(DISTINCT b.movie_id) AS movie_count, ROUND(AVG(b.rating), 2) AS avg_rating, ROUND(SUM(o.daily_box), 2) AS total_box FROM movie_basic b JOIN box_office o ON b.movie_id = o.movie_id CROSS APPLY ( SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(b.genres, '/', n.n), '/', -1) AS genre FROM (SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) n ) g GROUP BY g.genre ORDER BY total_box DESC;不过 MySQL 5.x 不支持 CROSS APPLY,这里用 SUBSTRING_INDEX 配合序号表模拟行转列,是兼容老版本 MySQL 的常见写法。实际 project 里的思路是把 genre 拆分的 SQL 笔记放在 visualization_sql.ipynb 里,读者可以直接跑。对比来看,Pandas 路线适合快速出图、即时验证想法;SQL 路线适合数据量大、需要多方 JOIN 的场景,跑出来的 SQL 也能直接贴在论文附录里,作为数据处理能力佐证。
3.3 图表选型:票房分布、评分关系、类型占比分别用什么图
这块在项目的结果图里体现得比较明显。p (1).png 到 p (19).png 这些图,内容大概是票房分布直方图、评分-票房散点图、类型占比饼图、年度票房趋势折线图等。
| 分析目标 | 推荐图表 | 对应特征字段 | 注意事项 |
|---|---|---|---|
| 票房整体分布 | 直方图 + KDE | daily_box | 长尾严重时先取对数 |
| 评分与票房关系 | 散点图 + 回归线 | rating vs daily_box | 样本量少时效果不稳定 |
| 类型票房占比 | 横向条形图 | genres | 类型字段先拆分 |
| 上映年份趋势 | 折线图 | release_date | 按年聚合后再画 |
这里要给个具体建议:票房字段的数值跨度极大,从几百万到几十亿,直方图画出来通常是左边一柱擎天、右边什么都看不见。常见做法是先对票房取 log1p 再画分布,或者直接画箱线图分位数展示。预测模型里也会遇到同样的偏态问题,后面专门讲。
4. 票房影响因素分析:从单变量到多变量,怎么得出能写进论文的结论
4.1 unit.py 和 attachfile.py 在分析链路里干什么
unit.py 在项目里承担公共函数的角色,比如数据库连接复用、字段切割、统计指标计算,各 ipynb 和 py 文件都 import 它。attachfile.py 的业务集中在为分析补数据,比如把某部电影的历史票房序列、续集关系、档期信息拼接到主 DataFrame 上。
看这个项目要理解一个点:票房预测和分析的项目,文件职责边界往往比较模糊。movie_basic.py 和 movie_detail.py 可能都有“读取表→清洗→输出 DataFrame”的逻辑,unit.py 里也有类似的 load_dataframe 函数。这不算设计缺陷,毕设项目里这种程度的重用完全正常。你使用的时候,只要确保每个 Notebook 开头都调用了同一个 unit.py 里的函数,避免各自实现一套读取逻辑就行。
4.2 因素拆解:评分、类型、时长、上映档期与票房的相关性
分析部分是论文的核心。常规路径是:先看单因素相关性,再做分组对比,最后给出结论。评分与票房是最常被验证的关系,项目里用 scipy 的 pearsonr 计算相关系数和 p 值。类型与票房的分析要看均值差异是否显著。时长与票房则要注意非线性——过长或过短的电影票房都不高。
from scipy.stats import pearsonr, ttest_ind df_cleaned = clean_movie_df(load_dataframe("movie_basic")) # 评分与票房相关性 corr, p_value = pearsonr(df_cleaned["rating"], df_cleaned["total_box"]) print(f"评分与票房相关系数 {corr:.3f}, p={p_value:.4g}") # 类型对比: 喜剧 vs 非喜剧票房是否显著差异 comedy_mask = df_cleaned["genres"].str.contains("喜剧", na=False) comedy_box = df_cleaned.loc[comedy_mask, "total_box"] other_box = df_cleaned.loc[~comedy_mask, "total_box"] stat, p_ttest = ttest_ind(comedy_box, other_box, equal_var=False) print(f"喜剧与非喜剧票房 t={stat:.3f}, p={p_ttest:.4g}")pearsonr 返回两个值,相关系数只对线性关系敏感,p 值小只能说明“不是零相关”,不代表相关性强。这里可以补一个 spearman 相关系数作为稳健性检验。ttest_ind 要设置 equal_var=False,因为两组票房的方差显然不齐;如果不设,Welch 修正不会生效,结果参考价值打折扣。
4.3 main.py 的组织方式:一键跑完整条分析
main.py 是入口脚本,它把数据初始化和核心分析串起来。理想状态下运行 python main.py 就能完成“建库 → 导入 → 清洗 → 输出可视化图表 → 生成分析报告”,方便答辩演示环境一键跑通。
from database import init_database from movie_basic import load_basic_data from movie_detail import load_detail_data from unit import ( compute_correlation, compute_genre_stats, export_charts ) def main(): config = load_config("config.json") init_database(config) df_basic = load_basic_data() df_detail = load_detail_data() df_merged = merge_data(df_basic, df_detail) corr_summary = compute_correlation(df_merged) genre_stats = compute_genre_stats(df_merged) export_charts(df_merged, output_dir="result") save_summary_report(corr_summary, genre_stats) if __name__ == "__main__": main()main.py 给读者的价值是理解项目的执行入口在哪里,而不是逐行读每个函数。建议你拿到源码后先把 config.json 里的数据库配置改成本地密码,然后直接跑 main.py 看输出是否和 result 目录下的 p*.png、预测*.png 一致,一致就说明代码跑通了。
5. 避坑与常见问题:跑通这个源码包的几条血泪经验
5.1 现象:图表里的中文全部变成方块或乱码
图表里中文变成像“□□”这样的方块,是 Python 可视化的经典翻车现场。原因是在 Linux 或部分 macOS 环境下没有 Windows 的 SimHei 字体,matplotlib 找不到中文字体就回退到默认字体。解决方案是先检查系统有哪些中文字体,再把字体名写进 plt.rcParams。
import matplotlib.font_manager as fm font_list = [f.name for f in fm.fontManager.ttflist if 'Hei' in f.name or 'WenQuanYi' in f.name] print(set(font_list))跑完这段能看到系统可用字体列表,比如 WenQuanYi Zen Hei、Noto Sans CJK SC。然后设置 plt.rcParams["font.sans-serif"]=[所选字体],再把 axes.unicode_minus 配成 False 处理负号显示。建议把字体配置统一写到 unit.py 顶部,所有 Notebook 引用同一个配置。
5.2 现象:导入 douban.sql 报错 1071 "Specified key was too long"
MySQL 报字段索引太长,本质是 utf8mb4 下 VARCHAR 字段做索引时,字符数乘以 4 字节超出了索引长度限制。原因不是项目代码的问题,而是本地 MySQL 版本或建表时没有指定合适的索引长度。解决方式是改 SQL 里的索引定义,只对前 N 个字符建索引:
ALTER TABLE movie_basic ADD INDEX idx_title (title(64));如果原始 SQL 里写的是 KEY idx_title (title),就手动改成 title(64)。另外注意 douban.sql 文件本身是 utf8 编码,导入前确认 MySQL 的 character_set_server 是 utf8mb4,否则中文数据写入后又可能变成问号,这属于另一层编码错位。
5.3 现象:票房字段是“1.2亿”“3200万”这种字符串,没法做计算
豆瓣爬下来的数据经常把单位写在值里,直接 astype(float) 会报错。原因是入库前没有做格式归一,或者清洗函数只处理了基本信息表、忘了处理票房表。解决方式就是写一个解析函数:
def parse_box_office(value): if isinstance(value, (int, float)): return float(value) value = str(value).strip() multiplier = 1 if value.endswith("亿"): multiplier = 1e8 value = value[:-1] elif value.endswith("万"): multiplier = 1e4 value = value[:-1] return float(value) * multiplier这种函数放进 unit.py 统一调用。注意“亿美元”这种别忽略外币单位,如果有海外电影就要判断货币类型。影视数据里单位乱是常态,清洗时要考虑到亿、万、千万混用的情况。
5.4 现象:运行 predict.ipynb 时预测结果几乎恒等于一个常数
模型输出一片平缓的线,或者所有样本的预测值都差不多,这是回归模型在你数据集上“躺平”了。常见原因有两个:一是特征里掺杂了未来信息,比如把“电影上映后的实际评分人数”拿来预测还未上映电影的票房,这类数据泄漏会让模型学到虚假规律;二是特征尺度差异太大,票房是百万级别、评分是 0-10 级别,线性模型会忽略小数值特征。
from sklearn.preprocessing import StandardScaler X = df[["rating", "duration", "release_year", "genre_encoded"]] scaler = StandardScaler() X_scaled = scaler.fit_transform(X)StandardScaler 标准化之后数值尺度统一,线性回归的系数才能真正反映特征重要性。如果你发现标准化后预测结果依然像常数,查一下训练集里是不是大量样本票房缺失被 fillna(0) 填充了,零值太多会让模型把多数样本预测到均值附近。把票房取 log1p 变换再做回归,能明显改善。
5.5 现象:Matplotlib 图 x 轴标签挤成一团
特别是年份图或者片名图,几十个标签全叠在一起。原因是没做两个事:一是 figsize 设得太小,二是没有旋转标签。常见做法是把 figsize 拉长到 12-16,再用 rotation=45 让标签斜着排,或者按一定步长抽样显示标签。
ax.tick_params(axis="x", rotation=45, labelsize=8) ax.xaxis.set_major_locator(plt.MaxNLocator(10))MaxNLocator(10) 强制最多显示 10 个刻度,自动跳过中间值。这两行加进去能解决绝大多数“标签叠成黑块”的问题。如果还要更细致的控制,用 MultipleLocator 手动指定步长。
6. 进阶:票房预测模型的验证技巧与参数调优
predict.ipynb 是项目里技术含量最高的部分,它不只是训练一个模型拿 R² 交差,还要能说清楚预测结果为什么可信、误差集中在哪里。我拆过不少类似的毕设项目,这部分最容易糊弄,也最值得做扎实。按照项目里 result 目录的预测1.png 和预测2.png 来看,它做了预测曲线对比和残差分布图,方向是对的,但从验证严谨性角度还差几步。
第一步是数据划分方式。票房数据天然有时间先后顺序,K-Fold 随机划分在这里是错误的,因为会让模型用 2018 年的数据训练、预测 2016 年的样本,时间泄漏直接影响泛化能力。正确做法是用按时间划分的 train_test_split,以前 80% 的时间段做训练,后 20% 做验证。写进 Notebook 里的代码应该这样:
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_index, test_index in tscv.split(X_scaled): X_train, X_test = X_scaled[train_index], X_scaled[test_index] y_train, y_test = y_log[train_index], y_log[test_index]TimeSeriesSplit 是 sklearn 自带的时序交叉验证,它保证每一次的训练集都严格在测试集之前。用这个替换默认 KFold 之后,R² 往往会掉一些,这是好事,说明之前的结果被数据泄漏抬高了。
第二步是特征工程。项目里现有的特征可能只有评分、类型、片长这几个基础维度。可以试试组合特征:导演历史票房均值、主演热度指数、上映月份编码、续集标记。这些特征的构造逻辑就是“用历史信息预测未来”,只能在训练集上计算,不能让测试集的数据混进来。
# 导演历史票房均值: 按导演分组在训练集内计算 train_df["director_avg_box"] = train_df.groupby("directors")["total_box"].transform("mean") test_df["director_avg_box"] = test_df["directors"].map( train_df.groupby("directors")["total_box"].mean() )transform("mean") 保留原始行数,map 的方式能避免测试集信息泄露到训练集。很多项目在这里直接用全局 mean,导致测试集数据间接参与了训练特征的构建。
第三步是模型参数。项目如果用的是线性回归,结果一般够用但天花板很低。换成随机森林或 LightGBM 之后,需要关注的参数是 n_estimators 和 max_depth。小数据集(几百部电影)用 LightGBM 容易过拟合,随机森林配 max_depth=10 左右反而更稳。预测结果出来后,重点看残差图:如果残差随真实票房增大而发散,说明模型在头部大片上系统性低估,这就是常说的长尾效应。
从那以后我每接一个票房预测相关的项目,都会强制先把“特征是否只由历史信息构成”这一条列成检查清单,过完清单再谈模型选型。这短短一条习惯帮我避开了无数个“训练时 R² 0.9、上线后直接翻车”的局面。预测模型这东西,评分是表面的体面,验证流程才是真正的底子,希望帮到你。
本文还有配套的精品资源,点击获取