基于数值和分类特征预测电影状态
众所周知,在娱乐业中预测电影的成功与否可以决定一家电影公司的财务前景。
准确的预测可以使电影公司对各个方面,如市场营销、分销和内容创作等做出明智的决策。
最重要的是,这些预测可以通过优化资源分配,帮助最大化利润和最小化损失。
幸运的是,机器学习技术为解决这个复杂的问题提供了强大的工具。毫无疑问,通过利用数据驱动的见解,电影公司可以显著提高其决策过程。
这个数据科学项目在Meta(Facebook)的招聘流程中被用作作业。在这个作业中,我们将发现Rotten Tomatoes是如何将电影标记为“烂片”、“新鲜”或“认证新鲜”的。
为了做到这一点,我们将开发两种不同的方法。
在我们的探索过程中,我们将讨论数据预处理、各种分类器以及改进模型性能的潜在方法。
通过本文的最后,您将了解如何使用机器学习来预测电影的成功,并将这些知识应用于娱乐业。
但在深入研究之前,让我们了解一下我们要处理的数据。
第一方法:基于数值和分类特征预测电影状态
在这种方法中,我们将使用数值和分类特征的组合来预测电影的成功。
我们考虑的特征包括预算、类型、时长和导演等因素。
我们将使用多种机器学习算法来构建我们的模型,包括决策树、随机森林和带有特征选择的加权随机森林。
让我们读取我们的数据并查看一下。
这是代码。
df_movie = pd.read_csv('rotten_tomatoes_movies.csv')
df_movie.head()
这是输出结果。
现在,让我们开始数据预处理。
我们的数据集中有很多列。
让我们看一下。
为了更好地了解统计特征,我们使用了 describe() 方法。这是代码。
df_movie.describe()
这是输出结果。
现在,我们对数据有了一个快速的概览,让我们进入预处理阶段。
数据预处理
在我们开始构建模型之前,预处理数据是非常重要的。
这包括通过处理分类特征并将其转换为数值表示,以及对数据进行缩放,以确保所有特征具有相等的重要性。
我们首先检查 content_rating 列以查看数据集中的唯一类别及其分布情况。
print(f'Content Rating category: {df_movie.content_rating.unique()}')
然后,我们将创建一个条形图来查看每个 content rating 类别的分布情况。
ax = df_movie.content_rating.value_counts().plot(kind='bar', figsize=(12,9))
ax.bar_label(ax.containers[0])
这是完整的代码。
print(f'Content Rating category: {df_movie.content_rating.unique()}')
ax = df_movie.content_rating.value_counts().plot(kind='bar', figsize=(12,9))
ax.bar_label(ax.containers[0])
这是输出结果。
将分类特征转换为数字形式对于需要数字输入的机器学习模型非常重要。对于这个数据科学项目中的多个元素,我们将应用两种通常被接受的方法:有序编码和独热编码。当类别意味着强度程度时,有序编码更好,但当没有提供大小表示时,独热编码是理想的选择。对于”content_rating”资产,我们将使用独热编码方法。
以下是代码:
content_rating = pd.get_dummies(df_movie.content_rating)
content_rating.head()
以下是输出结果:
让我们继续处理另一个特征,audience_status。
这个变量有两个选项:”Spilled”和”Upright”。
我们已经应用了独热编码,现在是时候使用有序编码将这个分类变量转换为数值变量了。
因为每个类别都表示一个数量级的顺序,我们将使用有序编码将它们转换为数值。
和之前一样,首先让我们找到唯一的观众状态。
print(f'Audience status category: {df_movie.audience_status.unique()}')
然后,让我们创建一个条形图并在条形上方打印出值。
# 可视化每个类别的分布
ax = df_movie.audience_status.value_counts().plot(kind='bar', figsize=(12,9))
ax.bar_label(ax.containers[0])
以下是完整的代码:
print(f'Audience status category: {df_movie.audience_status.unique()}')
# 可视化每个类别的分布
ax = df_movie.audience_status.value_counts().plot(kind='bar', figsize=(12,9))
ax.bar_label(ax.containers[0])
以下是输出结果:
好的,现在是时候使用replace方法进行有序编码了。
然后,让我们使用head()方法查看前五行。
以下是代码:
# 使用有序编码对观众状态变量进行编码
audience_status = pd.DataFrame(df_movie.audience_status.replace(['Spilled','Upright'],[0,1]))
audience_status.head()
以下是输出结果:
由于我们的目标变量tomatometer_status有三个不同的类别,’Rotten’,’Fresh’和’Certified-Fresh’,这些类别也代表了一个数量级的顺序。
这就是为什么我们将再次使用有序编码将这些分类变量转换为数值变量。
以下是代码:
# 使用有序编码对tomatometer状态变量进行编码
tomatometer_status = pd.DataFrame(df_movie.tomatometer_status.replace(['Rotten','Fresh','Certified-Fresh'],[0,1,2]))
tomatometer_status
以下是输出结果:
在将分类变量转换为数值变量后,现在是时候将这两个数据帧合并起来了。我们将使用Pandas的pd.concat()函数进行合并,并使用dropna()方法删除所有列中存在缺失值的行。
接下来,我们将使用head函数查看新形成的数据帧。
以下是代码:
df_feature = pd.concat([df_movie[['runtime', 'tomatometer_rating', 'tomatometer_count', 'audience_rating', 'audience_count', 'tomatometer_top_critics_count', 'tomatometer_fresh_critics_count', 'tomatometer_rotten_critics_count']], content_rating, audience_status, tomatometer_status], axis=1).dropna()
df_feature.head()
以下是输出结果:
很好,现在让我们使用describe方法来检查我们的数值变量。
以下是代码。
df_feature.describe()
以下是输出结果。
现在让我们使用len方法来检查我们的DataFrame的长度。
以下是代码。
len(df)
以下是输出结果。
在去除了缺失值并进行机器学习构建的转换之后,我们的数据框现在有17017行。
现在让我们分析一下目标变量的分布。
就像我们从一开始就一直在做的那样,我们将绘制一个条形图,并将值放在条形图的顶部。
以下是代码。
ax = df_feature.tomatometer_status.value_counts().plot(kind='bar', figsize=(12,9))
ax.bar_label(ax.containers[0])
以下是输出结果。
我们的数据集中包含7375部“烂片”,6475部“新鲜片”和3167部“认证新鲜片”,这说明存在类别不平衡的问题。
这个问题将在稍后解决。
目前,让我们使用80%和20%的比例将数据集分为测试集和训练集。
以下是代码。
X_train, X_test, y_train, y_test = train_test_split(df_feature.drop(['tomatometer_status'], axis=1), df_feature.tomatometer_status, test_size= 0.2, random_state=42)
print(f'训练数据的大小为{len(X_train)},测试数据的大小为{len(X_test)}')
以下是输出结果。
决策树分类器
在本节中,我们将介绍决策树分类器,这是一种常用于分类问题(有时也用于回归问题)的机器学习技术。
该分类器通过将数据点分成支路,每个支路都有一个内部节点(包含一组条件)和一个叶节点(包含预测值)。
通过遵循这些支路并考虑条件(真或假),数据点被分离到适当的类别中。该过程如下所示。
图片由作者提供
当我们应用决策树分类器时,我们可以调整多个超参数,例如树的最大深度和最大叶节点数。
对于我们的第一次尝试,我们将限制叶节点数为三,以使决策树简单易懂。
首先,我们将使用scikit-learn库中的DecisionTreeClassifier()函数定义一个最多有三个叶节点的决策树分类器对象。
random_state参数用于确保每次运行代码时产生相同的结果。
tree_3_leaf = DecisionTreeClassifier(max_leaf_nodes= 3, random_state=2)
然后是在训练数据(X_train和y_train)上训练决策树分类器,使用.fit()方法。
tree_3_leaf.fit(X_train, y_train)
接下来,我们使用训练过的分类器通过predict方法对测试数据(X_test)进行预测。
y_predict = tree_3_leaf.predict(X_test)
在这里,我们打印出预测值与测试数据实际目标值之间的准确率分数和分类报告。我们使用scikit-learn库中的accuracy_score()和classification_report()函数。
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
最后,我们将绘制混淆矩阵以可视化决策树分类器在测试数据上的性能。我们使用scikit-learn库中的plot_confusion_matrix()函数。
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(tree_3_leaf, X_test, y_test, cmap='cividis', ax=ax)
这是代码。
# 实例化最大叶节点数为3的决策树分类器
tree_3_leaf = DecisionTreeClassifier(max_leaf_nodes= 3, random_state=2)
# 在训练数据上训练分类器
tree_3_leaf.fit(X_train, y_train)
# 使用训练过的决策树分类器对测试数据进行预测
y_predict = tree_3_leaf.predict(X_test)
# 打印测试数据上的准确率和分类报告
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
# 在测试数据上绘制混淆矩阵
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(tree_3_leaf, X_test, y_test, cmap ='cividis', ax=ax)
这是输出结果。
从输出结果中可以清楚地看到,我们的决策树工作得很好,尤其是考虑到我们将其限制为三个叶节点。拥有简单分类器的一个优点是可以将决策树可视化并理解。
现在,为了了解决策树是如何做出决策的,让我们使用sklearn.tree中的plot_tree方法来可视化决策树分类器。
这是代码。
fig, ax = plt.subplots(figsize=(12, 9))
plot_tree(tree_3_leaf, ax= ax)
plt.show()
这是输出结果。
现在让我们分析这棵决策树,找出它是如何进行决策过程的。
具体来说,算法使用’tomatometer_rating’特征作为每个测试数据点分类的主要决定因素。
- 如果’tomatometer_rating’小于或等于59.5,则将数据点标记为0(’烂片’)。否则,分类器进入下一个分支。
- 在第二个分支中,分类器使用’tomatometer_fresh_critics_count’特征对剩余的数据点进行分类。
- 如果这个特征的值小于或等于35.5,则将数据点标记为1(’新鲜片’)。
- 否则,将其标记为2(’认证新鲜片’)。
这个决策过程与Rotten Tomatoes用于分配电影状态的规则和标准非常吻合。
根据Rotten Tomatoes网站,电影被分为:
- 如果它们的tomatometer_rating大于等于60%,则为’新鲜片’。
- 如果低于60%,则为’烂片’。
我们的决策树分类器遵循类似的逻辑,将电影分为’烂片’,如果它们的tomatometer_rating低于59.5,否则为’新鲜片’。
然而,在区分’新鲜片’和’认证新鲜片’电影时,分类器必须考虑更多特征。
根据Rotten Tomatoes的规定,电影必须满足特定的条件才能被分类为’认证新鲜片’,例如:
- 至少有75%的一致的Tomatometer评分
- 至少有五个来自顶级评论家的评价
- 对于大规模发行的电影,至少有80个评论
我们的决策树模型只考虑了来自顶级评论家的评论数量来区分“新鲜”和“认证新鲜”的电影。
现在,我们理解了决策树的逻辑。为了提高其性能,让我们按照相同的步骤进行操作,但这次不添加最大叶节点参数。
以下是我们代码的逐步解释。这次我不会像之前那样展开代码太多。
定义决策树分类器。
tree = DecisionTreeClassifier(random_state=2)
在训练数据上训练分类器。
tree.fit(X_train, y_train)
使用训练过的树分类器预测测试数据。
y_predict = tree.predict(X_test)
打印准确率和分类报告。
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
绘制混淆矩阵。
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(tree, X_test, y_test, cmap ='cividis', ax=ax)
太棒了,现在让我们一起看看它们。
下面是完整的代码。
fig, ax = plt.subplots(figsize=(12, 9))
# 使用默认超参数设置实例化决策树分类器
tree = DecisionTreeClassifier(random_state=2)
# 在训练数据上训练分类器
tree.fit(X_train, y_train)
# 使用训练过的树分类器预测测试数据
y_predict = tree.predict(X_test)
# 打印测试数据的准确率和分类报告
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
# 在测试数据上绘制混淆矩阵
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(tree, X_test, y_test, cmap ='cividis', ax=ax)
这是输出结果。
由于去除了最大叶节点限制,我们的分类器的准确率、精确率和召回率值都有所提高。分类器的准确率现在达到了99%,比之前的94%有所提升。
这表明当我们允许分类器自行选择最佳叶节点数时,它的性能更好。
尽管当前结果看起来已经很出色,但仍有可能通过进一步调整来达到更高的准确率。在下一部分中,我们将探讨这个选项。
随机森林分类器
随机森林是一种将多个决策树分类器组合成单个算法的集成方法。它使用装袋策略训练每个决策树,其中包括随机选择训练数据点。由于这种技术,每棵树都是在训练数据的不同子集上训练的。
装袋方法以使用自助法对数据点进行抽样而闻名,允许同一数据点被多个决策树选择。
图片由作者提供
通过使用scikit-learn,应用随机森林分类器非常简单。
使用Scikit-learn设置随机森林算法是一个简单的过程。
与决策树分类器一样,通过更改超参数值,如决策树分类器的数量、最大叶节点和最大树深度,可以提高算法的性能。
我们将首先使用默认选项。
让我们再次逐步查看代码。
首先,使用scikit-learn库的RandomForestClassifier()函数实例化一个随机森林分类器对象,并将random_state参数设置为2以保证可重复性。
rf = RandomForestClassifier(random_state=2)
然后,使用.fit()方法在训练数据(X_train和y_train)上训练随机森林分类器。
rf.fit(X_train, y_train)
接下来,使用训练过的分类器对测试数据(X_test)进行预测,使用.predict()方法。
y_predict = rf.predict(X_test)
然后,打印预测值与测试数据的实际目标值之间的准确率和分类报告。
我们再次使用scikit-learn库中的accuracy_score()和classification_report()函数。
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
最后,让我们绘制一个混淆矩阵来可视化随机森林分类器在测试数据上的性能。我们使用scikit-learn库中的plot_confusion_matrix()函数。
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(rf, X_test, y_test, cmap ='cividis', ax=ax)
以下是完整的代码。
# 实例化随机森林分类器
rf = RandomForestClassifier(random_state=2)
# 在训练数据上训练随机森林分类器
rf.fit(X_train, y_train)
# 使用训练好的模型预测测试数据
y_predict = rf.predict(X_test)
# 打印准确率和分类报告
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
# 绘制混淆矩阵
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(rf, X_test, y_test, cmap ='cividis', ax=ax)
以下是输出结果。
准确率和混淆矩阵的结果显示,随机森林算法的性能优于决策树分类器。这显示了集成方法(如随机森林)在个别分类算法上的优势。
此外,基于树的机器学习方法允许我们在模型训练后确定每个特征的重要性。因此,scikit-learn提供了feature_importances_函数。
非常好,让我们再次逐步查看代码以理解它。
首先,使用随机森林分类器对象的feature_importances_属性获取数据集中每个特征的重要性评分。
重要性评分表示每个特征对模型预测性能的贡献程度。
# 获取特征重要性
feature_importance = rf.feature_importances_
接下来,按重要性的降序打印特征重要性以及对应的特征名称。
# 打印特征重要性
for i, feature in enumerate(X_train.columns):
print(f'{feature} = {feature_importance[i]}')
然后,为了可视化特征的重要性,让我们使用numpy中的argsort()方法将特征从最重要到最不重要进行排序。
# 从最重要到最不重要的顺序可视化特征
indices = np.argsort(feature_importance)
最后,创建一个水平条形图来可视化特征的重要性。y轴上的特征按重要性从高到低排列,x轴上显示对应的重要性评分。
该图表使我们能够轻松识别数据集中最重要的特征,并确定哪些特征对模型性能影响最大。
plt.figure(figsize=(12,9))
plt.title('特征重要性')
plt.barh(range(len(indices)), feature_importance[indices], color='b', align='center')
plt.yticks(range(len(indices)), [X_train.columns[i] for i in indices])
plt.xlabel('相对重要性')
plt.show()
以下是完整的代码。
# 获取特征重要性
feature_importance = rf.feature_importances_
# 打印特征重要性
for i, feature in enumerate(X_train.columns):
print(f'{feature} = {feature_importance[i]}')
# 从最重要到最不重要的顺序可视化特征
indices = np.argsort(feature_importance)
plt.figure(figsize=(12,9))
plt.title('特征重要性')
plt.barh(range(len(indices)), feature_importance[indices], color='b', align='center')
plt.yticks(range(len(indices)), [X_train.columns[i] for i in indices])
plt.xlabel('相对重要性')
plt.show()
以下是输出结果。
通过观察这个图表,可以清楚地看到模型认为NR、PG-13、R和片长对于预测未知数据点不重要。在下一部分中,我们将看到解决这个问题是否能提高模型的性能。
使用特征选择的随机森林分类器
这是代码。
在上一节中,我们发现我们的一些特征在使用随机森林模型进行预测时被认为不太重要。
因此,为了提高模型的性能,让我们排除这些不相关的特征,包括NR、runtime、PG-13、R、PG、G和NC17。
在下面的代码中,我们首先获取特征的重要性,然后将其拆分为训练集和测试集,但在代码块内部,我们删除了这些不太重要的特征。然后我们将打印出训练集和测试集的大小。
这是代码。
# 获取特征重要性
feature_importance = rf.feature_importances_
X_train, X_test, y_train, y_test = train_test_split(df_feature.drop(['tomatometer_status', 'NR', 'runtime', 'PG-13', 'R', 'PG','G', 'NC17'], axis=1),df_feature.tomatometer_status, test_size= 0.2, random_state=42)
print(f'训练数据的大小为{len(X_train)},测试数据的大小为{len(X_test)}')
这是输出结果。
很好,由于我们删除了这些不太重要的特征,让我们看看我们的性能是否提高了。
因为我们这样做了很多次,我快速解释以下代码。
在下面的代码中,我们首先初始化一个随机森林分类器,然后在训练数据上训练随机森林。
rf = RandomForestClassifier(random_state=2)
rf.fit(X_train, y_train)
然后我们使用测试数据计算准确率和分类报告,并将其打印出来。
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
最后,我们绘制混淆矩阵。
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(rf, X_test, y_test, cmap ='cividis', ax=ax)
这是完整的代码。
# 初始化随机森林分类器
rf = RandomForestClassifier(random_state=2)
# 在特征选择后的训练数据上训练随机森林
rf.fit(X_train, y_train)
# 在特征选择后的测试数据上预测训练模型
y_predict = rf.predict(X_test)
# 打印准确率和分类报告
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
# 绘制混淆矩阵
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(rf, X_test, y_test, cmap ='cividis', ax=ax)
这是输出结果。
看起来我们的新方法效果很好。
进行特征选择后,准确率提高到了99.1%。
与以前的模型相比,我们的模型的误报率和漏报率也略有降低。
这表明拥有更多特征并不总是意味着模型更好。一些不重要的特征可能会引入噪声,从而降低模型的预测准确性。
现在,由于我们的模型性能已经提高到了这个程度,让我们尝试其他方法,看看是否还能进一步提高。
使用特征选择的加权随机森林分类器
在第一节中,我们意识到我们的特征有点不平衡。我们有三个不同的值,’Rotten’(表示为0),’Fresh’(表示为1)和’Certified-Fresh’(表示为2)。
首先,让我们看一下特征的分布情况。
这是用于可视化标签分布的代码。
ax = df_feature.tomatometer_status.value_counts().plot(kind='bar', figsize=(12,9))
ax.bar_label(ax.containers[0])
这是输出结果。
很明显,“Certified Fresh”特征的数据量要比其他特征少得多。
为了解决数据不平衡的问题,我们可以采用一些方法,比如使用SMOTE算法来对少数类进行过采样,或者在训练阶段为模型提供类权重信息。
这里我们将采用第二种方法。
为了计算类权重,我们将使用scikit-learn库中的compute_class_weight()函数。
在这个函数中,将class_weight参数设置为’balanced’以考虑到不平衡的类别,并将classes参数设置为df_feature中tomatometer_status列的唯一值。
y参数设置为df_feature中tomatometer_status列的值。
class_weight = compute_class_weight(class_weight='balanced', classes=np.unique(df_feature.tomatometer_status), y=df_feature.tomatometer_status.values)
然后,创建一个字典来将类权重映射到它们相应的索引。
这是通过使用dict()函数和zip()函数将类权重列表转换为字典来实现的。
range()函数用于生成与类权重列表的长度对应的整数序列,然后将其用作字典的键。
class_weight_dict = dict(zip(range(len(class_weight.tolist())), class_weight.tolist()
最后,让我们看看我们的字典。
class_weight_dict
这是完整的代码。
class_weight = compute_class_weight(class_weight='balanced', classes=np.unique(df_feature.tomatometer_status), y=df_feature.tomatometer_status.values)
class_weight_dict = dict(zip(range(len(class_weight.tolist())), class_weight.tolist()))
class_weight_dict
这是输出结果。
类别0(“Rotten”)的权重最小,而类别2(“Certified-Fresh”)的权重最大。
当我们应用随机森林分类器时,我们现在可以将这个权重信息作为参数。
剩下的代码与我们之前做过多次的代码相同。
让我们使用包含类权重数据的新的随机森林模型,对训练集进行训练,预测测试数据,并显示准确率和混淆矩阵。
这是代码。
# 使用包含权重信息的随机森林模型进行初始化
rf_weighted = RandomForestClassifier(random_state=2, class_weight=class_weight_dict)
# 在训练数据上训练模型
rf_weighted.fit(X_train, y_train)
# 使用训练好的模型对测试数据进行预测
y_predict = rf_weighted.predict(X_test)
# 打印准确率和分类报告
print(accuracy_score(y_test, y_predict))
print(classification_report(y_test, y_predict))
# 绘制混淆矩阵
fig, ax = plt.subplots(figsize=(12, 9))
plot_confusion_matrix(rf_weighted, X_test, y_test, cmap ='cividis', ax=ax)
这是输出结果。
我们的模型在添加了类权重后性能提高了,现在准确率为99.2%。
“Fresh”标签的正确预测数量也增加了一个。
使用类权重来解决数据不平衡问题是一种有效的方法,因为它在整个训练阶段鼓励我们的模型更多地关注具有较高权重的标签。
此数据科学项目的链接:https://platform.stratascratch.com/data-projects/rotten-tomatoes-movies-rating-prediction
Nate Rosidi是一位数据科学家,从事产品战略工作。他还是一位兼职教授,教授分析学,并且是StrataScratch的创始人,该平台通过提供来自顶级公司的真实面试问题,帮助数据科学家准备面试。您可以在Twitter上与他联系:StrataScratch或LinkedIn。