Plotly-和-Dash-智能仪表盘和数据应用-全-
Plotly 和 Dash 智能仪表盘和数据应用(全)
原文:
annas-archive.org/md5/e5313a0f17775ffeed6651154fcfc198译者:飞龙
前言
Plotly 的 Dash 框架允许 Python 程序员开发完整的分析数据应用程序和交互式仪表板。本书将帮助你探索 Dash 在不同方式下可视化数据的功能,并从数据中获取最大的价值。
你将从 Dash 生态系统、主要包及可用的第三方包(这些对于构建和结构化应用程序的不同部分至关重要)的概述开始。接下来,你将学习如何创建一个基础的 Dash 应用程序并为其添加不同的功能。然后,你将集成如下拉框、复选框、滑块、日期选择器等控件,并将它们与图表和其他输出连接。根据你可视化的数据,你还将添加几种类型的图表,包括散点图、折线图、柱状图、直方图、地图等,并探索定制这些图表的选项。
本书结束时,你将掌握创建和部署交互式仪表板所需的技能,能够处理复杂性和代码重构,并理解改善应用程序的过程。
本书适合人群
这本 Plotly Dash 书籍适合数据专业人士和数据分析师,他们希望通过不同的可视化和仪表板更好地理解他们的数据。希望读者具备基本到中级的 Python 编程知识,以便更有效地掌握书中涉及的概念。
本书内容
第一章,Dash 生态系统概述,将帮助你深入了解 Dash 生态系统、主要使用的包以及可用的第三方包。在本章结束时,你将能够区分应用程序的不同元素及其各自的职责,并且你将构建一个最简化的应用程序。
第二章,探索 Dash 应用程序的结构,展示了如何为我们之前创建的应用程序添加一些交互功能。我们将逐步了解应用程序的回调函数,并查看它们如何让用户连接应用程序的可视化元素,以及如何通过创建特定的回调函数让用户使用某些元素来控制其他元素。
第三章,使用 Plotly 的 Figure 对象,深入介绍了 Figure 对象及其组件,如何操作它以及如何将其转换为不同的格式。稍后,我们将利用这一理解来根据应用程序的需要构建特定类型的图表。
第四章,数据操作和准备 - 通向 Plotly Express,介绍了整洁数据的概述,以及高级别的 Plotly Express 包,展示了如何使用图形语法轻松生成图表并将数据映射到视觉元素。
第五章,使用柱状图和下拉菜单交互比较值,深入探讨了图表的可用选项,并探索了进一步的可能性。然后,我们将看到如何允许用户使用下拉菜单选择要比较的值。
第六章,使用散点图探索变量并使用滑块过滤子集,转向使用最频繁的图表类型之一:散点图。与条形图类似,我们将看到许多不同的自定义方法。散点图提供了更多选项,我们将探索这些选项,如将点的大小映射到某个变量,处理过度绘制以及处理大量数据点。
第七章,探索地图绘图并丰富您的 Markdown 仪表板,探索了许多情况下我们看到的一种新类型的图表。在地图上绘制数据有许多方法,我们将探索两种最常用的类型:散点图和区域图。
第八章,计算数据频率和构建交互式表格,探讨了创建直方图及其自定义方式的不同方法,以及按不同方式拆分数据并计算结果值的过程。
第九章,让您的数据通过机器学习自行表达,向我们展示了聚类的工作原理,并使用测试模型评估性能。我们还将探讨一种评估各种聚类的技术,最后,我们将使用 Kmeans 设计一个交互式应用程序。
第十章,通过高级回调使您的应用程序快速启动,介绍了模式匹配回调以实现基于用户交互和各种其他条件的动态应用程序修改。
第十一章,URL 和多页应用程序,介绍了一种新的架构,允许我们在一个应用程序中集成多个应用程序(每个对应一个页面)。我们将探索的另一个有趣特性是使用 URL 作为输入或输出,与应用程序中的不同元素进行交互。
第十二章,部署您的应用程序,向您展示如何在服务器上部署您的应用程序,使人们可以从任何地方访问它,以便与世界分享。有多种选择可用,我们将介绍两种可能有用的简单选项。
第十三章,下一步,展示了将您的应用程序提升到下一个层次的不同选择。本章提供了一些您可能想要探索的建议。
最大限度地发挥本书的作用
您需要一台连接良好的互联网的系统以及一个 AWS 账户。
如果您使用的是本书的数字版,我们建议您自己输入代码,或者通过 GitHub 仓库访问代码(链接在下节提供)。这样做有助于避免与复制和粘贴代码相关的潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,地址为:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash。如果代码有更新,它将更新到现有的 GitHub 仓库中。
我们的丰富书籍和视频目录中还有其他代码包,您可以在github.com/PacktPublishing/查看它们!
下载彩色图片
我们还提供了一个 PDF 文件,里面包含了本书中使用的截图/图表的彩色图片。您可以在此下载:static.packt-cdn.com/downloads/9781800568914_ColorImages.pdf。
代码实践
本书的《代码实践》视频可以通过以下链接观看 (bit.ly/3vaXYQJ)。
使用的约定
本书中使用了许多文本约定。
文中代码:表示文中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 用户名。例如: "我们的数据集将包含根目录下data文件夹中的文件。"
一块代码如下所示:
import plotly.express as px
gapminder = px.data.gapminder()
gapminder
当我们希望您注意代码块的某一部分时,相关的行或项目会以粗体显示:
import os
import pandas as pd
pd.options.display.max_columns = None
os.listdir(‚data')
[‚PovStatsSeries.csv',
'PovStatsCountry.csv',
'PovStatsCountry-Series.csv',
'PovStatsData.csv',
'PovStatsFootNote.csv']
粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的词汇通常以这种方式显示。示例如下:"另一个重要的列是限制和例外列。"
提示或重要说明
显示如下。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提到书名,并通过电子邮件联系我们:customercare@packtpub.com。
勘误:虽然我们已经尽力确保内容的准确性,但错误难免。如果您在本书中发现错误,恳请您向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接,并填写相关详情。
盗版:如果您在互联网上发现我们作品的任何非法版本,恳请您提供相关的网址或网站名称。请通过 copyright@packt.com 与我们联系,并附上相关链接。
如果您有兴趣成为作者:如果您在某个领域有专长,并且有兴趣撰写或参与编写一本书,请访问 authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,为什么不在您购买本书的网站上留下评论呢?潜在的读者可以通过您的公正评价来做出购买决策,我们在 Packt 也能了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。感谢您的支持!
欲了解更多关于 Packt 的信息,请访问 packt.com。
第一部分:构建 Dash 应用程序
本节提供了 Dash 生态系统的概述,并展示了如何通过一个最小功能的应用程序入门。
本节包括以下章节:
-
第一章**,Dash 生态系统概述
-
第二章**,探索 Dash 应用程序的结构
-
第三章**,使用 Plotly 的 Figure 对象
-
第四章**,数据处理与准备 - 为 Plotly Express 铺路
第一章:第一章:Dash 生态系统概述
在我们处理数据的工作中,唯一不变的因素就是数据的数量、来源和类型的变化。能够快速地结合来自不同来源的数据并进行探索是至关重要的。Dash不仅仅用于探索数据,它几乎可以用于数据分析过程中的所有阶段,从探索到生产环境中的操作。
在本章中,我们将概述 Dash 的生态系统,并重点关注构建应用程序的布局部分,即面向用户的部分。到本章结束时,您将能够构建一个运行中的应用程序,并能够使用几乎任何您想要的视觉组件,但没有交互性。
本章将涵盖以下主题:
-
设置您的开发环境
-
探索 Dash 和其他支持包
-
了解 Dash 应用程序的一般结构
-
创建并运行最简单的应用程序
-
将 HTML 和其他组件添加到应用程序中
-
学习如何结构化布局和管理主题
技术要求
每一章的要求可能略有不同,但有些要求是全书通用的。
您需要访问 Python 3.6 或更高版本,可以从www.python.org轻松下载,并且需要一个文本编辑器或集成开发环境(IDE),以便编辑代码。
在本章中,我们将使用Dash、Dash HTML Components和Dash Bootstrap Components,这些可以通过以下部分的说明与其他所需的包一起安装。所有本书所需的代码和数据可以从本书的 GitHub 仓库下载,仓库地址为github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash。正如我刚才提到的,以下部分将详细展示如何开始设置环境。
本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_01。
查看以下视频,观看代码的实际运行:bit.ly/3atXPjc。
设置您的开发环境
由于书中使用的所有包的更新速度很快,您很可能会遇到一些功能上的差异,因此,为了复现书中描述的确切结果,您可以克隆书籍的仓库,安装使用的包(指定版本),并使用包含的数据集。从命令行进入您想要构建项目的文件夹,并执行以下操作:
-
在一个名为
dash_project的文件夹中创建一个 Python 虚拟环境(或者你想要的任何其他名称)。这也会创建一个与你选择的名称相同的新文件夹:python3 –m venv dash_project -
激活虚拟环境。
在 Unix 或 macOS 上,运行以下命令:
source dash_project/bin/activate在 Windows 上,运行以下命令:
dash_project\Scripts\activate.bat -
进入创建的文件夹:
cd dash_project -
克隆这本书的 GitHub 仓库:
git clonehttps://github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash -
你现在应该有一个包含所需包及其版本的文件,名为
requirements.txt。你可以通过进入仓库文件夹并运行以下install命令来安装这些包:cd Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/ pip install -r requirements.txt
你应该在data文件夹中找到数据集的副本,数据集是从这个链接下载的:datacatalog.worldbank.org/dataset/poverty-and-equity-database。如果你想要,仍然可以获取最新版本,但和软件包一样,如果你想获得相同的结果,最好使用提供的数据集。
为了使 Plotly 图形和应用能够在 JupyterLab 中显示,你需要安装 Node.js,可以从nodejs.org安装。
你还需要安装 JupyterLab Plotly 扩展,可以通过在你的虚拟环境中从命令行运行以下命令来完成:
jupyter labextension install jupyterlab-plotly@4.14.1
注意,最后的版本号应与您正在运行的 Plotly 版本相对应。如果你想升级,可以替换前面的版本号(确保也升级 Plotly 的 Python 包)。
一旦你运行了之前的代码,你应该就有了跟随的所有必要条件。你会发现这本书的每一章都在前一章的基础上构建:我们将构建一个应用程序,随着章节的进行,逐步增加更多的功能和复杂性。
主要目标是尽可能将你置于一个实际的环境中。一般来说,创建任何独立的 Dash 组件是直接的,但当你已经在运行的应用中有一些组件时,它就变得更具挑战性。当你需要决定如何调整布局以适应新的变化,并且如何重构代码时,这一点变得尤为明显,需要专注于细节,但又不失大局。
现在环境已经建立,让我们来概览一下 Dash。
探索 Dash 及其他支持包
尽管不是严格必要的,但了解构建 Dash 及其依赖项的主要组件还是很有帮助的,尤其是对于更高级的用法,并且可以帮助你了解如何以及在哪里获取更多信息:

图 1.1 – Dash 的组成
注意
使用 Dash 的一个主要优点是,它允许我们使用纯 Python 创建完全交互的数据、分析和 Web 应用程序和界面,而无需担心 HTML、CSS 或 JavaScript。
如图 1.1 所示,Dash 使用 Flask 作为后台。为了生成图表,它使用 Plotly,尽管这并非强制要求,但它是数据可视化中最受支持的包。React 用于处理所有组件,实际上一个 Dash 应用程序就是作为一个单页 React 应用渲染的。对我们来说,最重要的是我们将在创建应用时使用的不同包,我们将在接下来的内容中讲解它们。
提示
对于熟悉或有兴趣学习 Matplotlib 的人,有一套专门的工具可以将 Matplotlib 图形转换为 Plotly 图形。你在 Matplotlib 中创建图形后,可以通过一个命令将其转换为 Plotly:mpl_to_plotly。截至本文撰写时,仅支持 Matplotlib<=3.0.3。以下是一个完整示例:
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
from plotly.tools import mpl_to_plotly
mpl_fig, ax = plt.subplots()
ax.scatter(x=[1, 2, 3], y=[23, 12, 34])
plotly_fig = mpl_to_plotly(mpl_fig)
plotly_fig
Dash 包含的不同包
Dash 不是一个包含所有功能的大型包。相反,它由几个包组成,每个包处理特定的方面。此外,正如我们稍后会看到的,还有一些第三方包被使用,社区也鼓励通过创建特殊的 Dash 包来开发自己的功能。
以下是我们在本章中主要使用的包,后续章节我们还将探索其他包:
-
dash.Dash对象。它还提供了一些用于管理交互性和异常的工具,我们将在构建应用程序时深入了解这些工具。 -
Dash 核心组件:一个提供一组可供用户操作的交互式组件的包。下拉框、日期选择器、滑动条等多种组件都包含在此包中。我们将在 第二章《探索 Dash 应用结构》中学习如何使用这些组件来管理响应式操作,并将在本书的 第二部分 中详细讨论如何使用它们。
-
在 Python 中,
dash_html_components.H1('Hello, World')会被转换为<h1>Hello, World</h1>并在浏览器中呈现出来。 -
从命令行运行
pip install dash。如果是升级,则使用pip install dash --upgrade。
现在我们将简要了解典型 Dash 应用的一般结构,之后我们将开始编写代码。
了解 Dash 应用的一般结构
以下图示展示了创建 Dash 应用程序的一般步骤。我们通常有一个名为 app.py 的文件,虽然你可以任意命名该文件。该文件显示为右侧的列,不同的部分通过线条分隔,目的是为了视觉上区分它们,而左侧则是每个部分的名称:

图 1.2 – Dash 应用结构
让我们详细看看每个应用部分:
-
导入(模板代码): 和任何 Python 模块一样,我们首先导入所需的包,并使用它们的常见别名。
-
这里的
app变量。name参数的__name__值用于让 Dash 方便地定位将用于应用的静态资源。 -
图中的
html.Div,它接受一个组件列表作为其children参数。这些组件将在应用渲染时按顺序显示,每个都位于前一个元素的下方。在接下来的部分中,我们将创建一个具有最小布局的简单应用程序。 -
回调函数:这是第二章的主题,探索 Dash 应用程序的结构,我们将在其中详细讲解交互性如何工作;本章不涉及这一内容。目前,只需要知道在这里我们可以定义任意数量的函数,将应用程序的可视元素互相连接,定义我们想要的功能。通常,函数是独立的,它们不需要定义在容器内,且函数的顺序在模块中没有关系。
-
运行应用程序:使用 Python 运行模块作为脚本的习惯,我们来运行应用程序。
正如我承诺的那样,我们现在准备好开始编写代码了。
创建并运行最简单的应用程序
使用我们刚才讨论的结构,并排除回调函数,接下来我们来构建第一个简单的应用程序!
创建一个文件并命名为app.py,然后编写以下代码:
-
使用它们通常的别名导入所需的包:
import dash import dash_html_components as html -
创建(实例化)应用程序:
app = dash.Dash(__name__) -
创建应用程序的布局:
app.layout = html.Div([ html.H1('Hello, World!') ]) -
运行应用程序:
if __name__ == '__main__': app.run_server(debug=True)
在运行应用程序之前,有几点需要说明。首先,我强烈建议你不要复制粘贴代码。记住自己编写的代码非常重要。探索每个组件、类或函数提供的可能性也很有用。大多数集成开发环境(IDE)会提供提示,告诉你可能的操作。
这个应用程序的布局包含一个元素,就是传递给html.Div的列表,作为其children参数。这将在页面上生成一个 H1 元素。最后,请注意,我在app.run_server方法中设置了debug=True。这会激活一些开发者工具,在开发和调试时非常有用。
现在你已经准备好运行第一个应用程序了。在命令行中,在你保存应用文件的同一个文件夹里,运行以下命令:
python app.py
如果你的系统没有默认配置为使用版本三,你可能需要使用python3来运行前面的命令:
python3 app.py
现在你应该会看到类似于图 1.3 所示的输出,表示应用程序正在运行:

图 1.3 – 运行应用程序时的命令行输出
恭喜你成功运行了你的第一个 Dash 应用!现在,如果你将浏览器指向输出中显示的 URL:http://127.0.0.1:8050,你应该会看到页面上 H1 中显示的“Hello, World!”消息。正如你所看到的,它显示了正在提供名为“app”的 Flask 应用,并且有一个警告,说明该服务器不适合用于生产环境。我们将在后续章节讨论部署问题,但这个服务器足够用于开发和测试你的应用。你还可以看到我们处于调试模式:

图 1.4 – 在浏览器中渲染的应用
如上所述,我们看到文本以 H1 的形式显示,我们还可以看到蓝色按钮。点击此按钮将打开浏览器中的一些选项,在有回调函数和/或运行应用时出错时,它将更加有用。如果我们将应用以 debug=False 运行(默认设置),则不会看到蓝色按钮。
现在我们已经对创建 Dash 应用的主要元素有了足够的了解,并且已经成功运行了一个最小化的应用,我们可以开始探索两个用于添加和管理可视化元素的包:首先是 Dash HTML 组件,接下来我们将学习如何使用 Dash Bootstrap 组件。
向应用添加 HTML 和其他组件
从现在到本章节结束,我们将主要关注应用的 app.layout 属性,并对其进行修改。这样做很简单;我们只需将元素添加到顶级 html.Div 元素的列表(children 参数)中:
html.Div(children=[component_1, component_2, component_3, …])
向 Dash 应用添加 HTML 组件
由于该包中的可用组件对应于实际的 HTML 标签,因此它是最稳定的包。让我们快速浏览一下所有组件共有的参数。
截至本文撰写时,Dash HTML 组件共有 131 个组件,并且有 20 个参数是所有组件共有的。
让我们来了解一些我们将频繁使用的重要参数:
-
children:通常这是组件内容的主要(也是第一个)容器。它可以是一个项目的列表,也可以是单个项目。 -
className:这与class属性相同,只是重命名为此。 -
id:虽然我们在本章节中不会详细讲解这个参数,但它是在实现交互功能时至关重要的参数,我们将在构建应用时广泛使用它。目前,知道你可以为组件设置任意 ID,以便在之后识别它们并用于管理交互功能就足够了。 -
style:这与同名的 HTML 属性类似,但有一些区别。首先,它的属性是使用驼峰命名法设置的。所以,假设你想在 Dash HTML 组件中设置以下属性:<h1 style="color:blue; font-size: 40px; margin-left: 20%">A Blue Heading</h1>你可以这样指定它们:
import dash_html_components as html html.H1(children='A Blue Heading', style={'color': 'blue', 'style attribute is set using a Python dictionary.
其他参数有不同的用途和规则,取决于它们所属于的各自组件。现在我们来练习将一些 HTML 元素添加到我们的应用中。回到相同的app.py文件,让我们尝试添加更多的 HTML 元素,并再次运行应用,就像我们刚才做的那样。我保持了顶部和底部部分不变,主要编辑了app.layout:
…
app = dash.Dash(__name__)
app.layout = html.Div([
html.H1('Poverty And Equity Database',
style={'color': 'blue',
'fontSize': '40px'}),
html.H2('The World Bank'),
html.P('Key Facts:'),
html.Ul([
html.Li('Number of Economies: 170'),
html.Li('Temporal Coverage: 1974 - 2019'),
html.Li('Update Frequency: Quarterly'),
html.Li('Last Updated: March 18, 2020'),
html.Li([
'Source: ',
html.A('https://datacatalog.worldbank.org/dataset/poverty-and-equity-database', href='https://datacatalog.worldbank.org/dataset/poverty-and-equity-database')
])
])
])
…
python app.py
这应该会生成如下屏幕:

图 1.5 – 浏览器中渲染的更新应用
提示
如果你熟悉 HTML,这应该看起来很直观。如果不熟悉,请查看在线的基本教程。一个很好的起点是 W3Schools:www.w3schools.com/html/。
在更新部分,我们只添加了一个<p>元素和一个无序列表<ul>,在其中我们添加了几个列表项<li>(使用 Python 列表),最后一个项包含了一个使用<a>元素的链接。
请注意,由于这些组件是作为 Python 类实现的,它们遵循 Python 的类名首字母大写的命名约定:html.P、html.Ul、html.Li、html.A等。
随意尝试其他选项:添加新的 HTML 组件、改变顺序、尝试设置其他属性等等。
学习如何构建布局和管理主题
到目前为止,我们已经讨论了 Dash 应用的基本结构,并简要概述了其主要元素:导入、应用实例化、应用布局、回调(将在下一章中讲解)以及运行应用。我们创建了一个基础的应用程序,然后学习了如何向其中添加一些 HTML 元素。现在,我们已经准备好从布局的角度将应用提升到下一个层次。我们将继续使用app.layout属性,并通过 Dash Bootstrap 组件包以更强大和灵活的方式控制它。
Bootstrap 基本上是一套工具,它将许多细节抽象化,用于处理网页的布局。以下是使用它的一些最重要的好处:
-
主题:正如我们稍后所看到的,改变应用的主题就像在实例化应用时提供一个额外的参数一样简单。Dash Bootstrap 组件带有一组可以选择和/或编辑的主题。
-
网格系统:Bootstrap 提供了一个强大的网格系统,因此我们可以从用户的角度(行和列)来考虑页面,而不必专注于屏幕属性(像素和百分比),尽管每当我们需要时,依然可以访问这些低级细节。
-
响应式设计:由于可能的屏幕尺寸种类繁多,几乎不可能正确地设计页面布局。Bootstrap 为我们处理了这个问题,我们还可以微调页面元素的行为,以控制它们在屏幕尺寸变化时的大小变化。
-
预构建组件:还提供了一组预构建的组件,我们将使用它们。警告框、按钮、下拉菜单和标签页是 Bootstrap 提供的一些组件。
-
编码颜色:我们还获得了一组颜色,方便与用户沟通,以便在有警告、错误、简单信息等情况时使用。
让我们逐一探索这些功能。
主题
首先,让我们看看如何轻松地更改应用的主题。在同一个 app.py 文件中,添加以下导入并为应用创建调用添加新的参数:
import dash_bootstrap_components as dbc
…
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
…
重新运行应用后,你应该会看到主题已更改。如图 1.6 所示,你还可以看到其他主题样本,我还在每页底部添加了它们的名称和设置方法。

图 1.6 – 主题样本及其设置方法
你可以看到,通过仅更改一个参数,就能轻松改变应用的外观和感觉。另请注意,<h1> 元素的颜色和字体大小在样式参数中被覆盖了。我们特别将颜色设置为 "blue",将大小设置为 "40px"。通常不建议这样做;例如,在图中的两个深色主题下,蓝色文本非常难以阅读。因此,在进行这样的更改时要小心。
网格系统与响应式布局
另一个我们从 Bootstrap 获得的强大好处是其网格系统。在添加 Dash HTML 组件时,我们看到可以通过将项目附加到主 html.Div 元素的 children 参数来完成。在这种情况下,每个添加的项目都会占据屏幕的整个宽度,并根据需要显示内容而占据相应的屏幕高度。列表中元素的顺序决定了它们在屏幕上的显示顺序。
在列中并排显示元素
虽然通过编辑任何 HTML 元素的 style 参数可以做到这一点,但这有点繁琐,且可能不稳定。你需要关注许多细节,且可能会出现意想不到的问题。使用 Bootstrap,你只需定义一个列,然后它就会作为一个独立的屏幕,按顺序显示其中的元素,每个元素占据这个小屏幕的整个宽度。列的宽度也可以以强大且灵活的方式进行指定。网格系统将屏幕划分为 12 列,列的宽度可以通过使用从 1 到 12 的数字来指定。图 1.7 展示了如何定义列,以及它们在不同屏幕尺寸下如何变化:

图 1.7 – 两个屏幕尺寸下相同的列布局
如你所见,两个屏幕是相同的,且调整大小会自动发生,同时保持比例。
在许多情况下,这可能并不是你想要的效果。当屏幕宽度变小时,将列扩展以便用户更容易阅读可能更有意义。为此,我们可以选择为五种可能的屏幕宽度指定列的宽度:xs(超小型)、sm(小型)、md(中型)、lg(大型)和 xl(超大型)。这些也是你可以设置的参数名称:

图 1.8 – 基于屏幕大小的列宽细粒度控制
图 1.8 显示了如何通过为列设置两个参数来实现这一点。设置这些值的方法很简单,如图中所示。完整的代码可能是这样的:
import dash_boostrap_components as dbc
dbc.Col(children=[child1, child2, …], lg=6, md=12)
lg=6, md=12 参数仅表示当屏幕较大(lg)时,我们希望该列宽度为六,即 6 ÷ 12,或者是屏幕宽度的一半。在中等大小的屏幕(md)上,设置列宽为 12,意味着屏幕的完整宽度(12 ÷ 12)。
你可能会想,如何才能将列放置在页面的中央,而不是像图 1.7 和 1.8 中那样从左侧开始。宽度和不同的尺寸参数也可以接受一个字典,其中一个键可以是 offset,它用来设置元素在屏幕上的水平位置:
dbc.Col(children=[child1, child2, …], lg={'size': 6, 'offset': 4}, md=12)
如你所见,lg 变成了一个字典,其中我们指明要让该列跳过从左边的前四列,之后再按照指定的大小显示。
最后,如果你想将多个列放在一起,只需将它们放入一个行元素(Row)中,它们就会并排显示:

图 1.9 – 并排显示的列
为了生成图 1.9 中的布局,我们只需要将三个列放在一个列表中,并将其作为 children 参数传递给一个行元素:
dbc.Row([
dbc.Col('Column 1', width=2),
dbc.Col('Column 2', width=5),
dbc.Col('Column 3', width=4),
])
预构建组件
虽然我们不会覆盖所有这些组件,但我们将使用其中的几个,这些组件通常很容易创建。有关每个组件的详细信息和建议,请查阅文档:dash-bootstrap-components.opensource.faculty.ai/。我们很快就会修改应用程序,加入一些预构建的组件。
编码颜色
虽然你可以使用十六进制表示法为文本、背景颜色及许多其他元素设置任何你想要的颜色,Bootstrap 提供了一套根据你传达的信息类型而定的命名颜色。这些颜色可以作为 color 参数设置在多个组件中,并且对用户具有视觉意义。例如,设置 color="danger" 会让组件显示为红色,而 color="warning" 则为黄色。可用的颜色名称有 primary(主色)、secondary(副色)、success(成功)、warning(警告)、danger(危险)、info(信息)、light(浅色)和 dark(深色)。
向应用中添加 Dash Bootstrap 组件
现在我们将向应用中添加两个新的相关组件:Tabs 和 Tab。正如你可能猜到的,Tabs 只是 Tab 组件的容器。我们希望得到的结果是将更多的信息添加到页面中,并将其按新的选项卡组织,如图 1.10 所示:

图 1.10 – 向应用中添加选项卡
提示
学习 Dash 时,最重要的技能之一就是代码重构。虽然应用的最新版本仍然非常简单,但确保你知道如何将旧版本的代码手动重构为新版本是一个非常好的主意。你在应用中拥有的组件越多,你就需要更多地关注重构的细节。我建议你始终手动进行重构,而不是简单地复制并粘贴应用的最新版本。
为了创建选项卡,并获得如图 1.10 所示的内容形式,你需要做以下更改:
html.H2('The World Bank'),
dbc.Tabs([
dbc.Tab([
html.Ul([
# same code to define the unordered list
]),
], label='Key Facts'),
dbc.Tab([
html.Ul([
html.Br(),
html.Li('Book title: Interactive Dashboards and Data Apps with Plotly and Dash'),
html.Li(['GitHub repo: ',
html.A('https://github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash',
href='https://github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash')])
])
], label='Project Info')
如你所见,我们添加了一个 Tabs 元素,在其中添加了两个 Tab 元素。在第一个选项卡中,我们简单地使用了定义有序列表的相同代码。在第二个选项卡中,我们添加了一个类似的无序列表,并加入了新的内容。好的,如果你愿意,可以复制这一部分!你还可以看到如何通过为 label 参数设置值来指定选项卡的标签。
现在你可以再次运行更新后的应用,确保新内容已正确放置,并且选项卡按预期工作。
现在我们准备为我们的应用添加一些交互性。
总结
我们已经学习了如何创建一个最小化的应用,并且确实看到这个过程是多么简单。然后我们探索了用于在网页上创建可视化元素的主要 Dash 包。通过本章的内容,你已经掌握了足够的信息来创建几乎任何布局,并且可以在页面上放置你想要的任何元素。然而,讨论和示例并不全面。我们将使用并讨论这些组件以及许多其他组件,以便你能够精通它们的使用。
在下一章中,我们将把注意力转向如何为我们的应用添加交互性。我们将设置应用,使用户能够通过选择他们想分析的数据集中的内容来探索不同的选项。
第二章:第二章:探索 Dash 应用的结构
我们现在准备好讨论 Dash 创建交互性的机制——可以说这是 Dash 的核心。一旦你熟悉了创建将布局中不同元素连接起来的回调函数,并结合你在第一章《Dash 生态系统概述》中学到的内容,你应该能够在非常短的时间内将数据集转化为交互式应用。接下来的部分将深入探讨更多细节,并提供多种方法来实现这一目标。然而,这两章足以帮助你创建视觉布局,并将其连接起来并实现交互性。本章将主要探讨回调函数,以下主题将被涵盖:
-
使用 Jupyter Notebooks 运行 Dash 应用
-
创建一个独立的纯 Python 函数
-
理解 Dash 组件的 ID 参数
-
使用 Dash 输入和输出
-
将函数整合到应用中——创建你的第一个响应式程序
-
运行你的第一个交互式应用
技术要求
除了我们在第一章《Dash 生态系统概述》中使用的包(例如 Dash、Dash HTML 组件和 Dash Bootstrap 组件)外,我们最重要的是将与 jupyter_dash 包一起使用,并结合 pandas 进行数据处理。
本章的代码文件可以在 GitHub 上找到,地址为 github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_02。
查看以下视频,了解代码的实际应用:bit.ly/3tC0ZsW。
使用 Jupyter Notebooks 运行 Dash 应用
通过更改导入并对应用实例化做一些小修改,我们可以轻松地在 Jupyter Notebook 环境中运行我们的应用。使这一切成为可能的包是 jupyter_dash。本质上,区别在于我们导入 JupyterDash 对象(而不是导入 Dash),并通过调用该对象来进行应用实例化,如下所示:
from jupyter_dash import JupyterDash
app = JupyterDash(__name__)
在笔记本环境中运行应用的一个优势是,进行小的修改、迭代并查看结果不会那么繁琐。在使用 IDE、命令行和浏览器时,你需要不断地在它们之间切换,而在笔记本环境中,一切都集中在一个地方。这使得引入简单的修改并进行测试变得更加容易,也让你的笔记本变得更强大和有趣。
jupyter_dash 包在运行应用时还提供了一个额外的选项,你可以决定是否希望在以下三种模式之一中运行应用:
-
external:在一个单独的浏览器窗口中,正如我们目前所做的那样 -
inline:在笔记本的代码输出区,位于代码单元下方 -
jupyterlab:在 JupyterLab 中运行时,在一个独立的标签页中
如果你愿意,也可以设置所需的宽度和高度。运行应用程序时需要额外的可选参数,如下所示:
app.run_server(mode='inline', height=600, width='80%')
如你所见,设置高度和宽度可以通过指定整数来完成,即像素数量,或者对于 width,通过字符串形式的屏幕大小百分比来设置。
然而,在 Jupyter Notebook 环境中运行应用程序还有另一个重要的好处,除了将代码和叙述放在一个地方。
隔离功能以便更好地管理和调试
在运行和开发应用程序时,你不可避免地会遇到 bugs 和问题。为了处理它们,你需要隔离原因,并创建最简单的可重现示例,使 bug 发生。只有这样,你才能正确地进行故障排除,只有这样,你才能向别人寻求帮助。我们不会等到 bugs 发生才去隔离问题并找出原因。我们会在融入新功能之前预先隔离所有新功能,这样我们就能更好地处理和管理它们。
从现在开始,介绍新功能将通过以下步骤进行:首先,在一个独立的环境中创建它们,我们将创建一个只包含该功能的最小应用程序。一旦我们确定它的工作原理并且它按预期运行,我们将保留一个副本以供参考,然后看看如何将其融入现有应用中。这也有助于我们在未来需要对特定功能进行更改时,重复同样的过程。
让我们从第一个示例开始,它将是一个包含三个值的下拉菜单。用户选择其中一个,紧接着会看到一条消息,显示他们选择的值。图 2.1 展示了最简单形式的示例:

图 2.1 – 根据用户选择的值显示用户的选择
以下代码将创建这个,除了显示用户选择的功能:
-
使用别名导入所需的包:
from jupyter_dash import JupyterDash import dash_core_components as dcc import dash_html_components as html -
实例化应用:
app = JupyterDash(__name__) -
创建应用的布局。我们现在将介绍一个新组件,即 Dash Core Components 的
options属性,用来设置用户可以选择的选项。这个参数通过使用字典列表来设置,每个选项一个字典,其中label是用户将看到的内容,而value是我们将要处理的实际值:app.layout = html.Div([ dcc.Dropdown(options=[{'label': color, 'value': color} for color in ['blue', 'green', 'yellow']]), html.Div() ]) -
像往常一样运行应用程序,唯一的不同是以
inline模式运行,以便在 JupyterLab 中更方便地进行交互工作:if __name__ == '__main__': app.run_server(mode='inline')
图 2.2 展示了在笔记本环境中运行时的示例:

图 2.2 – Dash 应用在 JupyterLab 中运行
我相信你已经注意到在下拉列表正下方添加的空html.Div文件。让我们来看看它如何融入到应用程序的结构中,并且如何实现剩余的功能。现在我们将探讨如何创建将下拉列表与空的 div 标签关联的函数。
创建一个独立的纯 Python 函数
这个函数将用来获取从下拉列表中选定的值,以某种方式处理它,并使用其返回值做一些用户可见的操作。
这个函数非常简单,不需要太多解释:
def display_selected_color(color):
if color is None:
color = 'nothing'
return 'You selected ' + color
如果用户没有输入任何内容(或取消选择当前选项),那么color变量会被设置为'nothing',函数将返回'You selected ' + <color>,并显示color变量所取的任何值。稍后的章节中,我们将创建一个更复杂的函数来获取一些关于国家的信息。
函数本质上是一个过程。它接收一个或多个参数(输入),对它们进行处理,然后返回一个或多个输出。因此,对于这个函数,Input是什么,Output会发生什么呢?你可以通过从布局中选择可用的组件来决定。
对于这个函数,下拉列表将提供Input。然后,经过处理后,函数的返回值,也就是其Output,将影响在下拉列表下方当前空的html.Div中显示的内容。基于图 2.1,图 2.3展示了我们尝试实现的目标。我们将通过使用我们刚定义的函数作为中介,来构建一种方法将下拉列表(Input)与显示文本的 div(Output)连接起来:

图 2.3 – 输入、输出和独立函数
为了使它在此应用程序的上下文中工作,函数需要知道其输入和输出是什么。
现在让我们来看一下如何通过设置组件的id值来识别组件。之后,我们将学习如何声明一个组件为Input或Output。
Dash 组件的 id 参数
如在第一章中简要提到的,Dash 生态系统概述,每个 Dash 组件都有一个id参数,你可以轻松地设置它以唯一标识该组件。实际上,这个参数没有其他复杂的内容,只需要确保你的组件具有唯一且描述性的名称。
注意
使用id参数有更高级的方式,稍后会在更高级的章节中讨论。然而,目前我们将仅关注它作为一个唯一标识符的作用。
随着应用程序复杂度的增长,为id参数使用描述性和明确的名称变得更加重要。当没有交互性时,这个参数是可选的,但当有交互性时,它是必需的。以下示例代码展示了如何为基本用例轻松设置id参数:
html.Div([
html.Div(id='empty_space'),
html.H2(id='h2_text'),
dcc.Slider(id='slider'),
])
将此应用到我们当前的独立应用中,我们为每个 id 参数设置了描述性的名称:
app.layout = html.Div([
dcc.Dropdown(id='color_dropdown',
options=[{'label': color, 'value': color}
for color in ['blue', 'green',
'yellow']]),
html.Div(id='color_output')
])
从布局角度来看,我们的应用现在已经完成,正如我们在 第一章 中所做的那样,Dash 生态系统概述。这里的不同之处在于,我们为 id 参数设置了值,并且我们正在 Jupyter Notebook 环境中运行它。一旦我们能够使用 id 参数识别组件,就可以确定哪些是 Input,哪些是 Output。通过更新我们概念图中的 ID 值,我们可以查看标签,如 图 2.4 所示:

图 2.4 – 可见的应用元素被赋予了名称(ID)
在为我们的组件命名后,我们现在准备将它们用于展示之外的其他用途。
Dash 输入和输出
下一步是确定哪个组件将成为输入(传递给我们的纯 Python 函数),哪个组件将接收函数的返回值(作为输出)并展示给用户。
确定你的输入和输出
dash.dependencies 模块有几个类,其中两个我们将在这里使用:Output 和 Input。
这些类可以通过将以下行添加到我们应用的 imports 部分来导入:
from dash.dependencies import Output, Input
在添加使功能正常运行的最终元素之前,先快速回顾一下我们之前所做的工作:
-
我们在 Jupyter Notebook 环境中实例化了一个应用。
-
我们创建了一个包含三种颜色的下拉框。
-
我们创建了一个常规函数,它返回一个字符串,并附上提供给它的值:
'Your selected' + <color>。 -
通过它们的
id参数,我们为组件指定了描述性的名称。 -
Input和Output从dash.dependencies导入。我们现在将定义我们的回调函数。
回调函数是装饰器,在最基本的用法中,它们需要三个内容:
-
空 div 的
children属性。在这种情况下,它可以像这样指定:Output(component_id='color_output', component_property='children') -
value属性:Input(component_id='color_dropdown', component_property='value') -
我们选择了
Input和Output。
图 2.5 显示了事物如何汇聚在一起的更新视图:

图 2.5 – 可见的应用元素通过某些属性连接
提示
Dash 中前端和后端的区别对我们来说极大地简化了。它们都存在于同一个模块中,我们无需担心许多传统的细节。现在,app.layout 中的任何内容都可以视为前端,而我们在其外定义的任何回调函数都可以统称为后端。
指定你的回调函数
指定回调函数的一般格式是将其定义为 app 变量的一个属性,使用 Python 类的点符号表示法,然后设置输出和输入,如下所示:
@app.callback(Output(component_id, component_property)
Input(component_id, component_property))
现在,我们已经将回调作为应用程序的一个属性创建,并确定了哪些组件的 ID 和属性相互影响,我们将 Python 函数带入,并简单地将其放在回调下方:
@app.callback(Output(component_id, component_property)
Input(component_id, component_property)
def regular_function(input):
output = do_something_with(input)
return output
现在我们的回调函数已完成,可以集成到我们的应用中。
实现回调
让我们利用这些抽象结构,通过我们独立应用的细节来实现:
@app.callback(Output('color_output', 'children'),
Input('color_dropdown', 'value')
def display_selected_color(color):
if color is None:
color = 'nothing'
return 'You selected ' + color
请记住,顺序很重要。Output必须在Input之前提供。
现在我们有了一个完整的回调函数,它属于我们的app。它知道将修改哪个Output的属性,以及将使用哪个Input的属性。然后,它使用display_selected_color函数进行处理,获取输出值并将其发送到id='color_output'的组件。这将反过来修改指定的属性(children)。
要在 JupyterLab 中运行它,您可以在图 2.6中看到完整的代码,并查看根据所选值生成的几种可能输出:

图 2.6 – 在 Jupyter Notebook 中的交互式 Dash 应用
我还引入了一个简单的新组件,html.Br,它仅提供一个常规的 HTML <br> 元素,以提高输出的可读性。
到此,我们已经完成了第一个独立且互动的应用。我们在 JupyterLab 中运行它,并一步步地进行分析每一个细节。我们刚刚构建的应用使用的是一个玩具数据集,并实现了非常简单的功能。我们这么做是为了聚焦于创建交互性的机制。告诉用户他们刚刚选择的颜色并没有太多实际意义。
有了这些知识后,我们将为用户回答一个实际问题——一个如果用户浏览整个数据集可能会觉得很繁琐的问题。
我们还将这个新功能整合到我们的应用中,查看它如何与我们已经创建的其他内容和功能契合。
将该功能集成到应用中
这是我们将要引入的功能计划:
-
使用我们数据集中可用的国家和地区创建一个下拉列表。
-
创建一个回调函数,获取所选国家,过滤数据集,并找到该国家在 2010 年的人口数据。
-
返回一个关于找到的数据的小报告。图 2.7 显示了期望的最终结果:
![图 2.7 – 用于显示所选国家人口的下拉列表]()
图 2.7 – 用于显示所选国家人口的下拉列表
重要提示
现在我们开始使用数据集,我们将从 data 文件夹中打开文件。这假设你正在运行的应用程序与该文件夹位于同一目录下。每一章的代码在 GitHub 仓库中都有单独的文件夹,便于访问;然而,代码只有在 data 文件夹和 app.py 文件在同一目录下时才有效。
图 2.8 显示了该文件夹结构可能的样子:

图 2.8 – 假定的应用文件夹结构
按照约定,我们将在 JupyterLab 中运行一个简化版的应用程序,确保其正常运行,保存副本,然后将其添加到应用程序中。
我们首先需要查看数据集,稍微探究一下,并学习如何实现新功能。
要查看数据集中包含哪些文件,我们可以运行以下代码:
import os
os.listdir('data')
['PovStatsSeries.csv',
'PovStatsCountry.csv',
'PovStatsCountry-Series.csv',
'PovStatsData.csv',
'PovStatsFootNote.csv']
如果你愿意,可以查看文件及其内容。现在,我们将使用 PovStatsData.csv 文件。为了快速了解其结构,我们可以运行以下代码:
import pandas as pd
poverty_data = pd.read_csv('data/PovStatsData.csv')
poverty_data.head(3)
在 JupyterLab 中运行这段代码会显示数据集的前三行,如下所示:

图 2.9 – 贫困数据集的前几行和列
看起来我们有两列固定变量(NaN)值,这些值位于各自的年份列下。这里,年份从 1974 到 2019(请注意,并不是所有年份都显示,以便更好地阅读)。国家和指标也有代码,这些代码在我们需要合并不同 DataFrame 时会派上用场。
提示
固定变量指的是那些预先已知且不变化的变量;在这种情况下,它们是国家和指标。被测量的变量是我们希望了解的数值,例如某国 A 在某年 B 的人口。固定变量也被称为“维度”。从技术上讲,它们是数据集中所有的列,这是一个有助于分析的概念性区分。
在 第四章,数据处理与准备 - 为 Plotly Express 铺路,我们将探讨数据格式及其如何影响我们的分析与可视化。目前的结构可以通过新增“年份”列和“数值”列来改进,这样可以使其更加标准化并且便于分析。现在,由于我们专注于回调函数,我们将保持数据格式不变,以免分心。
现在我们用代码实现计划:
-
首先,让我们创建一个下拉列表。在这里,我们使用 pandas 的
Series.unique方法来去重国家和地区。就在下方,我们创建一个空的 div,id='report':dcc.Dropdown(id='country', options=[{'label': country, 'value': country} for country in poverty_data['Country Name'].unique()]) html.Div(id='report') -
接下来,我们创建一个回调函数,该函数接收选定的国家,过滤数据集,并查找该国家在 2010 年的人口。过滤将分为两个步骤。
检查是否未向函数提供任何国家(这是用户首次访问页面或用户从下拉框中取消选择时发生的情况)。在这里,我们简单地返回空字符串:
if country is None: return ''现在,让我们集中处理过滤部分。首先,我们获取选定的国家并过滤
poverty_data数据框以获取人口值。然后我们定义filtered_df变量。这个变量获取选定的国家,并返回population变量所在的行。我们通过使用 pandas 的loc方法来实现,在该方法中我们选择所有的:行和列名values属性,并获取索引零的数值:filtered_df = countrydata[(countrydata['Country Name']==country) & (countrydata['Indicator Name']=='Population, total')] population = filtered_df.loc[:, '2010'].values[0] -
最后,让我们返回一个关于已找到数据的小报告。现在我们已经获得了感兴趣的人口数字,我们返回一个包含两个元素的列表。第一个是一个
元素,使用大字体显示
country变量。第二个是一个句子,其中包含两个动态值,这些值会被插入到相应的位置,您可以在以下代码片段中看到:return [ html.H3(country), f'The population of {country} in 2010 was {population:,.0f}.' ]
请注意,由于我们已经在布局中有一个 div 元素,并且我们已指示要修改其children属性(该属性可以是单个值或列表),所以该函数的返回值可以简单地是一个列表(或单个值)。
我已将报告中的population值进行了格式化,以便更易读。冒号表示后面的字符串是我们希望的格式。逗号表示我们希望千位数用逗号分隔。点号表示如何格式化小数位数。点号后的零表示小数位数的数量,而f表示我们正在处理浮动数值。
现在,我们准备重构代码,以包含新的视觉元素和新功能。
接着,我们从上个版本的应用程序中继续,回顾第一章,Dash 生态系统概述,下拉框和报告 div 应位于H2和Tabs组件之间:
…
html.H2('The World Bank'),
dcc.Dropdown(id='country',
options=[{'label': country, 'value': country}
for country in poverty_data['Country
Name'].unique()]),
html.Br(),
html.Div(id='report'),
dbc.Tabs([
dbc.Tab([
…
回调函数应位于应用程序的顶层html.Div的闭合括号之后。以下是该函数的完整代码:
@app.callback(Output('report', 'children'),
Input('country', 'value'))
def display_country_report(country):
if country is None:
return ''
filtered_df = poverty_data[(poverty_data['Country
Name']==country) &
(poverty_data['Indicator
Name']=='Population, total')]
population = filtered_df.loc[:, '2010'].values[0]
return [html.H3(country),
f'The population of {country} in 2010 was
{population:,.0f}.']
重新运行应用后,您应该能看到更新后的视图:

图 2.10 – 更新后的应用程序,包含下拉框和简单的人口报告
提示
app.run_server方法接受一个可选的port参数,默认为app.run_server(port=1234)。这同样适用于jupyter_dash。
现在我们已经启用了回调函数并使其正常工作,我们终于可以开始使用右下角的蓝色按钮了!点击它,然后选择回调函数,会显示一个互动式图表,精确展示我们所指定的组件。国家及其值,以及报告及其子元素。图 2.11展示了这一点:

图 2.11 – Dash 可视化调试器的实际操作
服务器按钮是绿色的,这意味着它运行正常。我们还可以看到“0 错误”提示。当你在一个运行中的应用中打开这个调试器,并修改组件时,你还可以看到回调的路径以及触发了什么。参与触发回调的组件会被高亮显示,这样你就可以“看到”发生了什么。在复杂情况下,这个功能会变得更加有用。图表中的节点也是互动式的,你可以通过放大/缩小整体图表来移动它们,这样就可以在任何你想要的地方进行缩放。而且,是的,这个图表是一个 Dash 应用,使用了 Dash 的另一个包。
中间的绿色矩形显示了两个有趣的数字。顶部显示的1告诉我们到目前为止该回调函数被触发了多少次。底部的数字显示运行该回调函数所花费的时间。这对于跟踪和分析性能非常有帮助。
到目前为止,我们只使用了单一值的输入(例如,不是列表)来修改输出。但如果我们想获取多个值并对其进行处理呢?如果我们想处理来自多个来源的值,例如,来自下拉框的值和日期呢?这些都可以通过 Dash 的回调函数实现。对了,我是不是提到过回调函数是 Dash 的核心?
我相信我们在这一章的编码已经足够了,我觉得现在回顾一下回调函数的强大功能、它们能做什么,以及它们的一些有趣属性是个不错的主意。这些只是目前需要牢记和了解的内容;我们将在后续章节中逐步探索每个功能是如何工作的。
Dash 回调函数的属性
让我们回顾一下 Dash 回调函数的属性,并介绍一些后面将更详细探讨的其他属性:
-
多个输入:正如我刚才提到的,我们可以为回调函数提供多个输入,并创建更复杂的功能。以我们的数据集为例,我们可以轻松想象一个下拉框用来选择国家,另一个用来选择日期,再一个用来指定你想要分析的经济指标。这些输入可以用来过滤 DataFrame 的子集,并根据多个条件返回你需要的值。
-
Input):国家选择器可以设置为接受多个值,这样我们就可以循环遍历它们,并在一张图表中(或每个国家一张图表)展示多个国家在相同指标上的趋势。 -
多个输出:与多个输入一样,多个输出也可以通过一个回调函数进行修改。在我们的示例中,我们可以设想产生两个输出——一个是可视化过滤后数据的图表,另一个是表格——为用户提供原始数据,如果他们想导出并进一步分析这个特定的子集。
-
它们可以在返回之前做其他事情:我们主要关注回调函数作为简单的数据处理器,但它们实际上可以在返回之前做任何事情。例如,你可以想象一个函数在特定条件下发送电子邮件。日志记录是另一个有趣的探索方向。你只需要简单地记录传递给每个函数的参数。这可以让你洞察人们感兴趣的内容,哪些功能被使用等等。你甚至可以解析这些日志,并基于此开发你自己的独立分析应用程序!
-
装饰器中的
Input应对应my_function的第一个参数。我在前面的代码片段中使用了相同的名称,以使其明确且清晰(dropdown和date)。输出也适用相同的规则。 -
State。在我们迄今讨论的示例中,回调函数在值变化时会立即触发。有时,你可能并不希望这样。例如,如果你有多个输入,用户在配置选项时,如果每次变化都触发输出变化,可能会让用户感到烦恼。想象一下,用户输入的每一个字母都在修改页面上的另一个元素,这并不是最佳的用户体验。使用State的典型场景是按钮。用户选择或输入值后,一旦准备好,他们可以点击按钮,只有在此时才会触发回调函数。
图 2.12 展示了一个更复杂的回调函数的概念图及其可能的样子:

图 2.12 – 处理多个输入和输出的回调函数,同时执行其他任务
我们现在已经在两个不同的上下文中创建并运行了两个回调函数。我们还将其中一个回调函数融入其中,并基于我们在第一章中所做的工作进行扩展,Dash 生态系统概述。通过几个额外的示例,你将掌握回调函数。接下来,你需要攻克的技能是管理复杂性,并在重构代码时保持代码的组织性和可控性。
让我们快速回顾一下本章所学的内容。
总结
首先,我们介绍了一种新的运行 Dash 应用程序的方式,即在 Jupyter Notebook 环境中运行它们。我们看到了这个过程的熟悉感,并在笔记本中创建了我们的第一个互动应用。我们详细讲解了整个过程,从创建布局组件、为它们赋予 ID、选择将使用哪些属性,到将所有这些与回调函数连接起来。我们又运行了一个示例,并熟悉了我们的数据集。最重要的是,我们学会了如何将新的工作整合到应用中,并运行了一个更新版,生成了简单的人口报告。恭喜!
在下一章中,我们将深入探讨 Plotly 的数据可视化功能。我们将主要关注Figure对象及其组件,如何查询它们,以及如何修改它们。这将使我们能够对所创建的可视化进行精细的控制。
第三章:第三章:使用 Plotly 的 Figure 对象
假设你发布了一篇包含图表的文章。假设读者平均花费 1 分钟时间查看图表。如果你的图表易于理解,那么他们可能会花 10 秒钟理解图表内容,接着花 50 秒时间思考、分析并琢磨图表的意义。另一方面,如果图表难以理解,他们可能会花费 50 秒“阅读”图表,而很少有时间去思考其含义。
本章旨在为你提供工具,帮助你最大限度减少观众理解图表的时间,并最大化他们分析和思考的时间。前两章主要讨论了如何构建应用程序并使其具备交互性。本章将讨论如何创建和控制构建应用程序所需的图表。我们将主要探索 Plotly 的 Figure 对象。本章将涉及以下主要主题:
-
理解 Figure 对象
-
了解数据属性
-
了解布局属性
-
学习图形轨迹及其添加方法
-
探索不同的图形转换方式
技术要求
我们将重点使用 plotly 包中 graph_objects 模块的 Figure 对象。在本章后续部分,我们将使用其他包来改进我们的应用程序,并为其添加一个交互式图表。提醒一下,我们将使用的包有 Dash、Dash HTML 组件、Dash 核心组件、Dash Bootstrap 组件、JupyterLab、Jupyter Dash 和 pandas。
这些软件包可以通过运行 pip install <package-name> 单独安装,但为了重现相同的结果,最好安装我们在此处使用的确切版本。你可以通过在存储库的根文件夹中运行一条命令 pip install -r requirements.txt 来一次性安装所有这些软件包。贫困数据集的最新版本可以从这个链接下载:datacatalog.worldbank.org/dataset/poverty-and-equity-database。不过,和软件包一样,如果你想重现相同的结果,你也可以从 Git 仓库根目录下的 data 文件夹访问数据集,除此之外,本章的所有代码也可以在 GitHub 上找到:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_03。
查看以下视频,观看代码演示:bit.ly/3x9VhAA。
理解 Figure 对象
Plotly 是一个功能完整的数据可视化系统,提供超过 50 种图表类型(例如,条形图、散点图和直方图)。它支持 2D 和 3D 可视化、三元图、地图等。可自定义几乎所有图表方面的选项非常详细,可能会让人感到有些复杂。正如人们所说,这也是一个“好问题”!
我们使用图表来揭示数据的某些特征或不同数据集之间的关系。然而,如果我们不知道可视化的内容,纯粹的数据可视化是没有意义的。想象一个矩形,里面有一堆点,且这些点有清晰的模式。如果你不知道 x 轴代表什么,它依然是没有意义的。例如,如果在一个图中有不同的形状和颜色,没有图例它们也没有任何意义。通常,标题和注释也很重要,它们帮助我们理解正在分析的数据的背景。
这两个组,data和其他支持元素,统称为layout,是 Plotly 的Figure对象的两个顶级属性。每个属性都有多个子属性,形成一个类似树状的结构。还有一个frames属性,主要用于动画,它不像另外两个属性那样总是在每个图表中出现,因此并不常见。本章将不会涉及这个属性。
现在,让我们来探讨这些属性,开始绘图,以更好地理解它们如何在Figure对象中相互结合:
-
data:数据的不同属性以及它们之间的关系,通过图形/几何形状来表达,比如圆形、矩形、线条等。这些形状的图形属性用于表达数据的各种属性。我们通过这些形状的相对大小、长度和距离来理解数据。由于它们是视觉化的,因此具有直观性,这些属性很容易理解,不需要太多解释。data属性对应的是我们试图理解的核心内容。你需要为data属性提供的值取决于图表的类型。例如,对于散点图,你需要提供x和y值;对于地图,你需要提供lat和lon。你可以将多个数据集叠加在同一个图表上,每个数据集被称为trace。每种图表类型可以接受许多其他可选值,很多内容将在本书后续部分详细介绍。 -
layout:所有与数据无关的内容都属于这个属性。layout属性的元素本质上更抽象,通常使用文本来告诉用户他们正在查看什么。许多元素也是样式元素,虽然它们可能不会添加太多信息,但可以使图表更易于理解或符合某些品牌指南。我们将探索许多属性,但最突出的属性是标题、轴标题、刻度和图例。它们又有子属性,例如字体大小、位置等。
通过实践学习要容易得多,我们现在可以开始创建我们的第一个图形。graph_objects模块通常作为go导入,我们通过调用go.Figure来实例化一个图形!图 3.1展示了一个空的Figure对象,以及如何创建和显示它:

图 3.1 – 在 JupyterLab 中显示的默认空 Figure 对象
当然,从这个空图形中我们什么也看不出来,但它是添加我们想要的元素之前的第一步。虽然我们可以通过在go.Figure调用中定义所有内容来创建并显示Figure对象,但我们将使用一种稍微更简单、更方便的方法。我们将创建的对象赋值给一个变量,然后迭代地添加和/或修改我们需要的元素。这个方法的一个重要好处是,创建图表后我们可以对其进行更改。
重要说明
一旦将Figure对象赋值给一个变量,这个变量就可以在全局范围内使用。由于它是可变的,您可以在代码的其他地方对其进行修改。对图表进行修改后,显示该图表将展示您所做的更改。我们将利用这个重要特性来管理我们的图表。
创建了基本对象后,我们现在可以开始将我们的第一个数据轨迹添加到我们的第一个图表中。
了解数据属性
首先,我们通过添加一个非常小且简单的数据集来开始绘制散点图。在本章的后面部分,我们将使用我们的贫困数据集来创建其他图表。一旦创建了Figure对象并将其赋值给一个变量,您就可以访问大量方便的方法来操作该对象。与添加数据轨迹相关的方法都以add_开头,后面跟着我们要添加的图表类型,例如add_scatter或add_bar。
让我们一起走完整个散点图创建过程:
-
导入
graph_objects模块:import plotly.graph_objects as go -
创建一个
Figure对象的实例并将其赋值给一个变量:fig = go.Figure() -
添加一个散点轨迹。此类型图表所需的最小参数是
x和y值的两个数组。这些值可以通过列表、元组、NumPy 数组或 pandasSeries提供:fig.add_scatter(x=[1, 2, 3], y=[4, 2, 3]) -
显示生成的图形。你可以简单地将变量放在代码单元的最后一行,它也会在 JupyterLab 中显示,一旦你运行它。你还可以显式调用
show方法,这样可以提供更多选项来定制图形的显示方式:fig.show()
你可以在 图 3.2 中看到完整的代码以及最终的输出:

图 3.2 – 在 JupyterLab 中显示的散点图
现在我们将添加另一个类似的散点图,叠加在这个图上。我们只需重复 步骤 3,但使用不同的值:
fig.add_scatter(x=[1, 2, 3, 4], y=[4, 5, 2, 3])
这将把新的散点图添加到同一图形中。如果我们在运行此代码后调用 fig.show(),我们将能够看到更新后的图形。请注意,这个轨迹有四个数据点,而前一个轨迹有三个。我们不需要担心这一点,因为这是通过某些默认值为我们处理的。如果需要,我们还可以修改这些默认值。
如果我们想修改任何关于 data 轨迹的方面,可以通过 add_<chart_type> 方法来实现。调用这些方法会提供许多选项,通过多种参数进行设置,这些选项是特定于你正在生成的图表类型的。本书的第二部分将深入探讨几种图表类型及其提供的不同选项。另一方面,如果我们想修改与 layout 属性相关的任何内容,我们可以通过访问并赋值我们想要的属性和/或子属性,采用简单的声明式方式来实现。这通常使用 Python 的点符号表示法,例如 figure.attribute.sub_attribute = value。这个指导原则并不完全正确,因为也存在一些例外情况,在某些情况下,有些属性属于 data 属性,但由 layout 属性来管理。例如,大多数情况下,这种区分是有帮助的。
现在让我们来看看在图形布局中可以更改的一些内容。
了解 layout 属性
对于我们正在处理的当前图形,让我们添加一个标题(针对整个图形),以及轴标题,看看效果如何:
fig.layout.title = 'The Figure Title'
fig.layout.xaxis.title = 'The X-axis title'
fig.layout.yaxis.title = 'The Y-axis title'
如你所见,我们正在探索图形的树形结构。title 属性直接位于 fig.layout 下方,此外,还有 fig.layout.xaxis 和 fig.layout.yaxis 的标题。为了让你了解可用选项的详细程度,图 3.3 展示了以 tick 开头的一些 xaxis 属性:

图 3.3 – 一些 Figure 对象的 layout.xaxis 选项
现在让我们来看看我们刚刚添加的四行代码的效果:

图 3.4 – 更新后的图形,包含两条轨迹、一个图例和标题
我们添加的三个标题是显而易见的。新轨迹采用新的默认颜色来区分它们。另一个有趣的事情是图例,它是自动添加的。当你只有一条轨迹时,通常不需要图例,但当你有多条轨迹时,它就变得非常重要。当然,描述性名称至关重要,轨迹 0 并没有太多意义,但我将其保留作为记忆辅助,帮助记住图形元素的名称。
我们刚刚创建并显示的图形就是你的用户将看到的内容。现在让我们通过交互式的方式来查看这个图形的各个组件。
交互式探索 Figure 对象
正如我之前提到的,show 方法提供了一些便捷的选项,用于自定义图形的展示方式。一个特别有用的选项是将 renderer 参数设置为 JSON。图 3.5 展示了这一点是如何有用的:

图 3.5 – 在 JupyterLab 中交互式探索 Figure 对象
在左上角,你可以看到默认视图。Figure 对象和两个顶层属性显示在它下方。我们还可以看到一个提示,表明我们的数据属性包含两个项目(这两个是我们添加的轨迹)。三角形及其方向指示相应的属性是否已经展开或折叠。
在左下角,你可以看到搜索功能的实际操作。这在你想要访问或修改某个属性时非常有用,但又不完全确定它的确切名称,或者它属于哪个属性。在右侧,我已展开了一些项目,你可以看到它们对应我们创建的图形。
重要提示
本章,甚至整本书,讲述的是如何创建你想要的图表和仪表盘。它不是关于数据可视化的最佳实践或统计推断。换句话说,它是关于如何创建你想要创建的东西,而不是关于你应该创建什么。我仍然会尽量分享好的实践,并做出合理的选择来选择图表和其细节,但重要的是要牢记这一点。
我相信你已经注意到我们创建的图形右上角的 "模式栏",其中包含互动按钮和控制项。有几种方式可以控制显示或隐藏哪些按钮,以及一些其他选项。这些都可以通过 show 方法的 config 参数进行控制。
Figure 对象的配置选项
config 参数接受一个字典,并控制多个有趣的选项。键用于控制修改哪个方面。此外,值可以是字符串或列表,具体取决于你正在修改的内容。例如,考虑以下代码片段:
fig.show(config={'displaylogo': False,
'modeBarButtonsToAdd': ['drawrect',
'drawcircle',
'eraseshape']})
这里列出了一些最重要的选项:
-
displayModeBar:默认为True。它控制是否显示整个模式栏。 -
responsive:默认为True。它控制是否根据浏览器窗口的大小调整图形的尺寸。有时,您可能希望保持图形尺寸不变。 -
toImageButtonOptions:模式栏中的相机图标允许用户将图形下载为图像。此选项控制下载图像的默认格式。它接受一个字典,您可以在其中设置默认格式(即 SVG、PNG、JPG 或 WebP)。您还可以设置默认的高度、宽度、文件名和缩放比例。 -
modeBarButtonsToRemove:这是一个您不希望出现在模式栏中的按钮列表。
现在我们已经学习了如何创建、检查和配置基本图表,让我们探索一下我们还能做些什么。我们如何将它们转换成其他格式?还有哪些格式可用?
探索转换图形的不同方式
控制转换图形的方法通常以to_或write_开头。让我们探索其中一些最有趣的方法。
将图形转换为 HTML
Plotly 图形实际上是 HTML 对象,并结合使其具有交互性的 JavaScript。如果我们想通过电子邮件与他人共享这些图形,我们可以轻松地捕获该 HTML 文件。例如,您可以考虑在您的仪表板中添加此功能。用户可以创建他们想要的图表或报告,将其转换为 HTML,下载并与同事共享。
您只需提供一个文件路径,指定保存位置即可。该方法还提供几个可选参数以进一步定制。让我们将图形转换为 HTML,并添加一个config选项,使其下载 SVG 格式的图像。当点击相机图标时,HTML 文件将反映这一效果。相关代码非常简单:
fig.write_html('html_plot.html',
config={'toImageButtonOptions':
{'format': 'svg'}})
我们现在可以将该文件作为单独的 HTML 文件在浏览器中打开,填满整个浏览器屏幕,如图 3.6所示:

图 3.6 – Figure 对象作为一个单独的 HTML 文件呈现在浏览器窗口中
将图形转换为图像
我们已经检查了允许用户手动下载Figure对象图像的选项。还有另一种编程方式,也很有趣。就像write_html方法一样,我们也有一个write_image方法。图像的格式可以显式提供,或者根据您提供的文件扩展名推断出来。您还可以设置height和width值。
这可能对大规模图像创建很有趣。例如,你可能想为每个国家创建许多图表,并将每个图表保存到单独的文件中,以便为每个国家生成单独的报告。手动完成这项工作会非常繁琐。你还可以将其作为用户回调的一部分。你可以允许用户生成某些报告,并点击一个按钮将其转换为图像并下载,例如。这可以像 HTML 转换器一样运行:
fig.write_image('path/to/image_file.svg',
height=600, width=850)
有了这些信息,我们现在可以更实际地探索数据集,了解更多内容。
使用真实数据集进行绘图
在 第二章《探索 Dash 应用程序的结构》中,我们创建了一个简单的报告,展示了 2010 年所选国家的人口。此类报告用于用户已经知道他们想要什么的情况。也就是说,他们有一个关于特定国家、指标和时间段的具体问题,我们的功能提供了答案。
我们可以将仪表板功能分为两大类。第一类,就像我们已经做过的那样,是回答特定问题的可视化或报告。第二类,我们现在将要做的是引导用户进行更具探索性的操作。在这种情况下,用户对某个话题了解不多,他们正在寻找一个概览。
用户可以在这两种类型的图表之间来回切换。例如,首先,他们可以探索过去十年的贫困情况。某个特定地区突出显示。然后,他们会就该地区提出一个具体问题。当他们意识到该地区的另一项指标异常高时,他们可以转到该指标的另一个探索性图表,以了解更多信息。
现在,我们将让用户选择一个年份,应用程序将显示该年份按人口排序的前 20 个国家。
快速提醒一下,我们的贫困数据集包含国家及其代码、指标及其代码,以及从 1974 年到 2019 年的每一年的列。
按照约定,让我们首先在 JupyterLab 中的隔离环境中进行操作:
-
导入
pandas,用它打开贫困数据集,并将其分配给poverty_data变量:import pandas as pd poverty_data = pd.read_csv('data/PovStats_csv/PovStatsData.csv') -
尽管关注的列名为
regions列表:regions = ['East Asia & Pacific', 'Europe & Central Asia', 'Fragile and conflict affected situations', 'High income', 'IDA countries classified as fragile situations', 'IDA total', 'Latin America & Caribbean', 'Low & middle income', 'Low income', 'Lower middle income', 'Middle East & North Africa', 'Middle income', 'South Asia', 'Sub-Saharan Africa', 'Upper middle income', 'World'] -
创建
population_df,这是一个子集 DataFrame,其中的regions列,以及 pandasSeries的isin方法检查Series中的值是否在某个列表中,而~(波浪符)是逻辑否定操作符:population_df = poverty_data[~poverty_data['Country Name'].isin(regions) & (poverty_data['Indicator Name']== 'Population, total')] -
结果 DataFrame 的前几行可以如下显示:
population_df.head()它看起来像以下屏幕截图:
![图 3.7 – population_df 的前几行]()
图 3.7 – population_df 的前几行
-
创建一个动态的
year变量,并创建一个包含国家列以及所选年份列的year_df变量。然后,将这些值按降序排序,并提取前 20 个:year = '2010' year_df = population_df[['Country Name', year]].sort_values(year, ascending=False)[:20] -
有了一个包含两个排序列的
year_df变量,我们可以像之前做散点图一样非常轻松地创建条形图。我们还可以添加一个包含年份作为变量的动态标题:fig = go.Figure() fig.add_bar(x=year_df['Country Name'], y=year_df[year]) fig.layout.title = f'Top twenty countries by population - {year}' fig.show()
这将产生以下输出:

图 3.8 – 显示 2010 年按人口排序的前 20 个国家的条形图
如你所见,一旦我们拥有一个适当的子集并对相关列进行排序,我们就可以用几行代码生成我们想要的图表。另外,注意y轴上的数字默认以十亿(或十亿的分数)格式显示,以便更容易阅读。
我们没有设置轴标题。在这里,图表标题隐含地告诉我们两个轴的信息:“国家”和“人口”。由于y轴是数字格式,x轴列出了国家名称,用户应该能很清楚地理解。
2010 年是一个任意年份,实际上我们希望用户能够从数据集中可用的年份中选择他们想要的年份。
生成该图表的代码只需要一个def语句和一些缩进,就能变成一个函数:
def plot_countries_by_population(year):
year_df = …
fig = go.Figure()
…
fig.show()
这个函数生成的图表与我们刚刚生成的图表相似,但它是基于给定的year参数。你可能认为,将这个函数转换为回调函数只需要添加一行代码。实际上,这正是我们接下来要做的事情,但首先,我想强调一个关于数据处理和准备的观察,以及它如何与数据可视化相关,因为这个例子很好地说明了这一点。
数据处理是数据可视化过程中不可或缺的一部分
上面的例子包含六个步骤。前五个步骤是为了准备数据并将其整理成两个数组:一个是国家名,另一个是人口数。第六个也是最后一个步骤是生成图表。为了准备数据所写的代码比生成图表的代码多得多。
如果你考虑到生成图表所需的心理努力和时间(也就是只有最后一步),你会很容易发现,这与我们在本章开头使用玩具数据集创建散点图所需的心理努力是相同的。我们只需运行add_scatter(x=[1, 2, 3], y=[4, 2, 3]),然后我们为条形图做了相同的操作,只是数值不同。
然而,如果你考虑到为条形图准备数据所花费的心理努力和时间,你会明显看到,与为散点图准备数据相比,差异巨大。我们需要知道在尝试访问某一年份的数据时会遇到KeyError。通常我们会在这些问题上花费更多时间和精力,而一旦数据格式合适,我们就能轻松地进行可视化。
在第四章,数据操作和准备 - 通往 Plotly Express 的道路中,我们将花更多时间讨论这个主题,并介绍一些在各种情况下可能有用的重要技术。然而,请记住,你在操作数据、重塑数据、合并数据集、正则表达式以及所有繁琐的数据准备工作方面的技能构成了你贡献的重要部分。这是大部分机会所在,很多都基于你的判断。领域知识也是必不可少的;例如,知道区域和国家之间的区别。一旦你有了特定格式的数据,就有许多高级技术和算法可以用来可视化、分析和运行各种机器学习流水线,而这些技术只需要相对较少的代码。
现在,让我们使用我们新创建的函数,并学习如何通过Dropdown组件和回调函数使其交互。
通过回调函数使图表交互
首先,我们将在 JupyterLab 中作为完全隔离的应用程序进行操作,之后再将其添加到我们的应用程序中。在隔离环境中,我们的app.layout属性将包含两个组件:
Dropdown:这将显示所有可用的年份,以便用户可以选择他们想要的年份。
Graph:这是一个我们尚未涵盖的新组件,我们将会大量使用它。将Graph组件添加到布局中会显示一个空图表。如果你还记得我们关于回调函数的讨论,当在回调函数中修改组件时,我们需要提供其component_id和component_property。在这种情况下,我们将要修改的属性是figure属性,它只属于Graph组件。
现在你已经熟悉了导入和应用程序实例化,所以我将主要关注应用程序的app.layout属性:
app.layout = html.Div([
dcc.Dropdown(id='year_dropdown',
value='2010',
options=[{'label': year, 'value':
str(year)}
for year in range(1974, 2019)]),
dcc.Graph(id='population_chart'),
])
目前,Graph组件没有什么特别之处。我们只是在Dropdown组件下面创建一个,并给它一个描述性的id参数。
我相信你也注意到了,这一次,在Dropdown组件的options列表中,label和value键的值略有不同。不同之处在于value键设置为str(year)。由于options是通过列表推导生成的字典列表,它将生成一个整数列表。所选数字将用于选择具有该值的列。在这个数据集中,所有列都是字符串,因此使用population_df[2010]是行不通的,因为实际上并没有这样的列(作为整数)。实际列名是2010,作为字符串。因此,我们将标签指定为整数,但回调函数将使用该整数的字符串表示(年份)。
我们还添加了一个新参数,之前没有讨论过。Dropdown组件的value参数作为默认值,首次显示给用户时会显示此值。这样比直接显示一个空图表要更好。
在某些情况下,你可能想做与这个示例中相反的事情。你可能希望保持value不变,但以某种方式修改label。例如,如果你的数据全是小写字母,你可能希望将选项显示为大写字母。在上一章的颜色示例中,我们也可以做类似的处理:
dcc.Dropdown(options=[{'label': color.title(), 'value':
color} for color in ['blue', 'green', 'yellow']])
从回调函数的角度看,颜色依然保持不变,因为它主要处理的是value属性。但是对于用户来说,这会将颜色显示为大写字母:"Blue"、"Green"和"Yellow"。
运行至今定义的两个组件会生成如图 3.9所示的应用:

图 3.9 – 一个带有下拉组件的应用,显示默认值和空图表
我们已经创建了一个正常的函数,它接受年份数据,并返回显示该年份前 20 个国家按人口排名的柱状图。将其转换为回调函数只需要一行代码:
@app.callback(Output('population_chart', 'figure'),
Input('year_dropdown', 'value'))
def plot_countries_by_population(year):
year_df = …
fig = go.Figure()
…
return fig
在之前的函数定义中,最后一行是fig.show(),而在回调函数中,我们则返回图表对象。这样做的原因是,在第一个例子中,我们是在交互式环境下运行的,并没有应用或回调上下文。而在这个例子中,我们有一个 ID 为population_chart的组件,更重要的是,我们希望修改它的figure属性。返回图表对象会将其交给Graph组件,从而修改它的figure属性。
运行这个应用后,可以根据用户的选择生成动态图表,正如你在图 3.10中所看到的:

图 3.10 – 一个根据选择的年份显示柱状图的应用
如果你将此与图 3.8进行对比,你会注意到这里国家名称是垂直显示的,而之前它们是以一定角度显示的。之所以会这样,是因为图表显示在了更宽的浏览器窗口中。这是 Plotly 为我们处理的又一个便捷默认设置,我们不需要做任何事情。这意味着我们的图表具有响应式特性,使它们非常灵活。这对于我们使用 Dash Bootstrap Components 样式的应用和组件同样适用。
现在我们已经创建了一个可以独立运行的应用,接下来我们来看如何将其添加到我们的应用中。
向我们的应用中添加新功能
到目前为止,应用程序的最新版本包含一个Dropdown组件,在其下方是 2010 年人口报告的Div,在其下方是Tabs组件。现在,让我们在报告区域下方、Tabs组件上方插入新的Dropdown和Graph组件。我们还要添加新的回调函数:
-
复制这两个新组件,并将它们放到
app.layout属性中应在的位置:… html.Br(), html.Div(id='report'), html.Br(), dcc.Dropdown(id='year_dropdown', value='2010', options=[{'label': year, 'value': str(year)} for year in range(1974, 2019)]), dcc.Graph(id='population_chart'), dbc.Tabs([ … -
复制回调函数定义,并将其放置在
app.layout的顶级Div的闭合标签之后的任何位置。为了更好的组织,你可以将它放在我们为更好的组织所创建的上一个回调函数下面,但在功能上它放置的位置无关紧要:@app.callback(Output('population_chart', 'figure'), Input('year_dropdown', 'value')) def plot_countries_by_population(year): fig = go.Figure() year_df = population_df[['Country Name', year]].sort_values(year, ascending=False)[:20] fig.add_bar(x=year_df['Country Name'], y=year_df[year]) fig.layout.title = f'Top twenty countries by population - {year}' return fig -
在定义
poverty_data之后,添加regions列表的定义,再添加population_df。顺序很重要,因为population_df依赖于先定义regions,而且它是poverty_data的子集,所以它也需要在poverty_data之后定义。这是这些变量需要定义的顺序:poverty_data = … regions = … population_df = …
现在,如果我们运行应用程序,你可以看到它的样子,如图 3.11所示:

图 3.11 – 添加了新组件的应用程序(下拉框和图表)。
如果你打开调试器并点击回调函数按钮,你还可以看到更新后的可用回调函数视图,并查看它们所连接的组件的名称(组件 ID 和组件属性)。图 3.12 展示了这一点:

图 3.12 – 视觉调试器中的应用回调函数
现在我们的应用程序显示了更多信息。它允许用户从数据集中互动式地获取信息。我们定义了两个回调函数,并且有一个包含多种类型组件的布局。我们总共有大约 90 行代码。通过将新组件插入到某个位置,可以顺利地添加新组件,直到应用程序中的组件数量足够大。然后,我们将需要学习如何更好地组织代码并进行重构。
让我们用一个有趣且易于使用的 Plotly Figure对象的方面来结束本章,它不需要太多的编码,然后回顾一下我们所讨论的主题。
为你的图形设置主题
为你的图形设置主题(而不是你的应用程序)可能会很有趣,并且如果需要更改主题,这样做可以节省大量时间。这可以通过layout中的template属性进行访问和修改:
fig.layout.template = template_name
图 3.13 展示了四种不同的模板及其名称:

图 3.13 – 四种不同的图形模板
完整的模板列表可以在plotly.io.templates中找到。
这在你希望图形具有与应用主题兼容的模板时非常有用。它也是一个很好的起点,可以让你选择一个模板,并根据需要修改其中的一些元素。
现在让我们回顾一下本章中涉及的主题。
总结
我们从介绍Figure对象、其组件和子组件开始。我们逐步学习了如何创建图形,以及如何修改它们的各个方面。我们还深入了解了图形的两个主要属性——data和layout属性。我们还探索了几种图形转换的方法,接着我们基于数据集创建了一个图表,并将其集成到我们的应用程序中。
到目前为止,通过你所阅读的章节,你已经知道如何创建和构建应用程序,如何通过创建回调函数将不同的页面组件连接起来使应用互动,以及如何构建适应整个系统的图表。
现在你已经知道如何构建完全互动的应用程序,并且通过本章所学,你还知道如何管理图形的各个方面,并确保它们易于阅读,这样用户就可以花更多时间进行分析,减少理解图表本身的时间。
我们简要观察了数据准备和处理的重要性,现在我们准备更深入地探讨它。在下一章中,我们将介绍Plotly Express,这是一种强大且更高层次的接口,用于简洁地创建图表。
第四章:第四章:数据操作与准备,为 Plotly Express 铺路
我们发现,准备数据可能比创建图表需要更多的脑力和代码。换句话说,如果我们在准备数据和决定如何以及做什么方面投入足够的时间,那么可视化过程将变得更加容易。到目前为止,我们只使用了数据集的一小部分,并且没有对其形状或格式进行任何更改。在制作图表时,我们遵循的是从头开始构建图表的方法,通过创建图形然后添加不同的层和选项,如轨迹、标题等。
在本章中,我们将深入熟悉数据集,并将其重塑为直观易用的格式。这将帮助我们使用一种新的方法来创建可视化,即使用Plotly Express。我们将不再从一个空白矩形开始并在其上构建图层,而是从数据集的特征(列)出发,根据这些特征创建可视化。换句话说,我们将不再是以屏幕或图表为中心,而是采用更以数据为导向的方法。我们还将比较这两种方法,并讨论何时使用它们。
我们将主要涵盖以下主题:
-
理解长格式(整洁型)数据
-
理解数据操作技能的作用
-
学习 Plotly Express
技术要求
从技术角度来看,本章不会使用任何新包,但作为 Plotly 的一个主要模块,我们可以把 Plotly Express 视为一个新的模块。我们还将广泛使用pandas进行数据准备、重塑和一般操作。所有这些主要将在 JupyterLab 中完成。我们的数据集将由存储在根目录data文件夹中的文件组成。
本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_04
查看以下视频,了解代码的实际应用:bit.ly/3suvKi4
让我们开始探索可以获取数据的不同格式,以及我们能为之做些什么。
理解长格式(整洁型)数据
我们将使用一个相对复杂的数据集。它由四个 CSV 文件组成,包含关于世界上几乎所有国家和地区的信息。我们有超过 60 个指标,跨越超过 40 年,这意味着有很多选择和组合可以选择。
在准备我们的数据集之前,我想通过一个简单的示例来展示我们的最终目标,这样你就能了解我们将要前进的方向。希望这也能解释为什么我们要投入时间进行这些更改。
Plotly Express 示例图表
Plotly Express 附带了一些数据集,方便你在任何时候进行练习和测试特定功能。它们位于plotly.express的data模块中,调用它们作为函数会返回相应的数据集。让我们来看看著名的 Gapminder 数据集:
import plotly.express as px
gapminder = px.data.gapminder()
gapminder
运行此代码将显示gapminder DataFrame 的示例行,如图 4.1所示:

图 4.1 – Plotly Express 中包含的 Gapminder 数据集
数据集结构看起来很简单。对于每一个独特的国家、大洲和年份的组合,我们有三个指标:lifeExp、pop和gdpPercap。iso_alpha和iso_num列似乎是国家的编码值。
让我们看看如何通过scatter图来总结gapminder的data_frame。
在x轴上,我们可以有y轴,最好能看到标记的size反映出相应国家的人口。
我们还可以将图表水平拆分(facet_col),在一行中为每个大洲创建子图,并使子图标题也能反映这一点。我们还可以为每个大洲的标记分配不同的color。为了更加清晰,我们可以将图表的title设置为'人均 GDP 与寿命预期 1952 – 2007'。
为了让它们更清晰,我们可以将 X 轴和 Y 轴标题的labels从'gdpPercap'更改为'人均 GDP',将'lifeExp'更改为'寿命预期'。
我们可以预期人均 GDP 存在离群值且不符合正态分布,因此我们可以将 X 轴的比例类型设置为对数(log_x)。Y 轴的范围(range_y)应为[20, 100]区间,这样我们就能看到在固定垂直范围内寿命预期的变化。
将鼠标悬停在标记上应该显示该国家的完整信息,悬停标签的标题(hover_name)应为该国的名称。将同一个图表叠加在所有年份上会显得非常杂乱,几乎无法阅读。因此,我们为每个年份设置一个单独的animation_frame。
如果我们能有一个播放按钮,当点击时,标记会按年移动,一个年为一帧,像视频一样播放,并且可以在某一年暂停,那就太好了。
图表的height应该是 600 像素:
px.scatter(data_frame=gapminder,
x='gdpPercap',
y='lifeExp',
size='pop',
facet_col='continent',
color='continent',
title='Life Expectancy and GDP per capita. 1952 - 2007',
labels={'gdpPercap': 'GDP per Capita',
'lifeExp': 'Life Expectancy'},
log_x=True,
range_y=[20, 100],
hover_name='country',
animation_frame='year',
height=600,
size_max=90)
运行上述代码将生成图 4.2中的可视化效果:

图 4.2 – 使用 Plotly Express 展示的 Gapminder 数据集交互式图表
我对这个过程的第一个观察是,描述图表所需的文字比代码多得多。实际上,只需要一行代码就能生成它。
点击播放按钮将动画化图表,每年会显示一个新帧。如果需要,你也可以暂停或跳转到某一年。这样你可以看到两个变量之间的关系如何随着年份推移而变化,就像看一部短片电影。
你还可以看到,当你悬停在表示某个国家的标记上时,会显示出所有相关数据,这些数据用于指定位置、大小、颜色以及我们可能设置的其他属性。hover_name 参数被设置为 'country',这就是为什么你看到它以粗体显示作为标签的标题。
在大多数情况下,我们有标记重叠,这使得理解图表变得困难。由于 Plotly 图形默认是交互式的,我们可以轻松使用模式栏按钮进行缩放,或者可以手动选择一个矩形进行放大。
通过选择仅包含非洲标记的矩形来放大非洲,图 4.3 展示了图表的变化,现在阅读非洲子图变得更容易了:

图 4.3 – 放大图表中的特定区域
请注意,其他大陆的图表也被放大到与非洲相同的缩放级别。可以自由探索更多交互式功能,但我希望这能展示出这种方法的强大和直观。
提示
本章中有许多彩色图表。我尽力确保你可以尽可能容易地区分不同的彩色标记。如果你正在阅读打印版,最好参考该书的彩色版本,该版本可以在线访问。
我们能够用一行代码创建如此丰富图表的原因有两个。首先,Plotly Express 拥有强大的功能,专门设计用来通过最少的代码生成这样的图表。稍后会详细介绍这一点。第二,数据集的结构在这个过程中起着重要作用。一旦我们的数据具有一致的格式,就很容易进行建模、可视化或进行任何类型的分析。
让我们来看看这种数据格式的主要方面。
长格式(整洁)数据的主要属性
该结构的一个关键特点是,它允许图表上的每个标记都通过一行独立表示。这些行中的每个值属于一个不同的列。反过来,这些列每个代表一个独立的变量,并具有自己的数据类型。这使得我们可以轻松地映射颜色、大小或任何其他视觉属性,只需声明我们希望用哪个视觉属性来表示哪个列的值。
请注意,我刚才说的内容接近 DataFrame 的定义:
-
一组列,每列只有一种数据类型。
-
DataFrame 中的列可以是不同类型的。
-
所有列的长度相同,即使它们可能包含缺失值。
从概念角度来看,长格式 DataFrame 和常规 DataFrame 之间的主要区别是每行包含一个观察值(例如:国家、个人、品牌或它们的组合),而每列包含一个变量(例如:人口、大小、长度、身高、收入等)。例如,国家列只包含国家信息,且该列中只会出现国家数据。因此,对于这些数据的访问不会产生任何歧义。
这种格式不是必需的,也不比其他格式更“正确”。它只是直观、一致且易于使用。我们刚刚制作的可视化的实际要求是:需要为 X 轴准备一组数值,Y 轴需要另一组相同长度的数值。对于其他特性,如颜色和大小,我们也需要相同长度的数字或名称集合,这样才能将它们正确地映射在一起。DataFrame 是这种需求的自然匹配。
在我们刚刚生成的图表中,你可以很容易地看到,我们可以通过移除size参数让所有标记保持相同的大小。将facet_col改为facet_row会立即将子图垂直堆叠,而不是并排显示。通过微小的调整,我们可以对可视化进行大幅改变。这就像在仪表盘上切换开关一样简单,带有一点幽默感!
我希望最终目标现在已经清楚了。我们要检查数据集中的四个文件,并查看如何生成长格式(整洁型)DataFrame。这样,每一列将包含关于一个变量的数据(例如:年份、人口、基尼指数等),而每一行则描述一个观察值(国家、年份、指标以及其他值的组合)。完成这些后,我们应该能够查看数据,指定我们想要的内容,并通过简洁的 Plotly Express 函数调用表达出来。
一旦开始准备过程,整个过程会更加清晰,所以我们现在就开始吧。
理解数据操作技能的作用
在实际情况下,我们的数据通常并不是我们希望的格式;我们通常有不同的数据集需要合并,而且常常需要对数据进行规范化和清理。正因如此,数据操作和准备将在任何数据可视化过程中发挥重要作用。因此,我们将在本章以及全书中重点关注这一点。
准备数据集的计划大致如下:
-
一一探索不同的文件。
-
检查可用的数据和数据类型,探索每种数据如何帮助我们对数据进行分类和分析。
-
在需要的地方重新塑形数据。
-
合并不同的 DataFrame,以增加描述数据的方式。
我们马上开始执行这些步骤。
探索数据文件
我们从读取data文件夹中的文件开始:
import os
import pandas as pd
pd.options.display.max_columns = None
os.listdir('data')
['PovStatsSeries.csv',
'PovStatsCountry.csv',
'PovStatsCountry-Series.csv',
'PovStatsData.csv',
'PovStatsFootNote.csv']
为了明确起见,我将使用每个文件名的独特部分作为每个 DataFrame 的变量名:'PovStats<name>.csv'。
系列文件
我们首先通过以下代码来探索series文件:
series = pd.DataFrame('data/'PovStatsSeries.csv')
print(series.shape)
series.head()
这将显示 DataFrame 的shape属性,以及前五行数据,如你在图 4.4中所见:

图 4.4 – PovStatsSeries 文件的前几行和列
似乎我们有 64 个不同的指标,并且每个指标都有 21 个属性、说明和注释。这个数据已经是长格式——列包含关于一个属性的数据,行是指标的完整表示,因此不需要做任何修改。我们只需要探索可用数据并熟悉这个表格。
使用这些信息,你可以轻松地设想为每个指标创建一个独立的仪表板,并将其放在单独的页面上。每一行似乎都包含足够的信息,以便生成一个独立的页面,包含标题、描述、详细信息等。页面的主要内容区域可以是该指标的可视化,涵盖所有国家和所有年份。这只是一个想法。
让我们更详细地看看一些有趣的列:
series['Topic'].value_counts()
Poverty: Poverty rates 45
Poverty: Shared prosperity 10
Poverty: Income distribution 8
Health: Population: Structure 1
Name: Topic, dtype: int64
我们可以看到这些指标分布在四个主题中,每个主题的计数可以在上面看到。
有一个计量单位的列,可能值得探索:
series['Unit of measure'].value_counts(dropna=False)
% 39
NaN 22
2011 PPP $ 3
Name: Unit of measure, dtype: int64
似乎我们有一些指标,其计量单位要么是百分比(比率),要么是不可用(NaN)。这可能会在以后帮助我们将某些类型的图表归为一类。
另一个重要的列是按主题列分组的series DataFrame,然后按计数和唯一值的数量总结限制和例外列的值:
(series
.groupby('Topic')
['Limitations and exceptions']
.agg(['count', pd.Series.nunique])
.style.set_caption('Limitations and Exceptions'))
输出可以在图 4.5中看到:

图 4.5 – 限制和例外的计数与唯一值
看起来这将成为我们了解不同指标的一个良好参考点。这对于用户也非常有帮助,这样他们也能更好地理解他们正在分析的内容。
国家文件
现在让我们来看一下下一个文件,'PovStatsCountry.csv':
country =\
pd.read_csv('data/PovStatsCountry.csv',na_values='',
keep_default_na=False)
print(country.shape)
country.head()
这将显示 DataFrame 的形状以及行和列的样本,如图 4.6所示:

图 4.6 – 来自国家文件的样本行和列
在调用read_csv时,我们指定了keep_default_na=False和na_values=''。原因是pandas将像NA和NaN这样的字符串解释为缺失值的指示符。纳米比亚这个国家有一个NA,因此它在 DataFrame 中缺失了。这就是我们需要进行此更改的原因。这是一个非常好的例子,说明事情可能以意想不到的方式出错。
这是关于我们数据集中国家和地区的非常有趣的元数据。它是一个非常小的数据集,但可以在丰富我们理解的同时,非常有助于提供更多的过滤和分组国家的选项。它也是长格式(tidy)。让我们看一看其中一些有趣的列。
Region 列似乎很直观。我们可以检查有哪些区域可用,以及每个区域内国家的数量:
country['Region'].value_counts(dropna=False).to_frame().style.background_gradient('cividis')
结果可以在图 4.7 中看到:

图 4.7 – 每个区域的国家数量
另一个可能有帮助的列是Income Group。一旦我们将其正确映射到相应的值,我们可能会考虑像本章第一部分中对大陆做的那样,按收入组拆分我们的子图:
country['Income Group'].value_counts(dropna=False)
Upper middle income 52
Lower middle income 47
High income 41
Low income 29
NaN 15
Name: Income Group, dtype: int64
拥有十五个NaN值与区域和分类的总数相符,稍后我们会看到这一点。国家的收入水平与其地理位置无关。
如果你查看Lower middle income,我认为区分它们是很重要的,我们可以轻松地为此创建一个特殊的列,这样我们就能区分国家和非国家。
is_country 布尔型列:
country['is_country'] = country['Region'].notna()
图 4.8 显示了包含国家和地区以及分类的行样本:

图 4.8 – 含有 is_country 列的国家和地区样本
可以通过获取country DataFrame 的子集,筛选出Region列为空值的行,然后获取Short Name列,查看这些分类的完整列表:
country[country['Region'].isna()]['Short Name']
37 IDA countries classified as fragile situations
42 East Asia & Pacific
43 Europe & Central Asia
50 Fragile and conflict affected situations
70 IDA total
92 Latin America & Caribbean
93 Low income
95 Lower middle income
96 Low & middle income
105 Middle East & North Africa
107 Middle income
139 South Asia
147 Sub-Saharan Africa
170 Upper middle income
177 World
Name: Short Name, dtype: object
遍历这个过程对帮助你规划仪表板和应用程序非常重要。例如,知道我们有四个收入水平的分类意味着并排创建它们的子图是合理的。但如果我们有 20 个分类,可能就不太适合这样做了。
让我们再创建一个列,然后继续处理下一个文件。
由于我们处理的是国家,可以使用国旗作为直观且易于识别的标识符。由于国旗是表情符号,且本质上是 Unicode 字符,它们可以像其他常规文本一样在我们的图表上呈现为文本。我们以后还可以考虑使用其他表情符号作为符号,帮助读者轻松识别增长与下降,例如(使用相关的箭头符号和颜色)。当空间有限而你仍然需要与用户沟通时,尤其是在小屏幕上,这也很有用。一张表情符号胜过千言万语!
关于国家国旗表情符号有趣的是,它们是由两个特殊字母连接而成,这些字母的名称是"REGIONAL INDICATOR SYMBOL LETTER <字母>"。例如,这些是字母 A 和 B 的区域指示符符号:AB。
你只需获取某个国家的两位字母代码,然后通过 unicodedata Python 标准库模块查找该国家的名称。lookup函数接受一个字符名称并返回该字符本身:
from unicodedata import lookup
lookup('LATIN CAPITAL LETTER E')
'E'
lookup("REGIONAL INDICATOR SYMBOL LETTER A")
'A'
一旦我们得到了代表国家的两位字母代码,我们就可以查找它们,并将它们连接起来生成相应国家的国旗。我们可以创建一个简单的函数来实现这一点。我们只需要处理那些提供的字母是NaN或不属于国家代码列表的情况。
我们可以创建一个country_codes变量并进行检查。如果提供的字母不在列表中,我们返回空字符,否则我们创建一个表情符号国旗:
country_codes = country[country['is_country']]['2-alpha code'].dropna().str.lower().tolist()
现在我们可以轻松地定义flag函数:
def flag(letters):
if pd.isna(letters) or (letters.lower() not in country_codes):
return ''
L0 = lookup(f'REGIONAL INDICATOR SYMBOL LETTER {letters[0]}')
L1 = lookup(f'REGIONAL INDICATOR SYMBOL LETTER {letters[1]}')
return L0 + L1
使用这个函数,我们可以创建我们的flag列:
country['flag'] =\
[flag(code) for code in country['2-alpha code']]
图 4.9 显示了随机选择的国家、它们的国旗以及is_country列:

图 4.9 – 显示国家及其国旗的行样本
如果是NaN的情况,因为在许多情况下我们可能希望将国家名称与其国旗连接起来,例如标题或标签,空字符串不会导致任何问题。请注意,如果你将数据框保存到文件并重新打开,pandas会将空字符串解释为NaN,你将需要将它们转换或防止它们被解释为NaN。
国家系列文件
我们的下一个文件 "PovStatsCountry-Series.csv" 简单地包含了国家代码的列表,并展示了它们的人口数据来源。我们将看看是否/何时可以将其作为元数据在相关图表中使用。
脚注文件
接下来,我们快速查看PovStatsFootNote.csv的脚注文件:
有一个空的列YR2015,因此我们从索引 2 开始提取字符。我们重命名了列,以使其与series数据框一致,这样在需要时便于合并:
footnote = pd.read_csv('data/PovStatsFootNote.csv')
footnote = footnote.drop('Unnamed: 4', axis=1)
footnote['Year'] = footnote['Year'].str[2:].astype(int)
footnote.columns = ['Country Code', Series Code', 'year', 'footnote']
footnote
图 4.10 显示了footnote数据框中的几行:

图 4.10 – 脚注文件中的行样本
看起来像是大量关于数据的注释。我们应该确保以某种方式包含它们,以确保读者能够获得完整的视图。这些脚注似乎是基于国家、指标和年份的组合。由于这三者在其他表格中以一致的方式编码,因此应该可以轻松地将它们整合并映射到其他地方的相关值。
数据文件
接下来是主数据文件,我们已经在前面的章节中使用过,但现在我们想要重新整理并与其他数据框合并,以便更直观、更强大地查看我们的数据集。
现在让我们探索这个文件:
data = pd.read_csv('data/PovStatsData.csv')
data = data.drop('Unnamed: 50', axis=1)
print(data.shape)
data.sample(3)
上面的代码删除了名为data的列,并显示了行的随机样本,正如你在图 4.11中看到的:

图 4.11 – 数据文件中的行和列样本
了解缺失值的数量及其占所有值的百分比总是很有趣的。有趣的部分是从 isna 方法返回的每列布尔值的 Series。取其均值即可得到每列缺失值的百分比,结果是一个 Series。再运行一次 mean 可以得到缺失值的总体百分比:
data.loc[:, '1974':].isna().mean().mean()
0.9184470475910692
我们有 91.8% 的单元格是空的。这对结果有重要的影响,因为大部分时间我们没有足够的数据,或者某些国家的数据缺失。例如,许多国家在九十年代初之前并没有以现有形式存在,这就是其中一个原因。你可以查看 series DataFrame,以及有关指标和数据收集问题的所有信息(如果适用)。
现在让我们探讨如何将 DataFrame 转换为长格式,并且更重要的是,为什么我们要这么做。
使 DataFrame 变长
你可能首先注意到的一点是,年份被分布在不同的列中,值对应于它们,每个值都在对应年份下的各自单元格中。问题是,1980 并不是真正的一个变量。一个更有用的方式是拥有一个 year 变量,在该列中,值会从 1974 年到 2019 年不等。如果你记得我们在本章创建第一个图表的方式,你就能明白这样做能让我们的工作变得更加轻松。让我用一个小数据集来说明我的意思,这样事情会更清楚,然后我们可以在 data DataFrame 上实施相同的方法。
图 4.12 展示了我们如何以不同的结构展示相同的数据,同时保持相同的信息:


图 4.12 – 包含相同信息的两个数据集,采用两种不同的格式
我们当前的 DataFrame 结构如右侧的表格所示,使用左侧那种格式会更加方便。
宽格式的难点在于变量的呈现方式不同。在某些情况下,变量是垂直显示在一列中(国家 和 指标),而在其他情况下,它们是水平显示在 2015 和 2020 等列中。访问长格式 DataFrame 中相同的数据非常简单:我们只需指定想要的列。此外,我们可以自动映射值。例如,从长格式 DataFrame 中提取 year 和 value 列时,系统会自动将 2015 映射为 100,2015 映射为 10,依此类推。同时,每一行都是我们所处理的案例的完整且独立的表示。
好消息是,这可以通过一次调用melt方法来实现:
wide_df.melt(id_vars=['country', 'indicator'],
value_vars=['2015', '2020'],
var_name='year')
下面是前述代码和参数的概述:
-
id_vars:将这些列作为行保留,并根据需要重复它们以保持映射关系。 -
value_vars:将这些列作为值,将它们“熔化”成一个新列,并确保与其他值的映射与之前的结构一致。如果我们没有指定value_vars,那么该操作将应用于所有未指定的列(除了id_vars)。 -
var_name:可选。您希望新创建的列命名为何—在此情况下为“year”。
让我们在我们的data数据框上执行此操作:
id_vars =['Country Name', 'Country Code', 'Indicator Name', 'Indicator Code']
data_melt = data.melt(id_vars=id_vars,
var_name='year').dropna(subset=['value'])
data_melt['year'] = data_melt['year'].astype(int)
print(data_melt.shape)
data_melt.sample(10)
这段代码与前面的示例几乎相同。我们首先创建了一个id_vars的列表,并将其用作同名参数的参数。紧接着,我们删除了value列下的缺失值。我们本可以通过使用value_name参数来更改该列的名称,但“value”似乎比较合适。然后,我们将年份转换为整数。运行这段代码会显示新data_melt数据框的形状和示例,见图 4.13:

图 4.13 – 数据框在被“熔化”后的样子
前四列与之前相同,每个唯一的组合保持不变。现在,我们将所有年份列及其值压缩成了两列,year和value。
现在让我们看看如何通过对其他列执行逆操作进一步改进结构。
数据框透视
指标名称列可以通过对我们刚才对年份列进行的操作的逆操作来改进。理想情况下,我们应该为人口、贫困率等分别创建不同的列。让我们首先使用我们的长格式(已“熔化”)示例数据框来演示,以便更清楚地理解。
假设我们想要使用pivot方法转换唯一值。这样可以通过使用melt方法实现“回程”,返回到原来的格式。这里,我正在对不同的列使用它:
melted.pivot(index=['year', 'indicator'],
columns='country',
values='value').reset_index()
运行此代码将把“熔化”后的数据框转换为宽格式(透视)数据框,您可以在图 4.14中看到:


图 4.14 – 从长格式到宽格式的转换
data_melt包含可以更好用作列名的名称,因此每个指标可以独立地表示在自己的列中,以便与我们的数据表示保持一致:
data_pivot =\
data_melt.pivot(index=['Country Name', 'Country Code', 'year'],
columns='Indicator Name',
values='value').reset_index()
print(data_pivot.shape)
data_pivot.sample(5)
这将生成我们的data_pivot数据框,您可以在图 4.15中看到其示例:

图 4.15 – 长格式(整洁)贫困数据框
如果我们的工作是正确的,那么每一行现在应该有一个唯一的国家和年份的组合。这实际上就是这个练习的核心。让我们来检查一下我们的工作是否正确:
data_pivot[['Country Code', 'year']].duplicated().any()
False
现在,行中包含了国家名称、代码和年份,以及所有不同指标的值。通过将country数据框中的元数据包含在内,国家信息可以得到丰富。我们来看一下merge函数,之后我们将开始使用 Plotly Express。
合并数据框
首先,让我们看一个简单的示例,了解合并是如何工作的,然后我们可以合并data_pivot和country数据框。图 4.16展示了如何将两个数据框进行合并:

图 4.16 数据框是如何合并的
合并操作可以通过merge函数来完成:
pd.merge(left=left, right=right,
left_on='country',
right_on='country',
how='left')
以下是前述pd.merge调用的详细信息:
-
left_on:来自left数据框的列名,用于合并。 -
right_on:来自right数据框的列名,用于合并。 -
how:合并方法。在这种情况下,"left"表示取left中的所有行,并只与right中值相同的行进行匹配。如果right中没有匹配的行,那么country列中的这些行将被丢弃。合并后的数据框应该与左侧数据框拥有相同的行数。
这个函数还有其他几个选项,非常强大。确保查看其他合并方法:inner、outer 和 right。对于我们的例子,我们将使用前面示范的选项,现在就开始吧。我们将以相同的方式合并data_pivot和country:
poverty = pd.merge(data_pivot, country,
left_on='Country Code',
right_on='Country Code',
how='left')
print(poverty.shape)
poverty
该合并操作生成了poverty数据框,您可以在图 4.17中看到:

图 4.17 – 合并 data_pivot 和 country
快速检查,确保我们的工作是正确的:
poverty[['Country Code', 'year']].duplicated().any()
False
右侧矩形中的八个附加列是我们添加到poverty数据框中的一些附加列。现在,过滤某个地区或收入组,按国家筛选,按其值着色,或按我们想要的方式进行分组变得非常容易。现在看起来像是 Gapminder 数据集,只是有更多的指标和年份,以及关于国家的更多元数据。
现在我们有了一个结构一致的数据框。
每一列都包含关于一个且仅一个变量的数据。列中的所有值都是相同的数据类型(或缺失值)。每一行都能独立表示一个完整的观测结果,因为它包含了所有可用的完整信息,就像其他行一样。
重要提示
长格式的主要缺点是它在存储上效率低下。从这个角度来看,我们不必要地重复了许多值,这占用了大量空间。我们稍后会处理这个问题,但请记住,这种格式在作为开发者的时间效率方面是极其高效的。正如我们在几个示例中看到的,一旦映射一致,创建和修改可视化就变得更加容易。
我强烈推荐阅读 Hadley Wickham 的 Tidy Data 论文,深入讨论数据格式的几种方式以及不同的解决方案。这里展示的示例灵感来源于这些原则:www.jstatsoft.org/article/view/v059i10。
我们现在准备好探索如何使用 Plotly Express,首先使用一个玩具数据集,然后使用我们准备的数据集。
学习 Plotly Express
Plotly Express 是一个更高级的绘图系统,建立在 Plotly 的基础上。它不仅处理一些默认设置,例如标注坐标轴和图例,还使我们能够利用数据通过视觉美学(如大小、颜色、位置等)表达其许多特征。只需声明我们希望通过哪个数据列表达哪些特征,基于一些关于数据结构的假设,就可以轻松做到这一点。因此,它主要为我们提供了从数据角度解决问题的灵活性,就像本章开头提到的那样。
让我们先创建一个简单的 DataFrame:
df = pd.DataFrame({
'numbers': [1, 2, 3, 4, 5, 6, 7, 8],
'colors': ['blue', 'green', 'orange', 'yellow', 'black', 'gray', 'pink', 'white'],
'floats': [1.1, 1.2, 1.3, 2.4, 2.1, 5.6, 6.2, 5.3],
'shapes': ['rectangle', 'circle', 'triangle', 'rectangle', 'circle', 'triangle', 'rectangle', 'circle'],
'letters': list('AAABBCCC')
})
df
这将生成 图 4.18 中的 DataFrame:

图 4.18 – 一个简单的示例 DataFrame
我们通常通过调用图表类型函数来使用 Plotly Express,例如 px.line、px.histogram 等。每个函数都有自己的一组参数,具体取决于它的类型。
有多种方式可以将参数传递给这些函数,我们将重点介绍两种主要的方法:
-
带有列名的 DataFrame:在大多数情况下,第一个参数是
data_frame。你设置要可视化的 DataFrame,然后指定你想要的参数所使用的列。对于我们的示例 DataFrame,如果我们想要创建一个散点图,可以使用px.scatter(data_frame=df, x='numbers', y='floats')。 -
数组作为参数:另一种指定参数的方式是直接传入列表、元组或任何类似数组的数据结构,而不使用
data_frame参数。我们可以通过运行px.scatter(x=df['numbers'], y=df['floats'])来创建相同的散点图。这是一种直接且非常快速的方法,适用于你想要探索的列表。
我们也可以将这些方法结合使用。我们可以设置一个data_frame参数,并将一些列名作为参数传入,当需要时,也可以为其他参数传入单独的列表。几个示例应该能轻松说明这些要点。以下代码展示了创建散点图是多么简单:
px.scatter(df, x='numbers', y='floats')
图 4.19 显示了在 JupyterLab 中的结果图:

图 4.19 – 使用 Plotly Express 创建散点图
我敢肯定你已经注意到,X 轴和 Y 轴的标题已经由系统默认设置。它会使用我们提供的参数名称(在这个例子中是数据框列名)来设置这些标题。
我们的数据框中还有其他变量,我们可能有兴趣检查它们之间是否存在任何关系。例如,让我们检查浮动和形状之间是否有关系。
我们可以重新运行相同的代码,并添加两个参数,使我们能够区分哪些标记属于哪个形状。我们可以使用color参数来做到这一点,系统会根据symbol参数为每个标记分配不同的颜色,以便轻松区分它们。这也使得彩屏的读者更容易理解,因为通过提供两个信号来区分标记:
Px.scatter(df,
x='numbers',
y='floats',
color='shapes',
symbol='shapes')
图 4.20 显示了在 JupyterLab 中的代码和结果图:

图 4.20 – 为标记分配颜色和符号
请注意,我们有一个图例帮助我们区分标记,告诉我们哪个颜色和符号属于哪个形状。它还拥有自己的标题,所有这些都是默认生成的。
似乎浮动和形状之间没有关系。那么,我们来尝试根据字母列来上色并设置符号,方法是使用以下代码:
px.scatter(df,
x='numbers',
y='floats',
color='letters',
symbol='letters',
size=[35] * 8)
图 4.21 演示了这一点:

图 4.21 – 使用独立列表设置标记大小
我们现在可以根据字母看到明显的差异。这展示了通过快速尝试不同的选项来探索数据集是多么容易。请注意,这次我们还混合了方法,给标记设置了size。大小没有映射到某个值,它是为了让符号更大、更容易看见。因此,我们只是传递了一个包含我们想要的标记大小的列表。这个列表的长度必须与我们要可视化的其他变量相同。
让我们用相同的方法和相同的数据集来探索条形图。我们可以通过barmode参数调整条形的显示方式,像这样:
px.bar(df, x='letters', y='floats', color='shapes', barmode='group')
图 4.22 展示了两种不同的条形显示方式——默认方式是将条形叠加在一起,而 "group" 方式则是将条形分组显示,正如你所看到的:


图 4.22 – 使用不同显示模式(barmode)创建条形图
关于长格式(整洁格式)数据的讨论应该能让你非常容易理解如何使用 Plotly。你只需要对图表类型及其工作原理有基本了解,然后你就可以轻松设置你想要的参数。
重要提示
Plotly Express 不要求数据必须是长格式的。它非常灵活,可以处理宽格式、长格式以及混合格式的数据。此外,pandas和numpy在数据处理上非常灵活。我只是认为,为了提高个人生产力,最好使用一致的方法。
现在让我们看看 Plotly Express 如何与Figure对象相关,以及何时使用哪种方法。
Plotly Express 和 Figure 对象
了解所有调用 Plotly Express 图表函数的返回值都是Figure对象是非常有帮助的,这个对象就是我们在第三章中讨论的与 Plotly 的 Figure 对象协作。这对于在创建图表后定制它们非常重要,以防你想更改默认设置。假设你创建了一个散点图,然后你想在图上添加一个注释来解释某些内容。你可以像在上一章中那样进行操作:
import plotly express as px
fig = px.scatter(x=[1, 2, 3], y=[23, 12, 34])
fig.add_annotation(x=1, y=23, text='This is the first value')
你所知道的关于Figure对象及其结构的所有内容都可以与 Plotly Express 一起使用,因此这建立在你已有的知识基础上。
这自然引出了一个问题:什么时候使用 Plotly Express,什么时候使用 Plotly 的graph_objects模块来从更低的层次创建图表。
这个问题可以通过问一个更一般性的问题来解决:给定两个在不同抽象层次执行相同操作的接口,我们如何在它们之间做出选择?
考虑三种不同的做披萨的方法:
-
订购方法:你打电话给餐厅,点了一份披萨。它半小时后送到你家门口,你开始吃。
-
超市方法:你去超市,买面团、奶酪、蔬菜和所有其他食材。然后你自己做披萨。
-
农场方法:你在后院种番茄。你养牛,挤奶,然后把奶转化为奶酪,等等。
当我们进入更高层次的接口,走向订购方法时,所需的知识量大大减少。其他人承担责任,市场力量——声誉和竞争——检查质量。
我们为此付出的代价是减少了自由度和选择的余地。每家餐厅都有一系列选择,你必须从中选择。
当深入到更低的层次时,所需的知识量增加,我们必须处理更多的复杂性,承担更多的结果责任,且花费更多的时间。我们在这里得到的是更多的自由和权力,可以按我们想要的方式自定义我们的结果。成本也是一个重要的好处,但只有在规模足够大的情况下。如果你今天只想吃一块披萨,可能订外卖更便宜。但如果你计划每天吃披萨,那么如果自己做,预计会有很大的成本节省。
这是你在选择更高层次的 Plotly Express 和更低层次的 Plotly graph_objects 之间的权衡。
由于 Plotly Express 返回的是 Figure 对象,因此通常这不是一个困难的决定,因为你可以事后修改它们。一般来说,在以下情况下使用 graph_objects 模块是个不错的选择:
-
非标准可视化:本书中创建的许多图表都是使用 Plotly 完成的。使用 Plotly Express 创建这类图表会相当困难,因为它们不是标准图表。
-
graph_objects模块。 -
graph_objects。
一般来说,Plotly Express 通常是创建图表的更好起点,正如我们看到它是多么强大和方便。
现在你已经准备好使用 poverty 数据集,利用 Plotly Express 从数据开始指定你想要的可视化。
使用数据集创建 Plotly Express 图表
让我们看看如何使用散点图总结 poverty data_frame:
-
创建
year、indicator和一个分组(grouper)度量变量用于可视化。分组度量将用于区分标记(通过颜色和符号),可以从数据集中提取任何类别值,如地区、收入组等:year = 2010 indicator = 'Population, total' grouper = 'Region' -
基于这些变量,创建一个 DataFrame,其中
year列等于year,按indicator排序,并移除indicator和grouper列中的任何缺失值:df = (poverty[poverty['year'].eq(year)] .sort_values(indicator) .dropna(subset=[indicator, grouper])) -
将
x轴的值设置为indicator,并将y轴的值设置为 "Country Name" 列。标记的color和symbol应使用grouper设置。X 轴值预计会有异常值,并且不是正态分布的,因此将log_x设置为True。每个悬浮标签的hover_name应包含国家名称及其国旗。将图表的title设置为indicator、"by"、grouper和year的组合。给标记一个固定的size,并将height设置为700像素:px.scatter(data_frame=df, x=indicator, y='Country Name', color=grouper, symbol=grouper, log_x=True, hover_name=df['Short Name'] + ' ' + df['flag'], size=[1]* len(df), title= ' '.join([indicator, 'by', grouper, str(year)]), height=700)这将创建图 4.23中的图表:

图 4.23 – 使用贫困数据集的 Plotly Express 图表
通过简单地玩弄 year、grouper 和 indicator 的不同组合,你可以生成数百个图表。图 4.24 展示了一些示例:


图 4.24 – 使用相同数据集的其他图表
借助这些强大的功能,以及将数据按变量组织为观测值的格式,我们可以通过几种视觉属性轻松地可视化数据的六个或七个属性:X 轴、Y 轴、标记大小、标记符号、标记颜色、面板(列或行)和动画。我们还可以使用悬停标签和注释来增加更多的上下文和信息。通过选择将哪个列映射到哪个属性,我们可以简单地探索这些属性的任何组合。
现在让我们来探索一下将外部资源轻松地加入到我们的数据集中有多简单。
向我们的数据集添加新数据和列
有很多方法可以添加更多数据,但我想突出介绍两种非常简单且有效的方法:
-
pandas的read_html函数可以下载网页上的所有表格,你可以非常轻松地下载任何此类列表。假设它包含国家代码,你可以将其与主数据框合并,然后开始相应地分析。这也可以是一个过滤机制,你只需要所有国家中的一个子集。 -
添加新数据:世界银行拥有成千上万的类似数据集。例如,我们这里的人口数据是总人口数。还有很多详细的、按性别、年龄和其他因素划分的人口数据集。通过世界银行的 API,你可以轻松获取其他数据,合并数据,并立即丰富你的分析。
现在让我们回顾一下我们在本章和本书的第一部分中做了什么。
总结
现在你已经掌握了足够的信息,并且看到了足够的示例,可以快速创建仪表板。在第一章《Dash 生态系统概览》中,我们了解了应用程序的结构,并学会了如何构建完整运行的应用程序,但没有交互性。在第二章《探索 Dash 应用程序的结构》中,我们通过回调函数探索了交互性的工作原理,并向应用程序添加了交互功能。第三章《使用 Plotly 的图形对象》介绍了 Plotly 图表的创建方法、组成部分以及如何操作它们以获得所需的结果。最后,在本章中,我们介绍了 Plotly Express,这是一个易于使用的高层接口,最重要的是,它遵循一种以数据为导向的直观方法,而非以图表为导向的方法。
创建可视化的最重要和最大部分之一是将数据准备为特定格式的过程,之后创建这些可视化就变得相对简单。投资于理解数据集的结构,并投入时间和精力来重塑数据,最终会带来丰厚回报,正如我们在本章的详细示例中所看到的那样。
凭借这些知识和示例,以及我们对数据集的熟悉和丰富它的简单机制,我们现在准备更详细地探索不同的 Dash 组件以及不同类型的图表。
第二部分将深入探讨不同的图表类型、如何使用它们,以及如何将它们与 Dash 提供的交互功能结合的不同方式。
第二部分:使用真实数据为您的应用添加功能
本节将向您展示如何开始使用真实数据进行构建,并探索如何利用 Dash 的全部交互选项。
本节包括以下章节:
-
第五章**,通过条形图和下拉菜单进行交互式比较值
-
第六章**,使用散点图探索变量并通过滑块过滤子集
-
第七章**,探索地图图表并通过 Markdown 丰富您的仪表板
-
第八章**,计算数据频率并构建交互式表格
第五章:第五章:使用条形图和下拉菜单进行交互式值比较
现在,你已经掌握了构建交互功能并链接页面元素的所有基本知识,能够轻松制作交互式仪表板。主要概念已经通过多个示例进行了介绍,接下来我们将专注于特定类型的图表及其提供的不同选项。更重要的是,我们将深入探讨如何定制图表,使其适应多种用途。首先,要确保它们足够好,能够发布并与更广泛的受众分享,而不仅仅是为了你的交互使用;其次,要确保它们能够适应可能包含其他组件的页面,并确保我们能够以最优化的方式利用可用空间。另一个需要讨论的重要方面是图表的动态特性,用户可以生成的图表会根据选择的交互组件选项,可能包含 7 个,甚至 70 个元素来绘制。在某些情况下,数据集可能不包含任何数据。这会极大地影响最终图表的效果和可用性,甚至在某些情况下可能会使它们难以阅读。我们将探讨几种解决方案,以应对这些情况。
换句话说,我们正在尝试从使用一个仅仅完成其预定功能的原型,转向使用一个可以共享或发布给广泛受众的产品。
本书第二部分的各章节将重点介绍一种图表类型和一个交互组件,以探索它们的选项。在本章中,我们将探索条形图及其如何与下拉菜单组件(来自Dash Core Component)结合使用。这些组件的性质并没有将某一特定组件与某种图表类型直接关联。它们只是为了组织目的一起使用。下拉菜单可以与任何类型的图表一起使用,任何类型的交互组件也可以用来操作条形图。
我们将专注于以下主题:
-
垂直和水平绘制条形图
-
将条形图与下拉菜单链接
-
探索显示多个条形图的不同方式(堆叠、分组、叠加和相对)
-
使用面板将图表拆分成多个子图——水平、垂直或包装式
-
探索下拉菜单的其他功能(允许多选、添加占位符文本等)
技术要求
我们将继续使用我们现在熟悉的包——JupyterDash和Dash,分别用于原型设计和集成到我们的应用程序中。对于数据处理,我们将使用pandas,而JupyterLab将是我们构建和测试各种选项的起点。然后,我们将使用 Dash 核心组件、Dash HTML 组件和 Dash Bootstrap 组件来更新我们的应用程序。
我们将使用的数据集与上一章中创建的poverty DataFrame 相同。该章节的代码文件可以在 GitHub 上找到,网址是github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_05。
请查看以下视频,了解代码如何运行:bit.ly/3ebv8sk。
让我们从探索条形图的两种主要显示方式开始——垂直和水平。
垂直和水平绘制条形图
条形图的默认显示方式是垂直的。这种方式直观且易于理解。每个类别或项在* x 轴上占据一个独立的位置,条形的高度代表了y*轴上的某个数量。条形图水平显示时也是如此,只不过此时条形的宽度代表了数量。通常,在值较少的情况下,垂直显示效果较好。然而,在以下两种情况下,水平显示可能更加有效:
-
当我们有很多类别时:在这种情况下,条形图可能无法完全显示在屏幕上,我们可能需要将条形的宽度缩小到比默认值更窄,或者可能需要强制启用横向滚动,这比垂直滚动显得不那么自然。
-
当类别名称较长时:这其实并不是一个大问题,解决方法也很简单。Plotly 已经为我们处理了这个问题,通过自动调整名称(刻度标签)显示的角度。如果需要,名称也可以垂直显示以充分利用空间。然而,水平显示文本是最自然的方式,尤其适合这种情况。
让我们通过poverty DataFrame 来实际查看这些选项的效果,同时也更好地了解我们的数据集。我们将看一下最常用的收入/财富不平等度量之一——基尼指数。它也被称为基尼比率或系数。为了了解它的一些基本信息,我们可以使用包含我们将要处理的指标信息的series DataFrame:
-
导入
pandas并创建series变量。我们根据文件名选择了该变量名,正如在上一章中所做的那样。请不要将其与pandas.Series对象混淆:import pandas as pd series = pd.read_csv('data/PovStatsSeries.csv') -
创建一个名为
gini的变量,作为使用指标长名称的简化替代方式:gini = 'GINI index (World Bank estimate)' -
使用同名列提取指标的详细定义:
series[series['Indicator Name']==gini]['Long definition'].values[0] Gini index measures the extent to which the distribution of income (or, in some cases, consumption expenditure) among individuals or households within an economy deviates from a perfectly equal distribution. A Lorenz curve plots the cumulative percentages of total income received against the cumulative number of recipients, starting with the poorest individual or household. The Gini index measures the area between the Lorenz curve and a hypothetical line of absolute equality, expressed as a percentage of the maximum area under the line. Thus a Gini index of 0 represents perfect equality, while an index of 100 implies perfect inequality. -
知道这些值的范围在 0 到 100 之间,让我们检查所有年份和国家的最极端值:
poverty[gini].min(), poverty[gini].max() (20.2, 65.8) -
我们还可以通过
pandas的describe方法来更好地了解这一列:Poverty[gini].describe() count 1674.000000 mean 38.557766 std 9.384352 min 20.200000 25% 31.300000 50% 36.400000 75% 45.275000 max 65.800000 Name: GINI index (World Bank estimate), dtype: float64
我们将深入了解这个指标,并交互式地探索和比较不同国家在不同年份的数据,但来自Limitations and exceptions栏目的这句话让我印象深刻:“因为基础的家庭调查在方法和收集的福利衡量标准上有所不同,所以不同国家之间,甚至同一国家不同年份之间的数据不能严格比较。”
所以,我们必须小心不要完全依赖这些值,并且记住这个限制。
现在我们对该指标稍微熟悉一些,准备探索使用条形图可视化它的各种选项:
-
我们首先创建一个名为
df的poverty数据框的子集。这个子集将包含year值等于任意选择的年份的数据。然后,我们去除缺失值,并使用gini列对可用数据进行排序:year = 1980 df =\ poverty[poverty['year']==year].sort_values(gini).dropna(subset=[gini]) -
我们现在可以轻松地使用 Plotly Express 创建我们的基尼指数条形图。代码还通过将指标名称与所选年份连接,动态生成标题:
import plotly.express as px px.bar(df, x='Country Name', y=gini, title=' - '.join([gini, str(year)]))运行之前的代码会生成图 5.1中的图表:

图 5.1 – 1980 年的基尼指数条形图
对于 1980 年,似乎我们只有三个国家的数据,将它们垂直显示似乎是可以接受的,既容易读取又清晰。现在让我们对 1990 年重复同样的过程,并在图 5.2中查看结果:

图 5.2 – 1990 年的基尼指数条形图
我们可以读取国家名称,但不像图 5.1那样自然,如果它们是水平显示的话。如果用户在更窄的屏幕上查看相同的图表,国家名称将会垂直显示,阅读起来就更加困难,如图 5.3所示:

图 5.3 – 1990 年的基尼指数条形图,国家名称垂直显示
在近年来,我们有更多国家的数据,在这种情况下,水平空间不足以容纳所有国家。一些国家名称甚至没有显示,除非你悬停在相应的条形上,或者放大到图表的该部分。例如,你可以在图 5.4中看到 2010 年的相同图表:

图 5.4 – 2010 年的基尼指数条形图,其中部分国家名称未显示
根据我们刚才看到的图表,我们现在对动态生成的交互式基尼指数水平条形图的挑战有了更好的理解。如果我们希望用户选择感兴趣的年份,那么我们需要处理一些问题。
首先,这个指标的可用值的数量从 3 到 150 多个,范围非常大。其次,最好且更安全使用水平排列,因为在所有情况下,国家名称将水平显示,无论名称多长,都容易阅读。通过在调用px.bar时设置orientation='h',这些问题可以轻松解决,但仍然有一个挑战。我们需要根据所选年份中可用国家的数量来确定图表的最佳高度,正如我们刚才看到的那样,范围有多大。我们首先看一下当图表显示为水平时的样子,然后设计一个交互式的解决方案。我们将运行相同的代码,但有两个主要区别。x和y参数需要交换,因为它们将分别取反方向的轴,并且我们还需要为orientation参数设置适当的值,在这种情况下为h表示“水平”:
year = 2000
px.bar(df,
x=gini,
y='Country Name',
title=' - '.join([gini, str(year)]),
orientation='h')
上面的代码生成了图 5.5,这是 2000 年所有可用国家的基尼指数水平条形图:

图 5.5 – 2000 年所有可用国家的基尼指数水平条形图
现在,国家名称非常容易读取(至少是显示的那些),但条形图过于狭窄且拥挤。图表看起来不必要地宽(尤其是考虑到最小值和最大值位于[20.2, 65.8]区间内)。如果需要,我们可以在函数调用中手动设置图表的宽度,但我们需要想办法动态设置图表的高度,可以通过height参数来设置。
一种方法是设置固定的像素高度。然后,根据df中的国家数量,我们可以为每个国家添加 20 像素。例如,如果df中有 10 个国家,那么我们的高度就是 200 + (10x20) = 400 像素。创建df后,我们可以轻松计算出它包含的国家数量,并将其赋值给一个变量n_countries。修改后的代码如下所示:
year = 2000
df =\
poverty[poverty['year']==year].sort_values(gini).dropna(subset=[gini])
n_countries = len(df['Country Name'])
px.bar(df,
x=indicator,
y='Country Name',
title=' - '.join([gini, str(year)]),
height=200 + (20*n_countries),
orientation='h')
运行上述代码,在三个国家数量不同的年份中,生成了图 5.6中的图表:

图 5.6 – 基于国家数量动态高度的各种水平条形图
右侧的长图已调整大小以适应页面,但在条形的高度和国家名称的可读性方面与其他图表基本相同。所有国家都清晰可见,易于阅读,且没有任何内容被隐藏。
通过这个解决方案,我们通过根据国家数量动态设置图形的总高度,处理了可以选择的国家数量的动态变化。
这种方法可以看作是一种探索方法。用户并不确切知道自己在寻找什么;他们选择一个年份,并查看该年份可用的所有数据。在选择了一些选项后,他们可能有兴趣深入了解某些国家的具体信息。例如,他们可能对某个国家的基尼指数随时间的变化感兴趣。接下来我们将实现这一点。
创建具有多个值的竖条图
当我们想让用户可视化一个国家的基尼指数(或任何其他指标)如何随时间变化时,我们可以通过竖条图来实现。因为年份代表了一系列事件,将它们并排显示是自然的,因为这显示了随时间变化的趋势。而且,由于年份是一个数字序列,我们没有像国家名称那样的可读性问题。即使条形图变得更窄,甚至某些条形图没有显示出来,用户也能轻松地在脑海中“填补空白”,在需要的地方理解数据。
生成这样的图表的代码与上一个非常相似,实际上更简单,因为我们不需要担心动态设置高度。我们将使用Country Name作为动态变量,而不是year。df的定义将取决于数据集中包含所选国家的行:
country = "Sweden"
df = poverty[poverty['Country Name']==country].dropna(subset=[gini])
现在我们可以通过以下代码直观地生成图表:
px.bar(df,
x='year',
y=gini,
title=' - '.join([gini, country]))
运行前面的代码生成的图表是针对瑞典的,如图 5.7所示:

图 5.7 – 带有年份作为 x 轴的竖条图
请注意,即使某些年份没有对应的值,这些年份仍然会出现在x轴上,即便这些年份没有条形图显示其值。这一点很重要,因为它展示了我们数据中的空缺。如果我们只显示包含数据的年份,这会产生误导性,给人一种所有年份都有连续数据的错误印象。
我们已经稍微熟悉了基尼指数数据,并测试了如何制作两种类型的动态图表。接下来,我们准备创建一个“基尼指数”部分并将其添加到我们的应用中。
连接条形图和下拉框
现在我们要将迄今为止所做的工作整合起来。计划是将两个下拉菜单并排放置,每个下方都有一个图表。第一个下拉菜单将提供年份选项,生成一个横向条形图。第二个下拉菜单将根据所选国家生成一个纵向条形图。最终目标是生成一个新的应用部分,类似于 图 5.8:

图 5.8 – 应用中的 Gini 指数部分,包含两个下拉菜单组件和两个条形图
首先,我们在 JupyterLab 中构建一个完整独立的应用,并确保其按预期工作:
-
我们首先运行必要的导入并实例化应用。我们已经覆盖了所有这些导入,除了
PreventUpdate异常。这个异常在回调函数处理的组件中没有选择值时非常有用;例如,当用户首次加载应用时,或者没有默认值的情况下。在这种情况下,来自Dropdown的输入值将是None,并且很可能会引发异常。在这种情况下,我们可以使用这个异常来冻结操作,直到传递一个有效的输入给回调函数:from jupyter_dash import JupyterDash import dash_html_components as html import dash_core_components as dcc import dash_bootstrap_components as dbc from dash.dependencies import Output, Input from dash.exceptions import PreventUpdate app = JupyterDash(__name__) -
创建
gini_df,它是poverty的一个子集,其中的 Gini 指数列没有缺失值:gini_df = poverty[poverty[gini].notna()] -
使用一个顶层 div 创建应用的布局,在其中我们将放置所有其他组件:
app.layout = html.Div() -
在我们刚刚创建的 div 中,我们要添加一个部分标题,并放置一个
dbc.Row组件。该行将包含两个dbc.Col元素,每个元素中将包含一个下拉菜单和一个图表。以下是将插入 div 中的元素列表:[ html.H2('Gini Index - World Bank Data', style={'textAlign': 'center'}), dbc.Row([ dbc.Col([ dcc.Dropdown(id='gini_year_dropdown', options=[{'label': year, 'value': year} for year in gini_df['year'].drop_duplicates().sort_values()]), dcc.Graph(id='gini_year_barchart') ]), dbc.Col([ dcc.Dropdown(id='gini_country_dropdown', options=[{'label': country, 'value': country} for country in gini_df['Country Name'].unique()]), dcc.Graph(id='gini_country_barchart') ]) ]) ] -
上述代码应该能够处理布局,当我们将其插入顶层 div 后。现在我们可以创建第一个回调函数,它接受年份作为输入并返回相应的图表。注意
PreventUpdate异常在函数开始时的使用:@app.callback(Output('gini_year_barchart', 'figure'), Input('gini_year_dropdown', 'value')) def plot_gini_year_barchart(year): if not year: raise PreventUpdate df =\ gini_df[gini_df['year'].eq(year)].sort_values(gini).dropna(subset=[gini]) n_countries = len(df['Country Name']) fig = px.bar(df, x=gini, y='Country Name', orientation='h', height=200 + (n_countries*20), title=gini + ' ' + str(year)) return fig -
我们还可以做同样的事情,创建另一个回调函数来处理 Gini 指数部分的第二部分:
@app.callback(Output('gini_country_barchart', 'figure'), Input('gini_country_dropdown', 'value')) def plot_gini_country_barchart(country): if not country: raise PreventUpdate df = gini_df[gini_df['Country Name']==country].dropna(subset=[gini]) fig = px.bar(df, x='year', y=gini, title=' - '.join([gini, country])) return fig -
最后,我们运行应用:
if __name__ == '__main__': app.run_server(mode='inline')
这应该会创建一个运行中的应用,如 图 5.8 所示。
现在我们要将这个新功能整合到现有应用中。我们只需将可视化组件插入到希望它们出现的位置即可。回调函数可以添加在应用的 layout 属性下方。你可以复制我们在 第三章 中创建的最新版本的应用,与 Plotly 的图形对象一起工作。你可以将新组件作为一个列表插入到 dcc.Graph(id='population_chart') 和 dbc.Tabs 之间,如以下代码片段所示:
…
dcc.Graph(id='population_chart'),
html.Br(),
html.H2('Gini Index - World Bank Data', style={'textAlign': 'center'}),
html.Br(),
dbc.Row([
dbc.Col([
…
dcc.Graph(id='gini_country_barchart')
]),
]),
dbc.Tabs(
dbc.Tab([
…
使用一个指标,我们创建了两个动态图表,第一个让用户探索特定年份的数据,显示所有可用的国家,另一个让用户探索某个国家在所有年份的数据。我们还探索了显示条形图的两种方式,横向和纵向,并讨论了在何种情况下使用每种方向更为合适。
接下来,我们将探讨如何在同一个图形上绘制多个条形图,并查看不同的绘制方式。我们还将使用这些新技术探索一组新的指标。
探索显示多个条形图的不同方式(堆叠、分组、重叠和相对)
当我们想要显示不同国家在相同年份的数据时,我们有几种选择,可以在每个X轴位置显示多个条形图。图 5.9展示了我们在可视化两个变量a和b时可以采用的不同方式:

图 5.10 – 显示各国收入分配五分位的样本行
对于每个国家和年份的组合,我们有五个值。每个值显示的是该组收入在该国家和年份中占总收入的百分比。我们希望让用户选择一个国家,并显示一个图表,展示这些五个值在所有可用年份中的变化情况。为了了解最终效果,可以查看图 5.11,其中展示了美国的这些值:

图 5.11 – 每个五分位的收入份额,按所选国家和所有可用年份显示
由于这些值的总和为 100(仅有微小的舍入误差),因此我们可以清晰地比较各年份之间的柱状图,因为它们的总长度相同。由于这些是比例,我们关心的是查看某一特定年份的分布情况,以及这些分布如何随年份变化。
如你所见,对于柱形图的最右边和最左边部分,我们可以很容易地看出它们的变化,因为它们有相同的基准线,不管是开始还是结束。但是对于中间的值,就不容易做到这一点。原因在于它们的大小和基准线都在变化。所以,增加更多的分段就会使得跨年份的比较变得更加困难。但由于 Plotly 的图表是互动式的,用户可以轻松地将鼠标悬停在柱子上,获取其准确值并进行比较。
生成此图表应该是直接的。我们已经创建了 DataFrame 并获得了我们的值。我们只需要设置 x 和 y 值,并设置 orientation='h',但是问题在于,DataFrame 中的类别是按字母顺序排序的,而我们希望它们按照数值含义排序,从最小到最大,以便用户能够轻松理解它们的相对位置。像往常一样,这主要是一个数据处理挑战。那么,我们来解决它:
-
我们首先需要重命名列,并按照其值从“最低”到“最高”排序。实现这一点的一种方法是为列名前加上数字,并按此排序。这可以通过
rename方法轻松完成。然后,我们使用sort_index方法对列进行排序,并设置axis=1,表示对列(而非 DataFrame 的索引)进行排序:income_share_df = income_share_df.rename(columns={ 'Income share held by lowest 20%': '1 Income share held by lowest 20%', 'Income share held by second 20%': '2 Income share held by second 20%', 'Income share held by third 20%': '3 Income share held by third 20%', 'Income share held by fourth 20%': '4 Income share held by fourth 20%', 'Income share held by highest 20%': '5 Income share held by highest 20%' }).sort_index(axis=1) -
检查我们的工作是否正确:
income_share_df.columns Index(['1 Income share held by lowest 20%', '2 Income share held by second 20%', '3 Income share held by third 20%', '4 Income share held by fourth 20%', '5 Income share held by highest 20%', 'Country Name', 'year'], dtype='object') -
我们现在要去除列中冗余的部分,并保留位置指示符和“20%”。我们可以使用标准库的
re模块来实现。我们将任何数字后跟Income share held by的内容替换为空字符串。然后,我们将结果字符串的大小写更改为标题格式:import re income_share_df.columns = [\ re.sub('\d Income share held by ', '', col).title() for col in income_share_df.columns ] -
接下来,我们创建一个变量
income_share_cols,用于引用我们感兴趣的列:income_share_cols = income_share_df.columns[:-2] income_share_cols Index(['Lowest 20%', 'Second 20%', 'Third 20%', 'Fourth 20%', 'Highest 20%'], dtype='object') -
现在,我们的 DataFrame 已经准备好绘图,且名称简短且适当。我们首先创建一个
country变量,用于过滤 DataFrame:country = 'China' -
使用
px.bar创建条形图。注意,当设置x参数的值时,我们使用的是一个列表。Plotly Express 也可以处理宽格式数据,这在这种情况下非常方便。我们本来也可以将数据框架进行“熔化”,并使用上一章中使用的方法。我们还设置了orientation='h'和barmode='stack'。标题将动态插入国家名称,正如你在这里看到的:fig = \ px.bar(income_share_df[income_share_df['Country Name']==country].dropna(), x=income_share_cols, y='Year', hover_name='Country Name', orientation='h', barmode='stack', height=600, title=f'Income Share Quintiles - {country}') fig.show() -
你可能注意到我将结果赋值给了一个变量
figure,这是因为我们还有一些细节需要改进。运行前面的代码会生成图 5.12中的图表:![图 5.12 – 按五分位显示的收入份额,使用默认选项]()
图 5.12 – 按五分位显示的收入份额,使用默认选项
-
x轴的标题,
fig.layout,以及设置它们非常简单。请注意,图例有x和y属性来设置其在图中的位置。我们将图例的x属性设置为 0.25,表示我们希望图例从图形原点的四分之一处开始:fig.layout.legend.orientation = 'h' fig.layout.legend.title = None fig.layout.xaxis.title = 'Percent of Total Income' fig.layout.legend.x = 0.25 -
对印度尼西亚运行前面的代码会生成图 5.13中的最终图表:

图 5.13 – 按五分位显示的收入份额,使用自定义选项
现在,让我们把一切整合在一起,并将新功能添加到我们的应用程序中。
将功能集成到我们的应用程序中
我们现在准备好再次将新功能添加到我们的应用程序中,使用我们刚刚创建的函数和图表。在这个阶段,不需要太多解释,因为我们已经做过很多次了,但我会回顾一下主要步骤,你可以随时参考代码库来检查你的工作:
-
在模块的顶部,我们首先进行数据框架的定义,以及列的更改,就像我们之前做的那样。确保以下代码放在创建
poverty数据框架之后,因为它依赖于它:income_share_df =\ poverty.filter(regex='Country Name|^year$|Income share.*?20').dropna() income_share_df = income_share_df.rename(columns={ 'Income share held by lowest 20%': '1 Income share held by lowest 20%', 'Income share held by second 20%': '2 Income share held by second 20%', 'Income share held by third 20%': '3 Income share held by third 20%', 'Income share held by fourth 20%': '4 Income share held by fourth 20%', 'Income share held by highest 20%': '5 Income share held by highest 20%' }).sort_index(axis=1) income_share_df.columns =\ [re.sub('\d Income share held by ', '', col).title() for col in income_share_df.columns] income_share_cols = income_share_df.columns[:-2] -
在布局部分,我们需要一个
h2元素作为新章节的标题,一个Dropdown组件用于选择国家,以及一个Graph组件,放在我们为基尼指数部分创建的最后一个图表下面:dcc.Dropdown(id='income_share_country_dropdown', options=[{'label': country, 'value': country} for country in income_share_df['Country Name'].unique()]), dcc.Graph(id='income_share_country_barchart') -
callback函数可以很容易地通过我们刚才处理的代码构建,最终如下所示:@app.callback(Output('income_share_country_barchart', 'figure'), Input('income_share_country_dropdown', 'value')) def plot_income_share_barchart(country): if country is None: raise PreventUpdate fig =\ px.bar(income_share_df[income_share_df['Country Name']==country].dropna(), x=income_share_cols, y='Year', barmode='stack', height=600, hover_name='Country Name', title=f'Income Share Quintiles - {country}', orientation='h') fig.layout.legend.title = None fig.layout.legend.orientation = 'h' fig.layout.legend.x = 0.25 fig.layout.xaxis.title = 'Percent of Total Income' return fig
将这段代码放到正确的位置应该能将新功能添加到我们的应用程序中。现在我们有多个指标,用户可以与之互动,其中一些提供了不同的方式来看待数据。
四种显示条形图的方式可能很有趣,但在我们的案例中,如果我们想允许用户比较多个国家,这会迅速变得几乎无法阅读。举个例子,回到我们的基尼指数国家图表,每个选定的国家通常会显示 20 到 30 个条形图,具体取决于可用数据的多少。对于四个国家,我们大约需要 100 个条形图,占据半页,真的很难阅读。
如果允许用户选择尽可能多的国家,并为每个所选国家生成单独的图表,这样他们就可以在多个图表上看到国家了,怎么样?
这就是分面的全部内容,我们将在接下来进行探索。
使用分面将图表拆分为多个子图表 – 水平、垂直或包装
这是一种非常强大的技术,允许我们为分析添加一个新的维度。我们可以从数据集中选择任何特征(列)来分割图表。如果您期望详细解释它的工作原理以及需要学会的内容,请别担心。就像 Plotly Express 中的大多数其他功能一样,如果您有一个长格式(整洁的)数据集,您只需选择一个列,并使用其名称作为facet_col或facet_row参数即可。就是这样。
让我们快速查看通过查看相关的分面参数可用的选项:
-
facet_col:这意味着您希望将图表拆分为列,并且所选列名将用于将它们拆分。这将导致图表并排显示(作为列)。 -
facet_row:类似地,如果您希望将图表拆分为行,您可以使用此参数,它将把图表拆分为在彼此上方显示的子图表。 -
facet_col_wrap:当您需要生成动态数量的分面时,这真的非常有用。如果您知道用户将生成多个图表,那么在多少个图表之后,应该在生成的图表网格的下一行中显示下一个图表?答案应该是一个整数,并且 Plotly Express 确保在此数字之后,图表在下一行中显示。这确保对于每一行,我们有一个最大数量的图表列。 -
facet_row_spacing和facet_col_spacing:正如它们的名称所示,您可以通过设置这些值来控制行和列之间的间距,范围为 [0, 1],作为总图大小的分数,水平或垂直。
让我们运行一个快速示例以确保这一点清楚:
-
创建一个国家列表以进行过滤:
countries = ['Algeria', 'Japan'] -
修改
df的定义以过滤掉'Country Name'在countries中的行。可以使用pandas方法isin来实现此目的。df =\ gini_df[gini_df['Country Name'].isin(countries)].dropna(subset=[gini]) -
运行
px.bar,只需简单添加facet_row='Country Name':px.bar(df, x='year', y=gini, facet_row='Country Name')运行此代码将生成图 5.14中的图表:
![图 5.14 – 使用 facet_row 参数生成的两个条形图]()
图 5.14 – 使用 facet_row 参数生成的两个条形图
-
如您所见,扩展我们的图表非常容易,我们还为子图标注了正确的国家名称。虽然它已经不错了,但仍然不如我们希望的那样完美。y轴标题重叠,而且您必须仔细查看垂直标题才能知道哪个子图属于哪个国家。所以让我们来改进一下。首先,我们可以通过修改
labels参数,提供一个字典,并将默认名称映射为我们想要的新名称,来修改y轴标题:labels={gini: 'Gini Index'} -
我们还可以通过为条形图按国家着色来帮助用户快速识别图表。这样会使它们更具辨识度,同时生成带有颜色引导的图例,使图表更容易区分。同样,这只需通过为
color参数提供一个参数来完成,这基本上是选择我们想用来标识的列名:color='Country Name' -
另一个有用的功能是为整个图形添加动态标题。我们可以显示完整的指标名称,下面列出已选择的国家名,以逗号分隔。Plotly 注释支持一些 HTML 标签,我们将使用
<br>标签来分隔指标名称和国家列表,如下所示:title='<br>'.join([gini, ', '.join(countries)]) -
在图表上显示两个国家很容易阅读,但如果用户决定选择七个国家呢?正如我们在吉尼指数水平条形图的动态高度中所做的那样,我们还需要根据所选国家的数量,为分面条形图设置动态高度。我们将使用相同的技术,但使用不同的值,因为我们在管理子图,而不是水平条形图:
height=100 + 250*len(countries) -
完整的更新代码可以在这里看到:
px.bar(df, x='year', y=gini, facet_row='Country Name', labels={gini: 'Gini Index'}, color='Country Name', title='<br>'.join([gini, ', '.join(countries)]), height=100 + 250*len(countries)) -
最终的图表可以在图 5.15中看到,显示了三个国家:

图 5.15 – 使用facet_row参数和自定义选项生成的三个条形图
图形和功能现已更新,生成基于所选国家的分面图表。我们要做的唯一剩余的更改是设置提供此选项的下拉框,以允许多选。我们接下来会做这个,并且对当前的仪表盘做一个整体查看,看看如何改善其布局和可用性。
探索下拉框的其他功能
Dropdown组件有一个可选参数multi,它接受一个布尔值作为参数,我们可以将其设置为True来允许这种操作:
dcc.Dropdown(id='gini_country_dropdown',
multi=True,
options=[{'label': country, 'value': country}
for country in gini_df['Country Name'].unique()]),
现在,您可以进行更改,并根据需要使用吉尼国家条形图。页面上该图形的高度会根据我们设置的动态高度动态扩展/收缩,因此我们也不需要担心布局的这个方面。用户在与组件互动时会自行管理。现在,让我们看看对新手来说,使用这些选项是否容易。
向下拉框添加占位符文本
如果你第一次查看应用程序中的基尼指数部分,你将看到两个下拉框,允许你做出选择,如图 5.16所示:

图 5.16 – 没有占位符文本的下拉框
但是,选择的到底是什么?
Dropdown组件有一个可选的placeholder参数,对于用户了解他们到底在选择什么非常有用。
我们可以轻松更新两个Dropdown组件的占位符文本,使其对用户更加清晰:
placeholder="Select a year"
placeholder="Select one or more countries"
我们可以通过使用 Dash Bootstrap Components 中的Label组件来使其更加明确,正如其名称所示,它提供了一个标签。这些标签可以放置在下拉框的上方:
dbc.Label("Year")
dbc.Label("Countries")
添加这些新选项后,更新后的消息如图 5.17所示:

图 5.17 – 带有占位符文本和标签的下拉框
我认为这样看起来更易于使用。我们还清楚地表明了哪个下拉框接受单个选项,哪个接受多个选项。我们还可以通过添加类似的标签(例如“国家”)和占位符文本“选择一个国家”来为收入份额分布部分做相同的事情。
我们的应用程序现在变得更大,提供了更多的选项。现在是时候从整体上查看一下,看看如何改善应用程序的外观和体验,使其更易于使用,并在所有图表中使用一致的主题。
修改应用程序的主题
我们已经看到如何轻松地更改我们应用程序的主题,这可以通过在实例化应用程序时将一个列表作为参数传递给external_style_sheets参数来完成。你可以尝试可用的主题,我们可以将其设置为COSMO:
app = dash.Dash(__name__,
external_stylesheets=[dbc.themes.COSMO])
这应该修改我们应用程序的几个视觉元素。
另一件我们可以考虑做的事情是使我们的主题与我们使用的图表主题保持一致。我们可以将应用程序的背景颜色设置为与 Plotly 图形中使用的默认颜色相同。通过在顶层的html.Div中使用style参数,我们可以将背景颜色设置为与默认的 Plotly 颜色一致:
app.layout = html.Div([
…
], style={'backgroundColor': '#E5ECF6'})
还需要做一个小的更改,以完成这个修改。
Plotly 的Figure对象包含两个主要区域,"plot"区域和"paper"区域。plot 区域是位于x和y轴之间的内部矩形。在我们所有生成的图表中,这个区域的颜色是浅蓝色的(或者如果你正在阅读印刷版的书籍,它是灰色的)。
包围较小矩形的大矩形是“纸张”区域。在我们到目前为止生成的所有图表中,它的颜色是白色的。我们也可以将其颜色设置为相同的颜色,这样就可以使我们应用程序的所有背景颜色一致。我们只需要在生成图表的回调函数中添加以下代码行:
fig.layout.paper_bgcolor = '#E5ECF6'
如果我们现在运行应用程序,我们将看到一些空白的图形,并且那些我们没有设置默认值的图形背景为白色。对于这些图形,我们还需要创建空白图形,但要确保它们的背景颜色与整个应用程序的主题一致。这样做非常简单,就像我们在第三章,“与 Plotly 图形对象一起工作”中所做的那样。dcc.Graph 组件有一个 figure 属性,我们可以将具有所需背景颜色的空白图形添加到这个属性中。用户做出选择时,这些图形会被修改。由于我们有几个这样的图形实例,最好创建一个函数,用来在需要时创建这些图形。以下代码实现了这一点:
import plotly.graph_objects as go
def make_empty_fig():
fig = go.Figure()
fig.layout.paper_bgcolor = '#E5ECF6'
fig.layout.plot_bgcolor = '#E5ECF6'
return fig
现在,我们可以在需要的地方添加对 make_empty_fig 的调用,正如下面的例子所示:
dcc.Graph(id='gini_year_barchart',
figure=make_empty_fig())
通过这些调整,我们选择了一个新的整体主题,并确保我们的应用程序中所有元素的背景颜色一致。
调整组件大小
另一个我们需要处理的问题是浏览器窗口的大小调整如何影响我们不同组件的大小和位置。默认情况下,图形是响应式的,但我们需要为并排放置的图形做出一些决策。在基尼指数部分,我们有两个这样的图表,放置在两个 dbc.Col 组件中,彼此并排。我们所要做的就是为这些图表设置所需的大小,适用于大屏幕(lg)和中等屏幕(md):
dbc.Col([
…
], md=12, lg=5),
在大屏幕(lg)上,这是最常见的情况,每个图形将占据 5 个(12 个中的 5)大小,这是 Bootstrap 分割屏幕的方式。如果您想复习相关知识,可以参考第一章,“Dash 生态系统概述”中关于 Bootstrap 布局、列和行以及其网格系统的讨论。在中等屏幕(md)上,图形将扩展到占据 12 个中的 12 个列,这意味着在该尺寸下它们将占满整个屏幕的宽度。
当我们开始学习交互性时,我们在应用程序的顶部创建了一个简单的报告。它显示了所选国家/地区 2010 年的人口。我们可以删除这个组件,因为它的功能非常有限,主要用于示范。删除它很简单,只需要删除该组件以及下面的输出区域,并删除处理它的回调函数。
根据我们在本章中所做的工作,您可以在图 5.18中看到我们应用程序的当前界面:

图 5.18 – 应用程序的最终布局
我强烈建议您自己手动进行这些更改,而不是查看代码库中的代码。我还鼓励您测试其他布局,尝试不同的选项,并生成大量的错误!
不断地进行更改并控制你的代码需要一致的组件、回调函数和变量命名规范。如果你能遵循一定的逻辑流程来组织你的组件,那将会非常有帮助。我们会多次进行这些操作,希望你能在这个过程中掌握这些技巧。
我们的应用现在看起来更好,使用起来更方便,如果你愿意,还可以与他人分享。在这一章中,我们涵盖了许多内容,将我们的应用提升到一个新层次,所以让我们回顾一下我们所涉及的内容。
总结
本章我们主要关注了条形图。我们还以多种方式使用了Dropdown组件。我们了解了使用横向和纵向布局的优缺点,并且在应用中实现了这两种布局。接着,我们分析了将多个条形图一起展示的不同方式,并实现了一个展示总值比例的图表。然后,我们探讨了面板(facets),看到了它们如何丰富我们的图表,并使它们更加灵活和可扩展。我们还将其与下拉框(dropdowns)结合,使得用户可以进行多重选择。确认一切正常工作后,我们通过选择一个新主题并确保所有背景颜色一致来为我们的应用进行外观更新。我们还通过为不同的屏幕尺寸设置不同的图表大小来管理不同屏幕大小下的布局。最后,我们为应用添加了一些有用的提示信息,以提升用户体验。最后,我们截取了结果应用的屏幕截图!
在下一章中,我们将探讨一种最常见的图表类型——散点图。我们还将学习如何将其与滑动条(sliders)结合使用,允许用户选择和修改数值或数值范围。
第六章:第六章:通过散点图探索变量,并使用滑块过滤子集
现在我们将探讨一种最通用、最有用且无处不在的图表类型——散点图。顾名思义,我们基本上是在笛卡尔平面上散布标记(可以是点、方块、圆圈、气泡或其他符号),其中它们的水平和垂直距离表达它们所代表的数值。其他视觉属性,如大小、颜色和符号,可能会用来表示其他属性,正如我们在一些前面的示例中看到的那样。由于大多数关于图形和创建图表的基础知识已经涵盖过,我们不会花太多时间在这方面,而是专注于散点图的具体细节和可用选项。我们还将探索并使用滑块这一新的交互式组件。我们现在就开始,但首先,以下是我们将要涵盖的主题:
-
了解使用散点图的不同方式:标记、线条和文本
-
在单个图表中创建多个散点图追踪
-
使用散点图映射和设置颜色
-
通过管理不透明度、符号和刻度来处理过度绘图和异常值
-
介绍滑块和范围滑块
-
自定义滑块的标记和值
技术要求
我们将在本章中使用上一章中使用的相同工具。我们还会稍微关注一下 Plotly 的graph_objects模块来创建散点图,因为它提供了其他工具,并且在进一步自定义我们的图表时非常有用。需要使用的包有 Plotly、Dash、Dash Core Components、Dash HTML Components、Dash Bootstrap Components、pandas 和 JupyterLab。
本章的代码文件可以在 GitHub 上的github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_06找到。
查看以下视频,观看代码演示:bit.ly/3ancblu。
我们首先探索使用散点图可以绘制的不同方式,或者说不同的内容。
了解使用散点图的不同方式:标记、线条和文本
使用graph_objects创建散点图时,我们有很多不同的选择,正如在引言中提到的那样,因此我们将与 Plotly Express 一起进行探索。为了让你了解散点图的多样性,以下代码提取了Figure对象可用的所有scatter方法,以及 Plotly Express 中可用的那些方法:
import plotly.graph_objects as go
import plotly.express as px
fig = go.Figure()
[f for f in dir(fig) if 'scatter' in f]
['add_scatter',
'add_scatter3d',
'add_scattercarpet',
'add_scattergeo',
'add_scattergl',
'add_scattermapbox',
'add_scatterpolar',
'add_scatterpolargl',
'add_scatterternary']
[f for f in dir(px) if 'scatter' in f]
['scatter',
'scatter_3d',
'scatter_geo',
'scatter_mapbox',
'scatter_matrix',
'scatter_polar',
'scatter_ternary']
如你所见,可用方法之间存在一些重叠,也有一些方法在两个模块中并不适用。我们不会详细讨论所有这些方法,但知道它们是很有帮助的,因为你可以轻松地将散点图的常规知识应用于其他类型的图表。现在让我们看看使用这些选项时的一些区别。
标记、线条和文本
go.Scatter对象中一个有趣的选项是mode参数。它可以包含标记、线条和/或文本的任意组合。你可以指定其中一个、两个或三个选项。当指定多个选项时,必须将它们作为一个单一字符串指定,其中各元素由加号分隔,例如"markers+text"。让我们首先了解一下我们将在本章中关注的指标,并立即探索绘图选项:
-
运行所需的导入并创建
poverty数据框:import pandas as pd import plotly.graph_objects as go poverty = pd.read_csv('data/poverty.csv') -
我们的数据集包含三个不同的日收入水平来衡量贫困。它们衡量的是“贫困线下的平均收入或消费不足——每天$1.90”。它们还针对其他两个水平($3.20 和$5.50)有相同的衡量标准。这些数据也可以在不同的列中以绝对数值呈现,但我们将在本章中重点关注百分比。它们的列名以贫困缺口(Poverty gap)开头,我们可以将其作为模式来提取我们需要的列:
perc_pov_cols =\ poverty.filter(regex='Poverty gap').columns perc_pov_cols Index(['Poverty gap at $1.90 a day (2011 PPP) (%)', 'Poverty gap at $3.20 a day (2011 PPP) (% of population)', 'Poverty gap at $5.50 a day (2011 PPP) (% of population)'], dtype='object') -
为了简化问题,我们将所有相关的变量名和对象以
perc_pov_开头,以明确我们正在处理贫困百分比。请记住,我们现在在应用中有几个对象和函数,我们希望确保保持简单、清晰和一致。我们现在使用刚刚创建的列表来创建三个变量,每个贫困水平一个:perc_pov_19 = perc_pov_cols[0] perc_pov_32 = perc_pov_cols[1] perc_pov_55 = perc_pov_cols[2] -
像往常一样,我们需要查看这些指标的描述,最重要的是,它们可能存在的限制:
series[series['Indicator Name']==\ perc_pov_19]['Short definition'][25] 'Poverty gap at $1.90 a day (2011 PPP) is the mean shortfall in income or consumption from the poverty line $1.90 a day (counting the nonpoor as having zero shortfall), expressed as a percentage of the poverty line. This measure reflects the depth of poverty as well as its incidence. As a result of revisions in PPP exchange rates, poverty rates for individual countries cannot be compared with poverty rates reported in earlier editions.' -
三个指标的定义基本相同,限制条件也与上一章看到的相似。请随意阅读细节,但请记住,这些数字并不完美,如果我们要做出任何解释时需要小心。我们现在为一个国家创建一个变量,并利用它创建一个包含
country和perc_pov_19数据的子集数据框:country = 'China' df =\ poverty[poverty['Country Name']==country][['year', perc_pov_19]].dropna() -
创建一个
Figure,然后使用相关方法添加一个散点图。mode参数应该给出之前讨论的选项之一,这里仅以mode显示:fig = go.Figure() fig.add_scatter(x=df['year'], y=df[perc_pov_19], text=df[perc_pov_19], mode=mode) fig.show()
图 6.1展示了运行前述代码时对于mode的每个可能选项的效果,图表标题显示了如何设置该选项:

图 6.1 – 设置散点图模式参数的不同方式
你还可以看到图 6.2中的其他选项:

图 6.2 – 设置散点图模式参数的其他方法
Plotly Express 为散点图和线图提供了独立的函数。你可以使用 scatter 函数绘制文本,可以通过选择 DataFrame 中包含文本的列,或提供一个文本元素列表来实现。Plotly Express 的 scatter 函数包含一个 text 参数,可以用来处理这个任务。
现在,我们来看一下如何利用这段代码创建多个散点图轨迹。
在单个图表中创建多个散点轨迹
我们将尽可能专注于使用 Plotly Express,因为它的便利性以及之前在第四章《数据处理与准备——为 Plotly Express 铺路》中讨论过的其他优点。尽管如此,了解如何使用 Figure 对象仍然非常重要,因为你会遇到许多需要与之打交道的情况,特别是当你需要进行大量自定义时。此外,尽管 Plotly Express 支持最重要的图表类型,但并非所有类型都得到支持。
让我们通过为其他国家添加轨迹来扩展前面的图表,并比较这两种方法。我们从 graph_objects 模块的 Figure 对象开始:
-
创建一个
countries列表进行过滤:countries = ['Argentina', 'Mexico', 'Brazil'] -
创建一个
poverty的子集,我们称之为df,其中Country Name列的值在countries列表中(使用isin方法)。然后,我们提取year、Country Name和perc_pov_19列,并删除缺失值:df = (poverty [poverty['Country Name'].isin(countries)] [['year','Country Name', perc_pov_19]] .dropna()) -
创建一个
Figure对象,并将其分配给一个变量fig:fig = go.Figure() -
现在,我们想为每个要绘制的国家添加一个轨迹。可以通过遍历国家并创建一个仅包含当前国家数据的子 DataFrame 来完成:
for country in countries: df_country = df[df['Country Name']==country] -
我们现在在相同的循环中(且具有相同的缩进级别)通过使用
add_scatter方法添加一个新轨迹。注意,我们设置了mode='markers+lines',并使用name属性设置了该轨迹在图例中的标题:fig.add_scatter(x=df_country['year'], y=df_country[perc_pov_19], name=country, mode='markers+lines') -
我们还需要为 y 轴添加标题,然后我们就可以轻松地显示图形:
fig.layout.yaxis.title = perc_pov_19 fig.show()
运行前面的代码将生成 图 6.3 中的图表:

图 6.3 – 使用 graph_objects 模块创建多个散点图
现在,让我们将其与使用 Plotly Express 的方法进行比较。生成图表的代码简洁明了,几乎不需要解释:
px.scatter(df, x='year', y=perc_pov_19, color='Country Name')
我们为 data_frame 参数选择了值,并从 df 中选择了我们想要的 x、y 和 color 参数的列。然后,代码生成了 图 6.4 中的图表:

图 6.4 – 使用 Plotly Express 创建多个散点图
我们还会自动给坐标轴加上标签,图例也会正确标注,甚至图例还会有标题,使用我们为color参数选择的列名。
不过,有一个小问题。与前面的图表相比,断开的点并不像之前那样容易阅读。这在本例中尤其重要,因为我们正在表达一系列事件,而这些线条使得表达更加清晰。对于交互式仪表板,我们无法预测用户将选择什么,这意味着他们可能会生成比这个图表更难以阅读的图表。Plotly Express 为散点图和线图提供了独立的函数,因此,为了使其成为“线条+标记”图表,我们需要将其分配给一个Figure对象,然后添加线条轨迹。以下是执行此操作的步骤:
-
创建一个
Figure对象,并将其赋值给一个变量fig:fig = px.scatter(df, x='year', y=perc_pov_19, color='Country Name') -
创建另一个
Figure对象,完全像上一个一样,只是名称和图表类型不同:fig_lines = px.line(df, x='year', y=perc_pov_19, color='Country Name') -
从
fig_lines中,我们希望将其轨迹添加到fig中。如果你记得,轨迹可以在Figure对象的data属性下找到。data属性是一个元组,每个元素对应一个轨迹。因此,我们需要遍历这些轨迹(即data属性中的轨迹),并将它们添加到fig中:for trace in fig_lines.data: trace.showlegend = False fig.add_trace(trace) fig.show()
请注意,每个新的线条轨迹都会在图例中有其标签。因此,我们会在图例中看到重复的线条标签,我们需要将它们去掉。我们通过将每个轨迹的showlegend属性设置为False来解决这个问题。运行这段代码会生成图6.5:

图 6.5 – 使用 Plotly Express 创建多个散点图和线图
比较用两种不同方法生成相同图表所需的脑力和代码量,我们可以看到并没有太大区别。这是当你想要生成自定义的或足够适合发布的内容时的典型情况。不过,对于探索目的来说,Plotly Express 显然是一个很好的起点,一旦你对数据有了足够的了解,你可以更好地决定采用哪种方法。
我们已经看过了如何在散点图中隐式地管理颜色(颜色是自动设置的),现在我们准备探索更多的颜色管理选项。如果你正在阅读打印的黑白版,你会看到不同的颜色阴影,这些阴影可能是可区分的,但正如我们之前所做的,我们还将使用符号来明确表示,使其易于理解。
现在,让我们来探索管理颜色的不同选项。
使用散点图进行映射和设置颜色
颜色在传递和表达我们图表中的信息中至关重要。这也是一个非常大的话题,完整讨论超出了本书的范围。我们将重点讨论两种类型变量的颜色——离散和连续。我们还将处理在图表中使用颜色的两种方式:将变量映射到颜色和手动设置颜色。
我们首先探索这两种类型变量之间的差异。
离散和连续变量
简单来说,连续变量是指在一定范围内可以取无限多个可能值的变量。例如,人口是一个可以取任何值的数字,基于一个国家的人口数量。连续变量通常是数字(整数或实数)。身高、体重和速度也是其他示例。
另一方面,离散变量是可以取有限集合中任一项值的变量。最重要的是,离散变量不能取这些项之间的值。国家就是一个这样的例子。一个国家要么是国家 A,要么是国家 B,但不能是 10%的 A 和 90%的 B。离散变量通常是文本变量,且通常具有相对较少的独特项。
我们使用颜色表达变量性质的方式如下:
-
对于连续变量,我们使用一种颜色渐变,随着其代表的值变化,颜色会在两种或多种颜色之间逐渐变化。例如,如果我们的颜色渐变从最低值为白色,最高值为蓝色,那么其中的所有值将会呈现白色和蓝色的不同深浅。一个颜色中蓝色比白色多的标记,意味着它的值更接近该变量的最大值,反之亦然。我们稍后会尝试这个。
-
离散变量是不同的项,我们为它们使用的颜色需要尽可能地彼此区分,尤其是那些彼此相邻的颜色。通过一些示例可以更清楚地理解这一点,我们从连续变量开始。
使用颜色表示连续变量
使用我们开始时的相同指标,我们希望选择一个任意年份,并绘制每个国家的指标值。我们已经知道如何做到这一点。现在,我们想为我们的图表添加一个新的维度。我们希望使用颜色来表示另一个值,例如人口。这将使我们能够看到人口和我们正在绘制的指标(此处为$1.90 贫困线)之间是否存在相关性。让我们准备好我们的变量和数据:
-
创建所选指标和年份的变量:
indicator = perc_pov_19 year = 1991 -
使用指标和年份,我们创建一个
poverty子集,其中年份列等于我们的变量year,并且is_country列为True。然后我们删除缺失值并根据这一列对数据进行排序。以下代码实现了这一点:df =\ poverty[poverty['year'].eq(year) & poverty['is_country']].dropna(subset=[indicator]).sort_values(indicator) -
我们只需要选择我们想要映射其值到适当颜色的列,然后像平时一样使用 Plotly Express:
px.scatter(df, x=indicator, y='Country Name', color='Population, total')
上述代码生成了图 6.6中的图表:

图 6.6 – 使用 Plotly Express 设置连续变量的颜色
我们基本上是为我们的可视化添加了一个新层次,即我们选择的那一列。每一个视觉属性都为图表添加了一个维度,使得图表更加丰富,但添加过多的维度可能会让图表显得过于复杂,难以阅读。我们需要找到一个合适的平衡,确保所展示的内容既有意义又易于阅读,适合我们的观众。
我们可以立刻看到,图表中人口最多的国家(美国,亮黄色)在我们的指标中是最小的值之一。我们也可以看到,由于其他大多数标记的颜色更接近紫色,这表明人口最多的国家在该指标上的值相较于其他国家来说非常极端。虽然它在人口上看起来像是一个异常值,但在贫困指标上却不是如此。当我们将鼠标悬停在标记上时,弹出的信息框也采用相同的颜色,而且由于它比标记大得多,因此很容易将颜色与其在颜色条上的相对位置关联。颜色color_scale_continuous。我们可以在图 6.7中看到如何做到这一点,并了解其效果,我们选择了cividis尺度:

图 6.7 – 选择不同的连续颜色尺度
这个图表没有额外的信息,唯一的变化是我们将颜色尺度更换成了不同的一个。这个颜色尺度很直观,颜色在深蓝色和亮黄色之间变化,并涵盖了所有介于两者之间的颜色组合。该尺度也被称为“顺序”尺度,因为它展示了从低值到高值的变化。你可以通过运行px.colors.named_colorscales()获取所有命名的颜色尺度列表,该命令将返回这些尺度的名称。更有趣的是,你可以查看并比较所有这些尺度,从而选择你想要的一个。你可以通过运行px.colors.sequential.swatches()生成一个包含所有可用顺序色阶的图表,部分输出如图 6.8所示:

图 6.8 – Plotly 中可用的前几个顺序色阶
另一个展示颜色尺度效果的有趣方式是使用swatches_continuous函数。例如,图 6.9展示了运行px.colors.sequential.swatches_continuous()的结果:

图 6.9 – Plotly 中的前几个连续比例尺,它们在色条中的显示效果
这样更好地展示了它们实际的外观,并展示了颜色之间的平滑过渡。
您可以使用swatches功能来获取其他类型的颜色标度和序列。只需运行上一个命令,并用以下任何一个替换sequential:carto、cmocean、colorbrewer、cyclical、diverging或qualitative。
到目前为止,我们已经通过选择要使用的列的值自动映射了数据值和颜色。还有手动设置颜色标度的选项。
手动创建颜色标度
一种方法是通过为color_continuous_scale参数提供两种或更多颜色的列表来实现。默认情况下,您提供的第一种颜色将分配给最小值,最后一种颜色将分配给最大值。中间的值将采用这两种颜色的组合,产生两种颜色的阴影。这显示了数据点距离极端值有多近。稍后我们将看到使用两种以上颜色的示例。使用相同的代码并设置color_continuous_scale=["steelblue", "darkorange"]将生成 图 6.10 中的图表:

图 6.10 – 手动设置连续颜色标度
这让人一窥选项的精细程度,但这只是表面。有时,您可能希望重新调整数据,使颜色从最小值到最大值呈现更平滑的过渡。我们刚刚创建的图表是这样的一个好例子。在人口方面我们有一个离群值,所以如果我们想要这样做,最好将color参数设置为我们数据的一个经过缩放的版本。总的来说,因为有许多经过良好验证和测试的比例尺可供选择,所以最好从中选择,而不是手动设置您自己的颜色。另一个重要考虑因素是色盲,尝试使用适合患有色盲症的人群的比例尺。您不希望使用一些读者无法区分的颜色。您可以通过在线搜索来简单检查一个颜色标度是否适合色盲人群。
现在让我们设置一个使用三种颜色的比例尺。RdBu(红蓝)比例尺从红色变到蓝色,中间值取白色作为它们的颜色。这是其中一个默认的比例尺。让我们用这个比例尺快速绘制一个简单的图表:
y = [-2, -1, 0, 1, 2, 3, 4, 5, 6]
px.scatter(x=range(1, len(y)+1),
y=y,
color=y,
color_continuous_scale='RdBu')
我们创建了一个在范围[-2, 6]内的整数列表,并将它们的颜色映射到RdBu比例尺,生成了 图 6.11 中的图表:

图 6.11 – 手动设置连续的分歧颜色标度
在这种情况下,你可以看到颜色从红色到白色再到蓝色的过渡,经过每种颜色的中间色调。这也被称为“分歧”色标。这里有一个中点(在这个例子中是白色点),颜色在这个点分歧,表示两种不同类型的值。通常,我们用这个来显示高于和低于某个特定值的值。在这种情况下,我们想用红色表示负值,白色表示零值,蓝色表示正值。但我们没有得到这个结果。白色的中点被设置为数据的中点,而这个中点恰好是我们列表中的第五个元素,其值为 2。
这可以通过使用color_continuous_midpoint参数来修正,正如你在图 6.12中看到的那样:

图 6.12 – 手动设置连续分歧颜色尺度的中点
我们现在有了一个更有意义的中点,在这个中点上,颜色的分歧使得正负值一目了然。另一个重要的效果是,它还展示了数据的偏斜程度。请注意,图表中没有红色标记。我们有两个粉红色标记,而蓝色值则更多。这与数字列表完全对应,列表中包含两个负值和六个正值。颜色条也清楚地表明,我们只覆盖了红色光谱的一部分,而蓝色则完全覆盖。
还有许多其他选项可用于设置颜色、缩放数据以及表示不同的值。我鼓励你进一步了解这个主题,幸运的是,Plotly 提供了许多选项,可以让你按自己的需求定制颜色。
现在让我们来看看颜色如何与离散变量一起使用。
使用颜色与离散变量
现在的目标不是可视化值之间的差异程度。我们现在想根据某个标准对值进行分组,并查看这些值组之间的差异。如果我们简单地将color参数设置为具有文本值的列,立刻就能看到结果。例如,我们可以设置color="Income Group"来获得图 6.13中的图表:

图 6.13 – 使用颜色与分类变量
一切都自动为我们处理了。仅仅因为我们选择了一个具有文本值的列,Plotly Express 就根据该列对数据进行了分组,并选择了一组彼此不同的颜色,使我们能够看到不同组之间值的变化。我们还使用了符号,使其在特别是灰度版本的图表中更易于查看。这是通过设置symbol='Income Group'实现的。
与连续变量一样,我们也可以通过提供一个颜色序列给color_discrete_sequence参数,来自定义自己的离散颜色序列。图 6.14展示了设置此参数后的效果,使用的是 Plotly 提供的其中一种颜色序列:

图 6.14 – 为分类变量设置不同的颜色序列
请注意,我们通过从可用列表中选择一个序列px.colors.qualitative.G10,并且正如你可能猜到的,你可以通过运行px.colors.qualitative.swatches()来生成所有可用的颜色序列。
就像我们对待连续变量一样,我们也可以通过提供一个命名颜色的列表,手动设置离散变量的颜色。我们还可以使用颜色的十六进制表示,例如#aeae14,或 RGB 值,例如'rgb(25, 85, 125)'。将我们选择的颜色传递给color_discrete_sequence参数后,我们得到的图表如图 6.15所示:

图 6.15 – 为分类变量手动设置颜色序列
当你手动选择所需颜色时,必须确保提供的颜色列表元素数量与你试图可视化的变量的唯一值数量相同。否则,它会循环使用你提供的颜色,这可能会导致误导。再次强调,通常最好选择可用的已建立的颜色序列,但如果你愿意,你也可以手动设置。当我们设置我们想要的颜色时,并没有指定哪一项应该使用哪个颜色。我们只是简单地声明我们希望唯一值使用这组颜色。有时,你可能希望明确地将某些颜色映射到特定类别。一旦知道了唯一值,你可以将字典提供给color_discrete_map参数,然后将每个值映射到你选择的颜色:
color_discrete_map={'High income': 'darkred',
'Upper middle income': 'steelblue',
'Lower middle income': 'orange',
'Low income': 'darkblue'}
设置此选项会生成图 6.16中的图表:

图 6.16 – 为分类变量的每个值手动设置颜色
请注意,大多数连续变量参数包含“scale”,而离散变量则包含“sequence”。这有助于记住并理解在将颜色映射到这两种变量时的基本区别。
对于连续变量,我们使读者能够根据颜色大致看到标记的值以及在数据集中的相对位置。这并不是非常清晰,但你可以大致看出某个国家的人口约为两千万,而且它看起来是该数据集中人口最多的国家之一。当然,用户可以悬停并查看精确值。如果是离散变量,我们主要更关心通过这些变量进行分组,并查看这些组之间的趋势。
我们展示了颜色处理的一小部分内容,现在我们将考虑一些可能在散点图中出现的其他问题,即异常值和绘制大量数据点。
通过管理不透明度、符号和尺度来处理重叠绘制和异常值
假设我们现在对查看变量与人口之间的关系感兴趣,且我们仍然使用我们之前工作的年份。我们希望将Population, total放在* x 轴上,将perc_pov_19放在 y *轴上。
我们首先创建一个poverty的子集,其中year等于 2010,is_country为True,并使用Population, total对值进行排序:
df =\
poverty[poverty['year'].eq(2010) & poverty['is_country']].sort_values('Population, total')
现在让我们来看一下当我们绘制这两个变量时,结果是什么样子的。下面是代码:
px.scatter(df,
y=perc_pov_19,
x='Population, total',
title=' - '.join([perc_pov_19, '2010']),
height=500)
运行此操作将生成图 6.17:

图 6.17 – 图表中的重叠绘制和异常值
存在一个异常值——中国,其人口接近 14 亿,这迫使所有标记被压缩到图表的一个非常窄的部分。我们还看到y-轴上有一小群数值超过 25,但差异远没有水平轴上的差异那样极端。另一个重要的问题是,许多标记彼此重叠。如果使用纯色标记,这意味着如果一个标记叠加在另一个标记上,结果不会有任何区别;即使是一千个标记也是如此。这两个问题同时存在,使得这个图表变得非常难以阅读。
我们将探索一些可能有助于这些情况的技术,并评估它们何时以及如何可能有用。
由于我们有许多点挤在图表的一个非常小的部分,我们很可能会有几个点重叠。让我们来看一下改变标记的不透明度和大小的效果。
控制标记的不透明度和大小
opacity参数的取值范围是[0, 1],包含 0 和 1。我们可以手动指定一个数字来控制我们希望标记的透明度。值为0表示完全透明,这也可以看作是隐藏标记(或它们的一个子集)的一种方式。值为1表示标记将完全不透明,呈现分配给它们的颜色,并完全覆盖它们所在的区域。这也意味着opacity为0.1时,标记的透明度为 10%。这意味着需要将 10 个标记叠加在一起,才能完全覆盖它们所在的区域。如果我们将其设置为0.5(或 50%),这意味着两个标记将完全覆盖该区域,依此类推。
由于标记点较小,并且我们没有那么多的数值,我们也可以增加它们的大小,以便更好地观察。size参数,像其他所有参数一样,可以取我们 DataFrame 中某列的名称,或者是数字列表。这是我们用来表达某一列值的另一个视觉属性,其中相对大小反映了每个标记所代表的相对值。它有时也被称为气泡图。对于这种情况,我们希望提供一个固定的大小。这可以通过提供与我们分析的 DataFrame 长度相同的列表来轻松实现。这将给标记一个统一的默认大小,可能这不是我们想要的,所以我们可以通过size_max参数来控制它。重新使用相同的代码,并设置opacity=0.1、size=[5]*len(df)和size_max=15,我们可以得到图 6.18中的图表:

图 6.18 – 修改标记的透明度和大小
这看起来稍微好一些。我们有了更大的标记,且opacity为0.1时,我们可以更清楚地看到大部分标记集中在原点附近。很可能还有更多的细节,但由于我们有异常值,那些差异看起来非常小。
透明度和可见性之间总是存在一个权衡。你的标记越透明,你就能越清楚地看到,特别是在有成百上千个标记的情况下。但与此同时,它们可能会变得如此透明,以至于你什么也看不见。在0.1的opacity下,我们正在接近那个阶段。
现在,让我们来看一下另一种技巧,它涉及到在坐标轴上使用对数刻度。
使用对数刻度
正常的刻度是直观且易于理解的。就像物理物体一样,一块木板的长度是另一块的两倍,它包含的木材也是两倍,前提是它们的宽度和深度相同。例如,在前面的两张图中,0 和 0.2 亿之间的距离与 0.2 和 0.4 亿之间的距离是相同的。这个“数据距离”也是相同的。在正常刻度下,在这个例子中,每个刻度对应的是一定量的增加(在此例中是 0.2 亿)。而在对数刻度下,每增加一个刻度,就意味着前一个刻度的倍数。
例如,数字 10、20、30 和 40 形成了一个典型的序列,这是你在正常刻度上可能看到的。如果刻度是对数刻度,我们不会再加 10,而是使用log_x=True,我们会得到更新后的图表,见图 6.19:

图 6.19 – 使用对数刻度
现在我们的图表看起来有了很大不同,但实际上它仍然是相同的图表。请注意,我们已将opacity值改为0.25,因为0.1太难看清,而且由于标记现在比之前更分散,我们有了一个更加细致的视图,了解人口是如何分布的。我们可以看到,最不透明的部分是在一千万附近。与 14 亿相比,这几乎为零,这也是之前图表告诉我们的内容,但现在我们有了更好的视角。
请注意,主要刻度每个都比前一个大 10 倍(10k、100k、1M、10M、100M和1B),或者每增加一个主要刻度就加一个零。同时,我们可以看到次要刻度,2和5,意味着这些位置分别表示前一个主要刻度值的两倍和五倍。
让我们探索另一种可能在这种情况下考虑的选项。这次我们不使用任何透明度,但我们通过更改使用的符号给标记引入了大量空间。设置符号的方式与设置离散颜色一样,可以通过symbol_sequence参数来管理,它将循环遍历我们提供的选项,并为列中的每个唯一离散值分配一个符号。我们为其提供一个包含单一值的列表,因此所有标记将使用相同的符号。
现在我们移除opacity参数,并将其替换为symbol_sequence=['circle_open'],得到如图 6.20所示的新图表:

图 6.20 – 修改标记的符号
这可能更好,因为我们通过更改透明度并没有牺牲任何可见性。我们达到了查看标记集中的位置的目的,因为需要很多开放的圆圈才能完全覆盖一个区域。对数坐标轴使标记在水平方向上分布,这样就更容易看出它们的分布情况。刻度标签清楚地显示了数值,但如果我们的受众不熟悉这种尺度,我们可能需要使其非常明确和直观。
我们可以想象为用户提供我们刚刚尝试的所有选项。我们可以考虑设置一个组件,允许用户修改透明度,另一个组件切换正常和对数坐标轴,或许还有一个组件用于更改符号。理想情况下,我们不应该让用户在阅读图表时感到困难。最好是我们自己做这项工作,探索数据后提供合理的默认值。根据我们目前所做的探索,我们来考虑这些默认值可能是什么。
我们知道这个图表绘制的是各个国家的数据,并且它们的数量不能超过 200 个。这意味着我们可以为这些标记设置一个适合的默认透明度级别。标记数量达到数千时,可能需要更低的opacity级别,比如0.02。空心圆似乎在引入空间方面对我们有很好的效果,所以我们也可以选择空心圆作为默认符号,完全忽略透明度问题。size参数也是如此。我们知道我们绘制的是人口数据,而且它很可能总是包含离群值,就像这个例子一样,因此我们可能会保持对数刻度作为默认值。
一种更通用的互动图表可能允许用户修改他们想要探索的指标。在这种情况下,我们可能会为他们提供这些选项。然而,随着自由度和通用性的增加,用户将需要自己处理更多的数据处理细节。
我们已经对我们的指标有了很好的理解,并且看到了许多国家的示例。正如我们刚刚看到的,这个探索过程对于构建具有合理默认设置的仪表盘至关重要。现在让我们来探讨本章的新互动组件——Slider组件。
引入滑块和区间滑块
Slider和RangeSlider组件基本上是用户可以水平或垂直拖动的圆形控件,用于设置或更改某个值。它们通常用于设置连续值,因为它们的外观和拖动功能非常适合这一用途。但这并不是强制要求,因为我们也可以将它们用于分类/离散值。我们已经看到perc_pov_指标有三个级别,并且我们知道可以从数据集中选择所有年份。现在,我们希望创建两个滑块。一个让用户选择他们想要分析的贫困水平,另一个让他们选择年份。每种选择组合将创建一个不同的子集,并生成不同的图表。图 6.21显示了我们将要实现的最终结果的上部分:

图 6.21 – 两个滑块控制图表
如你所见,新的功能需要三个主要组件——两个Slider组件和一个Graph组件。当然,我们还有其他一些组件来控制布局,以及标签,但重点主要是如何创建和集成这个新功能。
重要提示
RangeSlider组件几乎与Slider组件相同。主要的区别在于它包含多个滑块手柄,用户可以在其中调整他们希望筛选数据的最大值和最小值。现在,我们将重点关注普通的Slider组件,而RangeSlider组件将在后续章节中讨论。
和往常一样,我们将把这个创建为 JupyterLab 中的独立应用程序,一旦它正常工作,我们就将其添加到应用中。首先,让我们了解 Slider 组件,了解它是如何工作的,然后创建我们的应用布局。
你可以创建一个最小化的应用程序,并在应用的布局中创建 Slider 组件,就像你用其他组件一样,通过调用 dcc.Slider():
app = JupyterDash(__name__)
app.layout = html.Div([
dcc.Slider()
])
app.run_server(mode='inline')
这将创建一个包含单个组件的简单应用,正如你在图 6.22中看到的那样:

图 6.22 – 一个简单的滑块组件
这样视觉上很容易使用,用户可以清楚地看到他们可以水平滑动圆圈。然而,目前没有任何指导,用户也不知道他们在修改什么值,因此我们需要修复这一点。我们将从创建我们的第一个滑块开始,包含我们正在分析的三种贫困水平。让我们看看我们将要使用的参数:
-
min:顾名思义,这是滑块的最小值。 -
max:这也设置了值的上限。 -
step:当我们从min滑动到max时,增量的大小应该是多少?默认情况下,它设置为1,但你可以将其设置为更高或更低的值。例如,如果你希望用户调整透明度,你可以设置min=0、max=1和step=0.01。这将为用户提供 100 个选项可供选择。 -
dots:滑块是否应显示圆点,还是应该是简单的线条?在我们的例子中,我们希望用户从三个不同的值中选择,所以将此选项设置为True是有意义的。 -
included:请注意,图 6.22 中滑块左侧的蓝色部分和右侧的灰色部分。在滑动过程中,蓝色部分会随着滑块的滑动而扩展/收缩,这是默认行为。在我们的例子中,我们提供了三个不同的选项,因此我们希望移除这种行为来强调这一点,因此我们将其值设置为False。 -
value:这是滑块应取的默认值。
这是一个 Slider 组件的示例,范围从 0 到 10:
dcc.Slider(min=0,
max=10,
step=1,
dots=True,
included=False)
这生成了图 6.23中的新滑块:

图 6.23 – 带有自定义选项的滑块组件
现在,圆点引导用户选择位置,并且提示选项彼此是不同的,特别是我们设置了 included=False。
Slider 组件的另一个重要参数是 marks 参数。我们需要向用户展示每个圆点对应的值。在某些情况下,如果没有足够的空间显示所有值,我们会跳过一些值。在我们的年份滑块中会有这种情况,但首先让我们创建贫困指标滑块。我们首先不使用 marks 参数创建它,然后再添加该参数:
dcc.Slider(id='perc_pov_indicator_slider',
min=0,
max=2,
step=1,
value=0,
included=False)
对于id,与其他变量一样,我们遵循了以perc_pov_开头的规则,以便与应用中的其他相关对象保持一致。回调函数将从这个组件接收到的值将是0、1和2,这是基于我们给出的min、max和step参数的。现在,这些值在我们的情况中没有实际意义,因为我们实际上希望得到指标名称的完整文本。我们可以通过获取滑块的值并将其用作我们创建的perc_pov_cols列表的索引来简化处理。在回调函数中,我们将使用这个整数值来提取相应的指标。稍后我们会在构建回调函数时看到这一点。现在,让我们来创建滑块的刻度标记。
自定义滑块的刻度标记和值
创建这些刻度标记的最简单方法是使用字典:{0: '$1.9', 1: '$3.2', 2: '$5.5'}。字典的键将作为value属性使用,而字典的值将是用户在每个贫困水平上看到的内容。对于我们的情况,这就足够了,我们可以按此使用。
我们还可以选择性地自定义标签的样式,这可以是任何 CSS 属性的字典。如果你查看图 6.21,你会看到两个滑块的刻度(数字)颜色非常浅,可能会给人一种它们属于同一个滑块的印象。我们可以通过将它们的颜色设置为深色来改进这一点。我们还可以为指标滑块设置粗体字体。这有助于将它们与年份区分开来,并且也能突出它们的独特性。年份是容易立即理解的,但用户很可能不熟悉数据集中追踪的贫困水平。
我们希望获得与我们的图表一致的颜色。由于我们将使用 cividis 色标,因此这是一个了解如何提取其颜色的好机会。px.colors.sequential模块包含了顺序色标的颜色列表,其中就包括了 cividis。我们可以通过运行以下命令来获取 cividis:
px.colors.sequential.Cividis
['#00224e',
'#123570',
'#3b496c',
'#575d6d',
'#707173',
'#8a8678',
'#a59c74',
'#c3b369',
'#e1cc55',
'#fee838']
我们收到的列表包含了实际上用于构建该色标的 10 种颜色。回想一下,我们之前尝试过手动使用 2 个和 3 个颜色。还值得注意的是,通过在色标名称后面添加_r,你可以获得色标的反转版本,例如,px.colors.sequential.Cividis_r。这将给我们相同的色标,但在这种情况下,黄色将对应较低的值。
现在,我们希望为刻度标记的标签使用的颜色将是 cividis 色标中的最深色,我们可以很容易地提取并将其赋值给一个变量,如下所示:
cividis0 = px.colors.sequential.Cividis[0]
使用这个方法后,我们现在可以按照以下方式设置marks参数:
marks={0: {'label': '$1.9', 'style': {'color': cividis0, 'fontWeight': 'bold'}},
1: {'label': '$3.2', 'style': {'color': cividis0, 'fontWeight': 'bold'}},
2: {'label': '$5.5', 'style': {'color': cividis0, 'fontWeight': 'bold'}}}
我们所做的基本上是扩展字典,其中,值不再是字符串,而是以以下形式出现的字典:
{'label': <label>, 'style': {<attribute_1>: <value_1>, <attribute_2>: <value_2>}
重要提示
通常,CSS 属性如font-size和font-weight是用连字符分隔的,并且是小写字母书写的。而在 Dash 中,您可以使用相同的属性,但必须去掉连字符,改用驼峰式命名法(如fontSize和fontWeight),正如前面的代码片段所示。
类似于刚才所做的,现在让我们创建另一个具有类似自定义的滑块。首先,为了隔离我们的子集,我们可以为这些变量创建一个特殊的 DataFrame:
perc_pov_df =\
poverty[poverty['is_country']].dropna(subset=perc_pov_cols)
perc_pov_years = sorted(set(perc_pov_df['year']))
关键是我们从perc_pov_cols中删除了任何缺失值,并且我们还通过使用sorted和set创建了一个排序后的唯一年份列表perc_pov_years。
以下代码创建了我们的新滑块,用于选择年份:
dcc.Slider(id='perc_pov_year_slider',
min=perc_pov_years[0],
max=perc_pov_years[-1],
step=1,
included=False,
value=2018,
marks={year: {'label': str(year),
'style': {'color': cividis0}}
for year in perc_pov_years[::5]})
这与我们为指标所做的几乎相同。我们将默认值设置为 2018 年,这是我们拥有数据的最新年份。如果这是一个动态更新的应用程序,我们也可以将此值设置为perc_pov_years中的最大年份。请注意,我们将标记设置为每五年显示一个。如果不这样做,滑块将非常难以使用。通过这样设置,我们可以看到在图 6.24中字体和颜色的细微差别:

图 6.24 – 更新颜色的滑块
布局的最后部分将是Graph组件:
dcc.Graph(id='perc_pov_scatter_chart')
如我之前所提到的,我们还拥有Label组件,以及Col和Row组件,用于更好地管理布局,但这些组件未被讨论,因为我们已经创建了多个使用它们的示例。
现在我们准备好创建回调函数,将我们刚才创建的三个元素链接起来:
-
我们首先创建函数的装饰器。这个过程与之前的示例一样简单。唯一的不同之处在于,这次我们有两个输入参数。在函数的定义中,参数的顺序将对应于
Input元素的顺序,因此我们将根据顺序命名它们:@app.callback(Output('perc_pov_scatter_chart', 'figure'), Input('perc_pov_year_slider', 'value'), Input('perc_pov_indicator_slider', 'value')) -
在接下来的部分中,我们创建函数的签名,并写出前几行代码。参数名为
year和indicator。我们现在使用indicator值(一个整数)来从perc_pov_cols中获取相应的元素。然后,我们创建变量df,它会过滤perc_pov_df,只保留year年份的数据。接着,我们定义dropna和sort_values。有一个年份没有任何数据,但它必须出现在滑块的值中,因此我们需要处理用户选择该年份的情况。我们通过简单的检查if df.empty来完成,如下所示:def plot_perc_pov_chart(year, indicator): indicator = perc_pov_cols[indicator] df = (perc_pov_df [perc_pov_df['year'].eq(year)] .dropna(subset=[indicator]) .sort_values(indicator)) if df.empty: raise PreventUpdate -
现在我们已经准备好了数据框(DataFrame),可以创建
Figure并返回。大部分代码现在应该都很熟悉了。hover_name参数用于在用户悬停在标记上时显示弹出框标题。将其设置为Country Name会使标题显示相应国家的名称并加粗。我们还使用了上一章中使用过的动态高度技巧,在这里我们设置了固定高度,并为每个国家增加了 20 像素。我们在最后添加的ticksuffix选项应该是显而易见的,用来表明这些值是百分比:fig = px.scatter(df, x=indicator, y='Country Name', color='Population, total', size=[30]*len(df), size_max=15, hover_name='Country Name', height=250 +(20*len(df)), color_continuous_scale='cividis', title=indicator + '<b>: ' + f'{year}' +'</b>') fig.layout.paper_bgcolor = '#E5ECF6' fig.layout.xaxis.ticksuffix = '%' return fig
向我们的应用添加布局元素和回调后,我们最终获得了额外的功能,通过这两个滑块的组合可以生成超过 130 个图表。图 6.25 显示了最终结果:

图 6.25 – 两个滑块和一个散点图 – 最终结果
恭喜你又为你的应用增加了新功能!这一次,我们创建了第一个多输入回调,它丰富了用户可以生成的选项,而不会让人感到复杂或压倒性。
从功能角度来看,下拉框和滑块之间并没有本质上的区别。我们本来可以使用下拉框来实现同样的功能,并且它也会正常工作。下拉框的优势在于它们在空间利用方面极为高效。一个小矩形就可以容纳数十个甚至数百个隐藏选项,用户可以进行搜索。这些选项可以是很长的字符串,可能无法并排显示在例如滑块旁边。
另一方面,滑块提供了更好的视角。它们隐式地包含了关于选项的元数据。你可以立即看到最小值和最大值,以及它们的分布范围。当你选择一个选项时,你可以很容易地判断你的选择相对于其他可用选项的极端程度。在贫困水平滑块的情况下,用户可以立即看到所有可选项。最后,滑块更类似于我们与物理物品的互动方式,所以使用它们可能比其他互动组件更具吸引力。因此,空间限制、我们分析的变量类型以及我们希望展示的方式是影响我们选择使用哪些互动组件的因素。
你可能注意到我们在如何将组件组合到应用中的讨论上减少了,这其实是有意为之。我们已经多次讨论过这些话题,设计上也希望鼓励你亲自去实践并尝试其他选项。你随时可以回到代码库,检查你的工作以及细节部分。
现在让我们回顾一下本章所涵盖的内容。
总结
我们介绍了散点图,并展示了如何使用graph_objects模块和 Plotly Express 来创建它们。我们学习了如何创建多个轨迹,并尝试了不同的方法。接着,我们讨论了颜色映射和设置,并探讨了连续变量和离散(分类)变量之间的处理差异。我们看到了不同的刻度——顺序型、发散型和定性型。我们还展示了如何设置自定义颜色、序列和刻度。我们还解决了一些问题,比如异常值和过度绘图。我们尝试了不透明度、改变符号和标记大小,并使用对数刻度来使我们的图表更易读。我们还介绍了滑块并学习了它们的工作原理,创建了两个可以协同工作、生成表达三个值的图表的滑块(而之前只有两个值)。然后,我们创建了一个回调函数来管理这些交互,并将其集成到我们的应用程序中。
到目前为止,通过所有示例和技巧的讲解,我们已经接近于以创建幻灯片和演示文稿的方式来创建仪表板。一旦掌握了布局元素,定制任何我们想要的大小和位置就变得非常简单。现在,随着我们探索不同的图表类型和数据可视化技巧,管理和修改这些内容将变得更加容易。
到目前为止,我们所探索的所有图表都使用了常规的几何形状,包括圆形、线条和矩形。在下一章中,我们将探索不规则形状以及如何以地图的形式进行可视化。地图非常吸引人且容易识别,但不像简单的常规形状那样直接可视化。接下来,我们将探讨这一点。
第七章:第七章:探索地图图表并通过 Markdown 丰富仪表板
在本章中,我们将探索如何处理地图,这是最具吸引力的图表类型之一。创建和处理地图的方式有很多种,并且有许多类型的地图图表。地图也有许多专门的地理和科学应用。我们将主要关注两种最常见的地图图表类型:分级地图和散点图地图。分级地图是我们最熟悉的地图类型。这些地图通过为地理区域上色来表示一个国家、州、区或任意多边形,并显示它们之间的数量变化。我们在上一章建立的大部分知识可以轻松地应用到散点图地图上,因为它们本质上是相同的,只是有一些差异。类似于x和y轴,我们使用的是经度和纬度,并且还有不同的地图投影。我们还将学习 Dash Core Component 中的新组件Markdown。
然后,我们将探索如何使用Mapbox,它提供了丰富的接口,包含不同的图层、主题和缩放级别。它还允许我们创建分级地图和散点图地图。
我们将主要覆盖以下主题:
-
探索分级地图
-
使用动画帧为图表添加新图层
-
使用回调函数与地图配合
-
创建
Markdown组件 -
理解地图投影
-
使用散点图地图
-
探索 Mapbox 地图
-
探索其他地图选项和工具
-
将交互式地图集成到我们的应用中
技术要求
我们将使用与上一章类似的工具。我们主要使用 Plotly Express 来创建图表。所使用的包包括 Plotly、Dash、Dash Core Components、Dash HTML Components、Dash Bootstrap Components、pandas 和 JupyterLab。
本章的代码文件可以在 GitHub 上找到,地址是github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_07。
查看以下视频,了解代码在bit.ly/3sAY8z8中的实际操作。
我们将从探索如何轻松地为国家创建分级地图开始。
探索分级地图
Choropleth 地图基本上是表示地图上某个区域的着色多边形。Plotly 自带了国家地图(以及美国州地图),因此,如果我们有关于国家的信息,绘制地图非常容易。我们的数据集中已经包含了这类信息。每一行都包含了国家名称和国家代码。此外,我们还拥有年份、一些关于国家的元数据(地区、收入组等),以及所有指标数据。换句话说,每个数据点都与一个地理位置相关联。那么,让我们从选择一个年份和一个指标开始,看看我们选择的指标在各个国家之间的变化:
-
将
poverty文件加载到数据框中,并创建year和indicator变量:import pandas as pd poverty = pd.read_csv('data/poverty.csv') year = 2016 indicator = 'GINI index (World Bank estimate)' -
创建一个包含所选年份值并仅包含国家的
poverty子集:df = poverty[poverty['is_country'] & poverty['year'].eq(year)] -
使用 Plotly Express 中的
choropleth函数创建 choropleth 地图,选择标识国家的列和用于颜色的列:import plotly.express as px px.choropleth(df, locations="Country Code", color=indicator)
你可以在图 7.1中看到前面代码的结果:

图 7.1 – 国家 choropleth 地图
我们提供的国家代码已包含在 Plotly 中,并且采用三字母 ISO 格式。与散点图一样,你可以看到,由于我们为颜色提供了数值列,因此选择了连续的颜色刻度。否则,我们将得到离散的颜色序列。例如,设置color='Income Group'会产生图 7.2中的图表:

图 7.2 – 具有离散颜色序列的国家 choropleth 地图
如你所见,类似于我们在第六章中看到的,通过散点图探索变量并使用滑块过滤子集,颜色系统的工作方式是类似的。
我们也可以使用常规的国家名称来绘制图表。为此,我们只需要设置locationmode='country names',其他的工作方式与之前相同。这里有一个使用国家名称的示例:
px.choropleth(df,
locations=['Australia', 'Egypt', 'Chile'],
color=[10, 20, 30],
locationmode='country names')
这会生成图 7.3中的图表:

图 7.3 – 使用国家名称的国家 choropleth 地图
颜色条的标题是color,因为它的含义不明确,而且它不是数据框中某一列的名称。我们可以通过设置labels={'color': <metric_name>}来重新命名它,以指示在我们案例中的度量是什么。现在让我们看看如何使图表具有交互性(不使用回调)。
利用动画帧为图表添加新图层
在上一个示例中,我们将年份设置为一个变量,并获取了该年份所需指标的快照。由于年份代表的是连续的值,并且可以作为分组变量使用,我们可以将年份用于 animation_frame 参数,使图表具有交互性。这将在图表下方引入一个新的控制柄,用户可以拖动它到所需年份,或者按下播放按钮,观看相应指标在各年份中的变化。这将是一个帧序列,就像观看视频一样。这样做的效果是,对于选定的年份,我们将从数据框中获取一个子集,其中 year 列中的行等于所选年份。图表会自动更新,颜色会对应于所选年份的值。
这是更新后的代码,用于生成按年份动画的图表:
fig = px.choropleth(poverty[poverty['is_country']],|
color_continuous_scale='cividis',
locations='Country Code',
color=indicator,
animation_frame='year')
fig.show()
现在我们可以看到更新后的图表,见 图 7.4:

图 7.4 – 含动画帧的国家区域图
如你所见,我们所需做的就是选择一个列名用作 animation_frame,其他一切都由系统自动处理。我们使用了一个仅包含国家的数据框,其中包括所有年份。进一步的子集操作是通过传递给 animation_frame 的参数自动完成的。我们可以拖动控制柄到特定年份,或者按下播放按钮,观看它如何随着时间变化。请注意,我们还更改了颜色比例,尝试使用不同的比例。迄今为止使用的两种颜色比例也应能在灰度版本的地图上读取。
既然我们已经有了基本的地图,接下来让我们探索如何控制地图的多个方面。地图图表的 layout 属性有一个子属性叫做 geo,在其中有几个有用的地理属性,允许我们控制地图的许多方面。这些属性的工作方式与其他属性相同。
我们基本上通过运行 fig.layout.geo.<attribute> = value 来设置所需的值。让我们来探索一些这些属性及其对之前图表的影响:
-
移除地图周围的矩形框架:
fig.layout.geo.showframe = False -
显示国家边界,即使我们没有某些国家的数据:
fig.layout.geo.showcountries = True -
使用不同的地球投影。选择
natural earth投影类型(稍后会详细讲解):fig.layout.geo.projection.type = 'natural earth' -
通过设置地图应显示的最小和最大纬度值来限制图表的垂直范围,从而更专注于各个国家:
fig.layout.geo.lataxis.range = [-53, 76] -
使用相同的技术限制图表的水平范围:
fig.layout.geo.lonaxis.range = [-137, 168] -
将陆地的颜色改为
'white',以清晰地标识出哪些国家缺少数据:fig.layout.geo.landcolor = 'white' -
设置地图的背景颜色(即海洋的颜色),以及整幅图表的“纸张”背景颜色。使用我们为应用程序设置的相同颜色,以确保主题的一致性:
fig.layout.geo.bgcolor = '#E5ECF6' fig.layout.paper_bgcolor = '#E5ECF6' -
设置国家边界和海岸线的颜色为
'gray':fig.layout.geo.countrycolor = 'gray' fig.layout.geo.coastlinecolor = 'gray' -
由于颜色条的标题占用了很多水平空间,因此用
<br>字符替换空格,将标题分成多行:fig.layout.coloraxis.colorbar.title =\ indicator.replace(' ', '<br>')
结果,我们得到了更新后的图表,见图 7.5:

图 7.5 – 具有自定义地理选项的国家分级图
通过几条命令,我们改变了图表的外观。我们将范围限制为主要集中在国家和陆地上,尽可能减少其他元素。我们还设置了统一的背景颜色,并显示了国家边界。还有一些其他选项可以轻松在fig.layout.geo属性下进行探索。现在我们准备好使指标选择动态化了;让我们看看如何实现。
使用回调函数与地图配合
到目前为止,我们所做的一切都是基于一个指标,使用该指标从数据集中选择所需的列。我们可以轻松创建一个下拉框,允许用户选择任何可用的指标,并让他们探索整个数据集。year变量已经是交互式的,并且是图表的一部分,通过animation_frame参数使用。因此,这可以成为用户在我们的应用程序中开始的第一个交互式探索图表,帮助他们了解可用的指标及其随时间变化的趋势。
设置这个过程非常简单,就像我们之前做过的那样。我们将实现它,之后我们将看到如何使用Markdown组件来为地图图表和所选指标添加上下文。
让我们在 JupyterLab 中独立实现此功能的必要步骤:
-
创建一个
Dropdown组件,其中可选项是poverty的列名,使用第 3 列到第 54 列之间的列:dcc.Dropdown(id='indicator_dropdown', value='GINI index (World Bank estimate)', options=[{'label': indicator, 'value': indicator} for indicator in poverty.columns[3:54]]) -
在我们刚刚创建的下拉框下,创建一个空的
Graph组件:dcc.Graph(id='indicator_map_chart') -
指标名称的长度各异,有些指标长度很长,几乎占据了整个屏幕的一半。我们可以通过类似之前的方法来处理,创建一个简单的函数。该函数接受一个字符串,将其拆分为单词,每三个单词分为一组,然后用
<br>字符连接它们:def multiline_indicator(indicator): final = [] split = indicator.split() for i in range(0, len(split), 3): final.append(' '.join(split[i:i+3])) return '<br>'.join(final) -
创建一个回调函数,将下拉框与地图图表连接起来:
@app.callback(Output('indicator_map_chart', 'figure'), Input('indicator_dropdown', 'value')) -
定义一个函数,接受所选指标并返回所需的地图图表。请注意,我们通过使用指标作为标题值来设置图形的标题。我们还使用了
Country Name列来设置悬停名称,这是当用户悬停在某个国家时出现的框的标题。高度也被设置为650像素。其余的地理属性在此省略,以避免重复,但它们与我们之前设置的相同。我们还使用刚才创建的multiline_indicator函数来修改颜色条的标题:def display_generic_map_chart(indicator): df = poverty[poverty['is_country']] fig = px.choropleth(df, locations='Country Code', color=indicator, title=indicator, hover_name='Country Name', color_continuous_scale='cividis', animation_frame='year', height=650) fig.layout.geo.showframe = False … fig.layout.coloraxis.colorbar.title =\ multiline_indicator(indicator)
在 JupyterLab 中运行该应用程序,你可以探索不同的指标。图 7.6展示了通过选择不同的指标和年份生成的图表的几个示例:

图 7.6 – 交互式生成的地图图表示例
一旦提供选项,用户可以在下拉菜单中搜索各种关键词并选择他们感兴趣的内容。然而,许多指标的具体含义和它们的限制仍然不太清晰。现在是我们向用户展示这些细节的好机会,以便让他们明确看到自己正在查看的内容。如前所述,度量中的限制至关重要,必须加以强调,以确保用户意识到它们。让我们来看一下如何使用Markdown组件添加格式化文本。
创建一个 Markdown 组件
Markdown 是一种以易于编写和易于阅读的方式生成 HTML 的方法。输出将像任何 HTML 文档一样显示,但编写和阅读过程要容易得多。比较以下两个代码片段,它们生成相同的 HTML 输出:
使用纯 HTML,我们将编写如下代码:
<h1>This is the main text of the page</h1>
<h3>This is secondary text</h2>
<ul>
<li>first item</li>
<li>second item</li>
<li>third item</li>
</ul>
相同的代码可以用 Markdown 编写如下:
# This is the main text of the page
### This is secondary text
* first item
* second item
* third item
我认为很明显,Markdown 更容易编写,也更容易阅读,特别是当你有嵌套项(例如我们这里的<ul>无序列表)时。
Markdown组件的工作方式相同。上面的代码只需传递给children参数,它将呈现为之前展示的 HTML。让我们在 JupyterLab 中创建一个最小的应用程序,看看Markdown组件是如何工作的:
-
进行必要的导入并实例化应用程序:
from jupyter_dash import JupyterDash import dash_core_components as dcc app = JupyterDash(__name__) -
创建应用程序的布局属性:
app.layout = html.Div([]) -
将带有前述文本的
Markdown组件传递给刚刚创建的div。请注意,尤其在处理多行文本时,使用三引号更为方便:dcc.Markdown(""" # This is the main text of the page ### This is secondary text * first item * second item * third item """) -
运行该应用程序:
app.run_server(mode='inline')
上述代码创建了一个迷你应用程序,输出如图 7.7所示:

图 7.7 – Markdown 组件的示例输出
Markdown 有多种显示文本的方式,如编号列表、表格、链接、粗体和斜体文本等。我们将介绍其中的一些功能,但即使你不熟悉它们,也很容易掌握。请记住,不同平台使用的 Markdown 有各种“变种”。你可能会遇到稍有不同的标记/语法规则,但一般来说,它们有很多重叠之处。
我们现在将在用户选择了他们想要的指标后,向地图中添加一些信息。基本上,我们会将重要信息添加到地图和滑块下方。图 7.8展示了这将如何显示,以便给你一个我们正在努力实现的目标的概念:

图 7.8 – Markdown 组件的示例
你在图中看到的所有文本和格式都是由Markdown组件生成的。
为了在应用中为其创建一个特殊区域,我们只需在地图下方添加一个 Markdown 组件,并为其指定一个唯一的 ID。
生成这个组件的过程将发生在我们为生成地图所创建的相同回调函数中。该回调函数现在应该接收两个 Output 元素而非一个,并且返回时应该返回两个元素(图形和生成的 Markdown)。为了获取该组件所需的内容,我们需要打开包含所有指标详细信息的文件。这之前已经做过,但作为提醒,我们可以通过运行 series = pd.read_csv('data/PovStatsSeries.csv') 来获得。现在让我们实现这些步骤:
-
在
Graph组件正下方,添加新的Markdown组件(注意,我们还将其背景颜色设置为与地图一致,以保持整个应用的一致性)。_md后缀表示Markdown:dcc.Markdown(id='indicator_map_details_md', style={'backgroundColor': '#E5ECF6'}) -
更新回调函数,加入新组件:
@app.callback(Output('indicator_map_chart', 'figure'), Output('indicator_map_details_md', 'children'), Input('indicator_dropdown', 'value')) -
在回调函数中完成
fig变量的定义后,我们现在执行创建Markdown输出所需的步骤。通过获取Indicator Name列等于所选指标的行,创建series的适当子集:series_df =\ series[series['Indicator Name'].eq(indicator)] -
从
series_df中提取Limitations and exceptions列的值。请注意,由于一些值缺失,并且缺失的值不是字符串,我们用字符串N/A填充它们,并且如果存在,替换任何两个换行符\n\n为单个空格。然后,我们提取其values属性下的第一个元素:limitations =series_df['Limitations and\ exceptions'].fillna('N/A').str.replace('\n\n',\ ' ').values[0] -
现在我们已经定义了两个变量,
series_df和limitations,我们将使用 Python 的 f-string 格式化方法,通过花括号将变量插入到适当的位置:f'{<variable_name>}'。我们首先使用<h2>元素插入指标名称。在 Markdown 中,标题对应其 HTML 等效元素,其中哈希符号的数量对应标题级别。在这里,我们使用两个符号来表示<h2>:## {series_df['Indicator Name'].values[0]} -
接下来,我们以常规文本添加详细描述,前面不加任何哈希符号:
{series_df['Long definition'].values[0]} -
接下来,我们为
Unit of measure、Periodicity和Source添加项目符号。可以通过在每行前添加星号来创建项目符号。这是一个简单的过程,获取来自正确列的正确元素。请注意,对于Unit of measure的缺失值,我们使用单词count来填充,这将替换掉那些指标是简单计数而非百分比的缺失值。例如,人口就是一个这样的例子。对于Periodicity,我们只需在缺失的值处替换为N/A。在任何文本前后加上的星号会使其加粗,类似于运行<b>text</b>:* **Unit of measure** {series_df['\ Unit of measure'].fillna('count').values[0]} * **Periodicity**\ {series_df['Periodicity'].fillna('N/A').values[0]} * **Source** {series_df['Source'].values[0]} -
在
<h3>中添加Limitations and exceptions子标题:### Limitations and exceptions: -
接下来,我们在常规文本中添加已经创建的
limitations变量:{limitations}
将前面的代码整合在一起,这是创建Markdown组件的完整代码,并展示了它在回调函数中的相对位置。请注意,在某些情况下,series数据框中没有某些指标的详细信息。在这种情况下,我们将Markdown变量设置为一个字符串,表示缺少此类详细信息。此条件也可以在以下代码中看到,在检查series_df.empty时;否则,其他部分将像之前一样运行。
…
fig.layout.coloraxis.colorbar.title =\
multiline_indicator(indicator)
series_df = series[series['Indicator Name'].eq(indicator)]
if series_df.empty:
markdown = "No details available on this indicator"
else:
limitations = series_df['Limitations and exceptions'].fillna('N/A').str.replace('\n\n', ' ').values[0]
markdown = f"""
## {series_df['Indicator Name'].values[0]}
{series_df['Long definition'].values[0]}
* **Unit of measure** {series_df['Unit of measure'].fillna('count').values[0]}
* **Periodicity**
{series_df['Periodicity'].fillna('N/A').values[0]}
* **Source** {series_df['Source'].values[0]}
### Limitations and exceptions:
{limitations}
"""
return fig, markdown
我们最终返回一个元组fig, markdown,而不仅仅是之前版本中的fig。将这段代码添加到应用中会将相应的 Markdown 添加到地图,并为其提供更好的上下文,同时指出用户需要记住的限制。
接下来我们将讨论地图可以展示的不同投影,以及如何进行更改。
理解地图投影
我们在地图中使用了某种投影类型的示例,现在我们将更详细地探讨这个话题。当我们试图将地球(或其部分)绘制在一个平面矩形上时,形状不可避免地会发生某种程度的扭曲。因此,有不同的方式或投影可以使用。没有任何投影是完美的,它们在形状、面积、相对位置等方面存在权衡。哪个投影更合适的细节取决于应用场景,并超出了本书的讨论范围。不过,我们将探讨如何更改使用的投影,并查看如何获取可用的投影。
使用 Plotly Express,我们在地图函数中有一个projection参数,该参数接受一个字符串,可以用来设置所需的投影类型。或者,我们也可以像之前那样,通过将值赋给fig.layout.geo.projection.type来设置。
图 7.9展示了几种可用的投影选项及其相应的名称。

图 7.9 – 可用地图投影的示例
如你所见,有不同的方式来展示地球。虽然正投影可能在形状上看起来更逼真,但它的问题是我们只能看到地球的一部分,因此失去了透视感。而方位等面积投影实际上在交互使用并缩放到某些区域时相当逼真。可以随意尝试不同的投影方式,并选择最适合你的。
到目前为止,我们已经实验过多边形或区域地图,现在我们将探索另一种我们通常比较熟悉的地图类型:散点图地图。
使用散点地图图
x轴和y轴与经纬度之间的主要区别在于地球的形状。当我们接近赤道时,垂直经线之间的距离尽可能远,而当我们接近南北极时,它们之间的距离则尽可能近。图 7.10展示了这一点:

图 7.10 – 地球地图,显示经纬度线
换句话说,当我们接近赤道时,地图的形状更加接近矩形,因为一单位经度与一单位纬度几乎相等。接近极地时,比例完全不同,矩形开始接近三角形。这与矩形平面不同,在矩形平面上,垂直方向的单位距离对应于水平方向的相同单位距离,无论你处于平面上的哪个位置。当然,这假设了两个轴上的比例是线性的。例外的是对数坐标轴,关于这一点我们已经在第六章,使用散点图探索变量和通过滑块过滤子集中进行了讨论。地图投影会帮我们处理这个问题,所以我们不需要担心这个问题。因此,我们可以像思考 x 和 y 轴一样思考地图投影,并选择我们需要的投影。
让我们看看如何使用 scatter_geo 函数通过 Plotly Express 绘制散点图。
我们从一个非常简单的例子开始:
df =\
poverty[poverty['year'].eq(2010) & poverty['is_country']]
px.scatter_geo(df, locations='Country Code')
首先,我们创建了 df,其中年份为 2010,并过滤掉非国家数据。然后,就像我们在使用 choropleth 地图时所做的那样,我们选择了用于 locations 参数的列。这生成了图 7.11中的简单图表:

图 7.11 – 使用 scatter_geo 函数的散点图
你可以看到做这件事是多么简单。除了标记国家并显示国家代码值外,这张图表没有太多信息。
国家名称默认由 Plotly 提供支持。另一个有趣的应用可能是使用 lat 和 lon 参数在地图上绘制任意位置,正如你在下面的代码和图 7.12中所看到的那样:
px.scatter_geo(lon=[10, 12, 15, 18], lat=[23, 28, 31, 40])
这将产生以下输出:

图 7.12 – 使用经纬度数据的散点图
你可以轻松地应用我们在第六章,使用散点图探索变量和通过滑块过滤子集中讨论的概念,来修改大小和地图颜色,设置不透明度等等。
现在,我们将通过引入另一种更丰富的地图制作方式——使用 Mapbox,来探索这些选项。
探索 Mapbox 地图
Mapbox 是一个开源地图库,由同名公司支持,该公司还提供额外的服务、图层和主题,用于生成丰富的地图应用程序。我们在这里将使用的选项可以立即与 Plotly 一起使用,但还有一些其他样式和服务需要你注册账户并在每次生成地图时使用令牌。
一个例子应该能帮助我们快速入手,因为我们已经非常熟悉散点图:
px.scatter_mapbox(lon=[5, 10, 15, 20],
lat=[10, 7, 18, 5],
zoom=2,
center={'lon': 5, 'lat': 10},
size=[5]*4,
color_discrete_sequence=['darkred'],
mapbox_style='stamen-watercolor')
前面的代码应该很简单。lon 和 lat 参数相当于散点图中的 x 和 y 参数。size 和 color_discrete_sequence 参数已经讲解过。一个有趣的新参数是 zoom 参数,我们在这里将其设置为 2。这个参数可以取从 0(整个世界)到 22(建筑物级别缩放)之间的整数值,包括 0 和 22。我们还可以看到,设置地图中心是多么简单,我们是使用第一个点的坐标(5, 10)来设置的。最后,mapbox_style 参数提供了一些有趣的选项,可以用来以不同样式显示地图。stamen-watercolor 样式给它一种艺术感,如图 7.13所示:

图 7.13 – 使用 Mapbox 和自定义样式的散点图
将鼠标悬停在地图上的i上,会显示瓷砖和数据的来源。正如你所看到的,很多层和工作都凝聚在这个简单的函数中。现在,让我们使用相同的方法绘制一些来自数据集的数据。
因为 scatter_mapbox 主要处理纬度和经度数据,而我们的数据集没有关于国家的这种数据,所以我们将获取这些数据,进行合并,然后将标记放置到对应的位置。
有很多来源提供这种数据,快速的在线搜索可以找到一些好的来源。我们可以使用 pandas 的 read_html 函数来获取数据。它接受一个 URL,下载该 URL 上的所有 <table> 元素,并返回一个 DataFrame 对象的列表。我们只需要选择我们想要的那个。在这个例子中,它是第一个。以下代码实现了这一点,并创建了 lat_long 变量,这是一个 DataFrame:
lat_long =\
pd.read_html('https://developers.google.com/public-data/docs/canonical/countries_csv')[0]
如果你还记得我们在 第四章中讨论的内容,数据操作与准备 - 为 Plotly Express 铺路,我们讲解了几种数据操作,我们将使用 pandas 的 merge 函数,通过左连接操作将 lat_long 合并到 poverty 中。
我们首先通过在 JupyterLab 中打印 lat_long 数据框来查看其结构,你可以在图 7.14中看到顶部和底部的五行数据:

图 7.14 – 包含国家纬度和经度数据的 lat_long 数据框
poverty 数据框中还有一个名为 2-alpha code 的列,包含使用相同的两字母标准的国家代码,所以我们将使用这些列进行合并,如下所示:
poverty = pd.merge(left=poverty, right=lat_long, how='left',
left_on='2-alpha code', right_on='country')
这将把lat_long列添加到poverty中,按照它们所属的行对齐,并在必要时进行重复。请记住,我们使用left方法进行合并,这意味着left参数是合并的依据。你可以在图 7.15中查看合并后的一些随机行和重要列,以便更清楚地理解:

图 7.15 – 与 lat_long 合并的贫困 DataFrame 子集
请注意,在没有经度和纬度值的情况下,我们会得到NaN。例如,在有相同国家名称的情况下(如塔吉克斯坦),经度和纬度值会简单地被复制,以保持这些值与各自国家的映射,无论我们选择哪一行。
我们现在准备创建一个气泡图(散点图,其中标记的大小反映某个数量)。我们只需要创建一个包含国家并去除所需指标Population, total缺失值的poverty子集。可以使用以下代码完成:
df =\
poverty[poverty['is_country']].dropna(subset=['Population, total'])
创建气泡图需要调用scatter_mapbox函数,但我们将逐个讨论给定的参数:
-
调用刚刚创建的子集的函数:
px.scatter_mapbox(df, …) -
选择用于经度和纬度值的列:
lon='longitude', lat='latitude' -
设置所需的缩放级别,以显示整个地球:
zoom=1 -
将指标的值映射到标记的大小,并设置合适的最大值:
size='Population, total', size_max=80 -
将每个国家所属的收入组映射到标记的颜色上(在这种情况下是离散变量):
color='Income Group' -
选择
year列作为用于动画的列:animation_frame='year' -
设置合适的不透明度,因为我们肯定会有重叠的标记:
opacity=0.7 -
为整个图表设置适当的高度,以像素为单位:
height=650 -
向悬浮框添加更多信息,通过包括另外两列数据,使得用户将鼠标悬停在标记上时显示:
hover_data=['Income Group', 'Region'] -
选择自定义颜色序列,以区分各个国家所属的收入组:
color_discrete_sequence=px.colors.qualitative.G10 -
为地图设置自定义样式:
mapbox_style='stamen-toner' -
为悬浮框设置标题,使用国家名称:
hover_name=df['Country Name'] -
为图表设置标题:
title="Population by Country"
运行前面的代码会生成一个交互式图表,如图 7.16所示:

图 7.16 – 基于国家按年份动画显示人口的 scatter_mapbox 气泡图
这是我们刚才讨论的完整代码,以便更清晰地理解:
px.scatter_mapbox(df,
lon='longitude',
lat='latitude',
zoom=1,
size='Population, total',
size_max=80,
color='Income Group',
animation_frame='year',
opacity=0.7,
height=650,
hover_data=['Income Group', 'Region'],
color_discrete_sequence=px.colors.qualitative.G10,
mapbox_style='stamen-toner',
hover_name=df['Country Name'],
title='Population by Country')
你可以看到设置所有选项是多么简单,所涉及的代码是多么简洁。我们只需要了解选项及其工作原理。
由于这是一个交互式图表,用户可以进行缩放,因此通过简单地缩放一级,就能轻松处理我们所遇到的重叠问题。图 7.17展示了用户缩放后的同一图表:

图 7.17 – 放大显示更清晰的散点图 Mapbox 图表
泡泡图相比于地区图的一个优势是,它能够展示数值与国家(或任何地点)的地理区域之间的关系。例如,图 7.16 展示了加拿大、俄罗斯和澳大利亚三个有趣的案例,它们的人口相对于面积来说较少。换句话说,它们的人口密度较低。这为这个指标提供了更多的视角。
如你所见,显示和与地图互动的方式有很多种,我们只是触及了可做的事情的表面。接下来,我们将看看一些其他可用的选项,万一你有兴趣进一步了解。
探索其他地图选项和工具
以下是一些关于地图探索的提示,无需过多深入细节。
你可能已经考虑过将自定义多边形或区域可视化为地区图。目前我们所讨论的仅是标准的国家。当然,你也可以选择可视化一个包含任意点的自定义区域。
有一个标准的 GeoJSON 格式用于表示这些信息。它主要由点、线和多边形组成。点只是地图上的位置,类似于我们用于散点图的点。线是连接的一组点,按一定顺序排列,且第一个点和最后一个点不相同。正如你所猜测的,多边形类似于线,但条件是第一个点和最后一个点相同。请注意,许多国家由多个多边形组成。大多数 Plotly 地图函数支持 GeoJSON,你可以用它来进行自定义地图绘制。
当你有自定义数据用于特定位置时,并且需要获取相关数据时,这非常有用。
另一个重要且有用的项目是 geopandas,值得考虑学习。顾名思义,它是一个像 pandas 一样工作的专用库,提供了用于地理数据的特殊数据结构和技术,最显著的是 GeoDataFrame。如果你有更专业的地图需求,或者经常需要进一步自定义地图,它是值得学习的。
现在我们来将我们创建的功能添加到应用中。
将互动地图集成到我们的应用中
我们创建的地图,结合了 Dropdown 和 Markdown 组件,可以成为我们应用中的第一个探索工具。现在我们可以去掉人口条形图,代之以我们刚刚创建的组件,供用户探索所有指标,在地图上查看它们,滚动查看年份,并且对于每个指标,获取完整的详细信息,同时看到局限性和潜在问题。一旦某个内容引起用户注意,他们可以找到其他图表,获取更多关于他们感兴趣的指标的细节(如果有的话)。
为了将新功能完全整合到我们的应用中,我们需要按照以下步骤进行操作:
-
在
app.py模块的顶部添加series的定义:series = pd.read_csv('data/PovStatsSeries.csv') -
在
app.layout之前的任何位置添加multiline_indicator函数的定义:def multiline_indicator(indicator): final = [] split = indicator.split() for i in range(0, len(split), 3): final.append(' '.join(split[i:i+3])) return '<br>'.join(final) -
在应用的顶部,在我们之前放置人口条形图的位置,添加
Dropdown、Graph和Markdown组件。以下代码展示了如何添加这些组件,包括组件的 ID 以使其更清晰,但完整的定义已被省略。注意,还添加了一个Col组件,并且设置了另一个Col组件的宽度,两个都使用了lg(大)参数。第一个用来在显示内容之前插入一个空白列,第二个用来控制该列中内容的宽度:app.layout = html.Div([ dbc.Col([ html.Br(), html.H1('Poverty And Equity Database'), html.H2('The World Bank'), ], style={'textAlign': 'center'}), html.Br(), dbc.Row([ dbc.Col(lg=2), dbc.Col([ dcc.Dropdown(id='indicator_dropdown', ...), dcc.Graph(id='indicator_map_chart', ...), dcc.Markdown(id='indicator_map_details_md', ...) ], lg=8) ]), html.Br()
在这一章中,我们探讨了几个新的选项,现在让我们总结一下我们所做的工作。
总结
我们从探索如何创建热力图开始,这是一种我们都习惯看到的地图类型。我们还展示了如何为这些地图添加动画效果,如果我们有一个按顺序变化的值,譬如按年进展的某个指标。然后,我们创建了一个回调函数,使地图能够与我们所有可能的指标一起工作,用户可以浏览所有指标,然后决定接下来想要探索的内容。
之后,我们学习了如何使用 Markdown 来生成 HTML 内容,以及如何将其添加到 Dash 应用中。接着,我们探讨了不同的地图或投影显示方式,并了解了如何选择我们想要的投影。
我们了解了另一种类型的地图,即散点地图图表。基于前一章中建立的知识,调整这些知识来适应散点地图非常简单。我们还学习了 Mapbox 提供的丰富选项,并探讨了其他几个可以进一步探索的地图主题。最后,我们将这些新功能集成到我们的应用中,现在应用包含了大量关于几乎所有指标的解释性文本,用户可以更清晰地了解他们正在分析的内容。
在下一章,我们将处理另一种类型的图表,帮助统计数值并展示它们在数据集中的分布情况,即 直方图。我们还将探索一个新的组件,Dash DataTable,它允许我们以丰富的方式展示表格数据,并提供许多筛选、可视化、下载等选项。
第八章:第八章:计算数据频率并构建交互式表格
到目前为止,我们探索的所有图表类型都直接展示了我们的数据。换句话说,每个标记,无论是圆形、条形、地图或其他形状,都对应数据集中的一个数据点。而直方图则显示与数据点组相关的统计汇总值。直方图主要用于统计数据集中的值。它通过将数据分组或“分箱”到多个箱中,并显示每个箱中的观测值数量来实现。除了计数,当然也可以进行其他计算,比如计算均值或最大值,但计数是最常见的应用场景。计数结果以条形图的形式呈现,条形的高度对应每个箱中的计数(或其他计算结果)。另一个重要的结果是,我们可以看到数据是如何分布的,以及数据呈现什么样的分布形状/类型。观察值是否集中在某个点或多个点附近?它们是向左还是向右偏斜的?这些都能帮助我们全面了解数据的一个方面。
概率分布是统计学中的基础内容,对于了解我们的数据概况至关重要。了解数据值在我们的样本或数据集中是如何分布的,以及它们的集中位置非常重要。如果一个数据集看起来呈正态分布,我们可能会做出不同的假设,并有不同的预期,而不是假设它呈指数分布。直方图有助于揭示我们数据分布的形状。
在本章中,我们还将探索 Dash 的DataTable组件。这个组件灵活、强大且功能丰富,可以帮助我们完成多项任务,包括显示、过滤和导出数据表。
本章我们将讨论以下内容:
-
创建直方图
-
通过修改箱子并使用多个直方图来定制直方图
-
向直方图添加交互性
-
创建二维直方图
-
创建数据表
-
控制表格的外观和感觉(单元格宽度、高度、文本显示等)
-
向应用中添加直方图和表格
技术要求
我们将使用与上一章相似的工具,只是增加了一些内容。我们将使用 Plotly Express 和graph_objects模块来创建我们的图表。需要使用的软件包包括 Plotly、Dash、Dash Core Component、Dash HTML Components、Dash Bootstrap Components、pandas 以及新的dash_table包。您无需单独安装这个包(尽管可以),因为它在安装 Dash 时会一同安装。
本章的代码文件可以在 GitHub 上找到,链接地址为:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_08。
查看以下视频,了解代码的实际应用:bit.ly/3sGSCes。
创建直方图
我们希望了解如何获得数据样本的分布,并了解值的集中位置以及其变异性/扩展性。我们将通过创建直方图来实现这一目标。
和往常一样,我们从最简单的示例开始:
-
我们打开
poverty数据框,并创建一个子集,仅包含 2015 年各国的数据:import pandas as pd poverty = pd.read_csv('data/poverty.csv') df = poverty[poverty['is_country'] & poverty['year'].eq(2015)] -
导入 Plotly Express,并使用
histogram函数,将df作为data_frame参数的参数,并选择我们想要的指标作为x参数:import plotly.express as px gini = 'GINI index (World Bank estimate)' px.histogram(data_frame=df, x=gini)结果,我们得到了你在图 8.1中看到的基尼指标直方图:

图 8.1 – 基尼指标直方图
x轴使用我们选择的指标命名,y轴的标题为count。这是histogram函数的默认功能,从鼠标悬停在任何条形图上的提示框也可以清楚地看到这一点。在这里,我们得知 2015 年有 18 个国家的基尼指数位于区间(35, 39.9)之间。我们之前已经按国家可视化了该指标(逐个国家展示),但这次我们通过直方图了解每个箱子中的数值数量以及这些数值的分布情况。我们可以看到,大多数国家的基尼指数在 25 到 40 之间,而且基尼指数越高,国家数量越少。当然,这仅适用于这一特定年份。
我们使用的是默认的箱子数量,但如果需要,可以修改它。这是你通常想要在交互式设置中修改的内容,直到你获得一个好的视图。在交互式环境中,比如在仪表板上,允许用户修改箱子的数量可能是个好主意,特别是当你不确定他们会选择哪个指标以及该指标的值如何分布时。这正是我们在这个数据集中的情况。
让我们看看改变箱子数量以及其他可用修改的效果。
通过修改箱子数量和使用多个直方图来定制直方图
我们可以通过nbins参数更改箱子的数量。我们将首先看到使用两个极端值作为箱子数量的效果。设置nbins=2会生成图 8.2中的图表:

图 8.2 – 具有两个箱子的基尼指标直方图
如你所见,值被分为两个相等的箱子,(nbins=500会生成图 8.3中的图表:

图 8.3 – 具有 500 个箱子的基尼指标直方图
现在它更加详细了,可能比实际有用的还要详细。当你设置太多的箱子时,几乎就像是在查看原始数据。
默认的箱子数量导致箱子的大小是五个单位的区间。现在我们知道我们的值范围在 25 到 60 之间(45),我们可能希望看到数据如何在 45 个箱子中分布。这使得每个箱子的大小为 1。图 8.4 显示了设置 nbins=45 的结果:

图 8.4 – 具有 45 个箱子的基尼指数直方图
到目前为止,我们在本章中创建的所有图形都是基于相同的数据集。你可以看到,基于所选的箱子数量,分布看起来有多么不同。你也可以将其视为以不同的分辨率查看数据分布。通常,对于你的用例来说,有一个最优分辨率,你可以手动调整,直到找到最有用/最具洞察力的分辨率。这是使直方图具有交互性的主要优势,你可以让用户根据自己的需求进行探索。
回想一下,我们的数据集中有一些分类列,我们可以使用这些列来给条形图上色,从而更详细地查看数据。让我们看看如何实现这一点。
使用颜色进一步拆分数据
正如你可能猜到的那样,向 Plotly Express 图表添加颜色,实际上就是从我们使用的数据框中选择一列。设置 color='Income Group' 生成了你可以在 图 8.5 中看到的图表:

图 8.5 – 按收入组着色的基尼指数直方图
这就是完全相同的直方图,但通过数据集的另一个维度进行了丰富。每个条形根据 收入组 进行拆分,并相应地上色。我们现在可以看到每个箱子中,来自每个收入组的国家数量。
你还可以看到设置 color='Region', color_discrete_sequence=px.colors.qualitative.Set1 的效果,如 图 8.6 所示:

图 8.6 – 按地区着色的基尼指数直方图
再次,我们得到了相同的直方图,但使用不同的列进行了着色,barmode 参数。让我们看看这种方法如何应用于直方图。
提示
你可能已经注意到,直方图中的条形图是连在一起显示的,没有像条形图那样的间隔。这是一个视觉提示,用来表示直方图的连接特性。箱子是将一组观测值相互分开的任意分隔点。正如我们所看到的,这些分隔点可以不同选择,从而产生完全不同的形状。条形图通常用于离散或分类变量,并且通常会在条形之间留一些空隙来表达这一点。
探索在直方图中显示多个条形的其他方式
之前的两个直方图将每个区间的子柱状图堆叠在一起。这是有道理的,因为这些子柱状图代表了各自区间下的数据分组。换句话说,它们展示了每个区间下国家分组的分布。
在某些其他情况下,我们可能想做相同的事情,但针对两年数据。在这种情况下,将柱状图堆叠可能会产生一种错误的印象,即子柱状图对应于同一个区间的不同部分,而实际上它们对应的是同一个区间,但来自不同的年份。通过一个例子可以更容易地理解这一点:
-
创建一个包含仅有国家且年份范围为
[2010, 2015]的poverty子集:df = poverty[poverty['is_country'] & poverty['year'].isin([2010, 2015])] -
对基尼指数运行
histogram函数,按year着色并设置barmode='group':px.histogram(df, x=gini, color='year', barmode='group')这样就得到了以下输出:

图 8.7 – 按年份着色的基尼指标直方图,barmode 设置为 "group"
由于年份代表了相同指标和相同区间的“前后”视角,我认为将它们并排显示更为合理,这样我们可以看到每个区间的值如何在两个或多个选择的年份间增加或减少。
如果我们更关心突出显示分布整体变化的话,还有另一种处理方式。我们可以运行刚才使用的相同函数,但除了颜色外,使用 facets 来将直方图拆分为两部分。代码也很简单,并且包含了一个额外的参数,如下所示:
px.histogram(df, x=gini, color='year', facet_col='year')
这样就得到了以下输出:

图 8.8 – 按年份着色并拆分的基尼指标直方图
同样,最后两个图表以两种不同的方式显示相同的信息。在图 8.7中,非常容易比较每个区间内国家数量如何随着年份变化而变化。但要看清第一年和第二年之间分布的变化就稍微难一些。图 8.8则相反。请注意,我们也可以使用 facet_row,这样可以将图表显示在彼此之上。但我们选择将它们并排显示,因为我们更关注比较柱状图的高度,而并排显示时,比较会更容易。如果我们设置 orientation='h'(横向显示),那么在这种情况下,使用 facet_row 也会更方便。
有时我们可能更关心某个区间内值的百分比,而不是每个区间的绝对数量。得到这个结果也非常简单。我们只需设置 histnorm='percent'。我们首先创建一个 fig 对象并添加新的选项:
fig = px.histogram(df, x=gini, color='year', facet_col='year',
我们还可以通过在 y 轴刻度上添加百分号后缀来更明确地显示百分比。这可以通过以下代码实现:
fig.layout.yaxis.ticksuffix = '%'
我们还可能希望为y轴设置一个更具描述性的标题,这也可以通过以下代码轻松实现:
fig.layout.yaxis.title = 'Percent of total'
运行这段修改后的代码将生成图 8.9中的图表:

图 8.9 – 一个按年份着色并分割的基尼系数直方图,显示百分比
这张图表看起来与图 8.8中的图表相同。主要的区别是,条形的高度表示的是百分比,而不是绝对数值。通过刻度后缀和y轴标题,这一点也变得更加清晰。
我们已经探索了许多直方图的选项。现在让我们使我们的直方图具备交互性,并添加一些其他选项。
为直方图添加交互性
就像我们在第七章中做的那样,探索地图图表并用 Markdown 丰富你的仪表板,我们也可以对直方图做同样的事情。我们可以允许用户更好地了解某个指标在某一年或多个年份中的分布。不同之处在于,我们希望允许他们自定义区间的数量。既然我们现在已经能够处理多个输入和输出,让我们为用户添加更多选项。我们还可以允许用户选择多个年份,并使用分面显示多个年份的多个子图。图 8.10展示了我们将朝着这个目标努力的方向:

图 8.10 – 一个允许选择指标、年份和区间的直方图应用
我们现在就开始构建吧。我们不会讨论布局元素,例如颜色和宽度,但你可以随时参考代码库来获取确切的解决方案。我们将专注于为此添加交互性。稍后我们会将其添加到我们的应用程序中:
-
进行必要的导入:
from jupyter_dash import JupyterDash import dash_core_components as dcc import dash_html_components as html import dash_bootstrap_components as dbc from dash.dependencies import Output, Input -
创建一个
app对象及其layout属性:app = JupyterDash(__name__) app.layout = html.Div([]) -
将
Label和Dropdown组件作为第一个元素添加到刚刚创建的 div 中。Dropdown组件显示可用的指标,它与我们在第七章中创建的完全相同,探索地图图表并用 Markdown 丰富你的仪表板:html.Div([ dbc.Label('Indicator:'), dcc.Dropdown(id='hist_indicator_dropdown', index (World Bank estimate)', indicator, 'value': indicator} for indicator in poverty.columns[3:54]]), ]) -
在
dbc.Label和dcc.Dropdown组件中添加到 div 的列表中,以表示用户可以选择一个年份和实际要选择的年份,并允许多选。请注意,由于此下拉框允许多选,如果提供了默认值,则需要以列表的形式提供:dbc.Label('Years:'), dcc.Dropdown(id='hist_multi_year_selector', value=[2015], one or more years', year, 'value': year} for year in poverty['year'].drop_duplicates().sort_values()]), -
再次,在 div 中相同的列表中,我们添加了另一个
dbc.Label组件和一个dcc.Slider组件,这将允许用户修改生成的直方图中的 bin 数量。注意,如果不设置默认值,Plotly 将根据所分析的数据提供默认的 bin 数量。滑块中将显示为0。用户可以根据需要进行修改:dbc.Label('Modify number of bins:'), dcc.Slider(id='hist_bins_slider', min=0, step=5, marks={x: str(x) for x in range(0, 105, 5)}), -
最后,我们添加了一个
Graph组件,这将完成我们的布局:dcc.Graph(id='indicator_year_histogram')
运行这些步骤会创建我们应用程序的可视部分(布局),但没有任何功能。默认的外观如图 8.11所示,我将留给你修改颜色、对齐方式和相对位置,使用我们在 第一章中构建的知识,Dash 生态系统概述:

图 8.11 – 没有功能的直方图应用的默认视图
现在我们将开始构建交互功能。在这个案例中,我们需要构建一个函数,该函数接受三个输入(指标下拉框、年份下拉框和 bins 滑块)。它将返回一个Figure对象,用于修改图形底部的图表:
-
创建回调函数。这里没有什么特别的;我们只需确保设置 ID,以表明它们与直方图相关:
@app.callback(Output('indicator_year_histogram', 'figure'), Input('hist_multi_year_selector', 'value'), Input('hist_indicator_dropdown', 'value'), Input('hist_bins_slider', 'value')) -
创建生成直方图的函数,使用刚刚创建的输入。我们首先检查是否既没有提供
year也没有提供indicator,如果是这种情况,我们会raise PreventUpdate:def display_histogram(years, indicator, nbins): if (not years) or (not indicator): raise PreventUpdate -
通过选择仅包含国家的数据创建一个子集
df,并获取年份在提供的years参数中的行:df = poverty[poverty['year'].isin(years) & poverty['is_country']] -
我们现在准备创建图形,通过调用
histogram函数来完成。如我们在本章中所看到的,我们将df提供给data_frame参数,将indicator作为x参数,将year传递给color。图形的标题将通过将指标与Histogram字符串连接来设置。nbins参数将接受用户从滑块中选择的nbins值。对于子图,我们使用year列。由于我们不知道用户将选择多少年份,并且不希望他们最终创建一个难以阅读的图表,因此我们设置facet_col_wrap=4。这将确保每行图表最多包含四个,接下来的图表将添加到下一行中:fig = px.histogram(df, color='year', + ' Histogram', facet_col='year', height=700) -
一个新的且有趣的选项是我们到目前为止没有涉及的
for_each_xaxis属性。注意,这是多个for_each_属性中的一个,你可以单独探索其他属性。这在* x 轴属性的数量未知的情况下非常有用,比如在这种情况,或者当存在多个属性时。默认情况下,每个子图(或子图)都会有自己独立的 x *轴标题。正如你所知,很多指标名称很长,在这种情况下会发生重叠。为了解决这个问题,我们将所有xaxis标题设置为空字符串:fig.for_each_xaxis(lambda axis: axis.update(title='')) -
为了替换已删除的x轴标题,我们可以创建一个注释。注释是一个简单的字符串,可以通过
add_annotation方法轻松添加。因为我们希望注释的X位置位于图形的中央,所以我们将其x值设置为0.5。另外,由于我们希望Y位置略低于绘图区域,因此将y值设置为-0.12。现在,重要的是告诉 Plotly 我们提供的这些数字的含义或其参考。我们可以使用xref和yref参数来表示这些值应该以paper为参考。这意味着将这些点视为图表的分数,而不是数据点,例如散点图中的数据点。这很有用,因为这些注释将作为轴标题,因此我们希望它们的位置是固定的。默认情况下,注释会有指向所选点的箭头。我们可以通过设置showarrow=False来移除它,如下所示:fig.add_annotation(text=indicator, y=-0.12, yref='paper',
以下是该函数的完整代码,以便更清楚地了解:
@app.callback(Output('indicator_year_histogram', 'figure'),
Input('hist_multi_year_selector', 'value'),
Input('hist_indicator_dropdown', 'value'),
Input('hist_bins_slider', 'value'))
def display_histogram(years, indicator, nbins):
if (not years) or (not indicator):
raise PreventUpdate
df = poverty[poverty['year'].isin(years) & poverty['is_country']]
fig = px.histogram(df,
color='year',
+ ' Histogram',
facet_col='year',
height=700)
fig.for_each_xaxis(lambda axis: axis.update(title=''))
fig.add_annotation(text=indicator,
y=-0.12,
yref='paper',
fig
通过这个,我们创建了一个独立的应用程序,可以在 JupyterLab 中运行。我鼓励你完全运行它,看看是否会遇到问题,并对它进行定制和修改。
到目前为止,我们已经探索了如何可视化单个观察集的计数和分布。还有一种有趣的方式可以同时探索两个观察集,这可以通过二维直方图来实现。
创建一个二维直方图
在第一个案例中,我们基本上是统计了数据集中每个区间的观察值。在这个例子中,我们将做同样的事情,但对于两个数据集的区间组合。每个变量的区间将最终形成一个矩阵。一个简单的例子可以让这一点变得清晰。让我们创建一个例子并看看:
-
创建一个包含仅有 2000 年数据的
poverty子集:df = poverty[poverty['year'].eq(2000) & poverty['is_country']] -
创建一个
Figure对象并添加一个histogram2d轨迹(在撰写时,这种图表类型在 Plotly Express 中不可用)。我们只需选择希望一起绘制的任意两个指标,并将它们传递给x和y:fig = go.Figure() fig.add_histogram2d(x=df['Income share held by fourth 20%'], y=df['GINI index (World Bank estimate)'], colorscale='cividis') -
添加x轴和y轴的标题:
fig.layout.xaxis.title = 'Income share held by fourth 20%' fig.layout.yaxis.title = 'GINI index (World Bank estimate)' fig.show()
运行上述代码会生成图 8.12中的图表:

图 8.12 – 2D 直方图
这里值的频率表达方式有所不同。在一维直方图中,条形的高度表示各个区间内的值的频率。在二维直方图中,“高度”则通过连续的颜色尺度来表示。我们可以从色标中看到,计数范围从 0 到 10,具有最多值的区间组合是x区间(22, 22.9)和y区间(30, 39.9),对应的z值(高度)为10。通常,z用于指代第三维度,因此这也可以视为这个矩形的高度。
请注意,这与使用散点图展示两个变量不同。在散点图中,我们关注的是两个变量之间的相关性,或者至少是它们各自的变化情况。而在这里,我们尝试识别两个变量之间最常见的观测值,及其所处的箱体组合。
仍然有很多选项可以用来探索直方图,或是可视化分布和计数。我们已经探讨了很多这样的选项,现在我们将转向探索 Dash 中的另一个交互式组件——数据表。
创建数据表
从技术上讲,dash_table是一个独立的包,正如本章开头所提到的,它可以单独安装。它会随 Dash 自动安装,确保使用的是正确且最新的版本,这是推荐的做法。
很多时候,展示表格,特别是如果表格是交互式的,可以为我们的仪表盘用户增加很多价值。此外,如果我们的仪表盘或数据可视化对用户而言不够充分,或者如果用户希望运行他们自己的分析,那么允许他们获取原始数据也许是一个好主意。最后,DataTable组件允许通过自定义颜色、字体、大小等进行数据可视化。因此,我们可以通过表格的方式进一步理解和展示数据。在本章中,我们将探索一些可用的选项,但肯定不会涵盖所有选项。
让我们看看如何在一个简单的应用中使用 DataFrame 创建一个简单的数据表:
-
创建一个包含自 2000 年以来的
贫困子集的应用,且该子集只包含具有国家名称或包含收入分布中前 10%和后 10%人口的列。我们使用filter方法配合正则表达式来实现这一点:df = poverty[poverty['year'].eq(2000)&poverty['is_country']].filter(regex='Country Name|Income share.*10') -
在 JupyterLab 中创建一个带有
layout属性的应用:app = JupyterDash(__name__, external_stylesheets=[dbc.themes.COSMO]) app.layout = html.Div([]) -
将一个
DataTable对象传递给刚创建的 div。最基本的要求是表格需要提供data参数和columns参数的值。实现这一点的一个简单方法是通过将 DataFrame 转换为字典,使用to_dict('records')方法。columns需要是一个字典列表,每个字典包含name和id键。name是用户看到的内容,id则是实际使用的值:DataTable(data=df.to_dict('records'), columns=[{'name': col, 'id': col} col in df.columns])
使用app.run_server()运行这个简单的应用,会产生一个表格,正如图 8.13所示,显示了前几行数据:

图 8.13 – 一个简单的数据表
很多时候,表格或列标题可能无法完美地适应其所在的容器。例如,在我们的案例中,许多指标名称非常长,而它们的列包含的数据数字并不占用太多水平空间。让我们探索一些可以处理这个问题的选项。
控制表格的外观和感觉(单元格宽度、高度、文本显示等)
有许多选项可用于修改表格的外观,始终建议查阅文档以获取想法和解决方案。潜在的棘手之处在于当您有组合选项时。在某些情况下,这些选项可能会相互修改,并且可能不会显示出您想要的样子。因此,在调试时,尽可能隔离选项总是一个好习惯。
在图 8.13中,我们仅显示了三列和前几行。现在我们将看到如何显示更多列并让用户探索更多行:
-
修改
df以包含所有包含Income share的列:df = poverty[poverty['year'].eq(2000)&poverty['is_country']].filter(regex='Country Name|Income share') -
将 DataTable 放在所需宽度为
7的dbc.Col组件中。表格会自动采用其所在容器的宽度,因此这将隐式设置其宽度:dbc.Col([], lg=7) -
现在我们想确定列标题的行为方式,特别是它们的名称相当长。这可以通过
style_header参数实现。请注意,对于标题、单元格和表格,都有几个style_参数,它们还有_conditional变体,例如,style_cell_conditional,用于有条件地设置单元格的样式。我们现在使用以下选项指定标题样式,以允许文本在需要时溢出到多行:style_header={'whiteSpace': 'normal'} -
现在我们希望在滚动时,标题保持固定不动:
fixed_rows={'headers': True} -
为了控制整个表格的高度,我们可以简单地使用以下参数:
style_table={'height': '400px'} -
在我们有数千行的情况下,可能会很重,影响页面的性能,因此我们可以使用
virtualization。在我们的情况下,这是一个非常小的表格,但我们可以设置virtualization来演示其用法:virtualization=True
将代码放在一起,这是生成表格的完整代码:
dbc.Col([
DataTable(data=df.to_dict('records'),
columns=[{'name': col, 'id': col}
col in df.columns],
style_header={'whiteSpace': 'normal'},
fixed_rows={'headers': True},
virtualization=True,
style_table={'height': '400px'})
], lg =7),
运行此修改后的代码会生成图 8.14中的表格:

图 8.14 – 具有宽度、高度、滚动和虚拟化自定义选项的 DataTable
只有在光标指向那里时,滚动条才可见。它被保留用于演示,并清楚地表明滚动已启用。现在用户可以通过尽可能多地滚动来查看所有可用的行。我们现在将看到如何在表格中加入一些交互性,并将其添加到我们的应用程序中。我们还将利用这个机会演示 DataTable 组件提供的一些其他选项。
将直方图和表格添加到应用程序中
现在我们准备将表格功能整合到我们的应用程序中,并将其添加到我们已经创建的回调函数中。我们将显示用于生成直方图的数据,就在直方图图下方。由于直方图不显示数据点(仅聚合),如果用户愿意,他们可能会对自己看到的数据感兴趣。
让我们立即添加这个功能:
-
在直方图图下方添加一个新的 div:
html.Div(id='table_histogram_output') -
将此作为
Output添加到回调函数中:@app.callback(Output('indicator_year_histogram', 'figure'), Output('table_histogram_output', 'children'), Input('hist_multi_year_selector', 'value'), Input('hist_indicator_dropdown', 'value'), Input('hist_bins_slider', 'value')) -
在完成
Figure对象的定义后,我们添加了 DataTable 的定义。我们将使用之前相同的选项,并添加一些新的选项。首先,我们添加了对列进行排序的功能:sort_action='native' -
现在,我们添加了对列进行筛选的功能。这将在每个列标题下方添加一个空框,用户可以输入文本并按Enter键获取筛选后的表格:
filter_action='native' -
添加导出表格为 CSV 格式的功能:
export_format='csv' -
我们为单元格设置了最小宽度,以保持一致性,并避免由于不同列标题导致的格式问题:
style_cell={'minWidth': '150px'} -
最后,将表格添加到函数末尾的
return语句中,这样它就会返回两个项而不是一个:return fig, table
由于添加了此功能,我们更新后的应用将包含用于生成直方图的表格,用户可以导出或与之互动。图 8.15 展示了添加了自定义 DataTable 的应用:

图 8.15 – 一个显示用于生成直方图的数据的 DataTable
我们现在有了一个导出按钮,用户一点击就会立即触发浏览器中的下载功能。标题名称现在有了可以让用户进行排序的箭头,支持升序或降序排序。你还可以看到筛选选项,并且有一个筛选数据...的占位符文本,用户可以按照此进行筛选。
现在,为了将这个功能集成到我们的应用中,我们只需复制组件并将其放置在想要显示的位置。由于这可以被视为探索性功能(用户仍然不会深入了解指标),所以最好将其放在地图图表下方。
为了添加交互性,我们只需要像往常一样,在应用布局后添加我们创建的回调函数。
我们已经做过很多次了,这对你来说应该很简单。
我们的应用现在变得非常丰富。顶部有两个主要的互动式探索图表。地图允许用户选择一个指标并查看其在不同国家的变化。用户还可以选择年份和/或让它像视频一样播放。所选的指标会触发关于该指标的描述性文本,以便为用户提供更多背景信息。在其下方,我们提供了选择一个或多个年份来查看该指标如何通过直方图分布的选项。用户可以修改柱数以获得最佳视图。这也会更新他们可以与之互动并导出的表格。
在浏览过感兴趣的指标后,用户可以继续使用我们创建的三种专业图表来探索特定指标。
恭喜!我们现在已经完成了第二部分的内容,应该回顾一下本章的内容,以及第二部分,为第三部分做好准备。
总结
在这一章中,我们首先了解了直方图与我们至今所讨论的其他类型图表之间的主要区别。我们看到了创建直方图的简便性,更重要的是,我们看到了它们在使用barmode、颜色、分箱和小面板等方面的高度自定义性。接着,我们探索了如何通过回调函数将直方图与其他组件连接起来,进而为直方图添加交互性。
我们接着探索了二维直方图,并看到了它如何提供两个列之间更加丰富的可视化对比。
我们介绍了一个新的互动组件,DataTable。我们仅仅触及了表格功能的表面。我们使用它们让用户更容易获取、与之交互或仅仅查看我们直方图背后的原始数据。我们还探索了控制表格外观和感觉的不同方式。
最后,我们将表格功能与我们创建的回调函数结合起来,并将互动性添加到我们的应用中。
现在让我们快速回顾一下到目前为止在本书中所学习的内容,并为第三部分做好准备。
到目前为止我们已经覆盖的内容
在本书的第一部分,我们涵盖了 Dash 应用的基础知识。我们首先探索了它们的结构以及如何管理视觉元素。接着,我们探讨了如何创建交互性,主要通过使用回调函数。这样我们就可以创建完全互动的应用。随后,我们学习了Figure对象的结构,并了解了如何修改和操作它以生成我们需要的图表。之后,我们明白了数据处理和准备对于数据可视化的重要性。我们对数据集进行了重塑,使其更加直观易用。这为学习和使用 Plotly Express 铺平了道路。
第二部分主要讲解了熟悉几种类型的图表以及互动组件。我们在第一部分中建立的所有知识都得到了应用,但最重要的是,我们是在一个实际的环境中进行的。我们逐渐将更多的图表、组件和功能添加到一个应用中。在每一步,我们都需要考虑这些变化将如何影响整个应用,并确保从整体的角度去实现。现在你已经非常熟悉如何更改多种功能。尽管我们没有涵盖每种图表和组件,但其通用原理是相似的,你可以轻松地将所学的知识应用到新的情况中。
第三部分将讨论关于应用程序、URL、高级回调和部署等更一般性的主题。但接下来的章节将探讨一些机器学习的选项。我们的数据集包含许多国家、年份和指标,可能的组合数量庞大。因此,我们将探索一些有助于发现数据中趋势或关联的技术。
第三部分:将您的应用提升到新高度
本节进一步探讨了有关微调、改进和扩展应用的选项。它向您展示了几种新的策略和技术,用于修改和将您的应用推向云端。
本节包含以下章节:
-
第九章**,让数据通过机器学习为自己发声
-
第十章**,通过高级回调加速您的应用
-
第十一章**,URL 和多页面应用
-
第十二章**,部署您的应用
-
第十三章**,下一步
第九章:第九章:让你的数据为自己发声,利用机器学习
在制作直方图时,我们已经看到了一种可视化聚合数据而不是直接可视化数据点的技术。换句话说,我们可视化了关于我们数据的数据。在本章中,我们将进一步拓展这个概念,使用机器学习技术演示一些可用于分类或聚类数据的选项。正如你在本章中所看到的,甚至使用单一技术时,也有许多选项和组合可以探索。这就是交互式仪表板价值所在。如果用户需要通过手动创建图表来探索每一个选项,那将是非常繁琐的。
本章不是机器学习的介绍,也不假设你有任何先前的知识。我们将探索一种叫做sklearn的聚类技术。这将帮助我们将数据分组为相似的观察集合,同时与其他集合中的观察有所不同。我们将用一个简单的一维数据集来构建模型,然后看看如何将其应用于聚类我们贫困数据集中的国家。
如果你熟悉机器学习,那么本章应该能为你提供一些思路,帮助你为用户提供更多的能力,允许他们调节和探索模型的多个方面。如果不熟悉,依然可以完成本章内容,并且希望它能激发你进一步探索更多机器学习的概念和技术。
本章将涵盖以下主题:
-
理解聚类
-
找到最佳的聚类数量
-
按人口对国家进行聚类
-
使用
scikit-learn准备数据 -
创建一个交互式 K 均值聚类应用
技术要求
我们将探索一些来自sklearn和NumPy的选项。此外,我们将继续使用之前所用的工具。为了实现可视化和交互功能,我们将使用 Dash、JupyterDash、Dash 核心组件库、Dash HTML 组件、Dash Bootstrap 组件、Plotly 和 Plotly Express。对于数据处理和准备,我们将使用pandas和NumPy。JupyterLab 将用于探索和构建独立功能。最后,sklearn将用于构建机器学习模型,并准备我们的数据。
本章的代码文件可以在 GitHub 上找到,网址为github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_09。
查看以下视频,了解代码的实际演示:bit.ly/3x8PAmt。
理解聚类
那么,什么是聚类,何时它可能会有帮助呢?让我们从一个非常简单的例子开始。假设你有一群人,我们想为他们制作 T 恤。我们可以为每个人制作一件 T 恤,按需求的尺寸制作。主要的限制是我们只能制作一种尺寸。尺寸如下:[1, 2, 3, 4, 5, 7, 9, 11]。想一想你会如何解决这个问题。我们将使用KMeans算法来解决这个问题,下面我们就开始吧:
-
导入所需的包和模型。
NumPy将作为包导入,但我们从sklearn只会导入当前将要使用的唯一模型,如下代码片段所示:import numpy as np from sklearn.cluster import KMeans -
创建一个所需格式的尺寸数据集。请注意,每个观察(个人的尺寸)应表示为一个列表,因此我们使用
NumPy数组的reshape方法来获取所需格式的数据,如下所示:sizes = np.array([1, 2, 3, 4, 5, 7, 9, 11]).reshape(-1, 1) sizes array([[ 1], [ 2], [ 3], [ 4], [ 5], [ 7], [ 9], [11]]) -
创建一个
KMeans模型实例,并指定所需的聚类数量。该模型的一个重要特性是,我们需要为其提供所需的聚类数量。在这个例子中,我们面临一个限制,就是只能制作一种尺寸的 T 恤,所以我们想要找到一个单一的点,它将是我们发现的聚类的中心。之后我们将探索所选聚类数量的效果。运行以下代码:kmeans1 = KMeans(n_clusters=1) -
使用
fit方法将模型拟合到数据。这意味着我们希望刚创建的模型根据这个特定的算法和提供的参数/选项“学习”数据集。这是你需要的代码:kmeans1.fit(sizes) KMeans(n_clusters=1)
现在我们有了一个在该数据集上训练好的模型,可以继续检查它的一些属性。按照惯例,拟合模型的结果属性会带有一个下划线,如我们接下来所看到的。我们现在可以询问我们请求的聚类。cluster_centers_属性给出了答案。聚类中心(在这个例子中是一个中心)基本上是我们数据点聚类的均值。让我们查看结果,如下所示:
kmeans1.cluster_centers_
array([[5.25]])
我们以列表形式接收到特征数据。显然,我们的聚类中心是5.25。你可能会认为这是一种复杂的方式来计算我们数据集的均值,你说得对。看看以下代码片段:
sizes.mean()
5.25
确实,我们的聚类中心恰好是我们数据集的均值,这正是我们所期望的。为了可视化这个结果,以下截图显示了聚类中心相对于数据点的位置:

图 9.1 – 尺寸数据点,KMeans 提供的聚类中心
前面截图中显示的图表非常简单——我们只是将尺寸绘制在X轴上,Y轴则是一个任意的常数值。
为了评估我们模型的表现以及它与数据的拟合程度,有几种方法可以做到这一点——一种方法是检查inertia_属性。这是我们创建的实例的一个属性,在将其拟合到数据后,可以使用点符号来访问,如下所示:
kmeans1.inertia_
85.5
inertia_度量是样本到其最近聚类中心的平方距离的总和。如果模型表现良好,样本到提供的聚类中心的距离应该尽可能短(数据点离聚类中心很近)。一个完美的模型的惯性率将是0。从另一个角度看,我们也知道,要求只有一个聚类会给我们最差的结果,因为它只是一个聚类,要成为平均点,它必须远离极端数据点。
因此,我们可以通过添加更多的聚类来提高模型的性能,因为它们与中心的距离会减少。
现在,假设我给你打电话并分享一些好消息。我们有额外的预算来增加一个新的尺寸,现在我们希望制作两种尺寸的 T 恤。用机器学习语言来说,这意味着我们需要创建一个包含两个聚类的新模型。我们重复相同的步骤并修改n_clusters参数,如下所示:
kmeans2 = KMeans(n_clusters=2)
kmeans2.fit(sizes)
kmeans2.cluster_centers_
array([[3.],
[9.]])
现在,我们有了两个新的聚类中心,如指定的那样。
仅仅知道聚类中心是不够的。对于每个点,我们需要知道它属于哪个聚类,或者我们想知道给我们组内每个人的 T 恤尺寸。我们还可以对它们进行计数,并检查每个聚类中的数据点数量。
labels_属性包含了这些信息,可以在这里看到:
kmeans2.labels_
array([0, 0, 0, 0, 0, 1, 1, 1], dtype=int32)
注意,标签是从0开始的整数给出的。还要注意,这些数字并不代表任何量化的意义。标签为零的点并不来自第一个聚类;同样,标签为 1 的点在某种意义上也不“比”其他点更多。这些只是标签,例如将它们称为A 组、B 组,依此类推。
我们可以通过使用zip函数将标签映射到它们各自的值,如下所示:
list(zip(sizes, kmeans2.labels_))
[(array([1]), 0),
(array([2]), 0),
(array([3]), 0),
(array([4]), 0),
(array([5]), 0),
(array([7]), 1),
(array([9]), 1),
(array([11]), 1)]
这将在稍后使用这些标签绘制图表时非常重要。
让我们也可视化这两个聚类中心,以便更好地理解这个过程。以下截图显示了聚类中心相对于其他数据点的位置:

图 9.2 – 尺寸数据点,KMeans 提供了两个聚类中心
这些聚类中心在视觉上是合理的。我们可以看到前五个点彼此接近,而最后三个点则彼此分离,远离前五个点,且间隙较大。将 3 和 9 作为聚类中心是合理的,因为每个聚类中心都是其所在聚类值的平均值。现在让我们通过检查惯性率来数值验证我们是否提高了模型的性能,如下所示:
kmeans2.inertia_
18.0
的确,性能得到了极大的提升,从 85.5 降至 18.0。这里没有什么令人惊讶的地方。正如你所预期的那样,每增加一个聚类,性能都会改善,直到我们达到惯性为0的完美结果。那么,我们该如何评估选择聚类数的最佳选项呢?
寻找最优的聚类数
我们现在将看到选择最优聚类数的可选项及其含义,但首先让我们看一下下面的截图,以可视化从一个聚类到八个聚类的进展:

图 9.3 – 所有可能聚类数的 数据点和聚类中心
我们可以看到所有可能聚类数的完整范围,以及它们与数据点的关系。在最后,当我们指定 8 时,得到了完美的解决方案,每个数据点都是一个聚类中心。
实际上,你可能不想选择完整的解决方案,主要有两个原因。首先,从成本的角度来看,这可能是不可行的。想象一下,如果要制作 1000 件 T 恤,而 T 恤有几百种尺寸。其次,在实际情况下,通常在达成某种适配后,继续增加聚类并不会带来太大的价值。以我们的 T 恤示例为例,假设有两个人的尺寸分别为 5.3 和 5.27,他们可能仍然会穿相同的尺寸。
所以,我们知道最优的聚类数介于 1 和我们拥有的唯一数据点数之间。接下来,我们想要探索如何确定这个最优数量的权衡和选择。我们可以采用的一个策略是检查新增(或增量)聚类的值。当增加一个新聚类时,它是否带来了惯性的显著下降(改善)?一种这样的技术叫做“肘部法”。我们将惯性值与聚类数进行绘制,看看曲线方向发生急剧变化的地方。现在让我们来做一下这个操作。
我们从 1 到 8 进行循环,对于每个数字,我们都经历相同的过程:实例化一个KMeans对象,并获取该聚类数的惯性值。然后,我们将该值添加到我们的inertia列表中,如下面的代码片段所示:
inertia = []
for i in range(1, 9):
kmeans = KMeans(i)
kmeans.fit(sizes)
inertia.append(kmeans.inertia_)
inertia
[85.5, 18.0, 10.5, 4.5, 2.5, 1.0, 0.5, 0.0]
如预期的那样,我们的惯性从 85.5 改善到了最后的零。
我们现在将这些值绘制出来,看看“肘部”位置在哪里,如下所示:
import plotly.graph_objects as go
fig = go.Figure()
fig.add_scatter(x=list(range(1, 9)), y=inertia)
fig.layout.xaxis.title = 'Number of clusters'
fig.layout.yaxis.title = 'Inertia'
fig.show()
运行前面的代码会生成如下截图所示的图表:

图 9.4 – “肘部”方法,显示所有可能聚类数的惯性值
你可以清楚地看到,当聚类数量从 1 变为 2 时,惯性突然下降,随着聚类数量趋向最终值,惯性继续下降,但下降的速度变慢。所以,三个或许四个聚类可能是我们开始获得递减回报的点,这可能是我们聚类的最佳数量。我们也稍微作弊了一下,包含了一个聚类,因为我们已经知道它将是最差的聚类数量。你可以在以下截图中看到没有第一个值的相同图形:

图 9.5 – “肘部”法则,展示了所有可能的聚类数量的惯性值(不包括 1)
这看起来完全不同,并且表明我们不能在不了解数据、使用场景以及可能存在的任何限制条件的情况下,机械地做出决策。
我们探索的这个示例在观察数量和维度数量上非常简单,只有一个维度。KMeans 聚类(以及一般的机器学习)通常处理多个维度,概念基本相同:我们尝试找到聚类的中心,使得它们与数据点之间的距离最小。例如,下面的截图展示了在二维空间中,类似问题的样子:

图 9.6 – 二维空间中的聚类点
这可能对应于与我们的人群相关的额外测量值。例如,我们可能将他们的身高放在 x 轴上,体重放在 y 轴上。你可以想象 KMeans 在这种情况下会给我们什么结果。当然,现实中数据很少如此整齐地聚集在一起。你也可以看到,如果选择错误的聚类数量,我们可能会损失多少准确性。例如,如果我们指定三个聚类,图中的三个中间块可能会被认为是一个单一的聚类,尽管我们可以看到它们之间有明显的差异,而且它们的点非常接近彼此。此外,如果我们指定七个或八个聚类,我们可能会在聚类之间得到不必要的划分,或者我们已经越过了“肘部”图中的肘部。
我们现在准备好在我们的数据集中使用这一聚类理解了。
按人口对国家进行聚类
我们将首先通过一个我们熟悉的指标(人口)来理解这个问题,然后使其具有互动性。我们将根据各国的人口对其进行聚类。
我们从一个可能的实际情境开始。假设你被要求按人口将国家分组。你需要将国家分成两组:人口高的和人口低的。你该怎么做呢?你会在哪些地方画分界线,人口总数多少才算是“高”?假设你被要求将国家根据人口分成三组或四组。那么你将如何更新你的聚类呢?
我们可以轻松看到,KMeans聚类非常适合这种情况。
现在,我们用一个维度进行KMeans聚类,并将其与我们的地图绘制知识结合,操作如下:
-
导入
pandas并打开poverty数据集,如下所示:import pandas as pd poverty = pd.read_csv('data/poverty.csv') -
创建年份和所需指标的变量,如下所示:
year = 2018 indicators = ['Population, total'] -
实例化一个
KMeans对象,指定所需的簇数,如下所示:kmeans = KMeans(n_clusters=2) -
创建一个
df对象,这是包含所选年份的国家和数据的poverty数据框。运行以下代码来实现这一点:df = poverty[poverty['year'].eq(year) & poverty['is_country']] -
创建一个
data对象,这是我们选择的列的列表(在这种情况下,我们只选择了一列)。请注意,下面的代码片段中我们获取了它的values属性,这会返回底层的NumPy数组:data = df[indicators].values -
将模型拟合到数据,如下所示:
kmeans.fit(data)
我们现在已经在数据上训练了模型,并准备好可视化结果。记得我们在第七章中讨论过,探索地图绘图并通过 Markdown 丰富你的仪表盘,为了创建一张地图,我们只需要一个包含国家名称(或代码)的列的 DataFrame?这就足够生成一张地图。如果我们想要为国家上色,我们需要另一列(或任何类似列表的对象),其中包含相应的值。
我们刚刚训练的kmeans对象包含了各个国家的标签,并告诉我们哪个国家属于哪个簇。我们将利用这个信息来给国家上色,所以我们通过一次函数调用来完成这一操作。请注意,我们可以将标签转换为字符串,这会导致 Plotly Express 将它们视为分类变量,而非连续变量。代码如下所示:
px.choropleth(df,
locations='Country Name',
locationmode='country names',
color=[str(x) for x in kmeans.labels_])
这段代码生成了如下截图所示的图表:

图 9.7 – 按人口聚类的国家
由于我们已经开发了地图选项的模板,我们可以复制这个模板并用它来增强此地图,使其与我们应用程序的主题保持一致。让我们使用它,并查看在同一张地图上显示1、2、3和4个簇的效果,并讨论细节。下方截图显示了四张地图,每张地图的簇数不同:

图 9.8 – 按人口聚类的国家,使用不同数量的簇
重要提示
如果你正在查看灰度版本,这些地图上的颜色可能不容易区分,我建议你查看在线版本和代码库。
如你所见,将地图着色为一个簇(所有国家使用同一标签)会生成一个单一颜色的地图。当涉及到两个簇时,事情就变得有趣了,这也符合直觉。组成具有较高人口的簇的两个国家(即中国和印度)有着非常庞大的人口,而且彼此非常接近——分别为 13.9 亿和 13.5 亿。第三个国家,美国(US),人口为 3.27 亿。这正是 KMeans 应该做的事情。它将我们分成了两个国家群体,在每个簇中的国家彼此非常接近,而与其他簇的国家则相距较远。当然,我们通过选择两个簇数引入了一个重要的偏差,并且我们看到了这种选择可能并不是最优的情况。
当我们选择三个簇时,可以看到我们有一个中等人口的簇,其中美国是其中之一。然后,当我们选择四个簇时,你会看到俄罗斯和日本被移到了第三类,尽管它们在三个簇时属于第二类。
现在我们已经有足够的代码和知识来将其提升到一个新的层次。我们希望为用户提供选择簇的数量和他们想要的指标的选项。我们需要首先解决数据中的一些问题,所以让我们来探讨一下。
使用 scikit-learn 准备数据
scikit-learn是 Python 中最广泛使用且最全面的机器学习库之一。它与数据科学生态系统中的其他库(如NumPy、pandas和matplotlib)兼容得很好。我们将使用它来对我们的数据进行建模和预处理。
现在我们有两个问题需要首先解决:缺失值和数据缩放。让我们分别看两个简单示例,然后在我们的数据集中解决这些问题。首先从缺失值开始。
处理缺失值
模型需要数据,它们无法处理包含缺失值的一组数字。在这种情况下(我们的数据集中有很多类似情况),我们需要决定如何处理这些缺失值。
有多种选择,正确的选择取决于应用场景以及数据的性质,但我们不会深入讨论这些细节。为了简化,我们将做出一个通用选择,用合适的值替换缺失数据。
让我们通过一个简单的示例来探索如何填补缺失值,如下所示:
-
创建一个包含缺失值的简单数据集,格式合适,如以下代码片段所示:
data = np.array([1, 2, 1, 2, np.nan]).reshape(-1, 1) -
导入
scikit-learn中的SimpleImputer,如下所示:from sklearn.impute import SimpleImputer -
使用
mean策略创建此类的实例,这是默认策略。正如你可能猜到的,除了这个策略,还有其他策略用于填补缺失值。以下代码片段展示了这一点:imp = SimpleImputer(strategy='mean') -
将模型拟合到数据。这是模型根据我们在实例化时设置的条件和选项来学习数据的地方。代码如以下片段所示:
imp.fit(data) -
转换数据。现在,模型已经学习了数据,它能够根据我们设置的规则对数据进行转换。
transform方法在许多模型中都有,其含义取决于上下文。在此情况下,转换是指使用mean策略填充缺失数据。代码见下列代码片段:imp.transform(data) array([[1\. ], [2\. ], [1\. ], [2\. ], [1.5]])
如您所见,模型已通过将缺失值替换为 1.5 来转换数据。如果您查看其他非缺失值 [1, 2, 1, 2],您会发现它们的均值为 1.5,这正是我们得到的结果。我们本可以指定不同的缺失值填充策略,如中位数或最频繁值策略。每种策略都有其优缺点;我们这里只是探索在 Dash 中通过机器学习可以做什么。
接下来,我们将开始对数据进行标准化处理。
使用 scikit-learn 对数据进行标准化
在 图 9.6 中,我们看到了二维数据聚类的效果。如果我们想根据两个指标对贫困数据进行聚类,一个指标会在 x 轴上,另一个则在 y 轴上。现在,假设我们在一个轴上有人口数据,另一个轴上有百分比指标。人口轴上的数据范围是 0 到 14 亿,而另一个轴上的数据范围是 0 到 1(或 0 到 100)。百分比指标的任何差异对距离的影响可以忽略不计,均值主要通过人口数量的不成比例大小来计算。解决这个问题的一种方法是对数据进行标准化。
有不同的策略来缩放数据,我们将探索其中一种——即标准化缩放。StandardScaler 类为数据点分配 z 分数(或标准分数)并对其进行标准化。z 分数的计算方式是将每个值减去均值,然后除以标准差。虽然有其他计算方法,但我们将专注于一个简单的示例来更好地说明这一概念,如下所示:
-
创建一个简单的数据集,如下所示:
data = np.array([1, 2, 3, 4, 5]).reshape(-1, 1) -
导入
StandardScaler并创建其实例,如下所示:from sklearn.preprocessing import StandardScaler scaler = StandardScaler() -
将
scaler拟合到数据并进行转换。为了方便,许多具有fit和transform方法的模型,也有一个fit_transform方法,我们将使用这个方法,如下所示:scaler.fit_transform(data) array([[-1.41421356], [-0.70710678], [ 0. ], [ 0.70710678], [ 1.41421356]])
现在,我们已将数据转换为其相应的 z 分数。请注意,均值 3 现在变成了 0。大于 3 的数值为正,小于 3 的为负。这些数值还表示了对应数值与均值的距离(高或低)。
这样,当我们的数据集包含多个特征时,我们可以对它们进行标准化,进行比较,并一起使用。最终,我们关心的是某个值的极端程度以及它与均值的接近度。一个基尼指数为 90 的国家是一个极端案例。这就像一个人口达到 10 亿的国家。如果我们将这两者结合使用,10 亿的人口将主导并扭曲计算。标准化帮助我们以更好的方式处理不同尺度的数据。它仍然不是完美的,但比使用不同尺度的数据要好得多。现在,我们可以在聚类数据时使用多个特征。
创建一个互动式 KMeans 聚类应用程序
现在,让我们将所有内容整合起来,使用我们的数据集创建一个互动式聚类应用程序。我们将让用户选择年份,以及他们想要的指标。用户还可以选择聚类数,并根据发现的聚类以有色分区地图的形式获得聚类的可视化表示。
请注意,使用多个指标时,解读这些结果是具有挑战性的,因为我们将处理多个维度。如果你不是经济学家,并且不清楚哪些指标应该与其他哪些指标进行比较,那么理解这些结果也会很困难。
以下截图展示了我们将要实现的效果:

图 9.9 – 一个互动式 KMeans 聚类应用程序
正如你所看到的,这是一个非常丰富的应用程序,提供了多种选项组合。正如我之前提到的,这并不是一个直接易懂的过程,但正如本章多次提到的,我们只是在探索如何仅使用一种技术和其部分选项来实现目标。
本书中我们已经创建了许多滑块和下拉框,所以我们不会再讲解如何创建它们。我们只需要确保它们具有描述性的 ID,我将留给你填充空白。正如前面的截图所示,我们有两个滑块,一个下拉框和一个图形组件,因此我们需要为它们设置 ID 名称。像往常一样,以下组件应该放置在 app.layout 中你想要的位置:
dcc.Slider(id='year_cluster_slider', …),
dcc.Slider(id='ncluster_cluster_slider', …),
dcc.Dropdown(id='cluster_indicator_dropdown', …),
dcc.Graph(id='clustered_map_chart', …)
接下来,我们将逐步介绍如何创建回调函数,如下所示:
-
在回调函数中关联输入和输出,如下所示:
@app.callback(Output('clustered_map_chart', 'figure'), Input('year_cluster_slider', 'value'), Input('ncluster_cluster_slider', 'value'), Input('cluster_indicator_dropdown', 'value')) -
创建具有合适参数名称的函数签名,如下所示:
def clustered_map(year, n_clusters, indicators): -
实例化一个缺失值填充器、一个标准化缩放器和一个
KMeans对象。请注意,使用SimpleImputer时,我们还指定了缺失值的编码方式。在本例中,缺失值编码为np.nan,但在其他情况下,可能会使用不同的编码方式,比如N/A、0、-1或其他。代码如下所示:imp = SimpleImputer(missing_values=np.nan, strategy='mean') scaler = StandardScaler() kmeans = KMeans(n_clusters=n_clusters) -
创建
df,它是poverty的一个子集,仅包含国家数据和所选年份的数据,然后选择year和Country Name列,以及所选指标。代码如下所示:df = poverty[poverty['is_country'] & poverty['year'].eq(year)][indicators + ['Country Name', 'year']] -
创建
data,它是df的一个子集,仅包含所选指标的列。我们有两个不同的对象是因为df将用于绘制地图,并且还会使用年份和国家名称。同时,data仅包含数值,以便我们的模型能够处理它。代码如下所示:data = df[indicators] -
在某些情况下,正如我们在书中多次看到的,我们可能会遇到某一列完全为空的情况。在这种情况下,我们无法填充任何缺失值,因为没有均值,并且我们完全不知道该如何处理它。在这种情况下,我认为最好的做法是不要生成图表,并告知用户,对于所选的选项组合,数据不足以运行模型。我们首先检查是否存在这种情况。DataFrame 对象有一个
isna方法。当我们运行它时,它会返回一个填充了True和False值的 DataFrame,表示相应的值是否缺失。然后,我们对结果 DataFrame 运行all方法。这将告诉我们每列是否所有值都缺失。现在,我们有一个包含True和False值的 pandas Series。我们通过使用any方法检查其中是否有True。在这种情况下,我们创建一个空的图表,并附上说明性标题,如下所示:if df.isna().all().any(): return px.scatter(title='No available data for the selected combination of year/indicators.') -
如果一切正常,且我们没有一个列的所有值都缺失,我们继续创建一个没有缺失值的变量(如果有缺失值,则进行填充),如下所示:
data_no_na = imp.fit_transform(data) -
接下来,我们使用
StandardScaler实例对data_no_na进行标准化,如下所示:scaled_data = scaler.fit_transform(data_no_na) -
然后,我们将模型拟合到我们的标准化数据,如下所示:
kmeans.fit(scaled_data) -
我们现在拥有生成图表所需的一切——最重要的是
labels_属性——并且我们可以通过一次调用px.choropleth来生成图表。正如你在下面的代码片段中看到的,我们在此函数中使用的选项没有任何新内容:fig = px.choropleth(df, locations='Country Name', locationmode='country names', color=[str(x) for x in kmeans.labels_], labels={'color': 'Cluster'}, hover_data=indicators, height=650, title=f'Country clusters - {year}. Number of clusters: {n_clusters}<br>Inertia: {kmeans.inertia_:,.2f}')
然后,我们复制已经用于自定义地图的地理属性,并使其与应用程序整体保持一致。
这是完整的函数,包括地理选项,供你参考:
@app.callback(Output('clustered_map_chart', 'figure'),
Input('year_cluster_slider', 'value'),
Input('ncluster_cluster_slider', 'value'),
Input('cluster_indicator_dropdown', 'value'))
def clustered_map(year, n_clusters, indicators):
imp = SimpleImputer(missing_values=np.nan, strategy='mean')
scaler = StandardScaler()
kmeans = KMeans(n_clusters=n_clusters)
df = poverty[poverty['is_country'] & poverty['year'].eq(year)][indicators + ['Country Name', 'year']]
data = df[indicators]
if df.isna().all().any():
return px.scatter(title='No available data for the selected combination of year/indicators.')
data_no_na = imp.fit_transform(data)
scaled_data = scaler.fit_transform(data_no_na)
kmeans.fit(scaled_data)
fig = px.choropleth(df,
locations='Country Name',
locationmode='country names',
color=[str(x) for x in kmeans.labels_],
labels={'color': 'Cluster'},
hover_data=indicators,
height=650,
title=f'Country clusters - {year}. Number of clusters: {n_clusters}<br>Inertia: {kmeans.inertia_:,.2f}',
color_discrete_sequence=px.colors.qualitative.T10)
fig.layout.geo.showframe = False
fig.layout.geo.showcountries = True
fig.layout.geo.projection.type = 'natural earth'
fig.layout.geo.lataxis.range = [-53, 76]
fig.layout.geo.lonaxis.range = [-137, 168]
fig.layout.geo.landcolor = 'white'
fig.layout.geo.bgcolor = '#E5ECF6'
fig.layout.paper_bgcolor = '#E5ECF6'
fig.layout.geo.countrycolor = 'gray'
fig.layout.geo.coastlinecolor = 'gray'
return fig
在这一章中,我们在可视化和交互探索方面取得了很大的进展。我们还简要介绍了一种机器学习技术,用于对我们的数据进行聚类。理想情况下,提供给用户的选项将取决于你所从事的学科。你可能自己是处理领域的专家,或者你可能与这样的专家密切合作。这不仅仅是可视化和统计的问题,领域知识也是分析数据的关键方面,尤其在机器学习中,这一点至关重要。
我鼓励你进一步学习,看看你能取得什么成就。正如我们所看到的,掌握创建互动式仪表板的技能对运行机器学习模型是一个巨大的优势,它让你能够以更快的速度发现趋势并做出决策。最终,你将能够创建自动化解决方案,提供建议或基于特定输入做出决策。
现在,让我们回顾一下本章所学的内容。
总结
我们首先了解了聚类是如何工作的。我们为一个小型数据集构建了最简单的模型。我们多次运行模型,并评估了每次选择不同聚类数量时的性能和结果。
接着,我们探索了肘部法则来评估不同的聚类,并看到了如何发现收益递减点,在这个点之后,增加新的聚类不会带来显著的改进。通过这些知识,我们使用相同的技术根据一个大多数人都熟悉的指标对国家进行聚类,并亲身体验了它如何在真实数据上运作。
之后,我们设计了一个互动式 KMeans 应用,并探索了在运行模型之前准备数据的两种技术。我们主要探讨了填补缺失值和数据标准化。
这为我们提供了足够的知识,将数据整理成适合创建互动应用的格式,正如我们在本章末尾所做的那样。
接下来,我们探讨了 Dash 回调的高级功能——最显著的是模式匹配回调。到目前为止,我们运行的回调都是直接且固定的。许多时候,我们希望为用户创建更动态的界面。例如,基于在下拉框中选择的某个值,我们可能希望显示一种特殊类型的图表或创建另一个下拉框。我们将在下一章探讨这种方式的工作原理。
第十章:第十章:通过高级回调为应用程序加速
我们现在通过引入回调的新选项,将我们的应用程序提升到一个新的抽象层次和强大功能。我们遵循的基本模式是为用户提供一个可以交互的组件。基于该组件提供的选项集,用户可以影响某些动作,例如生成图表。我们将探索其他选项,例如推迟回调的执行,直到某个事件发生,例如点击“提交”按钮。我们还将看看如何允许用户修改应用程序的布局,允许他们向应用程序添加新的动态组件。我们将利用这些知识对在第九章中引入的聚类功能进行一些小但重要的改进,让数据为自己发声,使用机器学习。
我们将首先介绍回调中的可选State参数。到目前为止,我们的所有回调都在用户对任何输入进行更改时立即触发。在许多情况下,我们希望用户设置一些选项,然后点击“提交”按钮才会触发回调函数。当我们有多个输入项时,如果输出在用户仍在更改时就发生变化,这可能会令人烦恼或不方便。在其他情况下,这些回调可能需要较长时间执行和/或需要消耗大量资源。因此,在这种情况下,我们希望在用户决定触发回调之前阻止其执行。
一旦State的概念被建立,我们将探索一种新的动态回调类型,允许用户通过例如添加新图表来更改应用程序。到目前为止,我们允许用户仅仅修改交互组件中的输入值。通过引入基于用户交互生成的动态组件,我们可以将这一功能提升到新的层次。
最后,我们将概述模式匹配回调,它允许我们以简化的方式将动态创建和交互式组件连接起来。
以下是本章将要涵盖的主要内容:
-
理解
State -
创建控制其他组件的组件
-
允许用户向应用程序添加动态组件
-
引入模式匹配回调
技术要求
我们将使用到与前几章相同的基本工具,主要集中在回调函数中的一些新特性。我们将使用 Dash、Dash HTML 组件、Dash 核心组件和 Dash Bootstrap 组件来构建我们的应用程序。数据操作方面,我们将使用 pandas。图表和可视化方面,我们将使用 Plotly 和 Plotly Express,最后,我们将使用 JupyterLab 进行交互式探索,并独立创建新功能,最后将其整合到应用程序中。
本章的代码文件可以在 GitHub 上找到,链接地址是:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_10。
请查看以下视频,观看代码的实际运行:bit.ly/3v6ZYJw。
让我们从熟悉State开始。
理解 State
到目前为止,我们使用的典型回调函数结构包含一个或多个Output元素和一个或多个Input元素。如引言中所述,当用户修改Input元素时,回调会立即触发。我们希望稍微放宽这个选项。我们将从一个简单的示例开始,展示为什么以及如何使用State,这是一个可选参数,可以传递给我们的回调函数。
为了更清楚地说明我们试图解决的问题,请看一下图 10.1:

图 10.1 – 一个交互式应用,其输出与输入值未正确同步
如你所见,输出显示的是错误的值。原因是为了模拟你在应用程序中可能遇到的实际情况,我们故意引入了等待时间,这使得应用变得非常慢。实际上,输出并不是错误的;只是更新需要较长时间,因此当输入发生变化时,输出区域没有立即反映变化。在这种情况下尤为重要的是,因为有两个输入项控制输出。修改第一个和第二个输出之间的间隔可能导致这种混淆。
另一个更重要的问题是,这些选项可能会消耗更多的时间,并在计算能力和/或分析人员时间的损失上花费大量成本。我们的数据集非常小,所进行的计算也很简单,因此性能并不是问题。在实际情况中,你更可能处理更大的数据集,并进行需要大量时间的计算。例如,修改集群的数量以更新我们的模型,所需时间非常短。实际上,这可能需要几秒钟、几分钟,甚至更长时间。我们将通过添加一个“提交”按钮并在回调函数中引入State来解决这个问题。
按钮是 Dash HTML 组件包中的 HTML 组件。它们也可以通过 Dash Bootstrap 组件使用。使用后者有两个优点。首先,它们与所使用的主题很好地集成,确保了视觉上的一致性。其次,这一点可能更为重要的是,它们很容易显示出一种有意义的颜色,以便传达“成功”、“警告”、“危险”或其他任何可用的颜色/消息。
按钮可以通过dcc.Button或dbc.Button来实现。让我们看看它们如何控制应用程序的行为。
我们首先需要澄清回调函数中Input和State的区别。
理解Input和State之间的区别
首先,请记住,Input是触发函数的因素,而State只是应用程序所处的条件集。我们需要决定哪些组件作为State,哪些作为Input。
让我们更新回调函数的指导方针,以澄清刚刚引入的区别:
-
回调函数的参数顺序必须始终是
Output、Input和可选的State,按照这个顺序。如果我们有多个相同类型的元素,它们必须依次排列。 -
State是可选的。 -
Input元素是触发回调执行的因素。改变应用中的任何或所有State并不会导致回调的执行。 -
一旦
Input元素被修改,回调将会触发,并且自上次触发以来,所有变化的State也会被传入回调。
现在让我们看看如何生成显示在图 10.1中的应用程序代码,然后根据所需的行为进行修改。当前的回调函数看起来是这样的:
@app.callback(Output('output', 'children'),
Input('dropdown', 'value'),
Input('textarea', 'value'))
def display_values(dropdown_val, textarea_val):
return f'You chose "{dropdown_val}" from the dropdown, and wrote "{textarea_val}" in the textarea.'
这就是我们至今管理所有回调函数的方式。请注意,回调会在两个输入中的任何一个被修改时运行。我们要引入的更改将需要两步操作:
-
向页面添加一个按钮组件,放置在
Textarea组件下方:import dash_bootstrap_components as dbc dbc.Button("Submit", id="button") # OR import dash_html_components as html html.Button("Submit", id="button") -
将按钮作为
Input参数添加到回调函数中。现在我们引入了一个我们还没有见过的新属性,即n_clicks。顾名思义,它对应于用户会话期间在某个组件上点击的次数。每次点击,数字就会增加一次,我们可以使用这个变量来检查和控制回调的行为。注意,我们也可以给它一个默认的起始值,通常为零,但如果我们愿意,也可以设置为其他数字:Input("button", "n_clicks") -
现在,我们已经将按钮设定为
Input,我们希望保留Dropdown和Textarea,但将它们作为State参数,如下所示:@app.callback(Output('output', 'children'), Input('button', 'n_clicks'), State('dropdown', 'value'), State('textarea', 'value')) -
通过这些更改,回调现在等待
Input的变化。用户可以多次更改Dropdown和/或Textarea,而不会被中断,等他们准备好时,可以点击“提交”按钮来获得所需的结果。 -
当应用程序首次加载时,
n_clicks属性的默认值为None。此外,Textarea组件中没有任何内容,且下拉菜单尚未选择任何选项。所以,我们按常规操作:如果没有n_clicks的值,我们就使用raise PreventUpdate。为了更新我们所引入的函数,我们可以简单地对函数的签名进行如下更改。注意新增了相应的n_clicks参数,以及它的相对顺序:def display_values(n_clicks, dropdown_val, textarea_val): if not n_clicks: raise PreventUpdate return …
如果我们更新代码,可以看到事情会按预期工作,用户将对流程有更多的控制权。图 10.2 显示了更新后的功能:

图 10.2 – 一个互动应用,输出现在正确显示预期的输入值
我们可以通过提供视觉提示来进一步改善此体验,提示用户某些处理正在进行中。
牢记这些新知识后,我们可以利用它来修改我们在本章开头讨论的聚类功能的行为。图 10.3 显示了我们希望实现的预期结果:

图 10.3 – 带有提交按钮和视觉进度指示器的聚类功能
如你所见,我们引入了两个新特性。第一个是我们已经讨论并在独立小应用中实现的按钮。第二个是 Dash 核心组件 Loading 组件。该组件负责显示一个符号,这个符号在预期输出出现的地方移动或有时旋转。使用它非常简单,但在这种情况下非常关键。向用户确认他们的选择(或任何其他交互)已被识别并正在处理中总是件好事。事实上,我认为在所有输出中使用 Loading 组件给用户这种确认是非常好的。它非常容易实现,以下是如何更新应用以反映这一功能:
import dash_core_components as dcc
dcc.Loading([
dcc.Graph(id='clustered_map_chart')
])
Graph 组件已经存在于我们的应用中;我们只需要将它作为 children 参数添加到 Loading 组件中,正如你在之前的代码中看到的。这将导致动画符号保持动画状态,直到底层对象出现在其位置。
现在,让我们修改回调函数以进行所需的更改:
@app.callback(Output('clustered_map_chart', 'figure'),
Input('clustering_submit_button', 'n_clicks'),
State('year_cluster_slider', 'value'),
State('ncluster_cluster_slider', 'value'),
State('cluster_indicator_dropdown', 'value'))
def clustered_map(n_clicks, year, n_clusters, indicators):
我们基本上做了两项更改。首先,我们引入了 clustering_submit_button 作为 Input 元素,并将其他每个参数从 Input 重命名为 State。另一个更改是将 n_clicks 作为第一个参数传递给函数签名。请记住,参数的名称可以是任何名称,重要的是它们的顺序。我们为它们起了清晰的名称,以便我们在函数体内轻松引用和管理它们。
你现在已经修改了聚类功能,给用户提供了更多的控制权,并通过 Loading 组件使其更加直观清晰。你可以随意在应用的任何地方添加它。
现在我们可以将回调函数提升到另一个有趣的层次。
创建控制其他组件的组件
如何在页面上提供一个交互组件,其值(由用户设置)作为另一个函数的输入,后者负责最终的输出?图 10.4 展示了结果的样子,随后是关于细节和实现的讨论:

图 10.4 – 一个包含动态决定另一个组件值的应用
让我们逐一了解这个应用布局中的视觉元素:
-
成功消息:绿色条带在应用加载时不会出现。它仅在用户将选项添加到下拉菜单并点击设置选项按钮后才会显示。请注意,有一个动态消息显示用户所添加的值。另外,请注意,警告消息是“可关闭”的。右侧有x符号,允许用户移除此消息。
-
Textarea组件,用于传递给其下方Dropdown组件的options属性。在此时,Dropdown为空,且没有可以选择的选项,Graph组件也显示一个空的图表。 -
当点击
Textarea并点击此按钮时,这些行将成为Dropdown中的选项。 -
Dropdown组件。之后,选择特定的国家代码会通过获取与用户选择相等的国家代码行来过滤数据集。然后,它使用结果的 DataFrame 创建最终的图表。
这样的应用没有太大的实际价值,因为直接提供 Dropdown 组件中的选项并生成图表会更简单。我们只是用它来展示我们可以利用的新选项,并且使用我们熟悉的数据集。同时也存在很大的错误潜力。假如用户不知道某个国家的代码呢?如果他们输入错误怎么办?再次强调,这仅仅是为了演示目的。
这个应用大约需要 30 行代码,它为我们提供了两层选项,其中一层依赖于另一层。一个选项集“等待”并依赖于其他选项,以相应地生成其输出。
现在我们编写应用的布局代码,然后创建两个使其具有交互性的回调函数:
-
运行必要的导入:
from jupyter_dash import JupyterDash import dash_core_components as dcc import dash_html_components as html import dash_bootstrap_components as dbc import pandas as pd poverty = pd.read_csv('data/poverty.csv') -
创建应用及其布局。以下所有元素都将放入应用的布局中:
app = JupyterDash(__name__, external_stylesheets=[dbc.themes.COSMO]) app.layout = html.Div([ component_1, component_2 … ]) -
创建一个空的 div,用于容纳成功消息:
html.Div(id='feedback') -
创建一个
Label组件,告诉用户如何与应用交互:dbc.Label("Create your own dropdown, add options one per line:") -
创建一个空的
Textarea组件。请注意,它也可用,并且与 Dash Core Components 中的同名组件类似:dbc.Textarea(id='text', cols=40, rows=5) -
为生成下拉菜单及其选项添加一个按钮:
dbc.Button("Set options", id='button') -
创建一个空的
Dropdown:dcc.Dropdown(id='dropdown') -
创建一个空的
Graph组件:dcc.Graph(id=chart')
这应该足以满足我们应用的视觉元素。接下来,我们需要两个函数来创建所需的交互性:
-
set_dropdown_options:这个函数将从Textarea中获取行作为输入,并返回一个选项列表供Dropdown组件使用。 -
create_population_chart:这个函数从Dropdown组件获取输入,并在其下方生成一个人口图表。
现在我们从第一个开始:
-
创建具有适当
Output、Input和State参数的回调函数。这个函数会影响两个输出,第一个是Dropdown组件的options属性,第二个是包含成功消息的 div。对于我们的Input,我们会有一个按钮,而State将是Textarea组件:@app.callback(Output('dropdown', 'options'), Output('feedback', 'children'), Input('button', 'n_clicks'), State('text', 'value')) -
创建带有适当参数名称的函数签名:
def set_dropdown_options(n_clicks, options): -
创建一个变量,保存作为列表提供的文本。我们通过分割
Textarea中传入的文本来实现这一点。我们还确保检查没有点击的情况,并在这种情况下使用raise PreventUpdate:if not n_clicks: raise PreventUpdate text = options.split() -
创建成功消息作为一个
Alert组件,这是 Dash Bootstrap Components 中的一部分。注意,我们还给它上了一个叫做“success”的颜色。自然,你还可以考虑额外的功能来检查有效的输入,如果没有有效输入,消息的颜色可能会是“warning”或“danger”。注意,文本还会动态地添加用户提供的逗号分隔选项。我们还设置了dismissable=True,允许用户在需要时将其从页面移除:message = dbc.Alert(f"Success! you added the options: {', '.join(text)}", color='success', dismissable=True) -
创建选项列表,用来设置当前空的
Dropdown组件的options属性。我们使用text变量来实现:options = [{'label': t, 'value': t} for t in text] -
返回
options和message的元组:return options, message
现在让我们转到另一个函数,它将获取所选的国家代码,并利用它生成图表:
-
创建具有所需
Output和Input的回调函数:@app.callback(Output('chart', 'figure'), Input('dropdown', 'value')) -
创建函数签名以及检查
Dropdown中值是否可用:def create_population_chart(country_code): if not country_code: raise PreventUpdate -
根据输入值创建所需的 DataFrame 子集:
df = poverty[poverty['Country Code']==country_code] -
返回一个包含适当值的图表:
return px.line(df, x='year', y='Population, total', title=f"Population of {country_code}")
现在我们可以运行前面的代码并创建所需的应用程序。
为了更好地理解应用程序的结构,并熟悉如何检查回调链,我们可以通过运行app.run_server(debug=True)在调试模式下运行应用程序,并查看输入和输出如何相互关联,如图 10.5所示:

图 10.5 – 应用回调图
你可以轻松地看到代码中指定的组件名称及其 ID。你可以很容易地跟踪事件的顺序,从图表的左下角开始,沿着箭头走到右上角。
我们看到如何在某些组件中创建动态选项,这些选项依赖于其他组件的值。Dash 整洁地处理了组件的行为,并在输入可用时正确地触发了相应的函数。
让我们将事情提升到一个更抽象和强大的层次。现在,让我们允许用户通过点击按钮添加完整的组件。
允许用户向应用程序添加动态组件
用户不仅能够向应用程序的布局中添加组件,而且这些组件的内容也将动态生成。看看图 10.6,它展示了我们将要开始的最简单示例:

图 10.6 – 一个允许用户向应用程序布局中添加组件的应用程序
尽管这个应用程序非常简单,但其中的图表有不同的动态名称,如你在图表标题中看到的。这是基于n_clicks的动态值,它在每次点击时都会变化。
生成这个所需的代码量与任何简单应用程序类似;其中没有太多复杂性。我们只需要以新的视角来看待它。让我们从编写布局代码开始,布局将由两个简单的组件组成:
-
创建一个按钮,用于触发添加新图表:
dbc.Button("Add Chart", id='button') -
创建一个空的 div,并将其
children属性设置为空列表。这个空列表是我们将要操作的关键元素:html.Div(id='output', children=[])
当此应用程序第一次加载时,用户只会看到一个按钮,他们可以使用这个按钮来添加一个图表。每次点击按钮时,按钮下方的区域将填充一个新的图表。
现在让我们为这个应用程序创建回调函数:
-
像往常一样创建
Output、Input和State参数。这里需要注意的有趣部分是,空 div 的children属性既充当Output又充当State。我们通常会获取某个组件的值,并用它来影响或改变应用程序中另一个组件的状态。谁说我们不能获取一个组件,改变它,然后将其以新状态返回原处呢?这正是我们将要做的:@app.callback(Output('output', 'children'), Input('button', 'n_clicks'), State('output', 'children')) -
创建函数签名并检查
n_clicks。请注意,在这种情况下,children充当State:def add_new_chart(n_clicks, children): if not n_clicks: raise PreventUpdate -
创建一个带有动态标题的空条形图,使用
n_clicks属性:new_chart = dcc.Graph(figure=px.bar(title=f"Chart {n_clicks}")) -
将新图表附加到
children组件中。如果你记得,我们将空 div 中children的初始值设置为空列表。接下来的这一行将取出这个列表,并将new_chart添加到其中。这没有什么特别的;我们只是简单地使用 Python 的list.append方法:children.append(new_chart) -
现在我们的
children列表已经通过向其中添加新项而被修改,我们只需返回它。请记住,回调函数的返回值将传递到 div 中,因此它现在充当输出:return children
请注意,这个功能是通过应用简单原则创建的。我们并没有使用任何新功能。第一个技巧是将children传递给我们的回调,并从另一端接收它。第二个技巧是使用n_clicks属性来动态设置图表的标题。
图 10.7 中的图示显示了我们创建的元素之间的关系:

图 10.7 – 一个回调函数图,其中一个函数返回它接收到并变更的组件
这个图表在应用中添加多少个图表并不会改变。这意味着你无需担心管理与点击次数相同的回调函数。
如果你准备好进一步扩展,我们可以在每个图表下方再添加一个组件,例如一个Dropdown组件。我们可以让下拉框选择的值生成图表。每个下拉框的值将独立于其他值(如果是用户添加的),并且只会修改它所属的图表。好消息是,所有这些也将通过一个额外的回调函数,利用模式匹配回调来管理。
引入模式匹配回调
掌握这个功能(我们现在正在处理一个真正的新功能)将使你能够将应用提升到一个新的交互性和功能性水平。这个功能的最重要特点是,它允许我们处理那些以前不存在的组件的交互性。正如我们之前所做的,当我们允许用户通过点击按钮创建新的图表时,这些组件在应用中之前并不存在。更有趣的是,处理这些组件的回调函数与任何其他从下拉框获取值并生成图表的回调函数一样简单。诀窍在于稍微改变我们组件的id属性。
到目前为止,我们一直将id属性设置为字符串,唯一的要求是它们必须是唯一的。现在我们将介绍一种新的创建该属性的方法,即使用字典。让我们首先看看最终目标,然后修改布局、回调函数,最后讨论新的id属性处理方法。图 10.8展示了我们的应用将会是什么样子:

图 10.8 – 一个允许用户向应用布局中添加交互式组件的应用
在之前的应用程序中,我们能够让用户实时生成新组件,并且这些组件的内容也可以是动态的。我们通过图表标题演示了这一点,使用n_clicks属性动态设置标题。但是,在添加这些图表后,用户无法与它们进行互动。换句话说,它们是动态生成的,可能具有动态内容,但一旦生成,它们就变成了静态的,我们无法与它们互动。
我们现在引入的改进是,我们使得这些图表具有交互性(使用下拉框),并将每个图表与一个单独的组件(在此情况下为下拉框)关联。如图 10.8所示,每个图表都有自己的下拉框,用户可以在图表上独立生成多个图表并进行对比。之后,用户还可以通过选择不同的国家,查看不同图表的数据。当然,你可以想象一个更复杂的应用集合,用户可以在其中进行更多操作。
创建这个新功能的新增内容将分为三个步骤:
-
修改
add_new_chart函数:这将只是简单地在每个图表下添加一个下拉框,并附加两个组件,而不是一个。请注意,布局完全相同。我们只是在它下面有一个按钮和一个空的 div。 -
创建一个新的回调函数:这将把新生成的图表和下拉框对连接起来,以确定它们的行为。
-
修改应用中的
id属性:这是我们引入新功能的地方,也是主要功能,它允许我们通过一个回调函数管理多个附加组件及其交互性。
我们首先开始修改add_new_chart回调函数:
-
我们在函数中定义了
new_chart,这一部分保持不变。在它下面,我们要添加new_dropdown,以供用户选择他们想要可视化的国家:new_chart = dcc.Graph(figure=px.bar(title=f"Chart {n_clicks}")) countries = poverty[poverty['is_country']]['Country Name'].drop_duplicates().sort_values() new_dropdown = dcc.Dropdown(options=[{'label': c, 'value': c} for c in countries]) -
添加新组件。在第一个示例中,我们附加了
new_chart,但这次我们想要附加两个项目。唯一需要修改的地方是将这两个新组件放入一个新的 div 中,并附加这个新 div。这样,我们实际上是在附加一个包含两个元素的 div 元素:children.append(html.Div([ new_chart, new_dropdown ]))
这足以让按钮每次点击时添加两个项目。如你所见,变动非常简单。然而,稍后我们会设置这些组件的id属性,以使它们能够动态交互。
现在每次点击按钮时,我们都添加一对组件。其中一个必须是Output元素(图表),另一个必须是Input元素(下拉框)。与其他交互功能一样,它们需要通过回调函数进行关联。我们现在就创建这个回调函数,之后我们会看看如何将这些动态 ID 连接在一起,并管理这两个回调函数的交互。这个函数和我们之前创建的任何回调一样简单。下面是代码,但没有装饰器,我们稍后会讨论装饰器:
def create_population_chart(country):
if not country:
raise PreventUpdate
df = poverty[poverty['Country Name']==country]
fig = px.line(df,
x='year', y='Population, total',
title=f'Population of {country}')
return fig
图 10.9包含一个展示我们模式匹配回调函数的图示:

图 10.9 – 模式匹配回调的回调图
图 10.9中的顶部图表与图 10.7中的完全相同。它用于简单的功能,通过将新的图表附加到空 div 的子元素中来添加图表。请注意,id属性位于每个表示组件的框上方。这里它们是button和output。
第二个回调函数create_population_chart的图表显示了类似的结构,但其中的 ID 是字典类型,而不是字符串。
模式匹配回调使用这些字典来将不同的元素匹配在一起。让我们来拆解这些字典,然后看看它们如何与回调函数配合。
第一个是{"index": MATCH, "type": "dropdown"}。我相信type键是明确的。我们使用它来方便识别其他“类型”为“dropdown”的组件。需要注意的是,这些名称可以是任何内容,但显然我们会希望使用有意义且有帮助的名称。另一个字典的type键是chart。同样,这也是灵活的,但我认为在这里我们清楚地知道是指哪些元素。
现在,我们希望为每对组件提供独立的功能。换句话说,我们希望用户能够修改第二个下拉框,生成他们想要的任何图表,而不影响应用中的其他组件。我们怎么实现这一点呢?我们只需要告诉 Dash 使用MATCH来进行匹配。这个功能属于一个任意命名的键,MATCH是一个通配符对象,位于dash.dependencies模块中。还有ALL和ALLSMALLER,它们的工作方式相似,但有些细微差别,我们将主要关注MATCH。现在让我们来看一下,如何指定更新后的函数来适应这些 ID。好消息是,我们只需要修改相关组件的id属性,并将其传递给相应的回调函数参数。
现在,我们准备好添加适当的id属性,以完成模式匹配回调。第一个函数add_new_chart接受新字典id属性,用于表示允许用户添加到应用中的内部组件。请注意,这里的“index”键的值是n_clicks。正如我们之前多次看到的,它是动态的,每次用户点击按钮时都会发生变化。这意味着每次用户点击按钮时,我们都会得到一个新的唯一 ID,用来标识这个组件:
def add_new_chart(n_clicks, children):
new_chart = dcc.Graph(id={'type': 'chart',
'index': n_clicks},
figure=px.bar())
new_dropdown = dcc.Dropdown(id={'type': 'dropdown',
'index': n_clicks},
options=[option_1, option_2, …])
现在,我们需要在负责管理这些组件交互性的第二个函数中正确地映射这些 ID。"type"键将用于将"chart"映射到"chart"和将"dropdown"映射到"dropdown"。至于n_clicks,由于它是动态的,我们使用MATCH进行匹配:
@app.callback(Output({'type': 'chart', 'index': MATCH}, 'figure'),
Input({'type': 'dropdown', 'index': MATCH}, 'value'))
def create_population_chart(country):
…
这里是两者函数的完整代码,作为参考,帮助你全面了解:
from dash.dependencies import Output, Input, State, MATCH
@app.callback(Output('output', 'children'),
Input('button', 'n_clicks'),
State('output', 'children'))
def add_new_chart(n_clicks, children):
if not n_clicks:
raise PreventUpdate
new_chart = dcc.Graph(id={'type': 'chart',
'index': n_clicks},
figure=px.bar(title=f"Chart {n_clicks}"))
countries = poverty[poverty['is_country']]['Country Name'].drop_duplicates().sort_values()
new_dropdown = dcc.Dropdown(id={'type': 'dropdown',
'index': n_clicks},
options=[{'label': c, 'value': c}
for c in countries])
children.append(html.Div([
new_chart, new_dropdown
]))
return children
@app.callback(Output({'type': 'chart',
'index': MATCH}, 'figure'),
Input({'type': 'dropdown',
'index': MATCH}, 'value'))
def create_population_chart(country):
if not country:
raise PreventUpdate
df = poverty[poverty['Country Name']==country]
fig = px.line(df,
x='year',
y='Population, total',
title=f'Population of {country}')
return fig
你可以很容易想象,拥有这样的功能后,我们的应用将变得多么灵活和可扩展,更不用提回调管理的便利性了。然而,这些功能并不直接,可能需要一些时间去适应,我相信这是值得的。
我们介绍了许多关于回调的新概念,利用了一些技巧,并引入了新的功能。那么,接下来让我们回顾一下这一章中讲解的内容。
总结
我们首先介绍了回调函数装饰器中的可选State参数。我们展示了如何将其与Input结合使用,从而延迟执行函数,直到用户决定执行它们。我们还运行了几个示例,添加了按钮来触发执行。然后,我们创建了一个简单的应用,其中用户对某个组件的输入用于动态填充另一个“等待”组件的选项。这些新选项反过来又被用于创建另一个组件。
另一个有趣的应用是允许用户添加具有动态内容的新组件。
我们最终介绍了最强大和最灵活的功能——模式匹配回调。我们创建了一个应用,用户可以添加任意多的图表,而且这些图表相互独立,用户可以定制自己的仪表盘。
这涵盖了很多内容,接下来我们将讨论另一个功能,它允许我们扩展和扩展我们的应用。页面上的组件数量是有限的,超过一定数量就会变得杂乱无章。在许多情况下,为了实现不同的功能,创建单独的页面/URL 是有意义的,这也是下一章的主题。
第十一章:第十一章:URL 和多页面应用
到目前为止,我们一直在一个页面上构建所有内容。我们不断将新的图表和交互组件添加到一个 div 中,并根据需要将其整合。添加新的 URL 有助于节省空间,以免在一个页面上放置过多的组件。URL 还可以作为分类内容和提供上下文的工具,让用户知道他们“在哪里”,以及他们在做什么。
更有趣的是,能够通过编程生成应用程序的许多附加页面,只需根据 URL(或其任何部分)显示内容。这正是我们在本章中要做的。
一旦我们了解了 Location 和 Link 组件的工作原理,我们将对应用程序的结构进行一些小的改动,创建并隔离新的布局。这样,我们就能清楚地看到,制作一个多页面应用是多么简单。我们将有一个通用布局,中间有一个空白区域,使用一个简单的规则,内容将根据 URL 显示。
我们构建的所有功能主要基于指标。我们为这些指标创建了许多图表,展示了它们在时间和各国之间的变化。我们的用户也可能对面向特定国家的报告感兴趣。因此,我们将为每个国家创建一个页面,用户可以查看感兴趣国家的任何指标,并可以选择与其他国家进行比较。通过一些简单的更改,我们将为应用程序添加 169 个新页面。
本章将涵盖以下主题:
-
了解
Location和Link组件 -
提取和使用 URL 的属性
-
解析 URL 并使用其组件来修改应用程序的部分内容
-
重新构建应用程序以适应多个布局
-
将动态生成的 URL 添加到应用程序中
-
将新的 URL 交互功能融入到应用程序中
技术要求
对于我们将引入的新组件,我们仍然会使用相同的工具。Dash、Dash Core Components、Dash HTML Components 和 Dash Bootstrap Components 将用于构建我们的新功能并将其添加到我们的应用程序中。我们还将使用 pandas 进行数据处理,使用 JupyterLab 和 jupyter_dash 来尝试隔离功能,并使用 Plotly 和 Plotly Express 进行数据可视化。
本章的主要内容将是操作 URL 的各个部分,并将其作为输入来修改其他组件。这与使用我们应用程序的任何其他元素来创建我们所需的功能是一样的。让我们从了解这两个使其成为可能的组件开始。
本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/tree/master/chapter_11。
请查看以下视频,查看代码的实际应用:bit.ly/3eks3GI。
了解Location和Link组件:
这些组件是 Dash Core Components 的一部分,它们的名称已经非常清晰地表明了它们的功能。Location组件指的是浏览器的位置栏,也称为地址栏或 URL 栏。我们通常在应用中放置一个Location组件,它本身不会产生任何可见的内容。我们主要用它来发现我们在应用中的位置,基于这个信息,我们可以引入一些功能。让我们创建一个简单的示例,看看它如何在最简单的形式下使用:
-
创建一个简单的应用:
import dash_html_components as html import dash_core_components as dcc from jupyter_dash import JupyterDash from dash.dependencies import Output, Input app = JupyterDash(__name__) -
创建一个简单的布局,其中包含一个
Location组件,并在其下方放置一个空的div:app.layout = html.Div([ dcc.Location(id='location'), html.Div(id='output') ]) -
创建一个回调函数,获取
Location组件的href属性,并将其打印到空的div中:@app.callback(Output('output', 'children'), Input('location', 'href')) def display_href(href): return f"You are at: {href}." -
运行应用并观察其输出:
app.run_server(mode='inline') You are at: http://127.0.0.1:8050/.
这非常直接明了。我们只需请求Location组件告诉我们当前位置,并将其显示在空的div中。在这个例子中,我们请求了href属性,获得了当前页面的完整 URL。出于各种原因,我们可能对其他属性感兴趣,以实现更细致的功能。让我们再构建一个简单的应用,提取Location组件的其他可用属性,并了解如何使用Link组件。
了解Link组件:
顾名思义,这个组件用于生成链接。另一种创建链接的方式是使用 HTML 的<a>标签,这个标签在 Dash HTML Components 包中可用。虽然<a>标签更适合外部链接,但Link组件更适合内部链接。一个很好的优点是它仅仅改变pathname属性,而且是在不刷新页面的情况下进行的。所以它快速且响应迅速,就像在其他交互式组件中更改值一样。
在我们更新的简单应用中,我们将添加一个<a>链接,以及几个Link组件链接,这样你就可以体验刷新时的不同效果,并了解两种链接的不同。我们的Location组件现在将获取当前页面的位置,同时我们还将用它来提取所有可用的属性,并讨论它们可能的使用方式。让我们创建一些元素来构建我们应用的布局:
-
添加一个
Location组件:dcc.Location(id='location') -
通过 Dash HTML Components 添加一个
<a>组件,指向一个使用相对路径的内部页面:html.A(href='/path', children='Go to a directory path'), -
添加一个指向带有查询参数(search attribute)的页面的
Link组件:dcc.Link(href='/path/search?one=1&two=2', children='Go to search page') -
添加另一个指向带有哈希值的页面的
Link组件,也叫做片段:dcc.Link(href='path/?hello=HELLO#hash_string', children='Go to a page with a hash') -
添加一个空的
div来显示输出:html.Div(id='output')
这与之前的应用类似,正如你所看到的;我们只添加了几个链接。它们将作为普通的链接显示在页面上。接下来,我们将提取并显示我们感兴趣的部分,使用一个新的回调函数:
-
创建函数的装饰器,为
Location组件的每个属性添加一个单独的Input元素。正如你将看到的,位置是完整的 URL,但根据我们指定的内容,每个部分都会自动提取:@app.callback(Output('output', 'children'), Input('location', 'pathname'), Input('location', 'search'), Input('location', 'href'), Input('location', 'hash')) -
创建回调函数的签名,将每个
Location组件的属性作为Input元素传递。请注意,我们在hash参数后加上下划线,以明确表示我们没有使用同名的内置 Python 函数:def show_url_parts(pathname, search, href, hash_) -
在空 div 中返回我们所在的 URL 的不同属性:
return html.Div([ f"href: {href}", f"path: {pathname}", f"search: {search}", f"hash: {hash_}" ])
运行前面的代码并点击我们创建的链接,可以在 div 中显示不同的 URL 属性,如图 11.1所示:

图 11.1 – Location 组件显示不同 URL 的各个部分
如你所见,我们使用 Link 组件来更改 URL,并使用 Location 提取我们想要的属性。你可以很容易地看到,这在其他类型的回调函数中如何使用,这些回调函数不仅仅是显示这些属性。让我们看看如何使用 Python 的 parse_qs(解析查询字符串)函数来解析和提取查询参数及其值:
from urllib.parse import parse_qs
parse_qs('1=one&2=two&20=twenty')
{'1': ['one'], '2': ['two'], '20': ['twenty']}
现在,我们可以随意处理这些值。一个更实际的应用,以我们的数据集为例,是你可以创建特定的可共享 URL,用户可以通过提供 URL 来共享特定的图表及其选项:
parse_qs('country_code=CAN&year=2020&inidcator=SI.DST.02ND.20')
{'country_code': ['CAN'], 'year': ['2020'], 'inidcator': ['SI.DST.02ND.20']}
一旦你得到国家名称、年份和指标,你可以轻松地将其作为输入传递给回调函数,并根据这些选择生成所需的图表。你还可以想象,简化用户操作,使得他们在交互式组件中的选择会改变 Location 组件,从而轻松分享这些 URL。正如已经提到的,最棒的是,这个过程在不刷新页面的情况下工作,因此不会拖慢流程。
让我们看看这个在实际应用中如何工作,使用我们的数据集。
解析 URL 并使用其组件来修改应用的部分内容
在建立了关于 Location 和 Link 组件如何工作的基础知识后,我们希望在应用中使用它。计划是通过三个回调函数添加 169 个新页面,并添加一些新的布局元素。用户将有一个可以选择的国家下拉菜单。选择一个国家后,URL 会发生变化,从而渲染该国家的布局。该布局将包括一个标题、一个图表和一个关于该国家的表格。
图 11.2 展示了我们将构建的一个国家页面的示例:

图 11.2 – 一个展示包括其他国家的图表的示例国家页面
如您所见,我们现在有了一个国家页面的模板。之所以触发此模板,是因为 URL 中包含了数据集中可用的某个国家。如果没有,它将显示包含我们迄今为止创建的所有组件的主应用程序。用户还可以点击 Home 链接返回主页。
让我们先看看如何重新构建我们的应用。
为了适应多种布局,重构您的应用
在此阶段,我们还没有脱离我们在第一章中讨论的基本结构,Dash 生态系统概述,并且作为提醒,图 11.3 展示了当前结构的简化表示:

图 11.3 – Dash 应用的结构
除了布局部分之外,其他都将保持不变。目前,我们只有一个布局属性,所有内容都被添加到了主 div 中。在某些情况下,我们使用了标签页来高效地利用空间,并且从 Dash Bootstrap 组件中使用了 Row 和 Col 组件,以灵活地管理组件的显示方式。为了创建新的布局结构,我们需要创建一个主布局,它将作为我们应用的骨架。在这个布局中,我们将有一个空 div,根据当前的 URL,适当的内容将被填充进来。图 11.4 展示了这个骨架的可能样子。这里只是为了让大家更容易可视化;它永远不会显示一个这样的空白页面。请注意,我们还添加了一个导航栏,可以在其中添加多个其他元素,这可以被视为我们应用的另一项新增功能:

图 11.4 – 应用的新骨架布局,显示一个空的主内容区域
提示
如您在带有空主体的骨架页面上看到的,您可以使用 Dash 进行非常快速的原型设计和应用设计。在开始编码之前,您可以快速构建您想要的布局,与其他利益相关者分享,获取反馈,然后再开始编写交互性代码。
就像我们在同一图示中所做的,您还可以隔离或删除某些元素,以便让您的受众更容易理解您正在构建的应用结构。
现在让我们编写代码,升级我们的应用,添加国家 URL 和导航栏。首先我们创建几个独立的布局:
-
NavbarSimple组件,并在接下来的章节中详细讨论其细节。我们的布局还将包含脚部,其中有我们之前创建的标签页。这个布局的主体,就像本章开头的简单应用一样,将包含一个Location组件,并且还有一个空 div 用于显示所需的布局:main_layout = html.Div([ dbc.NavbarSimple([ … ]), dcc.Location(id='location'), html.Div(id='main_content'), dbc.Tabs([ … ]) ]) -
当满足正确条件时(如果 URL 不包含国家名称),
main_contentdiv 将被填充:indicators_dashboard = html.Div([ # all components we built so far ]) -
国家仪表盘:这也将被保存为一个变量,并在满足条件时显示,即当 URL 中有国家名称时。此布局的内容(图表和表格)将针对 URL 中的国家:
country_dashboard = html.Div([ html.H1(id='country_heading'), dcc.Graph(id='country_page_graph'), dcc.Dropdown(id='country_page_indicator_dropdown'), dcc.Dropdown(id='country_page_contry_dropdown'), html.Div(id='country_table') ]) -
validation_layout属性为我们解决了这个问题。这也是一个很好的、简单的方式,帮助我们了解并管理应用程序的整体布局。对于更复杂的应用程序,有更多的布局时,这会很有用。你可以把它当作应用程序的目录:app.validation_layout = html.Div([ main_layout, indicators_dashboard, country_dashboard, ])
我们仍然需要指定应用程序的 layout 属性,这样 Dash 才知道使用哪个作为默认布局。这非常简单:
app.layout = main_layout
现在让我们看看如何管理将在 main_layout 部分显示的内容。
基于 URL 显示内容
一旦我们的布局按照之前的方式设置好,我们需要一个非常简单的函数来管理内容。
该函数检查 Location 组件的路径名属性是否是可用国家之一。如果是,它将返回 country_dashboard 布局。否则,它将返回 indicators_layout。注意,第二个条件包括除可用国家之外的任何内容。由于我们在这个应用中没有其他功能,为了捕捉任何 URL 错误,最好将其他所有内容引导到主页。不过,在更复杂的应用中,创建错误页面可能更为合适。
我们需要注意两个简单的要点。首先,pathname 属性返回包含 "/<country_name>" 的路径,因此我们需要提取除了第一个字符之外的所有内容。其次,当我们更改 URL 时,如果它包含特殊字符,如空格,它们会被自动 URL 编码。可以使用 unquote 函数轻松处理:
from urllib.parse import unquote
unquote('Bosnia%20and%20Herzegovina')
'Bosnia and Herzegovina'
选择带有空格的国家(如本例所示)时,会将空格转换为其 URL 编码的等效形式 %20,因此我们需要使用 unquote 来处理名称,使其能够作为普通文本处理。
以下是创建 countries 列表的代码,包含所有可用的国家,以及根据 URL 管理内容显示的简单回调:
countries = countries = poverty[poverty['is_country']]['Country Name'].drop_duplicates().sort_values().tolist()
@app.callback(Output('main_content', 'children'),
Input('location', 'pathname'))
def display_content(pathname):
if unquote(pathname[1:]) in countries:
return country_dashboard
else:
return indicators_dashboard
我们刚刚概述了应用程序的新布局和结构的高层次描述,现在我们需要填补一些空白,并讨论如何创建导航栏、下拉菜单及其链接的细节。
向应用程序中添加动态生成的 URL
我们现在想要通过添加一个导航栏、主页链接以及一个国家下拉菜单来完成我们的主要布局。为此,我们引入了 Dash Bootstrap 组件中的 NavbarSimple 组件,来看我们如何使用它。
NavbarSimple 组件将包含一些元素,以创建我们想要的结构,具体如下:
-
我们首先创建导航栏,并为其提供
brand和brand_href参数,以指示名称以及它将链接到的位置:import dash_bootstrap_components as dbc dbc.NavbarSimple([ … ], brand="Home", brand_href="/") -
对于它的
children参数,我们将添加一个dbc.DropdownMenu组件。我们还将为它提供一个label值,以便用户点击菜单时知道会发生什么。在下一步中,我们将填充它的children参数:dbc.DropdownMenu(children=[ menu_item_1, menu_item_2, … ], label="Select country") -
我们现在需要为下拉菜单提供一系列
dbc.DropdownMenuItem组件。每个项目将获得children和href参数。两个参数都会对应我们在上一节创建的国家列表中的一个国家:dbc.DropdownMenu([ dbc.DropdownMenuItem(country, href=country) for country in countries ])
将完整的NavbarSimple组件代码整理在一起,你可以在这里看到它:
dbc.NavbarSimple([
dbc.DropdownMenu([
dbc.DropdownMenuItem(country, href=country)
for country in countries
], label='Select country')
], brand='Home',brand_href='/')
到此为止,我们已经实现了导航栏,我们还可以将 Tabs 组件添加到相同的布局中,这是我们在 第一章 中实现的,Dash 生态系统概述。现在,修改或添加任何导航元素变得非常简单,无论何时你想要,它都会对整个网站生效。
请注意,导航栏中的子项包含与Link组件相同功能的链接,因此在需要时,我们也可以使用此选项。
随着整个应用程序的完整布局已经准备好,并且根据 URL 加载了正确的布局,我们现在准备实现最后两个回调函数,它们将为我们生成 country_dashboard 布局。
但情况是这样的。我,你以前的同事,编写了代码并创建了功能。我在没有任何解释的情况下离开了公司,你无法与我取得联系。你需要自己弄清楚结构,并且你想对我的代码做一些修改。
这是你可能会遇到的典型情况,接下来我们看看如何解决它。
将新的 URL 交互性整合到应用程序中
创建了一个下拉菜单,自动根据所选的值更改 URL,我们已经允许用户根据自己的需求在页面间切换。现在,我们需要根据选定的国家/地区来管理正确内容的显示。
由于代码已经编写完成,你可以做的事情之一是以调试模式运行应用程序,并获取所有可用组件的可视化表示,查看它们如何通过回调函数连接。
图 11.5 显示了已经创建的回调函数图。让我们用它来理解这个功能是如何实现的:

图 11.5 – 管理 URL 功能的各种组件和回调函数
让我们从左到右浏览这张图,看看这里发生了什么。你可以参考 图 11.2 来查看这张图如何与应用程序中的可见组件对应:
-
一切从组件的 pathname 属性开始,该组件的 ID 为 location。这可以通过用户选择下拉菜单中的一个国家、直接输入包含国家名称的完整 URL,或点击网页中的链接来触发。
-
URL(它的 pathname 属性)会影响三个组件,如你所见。最重要的是,它决定了哪些内容会显示在
country_dashboard布局中,使用 display_content 回调来显示。这将展示某些组件,使得另外两个回调变得相关。 -
假设已选择一个国家,我们的第二个回调 set_dropdown_values 将使用该国家的值来填充下拉框,以便在此页面上选择要绘制的国家。
-
<h1>组件显示"<country name> 贫困数据"。dbc.Table组件展示关于该国家的各种详细信息,这些信息从包含数据的国家 CSV 文件中提取,该文件包含数据集中的每个国家。
之后,你从代码库中获取代码,查看这是如何实现的,你会看到两个回调,你想要审查并最终修改它们:
@app.callback(Output('country_page_contry_dropdown', 'value'),
Input('location', 'pathname'))
def set_dropdown_values(pathname):
if unquote(pathname[1:]) in countries:
country = unquote(pathname[1:])
return [country]
@app.callback(Output('country_heading', 'children'),
Output('country_page_graph', 'figure'),
Output('country_table', 'children'),
Input('location', 'pathname'),
Input('country_page_contry_dropdown', 'value'),
Input('country_page_indicator_dropdown', 'value'))
def plot_country_charts(pathname, countries, indicator):
if (not countries) or (not indicator):
raise PreventUpdate
if unquote(pathname[1:]) in countries:
country = unquote(pathname[1:]) df = poverty[poverty['is_country'] & poverty['Country Name'].isin(countries)]
fig = px.line(df,
x='year',
y=indicator,
title='<b>' + indicator + '</b><br>' + ', '.join(countries),
color='Country Name')
fig.layout.paper_bgcolor = '#E5ECF6'
table = country_df[country_df['Short Name'] == countries[0]].T.reset_index()
if table.shape[1] == 2:
table.columns = [countries[0] + ' Info', '']
table = dbc.Table.from_dataframe(table)
else:
table = html.Div()
return country + ' Poverty Data', fig, table
到现在为止,你应该能轻松地弄懂如何阅读新的代码,即使是包含你从未见过的组件的代码。应用程序的一般结构已经讲解和修改了很多次,涉及了很多组件,我相信你现在能自己搞清楚这些内容了。
这是书中的最后一个编码练习,也是我们将要添加的最后一个功能。这并不意味着应用已经完成。相反,还有许多可以修改和添加的地方。你可能考虑做的一件事是为每个指标实现一个特殊的 URL,并为其设置一个专门的页面,就像我们对国家做的那样。不同之处在于,这可能不像国家那样直接。我们有些指标是百分比,而另一些则是简单的数字。一些指标应该作为一个组一起考虑,比如显示不同收入分位数的那些。你可以在修改和添加时发挥创意,尝试不同的方式。
现在,让我们回顾一下这一章中涵盖的内容。
总结
我们首先了解了两个主要的组件,它们负责修改、读取和解析 URL,即 Location 和 Link 组件。我们创建了两个简单的应用,看到如何提取我们感兴趣的 URL 部分,并尝试了几种可以使用它们的方式。
我们接着学习了如何通过解析的 URL 获取值并修改应用的部分内容。有了这些知识,我们能够重新构建我们的应用。我们创建了一个骨架布局,并使用一个空的 div,在其中根据 URL 显示右侧的内容。
然后,我们将新功能整合进了我们的应用。接着,我们进行了一项最终的练习,这个练习你可以预见在实际工作中会遇到,那就是同事交给你一些代码,要求你自己搞明白并进行修改。
既然我们已经探索了许多选项、布局、组件和功能,下一步自然就是将我们的应用部署到公共服务器上,这样我们就能与世界分享它了。
这将是下一章的主题。
第十二章:第十二章:部署你的应用
我们已经做了很多工作,相信你也期待将这些工作与世界分享。应用当前的状态下,我们将通过设置服务器并将应用部署到公共地址的过程。
本质上,我们所做的是将数据和代码移到另一台计算机,并以类似于我们目前所做的方式运行应用程序。然而,我们需要设置一个托管账户、一个服务器以及Web 服务器网关接口(WSGI),以便我们的应用能够公开并可见。我们还需要为开发、部署和更新周期建立一个基本的工作流。
我们将简要介绍Git源代码管理系统,并进行一些基本的Linux 系统管理。我们将涵盖足够的内容,帮助我们的应用上线,但我们甚至不会触及这些系统的表面——我提到它们只是作为进一步研究的参考。我们所采用的方法是创建一个非常基础的安装,以尽可能快地将我们的应用上线。这不会通过简单的工具实现。相反——我们将使用一些最强大的工具,但我们会使用一个非常简单的设置。这样可以让我们在开始时保持简单,之后再探索如何扩展我们的应用和设置。
本章将涵盖以下主题:
-
建立一般的开发、部署和更新工作流
-
创建托管账户和虚拟服务器
-
使用安全外壳(SSH)连接到您的服务器
-
在服务器上运行应用
-
使用 WSGI 设置和运行应用
-
设置和配置 Web 服务器
-
管理维护和更新
技术要求
我们现在需要一台连接互联网的 Linux 服务器、数据文件和我们应用的代码。我们将安装Gunicorn(Green Unicorn,WSGI)和nginx(Web 服务器),以及我们应用的 Python 依赖包。我们将安装Dash及其主要包;Dash Bootstrap Components;pandas;和sklearn。我们需要一个源代码管理系统的账户,例如 Git,本章将以 GitHub 为例。
到目前为止,我们的开发工作流是在 JupyterLab 上测试某些功能并运行,一旦它工作正常,我们就将其集成到应用中。这个开发工作流不会改变。我们只是会在做出更改后,添加一些部署步骤和组件。那么,让我们首先建立我们将使用的工作流。
建立一般的开发、部署和更新工作流
当我们讨论部署时,我们假设我们对目前所开发的内容已经足够满意。这可能是在我们首次运行应用程序时,或者在进行了一些更改或修复了一些错误之后。所以,我们的数据和代码已经准备好了。我们的重点将是设置所需的基础设施,以便我们能够在网上运行代码。
我们将要进行的设置将是简单而直接的。我们将以 Linode 作为我们的托管服务商示例。Linode 的一个重要特点是它遵循“开放云”的理念。这意味着我们将使用的服务器将是一个普通的 Linux 服务器,使用开源组件和包,你可以根据自己的需求进行定制,并轻松地进行迁移。这里的潜在挑战是,随着更多自由度的增加,复杂性和责任也随之增加。在第四章《数据处理与准备——铺平 Plotly Express 之路》中,我们讨论了选择高层软件和低层软件之间的权衡,在这种情况下,我们将使用一个低层系统来运行我们的应用程序。
如果你有服务器管理经验,那么你将拥有所需的全部灵活性,可以跳过本章的大部分内容。对于初学者的好消息是,尽管你将“独自一人”,我们将运行一个非常简单的设置,使用简单的默认配置。这应该能让你轻松部署应用程序,并且你可以逐步学习如何定制你的设置,同时知道你可以完全访问一些顶级工具。
我们的部署工作流将包含以下三个主要组件:
-
本地工作站:到目前为止,这一部分已经有了广泛的讨论,你应该已经熟悉我们迄今为止所使用的本地设置。
-
源代码管理系统:你实际上并不需要这个来运行你的应用程序,但它是一个非常好的做法,尤其是当你的应用程序变得越来越大,且更多人参与到维护工作中时。
-
具有所需基础设施和设置的服务器:你的代码和数据将在这里运行,并且对外提供服务。
为了更清楚地说明,以下截图展示了我们正在讨论的各个元素,接下来是对它们的简要描述,以及它们如何与开发和部署周期中的每个阶段相关:

图 12.1 – 开发、部署和更新周期中的三个主要组件
一个或多个人在本地机器上编写代码,所写的代码在他们和中央 Git 仓库之间双向传输。需要注意的是,代码不会在项目协作者之间传输——例如,不能通过电子邮件发送;他们只能将代码更改或添加推送到 Git 仓库。使用中央 Git 仓库有许多好处。这是一个很大的话题,专门有书籍讲解,我们这里只会涉及一些基本概念,并鼓励你深入学习。Git 最重要的一个功能是让每个人都在同一组更改上进行协作——这简化了每个人的工作流程。中央仓库需要有一名或多名管理员,这些人负责批准哪些内容可以进入主仓库,哪些不能。此外,他们还需要能够解决冲突。两个人可能会分别在同一个文件上工作,并且他们可能都会推送彼此冲突的更改,因此需要有人决定在这种情况下该如何处理。
批准的版本从 Git 推送到服务器,网站随后发布。如你所见,在这个阶段,代码仅在一个方向上传输。即使你是单独工作,强烈建议你使用 Git 来管理更改。
另一个重要的好处是,每一次更改(称为“提交”)都会包含关于更改何时发生以及由谁完成的元数据。更重要的是,提交记录了它们所属的分支。如果你需要回滚某些更改,你也可以“检出”某个特定的提交或分支,将整个仓库恢复到某个状态。你可以在应用程序运行的同时修复错误并进行更改,然后再将其重新引入。
这只是对 Git 的功能和工作原理的简化描述,但我认为值得提及并深入了解。我们将使用它进行部署,并介绍一些基本命令。在本章的最后部分,我们将通过修改其中一个文件的过程,展示如何使用 Git 引入更改。
在这个阶段,你的代码已经在本地成功运行,并且拥有所有需要的数据,接下来你希望将它推送到一个中央 Git 仓库,然后再部署到服务器。或者,你也可以简单地克隆这本书的仓库,并将它部署到你的服务器上。
现在我们已经建立了工作的一般周期,并探索了主要的组件和步骤,我们准备好开始我们的在线工作,首先通过设置 Linode 帐户来实现。
创建一个托管帐户和虚拟服务器
在 Linode 上设置账户非常简单,您可以从 login.linode.com/signup 的注册页面进行操作。一旦注册并提供您的账单信息,您可以开始创建一个“Linode”。拥有自己互联网协议(IP)地址的虚拟服务器也叫 Linode(Linux + node),与公司的名称类似。
该过程非常简单,可以通过登录后进入的主仪表盘完成。
以下截图展示了一些创建和管理您账户的主要选项:

图 12.2 – 您可以在 Linode 上创建的主要对象;最重要的是“Linode”
一旦您选择了创建选项并选择Linode,您将看到几个可供选择的选项。我们将选择发行版。您可以将发行版看作是基于 Linux 内核的不同软件包,包含不同的组件。每个发行版都针对特定的使用场景进行定制。我们将为我们的 Linode 使用 Ubuntu 发行版。其他选项也可能很有趣——例如,市场提供了通过几次点击创建流行软件完整安装的选项。欢迎您也探索这些其他选项。图 12.3 显示了我们选择的发行版,之后您会看到几个简单的选项可供选择。最重要的是,您需要选择一个计划,计划有很多种,可以为您提供很大的灵活性。您现在可以选择最小的计划,之后您可以根据需要决定是否升级。以下截图中,您可以看到正在选择 Ubuntu 发行版:

图 12.3 – 在 Linode 上选择并配置一个发行版
一旦您完成了剩余选项的设置,您可以点击创建按钮,然后您应该会进入新创建的 Linode 仪表盘。以下截图显示了该界面的顶部:

图 12.4 – Linode 仪表盘,包含多个报告和详细信息
您可以在此屏幕上查看有关您新创建的 Linode 的所有相关细节,您可以参考它以获取性能数据和其他信息,或者如果您想进行任何更改时使用。稍后,您可能会想要升级计划或添加更多存储空间等。
对我们来说,这个屏幕最有趣的部分是SSH 访问部分。我们将利用这一点登录到我们的服务器,从现在起我们将不再使用 Web 界面。
重要提示
Linode 提供了多种与账户交互和管理账户的方式。它提供了应用程序编程接口(API)和命令行接口(CLI)等工具,你可以通过这些工具做几乎任何通过 Web 界面可以做的事情。对于像我们刚才完成的简单任务,使用 Web 界面更加方便。使用 API 和/或 CLI 在大规模自动化和执行任务时更加有用。
既然我们的服务器已经启动,我们就可以开始在上面工作了。这主要通过 SSH 界面来完成。
使用 SSH 连接到你的服务器
SSH 是一种在不安全的网络上安全传输数据的协议。这将使我们能够通过本地计算机的终端,访问并运行服务器上的代码。
我们先通过点击旁边的剪贴板图标来复制ssh root@127.105.72.121命令,你可以在图 12.4中看到该图标。
现在,打开本地计算机上的终端应用程序,并粘贴以下命令:
ssh root@172.105.72.121
The authenticity of host '172.105.72.121 (172.105.72.121)' can't be established.
ECDSA key fingerprint is SHA256:7TvPpP9fko2gTGG1lW/4ZJC+jj6fB/nzVzlW5pjepyU.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '172.105.72.121' (ECDSA) to the list of known hosts.
root@172.105.72.121's password:
如你所见,我们有两个主要的回应:一个是询问是否要连接并将 IP 添加到已知主机列表,我们回答了yes;另一个是在确认后,要求我们输入密码。
一旦你输入为该特定服务器(Linode)创建的密码,而不是为你的账户设置的密码,你将看到新的提示符,如下所示:
root@localhost:~#
现在,你已经可以从自己的终端应用程序访问服务器的 root 权限。立即做一件好事(并且要经常做)就是更新系统的软件包。下面是两个可以快速完成这项工作的命令:
apt-get update
apt-get upgrade
以后,你可能需要管理你单独安装的其他软件包的更新。
拥有 root 访问权限非常强大,你可以作为 root 用户做任何事情,这意味着这也可能带来安全风险。如果其他人能够不小心使用你的 root 用户登录,他们就可以任意添加或删除任何文件,完全没有任何限制。
因此,我们将遵循推荐的做法,立即创建一个受限用户,并仅使用该用户登录。按照以下步骤进行:
-
创建一个新的用户,使用你想要的任何名称,如下所示:
adduser elias -
系统会提示你为该用户输入并重新输入密码。到现在为止,你已经有了三个密码:一个是 Linode 账户的密码,另一个是该 Linode 的 root 用户密码,还有一个是我们刚创建的受限用户的密码。你还可以选择提供全名、电话号码等详细信息。如果你想跳过这些选项,可以直接按Enter键。尽管这个用户是受限的,但我们可以将它添加到
sudo(超级用户do!)组中。这样,当我们需要进行一些管理员任务或访问敏感文件时,这个用户可以暂时访问 root 权限。现在我们已经创建了一个新的受限用户,接下来将其添加到sudo组中,步骤如下:adduser elias sudo -
创建了一个有限的用户并能暂时使用
sudo权限后,我们可以退出 root 账户,然后重新登录使用新用户,如下所示:exit -
你应该会看到一条消息,表示与你的 IP 地址的连接已丢失,并且你应该看到本地机器终端中的提示符。现在,我们使用新用户登录,方法与之前相同,如下所示:
ssh elias@172.105.72.121 -
一旦你成功登录,你将看到一个显示登录用户的新提示,如下所示:
elias@localhost:~$
我们现在准备在服务器上构建应用程序,但在此之前,让我们看看如何访问敏感文件并使用sudo权限,如下所示:
-
尝试访问系统的一个日志文件,例如使用
cat命令,如下代码片段所示:elias@localhost:~$ cat /var/log/syslog cat: /var/log/syslog: Permission denied -
我们没有被授予权限,这是正确的结果。现在,我们可以通过运行相同的命令并在前面加上
sudo命令来请求sudo访问,如下所示:elias@localhost:~$ sudo cat /var/log/syslog [sudo] password for elias: -
输入该用户的密码并获取访问所请求的敏感文件的权限。
你将经常遇到这种情况,你可以轻松地使用sudo命令来获取临时的 root 权限。
我们的服务器已经准备好了一大部分,但还没有完全就绪。接下来,我们需要将文件和数据上传到服务器,并安装所需的 Python 包。
在服务器上运行应用程序
本节中我们将做的事情正是我们在第一章中所做的,Dash 生态系统概述。我们将从 GitHub 克隆代码和数据仓库,将它们传送到服务器,安装依赖项,并尝试运行应用程序。
你通常在这样的服务器上已经安装了 Python,但最好检查一下,确认如何获取它,以防没有安装。检查是否已安装以及获取版本的简单方法是从命令行运行python --version。请记住,python命令可以解释为 Python 2。升级到 Python 3 花了一段时间才完全实现,因此在那段时间里,为了区分两个版本,使用了python3命令,以明确表示希望运行 Python 版本 3。这同样适用于pip命令,它也可以作为pip3运行。
当我运行python3 --version时,我得到了版本 3.8.6。等你阅读本文时,默认版本可能会有所不同。此外,在撰写本文时,Python 3.9 已经发布,并被认为是稳定版本。这是我在命令行中尝试运行时得到的结果:
elias@localhost:~$ python3.9
Command 'python3.9' not found, but can be installed with:
sudo apt install python3.9
不需要解释。我们还被提醒,要安装这样的包,我们还需要使用sudo。这些是你可能会使用的示例以及可能遇到的情况,但 Linux 管理是一个庞大的话题,熟悉一些基本概念是很有帮助的。
现在让我们激活虚拟环境并克隆 GitHub 仓库,使用与 第一章,Dash 生态系统概览 中相同的步骤,如下所示:
-
在一个名为
dash_project(或你选择的其他名字)的文件夹中创建一个 Python 虚拟环境。这也会创建一个新的文件夹,名称与你选择的名字一致。请注意,你可能需要安装venv,才能让这一步骤正常工作,具体命令如前面例子所示:python3 -m venv dash_project -
激活虚拟环境。你现在应该能看到环境名称在括号中显示,表示环境已经被激活,如以下代码片段所示:
source dash_project/bin/activate (dash_project) elias@localhost:~/dash_project$ -
通过运行以下命令进入环境文件夹:
cd dash_project -
现在我们要克隆我们的 GitHub 仓库,并将所有可用的文件和代码获取到服务器上。我们将使用书中的仓库作为示例,但我鼓励你运行并克隆你自己的仓库。运行以下命令:
git clone https://github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash -
接下来,我们需要安装所需的包,通过进入主文件夹并运行相应的命令,如下所示:
cd Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash/ pip install -r requirements.txt -
现在我们可以进入任何章节的文件夹,运行该版本的应用程序。在以下的代码片段中,我们可以看到,如果我们进入 第十一章,URLs 和多页面应用程序的最终版本,会发生什么:
cd chapter_11 python app_v11_1.py
按照之前的步骤操作,得到的结果与我们在本地运行应用程序时熟悉的完全相同,正如下面的截图所示:

图 12.5 – 应用程序在服务器上运行
得到之前的消息意味着代码正常运行,没有问题。当然,它也给了我们一个大大的警告,表示我们仅在使用开发服务器,不能将其用于生产环境部署。
所以,我们将设置我们的 Web 服务器。但在此之前,我们需要使用一个接口,它使我们的 Web 框架(Flask)能够与任何我们想要的 Web 服务器协同工作。这个接口被称为 WSGI(发音为 Wiz-ghee 或 whisky!)。
首先,让我们对涉及到的组件和阶段建立一个基本的理解,当用户从浏览器访问我们的应用程序时,请求和响应会经过一个简单的流程,如下图所示:

图 12.6 – 我们应用程序的组件,部署在公共服务器上
请求从左侧开始(正如前面截图所示),然后经过多个组件,直到到达 Dash,运行我们的 app.py 模块。接着,我们的应用程序代码生成一个响应,反向通过相同的组件,直到到达用户的浏览器。
让我们简要讨论一下这些元素,如下所示:
-
浏览器:这是直接的,可以是任何 超文本传输协议(HTTP)客户端。当用户输入 统一资源定位符(URL)并按下 回车键时,浏览器会向 Web 服务器发出请求。
-
Web 服务器:Web 服务器的工作是处理它接收到的请求。我们的问题是服务器不能执行 Python 代码,因此我们需要某种方式来获取请求,解释它们,并返回响应。
-
WSGI 服务器:这是一个中间件,负责翻译服务器语言并与 Python 进行交互。设置好这个意味着我们的 Web 框架(在这个例子中是 Flask)不需要关心处理服务器或处理多个请求。它可以专注于创建 Web 应用,只需要确保它符合 WSGI 规范。这也意味着有了这个设置,我们可以在不修改应用代码的情况下,随时更换 Web 服务器或 WSGI 服务器。
-
Web 框架:这是 Dash 构建的 Flask Web 框架。Dash 应用本质上是一个 Flask 应用,我们已经对此做了相当广泛的介绍。
目前,我们无需了解更多关于 Web 服务器或 WSGI 服务器的知识。让我们看看使用 WSGI 服务器运行我们的应用是多么简单。
使用 WSGI 设置并运行应用
我们已经通过命令行使用 python app.py 命令运行了我们的应用。或者,在使用 jupyter_dash 时,我们使用了 app.run_server 方法。现在我们将通过 Gunicorn(我们的 WSGI 服务器)来运行它。
这个命令与之前的略有不同,按照以下模式运行:
gunicorn <app_module_name:server_name>
这里有两个主要的区别。首先,我们只使用模块名称,或者不带 .py 扩展名的文件名。然后,我们添加一个冒号,再加上服务器名称。这是一个简单的变量,我们需要定义它,且可以在定义完顶层的 app 变量后,使用一行代码来完成,如下所示:
app = dash.Dash(__name__)
server = app.server
现在我们已经将服务器定义为 server,假设我们的应用文件名为 app.py,我们可以通过命令行运行应用,如下所示:
gunicorn app:server
就这些,关于 WSGI 服务器的内容完结了!
一旦做出更改,我们可以去到我们的应用所在文件夹,使用前述命令运行它。以下截图显示了我们使用 gunicorn 命令运行应用时得到的输出:

图 12.7 – 使用 Gunicorn WSGI 服务器运行应用
这个输出表明我们的应用运行正常。我们还可以看到它监听的是不同的端口。Dash 的默认端口是 8050,而这里是 8000。
我们离浏览器和用户更近了一步。看起来代码在 WSGI 服务器上运行正常。现在让我们设置 Web 服务器,使我们的应用可以公开访问。
设置和配置 Web 服务器
在本示例中,我们将使用 nginx 作为 Web 服务器。你现在可以通过命令行使用Ctrl + C停止应用程序。或者,你也可以使用kill命令停止应用程序,但这需要你知道运行应用程序的进程 ID。如果你稍后登录并不知道哪些进程在运行,可以通过这种方式识别负责你应用程序的进程。
你可以从命令行运行ps -A进程状态命令,获取当前运行的所有进程。你可以滚动查找包含gunicorn的进程,或者通过管道在上一个命令的输出中搜索该进程,如下所示:
ps -A | grep gunicorn
在应用程序运行的同时运行上面的命令,会得到如下截图所示的输出:

图 12.8 – 如何查找包含特定文本模式的进程的进程 ID
进程 ID 与我们在运行 Gunicorn 时获得的 ID 相同,前面截图中也可以看到。
要停止应用程序,你可以使用kill命令,如下所示:
kill -9 54222
现在我们已经确认应用程序能够与我们的 WSGI 服务器一起运行,是时候设置 Web 服务器了。
如前所述,尽管我们将使用最简单的配置来简化操作,但我们仍将使用最强大的 Web 服务器之一。
让我们从安装它开始。从命令行登录到你的服务器后,运行以下命令:
sudo apt install nginx
我们现在要为我们的应用程序创建一个配置文件。安装 nginx 时会做几件事,其中之一是创建一个sites-enabled文件夹。我们想要在其中创建我们的配置文件,并设置基本选项。你可以使用任何文本编辑器,通常在 Linux 机器上能找到一个简单的编辑器nano。作为命令运行它,后面跟着文件名,打开该文件进行编辑(如果文件不存在,则创建它)。
从命令行运行以下命令来打开并编辑我们的文件:
sudo nano /etc/nginx/sites-enabled/dash_app
你应该得到一个空文件,你可以复制并粘贴以下代码,但确保将server_name后的 IP 地址替换为你自己的 IP 地址:
server {
listen 80;
server_name 172.105.72.121;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
如你所见,这段代码包含了server上下文的配置。它告诉服务器监听端口80,这是 Web 服务器的默认端口。它还将server_name定义为 IP 地址。稍后,你可以用这个 IP 地址来定义你自己的域名。
然后,它在另一个块中定义了location /的服务器行为。对我们来说最重要的是,我们通过proxy_pass指令将 nginx 设置为代理服务器,并告诉它监听 Gunicorn 正在监听的 URL 和端口。因此,现在,整个循环应该完成了。我们的 Web 服务器将通过正确的 URL 和端口发送和接收请求与响应,而与 Python 代码的接口将由 Gunicorn 处理。
安装 nginx 会创建一个默认配置文件,我们需要用以下命令将其解除链接:
sudo unlink /etc/nginx/sites-enabled/default
我们只需要在做完这个更改后重新加载 nginx。将来做任何更改时请记住这一点。你应该在任何更改发生时重新加载 nginx,可以使用以下命令来实现:
sudo nginx -s reload
现在,我们可以使用gunicorn app:server来运行应用,然后,通过浏览器访问我们的 IP 地址,看到应用在线,如下截图所示:

图 12.9 – 应用部署在公共地址上
恭喜!你的应用现在对任何有互联网连接的人可用;它已经部署在公共服务器上,你可以与全世界分享你的工作。
接下来会发生什么?你如何进行更改,如果你使用的包有升级怎么办?
在我们部署应用后,我们将讨论一些可能有助于维护阶段的简单指南。
管理维护和更新
发布应用后,可能需要管理和处理一些事项,我们将讨论其中的几个。
修复 bug 和进行更改
这应该遵循我们在本章开始时建立的相同工作流程。无论是修复 bug 还是增加功能,任何对代码的更改都应该以相同的方式进行。我们在本地编辑代码,确保其正确运行,然后推送到中央 Git 仓库。接着,在服务器上拉取这些更改并重新运行应用。
更新 Python 包
我们的应用依赖于多个包,你在日常工作中可能会遇到更多。这些包会定期发布更新,你需要确保它们是最新的。其中一些更新是安全更新,必须尽快处理,而其他更新则会为包引入新选项。通常,你可以运行pip install --upgrade <package_name>来实现这一点,但你仍然需要检查新的功能是否会改变应用的运行方式,或者是否会破坏现有代码。维护良好的包通常会发布任何此类重大更改,并在需要时提供升级说明。
一旦你决定升级包,可以先在本地运行升级,以测试应用并确保它与新版本兼容,如下所示:
-
从命令行进入你本地机器上的应用文件夹,如下所示:
cd /path/to/your/app -
激活虚拟环境,方法如下:
source /bin/activate -
现在你应该能看到你的环境名称在括号中
(env_name),你现在准备好升级你选择的包,如下所示:pip install --upgrade <package_name> -
假设一切正常,运行你的应用,并确保它按预期运行,执行以下命令:
python app.py -
如果一切顺利,现在你需要更新
requirements.txt文件,以反映包的新版本,并可能修改了其他依赖项的版本。我们首先使用pip freeze命令。该命令将当前环境中的所有可用包及其依赖项以及正确的版本号一起输出到stdout。现在,我们想要将这个输出重定向到requirements.txt文件,以便用更新的要求覆盖它。我们可以一步完成这两个步骤,但首先了解第一个命令的输出是好的,可以看到如下:pip freeze > requirements.txt -
将变更提交到 Git 仓库并推送到 GitHub。
git add命令将文件添加到暂存区,意味着它们现在已准备好被添加到仓库的历史记录中。下一步是使用git commit命令提交这些新增内容,该命令还需要一个消息,说明已做了什么更改。然后,我们使用push命令将变更提交到在线仓库,命令如下:git add requirements.txt git commit -m 'Update requirements file' git push -
现在,既然你已经在中央 Git 仓库中拥有了最新的
requirements.txt文件,你可以像本章中所做的那样将其拉取到你的服务器上。登录到服务器后,进入项目文件夹并激活虚拟环境,然后你可以拉取变更。git pull命令做了两件事。首先,它从远程服务器获取仓库的最新变更。然后,它将变更合并到本地副本中,你就得到了更新后的应用程序。命令如下所示:git pull -
我们在这个案例中获取并合并的变更是更新过的
requirements.txt文件。现在我们运行以下命令,在服务器上安装使用新版本的包:pip install -r requirements.txt gunicorn app:server
这应该会重新启动你的应用程序,并包含最新的更新。虽然在这个案例中我们修改了 requirements 文件,但我们也可以修改应用程序文件,或者可能添加新的数据。无论是什么变更,这是我们在应用这些变更时经历的一般流程。
现在你有了一个新的组件需要处理——你的服务器——你还需要管理和维护它。
维护你的服务器
以下列表简要列出了你可能感兴趣的事情,并且没有详细说明。正确的做法是深入学习 Linux 系统管理,但这些是你可能想要管理的内容,并且可以很容易找到相应的指南和文档:
-
添加自定义域名:你可能希望为你的应用程序起一个好名字,而不是一个 IP 地址。这是简单的,你需要从注册商购买一个域名,并进行必要的更改来启用它。你可以在 Linode 的文档中找到很多示例和指南,了解如何实现这一点。
-
设置安全证书:这是很重要的,现在已经变得简单且免费。很多指南和示例也可以参考。
-
更新包: 就像我们第一次登录时所做的那样,定期更新服务器的软件包非常重要,特别是确保拥有最新的安全更新。
-
sudo组。还有其他可以做的事情,比如使用认证密钥对增强 SSH 访问安全性,配置防火墙等其他操作。
在本章中,我们迈出了重要的一步,快速探索了非常强大的工具和技术,因此让我们回顾一下本章涵盖的内容。
摘要
我们首先建立了一个简单的工作流程来管理开发、部署和更新的周期。我们为这个工作流程定义了三个主要组成部分及它们之间的关系。我们讨论了本地工作站、中央 Git 仓库和 Web 服务器之间的关系,并设定了一些工作流程应如何流动的准则。
然后,我们创建了一个托管账户,设置了一个虚拟服务器,并准备在本地服务器上进行工作。然后,我们探讨了如何通过 SSH 本地访问服务器,并运行了一些基本的安全和管理任务。我们克隆了我们的代码库,并看到它可以在服务器上完全像在本地一样运行。
然后我们讨论了我们的应用程序公开可用的另外两个必需组件。我们通过使用 WSGI 服务器稍微不同地运行我们的应用程序。最后一步是安装和配置一个 Web 服务器,使用尽可能简单的设置。然后我们的应用程序可以在公共 IP 上访问了。
最后,我们探讨了哪些正在进行的维护任务可能会很有趣。最重要的是,我们完成了升级 Python 包的过程,修改了一个文件,提交到 Git,推送到在线仓库,并将更改合并到我们的服务器上。这绝对是你将会持续进行的工作。
本书的最后一章将讨论你可能有兴趣探索的其他方向和我们尚未涵盖的其他领域。你现在已经熟悉 Dash,并可以非常轻松地导航和找到你需要了解的任何内容,但还有许多其他内容可以探索。我们将在最后一章中快速浏览这些选项。
第十三章:第十三章:下一步
欢迎来到本书的结束,也是你 Dash 旅程的开始!虽然我们已经涵盖了许多主题、用例、图表类型和互动功能,但在使用 Dash 构建内容方面,天高地阔,潜力无限。
到现在为止,你应该已经像制作演示文稿一样熟练地构建仪表板。你应该能够使用各种数据可视化技巧和图表类型,熟练地处理和展示数据。
但本书只是让你踏上了道路,接下来还有很多东西值得探索,所以我们将介绍一些如何更深入地探索我们所涵盖主题的思路和建议。我们还将关注一些书中没有涉及的方面,你可能会有兴趣进一步探索。
本章将涵盖以下内容:
-
扩展数据处理和准备技能
-
探索更多数据可视化技巧
-
探索其他 Dash 组件
-
创建你自己的 Dash 组件
-
操作化和可视化机器学习模型
-
提升性能并使用大数据工具
-
使用 Dash Enterprise 进行大规模部署
技术要求
本章不涉及编码或部署,因此没有任何技术要求。
你可以通过两种方向学习新事物并进行探索:自上而下的方法,你想做某件事或被要求做某件事;或者自下而上的方法,从工具开始,探索它们的可能性以及你能用它们做什么:
-
自上而下:由于某种要求或限制,通常你需要做某件事——比如让某件事变得更快、更好或更容易。为了解决这些问题,或满足某些要求,你需要学习一些新东西。这种方法的价值主要在于它的实用性。你知道什么是有用的,什么是必需的,这帮助你将心思和精力集中在你想要的解决方案上,这些解决方案专注于解决实际问题。同时,如果你只专注于实际问题,你可能会错过一些新技术和方法,而这些可能会使你的实际生活变得更加轻松。
-
自下而上:这是一种从另一方向出发的方法。你从学习一些新事物开始,单纯为了探索或好奇。它可以是一些大问题,比如机器学习,或者像学习一个你每天都在用的函数中的新参数这么小的事。这突然会在你脑海中打开新的可能性,也拓展了你对可能性的认知。这是你可以主动去做的,无论你的工作需求如何。那句著名的格言“我越是练习,我就越幸运”似乎适用于这种情况。这种方法的好处是,你能正确地学习事物,建立扎实的理论基础,从而能更好地掌控手头的技巧。缺点是,你可能变得过于理论化,失去与现实的联系,忘记了什么是真正有用的,什么是没用的。
我发现自己时常在不同的阶段之间切换,有时候我把大部分时间都用来集中精力解决某个具体问题(自上而下),并产生非常实际的解决方案。然后,我会进入一个停滞期,感觉我的想象力不再活跃,创造力也没那么强。我接着进入一种更理论化的模式。在这个阶段,学习新事物变得非常有趣和引人入胜。在掌握了足够的新知识并对某个主题有了较好的理解后,我会突然获得新的想法,再回到实际操作模式,就这样循环往复。
你可以看看什么方法对你有效。现在,让我们探索一些你在 Dash 旅程中可能感兴趣的具体主题。
拓展你的数据处理和准备技能
如果你读过任何关于数据科学的入门书籍,你可能已经被告知,数据科学家花费大部分时间在清理数据、重新格式化数据和调整数据形态上。
你在阅读本书时,可能已经看到过这个过程的实际应用!
我们已经多次看到,要将数据转化为某种格式,所需的代码量、精神努力,尤其是领域知识有多少。一旦我们将数据转换为标准化格式,例如长格式(tidy)DataFrame,那么我们的生活就会变得更轻松。
你可能想学习更多关于 pandas 和 NumPy 的知识,以获得更完整的数据重塑技巧,按照你想要的方式进行数据处理。正如本章开始时提到的那样,学习没有实际目的的 pandas 技巧对于拓展你的想象力有很大帮助。学习正则表达式在文本分析中也非常有用,因为文本通常是非结构化的,找到并提取特定模式对你的数据清洗过程帮助很大。统计学和数值技巧无疑会带来巨大的改变。归根结底,我们基本上是在处理数字。
提升数据处理技能自然会引导我们实现更简便、更好的可视化。
探索更多数据可视化技巧
我们看到使用 Plotly Express 是多么简单,以及它有多么强大。我们也看到它为我们提供了丰富的选项。同时,我们受限于必须将数据整理成特定格式的要求,而这正是 Plotly Express 无法帮助的地方。作为数据科学家,我们需要在这一点上介入。
我们涵盖了四种主要的图表类型,而这只是可用图表的一个非常小的子集。正如本章开头提到的,数据可视化有几种方式。你可能需要生成某种特定的图表,进而不得不学习它。或者,你可能会学到一种新的图表,它激发你为某些特定的使用场景更好地总结某些数据类型。
你可能会根据图表使用的几何形状/属性了解新的图表类型,如饼图或点图。你也可以根据它们的用途来探索它们;例如,统计图表和财务图表。许多图表类型归结为基本形状,如点、圆、矩形、线条等等。它们的展示方式和组合方式使它们独具特色。
另一种有趣的可视化技术是使用子图。尽管我们在书中广泛使用了分面(faceting),但分面本质上是同一个可视化展示多个数据子集。而子图则允许你创建一组可以彼此独立的图表。这有助于你在一个图表中展示丰富的报告,其中每个子图展示数据的不同方面。
在探索并掌握了新的可视化技术和图表后,你可能会希望将它们放入一个应用程序中,并使它们具有交互性。
探索其他 Dash 组件
我们覆盖了基本的 Dash 组件,此外还有许多其他可用组件。请记住,这里有三种可能的方式:
-
dash_table,它能够提供相当复杂的功能和电子表格风格的选项。 -
DatePickerSingle和DatePickerRange,它们的功能不言自明。Interval组件允许你在某一时间段过后执行代码。Store组件允许你将数据存储在用户的浏览器中,以便你希望保存一些数据来增强应用的可用性/功能性。还有一个Upload组件用于上传文件。这些都可以在 Dash 核心组件中找到。还有一些其他有趣的包,适用于其他使用场景。例如,Dash Cytoscape 非常适合进行互动式图表(网络)可视化。我们在使用可视化调试工具时多次看到了它,看到它是如何大大简化我们对应用理解的。这在许多行业中都有广泛的应用。为了让用户能够在你的图表上绘制图形,你可以查看 Dash 提供的图像注释选项以及 Dash Canvas 包。它们一起提供了丰富的选项,让用户可以用鼠标在图表上直接绘制,使用矩形等设定形状,或仅通过拖动鼠标。 -
探索一些社区组件:由于 Dash 是一个开源项目,并且有一个创建和集成新组件的机制,许多人独立地创建了自己的 Dash 组件。Dash Bootstrap Components 就是其中之一,我们在工作中依赖了它。还有许多其他的组件,新的组件也一直在不断推出。
这将引出另一个话题,那就是创建你自己的 Dash 组件。
创建你自己的 Dash 组件
很有趣的是,Dash 的官方策略是成为“Python、R 和 Julia 的 React”。如你所知,React 是一个非常大的 JavaScript 框架,用于构建用户界面。React 有一个庞大的开源组件库,而 Dash 核心组件基本上是可以在 Python 中使用的 React 组件。这意味着,如果 Dash 没有提供某些功能,而你又希望能够使用,你可以考虑自己开发,雇佣开发者来构建,或者你也可以赞助该功能的开发,让 Plotly 团队来构建。有些我们使用的组件就是由那些希望获得某些尚未提供的功能的客户赞助的。这也是一种支持 Dash 的方式,它也使得所有使用开源 Dash 的人都受益。
有清晰的指引教你如何创建自己的 Dash 组件,作为一个 Dash 开发者,探索这个选项是很有益的。它肯定会让你更深入地理解这个库的运作方式,也许你最终会自己创建一个流行的组件!
通过所有的数据处理、可视化和组件,你拥有了一个丰富的词汇库,可以做的不仅仅是将数据点绘制在图表上。探索机器学习能够做的事情,可以大大提升你的模型,并使其可供他人使用。
使机器学习模型可操作化和可视化
机器学习和深度学习当然是完全不同的话题,但是通过前面提到的所有技能,您可以将机器学习提升到一个新的水平。在一天结束时,您将使用图表来表达关于数据的某些想法,并且通过良好的交互式数据可视化词汇,您可以为用户提供许多选项来测试不同的模型和调整超参数。
提升性能并使用大数据工具
这是一个非常重要的话题,我们始终需要确保我们的应用程序在可接受的水平上运行。我们在书中没有涉及这个问题,因为重点主要是学习如何创建一个具有使其工作的所有其他细节的 Dash 应用程序。我们还使用了一个非常小的数据集,仅有几兆字节。即使是小数据集,优化它也可能至关重要。大数据可以处理一个庞大的文件,也可以处理一个需要多次处理的小文件。
这些是优化性能可以做的一些事情,但大数据是一个完全不同的主题,因此以下是一些提示和一些可以探索的领域。
一旦我们知道我们的应用程序将如何运行以及我们将使用哪些功能,我们可以清理一些可能会妨碍应用程序性能的不必要的代码和数据。以下是一些可以立即对我们的应用程序进行的想法:
-
仅加载必要的数据:我们加载了整个文件,并且对每个回调,我们单独查询了 DataFrame。这可能是浪费的。例如,如果我们仅有人口数据的回调,我们可以创建一个单独的文件(然后是单独的子集)DataFrame,该 DataFrame 仅包含相关列,并仅查询它们,而不是使用整个 DataFrame。
-
优化数据类型:有时您需要加载包含许多重复的相同值的数据。例如,贫困数据集包含许多重复的国家名称。我们可以使用 pandas 分类数据类型来优化这些值:
-
加载
sys模块并查看字符串(国家名称)和整数的字节大小差异:import sys sys.getsizeof('Central African Republic') 73 -
获取整数值的大小:
sys.getsizeof(150) 28 -
我们可以看到字符串几乎占用整数大小的三倍的内存差异。这基本上是分类数据类型的作用。它创建一个字典,将每个唯一值映射到一个整数。然后使用整数来编码和表示这些值,您可以想象这可以节省多少空间。
-
加载贫困数据集:
import pandas as pd poverty = pd.DataFrame('data/poverty.csv') -
获取包含国家名称列并检查其内存使用情况的子集:
poverty[['Country Name']].info() <class 'pandas.core.frame.DataFrame'> RangeIndex: 8287 entries, 0 to 8286 Data columns (total 1 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Country Name 8287 non-null object dtypes: object(1) memory usage: 64.9+ KB -
将列转换为分类数据类型并检查内存使用情况:
poverty['Country Name'].astype('category').to_frame().info() <class 'pandas.core.frame.DataFrame'> RangeIndex: 8287 entries, 0 to 8286 Data columns (total 1 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Country Name 8287 non-null category dtypes: category(1) memory usage: 21.8 KB -
使用一个简单的命令,将我们的国家编码为整数,我们将内存使用量从 64.9 KB 减少到 21.8 KB,大约是原始大小的三分之一。
-
你可能还想考虑了解更多关于可用的大数据技术和技巧。目前最重要的项目之一是 Apache Arrow 项目。该项目是数据库社区和数据科学社区的领军人物之间的合作。该项目的一个重要目标是统一跨学科的努力,尤其是跨编程语言的努力。
-
当你想读取一个 CSV 文件时,举个例子,你希望它在内存中表示为一个 DataFrame。无论你使用 R、Python,还是其他任何语言,你都会执行非常相似的操作,如排序、选择、过滤等等。这里有很多重复的工作,每种语言都实现了自己的 DataFrame 规范。从性能角度来看,已经观察到大量的计算资源浪费在将对象从一种语言转换到另一种语言以及读写操作上。这在将对象保存到磁盘并在另一种语言中打开时也会发生。这会造成资源浪费,并且在许多情况下,迫使很多团队必须选择一种语言以便于沟通,减少浪费的时间和精力。
-
Apache Arrow 项目的目标之一是为数据对象(如 DataFrame)创建一个单一的内存表示方式。这样,数据对象可以在不同的编程语言之间传递,而无需进行任何转换。你可以想象这将使事情变得多么简单。此外,跨编程语言和学科的合作带来了巨大的收益,因为单一的规范正在被使用和维护。
-
每种编程语言可以实现基于单一规范的库。对于 Python,包是
pyarrow,它非常有趣,值得探索。在很多情况下,它可以单独使用,在其他情况下,它可以与 pandas 集成。 -
一个非常有趣的文件格式,也是该项目的一部分,就是
parquet格式。就像 CSV 和 JSON 一样,parquet是语言无关的。它是一个文件,可以用任何支持parquet阅读器的语言打开。好消息是 pandas 已经支持这个格式了。 -
parquet的一个重要特性是强大的压缩功能,可以大幅减少文件的大小。因此,它非常适合长期存储和高效利用空间。不仅如此,由于格式的缘故,打开和读取这些文件也非常高效。文件包含关于文件的元数据,以及模式信息。文件还被安排成独立的结构,以便高效读取。 -
parquet使用的某些技术如下:-
parquet主要按列存储数据。面向行的格式适合事务处理。例如,当用户登录网站时,我们需要检索与该用户相关的数据(一行),并且可能需要根据该用户的交互写入和更新这一行数据。但是在分析处理方面(这是我们感兴趣的领域),例如,如果我们想分析每个国家的平均收入,我们只需要读取数据集中的两列。如果数据是按列排列的,那么我们可以从列的开始跳到结尾,比起行式存储,它能够更快地提取数据。我们可以读取其他列,但只有在需要对它们进行进一步分析时才会这么做。 -
parquet还执行字典编码。它使用了其他几种编码方案。例如,存在增量编码,当数据包含大数字时,增量编码效果最佳。它保存列中第一个数字的值,并只保存与连续数字之间的差异。例如,如果你有以下列表:[1,000,000, 1,000,001, 1,000,002, 1,000,003],这些数字可以表示为:[1,000,000, 1, 1, 1]。我们保存了第一个元素的完整值,并且仅保存每个元素与前一个元素之间的差异。这样可以节省大量内存。这在使用时间戳时特别有用,时间戳通常表示为具有小差异的大整数,特别是在时间序列数据中。当你想要读取列表时,程序可以进行计算并给出原始数字。虽然还使用了其他编码策略,但这只是另一个示例。 -
parquet可以将一个文件拆分成多个文件,并从包含这些文件的文件夹中读取和合并它们。假设有一个包含人员数据的文件,该文件有 1000 万行。其中一列可能是性别,包含“男”和“女”两个值。如果我们将文件拆分成两个文件,每个文件对应一个值,那么我们就不需要再存储整列数据了。我们只需要在文件名中包含“female”,如果请求该列,程序就知道如何填充该列的所有值,因为它们都是相同的。 -
parquet对列中的数据组使用的一个方法是,它包含每个组的最小值、最大值和一些其他统计信息。假设有一个包含 1000 万行的文件,并且你只想读取介于 10 和 20 之间的值。假设这些行被拆分成每组 100 万行。现在,每个组的头部会包含该组的最小值和最大值。在扫描时,如果你遇到一个最大值为 6 的组,那么你就知道请求的值不在该组中。通过一次比较,你就跳过了 100 万个值。
-
现在,如果你已经掌握了所有这些技术,并且能够制作有洞察力的可视化效果、良好的交互性,并提供真正有帮助的仪表板,你仍然可能没有足够的经验(或兴趣)去处理 Dash 的大规模部署。此时,你可能会考虑 Dash Enterprise。
使用 Dash Enterprise 进行大规模部署
当你在一个拥有众多用户且已有现有基础设施的大型组织中进行部署时,可能会遇到一些你之前没有考虑或预见到的事情。想象一下,你的应用程序将由公司中的数百人使用。你如何处理应用程序的访问权限?你如何管理密码,当有人辞职或加入时会发生什么?你是否在安全方面有足够的经验,足以自信地处理如此大规模的部署?
在这些情况下,数据工程的角色比以往更为重要。有效且安全地存储数据变得更加重要。如果这不是你的专业领域,扩展性和管理起来可能会非常棘手。你的主要工作是设计和创建有助于发现洞察力的东西,而不是维护大规模的应用程序。在某些情况下,你可能具备所需的技能,但不想担心这些问题,主要想专注于界面、模型和可视化。
这正是 Dash Enterprise 可以提供帮助的地方。它基本上是你所熟知的 Dash,但具有许多专门为大规模部署设计的选项。
Dash Enterprise 还提供了一个完整的在线工作台,配有流行的 IDE 和笔记本,因此你还可以与同事在线协作。如果有大量用户需要共同工作,这可能会有所帮助。
专业服务也提供给大型客户,在这种情况下,你将能够接触到那些构建 Dash 的人,他们与许多经历过与你相似的经历的组织合作过,你可以从他们那里获得在许多重要领域的帮助。
这些只是一些想法,但最终决定性的是你的创造力、领域知识和努力工作,因此让我们总结一下本章和整本书所涉及的内容。
总结
我们从专注于处理数据的基本技能的重要性开始。我们强调了掌握数据处理和清洗技能的重要性,这将使你能够将数据格式化为你所需要的形状,并使其易于分析和可视化。我们还提到了各种数据可视化技巧和可以探索的图表类型,以提高你在视觉表达想法方面的流利度。
我们还讨论了书中未涉及的其他 Dash 组件,以及社区组件,这些组件正在不断开发中。最终,你可能决定开发自己的一组组件,并为 Dash 生态系统贡献额外的功能,我期待着pip install dash-your-components!
接着我们讨论了探索机器学习,以及如何使我们的模型变得可视化和互动。建立数据操作、可视化和交互技能将大大帮助你让模型更具可解释性和可用性,尤其是对于非技术受众。
我们探讨了一些大数据选项,并讨论了其中一个重要的项目,尽管还有许多其他项目需要考虑和探索。
最后,我们讨论了 Dash 提供的付费企业解决方案——Dash Enterprise;当你的项目是大型组织或部署的一部分时,这个解决方案可能会有意义。
非常感谢你的阅读,希望你喜欢这本书。我期待看到你自己的应用上线,拥有你自己的设计、模型、选项和定制功能。

订阅我们的在线数字图书馆,全面访问超过 7000 本书籍和视频,及行业领先的工具,帮助你规划个人发展并推进职业生涯。更多信息,请访问我们的网站。
第十四章:为什么要订阅?
-
通过来自 4000 多名行业专家的实用电子书和视频,减少学习时间,增加编码时间
-
使用专门为你设计的技能计划提升学习效果
-
每月获得一本免费电子书或视频
-
完全可搜索,便于访问重要信息
-
复制粘贴、打印和收藏内容
你知道吗,Packt 为每本出版的书籍提供电子书版本,提供 PDF 和 ePub 文件?你可以在packt.com升级到电子书版本,作为印刷书籍的用户,你还可以享受电子书的折扣。欲了解更多详情,请联系我们:customercare@packtpub.com。
在www.packt.com上,你还可以阅读一系列免费的技术文章,注册各种免费的电子邮件通讯,并获得 Packt 书籍和电子书的独家折扣和优惠。
你可能感兴趣的其他书籍
如果你喜欢这本书,可能还对 Packt 出版的这些书籍感兴趣:

动手数据可视化与 Bokeh
凯文·乔利
ISBN: 978-1-78913-540-4
-
安装 Bokeh 并理解其关键概念
-
使用符号(Bokeh 的基本构建块)创建图表
-
使用不同的数据结构(如 NumPy 和 Pandas)创建图表
-
使用布局和小部件可视化增强你的图表,并添加交互性
-
在 Bokeh 服务器上构建和托管应用程序
-
使用空间数据创建高级图表
专家数据可视化
乔斯·迪尔克森
ISBN: 978-1-78646-349-4
-
学习如何使用 D3.js 声明式定义可视化效果
-
使用 SVG 和 D3.js 的 API 从零开始创建图表
-
查看如何使用 D3.js 准备数据,以便进行便捷的可视化
-
使用 D3.js 提供的图表类型可视化层次化数据
-
探索 D3.js 提供的不同选项,用于可视化如图形等关联数据
-
通过添加交互性和动画来为你的可视化效果增添色彩
-
学习如何使用 D3.js 可视化并与与地理和地理信息系统(Geo- and GIS)相关的信息源互动
Packt 正在寻找像你这样的作者
如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并立即申请。我们与成千上万的开发者和技术专业人士合作,帮助他们将自己的见解分享给全球技术社区。您可以提交一般申请,申请我们正在招募的具体热门话题,或者提交您自己的想法。
留下评论 - 让其他读者了解您的想法
请通过在购买书籍的网站上留下评论,分享您对本书的想法。如果您是从亚马逊购买的书籍,请在该书的亚马逊页面上留下诚实的评价。这非常重要,因为其他潜在读者可以看到并使用您公正的意见来做出购买决策,我们可以了解客户对我们产品的看法,作者们也可以看到您对他们与 Packt 合作创作的书籍的反馈。只需要花费几分钟时间,但对于其他潜在客户、我们的作者和 Packt 都非常有价值。谢谢!






浙公网安备 33010602011771号