Plotly-和-Dash-智能仪表盘和数据应用-一-

88 阅读1小时+

Plotly 和 Dash 智能仪表盘和数据应用(一)

原文:annas-archive.org/md5/e5313a0f17775ffeed6651154fcfc198

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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/err…,选择您的书籍,点击“勘误提交表单”链接,并填写相关详情。

盗版:如果您在互联网上发现我们作品的任何非法版本,恳请您提供相关的网址或网站名称。请通过 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),以便编辑代码。

在本章中,我们将使用DashDash HTML ComponentsDash 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

设置您的开发环境

由于书中使用的所有包的更新速度很快,您很可能会遇到一些功能上的差异,因此,为了复现书中描述的确切结果,您可以克隆书籍的仓库,安装使用的包(指定版本),并使用包含的数据集。从命令行进入您想要构建项目的文件夹,并执行以下操作:

  1. 在一个名为dash_project的文件夹中创建一个 Python 虚拟环境(或者你想要的任何其他名称)。这也会创建一个与你选择的名称相同的新文件夹:

    python3 –m venv dash_project
    
  2. 激活虚拟环境。

    在 Unix 或 macOS 上,运行以下命令:

    source dash_project/bin/activate
    

    在 Windows 上,运行以下命令:

    dash_project\Scripts\activate.bat
    
  3. 进入创建的文件夹:

    cd dash_project
    
  4. 克隆这本书的 GitHub 仓库:

    git clonehttps://github.com/PacktPublishing/Interactive-Dashboards-and-Data-Apps-with-Plotly-and-Dash 
    
  5. 你现在应该有一个包含所需包及其版本的文件,名为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 的组成

图 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 应用结构

图 1.2 – Dash 应用结构

让我们详细看看每个应用部分:

  • 导入(模板代码): 和任何 Python 模块一样,我们首先导入所需的包,并使用它们的常见别名。

  • 这里的app变量。name参数的__name__值用于让 Dash 方便地定位将用于应用的静态资源。

  • 图中的html.Div,它接受一个组件列表作为其children参数。这些组件将在应用渲染时按顺序显示,每个都位于前一个元素的下方。在接下来的部分中,我们将创建一个具有最小布局的简单应用程序。

  • 回调函数:这是第二章的主题,探索 Dash 应用程序的结构,我们将在其中详细讲解交互性如何工作;本章不涉及这一内容。目前,只需要知道在这里我们可以定义任意数量的函数,将应用程序的可视元素互相连接,定义我们想要的功能。通常,函数是独立的,它们不需要定义在容器内,且函数的顺序在模块中没有关系。

  • 运行应用程序:使用 Python 运行模块作为脚本的习惯,我们来运行应用程序。

正如我承诺的那样,我们现在准备好开始编写代码了。

创建并运行最简单的应用程序

使用我们刚才讨论的结构,并排除回调函数,接下来我们来构建第一个简单的应用程序!

创建一个文件并命名为app.py,然后编写以下代码:

  1. 使用它们通常的别名导入所需的包:

    import dash
    import dash_html_components as html
    
  2. 创建(实例化)应用程序:

    app = dash.Dash(__name__)
    
  3. 创建应用程序的布局:

    app.layout = html.Div([
        html.H1('Hello, World!')
    ])
    
  4. 运行应用程序:

    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 – 运行应用程序时的命令行输出

图 1.3 – 运行应用程序时的命令行输出

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

图 1.4 – 在浏览器中渲染的应用

图 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 – 浏览器中渲染的更新应用

图 1.5 – 浏览器中渲染的更新应用

提示

如果你熟悉 HTML,这应该看起来很直观。如果不熟悉,请查看在线的基本教程。一个很好的起点是 W3Schools:www.w3schools.com/html/

在更新部分,我们只添加了一个<p>元素和一个无序列表<ul>,在其中我们添加了几个列表项<li>(使用 Python 列表),最后一个项包含了一个使用<a>元素的链接。

请注意,由于这些组件是作为 Python 类实现的,它们遵循 Python 的类名首字母大写的命名约定:html.Phtml.Ulhtml.Lihtml.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 – 主题样本及其设置方法

图 1.6 – 主题样本及其设置方法

你可以看到,通过仅更改一个参数,就能轻松改变应用的外观和感觉。另请注意,<h1> 元素的颜色和字体大小在样式参数中被覆盖了。我们特别将颜色设置为 "blue",将大小设置为 "40px"。通常不建议这样做;例如,在图中的两个深色主题下,蓝色文本非常难以阅读。因此,在进行这样的更改时要小心。

网格系统与响应式布局

另一个我们从 Bootstrap 获得的强大好处是其网格系统。在添加 Dash HTML 组件时,我们看到可以通过将项目附加到主 html.Div 元素的 children 参数来完成。在这种情况下,每个添加的项目都会占据屏幕的整个宽度,并根据需要显示内容而占据相应的屏幕高度。列表中元素的顺序决定了它们在屏幕上的显示顺序。

在列中并排显示元素

虽然通过编辑任何 HTML 元素的 style 参数可以做到这一点,但这有点繁琐,且可能不稳定。你需要关注许多细节,且可能会出现意想不到的问题。使用 Bootstrap,你只需定义一个列,然后它就会作为一个独立的屏幕,按顺序显示其中的元素,每个元素占据这个小屏幕的整个宽度。列的宽度也可以以强大且灵活的方式进行指定。网格系统将屏幕划分为 12 列,列的宽度可以通过使用从 1 到 12 的数字来指定。图 1.7 展示了如何定义列,以及它们在不同屏幕尺寸下如何变化:

图 1.7 – 两个屏幕尺寸下相同的列布局

图 1.7 – 两个屏幕尺寸下相同的列布局

如你所见,两个屏幕是相同的,且调整大小会自动发生,同时保持比例。

在许多情况下,这可能并不是你想要的效果。当屏幕宽度变小时,将列扩展以便用户更容易阅读可能更有意义。为此,我们可以选择为五种可能的屏幕宽度指定列的宽度:xs(超小型)、sm(小型)、md(中型)、lg(大型)和 xl(超大型)。这些也是你可以设置的参数名称:

Figure 1.8 – Granular control of column width based on screen size

图 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)中,它们就会并排显示:

Figure 1.9 – Columns side by side in a 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 组件

现在我们将向应用中添加两个新的相关组件:TabsTab。正如你可能猜到的,Tabs 只是 Tab 组件的容器。我们希望得到的结果是将更多的信息添加到页面中,并将其按新的选项卡组织,如图 1.10 所示:

图 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 – 根据用户选择的值显示用户的选择

图 2.1 – 根据用户选择的值显示用户的选择

以下代码将创建这个,除了显示用户选择的功能:

  1. 使用别名导入所需的包:

    from jupyter_dash import JupyterDash
    import dash_core_components as dcc
    import dash_html_components as html
    
  2. 实例化应用:

    app = JupyterDash(__name__)
    
  3. 创建应用的布局。我们现在将介绍一个新组件,即 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()
    ])
    
  4. 像往常一样运行应用程序,唯一的不同是以 inline 模式运行,以便在 JupyterLab 中更方便地进行交互工作:

    if __name__ == '__main__':
        app.run_server(mode='inline')
    

图 2.2 展示了在笔记本环境中运行时的示例:

图 2.2 – Dash 应用在 JupyterLab 中运行

图 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 – 输入、输出和独立函数

图 2.3 – 输入、输出和独立函数

为了使它在此应用程序的上下文中工作,函数需要知道其输入和输出是什么。

现在让我们来看一下如何通过设置组件的id值来识别组件。之后,我们将学习如何声明一个组件为InputOutput

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)

图 2.4 – 可见的应用元素被赋予了名称(ID)

在为我们的组件命名后,我们现在准备将它们用于展示之外的其他用途。

Dash 输入和输出

下一步是确定哪个组件将成为输入(传递给我们的纯 Python 函数),哪个组件将接收函数的返回值(作为输出)并展示给用户。

确定你的输入和输出

dash.dependencies 模块有几个类,其中两个我们将在这里使用:OutputInput

这些类可以通过将以下行添加到我们应用的 imports 部分来导入:

from dash.dependencies import Output, Input

在添加使功能正常运行的最终元素之前,先快速回顾一下我们之前所做的工作:

  1. 我们在 Jupyter Notebook 环境中实例化了一个应用。

  2. 我们创建了一个包含三种颜色的下拉框。

  3. 我们创建了一个常规函数,它返回一个字符串,并附上提供给它的值:'Your selected' + <color>

  4. 通过它们的 id 参数,我们为组件指定了描述性的名称。

  5. InputOutputdash.dependencies 导入。

    我们现在将定义我们的回调函数。

回调函数是装饰器,在最基本的用法中,它们需要三个内容:

  1. 空 div 的 children 属性。在这种情况下,它可以像这样指定:

    Output(component_id='color_output',
    component_property='children')
    
  2. value 属性:

    Input(component_id='color_dropdown',
    component_property='value')
    
  3. 我们选择了 InputOutput

图 2.5 显示了事物如何汇聚在一起的更新视图:

图 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 应用

图 2.6 – 在 Jupyter Notebook 中的交互式 Dash 应用

我还引入了一个简单的新组件,html.Br,它仅提供一个常规的 HTML <br> 元素,以提高输出的可读性。

到此,我们已经完成了第一个独立且互动的应用。我们在 JupyterLab 中运行它,并一步步地进行分析每一个细节。我们刚刚构建的应用使用的是一个玩具数据集,并实现了非常简单的功能。我们这么做是为了聚焦于创建交互性的机制。告诉用户他们刚刚选择的颜色并没有太多实际意义。

有了这些知识后,我们将为用户回答一个实际问题——一个如果用户浏览整个数据集可能会觉得很繁琐的问题。

我们还将这个新功能整合到我们的应用中,查看它如何与我们已经创建的其他内容和功能契合。

将该功能集成到应用中

这是我们将要引入的功能计划:

  1. 使用我们数据集中可用的国家和地区创建一个下拉列表。

  2. 创建一个回调函数,获取所选国家,过滤数据集,并找到该国家在 2010 年的人口数据。

  3. 返回一个关于找到的数据的小报告。图 2.7 显示了期望的最终结果:图 2.7 – 用于显示所选国家人口的下拉列表

图 2.7 – 用于显示所选国家人口的下拉列表

重要提示

现在我们开始使用数据集,我们将从 data 文件夹中打开文件。这假设你正在运行的应用程序与该文件夹位于同一目录下。每一章的代码在 GitHub 仓库中都有单独的文件夹,便于访问;然而,代码只有在 data 文件夹和 app.py 文件在同一目录下时才有效。

图 2.8 显示了该文件夹结构可能的样子:

图 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 – 贫困数据集的前几行和列

图 2.9 – 贫困数据集的前几行和列

看起来我们有两列固定变量(NaN)值,这些值位于各自的年份列下。这里,年份从 1974 到 2019(请注意,并不是所有年份都显示,以便更好地阅读)。国家和指标也有代码,这些代码在我们需要合并不同 DataFrame 时会派上用场。

提示

固定变量指的是那些预先已知且不变化的变量;在这种情况下,它们是国家和指标。被测量的变量是我们希望了解的数值,例如某国 A 在某年 B 的人口。固定变量也被称为“维度”。从技术上讲,它们是数据集中所有的列,这是一个有助于分析的概念性区分。

第四章数据处理与准备 - 为 Plotly Express 铺路,我们将探讨数据格式及其如何影响我们的分析与可视化。目前的结构可以通过新增“年份”列和“数值”列来改进,这样可以使其更加标准化并且便于分析。现在,由于我们专注于回调函数,我们将保持数据格式不变,以免分心。

现在我们用代码实现计划:

  1. 首先,让我们创建一个下拉列表。在这里,我们使用 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')
    
  2. 接下来,我们创建一个回调函数,该函数接收选定的国家,过滤数据集,并查找该国家在 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]
    
  3. 最后,让我们返回一个关于已找到数据的小报告。现在我们已经获得了感兴趣的人口数字,我们返回一个包含两个元素的列表。第一个是一个

    元素,使用大字体显示country变量。第二个是一个句子,其中包含两个动态值,这些值会被插入到相应的位置,您可以在以下代码片段中看到:

    return [
        html.H3(country),
        f'The population of {country} in 2010 was
    {population:,.0f}.'
    ]
    

请注意,由于我们已经在布局中有一个 div 元素,并且我们已指示要修改其children属性(该属性可以是单个值或列表),所以该函数的返回值可以简单地是一个列表(或单个值)。

我已将报告中的population值进行了格式化,以便更易读。冒号表示后面的字符串是我们希望的格式。逗号表示我们希望千位数用逗号分隔。点号表示如何格式化小数位数。点号后的零表示小数位数的数量,而f表示我们正在处理浮动数值。

现在,我们准备重构代码,以包含新的视觉元素和新功能。

接着,我们从上个版本的应用程序中继续,回顾第一章Dash 生态系统概述,下拉框和报告 div 应位于H2Tabs组件之间:

…
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 – 更新后的应用程序,包含下拉框和简单的人口报告

图 2.10 – 更新后的应用程序,包含下拉框和简单的人口报告

提示

app.run_server方法接受一个可选的port参数,默认为app.run_server(port=1234)。这同样适用于jupyter_dash

现在我们已经启用了回调函数并使其正常工作,我们终于可以开始使用右下角的蓝色按钮了!点击它,然后选择回调函数,会显示一个互动式图表,精确展示我们所指定的组件。国家及其,以及报告及其子元素图 2.11展示了这一点:

图 2.11 – Dash 可视化调试器的实际操作

图 2.11 – Dash 可视化调试器的实际操作

服务器按钮是绿色的,这意味着它运行正常。我们还可以看到“0 错误”提示。当你在一个运行中的应用中打开这个调试器,并修改组件时,你还可以看到回调的路径以及触发了什么。参与触发回调的组件会被高亮显示,这样你就可以“看到”发生了什么。在复杂情况下,这个功能会变得更加有用。图表中的节点也是互动式的,你可以通过放大/缩小整体图表来移动它们,这样就可以在任何你想要的地方进行缩放。而且,是的,这个图表是一个 Dash 应用,使用了 Dash 的另一个包。

中间的绿色矩形显示了两个有趣的数字。顶部显示的1告诉我们到目前为止该回调函数被触发了多少次。底部的数字显示运行该回调函数所花费的时间。这对于跟踪和分析性能非常有帮助。

到目前为止,我们只使用了单一值的输入(例如,不是列表)来修改输出。但如果我们想获取多个值并对其进行处理呢?如果我们想处理来自多个来源的值,例如,来自下拉框的值和日期呢?这些都可以通过 Dash 的回调函数实现。对了,我是不是提到过回调函数是 Dash 的核心?

我相信我们在这一章的编码已经足够了,我觉得现在回顾一下回调函数的强大功能、它们能做什么,以及它们的一些有趣属性是个不错的主意。这些只是目前需要牢记和了解的内容;我们将在后续章节中逐步探索每个功能是如何工作的。

Dash 回调函数的属性

让我们回顾一下 Dash 回调函数的属性,并介绍一些后面将更详细探讨的其他属性:

  • 多个输入:正如我刚才提到的,我们可以为回调函数提供多个输入,并创建更复杂的功能。以我们的数据集为例,我们可以轻松想象一个下拉框用来选择国家,另一个用来选择日期,再一个用来指定你想要分析的经济指标。这些输入可以用来过滤 DataFrame 的子集,并根据多个条件返回你需要的值。

  • Input):国家选择器可以设置为接受多个值,这样我们就可以循环遍历它们,并在一张图表中(或每个国家一张图表)展示多个国家在相同指标上的趋势。

  • 多个输出:与多个输入一样,多个输出也可以通过一个回调函数进行修改。在我们的示例中,我们可以设想产生两个输出——一个是可视化过滤后数据的图表,另一个是表格——为用户提供原始数据,如果他们想导出并进一步分析这个特定的子集。

  • 它们可以在返回之前做其他事情:我们主要关注回调函数作为简单的数据处理器,但它们实际上可以在返回之前做任何事情。例如,你可以想象一个函数在特定条件下发送电子邮件。日志记录是另一个有趣的探索方向。你只需要简单地记录传递给每个函数的参数。这可以让你洞察人们感兴趣的内容,哪些功能被使用等等。你甚至可以解析这些日志,并基于此开发你自己的独立分析应用程序!

  • 装饰器中的Input应对应my_function的第一个参数。我在前面的代码片段中使用了相同的名称,以使其明确且清晰(dropdowndate)。输出也适用相同的规则。

  • State。在我们迄今讨论的示例中,回调函数在值变化时会立即触发。有时,你可能并不希望这样。例如,如果你有多个输入,用户在配置选项时,如果每次变化都触发输出变化,可能会让用户感到烦恼。想象一下,用户输入的每一个字母都在修改页面上的另一个元素,这并不是最佳的用户体验。使用State的典型场景是按钮。用户选择或输入值后,一旦准备好,他们可以点击按钮,只有在此时才会触发回调函数。

图 2.12 展示了一个更复杂的回调函数的概念图及其可能的样子:

图 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属性提供的值取决于图表的类型。例如,对于散点图,你需要提供xy值;对于地图,你需要提供latlon。你可以将多个数据集叠加在同一个图表上,每个数据集被称为trace。每种图表类型可以接受许多其他可选值,很多内容将在本书后续部分详细介绍。

  • layout:所有与数据无关的内容都属于这个属性。layout属性的元素本质上更抽象,通常使用文本来告诉用户他们正在查看什么。许多元素也是样式元素,虽然它们可能不会添加太多信息,但可以使图表更易于理解或符合某些品牌指南。我们将探索许多属性,但最突出的属性是标题、轴标题、刻度和图例。它们又有子属性,例如字体大小、位置等。

通过实践学习要容易得多,我们现在可以开始创建我们的第一个图形。graph_objects模块通常作为go导入,我们通过调用go.Figure来实例化一个图形!图 3.1展示了一个空的Figure对象,以及如何创建和显示它:

图 3.1 – 在 JupyterLab 中显示的默认空 Figure 对象

图 3.1 – 在 JupyterLab 中显示的默认空 Figure 对象

当然,从这个空图形中我们什么也看不出来,但它是添加我们想要的元素之前的第一步。虽然我们可以通过在go.Figure调用中定义所有内容来创建并显示Figure对象,但我们将使用一种稍微更简单、更方便的方法。我们将创建的对象赋值给一个变量,然后迭代地添加和/或修改我们需要的元素。这个方法的一个重要好处是,创建图表后我们可以对其进行更改。

重要说明

一旦将Figure对象赋值给一个变量,这个变量就可以在全局范围内使用。由于它是可变的,您可以在代码的其他地方对其进行修改。对图表进行修改后,显示该图表将展示您所做的更改。我们将利用这个重要特性来管理我们的图表。

创建了基本对象后,我们现在可以开始将我们的第一个数据轨迹添加到我们的第一个图表中。

了解数据属性

首先,我们通过添加一个非常小且简单的数据集来开始绘制散点图。在本章的后面部分,我们将使用我们的贫困数据集来创建其他图表。一旦创建了Figure对象并将其赋值给一个变量,您就可以访问大量方便的方法来操作该对象。与添加数据轨迹相关的方法都以add_开头,后面跟着我们要添加的图表类型,例如add_scatteradd_bar

让我们一起走完整个散点图创建过程:

  1. 导入graph_objects模块:

    import plotly.graph_objects as go
    
  2. 创建一个Figure对象的实例并将其赋值给一个变量:

    fig = go.Figure()
    
  3. 添加一个散点轨迹。此类型图表所需的最小参数是xy值的两个数组。这些值可以通过列表、元组、NumPy 数组或 pandas Series提供:

    fig.add_scatter(x=[1, 2, 3], y=[4, 2, 3])
    
  4. 显示生成的图形。你可以简单地将变量放在代码单元的最后一行,它也会在 JupyterLab 中显示,一旦你运行它。你还可以显式调用 show 方法,这样可以提供更多选项来定制图形的显示方式:

    fig.show()
    

你可以在 图 3.2 中看到完整的代码以及最终的输出:

图 3.2 – 在 JupyterLab 中显示的散点图

图 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.xaxisfig.layout.yaxis 的标题。为了让你了解可用选项的详细程度,图 3.3 展示了以 tick 开头的一些 xaxis 属性:

图 3.3 – 一些 Figure 对象的 layout.xaxis 选项

图 3.3 – 一些 Figure 对象的 layout.xaxis 选项

现在让我们来看看我们刚刚添加的四行代码的效果:

图 3.4 – 更新后的图形,包含两条轨迹、一个图例和标题

图 3.4 – 更新后的图形,包含两条轨迹、一个图例和标题

我们添加的三个标题是显而易见的。新轨迹采用新的默认颜色来区分它们。另一个有趣的事情是图例,它是自动添加的。当你只有一条轨迹时,通常不需要图例,但当你有多条轨迹时,它就变得非常重要。当然,描述性名称至关重要,轨迹 0 并没有太多意义,但我将其保留作为记忆辅助,帮助记住图形元素的名称。

我们刚刚创建并显示的图形就是你的用户将看到的内容。现在让我们通过交互式的方式来查看这个图形的各个组件。

交互式探索 Figure 对象

正如我之前提到的,show 方法提供了一些便捷的选项,用于自定义图形的展示方式。一个特别有用的选项是将 renderer 参数设置为 JSON。图 3.5 展示了这一点是如何有用的:

图 3.5 – 在 JupyterLab 中交互式探索 Figure 对象

图 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 文件呈现在浏览器窗口中

图 3.6 – Figure 对象作为一个单独的 HTML 文件呈现在浏览器窗口中

将图形转换为图像

我们已经检查了允许用户手动下载Figure对象图像的选项。还有另一种编程方式,也很有趣。就像write_html方法一样,我们也有一个write_image方法。图像的格式可以显式提供,或者根据您提供的文件扩展名推断出来。您还可以设置heightwidth值。

这可能对大规模图像创建很有趣。例如,你可能想为每个国家创建许多图表,并将每个图表保存到单独的文件中,以便为每个国家生成单独的报告。手动完成这项工作会非常繁琐。你还可以将其作为用户回调的一部分。你可以允许用户生成某些报告,并点击一个按钮将其转换为图像并下载,例如。这可以像 HTML 转换器一样运行:

fig.write_image('path/to/image_file.svg',
                height=600, width=850)

有了这些信息,我们现在可以更实际地探索数据集,了解更多内容。

使用真实数据集进行绘图

第二章《探索 Dash 应用程序的结构》中,我们创建了一个简单的报告,展示了 2010 年所选国家的人口。此类报告用于用户已经知道他们想要什么的情况。也就是说,他们有一个关于特定国家、指标和时间段的具体问题,我们的功能提供了答案。

我们可以将仪表板功能分为两大类。第一类,就像我们已经做过的那样,是回答特定问题的可视化或报告。第二类,我们现在将要做的是引导用户进行更具探索性的操作。在这种情况下,用户对某个话题了解不多,他们正在寻找一个概览。

用户可以在这两种类型的图表之间来回切换。例如,首先,他们可以探索过去十年的贫困情况。某个特定地区突出显示。然后,他们会就该地区提出一个具体问题。当他们意识到该地区的另一项指标异常高时,他们可以转到该指标的另一个探索性图表,以了解更多信息。

现在,我们将让用户选择一个年份,应用程序将显示该年份按人口排序的前 20 个国家。

快速提醒一下,我们的贫困数据集包含国家及其代码、指标及其代码,以及从 1974 年到 2019 年的每一年的列。

按照约定,让我们首先在 JupyterLab 中的隔离环境中进行操作:

  1. 导入 pandas,用它打开贫困数据集,并将其分配给 poverty_data 变量:

    import pandas as pd
    poverty_data =
    pd.read_csv('data/PovStats_csv/PovStatsData.csv')
    
  2. 尽管关注的列名为 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'] 
    
  3. 创建 population_df,这是一个子集 DataFrame,其中的 regions 列,以及 pandas Seriesisin 方法检查 Series 中的值是否在某个列表中,而 ~(波浪符)是逻辑否定操作符:

    population_df = poverty_data[~poverty_data['Country
    Name'].isin(regions) & (poverty_data['Indicator
    Name']== 'Population, total')]
    
  4. 结果 DataFrame 的前几行可以如下显示:

    population_df.head()
    

    它看起来像以下屏幕截图:

    图 3.7 – population_df 的前几行

    图 3.7 – population_df 的前几行

  5. 创建一个动态的 year 变量,并创建一个包含国家列以及所选年份列的 year_df 变量。然后,将这些值按降序排序,并提取前 20 个:

    year = '2010'
    year_df = population_df[['Country Name',
    year]].sort_values(year, ascending=False)[:20]
    
  6. 有了一个包含两个排序列的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 个国家的条形图

图 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_idcomponent_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列表中,labelvalue键的值略有不同。不同之处在于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 – 一个带有下拉组件的应用,显示默认值和空图表

图 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.10 – 一个根据选择的年份显示柱状图的应用

如果你将此与图 3.8进行对比,你会注意到这里国家名称是垂直显示的,而之前它们是以一定角度显示的。之所以会这样,是因为图表显示在了更宽的浏览器窗口中。这是 Plotly 为我们处理的又一个便捷默认设置,我们不需要做任何事情。这意味着我们的图表具有响应式特性,使它们非常灵活。这对于我们使用 Dash Bootstrap Components 样式的应用和组件同样适用。

现在我们已经创建了一个可以独立运行的应用,接下来我们来看如何将其添加到我们的应用中。

向我们的应用中添加新功能

到目前为止,应用程序的最新版本包含一个Dropdown组件,在其下方是 2010 年人口报告的Div,在其下方是Tabs组件。现在,让我们在报告区域下方、Tabs组件上方插入新的DropdownGraph组件。我们还要添加新的回调函数:

  1. 复制这两个新组件,并将它们放到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([
    …
    
  2. 复制回调函数定义,并将其放置在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
    
  3. 在定义poverty_data之后,添加regions列表的定义,再添加population_df。顺序很重要,因为population_df依赖于先定义regions,而且它是poverty_data的子集,所以它也需要在poverty_data之后定义。这是这些变量需要定义的顺序:

    poverty_data = … 
    regions = …
    population_df = …
    

现在,如果我们运行应用程序,你可以看到它的样子,如图 3.11所示:

图 3.11 – 添加了新组件的应用程序(下拉框和图表)

图 3.11 – 添加了新组件的应用程序(下拉框和图表)。

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

图 3.12 – 视觉调试器中的应用回调函数

图 3.12 – 视觉调试器中的应用回调函数

现在我们的应用程序显示了更多信息。它允许用户从数据集中互动式地获取信息。我们定义了两个回调函数,并且有一个包含多种类型组件的布局。我们总共有大约 90 行代码。通过将新组件插入到某个位置,可以顺利地添加新组件,直到应用程序中的组件数量足够大。然后,我们将需要学习如何更好地组织代码并进行重构。

让我们用一个有趣且易于使用的 Plotly Figure对象的方面来结束本章,它不需要太多的编码,然后回顾一下我们所讨论的主题。

为你的图形设置主题

为你的图形设置主题(而不是你的应用程序)可能会很有趣,并且如果需要更改主题,这样做可以节省大量时间。这可以通过layout中的template属性进行访问和修改:

fig.layout.template = template_name

图 3.13 展示了四种不同的模板及其名称:

图 3.13 – 四种不同的图形模板

图 3.13 – 四种不同的图形模板

完整的模板列表可以在plotly.io.templates中找到。

这在你希望图形具有与应用主题兼容的模板时非常有用。它也是一个很好的起点,可以让你选择一个模板,并根据需要修改其中的一些元素。

现在让我们回顾一下本章中涉及的主题。

总结

我们从介绍Figure对象、其组件和子组件开始。我们逐步学习了如何创建图形,以及如何修改它们的各个方面。我们还深入了解了图形的两个主要属性——datalayout属性。我们还探索了几种图形转换的方法,接着我们基于数据集创建了一个图表,并将其集成到我们的应用程序中。

到目前为止,通过你所阅读的章节,你已经知道如何创建和构建应用程序,如何通过创建回调函数将不同的页面组件连接起来使应用互动,以及如何构建适应整个系统的图表。

现在你已经知道如何构建完全互动的应用程序,并且通过本章所学,你还知道如何管理图形的各个方面,并确保它们易于阅读,这样用户就可以花更多时间进行分析,减少理解图表本身的时间。

我们简要观察了数据准备和处理的重要性,现在我们准备更深入地探讨它。在下一章中,我们将介绍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.expressdata模块中,调用它们作为函数会返回相应的数据集。让我们来看看著名的 Gapminder 数据集:

import plotly.express as px
gapminder = px.data.gapminder()
gapminder

运行此代码将显示gapminder DataFrame 的示例行,如图 4.1所示:

图 4.1 – Plotly Express 中包含的 Gapminder 数据集

图 4.1 – Plotly Express 中包含的 Gapminder 数据集

数据集结构看起来很简单。对于每一个独特的国家大洲年份的组合,我们有三个指标:lifeExppopgdpPercapiso_alphaiso_num列似乎是国家的编码值。

让我们看看如何通过scatter图来总结gapminderdata_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 数据集交互式图表

图 4.2 – 使用 Plotly Express 展示的 Gapminder 数据集交互式图表

我对这个过程的第一个观察是,描述图表所需的文字比代码多得多。实际上,只需要一行代码就能生成它。

点击播放按钮将动画化图表,每年会显示一个新帧。如果需要,你也可以暂停或跳转到某一年。这样你可以看到两个变量之间的关系如何随着年份推移而变化,就像看一部短片电影。

你还可以看到,当你悬停在表示某个国家的标记上时,会显示出所有相关数据,这些数据用于指定位置、大小、颜色以及我们可能设置的其他属性。hover_name 参数被设置为 'country',这就是为什么你看到它以粗体显示作为标签的标题。

在大多数情况下,我们有标记重叠,这使得理解图表变得困难。由于 Plotly 图形默认是交互式的,我们可以轻松使用模式栏按钮进行缩放,或者可以手动选择一个矩形进行放大。

通过选择仅包含非洲标记的矩形来放大非洲,图 4.3 展示了图表的变化,现在阅读非洲子图变得更容易了:

图 4.3 – 放大图表中的特定区域

图 4.3 – 放大图表中的特定区域

请注意,其他大陆的图表也被放大到与非洲相同的缩放级别。可以自由探索更多交互式功能,但我希望这能展示出这种方法的强大和直观。

提示

本章中有许多彩色图表。我尽力确保你可以尽可能容易地区分不同的彩色标记。如果你正在阅读打印版,最好参考该书的彩色版本,该版本可以在线访问。

我们能够用一行代码创建如此丰富图表的原因有两个。首先,Plotly Express 拥有强大的功能,专门设计用来通过最少的代码生成这样的图表。稍后会详细介绍这一点。第二,数据集的结构在这个过程中起着重要作用。一旦我们的数据具有一致的格式,就很容易进行建模、可视化或进行任何类型的分析。

让我们来看看这种数据格式的主要方面。

长格式(整洁)数据的主要属性

该结构的一个关键特点是,它允许图表上的每个标记都通过一行独立表示。这些行中的每个值属于一个不同的列。反过来,这些列每个代表一个独立的变量,并具有自己的数据类型。这使得我们可以轻松地映射颜色、大小或任何其他视觉属性,只需声明我们希望用哪个视觉属性来表示哪个列的值。

请注意,我刚才说的内容接近 DataFrame 的定义:

  • 一组列,每列只有一种数据类型。

  • DataFrame 中的列可以是不同类型的。

  • 所有列的长度相同,即使它们可能包含缺失值。

从概念角度来看,长格式 DataFrame 和常规 DataFrame 之间的主要区别是每行包含一个观察值(例如:国家、个人、品牌或它们的组合),而每列包含一个变量(例如:人口、大小、长度、身高、收入等)。例如,国家列只包含国家信息,且该列中只会出现国家数据。因此,对于这些数据的访问不会产生任何歧义。

这种格式不是必需的,也不比其他格式更“正确”。它只是直观、一致且易于使用。我们刚刚制作的可视化的实际要求是:需要为 X 轴准备一组数值,Y 轴需要另一组相同长度的数值。对于其他特性,如颜色和大小,我们也需要相同长度的数字或名称集合,这样才能将它们正确地映射在一起。DataFrame 是这种需求的自然匹配。

在我们刚刚生成的图表中,你可以很容易地看到,我们可以通过移除size参数让所有标记保持相同的大小。将facet_col改为facet_row会立即将子图垂直堆叠,而不是并排显示。通过微小的调整,我们可以对可视化进行大幅改变。这就像在仪表盘上切换开关一样简单,带有一点幽默感!

我希望最终目标现在已经清楚了。我们要检查数据集中的四个文件,并查看如何生成长格式(整洁型)DataFrame。这样,每一列将包含关于一个变量的数据(例如:年份、人口、基尼指数等),而每一行则描述一个观察值(国家、年份、指标以及其他值的组合)。完成这些后,我们应该能够查看数据,指定我们想要的内容,并通过简洁的 Plotly Express 函数调用表达出来。

一旦开始准备过程,整个过程会更加清晰,所以我们现在就开始吧。

理解数据操作技能的作用

在实际情况下,我们的数据通常并不是我们希望的格式;我们通常有不同的数据集需要合并,而且常常需要对数据进行规范化和清理。正因如此,数据操作和准备将在任何数据可视化过程中发挥重要作用。因此,我们将在本章以及全书中重点关注这一点。

准备数据集的计划大致如下:

  1. 一一探索不同的文件。

  2. 检查可用的数据和数据类型,探索每种数据如何帮助我们对数据进行分类和分析。

  3. 在需要的地方重新塑形数据。

  4. 合并不同的 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 文件的前几行和列

图 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 – 限制和例外的计数与唯一值

图 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 – 来自国家文件的样本行和列

图 4.6 – 来自国家文件的样本行和列

在调用read_csv时,我们指定了keep_default_na=Falsena_values=''。原因是pandas将像NANaN这样的字符串解释为缺失值的指示符。纳米比亚这个国家有一个NA,因此它在 DataFrame 中缺失了。这就是我们需要进行此更改的原因。这是一个非常好的例子,说明事情可能以意想不到的方式出错。

这是关于我们数据集中国家和地区的非常有趣的元数据。它是一个非常小的数据集,但可以在丰富我们理解的同时,非常有助于提供更多的过滤和分组国家的选项。它也是长格式(tidy)。让我们看一看其中一些有趣的列。

Region 列似乎很直观。我们可以检查有哪些区域可用,以及每个区域内国家的数量:

country['Region'].value_counts(dropna=False).to_frame().style.background_gradient('cividis')

结果可以在图 4.7 中看到:

图 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 列的国家和地区样本

图 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 – 显示国家及其国旗的行样本

图 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 – 脚注文件中的行样本

图 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 – 数据文件中的行和列样本

图 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 – 包含相同信息的两个数据集,采用两种不同的格式图 4.12 – 包含相同信息的两个数据集,采用两种不同的格式

图 4.12 – 包含相同信息的两个数据集,采用两种不同的格式

我们当前的 DataFrame 结构如右侧的表格所示,使用左侧那种格式会更加方便。

宽格式的难点在于变量的呈现方式不同。在某些情况下,变量是垂直显示在一列中(国家指标),而在其他情况下,它们是水平显示在 20152020 等列中。访问长格式 DataFrame 中相同的数据非常简单:我们只需指定想要的列。此外,我们可以自动映射值。例如,从长格式 DataFrame 中提取 yearvalue 列时,系统会自动将 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 – 数据框在被“熔化”后的样子

图 4.13 – 数据框在被“熔化”后的样子

前四列与之前相同,每个唯一的组合保持不变。现在,我们将所有年份列及其值压缩成了两列,yearvalue

现在让我们看看如何通过对其他列执行逆操作进一步改进结构。

数据框透视

指标名称列可以通过对我们刚才对年份列进行的操作的逆操作来改进。理想情况下,我们应该为人口、贫困率等分别创建不同的列。让我们首先使用我们的长格式(已“熔化”)示例数据框来演示,以便更清楚地理解。

假设我们想要使用pivot方法转换唯一值。这样可以通过使用melt方法实现“回程”,返回到原来的格式。这里,我正在对不同的列使用它:

melted.pivot(index=['year', 'indicator'],
             columns='country',
             values='value').reset_index()

运行此代码将把“熔化”后的数据框转换为宽格式(透视)数据框,您可以在图 4.14中看到:

图 4.14 – 从长格式到宽格式的转换图 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 – 长格式(整洁)贫困数据框

图 4.15 – 长格式(整洁)贫困数据框

如果我们的工作是正确的,那么每一行现在应该有一个唯一的国家和年份的组合。这实际上就是这个练习的核心。让我们来检查一下我们的工作是否正确:

data_pivot[['Country Code', 'year']].duplicated().any()
False

现在,行中包含了国家名称、代码和年份,以及所有不同指标的值。通过将country数据框中的元数据包含在内,国家信息可以得到丰富。我们来看一下merge函数,之后我们将开始使用 Plotly Express。

合并数据框

首先,让我们看一个简单的示例,了解合并是如何工作的,然后我们可以合并data_pivotcountry数据框。图 4.16展示了如何将两个数据框进行合并:

图 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_pivotcountry

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

图 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

图 4.18 – 一个简单的示例 DataFrame

我们通常通过调用图表类型函数来使用 Plotly Express,例如 px.linepx.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 创建散点图

图 4.19 – 使用 Plotly Express 创建散点图

我敢肯定你已经注意到,X 轴和 Y 轴的标题已经由系统默认设置。它会使用我们提供的参数名称(在这个例子中是数据框列名)来设置这些标题。

我们的数据框中还有其他变量,我们可能有兴趣检查它们之间是否存在任何关系。例如,让我们检查浮动形状之间是否有关系。

我们可以重新运行相同的代码,并添加两个参数,使我们能够区分哪些标记属于哪个形状。我们可以使用color参数来做到这一点,系统会根据symbol参数为每个标记分配不同的颜色,以便轻松区分它们。这也使得彩屏的读者更容易理解,因为通过提供两个信号来区分标记:

Px.scatter(df,
           x='numbers',
           y='floats',
           color='shapes',
           symbol='shapes')

图 4.20 显示了在 JupyterLab 中的代码和结果图:

图 4.20 – 为标记分配颜色和符号

图 4.20 – 为标记分配颜色和符号

请注意,我们有一个图例帮助我们区分标记,告诉我们哪个颜色和符号属于哪个形状。它还拥有自己的标题,所有这些都是默认生成的。

似乎浮动和形状之间没有关系。那么,我们来尝试根据字母列来上色并设置符号,方法是使用以下代码:

px.scatter(df,
           x='numbers',
           y='floats',
           color='letters',
           symbol='letters',
           size=[35] * 8)

图 4.21 演示了这一点:

图 4.21 – 使用独立列表设置标记大小

图 4.21 – 使用独立列表设置标记大小

我们现在可以根据字母看到明显的差异。这展示了通过快速尝试不同的选项来探索数据集是多么容易。请注意,这次我们还混合了方法,给标记设置了size。大小没有映射到某个值,它是为了让符号更大、更容易看见。因此,我们只是传递了一个包含我们想要的标记大小的列表。这个列表的长度必须与我们要可视化的其他变量相同。

让我们用相同的方法和相同的数据集来探索条形图。我们可以通过barmode参数调整条形的显示方式,像这样:

px.bar(df, x='letters', y='floats', color='shapes', barmode='group')

图 4.22 展示了两种不同的条形显示方式——默认方式是将条形叠加在一起,而 "group" 方式则是将条形分组显示,正如你所看到的:

图 4.22 – 使用不同显示模式(barmode)创建条形图图 4.22 – 使用不同显示模式(barmode)创建条形图

图 4.22 – 使用不同显示模式(barmode)创建条形图

关于长格式(整洁格式)数据的讨论应该能让你非常容易理解如何使用 Plotly。你只需要对图表类型及其工作原理有基本了解,然后你就可以轻松设置你想要的参数。

重要提示

Plotly Express 不要求数据必须是长格式的。它非常灵活,可以处理宽格式、长格式以及混合格式的数据。此外,pandasnumpy在数据处理上非常灵活。我只是认为,为了提高个人生产力,最好使用一致的方法。

现在让我们看看 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

  1. 创建 yearindicator 和一个分组(grouper)度量变量用于可视化。分组度量将用于区分标记(通过颜色和符号),可以从数据集中提取任何类别值,如地区、收入组等:

    year = 2010
    indicator = 'Population, total'
    grouper = 'Region'
    
  2. 基于这些变量,创建一个 DataFrame,其中 year 列等于 year,按 indicator 排序,并移除 indicatorgrouper 列中的任何缺失值:

    df = (poverty[poverty['year'].eq(year)]
          .sort_values(indicator)
          .dropna(subset=[indicator, grouper]))
    
  3. x 轴的值设置为 indicator,并将 y 轴的值设置为 "Country Name" 列。标记的 colorsymbol 应使用 grouper 设置。X 轴值预计会有异常值,并且不是正态分布的,因此将 log_x 设置为 True。每个悬浮标签的 hover_name 应包含国家名称及其国旗。将图表的 title 设置为 indicator、"by"、grouperyear 的组合。给标记一个固定的 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 图表

图 4.23 – 使用贫困数据集的 Plotly Express 图表

通过简单地玩弄 yeargrouperindicator 的不同组合,你可以生成数百个图表。图 4.24 展示了一些示例:

图 4.24 – 使用相同数据集的其他图表图 4.24 – 使用相同数据集的其他图表

图 4.24 – 使用相同数据集的其他图表

借助这些强大的功能,以及将数据按变量组织为观测值的格式,我们可以通过几种视觉属性轻松地可视化数据的六个或七个属性:X 轴、Y 轴、标记大小、标记符号、标记颜色、面板(列或行)和动画。我们还可以使用悬停标签和注释来增加更多的上下文和信息。通过选择将哪个列映射到哪个属性,我们可以简单地探索这些属性的任何组合。

现在让我们来探索一下将外部资源轻松地加入到我们的数据集中有多简单。

向我们的数据集添加新数据和列

有很多方法可以添加更多数据,但我想突出介绍两种非常简单且有效的方法:

  • pandasread_html函数可以下载网页上的所有表格,你可以非常轻松地下载任何此类列表。假设它包含国家代码,你可以将其与主数据框合并,然后开始相应地分析。这也可以是一个过滤机制,你只需要所有国家中的一个子集。

  • 添加新数据:世界银行拥有成千上万的类似数据集。例如,我们这里的人口数据是总人口数。还有很多详细的、按性别、年龄和其他因素划分的人口数据集。通过世界银行的 API,你可以轻松获取其他数据,合并数据,并立即丰富你的分析。

现在让我们回顾一下我们在本章和本书的第一部分中做了什么。

总结

现在你已经掌握了足够的信息,并且看到了足够的示例,可以快速创建仪表板。在第一章《Dash 生态系统概览》中,我们了解了应用程序的结构,并学会了如何构建完整运行的应用程序,但没有交互性。在第二章《探索 Dash 应用程序的结构》中,我们通过回调函数探索了交互性的工作原理,并向应用程序添加了交互功能。第三章《使用 Plotly 的图形对象》介绍了 Plotly 图表的创建方法、组成部分以及如何操作它们以获得所需的结果。最后,在本章中,我们介绍了 Plotly Express,这是一个易于使用的高层接口,最重要的是,它遵循一种以数据为导向的直观方法,而非以图表为导向的方法。

创建可视化的最重要和最大部分之一是将数据准备为特定格式的过程,之后创建这些可视化就变得相对简单。投资于理解数据集的结构,并投入时间和精力来重塑数据,最终会带来丰厚回报,正如我们在本章的详细示例中所看到的那样。

凭借这些知识和示例,以及我们对数据集的熟悉和丰富它的简单机制,我们现在准备更详细地探索不同的 Dash 组件以及不同类型的图表。

第二部分将深入探讨不同的图表类型、如何使用它们,以及如何将它们与 Dash 提供的交互功能结合的不同方式。

第二部分:使用真实数据为您的应用添加功能

本节将向您展示如何开始使用真实数据进行构建,并探索如何利用 Dash 的全部交互选项。

本节包括以下章节:

  • 第五章*,通过条形图和下拉菜单进行交互式比较值*

  • 第六章*,使用散点图探索变量并通过滑块过滤子集*

  • 第七章*,探索地图图表并通过 Markdown 丰富您的仪表板*

  • 第八章*,计算数据频率并构建交互式表格*

第五章:第五章:使用条形图和下拉菜单进行交互式值比较

现在,你已经掌握了构建交互功能并链接页面元素的所有基本知识,能够轻松制作交互式仪表板。主要概念已经通过多个示例进行了介绍,接下来我们将专注于特定类型的图表及其提供的不同选项。更重要的是,我们将深入探讨如何定制图表,使其适应多种用途。首先,要确保它们足够好,能够发布并与更广泛的受众分享,而不仅仅是为了你的交互使用;其次,要确保它们能够适应可能包含其他组件的页面,并确保我们能够以最优化的方式利用可用空间。另一个需要讨论的重要方面是图表的动态特性,用户可以生成的图表会根据选择的交互组件选项,可能包含 7 个,甚至 70 个元素来绘制。在某些情况下,数据集可能不包含任何数据。这会极大地影响最终图表的效果和可用性,甚至在某些情况下可能会使它们难以阅读。我们将探讨几种解决方案,以应对这些情况。

换句话说,我们正在尝试从使用一个仅仅完成其预定功能的原型,转向使用一个可以共享或发布给广泛受众的产品。

本书第二部分的各章节将重点介绍一种图表类型和一个交互组件,以探索它们的选项。在本章中,我们将探索条形图及其如何与下拉菜单组件(来自Dash Core Component)结合使用。这些组件的性质并没有将某一特定组件与某种图表类型直接关联。它们只是为了组织目的一起使用。下拉菜单可以与任何类型的图表一起使用,任何类型的交互组件也可以用来操作条形图。

我们将专注于以下主题:

  • 垂直和水平绘制条形图

  • 将条形图与下拉菜单链接

  • 探索显示多个条形图的不同方式(堆叠、分组、叠加和相对)

  • 使用面板将图表拆分成多个子图——水平、垂直或包装式

  • 探索下拉菜单的其他功能(允许多选、添加占位符文本等)

技术要求

我们将继续使用我们现在熟悉的包——JupyterDashDash,分别用于原型设计和集成到我们的应用程序中。对于数据处理,我们将使用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:

  1. 导入pandas并创建series变量。我们根据文件名选择了该变量名,正如在上一章中所做的那样。请不要将其与pandas.Series对象混淆:

    import pandas as pd
    series = pd.read_csv('data/PovStatsSeries.csv')
    
  2. 创建一个名为gini的变量,作为使用指标长名称的简化替代方式:

    gini = 'GINI index (World Bank estimate)'
    
  3. 使用同名列提取指标的详细定义:

    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.
    
  4. 知道这些值的范围在 0 到 100 之间,让我们检查所有年份和国家的最极端值:

    poverty[gini].min(), poverty[gini].max()
    (20.2, 65.8)
    
  5. 我们还可以通过pandasdescribe方法来更好地了解这一列:

    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栏目的这句话让我印象深刻:“因为基础的家庭调查在方法和收集的福利衡量标准上有所不同,所以不同国家之间,甚至同一国家不同年份之间的数据不能严格比较。

所以,我们必须小心不要完全依赖这些值,并且记住这个限制。

现在我们对该指标稍微熟悉一些,准备探索使用条形图可视化它的各种选项:

  1. 我们首先创建一个名为 dfpoverty 数据框的子集。这个子集将包含 year 值等于任意选择的年份的数据。然后,我们去除缺失值,并使用 gini 列对可用数据进行排序:

    year = 1980
    df =\
    poverty[poverty['year']==year].sort_values(gini).dropna(subset=[gini]) 
    
  2. 我们现在可以轻松地使用 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 年的基尼指数条形图

图 5.1 – 1980 年的基尼指数条形图

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

图 5.2 – 1990 年的基尼指数条形图

图 5.2 – 1990 年的基尼指数条形图

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

图 5.3 – 1990 年的基尼指数条形图,国家名称垂直显示

图 5.3 – 1990 年的基尼指数条形图,国家名称垂直显示

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

图 5.4 – 2010 年的基尼指数条形图,其中部分国家名称未显示

图 5.4 – 2010 年的基尼指数条形图,其中部分国家名称未显示

根据我们刚才看到的图表,我们现在对动态生成的交互式基尼指数水平条形图的挑战有了更好的理解。如果我们希望用户选择感兴趣的年份,那么我们需要处理一些问题。

首先,这个指标的可用值的数量从 3 到 150 多个,范围非常大。其次,最好且更安全使用水平排列,因为在所有情况下,国家名称将水平显示,无论名称多长,都容易阅读。通过在调用px.bar时设置orientation='h',这些问题可以轻松解决,但仍然有一个挑战。我们需要根据所选年份中可用国家的数量来确定图表的最佳高度,正如我们刚才看到的那样,范围有多大。我们首先看一下当图表显示为水平时的样子,然后设计一个交互式的解决方案。我们将运行相同的代码,但有两个主要区别。xy参数需要交换,因为它们将分别取反方向的轴,并且我们还需要为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 年所有可用国家的基尼指数水平条形图

图 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 – 基于国家数量动态高度的各种水平条形图

图 5.6 – 基于国家数量动态高度的各种水平条形图

右侧的长图已调整大小以适应页面,但在条形的高度和国家名称的可读性方面与其他图表基本相同。所有国家都清晰可见,易于阅读,且没有任何内容被隐藏。

通过这个解决方案,我们通过根据国家数量动态设置图形的总高度,处理了可以选择的国家数量的动态变化。

这种方法可以看作是一种探索方法。用户并不确切知道自己在寻找什么;他们选择一个年份,并查看该年份可用的所有数据。在选择了一些选项后,他们可能有兴趣深入了解某些国家的具体信息。例如,他们可能对某个国家的基尼指数随时间的变化感兴趣。接下来我们将实现这一点。

创建具有多个值的竖条图

当我们想让用户可视化一个国家的基尼指数(或任何其他指标)如何随时间变化时,我们可以通过竖条图来实现。因为年份代表了一系列事件,将它们并排显示是自然的,因为这显示了随时间变化的趋势。而且,由于年份是一个数字序列,我们没有像国家名称那样的可读性问题。即使条形图变得更窄,甚至某些条形图没有显示出来,用户也能轻松地在脑海中“填补空白”,在需要的地方理解数据。

生成这样的图表的代码与上一个非常相似,实际上更简单,因为我们不需要担心动态设置高度。我们将使用Country Name作为动态变量,而不是yeardf的定义将取决于数据集中包含所选国家的行:

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 轴的竖条图

图 5.7 – 带有年份作为 x 轴的竖条图

请注意,即使某些年份没有对应的值,这些年份仍然会出现在x轴上,即便这些年份没有条形图显示其值。这一点很重要,因为它展示了我们数据中的空缺。如果我们只显示包含数据的年份,这会产生误导性,给人一种所有年份都有连续数据的错误印象。

我们已经稍微熟悉了基尼指数数据,并测试了如何制作两种类型的动态图表。接下来,我们准备创建一个“基尼指数”部分并将其添加到我们的应用中。

连接条形图和下拉框

现在我们要将迄今为止所做的工作整合起来。计划是将两个下拉菜单并排放置,每个下方都有一个图表。第一个下拉菜单将提供年份选项,生成一个横向条形图。第二个下拉菜单将根据所选国家生成一个纵向条形图。最终目标是生成一个新的应用部分,类似于 图 5.8

图 5.8 – 应用中的 Gini 指数部分,包含两个下拉菜单组件和两个条形图

图 5.8 – 应用中的 Gini 指数部分,包含两个下拉菜单组件和两个条形图

首先,我们在 JupyterLab 中构建一个完整独立的应用,并确保其按预期工作:

  1. 我们首先运行必要的导入并实例化应用。我们已经覆盖了所有这些导入,除了 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__)
    
  2. 创建 gini_df,它是 poverty 的一个子集,其中的 Gini 指数列没有缺失值:

    gini_df = poverty[poverty[gini].notna()]
    
  3. 使用一个顶层 div 创建应用的布局,在其中我们将放置所有其他组件:

    app.layout  = html.Div()
    
  4. 在我们刚刚创建的 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')
        ])
    ])
    ]
    
  5. 上述代码应该能够处理布局,当我们将其插入顶层 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
    
  6. 我们还可以做同样的事情,创建另一个回调函数来处理 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
    
  7. 最后,我们运行应用:

    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展示了我们在可视化两个变量ab时可以采用的不同方式:

![图 5.9 – 使用不同“barmode”选项显示多个条形图图 5.9 – 使用不同“barmode”选项显示多个条形图前面图表中的四个条形图显示了相同的信息,但以不同的方式呈现。这可以通过设置barmode参数来实现。使用relative时,条形图会相互叠加,负值显示在下方,正值显示在零线以上。如果设置为group,条形图会并排显示。使用overlay时,条形图会彼此重叠,默认情况下,条形图具有一定透明度,以便我们能看到两者。最后,如果设置为stack,条形图会像 relative 一样叠加在一起,但在这种情况下,负值会抵消正值,就像你在最后一个图表中看到的 2 和 4 的数值一样。如果你想比较每个值相对于总数的比例,特别是当它们的总和相同的时候,这是一个很好的方法。这正是我们将用来处理我们数据集中收入分配指标的方式。## 创建收入分配数据框让我们来看一下这五列,显示了各国人口五个五分位的收入分配份额。我们首先从poverty数据框中创建一个子集,并命名为income_share_df。这是通过使用这里展示的正则表达式过滤列来实现的。我们还删除了缺失值:pyincome_share_df =\poverty.filter(regex='Country Name|^year$|Income share.*?20').dropna()income_share_df运行前面的代码,我们可以看到来自新创建的income_share_df的一些样本行,如图 5.10所示:图 5.10 – 显示各国收入分配五分位的样本行

图 5.10 – 显示各国收入分配五分位的样本行

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

图 5.11 – 每个五分位的收入份额,按所选国家和所有可用年份显示

图 5.11 – 每个五分位的收入份额,按所选国家和所有可用年份显示

由于这些值的总和为 100(仅有微小的舍入误差),因此我们可以清晰地比较各年份之间的柱状图,因为它们的总长度相同。由于这些是比例,我们关心的是查看某一特定年份的分布情况,以及这些分布如何随年份变化。

如你所见,对于柱形图的最右边和最左边部分,我们可以很容易地看出它们的变化,因为它们有相同的基准线,不管是开始还是结束。但是对于中间的值,就不容易做到这一点。原因在于它们的大小和基准线都在变化。所以,增加更多的分段就会使得跨年份的比较变得更加困难。但由于 Plotly 的图表是互动式的,用户可以轻松地将鼠标悬停在柱子上,获取其准确值并进行比较。

生成此图表应该是直接的。我们已经创建了 DataFrame 并获得了我们的值。我们只需要设置 xy 值,并设置 orientation='h',但是问题在于,DataFrame 中的类别是按字母顺序排序的,而我们希望它们按照数值含义排序,从最小到最大,以便用户能够轻松理解它们的相对位置。像往常一样,这主要是一个数据处理挑战。那么,我们来解决它:

  1. 我们首先需要重命名列,并按照其值从“最低”到“最高”排序。实现这一点的一种方法是为列名前加上数字,并按此排序。这可以通过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)
    
  2. 检查我们的工作是否正确:

    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')
    
  3. 我们现在要去除列中冗余的部分,并保留位置指示符和“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
    ]
    
  4. 接下来,我们创建一个变量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')
    
  5. 现在,我们的 DataFrame 已经准备好绘图,且名称简短且适当。我们首先创建一个country变量,用于过滤 DataFrame:

    country = 'China'
    
  6. 使用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()
    
  7. 你可能注意到我将结果赋值给了一个变量figure,这是因为我们还有一些细节需要改进。运行前面的代码会生成图 5.12中的图表:图 5.12 – 按五分位显示的收入份额,使用默认选项

    图 5.12 – 按五分位显示的收入份额,使用默认选项

  8. x轴的标题,fig.layout,以及设置它们非常简单。请注意,图例有xy属性来设置其在图中的位置。我们将图例的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
    
  9. 对印度尼西亚运行前面的代码会生成图 5.13中的最终图表:

图 5.13 – 按五分位显示的收入份额,使用自定义选项

图 5.13 – 按五分位显示的收入份额,使用自定义选项

现在,让我们把一切整合在一起,并将新功能添加到我们的应用程序中。

将功能集成到我们的应用程序中

我们现在准备好再次将新功能添加到我们的应用程序中,使用我们刚刚创建的函数和图表。在这个阶段,不需要太多解释,因为我们已经做过很多次了,但我会回顾一下主要步骤,你可以随时参考代码库来检查你的工作:

  1. 在模块的顶部,我们首先进行数据框架的定义,以及列的更改,就像我们之前做的那样。确保以下代码放在创建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]
    
  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')
    
  3. 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_colfacet_row参数即可。就是这样。

让我们快速查看通过查看相关的分面参数可用的选项:

  • facet_col:这意味着您希望将图表拆分为列,并且所选列名将用于将它们拆分。这将导致图表并排显示(作为列)。

  • facet_row:类似地,如果您希望将图表拆分为行,您可以使用此参数,它将把图表拆分为在彼此上方显示的子图表。

  • facet_col_wrap:当您需要生成动态数量的分面时,这真的非常有用。如果您知道用户将生成多个图表,那么在多少个图表之后,应该在生成的图表网格的下一行中显示下一个图表?答案应该是一个整数,并且 Plotly Express 确保在此数字之后,图表在下一行中显示。这确保对于每一行,我们有一个最大数量的图表列。

  • facet_row_spacingfacet_col_spacing:正如它们的名称所示,您可以通过设置这些值来控制行和列之间的间距,范围为 [0, 1],作为总图大小的分数,水平或垂直。

让我们运行一个快速示例以确保这一点清楚:

  1. 创建一个国家列表以进行过滤:

    countries = ['Algeria', 'Japan']
    
  2. 修改df的定义以过滤掉'Country Name'countries中的行。可以使用pandas方法isin来实现此目的。

    df =\
    gini_df[gini_df['Country Name'].isin(countries)].dropna(subset=[gini])
    
  3. 运行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 参数生成的两个条形图

  4. 如您所见,扩展我们的图表非常容易,我们还为子图标注了正确的国家名称。虽然它已经不错了,但仍然不如我们希望的那样完美。y轴标题重叠,而且您必须仔细查看垂直标题才能知道哪个子图属于哪个国家。所以让我们来改进一下。首先,我们可以通过修改labels参数,提供一个字典,并将默认名称映射为我们想要的新名称,来修改y轴标题:

    labels={gini: 'Gini Index'}
    
  5. 我们还可以通过为条形图按国家着色来帮助用户快速识别图表。这样会使它们更具辨识度,同时生成带有颜色引导的图例,使图表更容易区分。同样,这只需通过为color参数提供一个参数来完成,这基本上是选择我们想用来标识的列名:

    color='Country Name' 
    
  6. 另一个有用的功能是为整个图形添加动态标题。我们可以显示完整的指标名称,下面列出已选择的国家名,以逗号分隔。Plotly 注释支持一些 HTML 标签,我们将使用<br>标签来分隔指标名称和国家列表,如下所示:

    title='<br>'.join([gini, ', '.join(countries)])
    
  7. 在图表上显示两个国家很容易阅读,但如果用户决定选择七个国家呢?正如我们在吉尼指数水平条形图的动态高度中所做的那样,我们还需要根据所选国家的数量,为分面条形图设置动态高度。我们将使用相同的技术,但使用不同的值,因为我们在管理子图,而不是水平条形图:

    height=100 + 250*len(countries)
    
  8. 完整的更新代码可以在这里看到:

    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))
    
  9. 最终的图表可以在图 5.15中看到,显示了三个国家:

图 5.15 – 使用facet_row参数和自定义选项生成的三个条形图

图 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 – 没有占位符文本的下拉框

图 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 – 带有占位符文本和标签的下拉框

图 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 区域是位于xy轴之间的内部矩形。在我们所有生成的图表中,这个区域的颜色是浅蓝色的(或者如果你正在阅读印刷版的书籍,它是灰色的)。

包围较小矩形的大矩形是“纸张”区域。在我们到目前为止生成的所有图表中,它的颜色是白色的。我们也可以将其颜色设置为相同的颜色,这样就可以使我们应用程序的所有背景颜色一致。我们只需要在生成图表的回调函数中添加以下代码行:

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 – 应用程序的最终布局

图 5.18 – 应用程序的最终布局

我强烈建议您自己手动进行这些更改,而不是查看代码库中的代码。我还鼓励您测试其他布局,尝试不同的选项,并生成大量的错误!

不断地进行更改并控制你的代码需要一致的组件、回调函数和变量命名规范。如果你能遵循一定的逻辑流程来组织你的组件,那将会非常有帮助。我们会多次进行这些操作,希望你能在这个过程中掌握这些技巧。

我们的应用现在看起来更好,使用起来更方便,如果你愿意,还可以与他人分享。在这一章中,我们涵盖了许多内容,将我们的应用提升到一个新层次,所以让我们回顾一下我们所涉及的内容。

总结

本章我们主要关注了条形图。我们还以多种方式使用了Dropdown组件。我们了解了使用横向和纵向布局的优缺点,并且在应用中实现了这两种布局。接着,我们分析了将多个条形图一起展示的不同方式,并实现了一个展示总值比例的图表。然后,我们探讨了面板(facets),看到了它们如何丰富我们的图表,并使它们更加灵活和可扩展。我们还将其与下拉框(dropdowns)结合,使得用户可以进行多重选择。确认一切正常工作后,我们通过选择一个新主题并确保所有背景颜色一致来为我们的应用进行外观更新。我们还通过为不同的屏幕尺寸设置不同的图表大小来管理不同屏幕大小下的布局。最后,我们为应用添加了一些有用的提示信息,以提升用户体验。最后,我们截取了结果应用的屏幕截图!

在下一章中,我们将探讨一种最常见的图表类型——散点图。我们还将学习如何将其与滑动条(sliders)结合使用,允许用户选择和修改数值或数值范围。