Dash-手册-全-

Dash 手册(全)

原文:zh.annas-archive.org/md5/e36c3fa9bff3aaf20fa5e7cc5d7d30e5

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

有人说信息就是力量,其他人则宣称数据是新的黄金。但是,原始信息和数据在没有背景的情况下往往是没有意义的。只有在适当分析、解读和理解的情况下,数据才是有价值的资产。因此,新的领域不断涌现。数据科学家、数据工程师、数据分析师、商业智能顾问和机器学习工程师都是越来越受欢迎的职业,它们都有一个共同的职责:使用图形和仪表盘等可视化工具来解读数据。

本书的目标是帮助你创建美观的仪表盘应用,让你能够用几行代码就能可视化数据。无论你是一个有抱负的专业人士,定期处理数据,还是仅仅喜欢玩数字,本书将为你提供工具和教育,帮助你充分利用数据的力量。

为什么是这本书

Plotly Dash 框架使你能够轻松构建自己的仪表盘。仪表盘是网络应用,允许你和你的用户通过交互式小部件动态地探索数据,这些小部件接收用户输入并展示输出。例如,它们可能包含滑块、文本框、按钮和下拉菜单,允许用户选择要在结果图表和图形中显示的数据,就像图 1 中你看到的地图和条形图。仪表盘应用的交互性是它们近年来日益流行的原因。

图 1:Google Analytics 仪表盘用于追踪网站使用情况

如果没有正确的指导,构建一个 Plotly Dash 应用可能会很困难。本书将教你如何使用实用的指引和易懂的教程创建第一个交互式、具有高度可视化的分析仪表盘应用。它将从 Dash 新手带领你快速、充满信心地创建自己的仪表盘应用。

阅读本书还将帮助你锻炼 21 世纪的关键技能,包括编程、数据分析和数据科学,以及数据可视化和展示。社区对动态增长数据集的惊人数据可视化需求不断增加,这些数据集来源于各种各样的数据源,如智能家居、工厂、网络商店、社交网络、视频托管服务和健康追踪设备。

随着数据规模和复杂性的激增,我们将看到对仪表盘应用程序的需求不断增长,这些应用可以为用户提供实时的数据驱动概览,展示世界上发生的事情。你很可能已经在自己的生活中使用过基于浏览器的仪表盘:Google Analytics、Facebook Ad Manager 和 Salesforce Dashboards 就是一些现实生活中的仪表盘应用示例。图 1 显示了 Google Analytics 仪表盘应用的截图,帮助你追踪实时网站流量。

这个仪表板告诉我们,在截图记录时,大多数美国和亚洲的用户仍在睡觉,而欧洲的用户已经在忙着搜索网络。当你通过仪表板应用正确地可视化数据时,这些洞察是如何无缝产生的!创建这种类型的应用程序一直以来只有熟练的编码人员和那些能够将实时数据源与动态网站连接的大型组织才能做到,从而创造出仪表板应用所提供的独特价值。

图 2 显示了约翰·霍普金斯大学的仪表板应用的截图,它使用最新和动态的数据流可视化 COVID-19 的传播情况。

图 2:约翰·霍普金斯大学仪表板可视化全球疾病传播

你可以在 https://coronavirus.jhu.edu/map.html 实时查看该仪表板。该仪表板显示了一个动态计数器,追踪全球病例数,一个追踪热点的地图,按国家划分的病例数据,传播情况随时间变化的进程,以及更多静态和动态统计数据。这个仪表板应用每月能够帮助数十万人从数据中获得洞察——这些洞察是通过仅使用电子表格和表格数据无法获得的。

图 3 显示了本书作者之一创建的资产配置工具的截图 (https://wealthdashboard.app)。

图 3: Wealthdashboard.com 应用程序可视化你的资产配置

这个工具允许你通过输入分配给股票、债券和现金的财富百分比,并使用历史数据来可视化资产配置的回报和风险概况,从而建模投资回报。这是你将在本书中学习制作的一个仪表板!

仪表板应用的潜在用途几乎是无限的。对于每个数据源,都有一个有用的仪表板应用等待着你来创建。

我们,作为作者,度过了无数个不眠之夜进行编码、调试,并摸索如何让我们的第一个 Dash 应用程序正常工作。关于 Dash 的书籍非常稀缺,更不用说那些既易于理解又适合 Python 初学者的书籍了。所以,开始这个项目时,我们决定是时候改变这种状况了!

查看 Dash 企业应用程序画廊中的许多示例应用程序,网址是 https://dash.gallery/Portal(参见 图 4)。其中一些应用程序,如 AI 语音识别应用程序,代码行数不到 100 行!

图 4:官方 Dash 画廊截图,展示了仪表板应用的多种用途和功能

为何选择 Plotly Dash

我们已经确认仪表板非常出色,那么让我们来看看为什么你应该选择 Plotly Dash 来完成这个工作。虽然有很多优秀的替代框架可以用来创建仪表板,包括 Streamlit、Shiny、Jupyter、Tableau、Infogram 等等,但我们发现有若干令人信服的理由,让 Dash 在各种使用场景中优于它的竞争对手:

  • Dash 应用程序可以用纯 Python 编写,这意味着如果你熟悉 Python,你将能够非常快速地启动和运行。这也意味着你可以轻松地将现有 Python 工作中的数据和结果集成到 Dash 应用程序中。

  • Python 非常具有表现力,Dash 代码可以相对紧凑,这意味着你可以更快速地进行原型设计和迭代,这在开发需要在截止日期前完成的应用程序,或者在需求经常变化的敏捷环境中非常有用。

  • Dash 会隐藏复杂性,例如 JavaScript 前端和 Python 后端之间的通信。因此,你无需承担复杂的任务,如序列化、反序列化、定义 API 端点或发起 HTTP 请求。这可以显著减少样板代码。

  • Dash 是由 Plotly 团队开发的,这意味着它与 Plotly 图形库有着出色的集成。Plotly,因此 Dash,是创建 web 应用程序的绝佳选择,因为这些交互式图形本身就是基于 web 技术的。

  • Dash 是建立在广泛使用的 Flask 框架之上的,这为我们提供了多种部署选项,从完全托管到自托管不等。

  • 虽然 Dash 仅能与 Python 一起使用,但它非常具有扩展性,允许你混合使用 CSS 和 JavaScript,甚至可以使用 React 和 Dash 组件生成器编写你自己的组件。

尽管 Dash 具有许多优点,但没有软件是完美的。为了帮助你决定最适合你的工具,以下是 Dash 的一些局限性:

  • Dash 性能良好,但如果你的应用程序包含大量组件,或者非常复杂,或者正在处理巨大的数据集,你可能会开始看到应用程序变慢。

  • Dash 启动和运行的复杂度稍高于一些无代码或低代码的替代方案,且与其他企业软件的集成没有替代框架那么完善;例如,PowerBI 与微软企业软件的集成非常紧密。

  • 虽然 Dash 是纯 Python 编写的,但要真正理解其中的工作原理,你需要掌握 HTML 和 CSS 的基础知识。

本书适合谁阅读

我们写这本书时考虑到了完全没有 Dash 经验的读者。虽然一些 Python 的基础知识会帮助你更好地理解本书内容,但我们并不假设你有很多编程经验,因此我们会解释一些基础知识,比如如何安装 Dash 和相关库,如何设置编程环境,以及如何使用像 pandas 这样的库。本书的范围并不包括完整的 Python 课程,但第一章将介绍一些构建 Dash 应用程序所需的 Python 基础知识,并引导你查找更多资源,帮助你在需要时深入学习。

在实践中,许多读者可能已经有一些 Python 编程经验。如果你知道 Python,但还没有经验设置编程环境,可以从第二章开始。

如果你已经知道 Python,并且已经设置好了编程环境(最好是 PyCharm),你可以直接跳到第三章,我们将在那里为你提供一个关于 pandas 库的简短教程。如果你是一个精通编码的专家,已经掌握了这些内容,直接跳过所有入门章节,从第四章开始,在那里我们将向你展示如何创建你的第一个 Dash 应用程序。

本书内容

本书分为两部分:第一部分将帮助你安装和设置构建 Dash 应用所需的所有工具;第二部分将带你构建四个逐渐复杂的应用程序,并在最后提供一些实用的建议。

第一部分:速成课程

第一章,Python 基础回顾,讨论了构建数据驱动应用程序时最重要的 Python 基础知识,包括数据类型和结构、函数,甚至包括一些面向对象编程的内容。

第二章,PyCharm 教程,指导你安装 PyCharm 编程环境、安装库、创建项目,并运行你的 Dash 应用程序。

第三章,pandas 速成课程,为你提供了一个关于处理表格数据的 pandas 库的视觉概览和 10 分钟回顾。

第二部分:构建应用程序

第四章,第一个 Dash 应用程序,展示了如何通过一个基于社交媒体分析的实际例子创建你的第一个 Dash 应用程序。它介绍了 Dash 应用的构建模块,包括布局和样式部分、Dash 组件、回调函数,以及使用 Plotly Express 进行可视化。阅读完这一章后,你将知道如何创建自己的基础 Dash 应用程序。

第五章,全球数据分析:高级布局与图表,使用世界银行的全球数据集来介绍更多的组件和样式功能。本章细化并扩展了你基本的 Dash 技能:你将与 API 进行交互,实时获取数据,并学习使用 dash-bootstrap-components 来创建更复杂的布局。

第六章,投资组合:构建更大的应用程序,深入探讨了更多高级的 Dash 组件,使用一个基于财富的仪表盘应用作为示例。你将学习如何构建和调试更大的 Dash 应用,使用更复杂的 Dash 和 Bootstrap 组件,并通过低级的 Plotly 图形对象库来构建图表。

第七章,探索机器学习,带你通过一个可视化机器学习模型的应用,并提供有关支持向量机的背景知识。这展示了 Dash 的另一个应用:可视化和探索算法的工作原理。你将深入了解两个常与 Dash 一起使用的数值库:NumPy 和 scikit-learn。我们将介绍等高线图,并探讨 Dash 回调的更复杂用法。

第八章,技巧与窍门,总结了本书的最佳技巧与窍门,并提供了一些进一步阅读的参考资料,内容涵盖调试、应用自动格式化、利用 Dash 社区以及探索更多的应用程序。

在线资源

在本书中,我们将推荐由我们这些作者制作的资源,以帮助你的学习。一本书所能包含的内容有限,因此为了保持专注,我们已将大量的代码、视频和文档提供在线。

第一章:1 PYTHON 刷新器

如果你打算做 Dash 应用开发,你可能已经至少了解一点 Python。然而,这本书并不假设你是专家,因此我们将回顾一些在 Dash 中更为相关的 Python 概念,包括列表、字典、面向对象编程以及装饰器函数。如果你已经对这些领域非常自信,可以直接跳到 第二章,该章将介绍我们在整本书中使用的 Python IDE:PyCharm。

列表

让我们快速回顾一下几乎所有 Dash 应用程序中都使用的最重要的容器数据类型:Python 列表!列表在 Dash 中很重要,因为它们用于定义布局、结合 Dash Bootstrap 主题,并且常见于回调函数和由 Plotly 构建的图形中。

列表容器类型用于存储元素的序列。列表是可变的,这意味着你可以在创建后修改它们。这里我们创建一个名为 lst 的列表并打印其长度:

lst = [1, 2, 2]

print(len(lst))

我们的输出仅为:

3

我们通过使用方括号和逗号分隔的元素来创建一个列表。列表可以包含任意的 Python 对象、重复值,甚至是其他列表,因此它们是 Python 中最灵活的容器类型之一。这里我们将列表 lst 填充了三个整数元素。len() 函数返回列表中元素的数量。

添加元素

有三种常见的方式可以向一个已存在的列表中添加元素:附加、插入和连接。

append() 方法将其参数放在列表的末尾。以下是附加的示例:

lst = [1, 2, 2]

lst.append(4)

print(lst)

这将打印:

[1, 2, 2, 4]

insert() 方法在给定位置插入一个元素,并将所有后续元素向右移动。下面是一个插入的示例:

lst = [1, 2, 4]

lst.insert(2,2)

print(lst)

这将打印相同的结果:

[1, 2, 2, 4]

最后,拼接操作:

print([1, 2, 2] + [4])

我们得到:

[1, 2, 2, 4]

对于拼接操作,我们使用加号(+)运算符。这样可以通过将两个现有列表连接起来,创建一个新列表。

所有操作都会生成相同的列表,[1, 2, 2, 4]。其中 append 操作是最快的,因为它不需要像插入操作那样遍历列表来将元素插入到正确的位置,也不需要像拼接操作那样通过两个子列表创建一个新列表。

若要向给定列表追加多个元素,可以使用 extend() 方法:

lst = [1, 2]

lst.extend([2, 4])

print(lst)

代码将按如下方式更改现有的列表对象 lst:

[1, 2, 2, 4]

上面的代码是一个示例,展示了一个可以容纳重复值的列表。

删除元素

我们可以使用 lst.remove(x) 从列表中移除一个元素 x,例如:

lst = [1, 2, 2, 4]

lst.remove(1)

print(lst)

这会给我们返回结果:

[2, 2, 4]

该方法直接操作列表对象本身——不会创建新列表,原始列表会被修改。

反转列表

你可以使用方法 lst.reverse() 来反转列表元素的顺序:

lst = [1, 2, 2, 4]

lst.reverse()

print(l)

这将打印:

[4, 2, 2, 1]

反转列表也会修改原始列表对象,而不是创建一个新列表对象。

排序列表

你可以使用方法 lst.sort() 来对列表元素进行排序:

lst = [2, 1, 4, 2]

lst.sort()

print(lst)

我们看到了排序后的列表:

[1, 2, 2, 4]

再次提醒,排序列表会修改原始列表对象。默认情况下,结果列表是按升序排序的。如果要按降序排序,可以传入 reverse=True,如下所示:

lst = [2, 1, 4, 2]

lst.sort(reverse=True)

print(lst)

我们看到结果是按相反顺序排列的:

[4, 2, 2, 1]

你还可以指定一个 key 函数,并将其作为参数传递给 sort() 以自定义排序行为。key 函数只是将一个列表元素转换成一个可排序的元素。例如,它可以通过使用 Dash 组件的字符串标识符作为关键字,将一个不可排序的对象(如 Dash 组件)转换为可排序类型。通常,这些 key 函数允许你对自定义对象列表进行排序;例如,按年龄对员工对象列表进行排序。以下示例对列表进行排序,但使用元素的相反值(负值)作为 key:

lst = [2, 1, 4, 2]

lst.sort(key=lambda x: −x)

print(lst)

这给我们带来了:

[4, 2, 2, 1]

元素 4 的 key 是负值 −4,它是所有列表元素中最小的值。因为列表是按升序排序的,所以这是结果排序列表的第一个值。

索引列表元素

你可以使用 list.index(x) 方法来确定指定列表元素 x 的索引,像这样:

print([2, 2, 4].index(2))

print([2, 2, 4].index(2,1))

index(x) 方法查找列表中元素 x 的第一次出现,并返回其索引。

你可以通过传递第二个参数来指定起始索引,从而设置搜索开始的位置。因此,第一行打印出值 2 第一次出现的索引,而第二行打印出值 2 第一次出现的索引,但从索引 1 开始搜索。该方法在两种情况下都会立即找到值 2 并打印:

0

1

索引基础

这里是 Python 中索引的快速概览,通过示例说明。假设我们有字符串 'universe',索引就是该字符串中字符的位置,从 0 开始:

索引             0      1      2      3      4      5      6      7

字符      u      n      i       v       e      r      s      e

第一个字符的索引是 0,第二个字符的索引是 1,而第 i个字符的索引是 i−1。

切片

切片 是从给定字符串中截取子字符串的过程。我们称这个子字符串为 切片。切片的表示方法如下:

string[start:stop:step]

start 参数是我们希望开始字符串的位置,它会包含在切片中,而 stop 参数是我们希望字符串停止的位置,它不包括在切片内。忘记 stop 索引不包含在内是一个常见的 bug 来源,所以请记住这一点。step 参数告诉 Python 包含哪些元素,因此 step 为 2 时会包含每隔一个的元素,而 step 为 3 时会包含每隔两个的元素。以下是一个步长为 2 的示例:

s = '----p-y-t-h-o-n----'

print(s[4:15:2])

这将给我们带来:

python

三个参数都是可选的,因此你可以跳过它们,使用默认值 start=0、stop=len(string) 和 step=1。如果在切片的冒号前省略了 start 参数,表示切片从第一个位置开始;如果省略了 stop 参数,表示切片直到字符串的最后一个元素;如果省略了 step 参数,则默认为步长 1。这里我们省略了 step 参数:

x = 'universe'

print(x[2:4])

这给我们带来了:

iv

在这里,我们指定了起始位置,但没有指定结束位置,并且给出了步长为 2,因此我们会得到每隔一个字符的子字符串,从第三个字符开始,一直到字符串的末尾:

x = 'universe'

print(x[2::2])

这给我们带来了:

ies

如果我们不小心给出一个超出最大序列索引的 stop 索引,Python 会假设我们是想在原始字符串的末尾结束切片。这里是一个例子:

word = "galaxy"

print(word[4:50])

这将打印:

xy

只需记住,如果切片超出了序列索引,什么意外的事情都不会发生。

你也可以为所有三个参数提供负整数。对于 start 或 stop 的负索引,会告诉 Python 从末尾开始计数。例如,string[–3:] 会从倒数第三个元素开始切片,string[–10:–5] 会从倒数第十个元素(包括)开始切片,并在倒数第五个元素(不包括)结束。负步长意味着 Python 会从右向左切片。例如,string[::–1] 会将字符串反转,string[::–2] 会从最后一个字符开始,逐个取每隔一个字符向左移动。

字典

字典 是一个非常有用的数据结构,用于存储键值对。我们用花括号定义一个字典,如下所示:

calories = {'apple': 52, 'banana': 89, 'choco': 546}

键位于前面,后面跟一个冒号,再后面是值。键值对应该用逗号分隔。这里 'apple' 是第一个键,52 是它的值。你可以通过指定字典和在方括号中指定键来访问字典中的单个元素。在以下示例中,我们将苹果的卡路里与一块巧克力的卡路里进行比较:

print(calories['apple'] < calories['choco'])

当然,它会返回:

True

字典是一个可变的数据结构,因此可以在创建后进行修改。例如,你可以添加、删除或更新现有的键值对。在这里,我们向字典中添加一个新的键值对,存储一杯卡布奇诺有 74 卡路里的信息:

calories['cappu'] = 74

print(calories['banana'] < calories['cappu'])

当我们断言卡布奇诺比香蕉含有更多的卡路里时,结果是:

False

我们使用 keys() 和 values() 函数来访问字典的所有键和值。在这里,我们检查字符串 'apple' 是否是字典的键之一,以及整数 52 是否是字典的值之一。实际上这两者的结果都是 True:

print('apple' in calories.keys())

print(52 in calories.values())

要访问字典的所有键值对,我们使用 dictionary.items() 方法。在下面的 for 循环中,我们遍历字典 calories 中的每个 (key, value) 对,并检查每个值是否大于 500 卡路里。如果是,它会打印出相关的键:

for key, value in calories.items():

500:

print(key)

我们唯一的结果是:

'choco'

这为我们提供了一种简单的方法,可以遍历字典中的所有键和值,而无需单独访问它们。

列表推导式

列表推导式是一种创建列表的紧凑方式,采用简单的一行公式 [expression + context]。其中的 context 告诉 Python 要将哪些元素添加到新的列表中,而 expression 则定义了在添加这些元素之前要对它们进行什么操作。例如,列表推导式语句

[x for x in range(3)]

创建了一个新的列表 [0, 1, 2]。这个例子中的 context 是 for x in range(3),因此循环变量 x 会依次取值 0、1 和 2。这里的表达式 x 非常简单:它只是将当前的循环变量添加到列表中而没有任何修改。然而,列表推导式能够处理更为复杂的表达式。

列表推导式通常用于仪表盘应用中;例如,它用于动态创建下拉菜单的多个选项。这里我们创建了一个字符串列表——工作日,然后使用列表推导式创建一个字典列表。我们将使用这些字典来创建 Dash 下拉菜单的标签和选项,正如图 1-1 所示:

days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

options = [{'label': day, 'value': day} for day in days]

图 1-1:一个 Dash 下拉菜单

上下文是 for day in days,所以我们遍历每个工作日 'Mon',…, 'Sun'。该表达式创建了一个字典,包含两个键值对: {'label': day, 'value': day}。这是创建以下字典列表的一种简洁方式:

[{'label': 'Mon', 'value': 'Mon'}, {'label': 'Tue', 'value': 'Tue'},

{'label': 'Wed', 'value': 'Wed'}, {'label': 'Thu', 'value': 'Thu'},

{'label': 'Fri', 'value': 'Fri'}, {'label': 'Sat', 'value': 'Sat'},

{'label': 'Sun', 'value': 'Sun'}]

另一种方法是使用常规的 Python for 循环,如下所示的三行代码:

options = []

for day in days:

options.append({'label': day, 'value': day})

你创建了一个字典列表,其中标签和对应的值与每个特定的日期相关联。在这里,下拉菜单将显示标签 'Mon',如果用户选择该标签,标签将与值 'Mon' 关联。

上下文包含任意数量的 for 和 if 语句。我们可以在列表推导式中使用 if 语句来过滤结果;例如,我们可以只创建包含工作日的下拉菜单选项:

options = [{'label': day, 'value': day} for day in days if day not in ['Sat', 'Sun']]

这里我们使用了if语句来排除结果列表中的Sat和Sun。这是在一个for循环中写这个常规if语句的一种更快速、更简洁的方式。

面向对象编程

在 Python 中,一切都是对象。即使是整数值也是对象。这与 C 语言等编程语言不同,在 C 语言中,整数、浮动值和布尔值是原始数据类型。这样,Python 是基于严格一致的面向对象范式构建的。

类与对象

面向对象的 Python 编程的核心是类。类是创建对象的蓝图。类的描述告诉你一个对象的外观和它的功能,分别称为对象的数据功能。数据通过属性来定义,属性是与特定对象相关联的变量。功能则通过方法来定义,方法是与特定对象相关联的函数。

让我们通过哈利·波特的例子来看这些概念如何应用。首先,我们将创建一个具有属性但没有方法的类。在这里,我们创建了一个Muggle类,并从中创建了两个Muggle对象:

class Muggle:

def init(self, age, name, liking_person):

self.age = age

self.name = name

self.likes = liking_person

Vernon = Muggle(52, "Vernon", None)

Petunia = Muggle(49, "Petunia", Vernon)

我们使用关键字class为Muggle对象创建一个新的蓝图。这决定了每个Muggle对象将具备的数据和功能。在这里,我们声明每个Muggle对象应该有一个年龄、一个名字和一个他们喜欢的人。

对于每个类,您必须使用方法init()来初始化包含数据的类。 每个Muggle对象都将具有属性agenamelikes。 通过将它们作为参数传递给def语句,我们使它们在创建对象时成为必需的参数。 任何类方法的第一个值都是对对象本身的引用,表示为self。 一旦您在代码中调用初始化方法,Python 就会创建一个空对象,您可以使用名称self访问它。

注意

尽管在定义方法时第一个参数是 self,但在调用方法时不需要指定此参数。 Python 会在内部为您执行这些操作。

创建一个来自该类的对象时,初始化方法init会自动首先调用,通过使用类名作为函数调用来实例化一个新对象。 调用Muggle(52, "Vernon", None)Muggle(49, "Petunia", Vernon)创建两个新的类对象,均定义了三个属性,如下所示:

Muggle

   age = 52

   name = "Vernon"

   likes = None

Muggle

   age = 49

   name = "Petunia"

   likes = "Vernon"

您可以看到这些对象遵循相同的蓝图,但是是Muggle的不同实例;它们具有相同的属性但不同的 DNA。

从现在起,这些对象将存储在您计算机的内存中,直到 Python 在程序终止时将它们清除。

目前故事中有令人伤感的元素吗? 佩图尼亚喜欢弗农,但弗农谁也不喜欢。 让我们稍微振作一点,好吗? 我们将为Vernonlikes属性更改为Petunia。 我们可以使用对象的名称、点符号和属性的名称来访问对象的不同属性,如下所示:

Vernon.likes = "Petunia"

print(Vernon.likes)

这将输出:

Petunia

让我们定义 Wizard 类,这样我们就可以在我们的小世界里创建一些巫师。这次我们将增加一些功能:

class Wizard:

def init(self, age, name):

self.age = age

self.name = name

self.mana = 100

def love_me(self, victim):

= 100:

victim.likes = self.name

self.mana = self.mana – 100

Wiz = Wizard(42, "Tom")

每个 Wizard 对象有三个属性:age、name 和 mana 等级(表示巫师剩余的魔法力量)。age 和 name 属性是在创建 Wizard 对象时,根据传入的参数值进行设置的。mana 属性则在 init 方法中被硬编码为 100。例如,调用 Wizard(42, "Tom") 将会把 self.age 设置为 42,self.name 设置为 "Tom",并将 self.mana 设置为 100。

我们还添加了一个方法 love_me(),它会对目标施下爱的魔法。如果法师的法力足够,他们可以通过将目标的 likes 属性设置为施法者的名字来强迫目标爱上他们。然而,这只有在法师的法力值大于或等于 100(self.mana = 100)时才有效。成功时,目标的 likes 属性指向施法者的名字,施法者的法力值减少 100。

我们创建了一个 42 岁的法师名叫 Tom。Tom 孤独且希望被喜欢。让我们让 Petunia 和 Vernon 爱上他。我们使用点符号访问对象的方法,并传入 Petunia 和 Vernon 对象:

Wiz.love_me(Petunia)

Wiz.love_me(Vernon)

print(Petunia.likes"Tom" and Vernon.likes"Tom")

你能判断 Tom 是否成功让 Petunia 和 Vernon 都爱上他吗?

面向对象编程中最常见的困惑之一是忘记在定义方法时包含 self 参数。另一个常见的困惑是初始化方法的定义使用了语法 init(),而你应该使用语法 ClassName() 来调用类创建方法,而不是像你预期的那样调用 ClassName.init()。这在代码中有所展示,我们并没有调用 Wizard.init(20, 'Ron'),而是直接调用 Wizard(20, 'Ron') 来创建一个新的 Wizard 对象。

这只是 Python 中面向对象编程的简要概述,但确保你完全理解如何在 Python 中构建类和对象是非常值得的。

如需更多信息,可以查看面向对象编程的备忘单,链接为 https://blog.finxter.com/object-oriented-programming-terminology-cheat-sheet

术语

在这里,我们将快速浏览面向对象 Python 中的一些关键定义。

      是创建对象的蓝图。类定义了对象的属性(数据)和功能(方法)。您可以通过点符号访问属性和方法。

对象      是根据类定义构建的一个封装数据及其相关功能的单元。对象也被称为类的实例。通常,一个对象用于模拟现实世界中的某个事物。例如,我们可以根据类定义创建对象。一个对象由任意数量的属性和方法组成,这些属性和方法被封装在一个单一的单元中。

实例化      是创建类的对象的过程。

方法      是与特定对象相关联的函数。我们在类定义中使用关键字 def 来定义方法。一个对象可以有任意数量的方法。

属性      是用于存储与类或实例相关数据的变量。

类属性是静态创建的变量,在类定义中创建,并且被从该类创建的所有对象共享。这些也被称为类变量静态变量静态属性

动态属性      是在程序执行过程中动态定义的对象属性,并未在任何方法中定义。例如,您可以通过调用o.my_attribute = 42来将新的属性<my_attribute>添加到任何对象o中。

实例属性是仅属于一个对象的变量。其他对象不能共享该变量,正如它们共享类属性一样。在大多数情况下,您在使用变量名创建实例时,会创建一个实例属性x,例如 self.x = 42。这些也被称为实例变量

继承       是一种编程概念,允许你通过在定义新类时重用部分或全部数据和功能,来创建作为现有类修改的新类。也就是说,类 A 可以从类B继承属性或方法,这样它就具有与类B相同的数据和功能,但类A可以改变行为或添加数据和方法。例如,类Dog可以从类 Animal 继承属性number_of_legs。在这种情况下,你将定义继承类Dog如下:class Dog(Animal):,然后是类的主体。

如果你已经理解了这些术语,你可以跟上大多数关于面向对象编程的讨论。掌握面向对象是精通 Python 的重要一步。

装饰器函数和注解

Dash 在很大程度上依赖于 Python 的装饰器装饰器函数的概念,它们在不修改代码本身的情况下,为现有代码添加功能。这在你想要修改或自定义现有函数的输出时非常有用,而无需更改函数的实际代码。例如,你可能无法访问某个函数的定义,但仍然想改变它的行为。装饰器函数正是来拯救你的!

把装饰器函数看作一个包装器。它接受一个原始函数,调用它,并根据程序员的需求修改它的行为。这样,你可以在函数最初定义后动态地改变它的行为。

让我们从一个简单的例子开始。我们定义一个函数,将一些文本打印到标准输出:

def print_text():

print("Hello world!")

print_text()

输出是:

Hello world!

这个函数始终会打印相同的消息。假设你想装饰这个输出,让它变得更有趣。一种方法是定义一个新的pretty_print()函数;但这还不是一个装饰器函数,因为它并没有改变另一个函数的行为。然而,它确实展示了如何包装另一个函数并修改它的行为:

def print_text():

print("Hello world!")

def pretty_print():

annotate = '+'

print(annotate * 30)

print_text()

print(annotate * 30)

pretty_print()

现在输出如下所示:

++++++++++++++++++++++++++++++

你好,世界!

++++++++++++++++++++++++++++++

外部函数 pretty_print() 调用内部函数 print_text(),并通过在内部函数 print_text() 输出前后添加 30 个加号(+)符号来修饰结果。本质上,你是 包装 内部函数的结果,并通过附加功能进行增强。

装饰器函数允许你像这样通用化代码。例如,你可能希望将一个任意的内部函数传递给 pretty_print() 函数,这样你就可以将它应用于任何 Python 函数。在这里,我们创建了一个装饰器函数,但请注意,为了展示它是如何工作的,我们采用了比较长的方法。稍后我们将查看 Python 提供的更简短的方法来实现相同的功能。这是长版本:

def pretty_print_decorator(f):

annotate = '+'

def pretty_print():

print(annotate * 50)

f()

print(annotate * 50)

return pretty_print

def print_text():

print("你好,世界!")

def print_text_2():

print("你好,宇宙!")

当我们像这样使用时:

pretty_print_decorator(print_text)()

pretty_print_decorator(print_text_2)()

我们将得到如下输出:

++++++++++++++++++++++++++++++++++++++++++++++++++

你好,世界!

++++++++++++++++++++++++++++++++++++++++++++++++++

++++++++++++++++++++++++++++++++++++++++++++++++++

Hello universe!

++++++++++++++++++++++++++++++++++++++++++++++++++

这里,装饰器函数接受一个函数作为输入,并返回另一个通过将输出包裹在 + 符号中来修改行为的函数。你可以传递任何打印输出的函数,并创建一个类似的函数,额外地将输出包裹在一系列的 + 符号中。

这个简单的装饰器函数接受一个函数对象,并应用一些输出修改,但装饰器函数可以做各种复杂的事情,比如分析输出、应用额外的逻辑,或者过滤掉一些不需要的信息。

这是一种不切实际的复杂方式来构建装饰器函数。由于这种模式非常常见,Python 提供了一种便捷的方法,通过更少的代码实现相同的功能:你只需在要装饰的函数前添加一行代码。这一行包括一个 "@" 符号,后面跟着你之前定义的装饰器函数的名称。这里我们定义了 pretty_print_decorator(f) 函数,然后在定义两个打印函数时应用它:

def pretty_print_decorator(f):

annotate = '+'

def pretty_print():

print(annotate * 50)

f()

print(annotate * 50)

return pretty_print

@pretty_print_decorator

def print_text():

print("Hello world!")

@pretty_print_decorator

def print_text_2():

print("Hello universe!")

我们像这样调用我们定义的两个函数:

print_text()

print_text_2()

然后我们应该得到这样的输出:

++++++++++++++++++++++++++++++++++++++++++++++++++

Hello world!

++++++++++++++++++++++++++++++++++++++++++++++++++

++++++++++++++++++++++++++++++++++++++++++++++++++

Hello universe!

++++++++++++++++++++++++++++++++++++++++++++++++++

你可以看到输出与之前完全相同。但这次,我们不是显式地调用装饰器函数pretty_print_factory,比如在pretty_print_decorator(print_text)中装饰现有函数print_text,而是直接使用带有@前缀的装饰器函数修改print_text()的行为。每次调用被装饰的函数时,它会自动通过装饰器函数。这种方式使我们能够堆叠任意复杂的函数层级,每个层级通过装饰另一个函数的输出,增加新的复杂性。

装饰器函数是 Dash 框架的核心。Dash 提供了高级功能,你可以通过将 Dash 已经定义的装饰器函数应用到你的任何函数上,利用@注解来访问这些功能。Dash 将这些装饰器函数称为回调装饰器。你将在本书讨论的仪表盘应用中看到许多这样的例子。

总结

这是对一些与使用 Dash 创建应用最相关的 Python 概念的简要概述。如果你觉得这些内容难以理解,建议在开始构建应用之前查看“Python 基础”附录。

但在我们开始创建仪表盘应用之前,让我们先深入了解我们推荐你用于本书的 PyCharm 框架。如果你已经是 PyCharm 专家,或者你有其他偏好的编程环境,可以直接跳到第三章。

第二章:2 PYCHARM 教程

本章将向你介绍 PyCharm IDE。IDE集成开发环境的缩写,它是一个文本编辑器,提供各种工具来帮助你编写代码,并有可能显著提高你的编程效率。现代 IDE 通常具有代码高亮、动态提示、自动补全、语法检查、检查代码风格问题的代码检查器、版本控制(用于保护编辑历史)、调试工具、可视化辅助工具以及性能优化工具和分析器等功能。

随着你的 Python 仪表盘应用程序逐渐增多,你也会需要将所有源代码汇聚到一个地方并使用一个统一的开发环境。日益复杂的项目很快就会要求使用集成开发环境(IDE)。为了跟随本书中的代码示例,我们建议使用 PyCharm,这是专为 Python 设计的 IDE。PyCharm 是最受欢迎的 IDE 之一,支持所有操作系统。它简化了高级应用程序的开发,并且丰富的在线教程和文档为支持提供了极大的帮助。PyCharm 还与 Dash 应用程序的集成非常好,它允许你运行和调试应用,快速轻松地安装所需的库,并使用语法检查和代码检查工具。然而,如果你更喜欢使用其他 IDE,例如 VS Code,本书中的说明也很容易适应。

安装 PyCharm

让我们从下载最新版本的 PyCharm 开始。这里的示例是基于 Windows 操作系统,但在 macOS 上步骤相似。如果你使用的是 Linux,可以查看我们在 PyCharm 教程中提供的有关解压和安装 IDE 的说明,网址为https://blog.finxter.com/pycharm-a-simple-illustrated-guide。在不同操作系统上使用 PyCharm 的方式非常相似。请访问https://www.jetbrains.com/pycharm/download,你应该会看到类似于图 2-1 的页面。

图 2-1:PyCharm 下载页面

点击下载以获取免费的社区版本(图 2-1),下载完成后,运行可执行安装程序并按照安装步骤操作。我们建议直接接受安装程序推荐的所有默认设置。

创建一个项目

在系统中找到 PyCharm 并运行它。选择 新建项目,你应该会看到一个类似于图 2-2 的窗口。在这个用户界面中,有几个选项需要注意:项目名称,你需要在“位置”字段中的后缀部分输入;虚拟环境;Python 解释器;以及创建 main.py 脚本的复选框。

图 2-2:设置 PyCharm 项目

我们将把我们的项目命名为 firstDashProject,但你可以使用任何你喜欢的名称。短小的全小写项目名称更符合惯例,但我们暂时使用一个更直观的名称。如果想换个名称,只需修改“位置”字段中最后一个反斜杠(\)后的文本即可。

虚拟环境和解释器字段应该会自动填充 PyCharm 在你的系统上检测到的内容。在图 2-2 中,就是 Python 3.7。因此,我们将使用与标准 Python 安装一起提供的虚拟环境 Virtualenv。使用虚拟环境意味着你安装的所有软件包默认只会安装在项目环境中,而不会安装到你的机器上,从而将所有与项目相关的内容保持在一个整洁的地方。像这样虚拟化项目的依赖关系有很多优点,其中之一是你可以为不同的项目安装冲突的版本,而不会弄乱操作系统。例如,如果一个项目使用旧版本的 Dash,而你需要为另一个项目使用新版本的 Dash,全球安装 Dash 几乎肯定会导致问题。当你在不同的虚拟环境中安装不同版本的 Dash——每个项目一个——就可以避免这些版本冲突。

最后,通过取消勾选底部的框,选择不创建 main.py 欢迎脚本。许多 Python 程序使用 main.py 作为程序的主要入口点。为了执行项目,它们会执行 main.py 文件,进而启动程序提供的所有其他功能。然而,对于 Dash 应用程序来说,根据约定,代码的主要入口点是 app.py 文件——尽管通常可以使用任意文件名。因此,我们建议在所有 Dash 项目中取消勾选 main.py 框。

其余的我们保持默认设置。

点击 创建,你应该会看到你的第一个 PyCharm 仪表板项目!它应该类似于图 2-3。

图 2-3:你的第一个 PyCharm 仪表板项目

在我们深入了解如何在 PyCharm 中创建仪表板应用程序之前,让我们快速浏览一下 PyCharm 界面(参见图 2-4)。

图 2-4:PyCharm 界面概览

图 2-4 展示了界面中最重要的元素:

1.  项目工具窗口让你查看项目文件夹的结构。对于较大的项目,保持对所有代码功能和模块如何协同工作以提供一个完整的整体的高层次概览至关重要。

2.  编辑窗口允许你打开、编写和编辑项目中的多个代码文件。你可以在项目工具窗口中浏览项目,并双击文件在编辑窗口中打开它们。这就是你编写和编辑代码的地方。

3.  导航栏提供了按钮和快捷键,用于快速执行最重要的功能,例如启动和停止应用程序、选择要执行的主模块、搜索文件和调试应用程序。

4.  在你启动应用程序后,你将会在运行工具窗口中看到其输出和执行状态。在图 2-4 中,我们刚刚启动了第一个仪表盘应用,因此运行窗口显示了我们可以点击或输入到浏览器中的 URL 来查看我们的仪表盘应用。如果你在代码中使用了 print() 语句,这就是打印输出将出现的位置。

5.  运行工具窗口还提供了另一个导航栏,允许你在不同的标签之间切换。例如,你可以打开一个 Python shell,打开 Windows 的命令行或 macOS 的终端,以访问操作系统的功能,或者以逐步方式调试你的应用程序。

PyCharm 提供了许多额外的窗口,但这些是你在任何应用程序中都会使用到的最重要窗口,无论它是否是一个仪表盘应用。其余的部分留给你在闲暇时探索。

运行一个 Dash 应用

现在,我们将看看一个来自官方 Dash 文档的示例仪表盘应用。此代码创建了示例仪表盘应用,在图 2-5 中展示了一个简单的条形图。它还在你的本地计算机上启动了一个服务器,以便你可以在浏览器中查看仪表盘应用。

图 2-5:一个示例 Dash 应用

在 PyCharm 中,右键点击左侧菜单面板中的项目,选择 新建文件。将文件命名为 app.py 并将代码从 https://dash.plotly.com/layout 中复制到文件中,代码也展示在清单 2-1 中。

使用 'python app.py' 运行此应用,并

访问 http://127.0.0.1:8050/ 在你的网页浏览器中。

from dash import Dash, html, dcc

import plotly.express as px

import pandas as pd

import pandas as pd

假设你有一个“长格式”的数据框

参见 https://plotly.com/python/px-arguments/ 了解更多选项

df = pd.DataFrame({

"水果": ["苹果", "橙子", "香蕉", "苹果", "橙子", "香蕉"],

"数量": [4, 1, 2, 2, 4, 5],

"城市": ["旧金山", "旧金山", "旧金山", "蒙特利尔", "蒙特利尔", "蒙特利尔"]

})

fig = px.bar(df, x="水果", y="数量", color="城市", barmode="group")

app.layout = html.Div(children=[

html.H1(children='你好 Dash'),

html.Div(children='''

Dash: 一个用于数据的 web 应用框架。

'''),

dcc.Graph(

id='example-graph',

figure=fig

)

])

if name == 'main':

app.run_server(debug=True)

示例 2-1:来自 Dash 文档的示例应用

我们并不指望你现在就理解这些代码,也不会在此详细解释。大体来说,这段代码导入了必要的库,构建了应用并设置了样式,创建了数据并在条形图中进行了可视化,并设置了总体布局以包含标题等内容。最后两行启动了服务器,以便你可以在浏览器中查看它(参见图 2-6)。在学习了后续章节后,你会发现这段代码其实很简单。

现在运行你的项目:去顶部菜单选择 Runapp.py。你也可以点击导航栏中的绿色播放按钮。然而,你会发现我们遇到了一些问题:运行程序时,在底部的运行工具窗口显示了一个错误,如 图 2-6 所示。我们的应用还不能正常工作,因为我们在导入 Dash,但 PyCharm 并未识别 Dash!原因是 Dash 不是 Python 标准库的一部分:你需要手动安装它才能在项目中使用。

图 2-6:PyCharm Dash 错误

你可能会好奇为什么我们没有早点安装 Dash。正如你将很快看到的那样,因为每个项目都在自己的虚拟环境中隔离,这样做是为了将来在实际使用 Dash 时遵循良好的实践。

通过 PyCharm 安装 Dash

有两种方式可以安装 Dash:一种是全局安装在您的计算机上,这意味着每个未来的项目都能导入 Dash;另一种是在虚拟环境中本地安装,这意味着只有当前项目能够导入 Dash,且您需要为其他虚拟环境中的项目重新安装它。推荐的方式是在虚拟环境中安装。

注意

PyCharm 在不同系统上的运行可能略有不同,因此如果您在此步骤遇到问题,请查看我们的完整指南,获得帮助: <wbr>blog<wbr>.finxter<wbr>.com<wbr>/how<wbr>-to<wbr>-install<wbr>-a<wbr>-library<wbr>-on<wbr>-pycharm

PyCharm 允许我们直接通过应用代码安装 Dash。点击红色下划线的 dash 库导入行并将光标悬停在那里;应该会出现一个小的红色灯泡,并显示一个菜单。选择图 2-7 中显示的 安装 Dash 包 选项。

图 2-7:通过 PyCharm 安装 Dash

请注意,此选项仅在您已在虚拟环境中创建了 PyCharm 项目时才会出现(请参见 图 2-2)。如果看不到安装包的选项,您可以打开运行工具窗口中的终端标签,并输入:

$ pip install dash

安装 Dash 库需要一些时间。请记住,库仅安装在此 虚拟环境 中——也就是说,不是在您的全局操作系统上,而仅在项目级别。如果是不同的项目,您可能需要再次安装 Dash。

根据你的本地环境,你可能还需要重复相同的步骤来安装 pandas 库。请访问 pandas 安装指南:https://blog.finxter.com/how-to-install-pandas-on-pycharm。我们将在第三章中讲解 pandas 的安装。

现在再次尝试运行 app.py,你应该会看到类似这样的内容:

Dash 正在运行于 http://127.0.0.1:8050/

* 正在服务 Flask 应用程序 "app"(懒加载)

* 环境:生产

警告:这是一个开发服务器。请勿在生产环境中使用。

请改用生产环境的 WSGI 服务器。

* 调试模式:开启

你的应用正在本地机器上托管,因此外部无法访问它。在内部,Dash 使用 Python 的 Flask 库来为用户提供网站。要测试你的应用,请将 http://127.0.0.1:8050/ 复制到浏览器中,或者在 PyCharm 的输出窗口中点击它。这个 URL 表示仪表盘应用运行在本地服务器上,服务器托管在你的机器上,IP 地址是 127.0.0.1——这是一个通常被称为 localhost回环 地址,可以理解为“你的本地计算机”——端口是 8050。

欲了解更多有关 PyCharm 的信息,请查看我们的多页博客教程:https://blog.finxter.com/pycharm-a-simple-illustrated-guide

使用 Dash 与 GitHub

学习 Dash 和熟悉 PyCharm 的一个绝佳方法是复制专家的现有 Dash 项目,并尝试修改它们的代码。从专家那里学习代码项目是测试和改进你思维的最佳方法之一。之前,你通过复制粘贴文件 app.py 中的代码来尝试了示例应用程序。考虑到许多代码项目包含多个文件和更复杂的文件夹结构,这并不总是最方便的方法。这里我们将克隆一个 GitHub 项目。大多数开源项目都可以在 GitHub 上找到,所以有很多可供你查看的项目。

注意

在我们开始之前,你需要安装 GitHub。如果还没有安装,你可以从官方网站下载 Git(git-scm.com/downloads或者通过 PyCharm 安装它。

要将 GitHub 项目克隆到新的 PyCharm 项目中,首先获取你想要克隆的 GitHub 仓库的 URL;在https://github.com/plotly/dash-sample-apps上有许多项目可供选择。图 2-8 展示了来自 Plotly 的一些 Dash Gallery 示例应用。

图 2-8:GitHub 上的 Dash Gallery 示例应用

点击仓库中的代码并复制 URL。例如,你可以使用https://github.com/plotly/dash-sample-apps.git访问包含所有 Dash 应用的仓库。

打开 PyCharm 并点击VCS从版本控制获取,如图 2-9 所示。输入 URL 地址。请注意,构建这个项目将从 Git 项目的 URL 创建一个新的项目,因此你从哪个项目开始都无所谓。

图 2-9:在 PyCharm 中打开 GitHub 仓库

点击克隆并等待操作完成。这个过程可能需要一些时间,因为该仓库包含了所有的 Dash Gallery 项目。安装整个仓库使你能够快速尝试许多不同的 Dash 项目,并查看专家是如何实现你感兴趣的 Dash 功能的。

接下来,PyCharm 会要求你设置一个虚拟环境,以安装示例应用所需的库(参见图 2-10)。点击确定。如需故障排除,请按照https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html中列出的详细策略操作。

图 2-10:在 PyCharm 中为已签出的 GitHub 仓库安装虚拟环境中的库

恭喜!你的 PyCharm 项目现在应该可以正常工作了。你已经创建了原始 GitHub 项目的克隆。克隆只是原始项目的副本,所以如果你更改了克隆中的代码,除了你自己,其他人是看不到这些更改的。

图 2-11 展示了如何打开单个仪表板应用的主入口点:文件app.py。在 PyCharm 中打开该文件,安装它依赖的任何库,运行它,并在你的浏览器中查看。

图 2-11:打开来自 Dash Gallery 的仪表板应用的 app.py 文件

如果你想查看更多示例 Dash 应用程序,Dash 画廊中的 https://dash.gallery/Portal 指向许多由 Dash 专家创建的 GitHub 仓库。克隆它们就像使用 Dash 画廊提供的网址重复这些步骤一样简单。如果你没有使用 PyCharm,可以查看这份关于如何克隆现有仓库的指南:https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository。别担心——你无法破坏任何东西,所以可以放心地玩转代码。像 Git 这样的版本控制系统可以让你轻松地回到初始状态。

总结

在本章中,你学习了如何设置 PyCharm,它是最流行的 Python IDE 之一。PyCharm 与 Python Dash 的集成非常好。具体来说,你学习了如何通过 PyCharm 安装 PyCharm 和第三方库(如 Dash),创建第一个简单的 Dash 项目,运行该项目,并在浏览器中查看你的仪表板应用。此外,你还学习了如何将 PyCharm 与最流行的版本控制系统 Git 集成,这样你就可以查看现有的 Dash 应用程序,与他人一起学习和协作。

事实上,现在正是跟随本教程的步骤、克隆画廊中现有的仪表板应用程序、运行它并调整一些简单的内容(如颜色和文本标签)以熟悉 Dash 的好时机!我们将在接下来的章节中详细解释所有内容,但在尝试关闭知识差距之前先打开它,难道不是更好的吗?

安装了 PyCharm 后,我们将继续介绍 pandas 库。pandas 库帮助你组织和处理要在仪表板应用中可视化的数据!

第三章:3 PANDAS 速成课程

仪表盘应用程序主要用于可视化数据。但在此之前,你需要对数据进行预处理、清理和分析。为了帮助你完成这些任务,Python 提供了一整套强大的数据分析模块,包括流行的 pandas 库。pandas 库提供了用于表示和操作数据的数据结构和功能。可以把它看作是代码中的高级电子表格程序,带有额外的功能,包括创建电子表格、通过名称访问单独的行、计算基本统计信息、对满足特定条件的单元格进行操作等。

本章快速介绍了最重要的 pandas 特性。它松散地基于官方的“10 分钟掌握 pandas”指南,但在这里我们将内容压缩为本书中最相关的信息。你可以在https://blog.finxter.com/pandas-quickstart找到一个 8 分钟的 pandas 速成视频教程。

视觉概览备忘单

图 3-1 提供了本章所述主题的图形化概览。

图 3-1:pandas 备忘单

在阅读本章时,随时可以返回查看此图形。接下来,我们将深入逐步解释以下各部分内容。

安装 pandas

使用以下命令在你的虚拟环境或系统中通过终端、命令行或 shell 安装 pandas 库:

$ pip install pandas

如果你已经安装了 pandas,我们建议使用命令pip install -U pandas更新到最新版本。

一些编辑器和 IDE 集成了终端,你可以用它来安装 pandas,包括 PyCharm,如图 3-2 所示。如果你使用的是其他 IDE,你可以使用它提供的终端,或者操作系统自带的终端。如果你已安装 PyCharm,可以在主编辑器中输入import pandas,此时工具提示会显示出来。当你点击工具提示时,它会给你提供安装 pandas 的选项,如图 3-2 所示。

安装 pandas 的两种方式在图 3-2 中展示。

图 3-2:使用工具提示(1)或集成终端并输入命令 pip install pandas(2)在 PyCharm 中安装 pandas。

要在你的 Python 脚本中访问 pandas 库,只需使用 import pandas 语句导入它。通常,为了方便访问和简洁起见,pandas 会被赋予别名 pd,因此添加到脚本顶部的完整行将是:

import pandas as pd

这样,你就可以用 pd.somefunction() 来替代 pandas.somefunction()。

故障排除

如果在尝试运行带有 pandas 的代码时发现它仍未正确安装,请按照以下步骤在 PyCharm 项目中正确安装 pandas:

  1. 在 PyCharm 菜单中选择 文件►设置►项目 。

  2. 选择你当前的项目。

  3. 点击项目选项卡中的 Python 解释器 选项卡。

  4. 点击小加号符号 (+) 以向项目添加新的库。

  5. 输入要安装的库名称,这里是 pandas,然后点击 安装包。

  6. 等待安装完成,然后关闭所有弹出窗口。

在 pandas 中创建对象

pandas 中最重要的两种数据类型是 Series 和 DataFrame。一个 pandas Series 是一个一维数据数组,类似于 Excel 表格中的一列。一个 pandas DataFrame 是一个二维标记的数据结构,很像一个完整的电子表格。Series 和 DataFrame 结构的目的是方便数据的存储、访问和分析。

为了便于使用索引访问单独的行或列,pandas 在创建 DataFrame 结构时会自动添加行和列索引。默认情况下,pandas 使用从零开始的索引,因此它从索引 0 开始,并将后续的索引按顺序递增,直到数据结构的末尾。

Series

要在 PyCharm IDE 中跟随 pandas 示例,请选择 文件新建项目,然后通过 文件新建Python 文件 创建一个新的空 Python 文件。你可以为项目和 Python 文件指定任何你喜欢的名称。在新的项目文件中,复制以下代码来创建一个简单的 Series 对象(确保你已安装 pandas):

import pandas as pd

s = pd.Series([42, 21, 7, 3.5])

print(s)

运行代码后,你应该会看到以下输出:

0      42.0

1      21.0

2      7.0

3      3.5

数据类型:float64

你刚刚通过 pd.Series() 构造函数创建了一个 Series,并传递给它一个值的列表。你也可以通过传递其他数据类型来创建 Series,例如整数列表、布尔元组或任何其他可迭代的数据值,pandas 会自动确定整个 Series 的数据类型并将其分配给 Series 对象,如输出的最后一行所示。

数据框

pandas 数据框类似于你代码中的数据表,包含行、列和填充了特定类型数据的单元格,如图 3-3 所示。

图 3-3:创建一个包含三列(不包括索引列)和三行的 pandas 数据框对象

列表 3-1 展示了如何创建一个简单的 DataFrame 对象。

import pandas as pd

df = pd.DataFrame({'age': 18,

'name': ['Alice', 'Bob', 'Carl'],

'cardio': [60, 70, 80]})

print(df)

列表 3-1:名为 df 的示例数据框

这将给我们一个如下所示的 DataFrame:

age name cardio
0 18 Alice 60
1 18 Bob 70
2 18 Carl 80

使用 pd.DataFrame() 构造函数来创建 DataFrame。当你使用字典初始化 DataFrame 时,就像我们在这里做的那样,字典的键是列名,字典的值是该列的行值。你也可以仅提供一个列值,例如 18,并将其分配给整个列名,例如 age,这样该列中的每个单元格都会填充值 18。

注意

从技术上讲,如果为某一特定列的所有行只提供一个单一的值,pandas 会自动将该值设置为 DataFrame 中所有现有行的值,这一过程被称为 广播

DataFrame 还可以通过从 CSV 文件中读取数据来构建。你可以使用 pandas 的 read_csv() 函数将 CSV 文件加载为 DataFrame,像这样:

import pandas as pd

path = "your/path/to/CSV/file.csv"

df = pd.read_csv(path)

你需要将文件的路径替换为你的具体路径;这个路径可以是绝对路径,也可以是相对于你脚本所在位置的相对路径。例如,如果 CSV 文件与 Python 脚本在同一目录下,你可以直接给出文件名作为相对路径。

在 DataFrame 中选择元素

Series 对象和 DataFrame 允许轻松访问单个元素。接下来我们将看到如何以简单、高效且可读的方式存储、访问和分析来自 DataFrame 的数据。Series 对象可以看作是仅有一维的 DataFrame,因此理解 DataFrame 的访问方式也有助于理解 Series 的访问方式。图 3-4 为你提供了便捷的备忘单,展示了数据访问的三种方式:按列选择(A),按索引和切片选择(B),按标签选择(C)。接下来的小节将简要概述每种方法,我们将在后续章节中深入探讨。

图 3-4:三种不同的 DataFrame 元素选择方式

按列选择

你可以使用已经熟悉的方括号表示法来访问列,这种表示法在 Python 列表和字典中也有使用。使用来自清单 3-1 的 df 数据框,我们可以像这样选择 age 列中的所有元素:

print(df['age'])

这将打印:

0     18

1     18

2     18

Name: age, dtype: int64

你可以使用访问的 DataFrame 名称和方括号中的列名来选择标记为 age 的列中的所有值。

注意,pandas 也允许使用替代语法 df.age 来访问列。尽管你会在某些 pandas 代码库中看到这种写法,但更常见的做法是使用方括号表示法 df['age'],这与标准的 Python 列表、字符串和字典索引类似。

按索引和切片选择

要访问 DataFrame 中的特定行,我们使用切片表示法 df[start:stop]。如第一章中所提到的,start 索引所在的行会被包括在内,而 stop 索引所在的行则会被排除在选择之外。不过,要小心使用 df.loc[start:stop],因为在这种情况下,stop 索引实际上是包含的,这是一个常见的混淆来源!

注意

你可以在 blog.finxter.com/introduction-to-slicing-in-python 找到关于 Python 切片的详细教程,以及在 blog.finxter.com/numpy-tutorial 中的切片教程。

要仅访问一行,设置相应的 start 和 stop 索引:

print(df[2:3])

这将打印索引为 2 的行,并通过指定 stop 索引为 3,表示不再打印更多行:

年龄 姓名 心脏健康
2 18 Carl 80

你还可以使用 iloc 索引来访问 DataFrame 元素,访问第 i 行和第 j 列。这里我们通过零基索引 2 和 1,分别访问 df DataFrame 的第三行和第二列:

print(df.iloc[2, 1])

第一个参数 i 访问第 i 行,第二个参数 j 访问第 j 列的数据值。此操作将打印第三行(索引为 2)和第二列(索引为 1)的数据,结果是 'Carl'。

布尔索引

访问符合某个条件的行的一个强大方法是使用 布尔索引。我们将再次使用 df DataFrame,并访问心脏健康(cardio)列中大于 60 的行(请稍等,我们稍后会解释):

60])

这将提取最后两行:

年龄 姓名 心脏健康
1 18 Bob 70
2 18 Carl 80

虽然这种语法一开始看起来可能有些奇怪,但它实际上是由 pandas 的创建者精心设计的。内部条件 60 产生了一个布尔值的序列,如果cardio列的第i个元素大于 60,结果为'True'。这对于 DataFrame 中的最后两行成立。因此,60 产生了如下的序列:

0    False

1     True

2     True

Name: Cardio, dtype: bool

这些布尔值随后作为索引传递给 df,结果是一个只包含两行而不是三行的 DataFrame。

按标签选择

就像在电子表格中一样,pandas 中的每一行和每一列都有标签。标签可以是整数索引号码,例如行索引,或者是字符串名称,例如中的cardio列名。要通过标签访问数据,我们使用索引机制 df.loc[rows, columns]。在这里,我们访问中的所有行name列:

print(df.loc[:, 'name'])

这将给我们以下结果:

0    Alice

1      Bob

2     Carl

Name: name, dtype: object

'''

我们在方括号内使用逗号分隔的切片索引方案 df.loc[:, 'name'],其中第一部分 : 用来选择行,第二部分 'name' 用来选择从 DataFrame 中提取的列。空的切片符号(没有指定起始和结束索引)表示你想访问所有行,不加任何限制。字符串 'name' 表示你只想提取 name 列的值,忽略其他列。

要访问 age 和 cardio 两列的所有行,我们可以传递一个包含列标签的列表,如下所示:

print(df.loc[:, ['age', 'cardio']])

这将得到以下结果:

age cardio
0 18 60
1 18 70
2 18 80

修改现有的 DataFrame

你可以使用赋值操作符 = 来修改甚至覆盖 DataFrame 中的部分数据,方法是选择要替换的数据并将新的数据放到右侧。这里我们将 age 列中的所有整数值覆盖为 16:

df['age'] = 16

print(s)

这里是结果:

age name cardio
0 16 Alice 60
1 16 Bob 70
2 16 Carl 80

你首先使用 df['age'] 选择 age 列,并将与 age 相关联的值覆盖为整数值 16。为了将单个整数复制到该列的所有行中,pandas 使用了广播。

图 3-5 显示了 pandas 备忘单的相关部分。

图 3-5:使用切片和广播修改 DataFrame 中年龄列的第 2 行和第 3 行

这是一个更高级的示例,使用切片和 loc 索引来覆盖 age 列的所有行(除了第一行)。首先我们将重新构建原始的 df DataFrame:

import pandas as pd

df = pd.DataFrame({'age': 18,

'name': ['Alice', 'Bob', 'Carl'],

'cardio': [60, 70, 80]})

这给我们带来了:

年龄 姓名 心血管
0 18 Alice 60
1 18 Bob 70
2 18 Carl 80

现在,我们通过使用标准的切片符号选择第二行和第三行来排除第一行的更改:

df.loc[1:,'age'] = 16

print(df)

我们可以看到 Alice 的年龄保持为 18:

age name cardio
0 18 Alice 60
1 16 Bob 70
2 16 Carl 80

为了丰富我们的示例,我们将使用一个新的系统,因为 pandas 非常灵活。通过理解不同的索引方式——括号表示法、切片、loc 和 iloc——你将能够覆盖现有数据并添加新数据。在这里,我们通过 loc 索引、切片和广播添加了一个新的列 friend:

df.loc[:,'friend'] = 'Alice'

print(df)

这将给我们以下结果:

age name cardio friend
0 18 Alice 60 Alice
1 16 Bob 70 Alice
2 16 Carl 80 Alice

请注意,使用这里显示的更简洁代码也可以达到相同的效果:

df['friend'] = 'Alice'

print(df)

然后我们得到了相同的结果:

年龄 姓名 心脏健康 朋友
0 18 Alice 60 Alice
1 16 Bob 70 Alice
2 16 Carl 80 Alice

总结

这是一本关于 pandas 最相关特性的快速入门课程,这些特性将在本书中使用。pandas 库还有更多功能,包括统计计算、绘图、分组和重塑等,仅举几例。我们建议你通过本章资源部分中的链接,按自己的节奏探索 pandas。一旦你理解了本章讨论的概念,你将能够阅读和理解许多其他 Dash 项目中现有的 pandas 代码。

现在,让我们开始你的第一个仪表盘应用吧!

资源

第四章:4 第一个 Dash 应用

在本章中,你将构建你的第一个 Dash 应用。我们将分析自 2011 年以来 16 位明星收到的 Twitter 点赞数。你可以通过本书的资源下载数据,网址是 https://nostarch.com/python-dash。我们进行的这种分析在社交媒体分析领域非常常见,通常用于更好地理解受众行为、帖子效果以及账户的整体表现。

第一个仪表盘将绘制每条推文的点赞数。一旦你掌握了使用 Dash 绘制这种简单图表的过程,你就能将你的技能扩展到其他更大、更复杂的数据领域:Instagram 帖子的浏览量、Facebook 个人主页的访问量、LinkedIn 帖子的点击率,以及 YouTube 视频的表现。

本章将为你提供足够的 Dash 知识,以便你创建自己的仪表盘应用。你将学习如何将数据集成到应用中,管理多个仪表盘组件,构建基本图表(如折线图),并通过回调装饰器为仪表盘添加交互功能。首先,让我们下载代码并运行应用,看看它的功能。

设置项目

打开 PyCharm,创建一个新项目,并命名为 my-first-app(项目名称应该是新建项目对话框中 Location 字段最后一个反斜杠后的后缀文本)。使用标准的 Virtualenv 设置你的虚拟环境。

注意

本章的代码假设你正在使用 Python IDE,如 PyCharm。如果你没有安装 IDE 或者没有设置虚拟环境,请回到第二章并完成 Python 环境的设置。如果你使用的是其他编程环境,只需根据你的环境调整本章的说明即可。本章的代码还要求 Python 3.6 或更高版本。

接下来,你需要将本章的仪表盘应用文件下载到你的项目文件夹中。不同于我们在第二章中使用 Git 克隆仓库的做法,这次我们将直接下载 ZIP 文件。尝试不同的项目设置方式是值得的,因为你可能会遇到一些不能直接作为 Git 仓库提供的项目。要使用 ZIP 文件,访问 GitHub 仓库 https://github.com/DashBookProject/Plotly-Dash,点击 Code,然后点击 Download ZIP,如图 4-1 所示。

图 4-1:从 GitHub 下载应用代码

一旦你将 Plotly-Dash-master.zip 文件下载到电脑中,打开它并进入 Chapter-4 文件夹。将该文件夹中的所有文件复制到你最近创建的 my-first-app 项目文件夹中。项目文件夹应具有以下结构:

– my-first-app

|––assets

––mystyles.css

|––tweets.csv

|––twitter_app.py

assets 文件夹将保存 CSS 脚本。tweets.csv 文件保存我们将使用的数据,twitter_app.py 是我们用来运行应用的主要文件。

现在,我们将在虚拟环境中安装必要的库。转到 PyCharm 窗口底部的 Terminal 标签,见图 4-2。

图 4-2:在 PyCharm 中打开终端

输入并执行以下代码行以安装 pandas 和 Dash(Plotly 包会随 Dash 自动安装,因此无需单独安装 Plotly,NumPy 包也会随 pandas 自动安装):

$ pip install pandas

$ pip install dash

要检查库是否正确安装,请输入:

$ pip list

这将创建一个列出当前虚拟环境中所有 Python 包的清单。如果所有包都列出,你可以继续。请注意,pandas 和 Dash 的所有依赖项也会被列出,所以你可能会看到比你安装的两个包更多的库。

接下来,在 PyCharm 中打开 twitter_app.py 并运行脚本。你应该看到以下信息:

  • Serving Flask app "twitter_app" (lazy loading)

  • Environment: production

WARNING: This is a development server. Do not use it in a production deployment.

Use a production WSGI server instead.

  • Debug mode: on

Dash is running on http://127.0.0.1:8050/

这个警告只是提醒我们应用正在开发服务器上运行,这是完全正常的。要打开你的应用,点击 HTTP 链接或将其复制并粘贴到浏览器的地址栏中。

恭喜!你现在应该能看到你的第一个 Dash 应用,它应该像图 4-3 那样。

图 4-3:Twitter 点赞分析应用

玩得开心!在你的仪表盘应用中随便玩玩。改变下拉框的值,点击链接,点击图表图例,并按住鼠标左键拖动鼠标来缩放到某个日期范围。看看你能推导出什么信息。

现在让我们来看一下应用的代码。大多数 Dash 应用程序的代码布局是类似的:

1.  导入必要的 Python 库。

2.  读取数据。

3.  分配样式表以描述应用的显示方式。

4.  构建应用布局,定义如何显示所有元素。

5.  创建回调函数,以实现应用组件之间的交互性。

由于 Dash 应用大多遵循这一大纲,我们将按此顺序来浏览代码。

导入库

首先看一下我们将要使用的库,见列表 4-1。

import pandas as pd

import plotly.express as px

from dash import Dash, dcc, html, Input, Output

列表 4-1:来自 twitter_app.py 的导入部分

我们首先导入 pandas 来处理数据。然后导入 Plotly,这是一个流行的 Python 可视化库。在 Plotly 中有两种主要的创建图表方式。我们使用的是Plotly Express,这是一个用于通过单次函数调用快速创建图表的高级接口,只需很少的代码行。它有足够的功能,能够让你无缝地快速构建图表,并且是两者中用于简单应用最容易使用的。

另一种选择是Plotly Graph Objects,这是一个从底层构建图表的低级接口。使用 Graph Objects 时,你需要定义数据、布局以及有时的帧,这些都使得图表构建过程更加复杂。然而,它的完整功能集允许你以多种方式自定义图表,从而增加图表的丰富性,所以一旦你掌握了 Dash 的基础,并且需要构建更复杂的图表时,你可能会选择使用 Plotly Graph Objects。我们在大多数情况下会使用 Plotly Express,只有在更复杂的情况下才会回退到 Graph Objects。

接下来,我们导入一些 Dash 库来处理组件和依赖项。组件是可以组合在一起为用户创建丰富复杂界面的构建模块,例如下拉菜单、范围滑块和单选按钮。Dash 附带了两个由 Plotly 维护的关键组件库:dash-html-components(HTML)和 dash-core-components(DCC)。dash-html-components 库包含诸如标题和分隔符等结构性元素,用于设置页面上元素的样式和位置,而 dash-core-components 提供应用程序的核心功能,如用户输入字段和图形。

数据管理

在这个应用程序中,我们使用 CSV 电子表格作为数据源。为了使用数据,我们需要通过 pandas 将其读取到内存中,但在此之前,我们必须 清洗 数据。这意味着通过执行标准化字符串的大小写、时间格式、去除空格以及为缺失值添加空值等操作,来准备数据以供分析和绘制图表。当数据是 脏的 时,通常它是无序的,并且可能包含缺失值。如果你尝试使用脏数据,图表可能无法正常工作,分析可能不准确,而且你会发现过滤数据很困难。清洗数据确保数据可读、可展示并且可以绘图。

列表 4-2 显示了代码中的数据管理部分。

❶ df = pd.read_csv("tweets.csv")

df["name"] = pd.Series(df["name"]).str.lower()

df["date_time"] = pd.to_datetime(df["date_time"])

df = (

df.groupby([df["date_time"].dt.date, "name"])[

["number_of_likes", "number_of_shares"]

]

.mean()

.astype(int)

)

df = df.reset_index()

列表 4-2:数据管理部分来自 twitter_app.py

在 ❶ 处,我们获取 CSV 电子表格并将其读取到一个名为 df 的 pandas DataFrame 中。Dash 应用程序开始时的 DataFrame 通常被称为 全局 DataFrame,而数据则是一个 全局变量全局意味着对象在函数外部声明,意味着它可以在应用程序中随处访问)。

为了清理数据,我们将名人 name 列的字符串转换为小写,以便于比较。我们将 date_time 列转换为 pandas 可识别的日期格式,并根据 date_time 和 name 对数据进行分组,以确保每一行都有唯一的日期和名称。如果不这样分组数据,就会导致多个相同日期和名称的行,最终生成一个杂乱无章、无法阅读的折线图。

要检查数据,在脚本中添加以下代码行,紧接在 df = df.reset_index() 之后:

print(df.head())

重新运行脚本后,你应该在 Python 终端中看到类似以下的内容:

date_time name number_of_likes number_of_shares
0 2010-01-06 selenagomez 278 695
1 2010-01-07 jtimberlake 62 189
2 2010-01-07 selenagomez 201 630
3 2010-01-08 jtimberlake 27 107
4 2010-01-08 selenagomez 349 935

如你所见,结果是一个整洁的 pandas DataFrame,其中的每一行数据表示每个名人在每天的点赞和分享的平均数。

在应用的开始阶段读取和准备数据总是一个好的做法,因为读取数据可能是一个消耗内存的任务;通过在开始时插入数据,你可以确保应用只会将数据加载到内存中一次,而不会在每次用户与仪表盘交互时重复此过程。

布局和样式

下一步是管理应用组件的布局和样式,比如标题、图表和下拉菜单。我们将在本章稍后的“Dash 组件”部分进一步了解这些组件;在这里,我们将重点关注布局部分。

在 Dash 应用中,布局 指的是组件在应用中的排列方式。样式 指的是元素的外观,比如颜色、大小、间距等属性(在 Dash 中被称为 props)。对应用进行样式设计可以实现更加定制化和专业化的展示。如果没有样式设置,你可能会得到像 图 4-4 中展示的那样的应用,其中标题没有居中,下拉框字段覆盖了整个页面,并且链接与其上方的下拉框之间没有间距。

图 4-4:没有适当布局和样式的 Twitter 点赞分析应用

对齐

Dash 应用是基于 Web 的,因此它使用网页的标准语言:HTML(超文本标记语言)。幸运的是,Dash 包含了 Dash HTML 组件模块,它将 Python 转换为 HTML,这意味着我们可以使用 Python 来编写 HTML。

HTML 最基本的组件之一是 Div,即 division(区块)的缩写,它只是一个用于容纳其他元素的容器,并且是将元素分组的一种方式。我们在 Dash 应用中使用的每个组件都将被包含在一个 Div 中,且一个 Div 可以包含多个组件。我们构建 Div,然后对其进行样式设置,以告诉网页浏览器该组件应该放置在哪里,以及应该占据多少空间。

假设我们正在创建一个包含三个下拉菜单的仪表盘应用,下拉菜单由关键字 Dropdown 表示,如 清单 4-3 中所示。

app.layout = html.Div([

html.Div(dcc.Dropdown()),

html.Div(dcc.Dropdown()),

html.Div(dcc.Dropdown()),

])

Listing 4-3:示例 Div 代码(非主应用部分)

这一行 app.layout 为这个 Dash 应用创建了一个布局。所有与布局相关的内容都必须放在 app.layout 中。然后,我们创建了一个包含三个下拉菜单的 Div。

默认情况下,Div 会占据父容器的整个宽度,这意味着它被认为是一个占满整个页面宽度的大单元格。这样,第一个 Dropdown 会出现在左上角,并从左到右填满整个页面。第二个 Dropdown 会出现在第一个 Dropdown 的正下方,并同样占满整个页面的宽度,第三个 Dropdown 依此类推。换句话说,每个 Div 都会占据页面的整个宽度,并将相邻的元素强制移到新的一行。

为了最好地控制一个 Div 被分配的空间,我们应该将网页定义为一个由行和列组成的网格,并将每个 Div 放置在网格中的特定单元格内。我们可以使用预制的 CSS 样式表快速定义行和列。CSS(层叠样式表)是另一种用于定义页面如何显示的网页语言。我们将样式表放在外部文件中,或从在线目录中调用一个样式表到我们的应用程序中。我们使用的是来自 https://codepen.io 的样式表。该样式表由 Plotly Dash 的创始人 Chris Parmer 编写,内容全面,适合用于基本的 Dash 应用程序。在 Listing 4-4 中,我们导入了 CSS。我们还告诉 twitter_app.py 从网络获取 CSS 样式表并将其集成到应用程序中,然后用 Dash 实例化我们的应用程序。

stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(name, external_stylesheets=stylesheets)

Listing 4-4:将样式表导入 twitter_app.py

我们的 CSS 样式表使用 CSS 类描述页面上列和行的宽度和高度。我们只需要在 Dash 代码中引用这些类,就能将 Div 内容放入网格中的特定单元格。

首先,我们必须分配行,因为列应该由行包裹。为此,我们将字符串值 "row" 设置为 className。让我们基于 Listing 4-3 中的 Div 示例进行扩展,假设此代码已导入自定义样式表;新代码部分已加粗(参见 Listing 4-5)。

app.layout = html.Div([

html.Div(dcc.Dropdown()),

html.Div(dcc.Dropdown()),

html.Div(dcc.Dropdown()),

], className="row")

Listing 4-5: 带有 className 的示例 Div 代码 (不是应用程序的主要部分)

在这里,我们为包含所有三个下拉框的 html.Div 分配了一行,因此所有这些下拉框将显示在页面的同一行中(Figure 4-5)。className 是一个可以从 CSS 样式表分配类的属性,用于告诉 Dash 如何样式化元素。在这里,我们为其分配了 row 类,这告诉应用程序该 Div 中的所有组件应该位于同一行中。每个 Dash 组件都有一个 className,通常用于样式化和定义布局。我们使用 html.Div 的 className 属性来描述每个 Div 的行和列布局。

定义行之后,我们需要定义列的宽度,以便 Dash 知道在该行中为每个组件分配多少列空间。我们为行中包含的每个 html.Div 执行此操作,如 Listing 4-6 中加粗部分所示。

app.layout = html.Div([

html.Div(dcc.Dropdown(), className="four columns"),

html.Div(dcc.Dropdown(), className="four columns"),

html.Div(dcc.Dropdown(), className="four columns"),

], className="row")

清单 4-6:设置列宽(不是主应用的一部分)

我们为每个 Div 组件设置它应占用的列数空间,通过设置 className 为字符串值,并格式化为 "one column" 或 "two columns" 等等。大多数网页的列数最多为 12 列(且行数可能是无限的),这意味着组件的列宽总和不能超过 12,所以我们在这里将它们设置为每个占 4 列。请注意,我们不需要填满所有 12 列。

图 4-5 显示了这个简单页面的显示方式。

图 4-5:一行三个下拉框的演示

考虑到这些,让我们看看清单 4-7,我们twitter_app.py 文件中的 html.Div 部分,它包含不到 12 列。

html.Div(

[

❶ html.Div(

dcc.Dropdown(

id="my-dropdown",

multi=True,

options=[

{"label": x, "value": x}

for x in sorted(df["name"].unique())

],

value=["taylorswift13", "cristiano", "jtimberlake"],

),

className="three columns",

),

❷ html.Div(

html.A(

id="my-link",

children="点击这里访问 Twitter",

href="https://twitter.com/explore",

target="_blank",

),

className="two columns",

),

],

className="row",

),

清单 4-7:Dropdown 部分 twitter_app.py

我们可以看到,这一行包含两个Div:一个提供多个名人选择的Dropdown ❶ 和一个用户点击的链接 ❷。这两个Div的列数总和为五列,意味着它们在页面上是左对齐的,如图 4-6 所示。

图 4-6:宽度为五列的组件

请注意,一些样式表,包括我们正在使用的这个样式表,要求我们首先创建父级的Div并为其指定一个行(row)。然后,在父级Div的子元素中,我们定义每个内部Div的列宽。

样式:美化你的应用

样式是赋予应用程序生命的元素。我们可以添加颜色、改变文本的字体和大小、加下划线,等等。改变应用程序样式的主要方式有两种:第一种是在 Dash HTML 组件中使用style属性。这允许用户指定将直接应用于该组件的 CSS 样式声明。

第二种方法是引用一个 CSS 样式表,类似于我们创建行和列的方式。我们将向你展示如何将额外的样式表 mystyles.css 集成到应用中;如果你按照本章前面“设置项目”的说明下载了文件,它应该位于你的 assets 文件夹中。我们首先来看一下如何使用 style 属性来修改应用。

使用 style 属性

style 属性期望一个 Python 字典,其中的键指定我们想要修改的方面,值设置样式。在我们的 twitter_app.py 文件中,我们将通过在用于添加 URL 链接的 html.A 组件中定义 style 属性,将链接的文本颜色修改为红色,正如在 列表 4-8 中所示。

html.Div(

html.A(id="my-link", children="点击这里访问 Twitter",

href="https://twitter.com/explore", target="_blank",

❶   style={"color": "red"}),

className="two columns")

列表 4-8:为 twitter_app.py 中的 HTML 元素设置样式

在 ❶ 处,我们将一个字典赋值给 style 属性,其中键为 color,值为 red。这告诉浏览器将该链接的文本呈现为红色。

现在,我们通过向字典中添加另一个键值对,为同一个链接添加黄色背景色:

style={"color": "red", "backgroundColor": "yellow"}

请注意,字典的键是驼峰式命名的字符串。在 Dash 中,style 字典中的键应该始终使用驼峰式命名。

最后,我们将把链接的字体大小修改为 40 像素:

style={"color": "red", "backgroundColor": "yellow", "fontSize": "40px"}

Dash 的一个美妙之处在于,样式不仅限于 HTML 组件;我们还可以为核心组件添加样式,例如 Dropdown。例如,要将下拉选项的文本颜色更改为绿色,我们需要在 dcc.Dropdown 中添加 style 属性,正如 清单 4-9 所示。

html.Div(

dcc.Dropdown(id="my-dropdown", multi=True,

options=[{"label": x, "value": x}

for x in sorted(df["name"].unique())],

value=["taylorswift13", "cristiano", "jtimberlake"],

style={"color": "green"}),

className="three columns"),

清单 4-9:在 twitter_app.py 中样式化核心组件

如 图 4-7 左下角所示,下拉选项现在将显示为绿色,而不是黑色。

图 4-7:运行在您计算机上的下拉选项,显示为绿色

使用样式表

样式化应用组件的第二种方法是通过元素或类定义样式。通常,当样式需要大量代码时,我们会使用这种方法。为了减少应用中样式代码的数量,我们将样式代码放在外部的 CSS 样式表中。CSS 样式表也是可重用的;您可以定义一个特定的类,并将其应用于多个组件。

我们将使用的 CSS 样式表是 mystyles.css,它应该已经在您与本书资源一起下载的 assets 文件夹中。通过双击在 PyCharm 或您首选的文本编辑器中打开该 CSS 样式表,您应该会看到以下代码:

/*

h1 {font-size: 8.6rem; line-height: 1.35; letter-spacing: -.08rem;

margin-bottom: 1.2rem; margin-top: 1.2rem;}

*/

/* 是注释语法,因此,要启用样式,删除 CSS 代码上下的 /* 和 */ 符号。在这里,h1 是 选择器,它指定我们要应用样式的元素;在这种情况下,它是所有的 h1 元素。在大括号内部,我们声明属性和属性值,这些属性将设置应用内的各种样式。在这个例子中,我们将元素的字体大小设置为 8.6,行高设置为 1.35,字母间距设置为 -0.08,上下边距设置为 1.2。

清单 4-10 展示了我们应用中的 H1 标题组件是如何使用这个 CSS 样式表的。

html.Div(html.H1("Twitter 名人点赞分析",

style={"textAlign": "center"}),

className="row"),

清单 4-10:在 twitter_app.py 中的 html.H1 组件

html.H1 到 html.H6 组件用于定义标题,其中 H1 代表最高级别的标题,而 H6 代表最低级别的标题。图 4-8 展示了这种标题样式的效果。

图 4-8:使用 CSS 样式的应用标题

如你所见,如果将 图 4-8 与 图 4-6 进行对比,你会发现应用标题的字体大小变大了,标题上下边距增多,字母间距减少。如果应用的标题没有改变大小,请重启应用以查看效果。

如果你想将标题的字体大小恢复到较小的尺寸,只需通过重新插入 /* 和 */ 符号来注释掉 CSS 代码,如下所示:

/*

h1 {font-size: 8.6rem; line-height: 1.35; letter-spacing: -.08rem;

margin-bottom: 1.2rem; margin-top: 1.2rem;}

*/

你已经学会了如何通过纯 Python 操作应用程序的样式和布局。然而,这仅仅是开始。在第五章中,我们将深入探讨 dash-bootstrap-components,它将使仪表板应用程序的布局设计和样式变得更加简单和多样化。

Dash 组件

在这里,我们将概述 Dash 中一些常用的组件,这些组件由 dash-html-components 和 dash-core-components 库提供。还有许多其他组件库,甚至可以编写自己的组件!但是 dash-html-components 和 dash-core-components 包含了我们所需的大部分基本功能。HTML 组件通常用于构建网页布局,包括 Div、Button、H1 和 Form 等组件。Core 组件——如 Dropdown、Checklist、RangeSlider 等——用于创建交互式体验。所有 HTML 和 Core 组件都有属性,可以增强其功能。有关这些属性及其组件的完整列表,请访问 Dash 文档中有关 HTML 和 Core 组件的页面:https://dash.plotly.com/dash-core-components.

HTML 组件

Dash HTML 组件是用 Python 编写的,并会自动转换为 HTML,因此使用 Dash 应用程序时无需成为 HTML 或 CSS 的专家。以下这行 Python 代码

名人推特点赞分析

大致等同于以下这行由网页浏览器读取的 HTML:

html.H1("名人推特点赞分析")

现在可以用纯 Python 编写完整的仪表板应用程序:Python 永远!

要创建一个 HTML 组件,你需要使用点符号(dot notation)来连接 html 关键字和组件名称。例如,对于 Div 组件,你会使用 html.Div,正如我们之前所见。我们还看到了两个额外的 HTML 组件:html.H1,它创建一个顶级标题,和 html.A,它创建一个超链接。让我们更仔细地看看如何使用 html.H1 来表示页面的标题,标题本身以字符串的形式编写,如下所示:

html.H1("Twitter 喜欢分析:名人")

这将字符串赋值给 children 属性,这通常是接受子组件的任何组件的第一个位置参数。在这个上下文中,children 是一个属性,它将一个组件或元素(例如文本标签)放置在另一个组件内。完整地写出,前一行看起来像这样:

html.H1(children="Twitter 喜欢分析:名人")

在以下代码的前三个示例中,children 属性向页面添加文本。在最后一个示例中,使用 html.Div 时,children 属性将 html.H1 组件添加到页面中,该组件也包含文本。children 属性可以接受整数、字符串、Dash 组件或这些的列表。所有这些示例都是可能的:

html.H1(children=2),

html.H1(children=["Twitter 喜欢分析:名人"]),

html.H1(children=["Twitter 喜欢分析:名人"]),

html.Div(children=[

html.H1("Twitter 喜欢分析:名人"),

html.H2("Twitter 喜欢分析:名人")

])

html.A 组件创建了一个 HTML5 元素,用于创建超链接。在此组件中,如 清单 4-11 所示,我们使用了四个属性:id、children、href 和 target。

html.A(id="my-link", children="点击这里访问 Twitter",

href="https://twitter.com/explore", target="_blank")

清单 4-11:HTML 链接组件在 twitter_app.py

我们赋值给 href 的值是完整的链接目标地址,用户点击链接后将跳转到该位置。target 属性指示链接将在何处打开:如果其赋值为 _self,链接将在当前浏览器标签页中打开;如果赋值为 _blank,链接将在新的浏览器标签页中打开。children 属性定义了组件的内容,在这里是一个表示链接文本的字符串,用户在页面上看到的就是这些文本。

id 属性非常重要,因为 Dash 组件使用 id 来相互识别和交互,这赋予了仪表板应用其交互能力。稍后在本章的“回调装饰器”部分,我们将详细介绍这一点。目前,只需注意,赋值给 id 的值必须是唯一的字符串,以便用于识别该组件。

核心组件

Dash 核心组件是 Dash 库中的预构建组件,允许用户以直观的方式与应用程序进行交互。在这个应用程序中,我们使用了两个核心组件:Graph 和 Dropdown。要构建或访问特定的核心组件,我们使用 dcc 关键字并使用点号语法来指定组件名称,例如 dcc.Dropdown。

Graph 组件

Graph 组件允许你将数据可视化图表、图形和图像以 Plotly 的形式嵌入到你的应用中。它是核心组件中最受欢迎的之一,你很可能会在每个分析仪表板应用中看到它。

一个 Graph 组件有两个主要的属性:id 和 figure。以下是定义 Graph 组件的模板:

dcc.Graph(id="line-chart", figure={})

id 属性为 Graph 组件提供了一个唯一的 ID。figure 属性是 Plotly 图表的占位符。一旦创建了 Plotly 图表,我们会将其分配给 figure 属性,替代空字典。例如,在我们的应用中,我们使用 列表 4-12 中的代码创建了一个 Plotly 线图。

import plotly.express as px

--省略--

fig = px.line(data_frame=df_filtered, x="date_time", y="number_of_likes",

color="name", log_y=True)

列表 4-12:在 twitter_app.py 中创建 Plotly 图表

我们将在本章的“Plotly Express 线图”部分详细介绍 Plotly 图表。现在,这一行代码简单描述了图表的外观,并将其分配给 fig 对象,从而将其转化为一个 Plotly 图形。然后,我们可以将 fig 插入到 dcc.Graph 的 figure 属性中,以在页面上显示线图。列表 4-13 显示了来自 twitter_app.py 文件的代码,它就是这样做的,并将图表分配给 app.layout。

html.Div(dcc.Graph(id="line-chart", figure=fig), className="row")

列表 4-13:将图表引入 Graph 组件中,位于 twitter_app.py

我们将 Graph 组件放入 Div 组件中,并将其分配给页面上的单个行。一旦完整的应用脚本激活,折线图应该会显示在页面上。

要观看关于 Dash Graph 组件及其用法的完整视频教程,请访问视频 “关于 Graph 组件的一切” 在 https://learnplotlydash.com

Dropdown 组件

Dropdown 组件允许用户从下拉菜单中选择选项,以动态过滤数据并更新图表。我们通过为四个属性提供值来定义 Dropdown 组件:id、multi、options 和 value,如 列表 4-14 所示。此菜单显示在 图 4-9 中。

dcc.Dropdown(id="my-dropdown", multi=True,

options=[{"label": x, "value": x}

for x in sorted(df["name"].unique())],

value=["taylorswift13", "cristiano", "jtimberlake"])

列表 4-14:在 twitter_app.py 中创建一个 Dropdown 组件

multi 属性允许我们选择用户是否可以一次选择多个值或一次只能选择一个值。当此属性设置为 True 时,应用用户可以选择多个值。当设置为 False 时,应用用户一次只能选择一个值。

options 属性表示用户在点击 Dropdown 时可以选择的值。我们为其分配一个字典列表,每个字典包含 label 和 value 键,其中每个字典表示一个菜单选项。label 是用户看到的选项名称,而 value 是应用程序读取的实际数据。

图 4-9:应用程序下拉选项

在清单 4-14 中,我们使用列表推导为我们的 pandas DataFrame 的name列中的每个唯一值创建一个包含label和value键的字典。

如果我们只有几个值,可能更容易逐个编写每个字典,而不是使用列表推导。例如,在清单 4-15 中,我们构建了一个仅包含两个值的Dropdown:taylorswift13和cristiano。

dcc.Dropdown(id="my-dropdown", multi=True,

options=[{"label": "Taylor", "value": "taylorswift13"},

{"label": "Ronaldo", "value": "cristiano"}]

)

清单 4-15:不在 twitter_app.py 的一个下拉示例

在这里,我们使用 DataFrame 中的值,以便更容易进行过滤。但是,我们可以选择一种用户友好的表示方式,将label键使其对用户更易识别。当用户点击下拉框时,他们将看到两个选项TaylorRonaldo,这些选项分别被应用程序读取为taylorswift13和cristiano。

最后一个Dropdown属性是value(不要与字典中的value键混淆),它包含了用户启动应用程序时Dropdown将采用的默认值。由于我们有一个多值Dropdown,我们使用来自 DataFrame 的name列的三个字符串作为初始值:taylorswift13,cristiano和jtimberlake。

这些字符串对应于在列表 4-14 中options属性生成的值。这些字符串是预加载的,因此在用户点击下拉菜单之前,这三个值会自动被选择。一旦用户在下拉菜单中选择了不同的值,这些值也会相应地变化。

关于 Dash Dropdown 组件及其用法的完整视频教程,请查看视频“Dropdown 选择器”在https://learnplotlydash.com

Dash 回调

一个 Dash 回调 使用户能够在仪表板应用中进行交互;它是将 Dash 组件彼此连接的机制,以便执行某个操作会导致其他事情发生。当应用用户选择下拉值时,图表会更新;当用户点击按钮时,应用标题的颜色会改变或另一个图表会被添加到页面。Dash 组件之间的可能交互是无限的,没有回调,应用程序将是静态的,用户无法修改任何内容。

Dash 回调有两个部分,一个是回调装饰器,用于识别相关组件,该部分在布局部分定义:

@app.callback()

和回调函数,它定义了 Dash 组件之间如何交互:

def function_name(y):

return x

这个简单的应用程序只有一个回调,虽然更复杂的应用程序会有很多个回调。

回调装饰器

回调装饰器将回调函数注册到你的 Dash 应用中,告诉它何时调用该函数,以及如何使用函数的返回值来更新应用程序。(我们在第一章中讨论了装饰器。)

回调装饰器应该放置在回调函数的正上方,装饰器和函数之间不能有空格。装饰器有两个主要参数:Output 和 Input,它们分别表示在用户对另一个组件进行操作时,应该改变的组件(Output)和用户操作的组件(Input)。例如,输出可能是折线图,其变化应根据用户在 Dropdown 组件中的输入而变化,如列表 4-16 所示。

@app.callback(

Output(component_id="line-chart", component_property="figure"),

[Input(component_id="my-dropdown", component_property="value")],

)

Listing 4-16: 来自 twitter_app.py 的回调装饰器

Output 和 Input 都接受两个参数:component_id,它应该对应于特定 Dash 组件的 id,以及 component_property,它应该对应于该组件的特定属性。在 Listing 4-16 中,Input 的 component_id 指的是我们之前定义的 my-dropdown Dropdown。component_property 具体指的是 my-dropdown 的 value 属性,它表示要显示的 Twitter 用户数据,初始值为 ["taylorswift13", "cristiano", "jtimberlake"],如 Listing 4-14 中所示。

在 Output 中,我们引用了 dcc.Graph 的 figure 属性,我们在布局中也之前定义了它,如 Listing 4-17 中所示。

dcc.Graph(id="line-chart", figure={})

Listing 4-17: 在 twitter_app.py 中的布局部分的 Graph 组件

在这里,figure 属性目前是一个空字典,因为回调函数将根据输入值创建一个折线图并将其分配给 figure。让我们深入了解回调函数,全面理解这一过程是如何发生的。

回调函数

我们应用的回调函数名为 update_graph(),它包含一系列的 if-else 语句,用于筛选 DataFrame df 并根据选择的输入值创建折线图。Listing 4-18 显示了我们应用中的回调函数。

def update_graph(chosen_value):

print(f"用户选择的值: {chosen_value}")

if len(chosen_value) == 0:

return {}

else:

df_filtered = df[df["name"].isin(chosen_value)]

fig = px.line(

data_frame=df_filtered,

x="date_time",

y="number_of_likes",

color="name",

log_y=True,

labels={

"number_of_likes": "点赞数",

"date_time": "日期",

"name": "名人",

},

)

return fig

清单 4-18: twitter_app.py 中的回调函数 twitter_app.py

我们稍后会逐行讲解这里的逻辑。但首先,让我们讨论一下这个函数的作用。执行时,update_graph() 会返回一个名为 fig 的对象,在本例中,它包含了 Plotly Express 的折线图。对象 fig 被返回到我们在回调装饰器中指定的组件和属性的 Output 中。正如我们所知道的,回调装饰器引用了布局中的 Dash 组件。这里,fig 被赋值给布局部分中的 Graph 组件的 figure 属性,因此回调函数告诉应用程序显示折线图。以下是回调函数 update_graph() 执行后,Graph 组件的样子:

dcc.Graph(id="line-chart", figure=fig)

figure 属性现在被分配给了对象 fig,而不是我们在 F 显示 4-17 中看到的空字典。

我们将总结这一点,因为这个过程非常重要!一旦用户输入激活回调函数,它会返回一个与回调装饰器中Outputcomponent_property相关联的对象。考虑到组件属性代表应用布局中某个组件的实际属性,结果就是一个通过用户交互不断更新的应用。

想了解 Dash 回调装饰器及其使用的完整视频教程,请参阅视频“Dash 回调——输入、输出、状态等”:https://learnplotlydash.com

激活回调

要激活回调,用户必须与回调装饰器中指定的 Input 组件进行交互。在这个应用中,组件的属性表示 Dropdown 的值,因此每当应用用户选择不同的下拉值(如 Twitter 账号)时,回调函数就会被触发。

如果回调装饰器有三个 Input,用户需要提供三个参数来触发回调函数。而在我们的例子中,回调装饰器只有一个 Input;因此,回调函数只需要一个参数:chosen_value。

函数如何工作

让我们查看一下 F 显示 4-19,它展示了应用回调函数内部发生的情况。

❶  def update_graph(chosen_value):

print(f"用户选择的值:{chosen_value}")

❷ if len(chosen_value) == 0:

return {}

else:

df_filtered = df[df["name"].isin(chosen_value)]

fig = px.line(

data_frame=df_filtered,

x="date_time",

y="number_of_likes",

color="name",

log_y=True,

labels={

"number_of_likes": "点赞数",

"date_time": "日期时间",

"name": "名人",

},

)

return fig

清单 4-19: 回调函数用于 twitter_app.py

chosen_value 参数 ❶ 指的是 dcc.Dropdown 的值,这是一个 Twitter 用户名的列表。每当用户选择新选项时,函数会被激活。用户可以选择任意数量的可用名人,chosen_value 列表中的项目数量将相应增加或减少。它可能是一个包含 3 个值、10 个值,甚至没有值的列表。因此,我们检查 chosen_value 列表的长度 ❷。如果它等于零,意味着是一个空列表,函数将返回一个空字典,并且返回的 fig 对象会显示一个空图表。

如果 chosen_value 列表的长度不等于零,在 else 分支中,我们使用 pandas 过滤 DataFrame,只保留包含选定 Twitter 用户名的行。过滤后的 DataFrame 被保存为 df_filtered,然后用作创建折线图的数据,该图被保存为 fig 对象。fig 对象被返回,用于在应用页面上显示折线图。

关于这些函数的一个重要提示:如果原始的 DataFrame 被以任何方式修改,你应该始终创建原始 DataFrame 的副本,正如我们在创建df_filtered时所做的那样。应用开始时定义的原始 DataFrame,在清单 4-2 中,是被视为全局变量的。全局变量不应被修改,因为修改它会影响到应用中其他用户所看到的变量。例如,如果某个用户在一个财务仪表板应用中修改了全局变量price_values,所有用户都将看到这些变化的价格。这可能会造成严重的损害和混乱。

回调图

Dash 有一个强大的回调图工具,可以显示回调的结构,并清晰地描述元素之间的联系。在定义回调时,特别是当回调具有多个输入和输出时,使用此工具非常有帮助,因为这时很难掌握回调结构。要打开回调图,请点击应用页面右下角的蓝色按钮,如图 4-10 所示。

图 4-10:点击右下角的按钮以打开菜单。

然后点击灰色的回调按钮,如图 4-11 所示。

图 4-11:点击回调按钮查看回调图。

结果应如图 4-12 所示。

图 4-12:回调图展示了 twitter_app.py

左侧的元素是输入的组件属性。中间的元素描述了回调在本次会话中被触发的次数(在本例中为一次),以及回调完全执行所需的时间(614 毫秒)。右侧的元素是输出的组件属性。该图帮助清晰地展示了下拉值(输入)如何影响折线图的图形(输出)。

继续通过更改主应用页面上的 Dropdown 名人名字来触发回调。你看到中间的绿色元素发生变化了吗?通过点击左侧和右侧的元素来探索这个图表;你应该会看到每个元素中的额外信息。

在将应用部署到网络之前,确保通过将 debug = False 关闭调试模式,以便关闭图表。否则,最终用户也可以访问该图表。

Plotly Express 折线图

在这里,我们将回顾如何创建 Plotly 图表。我们将专注于折线图,因为这是我们在这个应用中使用的图表类型,其他类型的图表将在未来的章节中回顾。

Plotly Express 是一个用于快速直观创建图表的高级接口。它包含了数十种图表类型供选择,涵盖了从科学、统计、金融图表到 3D 图表和地图等各种图表。每个图表都有许多属性,允许你根据用户的需求自定义图表。以下是适用于 Plotly Express 折线图的所有属性的完整列表,目前都设置为 None:

plotly.express.line(data_frame=None, x=None, y=None, line_group=None, color=None, line_dash=None, hover_name=None, hover_data=None, custom_data=None, text=None, facet_row=None, facet_col=None, facet_col_wrap=0, facet_row_spacing=None, facet_col_spacing=None, error_x=None, error_x_minus=None, error_y=None, error_y_minus=None, animation_frame=None, animation_group=None, category_orders={}, labels={}, orientation=None, color_discrete_sequence=None, color_discrete_map={}, line_dash_sequence=None, line_dash_map={}, log_x=False, log_y=False, range_x=None, range_y=None, line_shape=None, render_mode='auto', title=None, template=None,width=None, height=None)

Plotly Express 的美妙之处在于,在大多数情况下,创建图表所需了解的仅仅是前三个属性:data_frame、x 和 y,在示例中以粗体显示。这些分别表示数据框、用于 x 轴的数据列和用于 y 轴的数据列。下面是我们绘制的一个非常简单的折线图:

import plotly.express as px

px.line(data_frame=df, x="some_xaxis_data", y="some_yaxis_data")

fig.show()

这创建了最基本的折线图,绘制了两列数据之间的关系,类似于图 4-13。

图 4-13:最简单的折线图

随着你越来越熟悉 Plotly Express,你会发现自己不断向图表添加更多的属性。例如,为了通过颜色区分不同的数据组,我们添加了color属性,并将其分配给假设 DataFrame 中的一列:

px.line(data_frame=df, x="some_ xaxis _data", y="some_yaxis_data", color="some_data")

结果,我们会看到类似于图 4-14 的内容。

图 4-14:向简单图表添加颜色属性

要改变图形的高度,我们添加了height属性,并为其分配了一个像素值:

px.line(data_frame=df, x="some_xaxis_data", y='some_yaxis_data', height=300)

这里我们将整个图形的高度设置为 300 像素。

在我们的 Twitter 点赞分析应用中,折线图包括了<data_frame>、x、y、color属性,以及labels和log_y属性。清单 4-20 展示了我们的 Plotly 图表代码。

fig = px.line(

data_frame=df_filtered,

x="date_time",

y="number_of_likes",

color="name",

log_y=True,

labels={

"number_of_likes": "Likes",

"date_time": "Date",

"name": "Celebrity",

},

)

清单 4-20:Plotly 折线图用于 twitter_app.py

log_y 属性告诉应用使用对数刻度来显示 y 轴数据。当图表中的一些数据点比大部分数据点要大或小得多时,推荐使用对数刻度,因为它可以提供更清晰的可视化。我们不会在这里详细讨论对数刻度,但可以尝试将该属性从 True 更改为 False,然后刷新应用查看更新后的图表。你更喜欢哪一种?

labels 属性更改了应用用户所看到的轴标签。用于绘制折线图的三列是 date_time(x 轴)、number_of_likes(y 轴)和 name(颜色)。这些是 pandas DataFrame 中列的名称,我们必须保持它们的格式和拼写以确保与正确的列匹配。通过 labels 属性,我们改变用户在应用页面上看到的内容,使其更加用户友好,例如 number_of_likes 变成了 Likes。

每个属性在 Plotly 文档中都有详细描述,网址为 https://plotly.com/python-api-reference。花时间阅读这些描述是值得的,因为它将帮助你了解如何自定义折线图和其他类型的图形。

有关带有 Dropdown 的 Plotly Express 折线图的完整视频教程,请观看视频“Line Plot (Dropdown)”在 https://learnplotlydash.com

工具提示

有一个我们在应用中没有使用,但足够常见值得在这里提到的属性: hover_data,它允许你在用户用鼠标悬停在图表的特定元素上时,在工具提示中提供额外的信息。你可以将分配给 hover_data 的值放在列表或字典中。

当你使用列表时,图表的悬停工具提示将包含列表中的值。例如,如果我们使用 number_of_shares 列作为 hover_data 列表,悬停工具提示将在用户悬停在图表的线条上时包含这些数据。要尝试此方法,请做出以下更改并重新运行应用:

fig = px.line(data_frame=df_filtered, x="date_time", y="number_of_likes",

color="name", hover_data=["number_of_shares"])

下图展示了悬停信息的区别。

示例工具提示,包含“分享次数”在悬停数据中

完成后,请确保删除此更改。

当你使用字典而不是列表时,键是数据框(DataFrame)的列,值是布尔值,用于控制是否在悬停工具提示中显示数据(True)或不显示(False)。例如,如果你将 number_of_likes 列作为字典的键,并将 False 作为字典的值,那么代表每个名人喜欢数量的数据将不再显示在悬停工具提示中:

hover_data={"number_of_likes": False}

我们还可以使用 hover_data 字典来格式化在工具提示中显示的悬停数据。例如,默认情况下, number_of_likes 用字母“k”表示 10,000(200,000 会写作 200k)。然而,如果我们更喜欢显示带有逗号作为千位分隔符的完整数字(200,000),我们可以使用:

hover_data={"number_of_likes": ':,'}

总结

本章向你介绍了一个基础 Dash 应用的基本元素:编程应用所需的 Python 库、使用的数据、Dash HTML 和 Core 组件、使用布局部分将应用组件定位到页面上、使用回调函数将组件连接在一起并创建交互性,以及 Plotly Express 绘图库。在下一章,我们将基于这里学到的技能,开发更复杂的 Dash 应用。

第五章:5 全球数据分析:高级布局 与图表

在本章中,你将通过构建一个应用程序来扩展你对 Dash 的知识,该应用程序比较和分析世界数据的三个指标:互联网使用率、女性在议会中的比例和二氧化碳(CO[2])排放量。我们将这些指标称为指标。我们将更深入地了解 Dash 回调函数,并学习绘制热力图,这是一种通过在地图上特定区域(如国家、州、省等)内的阴影和颜色来表示定量数据的方式。你还将发现一种新的布局和样式管理方法,使用 dash-bootstrap-components 库,该库提供了复杂且响应式的布局。

为了收集这个应用程序的数据,我们将使用 pandas 访问世界银行的应用程序编程接口(API)。API 提供了一个接口,允许你连接到外部服务器并请求数据以供应用程序使用。

在本章结束时,你将能够更自如地在地图上绘制数据,管理更复杂的布局,理解回调函数,并使用 dash-core-components。但首先,让我们先设置应用程序和相应的代码。

设置项目

像往常一样,首先你需要创建项目文件夹并将应用程序代码放入其中。创建一个新的项目文件夹,命名为world-bank-app,并将 ZIP 文件中包含的* 第五章 文件夹放入其中,该文件夹可以从 https://github.com/DashBookProject/Plotly-Dash 下载,下载位置在第四章开始时提供。该文件夹应该包含两个文件:worldbank.py* 和 our_indicator.py。将这些文件复制到你的 world-bank-app 文件夹中。

项目文件夹应该如下所示:

  • world-bank-app

|--our_indicator.py

|--worldbank.py

我们需要四个库:常用的 pandas 和 Dash 库,以及 dash-bootstrap-components 和 pandas datareader。打开命令提示符(Mac 用户使用 Terminal)或在 PyCharm 或你选择的 Python IDE 中打开 Terminal 标签。然后,逐行输入以下命令来安装这四个库:

$ pip install pandas

$ pip install dash

$ pip install dash-bootstrap-components

$ pip install pandas-datareader

为了检查库是否已正确安装,请输入:

$ pip list

这将列出你当前安装的所有 Python 包。如果我们需要的四个库中有任何一个没有列出,请尝试重新输入相应的安装命令。

在查看代码之前,我们先来了解一下这个应用程序。打开你的 IDE 中的worldbank.py文件并运行脚本。你应该会看到一条带有 HTTP 链接的消息。点击该链接或将其复制到浏览器中:

Dash 正在运行于 http://127.0.0.1:8050/

* 正在服务 Flask 应用 "worldbank"(懒加载)

* 环境:生产

警告:这是一个开发服务器,勿在生产环境中使用。

请改用生产环境中的 WSGI 服务器。

* 调试模式:开启

你现在应该能看到世界银行数据分析仪表板应用,如图 5-1 所示。

图 5-1:世界银行数据分析应用

玩得开心一点!使用滑块更改日期,并使用单选按钮选择一个不同的世界银行数据指标,例如议会席位或 CO[2] 排放量的指标。移动地图,鼠标悬停在某些国家上以比较它们的数据。哪个国家在议会中女性的比例最高?哪个国家的互联网使用比例在时间上增长最大?熟悉一下应用程序,随着我们逐步分析代码,你会更容易理解。

导入库

在这个应用程序中,我们引入了两个新的 Python 库:dash-bootstrap-components 和 pandas datareader。

dash-bootstrap-components 是一个使得管理应用布局更加便捷的包。Bootstrap 提供了一些组件,可以让你精确地将应用元素放置在页面上,创建更多的组件(如图表和单选按钮),并且对每个元素进行非常详细的样式设计。它基本上是 Dash 内置布局能力的一个附加功能。

我们将使用 pandas 来过滤并准备数据进行绘图,就像我们在第四章中的应用一样。然而,这个应用还将使用 pandas datareader,这是一个通过 API 获取数据并从中创建 DataFrame 的 pandas 扩展。pandas datareader 扩展提供了从多个常见的互联网来源提取数据的方法,如 NASDAQ、加拿大银行、世界银行等。我们的应用只使用世界银行的数据,因此为了访问这些数据,我们需要从 datareader 扩展中导入 wb 世界银行模块,如清单 5-1 所示。

import dash_bootstrap_components as dbc

from pandas_datareader import wb

清单 5-1:worldbank.py 应用的导入部分

数据管理

下一部分是数据管理代码,我们将通过世界银行 API 将数据导入到我们的应用中。我们还将清理数据,去除损坏的值,提取我们需要的数据,并将其与另一个 DataFrame 合并以获取缺失的值。

连接到 API

连接到 API 使我们的应用能够动态读取数据,让我们能够即时添加和更改正在读取的数据,而不需要修改和上传静态的 Excel 文件。通过使用 pandas datareader 连接到 API,我们可以在请求时立即将新数据上传到应用中。

需要注意的是,一些 API 对个人请求的次数有限制,以防止 API 受到过载。如果超过这个限制,可能会被阻止在一段时间内继续发起请求。请求之间设置超时是避免过载 API 的一种方式。

wb 模块包含用于获取与世界银行相关的不同类型数据的函数。例如,download() 函数在传入一个指标作为参数时,会从世界银行的《世界发展指标》提取信息,而 get_countries() 会查询指定国家的信息。我们将在应用中重点使用这两个函数。

让我们从下载所需的国家数据开始,将其导入到我们的应用中,如清单 5-2 所示。

countries = wb.get_countries()

countries["capitalCity"].replace({" ": None}, inplace=True)

❶ countries.dropna(subset=["capitalCity"], inplace=True)

❷ countries = countries[["name", "iso3c"]]

countries = countries[countries["name"] != "Kosovo"]

countries = countries.rename(columns={"name": "country"})

清单 5-2:从世界银行 API 下载国家数据 worldbank.py 应用程序

首先,我们连接到世界银行 API,并使用 get_countries() 提取所有国家的名称。然而,数据并不像我们希望的那样干净,其中一些行实际上包含的是地区名称而不是国家名称。例如,如果你使用以下代码打印前 10 行:

countries = wb.get_countries()

print(countries.head(10)[['name']])

exit()

你会发现,第 1 行包含的是“非洲东部和南部”地区。我们的应用程序只关注国家,因此我们使用 dropna() 来排除地区,删除所有没有首都城市的行 ❶,这样我们应该只剩下国家名称。

为了在地图上绘制点,Plotly 使用国家代码而不是国家名称,因此接下来我们需要为应用程序提供国家代码。这些代码称为 alpha-3ISO3 代码,每个国家都有不同的代码。例如,奥地利的代码是 AUT,阿塞拜疆是 AZE,布隆迪是 BDI,等等。

我们不需要 get_countries() 返回的其他信息,因此我们将数据框限制为两个必要的列:name 列和 iso3c 国家代码列 ❷。

作者之前对我们的应用程序进行的实验表明,科索沃的 ISO3 数据已损坏,因此我们过滤掉数据框中的科索沃行。最后,我们将 name 列重命名为 country,以便后续更容易与另一个数据框合并(参见 清单 5-4)。

识别指标

构建好国家的 DataFrame 后,我们需要提取与三个指标相关的世界银行数据:互联网使用、女性政治家和排放数据。我们首先需要找到指标的确切名称,然后找到相应的 ID,以便正确查询 API。我们直接从世界银行网站获取指标名称。访问https://data.worldbank.org/indicator。要获取互联网使用指标的名称,点击页面顶部的 All Indicators 标签。然后,在基础设施部分,点击 Individuals Using the Internet (% of Population)。这就是我们将在应用中使用的指标的确切名称。如果世界银行网站更改了指标名称,确保你搜索相似的名称并获取准确的名称。如果你遇到困难,我们也会保持书中的代码资源更新。

接下来,我们使用指标名称通过你从书籍资源下载的 our_indicator.py 文件来获取其 ID。在你的项目文件夹中,打开 our_indicator.py 文件,并在新的 IDE 窗口中运行它:

df = wb.get_indicators()[['id','name']]

df = df[df.name == 'Individuals using the Internet (% of population)']

print(df)

这只是从 DataFrame 中抓取与世界银行网站相关的 name 和 id 列的条目。输出应该显示与该指标相关的 ID:

id name
8045 IT.NET.USER.ZS Individuals using the Internet (% of population)

你需要重复这个过程,从世界银行网站获取其余两个指标的名称,方法是将 'Individuals using the Internet (% of population)' 替换为另外两个指标的名称:'Proportion of seats held by women in national parliaments (%)',位于性别部分,以及 'CO2 emissions (kt)',位于气候变化部分。同样,这些名称会不时变化,因此如果没有结果,确保你在世界银行的指标页面上进行搜索,找到最接近的匹配项。然后,我们将指标的名称和 ID 存储在 worldbank.py 文件中的字典里,后续会用到,如清单 5-3 所示。

indicators = {

"IT.NET.USER.ZS": "使用互联网的个人(占人口比例)",

"SG.GEN.PARL.ZS": "女性在国家议会中所占席位比例(%)",

"EN.ATM.CO2E.KT": "二氧化碳排放量(千吨)",

}

列表 5-3:在 worldbank.py 中定义指标

你下载的主要代码会有这些 ID,但自己练习获取这些 ID 是很有用的,因为它们会不时发生变化。

提取数据

现在我们可以构建一个函数,下载这三个世界银行指标的历史数据,如列表 5-4 所示。我们将把数据保存到一个新的 DataFrame 中,名为 df。

def update_wb_data():

# 从 API 获取特定的世界银行数据

df = wb.download(

indicator=(list(indicators)), country=countries["iso3c"],

start=2005, end=2016

)

df = df.reset_index()

df.year = df.year.astype(int)

# 将国家 ISO3 ID 添加到主数据框

df = pd.merge(df, countries, on="country")

df = df.rename(columns=indicators)

return df

列表 5-4:历史数据下载部分 worldbank.py 中的代码

我们使用 wb.download() 方法来获取数据,该方法有一些参数。第一个是 indicator,它接受一个表示指标 ID 的字符串列表。在这里,我们将其赋值为 Listing 5-3 中 indicators 字典的键。下一个参数是 country,它接受一个表示国家 ISO3 代码的字符串列表。我们将其赋值为 Listing 5-2 中创建的 countries DataFrame 的 iso3c 列。最后,start 和 end 参数允许我们定义要拉取数据的年份范围。我们停留在 2016 年,因为那是世界银行为 CO[2]指标提供完整数据的最后一年。

然后,我们重置索引,使得 country 和 year 成为新的列,而不再是索引的一部分,并且我们拥有一个专门的 index 列,其中只包含整数,这将在后续的过滤操作中派上用场。你可以在 Listing 5-5 中看到重置索引的效果,我们展示了重置前后的 DataFrame。

IT.NET.USER.ZS SG.GEN.PARL.ZS EN.ATM.CO2E.KT
country year
阿鲁巴 2016 93.542454 NaN NaN
2015 88.661227 NaN NaN
2014 83.780000 NaN NaN
2013 78.900000 NaN NaN
2012 74.000000 NaN NaN
津巴布韦 2009 4.000000 14.953271 7750.0
2008 3.500000 15.238095 7600.0
2007 3.000000 16.000000 9760.0
2006 2.400000 16.666667 9830.0
2005 2.400000 16.000000 10510.0
[2520 行 x 3 列]
df.reset_index()
国家 年份 IT.NET.USER.ZS SG.GEN.PARL.ZS EN.ATM.CO2E.KT
0 阿鲁巴 2016 93.542454 NaN NaN
1 阿鲁巴 2015 88.661227 NaN NaN
2 阿鲁巴 2014 83.780000 NaN NaN
3 阿鲁巴 2013 78.900000 NaN NaN
4 阿鲁巴 2012 74.000000 NaN NaN
2515 津巴布韦 2009 4.000000 14.953271 7750.0
2516 津巴布韦 2008 3.500000 15.238095 7600.0
2517 津巴布韦 2007 3.000000 16.000000 9760.0
2518 津巴布韦 2006 2.400000 16.666667 9830.0
2519 津巴布韦 2005 2.400000 16.000000 10510.0
[2520 rows x 5 columns]

列表 5-5:重置索引前后的 DataFrame

在重置索引之前,country 和 year 是索引的一部分,但不是与索引元素相关联的结果行的一部分。重置索引后,它们都变成了 DataFrame 的独立列,这使得访问包含国家和年份数据的各行变得更加容易。

接下来,我们将 year 列中的值从字符串转换为整数,以便以后能用 pandas 正确地过滤数据。原始的 df DataFrame 中不包含我们需要的 ISO3 国家代码来查询 API,因此我们从 countries DataFrame 中提取这些代码,并通过 pd.merge 将两个 DataFrame 按照 country 列进行合并。最后,我们重命名列名,使其显示指标名称而非 ID,以便于人类阅读。例如,列 IT.NET.USER.ZS 现在将被命名为 使用互联网的个人(占总人口的百分比)。

update_wb_data() 函数现在已经完成,并将在应用启动时在第一个回调中被调用。你将在本章稍后学习这个过程。与此同时,我们来学习如何使用 dash-bootstrap-components 创建布局并为应用添加样式。

Dash Bootstrap 样式

Dash Bootstrap 是一个强大的工具,可以为 Dash 应用添加样式,帮助我们创建布局、为应用加样式,并添加 Bootstrap 组件,如按钮和单选项。按钮和单选项在 dash-core-components 中也存在,但我们将使用 dash-bootstrap-components 版本,以便与其他 Bootstrap 样式更好地兼容。Bootstrap 还包含存储各种样式表主题的模块,这些模块将项作为字符串存储,使我们可以简单地包含这些模块的链接来样式化元素。

为了将 Bootstrap 纳入 Dash 应用,我们首先必须选择一个主题,并将其分配给 external_stylesheets 参数,该参数位于导入部分下方,如 列表 5-6 所示。

import dash_bootstrap_components as dbc

from pandas_datareader import wb

app = Dash(name, external_stylesheets=[dbc.themes.BOOTSTRAP])

列表 5-6:Dash 被实例化的 worldbank.py 部分

Bootstrap 主题是一个在线托管的样式表,用于决定页面元素的字体类型、颜色、形状和大小。

在这个应用中,我们使用默认主题 BOOTSTRAP,它是主题列表中的第一个主题。Bootstrap 还有其他几个主题可以选择。要查看这些主题,你可以访问 https://hellodash.pythonanywhere.com,然后点击页面左侧的 Change Theme 按钮。如果你喜欢,可以更换本应用的主题;只需确保在将其分配给 external_stylesheets 参数时使用大写的准确名称。每次只能分配一个主题,因此如果选择了新主题,请确保替换掉 BOOTSTRAP。

若要观看完整的 Dash Bootstrap 视频教程,请查看视频“完整的 Bootstrap 仪表盘应用指南”,网址为 https://learnplotlydash.com

布局

正如你所知,我们通常将应用的布局称为网格,网格通常由 12 列和无限行组成。为了开始构建布局,我们需要创建一个容器来容纳所有的行和列,以及将放置在其中的组件。dbc.Container 语法与 html.Div 非常相似,但它更兼容 Bootstrap 样式。首先,我们声明行,然后声明每行内部的列。最后,我们将应用组件放入列中。最后一步确定了每个组件在页面上的位置。

为了避免向你展示我们应用中用于创建布局的 80 行代码,列表 5-7 提供了一个简化版本,去除了每个 html、dcc 和 dbc 组件中的 props,只显示了整体结构。

app.layout = dbc.Container(

[

❶ dbc.Row(

dbc.Col(

[

html.H1(),

dcc.Graph()

],

width=12,

)

),

❷ dbc.Row(

dbc.Col(

[

dbc.Label(),

dbc.RadioItems(),

],

width=4,

)

),

❸ dbc.Row(

[

dbc.Col(

[

dbc.Label(),

dcc.RangeSlider(),

dbc.Button()

],

width=6,

),

]

),

]

)

列 5-7:简化的应用布局

该应用包含三行。第一行 ❶ 有一个列组件,宽度为 12 列,其中包含 H1 标题和 Graph 可视化组件。这些对应于应用中的标题和区域图,见 图 5-1。

第二行 ❷ 中,我们放置了一个仅占四列宽度的列组件,里面放置了 Label 和 RadioItems。这些对应于应用中的“选择数据集”副标题和其下的三个单选按钮。

最后一行 ❸ 包含了 Label、RangeSlider 和 Button,它们都被包裹在一个宽度为六列的列组件中。

多组件行

需要重申的是,构建仪表板时,一种有效且受欢迎的方法是每页设置最多 12 列,并允许组件跨越多个列的宽度。在这个应用中,每行只有一个列组件,但如果我们要在一行中添加多个组件,我们必须确保它们的总宽度不超过 12。让我们来看一个例子:

dbc.Row([

dbc.Col([dropdown, button, checkbox], width=6),

dbc.Col([dropdown, slider, date-picker], width=5),

]),

在上面的代码中,总宽度为 11,这意味着所有的 Dash 组件会显示在同一行。以下是一个不推荐的示例:

dbc.Row([

dbc.Col([dropdown, button, checkbox], width=8),

dbc.Col([dropdown, slider, date-picker], width=6),

]),

当总宽度为 14 时,第二个 dbc.Col 中的 Dash 组件将换行显示在第一个 dbc.Col下面,导致出现两行而非一行,这可能会破坏你的布局。

组件与样式

Dash Bootstrap 组件类似于 Dash Core 组件,但其优势在于它们更易于使用,并且能与 Bootstrap 样式表更好地集成。在我们的应用中,我们使用了三个 Bootstrap 组件:Label、RadioItems 和 Button。让我们来看一下 Button 和 RadioItems 组件。

我们通过五个属性来定义 Button:id、children、n_clicks、color 和 className,如 Listing 5-8 所示。

dbc.Button(

id="my-button",

children="Submit",

n_clicks=0,

color="primary",

className="mt-4",

),

Listing 5-8: 定义一个 Bootstrap 按钮

id 属性用于唯一标识该组件,并将在 Dash 回调中分配给 component_id,以便与其他组件进行交互。这里我们将它命名为 my-button。children 属性表示按钮上显示的文本。n_clicks 属性计算按钮被用户点击的次数,因此我们将其初始化为 0。color 属性设置按钮背景的颜色。这里,它被分配了 Bootstrap 上下文颜色 primary,表示蓝色(我们也可以使用 secondary 来使其为灰色,success 表示绿色,warning 表示橙色,或 danger 表示红色)。请注意,primary 所代表的颜色取决于你选择的主题;如果你为 Dash 应用选择了 LUX 主题,那么 primary 将代表黑色,而 secondary 将是白色。

className 控制组件的样式。在这里,我们为它分配了 Bootstrap 类 mt-4,它控制按钮顶部和组件之间的边距大小。mt 代表 margin top,而 –4 在组件上方创建了四个单位的边距。这一切共同构成了图 Figure 5-2 中所示的按钮。

Figure 5-2: 我们应用的提交按钮

尝试将边距改为 mt-1,看看按钮和上方的范围滑块之间的空隙如何缩小。

你还可以在 className 属性中组合多个类,通过在每个附加类之间加空格来添加更多样式。例如,尝试在 mt-4 后添加 fw-bold,将其作为一个字符串传递给 className 属性,这样可以让“Submit”文本变为粗体,如下所示:

dbc.Button(

id="my-button",

children="Submit",

n_clicks=0,

color="primary",

className="mt-4 fw-bold",

),

还有一些我们没有使用,但值得注意的 Button 属性。href 属性可以分配一个 URL,点击按钮后会将用户带到一个新网站。size 属性通过赋值以下之一来控制按钮的大小:'lg'、'md' 或 'sm'。disabled 属性可以禁用按钮,当我们将 True 赋值给它时;例如,我们可能想创建一个回调,指示应用在不再需要按钮时禁用它。

接下来是 RadioItems(也称为单选按钮),它是一个小圆圈或框,旁边有标签,用户可以点击它。单选按钮与复选框类似,不同的是,复选框允许用户选择多个标签,而单选按钮一次只允许选择一个标签。用户将使用它来选择他们想要显示数据的指示器,如 图 5-3 所示。

图 5-3:指示器选择 RadioItems 组件

我们通过 清单 5-9 中显示的四个属性来定义 RadioItems。

dbc.RadioItems(

id="radio-indicator",

❶  options=[{"label": i, "value": i} for i in indicators.values()],

❷  value=list(indicators.values()) [0],

input_class_name="me-2",

),

清单 5-9:layout 部分中的 RadioItems 组件 worldbank.py

我们首先给 RadioItems 分配一个 id 名称。options 属性负责显示标签。我们传递给它一个字典列表 ❶,每个字典代表一个标签;我们使用列表推导遍历所有指示器并为每个项目创建一个标签。或者,如果以下面这种较长的方式编写代码,我们也可以像这个(简化版)一样将一个包含三个字典的列表分配给 RadioItems options 属性,这将实现完全相同的效果:

options=[

{"label": "使用中的个人…", "value": "使用中的个人…"},

{"label": "座位比例…", "value": "座位比例…"},

{"label": "CO2 排放量(kt)", "value": "CO2 排放量(kt)"}

]

每个字典有两个键:label 键决定要显示给用户的文本,而 value 键是指示器的实际值。例如,我们使用确切的文本“CO2 排放量(kt)”作为值,以便与指示器的字典键值匹配,如 列表 5-3 所示。这样,在回调部分过滤数据时会更容易。label 键可以是任何你想显示的内容,但在这里我们使用相同的字符串作为 label 和 value,因为这个字符串已经很清晰、信息量大,而且长度适中,适合显示。

下一个属性是 value ❷,它注册用户选择的值,取决于用户点击了哪个单选按钮;分配给 value 属性的对象在 列表 5-9 中表示应用首次加载时默认选择的值。我们使用 input_class_name 属性来设置单选按钮的样式;在这个例子中,我们将它设置为 Bootstrap 类 me-2,将圆形左边的间距设置为两单位。尝试更改数字,看看这如何影响外观。请注意,我们可以使用 Bootstrap 类来为 Dash Core 组件以及 Bootstrap 组件设置样式。

有无数的 Bootstrap 类,已在 https://dashcheatsheet.pythonanywhere.com 的备忘单中做了有益的总结和组织。例如,mt-4 类位于 Spacing 工具部分,而 fw-bold 类位于 Text 工具部分。可以尝试其他工具并为应用添加自己的个性化样式。由于 Bootstrap 类的数量庞大,我们在这里不会一一列举;而是建议您使用备忘单并尝试整合不同的类。

始终将一个 Bootstrap 主题分配给 external_stylesheets 参数,正如我们在 Listing 5-6 中所做的,否则 Bootstrap 布局、样式和元素将在整个应用中无法正常工作。

Dash 核心组件

我们将向应用程序中添加几个新的 Dash 核心组件,即 RangeSlider、Store 和 Interval。

RangeSlider 通常用于展示一个广泛的可选值范围,或者当用户可以选择一个范围而不是离散值时。在这个例子中,我们将使用它来让用户选择单个年份或一段年份范围,如 Figure 5-4 所示。

图 5-4:年份选择 RangeSlider 组件

我们将通过六个属性来定义我们的 RangeSlider,如 Listing 5-10 所示。

dcc.RangeSlider(

id="years-range",

min=2005,

max=2016,

step=1,

value=[2005, 2006],

marks={

2005: "2005",

2006: " '06",

2007: " '07",

2008: " '08",

2009: " '09",

2010:" '10",

2011:" '11",

2012:" '12",

2013:" '13",

2014:" '14",

2015:" '15",

2016:"2016",

},

),

列表 5-10:布局部分的 RangeSlider 组件 worldbank.py

min 和 max 属性定义了 RangeSlider 上的最小值和最大值,通常是从左到右。step 属性决定了移动滑块时的增量。我们将其值设置为 1,使得每次滑块的移动都会改变一年。但是,因为我们为每个年份都设置了标记,所以将 step 设置为另一个值,比如 3,也会得到相同的结果;用户的选择会自动对齐到最接近的标记。如果我们移除了 2005 年到 2016 年之间所有年份的标记,只保留这两个年份,假设你将 step 设置为 3,那么滑块就会以三为增量移动到最接近的值。

value 属性决定了应用加载时默认选中的初始范围;它还会检测应用用户选择的年份范围。marks 属性标记了标记点。我们为它分配一个字典:键决定年份在滑块上的位置,而值表示在该位置上显示的文本。

另一个常见的RangeSlider属性,这里没有使用,是allowCross,它允许RangeSlider的滑块(即你在图 5-4 中看到的 2005 年和'06 年上方的蓝色圆圈)在设置为True时相互交叉。默认情况下,allowCross=False,但如果你将其改为True,你就能够将 2005 年的滑块拉到右侧,跨过'06 年的滑块。关于RangeSlider的完整属性列表,参见 Dash 组件文档(http://dash.plotly.com/dash-core-components),并选择dcc.RangeSlider。属性可以在页面底部找到。有关 Dash RangeSlider的完整视频教程,请观看视频“Range Slider—Python Dash Plotly”,网址为https://learnplotlydash.com

Dash Store组件通常用于将仪表板数据保存在用户的 Web 浏览器的内存中,以便快速高效地调用和恢复这些数据。该存储是不可见的,不会出现在用户的页面上,但我们仍然需要在布局部分声明它,如列表 5-11 所示。

dcc.Store(id="storage", storage_type="local", data={}),

列表 5-11:布局中最后部分的 Store 组件 worldbank.py

该组件允许在回调之间无缝且快速地共享数据。然而,它能够存储的数据量是有限的:在移动环境中大约为 2MB,在大多数仅限桌面的应用中则为 5MB 到大约 10MB。我们将在下一节中看到回调如何使用这个存储。

id属性将在回调中用于标识此组件。data属性表示存储的数据;这些数据可以是字典、列表、整数、字符串或布尔值的形式。实际上,我们并不需要声明<data属性或分配一个空字典,正如我们在列表 5-11 中所做的那样,但我们在这里添加它是为了说明问题。Store组件会假定它存在,这就是为什么我们不需要声明它的原因。

prop storage_type 声明了我们希望如何存储数据。它有三个选项:session、local 和 memory。session 选项会保留数据,直到浏览器标签页或浏览器本身关闭,并且新标签页被打开。local 选项会将数据保存到浏览器中,直到所有浏览历史和 cookies 被删除。memory 选项会在浏览器刷新时重置数据。

我们的最后一个组件是 Dash 的 Interval,用于自动更新应用程序,而不需要手动刷新浏览器页面。这个通常与需要实时数据的应用程序一起使用,比如金融应用,它们需要每隔几秒更新一次数据。在我们的应用程序中,Interval 激活第一个回调,该回调从世界银行的 pandas API 拉取数据并创建 DataFrame。然后,每隔 60 秒,Interval 重新激活回调,再次拉取数据并创建一个新的 DataFrame。

Interval 有几个重要的属性,见 Listing 5-12。

dcc.Interval(id="timer", interval=1000 * 60, n_intervals=0),

Listing 5-12: 上一节布局中的 Interval 组件 worldbank.py

interval 属性告诉应用程序每次激活 Interval 之间应该间隔多少时间。我们将这个间隔设定为毫秒,因此在这里我们使用 1000 * 60,即 60 秒。每 60 秒,你应该看到浏览器标签页中出现“Updating”字样。n_intervals 属性计数 Interval 被激活的次数:60 秒后 n_intervals=1,120 秒后 n_intervals=2,以此类推,直到结束。这里没有展示的另一个常见属性是 max_intervals,它定义了 Interval 激活的最大次数。例如,如果 max_intervals=2 且 interval=1000*60,那么在 120 秒后,应用程序将停止更新。

实际上,我们并不需要每 60 秒更新一次数据,因为世界银行可能每隔几周才更新一次数据。我们选择了 60 秒的间隔,以便你能看到 Interval 组件的实际效果。

关于 Dash Interval 的完整视频教程,请观看视频 “Dash Interval 概述”:https://learnplotlydash.com

Dash 回调

我们的应用使用了两个回调。第一个回调负责通过 pandas datareader API 从世界银行获取数据,而第二个回调负责在应用中创建并显示 choropleth 地图。

数据获取回调

数据获取回调会调用适当的组件,每 60 秒检索一次所选数据,并返回该数据的 DataFrame,该数据将存储在用户的网页浏览器中。像往常一样,回调有两个部分:回调装饰器和回调函数,如 Listing 5-13 所示。

❶ @app.callback(Output("storage", "data"), Input("timer", "n_intervals"))

❷ def store_data(n_time):

dataframe = update_wb_data()

return dataframe.to_dict("records")

清单 5-13:第一个回调函数在 worldbank.py

在回调装饰器❶内,Input 和 Output 参数各自包含一个 component_id 和一个 component_property,这些值对应应用程序布局部分中的组件。此处 Input 参数的 component_id 为 "timer",而 component_property 对应 "n_intervals"。这些是位置参数,因此我们无需在装饰器函数的代码中包含这些参数。事实上,若将这行代码写得更长,应该是这样的:

@app.callback(

Output(component_id="storage", component_property="data"),

Input(component_id="timer", component_property="n_intervals")

)

如清单 5-13 所示,"timer"指的是 Dash Interval 组件的 id,而 "n_intervals" 指的是表示 Interval 被触发次数的属性。按照相同的逻辑,"storage" 指的是 Dash Store 组件的 id,而 "data" 指的是表示存储在用户浏览器中的数据的属性。

在回调函数内 ❷ 我们传入了唯一的 Input 参数 n_time。n_time 参数指的是分配给 Input 的 component_property,即 n_intervals。因为 n_time 指代的是 n_intervals,所以每当 Interval 被触发(每 60 秒一次)时,回调函数也会被触发。第一次触发发生在应用程序首次渲染页面时,或者页面被刷新时。

你可以将这个参数命名为你喜欢的任何名称;它不一定要叫做 n_time。然而,重要的是要注意,只有一个参数被传递到回调函数中,因为回调装饰器只有一个 Input。

一旦触发该函数,它会激活应用程序开始时的 update_wb_data 函数(见 Listing 5-4),并将结果保存到 dataframe 对象中。这个 DataFrame 现在包含来自世界银行的数据。接着,DataFrame 被返回。回调函数中返回的每个对象都对应着 Output 参数的 component_property。在这种情况下,返回的 DataFrame 对应于 Store 组件的 data 属性,具体参见 Listing 5-13。因此,检索到的世界银行数据会存储在用户的网页浏览器中以供未来使用。

我们的回调装饰器只有一个输出,因此回调函数中只返回一个对象。在回调装饰器有多个输出的应用中,你需要在回调函数中返回相同数量的对象。例如,这里回调函数返回了两个消息,因为装饰器函数有两个输出:

@app.callback(

Output("example-content1", "children"),

Output("example-content2", "children"),

Input("timer", "n_intervals")

)

def update_data(n_time):

message1 = "在第一个输出的 children prop 中显示的文本。"

message2 = "在第二个输出的 children prop 中显示的文本。"

return message1, message2

要查看有关 Dash 回调函数的完整视频教程,请观看视频“Dash 回调——输入、输出、状态等”,访问 https://learnplotlydash.com

禁用启动时的回调

默认情况下,所有回调在应用程序启动时都会触发。然而,有时你需要阻止这种情况的发生。例如,你可能有一个回调,只有在按钮被点击时才会返回一个图形,因此你不希望该回调在按钮被点击之前就激活。在应用程序首次加载时,有两种方法可以阻止回调自动触发。一种是在应用程序启动时添加 prevent_initial_callbacks 行,并将其设置为 True,如下所示:

app = Dash(name, external_stylesheets=[dbc.themes.BOOTSTRAP],

prevent_initial_callbacks=True)

这将阻止所有回调在页面首次加载或页面刷新时触发。第二种方法是将 prevent_initial_call=True 放入你不希望在页面加载时触发的特定回调中。如果我们在第一个回调中这样做,它将是这样的:

@app.callback(Output("storage", "data"), Input("timer", "n_intervals"),

prevent_initial_call=True)

图形创建回调

图形创建回调将从用户浏览器中检索存储的 DataFrame,基于用户选择的年份和数据集过滤 DataFrame,并返回一个可视化的图表来展示这些数据。装饰器函数有两个 Input 参数,两个 State 参数和一个 Output 参数,如 清单 5-14 所示。

@app.callback(

Output("my-choropleth", "figure"),

Input("my-button", "n_clicks"),

Input("storage", "data"),

State("years-range", "value"),

State("radio-indicator", "value"),

)

清单 5-14:第二个回调函数的回调装饰器 worldbank.py

第一个 Input 表示按钮被点击的次数,第二个则表示第一个回调函数在用户浏览器中存储的数据。接下来,我们定义了几个 State 参数。State 参数不会在其组件发生变化时触发回调,而是记录用户的选择。在这里,第一个 State 参数检查用户在 RangeSlider 上选择了哪个年份范围,第二个则指示用户从 RadioItems 中选择的指标。

当用户更改 RangeSlider 上选择的年份,或者选择了不同的 RadioItems 世界银行指标时,值会被保存,但 choropleth 地图不会更新,直到按钮被点击。这是因为按钮的 n_clicks 是一个 Input 参数的组件属性(见 清单 5-14)。记住,Input 参数总是会触发回调,而 State 参数则不会。

现在让我们来看回调函数。回调装饰器有四个非< samp class="SANS_TheSansMonoCd_W5Regular_11">Output的参数,因此回调函数也必须赋予四个参数,如列表 5-15 所示。

def update_graph(n_clicks, stored_dataframe, years_chosen, indct_chosen):

❶  dff = pd.DataFrame.from_records(stored_dataframe)

print(years_chosen)

❷  if years_chosen[0] != years_chosen[1]:

❸   dff = dff[dff.year.between(years_chosen[0], years_chosen[1])]

❹   dff = dff.groupby(["iso3c", "country"])[indct_chosen].mean()

dff = dff.reset_index()

fig = px.choropleth(

data_frame=dff,

locations="iso3c",

color=indct_chosen,

scope="world",

hover_data={"iso3c": False, "country": True},

labels={

indicators["SG.GEN.PARL.ZS"]: "% parliament women",

indicators["IT.NET.USER.ZS"]: "pop % using internet",

},

)

fig.update_layout(

geo={"projection": {"type": "natural earth"}},

margin=dict(l=50, r=50, t=50, b=50),

)

return fig

❺  if years_chosen[0] == years_chosen[1]:

❻   dff = dff[dff["year"].isin(years_chosen)]

❼   fig = px.choropleth(

data_frame=dff,

locations="iso3c",

color=indct_chosen,

scope="world",

hover_data={"iso3c": False, "country": True},

labels={

indicators["SG.GEN.PARL.ZS"]: "% parliament women",

indicators["IT.NET.USER.ZS"]: "pop % using internet",

},

)

fig.update_layout(

geo={"projection": {"type": "natural earth"}},

margin=dict(l=50, r=50, t=50, b=50),

)

return fig

列表 5-15:定义第二个回调函数的回调函数 worldbank.py

这四个参数对应于列表 5-14 中 State 和 Input 组件属性的定义方式:

n_clicks 转换为 n_clicks

stored_dataframe 转换为 data

years_chosen 转换为 value

indct_chosen 转换为 value

回调函数中的第一行代码❶将存储的数据(目前是字典列表)转换为 pandas DataFrame,以便更容易创建 Plotly Express 图表。

接下来,我们需要过滤数据,为绘制 Choropleth 地图做准备。要了解如何最好地过滤 RangeSlider 数据,可以进入应用程序,尝试移动滑块句柄选择多个年份或仅选择一个年份,然后点击 提交。查看 Python IDE 中打印出来的内容。你应该会看到类似这样的内容:

[2005, 2006]

[2005, 2009]

[2009, 2009]

我们可以看到,应用程序必须区分 years_chosen 列表中的两个值是不同的❷,还是相同的❺,以便知道是使用年份范围的数据还是单一年份的数据。现在我们知道数据的结构,过滤起来更容易。

如果这两个值不同,则意味着用户选择了一个范围。首先,我们创建一个只包含用户选择的年份范围的行的 DataFrame❸。如果用户将滑块句柄移动到选择[2005, 2009],则新的 DataFrame 将包括 2005 到 2009 年之间的所有年份。接下来,对于每个国家,我们提取所选指标的平均值。因为每个国家在多个行中出现——每年一次——我们还按 country 和 iso3c 列进行分组❹。这样可以确保每个国家在新的 DataFrame 中只出现一次。

如果你不确定某些代码行的作用,可以在行与行之间添加打印语句,以明确每次操作前后数据的变化。

如果 years_chosen 列表中的两个值相同❺,则意味着用户只选择了一个年份(例如,[2009, 2009])。因此,不需要使用 groupby,因为每个国家只会出现一次。最后,我们过滤 DataFrame 以确保它只包含所选年份的行❻。

数据完全过滤后,现在可以用来绘制 Choropleth 地图。我们将在本章的最后部分“Plotly Express Choropleth 地图”中讲解 Choropleth 图的创建❼。

回调图表

为了更清晰地描述回调中发生的情况,我们将查看回调图示,正如在第四章中所做的那样,以便获取回调触发的顺序、每个回调完全渲染所需的时间以及回调中被激活的组件信息。

首先,如清单 5-16 所示,将布局部分的间隔时间减少到 10 秒,这样回调会更频繁地触发,你可以看到每隔 10 秒钟回调图示的变化。同时检查代码末尾是否有 debug=True;否则,图示不会显示。

dcc.Interval(id="timer", interval=1000 * 10, n_intervals=0),

if name == "main":

app.run_server(debug=True)

清单 5-16:Interval 组件和代码中的最后一行,位于 worldbank.py

现在运行应用程序,并在浏览器中点击右下角的回调按钮。图 5-5 展示了应显示的图示。

图 5-5:回调图示

每个回调参数(Input,Output,或 State)由一个框表示,另一个框告诉你回调被触发的次数以及触发的速度。如你所见,第一个输入指的是 Interval 组件。第二行的框告诉我们回调已被触发一次(页面加载时),并且回调完成并将数据存储到浏览器中大约花费了一秒多(1,428 毫秒)。观察箭头如何从该框指向第三行的存储组件。你应该会看到第二行框中的顶部数字每 10 秒增加一次。

第三行的四个框代表第二个回调中的两个 Input 和两个 State 参数。下面的框告诉我们第二个回调已经触发一次,并且返回一个区域图的结果,耗时不到十分之一秒,作为一个 Output。

在第一次回调完成约一秒钟后,您应该会看到存储组件的轮廓在屏幕上以紫色突出显示。这是因为存储组件激活了第二次回调。

让我们看看当用户与应用程序交互时,图表如何变化。点击按钮,选择不同的 RadioItem,并调整 RangeSlider 的年份。每当您与组件交互时,图表中相应的蓝色框应该会突出显示。请注意,RadioItem 和 RangeSlider 并不会触发第二次回调;只有 Button 和 Store 组件会触发第二次回调,因为它们是 Input 参数,而不是 State 参数。

别忘了在布局部分将时间间隔设置回 60 秒,以避免因请求过多而导致 API 超载。

回调顺序

在继续之前,重要的是讨论我们编写回调的顺序。如果回调之间没有相互依赖,顺序无关紧要,因为页面首次加载时,回调可以按任意顺序调用。然而,对于那些相互依赖的回调(如本应用程序中的回调),编写顺序非常重要。需要首先触发的回调应该写在依赖于它的回调之上;因此,我们将存储数据的回调放在了使用存储数据绘制图形的回调之上。

若要查看完整的视频教程,了解 Dash 链式回调,请访问视频“Dash 中的链式回调”:https://learnplotlydash.com

Plotly Express 分层地图

分层地图通过不同的色调和颜色展示特定空间区域的定量数据。分层地图是一个非常有效的数据可视化工具,用于展示不同区域数据的变化。已知最早的分层地图由查尔斯·杜潘(Charles Dupin)于 1826 年创建,用来展示法国各部门基本教育的可用性,如图 5-6 所示。分层地图最初被称为cartes teintées,即“彩色地图”。

图 5-6:已知最早的分层地图(来源: https://en.wikipedia.org/wiki/Choropleth_map)

我们使用 Plotly Express 方法 px.choropleth 将数据可视化为一个分级色图。以下是 Plotly Express 中与分级色图相关的所有属性列表:

plotly.express.choropleth(data_frame=None, lat=None, lon=None, locations=None, locationmode=None, geojson=None, featureidkey=None, color=None, facet_row=None, facet_col=None, facet_col_wrap=0, facet_row_spacing=None, facet_col_spacing=None, hover_name=None, hover_data=None, custom_data=None, animation_frame=None, animation_group=None, category_orders=None, labels=None, color_discrete_sequence=None, color_discrete_map=None, color_continuous_scale=None, range_color=None, color_continuous_midpoint=None, projection=None, scope=None, center=None, fitbounds=None, basemap_visible=None, title=None, template=None, width=None, height=None)

为了构建我们的分级色图,我们只需要六个这些属性,如列表 5-17 所示。

fig = px.choropleth(

data_frame=dff,

locations="iso3c",

color=indct_chosen,

scope="world",

hover_data={"iso3c": False, "country": True},

labels={indicators["SG.GEN.PARL.ZS"]: "% 女性议员",

indicators["IT.NET.USER.ZS"]: "使用互联网的总人口百分比"},

)

列表 5-17:第二个回调函数中的分级色图,来自 worldbank.py

对于 data_frame 属性,我们分配了之前根据 years_chosen 参数筛选过的数据集。对于 locations,我们将 iso3c 列分配给它,其中包含根据 Natural Earth 网站定义的三字母国家代码(<wbr>www<wbr>.naturalearthdata<wbr>.com)。color 属性控制了地图如何使用颜色区分。我们将 indct_chosen 传递给它,该值对应于用户从 RadioItem 中选择的指标。

scope 属性描述了图形将要展示的地图区域,并且有一些我们可以为其分配的特定关键字:world、usa、africa、asia、europe、north america 或 south america。例如,如果绘制的数据仅仅是关于非洲的,那么应选择 africa 作为 scope,而不是 world。在这里,我们选择的是整个世界。hover_data 属性控制了当用户将鼠标悬停在地图上时,工具提示中显示的信息。在这里,我们将 "country": True 赋值给它,以显示国家名称,但隐藏国家代码。labels 属性指示应用程序更改某些列的名称。因为在这种情况下,这些名称既用于悬停工具提示,也用于图表右侧颜色条的标题,空间有限。因此,我们更改了标签的名称,以便它们更短并且能够适应在应用程序中显示的位置。

要调整 choropleth 布局的某些方面,我们必须使用 Plotly Graph Objects:这是一个低级接口,用于从底层创建图表。由于 Plotly Express 是构建在 Plotly Graph Objects 之上的,当图表需要更多复杂的自定义功能,而这些功能在 Plotly Express 中不存在时,可以使用 Graph Objects 中的图形属性。在示例 5-18 中,我们使用它来改变地图的显示形状,并减少地图周围的空白边距,从而放大地图本身。

fig.update_layout(

geo={"projection": {"type": "natural earth"}},

margin=dict(l=50, r=50, t=50, b=50),

)

示例 5-18:在第二个回调函数中更新 choropleth 图表的布局 worldbank.py

geo 属性可以包含许多字典键,用于更改地图的布局,包括 projection、oceancolor 和 resolution 等。projection 键有一个名为 type 的字典键,用于决定地图框架的形状。将 natural earth 赋值给 type 键,会使地图显示为椭圆形框架,而不是方框。尝试将 natural earth 改为 satellite 或 bonne,观察地图的形状如何变化。第二个属性 margin 通过将边距从默认的 80 像素减少到 50 像素,从而放大了显示的地图尺寸。关于 choropleth 图表的完整 Plotly Graph Objects 属性列表,请访问 https://plotly.com/python/reference/choropleth

总结

在本章中,你学到了几个新概念:你学会了使用 pandas datareader 从网络中提取数据;你了解了 Dash Bootstrap 组件来管理应用的布局和样式,以及一些新的重要 Dash Core 组件,如 Store 和 Interval;你学会了如何创建具有多个回调的应用;并且你深入探讨了流行的分级地图。凭借这些技能,你可以创建越来越有效且复杂的实时更新仪表板。

第六章:6 投资组合:构建更大的应用

在本章中,我们将展示如何创建一个投资组合应用,探索资产配置如何影响投资组合的回报。我们将从介绍资产配置以及它为何在投资中是一个重要概念开始。然后,我们将创建一个仪表板来探索一个包含自 1929 年以来现金、股票和债券年度回报数据的数据集。这个应用将展示使用 Dash 创建的交互式仪表板如何真正让数据栩栩如生。

这个应用包含比 第五章 中的应用更多的代码行。我们将介绍如何组织更大的应用,并提供一些维护和调试大型应用的技巧和窍门。同时,我们还会涉及一些高级回调技术。

到本章结束时,你将学会如何:

  • 结构化一个更大的项目,使其更易于维护和调试

  • 在你的应用中包含 FontAwesome 图标

  • 使用新的 Dash 核心组件:DataTable,Slider,和 Markdown

  • 使用新的 Dash Bootstrap 组件:Card,InputGroup,Table,Tabs,和 Tooltip

  • 使用 Plotly 图形对象制作颜色编码的图形

我们还将介绍一些高级回调技术,例如使用多个输入和输出的回调、在不触发回调的情况下从组件获取数据,以及通过回调同步组件。在我们开始编写代码之前,你需要对资产配置有一些基本的了解。

资产配置

投资的主要目标之一是以最低的风险获取最高的回报。资产配置是将投资组合划分为不同类别资产(如股票、债券和现金)的一种策略。其目的是通过多样化来降低风险。历史上,这些资产类别的回报并不是同步变化的。例如,当股票下跌时,债券往往会上涨。在你的投资组合中同时拥有这两种资产可以降低风险,因为它们相互抵消。

配置到每个资产类别的金额取决于你的目标、时间框架和风险承受能力。例如,股票比债券或现金更为波动,但通常长期回报更高。如果你为几十年后的退休做投资,你可能会更倾向于将更多的资产配置到股票中,因为你有时间等待市场的波动。尽管如此,很难提前知道当你看到账户余额下降时你的感受如何,而人们在看到股市下跌时容易惊慌失措,低位卖出股票,锁定损失。拥有其他不同时期不随股票下跌的资产可以帮助你坚持长期策略。

保守投资也存在风险。如果你将过多的资金配置为现金,你可能面临退休时资金不足的风险。然而,对于短期目标,例如能够应对突发的生活开支,持有现金是合适的。

如果你是投资新手,且目前这部分内容还不太明白,使用该应用程序可以帮助你理清这些概念。这正是数据可视化的优点之一。通过快速查看色彩编码的图表,你可以看到股票、债券和现金在一段时间内的表现。该应用将分析并直观地展示资产配置如何影响你选择的投资组合,允许你调整配置比例,查看不同配置的投资组合随时间的表现。

下载并运行应用

首先,我们来看一下完成后的应用。你可以在https://github.com/DashBookProject/Plotly-Dash找到完整的代码。按照第二章中的说明下载并在本地运行,或者在https://wealthdashboard.app上查看实时版本。图 6-1 展示了应用的截图。

图 6-1:资产配置可视化应用的截图

你可以看到,这个应用比我们之前的 Dash 应用包含了更多的元素。试着使用应用,看看它是如何运作的。输入不同的数字到输入框中。移动滑块来选择不同的资产配置。看看如果你将所有资金投入现金、股票或债券,或者三者的不同组合时,你的投资回报将会是什么。使用单选按钮选择不同的时间段,看看如果你在互联网泡沫高峰期或大萧条低谷期开始投资,你的投资组合会有怎样的表现。

注意应用中的各个组件是如何交互的,以及饼图、折线图、表格和结果字段是如何更新的。你将在本章稍后的“Dash 回调”部分学到如何实现这一点。

同时,请注意布局设计。这个应用程序的左侧有滑块、输入框和复选框选项,右侧则显示饼图、折线图和汇总表格等输出。你将在本章的“布局与样式”部分学习如何实现这一点。

应用程序结构

Dash 的一个显著特点是,使用少量代码就能轻松创建视觉交互式应用程序。本书中的前两个应用就是很好的例子,您可以在 Dash 教程和 Dash 企业应用程序画廊中查看更多实例。然而,当你开始创建自己的应用时,你会发现随着添加更多的功能和组件、构建多页应用,代码容易增长,可能达到数百行甚至上千行。本章中的应用大约有 700 行代码,但它仍然相对简单。

当你开始处理更大的应用程序时,你会迅速理解为什么应用程序结构很重要。在小型应用程序中,直接在布局或回调中定义组件可能非常方便,但随着你添加更多功能,这种方法会使布局变得庞大,难以管理、修改和调试。

在更大的应用程序中,我们可能会将不同的部分分离到各自的文件中。在这个应用程序中,由于它仍然相对较小,我们将所有代码保持在一个文件中,但通过将相关元素组织在一起,使代码结构清晰。例如,我们将图表、表格、标签页和 Markdown 组件分别归类。每个组件要么在一个函数中定义,要么分配一个变量名。因为这些组件是这样结构化的,它们就变成了构建块,我们可以根据需要使用函数调用或变量名来将它们放入布局中。这样的结构也使得在其他应用中重用这些组件变得容易。我们还将执行数据处理的辅助函数,比如计算投资回报率,拆分成独立部分。通过这种方式组织组件,我们能够使布局部分的代码保持简洁,仅需 30 行。最后一部分是应用回调。

有许多方法可以组织应用程序。例如,您可以将一些部分放在不同的模块中,并将其导入主应用程序。对于多页应用,通常的做法是将每个页面放在不同的模块中。您将在第七章中看到这种做法的例子。重要的是,要有一种一致的应用程序组织和结构方法,这对你来说有效,并且对项目有意义。对于这个应用程序,鉴于它的规模并且是单页应用,我们更倾向于将所有内容放在一起。

设置项目

和往常一样,我们将从导入库和管理数据开始。

导入库

我们从导入我们在应用中使用的模块开始(清单 6-1)。此应用中新增的模块有 data_table、State、callback_context 和 plotly.graph_objects。

from dash import Dash, dcc, html, dash_table, Input, Output, State, callback_context

import dash_bootstrap_components as dbc

import plotly.graph_objects as go

import pandas as pd

清单 6-1:应用的导入部分 app.py

我们使用 data_table 来显示结果和源数据。我们在回调中使用 State 和 callback_context,并且我们使用 Plotly 图形对象(Plotly Graph Objects)而不是 Plotly Express 来创建图表。我们将在稍后详细讨论这些内容。

添加样式表

接下来,我们将添加 Bootstrap CSS 和 FontAwesome 图标作为外部样式表。在第五章中,我们为应用添加了 BOOTSTRAP 主题,而在此应用中,我们使用的是 SPACELAB 主题,如下所示:

app = Dash(name, external_stylesheets=[dbc.themes.SPACELAB, dbc.icons.FONT_AWESOME])

SPACELAB 主题为我们提供了字体、配色方案、形状和页面元素的大小,这些都可以在图 6-1 中看到。

FontAwesome 库拥有丰富的图标集合,可以帮助让应用程序更加吸引眼球。图 6-2 展示了我们在应用中使用的 FontAwesome 图标。

图 6-2:我们在应用中使用的 FontAwesome 图标

Dash Bootstrap Components 库有一个模块,包含 FontAwesome 和 Bootstrap 图标的 URL,以及各种 Bootstrap 主题的 URL。这使得将它们添加到应用中更加方便。例如,你可以指定主题为 dbc.themes.SPACELAB,而不是 https://cdn.jsdelivr.net/npm/bootswatch@5.1.3/dist/spacelab/bootstrap.min.css

数据管理

本应用的数据来源于阿斯瓦斯·达莫达兰教授,他在纽约大学斯特恩商学院教授公司金融和估值课程。数据包含了三种资产类别的回报——现金、债券和股票——分别由三个月期美国国债、十年期美国国债和标准普尔 500 指数表示。你可以在 http://people.stern.nyu.edu/adamodar/New_Home_Page/data.html 了解更多关于此数据的信息。

我们已经下载了数据,并将其保存为名为 historic.csv 的 Excel 表格,存放在 assets 文件夹中。这里我们在应用中包含了这些数据:

df = pd.read_csv("assets/historic.csv")

接下来,我们采取一些步骤使得应用程序更易于维护。首先,我们将数据系列的起始年份和结束年份设为全局变量,因为我们在应用中的多个地方都会用到这些日期。现在,每年我们更新应用时,只需要更新新数据,而不必修改代码中的日期:

MAX_YR = df.Year.max()

MIN_YR = df.Year.min()

START_YR = 2007

START_YR 是应用程序首次运行时投资周期的默认起始年份。我们使用这个全局变量,而不是在应用的多个地方硬编码“2007”。如果你决定想要更改起始年份,只需修改这行代码即可。

我们还将颜色设置为全局变量。我们为股票、债券和现金的图表使用自定义颜色,选择的颜色与我们的 Bootstrap 主题相匹配。如果你想切换到不同的 Bootstrap 主题,可以通过在 COLORS 字典中更新颜色值来更新图表的颜色,应用中的颜色会随之更新:

COLORS = {

"cash": "#3cb521",

"bonds": "#fd7e14",

"stocks": "#446e9b",

"inflation": "#cd0200",

"background": "whitesmoke",

}

使用这个字典还使代码更具可读性和自文档性,因为你可以像这样指定颜色:

COLORS["stocks"]

而不是像这样:

"#446e9b"

布局与样式

在本节中,我们将所有组件和图形分离出来,进行模块化,这样我们就可以将它们添加到布局中的任何地方,正如本章开始时所提到的。主要布局仅占应用程序 700 行代码中的大约 30 行。为了保持布局简洁,我们在应用程序的其他部分定义组件和图形,并为它们指定变量名,以便在布局部分调用它们。以这种方式构建布局可以明确应用程序结构,并简化设计更改。

列表 6-2 展示了主布局的 app.layout 代码。

app.layout = dbc.Container(

[

❶ dbc.Row(

dbc.Col(

html.H2(

"资产配置可视化工具",

className="text-center bg-primary text-white p-2",

),

)

),

❷ dbc.Row(

[

dbc.Col(tabs, width=12, lg=5, className="mt-4 border"),

dbc.Col(

[

dcc.Graph(id="allocation_pie_chart", className="mb-2"),

dcc.Graph(id="returns_chart", className="pb-4"),

html.Hr(),

html.Div(id="summary_table"),

html.H6(datasource_text, className="my-2"),

],

width=12,

lg=7,

className="pt-4",

),

],

className="ms-1",

),

dbc.Row(dbc.Col(footer)),

],

❸ fluid=True,

)

清单 6-2:布局代码

整个应用程序的内容被包装在一个dbc.Container中,这是 Bootstrap 中最基本的布局元素,使用行和列网格系统时是必需的。

布局的第一行❶定义了蓝色的头部栏,如图 6-3 所示,定义为一个包含单列的 Bootstrap 行,列宽度为整个屏幕的宽度。

图 6-3:资产配置可视化应用程序的头部栏

我们使用 Bootstrap 的工具类来美化头部。我们使用text-center来使文本居中,使用bg-primary将背景设置为SPACELAB主题的主色,使用text-white设置文本颜色,并使用p-2添加内边距。要查看所有可用于美化应用程序的 Bootstrap 工具类,请参见 Dash Bootstrap 备忘单,网址为https://dashcheatsheet.pythonanywhere.com

第二行❷有两列,包含应用程序的主要内容,如图 6-4 所示。左侧列包含用户输入控制面板标签,右侧列则包含主要输出,包括饼图、折线图和汇总表格可视化。在这一行中有很多信息。如果屏幕是静态的,某人如果在小屏幕上查看,将需要缩放并进行大量滚动才能看到信息。好消息是,Bootstrap 可以轻松使我们的应用程序响应使用设备的大小。我们只需将小于平板尺寸的屏幕设置为一次只显示一列,而大于平板尺寸的屏幕则同时显示两列。

图 6-4:生成的应用程序的主要内容

由于 Bootstrap 的行有 12 列,我们通过将宽度设置为 12 来使行跨越整个屏幕。然后,在大屏幕上,我们将第一列的宽度设置为 5,第二列的宽度设置为 7,这样它们就会并排显示。以下是一个简化的示例:

dbc.Row(

[

dbc.Col("column 1", width=12, lg=5),

dbc.Col("column 2", width=12, lg=7)

]

)

在底部我们放置了页脚,如图 6-5 所示。为了保持一致性,页脚的样式与页头相同。一个网站的页脚通常位于每个页面的底部,通常包括次要信息,如联系信息、法律声明和网站地图。

图 6-5:应用程序的页脚

最后,我们设置属性 fluid=True ❸。这使得内容跨越视口的宽度,视口是网页中对用户可见的区域,并且随着设备的不同而变化。手机和平板电脑上的视口比计算机屏幕上的视口要小,因此设置 fluid=True 使得应用程序具备响应式设计,适应这一变化。稍后我们会详细讨论这个话题。这就是主布局的全部内容!

组件

现在我们将更详细地描述如何定义我们添加到布局中的每个组件。你会注意到,应用程序有不同的标签,定义了不同的内容面板,如教程(学习标签)、应用控制(播放标签)和数据(结果标签)。如果你在标签之间切换,你会看到只有第一列的内容发生变化;第二列显示的图表和汇总表格在切换标签时始终保持不变。

播放标签是最繁忙的,我们将详细查看以下每个元素:

  • dcc.Markdown 用于格式化和显示介绍文本

  • dbc.Card 和 dcc.Slider 组件用于设置资产配置百分比

  • dbc.Input、dbc.InputGroup 和 dbc.InputGroupText 用于创建输入数字数据的表单

  • dbc.Tooltip 用于在悬停时显示附加数据

在 Results 标签中,我们使用 DataTable 来显示源数据和可视化的结果。

标签

在 Dash 中,Tabs 组件提供了一种方便的方法来将内容分隔到不同的面板中。用户可以点击一个标签来查看该面板,最棒的是该组件会自动为我们处理这个导航。我们需要做的就是定义每个标签中的内容。图 6-6 展示了我们应用中的标签。

图 6-6:资产配置可视化器标签

Learn 标签包含一些文本。Play 标签是应用程序的主要控制面板,允许用户输入他们的选择。Results 标签包含关于投资组合年回报和数据源的详细表格。这些数据将在第二列中进行可视化。

在 app.layout 中,我们通过将变量名 tabs 放在第二行的第一列来包含这些标签:

--snip--

dbc.Row(

dbc.Col(tabs, width=12, lg=5, className="mt-4 border"),

--snip--

我们定义了我们的 tab,如[清单 6-3 所示。

tabs = dbc.Tabs(

[

dbc.Tab(learn_card, tab_id="tab1", label="Learn"),

❶  dbc.Tab(

[asset_allocation_text, slider_card, input_groups,

time_period_card],

tab_id="tab-2",

label="Play",

className="pb-4",

),

dbc.Tab([results_card, data_source_card],

tab_id="tab-3", label="Results"),

],

id="tabs",

❷  active_tab="tab-2",

)

清单 6-3:定义标签

我们创建了一个 tabs 容器,使用了 dbc.Tabs 组件,该组件包含我们的三个独立的 Tab 面板。我们为每个 dbc.Tab 提供了要显示的内容、ID 和将在屏幕上显示的标签。看看第二个 dbc.Tab ❶,也就是在图 6-7 中显示的 Play 标签,你会发现 children 属性包括了一系列变量名,这些变量名对应了我们在代码的组件部分单独定义的组件。asset_allocation_text 包含了介绍文字。slider_card 包含了两个 dcc.Slider,用户可以通过它们设置现金、股票和债券之间的配置比例。input_groups 定义了用于输入所有用户数据的区域:初始金额、年数和起始年份。我们使用这些输入来计算投资回报。time_period_card 让用户选择一些有趣的时间段,比如互联网泡沫或大萧条。

图 6-7:Play 标签的完整内容

现在尝试将 asset_allocation_text 变量名移动到列表的最后一个项目。当你运行这个更改后的应用时,你会看到介绍文字被移到了标签页面的底部。这展示了使用这种技术结构化应用时,进行设计更改是多么容易。在接下来的部分,我们将更详细地讨论每个部分是如何定义的。

active_tab 属性 ❷ 指定了应用启动时显示的默认标签。通过将其设置为 2,我们确保应用总是打开在“Play”标签上。

卡片容器与滑块

Bootstrap dbc.Card 是一个方便的容器,用于存放相关内容。它通常是一个带有边框和填充的框,提供标题、页脚和其他内容的选项。我们还可以使用 Bootstrap 工具类轻松地对卡片进行样式和定位。

我们在应用中的多个地方使用了 dbc.Card,但我们将只检查图 6-8 中显示的卡片代码作为代表性示例。

图 6-8:示例 dbc.Card

清单 6-4 显示了图 6-6 中展示的 Card 组件的代码。

slider_card = dbc.Card(

[

html.H4("First set cash allocation %:", className="card-title"),

dcc.Slider(

id="cash",

marks={i: f"{i}%" for i in range(0, 101, 10)},

min=0,

max=100,

step=5,

value=10,

included=False,

),

html.H4("Then set stock allocation % ", className="card-title mt-3",),

html.Div("(其余将是债券)", className="card-title"),

dcc.Slider(

id="stock_bond",

marks={i: f"{i}%" for i in range(0, 91, 10)},

min=0,

max=90,

step=5,

value=50,

included=False,

),

],

body=True,

className="mt-4",

)

清单 6-4:分配滑块卡片

对于滑块标签,我们使用 Dash 组件 html.H4 来设置一个 4 级标题,并使用 Bootstrap 类 card-title 来设置选定主题的一致间距。

dcc.Slider 是 Dash 核心组件。在 第五章 中,我们看到了 dcc.RangeSlider,它允许用户选择一个范围的起始值和结束值。dcc.Slider 类似,但只允许选择一个单一的值。我们为滑块指定一个 id,以便在回调中引用,并设置 marks、min、max、step 和 value 作为滑块的初始设置。这些是应用启动时看到的默认设置。

included 属性设置了滑块轨道的样式。默认情况下,滑块轨道在滑块手柄之前的部分会被高亮显示。然而,在我们的应用中,我们指定了一个离散的值,因此仅高亮显示该值而不是一个范围是有意义的。我们通过设置 included=False 来实现这一点。

寻找应用中的其他卡片,你会看到它们的构建方式相似,但包含不同的组件,如 dbc.RadioItems、dbc.InputGroup 和 dcc.Markdown。

输入容器

dbc.Input 组件处理用户输入,而 dbc.InputGroup 是一个容器,通过更多功能增强了 dbc.Input,如图标、文本、按钮和下拉菜单。

图 6-9 展示了我们如何在应用程序中使用 dbc.InputGroup 创建一个包含变量名 input_groups 的表单。

图 6-9:输入表单

每一行都是一个 dbc.InputGroup 项目,所以这里我们将五个项目一起放入一个容器中。在这种情况下,虽然可以使用 Card 作为容器,但我们使用 html.Div 容器,因为它默认没有边框和内边距。下面是包含的 Div:

input_groups = html.Div(

[start_amount, start_year, number_of_years, end_amount, rate_of_return],

className="mt-4 p-4",

)

我们分别定义每个 InputGroup 项目,并将其添加到 html.Div 容器中。每个项目都非常相似,所以我们只需要详细查看其中的几个。这里我们定义第二个项目——“开始年份”:

start_year = dbc.InputGroup(

[

dbc.InputGroupText("Start Year"),

dbc.Input(

id="start_yr",

placeholder=f"min {MIN_YR}  max {MAX_YR}",

type="number",

min=MIN_YR,

max=MAX_YR,

value=START_YR,

),

],

className="mb-3",

)

你可以使用 dbc.InputGroupText 组件在输入字段的前后或两侧添加文本,这使得表单更加美观。例如,下面我们使用 dbc.InputGroupText("Start Year") 在 dbc.Input 前面显示文本“开始年份”。

我们设置了 min 和 max,这两个是 dbc.Input 的属性,用来提供一个接受的值范围,并且将 type 设置为只接受数字;这有助于数据验证。对于 min 和 max 的值,我们使用了之前讨论的全局变量 MIN_YR 和 MAX_YR。如果输入框为空,placeholder 会显示有关有效范围的提示信息。因为我们在更新数据文件时使用了全局变量,所以在更改日期范围时,我们不需要对该组件做任何修改。

最后两个 InputGroup 项目实际上并不是用来输入的,而是用来显示一些结果的。我们设置了 disabled=True,这样就不能输入内容,并且将背景颜色设置为灰色以便区分这些项目。在回调中,我们将用投资结果更新此字段。使用输入组件作为输出字段可能看起来有些奇怪,但这样做能使这个组件保持一致的外观。此外,将来我们可能决定允许在这里输入数据。例如,用户可以输入最终的金额,然后查看在不同市场条件下,需要投资多少金额以及多长时间才能达到这个目标。rate_of_return 输入组的代码如下:

rate_of_return = dbc.InputGroup(

[

dbc.InputGroupText(

"年复合增长率(CAGR)",

id="tooltip_target",

className="text-decoration-underline",

),

dbc.Input(id="cagr", disabled=True, className="text-black"),

dbc.Tooltip(cagr_text, target="tooltip_target")

],

className="mb-3",

)

工具提示

工具提示 是当用户将鼠标悬停在某个组件上时出现的文本提示,如 图 6-10 所示。要添加工具提示,我们使用 dbc.Tooltip 组件并用 Bootstrap 进行样式设置。你只需指定 Tooltip 的 target id——无需回调!我们在应用中使用它来提供 CAGR 的定义,因为这是许多人不太熟悉的术语。前一部分结尾的代码片段展示了 Tooltip 在 rate_of_return 输入组中的代码。

图 6-10:工具提示示例

数据表

DataTable 是一个交互式表格,用于查看、编辑和探索大规模数据集。这个应用程序只使用了其部分功能,所以一定要查看 Dash 文档,了解更多如何使用这个强大工具的示例。我们使用 DataTable 在结果标签页中显示 total_returns_table,如 图 6-11 所示。

图 6-11:完整的结果标签页

列表 6-5 显示了 DataTable 的代码。

total_returns_table = dash_table.DataTable(

id="total_returns",

columns=[{"id": "Year", "name": "Year", "type": "text"}]

+ [

{"id": col, "name": col, "type": "numeric", "format": {"specifier": "$,.0f"}}

for col in ["Cash", "Bonds", "Stocks", "Total"]

],

page_size=15,

style_table={"overflowX": "scroll"},

)

列表 6-5:显示在 图 6-11 中的 DataTable 代码

和应用中的其他元素一样,我们将分配给一个变量,以便在布局中方便调用。

我们使用以下组件属性来定义我们的表格:

  • 表格id是"total_returns";我们用它来在回调中标识此组件。

  • 列id与我们 pandas DataFrame 中的列名匹配,这就是我们用来更新表格单元格中的数据的字段。

  • 列name是列头中显示的文本。

  • 列type设置数据类型为text或numeric。

  • "format": {"specifier": "\(,.0f"}格式化单元格,显示美元符号(\))并且没有小数(0),使得数据以整数美元显示。请注意,数据type必须是numeric,格式才会正确应用。

  • page_size属性控制表格高度,并在表格底部添加分页按钮。我们将其设置为15,以便每页显示 15 行数据。style_table={"overflowX": "scroll"}语法通过添加滚动条来控制宽度,以防表格溢出父容器。

内容表格

dbc.Table组件是使用 Bootstrap 主题为基本 HTML 表格样式的绝佳方式。HTML 表格在你只需要显示少量项目时非常方便,而且它们还可以包含其他 Dash 组件,如dcc.Graph或dbc.Button作为内容。当前,使用 Dash 的DataTable是无法做到这一点的。

我们在应用中使用dbc.Table来显示图 6-12 中的汇总表格。这使我们能够在汇总表格中包含 Dash 组件,并使用 FontAwesome 图标。

图 6-12:资产配置可视化应用中的汇总表格

清单 6-6 显示了部分代码,但总体来说,我们通过一个函数创建了这个表格。

def make_summary_table(dff):

# 创建包含表格信息的新数据框

df_table = pd.DataFrame(…)

return dbc.Table.from_dataframe (df_table, bordered=True, hover=True)

清单 6-6:汇总表代码摘录

我们将在本章后面的回调函数中使用这个功能。函数的参数是从用户输入中创建的 DataFrame。然后,我们创建另一个 DataFrame,里面仅包含我们想在汇总表中显示的信息。我们使用 dash-bootstrap-components 辅助函数dbc.Table.from_dataframe()来构建 HTML 表格。

Markdown 文本

Markdown是一种用于格式化网页文本的标记语言,它是最流行的方式之一,可以用来添加并格式化粗体、斜体、标题、列表等文本。要了解更多 Markdown 语法,请查看https://commonmark.org/help的教程。

我们使用dcc.Markdown组件将格式化的文本添加到应用程序中。在这个应用中,我们用它来添加资产配置的描述,如图 6-13 所示。Markdown 语法使用**来突出显示文本,使用来将文本格式化为引用块。

图 6-13:使用 Markdown 添加文本

这是Markdown组件的代码:

asset_allocation_text = dcc.Markdown(

" ""

资产配置是影响投资组合风险和收益的主要因素之一。动手试试应用,亲自体验吧!

在滑动条上更改现金、债券和股票的配置,查看你的投资组合在图表中随时间的表现。

也可以尝试输入不同的时间段和金额。

" ""

)

我们需要再进行一步设置,使块引用在应用中显示为所见的样式。块引用通常是来自不同来源的扩展引用,但也可以用于重复或突出显示某些内容。块引用通常具有额外的边距或填充,或者其他格式设置,使其更加突出,这也是我们在此情境下选择它的原因。

Bootstrap 中块引用的默认样式是:

blockquote {

margin: 0 0 1rem;

}

这不会在顶部或右侧添加边距,仅在底部添加 1rem 的边距。(rem 是根元素的字体大小,通常是 16 像素。)这并没有使文本突出显示,所以我们再增加一些边距并添加颜色,如下所示:

blockquote {

border-left: 4px var(--bs-primary) solid;

padding-left: 1rem;

margin-top: 2rem;

margin-bottom: 2rem;

margin-left: 0rem;

}

这段代码为块引用添加了一个宽度为 4 像素的左边框,并将颜色与页眉和页脚的颜色匹配。它还添加了一些额外的边距和填充。这里有一个在使用 Bootstrap 时非常有用的 CSS 小技巧:你可以不用指定颜色的十六进制数(如 #446e9b),而是使用 Bootstrap 的颜色名称,如:var(--bs-primary)。这段代码会使颜色与 Bootstrap 主题中的“主色”相匹配。如果你在应用中更改了 Bootstrap 主题,这个块引用的左边框颜色会自动更新为该主题的主色,以保持应用中一致的外观。

这个自定义 CSS 文件保存在 mycss.css 文件中,位于 assets 文件夹中。你可以根据自己的喜好命名该文件,只要它有 .css 扩展名,Dash 会自动在应用中包含这个自定义 CSS。

使用 Plotly Graph Objects 创建饼图

这是一个简短的介绍,讲解如何使用 Plotly Graph Objects 创建图表,Plotly Graph Objects 提供了比更简单的 Plotly Express 更加精细的图表和图形创建选项。应用中的饼图,见 图 6-14,在用户移动滑块时会实时更新资产配置。

图 6-14:Plotly 饼图示例

与我们在前几章中使用 Plotly Express 制作图表不同,在这里我们使用 Plotly Graph Objects。Plotly Express 会预配置许多常见的参数,使得你能够快速制作图表并且代码更少。然而,当你需要更多自定义时,可能会更喜欢使用 Plotly Graph Objects。列表 6-7 显示了创建饼图的代码。

def make_pie(slider_input, title):

fig = go.Figure(

data=[

go.Pie(❶

labels=["现金", "债券", "股票"],

values=slider_input,

textinfo="label+"percent",

textposition="inside",

marker={"colors": [COLORS["cash"], COLORS["bonds"], COLORS["stocks"]]}, ❷

sort=False, ❸

hoverinfo="none",

)

]

)

fig.update_layout(

title_text=title,

title_x=0.5,

margin=dict(b=25, t=75, l=35, r=25),

height=325,

paper_bgcolor=COLORS["background"],

)

return fig

清单 6-7:创建一个 Plotly 图形对象的饼图

要创建我们的饼图,我们首先创建一个图形实例,使用 fig = go.Figure。这里的 Figure 语法指的是在 plotly.graph_objects 模块中定义的主要类之一(通常作为 go 导入),并表示整个图形。我们使用这个类是因为该类的实例自带了许多方便的方法,可以操作它们的属性,包括 .update.layout() 和 .add.trace()。事实上,Plotly Express 函数使用图形对象,并返回一个 plotly.graph_objects.Figure 实例。

Plotly 图形对象中的饼图对象是 go.Pie ❶,它允许我们轻松地为每个扇区设置自定义颜色。请注意,这里我们使用的是 COLORS 字典作为全局变量 ❷,而不是直接为 marker 设置颜色。这意味着,如果我们以后决定更改颜色,只需要更新 COLORS 字典中的代码,而不需要修改图形中的代码。在我们的应用中,我们希望每个资产的颜色保持不变,即使其值发生变化。我们通过设置 sort=False ❸ 来实现这一点。(默认值为 True,会按降序排序值,因此最大值总是拥有相同的颜色。)

与 清单 6-6 中的表格一样,我们将这个饼图创建在一个函数中,以便在回调中更新它。输入参数是滑块的值和标题。

使用 Plotly 图形对象的折线图

我们将再次使用 Plotly 图形对象来绘制折线图,以便为每个轨迹定制颜色和标记——这在使用 Plotly Express 时会显得比较繁琐。

再次,我们将折线图创建在一个函数中,并将一个 DataFrame 作为参数输入。这个 DataFrame 基于用户的选择:资产配置、时间段、起始金额和年数。你将在本章稍后的回调中了解如何创建这个 DataFrame。图 6-15 显示了折线图。

图 6-15:Plotly 折线图示例

清单 6-8 提供了折线图的代码。

def make_line_chart(dff):

start = dff.loc[1, "Year"]

yrs = dff["Year"].size - 1

dtick = 1 if yrs < 16 else 2 if yrs in range(16, 30) else 5

fig = go.Figure() ❶

fig.add_trace(

go.Scatter(

x=dff["Year"],

y=dff["all_cash"],

name="所有现金",

marker_color=COLORS["cash"],

)

)

fig.add_trace(

go.Scatter(

x=dff["Year"],

y=dff["all_bonds"],

name="所有债券(10 年期国债)",

marker_color=COLORS["bonds"],

)

)

# 为简洁起见,省略了“所有股票”、“我的投资组合”和“通货膨胀”的数据追踪

fig.update_layout(

title=f"从 {start} 开始的 {yrs} 年回报率",

template="none",

showlegend=True,

legend=dict(x=0.01, y=0.99),

height=400,

margin=dict(l=40, r=10, t=60, b=55),

yaxis=dict(tickprefix="$", fixedrange=True),

xaxis=dict(title="Year Ended", fixedrange=True, dtick=dtick),

)

return fig

清单 6-8:创建一个 Plotly Graph Objects 折线图

通过使用 graph_objects,我们可以轻松自定义每个轨迹(在本例中是折线)。我们首先通过 fig=go.Figure() ❶ 创建图形,然后使用 fig.add_trace() 单独将每个轨迹添加到图表中。对于这个函数,x 和 y 属性分别是图形的 x 轴和 y 轴数据。每个轨迹的 x 数据来自 DataFrame 的 Year 列,表示年份。y 数据包含在 DataFrame 对应的列中。例如,“All Cash”线的数据位于 DataFrame 列 dff["all_cash"] 中。name 属性将在图例中和悬停时显示每个轨迹的名称。marker_color 属性设置每个轨迹的颜色。还有许多其他自定义轨迹的属性,可以在 Plotly 文档中查看。

我们使用 fig.update_layout() 方法来自定义图形中非数据部分的定位和配置,例如设置标题、高度和边距。yaxis 和 xaxis 属性需要稍作解释:

  • tickprefix=$ 在 y 轴标签上添加了美元符号。

  • fixedrange=True 禁用了 x 轴和 y 轴的缩放。这可以防止在触摸屏上不小心放大;当你试图滚动页面却意外放大图形时,可能会很烦人。

  • dtict=dict 用于设置 x 轴标签之间的步长。你可以看到,当用户选择不同的时间段时,标签是如何变化的。我们是这样计算的:

dtick = 1 if yrs < 16 else 2 if yrs in range(16, 30) else 5

Dash 回调

现在进入有趣的部分。回调函数让应用变得互动。当输入组件的属性发生变化时,回调函数会自动被调用。我们从一个简单的回调开始,它根据两个滑块的值更新饼图。

接下来我们将展示如何使用 State 获取数据,而不触发回调。

然后我们将讨论一个通过将相同的参数作为输入和输出来同步组件的回调。

最后,我们将展示一个包含多个输入和输出的回调,并展示如何在回调中使用函数来使大型回调变得更易于管理。

交互式图形

让我们从更新饼图的回调开始。以下是定义:

@app.callback(

Output("allocation_pie_chart", "figure"),

Input("stock_bond", "value"),

Input("cash", "value"),

)

首先,我们有回调的 Output,它通过更新 dcc.Graph 的 figure 属性来更新饼图。你可以在 app.layout 中找到这个 dcc.Graph:

dcc.Graph(id="allocation_pie_chart", className="mb-2")

然后,我们定义回调的输入,这些输入是滑动条的值属性,分别为 id "stock_bond" 和 id "cash"。

接下来是回调函数,详见列表 6-9。

def update_pie(stocks, cash):

bonds = 100 - stocks - cash

slider_input = [cash, bonds, stocks]

= 70:

investment_style = "进取型"

elif stocks <= 30:

investment_style = "保守型"

else:

investment_style = "稳健型"

figure = make_pie(slider_input, investment_style + " 资产配置")

return figure

列表 6-9:update_pie() 回调函数

我们首先根据用户在滑动条上选择的现金和股票的比例来计算债券的百分比,使用操作 bonds = 100 – stocks – cash。

接下来,我们更新饼图的标题文本。我们的经验法则是,股票配置超过 70% 的投资组合为“进取型”投资风格,股票配置低于 30% 的投资组合为“保守型”,其余则为“稳健型”。随着用户移动滑块,标题会动态更新。我们将该标题作为属性传递给 make_pie() 函数。

最后,我们通过调用我们的函数 make_pie() 来创建图形,该函数在列表 6-7 中定义。通过使用函数来创建图形,我们减少了回调中代码的数量,并且可以在其他回调中重用该函数。结果是,代码更易读、更易维护。

现在,你可以回到应用程序,查看移动滑块如何更新饼图,并了解其实现方式。

使用状态的回调

第二个回调函数通过单向同步来同步这两个滑块:一个滑块用于更新另一个滑块的值。"cash" 滑块将更新 "stock_bond" 滑块,但 "stock_bond" 滑块不会更新 "cash" 滑块。在用户选择了 "cash" 滑块上的现金分配后,我们更新 "stock_bond" 滑块组件:

@app.callback(

Output("stock_bond", "max"),

Output("stock_bond", "marks"),

Output("stock_bond", "value"),

Input("cash", "value"),

❶  State("stock_bond", "value"),

)

❷  def update_stock_slider(cash, initial_stock_value):

max_slider = 100 - int(cash)

stocks = min(max_slider, initial_stock_value)

# 格式化滑块比例

50:

marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1, 10)}

elif max_slider <= 15:

marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1, 1)}

else:

marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1, 5)}

return max_slider, marks_slider, stocks

我们在函数定义中使用了State ❶,因为我们需要知道滑块的当前输入值,以便计算新的输出值。State 不会触发回调;它的目的是提供在回调触发时该属性的当前值(即状态)。

在 ❷,我们开始回调函数。当用户选择现金配置时,我们会调整可以分配给股票或债券的金额。例如,如果用户将现金配置更改为 20%,则股票配置的最大值为 80%,因此我们需要将 "stock_bond" 滑块的值从当前值更新为新的最大值 80%。

我们还通过更新标记来更新滑块上的比例。请注意,图 6-16 的上半部分股票配置百分比滑块的刻度按十进制递增,而下半部分按个位数递增。

图 6-16:现金与股票配置滑块的更新前(顶部)和更新后(底部)

我们根据滑块的最大值计算滑块标记。例如,在底部的滑块设置中,现金配置是 95%,所以股票配置的最大值是 5%。这意味着在创建滑块标记的函数中,max_slider的值是5:

marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1)}

这比以下这种写法更简洁:

marks_slider={

0: '0%',

1: '1%',

2: '2%',

3: '3%',

4: '4%',

5: '5%'

},

现在你可以回到应用程序,看看如何移动 "cash" 滑块更新 "max"、"marks" 和 "value",进而影响 "stock_bond" 滑块。

圆形回调与同步组件

Dash 还支持组件的双向同步。例如,如果你希望用户能够通过输入框输入一个数字或者通过移动滑块来设置某个值,那么就需要确保这两个值匹配。在这种情况下,滑块会更新输入框,输入框也会更新滑块。这就是一个 圆形回调 的例子。

我们在应用程序中使用循环回调来同步控制面板中的某些组件。回想一下,用户可以在输入框中输入开始年份和年数来计算投资回报,但也可以从列表中选择某个感兴趣的时间段,例如大萧条。这个回调函数保持这三个输入的同步。当你从列表中选择“大萧条”时,它会将输入框中的开始年份更改为 1929 年,并将规划时间更改为 20 年,以突出股票恢复的时间。如果用户随后在输入框中输入 2010 年,这就不再是大萧条时期,因此该单选按钮会被取消选择。

现在让我们更仔细地看一下这个回调函数:

@app.callback(

Output("planning_time", "value"),

Output("start_yr", "value"),

Output("time_period", "value"),

Input("planning_time", "value"),

Input("start_yr", "value"),

Input("time_period", "value"),

)

请注意,在 @app.callback 装饰器函数下,三个输出与三个输入完全相同。这使得能够同步这三个组件的值。列表 6-10 展示了回调函数。

def update_time_period(planning_time, start_yr, period_number):

" ""同步输入和选择的时间段" ""

❶ ctx = callback_context

❷ input_id = ctx.triggered[0]["prop_id"].split(".")[0]

如果 input_id == "time_period":

planning_time = time_period_data[period_number]["planning_time"]

start_yr = time_period_data[period_number]["start_yr"]

如果 input_id 在 ["planning_time", "start_yr"] 中:

period_number = None

返回 planning_time, start_yr, period_number

列表 6-10:用于同步的回调函数

为了正确更新输出,回调函数必须知道是哪一个输入触发了回调,我们可以通过另一个高级回调特性来找出:callback_context ❶。这是一个全局变量,仅在 Dash 回调内部有效。callback_context 有一个属性叫做 triggered,它是一个变更属性的列表。我们解析这个列表来找到触发输入的 id ❷。

接下来,我们使用 input_id 来根据触发回调的输入更新不同的内容。如果是用户选择了某个时间周期触发的回调,我们会更新年份和计划时间的输入框。如果是用户在输入框中输入了内容触发的回调,我们会取消选中时间周期的单选按钮。这样可以保持用户界面的同步。

请注意,要实现这种同步组件,必须在同一个回调中包含输入和输出,接下来我们将讨论这一点。

多个输入和多个输出的回调

Dash 的一个当前局限是,它不允许多个回调更新同一个输出。目前唯一可用的解决方案是将所有更新输出的输入包含在同一个回调中。这样做的缺点是回调可能变得非常庞大和复杂,难以理解、维护和调试。解决这一问题的策略是为回调中的每个过程创建独立的函数。在列表 6-11 中的回调就展示了这种做法。

这个回调是应用程序的核心。每当滑块或输入框中的任何输入发生变化时,都会触发该回调,更新总回报表、折线图、摘要表、最终金额和回报率。哇。如果我们把所有这些代码都包含在回调中,它将有数百行长。相反,它只有 15 行(不包括注释和空白)。我们能做到这么简洁,是因为我们创建并调用了处理特定变更的函数。列表 6-11 显示了完整的回调代码。

@app.callback(

Output("total_returns", "data"),

Output("returns_chart", "figure"),

Output("summary_table", "children"),

Output("ending_amount", "value"),

Output("cagr", "value"),

Input("stock_bond", "value"),

Input("cash", "value"),

Input("starting_amount", "value"),

Input("planning_time", "value"),

Input("start_yr", "value"),

)

def update_totals(stocks, cash, start_bal, planning_time, start_yr):

# 设置无效输入的默认值

start_bal = 10 if start_bal is None else start_bal

planning_time = 1 if planning_time is None else planning_time

start_yr = MIN_YR if start_yr is None else int(start_yr)

# 计算有效的规划时间起始年份

max_time = MAX_YR + 1 - start_yr

planning_time = min(max_time, planning_time)

if start_yr + MAX_YR:

start_yr = min(df.iloc[-planning_time, 0], MAX_YR) # 0 是年份列

# 创建投资回报的数据框

dff = backtest(stocks, cash, start_bal, planning_time, start_yr)

# 创建 DataTable 的数据

data = dff.to_dict("records")

fig = make_line_chart(dff) ❶

summary_table = make_summary_table(dff) ❷

# 格式化结算余额

ending_amount = f"${dff['Total'].iloc[-1]:0,.0f}"

# 计算 cagr

ending_cagr = cagr(dff["Total"])

return data, fig, summary_table, ending_amount, ending_cagr

Listing 6-11: 更新多个输出的完整回调

❶和❷行是我们使用两个函数的例子,这两个函数在本章前面的“使用 Plotly 图形对象绘制折线图”和“内容表格”部分中有描述。第一个函数生成折线图,第二个函数生成摘要表格。我们使用两个函数backtest()和cagr()来计算投资回报。这些函数在本章中没有详细讨论,但你可以在 GitHub 上的辅助函数部分看到它们。

总结

我们将在本章的最后总结大型应用结构化的策略:

  • 对于常量应用数据,如主题颜色和启动默认值,使用全局变量。

  • 给组件分配变量名,以便在布局中更容易调用。这也意味着我们可以像搭积木一样在其他应用中复用这些组件。

  • 组织代码以保持相似元素的集中;例如,将视觉元素如表格、图表和输入分开成独立的部分。

  • 使用函数将逻辑打包,使代码更易读易懂。这在有多个输入和输出的回调函数中尤为有用。

在开发大型应用或为现有应用添加新功能时,先创建一个独立的最小可工作示例来实现新功能会很有帮助。这个小版本更容易调试,因为你不需要在数百或数千行代码中寻找错误源。

在下一章,你将学习更多应用结构化的技巧,例如使用多个文件和可重用组件。

第七章:7 探索机器学习

本章解释了如何使用 Dash 以可视化的方式探索和展示机器学习模型及分类算法的内部结构。例如,假设你为自动驾驶汽车创建了一个机器学习模型,能够将物体分类为人类、植物和其他汽车,并且你需要向其他程序员和非技术管理人员解释你的模型是如何工作的以及为什么如此。仪表盘应用可以帮助你以快速、令人信服且视觉吸引的方式完成这项任务。

本章特别从概念性介绍开始,介绍了支持向量机(SVM),这一流行的机器学习分类算法。SVM 提供了一种通过告诉我们如何准确地划分数据,从而将其放入正确类别的方法。我们将通过在仪表盘应用中使用各种图表和图形来可视化 SVM 的工作原理。

接下来,我们将使用强大的 NumPy 库进行数值计算,并使用 scikit-learn 提供的易用机器学习算法。最重要的是,你将体验到使用画廊作为基础深入了解由专业人士编写的更高级仪表盘应用程序的巨大好处。

本章还介绍了包装函数,这是 Dash 的新概念,用于创建自定义、可重用的组件,提供比预定义的 Dash 和 HTML 组件更多的选择。你还将了解一些新的 Dash 标准组件,如等高线图和图表,我们还将介绍 Dash 的加载旋转器,当特定的仪表盘组件正在加载时,它会为用户提供视觉反馈。加载旋转器对于构建较慢的复杂仪表盘应用特别有用,通常由于计算负载较重。

注意

本章的目的是向你提供 Dash 能够实现的功能概述,并帮助你进一步提升技能。我们不会深入探讨任何一个话题。我们的目的是提供信息性内容,而非全面性内容,因此如果你对某个内容特别感兴趣,我们建议你查看 Charming Data YouTube 频道和本书配套网站上的补充资料: learnplotlydash.com

仪表盘应用让机器学习模型 变得更加直观

随着机器学习在计算机科学和我们日常生活中的普及,理解计算机如何得出结论变得越来越重要。机器能够在国际象棋和围棋中击败人类大师,减少许多交通场景中的事故率,并在工厂环境中生产出比人类工人更多的商品。在可衡量的性能方面,机器通常能够证明其优越性,甚至能够说服最激烈的批评者,认为它们的力量能够解放人类劳动力。

然而,仅通过机器的性能指标来观察其有效性可能是危险的。我们无法知道机器在那些无法从数据集中学到的极端情况中的表现;数据驱动的方法总是倾向于过去的经验。如果一台机器在 100 年的股市历史中从未观察到 95%的股市崩盘,它很可能不会在其模型中考虑这种情况,但这种情况很可能在未来某天发生。

为了降低这种风险,组织必须更好地理解机器“智能”的来源。它们的假设是什么?它们基于什么来得出结论?当面对极端输入时,它们的行为会如何变化?1960 年代的机器学习模型无疑会将负利率视为“极端”甚至是“不可能”的。但今天我们已经知道更好。

这引出了机器学习仪表板的动机。仪表板是可视化机器内部运作的强大工具。你可以训练一个模型,并观察它在不同输入下的表现。你可以测试极端情况。你可以查看模型的内部,并通过将学习过度拟合到过去的数据来评估潜在的风险。

可视化机器学习模型使你能够展示你的模型给客户,让他们能够调整输入参数,并建立对模型的信任,这是命令行模型无法实现的。仪表板帮助将机器的智能变得可触摸。

分类:简短指南

你不需要深入理解分类或支持向量机(SVM)就能跟随本章中的应用。我们会详细讲解一些内容,方便那些感兴趣的读者理解,但如果你想跳过这一部分以及后面的 SVM 部分,完全可以这样做,并将 SVM 算法当作一个黑盒,继续阅读本章其余内容。

还在这里吗?好吧,让我们深入探讨机器学习中分类问题的基本概念。

通常,分类问题试图根据一组提供的带标签(已分类)训练数据中的学习结果,将类别(即类别)分配给输入数据。例如,如果我们想根据训练数据预测学生在大学可能选择的专业,我们可能会测量每个学生的创造性和逻辑性思维能力。目标是创建一个分类算法,通过特征:创造性与逻辑性思维的能力,来预测标签——学生的预测专业。

支持向量机(SVMs),如此仪表板应用中可视化的那样,是分类算法。分类算法获取一组数据,并根据训练数据的模型学习,为每个数据点分配一个对应于特定类别的标签。更具体地说,分类算法会寻找一个决策边界,将数据分为两个或多个类别。线性 SVM 将决策边界在n维空间中建模为一个(n – 1)维平面,该平面将数据点划分为两个类别。所有位于决策边界一侧的数据点属于一个类别,而所有位于另一侧的数据点属于另一个类别。因此,假设你可以将所有数据点表示在n维空间中,并且你有一个(n – 1)维决策边界,你可以利用决策边界对新数据进行分类,因为任何新的数据点都恰好位于边界的一侧。粗略地说,分类的目标就是识别出能够很好地将训练数据和测试数据分开的决策边界。

图 7-1 给出了一个例子,灵感来源于但稍作修改的Python One-Liners(作者:Christian Mayer,出版商:No Starch Press,2020 年)。

图 7-1:示例分类问题:不同的决策边界会导致对新数据点的不同分类(即“计算机科学”或“艺术”)

这个分类场景创建了一个分类模型,帮助有志向的大学生找到可能适合他们优势的学习领域。我们拥有来自两个领域的前学生的训练数据:计算机科学和艺术。幸运的是,学生们已经向我们提供了他们自己在逻辑和创造性思维方面的估算。当这些数据被映射到一个二维空间中,逻辑思维和创造性思维作为独立的轴时,数据似乎被聚集成簇,计算机科学的学生通常在逻辑思维上较强,而艺术家则在创造性思维上较强。我们使用这些数据来找到一个决策边界,从而最大化训练数据的分类准确度。从技术角度来看,得到的分类模型只会给有志的学生提供一个关于他们可能根据自己的优势选择什么的提示。它不一定帮助他们决定应该选择什么(例如,为了最大化他们的幸福感)。那是一个更难的问题。

我们将使用决策边界来对新用户进行分类,这些用户只有关于他们逻辑和创造力的数据。图中展示了两个线性分类器(用线条表示),当作为决策边界使用时,它们能完美地将数据分开。在分类测试数据时,它们的准确率达到了 100%,因此两条线似乎都同样优秀。为了使机器学习算法表现良好,它必须明智地选择决策边界。那么我们如何找到最佳的决策边界呢?

支持向量机

SVMs 尝试最大化来自两类的最接近数据点与决策边界之间的距离;这个距离被称为安全边距边界安全,或简单称为边距。这些最接近的数据点被称为支持向量。通过最大化安全边距,SVM 旨在最小化在分类接近决策边界的新点时的误差。

作为一个视觉示例,请查看图 7-2。

图 7-2:具有决策边界和支持向量的 SVM 分类示例

SVM 分类器为每个类别找到支持向量,并将线放置在离每个支持向量最远的地方(两者之间的中点),以使不同支持向量之间的区域尽可能厚。这条线就是决策边界。在 图 7-2 中,添加了一个需要分类的新数据点;然而,由于该数据点位于间隔区,模型无法自信地决定它属于艺术类还是计算机科学类。这很好地展示了 SVM 自带一个机制,可以明确告诉我们模型是否进行的是边界分类。例如,SVM 可能告诉我们,一个在创造力上排名高的学生属于艺术类,而一个在逻辑思维上排名高的学生属于计算机科学类,但一个在创造力 逻辑上都排名高的学生则无法自信地归入任何一个类别。

请注意,SVM 模型还可以以允许训练数据中有 异常值 的方式进行训练;这些点位于决策边界的一侧,但属于另一侧。这是现实世界数据中最常见的情况。然而,在不进一步探讨这些 SVM 优化的情况下,我们建议您查看本章末尾列出的优秀 SVM 分类教程,这样我们就可以立刻深入了解这个令人兴奋的仪表板应用程序。

SVM Explorer 应用程序

图 7-3 展示了如何使用 SVM Explorer 应用程序可视化 SVM,该应用程序是一个来自图库的 Python 仪表板应用程序,使用各种类型的图表和图形。欢迎在 https://dash-gallery.plotly.host/dash-svm 上玩转这个实时项目。

图 7-3:来自图库的 SVM Explorer 应用程序

我们首先为您概述 SVM Explorer 应用程序,以便您能大致了解。该应用程序展示了给定的 SVM 模型如何分类给定的训练数据集。您通过仪表板控件(如滑块、下拉框和单选按钮)控制模型。根据您的选择,输出图表和图形会发生变化,以反映 SVM 模型实例的变化。

该应用程序的作者之一,邢涵,慷慨地为我们提供了 SVM Explorer 应用程序的快速概述:

该应用程序完全用 Dash 和 scikit-learn 编写。所有组件都作为输入参数用于 scikit-learn 函数,这些函数根据用户更改的参数生成模型。然后,模型执行预测,并在等高线图上显示其预测结果,同时评估这些预测以创建 ROC [接收者操作特性] 曲线和混淆矩阵。除了创建模型外,它还使用 scikit-learn 生成您看到的数据集,以及用于指标图的所需数据。

让我们快速检查每个可见组件。仪表板的左列包含多个输入组件:

  • 选择数据集下拉菜单允许你选择用于训练和测试的合成数据集。默认选择是 Moon 数据集,因其数据形态像月亮而得名。此输入下拉菜单允许你探索 SVM 模型如何处理具有不同固有特性的不同数据。例如,你可以选择 Circles 数据集(图中未显示),这是一个非线性数据集,其中两个待分类的数据集形状像一个内圈和围绕该内圈的外环。SVM 模型也可以处理这类数据!

  • 样本大小滑块允许你控制用于测试和训练模型的数据点数量。较大的样本大小通常会导致更准确的模型,这也是为什么机器学习公司不断收集更多数据的原因!然而,在我们的玩具仪表板中,较大的样本大小可能会导致可视化变得更加拥挤。

  • 噪声级别滑块允许你控制添加到数据中的高斯噪声的标准差。更高的噪声级别会导致模型的准确性降低,因为噪声会减少数据中模式的清晰度,并使得在训练阶段更难找到分隔决策边界。然而,你可以使用噪声级别滑块来检查 SVM 模型在实际应用中的鲁棒性,因为现实世界的数据通常是噪声较大的。

  • 阈值滑块允许你对某个类或另一个类添加偏置。粗略来说,通过增加阈值,你可以将决策边界从 A 类更多地推向 B 类(或者通过降低阈值反之),从而对于给定的输入,增加被分类为 A 类的可能性。例如,如果阈值是 0.4,那么任何大于 0.4 的分数都被视为正预测,任何小于 0.4 的分数则被视为负预测,用以判断某个点是否属于某个特定类别。

  • 重置阈值按钮将阈值重置为默认值,而不使用自定义的阈值或偏置。

  • 核函数下拉菜单、成本滑块以及其他控件,如伽玛滑块和收缩单选按钮,允许你进一步控制 SVM 的其他参数及其对分类准确性的影响。讨论这些参数需要的不仅仅是一个或两个句子,在这个简要概述中我们将跳过。如果你有兴趣了解这些控件背后的理论,可以查阅《信息检索导论》一书的第十五章(剑桥大学出版社,2008)。你可以在https://nlp.stanford.edu/IR-book/pdf/15svm.pdf免费阅读该章节。

模型变化时,三个输出组件会发生变化:

  • Dash Graph 组件 是一个等高线图,能够在热力图叠加上可视化训练和测试数据,以及模型分类的置信度。数据点代表训练数据,三角形代表测试数据。红色数据点属于一个类别,蓝色数据点属于另一个类别。首先,我们根据一部分样本数据训练 SVM 模型。然后,我们使用训练好的模型对测试数据进行分类,并在可视化中绘制预测类别。

  • ROC 曲线图 是衡量 SVM 模型在给定数据集上质量的指标。它衡量的是 真正率,即正确分类的数据点比例,和 假正率,即错误分类的数据点比例。

  • 混淆矩阵 指的是预测类别与实际类别的对比。具体来说,它是一个条形图,显示了测试数据中真正正例、真正负例、假正例和假负例的数量。你可以把它看作是衡量 SVM 模型在给定数据集上进行训练和分类任务的另一种方式。

我们在本章末尾提供了有关 Dash Graph、ROC 曲线图和混淆矩阵的更详细解释链接。然而,一个更好的理解方式是玩一下 SVM Explorer 应用。我们建议花 10 到 20 分钟玩一玩这个应用,以充分理解每个组件的概念。

你可以在 GitHub 仓库中找到该应用的代码,网址为https://github.com/DashBookProject/Plotly-Dash/tree/master/Chapter-7。完整的代码有超过 650 行,但不用担心,我们将只关注最重要的部分。请注意,维护良好的代码很少会永远保持不变。自本章写作以来,作者们已经通过为应用添加新样式等方式更新了原始代码库。但由于应用的核心没有改变,我们已经在指定的 GitHub 仓库中提供了原始代码,你可以下载并重现本章中解释的完整应用。我们强烈建议你下载代码以加速你的学习。

事不宜迟,让我们直接进入代码吧!

Python 库

我们将借助巨人的肩膀,依赖几个 Python 库来创建我们的 SVM 仪表板应用。列表 7-1 显示了本项目中使用的库。

import time

import importlib

from dash import Dash, dcc, html, Input, Output, State

import numpy as np

from dash.dependencies import Input, Output, State

❶ from sklearn.model_selection import train_test_split

from sklearn.preprocessing import StandardScaler

from sklearn import datasets

from sklearn.svm import SVC

示例 7-1: SVM 应用的依赖项

你已经看到 Dash 库的语句,它们导入了核心组件和 HTML 组件,以及整个 Dash 应用的功能。本章的核心代码包括 SVM 的计算。我们不会从零实现自己的 SVM,而是依赖于 scikit-learn 库提供的优秀实现。因此,我们从这个库中导入了一些模块,我们会在它们出现时详细讲解 ❶。如果你对机器学习感兴趣,scikit-learn 是你最好的朋友!

数据管理

scikit-learn 提供了一些很棒的合成数据集,用于测试各种分类和预测算法。在示例 7-2 中,我们展示了如何使用函数 generate_data() 动态生成数据集,参数包括样本点数、数据集类型和噪声水平,这些参数在 SVM Explorer 应用程序的左侧列中指定,如图 7-3 所示。我们将使用函数 datasets.make_moons()、datasets.make_circles() 和 datasets.make_classification() 来根据输入下拉菜单中的值生成不同的数据集(分别为 "moons"、"circles" 和 "linear")。这些数据集稍后将用于训练和测试我们的 SVM。

def generate_data(n_samples, dataset, noise):

if dataset == "moons":

return datasets.make_moons(n_samples=n_samples,

noise=noise, random_state=0)

elif dataset == "circles":

return datasets.make_circles(

n_samples=n_samples, noise=noise, factor=0.5, random_state=1

)

elif dataset == "linear":

X, y = datasets.make_classification(

n_samples=n_samples,

n_features=2,

n_redundant=0,

n_informative=2,

random_state=2,

n_clusters_per_class=1,

)

rng = np.random.RandomState(2)

X += noise * rng.uniform(size=X.shape)

linearly_separable = (X, y)

return linearly_separable

else:

raise ValueError(

"数据类型指定错误。请选择一个已有的数据集。"

)

清单 7-2: SVM 应用程序的数据管理

从高层次来看,代码由 if…elif…elif…else 语句组成,用于区分用户输入。这使得用户可以在三个数据集之间进行选择: "moons"、"circles" 和 "linear"。在每种情况下,都会使用 scikit-learn 的 dataset.make_X() 函数创建一个新数据集,该函数接收不同的输入参数(例如样本点的数量)并返回 NumPy 数组格式的数据。感兴趣的读者可以在 https://scikit-learn.org/stable/modules/classes.html#module-sklearn.datasets 上了解更多我们在此使用的输入参数。

布局与样式

布局和样式部分为你展示了 SVM Explorer 应用程序的结构以及它是由哪些基本的 Dash 组件构成的。让我们从项目的整体布局开始。

布局

当你开始开发更大的应用程序时,app.py 文件中的代码行数会迅速变得难以管理。为了帮助管理代码,SVM Explorer 应用程序包括一个 utils 文件夹,里面有两个辅助模块,dash_resuable_components.pyfigures.py,它们包含一些自定义的 Dash 组件定义,稍后我们会在本章中更详细地探讨这些组件,以及一些绘图和样式功能。从 app.py 文件中提取出实用功能到一些外部模块中进行导入是一个好的实践,它有助于保持主 app.py 文件的简洁和专注,适用于你自己的更大仪表板项目。

SVM Explorer 应用程序的结构如下:

  • app.py

  • utils/

|--dash_reusable_components.py

|--figures.py

应用程序的布局是一个层级嵌套的 HTML 元素结构,如列表 7-3 所示。

app.layout = html.Div(

children=[html.Div(…), # 标题等

html.Div(…)] # 主体

)

列表 7-3:SVM 应用程序布局的放大显示

外层的第一个子元素是包含应用程序标题、徽标和其他元信息的 Div。第二个子元素包含应用程序的主体,这是应用程序的核心部分。列表 7-4 显示了我们 SVM Explorer 应用程序布局部分的完整代码。只需浏览一下,尝试理解应用程序的结构;我们稍后会讨论相关部分。

❶ app.layout = html.Div(

children=[

# .container 类是固定的,.container.scalable 是可伸缩的

❷ html.Div(

className="banner",

children=[

html.Div(

className="container scalable",

children=[

# 在此更改应用名称

html.H2(

id="banner-title",

children=[

html.A(

"支持向量机 (SVM) 探索器",

href=("https://github.com/"

"plotly/dash-svm"),

style={

"text-decoration": "none",

"color": "inherit",

},

)

],

),

html.A(

id="banner-logo",

children=[

html.Img(src=app.get_asset_url(

"dash-logo-new.png"))

],

href="https://plot.ly/products/dash/",

),

],

)

],

),

❸ html.Div(

id="body",

className="container scalable",

children=[

html.Div(

id="app-container",

# className="row",

children=[

html.Div(

# className="three columns",

id="left-column",

children=[

… 查看 Dash 组件

],

),

html.Div(

id="div-graphs",

children=dcc.Graph(

id="graph-sklearn-svm",

figure=dict(

layout=dict(

plot_bgcolor="#282b38",

paper_bgcolor="#282b38"

)

),

),

),

],

)

],

),

]

)

示例 7-4:在 SVM 应用布局中进行多级缩放

这段代码引用了样式表和 Dash 组件,我们将在后面的章节中讨论,因此目前可能不太清楚这一部分是如何工作的。但它展示了一个非平凡的 Dash 应用的外观:层级嵌套的 HTML 组件,使用 dash-html-components。在大型应用中,您将使用这种结构来添加更多组件,随着应用外观和感觉的修改而改变。

与我们在前面章节中看到的小型应用一样,应用由一个外部的Div ❶组成,该元素包含两个内部的Div元素,分别位于❷和❸。第一个内部的Div包含元信息,如标题和徽标。第二个内部的Div包含应用的主体内容。

在本章稍后的“可重用组件”部分,我们将重点介绍不同的 Dash 组件,了解它们是如何独立工作的。

接下来,我们将查看用于为 SVM Explorer 应用设置样式的 CSS 样式表。

样式设计

我们从第四章和第五章了解到,我们可以使用 CSS 样式表或 dash-bootstrap-components 来为 HTML 元素设置样式。在这个应用中,我们选择了 CSS 样式表,因为它允许我们通过设置边距、内边距、颜色、字体和边框来创建更个性化的外观和感觉。请注意,主要的样式已经内置在默认的 Plotly Dash 组件中,因此使用自定义样式表是 SVM Explorer 应用开发者的一个相对较小的设计决策。

我们在assets子文件夹中定义了样式表,结构如下:

  • app.py

  • assets/

|--base-styles.css

|--custom-styles.css

--snip--

我们将使用两个样式表:base-styles.csscustom-styles.css,这两个文件是由应用开发者添加到 SVM Explorer 应用中的。base-styles.css样式表定义了如何为基本 HTML 元素(如标题和段落)设置样式。custom-styles.css样式表定义了如何为 Dash 特定的元素(如命名滑块、图表容器和卡片)设置样式。让我们快速浏览一下base-styles.css,看看如何调整默认样式。

base-styles.css样式表由 13 个部分组成,如示例 7-5 所示,每个部分都定义了特定类型 HTML 元素的外观。

/* 目录

—————————————————————————

  • 网格

  • 基本样式

  • 排版

  • 链接

  • 按钮

  • 表单

  • 列表

  • 代码

  • 表格

  • 间距

  • 工具类

  • 清除浮动

  • 媒体查询

*/

示例 7-5: base-styles.css 概述

该样式表允许您定义字体、背景颜色、边距和填充等基本元素的样式。例如,在排版部分(如示例 7-6 所示),我们为不同的标题定义了字体大小、粗细和间距。

/* 排版

—————————————————————————*/

h1, h2, h3, h4, h5, h6 {

margin-top: 0;

margin-bottom: 0;

font-weight: 300;}

h1 {font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem;}

h2 {font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;}

h3 {font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;}

h4 {font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;}

h5 {font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;}

h6 {font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;}

p {

margin-top: 0;}

列表 7-6:放大查看 base-styles.css 中的排版部分

你可以看到,我们将顶级标题 h1 的字体大小设置为 4.5rem,使其成为最大的。

在本章中,我们不会深入讨论每个元素的具体内容,尽管我们确实建议你快速查看一下代码,看看我们是如何对不同元素应用自定义样式的。

让我们不要在小的 CSS 细节中迷失(你可以选择在自己的仪表盘应用中忽略这些细节,使用 Dash 默认的样式),而是来看看 SVM 应用的核心部分:Dash 组件。

可重用组件

在这里,我们将介绍 Dash 中的一个新概念——可重用组件,它允许你为现有组件添加自己的样式和功能。我们在 SVM Explorer 应用中使用了几个组件,这些组件的模式类似于内置组件,但有一些细微的差异,比如具有不同标签和值范围的下拉菜单。我们在 dash_reusable_components.py 文件中定义这些组件,并在 app.py 中实例化它们并赋予自定义特性。首先,我们将 dash_reusable_components.py 模块添加到 utils 文件夹中:

  • app.py

  • assets/

  • utils/

|--dash_reusable_components.py

--省略--

假设我们的目标是创建一个自定义按钮,我们将在代码中多次使用它。自定义按钮组件可以非常复杂;它可能只是包含一个按钮标签,或者可能包含更复杂的内容,比如一个图表,显示按钮随时间点击的频率(没错,Dash 可以做到这一点!)。为了清晰和简洁,我们希望避免在 app.py 文件中重复创建自定义按钮。为此,我们将此自定义按钮作为自定义类 CustomButton 的实例来创建。我们在 dash_reusable_components.py 模块中定义该类一次,然后可以在主 app.py 文件中根据需要多次实例化自定义按钮组件,每次都可以有不同的特性,如不同的背景颜色或文本。

定义一个卡片

在第六章中,我们使用了一个 Bootstrap Card组件来创建一个用于内容的小容器区域。在这里,我们将创建一个包含多个组件的Card:一个标签、一个滑动条和一个按钮。你可以把Card看作是一个包含多个子组件的元组件,使用特定的(相对)宽度和内边距,并在底部添加一个实心灰色边框,以便在视觉上将这些组件分组。Card实际上是 HTML 组件html.Section的一个包装器,它是一个容器,用来将不同的 HTML 元素或文本分组在一个(可能)有样式的区域内。Section中的所有内容在语义或主题上是相关联的。图 7-4 展示了我们在 SVM Explorer 应用中使用的一个Card示例,它利用html.Section元素将三个组件分组在一起:一个标签、一个滑动条和一个按钮。

图 7-4:自定义 Card 的示例

什么是包装器?

包装器是一个功能,它的唯一目的是调用另一个函数。通过这样做,包装器将调用者与不必要的复杂性或冗余隔离开来。例如,内部函数调用可能很复杂,带有许多调用者不了解的特定参数。包装器函数调用通过要求较少的参数并硬编码其余参数来简化访问内部函数。这提高了代码的可读性,减少了复杂性,并提升了代码的可维护性。

列表 7-7 展示了来自dash_reusable_components.pyCard包装器函数的实际定义。

def Card(children, **kwargs):

return html.Section(className="card", children=children, **_omit(["style"], kwargs))

列表 7-7:定义 Card 组件

为了全面了解Card组件的工作原理,让我们深入研究这些参数:

children    是一个其他 Dash 元素的列表,这些元素被包含在 Card** 中,因此在仪表盘应用中会一起显示。你可以创建各种嵌套和层级的 HTML 树,并将任何可迭代的 HTML 元素传递给 Card**。然后,Card 会将这些元素包装成一个类似物理卡片的共同元素:一个 2D 的框,具有一致的样式,包含其他设计元素。

kwargs   代表 任意关键字参数kwargs 参数将所有传入函数调用的关键字参数打包成一个单一的 kwargs 字典。关键字参数的名称是字典的键,而关键字参数的值是字典的值。例如,如果某人调用了函数 Card(children, example = "123"),我们可以在函数内部使用 kwargs['example'] 来获取值 "123"。我们还可以稍后使用这个字典将可能大量的参数解包到 html.Section() 构造函数中,包括一些元数据,比如该部分的语言或该组件被用户点击的次数。我们在 SVM Explorer 应用中并未充分利用传递任意关键字参数的机会,但这是 Card 组件的一个有趣方面。你可以在 https://blog.finxter.com/python-double-asterisk 上找到关于双星号操作符的详细教程。

另一个参数实际上是一个函数。让我们更仔细地看一下它:

_omit    允许我们在不需要的情况下排除某些元素。例如,我们可能会移除字典中的 "style" 键,因为在 html.Section() 构造函数中并不需要它,因为我们已经通过 CSS 样式表定义了样式。_omit() 函数接受两个参数:一个包含字符串的列表 omitted_keys 和一个字典 d。该函数返回一个新的字典,包含原字典 d 中的元素,所有在 omitted_keys 中的键及其对应的值会被过滤掉。以下是 SVM Explorer 应用的作者如何简洁地完成这一任务:

def _omit(omitted_keys, d):

return {k: v for k, v in d.items() if k not in omitted_keys}

在 SVM Explorer 应用中,你调用 **_omit(["style"], kwargs) 来传递关键字参数字典 kwargs,该字典来自 Card() 调用,并在使用 _omit() 函数移除 "style" 键后传递。双星号前缀将字典中的所有值解包,并传递给 html.Section() 构造函数的参数列表。

app.py 中,我们现在可以使用 Card 可重用组件来创建一个包含命名滑块和按钮的卡片,如 列表 7-8 所示。

.drc.Card(

id="button-card",

children=[

drc.NamedSlider(

name="Threshold",

id="slider-threshold",

min=0,

max=1,

value=0.5,

step=0.01,

),

html.Button(

"Reset Threshold",

id="button-zero-threshold",

),

],

)

列表 7-8:在 Card 定义中结合命名的滑块和按钮组件

请注意,drc.NamedSlider 本身就是一个可重用的组件,因此我们将另一个可重用组件 drc.Card 层叠在这个可重用组件上。

图 7-4 展示了 drc.Card 在 SVM Explorer 应用中的样子。这个命名的滑块由两个组件组成:一个 HTML 组件用于显示文本 "Threshold",另一个是 Dash Slider 组件,用于设置介于 0 和 1 之间的浮动值。

阈值稍后将作为我们 SVM 模型的输入值,用于控制分类模型对某一类别的偏向。虽然这是一个特定分类模型中的参数,但你可以使用相同的策略来展示机器学习中不同模型参数的性能影响。探索关键参数的影响变得和在智能手机上使用滑块一样简单!当你向公众展示下一个机器学习模型时,难道这不会给人留下深刻印象吗?

现在你已经知道如何通过为另一个组件包裹一个函数来创建可重用的组件。如果你没有掌握所有细节也不用担心;我们只希望你理解大概的思路:如何通过包装函数来创建可重用的组件。接下来让我们深入了解应用程序中使用的下一个自定义组件:格式化滑块。

定义格式化滑块

格式化滑块是另一个自定义包装器,由一个 HTML Div 元素和一个 dcc.Slider 组成,后者是 第六章中介绍的 Dash 核心组件。格式化滑块是一个已应用预定义格式化的 dcc.Slider 组件,通常与填充(padding)相关。为了简化使用,我们通常会使用简单的 CSS 将格式与滑块组件关联,但该应用的作者可能考虑以后添加一些更高级的组件或功能,因此他们将其设计为一个易于扩展的可重用组件。

Listing 7-9 展示了我们放置在dash_reusable_components.py中的包装函数代码。

def FormattedSlider(**kwargs):

return html.Div(

style=kwargs.get("style", {}),

children=dcc.Slider(**_omit(["style"], kwargs))

)

Listing 7-9:定义 FormattedSlider 组件

app.py中,我们创建了一个特定实例的格式化滑块,如 Figure 7-5 所示,使用以下滑块创建代码片段:

drc.FormattedSlider(

id="slider-svm-parameter-C-coef",

min=1,

max=9,

value=1,

)

这将创建一个格式化的滑块,最小值为 1,最大值为 9,两个连续值之间的滑块粒度为 1。我们将四个关键字参数传递给< s amp class="SANS_TheSansMonoCd_W5Regular_11">FormattedSlider()函数,它们随后被打包到< s amp class="SANS_TheSansMonoCd_W5Regular_11">kwargs字典中。字典中没有< s amp class="SANS_TheSansMonoCd_W5Regular_11">style键,因此 Listing 7-9 中的< s amp class="SANS_TheSansMonoCd_W5Regular_11">kwargs.get("style", {})调用返回空字典。在这种情况下,Dash 使用默认样式。我们将字典中的其余键值对作为关键字参数传递给< s amp class="SANS_TheSansMonoCd_W5Regular_11">dcc.Slider()创建例程。这些参数构建了一个具有指定范围的新滑块;请注意,Dash 自动为 SVM Explorer 应用程序中显示的特定格式化滑块添加了 1、3、5、7 和 9 的标签(参见 Figure 7-5)。如果您自己试用该滑块,您会意识到滑块的粒度为 1,即使刻度标记只显示每隔一个值。当然,如果需要,您可以通过添加另一个< s amp class="SANS_TheSansMonoCd_W5Regular_11">marks参数,按字典映射滑块值到文本标签,从而自定义刻度标记。

图 7-5:格式化滑块示例

定义命名滑块

命名滑块是 dcc.Slider 组件的另一种封装,它添加了一个自定义标题。图 7-6 显示了我们在 SVM Explorer 应用中命名为 Degree 的滑块。

图 7-6:命名滑块示例

定义 NamedSlider 组件的代码在 dash_reusable_components.py 文件中展示,见清单 7-10。

def NamedSlider(name, **kwargs):

return html.Div(

style={"padding": "20px 10px 25px 4px"},

children=[

html.P(f"{name}😊,

html.Div(style={"margin-left": "6px"},

children=dcc.Slider(**kwargs)),

],

)

清单 7-10:定义 NamedSlider 组件

我们创建了一个 HTML Div 容器,里面包含两个元素:一个 HTML 段落元素,使用 html.P() 为命名滑块添加标签,另一个 Div 容器包含一个常规的 Dash dcc.Slider() 元素。这里我们通过设置外层 Div 的样式字典中的 padding 属性来硬编码一些样式元素。这正是我们为什么可能选择使用 _omit() 从字典中移除 style 键的一个很好的例子;如果我们想要更改样式,我们将使用 Dash HTML 组件的这个特定样式参数。在我们的例子中,自定义样式扩展了命名滑块组件周围的框宽度。如果我们在 dash_reusable_components.py 中更改这个设置,那么我们在 app.py 中创建的每个实例都会自动调整以匹配这个变化!

我们使用格式化字符串 f"{name}:" 来访问 NamedSlider() 调用中 name 参数的值,并将其放入作为滑块标签的字符串中。这使得我们能够为每个滑块提供唯一的标签。

内部 Div 的 "margin-left" 属性将整个滑块稍微向右移动,给人一种滑块组件缩进的效果。

注意

在 dash_reusable_components.py 中的自定义函数名称按约定以大写字母开头,因为 Dash 组件本身也是大写的。这样,调用一个可重用的组件就像调用一个预定义的 Dash 组件一样。

Listing 7-11 显示了 app.py 中的代码,该代码实例化了命名滑块,参见 图 7-6。

drc.NamedSlider(

name="Degree",

id="slider-svm-parameter-degree",

min=2,

max=10,

value=3,

step=1,

marks={

str(i): str(i) for i in range(2, 11, 2)

},

)

Listing 7-11: 实例化 NamedSlider 组件

滑块的最小值为 2,最大值为 10。我们还将滑块的标记设置为整数 2、4、6、8 和 10,这些标记是通过生成器表达式 str(i) for i in range(2, 11, 2) 创建的。

定义一个命名下拉框

与 Slider 一样,我们将基于 dcc.Dropdown() 来创建一个带标签的命名下拉框。这个过程与创建命名滑块类似,因此我们将非常简要地介绍,向你展示它在不同上下文中的应用。Listing 7-12 显示了定义内容,位于 dash_reusable_components.py 文件中。

def NamedDropdown(name, **kwargs):

return html.Div(

style={"margin": "10px 0px"},

children=[

html.P(children=f"{name}:", style={"margin-left": "3px"}),

dcc.Dropdown(**kwargs),

],

)

清单 7-12:定义 NamedDropdown 组件

我们通过双星号运算符传递关键字参数列表,以便同时捕获所有关键字参数到kwargs字典中,并将所有这些关键字参数解包到dcc.Dropdown()创建例程中。创建NamedDropdown实例时传入的函数参数name将作为 HTML 段落元素中的文本标签。

结果生成的NamedDropdown可重用组件看起来类似于图 7-7。

图 7-7:命名下拉框示例

在清单 7-13 中,我们在app.py中创建了这个组件。

drc.NamedDropdown(

name="Select Dataset",

id="dropdown-select-dataset",

options=[

{"label": "Moons", "value": "moons"},

{

"label": "Linearly Separable",

"value": "linear",

},

{

"label": "Circles",

"value": "circles",

},

],

clearable=False,

searchable=False,

value="moons",

)

清单 7-13:实例化命名的下拉组件 组件

我们调用新创建的 drc.NamedDropdown() 函数,并为命名的下拉菜单组件指定我们想要的名称。剩余的关键字参数 id(HTML 元素的标识符),options(下拉菜单的标签和值),clearable(布尔值,决定是否允许用户通过点击小图标清除当前选择的条目),searchable(布尔值,决定是否允许用户搜索下拉菜单中的特定值),以及 value(默认下拉值)被打包进 kwargs 字典并传递给 dcc.Dropdown() 创建例程。

此实例化将在图 7-8 中创建命名的下拉菜单,默认数据集设置为“Moons”,并且禁用了searchable和clearable功能。

图 7-8:命名下拉菜单点击状态

使用可重用组件是扩展应用程序并为全球使用创建全新库的极为高效的方式。只需在 dash_reusable_components.py 文件中定义自己的组件,并在主程序文件中使用现有 Dash 和 HTML 组件的包装函数。可重用组件让你可以轻松自定义应用的外观和感觉,并使代码更容易理解、更加简洁,并且更容易维护,即使你的应用程序需要数千行代码!

接下来,我们将深入探讨 SVM Explorer 应用中一些尚未介绍的新 Dash 组件。

使用 Dash 图表

整个 SVM Explorer 应用的核心组件当然是可视化所选训练数据的学习和分类性能的图表。图 7-9 展示了最终的图表。

图 7-9:Dash 图表示例

首先,我们使用仪表板中不同控件的输入参数来训练模型。然后,我们测试模型在测试数据集上的准确性。圆点表示训练数据,三角形表示测试数据。红色数据点属于一类,蓝色数据点属于另一类;我们将它们分别称为类别 X 和类别 Y。对于每一条训练数据,我们已经知道它是 X 类还是 Y 类;也就是说,它是位于决策边界的哪一侧。然后,模型基于从训练数据学到的决策边界,估计每一条测试数据属于哪个类别。

以下的函数调用实现了这一强大的可视化(在 app.py 示例项目中的第 434 行):

dcc.Graph(id="graph-sklearn-svm", figure=prediction_figure)

我们创建了一个 dcc.Graph 组件,并指定 id "graph-sklearn-svm"。作为 figure 参数,我们传入了在 app.py 中第 410 到 421 行定义的 prediction_figure 变量(参见 Listing 7-14)。

prediction_figure = figs.serve_prediction_plot(

model=clf,

X_train=X_train,

X_test=X_test,

y_train=y_train,

y_test=y_test,

Z=Z,

xx=xx,

yy=yy,

mesh_step=h,

threshold=threshold,

)

Listing 7-14: 定义图形的属性

我们在这里不会深入讨论太多的技术细节,但请注意,函数调用使用了四个主要数据集:X_train 和 y_train,以及 X_test 和 y_test。与所有监督学习一样,我们使用由 (X, y) 元组组成的训练数据集训练模型,其中 X 是输入数据,y 是输出数据,以获得映射关系 Xy。我们将所有这些信息传递给以下函数:

figs.serve_prediction_plot()

该函数绘制了 SVM 的预测轮廓、阈值线以及测试和训练散点数据。然后,它返回生成的图形对象,可以将其传递给如前所示的dcc.Graph组件。我们将分解并讨论其组成部分。首先,figs部分指的是在app.py的头部引用的此导入语句:

import utils.figures as figs

我们从utils文件夹中导入了figures模块并命名为figs。该模块包含用于创建仪表板中显示的各种图表的工具函数,其中包括用于 SVM 模型训练和测试数据可视化的serve_prediction_plot()函数。

函数serve_prediction_plot()创建了一个 Plotly 图形对象,用于可视化训练数据和测试数据以及轮廓图(见图 7-10)。我们在figures.py模块的第 7 行到 96 行中定义了它,见清单 7-15。

import plotly.graph_objs as go

def serve_prediction_plot(...):

...

# 创建图形

# 绘制 SVM 的预测轮廓

trace0 = go.Contour(

...

)

# 绘制阈值

trace1 = go.Contour(

...

)

# 绘制训练数据

trace2 = go.Scatter(

...

)

trace3 = go.Scatter(

...

)

layout = go.Layout(

...

)

data = [trace0, trace1, trace2, trace3]

figure = go.Figure(data=data, layout=layout)

return figure

列表 7-15:创建图形对象并填充数据

这个代码框架展示了如何创建在图 7-10 中展示的轮廓图,这些图可视化了 SVM 信心水平,以及训练数据和测试数据的两个散点图。我们将这些图存储在四个变量中:trace0、trace1、trace2 和 trace3。然后,我们将这些变量作为数据输入参数传递给 go.Figure() 构造函数,该构造函数创建一个包含四个数据集的 Plotly 图形对象。

我们接下来将查看 go.Contour 组件。

创建 Plotly 轮廓图

轮廓线是将三维数据可视化为二维图形的一个好方法。二维空间中的每个点 (x,y) 都有一个对应的 z 值,可以将其视为该点的“高度”(例如,二维地图中的海拔值)。所有在同一轮廓线上的点具有相同的 z 值。图 7-10 展示了轮廓线的示例。

图 7-10:示例轮廓图

为了定义这些轮廓线,我们在二维数组中定义 z 值,其中单元格 (x,y) 定义了空间中相应 xy 点的 z 值。然后,Python 会自动“连接”这些点形成轮廓线。列表 7-16 中的代码片段生成了此图。

import plotly.graph_objects as go

fig = go.Figure(data =

go.Contour(

z = [[1, 2, 3],

[2, 3, 4],

[3, 4, 5]]

))

fig.show()

列表 7-16:创建基本的轮廓图

z 数组中,哪些单元格 (x,y)z 值为 3?这三个单元格分别是 (0,2)、(1,1) 和 (2,0)。现在,检查等高线图,找到这些 2D 空间中的点 (x,y)。可视化的 z 值是否与 3 相同?

你可以看到,通过定义几个具有相似 z 值的点,Plotly 完成了所有的重任,帮助你可视化等高线图,甚至为它着色!如果你想了解更多关于等高线图的内容(例如,如何自定义 xy 值,或等高线的形状),请访问 https://plotly.com/python/contour-plots

在 SVM 模型的等高线图中,等高线表示生成相同确定性的点,即点属于某个特定类别的概率。这种“确定性”叫做 决策函数,它为空间中的每个点关联一个值。它是机器学习模型的核心。你可以认为决策函数 就是 模型。对于给定的输入 x,决策函数 f(x) 的符号定义了模型 f 是否预测 x 属于某个类别。如果是正值,则属于类别 X;如果是负值,则属于类别 Y。决策函数的值越大或越小,输入点属于该类别的确定性就越高。

使用 Dash 加载

在前面“使用 Dash 图形”一节中,你了解了带有 prediction_figure 参数的 dcc.Graph 组件。涉及的计算比较复杂,可能需要一些加载或初始化时间。用户可能需要等待,这会影响可用性并显得有些笨拙。因此,SVM Explorer 应用的设计者决定将 dcc.Graph 组件包装在 dcc.Loading 组件中。这个想法很简单:当 Python 解释器处理计算时,Dash 会显示一个加载符号(加载旋转器)。始终保持用户的知情!

图 7-11 展示了在不同时间点上该加载符号的样子。

图 7-11:示例 Dash 加载符号

这个动态加载符号会一直展示给用户,直到加载被 dcc.Loading 组件包装的 Dash 组件完成。

现在让我们看看在 SVM Explorer 应用中是如何使用 dcc.Loading 组件的(请参见 清单 7-17)。

children=dcc.Loading(

className="graph-wrapper",

children=dcc.Graph(id="graph-sklearn-svm", figure=prediction_figure),

style={"display": "none"},

),

Listing 7-17: 设置加载行为

这个函数调用有三个参数:

className   我们从 CSS 样式表中关联了 graph-wrapper 类定义。这只是定义了组件的一些宽度和高度限制。

children   这是要被 dcc.Graph 对象包裹的 dcc.Loading 组件。在此对象加载时,应显示加载符号。

style   我们向元素添加了一个样式属性字典。特别地,我们将 "display" 属性设置为 "none",这基本上隐藏了整个元素。然而,在样式表中,我们将 "display" 属性覆盖为 "flex",这使得元素根据可用空间灵活调整大小。代码永远不是完美的,SVM 应用的创建者本可以更简洁地编写这一部分代码。

事实证明,即使我们运行 SVM Explorer 应用,我们也不会看到加载符号,因为组件加载得非常快。我们怀疑这个应用最初是为较慢版本的 Dash 实现的。但是 Dash 的速度和可用性都在迅速提升,因此现在这个 SVM 应用可以很快完成计算——我们甚至可以跳过 dcc.Loading 包裹器。

要查看关于在 Dash 应用中使用加载旋转器的完整视频教程,请参考视频“Dash Bootstrap Spinner & Progress Bar”,可以在https://learnplotlydash.com观看。

Dash 回调

SVM Explorer 应用是一个高级应用,包含许多交互式代码模块。到目前为止,我们已专注于那些你在其他应用中未见过的独立组件。现在是时候再次查看全局视角,探索不同组件是如何相互作用的。

为了快速概览该集中哪些部分,让我们从 Dash 框架在你运行应用时提供的回调图开始(参见 Listing 7-18)。

运行服务器

if name == "main":

app.run_server(debug=True)

清单 7-18:启用调试

现在,你可以通过图 7-12 中显示的按钮菜单访问自动生成的回调图。

图 7-12:回调图按钮菜单

此按钮菜单应出现在浏览器中 Dash 应用程序的右下角。点击回调图以获得图 7-13 中显示的内容。

图 7-13:SVM Explorer 应用程序的回调图

框上方的名称是你在 app.py 文件中定义的 Dash 组件。清单 7-19 显示了一个命名滑块的代码示例。

drc.NamedSlider(

name="成本 (C)",

id="slider-svm-parameter-C-power",

min=-2,

max=4,

value=0,

marks={

i: "{}".format(10 ** i)

for i in range(-2, 5)

},

)

清单 7-19:NamedSlider 组件定义,展示回调图中名称的来源

你可以在四个上方框中的一个找到名称 slider-svm-parameter-C-power。使用命名滑块,你可以将其传递给 slider-svm-parameter-C-coef 组件。所有组件都会传递给包含我们所有 SVM 图形的 div-graphs 组件。

所以,让我们专注于回调函数,它将所有这些输入映射到 app.py 主文件中的单个输出组件 div-graphs,该函数在 346 至 453 行之间。在这里,我们特别从输入和输出注解以及函数定义开始,如清单 7-20 所示。

@app.callback(

Output("div-graphs", "children"),

[

Input("dropdown-svm-parameter-kernel", "value"),

Input("slider-svm-parameter-degree", "value"),

Input("slider-svm-parameter-C-coef", "value"),

Input("slider-svm-parameter-C-power", "value"),

Input("slider-svm-parameter-gamma-coef", "value"),

Input("slider-svm-parameter-gamma-power", "value"),

Input("dropdown-select-dataset", "value"),

Input("slider-dataset-noise-level", "value"),

Input("radio-svm-parameter-shrinking", "value"),

Input("slider-threshold", "value"),

Input("slider-dataset-sample-size", "value"),

],

)

def update_svm_graph(

kernel,

degree,

C_coef,

C_power,

gamma_coef,

gamma_power,

dataset,

noise,

shrinking,

threshold,

sample_size,

):

Listing 7-20: SVM 图形的输入和输出注解

函数并非只有单一输入,而是包含了一个输入列表,如回调图中所示。所有这些输入都需要用于计算 SVM 模型。然后,使用此 SVM 模型来生成你在 SVM Explorer 应用中看到的所有图形。

Listing 7-21 显示了生成不同图形的代码。

# … 为了可读性省略了模型计算部分 …

return [

html.Div(

id="svm-graph-container",

children=dcc.Loading(

className="graph-wrapper",

children=dcc.Graph(id="graph-sklearn-svm",

figure=prediction_figure),

style={"display": "none"},

),

),

html.Div(

id="graphs-container",

children=[

dcc.Loading(

className="graph-wrapper",

children=dcc.Graph(id="graph-line-roc-curve",

figure=roc_figure),

),

dcc.Loading(

className="graph-wrapper",

children=dcc.Graph(

id="graph-pie-confusion-matrix",

figure=confusion_figure

),

),

],

),

]

列表 7-21: 返回值的 update_svm_graph 函数,该函数生成 SVM Explorer 应用中的图表

返回值是一个包含两个 Div 元素的列表。第一个包含本章前面“创建 Plotly 等高线图”部分讨论的预测图。第二个包含另外两个 dcc.Graph 元素:一个折线图和一个饼图。图 7-14 显示了这三张生成的图。

图 7-14:三个 dcc.Graph 元素

总结

本章介绍了许多高级 Dash 概念。你学习了强大的 SVM 分类算法,以及仪表盘如何帮助你可视化机器学习模型。你学习了如何将 NumPy 和 scikit-learn 集成到你的仪表盘应用中,以及如何创建可重用的组件。你还学习或加深了对 Dash HTML 组件的理解,如 html.Div、html.A、html.Section、html.P、html.Button 和 html.H2,以及标准 Dash 组件,如 dcc.Graph、dcc.Slider 和 dcc.Dropdown。

现在你掌握了创建自己复杂仪表盘应用的技能,并可以深入图库学习更多关于 Dash 组件和功能的高级内容。我们不仅仅给了你现成的成果;我们告诉了你如何以及在哪里自己去获取这些成果。图库就像是一片鱼的海洋,如果你将来想要更多,你就知道该去哪里了!

资源

若想深入了解 SVM Explorer 应用,欢迎查阅 Xing Han(该应用的创建者之一)向我们推荐的以下资源:

第八章:8 技巧与窍门

在丰富的 Dash 库中,还有很多内容等待你去探索。在本章中,我们整理了一些提示,帮助你在 Dash 之旅中迈出下一步。这些都是我们在学习 Dash 并开始构建更复杂应用时发现的有用技巧。

我们将深入探讨 Dash 企业应用库,在这里你可以发现用于构建特定行业内更先进应用的开源代码。你还将学会利用 Plotly 社区帮助你解决编程过程中的问题。我们将分享一些 Bootstrap 主题和调试工具,帮助你美化应用并解决 bug。我们还会指导你如何浏览 dash-labs 仓库,在那里最前沿的 Dash 功能正在不断开发。最后,本章还将为你提供一套 Dash 学习资源,帮助你提升知识,并让使用 Dash 编程变得更加有趣和激动人心。

Dash 企业应用库

正如我们在本书中提到的,一个学习更高级和复杂 Dash 应用的有效方式是探索 Dash 企业应用库(https://dash.gallery/Portal)。这些应用中的许多都是开源的,意味着你可以在 GitHub 上完全访问其代码。要查看某个应用是否为开源,可以点击位于应用卡片右下角的信息图标(如图 8-1 所示);这时会弹出一个模态框,如果显示类似“未经认证:任何人都可以访问此应用”的信息,那么该应用就是开源的。该信息卡片还会告诉你该应用使用的编程语言;如你所料,绝大多数应用都是用 Python 编写的。

图 8-1:Dash 应用库中应用卡片上的信息图标

应用库正在不断更新。为了找到与你需求最相关的应用,你可以通过点击页面顶部的特定行业来筛选页面。一个小提示:当你浏览这些应用时,考虑一下自己当前应用项目的布局。如果你发现某个布局很有趣,可以访问其开源代码,看看能否在你的应用中复制这个布局。

通过 Plotly 论坛提升你的学习

Plotly 论坛(https://community.plotly.com)是一个为 Plotly 和 Dash 用户提供的社区论坛。如果你还没有 Plotly 论坛的账号,现在就应该注册一个。在你使用 Dash 开发的过程中,论坛社区的成员会帮助你更好地了解 Dash 和 Plotly 图表库,克服困难,并解决特定的 bug。即使你现在没有代码问题,我们也建议你抽时间浏览论坛,阅读一些你感兴趣的话题。通过阅读其他用户的经验,你会学到很多;最重要的是,浏览论坛将帮助你了解如何创建话题、提供有用的答案,并以一种能够得到可操作回答的方式提问。你很快就会看到,论坛如何为社区的成长做出贡献。图 8-2 显示了论坛的登录页面;当然,每次访问时它会有所不同,所以它看起来不会完全一样。

图 8-2: Plotly 论坛登录页面

在页面的左侧,你会找到主要的类别,DashPlotly。在右侧,你会看到这两个类别中最新的帖子,不论主题是什么。

论坛上的社区通常非常活跃并乐于助人。为了确保你的问题能够被看到并得到回答,务必使用一个适合你问题的帖子标题,并清晰地陈述你遇到的问题;另外,确保你附上与你的问题相关的代码。这个代码通常被称为 最小工作示例,它使得潜在的回复者能够复制你的代码并在他们的系统上进行测试,看是否能复现相同的问题或错误。确保代码格式正确,使用编辑工具箱中的预格式化文本符号 </>。

随着你在 Dash 中获得更多经验,花时间回馈社区,帮助他人解答问题。最后,我们鼓励你通过使用 展示和讲解 标记你的帖子,将你创建的应用与社区分享。

应用主题浏览器

在 第五章 中,你学习了如何将 Bootstrap 主题添加到你的应用中,如下所示:

app = Dash(name, external_stylesheets=[dbc.themes.BOOTSTRAP])

这些主题将仅应用于应用程序中的 Bootstrap 组件。要将主题完全应用到你的应用中,还需要将其应用到 Dash DataTable、Dash 核心组件和 Plotly 图形中。位于https://hellodash.pythonanywhere.comDash Bootstrap 主题浏览器,如图 8-3 所示,允许你选择一个主题并查看它在页面上所有组件、文本和图形中的效果。要查看可用的主题,点击页面左侧的更改主题。一个面板应该会滑出,列出可用的主题。点击其中一个,看看下拉框、复选框组件、标题、文本、图表和DataTable的样式和颜色变化。

图 8-3:Dash Bootstrap 主题浏览器登陆页面

选择一个适合你应用程序的主题,并按照以下四个步骤将其添加到应用程序的所有元素中。我们将在一个示例应用程序中实现主题作为例子;完整的app.py文件可以在书籍资源的* Chapter-8 *文件夹中找到,网址为https://github.com/DashBookProject/Plotly-Dash

  1. 安装dash_bootstrap_templates库,然后导入load_figure_templatedash_bootstrap_components。为此,请打开 PyCharm 终端并输入:

$ pip install dash-bootstrap-templates

要导入必要的库,请在主应用程序文件中键入以下内容:

import dash_bootstrap_components as dbc

from dash_bootstrap_templates import load_figure_template

  1. 将预构建的 Dash Bootstrap 样式表添加到你的应用程序,并选择一个主题。在这里我们选择了VAPOR。确保在替换主题时保持大写风格:

dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates

@V1.0.4/dbc.min.css"

app = Dash(name, external_stylesheets=[dbc.themes.VAPOR, dbc_css])

load_figure_template(["vapor"])

  1. 将选定的主题纳入条形图的template属性:

fig = px.bar(df, x="Fruit", y="Amount", color="City", barmode="group",

template="vapor")

4.  最后,将 className="dbc" 添加到应用程序的外部容器中,如下代码所示:

app.layout = dbc.Container([

html.H1("Hello Dash", style={'textAlign': 'center'}),

html.P("在此处输入内容:"),

dcc.Input(className="mb-2"),

dcc.Graph(

id='example-graph',

figure=fig

)

],

fluid=True,

className="dbc"

)

这个 app.py 文件在本示例中执行后,应该会生成图 8-4 中所示的应用程序。

图 8-4:完整的示例应用程序

调试 Dash 应用程序

了解如何有效地调试你的应用程序将为你节省许多试错的时间。当出现错误时,掌握调试的完整技能超出了本章的范围;然而,我们在这里整理了一些资料,帮助你入门。

Python 提供了一些免费的调试器包。对于 Dash,我们推荐使用 ipdb 包。要安装它,请打开终端并输入:

$ pip install ipdb

让我们看一个调试可能有帮助的例子。找到书中的 debug-demo.py 文件,位于 https://github.com/DashBookProject/Plotly-Dash 上。当你在计算机上运行它时,应该会看到类似图 8-5 的效果。这个应用程序本应图示随时间变化的账单总额。

图 8-5:调试演示应用程序执行后的效果

这非常令人沮丧:我们的应用程序没有抛出任何错误,但显然有问题,因为图表没有显示任何数据。让我们调试应用程序,找出问题所在。

首先,取消注释 debug-demo.py 中的第一行代码,以 import ipdb。然后,通过取消注释这行代码,激活回调函数中的调试功能:

ipdb.set_trace()

当然,你可以调试应用程序的任何部分;在我们的案例中,我们将从构建图表的回调函数开始,因为问题出在图表上。最后,关闭原生 Dash 调试机制,并关闭应用程序的多线程功能,以免由于重叠的 ipdb 实例而中断会话,就像在 debug-demo.py 中那样:

if name == 'main':

app.run_server(debug=False, threaded=False, port=8004)

保存并运行修改后的 debug-demo.py 文件,然后点击 HTTP 链接在浏览器中打开你的应用程序。返回到运行工具窗口,你应该会看到类似 图 8-6 的内容。

图 8-6:在 PyCharm 运行窗口中激活调试

如果你尝试在运行窗口执行 print(dff.head()),你会收到一个错误,提示 dff 未定义。这是因为创建和定义 dff 的代码行在第 23 行,而该行尚未执行。为了告诉调试器执行下一行代码,在运行窗口输入小写字母n。现在,如果你再次执行 print(dff.head()),你应该能看到 DataFrame 的前五行,如 图 8-7 所示。

图 8-7:在运行窗口中打印的 DataFrame

然而,当你下一次按下n以执行代码行 24,并再次输入 print(dff.head()) 时,你会在运行窗口看到一个通知,告诉你 DataFrame 是空的:

空的 DataFrame

Columns: [total_bill, tip, sex, smoker, day, time, size]

Index: []

这是因为第 24 行将 day 列过滤,只保留包含 'Mon' 的行。看起来没有任何行的 day 列值是 'Mon',这就是为什么 DataFrame 是空的。要检查 day 列中存在的唯一值,可以在运行窗口中输入 print(df.day.unique())。你会发现 day 列中只有 ['Sun' 'Sat' 'Thur' 'Fri'] 这些值。这就是当应用程序执行时,图表没有绘制任何内容的原因:没有数据可供绘制。

要修复你的应用程序,请将第 24 行的 'Mon' 改为 'Fri',然后重新启动 debug-demo.py 文件。(如果你的应用无法重启,可以将最后的端口号从 8004 改为其他任何数字。)回到终端时,不必为每一行代码输入 n,你可以输入 c 来继续执行程序直到完成。因为应用中没有其他错误(断点),所以它将成功执行,并且应该像 图 8-8 所示。

图 8-8:该 debug-demo.py 应用程序在调试后成功执行

如需获取 ipdb 的备忘单,请访问 https://wangchuan.github.io/coding/2017/07/12/ipdb-cheat-sheet.html

调试愉快!

dash-labs

dash-labs 是一个由 Plotly 启动的 GitHub 仓库,作为潜在未来 Dash 特性的一项技术预览,地址为 https://github.com/plotly/dash-labs。社区反馈和积极参与对该仓库的成功至关重要,因为这些功能是为社区并与社区的帮助下开发的。过去几年里,dash-labs 中开发的一些功能包括灵活的回调签名(https://dash.plotly.com/flexible-callback-signatures)和长回调(https://dash.plotly.com/long-callbacks)。

在撰写本文时,dash-labs 中有两个活跃的项目,包括用于快速和无缝编写多页面应用程序的 Multipage Apps 功能和允许通过 Dash 执行 Markdown 文档的 Dashdown 功能。要开始探索 dash-labs,请点击 dash-labs 仓库中的 docs 文件夹(图 8-9),并了解目前已开发的功能。

图 8-9:与 dash-labs 仓库相关的 文档文件夹

要亲自试试其中的一些功能,你可以 git clone dash-labs 并运行 demos 文件夹中的任意 app.pyapp_dbc.py 文件。

使用 Black 格式化代码

以一种良好格式编写代码不仅具有视觉上的美感,而且对于可读性至关重要。随着你技能的提升,你所编写的程序会变得越来越大、越来越复杂。如果这些程序没有良好的格式,你很容易在自己的代码中迷失方向。手动按照 PEP8 格式(Python 官方风格指南)格式化代码是非常耗时的。幸运的是,我们有 Python 工具 Black:一个代码格式化工具。

让我们看看 Black 是如何工作的。首先,安装 Black:

$ pip install black

然后从 https://github.com/DashBookProject/Plotly-Dash 下载 pre-black-formatting.py 文件并打开它(列表 8-1)。

from dash import Dash, dcc, html

import plotly.express as px

import pandas as pd

app = Dash(name)

df = pd.DataFrame({

❶ 'Fruit': ["Apples", "Oranges", "Bananas", "Apples", "Oranges",

"Bananas"],

"Amount": [4, 1, 2, 2, 4, 5],

"City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"]

})

❷ fig=px.bar(df, x="Fruit", y="Amount", color="City")

app.layout = html.Div([

html.H1("Fruit Analysis App", style={'textAlign':'center'}),

❸  dcc.Graph(

id='example-graph',

figure=fig

)

],

)

if name == 'main':

app.run_server(debug=True)

清单 8-1:The pre-black-formatting.py 文件

代码中存在一些格式不一致的地方。例如,Fruit 键❶ 被单引号括起来,而< sampa class="SANS_TheSansMonoCd_W5Regular_11">Amount和< samp class="SANS_TheSansMonoCd_W5Regular_11">City 键则被双引号括起来。同样,Fruit 键的值跨越了两行代码,而其他键值都写在了一行代码上。而且,在构建 Plotly Express 柱状图的那行代码❷中,我们可以看到等号前后没有空格(fig=px.bar)。最后,我们看到 Dash 的 Graph 组件跨越了四行代码❸,而位于其上方的 html.H1 组件仅用了一行代码。代码中还有一些其他的不一致之处,看看在使用 Black 之前你能发现多少。

要使用 Black,打开终端并进入包含pre-black-formatting.py的目录。进入后,输入命令并跟上文件名,如下所示:

$ black pre-black-formatting.py

Black 会自动格式化文件而不更改文件名。为了本演示的目的,我们将文件重命名为 post-black-formatting.py,该文件也位于书籍的 GitHub 仓库中(清单 8-2)。

from dash import Dash, dcc, html

import plotly.express as px

import pandas as pd

app = Dash(name)

df = pd.DataFrame(

{

"Fruit": ["苹果", "橙子", "香蕉", "苹果", "橙子", "香蕉"], ❶

"Amount": [4, 1, 2, 2, 4, 5],

"City": ["SF", "SF", "SF", "Montreal", "Montreal", "Montreal"],

}

)

fig = px.bar(df, x="Fruit", y="Amount", color="City") ❷

app.layout = html.Div(

[

html.H1("水果分析应用", style={"textAlign": "center"}),

dcc.Graph(id="example-graph", figure=fig), ❸

],

)

if name == "main":

app.run_server(debug=True)

示例 8-2:用 Black 格式化的 post-black-formatting.py 文件

我们可以看到,所有的单引号都已被替换为双引号,Fruit 键的值已经写在一行代码中,而不是两行 ❶,等号前后也有相等的空格 ❷,而 Graph 组件也被写在一行,而不是四行 ❸。正如你所看到的,Black 格式化的代码一致且更易读。

后续资源

重要的事情不是停止提问;好奇心有其存在的理由。

—阿尔伯特·爱因斯坦

理解到学习永无止境,我们整理了一些资源,帮助你成为 Dash 的专家:

附录

PYTHON 基础知识

本附录的重点是快速回顾 Python 的基础知识。Dash 书籍的范围不包括完整的 Python 速成课程,我们只会介绍关键词、数据结构、控制流和函数等基础知识。您可以通过一些优秀的资源全面学习 Python,包括我们的免费电子邮件学院:https://blog.finxter.com/email-academy

注意

介绍部分使用了 Python One-Liners (No Starch Press, 2020) 中的代码示例和文本片段,该书是本书的一位作者所著。我们建议您阅读此书,以全面了解 Python 单行代码的含义。

安装和入门

如果您尚未安装 Python,则需要在计算机上设置 Python。由于 Python 不断发展,我们将保持这些信息的概括。

1.  首先,请访问官方 Python 网站https://www.python.org/downloads,下载适合你操作系统的最新版本的 Python。

2.  在你的计算机上运行安装程序。根据版本和操作系统的不同,你应该会看到一个类似于图 A-1 的对话框。确保点击框以将 Python 添加到 PATH 中,以便通过 Python 访问计算机上的任何目录。

图 A-1:安装 Python 弹出窗口

3.  通过在命令行(Windows)、终端(macOS)或 shell(Linux)中运行以下命令,检查您的 Python 安装是否正常运行:

$ python—version

Python 3.x.x

注意

美元符号($)只是一个提示,告诉你在终端或代码 shell 中运行接下来的代码。粗体文本是你应输入的命令。

恭喜!您已在计算机上安装了 Python。您可以开始使用系统内置的 IDLE 编辑器编写自己的程序。只需在操作系统中搜索 IDLE 并打开该程序即可。

作为第一个程序,将以下命令输入到您的 shell 中:

print('hello world!')

Python 将解释您的命令并将所需的文字打印到您的 shell(参见图 A-2):

hello world!

图 A-2:Python 中的 hello world 程序

与 Python 解释器进行交互式双向通信的这种模式称为交互模式。它的优点是能够立即反馈。然而,编程计算机最令人兴奋的结果是自动化:编写一次程序并多次运行它。

让我们从一个简单的程序开始,每次运行时都用你的名字打招呼。你可以保存该程序,并在任何时候重新运行。此类程序称为脚本,你可以将它们保存为 .py 后缀文件,如 my_first_program.py,以保存为 Python 文件。

你可以通过 IDLE shell 的菜单创建脚本,如 图 A-3 所示。

图 A-3:创建你自己的模块

点击 文件新建文件,并将以下代码复制粘贴到你的新文件中:

name = input("What's your name?")

print('hello' + name)

将文件保存为 hello.py,存放在桌面或其他任何位置。你当前的脚本应该类似于 图 A-4。

图 A-4:一个接受用户输入并将响应打印到标准输出的程序

现在,让我们开始一些操作:点击 运行运行模块。Python 程序将在交互式 shell 中开始执行,无需你逐行输入。它会逐行执行代码文件。第一行会提示你输入名字,并等待你输入。第二行则会接收你的名字并将其打印到 shell 中。图 A-5 展示了程序的执行效果。

图 A-5:程序在图 A-4 中的示例执行

数据类型

现在你已经看到一个 Python 程序的运行,我们将回顾一些基本数据类型。

布尔值

布尔数据类型表示的是关键字 False 或 True。在 Python 中,布尔和整数数据类型紧密相关,因为布尔类型在内部使用整数值:False 表示整数 0,True 表示整数 1。布尔值通常用于断言或作为比较的结果。以下代码片段展示了这两个布尔关键字的使用示例:

2

print(x)

False

1

print(y)

True

在评估给定的表达式后,变量 x 代表值 False,变量 y 代表值 True。布尔值允许我们创建条件执行的代码,因此它们在处理数据时非常重要,因为它们可以让我们在使用某个值之前检查它是否超过阈值(有关基于阈值的数据分类,请参见 第七章 中的 SVM Explorer 应用)。

布尔值有几个主要运算符,表示基本的逻辑运算符:and、or 和 not。关键字 and 在表达式 x and y 中,当 x 为 True 且 y 为 True 时,评估为 True。如果其中只有一个为 False,则整个表达式为 False。

关键字 or 在表达式 x or y 中,如果 x 为 True y 为 True 两者都是 True,则评估为 True。如果其中只有一个为 True,则整个表达式为 True。

关键字 not 在表达式 not x 中,当 x 为 False 时,评估为 True。考虑以下使用每个布尔运算符的 Python 代码示例:

x, y = True, False

print(x or y)

True

print(x and y)

True

print(not y)

True

通过使用这三种操作符——and、or 和 not——你可以表达你所需要的所有逻辑表达式。

布尔运算符按优先级排序。运算符 not 具有最高优先级,其次是运算符 and,最后是运算符 or。请考虑以下示例:

x, y = True, False

print(x and not y)

True

print(not x and y or x)

True

我们将变量 x 设置为 True,将 y 设置为 False。当调用 not x and y or x 时,Python 会将其解释为 ((not x) and y) or x,这与例如 (not x) and (y or x) 不同。作为练习,弄明白为什么

数值类型

两种最重要的数值数据类型是整数和浮点数。整数是没有浮动精度的正数或负数(例如,3)。浮点数是具有浮动精度的正数或负数(例如,3.14159265359)。Python 提供了多种内置的数值操作,还可以将不同的数值数据类型之间进行转换。以下是几个算术操作的示例。首先,我们将创建一个值为 3 的 x 变量和一个值为 2 的 y 变量:

x, y = 3, 2

x + y

5

x — y

1

x * y

6

x / y

1.5

x // y

1

x % y

1

-x

-3

abs(-x)

3

int(3.9)

3

float(3)

3.0

x ** y

9

前四种运算分别是加法、减法、乘法和除法。// 运算符执行整数除法。结果是一个整数值,会向下舍入到较小的整数(例如,3 // 2 == 1)。% 运算是 取模运算,它仅返回除法的余数。减号运算符 – 将值转换为负数。abs() 给出绝对值(即值作为非负数)。int() 将值转换为整数,舍弃小数点后的任何数字。float() 将给定值转换为浮动点数。双星号 ** 表示乘方运算。运算符优先级与您在学校学到的一样:括号优先于指数运算,指数优先于乘法,乘法优先于加法,依此类推。

字符串

Python 字符串是字符序列。字符串是不可变的,因此一旦创建就不能更改;如果需要更改字符串,必须创建一个新的字符串。字符串通常是引号内的文本(包括数字):"this is a string"。以下是创建字符串的五种最常见方法:

单引号:'Yes'

双引号:"Yes"

三引号(用于多行字符串):'''Yes''' 或 " ""Yes" ""

字符串方法:str(yes) == 'yes' 是 True

字符串连接:'Py' + 'thon' 变为 'Python'

要在字符串中使用空白字符,您需要明确地指定它们。要在字符串内开始新的一行,请使用换行符 '\n'。要添加一个制表符的空格,请使用制表符字符 '\t'。

字符串也有自己的一组方法。strip() 方法移除字符串的前导和尾随空白字符,包括空格、制表符和换行符:

y = " This is lazy\t\n "

print(y.strip())

结果更加整洁:

'This is lazy'

lower() 方法将整个字符串转为小写:

print("DrDre".lower())

我们得到了:

'drdre'

upper() 方法将整个字符串转换为大写:

print("attention".upper())

这给我们带来了:

'ATTENTION'

startswith() 方法检查您提供的参数是否出现在字符串的开头:

print("smartphone".startswith("smart"))

它返回一个布尔值:

True

endswith() 方法检查您提供的参数是否出现在字符串的末尾:

print("smartphone".endswith("phone"))

这也返回一个布尔值:

True

find() 方法返回子字符串在原始字符串中第一次出现的索引:

print("another".find("other"))

如下所示:

Match index: 2

replace() 方法将第一个参数中的字符替换为第二个参数中的字符:

print("cheat".replace("ch", "m"))

cheat 变成:

meat

join() 方法将可迭代参数中的所有值组合起来,使用调用它的字符串作为分隔符:

print(','.join(["F", "B", "I"]))

我们得到了:

F,B,I

len() 方法返回字符串中的字符数量,包括空白字符:

print(len("Rumpelstiltskin"))

这给我们带来了:

String length: 15

in 关键字在与字符串操作数一起使用时,会检查一个字符串是否出现在另一个字符串中:

print("ear" in "earth")

这也返回一个布尔值:

Contains: True

这个非排他性的字符串方法列表表明,Python 的字符串数据类型是灵活且强大的,你可以利用内置的 Python 功能解决许多常见的字符串问题。

控制流

让我们深入了解一些让我们的代码做出决策的编程逻辑。算法就像一个烹饪食谱:如果这个食谱只是一个顺序的命令列表——将水倒入锅中,加入盐,加入米,倒掉水,最后上桌——你可能几秒钟内就完成了步骤,却得到了一碗未煮熟的米饭。我们需要根据不同的情况作出不同的反应:只有米饭变软时,才从锅中去除水;水开始沸腾时,才把米饭放入锅中。根据不同条件做出不同响应的代码被称为条件执行代码。在 Python 中,条件执行的关键字有 if、else 和 elif。

这是一个比较两个数字的基本示例:

half_truth = 21

if 2 * half_truth == 42:

print('Truth!')

else:

print('Lie!')

这将打印:

Truth!

if 条件 2 * half_truth == 42 会生成一个结果,结果可以是 True 或 False。如果表达式的结果是 True,则进入第一个分支并打印 Truth!。如果表达式的结果是 False,则进入第二个分支并打印 Lie!。由于表达式的结果是 True,所以进入第一个分支,shell 输出为 Truth!。

每个 Python 对象,如变量或列表,都有一个隐式关联的布尔值,这意味着我们可以将 Python 对象用作条件。例如,一个空列表的布尔值为 False,而非空列表的布尔值为 True:

lst = []

if lst:

print('Full!')

else:

print('Empty!')

这会打印:

Empty!

如果你不需要 else 分支,可以简单跳过它,当条件评估为 False 时,Python 会跳过整个代码块:

if 2 + 2 == 4:

print('FOUR')

这会打印:

FOUR

只有当 if 条件评估为 True 时,才会打印输出。否则,不会发生任何事情。代码没有副作用,因为它会被执行流跳过。

你也可以有超过两个条件的代码。在这种情况下,你可以使用 elif 语句:

x = input('Your Number: ')

if x == '1':

print('ONE')

elif x == '2':

print('TWO')

elif x == '3':

print('THREE')

else:

print('MANY')

这段代码会接收你的输入并将其与字符串 '1'、'2' 和 '3' 进行比较。在每种情况下,都会输出不同的结果。如果输入与任何字符串都不匹配,则进入最后一个分支,输出 'MANY'。

以下代码片段获取用户输入,将其转换为整数,并将其存储在变量x中。然后,它测试该变量是否大于、等于或小于 3,并根据评估结果打印不同的消息。换句话说,这段代码对现实世界中不可预测的输入做出有差异的响应:

x = int(input('请输入您的值: '))

3:

print('Big')

elif x == 3:

print('Medium')

else:

print('Small')

我们给出关键字if,后跟一个条件,该条件决定执行路径。若条件为True,执行路径将沿着紧接着的缩进代码块中的第一分支。如果条件为False,执行流会继续向下,执行以下三种情况之一:

1.  根据任意数量的elif分支进一步评估其他条件

2.  如果没有满足条件的if或elif条件,则进入else分支

3.  当没有给出else分支,并且没有满足条件的elif分支时,跳过整个结构

规则是,执行路径从顶部开始,向下移动,直到某个条件匹配——在这种情况下,执行相应的代码分支——或者所有条件都被探查过,但没有任何条件匹配。

这里你可以看到,将对象传入if条件并像布尔值一样使用它们是可能的:

if None or 0 or 0.0 or ' ' or [] or {} or set():

print('Dead code') # 不会到达

if 条件计算结果为 False,因此 print 语句永远不会被执行。这是因为以下值会被计算为布尔值 False:关键字 None、整数值 0、浮点值 0.0、空字符串和空容器类型。表达式 None or 0 or 0.0 or ' ' or [] or {} or set() 会被计算为 True,如果 Python 能隐式地将任何操作数转换为 True,但在这里并不会,因为所有这些值都会被转换为 False。

重复执行

为了允许重复执行类似的代码片段,Python 提供了两种循环类型:for 循环和 while 循环。我们将创建一个 for 循环和一个 while 循环,以不同的方式实现相同的功能:将整数 0、1 和 2 打印到 Python shell 中。

下面是 for 循环:

for i in [0, 1, 2]:

print(i)

这将输出:

0

1

2

for 循环通过声明一个循环变量 i,该变量会依次取出列表 [0, 1, 2] 中的所有值。然后它会打印变量 i,直到列表中的值用尽。

下面是具有相似语义的 while 循环版本:

i = 0

while i < 3:

print(i)

i = i + 1

这也将输出:

0

1

2

while循环只要条件满足,就会执行循环体——在我们的例子中,只要i < 3。使用哪种方法取决于你的具体情况。通常,当你遍历一个固定数量的元素时(例如,遍历列表中的所有元素),你会使用for循环;当你希望重复某个操作,直到达成某个结果时(例如,猜密码直到成功),你会使用while循环。

终止循环有两种基本方式:定义一个循环条件,使其评估为False,如前面的例子所示,或者在循环体内的确切位置使用关键字break来停止循环。这里我们使用break来退出原本会变成无限循环的情况:

while True:

break # No infinite loop

print('hello world')

从中我们得到:

hello world

我们创建了一个while循环,循环条件始终评估为True,因为循环条件while True本身就固有地为True。循环在break处提前结束,因此代码会继续执行并输出print('hello world')。

你可能会想,如果我们不希望它一直运行,为什么还要创建一个无限循环呢?这是一个常见的做法,例如,在开发需要使用无限循环等待新网页请求并提供服务的 Web 服务器时。然而,你仍然希望能够在循环未完成时提前终止它。在 Web 服务器的例子中,如果服务器检测到遭受攻击,可能会希望停止提供文件服务。在这些情况下,你可以使用关键字break,当满足某个条件时停止循环。

也可以强制 Python 解释器跳过循环中的某些部分,而不是提前结束循环。在我们的 Web 服务器示例中,你可能希望跳过恶意的 Web 请求,而不是完全停止服务器。你可以使用 continue 关键字实现这一点,它会结束当前的循环迭代,并将执行流带回到循环条件的开始:

while True:

continue

print('43') # 死代码

这段代码将永远执行下去,而不会执行一次 print 语句,因为 continue 语句结束了当前的循环迭代,并将执行流带回到循环开始处,未到达 print() 语句。因此,print() 语句现在被视为 死代码:即永远不会被执行的代码。continue 语句和 break 语句通常只有在与 if-else 语句配合使用时才有意义,像这样:

while True:

user_input = input('请输入密码: ')

if user_input == '42':

break

print('错误!') # 死代码

print('恭喜,你找到了秘密密码!')

这段代码会请求一个密码,并会一直执行下去,直到用户猜到正确的密码。如果他们输入正确的密码 42,程序会执行到 break 语句,结束循环,并执行成功的 print 语句。如果密码错误,循环会中断,执行返回到开始位置,用户需要重新尝试。以下是一个示例用法:

your password: 41

错误!

your password: 21

错误!

your password: 42

恭喜,你找到了秘密密码!

这些是控制程序执行流程的最重要关键字。

其他有用的关键字

让我们来看一些额外有用的关键字。in 关键字检查某个元素是否存在于给定的序列或容器类型中。这里我们检查 42 是否在后面的列表中,然后检查 21 是否可以作为字符串在集合中找到:

print(42 in [2, 39, 42])

True

print('21' in {'2', '39', '42'})

False

in 关键字返回一个布尔值,因此第一个语句会返回 True,第二个语句会返回 False。

is 关键字检查两个变量是否指向内存中的同一个对象。Python 初学者通常会对 is 关键字的确切含义感到困惑,但值得花时间理解它的真正含义。在这里,我们可以看到两个变量指向内存中的同一个对象与两个看似相似但指向不同对象的列表之间的区别:

x = 3

y = x

print(x is y)

True

print([3] is [3])

False

如你在后面的例子中所见,如果你创建了两个列表——即使它们包含相同的元素——它们仍然在内存中指向两个不同的列表对象。如果你后来决定修改其中一个列表对象,这不会影响另一个列表对象。如果你检查某个列表是否指向内存中的同一个对象,结果是 False。在这里,当我们检查 x 是否与 y 相同时,得到的是 True,因为我们显式地将 y 设置为指向 x。然而,当我们检查列表 [3] 是否与 [3] 相同时,我们得到的是 False,因为这两个列表指向内存中的不同对象。如果你修改其中一个,另一个不会改变!

函数

函数 是可重用的代码片段,用来完成特定任务。程序员可以并且经常将函数与其他程序员共享,以应对特定的任务,节省自己编写代码的时间和精力。

你可以使用 def 关键字定义一个函数。这里我们定义了两个简单的函数,每个函数都打印一个字符串:

def say_hi():

print('hi!')

def say_hello():

print('hello!')

函数由函数名和括号组成,前面加上关键字 def,后面跟随函数体,它是一个缩进的代码块。这个代码块可以包含其他缩进的块,比如 if 语句,甚至是更多的函数定义。和 Python 中任何的块定义一样,函数体必须缩进。

这是一个打印两个字符串并分别换行的函数:

def say_bye():

print('Time to go…')

print('Bye! ')

像这样在你的 Python shell 中运行这三个函数:

Say_hi()

Say_hello()

Say_bye()

这是输出结果:

hi!

hello!

Time to go…

Bye!

函数按顺序执行。

参数

函数还可以在括号内接受参数。参数 允许你定制输出。考虑这个函数,它以名字作为唯一参数,并向 shell 打印一个定制的字符串:

def say_hi(name):

print('hi ' + name)

say_hi('Alice')

say_hi('Bob')

我们定义了函数,然后用不同的参数分别运行它两次:首先是 'Alice',然后是 'Bob'。因此,函数执行的输出是不同的:

hi Alice

hi Bob

函数还可以接受多个参数:

def say_hi(a, b):

print(a + ' 向 ' + b + ' 打招呼')

say_hi('Alice', 'Bob')

say_hi('Bob', 'Alice')

输出结果如下:

Alice 向 Bob 打招呼

Bob 向 Alice 打招呼

在第一次函数执行时,参数变量 a 的值为 'Alice',参数变量 b 的值为 'Bob'。第二次执行函数时,这两个值会交换,输出结果也不同。

函数也可以有返回值,因此你可以将一个值传入函数,并获得一个返回值,之后可以在代码中使用它,正如在清单 A-1 中所示。

def f(a, b):

return a + b

x = f(2, 2)

y = f(40, 2)

print(x)

print(y)

清单 A-1:使用 return 关键字

返回值是紧跟在 return 关键字后面的表达式,这也会终止函数的执行。Python 在遇到 return 关键字时检查返回值并结束函数,立即将该值返回给函数的调用者。

如果你没有显式提供返回表达式,Python 会隐式地在函数末尾加上表达式 return None。关键字 None 意味着 没有值。其他编程语言(例如 Java)使用关键字 null,这常常会导致初学者误认为它等于整数值 0。实际上,Python 使用关键字 None 来表示它是一个空对象,比如一个空列表或空字符串,而不是数字 0。

当一个函数执行完成时,执行权总是返回给调用该函数的地方;return 关键字只是让你更精确地控制 何时 结束函数和 返回什么 值。

我们将 a=2 和 b=2 传入函数 列表 A-1,得到结果 4。然后我们传入 a=40 和 b=2,并得到(唯一的)答案 42。这是输出结果:

4

42

几乎每个仪表板应用程序都包含至少一个增加交互性的函数。通常,你可能有一个根据用户输入更新图表的函数,类似这样的:

def update_graph(value):

if value == 2:

return 'something'

接下来,我们将讨论 Python 的一个更高级且高度相关的特性:默认函数参数

默认函数参数

默认参数允许你在 Python 中定义带有可选参数的函数。如果用户在调用函数时选择不提供参数,则会使用默认参数。你可以通过在参数名后使用等号 (=) 并附加默认值来设置默认参数。

附录 A-2 展示了一个更有趣的例子。在这里,我们定义了一个函数 add(),它返回函数参数 a 和 b 的和。因此,add(1,2) 将返回 3,add(41,1) 将返回 42。我们为函数参数指定了默认值:a 默认为 0,b 默认为 1。如果在函数调用中未传递任何值给这些参数中的一个或两个,它将使用其默认值。因此,add(1) 将返回 2,add(-1) 将返回 0,add() 将返回 1,因为分别使用了 a 和 b 的默认值 0 和 1。

def add(a=0, b=1):

return a + b

print(add(add(add())))

附录 A-2:定义具有默认参数的函数

这将打印:

3

在我们在 print(add(add(add()))) 中突出显示的最内层函数调用中,我们调用了函数 add(),没有传递任何参数,因此它使用了 a 和 b 的默认值(分别为 0 和 1)。

对于剩余的两个调用,你只需传递一个参数给 add(),该参数是前一个函数调用的返回值。这个参数将接收 a,根据参数的位置来决定将哪个参数传递给哪个变量,而 b 将具有默认值 1。第一个,最内层的 add() 调用返回 1。这个值传递给第二次调用的 add(),然后增加 1,接着这个值在第三次调用时再增加 1。

以下是我们执行 Listing A-2 时发生的幕后步骤:

add(add(add()))

= add(add(1))

= add(2)

= 3

你可以看到,默认参数可以帮助你让函数在处理输入时更加灵活。

Python 资源与进一步阅读

第一部分 快速教程

Dash 基于 Python 构建。虽然我们并不认为你一定是 Python 专家,但我们希望你对 Python 的基本概念和语法有扎实的理解。在这一部分,我们将回顾你在本书中最需要的相关 Python、PyCharm 和 pandas 信息,以帮助你最大限度地发挥这本书的价值。如果你在代码方面遇到困难,我们已经添加了一个“Python 基础”附录,帮助你在继续阅读之前稍微提高一下 Python 技能。如果你需要更全面的介绍,可以随时查看 Finxter 邮件学院提供的详尽 Python 快速教程和备忘单:https://blog.finxter.com/email-academy

如果你对 Python 和 pandas 有信心,并且已经有了自己喜欢的集成开发环境(IDE),可以直接跳到 第四章。如果你觉得自己需要快速复习一下 Python 和 pandas 技能,或者希望设置与本书中使用的相同的 Python IDE,请继续阅读!

第二部分 构建应用程序

第二部分将带你了解四个不同的应用程序,教你如何利用 Dash 中最常见的组件,并介绍 Dash 中最重要的概念。你还将了解来自 Plotly 绘图库的几种流行图表,我们将解释如何可视化各种数据集。你将创建一个分析社交媒体数据的应用,一个实时获取数据的应用,一个探讨资产配置如何影响投资组合回报的应用,以及一个可视化机器学习模型的应用。最后,我们将为你提供一些技巧,帮助你更轻松、更愉快地学习 Dash 并构建自己的应用程序。

posted @ 2025-11-25 17:06  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报