Python Dash 进阶教程:打造互动式数据可视化仪表盘
欢迎来到 “Python Dash 进阶教程”!在之前的教程中,我们已经学习了如何利用 Dash 构建一个基础的 IMDB 数据仪表盘,了解了 Dash 的核心概念和一些常用功能。而现在,随着对 Dash 组件和 Python 的更深入了解,接下来我将带你一步步进入一个更复杂、更具互动性的数据可视化仪表盘。在分析电影数据时,电影类型分布是一个重要的维度。例如,哪些类型的电影数量最多?哪些类型是近几
第二个仪表盘:统计电影类型数量,探究类型分布规律
欢迎来到 “Python Dash 进阶教程”!
在之前的教程中,我们已经学习了如何利用 Dash 构建一个基础的 IMDB 数据仪表盘,了解了 Dash 的核心概念和一些常用功能。而现在,随着对 Dash 组件和 Python 的更深入了解,接下来我将带你一步步进入一个更复杂、更具互动性的数据可视化仪表盘。
我们还是使用之前的数据:IMDB Movie Dataset
在分析电影数据时,电影类型分布是一个重要的维度。例如,哪些类型的电影数量最多?哪些类型是近几年比较热门的?但在原始数据中,一个电影可能属于多个类型(如既是动作片又是喜剧片)。为了准确统计各类型的数量,我们需要对数据进行处理。
我们将通过以下步骤完成:
- 加载数据并清洗无关列。
- 处理电影收入和类型数据,拆分多类型为独立行。
- 统计每种类型的电影数量。
第一步:加载数据并移除无用列
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 中的组件发生变动(即用户产生点击等交互动作时),回调函数会自动调用更新函数,将用户交互后所要呈现的图返回到页面上,实现页面与用户的实时交互。
回调与更新的工作原理:
- 用户在复选框中选择或取消某些类型;
- 复选框的值变化触发回调函数;
- 回调函数调用更新函数,将过滤后的数据重新绘制成柱状图和饼图;
- 页面实时更新,展示新的图表内容。
最后,我们运行该应用:
# 运行应用
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. 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)
运行代码后,我们得到以下结果:
我们可以看到:
- 模型的 R² 分数为 0.9506,说明模型拟合效果较好。
- MAE 表明预测值与真实值的平均误差为 7.6 部电影,说明误差较小。
- 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. 数据加载与预处理
在上一个仪表盘中,我们通过回归模型预测了后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 应用布局
接着,我们在布局中新增了两个核心组件:
- 输入框(dcc.Input):
• 提供用户交互功能,允许输入预测年数;
• 参数 value 定义默认值,min 确保输入至少为 1。 - 定时器组件(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. 动态更新图表的回调函数
“回调函数是动态仪表盘的核心,用于实现以下逻辑:
- 用户输入预测年数后,重置定时器并重新计算预测结果;
• 如果用户修改输入值,回调函数会重置 n_intervals 并重新计算预测结果。 - 按时间间隔逐步更新预测曲线,实现动态显示。”
• 通过 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轴表示票房,点的大小表示时长,颜色表示专家评分。
新增的功能与组件
- 新增布局组件
我们在布局中新增了一个 dcc.Graph 用于显示散点图,并添加了两个交互组件:
• dcc.Input:用于选择年份;
• html.Button:用于控制动画播放和暂停,以及显示用户选择的年份。 - 动态回调函数
新增回调函数负责:
• 根据用户输入的年份或动画播放的年份更新散点图;
• 控制动画播放与暂停。
具体功能如下:
- 播放/暂停按钮 (html.Button):控制动画播放状态;
• id=‘play-pause-scatter’ 用于切换定时器的状态(播放或暂停)。
• 每次点击按钮,动画就会开始或暂停,具体通过 id=‘play-pause-scatter’ 来识别这个按钮。 - 年份输入框 (dcc.Input):允许用户输入特定年份;
• 这是一个小输入框,用户可以在这里输入想要查看的年份。比如你想看某一年的电影数据,只需要输入年份就可以。
• placeholder 显示提示文字,引导用户输入;
• min 和 max 限定年份范围,防止输入无效数据。 - 散点图组件 (dcc.Graph):用来显示动态更新的散点图;
• 这个就是动态散点图的主角!图表会根据年份显示电影的评分和票房关系;
• 通过 id=‘runtime-rating-scatter’,这个图表会和回调函数连接起来。 - 定时器组件 (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) # 动态更新定时器
回调部分: 加入散点图与控制动画
- 更新散点图
年份选择逻辑:
• 如果我们输入有效年份并点击 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轴标题
}
}
- 控制动画播放与暂停
• 每次点击按钮:按钮就像一个开关,每次点击都会切换定时器(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)
友情提示:完整代码中每一部分都放在正确的位置,回调函数和组件布局的定义顺序都已经安排妥当,所以直接复制粘贴到你的开发环境就能跑起来!
至此,第四个仪表盘也完成啦,本人也是初次接触,有任何想法欢迎各位批评与讨论!
更多推荐
所有评论(0)