引言:电影票房预测的挑战与机遇

电影票房预测是娱乐产业中最具挑战性的数据分析任务之一。一部电影的票房收入受到众多因素的影响,包括演员阵容、导演声誉、制作预算、上映档期、营销投入、口碑评价等。传统的预测方法往往依赖于专家经验和简单的统计模型,但随着机器学习技术的发展,我们可以通过数据驱动的方法来提高预测的准确性。

Kaggle作为全球最大的数据科学竞赛平台,提供了丰富的电影数据集和竞赛环境,让数据科学家们能够在这个领域大展身手。本指南将带你从零开始,完成一个完整的电影票房预测项目,涵盖数据清洗、特征工程、模型构建和优化等关键步骤。

第一部分:数据获取与理解

1.1 数据集介绍

在Kaggle上,最常用的电影票房预测数据集是TMDB(The Movie Database)电影数据集。这个数据集包含了数万部电影的详细信息,如预算、票房、演员、导演、类型、关键词等。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')

# 加载数据
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')

# 查看数据基本信息
print("训练集形状:", train_df.shape)
print("测试集形状:", test_df.shape)
print("\n训练集前5行:")
print(train_df.head())

1.2 数据探索性分析(EDA)

在进行数据清洗之前,我们需要先了解数据的基本情况,包括缺失值、数据类型、分布情况等。

# 查看数据基本信息
print("训练集信息:")
train_df.info()

# 查看缺失值情况
print("\n训练集缺失值统计:")
print(train_df.isnull().sum())

# 查看数值型列的统计信息
print("\n数值型列统计信息:")
print(train_df.describe())

# 可视化票房分布
plt.figure(figsize=(12, 6))
sns.histplot(train_df['revenue'], bins=50, kde=True)
plt.title('电影票房分布')
plt.xlabel('票房收入')
plt.ylabel('频数')
plt.show()

1.3 初步观察与假设

通过初步探索,我们可能会发现:

  1. 票房分布严重右偏,大部分电影票房较低,少数大片票房极高
  2. 预算与票房之间可能存在正相关关系
  3. 演员和导演的影响力可能对票房有重要影响
  4. 电影类型和上映时间可能影响票房表现

第二部分:数据清洗与预处理

2.1 处理缺失值

电影数据通常存在大量缺失值,需要根据业务逻辑进行合理填充或删除。

# 处理预算缺失值:用中位数填充(对异常值不敏感)
train_df['budget'] = train_df['budget'].replace(0, np.nan)  # 0预算可能是缺失值
train_df['budget'] = train_df['budget'].fillna(train_df['budget'].median())

test_df['budget'] = test_df['budget'].replace(0, np.nan)
test_df['budget'] = test_df['budget'].fillna(test_df['budget'].median())

# 处理运行时间缺失值:用中位数填充
train_df['runtime'] = train_df['runtime'].fillna(train_df['runtime'].median())
test_df['runtime'] = test_df['runtime'].fillna(test_df['runtime'].median())

# 处理语言缺失值:用最常见的语言填充
train_df['original_language'] = train_df['original_language'].fillna(train_df['original_language'].mode()[0])
test_df['original_language'] = test_df['original_language'].fillna(test_df['original_language'].mode()[0])

2.2 处理JSON格式数据

电影数据中很多字段(如演员、导演、类型、关键词)以JSON格式存储,需要解析。

import json
from ast import literal_eval

# 定义解析JSON字段的函数
def parse_json_field(df, field_name):
    """
    解析JSON格式的字段,提取主要信息
    """
    df[field_name] = df[field_name].fillna('[]')  # 填充空值
    df[field_name] = df[field_name].apply(literal_eval)  # 将字符串转换为列表/字典
    
    # 提取主要信息(如演员名、导演名、类型名)
    if field_name in ['cast', 'crew']:
        # 提取前3名演员或导演
        df[field_name + '_list'] = df[field_name].apply(
            lambda x: [item['name'] for item in x[:3]] if isinstance(x, list) else []
        )
    elif field_name in ['genres', 'keywords']:
        # 提取所有类型或关键词
        df[field_name + '_list'] = df[field_name].apply(
            lambda x: [item['name'] for item in x] if isinstance(x, list) else []
        )
    
    return df

# 应用解析函数
for field in ['genres', 'keywords', 'cast', 'crew']:
    train_df = parse_json_field(train_df, field)
    test_df = parse_json_field(test_df, field)

# 提取导演信息
def extract_director(crew_list):
    for item in crew_list:
        if item.get('job') == 'Director':
            return item.get('name', '')
    return ''

train_df['director'] = train_df['crew_list'].apply(extract_director)
test_df['director'] = test_df['crew_list'].apply(extract_director)

2.3 处理异常值

# 移除预算或票房为0的样本(可能是数据缺失)
train_df = train_df[(train_df['budget'] > 0) & (train_df['revenue'] > 0)]

# 对数变换处理右偏分布
train_df['log_revenue'] = np.log1p(train_df['revenue'])
train_df['log_budget'] = np.log1p(train_df['budget'])

# 可视化对数变换后的分布
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
sns.histplot(train_df['log_revenue'], bins=50, kde=True)
plt.title('对数变换后的票房分布')

plt.subplot(1, 2, 2)
sns.histplot(train_df['log_budget'], bins=50, kde=True)
plt.title('对数变换后的预算分布')
plt.show()

第三部分:特征工程

特征工程是机器学习项目中最关键的一步,好的特征能极大提升模型性能。

3.1 基础数值特征

# 选择基础特征
numeric_features = ['budget', 'popularity', 'runtime', 'vote_average', 'vote_count']
categorical_features = ['original_language']

# 创建基础特征集
features = numeric_features.copy()

# 创建预算与运行时间的交互特征
train_df['budget_per_minute'] = train_df['budget'] / (train_df['runtime'] + 1)
test_df['budget_per_minute'] = test_df['budget'] / (test_df['runtime'] + 1)
features.append('budget_per_minute')

# 预算与投票数的交互
train_df['budget_per_vote'] = train_df['budget'] / (train_df['vote_count'] + 1)
test_df['budget_per_minute'] = test_df['budget'] / (test_df['vote_count'] + 1)
features.append('budget_per_vote')

3.2 分类变量编码

# 语言编码:使用目标编码(Target Encoding)
language_mean_revenue = train_df.groupby('original_language')['log_revenue'].mean()
train_df['language_encoded'] = train_df['original_language'].map(language_mean_revenue)
test_df['language_encoded'] = test_df['original_language'].map(language_mean_revenue)
features.append('language_encoded')

# 填充测试集中未出现的语言
test_df['language_encoded'] = test_df['language_encoded'].fillna(train_df['log_revenue'].mean())

# 类型、关键词、演员、导演的One-Hot编码
def multi_label_binarize(df, field, top_n=10):
    """
    多标签二值化:只保留最常见的前N个值
    """
    # 统计每个值出现的次数
    all_values = []
    for lst in df[field + '_list']:
        all_values.extend(lst)
    
    value_counts = pd.Series(all_values).value_counts().head(top_n)
    top_values = value_counts.index.tolist()
    
    # 创建二值化特征
    for value in top_values:
        df[field + '_' + value] = df[field + '_list'].apply(lambda x: 1 if value in x else 0)
        features.append(field + '_' + value)
    
    return df

# 应用多标签二值化
for field in ['genres', 'keywords', 'cast', 'director']:
    train_df = multi_label_binarize(train_df, field, top_n=10)
    test_df = multi_label_binarize(test_df, field, top_n=10)

3.3 时间特征

# 转换日期格式
train_df['release_date'] = pd.to_datetime(train_df['release_date'], errors='coerce')
test_df['release_date'] = pd.to_datetime(test_df['release_date'], errors='coerce')

# 提取时间特征
def extract_time_features(df):
    df['release_year'] = df['release_date'].dt.year
    df['release_month'] = df['releas_date'].dt.month
    df['release_day'] = df['release_date'].dt.day
    df['release_dayofweek'] = df['release_date'].dt.dayofweek
    df['release_quarter'] = df['release_date'].dt.quarter
    
    # 假期特征(如暑期档、圣诞档)
    df['is_summer'] = df['release_month'].isin([6, 7, 8]).astype(int)
    df['is_holiday'] = df['release_month'].isin([11, 12]).astype(int)
    
    return df

train_df = extract_time_features(train_df)
test_df = extract_time_features(test_df)

# 时间特征加入特征列表
time_features = ['release_year', 'release_month', 'release_day', 'release_dayofweek', 
                 'release_quarter', 'is_summer', 'is_holiday']
features.extend(time_features)

# 填充时间特征缺失值
for col in time_features:
    train_df[col] = train_df[col].fillna(train_df[col].median())
    test_df[col] =col].fillna(train_df[col].median())

3.4 文本特征(高级)

from sklearn.feature_extraction.text import TfidfVectorizer

# 使用TF-IDF处理关键词文本
def process_text_features(df):
    # 将关键词列表转换为字符串
    df['keywords_text'] = df['keywords_list'].apply(lambda x: ' '.join(x))
    
    # 创建TF-IDF矩阵
    vectorizer = TfidfVectorizer(max_features=100, stop_words='english')
    tfidf_matrix = vectorizer.fit_transform(df['keywords_text'])
    
    # 将TF-IDF矩阵转换为DataFrame
    tfidf_df = pd.DataFrame(tfidf_matrix.toarray(), 
                           columns=['keyword_' + col for col in vectorizer.get_feature_names_out()])
    
    # 合并到原数据
    df = pd.concat([df.reset_index(drop=True), tfidf_df.reset_index(drop=True)], axis=1)
    
    # 更新特征列表
    new_features = ['keyword_' + col for col in vectorizer.get_feature_names_out()]
    return df, new_features

train_df, keyword_features = process_text_features(train_df)
test_df, _ = process_text_features(test_df)
features.extend(keyword_features)

第四部分:模型构建与训练

4.1 数据准备

# 准备训练数据
X = train_df[features]
y = train_df['log_revenue']  # 使用对数变换后的票房

# 处理缺失值(再次检查)
X = X.fillna(0)
test_X = test_df[features].fillna(0)

# 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 特征缩放
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
test_X_scaled = scaler.transform(test_X)

print(f"训练集形状: {X_train.shape}")
print(f"验证集形状: {X_val.shape}")
print(f"测试集形状: {test_X.shape}")
print(f"特征数量: {len(features)}")

4.2 基础模型:随机森林

# 初始化随机森林模型
rf_model = RandomForestRegressor(
    n_estimators=100,
    max_depth=10,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42,
    n_jobs=-1
)

# 训练模型
rf_model.fit(X_train_scaled, y_train)

# 预测
y_train_pred = rf_model.predict(X_train_scaled)
y_val_pred = rf_model.predict(X_val_scaled)

# 评估模型
def evaluate_model(y_true, y_pred, dataset_name):
    mse = mean_squared_error(y_true, y_pred)
    rmse = np.sqrt(mse)
    r2 = r2_score(y_true, y_pred)
    print(f"{dataset_name} - RMSE: {rmse:.4f}, R²: {r2:.4f}")
    return rmse, r2

print("随机森林模型评估:")
train_rmse, train_r2 = evaluate_model(y_train, y_train_pred, "训练集")
val_rmse, val_r2 = evaluate_model(y_val, y_val_pred, "验证集")

# 特征重要性分析
feature_importance = pd.DataFrame({
    'feature': features,
    'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)

print("\nTop 10重要特征:")
print(feature_importance.head(10))

# 可视化特征重要性
plt.figure(figsize=(10, 6))
sns.barplot(data=feature_importance.head(20), x='importance', y='feature')
plt.title('Top 20 Feature Importance')
plt.tight_layout()
plt.show()

4.3 模型对比:XGBoost

import xgboost as xgb

# 初始化XGBoost模型
xgb_model = xgb.XGBRegressor(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    n_jobs=-1,
    objective='reg:squarederror'
)

# 训练模型
xgb_model.fit(X_train_scaled, y_train)

# 预测
y_train_pred_xgb = xgb_model.predict(X_train_scaled)
y_val_pred_xgb = xgboost.predict(X_val_scaled)

# 评估
print("\nXGBoost模型评估:")
train_rmse_xgb, train_r2_xgb = evaluate_model(y_train, y_train_pred_xgb, "训练集")
val_rmse_xgb, val_r2_xgb = evaluate_model(y_val, y_val_pred_xgb, "验证集")

4.4 模型对比:LightGBM

import lightgbm as lgb

# 初始化LightGBM模型
lgb_model = lgb.LGBMRegressor(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    subsample=0.8,
    colsample_bytree=0.0.8,
    random_state=42,
    n_jobs=-1
)

# 训练模型
lgb_model.fit(X_train_scaled, y_train)

# 预测
y_train_pred_lgb = lgb_model.predict(X_train_scaled)
y_val_pred_lgb = lgb_model.predict(X_val_scaled)

# 评估
print("\nLightGBM模型评估:")
train_rmse_lgb, train_r2_lgb = evaluate_model(y_train, y_train_pred_lgb, "训练集")
val_rmse_lgb, val_r2_lgb = evaluate_model(y_val, y_val_pred_lgb, "验证集")

第五部分:模型优化与调参

5.1 超参数网格搜索

from sklearn.model_selection import GridSearchCV

# 定义参数网格
param_grid = {
    'n_estimators': [100, 200, 300],
    'max_depth': [5, 7, 9],
    'learning_rate': [0.05, 0.1, 0.15],
    'subsample': [0.7, 0.8, 0.9],
    'colsample_bytree': [0.7, 0.8, 0.9]
}

# 创建模型
xgb_model = xgb.XGBRegressor(random_state=42, n_jobs=-1)

# 网格搜索
grid_search = GridSearchCV(
    xgb_model,
    param_grid,
    cv=3,
    scoring='neg_mean_squared_error',
    n_jobs=-1,
    verbose=1
)

# 执行搜索(注意:这可能需要较长时间)
# grid_search.fit(X_train_scaled, y_train)

# print("最佳参数:", grid_search.best_params_)
# print("最佳分数:", grid_search.best_score_)

# 使用最佳参数训练最终模型
# best_xgb = grid_search.best_estimator_

5.2 贝叶斯优化(更高效)

from skopt import BayesSearchCV
from skopt.space import Real, Integer

# 定义搜索空间
search_space = {
    'n_estimators': Integer(100, 500),
    'max_depth': Integer(3, 10),
    'learning_rate': Real(0.01, 0.3, prior='log-uniform'),
    'subsample': Real(0.6, 1.0),
    'colsample_bytree': Real(0.6, 1.0)
}

# 贝叶斯优化
bayes_search = BayesSearchCV(
    xgb.XGBRegressor(random_state=42, n_jobs=-1),
    search_space,
    n_iter=32,  # 迭代次数
    cv=3,
    scoring='neg_mean_squared_error',
    n_jobs=-1,
    verbose=1
)

# 执行搜索
# bayes_search.fit(X_train_scaled, y_train)
# print("最佳参数:", bayes_search.best_params_)

5.3 模型集成

from sklearn.ensemble import VotingRegressor

# 创建集成模型
rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
xgb = xgb.XGBRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42)
lgb = lgb.LGBMRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42)

# 投票回归器
voting_model = VotingRegressor(
    estimators=[
        ('rf', rf),
        ('xgb', xgb),
        ('lgb', lgb)
    ],
    weights=[1, 2, 2]  # 给XGBoost和LightGBM更高权重
)

# 训练集成模型
voting_model.fit(X_train_scaled, y_train)

# 预测
y_val_pred_ensemble = voting_model.predict(X_val_scaled)

# 评估
print("\n集成模型评估:")
val_rmse_ensemble, val_r2_ensemble = evaluate_model(y_val, y_val_pred_ensemble, "验证集")

5.4 特征选择

from sklearn.feature_selection import SelectKBest, f_regression

# 选择最重要的K个特征
selector = SelectKBest(score_func=f_regression, k=50)
X_train_selected = selector.fit_transform(X_train_scaled, y_train)
X_val_selected = selector.transform(X_val_scaled)

# 获取选中的特征
selected_features = [features[i] for i in selector.get_support(indices=True)]
print(f"选中的特征 ({len(selected_features)}个):")
print(selected_features)

# 用选中的特征重新训练模型
xgb_selected = xgb.XGBRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42)
xgb_selected.fit(X_train_selected, y_train)
y_val_pred_selected = xgb_selected.predict(X_val_selected)

# 评估
val_rmse_selected, val_r2_selected = evaluate_model(y_val, y_val_pred_selected, "验证集(特征选择后)")

第六部分:高级技巧与实战建议

6.1 交叉验证策略

from sklearn.model_selection import KFold, cross_val_score

# 使用K折交叉验证
kf = KFold(n_splits=5, shuffle=True, random_state=42)

# 计算交叉验证分数
cv_scores = cross_val_score(
    xgb.XGBRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42),
    X_train_scaled, y_train,
    cv=kf,
    scoring='neg_mean_squared_error',
    n_jobs=-1
)

print("交叉验证RMSE:", np.sqrt(-cv_scores))
print("平均RMSE:", np.sqrt(-cv_scores.mean()))

6.2 处理数据泄露

# 重要:在特征工程中避免数据泄露
# 错误做法:使用整个数据集计算统计量
# 正确做法:在交叉验证循环中计算

def safe_target_encoding(X_train, y_train, X_test, column):
    """
    安全的目标编码,避免数据泄露
    """
    # 在训练集上计算统计量
    mapping = y_train.groupby(X_train[column]).mean()
    
    # 应用到训练集和测试集
    X_train_encoded = X_train.copy()
    X_test_encoded = X_test.copy()
    
    X_train_encoded[column + '_encoded'] = X_train[column].map(mapping)
    X_test_encoded[column + '_encoded'] = X_test[column].map(mapping)
    
    # 填充未见过的类别
    mean_value = y_train.mean()
    X_train_encoded[column + '_encoded'] = X_train_encoded[column + '_encoded'].fillna(mean_value)
    X_test_encoded[column + '_encoded'] = X_test_encoded[column + '_encoded'].fillna(mean_value)
    
    return X_train_encoded, X_test_encoded

6.3 模型解释性

import shap

# 计算SHAP值
explainer = shap.TreeExplainer(xgb_model)
shap_values = explainer.shap_values(X_val_scaled)

# 可视化
shap.summary_plot(shap_values, X_val_scaled, feature_names=features)
shap.summary_plot(shap_values, X_val_scaled, feature_names=features, plot_type="bar")

# 单个样本解释
shap.force_plot(explainer.expected_value, shap_values[0,:], X_val_scaled[0,:], feature_names=features)

6.4 处理时间序列问题

# 电影票房预测中的时间序列特性
# 1. 考虑历史票房趋势
# 2. 考虑同档期竞争电影
# 3. 考虑季节性因素

# 创建历史趋势特征
def create_time_series_features(df):
    # 按时间排序
    df_sorted = df.sort_values('release_date')
    
    # 计算滚动统计量
    df_sorted['rolling_mean_30'] = df_sorted['revenue'].rolling(window=30, min_periods=1).mean()
    df_sorted['rolling_std_30'] = df_sorted['revenue'].rolling(window=30, min_periods=1).std()
    
    # 计算历史平均票房(按月份)
    monthly_avg = df_sorted.groupby(df_sorted['release_date'].dt.month)['revenue'].mean()
    df_sorted['month_avg'] = df_sorted['release_date'].dt.month.map(monthly_avg)
    
    return df_sorted

# 注意:实际应用中需要避免数据泄露,这里仅展示思路

第七部分:实战建议与常见陷阱

7.1 数据理解的重要性

  • 不要忽视数据字典:理解每个字段的含义至关重要
  • 检查数据质量:预算为0、运行时间为0等异常值需要特别处理
  • 理解业务逻辑:例如,某些电影可能有隐藏的营销预算

7.2 特征工程的创造性

  • 组合特征:预算/运行时间、演员知名度/预算等
  • 外部数据:考虑加入社交媒体数据、预告片播放量等
  • 时间序列特征:考虑电影上映时的市场环境

7.3 模型选择的权衡

  • 简单模型优先:线性模型作为baseline
  • 集成学习:XGBoost/LightGBM通常表现优异
  • 模型复杂度:避免过拟合,注意验证集表现

7.4 评估指标的选择

# 电影票房预测常用评估指标
def calculate_metrics(y_true, y_pred):
    """
    计算多种评估指标
    """
    # RMSE
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    
    # MAE
    mae = mean_absolute_error(y_true, y_pred)
    
    # MAPE(平均绝对百分比误差)
    mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
    
    # R²
    r2 = r2_score(y_true, y_pred)
    
    # 对数变换后的指标
    log_rmse = np.sqrt(mean_squared_error(np.log1p(y_true), np.log1p(y_pred)))
    
    print(f"RMSE: {rmse:.2f}")
    print(f"MAE: {mae:.2f}")
    # print(f"MAPE: {mape:.2f}%")
    print(f"R²: {r2:.4f}")
    print(f"Log RMSE: {log_rmse:.4f}")
    
    return {
        'rmse': rmse,
        'mae': mae,
        'mape': mape,
        'r2': r2,
        'log_rmse': log_rmse
    }

from sklearn.metrics import mean_absolute_error

7.5 提交预测

# 生成最终预测
final_predictions = voting_model.predict(test_X_scaled)

# 反变换(如果使用了对数变换)
final_predictions = np.expm1(final_predictions)

# 创建提交文件
submission = pd.DataFrame({
    'id': test_df['id'],
    'revenue': final_predictions
})

# 保存
submission.to_csv('submission.csv', index=False)
print("提交文件已保存:submission.csv")
print(submission.head())

第八部分:总结与展望

电影票房预测是一个复杂但有趣的机器学习应用领域。通过本指南,你已经学习了:

  1. 数据清洗:处理缺失值、异常值和JSON格式数据
  2. 特征工程:创建数值、分类、时间、文本特征
  3. 模型构建:使用随机森林、XGBoost、LightGBM等算法
  4. 模型优化:超参数调优、模型集成、特征选择
  5. 高级技巧:交叉验证、模型解释、避免数据泄露

关键成功因素

  • 数据质量:高质量的数据是成功的基础
  • 特征工程:创造性地构建特征能极大提升性能
  1. 模型选择:理解不同模型的优缺点
  2. 验证策略:可靠的验证方法防止过拟合

未来方向

  • 深度学习:使用神经网络处理文本和图像数据
  • 多模态学习:结合预告片、海报等视觉数据
  • 实时预测:结合社交媒体热度进行动态预测
  • 因果推断:理解营销投入对票房的真实影响

通过不断实践和优化,你将能够构建更精准的票房预测模型,为电影产业的数据驱动决策提供有力支持!# Kaggle预测票房实战指南:从数据清洗到模型优化,教你如何利用机器学习精准预测电影票房收入与市场表现

引言:电影票房预测的挑战与机遇

电影票房预测是娱乐产业中最具挑战性的数据分析任务之一。一部电影的票房收入受到众多因素的影响,包括演员阵容、导演声誉、制作预算、上映档期、营销投入、口碑评价等。传统的预测方法往往依赖于专家经验和简单的统计模型,但随着机器学习技术的发展,我们可以通过数据驱动的方法来提高预测的准确性。

Kaggle作为全球最大的数据科学竞赛平台,提供了丰富的电影数据集和竞赛环境,让数据科学家们能够在这个领域大展身手。本指南将带你从零开始,完成一个完整的电影票房预测项目,涵盖数据清洗、特征工程、模型构建和优化等关键步骤。

第一部分:数据获取与理解

1.1 数据集介绍

在Kaggle上,最常用的电影票房预测数据集是TMDB(The Movie Database)电影数据集。这个数据集包含了数万部电影的详细信息,如预算、票房、演员、导演、类型、关键词等。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')

# 加载数据
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')

# 查看数据基本信息
print("训练集形状:", train_df.shape)
print("测试集形状:", test_df.shape)
print("\n训练集前5行:")
print(train_df.head())

1.2 数据探索性分析(EDA)

在进行数据清洗之前,我们需要先了解数据的基本情况,包括缺失值、数据类型、分布情况等。

# 查看数据基本信息
print("训练集信息:")
train_df.info()

# 查看缺失值情况
print("\n训练集缺失值统计:")
print(train_df.isnull().sum())

# 查看数值型列的统计信息
print("\n数值型列统计信息:")
print(train_df.describe())

# 可视化票房分布
plt.figure(figsize=(12, 6))
sns.histplot(train_df['revenue'], bins=50, kde=True)
plt.title('电影票房分布')
plt.xlabel('票房收入')
plt.ylabel('频数')
plt.show()

1.3 初步观察与假设

通过初步探索,我们可能会发现:

  1. 票房分布严重右偏,大部分电影票房较低,少数大片票房极高
  2. 预算与票房之间可能存在正相关关系
  3. 演员和导演的影响力可能对票房有重要影响
  4. 电影类型和上映时间可能影响票房表现

第二部分:数据清洗与预处理

2.1 处理缺失值

电影数据通常存在大量缺失值,需要根据业务逻辑进行合理填充或删除。

# 处理预算缺失值:用中位数填充(对异常值不敏感)
train_df['budget'] = train_df['budget'].replace(0, np.nan)  # 0预算可能是缺失值
train_df['budget'] = train_df['budget'].fillna(train_df['budget'].median())

test_df['budget'] = test_df['budget'].replace(0, np.nan)
test_df['budget'] = test_df['budget'].fillna(test_df['budget'].median())

# 处理运行时间缺失值:用中位数填充
train_df['runtime'] = train_df['runtime'].fillna(train_df['runtime'].median())
test_df['runtime'] = test_df['runtime'].fillna(test_df['runtime'].median())

# 处理语言缺失值:用最常见的语言填充
train_df['original_language'] = train_df['original_language'].fillna(train_df['original_language'].mode()[0])
test_df['original_language'] = test_df['original_language'].fillna(test_df['original_language'].mode()[0])

2.2 处理JSON格式数据

电影数据中很多字段(如演员、导演、类型、关键词)以JSON格式存储,需要解析。

import json
from ast import literal_eval

# 定义解析JSON字段的函数
def parse_json_field(df, field_name):
    """
    解析JSON格式的字段,提取主要信息
    """
    df[field_name] = df[field_name].fillna('[]')  # 填充空值
    df[field_name] = df[field_name].apply(literal_eval)  # 将字符串转换为列表/字典
    
    # 提取主要信息(如演员名、导演名、类型名)
    if field_name in ['cast', 'crew']:
        # 提取前3名演员或导演
        df[field_name + '_list'] = df[field_name].apply(
            lambda x: [item['name'] for item in x[:3]] if isinstance(x, list) else []
        )
    elif field_name in ['genres', 'keywords']:
        # 提取所有类型或关键词
        df[field_name + '_list'] = df[field_name].apply(
            lambda x: [item['name'] for item in x] if isinstance(x, list) else []
        )
    
    return df

# 应用解析函数
for field in ['genres', 'keywords', 'cast', 'crew']:
    train_df = parse_json_field(train_df, field)
    test_df = parse_json_field(test_df, field)

# 提取导演信息
def extract_director(crew_list):
    for item in crew_list:
        if item.get('job') == 'Director':
            return item.get('name', '')
    return ''

train_df['director'] = train_df['crew_list'].apply(extract_director)
test_df['director'] = test_df['crew_list'].apply(extract_director)

2.3 处理异常值

# 移除预算或票房为0的样本(可能是数据缺失)
train_df = train_df[(train_df['budget'] > 0) & (train_df['revenue'] > 0)]

# 对数变换处理右偏分布
train_df['log_revenue'] = np.log1p(train_df['revenue'])
train_df['log_budget'] = np.log1p(train_df['budget'])

# 可视化对数变换后的分布
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
sns.histplot(train_df['log_revenue'], bins=50, kde=True)
plt.title('对数变换后的票房分布')

plt.subplot(1, 2, 2)
sns.histplot(train_df['log_budget'], bins=50, kde=True)
plt.title('对数变换后的预算分布')
plt.show()

第三部分:特征工程

特征工程是机器学习项目中最关键的一步,好的特征能极大提升模型性能。

3.1 基础数值特征

# 选择基础特征
numeric_features = ['budget', 'popularity', 'runtime', 'vote_average', 'vote_count']
categorical_features = ['original_language']

# 创建基础特征集
features = numeric_features.copy()

# 创建预算与运行时间的交互特征
train_df['budget_per_minute'] = train_df['budget'] / (train_df['runtime'] + 1)
test_df['budget_per_minute'] = test_df['budget'] / (test_df['runtime'] + 1)
features.append('budget_per_minute')

# 预算与投票数的交互
train_df['budget_per_vote'] = train_df['budget'] / (train_df['vote_count'] + 1)
test_df['budget_per_minute'] = test_df['budget'] / (test_df['vote_count'] + 1)
features.append('budget_per_vote')

3.2 分类变量编码

# 语言编码:使用目标编码(Target Encoding)
language_mean_revenue = train_df.groupby('original_language')['log_revenue'].mean()
train_df['language_encoded'] = train_df['original_language'].map(language_mean_revenue)
test_df['language_encoded'] = test_df['original_language'].map(language_mean_revenue)
features.append('language_encoded')

# 填充测试集中未出现的语言
test_df['language_encoded'] = test_df['language_encoded'].fillna(train_df['log_revenue'].mean())

# 类型、关键词、演员、导演的One-Hot编码
def multi_label_binarize(df, field, top_n=10):
    """
    多标签二值化:只保留最常见的前N个值
    """
    # 统计每个值出现的次数
    all_values = []
    for lst in df[field + '_list']:
        all_values.extend(lst)
    
    value_counts = pd.Series(all_values).value_counts().head(top_n)
    top_values = value_counts.index.tolist()
    
    # 创建二值化特征
    for value in top_values:
        df[field + '_' + value] = df[field + '_list'].apply(lambda x: 1 if value in x else 0)
        features.append(field + '_' + value)
    
    return df

# 应用多标签二值化
for field in ['genres', 'keywords', 'cast', 'director']:
    train_df = multi_label_binarize(train_df, field, top_n=10)
    test_df = multi_label_binarize(test_df, field, top_n=10)

3.3 时间特征

# 转换日期格式
train_df['release_date'] = pd.to_datetime(train_df['release_date'], errors='coerce')
test_df['release_date'] = pd.to_datetime(test_df['release_date'], errors='coerce')

# 提取时间特征
def extract_time_features(df):
    df['release_year'] = df['release_date'].dt.year
    df['release_month'] = df['release_date'].dt.month
    df['release_day'] = df['release_date'].dt.day
    df['release_dayofweek'] = df['release_date'].dt.dayofweek
    df['release_quarter'] = df['release_date'].dt.quarter
    
    # 假期特征(如暑期档、圣诞档)
    df['is_summer'] = df['release_month'].isin([6, 7, 8]).astype(int)
    df['is_holiday'] = df['release_month'].isin([11, 12]).astype(int)
    
    return df

train_df = extract_time_features(train_df)
test_df = extract_time_features(test_df)

# 时间特征加入特征列表
time_features = ['release_year', 'release_month', 'release_day', 'release_dayofweek', 
                 'release_quarter', 'is_summer', 'is_holiday']
features.extend(time_features)

# 填充时间特征缺失值
for col in time_features:
    train_df[col] = train_df[col].fillna(train_df[col].median())
    test_df[col] = test_df[col].fillna(train_df[col].median())

3.4 文本特征(高级)

from sklearn.feature_extraction.text import TfidfVectorizer

# 使用TF-IDF处理关键词文本
def process_text_features(df):
    # 将关键词列表转换为字符串
    df['keywords_text'] = df['keywords_list'].apply(lambda x: ' '.join(x))
    
    # 创建TF-IDF矩阵
    vectorizer = TfidfVectorizer(max_features=100, stop_words='english')
    tfidf_matrix = vectorizer.fit_transform(df['keywords_text'])
    
    # 将TF-IDF矩阵转换为DataFrame
    tfidf_df = pd.DataFrame(tfidf_matrix.toarray(), 
                           columns=['keyword_' + col for col in vectorizer.get_feature_names_out()])
    
    # 合并到原数据
    df = pd.concat([df.reset_index(drop=True), tfidf_df.reset_index(drop=True)], axis=1)
    
    # 更新特征列表
    new_features = ['keyword_' + col for col in vectorizer.get_feature_names_out()]
    return df, new_features

train_df, keyword_features = process_text_features(train_df)
test_df, _ = process_text_features(test_df)
features.extend(keyword_features)

第四部分:模型构建与训练

4.1 数据准备

# 准备训练数据
X = train_df[features]
y = train_df['log_revenue']  # 使用对数变换后的票房

# 处理缺失值(再次检查)
X = X.fillna(0)
test_X = test_df[features].fillna(0)

# 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 特征缩放
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)
test_X_scaled = scaler.transform(test_X)

print(f"训练集形状: {X_train.shape}")
print(f"验证集形状: {X_val.shape}")
print(f"测试集形状: {test_X.shape}")
print(f"特征数量: {len(features)}")

4.2 基础模型:随机森林

# 初始化随机森林模型
rf_model = RandomForestRegressor(
    n_estimators=100,
    max_depth=10,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42,
    n_jobs=-1
)

# 训练模型
rf_model.fit(X_train_scaled, y_train)

# 预测
y_train_pred = rf_model.predict(X_train_scaled)
y_val_pred = rf_model.predict(X_val_scaled)

# 评估模型
def evaluate_model(y_true, y_pred, dataset_name):
    mse = mean_squared_error(y_true, y_pred)
    rmse = np.sqrt(mse)
    r2 = r2_score(y_true, y_pred)
    print(f"{dataset_name} - RMSE: {rmse:.4f}, R²: {r2:.4f}")
    return rmse, r2

print("随机森林模型评估:")
train_rmse, train_r2 = evaluate_model(y_train, y_train_pred, "训练集")
val_rmse, val_r2 = evaluate_model(y_val, y_val_pred, "验证集")

# 特征重要性分析
feature_importance = pd.DataFrame({
    'feature': features,
    'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)

print("\nTop 10重要特征:")
print(feature_importance.head(10))

# 可视化特征重要性
plt.figure(figsize=(10, 6))
sns.barplot(data=feature_importance.head(20), x='importance', y='feature')
plt.title('Top 20 Feature Importance')
plt.tight_layout()
plt.show()

4.3 模型对比:XGBoost

import xgboost as xgb

# 初始化XGBoost模型
xgb_model = xgb.XGBRegressor(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    n_jobs=-1,
    objective='reg:squarederror'
)

# 训练模型
xgb_model.fit(X_train_scaled, y_train)

# 预测
y_train_pred_xgb = xgb_model.predict(X_train_scaled)
y_val_pred_xgb = xgb_model.predict(X_val_scaled)

# 评估
print("\nXGBoost模型评估:")
train_rmse_xgb, train_r2_xgb = evaluate_model(y_train, y_train_pred_xgb, "训练集")
val_rmse_xgb, val_r2_xgb = evaluate_model(y_val, y_val_pred_xgb, "验证集")

4.4 模型对比:LightGBM

import lightgbm as lgb

# 初始化LightGBM模型
lgb_model = lgb.LGBMRegressor(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    n_jobs=-1
)

# 训练模型
lgb_model.fit(X_train_scaled, y_train)

# 预测
y_train_pred_lgb = lgb_model.predict(X_train_scaled)
y_val_pred_lgb = lgb_model.predict(X_val_scaled)

# 评估
print("\nLightGBM模型评估:")
train_rmse_lgb, train_r2_lgb = evaluate_model(y_train, y_train_pred_lgb, "训练集")
val_rmse_lgb, val_r2_lgb = evaluate_model(y_val, y_val_pred_lgb, "验证集")

第五部分:模型优化与调参

5.1 超参数网格搜索

from sklearn.model_selection import GridSearchCV

# 定义参数网格
param_grid = {
    'n_estimators': [100, 200, 300],
    'max_depth': [5, 7, 9],
    'learning_rate': [0.05, 0.1, 0.15],
    'subsample': [0.7, 0.8, 0.9],
    'colsample_bytree': [0.7, 0.8, 0.9]
}

# 创建模型
xgb_model = xgb.XGBRegressor(random_state=42, n_jobs=-1)

# 网格搜索
grid_search = GridSearchCV(
    xgb_model,
    param_grid,
    cv=3,
    scoring='neg_mean_squared_error',
    n_jobs=-1,
    verbose=1
)

# 执行搜索(注意:这可能需要较长时间)
# grid_search.fit(X_train_scaled, y_train)

# print("最佳参数:", grid_search.best_params_)
# print("最佳分数:", grid_search.best_score_)

# 使用最佳参数训练最终模型
# best_xgb = grid_search.best_estimator_

5.2 贝叶斯优化(更高效)

from skopt import BayesSearchCV
from skopt.space import Real, Integer

# 定义搜索空间
search_space = {
    'n_estimators': Integer(100, 500),
    'max_depth': Integer(3, 10),
    'learning_rate': Real(0.01, 0.3, prior='log-uniform'),
    'subsample': Real(0.6, 1.0),
    'colsample_bytree': Real(0.6, 1.0)
}

# 贝叶斯优化
bayes_search = BayesSearchCV(
    xgb.XGBRegressor(random_state=42, n_jobs=-1),
    search_space,
    n_iter=32,  # 迭代次数
    cv=3,
    scoring='neg_mean_squared_error',
    n_jobs=-1,
    verbose=1
)

# 执行搜索
# bayes_search.fit(X_train_scaled, y_train)
# print("最佳参数:", bayes_search.best_params_)

5.3 模型集成

from sklearn.ensemble import VotingRegressor

# 创建集成模型
rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
xgb = xgb.XGBRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42)
lgb = lgb.LGBMRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42)

# 投票回归器
voting_model = VotingRegressor(
    estimators=[
        ('rf', rf),
        ('xgb', xgb),
        ('lgb', lgb)
    ],
    weights=[1, 2, 2]  # 给XGBoost和LightGBM更高权重
)

# 训练集成模型
voting_model.fit(X_train_scaled, y_train)

# 预测
y_val_pred_ensemble = voting_model.predict(X_val_scaled)

# 评估
print("\n集成模型评估:")
val_rmse_ensemble, val_r2_ensemble = evaluate_model(y_val, y_val_pred_ensemble, "验证集")

5.4 特征选择

from sklearn.feature_selection import SelectKBest, f_regression

# 选择最重要的K个特征
selector = SelectKBest(score_func=f_regression, k=50)
X_train_selected = selector.fit_transform(X_train_scaled, y_train)
X_val_selected = selector.transform(X_val_scaled)

# 获取选中的特征
selected_features = [features[i] for i in selector.get_support(indices=True)]
print(f"选中的特征 ({len(selected_features)}个):")
print(selected_features)

# 用选中的特征重新训练模型
xgb_selected = xgb.XGBRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42)
xgb_selected.fit(X_train_selected, y_train)
y_val_pred_selected = xgb_selected.predict(X_val_selected)

# 评估
val_rmse_selected, val_r2_selected = evaluate_model(y_val, y_val_pred_selected, "验证集(特征选择后)")

第六部分:高级技巧与实战建议

6.1 交叉验证策略

from sklearn.model_selection import KFold, cross_val_score

# 使用K折交叉验证
kf = KFold(n_splits=5, shuffle=True, random_state=42)

# 计算交叉验证分数
cv_scores = cross_val_score(
    xgb.XGBRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42),
    X_train_scaled, y_train,
    cv=kf,
    scoring='neg_mean_squared_error',
    n_jobs=-1
)

print("交叉验证RMSE:", np.sqrt(-cv_scores))
print("平均RMSE:", np.sqrt(-cv_scores.mean()))

6.2 处理数据泄露

# 重要:在特征工程中避免数据泄露
# 错误做法:使用整个数据集计算统计量
# 正确做法:在交叉验证循环中计算

def safe_target_encoding(X_train, y_train, X_test, column):
    """
    安全的目标编码,避免数据泄露
    """
    # 在训练集上计算统计量
    mapping = y_train.groupby(X_train[column]).mean()
    
    # 应用到训练集和测试集
    X_train_encoded = X_train.copy()
    X_test_encoded = X_test.copy()
    
    X_train_encoded[column + '_encoded'] = X_train[column].map(mapping)
    X_test_encoded[column + '_encoded'] = X_test[column].map(mapping)
    
    # 填充未见过的类别
    mean_value = y_train.mean()
    X_train_encoded[column + '_encoded'] = X_train_encoded[column + '_encoded'].fillna(mean_value)
    X_test_encoded[column + '_encoded'] = X_test_encoded[column + '_encoded'].fillna(mean_value)
    
    return X_train_encoded, X_test_encoded

6.3 模型解释性

import shap

# 计算SHAP值
explainer = shap.TreeExplainer(xgb_model)
shap_values = explainer.shap_values(X_val_scaled)

# 可视化
shap.summary_plot(shap_values, X_val_scaled, feature_names=features)
shap.summary_plot(shap_values, X_val_scaled, feature_names=features, plot_type="bar")

# 单个样本解释
shap.force_plot(explainer.expected_value, shap_values[0,:], X_val_scaled[0,:], feature_names=features)

6.4 处理时间序列问题

# 电影票房预测中的时间序列特性
# 1. 考虑历史票房趋势
# 2. 考虑同档期竞争电影
# 3. 考虑季节性因素

# 创建历史趋势特征
def create_time_series_features(df):
    # 按时间排序
    df_sorted = df.sort_values('release_date')
    
    # 计算滚动统计量
    df_sorted['rolling_mean_30'] = df_sorted['revenue'].rolling(window=30, min_periods=1).mean()
    df_sorted['rolling_std_30'] = df_sorted['revenue'].rolling(window=30, min_periods=1).std()
    
    # 计算历史平均票房(按月份)
    monthly_avg = df_sorted.groupby(df_sorted['release_date'].dt.month)['revenue'].mean()
    df_sorted['month_avg'] = df_sorted['release_date'].dt.month.map(monthly_avg)
    
    return df_sorted

# 注意:实际应用中需要避免数据泄露,这里仅展示思路

第七部分:实战建议与常见陷阱

7.1 数据理解的重要性

  • 不要忽视数据字典:理解每个字段的含义至关重要
  • 检查数据质量:预算为0、运行时间为0等异常值需要特别处理
  • 理解业务逻辑:例如,某些电影可能有隐藏的营销预算

7.2 特征工程的创造性

  • 组合特征:预算/运行时间、演员知名度/预算等
  • 外部数据:考虑加入社交媒体数据、预告片播放量等
  • 时间序列特征:考虑电影上映时的市场环境

7.3 模型选择的权衡

  • 简单模型优先:线性模型作为baseline
  • 集成学习:XGBoost/LightGBM通常表现优异
  • 模型复杂度:避免过拟合,注意验证集表现

7.4 评估指标的选择

# 电影票房预测常用评估指标
def calculate_metrics(y_true, y_pred):
    """
    计算多种评估指标
    """
    # RMSE
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    
    # MAE
    mae = mean_absolute_error(y_true, y_pred)
    
    # MAPE(平均绝对百分比误差)
    mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
    
    # R²
    r2 = r2_score(y_true, y_pred)
    
    # 对数变换后的指标
    log_rmse = np.sqrt(mean_squared_error(np.log1p(y_true), np.log1p(y_pred)))
    
    print(f"RMSE: {rmse:.2f}")
    print(f"MAE: {mae:.2f}")
    # print(f"MAPE: {mape:.2f}%")
    print(f"R²: {r2:.4f}")
    print(f"Log RMSE: {log_rmse:.4f}")
    
    return {
        'rmse': rmse,
        'mae': mae,
        'mape': mape,
        'r2': r2,
        'log_rmse': log_rmse
    }

from sklearn.metrics import mean_absolute_error

7.5 提交预测

# 生成最终预测
final_predictions = voting_model.predict(test_X_scaled)

# 反变换(如果使用了对数变换)
final_predictions = np.expm1(final_predictions)

# 创建提交文件
submission = pd.DataFrame({
    'id': test_df['id'],
    'revenue': final_predictions
})

# 保存
submission.to_csv('submission.csv', index=False)
print("提交文件已保存:submission.csv")
print(submission.head())

第八部分:总结与展望

电影票房预测是一个复杂但有趣的机器学习应用领域。通过本指南,你已经学习了:

  1. 数据清洗:处理缺失值、异常值和JSON格式数据
  2. 特征工程:创建数值、分类、时间、文本特征
  3. 模型构建:使用随机森林、XGBoost、LightGBM等算法
  4. 模型优化:超参数调优、模型集成、特征选择
  5. 高级技巧:交叉验证、模型解释、避免数据泄露

关键成功因素

  • 数据质量:高质量的数据是成功的基础
  • 特征工程:创造性地构建特征能极大提升性能
  1. 模型选择:理解不同模型的优缺点
  2. 验证策略:可靠的验证方法防止过拟合

未来方向

  • 深度学习:使用神经网络处理文本和图像数据
  • 多模态学习:结合预告片、海报等视觉数据
  • 实时预测:结合社交媒体热度进行动态预测
  • 因果推断:理解营销投入对票房的真实影响

通过不断实践和优化,你将能够构建更精准的票房预测模型,为电影产业的数据驱动决策提供有力支持!