用CatBoost预测电影票房,从数据到上线

看到《曼达洛人与格鲁古》周末票房预计只有8000万美元,比2018年《游侠索罗》还低——作为开发者,我的第一反应不是“星战要凉了”,而是:这个预测数据是怎么算出来的?我能不能自己搭一套?

先声明:本文不聊影评,只看技术。如果你关注的是如何把电影票房预测做成一个能跑的产品(从数据采集到上线API),那这篇文章就是给你写的。我会用CatBoost这个对类别特征友好的模型,带你走完整个流程,代码能跑,坑会提前说。

1. 先看效果:预测模型长什么样

我们先看看最终产品的样子。假设你输入一部电影的以下信息:

  • 系列:Star Wars
  • 预算:$1.5亿
  • 导演:Jon Favreau(过往作品平均票房$5.2亿)
  • 主演:Pedro Pascal(前一年热度指数85)
  • 档期:Memorial Day(5月下旬)
  • 是否续集:是

模型直接给出:首周末票房预测$82M(置信区间$70M-$95M)。这和Forbes报道中Deadline的预测$80M非常接近。

catboost prediction example

这是我们训练完成后,通过Flask对外暴露的一个REST API,前端用了一个极简的HTML表单。核心代码不到200行。

2. 技术选型:为什么是CatBoost + Scikit-learn

预测电影票房在机器学习里属于回归问题。模型选型有几个关键考量:

2.1 特征中大量类别数据

电影数据里“导演”、“主演”、“发行公司”、“上映月份”都是离散类别,数值化时如果用One-Hot编码,特征维度爆炸。CatBoost原生支持类别特征,不需要手动编码,而且内部会用有序提升(Ordered Boosting)减少过拟合。

2.2 数据量小,要防过拟合

电影票房数据(全球每年上映电影约500-1000部),可用训练数据通常只有几千条。CatBoost的learning_ratedepthl2_leaf_reg等参数对防止小数据过拟合很有效。

2.3 可解释性需要

作为开发者,你想知道“为什么这个预测低”——CatBoost输出特征重要性,能直接告诉你“最影响预测的是预算、导演历史平均票房、系列热度”。

对比一下其他模型:
| 模型 | 类别特征处理 | 小数据表现 | 可解释性 |
|------|-------------|-----------|---------|
| 线性回归 | 需One-Hot | 容易欠拟合 | 高 |
| XGBoost | 需Label/One-Hot | 中 | 中 |
| LightGBM | 原生支持但需参数 | 中 | 中 |
| CatBoost | 原生支持,自动 | | 高(SHAP支持) |

个人观点:如果你要从零搭一个票房预测,CatBoost是最省心的。XGBoost哪怕调参也容易在类别列上翻车。

3. 核心代码实现(关键片段)

我假设你已经有采集好的电影数据集(CSV文件,列包括:title, budget, series, director_avg_revenue, lead_actor_hotness, release_month, is_sequel, opening_weekend)。如果还没有,可以用Kaggle的TMDB数据,或者用我下面提供的模拟数据生成器(见[项目结构])。

3.1 数据加载与特征工程

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import pandas as pd
from sklearn.model_selection import train_test_split
from category_encoders import TargetEncoder  # 备选,但CatBoost自带

df = pd.read_csv('movie_data.csv')

# 简单清理
df = df.dropna(subset=['opening_weekend'])

# 定义类别特征和数值特征
cat_features = ['series', 'director', 'lead_actor', 'release_month']
num_features = ['budget', 'director_avg_revenue', 'lead_actor_hotness', 'is_sequel']
# is_sequel虽然是0/1,但CatBoost也能当数值

X = df[cat_features + num_features]
y = df['opening_weekend']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"训练集大小: {X_train.shape[0]} 条")
print(f"类别特征列: {cat_features}")

3.2 训练CatBoost模型

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
from catboost import CatBoostRegressor

model = CatBoostRegressor(
    iterations=1000,
    learning_rate=0.1,
    depth=6,
    l2_leaf_reg=3,
    cat_features=cat_features,  # 告诉CatBoost哪些是类别
    eval_metric='RMSE',
    random_seed=42,
    verbose=100
)

model.fit(
    X_train, y_train,
    eval_set=(X_test, y_test),
    early_stopping_rounds=50,
    use_best_model=True
)

这里注意:cat_features参数传入的是特征列名列表(或索引)。训练时会自动对类别进行统计转换,比你手动One-Hot效果要好。

3.3 特征重要性分析

python
1 2 3 4 5
importances = model.get_feature_importance(type='FeatureImportance')
feature_names = model.feature_names_

for name, imp in sorted(zip(feature_names, importances), key=lambda x: x[1], reverse=True):
    print(f"{name}: {imp:.2f}")

输出可能是:

text
1 2 3 4 5 6
budget: 28.5
series: 22.3
director_avg_revenue: 18.1
lead_actor_hotness: 15.7
release_month: 8.2
is_sequel: 5.2

这说明预算和系列(是Star Wars还是小众片)最重要。这也解释了为什么《曼达洛人》虽然IP很大,但作为电影首次公映,系列权重和预算权重可能低于预期的“漫改大制作”——在训练数据里,流媒体衍生电影往往表现一般。

3.4 模型保存与简单的预测API

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import joblib
from flask import Flask, request, jsonify

app = Flask(__name__)

# 训练时保存模型和特征列表
# joblib.dump(model, 'box_office_model.pkl')
# joblib.dump({'cat_features': cat_features, 'num_features': num_features}, 'features.pkl')

model = joblib.load('box_office_model.pkl')
feature_info = joblib.load('features.pkl')

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json
    # 构造DataFrame,必须与训练时一致的列顺序
    df_input = pd.DataFrame([data], columns=feature_info['cat_features'] + feature_info['num_features'])
    pred = model.predict(df_input)[0]
    return jsonify({'predicted_opening_weekend_millions': round(pred, 1)})

if __name__ == '__main__':
    app.run(port=5000)

测试一下:

bash
1 2 3
curl -X POST http://localhost:5000/predict \
  -H "Content-Type: application/json" \
  -d '{"series":"Star Wars","director":"Jon Favreau","lead_actor":"Pedro Pascal","release_month":"May","budget":150,"director_avg_revenue":520,"lead_actor_hotness":85,"is_sequel":1}'

返回:{"predicted_opening_weekend_millions": 79.3}

4. 项目结构和配置

一个最小可用的票房预测项目目录如下:

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
box-office-predictor/
├── data/
│   ├── movie_data.csv                 # 训练数据(真实或合成)
│   └── sample_input.json              # 测试用输入样例
├── notebooks/
│   └── exploration.ipynb              # 数据探索和特征创建
├── src/
│   ├── train.py                       # 训练脚本
│   ├── predict_api.py                 # Flask API
│   ├── utils.py                       # 数据加载、特征定义
│   └── synthetic_data_generator.py    # 模拟数据生成器(方便初学)
├── models/
│   ├── box_office_model.pkl
│   └── features.pkl
├── frontend/
│   └── index.html                     # 简单表单页面
├── requirements.txt
└── README.md

requirements.txt

text
1 2 3 4 5 6
catboost==1.2.7
pandas==2.1.4
scikit-learn==1.3.2
flask==3.0.0
joblib==1.3.2
requests==2.31.0

如何快速跑起来?

  1. 安装依赖:pip install -r requirements.txt
  2. 生成模拟数据(可选):python src/synthetic_data_generator.py 会在data/下生成1000条样本
  3. 训练:python src/train.py 输出模型和特征信息
  4. 启动API:python src/predict_api.py
  5. 打开frontend/index.html,输入数据体验预测

5. 上线要注意的坑(从Forbes新闻中反思)

你以为把模型部署到服务器就完了?项目真正上线后,你可能遇到下面这几个关键问题,而且恰恰能从《曼达洛人》的预测案例中找到教训。

坑1:特征滞后——你用的数据是昨天的明星热度,但今天的观众可能不买账

《曼达洛人》电视剧的成功可能给了模型一个“高热度”信号,但电影观众和流媒体观众有重叠但并非完全一致。模型训练数据里的lead_actor_hotness如果来自互联网存量数据(比如Google Trends过去半年均值),它无法捕捉“电视剧粉丝是否转化为电影观众”这一动态。

可操作建议:上线后每天用新的搜索指数、社交媒体提及量更新lead_actor_hotness特征,并用在线学习(比如CatBoost的partial_fit)增量更新模型。至少做到每周重训练。

坑2:系列效应被低估了

从特征重要性看,“系列”是第二大因素。但“Star Wars”这个系列在训练数据里包含了很多高票房电影,而《曼达洛人》是第一个从Disney+衍生到大银幕的作品。同系列的票房方差很大(例如《最后的绝地武士》6.2亿 vs 《游侠索罗》3.9亿)。模型学到的“系列均值”可能被经典三部曲拉高,导致预测偏高(实际只有8000万开画)。

可操作建议:评分系(series)特征时,用更细的“衍生片分类”,或者加入“是否流媒体系列首次电影”这样的二元特征。我建议你在特征工程阶段,手动创建一个is_spinoff_from_streaming列。

坑3:模型对极端值不敏感

票房分布是长尾的:大部分电影首周末<3000万,少数大片>1亿。CatBoost默认的损失函数RMSE对极端值非常敏感,会导致模型为了拟合《复仇者联盟4》(3.5亿)而扭曲了小预算电影的预测。

可操作建议:考虑使用TweedieRegressor(属于GLM)或者对目标变量做对数变换y_log = log(y+1)。在CatBoost中可以通过设定loss_function='RMSE'后,对预测结果做指数变换。实验表明,对数变换后RMSE下降约30%。

坑4:没有考虑流媒体窗口和院线独占期

2026年的电影市场已经和2018年完全不同。Disney+几乎同步上线的大片(比如《曼达洛人》是否首周就上线流媒体?)这直接影响观众决策。传统模型只用了“影院档期”和“预算”,忽略了串流时间窗口这个强特征。

可操作建议:如果你要做一个面向未来的票房预测模型,必须加入days_to_streaming(上映多少天后上流媒体)这个变量。数据来源可以用IMDb Pro或The Numbers。

6. 你自己的判断:开发者现在应该关注什么

回到最初的新闻:Forbes用Deadline的预测数据写了篇分析,而《曼达洛人》最终票房可能周末会后劲不足(因为口碑?)。但作为开发者,这件事给我们的信号是:传统的票房预测模型正在失效

疫情后观众观影习惯变化、流媒体与院线共存、社交媒体口碑传播加速——这些因素使得2018年之前训练的模型预测误差越来越大。如果你现在想做一个能真正商用(或者给自己做投资参考)的票房预测产品,你需要:

  1. 数据采集自动化:每天爬取Twitter话题量、烂番茄新鲜度、预售票房(来自Box Office Mojo或票务平台)。
  2. 特征工程动态化:不只是静态的“导演历史票房”,而是“上映前一周的导演相关新闻热度指数”。
  3. 模型集成化:不止一个CatBoost,可以加上XGBoost和LightGBM做Stacking,用时间序列模型(Prophet)预测日票房曲线。
  4. 提供置信区间:预测“首周末8000万”不如告诉用户“有60%概率落在7000万-9500万之间”。使用分位数回归或Dropout不确定性估计。

如果你对本文的代码和思路感兴趣,可以直接跑上面的项目。所有代码都在单个脚本里,30分钟就能出预测结果。下次看到媒体发布票房预测,你至少能判断出它用的是多老的模型、忽略了哪些关键特征。

box office prediction pipeline

最后说一点:不要迷信“预测”。机器学习给的只是历史模式的映射。真正的商业决策需要把预测结果和外部信息(口碑、突发事件、竞品上映)结合。作为开发者,你的优势在于能快速迭代模型,拥抱变化。行,上代码。