第二个仪表盘:统计电影类型数量,探究类型分布规律

欢迎来到 “Python Dash 进阶教程”!

在之前的教程中,我们已经学习了如何利用 Dash 构建一个基础的 IMDB 数据仪表盘,了解了 Dash 的核心概念和一些常用功能。而现在,随着对 Dash 组件和 Python 的更深入了解,接下来我将带你一步步进入一个更复杂、更具互动性的数据可视化仪表盘。

我们还是使用之前的数据:IMDB Movie Dataset

在分析电影数据时,电影类型分布是一个重要的维度。例如,哪些类型的电影数量最多?哪些类型是近几年比较热门的?但在原始数据中,一个电影可能属于多个类型(如既是动作片又是喜剧片)。为了准确统计各类型的数量,我们需要对数据进行处理。

我们将通过以下步骤完成:

  1. 加载数据并清洗无关列。
  2. 处理电影收入和类型数据,拆分多类型为独立行。
  3. 统计每种类型的电影数量。

第一步:加载数据并移除无用列

import pandas as pd
 
# 加载 IMDB 数据
df = pd.read_csv('imdb_movie_dataset.csv')
 
# 删除无关列 'Rank' 和 'Metascore',这些信息不影响我们的类型统计。
df.drop(['Rank', 'Metascore'], axis=1, inplace=True)

第二步:清洗收入列
电影收入存储在 ‘Revenue (Millions)’ 列中,但有些可能缺失或格式异常。我们需要将其转换为数值类型,并清理缺失值:

# 转换收入为数值类型,无法转换的部分变为 NaN
df['Revenue (Millions)'] = pd.to_numeric(df['Revenue (Millions)'], errors='coerce')
 
# 删除缺失值所在行,确保数据完整性
df.dropna(inplace=True)

第三步:处理电影类型
每个电影可能属于多个类型,我们需要将这些类型拆分成独立行:

# 拆分 'Genre' 列,将逗号分隔的类型转换为列表
df['Genre'] = df['Genre'].str.split(',')

# 使用 explode 将类型拆分成多行
df = df.explode('Genre')
 
# 去掉类型前后的空格,确保干净整齐
df['Genre'] = df['Genre'].str.strip()

第四步:统计类型数量
通过 value_counts() 方法,统计每种类型的电影数量:

# 按类型统计电影数量
genre_counts = df['Genre'].value_counts()

通过上述操作,我们已经成功统计出了每种类型的电影数量,我们可以运行以下代码查看结果:

# 打印结果
print(genre_counts)

在这里插入图片描述
前期的数据处理工作都已完成,接下来,我们将开始用dash实现仪表盘的制作与交互。

首先我们初始化 Dash 应用

from dash import dcc, html, Dash
# 创建 Dash 应用
app = Dash(__name__)

页面布局定义了仪表盘的“骨架”,决定页面内容的排列方式和显示效果。在这个仪表盘中,我们需要添加以下组件:
• 标题:使用 html.H1 设置。
• 复选框筛选器:使用 dcc.Checklist 创建,用户可以选择需要分析的电影类型。
• 图表区域:通过 dcc.Graph 添加柱状图和饼图。

# 定义页面布局
app.layout = html.Div([
    # 页面标题
    # html.H1 用于创建一级标题,用来描述仪表盘的主题
    html.H1("Dashboard for Movie Genre Count Statistics",  # 标题内容
            style={'text-align': 'center'}),  # 设置标题居中显示
    # 类型筛选器
    # Checklist 是一个复选框组件,允许用户选择多个选项
    dcc.Checklist(
        id='genre-checklist',  # 组件的唯一 ID,用于回调函数引用
        options=[{'label': genre, 'value': genre} for genre in genre_counts.index],  # 动态生成所有电影类型
        value=genre_counts.index.tolist(),  # 默认选中所有类型
        inline=True),  # 设置选项横向排列
    # 图表容器,用于展示两个图表
    html.Div([
        # 柱状图:展示电影类型的数量
        # dcc.Graph 是一个用于绘制交互式图表的组件
        dcc.Graph(id='bar-chart',  # 图表的唯一 ID,用于后续回调函数
                  style={'display': 'inline-block', 'width': '49%'}),  # 设置柱状图宽度为页面的一半,并与饼图并排显示
        # 饼图:展示电影类型的占比
        dcc.Graph(id='pie-chart',  # 图表的唯一 ID,用于后续回调函数
                  style={'display': 'inline-block', 'width': '49%'})  # 设置饼图宽度为页面的一半,并与柱状图并排显示
    ])  # 结束图表容器
])  # 结束页面布局

PS:这里只是先定义了页面的布局,包括了大标题、用户交互的选项及位置、 两个子图的图名、位置,具体的绘图功能在接下来的回调函数中实现,这也是 dash 的灵魂所在。

Dash 回调函数与动态交互
在 Dash 中,回调函数是实现页面动态交互的核心机制。简单来说,回调函数会监听页面中某些组件的变化(如复选框被点击),并通过更新函数自动更新页面内容。这里@app.callback()是 dash 回调函数的标准写法,各位记住就好,不必深究。Output()以及 Input()中,填写的是组件的 id,也就是上文中 dcc.Checklist。这里提到的 id=‘genre-checklist’, checklist是一类组件的名称,表示可复选的按钮列,而 genre-checklist 是该类的一个具体的组件的名字,二者的关系就像是水果与苹果一样,一个是抽象的类型概念,一个是具体的实例物体。

from dash.dependencies import Input, Output
import plotly.express as px

# 回调函数
# 使用 @app.callback 装饰器定义回调函数,绑定输入与输出
@app.callback(
    # 定义两个输出组件,分别为柱状图和饼图
    [Output('bar-chart', 'figure'), Output('pie-chart', 'figure')],
    # 定义一个输入组件,当复选框的值发生变化时触发回调
    [Input('genre-checklist', 'value')])

在 dash 中,一个回调函数之后必定跟随一个更新函数(两者需要同时运行),这两个函数是耦合出现的,更新函数的作用是根据用户的交互更新页面内容。更新函数如下:

# 更新函数
# 该函数将在回调触发时自动调用,并生成新的图表
def update_charts(selected_genres):
    # 根据用户选择的电影类型,动态更新柱状图和饼图。
    # 过滤数据:保留用户选择的类型数据
    filtered_df = df[df['Genre'].isin(selected_genres)]
    # 统计类型数量:计算每种类型的电影数量
    filtered_counts = filtered_df['Genre'].value_counts()

    # 创建柱状图
    bar_chart = px.bar(
        filtered_counts,  # 数据
        x=filtered_counts.index,  # X轴为电影类型
        y=filtered_counts.values,  # Y轴为每种类型的电影数量
        title="Movie Genre Count Statistics (Bar Chart)",
        labels={'y': 'Number of Movies', 'x': 'Movie Genre'})  # 图表标题

    # 创建饼图
    pie_chart = px.pie(
        filtered_counts,  # 数据
        names=filtered_counts.index,  # 饼图的名称为电影类型
        values=filtered_counts.values,  # 饼图的大小为每种类型的数量
        title="Movie Genre Count Statistics (Pie Chart)")  # 图表标题
    # 返回生成的两个图表
    return bar_chart, pie_chart

在更新函数里面,我们基于 plotly.express 库函数编写了柱状图以及饼状图的绘制函数,并返回两个图。与上一章的绘图函数不同,这里的绘图函数不直接被调用,而是与回调函数绑定,当回调函数的 Input 中的组件发生变动(即用户产生点击等交互动作时),回调函数会自动调用更新函数,将用户交互后所要呈现的图返回到页面上,实现页面与用户的实时交互。
回调与更新的工作原理:

  1. 用户在复选框中选择或取消某些类型;
  2. 复选框的值变化触发回调函数;
  3. 回调函数调用更新函数,将过滤后的数据重新绘制成柱状图和饼图;
  4. 页面实时更新,展示新的图表内容。
    最后,我们运行该应用:
# 运行应用
if __name__ == '__main__':
    app.run_server(debug=True)

在这里插入图片描述
我们通过点击上方的按钮,可以选择自己想要查看的电影类型及占比,如我们想查看除去Drama类之后,实现的电影类型的数量及占比。
在这里插入图片描述
为了让仪表盘更美观并支持动态主题切换,我们使用了 dash_bootstrap_templates 库。其中,ThemeChangerAIO:提供动态主题切换的全局组件。template_from_url:从指定的 URL 加载主题模板,用于Plotly图表。
首先,在终端或命令行运行以下命令安装 dash-bootstrap-templates:

!pip install dash-bootstrap-templates

以下是需要被改动的代码(需要改动的部分用注释标注):

from dash_bootstrap_templates import ThemeChangerAIO, template_from_url  # 引入动态主题相关模块
import dash_bootstrap_components as dbc  # 引入 Dash 的 Bootstrap 组件库

# 创建 Dash 应用,并引入 Bootstrap 外部样式表
# external_stylesheets 中添加了两个样式表:
# 1. dbc.themes.BOOTSTRAP:基础的 Bootstrap 主题
# 2. dbc.icons.FONT_AWESOME:用于提供图标支持
app = Dash(__name__, 
           external_stylesheets=[dbc.themes.BOOTSTRAP,
                                 dbc.icons.FONT_AWESOME]
)

# 使用 dbc.Container 替代原来的 html.Div,以支持 Bootstrap 样式
# Container 提供了响应式的布局容器,可以更好地适应不同屏幕尺寸
app.layout = dbc.Container([
    html.H1("Dashboard for Movie Genre Count Statistics"),
    
    # 添加主题切换组件 ThemeChangerAIO
    # aio_id:组件的唯一标识符
    # radio_props:设置单选按钮的属性,默认选中 Bootstrap 主题
    # button_props:设置按钮的样式,这里使用 primary 颜色
    ThemeChangerAIO(
        aio_id="theme",
        radio_props={"value": dbc.themes.BOOTSTRAP},
        button_props={"color": "primary"}
    ),

    dcc.Checklist(
        id='genre-checklist',
        options=[{'label': genre, 'value': genre} for genre in genre_counts.index],
        value=genre_counts.index.tolist(),
        inline=True
    ),

    html.Div([
        dcc.Graph(id='bar-chart', style={'display': 'inline-block', 'width': '49%'}),
        dcc.Graph(id='pie-chart', style={'display': 'inline-block', 'width': '49%'})
    ])
])

# 在回调函数中添加主题切换的输入
# 除了原有的 genre-checklist 输入外,还添加了主题切换的输入
# ThemeChangerAIO.ids.radio("theme") 用于获取当前选择的主题
@app.callback(
    [Output('bar-chart', 'figure'), Output('pie-chart', 'figure')],
    [Input('genre-checklist', 'value'), 
     Input(ThemeChangerAIO.ids.radio("theme"), 'value')]
)

# 在更新函数中添加主题参数并应用到图表
# theme 参数会接收当前选择的主题值
def update_charts(selected_genres, theme):
    filtered_df = df[df['Genre'].isin(selected_genres)]
    filtered_counts = filtered_df['Genre'].value_counts()

    # 在创建图表时应用选择的主题
    # template_from_url(theme) 将选择的主题转换为 Plotly 可用的模板
    bar_chart = px.bar(
        filtered_counts,
        x=filtered_counts.index,
        y=filtered_counts.values,
        title="Movie Genre Count Statistics (Bar Chart)",
        template=template_from_url(theme),  # 应用主题到柱状图
        labels={'y': 'Number of Movies', 'x': 'Movie Genre'}
    )

    pie_chart = px.pie(
        filtered_counts,
        names=filtered_counts.index,
        values=filtered_counts.values,
        title="Movie Genre Count Statistics (Pie Chart)",
        template=template_from_url(theme)  # 应用主题到饼图
    )
    
    return bar_chart, pie_chart

if __name__ == '__main__':
    app.run_server(debug=True)

我们在布局上添加了一个组件 ThemeChangerAIO,这个组件可以让我们自由选择自己喜欢的主题,同时我们在回调函数的 Input 中添加了ThemeChangerAIO.ids.radio(“theme”), ‘value’),用户点击对应的主题前方的按钮,即可实时更新整个页面的主题。比如,我们选择 QUARTZ 主题:
在这里插入图片描述

第三个仪表盘:基于多维变量的回归预测仪表盘

前面的两个仪表盘基于简单的数据处理和图表绘制,而本次我们将尝试绘制一个基于多维变量回归预测的仪表盘,用于分析和预测未来的电影数据。在这个部分,我们使用多项式回归模型来预测未来五年的电影数量。

多项式回归是回归分析的一种扩展形式,可以用来拟合非线性数据。我们基于历史年度电影数据,使用多项式回归拟合关系曲线,并预测未来的电影数量。以下是主要步骤:

  1. 加载并清洗数据。
  2. 统计每年的电影数量。
  3. 构建多项式回归模型并拟合数据。
  4. 使用模型预测未来五年的电影数量。
  5. 评估模型的预测效果。

在这一部分中,我们首次使用了一些与机器学习相关的模块,用于模型构建、预测和评估。以下是这些模块的功能说明:
1. PolynomialFeatures
• 作用:用于生成多项式特征,将线性数据扩展为非线性特征,帮助模型拟合更复杂的关系。
• 特点:可以根据设定的多项式阶数(如 3 阶)自动生成如 [1, x, x², x³] 的特征矩阵。

2. LinearRegression
• 作用:实现线性回归模型,用于拟合多项式特征和目标值的关系。
• 特点:通过最小化误差来训练模型,是经典的机器学习回归算法。

3. mean_squared_error
• 作用:计算预测值与真实值之间误差平方的平均值,用于衡量模型的预测精度。
• 特点:对异常值较敏感,数值越小表示模型预测效果越好。

4. mean_absolute_error
• 作用:计算预测值与真实值之间误差绝对值的平均值,用于评估预测效果。
• 特点:相比 MSE,不受异常值的影响,数值越小越好。

5. r2_score
• 作用:评估模型的拟合优度,值范围在 [0, 1] 之间。
• 特点:R² 越接近 1,表示模型越能够解释数据的变化。

import pandas as pd  # 数据处理库
import numpy as np  # 数值计算库
from sklearn.preprocessing import PolynomialFeatures  # 用于生成多项式特征
from sklearn.linear_model import LinearRegression  # 用于训练线性回归模型
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score  # 模型评估指标

# 加载数据
df = pd.read_csv('imdb_movie_dataset.csv')  # 从 CSV 文件中加载电影数据
df.drop(['Rank', 'Metascore'], axis=1, inplace=True)  # 删除无关列
df['Revenue (Millions)'] = pd.to_numeric(df['Revenue (Millions)'], errors='coerce')  # 转换收入列为数值
df.dropna(inplace=True)  # 删除包含空值的行

# 统计每年电影数量
df['Year'] = df['Year'].astype(int)  # 将年份转换为整数类型
year_counts = df['Year'].value_counts().sort_index()  # 按年份统计电影数量并排序
year_counts = year_counts.reindex(range(year_counts.index.min(), year_counts.index.max() + 1), fill_value=0)
# 填补缺失年份的数据,用 0 代替缺失值

# 为多项式回归准备数据
X = year_counts.index.values.reshape(-1, 1)  # 特征:年份,转换为二维数组
y = year_counts.values  # 标签:每年的电影数量

# 训练多项式回归模型
degree = 3  # 多项式回归的阶数
poly = PolynomialFeatures(degree)  # 用于生成多项式特征
X_poly = poly.fit_transform(X)  # 将原始特征转换为多项式特征

model = LinearRegression().fit(X_poly, y)  # 使用线性回归模型拟合多项式特征与目标值

# 预测未来五年的数据
future_years = np.arange(year_counts.index[-1] + 1, year_counts.index[-1] + 6).reshape(-1, 1)  # 生成未来五年的年份
future_years_poly = poly.transform(future_years)  # 将未来年份转换为多项式特征
predicted_counts = model.predict(future_years_poly)  # 使用模型预测未来电影数量

# 评估模型
y_pred = model.predict(X_poly)  # 使用训练数据预测模型输出
mse = mean_squared_error(y, y_pred)  # 计算均方误差
mae = mean_absolute_error(y, y_pred)  # 计算平均绝对误差
r2 = r2_score(y, y_pred)  # 计算 R² 分数

# 输出评估结果
print("Mean Squared Error (MSE):", mse)
print("Mean Absolute Error (MAE):", mae)
print("R² Score:", r2)

运行代码后,我们得到以下结果:
在这里插入图片描述
我们可以看到:

  1. 模型的 R² 分数为 0.9506,说明模型拟合效果较好。
  2. MAE 表明预测值与真实值的平均误差为 7.6 部电影,说明误差较小。
  3. MSE 较大,这可能是由于数据量较少导致的。

通过上述步骤,我们完成了多项式回归模型的训练与评估,下一步我们将把预测结果与图表相结合,展示在仪表盘中。

from dash import Dash, dcc, html  # 导入 Dash 框架的核心模块

# 创建 Dash 应用
app = Dash(__name__)

# 定义页面布局
app.layout = html.Div([
    # 定义图表组件
    dcc.Graph(
        id='movie-count-prediction',  # 图表的唯一 ID
        figure={
            'data': [
                # 实际数据点的折线图
                {
                    'x': year_counts.index,  # X 轴为年份
                    'y': y,  # Y 轴为每年的电影数量
                    'type': 'scatter',  # 图表类型为散点图
                    'mode': 'lines',  # 显示为连线模式
                    'name': 'Actual'  # 图例名称为 "Actual"
                },
                # 预测数据点的折线图
                {
                    'x': future_years.flatten(),  # X 轴为预测的未来年份
                    'y': predicted_counts,  # Y 轴为预测的电影数量
                    'type': 'scatter',  # 图表类型为散点图
                    'mode': 'lines',  # 显示为连线模式
                    'name': 'Predicted',  # 图例名称为 "Predicted"
                    'line': {'dash': 'dash'}  # 使用虚线样式
                },
                # 实际与预测数据的连接线(视觉过渡效果)
                {
                    'x': [year_counts.index[-1], future_years[0, 0]],  # X 轴连接最后一个实际年份和第一个预测年份
                    'y': [y[-1], predicted_counts[0]],  # Y 轴连接实际最后一年的值和预测第一年的值
                    'type': 'scatter',  # 图表类型为散点图
                    'mode': 'lines',  # 显示为连线模式
                    'line': {
                        'dash': 'dot',  # 使用点状虚线样式
                        'color': '#FF5733'  # 线的颜色为橙红色
                    },
                    'showlegend': False  # 不在图例中显示此连接线
                }
            ],
            'layout': {
                'title': 'Movie Count Predictions',  # 图表标题
                'xaxis': {'title': 'Year'},  # 设置 X 轴标题
                'yaxis': {'title': 'Movie Count'}  # 设置 Y 轴标题
            }
        }
    )
])

# 启动应用
if __name__ == '__main__':
    app.run_server(debug=True)

运行代码后,我们可以看到预测结果显示,后五年的电影数量会处于一个稳步上升的模式。
在这里插入图片描述
接下来,我们将分析各变量之间的相关性,借助相关矩阵(Correlation Matrix)来量化变量间的线性关系。相关矩阵是一个方阵,其中的每个元素表示两个变量之间的相关系数,范围为 -1 到 1:
• 1:完全正相关(变量随着另一个变量增加而增加)。
• 0:无相关性(变量之间没有线性关系)。
• -1:完全负相关(变量随着另一个变量增加而减少)。

我们选取了以下有量化意义的属性进行分析:
• Year: 电影的发行年份;
• Runtime (Minutes): 电影的时长;
• Rating: 电影的观众评分;
• Votes: 参与评分的观众数量;
• Revenue (Millions): 电影的票房收入;
• Metascore: Metacritic 的评分。

import pandas as pd  # 导入 pandas 库,用于数据处理

# 读取并处理数据
df = pd.read_csv('imdb_movie_dataset.csv')  # 从 CSV 文件中加载数据

# 只保留以下列:Year,Runtime (Minutes),Rating,Votes,Revenue (Millions),Metascore
df = df[['Year', 'Runtime (Minutes)', 'Rating', 'Votes', 'Revenue (Millions)', 'Metascore']].dropna()  # 删除包含空值的行

# 将票房收入列转换为数值类型
# - 如果数据无法转换(例如含有无效值),则会被标记为 NaN
df['Revenue (Millions)'] = pd.to_numeric(df['Revenue (Millions)'], errors='coerce')

# 计算相关矩阵
# - 相关矩阵(Correlation Matrix)用于量化变量间的线性关系
# - 范围为 [-1, 1]:1 表示完全正相关,0 表示无相关,-1 表示完全负相关
corr = df.corr()  # 使用默认的 Pearson 相关系数计算

在计算出变量间的相关矩阵后,我们接下来将通过 热力图(Heatmap) 对相关性进行可视化展示,代码如下:

import plotly.graph_objects as go  # 导入 Plotly 的绘图模块
 
# 创建热力图及相关系数标注
heatmap = go.Heatmap(
    z=corr.values,  # 热力图的数值部分,使用相关矩阵的值
    x=corr.columns,  # X 轴标签,使用相关矩阵的列名
    y=corr.columns  # Y 轴标签,使用相关矩阵的列名
)
 
# 创建热力图上的相关系数标注
annotations = [
    dict(
        x=corr.columns[j],  # 标注的横坐标,对应矩阵的列名
        y=corr.columns[i],  # 标注的纵坐标,对应矩阵的行名
        text=str(round(corr.iloc[i, j], 2)),  # 标注的文本内容,保留两位小数
        showarrow=False,  # 去掉箭头,仅显示文本
        font=dict(size=12)  # 设置文本字体大小
    )
    for i in range(len(corr.columns))  # 遍历相关矩阵的行索引
    for j in range(len(corr.columns))  # 遍历相关矩阵的列索引
]

在生成了热力图和相关系数标注后,我们接下来将使用 Dash 将热力图集成到交互式仪表盘中。

from dash import dcc, html, Dash  # 导入 Dash 核心模块
import dash_bootstrap_components as dbc  # 引入 Bootstrap 样式库

# 创建 Dash 应用
app = Dash(
    __name__,  # 当前文件的模块名
    external_stylesheets=[dbc.themes.QUARTZ]  # 使用 Quartz 样式主题
)

# 定义页面布局
app.layout = html.Div([
    # 使用 dcc.Graph 显示热力图
    dcc.Graph(
        figure=go.Figure(  # 创建 Plotly 图表
            data=[heatmap],  # 热力图数据
            layout=go.Layout(
                title='Correlation Heatmap',  # 图表标题
                annotations=annotations,  # 热力图上的数值标注
                xaxis_title='Features',  # X 轴标题
                yaxis_title='Features')))])  # Y 轴标题

# 启动 Dash 应用
if __name__ == '__main__':
    app.run_server(debug=True)  # 调试模式运行应用

在这里插入图片描述
最后,我们将两张图放在一起,结果如下:

# 创建 Dash 应用并布局
app = Dash(__name__, external_stylesheets=[dbc.themes.QUARTZ])

# 布局:将两张图上下显示
app.layout = dbc.Container([
    html.H1("IMDB Movie Analysis Dashboard", className="text-center mb-4"),  # 仪表盘标题

    # 第一行:折线图
    dbc.Row([
        dbc.Col(
            dcc.Graph(
                id='movie-count-prediction',
                figure={
                    'data': [
                        {'x': year_counts.index, 'y': y, 'type': 'scatter', 'mode': 'lines', 'name': 'Actual'},  # 实际数据
                        {'x': future_years.flatten(), 'y': predicted_counts, 'type': 'scatter', 'mode': 'lines', 
                         'name': 'Predicted', 'line': {'dash': 'dash'}},  # 预测数据
                        {'x': [year_counts.index[-1], future_years[0, 0]], 'y': [y[-1], predicted_counts[0]], 
                         'type': 'scatter', 'mode': 'lines', 
                         'line': {'dash': 'dot', 'color': '#FF5733'}, 'showlegend': False}],  # 连接线
                    'layout': {
                        'title': 'Movie Count Predictions',  # 图表标题
                        'xaxis': {'title': 'Year'},  # X 轴标题
                        'yaxis': {'title': 'Movie Count'}}}))]),  # Y 轴标题

    # 第二行:热力图
    dbc.Row([
        dbc.Col(
            dcc.Graph(
                id='correlation-heatmap',
                figure=go.Figure(data=[heatmap], layout=go.Layout(
                    title='Correlation Heatmap',  # 图表标题
                    annotations=annotations,  # 数值标注
                    xaxis_title='Features',  # X 轴标题
                    yaxis_title='Features'))))])], fluid=True)  # Y 轴标题

if __name__ == '__main__':
    app.run_server(debug=True)

在这里插入图片描述
从热力图我们可以了解到:

  1. 电影收入与时长、投票数、评分、专家评分呈正相关,投资方可以优化这些指标提升收益。
  2. 高评分电影往往伴随较长的时长和较高的专家评分,可以作为观众筛选电影的依据。
  3. 尽管电影数量快速增长,但评分、时长等质量指标逐渐下降,显示行业整体质量下滑趋势,值得关注。

第四个仪表盘(动态仪表盘)

先前的三个仪表盘尽管逐步复杂,但都是静态的,这一次我们将尝试制作动态的仪表盘,使数据更为生动地展示出来。

1. 数据加载与预处理
在上一个仪表盘中,我们通过回归模型预测了后5年的电影数量,这次我们让用户进行选择,绘制出后n年的电影数量,并将其动态地展示出来,加强用户的交互体验!我们首先加载IMDb数据,并对其进行清理和预处理,只保留有用的字段:Year、Revenue (Millions)、Runtime (Minutes)、Rating。这里我们的操作与之前相似,以下是代码:

# 导入必要的库
# 因为后续代码中会使用到数据处理、回归建模和仪表盘构建相关功能,所以这里提前导入所有需要的库
import pandas as pd  # 用于数据加载和表格数据处理,如读取 CSV 文件和数据清洗
import numpy as np  # 用于数值计算和数组操作,方便处理年份和预测数据
from sklearn.preprocessing import PolynomialFeatures  # 用于生成多项式特征,适合非线性回归建模
from sklearn.linear_model import LinearRegression  # 用于训练线性回归模型,预测未来电影数量
import dash  # 用于构建交互式仪表盘应用
from dash import dcc, html  # 用于创建仪表盘布局和交互组件
from dash.dependencies import Input, Output, State  # 用于定义回调函数,实现组件交互逻辑
import plotly.graph_objs as go  # 用于创建动态图表,用于展示实际和预测数据
# 数据加载与预处理
df = pd.read_csv('imdb_movie_dataset.csv')
df.drop(['Rank'], axis=1, inplace=True)  # 删除无关列
for col in ['Revenue (Millions)', 'Runtime (Minutes)', 'Rating']:
    df[col] = pd.to_numeric(df[col], errors='coerce')  # 转换为数值类型
df.dropna(inplace=True)  # 删除空值
df['Year'] = df['Year'].astype(int)  # 确保年份为整数

2. Dash 应用布局
接着,我们在布局中新增了两个核心组件:

  1. 输入框(dcc.Input):
    • 提供用户交互功能,允许输入预测年数;
    • 参数 value 定义默认值,min 确保输入至少为 1。
  2. 定时器组件(dcc.Interval):
    • 用于动态触发回调函数;
    • 参数 interval=1000 表示每 1 秒触发一次;
    • n_intervals 用于记录触发次数,动态更新预测结果。
# 创建 Dash 应用
app = dash.Dash(__name__)

# 定义应用布局
app.layout = html.Div([
    html.Div([
        html.Label("Enter number of years to predict:"),  # 标签文本
        dcc.Input(
            id='n-predict-input',  # 输入框的唯一 ID
            type='number',  # 输入框类型限制为数字
            value=1,  # 默认值为 1
            min=1  # 最小值为 1,确保用户输入合理
        )
    ]),
    dcc.Graph(id='movie-count-animation'),  # 用于展示预测结果的动态图表
    dcc.Interval(
        id='interval-prediction',  # 定时器组件 ID
        interval=1000,  # 定时器触发间隔设置为 1 秒
        n_intervals=0,  # 初始化触发次数为 0
        disabled=False  # 默认启用
    )
])

3. 动态更新图表的回调函数
“回调函数是动态仪表盘的核心,用于实现以下逻辑:

  1. 用户输入预测年数后,重置定时器并重新计算预测结果;
    • 如果用户修改输入值,回调函数会重置 n_intervals 并重新计算预测结果。
  2. 按时间间隔逐步更新预测曲线,实现动态显示。”
    • 通过 n_intervals 限制每次显示的预测年份和对应数据,实现动态更新。
    • 图表包含两条曲线:实际数据(Actual Data)和逐步更新的预测数据(Predicted Data)。
    这个过程主要分为6个步骤:
@app.callback(
    [Output('movie-count-animation', 'figure'),  # 图表的内容更新
     Output('interval-prediction', 'n_intervals')],  # 定时器的触发次数更新
    [Input('interval-prediction', 'n_intervals'),  # 定时器触发次数
     Input('n-predict-input', 'value')]  # 用户输入的预测年数
)
# n_intervals: 定时器的触发次数,用于控制预测数据逐步显示。
# n_predict: 用户输入的预测年数。
def update_prediction(n_intervals, n_predict):
    # **步骤 1:重置触发次数和预测年数**
    # 如果输入框的值发生变化(用户输入新年数),重置触发次数和预测范围
    if n_predict is None or dash.callback_context.triggered_id == 'n-predict-input':
        n_intervals = 0  # 定时器的触发次数归零
        n_predict = 1 if n_predict is None else int(n_predict)  # 如果输入为空,默认预测 1 年

    # **步骤 2:计算年度电影数量**
    # 从数据集中按年份统计电影数量,补全空缺年份为 0
    year_counts = df['Year'].value_counts().sort_index()  # 按年份统计电影数量并排序
    year_counts = year_counts.reindex(
        range(year_counts.index.min(), year_counts.index.max() + 1), 
        fill_value=0  # 缺失年份的电影数量填充为 0
    )

    # **步骤 3:训练多项式回归模型**
    # 准备训练数据:年份作为特征,电影数量作为目标值
    X = np.array(year_counts.index).reshape(-1, 1)  # 将年份转换为二维数组
    y = year_counts.values  # 获取对应的电影数量
    # 使用三次多项式扩展年份特征并训练回归模型
    model = LinearRegression().fit(PolynomialFeatures(3).fit_transform(X), y)

    # **步骤 4:生成未来年份并预测电影数量**
    # 根据用户输入的预测年数,生成未来年份
    future_years = np.array(
        [year_counts.index[-1] + i for i in range(1, n_predict + 1)]  # 从最后一年开始往后扩展
    ).reshape(-1, 1)
    # 使用模型预测未来年份的电影数量
    predicted = model.predict(PolynomialFeatures(3).fit_transform(future_years))

    # **步骤 5:逐步更新图表**
    # 如果定时器触发次数未超过预测年份范围,更新预测数据
    if n_intervals <= len(future_years):
        # 当前需要显示的预测年份和电影数量
        pred_years = np.concatenate(([year_counts.index[-1]], future_years[:n_intervals + 1].flatten()))
        pred_counts = np.concatenate(([year_counts.values[-1]], predicted[:n_intervals + 1]))

        # 返回更新后的图表内容
        return {
            'data': [
                # 折线图 1:实际数据
                go.Scatter(
                    x=year_counts.index,  # 横轴:已有年份
                    y=year_counts.values,  # 纵轴:实际电影数量
                    name='Actual Data',  # 图例名称
                    mode='lines+markers'  # 显示折线和数据点
                ),
                # 折线图 2:逐步更新的预测数据
                go.Scatter(
                    x=pred_years,  # 横轴:逐步更新的预测年份
                    y=pred_counts,  # 纵轴:逐步更新的预测电影数量
                    name='Predicted Data',  # 图例名称
                    line=dict(dash='dot')  # 设置为虚线样式
                )
            ],
            'layout': {
                'title': 'Movie Count Trend Prediction',  # 图表标题
                'xaxis': {'title': 'Year'},  # 横轴标题
                'yaxis': {'title': 'Movie Count'}  # 纵轴标题
            }
        }, n_intervals  # 更新触发次数并返回

    # **步骤 6:超出预测范围时不更新**
    # 如果触发次数超过了预测范围,保持图表内容不变
    return dash.no_update, dash.no_update

4. 启动应用
最后,我们运行应用程序,体验动态更新的效果:

if __name__ == '__main__':
    app.run_server(debug=True)

在这里插入图片描述
接下来,我们再画一个动态散点图,X轴表示观众评分,Y轴表示票房,点的大小表示时长,颜色表示专家评分。

新增的功能与组件

  1. 新增布局组件
    我们在布局中新增了一个 dcc.Graph 用于显示散点图,并添加了两个交互组件:
    • dcc.Input:用于选择年份;
    • html.Button:用于控制动画播放和暂停,以及显示用户选择的年份。
  2. 动态回调函数
    新增回调函数负责:
    • 根据用户输入的年份或动画播放的年份更新散点图;
    • 控制动画播放与暂停。

具体功能如下:

  1. 播放/暂停按钮 (html.Button):控制动画播放状态;
    • id=‘play-pause-scatter’ 用于切换定时器的状态(播放或暂停)。
    • 每次点击按钮,动画就会开始或暂停,具体通过 id=‘play-pause-scatter’ 来识别这个按钮。
  2. 年份输入框 (dcc.Input):允许用户输入特定年份;
    • 这是一个小输入框,用户可以在这里输入想要查看的年份。比如你想看某一年的电影数据,只需要输入年份就可以。
    • placeholder 显示提示文字,引导用户输入;
    • min 和 max 限定年份范围,防止输入无效数据。
  3. 散点图组件 (dcc.Graph):用来显示动态更新的散点图;
    • 这个就是动态散点图的主角!图表会根据年份显示电影的评分和票房关系;
    • 通过 id=‘runtime-rating-scatter’,这个图表会和回调函数连接起来。
  4. 定时器组件 (dcc.Interval):
    • 这是动画的幕后控制器!它就像一个“时钟”,每隔一秒触发一次更新,让散点图根据年份变化逐步显示数据;
    • 一开始它是关闭的(disabled=True),只有在点击播放按钮后,才会启动动画;
    • id=‘interval-scatter’ 独立于折线图的定时器,用于更新散点图;
    • interval=1000 表示每秒更新一次;
# 在已有布局中新增以下代码
html.Div([
    html.Button('Play/Pause Scatter', id='play-pause-scatter', n_clicks=0),  # 播放/暂停按钮
    dcc.Input(
        id='year-input',  # 输入框 ID
        type='number',  # 限定输入为数字
        placeholder='Enter year',  # 输入框提示文字
        min=df['Year'].min(),  # 最小年份
        max=df['Year'].max()   # 最大年份
    ),
    html.Button('Show Year', id='show-year-button', n_clicks=0)  # 显示年份按钮
]),
dcc.Graph(id='runtime-rating-scatter'),  # 散点图
dcc.Interval(id='interval-scatter', interval=1000, n_intervals=0, disabled=True)  # 动态更新定时器

回调部分: 加入散点图与控制动画

  1. 更新散点图
    年份选择逻辑:
    • 如果我们输入有效年份并点击 Show Year 按钮(比如 2010 年),图表会直接跳转到显示这一年的数据;
    • 如果用户没有输入年份,而是点击了播放按钮,图表会进入自动播放模式,按 n_intervals 动态更新年份,每秒显示下一年的数据,就像一个“按年份播放的电影时间线。
    点的样式:
    • size:点大小与电影时长成正比;
    • color:点颜色由专家评分决定;
    • text:显示电影标题,便于用户识别。
@app.callback(
    Output('runtime-rating-scatter', 'figure'),  # 更新散点图内容
    [Input('interval-scatter', 'n_intervals'), Input('show-year-button', 'n_clicks')],  # 动态播放和用户选择触发
    [State('year-input', 'value')]  # 获取用户输入的年份
)

# n_intervals:定时器的触发次数,用于动态更新年份。
# n_clicks:用户点击 "Show Year" 按钮的次数,用于选择特定年份。
# selected_year:用户输入的年份,用于直接展示该年份的数据。
def update_scatter(n_intervals, n_clicks, selected_year):
    # 1. 确定要显示的年份
    if dash.callback_context.triggered_id == 'show-year-button' and selected_year is not None:
        year = selected_year  # 用户选择的年份
    else:
        year = df['Year'].min() + (n_intervals or 0)  # 动态播放时的年份
        if year > df['Year'].max():  # 如果超出最大年份范围,停止更新
            return dash.no_update

    # 2. 获取指定年份的数据
    current_data = df[df['Year'] == year]

    # 3. 返回散点图
    return {
        'data': [go.Scatter(
            x=current_data['Rating'],  # X轴:观众评分
            y=current_data['Revenue (Millions)'],  # Y轴:票房收入
            mode='markers',  # 点图模式
            marker=dict(
                size=current_data['Runtime (Minutes)'] / 10,  # 点大小:时长
                color=current_data['Metascore'],  # 点颜色:专家评分
                colorscale='Viridis',  # 颜色渐变
                colorbar=dict(title="Metascore"),  # 颜色条标题
                showscale=True  # 显示颜色条
            ),
            text=current_data['Title']  # 点的标签显示电影标题
        )],
        'layout': {
            'title': f'Movie Rating vs Revenue in {year}',  # 图表标题显示年份
            'xaxis': {'title': 'Rating'},  # X轴标题
            'yaxis': {'title': 'Revenue (Millions)'}  # Y轴标题
        }
    }
  1. 控制动画播放与暂停
    • 每次点击按钮:按钮就像一个开关,每次点击都会切换定时器(interval-scatter)的状态:
    • 暂停时禁用定时器:当动画暂停时,定时器会被禁用(disabled=True),停止触发动画更新。
    • 重新播放时重置计数器:当重新播放时,n_intervals 会从头开始计数,确保动画从第一个年份重新播放。
@app.callback(
    [Output('interval-scatter', 'disabled'), Output('interval-scatter', 'n_intervals')],  # 控制定时器状态和触发次数
    [Input('play-pause-scatter', 'n_clicks')],  # 点击播放/暂停按钮触发
    [State('interval-scatter', 'disabled')]  # 当前定时器状态
)
#  n_clicks:按钮的点击次数。
# current_state:当前定时器的状态(启用或禁用)。
def toggle_scatter_animation(n_clicks, current_state):
    
    # 每次点击切换播放状态,并重置触发次数
    return (not current_state, 0) if n_clicks else (True, 0)

恭喜你! 到现在为止,我们已经把新功能一块一块地搭建起来了,包括播放/暂停按钮、年份选择、以及动态散点图的动画效果。为了确保大家清楚每一部分代码应该放哪里,我准备了完整的代码,把刚刚讲解的所有新功能加入到已有的面板中。
以下是第四个面板的完整代码:

import pandas as pd
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State
import plotly.graph_objs as go
 
df = pd.read_csv('imdb_movie_dataset.csv')
df.drop(['Rank'], axis=1, inplace=True)
for col in ['Revenue (Millions)', 'Runtime (Minutes)', 'Rating']:
    df[col] = pd.to_numeric(df[col], errors='coerce')
df.dropna(inplace=True)
df['Year'] = df['Year'].astype(int)
 
app = dash.Dash(__name__)
 
app.layout = html.Div([
    html.Div([
        html.Label("Enter number of years to predict:"),
        dcc.Input(id='n-predict-input', type='number', value=1, min=1),
    ]),
    dcc.Graph(id='movie-count-animation'),
    
    html.Div([
        html.Button('Play/Pause Scatter', id='play-pause-scatter', n_clicks=0),
        dcc.Input(id='year-input', type='number', placeholder='Enter year', 
                 min=df['Year'].min(), max=df['Year'].max()),
        html.Button('Show Year', id='show-year-button', n_clicks=0),
    ]),
    dcc.Graph(id='runtime-rating-scatter'),
    
    dcc.Interval(id='interval-prediction', interval=1000, n_intervals=0, disabled=False),
    dcc.Interval(id='interval-scatter', interval=1000, n_intervals=0, disabled=True)
])
 
@app.callback(
    [Output('movie-count-animation', 'figure'), Output('interval-prediction', 'n_intervals')],
    [Input('interval-prediction', 'n_intervals'), Input('n-predict-input', 'value')]
)
def update_prediction(n_intervals, n_predict):
    if n_predict is None or dash.callback_context.triggered_id == 'n-predict-input':
        n_intervals = 0
        n_predict = 1 if n_predict is None else int(n_predict)
    
    year_counts = df['Year'].value_counts().sort_index()
    year_counts = year_counts.reindex(range(year_counts.index.min(), year_counts.index.max() + 1), fill_value=0)
    
    X = np.array(year_counts.index).reshape(-1, 1)
    model = LinearRegression().fit(PolynomialFeatures(3).fit_transform(X), year_counts.values)
    
    future_years = np.array([year_counts.index[-1] + i for i in range(1, n_predict + 1)]).reshape(-1, 1)
    predicted = model.predict(PolynomialFeatures(3).fit_transform(future_years))
    
    if n_intervals <= len(future_years):
        pred_years = np.concatenate(([year_counts.index[-1]], future_years[:n_intervals + 1].flatten()))
        pred_counts = np.concatenate(([year_counts.values[-1]], predicted[:n_intervals + 1]))
        
        return {
            'data': [
                go.Scatter(x=year_counts.index, y=year_counts.values, name='Actual Data'),
                go.Scatter(x=pred_years, y=pred_counts, name='Predicted Data', line=dict(dash='dot'))
            ],
            'layout': {
                'title': 'Movie Count Trend Prediction',
                'xaxis': {'title': 'Year'},
                'yaxis': {'title': 'Movie Count'}
            }
        }, n_intervals
    return dash.no_update, dash.no_update
 
@app.callback(
    Output('runtime-rating-scatter', 'figure'),
    [Input('interval-scatter', 'n_intervals'), Input('show-year-button', 'n_clicks')],
    [State('year-input', 'value')]
)
def update_scatter(n_intervals, n_clicks, selected_year):
    if dash.callback_context.triggered_id == 'show-year-button' and selected_year is not None:
        year = selected_year
    else:
        year = df['Year'].min() + (n_intervals or 0)
        if year > df['Year'].max():
            return dash.no_update
    
    current_data = df[df['Year'] == year]
    return {
        'data': [go.Scatter(
            x=current_data['Rating'],
            y=current_data['Revenue (Millions)'],
            mode='markers',
            marker=dict(
                size=current_data['Runtime (Minutes)'] / 10,
                color=current_data['Metascore'],
                colorscale='Viridis',
                colorbar=dict(title="Metascore"),
                showscale=True
            ),
            text=current_data['Title']
        )],
        'layout': {
            'title': f'Movie Rating vs Revenue in {year}',
            'xaxis': {'title': 'Rating'},
            'yaxis': {'title': 'Revenue (Millions)'}
        }
    }
 
@app.callback(
    [Output('interval-scatter', 'disabled'), Output('interval-scatter', 'n_intervals')],
    [Input('play-pause-scatter', 'n_clicks')],
    [State('interval-scatter', 'disabled')]
)
def toggle_scatter_animation(n_clicks, current_state):
    return (not current_state, 0) if n_clicks else (True, 0)
 
if __name__ == '__main__':
    app.run_server(debug=True)

友情提示:完整代码中每一部分都放在正确的位置,回调函数和组件布局的定义顺序都已经安排妥当,所以直接复制粘贴到你的开发环境就能跑起来!
在这里插入图片描述
在这里插入图片描述

至此,第四个仪表盘也完成啦,本人也是初次接触,有任何想法欢迎各位批评与讨论!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐