Python-开发者的-Matplotlib-第二版-全-
Python 开发者的 Matplotlib 第二版(全)
原文:
annas-archive.org/md5/390191aec2993c8974efd865f6620359译者:飞龙
前言
Python 是一种通用编程语言,越来越多地被用于数据分析和可视化。Matplotlib 是一个流行的 Python 数据可视化包,用于设计有效的图表和图形。本书是一本实用的资源,帮助你使用 Matplotlib 库在 Python 中进行数据可视化。
本书教你如何使用 Matplotlib 创建吸引人的图表、图形和绘图。你还将快速了解第三方包 Seaborn、pandas、Basemap 和 Geopandas,并学习如何将它们与 Matplotlib 配合使用。之后,你将把图表嵌入并定制到 GTK+、Qt 5 和 WXWIDGETS 等第三方工具中。
通过本书提供的实用示例,你还将能够调整可视化的外观和风格。接下来,你将通过基于云平台的第三方软件包(如 Flask 和 Django)在网上探索 Matplotlib 2.1.x。最后,你将通过实际的世界级示例,将交互式、实时的可视化技术整合到当前的工作流程中。
本书结束时,你将完全掌握流行的 Python 数据可视化库 Matplotlib 2.1.x,并能利用其强大功能构建吸引人、有洞察力且强大的可视化图表。
适用对象
本书适用于任何希望使用 Matplotlib 库创建直观数据可视化的人。如果你是数据科学家或分析师,且希望使用 Python 创建吸引人的可视化图表,你会发现本书非常有用。你只需具备一些 Python 编程基础,就能开始阅读本书。
本书涵盖的内容
第一章,Matplotlib 简介,让你熟悉 Matplotlib 的功能和特性。
第二章,Matplotlib 入门,带你掌握使用 Matplotlib 语法进行基本绘图的技巧。
第三章,使用绘图样式和类型装饰图表,展示了如何美化你的图表,并选择能有效传达数据的合适图表类型。
第四章,高级 Matplotlib,教你如何使用非线性刻度、轴刻度、绘图图像和一些流行的第三方软件包将多个相关的图形组合到一个图形中的子图。
第五章,将 Matplotlib 嵌入 GTK+3 中,展示了如何在使用 GTK+3 的应用程序中嵌入 Matplotlib 的示例。
第六章,将 Matplotlib 嵌入 Qt 5 中,解释了如何将图形嵌入 QWidget,使用布局管理器将图形打包到 QWidget 中,创建计时器,响应事件,并相应地更新 Matplotlib 图表。我们使用 QT Designer 绘制了一个简单的 GUI 来展示 Matplotlib 嵌入。
第七章,在 wxWidgets 中嵌入 Matplotlib,使用 wxPython,展示了如何在 wxWidgets 框架中使用 Matplotlib,特别是使用 wxPython 绑定。
第八章,将 Matplotlib 与 Web 应用程序集成,教你如何开发一个简单的网站,显示比特币的价格。
第九章,Matplotlib 在实际应用中的使用,通过实际案例开始探索更高级的 Matplotlib 用法。
第十章,将数据可视化集成到工作流程中,涵盖了一个结合数据分析技巧与可视化技术的迷你项目。
为了充分利用本书
需要安装 Python 3.4 或更高版本。可以从www.python.org/download/获取默认的 Python 发行版。软件包的安装在各章节中都有介绍,但你也可以参考官方文档页面获取更多细节。建议使用 Windows 7+、macOS 10.10+或 Linux 系统,且电脑内存至少为 4GB。
下载示例代码文件
你可以从你的账户在www.packtpub.com下载本书的示例代码文件。如果你是从其他地方购买的本书,可以访问www.packtpub.com/support并注册,文件将直接发送到你的邮箱。
你可以通过以下步骤下载代码文件:
-
登录或注册账户,访问www.packtpub.com。
-
选择 SUPPORT 标签。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
文件下载后,请确保使用最新版的解压或提取工具解压文件夹:
-
WinRAR/7-Zip 适用于 Windows
-
Zipeg/iZip/UnRarX 适用于 Mac
-
7-Zip/PeaZip 适用于 Linux
本书的代码包也托管在 GitHub 上,访问地址为github.com/PacktPublishing/Matplotlib-for-Python-Developers-Second-Edition/。如果代码有更新,它将被更新到现有的 GitHub 仓库。
我们还提供了其他代码包,来自我们丰富的书籍和视频目录,访问地址为github.com/PacktPublishing/。快来看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书使用的截图/图表的彩色图片。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/MatplotlibforPythonDevelopersSecondEdition_ColorImages.pdf。
使用的约定
本书中使用了一些文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个例子:“另一个调节参数是dash_capstyle。”
一段代码块的格式如下:
import matplotlib.pyplot as plt
plt.figure(figsize=(4,4))
x = [0.1,0.3]
plt.pie(x)
plt.show()
当我们希望您特别注意某个代码块的部分时,相关的行或项目会以粗体显示:
self.SetSize((500, 550))
self.button_1 = wx.Button(self, wx.ID_ANY, "button_1")
##Code being added***
self.Bind(wx.EVT_BUTTON, self.__updat_fun, self.button_1)
#Setting up the figure, canvas and axes
任何命令行输入或输出都以以下方式书写:
python3 first_gtk_example.py
粗体:表示新术语、重要词汇或屏幕上出现的文字。例如,菜单或对话框中的词汇在文本中呈现如下。这里有一个例子:“在文件和类中选择 Qt,在中间面板选择 Qt Designer 表单。”
警告或重要说明以这种方式呈现。
提示和技巧呈现如下。
获取联系
我们始终欢迎读者的反馈。
一般反馈:通过电子邮件联系feedback@packtpub.com并在邮件主题中提及书名。如果您对本书的任何部分有疑问,请通过questions@packtpub.com与我们联系。
勘误:尽管我们已经尽力确保内容的准确性,但错误总会发生。如果您发现本书中的错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接并输入详细信息。
盗版:如果您在互联网上发现我们的作品的任何非法复制品,无论形式如何,我们将非常感激您提供位置地址或网站名称。请通过copyright@packtpub.com与我们联系,并附上该材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专长并且有意写书或为书籍贡献内容,请访问authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,为什么不在购买该书的站点上留下评论呢?潜在读者可以看到并利用您的公正意见做出购买决策,我们 Packt 也能了解您对我们产品的看法,我们的作者能看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packtpub.com。
Matplotlib 简介
“一幅画胜过千言万语。” - 弗雷德·R·巴纳德
欢迎加入创造优秀数据可视化的旅程。在这个大数据爆炸的时代,我们可能都清楚数据分析的重要性。开发者们渴望参与数据挖掘的游戏,并构建工具来收集和建模各种数据。即使是非数据分析师,像性能测试结果和用户反馈等信息,也往往在改善正在开发的软件中至关重要。虽然强大的统计技能无疑为成功的软件开发和数据分析奠定了基础,但即使是最好的数据处理结果,好的故事叙述也是至关重要的。图形数据表现的质量往往决定了你能否在探索性数据分析过程中提取出有用的信息,并在演示中传达出核心信息。
Matplotlib 是一个多功能且强大的 Python 绘图库;它提供了简洁易用的方式来生成各种高质量的数据图形,并且在定制化方面提供了巨大的灵活性。
在本章中,我们将介绍 Matplotlib,内容包括:它的功能、为什么你要使用它以及如何开始使用。我们将覆盖以下主题:
-
什么是 Matplotlib?
-
Matplotlib 的优点
-
Matplotlib 有哪些新特性?
-
Matplotlib 网站和在线文档。
-
输出格式和后端。
-
配置 Matplotlib。
第一章:什么是 Matplotlib?
Matplotlib 是一个用于数据可视化的 Python 包。它允许轻松创建各种图形,包括线形图、散点图、柱状图、箱线图和径向图,并具有高度的灵活性,支持精细的样式定制和注释。多功能的 artist 模块允许开发者定义几乎任何类型的可视化。对于常规使用,Matplotlib 提供了一个简洁的面向对象接口——pyplot 模块,用于简单绘图。
除了生成静态图形,Matplotlib 还支持交互式接口,不仅有助于创建各种各样的图表,还非常适合用于创建基于 Web 的应用程序。
Matplotlib 可以方便地与流行的开发环境(如 Jupyter Notebook)集成,并且支持许多更高级的数据可视化包。
Matplotlib 的优点
使用代码创建数据可视化有许多优势,因为可视化流程能够顺利融入结果生成流程的一部分。让我们来看看 Matplotlib 库的一些关键优点。
易于使用
Matplotlib 绘图库有多种易用方式:
-
首先,面向对象的模块结构简化了绘图过程。通常情况下,我们只需要调用
import matplotlib.pyplot as plt来导入绘图 API,从而创建并自定义许多基本图形。 -
Matplotlib 与两个常见的数据分析包——pandas 和 NumPy——高度集成。例如,我们可以简单地将
.plot()附加到 pandas DataFrame 上,例如通过df.plot()创建一个简单的图表,并使用 Matplotlib 语法自定义其样式。 -
在样式方面,Matplotlib 提供了可以修改每个特征外观的函数,并且也有现成的默认样式表,以避免在不需要精细美学的情况下进行这些额外步骤。
多样的图形类型
在数据分析中,我们常常需要复杂的图形来表达数据。Matplotlib 本身提供了许多绘图 API,并且还是一系列第三方包的基础,这些包提供了额外的功能,包括:
-
Seaborn:提供简单的绘图 API,包括一些高级图形类型,具有美观的默认样式
-
HoloViews:根据捆绑数据创建带有元数据注释的交互式图形
-
Basemap/GeoPandas/Canopy:将数据值映射到地理地图上的颜色
我们将在后续章节中学习这些第三方包在高级绘图中的一些应用。
核心可操作(仅在需要时)
当我们想要超越默认设置,确保生成的图形满足我们特定的需求时,可以自定义每个图形特征的外观和行为:
-
可以进行逐元素样式定制
-
将数据值绘制为颜色并绘制任何形状的补丁的能力,使得几乎可以创建任何类型的可视化
-
在自定义由 Seaborn 等扩展创建的图形时非常有用
开源与社区支持
由于 Matplotlib 是开源的,它使得开发者和数据分析师可以免费使用它。用户还可以自由地改进和贡献 Matplotlib 库。作为开源体验的一部分,用户可以在各种平台和论坛上从全球社区成员那里获得及时的在线支持。
Matplotlib 2.x 中的新特性?
Matplotlib 从 1.2 版本(发布于 2013 年)开始支持 Python 3。Matplotlib 2.0 版本引入了许多变化和升级,以改善数据可视化项目的效果。让我们来看一下其中的一些关键改进和升级。
改进的功能和性能
Matplotlib 2.0 提供了新的功能,改善了用户体验,包括速度、输出质量和资源使用。
改进的颜色转换 API 和 RGBA 支持
Matplotlib 2.0 完全支持指定透明度级别的 alpha 通道。
改进的图像支持
Matplotlib 2.0 现在使用更少的内存和数据类型转换来重新采样图像。
更快的文本渲染
社区开发者声称,Agg 后端的文本渲染速度提高了 20%。
默认动画编解码器的更改
现在默认使用一个非常高效的编解码器 H.264,替代了 MPEG-4,用于为动画图表生成视频输出。借助 H.264,我们现在可以实现更长的录像时间,更少的数据流量和加载时间,这得益于更高的压缩率和更小的输出文件大小。还注意到,H.264 视频的实时播放优于那些采用 MPEG-4 编码的视频。
默认样式的变化
有许多样式上的变化,旨在改善可视化效果,例如默认的颜色更加直观。我们将在图表美学章节中详细讨论这些变化。
有关所有 Matplotlib 更新的详细信息,请访问 matplotlib.org/devdocs/users/whats_new.html。
Matplotlib 网站和在线文档
作为开发者,你可能已经认识到阅读文档和手册以熟悉语法和功能的重要性。我们想再次强调阅读库文档的重要性,并鼓励你也这样做。你可以在这里找到文档:matplotlib.org。在官方的 Matplotlib 网站上,你可以找到每个函数的文档,最新版本的新闻和正在进行的开发,以及第三方包的列表,还有教程和示例图表的图库。
然而,通过从头开始阅读文档来构建高级和复杂的图表意味着更陡峭的学习曲线,并且会花费更多的时间,尤其是在文档不断更新以便更好地理解的情况下。本书旨在为读者提供一份引导式的路线图,以加速学习过程,节省时间和精力,并将理论付诸实践。在线手册可以作为你随时查阅的地图,帮助你进一步探索。
Matplotlib 的源代码可在 GitHub 上找到,网址为github.com/matplotlib/matplotlib。我们鼓励读者将其分叉并加入自己的创意!
输出格式和后端
Matplotlib 使用户能够将输出图表获取为静态图像。通过交互式后端,图表也可以被管道化并变得响应式。
静态输出格式
静态图像是报告和演示中最常用的输出格式,也是我们快速检查数据的常见方式。静态图像可以分为两类。
栅格图像
Raster 是经典的图像格式,支持多种图像文件,包括 PNG、JPG 和 BMP。每个栅格图像可以视为一个密集的颜色值数组。对于栅格图像,分辨率非常重要。
图像细节的保留量通过每英寸点数(DPI)来衡量。DPI 值越高(即保留的像素点越多),即使图像被拉伸到更大尺寸,图像也会更加清晰。当然,相应的文件大小和渲染所需的计算资源也会增加。
向量图像
对于向量图像,信息不是以离散的颜色点矩阵的形式保存,而是作为路径保存,路径是连接点的线条。它们可以无失真地缩放:
-
SVG
-
PDF
-
PS
设置 Matplotlib
现在我们已经全面了解了 Matplotlib 的功能和特性,接下来我们可以开始实际操作,完成一些示例。在开始之前,我们需要确保已经设置好 Matplotlib 环境。请按照之前讨论的步骤来设置环境。
安装 Python
从 2.0 版本开始,Matplotlib 支持 Python 2.7 和 3.4+。本书使用的是 Python 3,这是最新的稳定版 Python。你可以从www.python.org/download/下载 Python。
Windows 上的 Python 安装
Python 在 Windows 上提供安装程序或压缩的源代码。我们推荐使用可执行安装程序。选择适合你计算机架构的版本,以获得最佳性能。你可以通过按下 Windows + R 键并输入cmd.exe来调用 Python,如下图所示:

macOS 上的 Python 安装
macOS 默认自带 Python 2.7。若要安装 Python 3.4+,请下载安装向导并按照指示进行安装。以下是向导第一步的截图:

某些 Python 包需要 Xcode 命令行工具才能正确编译。Xcode 可以从 Mac App Store 获得。要安装命令行工具,请在终端中输入以下命令:xcode-select --install,然后按照提示进行安装。
Linux 上的 Python 安装
大多数 Linux 发行版预装了 Python 3.4。您可以通过在终端中输入 python3 来确认。如果看到以下内容,则表示已安装 Python 3.4:
Python 3.6.3 (default, Oct 6 2017, 08:44:35) [GCC 5.4.0 20160609] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
如果命令行中没有出现 Python shell,您可以通过 apt,即 Linux 软件管理工具,安装 Python 3:
sudo apt update
sudo apt install Python3 build-essential
build-essential 包包含用于构建非纯 Python 包的编译器。同时,如果您使用的是 Ubuntu 14.04 或更早版本,可能需要将 apt 替换为 apt-get。
安装 Matplotlib
Matplotlib 需要大量的依赖项。我们建议通过 Python 包管理器安装 Matplotlib,这将帮助您在每次安装或升级包时自动解决并安装依赖项。我们将演示如何使用pip安装 Matplotlib。
关于依赖项
Matplotlib 依赖许多 Python 包进行后台计算、图形渲染、交互等。它们包括 NumPy、libpng 和 FreeType 等。根据使用情况,用户可以安装额外的后台包,例如 PyQt5,以获得更好的用户界面。
安装 pip Python 包管理器
我们建议使用 Python 包管理器 pip 安装 Matplotlib;它会自动解析基础依赖项。pip 已与 Python 2 >= 2.7.9 或 Python 3 >= 3.4 二进制文件一起安装。
如果没有安装 pip,您可以通过下载 get-pip.py 文件(bootstrap.pypa.io/get-pip.py),并在控制台中运行它进行安装:
python3 get-pip.py
要将 pip 升级到最新版本,请执行以下操作:
pip3 install --upgrade pip
pip 的文档可以在 pip.pypa.io 找到。
使用 pip 安装 Matplotlib
在终端/命令提示符中输入 python3 -m pip install matplotlib 进行安装。如果是没有 root/admin 权限的用户,安装时请根据需要添加 --user 选项。
设置 Jupyter Notebook
为了创建我们的图表,我们需要一个用户友好的开发环境。
Jupyter Notebook 提供了一个交互式编码环境,您可以编辑和运行代码,显示结果,并整洁地记录它们。数据和方法可以加载到内存中,在会话内重复使用。由于每个 Notebook 都作为 Web 服务器托管,您可以在浏览器中连接到远程服务器上运行的 Notebook 实例。
如果您迫不及待想在安装前试用,您可以访问 try.jupyter.org 并打开一个 Python 3 的 Notebook。
要安装 Jupyter,请在控制台中输入以下命令:
python3 -m pip install jupyter
启动 Jupyter Notebook 会话
只需在控制台中输入 jupyter notebook。这将作为 Web 服务器启动 Jupyter Notebook 会话。
默认情况下,Notebook 会在您的默认浏览器中弹出。如果需要手动打开页面,请在浏览器中输入 localhost:8888 作为网址。然后,您将进入 Jupyter Notebook 的首页:

你可以选择将笔记本托管在不同的端口上,例如当你运行多个笔记本时。你可以使用--port=<自定义端口号>选项来指定使用的端口。
自 4.3 版本发布以来,Jupyter 已增加了令牌身份验证,因此在进入笔记本主页之前,你可能会被要求提供令牌密码,如下图所示:

要获取令牌,例如当你从其他浏览器或机器访问正在运行的笔记本时,你可以从控制台调用jupyter notebook list:

在远程服务器上运行 Jupyter Notebook
要打开运行在远程服务器上的笔记本,你可以在 SSH 时设置端口转发,方法如下:
ssh –L 8888:localhost:8888 mary@remoteserver
然后你可以再次使用 localhost:8888 作为 URL 打开笔记本。
当多个用户在同一服务器的同一端口(比如默认的8888端口)上运行 Jupyter Notebooks,并且每个用户都使用相同的端口转发时,可能会出现将你的笔记本内容转发到其他用户的情况,而他们无法查看自己的内容,除非更改端口。尽管这个问题可能会在后续版本中修复,但建议更改默认端口。
要从先前版本升级,请运行以下命令:
pip3 install --upgrade matplotlib
pip将自动为你收集并安装 Matplotlib 的依赖。
编辑并运行代码
一个 Jupyter Notebook 包含称为 单元格 的框。默认情况下,它以代码编辑的文本输入区开始,称为灰色框单元格。要插入和编辑代码,请按以下步骤操作:
-
点击灰色框内。
-
在其中输入你的 Python 代码。
-
点击播放按钮或按 Shift + Enter 运行当前单元格并将光标移动到下一个单元格:

一旦执行一个单元格,相关的数据和方法将被加载到内存中,并且可以在同一笔记本内核的不同单元格间使用。除非有特定更改,否则无需重新加载。这节省了调试和重新加载大型数据集的时间和精力。
操作笔记本内核和单元格
你可以使用顶部的工具栏来操作单元格和内核。可用的 功能 如下所示:

在运行单元格之前验证输出量!巨大的输出流通常不会导致控制台崩溃,但它很容易在几秒钟内使你的浏览器和笔记本崩溃。自 Jupyter 4.2 以来,已通过停止大量输出来解决这个问题。然而,它并不能保证捕捉所有不停的输出。因此,建议读者保持谨慎,避免尝试在单元格中获取大量输出结果。考虑将其分片查看,或将输出保存到其他文件中:

嵌入你的 Matplotlib 图表
Matplotlib 与 Jupyter Notebook 高度集成。使用 Jupyter 内置的 magic 命令 %matplotlib inline(在当前版本中默认设置)将结果图表显示为每个单元的静态图像输出:

或者,你可以运行一个魔法单元命令—%matplotlib notebook 来使用交互式 Matplotlib 图形用户界面,以便在相同的输出区域进行缩放或旋转:

使用 Markdown 进行文档编写
Jupyter Notebook 支持 Markdown 语法来组织文档:
-
从工具栏中的下拉列表中选择 Markdown。
-
在灰色输入框中写下你的笔记。
-
点击运行或 Shift + Enter:

运行单元后,文本将在显示中以样式呈现:

你可以在 github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet 上找到 Adam Pritchard 提供的详细 Markdown 备忘单。
保存你的辛勤工作!
Jupyter Notebook 每 2 分钟自动保存一次。作为良好的实践,你应该通过点击工具栏上的软盘图标,或更方便地使用 Ctrl + S 更频繁地保存它。
你在 Jupyter 上打开的每个项目都会以基于 JSON 的 .ipynb 笔记本格式保存:

.ipynb 笔记本可以跨不同的 Jupyter 服务器移植。笔记本可以导出为基本可运行的 Python 脚本 .py、用于文档的 Markdown .md,以及网页格式 .html,便于项目流程的即时展示,而无需读者提前安装 Jupyter Notebook。它还支持 LaTex 格式和通过安装依赖项 Pandoc 进行 PDF 转换。如果你感兴趣,可以查看安装说明:pandoc.org/installing.html。
总结
哇哦!我们已经迈出了在 Matplotlib 旅程中的第一步。你可以放心地知道,你已经全面了解了 Matplotlib 的功能,并且已经设置好了必要的环境。现在,我们已经成功涉足数据可视化并打下了基础,让我们继续构建我们的第一个图表吧!
第二章:开始使用 Matplotlib
现在我们已经熟悉了 Matplotlib 的功能,并且已经配置好 Python 环境,让我们直接开始创建我们的第一个图表。
在本章中,我们将学习如何:
-
绘制基本的线图和散点图
-
在同一图表上叠加多个数据系列
-
调整网格、坐标轴和标签
-
添加标题和图例
-
将创建的图表保存为单独的文件
-
配置 Matplotlib 全局设置
加载数据
在我们开始绘图之前,需要导入我们打算绘制的数据,并熟悉 Matplotlib 中的基本绘图命令。让我们开始了解这些基本命令!
在进行数据可视化项目时,我们需要确保对用于数据处理的工具有基本的熟悉和理解。在开始之前,让我们简要回顾一下处理数据时您会遇到的最常见数据结构。
列表
这是最基本的 Python 数据结构,它存储一组值。虽然您可以将任何数据类型作为元素存储在 Python 列表中,但在数据可视化的目的下,我们主要处理数值类型的列表作为数据输入,或者最多是具有相同数据类型元素的列表,如字符串,用于存储文本标签。
列表由方括号[]指定。要初始化一个空列表,可以通过l = []将[]赋值给变量。要创建一个列表,我们可以写如下内容:
fibonacci = [1,1,2,3,5,8,13]
有时,我们可能希望得到一个算术序列的列表。我们可以通过使用list(range(start, stop, step))来实现。
请参见以下示例:
In [1]: fifths = list(range(10,30,5))
fifths
Out[1]: [10, 15, 20, 25]
In [2]: list(range(10,30,5))==[10, 15, 20, 25]
Out[2]: True
与 Python 2.7 不同,在 Python 3.x 中,您不能将range()对象与列表互换使用。
NumPy 数组
NumPy 允许创建 n 维数组,这也是数据类型numpy.ndarray名称的来源。它处理许多复杂的科学和矩阵运算,并提供许多线性代数和随机数功能。
NumPy 是许多计算的核心,这些计算在数学上支持 Matplotlib 和许多其他 Python 包。因此,它是许多常用包的依赖项,并且通常与 Python 发行版一起提供。例如,它为 SciPy 提供了基础数据结构,SciPy 是一个处理统计计算的包,这些计算对科学和许多其他领域都有用。
要导入 NumPy,输入以下内容:
import numpy as np
要从列表创建 NumPy 数组,请使用以下内容:
x = np.array([2,3,1,0])
您还可以通过使用np.linspace(start, stop, number)来使用 NumPy 创建非整数的算术序列。
请参见以下示例:
In [1]: np.linspace(3,5,20)
Out[1]: array([ 3\. , 3.10526316, 3.21052632, 3.31578947, 3.42105263,
3.52631579, 3.63157895, 3.73684211, 3.84210526, 3.94736842,
4.05263158, 4.15789474, 4.26315789, 4.36842105, 4.47368421,
4.57894737, 4.68421053, 4.78947368, 4.89473684, 5\. ])
矩阵运算可以应用于 NumPy 数组。这里是一个乘以两个数组的例子:
In [2]: a = np.array([1, 2, 1])
In [3]: b = np.array([2, 3, 8])
In [4]: a*b
Out[4]: array([2, 6, 8])
pandas DataFrame
您可能经常看到df出现在基于 Python 的数据科学资源和文献中。这是表示 pandas DataFrame 结构的常规方式。pandas 使我们能够通过简单的命令执行本来繁琐的表格(数据框)操作,例如dropna()、merge()、pivot()和set_index()。
pandas 旨在简化常见数据类型(如时间序列)的处理过程。虽然 NumPy 更专注于数学计算,但 pandas 具有内建的字符串处理功能,并允许通过apply()函数将自定义函数应用于每个单元格。
使用前,我们通过以下传统缩写导入该模块:
pd.DataFrame(my_list_or_array)
要从现有文件读取数据,只需使用以下命令:
pd.read_csv()
对于制表符分隔的文件,只需将 '\t' 作为分隔符:
pd.read_csv(sep='\t')
pandas 支持从多种常见文件结构导入数据,以便进行数据处理和处理,从pd.read_xlsx()导入 Excel 文件,pd.read_sql_query()导入 SQL 数据库,直到最近流行的 JSON、HDF5 和 Google BigQuery。
pandas 提供了一系列便捷的数据操作方法,是 Python 数据科学家或开发者工具箱中不可或缺的工具。
我们鼓励读者在我们的 Mapt 平台上寻求资源和书籍,以更好、更深入地了解 pandas 库的使用。
要完全理解和利用功能,您可能想要阅读更多来自官方文档的内容:
pandas.pydata.org/pandas-docs/stable/
我们的第一个 Matplotlib 绘图
我们刚刚回顾了使用 Python 进行数据处理的基本方法。接下来,让我们创建我们的第一个 "Hello World!" 绘图示例。
导入 pyplot
要从对象(如列表和 ndarray)创建 pandas DataFrame,您可以调用:
import pandas as pd
要开始创建 Matplotlib 图形,我们首先通过输入此命令导入绘图 API pyplot:
import matplotlib.pyplot as plt
这将启动你的绘图例程。
在 Jupyter Notebook 中,启动内核后,一旦开始 notebook 会话,您需要导入模块。
线形图
在导入 matplotlib.pyplot 作为 plt 后,我们使用 plt.plot() 命令绘制线形图。
这是一个简单的代码片段,用于绘制一周温度的示例:
# Import the Matplotlib module
import matplotlib.pyplot as plt
# Use a list to store the daily temperature
t = [22.2,22.3,22.5,21.8,22.5,23.4,22.8]
# Plot the daily temperature t as a line plot
plt.plot(t)
# Show the plot
plt.show()
运行代码后,以下图表将作为输出显示在 notebook 单元格中:

当解析一个参数时,数据值将假定在 y 轴上,索引在 x 轴上。
记得每次绘图后调用 plt.show()。如果忘记此操作,绘图对象将作为输出显示,而不是图形。如果你没有通过其他绘图命令覆盖图形,你可以在下一个运行的单元格中调用 plt.show() 来显示图形。以下是为说明此情况而制作的截图:

此外,如果你在调用plt.show()之前多次运行绘图命令,下次你再次添加这行代码并运行时,输出区域会出现多个图形或带有意外元素的图形(例如,颜色变化)。我们可以通过在两个连续运行的单元格中复制相同的绘图命令来演示这一点。以下截图中,你会看到颜色从默认的蓝色(如之前所示)变为棕色。这是因为第一个命令绘制的蓝色线被第二个命令绘制的棕色线覆盖:

如果出现这种情况,请不要惊慌。你可以重新运行单元格,达到预期的结果。
抑制函数输出:有时,图表可能会在没有调用plt.show()的情况下显示,但matplotlib对象的输出也会显示,并且没有提供有用的信息。我们可以在代码行的末尾加上分号(;)来抑制其输入。例如,在以下快速示例中,当我们在绘图命令后加上;时,我们不会在输出中看到 Matplotlib 对象[<matplotlib.lines.Line2D at 0x7f6dc6afe2e8>]:

要指定自定义的x轴,只需将其作为第一个参数传递给plt.plot()。假设我们绘制 11^(th)日期的温度。我们可以通过调用plt.plot(d, t)来绘制温度t与日期列表d之间的关系。这里是结果,你可以在x轴上观察到指定的日期:

散点图
另一种基本的图表类型是散点图,即点图。你可以通过调用plt.scatter(x, y)来绘制它。以下示例显示了一个随机点的散点图:
import numpy as np
import matplotlib.pyplot as plt
# Set the random seed for NumPy function to keep the results reproducible
np.random.seed(42)
# Generate a 2 by 100 NumPy Array of random decimals between 0 and 1
r = np.random.rand(2,100)
# Plot the x and y coordinates of the random dots on a scatter plot
plt.scatter(r[0],r[1])
# Show the plot
plt.show()
以下图表是前述代码的结果:

在图表中叠加多个数据系列
我们可以在使用plt.show()结束绘图之前堆叠多个绘图命令,以创建一个包含多个数据系列的图表。每个数据系列可以使用相同或不同的绘图类型进行绘制。以下是包含多个数据系列的折线图和散点图的示例,以及结合这两种图类型显示趋势的例子。
多行图表
例如,要创建一个多行图表,我们可以为每个数据系列绘制一条折线图,然后再结束图表。让我们尝试使用以下代码绘制三座不同城市的温度:
import matplotlib.pyplot as plt
# Prepare the data series
d = [11,12,13,14,15,16,17]
t0 = [15.3,15.4,12.6,12.7,13.2,12.3,11.4]
t1 = [26.1,26.2,24.3,25.1,26.7,27.8,26.9]
t2 = [22.3,20.6,19.8,21.6,21.3,19.4,21.4]
# Plot the lines for each data series
plt.plot(d,t0)
plt.plot(d,t1)
plt.plot(d,t2)
plt.show()
这是前面代码生成的图表:

这个例子改编自 2017 年 12 月三座城市一周内的最高气温。从图表中,你能辨认出哪两条线更有可能代表来自同一大洲的城市吗?
用散点图显示聚类
虽然我们之前已经看到过随机点的散点图,但散点图在表示显示趋势或聚类的离散数据点时最为有用。默认情况下,每个数据系列将在每个绘图命令中以不同的颜色绘制,这有助于我们区分每个系列中的不同点。为了演示这一概念,我们将使用 NumPy 中的一个简单随机数生成函数生成两个人工数据点聚类,如下所示:
import matplotlib.pyplot as plt
# seed the random number generator to keep results reproducible
np.random.seed(123)
# Generate 10 random numbers around 2 as x-coordinates of the first data series
x0 = np.random.rand(10)+1.5
# Generate the y-coordinates another data series similarly
np.random.seed(321)
y0 = np.random.rand(10)+2
np.random.seed(456)
x1 = np.random.rand(10)+2.5
np.random.seed(789)
y1 = np.random.rand(10)+2
plt.scatter(x0,y0)
plt.scatter(x1,y1)
plt.show()
从以下图表中,我们可以看到两个人工创建的数据点聚类,分别用蓝色(大致位于左半部分)和橙色(大致位于右半部分)表示:

还有另一种生成聚类并在散点图中展示它们的方法。我们可以使用名为sklearn的包中的make_blobs()函数,更直接地生成测试和演示所需的数据点聚类,该包是为更高级的数据分析和数据挖掘而开发的,如下所示的代码片段所示。我们可以根据指定的特征(聚类标识)来指定颜色:
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
# make blobs with 3 centers with random seed of 23
blob_coords,features = make_blobs(centers=3, random_state=23)
# plot the blobs, with c value set to map colors to features
plt.scatter(blob_coords[:, 0], blob_coords[:, 1], marker='x', c=features)
plt.show()
由于make_blob函数是基于各向同性高斯分布生成点,因此我们可以从结果图中看到,数据点更好地聚集成三个独立的点群,分别集中在三个点上:

scikit-learn 是一个强大的 Python 包,提供了许多用于数据挖掘和数据分析的简单函数。它有一套多功能的算法,适用于分类、回归、聚类、降维和建模。它还允许数据预处理并支持多个处理阶段的流水线操作。
为了熟悉 scikit-learn 库,我们可以使用包中预加载的数据集,如著名的鸢尾花数据集,或者生成符合指定分布的数据集,如前所示。这里我们只使用这些数据来演示如何使用散点图进行简单的可视化,暂时不涉及更多细节。更多示例可通过点击以下链接找到:
scikit-learn.org/stable/auto_examples/datasets/plot_random_dataset.html
前面的示例演示了一种更简单的方法来映射点的颜色与标记特征(如果有的话)。make_blobs()和其他 scikit-learn 函数的细节超出了本章介绍基本图表的范围。
我们鼓励读者访问我们的 Mapt 平台,查阅有关 scikit-learn 库使用的资源和书籍,以便更好地理解该库的用法。
或者,读者也可以在此处阅读 scikit-learn 的文档:scikit-learn.org。
在散点图上添加趋势线
多种图表类型可以叠加在一起。例如,我们可以在散点图上添加趋势线。以下是一个将趋势线添加到 10 个y坐标上的例子,这些坐标与x坐标之间存在微小的线性偏差:
import numpy as np
import matplotlib.pyplot as plt
# Generate th
np.random.seed(100)
x = list(range(10))
y = x+np.random.rand(10)-0.5
# Calculate the slope and y-intercept of the trendline
fit = np.polyfit(x,y,1)
# Add the trendline
yfit = [n*fit[0] for n in x]+fit[1]
plt.scatter(x,y)
plt.plot(yfit,'black')
plt.show()
我们可以从下图中观察到,趋势线覆盖了向上倾斜的点:

调整坐标轴、网格、标签、标题和图例
我们刚刚学会了如何通过 Matplotlib 将数值转化为点和线。默认情况下,Matplotlib 会通过后台计算合理的坐标轴范围和字体大小来优化显示。然而,良好的可视化通常需要更多的设计输入,以适应我们的自定义数据可视化需求和目的。此外,很多情况下,需要文本标签来使图表更加信息化。在接下来的章节中,我们将展示如何调整这些元素。
调整坐标轴范围
尽管 Matplotlib 会自动选择 x 和 y 坐标轴的范围,将数据扩展到整个绘图区域,但有时我们可能需要进行一些调整,比如希望将 100%显示为最大值,而不是较低的某个数值。要设置 x 和 y 坐标轴的范围,我们可以使用plt.xlim()和plt.ylim()命令。在我们的日常温度示例中,自动缩放使得温度变化不到 2 度的情况看起来非常剧烈。以下是如何进行调整,比如仅显示前 5 天的温度数据,y 轴的下限设为 0 度:
import matplotlib.pyplot as plt
d = [11,12,13,14,15,16,17]
t0 = [15.3,12.6,12.7,13.2,12.3,11.4,12.8]
t1 = [26.1,26.2,24.3,25.1,26.7,27.8,26.9]
t2 = [22.3,20.6,19.8,21.6,21.3,19.4,21.4]
plt.plot(d,t0)
plt.plot(d,t1)
plt.plot(d,t2)
# Set the limit for each axis
plt.xlim(11,15)
plt.ylim(0,30)
plt.show()
上述代码生成的图表,y 轴范围为 0 到 30,如下所示:

添加坐标轴标签
为了使 x 轴和 y 轴上的数值有意义,我们需要关于数据性质和类型的信息,以及其对应的单位。我们可以通过在plt.xlabel()或plt.ylabel()中添加坐标轴标签来提供这些信息。
让我们继续以多个城市的温度图作为例子。我们将添加plt.xlabel('温度 (°C)')和plt.ylabel('日期')来标注坐标轴,从而生成如下图表:

与许多其他涉及文本的 Matplotlib 函数类似,我们可以在plt.xlabel()和plt.ylabel()函数中通过传递属性参数来设置文本属性,例如字体大小和颜色。在这里,我们为标签指定了较粗的字体权重,以实现一定的层次感:
plt.xlabel('Date',size=12,fontweight='semibold')
plt.ylabel('Temperature (°C)',size=12,fontweight='semibold')
如你所见,Matplotlib 支持对许多文本元素进行内联字体调整。在这里,我们为标签指定了较粗的字体权重,以实现一定的层次感:

添加网格
虽然空白图表背景显得简洁,但有时我们可能希望添加一些参考网格线,以便更好地参考,特别是在多行图表中。
我们可以在 plt.show() 之前调用 plt.grid(True) 来打开背景网格线。例如,我们可以将此命令添加到前述的多城市温度图中,得到以下图表:

同样地,当我们不再需要网格时,例如当使用具有网格线作为默认样式时,我们可以使用 plt.grid(False) 来移除网格。
在下一章节将详细讨论详细的样式选项。前述示例中的网格显得太过突出,干扰了线图的解释。网格线的属性,如线宽、颜色和虚线模式,在 plt.grid() 命令中是可以调整的;这里是使网格线更加柔和的简要示例:
plt.grid(True,linewidth=0.5,color='#aaaaaa',linestyle='-')
如下图所示,与上一个示例中默认网格颜色相比,网格线变得更加淡化,不再干扰数据线:

标题和图例
根据我们的图表将被呈现的位置和方式,它们可能会或者不会伴随一个描述背景和结果的图表标题。我们可能需要添加一个标题来简洁地总结和传达结果。
同时,虽然轴标签足以识别柱状图和箱线图等某些图形类型的数据系列,但可能会有需要额外图例键的情况。以下是添加和调整这些文本元素的方法,以使我们的图表更具信息性。
添加标题
要描述绘制数据的信息,我们可以为我们的图表添加一个标题。这可以通过简单的命令 plt.title(yourtitle) 来完成:
plt.title("Daily temperature of 3 cities in the second week of December")
同样,我们可以指定文本样式属性。在这里,我们将标题字体设置为比其他标签更大:
plt.title("Daily temperature of 3 cities in the second week of December", size=14, fontweight='bold')
下图已添加标题:

添加图例
为了匹配图表上的数据系列及其标签,例如通过它们的线条样式和标记样式,我们添加如下内容:
plt.legend()
每个数据系列的标签可以在每个 plt.plot() 命令中通过 label 参数指定。
默认情况下,Matplotlib 选择最佳位置以最小化与数据点的重叠,并在重叠时添加图例面板颜色的透明度。然而,这并不总能保证每种情况下的位置都是理想的。要调整位置,我们可以通过传递 loc 设置来实现,例如使用 plt.legend(loc='upper left')。
可能的 loc 设置如下:
-
'best': 0(仅适用于轴的图例) -
'upper right': 1 -
'upper left': 2 -
'lower left': 3 -
'lower right': 4 -
'right': 5(与 'center right' 相同;为了向后兼容性) -
'center left': 6 -
'center right': 7 -
'lower center': 8 -
'upper center': 9 -
'center': 10
你还可以将loc设置为相对于父元素的归一化坐标,通常是坐标轴区域;也就是说,坐标轴的边缘位于 0 和 1 的位置。例如,plt.legend(loc=(0.5,0.5))将图例设置在正中间。
让我们尝试将图例设置到多线图的右下角,使用绝对坐标plt.legend(loc=(0.64,0.1)),如下所示:

完整示例
为了更好地熟悉 Matplotlib 函数,让我们绘制一个包含坐标轴、标签、标题和图例的多线图,并在一个简单的代码段中完成配置。
在这个例子中,我们使用世界银行的农业真实数据。随着全球人口的不断增长,粮食安全继续成为一个重要的全球性问题。让我们通过绘制以下代码的多线图来看看最近十年几种主要作物的生产数据:
Data source: https://data.oecd.org/agroutput/crop-production.htm
OECD (2017), Crop production (indicator). doi: 10.1787/49a4e677-en (Accessed on 25 December 2017)
# Import relevant modules
import pandas as pd
import matplotlib.pyplot as plt
# Import dataset
crop_prod = pd.read_csv('OECD-THND_TONNES.txt',delimiter='\t')
years = crop_prod[crop_prod['Crop']=='SOYBEAN']['Year']
rice = crop_prod[crop_prod['Crop']=='RICE']['Value']
wheat = crop_prod[crop_prod['Crop']=='WHEAT']['Value']
maize = crop_prod[crop_prod['Crop']=='MAIZE']['Value']
soybean = crop_prod[crop_prod['Crop']=='SOYBEAN']['Value']
# Plot the data series
plt.plot(years, rice, label='Rice')
plt.plot(years, wheat, label='Wheat')
plt.plot(years, maize, label='Maize')
plt.plot(years, soybean, label='Soybean')
# Label the x- and y-axes
plt.xlabel('Year',size=12,fontweight='semibold')
plt.ylabel('Thousand tonnes',size=12,fontweight='semibold')
# Add the title and legend
plt.title('Total OECD crop production in 1995-2016', size=14, fontweight='semibold')
plt.legend()
# Show the figure
plt.show()
从结果图中,我们可以观察到玉米 > 小麦 > 大豆 > 水稻的生产趋势,一般呈现作物产量的增长趋势,以及大豆产量的稳定增长:

将图形保存为文件
要保存图形,我们将在绘图命令的末尾使用plt.savefig(outputpath)。它可以替代plt.show(),直接保存图形而不显示。
如果你希望将图形保存为文件并在 notebook 输出中显示,你可以同时调用plt.savefig()和plt.show()。
颠倒顺序可能导致图形元素被清除,留下一个空白画布用于保存的图形文件。
设置输出格式
plt.savefig()会自动检测指定输出路径的文件扩展名,并生成相应的文件格式(如果支持)。如果输入中未指定文件扩展名,则将使用默认后端输出 PNG 格式文件。它支持多种图像格式,包括 PNG、JPG、PDF 和 PostScript:
import numpy as np
import matplotlib.pyplot as plt
y = np.linspace(1,2000)
x = 1.0/np.sin(y)
plt.plot(x,y,'green')
plt.xlim(-20,20)
plt.ylim(1000,2400)
plt.show()
plt.savefig('123')
设置图形分辨率
根据显示的格式、位置和目的,每个图形可能需要不同的分辨率。通常,较大的印刷材料,如海报,需要更高的分辨率。我们可以通过指定每英寸点数(DPI)值来设置分辨率,例如如下所示:
plt.savefig(dpi=300)
对于8x12英寸的方形图和 300 DPI 的输出,图像中的像素将是(8x300)x(12x300) = 2400x3600像素。
Jupyter 支持
Matplotlib 已原生集成到 Jupyter Notebook 中;这种集成使得图形可以直接作为每个 notebook 单元的输出静态显示。有时,我们可能希望使用 Matplotlib 的交互式 GUI,例如缩放或平移图形,以从不同角度查看。我们可以通过一些简单的步骤继续在 Jupyter Notebook 中工作。
交互式导航工具栏
要访问 Jupyter Notebook 中的交互式导航工具栏,首先调用 Jupyter 单元魔法命令:
%matplotlib notebook
我们将通过一个具有更动态形状的图表来演示:
import numpy as np
import matplotlib.pyplot as plt
y = np.linspace(1,2000)
x = 1.0/np.sin(y)
plt.plot(x,y,'green')
plt.xlim(-20,20)
plt.ylim(1000,2400)
plt.show()
如图所示,这里我们有一个嵌入在 GUI 框中的圣诞树形状图表:

你可以在左下角找到工具栏。按钮从左到右的功能如下:
-
主页图标:重置原始视图
-
左箭头:返回到上一个视图
-
右箭头:前进到下一个视图
-
四方向箭头:按住左键拖动进行平移;使用右箭头键在屏幕上进行缩放
-
矩形:通过拖动图表上的矩形进行缩放
-
软盘图标:下载图表
这是通过在图表上拖动来进行平移的示例:

以下插图展示了通过拖动矩形框进行缩放的结果:

要恢复为内联输出模式,可以使用单元格魔法命令 %matplotlib inline,或点击右上角的电源按钮。
配置 Matplotlib
我们已经学会了如何调整 Matplotlib 图表中的一些主要元素。当我们反复生成相似风格的图表时,能够存储并应用持久的全局设置会非常方便。Matplotlib 提供了几种配置选项。
在 Python 代码中进行配置
为了在当前会话中保持设置,我们可以执行 matplotlib.rcParams 来覆盖配置文件中的设置。
例如,我们可以通过以下方式将所有图表中文本的字体大小设置为 18:
matplotlib.rcParams['font.size'] = 18
另外,我们可以调用 matplotlib.rc() 函数。由于 matplotlib.rc() 只改变一个属性,要更改多个设置,我们可以使用 matplotlib.rcParams.update() 函数,并以键值对字典的形式传递参数:
matplotlib.rcParams.update({'font.size': 18, 'font.family': 'serif'})
恢复到默认设置
要恢复为默认设置,可以调用 matplotlib.rcdefaults() 或 matplotlib.style.use('default')。
通过配置 rc 文件进行全局设置
如果你有一组配置想要全局应用而不需要每次设置,你可以设置 matplotlibrc 默认值。为了在某一组参数上进行持久且有选择性的更改,我们将选项存储在 rc 配置文件中。
查找 rc 配置文件
在 Linux/Unix 系统上,你可以通过编辑 /etc/matplotlibrc、$HOME/.matplotlib/matplotlib/rc 或 ~/.config/matplotlib/matplotlibrc 来为机器上的所有用户设置全局配置。
在 Windows 上,默认的 matplotlibrc 文件可能位于 C:\Python35\Lib\site-packages。要查找当前活动的 matplotlibrc 文件路径,我们可以在 Python shell 中使用 Matplotlib 的 matplotlib_fname() 函数,如下所示:
In [1]: import matplotlib as mpl
mpl.matplotlib_fname()
Out[1]: '/home/mary/.local/lib/python3.6/site-packages/matplotlib/mpl-data/matplotlibrc'
rc配置文件位于$INSTALL_DIR/matplotlib/mpl-data/matplotlibrc,其中$INSTALL_DIR是 Matplotlib 的安装路径,通常看起来像是python3.6/site-packages/。安装目录中的rc文件会在每次更新时被覆盖。为了在版本更新时保持更改不丢失,请将其保存在本地配置目录中,如'/home/mary/.config/matplotlib/matplotlibrc'。
编辑 rc 配置文件
文件的基本格式是option: value的形式。例如,若要将图例始终显示在右侧,我们可以这样设置:
legend.loc: right
Matplotlib 提供了大量的图形可配置选项,下面列出了几个可以控制定制的地方:
-
全局机器配置文件:Matplotlib 为每个用户配置全局机器配置文件
-
用户配置文件:每个用户的唯一文件,在此文件中可以覆盖全局配置文件,选择自己的设置(注意用户可以随时执行与 Matplotlib 相关的代码)
-
当前目录中的配置文件:通过使用此目录,可以针对当前脚本或程序进行特定的定制
这在不同程序有不同需求的情况下尤其有用,使用外部配置文件要比在代码中硬编码设置要好。
总结
恭喜!我们现在已经掌握了使用 Matplotlib 语法的基本绘图技巧!记住,数据可视化项目的成功在于制作吸引人的视觉效果。
在接下来的章节中,我们将学习如何美化图形,并选择合适的图表类型,以有效地传达我们的数据!
第三章:装饰图表的绘图样式和类型
在上一章中,我们学习了一些基本概念,使用 Matplotlib 绘制折线图和散点图,并对几个元素进行了调整。现在我们熟悉了 Matplotlib 语法,准备深入探索 Matplotlib 的潜力。
本章将讨论:
-
颜色规格
-
线条样式定制
-
点样式定制
-
更多原生绘图类型
-
插入文本和其他注释
-
绘图样式的注意事项
控制颜色
颜色是任何视觉效果中的一个重要元素。它会对图形的感知产生巨大影响。例如,鲜明的颜色对比可以用来突出焦点;几种不同颜色的组合有助于建立层次感。
在 Matplotlib 2 中,颜色已默认设置为更好地区分不同类别;并且为了更直观地感知连续数值,我们通常需要更好地控制颜色以表示数据。在本节中,我们将介绍 Matplotlib 中常见的颜色选项。
默认颜色循环
颜色循环是一个颜色列表,用于自动控制一系列元素的颜色,例如多条数据线图中的每一条数据系列。在 Matplotlib 2.0 中,默认的颜色循环从 7 种颜色扩展到 10 种颜色,采用 Data-Driven Documents (D3)[https://github.com/d3] 和 Vega(一种声明性可视化语法语言)中的 category10 调色板。这些颜色设计用于在不同类别之间显示良好的对比度。每种颜色的名称为 'C0' 到 'C9',可以通过指定预设颜色循环中的颜色手动调用。以下是一个包含默认颜色循环中每种颜色的多线图的示例:
import matplotlib.pyplot as plt
for i in range(10):
plt.plot([i]*5,c='C'+str(i),label='C'+str(i))
plt.xlim(0,5)
plt.legend()
plt.show()
以下是图形输出。图例显示了默认颜色循环中每种颜色的名称:

若要访问颜色的十六进制代码,可以使用以下代码:
import matplotlib as mpl
mpl.rcParams['axes.prop_cycle']
基本颜色的单字母缩写
有几个常用颜色,它们具有内置的单字母标准缩写,便于快速使用。它们如下所示:
-
'b':蓝色 -
'g':绿色 -
'r':红色 -
'c':青色 -
'm':品红色 -
'y':黄色 -
'k':黑色 -
'w':白色
标准 HTML 颜色名称
当我们想要从更广泛的颜色范围中快速构建调色板时,普通英语中的颜色名称可能比数字代码更直观。现代浏览器支持 HTML 中超过一百种不同的颜色名称。它们在 Matplotlib 中也得到了良好的支持,例如鲑鱼色、橙色、黄绿色、巧克力色和矢车菊蓝色。
你可以在此找到与颜色名称匹配的完整列表:matplotlib.org/examples/color/named_colors.html。相应的十六进制代码可以在这里找到:www.w3schools.com/colors/colors_names.asp。
RGB 或 RGBA 颜色代码
颜色也可以指定为三个到四个介于零到一之间的浮点数的元组,例如 (0.1,0.1,0.2) 或 (0.2,0.2,0.3,0.8)。前三个数字定义了应该混合多少红色、绿色和蓝色光以生成所需的颜色输出。可选的第四个数字是 alpha 值,用于控制透明度。
十六进制颜色代码
类似于 RGBA 值,十六进制(hex)颜色代码控制红色、绿色和蓝色光的量。它们还通过一个两位数的十六进制数字来控制透明度,每个数字以井号 '#' 开头,例如 '#81d8d0ec'。因此,纯红色、绿色、蓝色、黑色和白色的十六进制代码分别是 '#ff0000'、'#00ff00'、'#0000ff'、'#000000' 和 '#ffffff'。
灰度深度
你可以在浮点数的字符串中指定任何值,范围为 0-1,例如 '0.5'。数值越小,灰度越暗。
颜色映射
颜色映射将数值映射到一系列颜色。
从 Matplotlib 2.0 开始,默认的颜色映射已经从 'jet'(横跨从红色到蓝色的可见光谱)更改为 'viridis',这是一种从黄色到蓝色的感知均匀的渐变色。这样做使得感知连续值变得更加直观:
import numpy as np
import matplotlib.pyplot as plt
N = M = 200
X, Y = np.ogrid[0:20:N*1j, 0:20:M*10]
data = np.sin(np.pi * X*2 / 20) * np.cos(np.pi * Y*2 / 20)
fig, (ax2, ax1) = plt.subplots(1, 2, figsize=(7, 3)) # cmap=viridis by default
im = ax1.imshow(data, extent=[0, 200, 0, 200])
ax1.set_title("v2.0: 'viridis'")
fig.colorbar(im, ax=ax1, shrink=0.85)
im2 = ax2.imshow(data, extent=[0, 200, 0, 200], cmap='jet')
fig.colorbar(im2, ax=ax2, shrink=0.85)
ax2.set_title("classic: 'jet'")
fig.tight_layout()
plt.show()
查看以下使用前述代码生成的图像,了解感知颜色均匀性意味着什么:

Matplotlib 还提供了一些预设的颜色映射,优化了显示发散值或定性类别的效果。欢迎查看:matplotlib.org/2.1.0/tutorials/colors/colormaps.html。
创建自定义颜色映射
我们可以设置自定义的颜色映射。这在自定义热图和表面图时非常有用。
创建自定义线性颜色映射的一种简单方法是准备一个颜色列表,并让 Matplotlib 处理颜色过渡。让我们看一下以下示例:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors
# Create a 30 random dots
np.random.seed(52)
x,y,c = zip(*np.random.rand(30,3))
# Create a custom linear colormap
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["red","yellow","green"])
plt.scatter(x,y,c=c, cmap=cmap)
plt.colorbar()
plt.show()
这里,我们有一个散点图,使用了我们自定义的颜色映射,从 红色 通过 黄色 到 绿色:

线条和标记样式
在上一章中,我们已经展示了如何绘制折线图和散点图。我们知道,散点图由表示每个数据点的点组成,而折线图是通过连接数据点来生成的。在 Matplotlib 中,标记用来标示数据点的位置,可以定制其形状、大小、颜色和透明度。类似地,连接数据点的线段以及共享相同类的不同 2D 线条,也可以调整它们的样式,如在上一章的网格部分简要演示的那样。调整标记和线条样式对于使数据系列更加易于区分非常有用,有时也是出于美学考虑。在本节中,我们将详细介绍 Matplotlib 中标记和线条样式的实现方法。
标记样式
对于表示数据点的标记,我们可以调整它们的形状、大小和颜色。默认情况下,Matplotlib 会将标记绘制为单个圆形点。这里,我们介绍调整方法。
选择标记的形状
有几十种可用的标记来表示数据点。它们被分为未填充的 markers 和更粗的 filled_markers。以下是一些示例:
-
'o':圆形 -
'x':叉号 -
'+':加号 -
'P':填充加号 -
'D':填充菱形 -
's':方形 -
'^':三角形
我们可以通过 mpl.lines.Line2D.markers 访问所有可用标记形状的键和名称。以下是一个代码片段,用于概览所有标记形状:
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
for i,marker in enumerate(Line2D.markers):
plt.scatter(i%10,i,marker=marker,s=100) # plot each of the markers in size of 100
plt.show()
以下是标记的图形输出:

使用自定义字符作为标记
Matplotlib 支持使用自定义字符作为标记,现在包括数学文本和表情符号。要使用字符作为自定义标记,我们将两个美元符号 '$' 连接在字符前后,并将其作为 marker 参数传递。
以反斜杠 '\' 开头的符号,如 '\clubsuit',是数学文本(mathtext),将在本章稍后的文本和注释部分介绍。
这是一个使用数学符号和表情符号的散点图示例:
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
custom_markers = ['$'+x+'$' for x in ['\$','\%','\clubsuit','\sigma','']]
for i,marker in enumerate(custom_markers):
plt.scatter(i%10,i,marker=marker,s=500) # plot each of the markers in size of 100
plt.show()
从以下图示可以看到,我们成功地在散点图中使用了符号、希腊字母以及表情符号作为自定义标记:

调整标记的大小和颜色
在散点图中,我们可以使用参数 s 来指定标记的大小,使用 c 来指定标记的颜色,在 plt.scatter() 函数中实现。
要在折线图上绘制标记,我们首先在 plt.plot() 函数中指定标记的形状,如 marker='x'。标记的颜色将与线条颜色一致。
请注意,散点图接受列表类型的大小和颜色值,这对于可视化数据聚类非常方便,而折线图每个数据系列只接受单一值。
让我们看一下以下示例:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors
# Prepare a list of integers
n = list(range(5))
# Prepare a list of sizes that increases with values in n
s = [i**2*100+100 for i in n]
# Prepare a list of colors
c = ['red','orange','yellow','green','blue']
# Draw a scatter plot of n points with sizes in s and colors in c
plt.scatter(n,n,s=s,c=c)
# Draw a line plot with n points with black cross markers of size 12
plt.plot(n,marker='x',color='black',ms=12)
# Set axis limits to show the markers completely
plt.xlim(-0.5,4.5)
plt.ylim(-1,5)
plt.show()
这段代码生成了一个散点图,标记大小随着数据值增加,同时也有一个线性图,标记为固定大小的十字形:

通过关键字参数精细调整标记样式
我们可以通过一些额外的关键字参数进一步精细调整标记样式。例如,对于 plt.plot(),我们可以更改 markeredgecolor、markeredgewidth 和 markerfacecolor。
这里有一个代码示例:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors
# Prepare data points
x = list(range(5))
y = [1]*5
# Set the face color, edge color, and edge width of markers
plt.plot(x,y,marker='o',ms=36,markerfacecolor='floralwhite',markeredgecolor='slateblue',markeredgewidth=10)
plt.show()
这是添加额外关键字参数后的结果:

线条样式
线条是 Matplotlib 可视化中最常见的元素之一,从表示数据序列的线条到标记坐标轴、网格和任何形状轮廓的线条。因此,理解如何调整线条样式非常重要。Matplotlib 中的线条由 Line2D 类控制。Matplotlib 的面向对象结构使得通过关键字参数轻松调整线条样式,每个 API 中的语法都很相似。在这里,我们将介绍几个常用的 Matplotlib 线条调整方面。
颜色
设置线条图中线条的颜色非常简单,只需要在 plt.plot() 命令中添加 color 或其简写 c 参数即可。颜色选项在许多其他 Matplotlib API 中也可用。
线条粗细
线条的粗细通过大多数 Matplotlib 涉及线条的元素中的 linewidth 或 lw 参数设置,包括线条图。
虚线模式
线条的虚线模式由 linestyle 或 ls 参数指定。有时为了方便,它可以作为位置参数使用。例如,在线条图中,我们可以指定以下内容:
-
'solid'或'-':实线;默认值 -
'dashed'或'--':等距虚线 -
'dashdot'或'-.':交替虚线和点线 -
'.':松散的虚线 -
':':紧密的点线 -
'None'、' '、'':没有可见的线条 -
(offset, on-off-dash-seq):自定义虚线
以下是不同虚线模式的线条示例:
import matplotlib.pyplot as plt
# Prepare 4 data series of sine curves
y = [np.sin(i) for i in np.arange(0.0, 10.0, 0.1)]
dash_capstyles = ['-','--','-.','.',':']
# Plot each data series in different cap dash styles
for i,x in enumerate(dash_capstyles):
plt.plot([n*(i+1) for n in y],x,label=x)
plt.legend(fontsize=16,loc='lower left')
plt.show()
我们可以在下图中看到每种虚线样式的效果:

设计自定义虚线样式
Matplotlib 不仅仅限制于其预设的线条样式。实际上,我们可以通过指定每个重复虚线单元的长度和间距,设计自己的虚线模式,例如 (0, (5,3,1,3,1,3))。
端点样式
另一个调节参数是 dash_capstyle。它控制虚线端点的样式:
-
'butt':钝头 -
'projecting':延伸长度 -
'round':圆形末端
为了展示不同的端点样式,我们有一段多线图的代码示例,使用粗线条来放大虚线:
import matplotlib.pyplot as plt
# Prepare 4 data series of sine curves
y = list(range(10))
dash_capstyles = ['butt','projecting','round']
# Plot each data series in different cap dash styles
for i,x in enumerate(dash_capstyles):
plt.plot([n*(i+1) for n in y],lw=10,ls='--',dash_capstyle=x,label=x)
plt.legend(fontsize=16)
plt.show()
从下图中,我们可以清楚地看到钝头和圆形虚线给人不同的锋利度和微妙感:

坐标轴
在 Matplotlib 中,脊柱指的是围绕绘图区域坐标轴的线条。我们可以设置每个脊柱具有不同的线条样式或设置为不可见。
首先,我们使用plt.gca()访问坐标轴,其中gca代表获取当前坐标轴,并将其存储在一个变量中,例如ax。
然后我们通过ax.spines调整每个脊柱的属性,分别为'top'、'right'、'bottom'或'left'。常见的设置包括线宽、颜色和可见性,下面的代码片段演示了这些设置。
下面是一个示例,演示如何去除顶部和右侧脊柱,这在某些科学图表中常见,并且通常为了简化视觉效果而去除它们。通常还会加粗剩余的脊柱。颜色的变化如下所示,作为演示。我们可以根据图表显示的整体设计进行调整:
import matplotlib.pyplot as plt
y = list(range(4))
plt.plot(y)
# Store the current axes as ax
ax = plt.gca()
# Set the spine properties
ax.spines['left'].set_linewidth(3)
ax.spines['bottom'].set_linewidth(3)
ax.spines['left'].set_color('darkblue')
ax.spines['bottom'].set_color('darkblue')
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.show()
这将创建一个显示左侧和底部为蓝色脊柱的图:

更多本地的 Matplotlib 图表类型
除了最基本的散点图和折线图,Matplotlib 还提供了多种多样的图表类型,用于不同的数据可视化需求。在本节中,我们将介绍图表类型选择的原理以及每种类型的使用方法。
选择合适的图表
成功的可视化必须能够有效地传达信息。为了实现这个目标,我们需要对数据的性质有清晰的理解,同时了解每种图表类型在展示不同数据关系时的优缺点。
在选择合适的图表类型时,我们有以下几个考虑因素:
-
变量的数量
-
数据分布
-
数据系列之间的关系
直方图
直方图有助于调查数据的分布。例如,当我们希望查看某个人群中年龄分布,照片中的光照暴露,或一个城市每个月的降水量时,可以使用直方图绘制数据。
在 Matplotlib 中,我们调用plt.hist()函数并传入线性数组。Matplotlib 会自动将数据点分组为bins,并绘制每个 bin 的频率作为条形图。我们还可以通过plt.hist(array, bins=binsize)指定 bin 的大小。
下面是一个绘制随机生成的二项分布的示例:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(8)
x = np.random.binomial(100, 0.5, size=10000)
plt.hist(x,bins=20) # or plt.hist(x,20)
plt.show()
生成的直方图如下:

条形图
条形图对于比较离散数据系列的绝对水平非常有用。它们可以通过 Matplotlib 中的plt.bar(labels, heights)函数创建。
让我们来看一下今天备受关注的加密货币的市值示例。这里展示的是市值排名前五的加密货币:
import matplotlib.pyplot as plt
# Data retrieved from https://coinmarketcap.com on Jan 8, 2018
# Prepare the data series
cc = ['BTC','XRP','ETH','BCH','ADA']
cap = [282034,131378,107393,49999,26137]
# Plot the bar chart
plt.bar(cc,cap)
plt.title('Market capitalization of five top cryptocurrencies in Jan 2018')
plt.xlabel('Crytocurrency')
plt.ylabel('Market capitalization (million USD)')
plt.show()
从下图中可以看到,Matplotlib 并没有按照输入顺序,而是按字母顺序排列了标签,并输出了一个条形图:

要按指定顺序创建条形图,我们可以利用 Pandas 及其与 Matplotlib 的集成。步骤如下:
-
创建一个 Pandas DataFrame
df -
使用
df.plot(kind='bar')绘制条形图 -
设置
xticks的标签 -
调整其他图表属性
-
使用
plt.show()显示图表
请注意,默认情况下,df.plot()包含一个图例。我们需要指定legend=False来关闭它。
这里是一个示例,展示如何重新排序之前输出图中的条形图:
import pandas as pd
import matplotlib.pyplot as plt
df = pd.DataFrame({'cc':cc,'cap':cap}, legend=False)
ax = df.plot(kind='bar')
ax.set_xticklabels(df['cc'])
plt.title('Market capitalization of five top cryptocurrencies in Jan 2018')
plt.xlabel('Crytocurrency')
plt.ylabel('Market capitalization (million USD)')
plt.show()

设置条形图属性
我们可以在plt.bar()中作为关键字参数设置条形图的width、color和bottom坐标。
条形图的width是按比例设置的,而颜色则如本章早期部分所介绍的那样设置。
对于可能包含实验或测量误差的数据,我们可以输入yerr(和xerr)值的列表来显示精确度。
使用多变量数据绘制带误差条的条形图
我们可以通过 Pandas 的df.plot()轻松创建带有多个数据系列的条形图。这个 API 还允许我们通过提供xerr和yerr参数轻松添加误差条。让我们看一个示例,展示如何使用这个函数并调整条形图属性。
以下代码段绘制了一个多条形图,展示了一种假想药物治疗炎症的效果,通过比较治疗前后炎症蛋白的水平以及安慰剂对照组:
import pandas as pd
import matplotlib.pyplot as plt
# Prepare the data series
labels_drug = ['Drug (Before)', 'Drug (After)']
labels_placebo = ['Placebo (Before)', 'Drug (After)']
drug = [2.88,1.42]
placebo = [2.72,2.68]
yerr_drug = [0.12,0.08]
yerr_placebo = [0.24,0.13]
df = pd.DataFrame([drug,placebo])
df.columns = ['Before', 'After']
df.index = ['Drug','Placebo']
# Plot the bar chart with error bars
df.plot(kind='bar',width=0.4,color=['midnightblue','cornflowerblue'],\
yerr=[yerr_drug,yerr_placebo])
plt.title('Effect of Drug A Treatment')
plt.xlabel('Condition')
plt.ylabel('[hsCRP] (mg/L)')
plt.xticks(rotation=0) # to keep the xtick labels horizontal
plt.legend(loc=(0.4,0.8))
plt.show()
在这里,你得到一个双条件的配对条形图。看起来药物相比安慰剂可能有一些效果。你能想到更多的数据示例,用来绘制多条形图吗?

除了使用 Pandas 外,我们还可以调用多个plt.bar()命令来绘制多个系列的条形图。注意,我们需要调整坐标,以确保条形图不会重叠。
均值和误差图
对于实验科学,数据点通常是通过多次实验的平均值得到的,这就需要显示误差范围来说明精度水平。在这种情况下,均值和误差图比条形图更为合适。在 Matplotlib 中,均值和误差图是通过plt.errorbar()API 生成的。
当正误差和负误差相同时,我们可以将 1D 数组输入为误差值,绘制对称的误差条。否则,我们将输入 2D 数组[正误差,负误差]来绘制不对称的误差条。虽然只绘制y轴误差的图更为常见,但x和y轴的误差值都被支持。
默认情况下,Matplotlib 会绘制连接每个误差条的线,格式fmt设置为'.-'。对于离散数据集,我们可以添加关键字参数fmt='.'来去除这条线。让我们来看一个简单的例子:
import matplotlib.pyplot as plt
import numpy as np
# Prepare data for a sine curve
x = np.arange(0, 5, 0.3)
y = np.sin(-x)
# Prepare random error to plot the error bar
np.random.seed(100)
e1 = 0.1 * np.abs(np.random.randn(len(y)))
# Plotting the error bar
plt.errorbar(x, y, yerr=e1, fmt='.-')
plt.show()
我们现在得到了一个带误差条的正弦曲线,如下所示。试着用你获得的一些真实测试数据来替换它:

饼图
饼图是以圆形表示各组成部分比例的图表。每个扇区的角度及其弧长(也叫 楔形)代表各组成部分相对于整体的比例。
Matplotlib 提供了 plt.pie() 函数来绘制饼图。我们可以通过 labels 为每个扇区添加标签,并且可以通过 autopct 自动显示百分比。有关如何自定义百分比字符串格式的不同方式,请参考:pyformat.info/。
为了保持饼图的圆形形状,我们通过 plt.figure(figsize=(n,n)) 为正方形图形指定相同的宽度和长度。
这里是 2017 年 1 月第一周的网络服务器使用情况示例:
# Data obtained from https://trends.builtwith.com/web-server on Jan 06, 2017
import matplotlib.pyplot as plt
plt.figure(figsize=(4,4))
x = [0.31,0.3,0.14,0.1,0.15]
labels = ['nginx','Apache','IIS','Varnish','Others']
plt.pie(x,labels=labels,autopct='%1.1f%%')
plt.title('Web Server Usage Statistics')
plt.show()
生成的饼图如下所示:

我们还可以通过传递一个比率列表给关键字参数 explode 来分离每个扇区。例如,在之前的 plt.pie() 绘图中添加 explode=[0.1]*5 参数,将会生成如下结果:

请注意,如果输入数组的和小于 1,输出的饼图将不完整,正如下图所示:
import matplotlib.pyplot as plt
plt.figure(figsize=(4,4))
x = [0.1,0.3]
plt.pie(x)
plt.show()
如图所示,饼图并不是一个完整的圆,而是一个不完整的扇形图:

在这种情况下,我们必须明确指定每个项的比例。例如,在之前的示例中,将 x = [0.1,0.3] 改为 x = [0.25,0.75]。
极坐标图
极坐标图用于显示多维数据,也被称为雷达图或蜘蛛图。它常用于展示不同对象在不同方面的强度对比,例如评估硬件的价格和各种规格,或者游戏角色的能力。
此外,极坐标图在绘制数学函数时也很有用,下面我们将进行演示。在 Matplotlib 中,我们使用命令 plt.polar() 绘制极坐标图。除了我们熟悉的 x、y 坐标系外,极坐标用于极坐标图、角度和半径。中心点称为 极点。注意,Matplotlib 采用角度单位来输入角度。
这是绘制极坐标玫瑰图的代码:
import numpy as np
import matplotlib.pyplot as plt
theta = np.arange(0., 2., 1./180.)*np.pi
plt.polar(3*theta, theta/6)
plt.polar(theta, np.cos(6*theta))
plt.polar(theta, [1.2]*len(theta))
plt.savefig('mpldev_03_polarrose.png')
plt.show()
这是结果。你看到了多少个花瓣?

我们还可以利用极坐标系创建图表,例如地理学中表示地球风速的热力图,或工程学中表示圆形物体表面温度的图表。我们将把这些高级用法留作练习,等你完成本书后再尝试。
控制径向和角度网格
有两个函数可以控制径向网格和角度网格:rgrid() 和 thetagrid()。我们可以将 radii、labels 和 angle 参数传递给 rgrid() 函数,将 angles、labels 和 frac 参数传递给 thetagrid() 函数。
文本与注释
为了更好地理解图表细节,我们有时会添加文本注释来进行说明。接下来我们将介绍在 Matplotlib 图表中添加和调整文本的方法。
添加文本注释
我们可以通过调用 plt.text(x,y,text) 向图表中添加文本;我们需要指定 x 和 y 坐标以及文本字符串。
这里是一个快速示例:
plt.text(0.25,0.5,'Hello World!',fontsize=30)
plt.show()
你可以在下图中看到Hello World!消息出现在图形的中心:

字体
下面是一些常见的可调节字体属性:
-
字体大小:浮动或相对大小,例如,smaller 和 x-large
-
字体粗细:例如,bold 或 semibold
-
字体样式:例如,斜体
-
字体家族:例如,Arial
-
旋转:以度为单位的角度;可以是垂直或水平
Matplotlib 现在支持 Unicode 和 Emoji。
数学符号
作为绘图工具,数学符号非常常见。我们可以使用内置的 mathtext 或 LaTeX 在 Matplotlib 中渲染数学符号。
Mathtext
要创建 mathtext 符号,我们可以在字符串前加上 r,例如 r'$\alpha'。以下是一个简短的演示代码:
plt.title(r'$\alpha > \beta$')
plt.show()
以下图中的 Alpha 和 Beta 是通过 MathTex 打印的:

LaTeX 支持
Matplotlib 支持 LaTeX,尽管其渲染速度比 mathtext 慢;因此,它允许更灵活的文本渲染。以下是 LaTeX 用法的更多细节:matplotlib.org/users/usetex.html。
外部文本渲染器
如果我们已经安装了 LaTeX,可以通过 matplotlib.rc('text', usetex='false') 让外部 LaTeX 引擎渲染文本元素。
箭头
为了突出图表中的特定特征,我们可以使用 plt.arrow() 函数绘制箭头。以下代码演示了不同可用的箭头注释样式:
import matplotlib.pyplot as plt
plt.axis([0, 9, 0, 18])
arrstyles = ['-', '->', '-[', '<-', '<->', 'fancy', 'simple', 'wedge']
for i, style in enumerate(arrstyles):
plt.annotate(style, xytext=(1, 2+2*i), xy=(4, 1+2*i), \
arrowprops=dict(arrowstyle=style))
connstyles=["arc", "arc,angleA=10,armA=30,rad=15", \
"arc3,rad=.2", "arc3,rad=-.2", "angle", "angle3"]
for i, style in enumerate(connstyles):
plt.annotate("", xytext=(6, 2+2*i), xy=(8, 1+2*i), \
arrowprops=dict(arrowstyle='->', connectionstyle=style))
plt.show()
它生成以下图形,列出了可用的箭头形状进行注释:

使用样式表
到目前为止,我们已经一步步学习了如何为图表设置样式。为了获得更持久和可移植的设置,我们可以通过 matplotlib.style 模块应用预定义的全局样式:
## Available styles
Matplotlib provides a number of pre-built style sheets. You can check them out by with `matplotlib.style.available`.
import matplotlib as mpl
mpl.style.available
Out[1]: ['seaborn-talk',
'seaborn-poster',
'_classic_test',
'seaborn-ticks',
'seaborn-paper',
'ggplot',
'seaborn',
'seaborn-dark',
'seaborn-bright',
'seaborn-pastel',
'fivethirtyeight',
'Solarize_Light2',
'classic',
'grayscale',
'bmh',
'seaborn-dark-palette',
'seaborn-whitegrid',
'seaborn-white',
'dark_background',
'seaborn-muted',
'fast',
'seaborn-notebook',
'seaborn-darkgrid',
'seaborn-colorblind',
'seaborn-deep']
应用样式表
我们可以调用 plt.style.use(stylename) 来应用样式。此函数可以接受内置样式表、本地路径和 URL。
创建自己的样式表
你也可以创建自己的样式表。关于 Matplotlib 样式表文件的规格,请参考文档页面:matplotlib.org/users/customizing.html。
重置为默认样式
样式表设置的效果会在新的图形中持续。如果要恢复默认参数,请调用plt.rcdefaults()。
样式设计中的美学与可读性考虑
由于可视化是为了传递信息,从读者的角度思考越多,效果就会越好。一个吸引人的图形更容易引起注意。图形越容易阅读,读者越能理解其中的信息。以下是设计数据图形时的一些基本原则。
合适的字体样式
层次结构最多可以使用三种字体系列、粗细和大小的级别。尽量使用不那么花哨的字体系列,若可能,使用无衬线字体。确保字体大小足够大,便于阅读。
衬线与无衬线
衬线字体是字母上带有装饰性边缘的字体。无衬线字体在法语中意为“没有衬线”。如其名所示,无衬线字体通常比衬线字体更简洁、朴素。以微软 Office 中最常用的默认字体为例,2007 及之前版本使用的 Times New Roman 就是衬线字体,而更新版的 Calibri 则是无衬线字体。
有效使用颜色
-
使用更强烈的颜色对比来突出重点和区分
-
谨慎使用额外的颜色,例如每个数据系列只用一种颜色
-
对色弱的读者友好;例如,避免红绿组合
保持简单
"少即是多。"
– 安德烈亚·德尔·萨托(《无瑕画家》) 罗伯特·布朗宁
这句话阐明了前面建议的基本原则。极简主义设计哲学激发了许多杰出的作品,无论是建筑还是平面设计。虽然使用不同的颜色和样式能创造出区别性和层次感,同时增加图形的吸引力,但我们必须尽可能减少复杂性。这有助于读者集中注意力于主要信息,同时也帮助我们的图形保持专业的印象。
摘要
恭喜!你现在已经掌握了最常用的绘图方法以及自定义图形的基本技巧。接下来,我们将进入更高级的 Matplotlib 使用方法。
在下一章中,我们将介绍更多图形类型,并借助第三方包来优化多图和多个坐标轴的显示,处理特定比例尺下的显示效果,以及显示图像中的像素。敬请期待!
第四章:高级 Matplotlib
在之前的章节中,我们学习了基础的 Matplotlib API 的多种用法,可以创建并自定义各种类型的图表。为了为我们的数据创建更合适的可视化图形,还有一些更高级的技术来制作更精细的图形。实际上,我们不仅可以利用原生的 Matplotlib 功能,还可以利用一些建立在 Matplotlib 之上的第三方包。这些包提供了创建更加先进且默认具有美学样式的图形的简便方法。我们可以利用 Matplotlib 技术进一步优化我们的数据图形。
在本章中,我们将进一步探索 Matplotlib 的高级用法。我们将学习如何将多个相关图表分组为一个图形中的子图,使用非线性坐标轴比例,绘制图像,并在一些流行的第三方包的帮助下创建高级图表。以下是我们将涵盖的详细主题列表:
-
绘制子图
-
使用非线性坐标轴比例
-
绘制图像
-
使用 Pandas-Matplotlib 绘图集成
- 双变量数据集的六边形图
-
使用 Seaborn 构建:
-
用于双变量数据的核密度估计图
-
有/无层次聚类的热图
-
使用
mpl_finance绘制金融数据
-
-
使用
Axes3D进行 3D 绘图 -
使用 Basemap 和 GeoPandas 可视化地理数据
绘制子图
在设计视觉辅助工具的布局时,通常需要将多个相关的图形组织到同一个图形中的面板中,比如在展示同一数据集的不同方面时。Matplotlib 提供了几种方法来创建具有多个子图的图形。
使用 plt.figure() 初始化图形
plt.figure() API 是用来初始化图形的 API,它作为绘图的基础画布。它接受参数来确定图形的数量以及绘图图像的大小、背景颜色等参数。调用时,它会显示一个新的区域作为绘制 axes 的画布。除非添加其他绘图元素,否则不会得到任何图形输出。如果此时调用 plt.show(),将会返回一个 Matplotlib figure 对象,如下图所示:

当我们绘制简单图形时,如果只涉及单个图表且不需要多个面板,可以省略调用 plt.figure()。如果没有调用 plt.figure() 或没有给 plt.figure() 传递参数,则默认会初始化一个单一图形,相当于 plt.figure(1)。如果图形的比例非常关键,我们应该通过传递一个 (width, height) 元组作为 figsize 参数来调整它。
使用 plt.subplot() 初始化子图作为坐标轴
要初始化实际框住每个图形的坐标轴绘图实例,我们可以使用plt.subplot()。它需要三个参数:行数、列数和图号。当总图形数量少于 10 时,我们可以省略输入参数中的逗号。这里是一个代码示例:
import matplotlib.pyplot as plt
# Initiates a figure area for plotting
fig = plt.figure()
# Initiates six subplot axes
ax1 = plt.subplot(231)
ax2 = plt.subplot(232)
ax3 = plt.subplot(233)
ax4 = plt.subplot(234)
ax5 = plt.subplot(235)
ax6 = plt.subplot(236)
# Print the type of ax1
print(type(ax1))
# Label each subplot with corresponding identities
ax1.text(0.3,0.5,'231',fontsize=18)
ax2.text(0.3,0.5,'232',fontsize=18)
ax3.text(0.3,0.5,'233',fontsize=18)
ax4.text(0.3,0.5,'234',fontsize=18)
ax5.text(0.3,0.5,'234',fontsize=18)
ax6.text(0.3,0.5,'236',fontsize=18)
plt.show()
上面的代码生成了以下图形。请注意子图是从左到右、从上到下排列的。在添加实际的绘图元素时,必须相应地放置它们:

还需要注意的是,打印其中一个坐标轴的类型会返回<class 'matplotlib.axes._subplots.AxesSubplot'>作为结果。
使用plt.figure.add_subplot()添加子图
在plt.figure()下,有一个类似于plt.subplot()的add_subplot()函数,允许我们在同一个图形下创建额外的子图。与plt.subplot()类似,它接受行号、列号和图号作为输入参数,并且对于少于 10 个子图时,可以省略输入参数中的逗号。
我们还可以使用这个函数来初始化第一个子图。下面是一个快速的代码示例:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
plt.show()
这将创建一个空白的绘图区域,四个边框包含了x轴和y轴,如下所示。请注意,我们必须在figure下调用add_subplot()函数,而不是通过plt:

让我们进一步比较fig.add_subplot()和plt.subplot()之间的区别。在这里,我们将创建三个不同大小和面色的空子图。
我们将首先尝试使用fig.add_subplot():
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(111,facecolor='red')
ax2 = fig.add_subplot(121,facecolor='green')
ax3 = fig.add_subplot(233,facecolor='blue')
plt.show()
我们在同一图形上得到三个重叠的子图,如下所示:

接下来,我们将fig.add_subplot()替换为plt.subplot():
import matplotlib.pyplot as plt
fig = plt.figure() # Note this line is optional here
ax1 = plt.subplot(111,facecolor='red')
ax2 = plt.subplot(121,facecolor='green')
ax3 = plt.subplot(233,facecolor='blue')
plt.show()
请注意,在以下图片中,红色的ax1子图无法显示:

如果我们已经使用plt.subplot()绘制了第一个子图,并且想要创建更多的子图,可以调用plt.gcf()函数来获取figure对象并将其存储为变量。然后,我们可以像之前的示例一样调用fig.add_subplot()。
因此,以下代码是一种生成三个重叠子图的替代方法:
import matplotlib.pyplot as plt
ax1 = plt.subplot(111,facecolor='red')
fig = plt.gcf() # get current figure
ax2 = fig.add_subplot(121,facecolor='green')
ax3 = fig.add_subplot(233,facecolor='blue')
plt.show()
使用plt.subplots()初始化一组子图
当我们需要创建大量相同大小的子图时,逐个使用plt.subplot()或fig.add_subplot()函数生成它们会非常低效。在这种情况下,我们可以调用plt.subplots()一次性生成一组子图。
plt.subplots()接受行数和列数作为输入参数,并返回一个Figure对象以及存储在 NumPy 数组中的子图网格。当没有输入参数时,plt.subplots()默认等同于plt.figure()加上plt.subplot()。
这是一个演示用的代码片段:
import matplotlib.pyplot as plt
fig, axarr = plt.subplots(1,1)
print(type(fig))
print(type(axarr))
plt.show()
从结果截图中,我们可以观察到plt.subplots()也返回了Figure和AxesSubplot对象:

下一个示例演示了plt.subplots()的更有用的应用案例。
这次,我们将创建一个 3x4 子图的图形,并在一个嵌套的for循环中标记每个子图:
import matplotlib.pyplot as plt
fig, axarr = plt.subplots(3,4)
for i in range(3):
for j in range(4):
axarr[i][j].text(0.3,0.5,str(i)+','+str(j),fontsize=18)
plt.show()
再次,我们可以从这个图形中观察到,子图是按行排列,然后是列排列的,就像之前的示例所示:

也可以只向plt.subplots()提供一个输入参数,这将被解释为指定数量的子图,垂直堆叠在行中。由于plt.subplots()函数本质上包含了plt.figure()函数,我们还可以通过向figsize参数提供输入来指定图形尺寸:
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0.0, 1.0, 0.01)
y1 = np.sin(8*np.pi*x)
y2 = np.cos(8*np.pi*x)
# Draw 1x2 subplots
fig, axarr = plt.subplots(2,figsize=(8,6))
axarr[0].plot(x,y1)
axarr[1].plot(x,y2,'red')
plt.show()
请注意,axarr的类型是<class 'numpy.ndarray'>。
上述代码会产生一个包含两行子图的图形:

共享轴
当使用plt.subplots()时,我们可以指定子图应共享* x 轴和/或 y *轴,以避免混乱。
回到之前的 3x4 子图示例,假设我们通过提供sharex=True和sharey=True作为参数,在plt.subplots()中启用共享轴选项,如下所示:
fig, axarr = plt.subplots(3,4,sharex=True,sharey=True)
现在我们得到如下图形。与之前的示例相比,子图的轴标签被移除,除了最左边和最下面的标签,看起来更加整洁:

使用plt.tight_layout()设置边距
接下来,我们可以调整对齐方式。我们可能希望调整每个子图之间的边距,或者干脆不留边距,而是避免出现行和列之间的离散框。此时,我们可以使用plt.tight_layout()函数。默认情况下,当没有提供参数时,它会将所有子图适应到图形区域内。它接受关键字参数pad、w_pad和h_pad来控制子图周围的填充。让我们看一下下面的代码示例:
import matplotlib.pyplot as plt
fig, axarr = plt.subplots(3,4,sharex=True,sharey=True)
for i in range(3):
for j in range(4):
axarr[i][j].text(0.3,0.5,str(i)+','+str(j),fontsize=18)
plt.tight_layout(pad=0, w_pad=-1.6, h_pad=-1)
从下面的图形中,我们可以看到现在子图之间没有间距,但轴刻度有些重叠。
我们将在后续部分学习如何调整刻度属性或移除刻度:

使用plt.subplot2grid()对不同尺寸的子图进行对齐
虽然plt.subplots()提供了一种方便的方法来创建大小相同的子图网格,但有时我们可能需要将不同大小的子图组合在一起。这时plt.subplot2grid()就派上用场了。
plt.subplot2grid()接受三个到四个参数。第一个元组指定网格的整体尺寸。第二个元组确定子图左上角在网格中的起始位置。最后,我们使用rowspan和colspan参数描述子图的尺寸。
下面是一个代码示例,展示了如何使用这个函数:
import matplotlib.pyplot as plt
axarr = []
axarr.append(plt.subplot2grid((3,3),(0,0)))
axarr.append(plt.subplot2grid((3,3),(1,0)))
axarr.append(plt.subplot2grid((3,3),(0,2), rowspan=3))
axarr.append(plt.subplot2grid((3,3),(2,0), colspan=2))
axarr.append(plt.subplot2grid((3,3),(0,1), rowspan=2))
axarr[0].text(0.4,0.5,'0,0',fontsize=16)
axarr[1].text(0.4,0.5,'1,0',fontsize=16)
axarr[2].text(0.4,0.5,'0,2\n3 rows',fontsize=16)
axarr[3].text(0.4,0.5,'2,0\n2 cols',fontsize=16)
axarr[4].text(0.4,0.5,'0,1\n2 rows',fontsize=16)
plt.show()
以下是生成的图形。请注意不同大小的子图是如何对齐的:

使用fig.add_axes()绘制插图
子图不一定要并排对齐。在某些情况下,例如放大或缩小时,我们也可以将子图嵌入父图层上方。通过fig.add_axes()可以实现这一点。添加子图的基本用法如下:
fig = plt.figure() # or fig = plt.gcf()
fig.add_axes([left, bottom, width, height])
left、bottom、width和height参数是相对于父图的float值来指定的。注意,fig.add_axes()返回一个坐标轴对象,因此你可以将其存储为变量,如ax = fig.add_axes([left, bottom, width, height]),以便进一步调整。
以下是一个完整示例,我们尝试在一个较小的嵌入子图中绘制概览:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
np.random.seed(100)
# Prepare data
x = np.random.binomial(1000,0.6,1000)
y = np.random.binomial(1000,0.6,1000)
c = np.random.rand(1000)
# Draw the parent plot
ax = plt.scatter(x,y,s=1,c=c)
plt.xlim(580,650)
plt.ylim(580,650)
# Draw the inset subplot
ax_new = fig.add_axes([0.6, 0.6, 0.2, 0.2])
plt.scatter(x,y,s=1,c=c)
plt.show()
让我们查看图中的结果:

使用plt.subplots_adjust调整子图尺寸
我们可以使用plt.subplots_adjust()来调整子图的尺寸,它接受任意组合的参数——left、right、top和bottom——这些参数是相对于父坐标轴的。
调整坐标轴和刻度
在数据可视化中,仅仅展示趋势在相对意义上往往是不够的。轴的刻度对于正确解释和便于价值估算至关重要。刻度是轴上的标记,表示此目的的比例。根据数据的性质和图形布局,我们常常需要调整刻度和间距,以提供足够的信息而不显得杂乱。在本节中,我们将介绍一些自定义方法。
使用定位器自定义刻度间距
每个轴上有两组刻度标记:主刻度和次刻度。默认情况下,Matplotlib 会自动根据输入的数据优化刻度间距和格式。如果需要手动调整,可以通过设置以下四个定位器来实现:xmajorLocator、xminorLocator、ymajorLocator、yminorLocator,通过set_major_locator或set_minor_locator函数在相应的轴上进行设置。以下是一个使用示例,其中ax是一个坐标轴对象:
ax.xaxis.set_major_locator(xmajorLocator)
ax.xaxis.set_minor_locator(xminorLocator)
ax.yaxis.set_major_locator(ymajorLocator)
ax.yaxis.set_minor_locator(yminorLocator)
这里列出了常见的定位器及其用法。
使用 NullLocator 移除刻度
当使用NullLocator时,刻度会从视图中移除。
使用 MultipleLocator 定位刻度倍数
顾名思义,MultipleLocator根据用户指定的基数生成倍数刻度。例如,如果我们希望刻度标记为整数而不是浮动数值,可以通过MultipleLocator(1)来初始化基数。
显示日期和时间的定位器
对于时间序列绘图,Matplotlib 提供了一系列的刻度定位器,用于作为日期时间标记:
-
MinuteLocator -
HourLocator -
DayLocator -
WeekdayLocator -
MonthLocator -
YearLocator -
RRuleLocator,允许指定任意的日期刻度 -
AutoDateLocator -
MultipleDateLocator
要绘制时间序列图,我们也可以使用 Pandas 来指定 x 轴上数据的日期时间格式。
时间序列数据可以通过聚合方法进行重采样,如 mean()、sum() 或自定义函数。
使用格式化器自定义刻度标签格式
刻度格式化器控制刻度标签的格式。它的使用方式类似于刻度定位器,具体如下:
ax.xaxis.set_major_formatter(xmajorFormatter)
ax.xaxis.set_minor_formatter(xminorFormatter)
ax.yaxis.set_major_formatter(ymajorFormatter)
ax.yaxis.set_minor_formatter(yminorFormatter)
使用非线性坐标轴刻度
根据数据的分布情况,线性刻度可能并不是将所有有效数据点都适合图中的最佳方式。在这种情况下,我们可能需要将坐标轴的刻度调整为对数刻度或对称对数刻度。在 Matplotlib 中,可以通过在定义坐标轴之前使用 plt.xscale() 和 plt.yscale(),或者在定义坐标轴之后使用 ax.set_xscale() 和 ax.set_yscale() 来完成此操作。
我们不需要更改整个坐标轴的刻度。为了以线性刻度显示坐标轴的一部分,我们可以通过 linthreshx 或 linthreshy 参数来调整线性阈值。为了获得平滑的连续线条,我们还可以通过 nonposx 或 nonposy 参数来屏蔽非正数。
以下代码片段是不同坐标轴刻度的示例。为了简化说明,我们只更改了 y 轴的刻度。类似的操作也可以应用于 x 轴:
import numpy as np
import matplotlib.pyplot as plt
# Prepare 100 evenly spaced numbers from -200 to 200
x = np.linspace(-1000, 1000, 100)
y = x * 2
# Setup subplot with 3 rows and 2 columns, with shared x-axis.
# More details about subplots will be discussed in Chapter 3.
f, axarr = plt.subplots(2,3, figsize=(8,6), sharex=True)
for i in range(2):
for j in range(3):
axarr[i,j].plot(x, y)
# Horizontal line (y=10)
axarr[i,j].scatter([0], [10])
# Linear scale
axarr[0,0].set_title('Linear scale')
# Log scale, mask non-positive numbers
axarr[0,1].set_title('Log scale, nonposy=mask')
axarr[0,1].set_yscale('log', nonposy='mask')
# Log scale, clip non-positive numbers
axarr[0,2].set_title('Log scale, nonposy=clip')
axarr[0,2].set_yscale('log', nonposy='clip')
# Symlog
axarr[1,0].set_title('Symlog scale')
axarr[1,0].set_yscale('symlog')
# Symlog scale, expand the linear range to -100,100 (default=None)
axarr[1,1].set_title('Symlog scale, linthreshy=100')
axarr[1,1].set_yscale('symlog', linthreshy=100)
# Symlog scale, expand the linear scale to 3 (default=1)
# The linear region is expanded, while the log region is compressed.
axarr[1,2].set_title('Symlog scale, linscaley=3')
axarr[1,2].set_yscale('symlog', linscaley=3)
plt.show()
让我们比较一下以下图表中每种坐标轴刻度的结果:

更多关于 Pandas 和 Matplotlib 集成的内容
Pandas 提供了常用于处理多变量数据的 DataFrame 数据结构。通常在使用 Pandas 包进行数据输入/输出、存储和预处理时,它还提供了与 Matplotlib 的多个原生集成,便于快速可视化。
要创建这些图表,我们可以调用 df.plot(kind=plot_type)、df.plot.scatter() 等等。以下是可用的图表类型列表:
-
line: 线图(默认) -
bar: 垂直条形图 -
barh: 水平条形图 -
hist: 直方图 -
box: 箱型图 -
kde: 核密度估计 (KDE) 图 -
density: 与kde相同 -
area: 区域图 -
pie: 饼图
在前几章中,我们已经创建了一些简单的图表。在这里,我们将以密度图为例进行讨论。
使用 KDE 图显示分布
类似于直方图,KDE 图是可视化数据分布形态的一种方法。它通过核平滑创建平滑曲线,通常与直方图结合使用。这在探索性数据分析中非常有用。
在以下示例中,我们将比较不同国家各年龄组的收入数据,这些数据来自按不同年龄分组的调查结果。
这里是数据整理的代码:
import pandas as pd
import matplotlib.pyplot as plt
# Prepare the data
# Weekly earnings of U.S. wage workers in 2016, by age
# Downloaded from Statista.com
# Source URL: https://www.statista.com/statistics/184672/median-weekly-earnings-of-full-time-wage-and-salary-workers/
us_agegroups = [22,29.5,39.5,49.5]
# Convert to a rough estimation of monthly earnings by multiplying 4
us_incomes = [x*4 for x in [513,751,934,955]]
# Monthly salary in the Netherlands in 2016 per age group excluding overtime (Euro)
# Downloaded from Statista.com
# Source URL: https://www.statista.com/statistics/538025/average-monthly-wage-in-the-netherlands-by-age/
# take the center of each age group
nl_agegroups = [22.5, 27.5, 32.5, 37.5, 42.5, 47.5, 52.5]
nl_incomes = [x*1.113 for x in [1027, 1948, 2472, 2795, 2996, 3069, 3070]]
# Median monthly wage analyzed by sex, age group, educational attainment, occupational group and industry section
# May-June 2016 (HKD)
# Downloaded form the website of Censor and Statistics Department of the HKSAR government
# Source URL: https://www.censtatd.gov.hk/fd.jsp?file=D5250017E2016QQ02E.xls&product_id=D5250017&lang=1
hk_agegroups = [19.5, 29.5, 39.5, 49.5]
hk_incomes = [x/7.770 for x in [11900,16800,19000,16600]]
现在我们来绘制 KDE 图进行比较。我们准备了一个可重复使用的函数,用于绘制三组数据,减少代码的重复性:
import seaborn as sns
def kdeplot_income_vs_age(agegroups,incomes):
plt.figure()
sns.kdeplot(agegroups,incomes)
plt.xlim(0,65)
plt.ylim(0,6000)
plt.xlabel('Age')
plt.ylabel('Monthly salary (USD)')
return
kdeplot_income_vs_age(us_agegroups,us_incomes)
kdeplot_income_vs_age(nl_agegroups,nl_incomes)
kdeplot_income_vs_age(hk_agegroups,hk_incomes)
现在我们可以查看结果,按顺序分别为美国、荷兰和香港:


当然,图中的数据并不完全准确地反映原始数据,因为在进行任何调整之前就已经进行了外推(例如,这里并没有儿童劳动数据,但等高线图扩展到了 10 岁以下的儿童)。然而,我们仍然可以观察到三种经济体中,20 岁和 50 岁收入结构的总体差异,以及下载的公共数据与之的可比性。然后,我们可能能够建议进行更多有用分组的调查,并或许获取更多原始数据点来支持我们的分析。
使用六边形图展示双变量数据的密度
散点图是一种常见的方法,用于展示数据的分布,以较为原始的形式呈现。但当数据密度超过某一阈值时,可能不再是最好的可视化方法,因为点可能重叠,我们将失去关于实际分布的信息。
六边形图(hexbin map)是一种通过颜色强度展示区域内数据密度的方式,从而改善对数据密度的解读。
这是一个示例,用于比较将数据聚集在中心的相同数据集的可视化:
import pandas as pd
import numpy as np
# Prepare 2500 random data points densely clustered at center
np.random.seed(123)
df = pd.DataFrame(np.random.randn(2500, 2), columns=['x', 'y'])
df['y'] = df['y'] = df['y'] + np.arange(2500)
df['z'] = np.random.uniform(0, 3, 2500)
# Plot the scatter plot
ax1 = df.plot.scatter(x='x', y='y')
# Plot the hexbin plot
ax2 = df.plot.hexbin(x='x', y='y', C='z', reduce_C_function=np.max,gridsize=25)
plt.show()
这是 ax1 中的散点图。我们可以看到许多数据点是重叠的:

至于 ax2 中的六边形图,虽然并未显示所有离散的原始数据点,但我们可以清晰地看到数据分布在中心的变化:

使用 Seaborn 扩展图表类型
要安装 Seaborn 包,我们打开终端或命令提示符,并调用 pip3 install --user seaborn。每次使用时,我们通过 import seaborn as sns 导入该库,其中 sns 是常用的简写形式,旨在减少输入量。
使用热力图可视化多变量数据
热力图是一种在变量较多时展示多变量数据的有用可视化方法,适用于大数据分析等场景。它是一种在网格中使用颜色渐变来显示数值的图表。它是生物信息学家最常用的图表之一,用于在一张图中展示数百或数千个基因表达值。
使用 Seaborn,绘制热力图只需要一行代码,它通过调用 sns.heatmap(df) 来完成,其中 df 是要绘制的 Pandas DataFrame。我们可以提供 cmap 参数来指定要使用的颜色映射(“colormap”)。你可以回顾上一章,以了解更多关于颜色映射的使用细节。
为了更好地理解热力图,以下示例中,我们演示了使用英特尔 Core CPU 第 7^(代) 和 8^(代) 处理器系列的应用,涉及几十种型号和四个选择的指标。在查看绘图代码之前,我们先来看看存储数据的 Pandas DataFrame 结构:
# Data obtained from https://ark.intel.com/#@Processors
import pandas as pd
cpuspec = pd.read_csv('intel-cpu-7+8.csv').set_index('Name')
print(cpuspec.info())
cpuspec.head()
从以下输出的屏幕截图中,我们可以看到,我们只是简单地将标签作为索引,将不同的属性放在每一列中:

请注意,有 16 个模型在没有最大频率属性值的情况下不支持提升。考虑到我们在此的目的,使用基础频率作为最大值是合理的。我们将用相应的基础频率填充NA值:
cpuspec['Max Frequency'] = cpuspec['Max Frequency'].fillna(cpuspec['Base Frequency'])
现在,我们使用以下代码来绘制热图:
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(13,13))
sns.heatmap(cpuspec.drop(['Gen'],axis=1),cmap='Blues')
plt.xticks(fontsize=16)
plt.show()
很简单,不是吗?其实只需一行代码就能绘制热图。这也是一个示例,说明我们如何使用基本的 Matplotlib 代码来调整图表的其他细节,例如在这个例子中调整图形的维度和xticks字体大小。我们来看一下结果:

从图形中,即使我们对这些 CPU 型号毫无了解,也能很容易地从顶部 i7 型号的较深颜色推测出一些信息。它们是为更高性能而设计的,具有更多的核心和缓存空间。
使用聚类图显示多变量数据的层次结构
有时,当热图中存在过多交替的颜色带时,可能难以解读。这是因为我们的数据可能没有按相似性排序。在这种情况下,我们需要将更相似的数据分组,以便看到结构。
为此,Seaborn 提供了clustermap API,它结合了热图和树状图。树状图是一种树形图,将相似的变量聚类在同一分支/叶子下。绘制树状图通常涉及无监督的层次聚类,默认情况下,当我们调用clustermap()函数时,它会在后台运行。
除了无监督聚类外,如果我们事先知道某些标签,我们还可以使用row_colors关键字参数将其以颜色的形式显示出来。
在这里,我们从前面的 CPU 型号热图示例扩展,绘制了一个聚类热图,并将代际信息标记为行颜色。让我们看看代码:
import seaborn as sns
row_colors = cpuspec['Gen'].map({7:'#a2ecec',8:'#ecaabb'}) # map color values to generation
sns.clustermap(cpuspec.drop(['Gen'],axis=1),standard_scale=True,cmap='Blues',row_colors=row_colors);
再次调用 API 就像前面的热图一样简单,我们生成了以下图形:

除了帮助显示多个样本的多个属性之外,通过一些调整,clustermap 还可以用于成对聚类,显示考虑所有可用属性后样本之间的相似性。
要绘制成对聚类热图,我们首先需要计算来自不同属性值的样本之间的相关性,将相关矩阵转换为距离矩阵,然后执行层次聚类生成树状图的连接值。我们使用scipy包来实现这一目的。要了解更多关于连接值计算方法的内容,请参考 SciPy 文档。
我们将在此提供用户自定义函数:
from scipy.cluster import hierarchy
from scipy.spatial import distance
import seaborn as sns
def pairwise_clustermap(df,method='average',metric='cityblock',figsize=(13,13),cmap='viridis',**kwargs):
correlations_array = np.asarray(df.corr())
row_linkage = hierarchy.linkage(
distance.pdist(correlations_array), method=method)
col_linkage = hierarchy.linkage(
distance.pdist(correlations_array.T), method=method)
g = sns.clustermap(correlations, row_linkage=row_linkage, col_linkage=col_linkage, \
method=method, metric=metric, figsize=figsize, cmap=cmap,**kwargs)
return g
这是配对聚类图的结果:

从这两个热力图中,我们可以观察到,根据这四个属性,CPU 似乎根据产品线后缀(如 U、K、Y)而非品牌修饰符(如 i5 和 i7)进行更好的聚类。在处理数据时,这是一项需要观察大组相似性的分析技能。
图像绘制
在分析图像时,第一步是将颜色转换为数值。Matplotlib 提供了用于读取和显示 RGB 值图像矩阵的 API。
以下是一个快速的代码示例,演示如何使用 plt.imread('image_path') 将图像读取为 NumPy 数组,并使用 plt.imshow(image_ndarray) 展示它。确保已安装 Pillow 包,以便处理 PNG 以外的更多图像类型:
import matplotlib.pyplot as plt
# Source image downloaded under CC0 license: Free for personal and commercial use. No attribution required.
# Source image address: https://pixabay.com/en/rose-pink-blossom-bloom-flowers-693155/
img = plt.imread('ch04.img/mpldev_ch04_rose.jpg')
plt.imshow(img)
这里是使用前面代码显示的原始图像:

在展示原始图像后,我们将尝试通过改变图像矩阵中的颜色值来转换图像。我们将通过将 RGB 值设置为 0 或 255(最大值)并设定阈值为 160 来创建高对比度图像。以下是操作方法:
# create a copy because the image object from `plt.imread()` is read-only
imgcopy = img.copy()
imgcopy[img<160] = 0
imgcopy[img>=160] = 255
plt.imshow(imgcopy)
plt.show()
这是转换后图像的结果。通过人为地增加对比度,我们创造了一幅波普艺术风格的图像!

为了展示 Matplotlib 图像处理功能的更实际应用,我们将展示 MNIST 数据集。MNIST 是一个著名的手写数字数据集,常用于机器学习算法的教程。在这里,我们不深入探讨机器学习,而是尝试重现一个情景,在探索性数据分析阶段,我们通过视觉检查数据集。
我们可以从官方网站下载整个 MNIST 数据集:yann.lecun.com/exdb/mnist/。为了简化讨论并引入有用的 Python 机器学习包,我们从 Keras 加载数据。Keras 是一个高级 API,便于神经网络的实现。Keras 包中的 MNIST 数据集包含 70,000 张图像,按坐标和相应标签的元组排列,方便在构建神经网络时进行模型训练和测试。
让我们首先导入这个包:
from keras.datasets import mnist
数据只有在调用load_data()时才会被加载。因为 Keras 主要用于训练,所以数据会以训练集和测试集的元组形式返回,每个元组包含实际的图像颜色值和标签,在此约定中命名为X和y:
(X_train,y_train),(X_test,y_test) = mnist.load_data()
当首次调用load_data()时,可能需要一些时间来从在线数据库下载 MNIST 数据集。
我们可以按如下方式检查数据的维度:
for d in X_train, y_train, X_test, y_test:
print(d.shape)
这是输出结果:
(60000, 28, 28)
(60000,)
(10000, 28, 28)
(10000,)
最后,让我们从 X_train 集合中取出一张图像,并使用 plt.imshow() 将其以黑白方式绘制:
plt.imshow(X_train[123], cmap='gray_r')
从下图中,我们可以轻松地用肉眼读出七个数据点。在解决实际的图像识别问题时,我们可能会对一些被误分类的图像进行采样,并考虑优化训练算法的策略:

财务绘图
在某些情况下,为了理解预测趋势,我们需要每个时间点的更多原始值。蜡烛图是金融技术分析中常用的一种可视化方式,用于展示价格趋势,最常见于股市。要绘制蜡烛图,我们可以使用 mpl_finance 包中的 candlestick_ohlc API。
mpl_finance 可以从 GitHub 上下载。在 Python 的 site-packages 目录中克隆仓库后,在终端中运行 python3 setup.py install 来安装它。
candlestick_ohlc() 接受一个 Pandas DataFrame 作为输入,DataFrame 包含五列:date(浮动数值)、open、high、low 和 close。
在我们的教程中,我们以加密货币市场的价值为例。让我们再次查看我们获得的数据表:
import pandas as pd
# downloaded from kaggle "Cryptocurrency Market Data" dataset curated by user jvent
# Source URL: https://www.kaggle.com/jessevent/all-crypto-currencies
crypt = pd.read_csv('crypto-markets.csv')
print(crypt.shape)
crypt.head()
这是表格的样子:

让我们选择第一个加密货币,比特币,作为示例。以下代码选择了 2017 年 12 月的 OHLC 值,并将索引设置为 date,格式为日期时间格式:
from matplotlib.dates import date2num
btc = crypt[crypt['symbol']=='BTC'][['date','open','high','low','close']].set_index('date',drop=False)
btc['date'] = pd.to_datetime(btc['date'], format='%Y-%m-%d').apply(date2num)
btc.index = pd.to_datetime(btc.index, format='%Y-%m-%d')
btc = btc['2017-12-01':'2017-12-31']
btc = btc[['date','open','high','low','close']]
接下来,我们将绘制蜡烛图。回顾设置坐标轴刻度以微调时间标记的技巧:
import matplotlib.pyplot as plt
from matplotlib.dates import WeekdayLocator, DayLocator, DateFormatter, MONDAY
from mpl_finance import candlestick_ohlc
# from matplotlib.finance import candlestick_ohlc deprecated in 2.0 and removed in 2.2
fig, ax = plt.subplots()
candlestick_ohlc(ax,btc.values,width=0.8)
ax.xaxis_date() # treat the x data as dates
ax.xaxis.set_major_locator(WeekdayLocator(MONDAY)) # major ticks on the Mondays
ax.xaxis.set_minor_locator(DayLocator()) # minor ticks on the days
ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d'))
# Align the xtick labels
plt.setp(ax.get_xticklabels(), horizontalalignment='right')
# Set x-axis label
ax.set_xlabel('Date',fontsize=16)
# Set y-axis label
ax.set_ylabel('Price (US $)',fontsize=16)
plt.show()
mpl_finance 可以通过运行以下命令进行安装:
pip3 install --user https://github.com/matplotlib/mpl_finance/archive/master.zip
我们可以观察到,比特币在 12 月初的快速上涨,在 2017 年 12 月中旬出现了方向的转变:

使用 Axes3D 绘制 3D 图
到目前为止,我们讨论了二维绘图。事实上,在很多情况下,我们可能需要进行 3D 数据可视化。例子包括展示更复杂的数学函数、地形特征、物理学中的流体动力学,以及展示数据的其他方面。
在 Matplotlib 中,这可以通过 mpl_toolkits 中的 mplot3d 库中的 Axes3D 来实现。
我们只需要在导入库后定义一个坐标轴对象时指定 projection='3d'。接下来,我们只需定义带有 x、y 和 z 坐标的坐标轴。支持的图形类型包括散点图、线图、条形图、等高线图、网格框架图和表面图(带或不带三角化)。
以下是绘制 3D 曲面图的示例:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
x = np.linspace(-2, 2, 60)
y = np.linspace(-2, 2, 60)
x, y = np.meshgrid(x, y)
r = np.sqrt(x**2 + y**2)
z = np.cos(r)
surf = ax.plot_surface(x, y, z, rstride=2, cstride=2, cmap='viridis', linewidth=0)
Matplotlib 的 Axes3D 对于使用常见的 Matplotlib 语法和外观绘制简单的 3D 图非常有用。对于需要高渲染的高级科学 3D 绘图,建议使用 Mayavi 包。这里是该项目的官方网站,供您了解更多信息:code.enthought.com/pages/mayavi-project.html。
从以下截图中,我们可以看到,颜色渐变有助于展示 3D 图的形状:

地理绘图
为了展示 Matplotlib 与第三方包的强大功能,我们将演示其在空间分析中的应用。自卫星发明以来,产生了大量有用的地理信息系统(GIS)数据,帮助各种分析,从自然现象到人类活动。
为了利用这些数据,Matplotlib 集成了多个常见的 Python 包来展示空间数据,如 Basemap、GeoPandas、Cartopy 和 Descartes。在本章的最后部分,我们将简要介绍前两个包的用法。
Basemap
Basemap 是最受欢迎的基于 Matplotlib 的绘图工具包之一,用于在世界地图上绘图。它是展示任何地理位置的便捷方式。
安装 Basemap 的步骤如下:
-
解压到
$Python3_dir/site-packages/mpl_toolkits -
进入 Basemap 安装目录:
cd $basemap_dir -
进入
Basemap目录中的geos目录:cd $basemap/geos-3.3.3 -
使用
./configure、make和make install安装 GEOS 库。 -
安装 PyProj(参见以下提示)
-
返回 Basemap 安装目录并运行
python3 setup.py install。 -
设置环境变量
`PROJ_DIR=$pyproj_dir/lib/pyproj/data`
Basemap 需要 PyProj 作为依赖项,但常有安装失败的报告。我们建议先安装 Cython 依赖,再从 GitHub 安装。
-
从
github.com/jswhit/pyproj克隆 PyProj GitHub 仓库到 Python 站点包目录。 -
使用
pip install --user cython安装 Cython 依赖。 -
进入 PyProj 目录并使用
python3 setup.py install安装。
对于 Windows 用户,通过 Anaconda 安装可能更为简便,使用命令行conda install -c conda-forge geopandas。
作为简短介绍,我们将通过以下代码片段展示如何绘制美丽的地球及阴影地形:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
# Initialize a Basemap object
# Use orthogonal spherical projection
# Adjust the focus by setting the latitude and longitude
map = Basemap(projection='ortho', lat_0=20, lon_0=80)
# To shade terrain by relief. This step may take some time.
map.shadedrelief()
# Draw the country boundaries in white
map.drawcountries(color='white')
plt.show()
这是绘图的效果:

除了展示如前图所示的正交投影的地球作为球体外,我们还可以设置projection='cyl',使用米勒圆柱投影来展示平面矩形图。
Basemap 提供了许多地图绘制功能,如绘制海岸线和在地图上绘制数据(使用 hexbin 或 streamplot)。详细信息可以在官方教程中找到basemaptutorial.readthedocs.io。由于深入的地理分析超出了本书的范围,我们将把更具体的用法留给感兴趣的读者作为练习。
GeoPandas
GeoPandas 是与 Matplotlib 集成的地理绘图库,具有读取常见 GIS 文件格式的全面功能。
要使用 GeoPandas,我们将按如下方式导入库:
import geopandas as gpd
import matplotlib.pyplot as plt
在接下来的示例中,我们将探讨世界银行集团准备的气候变化数据。
我们选择了基于 B1 场景的 2080-2099 年降水投影:这是一个收敛的世界,全球人口在本世纪中期达到峰值后开始下降。故事情节描述了经济逐渐转向以服务和信息为主,采用清洁和资源高效的技术,但没有额外的气候行动。
作为输入,我们已经下载了 shapefile(.shp),这是地理数据分析中使用的标准格式之一:
# Downloaded from the Climate Change Knowledge portal by the World Bank Group
# Source URL: http://climate4development.worldbank.org/open/#precipitation
world_pr = gpd.read_file('futureB.ppt.totals.median.shp')
world_pr.head()
我们可以查看 GeoPandas DataFrame 的前几行。请注意,形状数据存储在 geometry 列中:

接下来,我们将在世界地图上添加边界,以便更好地识别位置:
# Downloaded from thematicmapping.org
# Source URL http://thematicmapping.org/downloads/world_borders.php
world_borders = gpd.read_file('TM_WORLD_BORDERS_SIMPL-0.3.shp')
world_borders.head()
在这里,我们检查 GeoPandas DataFrame。正如预期的那样,形状信息也存储在 geometry 中:

几何数据将作为填充的多边形绘制。为了仅绘制边界,我们将通过 GeoSeries.boundary 生成边界几何:
# Initialize an figure and an axes as the canvas
fig,ax = plt.subplots()
# Plot the annual precipitation data in ax
world_pr.plot(ax=ax,column='ANNUAL')
# Draw the simple worldmap borders
world_borders.boundary.plot(ax=ax,color='#cccccc',linewidth=0.6)
plt.show()
现在,我们已经获得了以下结果:

网站还提供了另一个场景 A2 的数据,描述了一个非常异质的世界,本地身份得以保存。那幅图将是什么样的?它会看起来相似还是截然不同?让我们下载文件来看看!
同样,GeoPandas 提供了许多 API 供更高级的使用。读者可以参考 http://geopandas.org/ 获取完整的文档或更多细节。
概要
恭喜你!我们在 Matplotlib 的高级使用方面已经取得了长足的进展。在本章中,我们学习了如何在子图之间共享和绘制坐标轴,使用非线性坐标轴尺度,调整刻度格式化器和定位器,绘制图像,使用 Seaborn 创建高级图表,创建金融数据的蜡烛图,使用 Axes3D 绘制简单的 3D 图,以及使用 Basemap 和 GeoPandas 可视化地理数据。
你已经准备好深入将这些技能与所需的应用程序整合了。在接下来的几个章节中,我们将使用 Matplotlib 支持的不同后端。敬请期待!
第五章:在 GTK+3 中嵌入 Matplotlib
到目前为止,我们已经做了不少示例,并且已经打下了良好的基础,能够使用 Matplotlib 生成数据图表和图形。虽然单独使用 Matplotlib 在生成交互式图形、实验数据集和理解数据的子结构方面非常方便,但也可能出现需要一个应用程序来获取、解析并显示数据的情况。
本章将研究如何通过 GTK+3 将 Matplotlib 嵌入应用程序的示例。
安装和设置 GTK+3
设置 GTK+3 相对简单直观。根据操作系统版本和环境的不同,安装 GTK+3 有多种方式。
我们建议读者参考链接:python-gtk-3-tutorial.readthedocs.io/en/latest/install.html以获取最新的安装更新和信息。
在写这本书时,官方网站建议用户通过 JHBuild 安装 GTK+3。然而,用户发现 JHBuild 在 macOS El Capitan 上存在兼容性问题。
我们建议 macOS 用户使用包管理器brew来安装 GTK+3。
如果你的 macOS 已经安装了brew,则可以简单地安装 GTK+3:
#Installing the gtk3 package
brew install gtk3
#Installing PyGObject
brew install pygobject3
对于像 Ubuntu 这样的 Linux 系统,GTK+3 默认已经安装。对于那些喜欢更自定义安装方式的高级用户,我们建议访问官网获取最新的安装信息。
我们观察到 GTK+3 在 IPython Notebook 中的可视化兼容性不如预期。我们建议你在终端中运行代码以获得最佳效果。
GTK+3 简要介绍
在探索各种示例和应用之前,让我们先对 GTK+3 进行一个简要的高层次了解。
GTK+3 包含一组图形控件元素(小部件),是一个功能丰富、易于使用的工具包,用于开发图形用户界面。它具有跨平台兼容性,且相对容易使用。GTK+3 是一个面向对象的小部件工具包,用 C 语言编写。因此,当在 Python 中运行 GTK+3 时,我们需要一个包装器来调用 GTK+3 库中的函数。在这种情况下,PyGObject 是一个 Python 模块,它作为包装器为我们节省了不必学习两种语言来绘制图形的时间。PyGObject 专门支持 GTK+3 或更高版本。如果你更喜欢在应用程序中使用 GTK+2,我们建议使用 PyGTK。
与 Glade GUI 构建器一起,它们提供了一个非常强大的应用程序开发环境。
GTK+3 信号系统简介
GTK+3 是一个事件驱动的工具包,这意味着它始终在一个循环函数中处于休眠状态,等待(监听)事件的发生;然后它将控制权交给相应的函数。事件的例子有点击按钮、激活菜单项、勾选复选框等。当小部件接收到事件时,它们通常会发出一个或多个信号。这个信号将调用你连接的函数,在这种情况下称为回调函数。控制的传递是通过信号的概念来完成的。
尽管术语几乎相同,但 GTK+3 的信号与 Unix 系统信号不同,并且并非使用它们实现。
当像鼠标按钮按下这样的事件发生时,点击接收到小部件的控件会发出相应的信号。这是 GTK+3 工作原理中最重要的部分之一。有些信号是所有小部件都继承的,例如destroy和delete-event,还有一些是特定于小部件的信号,例如切换按钮的切换。为了使信号框架生效,我们需要设置一个信号处理程序来捕获这些信号并调用相应的函数。
从更抽象的角度来看,一个通用的例子如下:
handler_id = widget.connect("Event", callback, data )
在这个通用示例中,widget是我们之前创建的小部件的一个实例。它可以显示小部件、按钮、切换按钮或文本数据输入。每个小部件都有自己的特定事件,只有当该事件发生时,它才会响应。如果小部件是按钮,当发生点击等动作时,信号将被发出。callback参数是回调函数的名称。当事件发生时,回调函数将被执行。最后,data参数包括任何在生成信号时需要传递的数据;这是可选的,如果回调函数不需要参数,可以省略。
这是我们第一个 GTK+3 示例:
#In here, we import the GTK module in order to access GTK+3's classes and functions
#We want to make sure we are importing GTK+3 and not any other version of the library
#Therefore we require_version('Gtk','3.0')
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
#This line uses the GTK+3 functions and creates an empty window
window = Gtk.Window(title="Hello World!")
#We created a handler that connects window's delete event to ensure the application
#is terminated if we click on the close button
window.connect("destroy",Gtk.main_quit)
#Here we display the window
window.show_all()
#This tells the code to run the main loop until Gtk.main_quit is called
Gtk.main()
要运行此代码,读者可以选择复制并粘贴,或将代码保存到名为first_gtk_example.py的文件中,并在终端中运行,如下所示:
python3 first_gtk_example.py
读者应该能够创建一个空白的 200x200 像素窗口(默认情况下,如果没有指定其他内容),如下所示:

图 1
为了充分理解 GTK3+的实用性,建议将代码编写为 PyGObject。
以下代码演示了一个修改版的稍微复杂的示例,其中一个窗口中有两个点击按钮,每个按钮执行不同的任务!
读者在运行本章示例之前,应通过pip3安装cairocffi:
pip3 install cairocffi
cairocffi库是一个基于 CFFI 的替代库,用于替代 Pycairo,在此案例中是必需的。现在让我们深入了解代码:
#Again, here we import the GTK module
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
#From here, we define our own class, namely TwoClicks.
#This is a sub-class of Gtk.Window
class TwoClicks(Gtk.Window):
#Instantiation operation will creates an empty object
#Therefore, python3 uses __init__() to *construct* an object
#__init__() will be automatically invoked when the object is being created!
#You can call this the constructor in Python3
#Noted that *self* here indicates the reference of the object created from this class
#Anything starting with self.X refers to the local function or variables of the object itself!
def __init__(self):
#In here, we are essentially constructing a Gtk.Window object
#And parsing the information title="Hello world" to the constructor of Gtk.Window
#Therefore, the window will have a title of "Hello World"
Gtk.Window.__init__(self, title="Hello World")
#Since we have two click buttons, we created a horizontally oriented box container
#with 20 pixels placed in between children - the two click buttons
self.box = Gtk.Box(spacing=100)
#This assigns the box to become the child of the top-level window
self.add(self.box)
#Here we create the first button - click1, with the title "Print once!" on top of it
self.click1 = Gtk.Button(label="Print once!")
#We assign a handler and connect the *Event* (clicked) with the *callback/function* (on_click1)
#Noted that, we are now calling the function of the object itself
#Therefore we are using *self.onclick1
self.click1.connect("clicked", self.on_click1)
#Gtk.Box.pack_start() has a directionality here, it positions widgets from left to right!
self.box.pack_start(self.click1, True, True, 0)
#The same applies to click 2, except that we connect it with a different function
#which prints Hello World 5 times!
self.click2 = Gtk.Button(label="Print 5 times!")
self.click2.connect("clicked", self.on_click2)
self.box.pack_start(self.click2, True, True, 0)
#Here defines a function on_click1 in the Class TwoClicks
#This function will be triggered when the button "Print once!" is clicked
def on_click1(self, widget):
print("Hello World")
#Here defines a function on_click2 in the Class TwoClicks
#This function will be triggered when the button "Print 5 times!" is clicked
def on_click2(self, widget):
for i in range(0,5):
print("Hello World")
#Here we instantiate an object, namely window
window = TwoClicks()
#Here we want the window to be close when the user click on the close button
window.connect("delete-event", Gtk.main_quit)
#Here we display the window!
window.show_all()
#This tells the code to run the main loop until Gtk.main_quit is called
Gtk.main()
以下是你从上面的代码片段中得到的结果:

图 2
点击不同的按钮将导致在终端上获得不同的结果。
这个示例作为面向对象编程(OOP)风格的介绍。对于新手用户来说,OOP 可能有些复杂,但它是组织代码、创建模块以及增强代码可读性和可用性的最佳方式之一。虽然新手用户可能没有注意到,但在前四章中,我们已经使用了许多 OOP 概念。
通过理解init()和self,我们现在可以深入研究更高级的编程技巧了。那么,让我们尝试一些更高级的例子!如果我们想要将我们制作的一些图表嵌入到 GTK+3 窗口中,我们可以这样做:
#Same old, importing Gtk module, we are also importing some other stuff this time
#such as numpy and the backends of matplotlib
import gi, numpy as np, matplotlib.cm as cm
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
#From here, we are importing some essential backend tools from matplotlib
#namely the NavigationToolbar2GTK3 and the FigureCanvasGTK3Agg
from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3 as NavigationToolbar
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
from matplotlib.figure import Figure
#Some numpy functions to create the polar plot
from numpy import arange, pi, random, linspace
#Here we define our own class MatplotlibEmbed
#By simply instantiating this class through the __init__() function,
#A polar plot will be drawn by using Matplotlib, and embedded to GTK3+ window
class MatplotlibEmbed(Gtk.Window):
#Instantiation
def __init__(self):
#Creating the Gtk Window
Gtk.Window.__init__(self, title="Embedding Matplotlib")
#Setting the size of the GTK window as 400,400
self.set_default_size(400,400)
#Readers should find it familiar, as we are creating a matplotlib figure here with a dpi(resolution) 100
self.fig = Figure(figsize=(5,5), dpi=100)
#The axes element, here we indicate we are creating 1x1 grid and putting the subplot in the only cell
#Also we are creating a polar plot, therefore we set projection as 'polar
self.ax = self.fig.add_subplot(111, projection='polar')
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.width = pi / 4 * random.rand(30)
self.bars = self.ax.bar(self.theta, self.radii, width=self.width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.ax.plot()
#Here comes the magic, a Vbox is created
#VBox is a containder subclassed from Gtk.Box, and it organizes its child widgets into a single column
self.vbox = Gtk.VBox()
#After creating the Vbox, we have to add it to the window object itself!
self.add(self.vbox)
#Creating Canvas which store the matplotlib figure
self.canvas = FigureCanvas(self.fig) # a Gtk.DrawingArea
# Add canvas to vbox
self.vbox.pack_start(self.canvas, True, True, 0)
# Creating toolbar, which enables the save function!
self.toolbar = NavigationToolbar(self.canvas, self)
self.vbox.pack_start(self.toolbar, False, False, 0)
#The code here should be self-explanatory by now! Or refer to earlier examples for in-depth explanation
window = MatplotlibEmbed()
window.connect("delete-event", Gtk.main_quit)
window.show_all()
Gtk.main()
在这个例子中,我们创建了一个垂直框,并将画布(带有图表)和工具栏放入其中:

图 3
看起来很容易将 Matplotlib 图表直接整合到 GTK+3 中,不是吗?如果您有自己的图表想要将其插入 GTK+3 引擎中,只需扩展极坐标图表示例,然后您就可以使用此模板开始处理自己的图表了!
我们在这里额外做的一件事是创建了一个工具栏,并将其放置在图表的底部。请记住,我们在组织小部件时使用的是 VBox?这里的 V 代表垂直,即从上到下组织数据。因此,将工具栏放置在画布之后时,我们有这样的顺序。工具栏是一个优雅地修改和保存图表的好地方。
因此,让我们尝试几个例子,看看如何通过结合 GTK+3 和 Matplotlib 创建一些交互式图表。一个非常重要的概念是通过画布与 Matplotlib 建立事件连接;这可以通过调用mpl_connect()函数来实现。
在Matplotlib Cookbook在线上可以找到许多好的例子。
让我们走过一个提供交互式放大功能的例子。这里是代码输出的预览:

图 4
窗口包括两个子图;左侧的图表是大图,而右侧的图表是放大版本。在左侧选择放大的区域由灰色框指定,灰色框可以随鼠标点击移动。这听起来可能有些复杂,但只需几行代码就可以轻松实现。我们建议读者首先阅读以下包含DrawPoints类的代码,并尝试从window = Gtk.Window()开始追溯逻辑。
下面是代码的详细解释:
#Same old, importing Gtk module, we are also importing some other stuff this time
#such as numpy and the backends of matplotlib
import gi, numpy as np, matplotlib.cm as cm
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
#From here, we are importing some essential backend tools from matplotlib
#namely the NavigationToolbar2GTK3 and the FigureCanvasGTK3Agg
from numpy import random
from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3 as NavigationToolbar
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle
#Here we created a class named DrawPoints
class DrawPoints:
#Upon initiation, we create 4 randomized numpy array, those are for the coordinates, colors and size of dots
#on the scatter plot. After that we create a figure object, put in two subplots and create a canvas to store
#the figure.
def __init__(self):
#Namely we are creating 20 dots, therefore n = 20
self.n = 20
#X and Y coordinates
self.xrand = random.rand(1,self.n)*10
self.yrand = random.rand(1,self.n)*10
#Sizes
self.randsize = random.rand(1,self.n)*200
#Colors
self.randcolor = random.rand(self.n,3)
#Here creates the figure, with a size 10x10 and resolution of 80dpi
self.fig = Figure(figsize=(10,10), dpi=80)
#Stating that we are creating two plots side by side and adding
#self.ax as the first plot by add_subplot(121)
self.ax = self.fig.add_subplot(121)
#Adding the second subplot by stating add_subplot(122)
self.axzoom = self.fig.add_subplot(122)
#Create a canvas to store the figure object
self.canvas = FigureCanvas(self.fig)
#Here draw the scatterplot on the left
def draw(self):
#Here is the key - cla(), when we invoke the draw() function, we have to clear the
#figure and redraw it again
self.ax.cla()
#Setting the elements of the left subplot, in this case - grid
self.ax.grid(True)
#Set the maximum value of X and Y-axis in the left subplot
self.ax.set_xlim(0,10)
self.ax.set_ylim(0,10)
#Draw the scatter plot with the randomized numpy array that we created earlier in __init__(self)
self.ax.scatter(self.xrand, self.yrand, marker='o', s=self.randsize, c=self.randcolor, alpha=0.5)
#This zoom function is invoked by updatezoom() function outside of the class Drawpoints
#This function is responsible for things:
#1\. Update X and Y coordinates based on the click
#2\. invoke the draw() function to redraw the plot on the left, this is essential to update the position
# of the grey rectangle
#3\. invoke the following drawzoom() function, which will "Zoom-in" the designated area by the grey rectangle
# and will redraw the subplot on the right based on the updated X & Y coordinates
#4\. draw a transparent grey rectangle based on the mouse click on the left subplot
#5\. Update the canvas
def zoom(self, x, y):
#Here updates the X & Y coordinates
self.x = x
self.y = y
#invoke the draw() function to update the subplot on the left
self.draw()
#invoke the drawzoom() function to update the subplot on the right
self.drawzoom()
#Draw the transparent grey rectangle at the subplot on the left
self.ax.add_patch(Rectangle((x - 1, y - 1), 2, 2, facecolor="grey", alpha=0.2))
#Update the canvas
self.fig.canvas.draw()
#This drawzoom function is being called in the zoom function
#The idea is that, when the user picked a region (rectangle) to zoom, we need to redraw the zoomed panel,
#which is the subplot on the right
def drawzoom(self):
#Again, we use the cla() function to clear the figure, and getting ready for a redraw!
self.axzoom.cla()
#Setting the grid
self.axzoom.grid(True)
#Do not be confused! Remember that we invoke this function from zoom, therefore self.x and self.y
#are already updated in that function. In here, we are simply changing the X & Y-axis minimum and
#maximum value, and redraw the graph without changing any element!
self.axzoom.set_xlim(self.x-1, self.x+1)
self.axzoom.set_ylim(self.y-1, self.y+1)
#By changing the X & Y-axis minimum and maximum value, the dots that are out of range will automatically
#disappear!
self.axzoom.scatter(self.xrand, self.yrand, marker='o', s=self.randsize*5, c=self.randcolor, alpha=0.5)
def updatecursorposition(event):
'''When cursor inside plot, get position and print to statusbar'''
if event.inaxes:
x = event.xdata
y = event.ydata
statbar.push(1, ("Coordinates:" + " x= " + str(round(x,3)) + " y= " + str(round(y,3))))
def updatezoom(event):
'''When mouse is right-clicked on the canvas get the coordiantes and send them to points.zoom'''
if event.button!=1: return
if (event.xdata is None): return
x,y = event.xdata, event.ydata
points.zoom(x,y)
#Readers should be familiar with this now, here is the standard opening of the Gtk.Window()
window = Gtk.Window()
window.connect("delete-event", Gtk.main_quit)
window.set_default_size(800, 500)
window.set_title('Interactive zoom')
#Creating a vertical box, will have the canvas, toolbar and statbar being packed into it from top to bottom
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
#Adding the vertical box to the window
window.add(box)
#Instantiate the object points from the Class DrawPoints()
#Remember that at this point, __init__() of DrawPoints() are invoked upon construction!
points = DrawPoints()
#Invoke the draw() function in the object points
points.draw()
#Packing the canvas now to the vertical box
box.pack_start(points.canvas, True, True, 0)
#Creating and packing the toolbar to the vertical box
toolbar = NavigationToolbar(points.canvas, window)
box.pack_start(toolbar, False, True, 0)
#Creating and packing the statbar to the vertical box
statbar = Gtk.Statusbar()
box.pack_start(statbar, False, True, 0)
#Here is the magic that makes it happens, we are using mpl_connect to link the event and the canvas!
#'motion_notify_event' is responsible for the mouse motion sensing and position updating
points.fig.canvas.mpl_connect('motion_notify_event', updatecursorposition)
#'button_press_event' is slightly misleading, in fact it is referring to the mouse button being pressed,
#instead of a GTK+3 button being pressed in this case
points.fig.canvas.mpl_connect('button_press_event', updatezoom)
window.show_all()
Gtk.main()
正如您从前面的例子中看到的,事件处理和选取是使交互部分比我们想象中更容易的元素。因此,重要的是快速回顾一下FigureCanvasBase中可用的事件连接。
| 事件名称 | 类和描述 |
|---|---|
button_press_event |
鼠标事件: 鼠标按下按钮 |
button_release_event |
MouseEvent: 鼠标按钮被释放 |
scroll_event |
MouseEvent: 鼠标滚轮被滚动 |
motion_notify_event |
MouseEvent: 鼠标移动 |
draw_event |
DrawEvent: 画布绘制 |
key_press_event |
KeyEvent: 键被按下 |
key_release_event |
KeyEvent: 键被释放 |
pick_event |
PickEvent: 画布中的一个对象被选中 |
resize_event |
ResizeEvent: 图形画布被调整大小 |
figure_enter_event |
LocationEvent: 鼠标进入一个新图形 |
figure_leave_event |
LocationEvent: 鼠标离开一个图形 |
axes_enter_event |
LocationEvent: 鼠标进入一个新轴 |
axes_leave_event |
LocationEvent: 鼠标离开一个轴 |
安装 Glade
安装 Glade 非常简单;你可以从其网页上获取源文件,或者直接使用 Git 获取最新版本的源代码。通过 Git 获取 Glade 的命令如下:
git clone git://git.gnome.org/glade
使用 Glade 设计 GUI
使用 Glade 设计 GUI 非常简单。只需启动 Glade 程序,你将看到这个界面(从 macOS 上显示,或者如果使用其他操作系统,则会看到类似的界面):

图 5
现在让我们来看看 Glade 界面。我们将主要使用四个按钮:顶级窗口、容器、控制和显示。前面的截图显示,GtkWindow 列在了 顶级窗口 中,它作为构建的基本单元。点击 GtkWindow,看看会发生什么:

图 6
现在一个 GtkWindow 正在构建,但里面没有任何内容。让我们将这个 GtkWindow 的大小设置为:400x400。可以通过在右侧面板的下方设置默认宽度和高度为 400 来实现。右侧面板当前展示的是该 GtkWindow 的常规属性。
还记得我们在之前的示例中使用了很多垂直框吗?现在让我们在 GtkWindow 中添加一个垂直框!可以通过点击容器并选择 GtkBox 来实现,正如下图所示:

图 7
选择 GtkBox 后,点击中间面板中的 GtkWindow,GtkBox 将作为 GtkWindow 的子模块或子窗口创建。可以通过检查左侧面板来确认这一点,正如下图所示:

图 8
GtkBox 位于 GtkWindow 下方,并且在左侧面板中有缩进。由于我们选择了垂直框,所以在常规设置中,方向为垂直。你还可以指定 GtkBox 中包含的间距和项数。现在让我们在顶部垂直框中添加一个菜单栏。可以参考图 9中的操作方法。在容器中,选择 GtkMenubar 并点击顶部垂直框。它将添加一个菜单栏,其中包含以下选项:文件、编辑、视图和帮助。

图 9
如同大家可以想象的,我们可以轻松地使用 Glade 设计我们喜欢的 GUI。我们可以导入一个具有自定义大小的标签,如图 10所示。还有许多其他选项,我们可以选择来自定义我们的 GUI。

图 10
通过 Glade 设计最有效的 GUI 超出了本书的范围,因此我们不会进一步探讨 Glade 中的高级选项。
然而,我们想扩展我们之前处理的一个示例,并展示将基于 Glade 的 GUI 融入到我们的工作流程中只需要几行代码。
首先,我们将使用基于类的极坐标图示例。首先,我们通过 Glade 设计最基本的 GtkWindow,大小为 400x400(就这样!),并将其保存为文件。
该文件非常简单且易于理解:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<property name="default_width">400</property>
<property name="default_height">400</property>
<signal name="destroy" handler="on_window1_destroy" swapped="no"/>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</interface>
读者可能理解我们只是创建了一个大小为 400x400 的 GtkWindow,并且添加了一个作为 GtkScrolledWindow 的子元素。这可以在 Glade 中通过几次点击完成。
现在我们要做的是使用 Gtk.Builder() 来读取 Glade 文件;一切都会自动构建。实际上,这为我们节省了定义垂直框架所有元素的工作!
#Same old, importing Gtk module, we are also importing some other stuff this time
#such as numpy and the backends of matplotlib
import gi, numpy as np, matplotlib.cm as cm
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from matplotlib.figure import Figure
from numpy import arange, pi, random, linspace
import matplotlib.cm as cm
#Possibly this rendering backend is broken currently
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
#New class, here is to invoke Gtk.main_quit() when the window is being destroyed
#Necessary to quit the Gtk.main()
class Signals:
def on_window1_destroy(self, widget):
Gtk.main_quit()
class MatplotlibEmbed(Gtk.Window):
#Instantiation, we just need the canvas to store the figure!
def __init__(self):
#Readers should find it familiar, as we are creating a matplotlib figure here with a dpi(resolution) 100
self.fig = Figure(figsize=(5,5), dpi=100)
#The axes element, here we indicate we are creating 1x1 grid and putting the subplot in the only cell
#Also we are creating a polar plot, therefore we set projection as 'polar
self.ax = self.fig.add_subplot(111, projection='polar')
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.width = pi / 4 * random.rand(30)
self.bars = self.ax.bar(self.theta, self.radii, width=self.width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.ax.plot()
#Creating Canvas which store the matplotlib figure
self.canvas = FigureCanvas(self.fig) # a Gtk.DrawingArea
#Here is the magic, we create a GTKBuilder that reads textual description of a user interface
#and instantiates the described objects
builder = Gtk.Builder()
#We ask the GTKBuilder to read the file and parse the information there
builder.add_objects_from_file('/Users/aldrinyim/Dropbox/Matplotlib for Developer/Jupyter notebook/ch05/window1_glade.glade', ('window1', '') )
#And we connect the terminating signals with Gtk.main_quit()
builder.connect_signals(Signals())
#We create the first object window1
window1 = builder.get_object('window1')
#We create the second object scrollwindow
scrolledwindow1 = builder.get_object('scrolledwindow1')
#Instantiate the object and start the drawing!
polar_drawing = MatplotlibEmbed()
#Add the canvas to the scrolledwindow1 object
scrolledwindow1.add(polar_drawing.canvas)
#Show all and keep the Gtk.main() active!
window1.show_all()
Gtk.main()
前面的代码演示了我们如何使用 Glade 快速生成一个框架并轻松执行它。

图 11
希望通过这个示例,读者能更好地理解 Glade 的强大功能,它使程序员能够通过可视化的方式来设计 GUI,而不是通过代码抽象化。这在 GUI 变得复杂时特别有用。
总结
在本章中,我们通过实例讲解了如何将 Matplotlib 图形嵌入到简单的 GTK+3 窗口中,添加 Matplotlib 导航工具栏,在交互式框架中绘制数据,以及使用 Glade 设计 GUI。我们保持了实例的简洁,以突出重点部分,但我们鼓励读者进一步探索更多可能性。GTK+3 不是唯一可以使用的 GUI 库,在接下来的章节中,我们将看到如何使用另外两个重要的库!
第六章:在 Qt 5 中嵌入 Matplotlib
有多种 GUI 库可供选择,其中一个广泛使用的库是 Qt。在本书中,我们将使用 Qt 5,这是该库的最新主要版本。除非明确提及,否则我们在本章节中提到的 Qt 都是指 Qt 5。
我们将遵循与 第五章 在 GTK+3 中嵌入 Matplotlib 类似的进度,展示类似的示例,但这次是用 Qt 编写的。
我们认为这种方法将使我们能够直接比较各个库,并且它的优点是不会留下 我如何使用库 X 编写某个东西? 这个问题没有答案。
在本章中,我们将学习如何:
-
将 Matplotlib 图形嵌入到 Qt 小部件中
-
将图形和导航工具栏嵌入到 Qt 小部件中
-
使用事件实时更新 Matplotlib 图形
-
使用 QT Designer 绘制 GUI,然后在简单的 Python 应用程序中与 Matplotlib 一起使用
我们将从对该库的介绍开始。
Qt 5 和 PyQt 5 的简要介绍
Qt 是一个跨平台的应用程序开发框架,广泛用于图形程序(GUI)以及非图形工具。
Qt 由 Trolltech(现为诺基亚所有)开发,可能最著名的是作为 K 桌面环境 (KDE) 的基础,KDE 是 Linux 的桌面环境。
Qt 工具包是一个类集合,旨在简化程序的创建。Qt 不仅仅是一个 GUI 工具包,它还包括用于网络套接字、线程、Unicode、正则表达式、SQL 数据库、SVG、OpenGL 和 XML 的抽象组件。它还具有一个完全功能的 Web 浏览器、帮助系统、多媒体框架以及丰富的 GUI 小部件集合。
Qt 可在多个平台上使用,尤其是 Unix/Linux、Windows、macOS X,以及一些嵌入式设备。由于它使用平台的原生 API 来渲染 Qt 控件,因此使用 Qt 开发的应用程序具有适合运行环境的外观和感觉(而不会看起来像是外来物)。
尽管 Qt 是用 C++ 编写的,但通过可用于 Ruby、Java、Perl 以及通过 PyQt 也支持 Python,Qt 也可以在多个其他编程语言中使用。
PyQt 5 可用于 Python 2.x 和 3.x,但在本书中,我们将在所有代码中一致使用 Python 3。PyQt 5 包含超过 620 个类和 6,000 个函数和方法。在我们进行一些示例之前,了解 Qt 4/PyQt 4 和 Qt 5/PyQt 5 之间的区别是很重要的。
Qt 4 和 PyQt 4 之间的区别
PyQt 是 Qt 框架的全面 Python 绑定集。然而,PyQt 5 与 PyQt 4 不兼容。值得注意的是,PyQt 5 不支持 Qt v5.0 中标记为弃用或过时的任何 Qt API。尽管如此,可能会偶尔包含一些这些 API。如果包含了它们,它们会被视为错误,并在发现时被删除。
如果你熟悉 Qt 4 或者已经读过本书的第一版,需要注意的是,信号与槽的机制在 PyQt 5 中已不再被支持。因此,以下内容在 PyQt 5 中未实现:
-
QtScript -
QObject.connect() -
QObject.emit() -
SIGNAL() -
SLOT()
此外,disconnect()也做了修改,调用时不再需要参数,且会断开所有与QObject实例的连接。
然而,已经引入了新模块,如下所示:
-
QtBluetooth -
QtPositioning -
Enginio
让我们从一个非常简单的例子开始——调用一个窗口。同样,为了获得最佳性能,请复制代码,将其粘贴到文件中,并在终端中运行脚本。我们的代码仅优化用于在终端中运行:
#sys.argv is essential for the instantiation of QApplication!
import sys
#Here we import the PyQt 5 Widgets
from PyQt5.QtWidgets import QApplication, QWidget
#Creating a QApplication object
app = QApplication(sys.argv)
#QWidget is the base class of all user interface objects in PyQt5
w = QWidget()
#Setting the width and height of the window
w.resize(250, 150)
#Move the widget to a position on the screen at x=500, y=500 coordinates
w.move(500, 500)
#Setting the title of the window
w.setWindowTitle('Simple')
#Display the window
w.show()
#app.exec_() is the mainloop of the application
#the sys.exit() is a method to ensure a real exit upon receiving the signal of exit from the app
sys.exit(app.exec_())

语法与你在第五章中看到的将 Matplotlib 嵌入 GTK+3非常相似。一旦你对某个特定的 GUI 库(例如 GTK+3)掌握得比较好,就可以很容易地适应新的 GUI 库。代码与 GTK+3 非常相似,逻辑也跟着走。QApplication管理 GUI 应用程序的控制流和主要设置。它是主事件循环执行、处理和分发的地方。它还负责应用程序的初始化和最终化,并处理大多数系统级和应用级的设置。由于QApplication处理整个初始化阶段,因此必须在创建与 UI 相关的任何其他对象之前创建它。
qApp.exec_()命令进入 Qt 主事件循环。一旦调用了exit()或quit(),它就会返回相关的返回码。在主循环开始之前,屏幕上不会显示任何内容。调用此函数是必要的,因为主循环处理来自应用程序小部件和窗口系统的所有事件和信号;本质上,在调用之前无法进行任何用户交互。
读者可能会疑惑,为什么exec_();中有一个下划线。原因很简单:exec()是 Python 中的保留字,因此在exec()的 Qt 方法中加了下划线。将其包装在sys.exit()内,可以让 Python 脚本以相同的返回码退出,告知环境应用程序的结束状态(无论是成功还是失败)。
对于经验更丰富的读者,你会发现前面的代码中有些不寻常的地方。当我们实例化QApplication类时,需要将sys.argv(在此情况下是一个空列表)传递给QApplication的构造函数。至少当我第一次使用 PyQt 时,这让我感到意外,但这是必须的,因为实例化会调用 C++类QApplication的构造函数,并且它使用sys.argv来初始化 Qt 应用程序。在QApplication实例化时解析sys.argv是 Qt 中的一种约定,需要特别注意。另外,每个 PyQt 5 应用程序必须创建一个应用程序对象。
再次尝试以面向对象编程(OOP)风格编写另一个示例:
#Described in earlier examples
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QApplication
#Here we create a class with the "base" class from QWidget
#We are inheriting the functions of the QWidget from this case
class Qtwindowexample(QWidget):
#Constructor, will be executed upon instantiation of the object
def __init__(self):
#Upon self instantiation, we are calling constructor of the QWidget
#to set up the bases of the QWidget's object
QWidget.__init__(self)
#Resizing, moving and setting the window
self.resize(250, 150)
self.move(300, 300)
self.setWindowTitle('2 Click buttons!')
#Here we create the first button - print1button
#When clicked, it will invoke the printOnce function, and print "Hello world" in the terminal
self.print1button = QPushButton('Print once!', self)
self.print1button.clicked.connect(self.printOnce)
#Here we create the second button - print5button
#When clicked, it will invoke the printFive function, and print "**Hello world" 5 times in the terminal
self.print5button = QPushButton('Print five times!', self)
self.print5button.clicked.connect(self.printFive)
#Something very familiar!
#It is the vertical box in Qt5
self.vbox=QVBoxLayout()
#Simply add the two buttons to the vertical box
self.vbox.addWidget(self.print1button)
self.vbox.addWidget(self.print5button)
#Here put the vertical box into the window
self.setLayout(self.vbox)
#And now we are all set, show the window!
self.show()
#Function that will print Hello world once when invoked
def printOnce(self):
print("Hello World!")
#Function that will print **Hello world five times when invoked
def printFive(self):
for i in range(0,5):
print("**Hello World!")
#Creating the app object, essential for all Qt usage
app = QApplication(sys.argv)
#Create Qtwindowexample(), construct the window and show!
ex = Qtwindowexample()
#app.exec_() is the mainloop of the application
#the sys.exit() is a method to ensure a real exit upon receiving the signal of exit from the app
sys.exit(app.exec_())
上述代码创建了两个按钮,每个按钮都会调用一个独立的函数——在终端中打印一次 Hello world 或打印五次 Hello World。读者应该能轻松理解代码中的事件处理系统。
这是输出结果:

这是来自第五章的另一个两个按钮示例,将 Matplotlib 嵌入 GTK+3,这个示例的目的是展示在 PyQt 5 中的信号处理方法,并与 GTK+3 进行对比。读者应该会发现它非常相似,因为我们故意将其写得更接近 GTK+3 示例。
让我们尝试将 Matplotlib 图形嵌入 Qt 窗口。请注意,与上一章的示例不同,这个图形将每秒刷新一次!因此,我们也在这里使用了 QtCore.QTimer() 函数,并将 update_figure() 函数作为事件-动作对进行调用:
#Importing essential libraries
import sys, os, random, matplotlib, matplotlib.cm as cm
from numpy import arange, sin, pi, random, linspace
#Python Qt5 bindings for GUI objects
from PyQt5 import QtCore, QtWidgets
# import the Qt5Agg FigureCanvas object, that binds Figure to
# Qt5Agg backend.
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
#The class DynamicCanvas contains all the functions required to draw and update the figure
#It contains a canvas that updates itself every second with newly randomized vecotrs
class DynamicCanvas(FigureCanvas):
#Invoke upon instantiation, here are the arguments parsing along
def __init__(self, parent=None, width=5, height=4, dpi=100):
#Creating a figure with the requested width, height and dpi
fig = Figure(figsize=(width,height), dpi=dpi)
#The axes element, here we indicate we are creating 1x1 grid and putting the subplot in the only cell
#Also we are creating a polar plot, therefore we set projection as 'polar
self.axes = fig.add_subplot(111, projection='polar')
#Here we invoke the function "compute_initial_figure" to create the first figure
self.compute_initial_figure()
#Creating a FigureCanvas object and putting the figure into it
FigureCanvas.__init__(self, fig)
#Setting this figurecanvas parent as None
self.setParent(parent)
#Here we are using the Qtimer function
#As you can imagine, it functions as a timer and will emit a signal every N milliseconds
#N is defined by the function QTimer.start(N), in this case - 1000 milliseconds = 1 second
#For every second, this function will emit a signal and invoke the update_figure() function defined below
timer = QtCore.QTimer(self)
timer.timeout.connect(self.update_figure)
timer.start(1000)
#For drawing the first figure
def compute_initial_figure(self):
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.axes.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.axes.plot()
#This function will be invoke every second by the timeout signal from QTimer
def update_figure(self):
#Clear figure and get ready for the new plot
self.axes.cla()
#Identical to the code above
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.axes.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.axes.plot()
self.draw()
#This class will serve as our main application Window
#QMainWindow class provides a framework for us to put window and canvas
class ApplicationWindow(QtWidgets.QMainWindow):
#Instantiation, initializing and setting up the framework for the canvas
def __init__(self):
#Initializing of Qt MainWindow widget
QtWidgets.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
#Instantiating QWidgets object
self.main_widget = QtWidgets.QWidget(self)
#Creating a vertical box!
vbox = QtWidgets.QVBoxLayout(self.main_widget)
#Creating the dynamic canvas and this canvas will update itself!
dc = DynamicCanvas(self.main_widget, width=5, height=4, dpi=100)
#adding canvas to the vertical box
vbox.addWidget(dc)
#This is not necessary, but it is a good practice to setFocus on your main widget
self.main_widget.setFocus()
#This line indicates that main_widget is the main part of the application
self.setCentralWidget(self.main_widget)
#Creating the GUI application
qApp = QtWidgets.QApplication(sys.argv)
#Instantiating the ApplicationWindow widget
aw = ApplicationWindow()
#Set the title
aw.setWindowTitle("Dynamic Qt5 visualization")
#Show the widget
aw.show()
#Start the Qt main loop , and sys.exit() ensure clean exit when closing the window
sys.exit(qApp.exec_())
同样,本示例中的图形会通过 QTimer 随机化数据,并每秒更新一次,具体如下:

引入 QT Creator / QT Designer
上面四个图形是 PyQt 5 窗口的截图,它会每秒刷新一次。
对于简单的示例,直接在 Python 代码中设计 GUI 已经足够,但对于更复杂的应用程序,这种解决方案无法扩展。
有一些工具可以帮助你为 Qt 设计 GUI,其中一个最常用的工具是 QT Designer。在本书的第一版中,本部分讲述的是如何使用 QT Designer 制作 GUI。自从 QT4 后期开发以来,QT Designer 已经与 QT Creator 合并。在接下来的示例中,我们将学习如何在 QT Creator 中打开隐藏的 QT Designer 并创建一个 UI 文件。
类似于 Glade,我们可以通过屏幕上的表单和拖放界面设计应用程序的用户界面。然后,我们可以将小部件与后端代码连接,在那里我们开发应用程序的逻辑。
首先,让我们展示如何在 QT Creator 中打开 QT Designer。当你打开 QT Creator 时,界面如下所示:

难点在于:不要通过点击 Creator 中的“新建文件”或“新建项目”按钮来创建项目。相反,选择“新建项目”:

在文件和类中选择 Qt,并在中间面板中选择 Qt Designer Form:

有一系列模板选择,如 Widget 或 Main Window。在我们的例子中,我们选择 Main Window,并简单地按照其余步骤进行操作:

最终,我们将进入 QT Designer 界面。你在这里做的所有工作将被保存到你指定的文件夹中,作为 UI 文件:

在使用 QT Creator / QT Designer 制作的 GUI 中嵌入 Matplotlib。
为了快速演示如何使用 QT Creator 在 Qt 5 中嵌入 Matplotlib 图形,我们将使用前面的例子,并将其与 QT Creator 生成的脚本结合起来。
首先,在右下角面板调整 MainWindow 的 Geometry;将宽度和高度改为 300x300:

然后,从左侧面板的 Container 中拖动一个 Widget 到中间的 MainWindow 中。调整大小,直到它恰好适合 MainWindow:

基本设计就是这样!现在将其保存为 UI 文件。当你查看 UI 文件时,它应该显示如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="widget" native="true">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>261</width>
<height>221</height>
</rect>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
这个文件是 XML 格式的,我们需要将它转换为 Python 文件。可以简单地通过使用以下命令来完成:
pyuic5 mainwindow.ui > mainwindow.py
现在我们将得到一个像这样的 Python 文件:
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(300, 300)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.widget = QtWidgets.QWidget(self.centralwidget)
self.widget.setGeometry(QtCore.QRect(20, 10, 261, 221))
self.widget.setObjectName("widget")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 300, 22))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
请注意,这只是 GUI 的框架;我们仍然需要添加一些内容才能使其正常工作。
我们必须添加init()来初始化UiMainWindow,并将DynamicCanvas与MainWindow中间的 widget 连接起来。具体如下:
#Replace object to QtWidgets.QMainWindow
class Ui_MainWindow(QtWidgets.QMainWindow):
#***Instantiation!
def __init__(self):
# Initialize and display the user interface
QtWidgets.QMainWindow.__init__(self)
self.setupUi(self)
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(300, 300)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.widget = QtWidgets.QWidget(self.centralwidget)
self.widget.setGeometry(QtCore.QRect(20, 10, 261, 221))
self.widget.setObjectName("widget")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 300, 22))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
#***Putting DynamicCanvas into the widget, and show the window!
dc = DynamicCanvas(self.widget, width=5, height=4, dpi=100)
self.show()
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
我们在这里只添加了五行代码。我们可以简单地用这个替换ApplicationWindow类,最终的结果如下:

这是生成上述图形的完整代码:
#Importing essential libraries
import sys, os, random, matplotlib, matplotlib.cm as cm
from numpy import arange, sin, pi, random, linspace
#Python Qt5 bindings for GUI objects
from PyQt5 import QtCore, QtGui, QtWidgets
# import the Qt5Agg FigureCanvas object, that binds Figure to
# Qt5Agg backend.
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
#The class DynamicCanvas contains all the functions required to draw and update the figure
#It contains a canvas that updates itself every second with newly randomized vecotrs
class DynamicCanvas(FigureCanvas):
#Invoke upon instantiation, here are the arguments parsing along
def __init__(self, parent=None, width=5, height=5, dpi=100):
#Creating a figure with the requested width, height and dpi
fig = Figure(figsize=(width,height), dpi=dpi)
#The axes element, here we indicate we are creating 1x1 grid and putting the subplot in the only cell
#Also we are creating a polar plot, therefore we set projection as 'polar
self.axes = fig.add_subplot(111, projection='polar')
#Here we invoke the function "compute_initial_figure" to create the first figure
self.compute_initial_figure()
#Creating a FigureCanvas object and putting the figure into it
FigureCanvas.__init__(self, fig)
#Setting this figurecanvas parent as None
self.setParent(parent)
#Here we are using the Qtimer function
#As you can imagine, it functions as a timer and will emit a signal every N milliseconds
#N is defined by the function QTimer.start(N), in this case - 1000 milliseconds = 1 second
#For every second, this function will emit a signal and invoke the update_figure() function defined below
timer = QtCore.QTimer(self)
timer.timeout.connect(self.update_figure)
timer.start(1000)
#For drawing the first figure
def compute_initial_figure(self):
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.axes.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.axes.plot()
#This function will be invoke every second by the timeout signal from QTimer
def update_figure(self):
#Clear figure and get ready for the new plot
self.axes.cla()
#Identical to the code above
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.axes.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.axes.plot()
self.draw()
#Created by Qt Creator!
class Ui_MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# Initialize and display the user interface
QtWidgets.QMainWindow.__init__(self)
self.setupUi(self)
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(550, 550)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.widget = QtWidgets.QWidget(self.centralwidget)
self.widget.setGeometry(QtCore.QRect(20, 10, 800, 800))
self.widget.setObjectName("widget")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 300, 22))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
dc = DynamicCanvas(self.widget, width=5, height=5, dpi=100)
#self.centralwidget.setFocus()
#self.setCentralWidget(self.centralwidget)
self.show()
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
#Creating the GUI application
qApp = QtWidgets.QApplication(sys.argv)
#Instantiating the ApplicationWindow widget
aw = Ui_MainWindow()
#Start the Qt main loop , and sys.exit() ensure clean exit when closing the window
sys.exit(qApp.exec_())
总结
使用 QT Creator / QT Designer 进行 GUI 设计本身就有足够的内容可以写成一本书。因此,在本章中,我们旨在通过 PyQt 5 向你展示 GUI 设计的冰山一角。完成本章后,读者应能理解如何在 QWidget 中嵌入图形,使用布局管理器将图形放入 QWidget 中,创建计时器,响应事件并相应更新 Matplotlib 图形,以及使用 QT Designer 为 Matplotlib 嵌入绘制一个简单的 GUI。
我们现在准备学习另一个 GUI 库,wxWidgets。
第七章:使用 wxPython 将 Matplotlib 嵌入 wxWidgets
本章将解释如何在 wxWidgets 框架中使用 Matplotlib,特别是通过 wxPython 绑定。
本章内容如下:
-
wxWidgets 和 wxPython 的简要介绍
-
嵌入 Matplotlib 到 wxWidgets 的一个简单示例
-
将前一个示例扩展,包含 Matplotlib 导航工具栏
-
如何使用 wxWidgets 框架实时更新 Matplotlib 图表
-
如何使用 wxGlade 设计 GUI 并将 Matplotlib 图形嵌入其中
让我们从 wxWidgets 和 wxPython 的特点概述开始。
wxWidgets 和 wxPython 的简要介绍
wxWidgets 最重要的特性之一是跨平台的可移植性;它目前支持 Windows、macOS X、Linux(支持 X11、Motif 和 GTK+ 库)、OS/2 和多个其他操作系统与平台(包括正在开发中的嵌入式版本)。
wxWidgets 最好描述为一种本地模式工具包,因为它在各个平台之间提供了一个薄的 API 抽象层,并且在后台使用平台本地的控件,而不是模拟它们。使用本地控件使得 wxWidgets 应用程序具有自然且熟悉的外观和感觉。另一方面,引入额外的层次可能会导致轻微的性能损失,尽管在我们常开发的应用程序中,这种损失不太容易察觉。
wxWidgets 并不仅限于 GUI 开发。它不仅仅是一个图形工具包,还提供了一整套额外的功能,如数据库库、进程间通信层、网络功能等。虽然它是用 C++ 编写的,但有许多绑定可供多种常用编程语言使用。其中包括由 wxPython 提供的 Python 绑定。
wxPython(可在 www.wxpython.org/ 获取)是一个 Python 扩展模块,提供了来自 wxWidgets 库的 Python 语言绑定。这个扩展模块允许 Python 程序员创建 wxWidgets 类的实例,并调用这些类的方法。
现在是引入 wxPython 的好时机,因为 wxPython 4 在一年前发布了。到目前为止(2018 年 4 月),wxPython 的最新版本是 4.0.1,并且它与 Python 2 和 Python 3 都兼容。
从 2010 年开始,凤凰计划是清理 wxPython 实现并使其兼容 Python 3 的努力。正如大家所想,wxPython 完全重写,重点放在了性能、可维护性和可扩展性上。
让我们走一遍使用 wxPython 的最基本示例!
#Here imports the wxPython library
import wx
#Every wxPython app is an instance of wx.App
app = wx.App(False)
#Here we create a wx.Frame() and specifying it as a top-level window
#by stating "None" as a parent object
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
#Show the frame!
frame.Show(True)
#Start the applciation's MainLoop, and ready for events handling
app.MainLoop()

在前面的示例基础上,有一件非常重要的事情是初学者需要注意的。
wx.Frame和wx.Window()是非常不同的。wx.Window是 wxWidgets 中所有视觉元素的基类,如按钮和菜单;在 wxWidgets 中,我们通常将程序窗口称为wx.Frame。
构造wx.Frame的语法为wx.Frame(Parent, ID, Title)。当将Parent指定为None时,如刚才所示,我们实际上是在说这个框架是一个顶级window。
wxWidgets 中还有一个ID 系统。各种控件和 wxWidgets 的其他部分都需要一个 ID。有时,ID 由用户提供;另外,ID 也有预定义的值。然而,在大多数情况下(如前面的示例),ID 的值并不重要,我们可以使用wx.ID_ANY作为对象的 ID,告诉 wxWidgets 自动分配 ID。请记住,所有自动分配的 ID 都是负数,而用户定义的 ID 应始终为正数,以避免与自动分配的 ID 冲突。
现在,让我们来看一个需要事件处理的面向对象风格的示例——Hello world按钮示例:
#Here imports the wxPython library
import wx
#Here is the class for the Frame inheriting from wx.Frame
class MyFrame(wx.Frame):
#Instantiation based on the constructor defined below
def __init__(self, parent):
#creating the frame object and assigning it to self
wx.Frame.__init__(self, parent, wx.ID_ANY)
#Create panel
self.panel = wx.Panel(self)
#wx.BoxSizer is essentially the vertical box,
#and we will add the buttons to the BoxSizer
self.sizer = wx.BoxSizer(wx.VERTICAL)
#Creating button 1 that will print Hello World once
self.button1 = wx.Button(self.panel,label="Hello World!")
#Create button 2 that will print Hello World twice
self.button2 = wx.Button(self.panel,label="Hello World 5 times!")
#There are two ways to bind the button with the event, here is method 1:
self.button1.Bind(wx.EVT_BUTTON, self.OnButton1)
self.button2.Bind(wx.EVT_BUTTON, self.OnButton2)
#Here is method 2:
#self.Bind(wx.EVT_BUTTON, self.OnButton1, self.button1)
#self.Bind(wx.EVT_BUTTON, self.OnButton2, self.button2)
#Here we add the button to the BoxSizer
self.sizer.Add(self.button1,0,0,0)
self.sizer.Add(self.button2,0,0,0)
#Put sizer into panel
self.panel.SetSizer(self.sizer)
#function that will be invoked upon pressing button 1
def OnButton1(self,event):
print("Hello world!")
#function that will be invoked upon pressing button 2
def OnButton2(self,event):
for i in range(0,5):
print("Hello world!")
#Every wxPython app is an instance of wx.App
app = wx.App()
#Here we create a wx.Frame() and specifying it as a top-level window
#by stating "None" as a parent object
frame = MyFrame(None)
#Show the frame!
frame.Show()
#Start the applciation's MainLoop, and ready for events handling
app.MainLoop()
输出结果如下:

正如读者可能注意到的,我们讨论过的所有三个 GUI 库的语法都很相似。因此,熟悉其中一个库后,你可以轻松地在它们之间切换。
wxWidgets 的布局管理器是sizer小部件:它们是小部件(包括其他 sizer)的容器,根据我们的配置处理小部件尺寸的视觉排列。BoxSizer接受一个参数,表示其方向。在这种情况下,我们传递常量wx.VERTICAL来将小部件按列排列;如果需要一排小部件,也可以使用常量wx.HORIZONTAL:
self.sizer.Add(self.button1, 1, wx.LEFT | wx.TOP | wx.EXPAND)
我们现在能够将FigureCanvas对象添加到sizer中了。Add()函数的参数非常重要:
-
第一个参数是要添加的对象的引用。
-
然后,我们有第二个参数——比例。这个值用于表示应将多少额外的空闲空间分配给此小部件。通常,GUI 中的小部件不会占据所有空间,因此会有一些额外的空闲空间可用。这些空间会根据每个小部件与 GUI 中所有小部件的比例值进行重新分配。举个例子:如果我们有三个小部件,它们的比例分别为
0、1和2,那么第一个(比例为0)将完全不变化。第三个(比例为2)将比第二个(比例为1)变化两倍。在书中的示例中,我们将比例设置为1,因此我们声明该小部件在调整大小时应占用一份空闲空间。 -
第三个参数是一个标志组合,用于进一步配置在
sizer中小部件的行为。它控制边框、对齐、各小部件之间的间隔以及扩展。在这里,我们声明FigureCanvas应在窗口调整大小时进行扩展。
让我们尝试一个示例,将 Matplotlib 图形(极坐标图)嵌入到由 wxWidgets 支持的 GUI 中:
#Specifying that we are using WXAgg in matplotlib
import matplotlib
matplotlib.use('WXAgg')
#Here imports the wxPython and other supporting libraries
import wx, sys, os, random, matplotlib, matplotlib.cm as cm, matplotlib.pyplot as plt
from numpy import arange, sin, pi, random, linspace
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure
class MyFrame(wx.Frame):
def __init__(self):
# Initializing the Frame
wx.Frame.__init__(self, None, -1, title="", size=(600,500))
#Create panel
panel = wx.Panel(self)
#Here we prepare the figure, canvas and axes object for the graph
self.fig = Figure(figsize=(6,4), dpi=100)
self.canvas = FigureCanvas(self, -1, self.fig)
self.ax = self.fig.add_subplot(111, projection='polar')
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.ax.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we generate the figure
self.ax.plot()
#Creating the vertical box of wxPython
self.vbox = wx.BoxSizer(wx.VERTICAL)
#Add canvas to the vertical box
self.vbox.Add(self.canvas, wx.ALIGN_CENTER|wx.ALL, 1)
#Add vertical box to the panel
self.SetSizer(self.vbox)
#Optimizing the size of the elements in vbox
self.vbox.Fit(self)
#Every wxPython app is an instance of wx.App
app = wx.App()
#Here we create a wx.Frame() and specifying it as a top-level window
#by stating "None" as a parent object
frame = MyFrame()
#Show the frame!
frame.Show()
#Start the applciation's MainLoop, and ready for events handling
app.MainLoop()
输出:

我们已经展示了如何将 Matplotlib 图形嵌入到 GUI 中;然而,我们还没有展示 Matplotlib 和 wxWidgets 之间的交互。通过添加一个按钮并将一个函数绑定(Bind)到按钮上,可以轻松实现这一点。每次用户点击按钮时,这将更新图形。
让我们走一步通过点击按钮来更新图形的示例!尽管我们使用的是相同的图形,但底层的更新方法不同。这里我们将通过点击事件来更新图形,而不是如第六章中所示的自动计时器,在 Qt 5 中嵌入 Matplotlib:
#Specifying that we are using WXAgg in matplotlib
import matplotlib
matplotlib.use('WXAgg')
#Here imports the wxPython and other supporting libraries
import wx, numpy, matplotlib.cm as cm, matplotlib.pyplot as plt
from numpy import arange, sin, pi, random, linspace
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure
#This figure looks like it is from a radar, so we name the class radar
class Radar(wx.Frame):
#Instantiation of Radar
def __init__(self):
# Initializing the Frame
wx.Frame.__init__(self, None, -1, title="", size=(600,500))
# Creating the panel
panel = wx.Panel(self)
#Setting up the figure, canvas and axes for drawing
self.fig = Figure(figsize=(6,4), dpi=100)
self.canvas = FigureCanvas(self, -1, self.fig)
self.ax = self.fig.add_subplot(111, projection='polar')
#Here comes the trick, create the button "Start Radar!"
self.updateBtn = wx.Button(self, -1, "Start Radar!")
#Bind the button with the clicking event, and invoke the update_fun function
self.Bind(wx.EVT_BUTTON, self.update_fun, self.updateBtn)
#Create the vertical box of Widgets
self.vbox = wx.BoxSizer(wx.VERTICAL)
#Add the canvas to the vertical box
self.vbox.Add(self.canvas, wx.ALIGN_CENTER|wx.ALL, 1)
#Add the button to the vertical box
self.vbox.Add(self.updateBtn)
#Add the vertical box to the Frame
self.SetSizer(self.vbox)
#Make sure the elements in the vertical box fits the figure size
self.vbox.Fit(self)
def update_fun(self,event):
#Make sure we clear the figure each time before redrawing
self.ax.cla()
#updating the axes figure
self.ax = self.fig.add_subplot(111, projection='polar')
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.ax.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
#Here we draw on the canvas!
self.fig.canvas.draw()
#And print on terminal to make sure the function was invoked upon trigger
print('Updating figure!')
#Every wxPython app is an instance of wx.App
app = wx.App(False)
#Here we create a wx.Frame() and specifying it as a top-level window
#by stating "None" as a parent object
frame = Radar()
#Show the frame!
frame.Show()
#Start the applciation's MainLoop, and ready for events handling
app.MainLoop()
输出:


通过点击“开始雷达!”按钮,我们调用update_fun函数,每次都会重新绘制一个新的图形。
在 wxGlade 中嵌入 Matplotlib 图形
对于非常简单的应用程序,GUI 界面比较有限的情况下,我们可以在应用程序源代码内部设计界面。一旦 GUI 变得更加复杂,这种方案就不可行,我们需要一个工具来支持我们进行 GUI 设计。wxWidgets 中最著名的工具之一就是 wxGlade。
wxGlade 是一个使用 wxPython 编写的界面设计程序,这使得它可以在所有支持这两个工具的平台上运行。
该理念与著名 GTK+图形用户界面设计工具 Glade 相似,外观和体验也非常相似。wxGlade 是一个帮助我们创建 wxWidgets 或 wxPython 用户界面的程序,但它并不是一个功能完整的代码编辑器;它只是一个设计器,生成的代码只是显示已创建的小部件。
尽管凤凰计划和 wxPython 4 相对较新,但它们都得到了 wxGlade 的支持。wxGlade 可以从 SourceForge 下载,用户可以轻松下载压缩包,解压缩并通过python3命令运行 wxGlade:
python3 wxglade.py
这里就是用户界面!

这里是图 5中三个主要窗口的细节。左上角的窗口是主要的调色板窗口。第一行的第一个按钮(Windows)是创建一个框架作为一切基础的按钮。左下角的窗口是属性窗口,它让我们显示和编辑应用程序、窗口和控件的属性。右边的窗口是树形窗口。它让我们可视化结构,允许编辑项目的结构,包括其应用程序、窗口、布局和控件。通过在树形窗口中选择一个项目,我们可以在属性窗口中编辑其对应的属性。
让我们点击按钮以 添加一个框架。接下来会出现一个小窗口:

选择基础类为 wxFrame;然后我们将在以下截图中生成一个 设计 窗口。从这里开始,我们可以点击并添加我们喜欢的按钮和功能:

首先,让我们为之前展示的代码创建一个容器。在点击任何按钮之前,我们先回顾一下前面 GUI 所需的基本元素:
-
框架
-
垂直框(
wx.BoxSizer) -
按钮
所以这非常简单;让我们点击 调色板 窗口中的 sizer 按钮,然后点击 设计 窗口:

从 树状窗口 中,我们可以看到 GUI 的结构。我们有一个包含两个插槽的框架。然而,我们希望使用垂直框而不是水平框;这可以在点击 sizer_2 时在 属性 窗口中进行修改:

现在让我们向插槽 2 添加一个按钮!这可以通过点击 调色板 窗口中的 按钮,然后点击 设计 窗口下部的插槽来完成。不过,从这里看,按钮的显示效果并不好。它位于下方面板的左侧。我们可以通过在 树状窗口中选择 _button1,并在 属性 窗口的布局选项卡中修改对齐方式来更改它。
在这里,我们选择了 wxEXPAND 和 wxALIGN_CENTER,这意味着它必须扩展并填充框架的宽度;这也确保它始终对齐到插槽的中心:

到目前为止,框架已经设置完成。让我们通过选择 文件 然后 生成代码来导出代码:

点击生成代码后,文件将保存在所选文件夹中(即用户保存 wxWidget 文件的文件夹),以下是一个代码片段:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# generated by wxGlade 0.8.0 on Sun Apr 8 20:35:42 2018
#
import wx
# begin wxGlade: dependencies
# end wxGlade
# begin wxGlade: extracode
# end wxGlade
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
# begin wxGlade: MyFrame.__init__
kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
self.SetSize((500, 550))
self.button_1 = wx.Button(self, wx.ID_ANY, "button_1")
self.__set_properties()
self.__do_layout()
# end wxGlade
def __set_properties(self):
# begin wxGlade: MyFrame.__set_properties
self.SetTitle("frame")
# end wxGlade
def __do_layout(self):
# begin wxGlade: MyFrame.__do_layout
sizer_2 = wx.BoxSizer(wx.VERTICAL)
sizer_2.Add((0, 0), 0, 0, 0)
sizer_2.Add(self.button_1, 0, wx.ALIGN_CENTER | wx.EXPAND, 0)
self.SetSizer(sizer_2)
self.Layout()
# end wxGlade
# end of class MyFrame
class MyApp(wx.App):
def OnInit(self):
self.frame = MyFrame(None, wx.ID_ANY, "")
self.SetTopWindow(self.frame)
self.frame.Show()
return True
# end of class MyApp
if __name__ == "__main__":
app = MyApp(0)
app.MainLoop()
上述代码提供了一个独立的图形界面。然而,它缺少一些关键功能来使一切正常工作。让我们快速扩展一下,看看它是如何工作的:
import matplotlib
matplotlib.use('WXAgg')
import wx, numpy, matplotlib.cm as cm, matplotlib.pyplot as plt
from numpy import arange, sin, pi, random, linspace
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
# begin wxGlade: MyFrame.__init__
kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
self.SetSize((500, 550))
self.button_1 = wx.Button(self, wx.ID_ANY, "button_1")
##Code being added***
self.Bind(wx.EVT_BUTTON, self.__updat_fun, self.button_1)
#Setting up the figure, canvas and axes
self.fig = Figure(figsize=(5,5), dpi=100)
self.canvas = FigureCanvas(self, -1, self.fig)
self.ax = self.fig.add_subplot(111, projection='polar')
##End of Code being added***self.__set_properties()
self.__do_layout()
# end wxGlade
def __set_properties(self):
# begin wxGlade: MyFrame.__set_properties
self.SetTitle("frame")
# end wxGlade
def __do_layout(self):
# begin wxGlade: MyFrame.__do_layout
sizer_2 = wx.BoxSizer(wx.VERTICAL)
sizer_2.Add(self.canvas, 0, wx.ALIGN_CENTER|wx.ALL, 1)
sizer_2.Add(self.button_1, 0, wx.ALIGN_CENTER | wx.EXPAND, 0)
self.SetSizer(sizer_2)
self.Layout()
# end wxGlade
##The udpate_fun that allows the figure to be updated upon clicking
##The __ in front of the update_fun indicates that it is a private function in Python syntax
def __updat_fun(self,event):
self.ax.cla()
self.ax = self.fig.add_subplot(111, projection='polar')
#Here, we borrow one example shown in the matplotlib gtk3 cookbook
#and show a beautiful bar plot on a circular coordinate system
self.theta = linspace(0.0, 2 * pi, 30, endpoint=False)
self.radii = 10 * random.rand(30)
self.plot_width = pi / 4 * random.rand(30)
self.bars = self.ax.bar(self.theta, self.radii, width=self.plot_width, bottom=0.0)
#Here defines the color of the bar, as well as setting it to be transparent
for r, bar in zip(self.radii, self.bars):
bar.set_facecolor(cm.jet(r / 10.))
bar.set_alpha(0.5)
self.fig.canvas.draw()
print('Updating figure!')
# end of class MyFrame class MyApp(wx.App):
def OnInit(self):
self.frame = MyFrame(None, wx.ID_ANY, "")
self.SetTopWindow(self.frame)
self.frame.Show()
return True
# end of class MyApp
if __name__ == "__main__":
app = MyApp(0)
app.MainLoop()
总结
现在,我们可以开发 wxWidgets 应用程序,并将 Matplotlib 嵌入其中。具体来说,读者应该能够在 wxFrame 中嵌入 Matplotlib 图形,使用 sizer 将图形和导航工具栏都嵌入到 wxFrame 中,通过交互更新图表,并使用 wxGlade 设计一个用于 Matplotlib 嵌入的 GUI。
我们现在准备继续前进,看看如何将 Matplotlib 集成到网页中。
第八章:将 Matplotlib 与 Web 应用程序集成
基于 Web 的应用程序(Web 应用)具有多重优势。首先,用户可以跨平台享受统一的体验。其次,由于无需安装过程,用户可以享受更简化的工作流。最后,从开发者的角度来看,开发周期可以简化,因为需要维护的特定平台代码较少。鉴于这些优势,越来越多的应用程序正在在线开发。
由于 Python 的流行和灵活性,Web 开发者使用基于 Python 的 Web 框架(如 Django 和 Flask)开发 Web 应用程序是有道理的。事实上,根据 hotframeworks.com/ 的数据,Django 和 Flask 分别在 175 个框架中排名第 6 和第 13。这些框架是 功能齐全的。从用户身份验证、用户管理、内容管理到 API 设计,它们都提供了完整的解决方案。代码库经过开源社区的严格审查,因此使用这些框架开发的网站可以防御常见攻击,如 SQL 注入、跨站请求伪造和跨站脚本攻击。
在本章中,我们将学习如何开发一个简单的网站,展示比特币的价格。将介绍基于 Django 的示例。我们将使用 Docker 18.03.0-ce 和 Django 2.0.4 进行演示。首先,我们将通过初始化基于 Docker 的开发环境的步骤来开始。
安装 Docker
Docker 允许开发者在自包含且轻量级的容器中运行应用程序。自 2013 年推出以来,Docker 迅速在开发者中获得了广泛的关注。在其技术的核心,Docker 使用 Linux 内核的资源隔离方法,而不是完整的虚拟化监控程序来运行应用程序。
这使得代码的开发、打包、部署和管理变得更加简便。因此,本章中的所有代码开发工作将基于 Docker 环境进行。
Docker for Windows 用户
在 Windows 上安装 Docker 有两种方式:名为 Docker for Windows 的包和 Docker Toolbox。我推荐使用稳定版本的 Docker Toolbox,因为 Docker for Windows 需要在 64 位 Windows 10 Pro 中支持 Hyper-V。同时,Docker for Windows 不支持较旧版本的 Windows。详细的安装说明可以在 docs.docker.com/toolbox/toolbox_install_windows/ 中找到,但我们也将在这里介绍一些重要步骤。
首先,从以下链接下载 Docker Toolbox:github.com/docker/toolbox/releases。选择名为 DockerToolbox-xx.xx.x-ce.exe 的文件,其中 x 表示最新版本号:

接下来,运行下载的安装程序。按照每个提示的默认说明进行安装:

Windows 可能会询问你是否允许进行某些更改,这是正常的,确保你允许这些更改发生。
最后,一旦安装完成,你应该能够在开始菜单中找到 Docker Quickstart Terminal:

点击图标启动 Docker Toolbox 终端,这将开始初始化过程。当该过程完成时,将显示以下终端:

Mac 用户的 Docker
对于 Mac 用户,我推荐 Docker CE for Mac(稳定版)应用程序,可以在 store.docker.com/editions/community/docker-ce-desktop-mac 下载。此外,完整的安装指南可以通过以下链接找到:docs.docker.com/docker-for-mac/install/。
Docker CE for Mac 的安装过程可能比 Windows 版本更简单。以下是主要步骤:
- 首先,双击下载的
Docker.dmg文件以挂载映像。当你看到以下弹窗时,将左侧的 Docker 图标拖动到右侧的应用程序文件夹中:

- 接下来,在你的应用程序文件夹或启动台中,找到并双击 Docker 应用程序。如果 Docker 启动成功,你应该能够在顶部状态栏看到一个鲸鱼图标:

- 最后,打开应用程序 | 实用工具文件夹中的终端应用程序。键入
docker info,然后按 Enter 键检查 Docker 是否正确安装:

更多关于 Django
Django 是一个流行的 web 框架,旨在简化 web 应用程序的开发和部署。它包括大量的模板代码,处理日常任务,如数据库模型管理、前端模板、会话认证和安全性。Django 基于 模型-模板-视图(MTV)设计模式构建。
模型可能是 MTV 中最关键的组件。它指的是如何通过不同的表格和属性来表示你的数据。它还将不同数据库引擎的细节抽象化,使得相同的模型可以应用于 SQLite、MySQL 和 PostgreSQL。同时,Django 的模型层会暴露特定于引擎的参数,如 PostgreSQL 中的 ArrayField 和 JSONField,用于微调数据表示。
模板类似于经典 MTV 框架中视图的作用。它处理数据的展示给用户。换句话说,它不涉及数据是如何生成的逻辑。
Django 中的视图负责处理用户请求,并返回相应的逻辑。它位于模型层和模板层之间。视图决定应该从模型中提取何种数据,以及如何处理数据以供模板使用。
Django 的主要卖点如下:
-
开发速度:提供了大量的关键组件;这减少了开发周期中的重复任务。例如,使用 Django 构建一个简单的博客只需几分钟。
-
安全性:Django 包含了 Web 安全的最佳实践。SQL 注入、跨站脚本、跨站请求伪造和点击劫持等黑客攻击的风险大大降低。其用户认证系统使用 PBKDF2 算法和加盐的 SHA256 哈希,这是 NIST 推荐的。其他先进的哈希算法,如 Argon2,也可用。
-
可扩展性:Django 的 MTV 层使用的是无共享架构。如果某一层成为 Web 应用程序的瓶颈,只需增加更多硬件;Django 将利用额外的硬件来支持每一层。
在 Docker 容器中进行 Django 开发
为了保持整洁,让我们创建一个名为Django的空目录来托管所有文件。在Django目录内,我们需要使用我们喜欢的文本编辑器创建一个Dockerfile来定义容器的内容。Dockerfile定义了容器的基础镜像以及编译镜像所需的命令。
欲了解更多有关 Dockerfile 的信息,请访问docs.docker.com/engine/reference/builder/。
我们将使用 Python 3.6.5 作为基础镜像。请将以下代码复制到您的 Dockerfile 中。一系列附加命令定义了工作目录和初始化过程:
# The official Python 3.6.5 runtime is used as the base image
FROM python:3.6.5-slim
# Disable buffering of output streams
ENV PYTHONUNBUFFERED 1
# Create a working directory within the container
RUN mkdir /app
WORKDIR /app
# Copy files and directories in the current directory to the container
ADD . /app/
# Install Django and other dependencies
RUN pip install -r requirements.txt
如您所见,我们还需要一个文本文件requirements.txt,以定义项目中的任何包依赖。请将以下内容添加到项目所在文件夹中的requirements.txt文件中:
Django==2.0.4
Matplotlib==2.2.2
stockstats==0.2.0
seaborn==0.8.1
现在,我们可以在终端中运行docker build -t django来构建镜像。构建过程可能需要几分钟才能完成:
在运行命令之前,请确保您当前位于相同的项目文件夹中。

如果构建过程完成,将显示以下消息。Successfully built ...消息结尾的哈希码可能会有所不同:
Successfully built 018e75992e59
Successfully tagged django:latest
启动一个新的 Django 站点
我们现在将使用docker run命令创建一个新的 Docker 容器。-v "$(pwd)":/app参数创建了当前目录到容器内/app的绑定挂载。当前目录中的文件将在主机和客机系统之间共享。
第二个未标记的参数 django 定义了用于创建容器的映像。命令字符串的其余部分如下:
django django-admin startproject --template=https://github.com/arocks/edge/archive/master.zip --extension=py,md,html,env crypto_stats
这被传递给客户机容器以执行。它使用 Arun Ravindran 的边缘模板 (django-edge.readthedocs.io/en/latest/) 创建了一个名为 crypto_stats 的新 Django 项目:
docker run -v "$(pwd)":/app django django-admin startproject --template=https://github.com/arocks/edge/archive/master.zip --extension=py,md,html,env crypto_stats
成功执行后,如果您进入新创建的 crypto_stats 文件夹,您应该能看到以下文件和目录:

Django 依赖项的安装
crypto_stats 文件夹中的 requirements.txt 文件定义了我们的 Django 项目的 Python 包依赖关系。要安装这些依赖项,请执行以下 docker run 命令。
参数 -p 8000:8000 将端口 8000 从客户机暴露给主机机器。参数 -it 创建一个支持 stdin 的伪终端,以允许交互式终端会话。
我们再次使用 django 映像,但这次我们启动了一个 Bash 终端 shell:
docker run -v "$(pwd)":/app -p 8000:8000 -it django bash
cd crypto_stats
pip install -r requirements.txt
在执行命令时,请确保您仍然位于项目的根目录(即 Django)中。
命令链将产生以下结果:

Django 环境设置
敏感的环境变量,例如 Django 的 SECRET_KEY (docs.djangoproject.com/en/2.0/ref/settings/#std:setting-SECRET_KEY),应该保存在一个从版本控制软件中排除的私有文件中。为简单起见,我们可以直接使用项目模板中的示例:
cd src
cp crypto_stats/settings/local.sample.env crypto_stats/settings/local.env
接下来,我们可以使用 manage.py 来创建一个默认的 SQLite 数据库和超级用户:
python manage.py migrate
python manage.py createsuperuser
migrate 命令初始化数据库模型,包括用户认证、管理员、用户配置文件、用户会话、内容类型和缩略图。
createsuperuser 命令将询问您一系列问题以创建超级用户:

运行开发服务器
启动默认的开发服务器非常简单;实际上,只需一行代码:
python manage.py runserver 0.0.0.0:8000
参数 0.0.0.0:8000 将告诉 Django 在端口 8000 上为所有地址提供网站服务。
在您的主机上,您现在可以启动您喜欢的浏览器,并访问 http://localhost:8000 查看您的网站:

网站的外观还不错,是吗?
使用 Django 和 Matplotlib 显示比特币价格
现在,我们仅使用几个命令就建立了一个完整的网站框架。希望您能欣赏使用 Django 进行网页开发的简便性。现在,我将演示如何将 Matplotlib 图表集成到 Django 网站中,这是本章的关键主题。
创建一个 Django 应用程序
Django 生态系统中的一个应用指的是在网站中处理特定功能的应用程序。例如,我们的默认项目已经包含了 profile 和 account 应用程序。澄清了术语后,我们准备构建一个显示比特币最新价格的应用。
我们应该让开发服务器在后台运行。当服务器检测到代码库的任何更改时,它将自动重新加载以反映更改。因此,现在我们需要启动一个新的终端并连接到正在运行的服务器容器:
docker exec -it 377bfb2f3db4 bash
bash前面的那些看起来很奇怪的数字是容器的 ID。我们可以从持有正在运行的服务器的终端中找到该 ID:

或者,我们可以通过发出以下命令来获取所有正在运行的容器的 ID:
docker ps -a
docker exec命令帮助你返回到与开发服务器相同的 Bash 环境。我们现在可以启动一个新应用:
cd /app/crypto_stats/src
python manage.py startapp bitcoin
在主机计算机的项目目录中,我们应该能够看到crypto_stats/src/下的新bitcoin文件夹:

创建一个简单的 Django 视图
我将通过一个简单的折线图演示创建 Django 视图的工作流程。
在新创建的比特币应用文件夹中,你应该能够找到views.py,它存储了应用中的所有视图。让我们编辑它并创建一个输出 Matplotlib 折线图的视图:
from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
from io import BytesIO
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def test_view(request):
# Create a new Matplotlib figure
fig, ax = plt.subplots()
# Prepare a simple line chart
ax.plot([1, 2, 3, 4], [3, 6, 9, 12])
ax.set_title('Matplotlib Chart in Django')
plt.tight_layout()
# Create a bytes buffer for saving image
fig_buffer = BytesIO()
plt.savefig(fig_buffer, dpi=150)
# Save the figure as a HttpResponse
response = HttpResponse(content_type='image/png')
response.write(fig_buffer.getvalue())
fig_buffer.close()
return response
由于我们的服务器容器中没有 Tkinter,我们需要通过首先调用matplotlib.use('Agg')来将 Matplotlib 图形后端从默认的 TkAgg 切换到 Agg。
matplotlib.use('Agg')必须在import matplotlib之后,并且在调用任何 Matplotlib 函数之前立即调用。
函数test_view(request)期望一个 Django HttpRequest对象(docs.djangoproject.com/en/2.0/ref/request-response/#django.http.HttpRequest)作为输入,并输出一个 Django HttpResponse对象(docs.djangoproject.com/en/2.0/ref/request-response/#django.http.HttpResponse)。
为了将 Matplotlib 图表导入到HttpResponse对象中,我们需要先将图表保存到一个中间的BytesIO对象中,该对象可以在io包中找到(docs.python.org/3/library/io.html#binary-i-o)。BytesIO对象充当二进制图像文件的缓冲区,以便plt.savefig能够直接将 PNG 文件写入其中。
接下来,我们创建一个新的HttpResponse()对象,并将content_type参数设置为image/png。缓冲区中的二进制内容通过response.write(fig_buffer.getvalue())导出到HttpResponse()对象中。最后,关闭缓冲区以释放临时内存。
为了将用户引导到这个视图,我们需要在{Project_folder}/crypto_stats/src/bitcoin文件夹内创建一个名为urls.py的新文件。
from django.urls import path
from . import views
app_name = 'bitcoin'
urlpatterns = [
path('test/', views.test_view),
]
这一行path('test/', views.test_view)表示所有以test/结尾的 URL 将被定向到test_view。
我们还需要将应用的url模式添加到全局模式中。让我们编辑{Project_folder}/crypto_stats/src/crypto_stats/urls.py,并添加以下两行注释:
...
import profiles.urls
import accounts.urls
# Import your app's url patterns here
import bitcoin.urls
from . import views
...
urlpatterns = [
path('', views.HomePage.as_view(), name='home'),
path('about/', views.AboutPage.as_view(), name='about'),
path('users/', include(profiles.urls)),
path('admin/', admin.site.urls),
# Add your app's url patterns here
path('bitcoin/', include(bitcoin.urls)),
path('', include(accounts.urls)),
]
...
这一行path('bitcoin/', include(bitcoin.urls)),表示所有以<your-domain>/bitcoin开头的 URL 将被定向到比特币应用。
等待几秒钟直到开发服务器重新加载。现在,你可以前往localhost:8000/bitcoin/test/查看你的图表。

创建比特币 K 线图视图
在这一部分,我们将从 Quandl API 获取比特币的历史价格。请注意,我们无法保证所展示的可视化数据的准确性、完整性或有效性;也不对可能发生的任何错误或遗漏负责。数据、可视化和分析仅以现状提供,仅供教育用途,且不提供任何形式的保证。建议读者在做出投资决策之前,先进行独立的个别加密货币研究。
如果你不熟悉 Quandl,它是一个金融和经济数据仓库,存储着来自数百个发布者的数百万数据集。在使用 Quandl API 之前,你需要在其网站上注册一个账户(www.quandl.com)。可以通过以下链接的说明获取免费的 API 访问密钥:docs.quandl.com/docs#section-authentication。在下一章我会介绍更多关于 Quandl 和 API 的内容。
现在,删除crypto_stats/src/bitcoin文件夹中的现有views.py文件。从本章的代码库中将views1.py复制到crypto_stats/src/bitcoin,并将其重命名为views.py。我会相应地解释views1.py中的每一部分。
在 Bitstamp 交易所的比特币历史价格数据可以在此找到:www.quandl.com/data/BCHARTS/BITSTAMPUSD-Bitcoin-Markets-bitstampUSD。我们目标数据集的唯一标识符是BCHARTS/BITSTAMPUSD。尽管 Quandl 提供了官方的 Python 客户端库,我们为了演示导入 JSON 数据的一般流程,将不使用该库。get_bitcoin_dataset函数仅使用urllib.request.urlopen和json.loads来从 API 获取 JSON 数据。最后,数据被处理为 pandas DataFrame,以供进一步使用。
... A bunch of import statements
def get_bitcoin_dataset():
"""Obtain and parse a quandl bitcoin dataset in Pandas DataFrame format
Quandl returns dataset in JSON format, where data is stored as a
list of lists in response['dataset']['data'], and column headers
stored in response['dataset']['column_names'].
Returns:
df: Pandas DataFrame of a Quandl dataset"""
# Input your own API key here
api_key = ""
# Quandl code for Bitcoin historical price in BitStamp exchange
code = "BCHARTS/BITSTAMPUSD"
base_url = "https://www.quandl.com/api/v3/datasets/"
url_suffix = ".json?api_key="
# We want to get the data within a one-year window only
time_now = datetime.datetime.now()
one_year_ago = time_now.replace(year=time_now.year-1)
start_date = one_year_ago.date().isoformat()
end_date = time_now.date().isoformat()
date = "&start_date={}&end_date={}".format(start_date, end_date)
# Fetch the JSON response
u = urlopen(base_url + code + url_suffix + api_key + date)
response = json.loads(u.read().decode('utf-8'))
# Format the response as Pandas Dataframe
df = pd.DataFrame(response['dataset']['data'], columns=response['dataset']['column_names'])
# Convert Date column from string to Python datetime object,
# then to float number that is supported by Matplotlib.
df["Datetime"] = date2num(pd.to_datetime(df["Date"], format="%Y-%m-%d").tolist())
return df
记得在这一行指定你自己的 API 密钥:api_key = ""。
df中的Date列是作为一系列 Python 字符串记录的。尽管 Seaborn 可以在某些函数中使用字符串格式的日期,Matplotlib 却不行。为了使日期能够进行数据处理和可视化,我们需要将其转换为 Matplotlib 支持的浮动点数。因此,我使用了matplotlib.dates.date2num来进行转换。
我们的数据框包含每个交易日的开盘价和收盘价,以及最高价和最低价。到目前为止,我们描述的所有图表都无法在一个图表中描述所有这些变量的趋势。
在金融世界中,蜡烛图几乎是描述股票、货币和商品在一段时间内价格变动的默认选择。每根蜡烛由描述开盘价和收盘价的主体,以及展示最高价和最低价的延伸蜡烛线组成,表示某一特定交易日。如果收盘价高于开盘价,蜡烛通常为黑色。相反,如果收盘价低于开盘价,蜡烛则为红色。交易者可以根据颜色和蜡烛主体的边界来推断开盘价和收盘价。
在下面的示例中,我们将准备一个比特币在过去 30 个交易日的数据框中的蜡烛图。candlestick_ohlc函数是从已废弃的matplotlib.finance包中改编而来。它绘制时间、开盘价、最高价、最低价和收盘价为一个从低到高的垂直线。它进一步使用一系列彩色矩形条来表示开盘和收盘之间的跨度。
def candlestick_ohlc(ax, quotes, width=0.2, colorup='k', colordown='r',
alpha=1.0):
"""
Parameters
----------
ax : `Axes`
an Axes instance to plot to
quotes : sequence of (time, open, high, low, close, ...) sequences
As long as the first 5 elements are these values,
the record can be as long as you want (e.g., it may store volume).
time must be in float days format - see date2num
width : float
fraction of a day for the rectangle width
colorup : color
the color of the rectangle where close >= open
colordown : color
the color of the rectangle where close < open
alpha : float
the rectangle alpha level
Returns
-------
ret : tuple
returns (lines, patches) where lines is a list of lines
added and patches is a list of the rectangle patches added
"""
OFFSET = width / 2.0
lines = []
patches = []
for q in quotes:
t, open, high, low, close = q[:5]
if close >= open:
color = colorup
lower = open
height = close - open
else:
color = colordown
lower = close
height = open - close
vline = Line2D(
xdata=(t, t), ydata=(low, high),
color=color,
linewidth=0.5,
antialiased=True,
)
rect = Rectangle(
xy=(t - OFFSET, lower),
width=width,
height=height,
facecolor=color,
edgecolor=color,
)
rect.set_alpha(alpha)
lines.append(vline)
patches.append(rect)
ax.add_line(vline)
ax.add_patch(rect)
ax.autoscale_view()
return lines, patches
bitcoin_chart函数处理用户请求的实际处理和HttpResponse的输出。
def bitcoin_chart(request):
# Get a dataframe of bitcoin prices
bitcoin_df = get_bitcoin_dataset()
# candlestick_ohlc expects Date (in floating point number), Open, High, Low, Close columns only
# So we need to select the useful columns first using DataFrame.loc[]. Extra columns can exist,
# but they are ignored. Next we get the data for the last 30 trading only for simplicity of plots.
candlestick_data = bitcoin_df.loc[:, ["Datetime",
"Open",
"High",
"Low",
"Close",
"Volume (Currency)"]].iloc[:30]
# Create a new Matplotlib figure
fig, ax = plt.subplots()
# Prepare a candlestick plot
candlestick_ohlc(ax, candlestick_data.values, width=0.6)
ax.xaxis.set_major_locator(WeekdayLocator(MONDAY)) # major ticks on the mondays
ax.xaxis.set_minor_locator(DayLocator()) # minor ticks on the days
ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d'))
ax.xaxis_date() # treat the x data as dates
# rotate all ticks to vertical
plt.setp(ax.get_xticklabels(), rotation=90, horizontalalignment='right')
ax.set_ylabel('Price (US $)') # Set y-axis label
plt.tight_layout()
# Create a bytes buffer for saving image
fig_buffer = BytesIO()
plt.savefig(fig_buffer, dpi=150)
# Save the figure as a HttpResponse
response = HttpResponse(content_type='image/png')
response.write(fig_buffer.getvalue())
fig_buffer.close()
return response
ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d')) 对于将浮动点数转换回日期非常有用。
与第一个 Django 视图示例类似,我们需要修改urls.py,将 URL 指向我们的bitcoin_chart视图。
from django.urls import path
from . import views
app_name = 'bitcoin'
urlpatterns = [
path('30/', views.bitcoin_chart),
]
完成!你可以通过访问http://localhost:8000/bitcoin/30/查看比特币蜡烛图。

集成更多的价格指标
当前形式的蜡烛图有些单调。交易者通常会叠加股票指标,如平均真实范围(ATR)、布林带、商品通道指数(CCI)、指数移动平均线(EMA)、平滑异同移动平均线(MACD)、相对强弱指数(RSI)等,用于技术分析。
Stockstats (github.com/jealous/stockstats) 是一个很棒的包,可以用来计算前面提到的指标/统计数据以及更多内容。它基于 pandas DataFrame,并在访问时动态生成这些统计数据。
在这一部分,我们可以通过stockstats.StockDataFrame.retype()将一个 pandas DataFrame 转换为一个 stockstats DataFrame。然后,可以通过遵循StockDataFrame["variable_timeWindow_indicator"]的模式访问大量的股票指标。例如,StockDataFrame['open_2_sma']会给我们开盘价的 2 日简单移动平均。某些指标可能有快捷方式,因此请参考官方文档获取更多信息。
我们代码库中的views2.py文件包含了创建扩展比特币定价视图的代码。你可以将本章代码库中的views2.py复制到crypto_stats/src/bitcoin目录,并将其重命名为views.py。
下面是我们之前代码中需要的重要更改:
# FuncFormatter to convert tick values to Millions
def millions(x, pos):
return '%dM' % (x/1e6)
def bitcoin_chart(request):
# Get a dataframe of bitcoin prices
bitcoin_df = get_bitcoin_dataset()
# candlestick_ohlc expects Date (in floating point number), Open, High, Low, Close columns only
# So we need to select the useful columns first using DataFrame.loc[]. Extra columns can exist,
# but they are ignored. Next we get the data for the last 30 trading only for simplicity of plots.
candlestick_data = bitcoin_df.loc[:, ["Datetime",
"Open",
"High",
"Low",
"Close",
"Volume (Currency)"]].iloc[:30]
# Convert to StockDataFrame
# Need to pass a copy of candlestick_data to StockDataFrame.retype
# Otherwise the original candlestick_data will be modified
stockstats = StockDataFrame.retype(candlestick_data.copy())
# 5-day exponential moving average on closing price
ema_5 = stockstats["close_5_ema"]
# 10-day exponential moving average on closing price
ema_10 = stockstats["close_10_ema"]
# 30-day exponential moving average on closing price
ema_30 = stockstats["close_30_ema"]
# Upper Bollinger band
boll_ub = stockstats["boll_ub"]
# Lower Bollinger band
boll_lb = stockstats["boll_lb"]
# 7-day Relative Strength Index
rsi_7 = stockstats['rsi_7']
# 14-day Relative Strength Index
rsi_14 = stockstats['rsi_14']
# Create 3 subplots spread across three rows, with shared x-axis.
# The height ratio is specified via gridspec_kw
fig, axarr = plt.subplots(nrows=3, ncols=1, sharex=True, figsize=(8,8),
gridspec_kw={'height_ratios':[3,1,1]})
# Prepare a candlestick plot in the first axes
candlestick_ohlc(axarr[0], candlestick_data.values, width=0.6)
# Overlay stock indicators in the first axes
axarr[0].plot(candlestick_data["Datetime"], ema_5, lw=1, label='EMA (5)')
axarr[0].plot(candlestick_data["Datetime"], ema_10, lw=1, label='EMA (10)')
axarr[0].plot(candlestick_data["Datetime"], ema_30, lw=1, label='EMA (30)')
axarr[0].plot(candlestick_data["Datetime"], boll_ub, lw=2, linestyle="--", label='Bollinger upper')
axarr[0].plot(candlestick_data["Datetime"], boll_lb, lw=2, linestyle="--", label='Bollinger lower')
# Display RSI in the second axes
axarr[1].axhline(y=30, lw=2, color = '0.7') # Line for oversold threshold
axarr[1].axhline(y=50, lw=2, linestyle="--", color = '0.8') # Neutral RSI
axarr[1].axhline(y=70, lw=2, color = '0.7') # Line for overbought threshold
axarr[1].plot(candlestick_data["Datetime"], rsi_7, lw=2, label='RSI (7)')
axarr[1].plot(candlestick_data["Datetime"], rsi_14, lw=2, label='RSI (14)')
# Display trade volume in the third axes
axarr[2].bar(candlestick_data["Datetime"], candlestick_data['Volume (Currency)'])
# Label the axes
axarr[0].set_ylabel('Price (US $)')
axarr[1].set_ylabel('RSI')
axarr[2].set_ylabel('Volume (US $)')
axarr[2].xaxis.set_major_locator(WeekdayLocator(MONDAY)) # major ticks on the mondays
axarr[2].xaxis.set_minor_locator(DayLocator()) # minor ticks on the days
axarr[2].xaxis.set_major_formatter(DateFormatter('%Y-%m-%d'))
axarr[2].xaxis_date() # treat the x data as dates
axarr[2].yaxis.set_major_formatter(FuncFormatter(millions)) # Change the y-axis ticks to millions
plt.setp(axarr[2].get_xticklabels(), rotation=90, horizontalalignment='right') # Rotate x-tick labels by 90 degree
# Limit the x-axis range to the last 30 days
time_now = datetime.datetime.now()
datemin = time_now-datetime.timedelta(days=30)
datemax = time_now
axarr[2].set_xlim(datemin, datemax)
# Show figure legend
axarr[0].legend()
axarr[1].legend()
# Show figure title
axarr[0].set_title("Bitcoin 30-day price trend", loc='left')
plt.tight_layout()
# Create a bytes buffer for saving image
fig_buffer = BytesIO()
plt.savefig(fig_buffer, dpi=150)
# Save the figure as a HttpResponse
response = HttpResponse(content_type='image/png')
response.write(fig_buffer.getvalue())
fig_buffer.close()
return response
再次提醒,请确保在get_bitcoin_dataset()函数中的代码行内指定你自己的 API 密钥:api_key = ""。
修改后的bitcoin_chart视图将创建三个子图,它们跨越三行,并共享一个x轴。子图之间的高度比通过gridspec_kw进行指定。
第一个子图将显示蜡烛图以及来自stockstats包的各种股票指标。
第二个子图显示了比特币在 30 天窗口中的相对强弱指数(RSI)。
最后,第三个子图显示了比特币的交易量(美元)。自定义的FuncFormatter millions被用来将y轴的值转换为百万。
你现在可以访问相同的链接localhost:8000/bitcoin/30/来查看完整的图表。

将图像集成到 Django 模板中
要在首页显示图表,我们可以修改位于{Project_folder}/crypto_stats/src/templates/home.html的首页模板。
我们需要修改<!-- Benefits of the Django application -->注释后的代码行,修改为以下内容:
{% block container %}
<!-- Benefits of the Django application -->
<a name="about"></a>
<div class="container">
<div class="row">
<div class="col-lg-8">
<h2>Bitcoin pricing trend</h2>
<img src="img/" alt="Bitcoin prices" style="width:100%">
<p><a class="btn btn-primary" href="#" role="button">View details »</a></p>
</div>
<div class="col-lg-4">
<h2>Heading</h2>
<p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>
<p><a class="btn btn-primary" href="#" role="button">View details »</a></p>
</div>
</div>
</div>
{% endblock container %}
基本上,我们的bitcoin_chart视图是通过<img src="img/" alt="Bitcoin prices" style="width:100%">这一行作为图像加载的。我还将容器部分的列数从 3 列减少到了 2 列,并通过将类设置为col-lg-8来调整了第一列的大小。
如果你访问首页(即http://localhost:8000),当你滚动到页面底部时,你会看到以下屏幕:

这个实现有一些注意事项。首先,每次访问页面都会触发一次 API 调用到 Quandl,因此你的免费 API 配额会很快被消耗。更好的方法是每天获取一次价格,并将数据记录到合适的数据库模型中。
其次,当前形式的图像输出并没有集成到特定的应用模板中。这超出了本书以 Matplotlib 为主题的范围。然而,感兴趣的读者可以参考在线文档中的说明(docs.djangoproject.com/en/2.0/topics/templates/)。
最后,这些图像是静态的。像mpld3和 Plotly 这样的第三方包可以将 Matplotlib 图表转换为基于 Javascript 的交互式图表。使用这些包可以进一步增强用户体验。
总结
在本章中,你了解了一个流行的框架,旨在简化 Web 应用程序的开发和部署,即 Django。你还进一步学习了如何将 Matplotlib 图表集成到 Django 网站中。
在下一章中,我们将介绍一些有用的技术,用于定制图形美学,以便有效讲述故事。
第九章:Matplotlib 在现实世界中的应用
到目前为止,我们希望你已经掌握了使用 Matplotlib 创建和定制图表的技巧。让我们在已有的基础上进一步深入,通过现实世界的例子开始我们的 Matplotlib 高级用法之旅。
首先,我们将介绍如何获取在线数据,这通常是通过 应用程序编程接口 (API) 或传统的网页抓取技术获得的。接下来,我们将探索如何将 Matplotlib 2.x 与 Python 中的其他科学计算包集成,用于不同数据类型的可视化。
常见的 API 数据格式
许多网站通过其 API 分发数据,API 通过标准化架构将应用程序连接起来。虽然我们在这里不会详细讨论如何使用 API,但我们会介绍最常见的 API 数据交换格式——CSV 和 JSON。
感兴趣的读者可以访问特定网站的文档,了解如何使用 API。
我们在第四章,高级 Matplotlib 中简要介绍了 CSV 文件的解析。为了帮助你更好地理解,我们将同时使用 CSV 和 JSON 来表示相同的数据。
CSV
逗号分隔值 (CSV) 是最早的文件格式之一,远在万维网存在之前就已被引入。然而,随着 JSON 和 XML 等先进格式的流行,CSV 正在逐渐被淘汰。顾名思义,数据值是通过逗号分隔的。预安装的 csv 包和 pandas 包都包含了读取和写入 CSV 格式数据的类。以下 CSV 示例定义了一个包含两个国家的 population(人口)表:
Country,Time,Sex,Age,Value
United Kingdom,1950,Male,0-4,2238.735
United States of America,1950,Male,0-4,8812.309
JSON
JavaScript 对象表示法 (JSON) 由于其高效性和简洁性,近年来越来越受欢迎。JSON 允许指定数字、字符串、布尔值、数组和对象。Python 提供了默认的 json 包来解析 JSON。或者,pandas.read_json 类可以用来将 JSON 导入为 pandas DataFrame。前述的人口表可以用 JSON 表示如下:
{
"population": [
{
"Country": "United Kingdom",
"Time": 1950,
"Sex", "Male",
"Age", "0-4",
"Value",2238.735
},{
"Country": "United States of America",
"Time": 1950,
"Sex", "Male",
"Age", "0-4",
"Value",8812.309
},
]
}
从 JSON API 导入和可视化数据
现在,让我们学习如何解析来自 Quandl API 的金融数据,以创建有价值的可视化图表。Quandl 是一个金融和经济数据仓库,存储了来自数百个发布者的数百万数据集。Quandl 的最大优点是,这些数据集通过统一的 API 提供,用户无需担心如何正确解析数据。匿名用户每天可以获得最多 50 次 API 调用,注册用户则可以获得最多 500 次免费 API 调用。读者可以在 www.quandl.com/?modal=register 上注册免费 API 密钥。
在 Quandl 中,每个数据集都有一个唯一的 ID,由每个搜索结果网页上的 Quandl 代码定义。例如,Quandl 代码GOOG/NASDAQ_SWTX定义了 Google Finance 发布的历史 NASDAQ 指数数据。每个数据集都提供三种不同的格式——CSV、JSON 和 XML。
尽管 Quandl 提供了官方的 Python 客户端库,我们不会使用它,而是为了演示从 API 导入 JSON 数据的通用流程。根据 Quandl 的文档,我们可以通过以下 API 调用获取 JSON 格式的数据表:
GET https://www.quandl.com/api/v3/datasets/{Quandl code}/data.json
首先,让我们尝试从 Quandl 获取大麦克指数数据。大麦克指数由经济学人于 1986 年发明,作为一种轻松的方式来判断货币是否处于正确的水平。它基于购买力平价(PPP)理论,并被视为货币在购买力平价下的非正式汇率衡量标准。它通过将货币与一篮子类似的商品和服务进行比较来衡量其价值,在这种情况下是大麦克。市场汇率下的价格差异意味着某种货币被低估或高估:
from urllib.request import urlopen
import json
import time
import pandas as pd
def get_bigmac_codes():
"""Get a pandas DataFrame of all codes in the Big Mac index dataset
The first column contains the code, while the second header
contains the description of the code.
E.g.
ECONOMIST/BIGMAC_ARG,Big Mac Index - Argentina
ECONOMIST/BIGMAC_AUS,Big Mac Index - Australia
ECONOMIST/BIGMAC_BRA,Big Mac Index - Brazil
Returns:
codes: pandas DataFrame of Quandl dataset codes"""
codes_url = "https://www.quandl.com/api/v3/databases/ECONOMIST/codes"
codes = pd.read_csv(codes_url, header=None, names=['Code', 'Description'],
compression='zip', encoding='latin_1')
return codes
def get_quandl_dataset(api_key, code):
"""Obtain and parse a quandl dataset in pandas DataFrame format
Quandl returns dataset in JSON format, where data is stored as a
list of lists in response['dataset']['data'], and column headers
stored in response['dataset']['column_names'].
E.g. {'dataset': {...,
'column_names': ['Date',
'local_price',
'dollar_ex',
'dollar_price',
'dollar_ppp',
'dollar_valuation',
'dollar_adj_valuation',
'euro_adj_valuation',
'sterling_adj_valuation',
'yen_adj_valuation',
'yuan_adj_valuation'],
'data': [['2017-01-31',
55.0,
15.8575,
3.4683903515687,
10.869565217391,
-31.454736135007,
6.2671477203176,
8.2697553162259,
29.626894343348,
32.714616745128,
13.625825886047],
['2016-07-31',
50.0,
14.935,
3.3478406427854,
9.9206349206349,
-33.574590420925,
2.0726096168216,
0.40224795003514,
17.56448458418,
19.76377270142,
11.643103380531]
],
'database_code': 'ECONOMIST',
'dataset_code': 'BIGMAC_ARG',
... }}
A custom column--country is added to denote the 3-letter country code.
Args:
api_key: Quandl API key
code: Quandl dataset code
Returns:
df: pandas DataFrame of a Quandl dataset
"""
base_url = "https://www.quandl.com/api/v3/datasets/"
url_suffix = ".json?api_key="
# Fetch the JSON response
u = urlopen(base_url + code + url_suffix + api_key)
response = json.loads(u.read().decode('utf-8'))
# Format the response as pandas Dataframe
df = pd.DataFrame(response['dataset']['data'], columns=response['dataset']['column_names'])
# Label the country code
df['country'] = code[-3:]
return df
quandl_dfs = []
codes = get_bigmac_codes()
# Replace this with your own API key
api_key = "INSERT-YOUR-KEY-HERE"
for code in codes.Code:
# Get the DataFrame of a Quandl dataset
df = get_quandl_dataset(api_key, code)
# Store in a list
quandl_dfs.append(df)
# Prevents exceeding the API speed limit
time.sleep(2)
# Concatenate the list of data frames into a single one
bigmac_df = pd.concat(quandl_dfs)
bigmac_df.head()
这是预期的结果,显示数据框的前五行:
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| Date | 31-07-17 | 31-01-17 | 31-07-16 | 31-01-16 | 31-07-15 |
| local_price | 5.9 | 5.8 | 5.75 | 5.3 | 5.3 |
| dollar_ex | 1.303016 | 1.356668 | 1.335738 | 1.415729 | 1.35126 |
| dollar_price | 4.527955 | 4.27518 | 4.304737 | 3.743655 | 3.922265 |
| dollar_ppp | 1.113208 | 1.146245 | 1.140873 | 1.075051 | 1.106472 |
| dollar_valuation | -14.56689 | -15.510277 | -14.588542 | -24.06379 | -18.115553 |
| dollar_adj_valuation | -11.7012 | -11.9234 | -11.0236 | -28.1641 | -22.1691 |
| euro_adj_valuation | -13.0262 | -10.2636 | -12.4796 | -22.2864 | -18.573 |
| sterling_adj_valuation | 2.58422 | 7.43771 | 2.48065 | -22.293 | -23.1926 |
| yen_adj_valuation | 19.9417 | 9.99688 | 4.39776 | -4.0042 | 6.93893 |
| yuan_adj_valuation | -2.35772 | -5.82434 | -2.681 | -20.6755 | -14.1711 |
| country | AUS | AUS | AUS | AUS | AUS |
解析 Quandl API 中的 JSON 数据的代码有点复杂,因此需要额外的解释。第一个函数get_bigmac_codes()解析 Quandl 经济学人数据库中所有可用数据集代码的列表,并将其转换为 pandas DataFrame。同时,第二个函数get_quandl_dataset(api_key, code)将 Quandl 数据集 API 查询的 JSON 响应转换为 pandas DataFrame。所有获取的数据集通过pandas.concat()合并为一个单独的数据框。
我们应该记住,大麦克指数在不同国家之间并不直接可比。通常,我们会预期贫穷国家的商品价格低于富裕国家。为了更公平地展示指数,最好展示大麦克价格与国内生产总值(GDP)人均之间的关系。
为了达到这一目的,我们将从 Quandl 的世界银行世界发展指标(WWDI)数据库中获取 GDP 数据集。基于之前从 Quandl 获取 JSON 数据的代码示例,你能尝试将其修改为下载人均 GDP 数据集吗?
对于那些急于查看的用户,以下是完整的代码:
import urllib
import json
import pandas as pd
import time
from urllib.request import urlopen
def get_gdp_dataset(api_key, country_code):
"""Obtain and parse a quandl GDP dataset in pandas DataFrame format
Quandl returns dataset in JSON format, where data is stored as a
list of lists in response['dataset']['data'], and column headers
stored in response['dataset']['column_names'].
Args:
api_key: Quandl API key
country_code: Three letter code to represent country
Returns:
df: pandas DataFrame of a Quandl dataset
"""
base_url = "https://www.quandl.com/api/v3/datasets/"
url_suffix = ".json?api_key="
# Compose the Quandl API dataset code to get GDP per capita (constant 2000 US$) dataset
gdp_code = "WWDI/" + country_code + "_NY_GDP_PCAP_KD"
# Parse the JSON response from Quandl API
# Some countries might be missing, so we need error handling code
try:
u = urlopen(base_url + gdp_code + url_suffix + api_key)
except urllib.error.URLError as e:
print(gdp_code,e)
return None
response = json.loads(u.read().decode('utf-8'))
# Format the response as pandas Dataframe
df = pd.DataFrame(response['dataset']['data'], columns=response['dataset']['column_names'])
# Add a new country code column
df['country'] = country_code
return df
api_key = "INSERT-YOUR-KEY-HERE" #Change this to your own API key
quandl_dfs = []
# Loop through all unique country code values in the BigMac index DataFrame
for country_code in bigmac_df.country.unique():
# Fetch the GDP dataset for the corresponding country
df = get_gdp_dataset(api_key, country_code)
# Skip if the response is empty
if df is None:
continue
# Store in a list DataFrames
quandl_dfs.append(df)
# Prevents exceeding the API speed limit
time.sleep(2)
# Concatenate the list of DataFrames into a single one
gdp_df = pd.concat(quandl_dfs)
gdp_df.head()
几个地区的 GDP 数据缺失,但这应该可以通过try...except代码块在get_gdp_dataset函数中优雅地处理。运行前面的代码后,你应该看到如下内容:
WWDI/EUR_NY_GDP_PCAP_KD HTTP Error 404: Not Found
WWDI/ROC_NY_GDP_PCAP_KD HTTP Error 404: Not Found
WWDI/SIN_NY_GDP_PCAP_KD HTTP Error 404: Not Found
WWDI/UAE_NY_GDP_PCAP_KD HTTP Error 404: Not Found
| 日期 | 值 | 国家 | |
|---|---|---|---|
| 0 | 2016-12-31 | 55478.577294 | AUS |
| 1 | 2015-12-31 | 54800.366396 | AUS |
| 2 | 2014-12-31 | 54293.794205 | AUS |
| 3 | 2013-12-31 | 53732.003969 | AUS |
| 4 | 2012-12-31 | 53315.029915 | AUS |
接下来,我们将使用pandas.merge()合并包含“大麦指数”或人均 GDP 的两个 pandas 数据框。WWDI 的最新人均 GDP 数据记录是在 2016 年底收集的,因此我们将其与 2017 年 1 月的最新大麦指数数据集配对。
对于熟悉 SQL 语言的人来说,pandas.merge()支持四种连接模式,分别是左连接、右连接、内连接和外连接。由于我们只关心在两个 pandas 数据框中都有匹配国家的行,因此我们将选择内连接:
merged_df = pd.merge(bigmac_df[(bigmac_df.Date == "2017-01-31")], gdp_df[(gdp_df.Date == "2016-12-31")], how='inner', on='country')
merged_df.head()
这是合并后的数据框:
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 日期 _x | 31-01-17 | 31-01-17 | 31-01-17 | 31-01-17 | 31-01-17 |
| 本地价格 | 5.8 | 16.5 | 3.09 | 2450 | 55 |
| 美元汇率 | 1.356668 | 3.22395 | 0.828775 | 672.805 | 15.8575 |
| 美元价格 | 4.27518 | 5.117945 | 3.728394 | 3.641471 | 3.46839 |
| 美元购买力平价 | 1.146245 | 3.26087 | 0.610672 | 484.189723 | 10.869565 |
| 美元估值 | -15.510277 | 1.145166 | -26.316324 | -28.034167 | -31.454736 |
| 美元调整估值 | -11.9234 | 67.5509 | -18.0208 | 11.9319 | 6.26715 |
| 欧元调整估值 | -10.2636 | 70.7084 | -16.4759 | 14.0413 | 8.26976 |
| 英镑调整估值 | 7.43771 | 104.382 | 0 | 36.5369 | 29.6269 |
| 日元调整估值 | 9.99688 | 109.251 | 2.38201 | 39.7892 | 32.7146 |
| 人民币调整估值 | -5.82434 | 79.1533 | -12.3439 | 19.6828 | 13.6258 |
| 国家 | AUS | BRA | GBR | CHL | ARG |
| 日期 _y | 31-12-16 | 31-12-16 | 31-12-16 | 31-12-16 | 31-12-16 |
| 值 | 55478.5773 | 10826.2714 | 41981.3921 | 15019.633 | 10153.99791 |
使用 Seaborn 简化可视化任务
散点图是科学和商业领域中最常见的图形之一。它特别适合用来展示两个变量之间的关系。虽然我们可以简单地使用 matplotlib.pyplot.scatter 来绘制散点图(有关更多详细信息,请参见第二章,Matplotlib 入门 和 第四章,高级 Matplotlib),我们也可以使用 Seaborn 来构建具有更多高级功能的类似图形。
这两个函数,seaborn.regplot() 和 seaborn.lmplot(),通过散点图、回归线以及回归线周围的 95%置信区间,展示了变量之间的线性关系。它们之间的主要区别在于,lmplot() 将 regplot() 与 FacetGrid 结合在一起,允许我们创建带有颜色编码或分面显示的散点图,从而展示三个或更多变量对之间的交互关系。
seaborn.regplot() 最简单的形式支持 NumPy 数组、pandas Series 或 pandas DataFrame 作为输入。可以通过指定 fit_reg=False 来去除回归线和置信区间。
我们将调查这样一个假设:在人均 GDP 较低的国家,巨无霸价格较便宜,反之亦然。为此,我们将尝试找出巨无霸指数与人均 GDP 之间是否存在相关性:
import seaborn as sns
import matplotlib.pyplot as plt
# seaborn.regplot() returns a matplotlib.Axes object
ax = sns.regplot(x="Value", y="dollar_price", data=merged_df, fit_reg=False)
# We can modify the axes labels just like other ordinary
# Matplotlib objects
ax.set_xlabel("GDP per capita (constant 2000 US$)")
ax.set_ylabel("BigMac index (US$)")
plt.show()
代码将用一个经典的散点图来迎接你:

到目前为止,一切顺利!看起来巨无霸指数与人均 GDP 呈正相关。我们将重新开启回归线,并标记出一些显示极端巨无霸指数值的国家(即 ≥ 5 或 ≤ 2)。同时,默认的绘图样式有些单调;我们可以通过运行 sns.set(style="whitegrid") 来使图表更具活力。还有四种其他样式可供选择,分别是 darkgrid、dark、white 和 ticks:
sns.set(style="whitegrid")
ax = sns.regplot(x="Value", y="dollar_price", data=merged_df)
ax.set_xlabel("GDP per capita (constant 2000 US$)")
ax.set_ylabel("BigMac index (US$)")
# Label the country codes which demonstrate extreme BigMac index
for row in merged_df.itertuples():
if row.dollar_price >= 5 or row.dollar_price <= 2:
ax.text(row.Value,row.dollar_price+0.1,row.country)
plt.show()
这是带标签的图:

我们可以看到,许多国家的点都落在回归线的置信区间内。根据每个国家的人均 GDP,线性回归模型预测了相应的巨无霸指数。如果实际指数偏离回归模型,则货币价值可能表明其被低估或高估。
通过标记出显示极高或极低值的国家,我们可以清楚地看到,巴西和瑞士的巨无霸价格被高估,而南非、马来西亚、乌克兰和埃及则被低估。
由于 Seaborn 不是一个用于统计分析的包,我们需要使用其他包,例如 scipy.stats 或 statsmodels,来获得回归模型的参数。在下一个示例中,我们将从回归模型中获取斜率和截距参数,并为回归线上下的点应用不同的颜色:
from scipy.stats import linregress
ax = sns.regplot(x="Value", y="dollar_price", data=merged_df)
ax.set_xlabel("GDP per capita (constant 2000 US$)")
ax.set_ylabel("BigMac index (US$)")
# Calculate linear regression parameters
slope, intercept, r_value, p_value, std_err = linregress(merged_df.Value, merged_df.dollar_price)
colors = []
for row in merged_df.itertuples():
if row.dollar_price > row.Value * slope + intercept:
# Color markers as darkred if they are above the regression line
color = "darkred"
else:
# Color markers as darkblue if they are below the regression line
color = "darkblue"
# Label the country code for those who demonstrate extreme BigMac index
if row.dollar_price >= 5 or row.dollar_price <= 2:
ax.text(row.Value,row.dollar_price+0.1,row.country)
# Highlight the marker that corresponds to China
if row.country == "CHN":
t = ax.text(row.Value,row.dollar_price+0.1,row.country)
color = "yellow"
colors.append(color)
# Overlay another scatter plot on top with marker-specific color
ax.scatter(merged_df.Value, merged_df.dollar_price, c=colors)
# Label the r squared value and p value of the linear regression model.
# transform=ax.transAxes indicates that the coordinates are given relative to the axes bounding box,
# with 0,0 being the lower left of the axes and 1,1 the upper right.
ax.text(0.1, 0.9, "$r²={0:.3f}, p={1:.3e}$".format(r_value ** 2, p_value), transform=ax.transAxes)
plt.show()
这张截图展示了带有颜色标签的图:

与普遍看法相反,看起来中国的货币在 2016 年并没有显著低估,因为其价值位于回归线的 95%置信区间内。
我们还可以将x和y值的直方图与散点图结合,使用seaborn.jointplot:
通过在jointplot中额外指定kind参数为reg、resid、hex或kde中的任意一个,我们可以迅速将图表类型分别更改为回归图、残差图、六边形箱型图或 KDE 轮廓图。
# seaborn.jointplot() returns a seaborn.JointGrid object
g = sns.jointplot(x="Value", y="dollar_price", data=merged_df)
# Provide custom axes labels through accessing the underlying axes object
# We can get matplotlib.axes.Axes of the scatter plot by calling g.ax_joint
g.ax_joint.set_xlabel("GDP per capita (constant 2000 US$)")
g.ax_joint.set_ylabel("BigMac index (US$)")
# Set the title and adjust the margin
g.fig.suptitle("Relationship between GDP per capita and BigMac Index")
g.fig.subplots_adjust(top=0.9)
plt.show()
jointplot如图所示:

这里有一个重要的免责声明。即便我们手中有所有数据,现在依然为时过早,无法对货币估值做出任何结论!劳动力成本、租金、原材料成本和税收等不同的商业因素都可能影响“大麦”定价模型,但这超出了本书的范围。
从网站抓取信息
世界各国的政府或司法管辖区越来越重视开放数据,这旨在增加公民参与和知情决策,并使政策更加开放,接受公众审查。全球一些开放数据倡议的例子包括www.data.gov/(美国)、data.gov.uk/(英国)和data.gov.hk/en/(香港)。
这些数据门户网站通常提供用于程序化访问数据的 API。然而,并非所有数据集都提供 API,因此我们需要依靠老式的网页抓取技术,从网站中提取信息。
Beautiful Soup (www.crummy.com/software/BeautifulSoup/) 是一个非常有用的抓取网站信息的包。基本上,所有带有 HTML 标签的内容都可以使用这个强大的包进行抓取。Scrapy 也是一个不错的网页抓取包,但它更像是一个编写强大网络爬虫的框架。所以,如果你只是需要从页面抓取一个表格,Beautiful Soup 提供了更简单的操作方式。
本章将使用 Beautiful Soup 版本 4.6。要安装 Beautiful Soup 4,我们可以再次通过 PyPI 来安装:
pip install beautifulsoup4
美国失业率和按教育程度划分的收入数据(2017 年)可以通过以下网站获得:www.bls.gov/emp/ep_table_001.htm。目前,Beautiful Soup 不处理 HTML 请求。所以我们需要使用urllib.request或requests包来获取网页。在这两个选项中,requests包由于其更高层次的 HTTP 客户端接口,使用起来显得更加简便。如果你的系统中没有requests,我们可以通过 PyPI 安装:
pip install requests
在编写网页爬取代码之前,让我们先看一下网页。如果我们使用 Google Chrome 访问劳动统计局网站,就可以检查对应我们需要的表格的 HTML 代码:

接下来,展开<div id="bodytext" class="verdana md">,直到你能看到<table class="regular" cellspacing="0" cellpadding="0" xborder="1">...</table>。当你将鼠标悬停在 HTML 代码上时,页面中的对应部分会被高亮显示:

扩展<table>的 HTML 代码后,我们可以看到列名定义在<thead>...</thead>部分,而表格内容则定义在<tbody>...</tbody>部分。
为了指示 Beautiful Soup 爬取我们需要的信息,我们需要给它明确的指示。我们可以右键单击代码检查窗口中的相关部分,复制格式为 CSS 选择器的唯一标识符:

让我们尝试获取thead和tbody的 CSS 选择器,并使用BeautifulSoup.select()方法来爬取相应的 HTML 代码:
import requests
from bs4 import BeautifulSoup
# Specify the url
url = "https://www.bls.gov/emp/ep_table_001.htm"
# Query the website and get the html response
response = requests.get(url)
# Parse the returned html using BeautifulSoup
bs = BeautifulSoup(response.text)
# Select the table header by CSS selector
thead = bs.select("#bodytext > table > thead")[0]
# Select the table body by CSS selector
tbody = bs.select("#bodytext > table > tbody")[0]
# Make sure the code works
print(thead)
你将看到表头的 HTML 代码:
<thead>
<tr>
<th scope="col"><p align="center" valign="top"><strong>Educational attainment</strong></p></th>
<th scope="col"><p align="center" valign="top">Unemployment rate (%)</p></th>
<th scope="col"><p align="center" valign="top">Median usual weekly earnings ($)</p></th>
</tr>
</thead>
接下来,我们将找到所有包含每一列名称的<th></th>标签。我们将构建一个以列头为键的字典列表来保存数据:
# Get the column names
headers = []
# Find all header columns in <thead> as specified by <th> html tags
for col in thead.find_all('th'):
headers.append(col.text.strip())
# Dictionary of lists for storing parsed data
data = {header:[] for header in headers}
最后,我们解析表格的剩余行,并将数据转换为 pandas DataFrame:
import pandas as pd
# Parse the rows in table body
for row in tbody.find_all('tr'):
# Find all columns in a row as specified by <th> or <td> html tags
cols = row.find_all(['th','td'])
# enumerate() allows us to loop over an iterable,
# and return each item preceded by a counter
for i, col in enumerate(cols):
# Strip white space around the text
value = col.text.strip()
# Try to convert the columns to float, except the first column
if i > 0:
value = float(value.replace(',','')) # Remove all commas in string
# Append the float number to the dict of lists
data[headers[i]].append(value)
# Create a data frame from the parsed dictionary
df = pd.DataFrame(data)
# Show an excerpt of parsed data
df.head()
我们现在应该能够重现主表格的前几行:
| 学历 | 中位数通常每周收入($) | 失业率(%) | |
|---|---|---|---|
| 0 | 博士学位 | 1743.0 | 1.5 |
| 1 | 专业学位 | 1836.0 | 1.5 |
| 2 | 硕士学位 | 1401.0 | 2.2 |
| 3 | 本科及以上学位 | 1173.0 | 2.5 |
| 4 | 大专及以上学位 | 836.0 | 3.4 |
主 HTML 表格已经被格式化为结构化的 pandas DataFrame。我们现在可以继续可视化数据了。
Matplotlib 图形后端
绘图的代码被认为是 Matplotlib 中的前端部分。我们第一次提到后端是在第一章,Matplotlib 简介,当时我们在谈论输出格式。实际上,Matplotlib 后端有着比仅仅支持图形格式更多的差异。后端在幕后处理了很多事情!这决定了绘图功能的支持。例如,LaTeX 文本布局仅由 Agg、PDF、PGF 和 PS 后端支持。
非交互式后端
到目前为止,我们已经使用了几种非交互式后端,包括 Agg、Cairo、GDK、PDF、PGF、PS 和 SVG。大多数后端无需额外依赖即可工作,但 Cairo 和 GDK 分别需要 Cairo 图形库或 GIMP 绘图工具包才能运行。
非交互式后端可以进一步分为两组——矢量或光栅。矢量图形通过点、路径和形状来描述图像,这些都是通过数学公式计算得出的。无论缩放多少,矢量图形总是显得平滑,并且其大小通常比光栅图形要小。PDF、PGF、PS 和 SVG 后端属于矢量组。
光栅图形通过有限数量的微小颜色块(像素)来描述图像。所以,如果我们足够放大,就会看到图像的不平滑表现,换句话说,就是像素化。通过提高图像的分辨率或每英寸点数(DPI),我们不太可能观察到像素化现象。Agg、Cairo 和 GDK 属于这一类后端。下表总结了非交互式后端的主要功能和差异:
| 后端 | 矢量还是光栅? | 输出格式 |
|---|---|---|
| Agg | 光栅 | .png |
| Cairo | 矢量/光栅 | .pdf, .png, .ps, .svg |
| 矢量 | .pdf |
|
| PGF | 矢量 | .pdf, .pgf |
| PS | 矢量 | .ps |
| SVG | 矢量 | .svg |
| GDK* | 光栅 | .png, .jpg, .tiff |
*Matplotlib 2.0 中已弃用。
通常,我们不需要手动选择后端,因为默认的选择适用于大多数任务。另一方面,我们可以通过在首次导入 matplotlib.pyplot 之前使用 matplotlib.use() 方法指定后端:
import matplotlib
matplotlib.use('SVG') # Change to SVG backend
import matplotlib.pyplot as plt
import textwrap # Standard library for text wrapping
# Create a figure
fig, ax = plt.subplots(figsize=(6,7))
# Create a list of x ticks positions
ind = range(df.shape[0])
# Plot a bar chart of median usual weekly earnings by educational attainments
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
# Set the x-axis label
ax.set_xlabel('Median weekly earnings (USD)')
# Label the x ticks
# The tick labels are a bit too long, let's wrap them in 15-char lines
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
# Give extra margin at the bottom to display the tick labels
fig.subplots_adjust(left=0.3)
# Save the figure in SVG format
plt.savefig("test.svg")

交互式后端
Matplotlib 可以构建比静态图形更具互动性的图形,这对于读者来说更具吸引力。有时,图形可能会被过多的图形元素淹没,使得难以分辨单独的数据点。在其他情况下,一些数据点可能看起来非常相似,肉眼很难察觉它们之间的差异。交互式图形可以通过允许我们缩放、平移和按照自己的方式探索图形来解决这两种情况。
通过使用交互式后端,Matplotlib 中的图形可以嵌入到图形用户界面(GUI)应用程序中。默认情况下,Matplotlib 支持将 Agg 光栅图形渲染器与多种 GUI 工具包配对,包括 wxWidgets(Wx)、GIMP 工具包(GTK+)、Qt 和 TkInter(Tk)。由于 Tkinter 是 Python 的事实标准 GUI,构建于 Tcl/Tk 之上,我们只需在独立的 Python 脚本中调用 plt.show() 就可以创建交互式图形。我们可以尝试将以下代码复制到单独的文本文件中,并命名为 interactive.py。然后,在终端(Mac/Linux)或命令提示符(Windows)中输入 python interactive.py。如果你不确定如何打开终端或命令提示符,请参考第一章,Matplotlib 介绍,以获取更多细节:
import matplotlib
import matplotlib.pyplot as plt
import textwrap
import requests
import pandas as pd
from bs4 import BeautifulSoup
# Import Matplotlib radio button widget
from matplotlib.widgets import RadioButtons
url = "https://www.bls.gov/emp/ep_table_001.htm"
response = requests.get(url)
bs = BeautifulSoup(response.text)
thead = bs.select("#bodytext > table > thead")[0]
tbody = bs.select("#bodytext > table > tbody")[0]
headers = []
for col in thead.find_all('th'):
headers.append(col.text.strip())
data = {header:[] for header in headers}
for row in tbody.find_all('tr'):
cols = row.find_all(['th','td'])
for i, col in enumerate(cols):
value = col.text.strip()
if i > 0:
value = float(value.replace(',',''))
data[headers[i]].append(value)
df = pd.DataFrame(data)
fig, ax = plt.subplots(figsize=(6,7))
ind = range(df.shape[0])
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
ax.set_xlabel('Median weekly earnings (USD)')
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
fig.subplots_adjust(left=0.3)
# Create axes for holding the radio selectors.
# supply [left, bottom, width, height] in normalized (0, 1) units
bax = plt.axes([0.3, 0.9, 0.4, 0.1])
radio = RadioButtons(bax, ('Weekly earnings', 'Unemployment rate'))
# Define the function for updating the displayed values
# when the radio button is clicked
def radiofunc(label):
# Select columns from dataframe depending on label
if label == 'Weekly earnings':
data = df["Median usual weekly earnings ($)"]
ax.set_xlabel('Median weekly earnings (USD)')
elif label == 'Unemployment rate':
data = df["Unemployment rate (%)"]
ax.set_xlabel('Unemployment rate (%)')
# Update the bar heights
for i, rect in enumerate(rects):
rect.set_width(data[i])
# Rescale the x-axis range
ax.set_xlim(xmin=0, xmax=data.max()*1.1)
# Redraw the figure
plt.draw()
radio.on_clicked(radiofunc)
plt.show()
我们将看到一个类似于以下的弹出窗口。我们可以平移、缩放以选择区域、配置子图边距、保存,并通过点击底部工具栏上的按钮在不同视图之间来回切换。如果我们将鼠标悬停在图表上,还可以在右下角观察到精确的坐标。这个功能对于剖析彼此接近的数据点非常有用:

接下来,我们将通过在图形上方添加一个单选按钮控件来扩展应用程序,从而可以在显示每周收入或失业率之间切换。单选按钮位于matplotlib.widgets中,我们将把一个数据更新函数附加到按钮的.on_clicked()事件上。你可以将以下代码粘贴到之前代码示例(interactive.py)中的plt.show()行之前。让我们看看它是如何工作的:
# Import Matplotlib radio button widget
from matplotlib.widgets import RadioButtons
# Create axes for holding the radio selectors.
# supply [left, bottom, width, height] in normalized (0, 1) units
bax = plt.axes([0.3, 0.9, 0.4, 0.1])
radio = RadioButtons(bax, ('Weekly earnings', 'Unemployment rate'))
# Define the function for updating the displayed values
# when the radio button is clicked
def radiofunc(label):
# Select columns from dataframe, and change axis label depending on selection
if label == 'Weekly earnings':
data = df["Median usual weekly earnings ($)"]
ax.set_xlabel('Median weekly earnings (USD)')
elif label == 'Unemployment rate':
data = df["Unemployment rate (%)"]
ax.set_xlabel('Unemployment rate (%)')
# Update the bar heights
for i, rect in enumerate(rects):
rect.set_width(data[i])
# Rescale the x-axis range
ax.set_xlim(xmin=0, xmax=data.max()*1.1)
# Redraw the figure
plt.draw()
# Attach radiofunc to the on_clicked event of the radio button
radio.on_clicked(radiofunc)
你将看到图表顶部出现一个新的单选框。尝试在两种状态之间切换,看看图形是否会相应更新。完整代码也可以在代码包中找到:

在我们结束本节之前,我们将介绍一种很少在书籍中提及的交互式后端。从 Matplotlib 1.4 开始,提供了一种专为 Jupyter Notebook 设计的交互式后端。要调用它,我们只需要在笔记本的开始处粘贴%matplotlib notebook。我们将调整本章早些时候的一个示例来使用这个后端:
# Import the interactive backend for Jupyter Notebook
%matplotlib notebook
import matplotlib
import matplotlib.pyplot as plt
import textwrap
fig, ax = plt.subplots(figsize=(6,7))
ind = range(df.shape[0])
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
ax.set_xlabel('Median weekly earnings (USD)')
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
fig.subplots_adjust(left=0.3)
# Show the figure using interactive notebook backend
plt.show()
以下交互式图表将嵌入到你的 Jupyter Notebook 中:

创建动画图
Matplotlib 最初并不是为动画包设计的,因此在某些高级用途中它的表现可能显得有些迟缓。对于以动画为中心的应用程序,PyGame 是一个非常好的替代方案(www.pygame.org),它支持 OpenGL 和 Direct3D 加速图形,提供极致的动画速度。不过,Matplotlib 在大多数时候的表现是可以接受的,我们将引导你完成创建比静态图更具吸引力的动画的步骤。
在开始制作动画之前,我们需要在系统上安装 FFmpeg、avconv、mencoder 或 ImageMagick 其中之一。这些附加依赖项没有与 Matplotlib 捆绑在一起,因此我们需要单独安装它们。我们将带你逐步完成安装 FFmpeg 的步骤。
对于基于 Debian 的 Linux 用户,只需在终端中输入以下命令即可安装 FFmpeg。
sudo apt-get install ffmpeg
对于 Mac 用户,Homebrew(brew.sh/)是搜索和安装ffmpeg软件包的最简单方式。如果你没有安装 Homebrew,可以将以下代码粘贴到终端中进行安装。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
然后,我们可以通过在终端输入以下命令来安装 FFmpeg:
brew install ffmpeg
另外,您也可以通过将二进制文件复制到系统路径(例如,/usr/local/bin)来安装 FFmpeg(evermeet.cx/ffmpeg/)。
对于 Windows 用户,安装过程稍微复杂一些,但幸运的是,wikiHow 上有一份详细的安装指南(www.wikihow.com/Install-FFmpeg-on-Windows)。
Matplotlib 提供了两种主要的动画创建接口:TimedAnimation 和 FuncAnimation。TimedAnimation 适用于创建基于时间的动画,而 FuncAnimation 可以根据自定义函数来创建动画。由于 FuncAnimation 提供了更高的灵活性,我们将在本节中仅探讨 FuncAnimation 的使用。有兴趣的读者可以参考官方文档(matplotlib.org/api/animation_api.html)了解更多关于 TimedAnimation 的信息。
在以下示例中,我们通过假设每年增加 5% 来模拟中位数周薪的变化。我们将创建一个自定义函数—animate,该函数返回在每一帧中发生变化的 Matplotlib Artist 对象。该函数将与一些额外的参数一起传递给 animation.FuncAnimation():
import textwrap
import matplotlib.pyplot as plt
import random
# Matplotlib animation module
from matplotlib import animation
# Used for generating HTML video embed code
from IPython.display import HTML
# Adapted from previous example, codes that are modified are commented
fig, ax = plt.subplots(figsize=(6,7))
ind = range(df.shape[0])
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
ax.set_xlabel('Median weekly earnings (USD)')
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
fig.subplots_adjust(left=0.3)
# Change the x-axis range
ax.set_xlim(0,7600)
# Add a text annotation to show the current year
title = ax.text(0.5,1.05, "Median weekly earnings (USD) in 2017",
bbox={'facecolor':'w', 'alpha':0.5, 'pad':5},
transform=ax.transAxes, ha="center")
# Animation related stuff
n=30 #Number of frames
def animate(frame):
# Simulate 5% annual pay rise
data = df["Median usual weekly earnings ($)"] * (1.05 ** frame)
# Update the bar heights
for i, rect in enumerate(rects):
rect.set_width(data[i])
# Update the title
title.set_text("Median weekly earnings (USD) in {}".format(2016+frame))
return rects, title
# Call the animator. Re-draw only the changed parts when blit=True.
# Redraw all elements when blit=False
anim=animation.FuncAnimation(fig, animate, blit=False, frames=n)
# Save the animation in MPEG-4 format
anim.save('test.mp4')
# OR--Embed the video in Jupyter Notebook
HTML(anim.to_html5_video())
以下是生成的视频:
在前面的示例中,我们以 MPEG-4 编码视频的形式输出动画。该视频也可以以 H.264 编码视频的形式嵌入到 Jupyter Notebook 中。只需要调用 Animation.to_html5_video() 方法,并将返回的对象传递给 IPython.display.HTML,视频编码和 HTML5 代码生成会在后台自动完成。
从版本 2.2.0 开始,Matplotlib 支持通过 Pillow 图像库和 ImageMagick 创建动画 GIF。由于互联网对 GIF 的热爱永无止境,让我们来学习如何创建一个 GIF 吧!
在我们能够创建动画 GIF 之前,我们需要先安装 ImageMagick。所有主要平台的下载链接和安装说明可以在此找到:www.imagemagick.org/script/download.php。
安装该包后,我们可以通过将 anim.save('test.mp4') 改为 anim.save('test.gif', writer='imagemagick', fps=10) 来生成动画 GIF。fps 参数表示动画的帧率。
以下是生成的动画 GIF:
概述
在本章中,你学习了如何使用多功能的 pandas 包解析在线的 CSV 或 JSON 格式数据。你还进一步学习了如何筛选、子集化、合并和处理数据以提取洞察。最后,你学会了如何直接从网站上抓取信息。现在,你已经掌握了可视化时间序列、单变量和双变量数据的知识。本章以一系列有用的技巧结束,这些技巧可以帮助你定制图形美学,以进行有效的故事讲述。
呼!我们刚刚完成了一个长章节,去吃个汉堡,休息一下,放松放松吧。
第十章:将数据可视化集成到工作流中
我们现在来到了本书的最后一章。在本书的整个过程中,你已经掌握了如何使用来自网络的不同格式的真实世界数据,创建和定制静态与动态图表的技巧。作为总结,我们将在本章开始一个迷你项目,将数据分析技能与您学到的可视化技术结合起来。我们将演示如何将可视化技术整合到当前的工作流中。
在大数据时代,机器学习成为简化分析工作的重要工具,通过用自动预测替代大量的手动整理。尽管如此,在我们进入模型构建之前,探索性数据分析(EDA)始终是获取数据基本情况的关键。优化过程中的不断回顾也有助于改进我们的训练策略和结果。
高维数据通常需要特殊的处理技术才能直观地进行可视化。统计方法,如主成分分析(PCA)和t-分布随机邻居嵌入(t-SNE),是将数据降维以便有效可视化的重要技能。
作为展示,我们将演示在一个涉及使用卷积神经网络(CNN)识别手写数字的工作流中使用各种可视化技术。
一个重要的注意点是,我们并不打算在本章详细说明所有的数学和机器学习方法。我们的目标是可视化其中的一些过程。希望读者能意识到探索如损失函数在训练 CNN 时的作用,或使用不同参数可视化降维结果等过程的重要性。
开始
回顾我们在第四章《高级 Matplotlib》中简要提到的 MNIST 数据集。它包含了 70,000 张手写数字的图像,通常用于数据挖掘教程中作为机器学习基础。在本章的项目中,我们将继续使用类似的手写数字图像数据集。
我们几乎可以确定,在开始本课程之前,你已经听说过一些热门关键词——深度学习或机器学习。正因为如此,我们将它作为展示案例。由于机器学习中的一些详细概念,如超参数调优来优化性能,超出了本书的范围,我们不会深入探讨。但是,我们将以食谱式的方式讲解模型训练部分。我们将重点讲解可视化如何帮助我们的工作流。对于那些对机器学习细节感兴趣的读者,我们建议进一步探索大量在线资源。
可视化数据集中的样本图像
数据清洗和探索性数据分析(EDA)是数据科学中不可或缺的组成部分。在我们开始分析数据之前,理解输入数据的一些基本特性是很重要的。我们使用的数据集包含标准化的图像,形状规则且像素值已归一化。特征简单,主要是细线条。我们的目标也很直接,就是从图像中识别数字。然而,在许多实际应用的案例中,问题可能更复杂;我们收集的数据往往是原始的,且异质性较强。在解决问题之前,通常值得花时间抽取少量数据进行检查。试想训练一个模型来识别拉面,只是为了让你垂涎三尺;)。你可能会查看一些图像,以决定哪些特征可以作为一个好的输入样本,来展示碗的存在。除了初步的准备阶段,在模型构建过程中,剔除一些标签错误的样本进行检查,也有助于我们制定优化策略。
如果你想知道拉面这个想法是从哪里来的,一个名为 Kenji Doi 的数据科学家创建了一个模型,用来识别一碗拉面是在哪家餐厅分店制作的。你可以在 Google Cloud 大数据与机器学习博客中阅读更多内容,链接在此:cloud.google.com/blog/big-data/2018/03/automl-vision-in-action-from-ramen-to-branded-goods。
导入 UCI ML 手写数字数据集
虽然我们将使用 MNIST 数据集,正如在第四章中所示的高级 Matplotlib(因为我们将展示机器学习中的可视化和模型构建),但是我们将采取捷径以加速训练过程。我们不会使用包含 60,000 张 28x28 像素图像的 MNIST 数据集,而是从 scikit-learn 包中导入另一个类似的 8x8 像素图像数据集。
该数据集来自加利福尼亚大学欧文分校机器学习资源库,地址为archive.ics.uci.edu/ml/datasets/Optical+Recognition+of+Handwritten+Digits。它是 43 个人手写数字图像的预处理版本,其中 30 人用于训练集,13 人用于测试集。预处理方法在 M. D. Garris、J. L. Blue、G. T. Candela、D. L. Dimmick、J. Geist、P. J. Grother、S. A. Janet 和 C. L. Wilson 的论文《NIST 表单式手写识别系统》,NISTIR 5469,1994 中有详细描述。
导入数据集的代码如下:
from sklearn.datasets import load_digits
首先,让我们将数据集存储到一个变量中。我们将其命名为digits,并在本章中多次使用它:
digits = load_digits()
让我们通过打印出变量digits来查看load_digits()函数加载了什么内容:
print(type(digits))
print(digits)
digits 的类型是 <class 'sklearn.utils.Bunch'>,这是专门用于加载示例数据集的。
由于 print(digits) 的输出比较长,我们将在两张截图中展示它的开头和结尾:

以下截图展示了输出的尾部:

我们可以看到,digits 类中有五个成员:
-
'data':像素值压平为一维 NumPy 数组 -
'target':数据集中每个元素的身份标签的 NumPy 数组 -
'target_names':数据集中存在的唯一标签的列表—在这里是整数 0-9 -
'images':像素值重新排列成二维 NumPy 数组,表示图像的维度 -
'DESCR':数据集的描述
除了图像尺寸比 MNIST 更小外,这个数据集的图像数量也少得多。那么,究竟有多少张图像呢?我们可以通过 nd.shape 获取 NumPy 数组的维度元组,其中 nd 是数组。因此,要查询 digits.image 的形状,我们调用:
print(digits.images.shape)
我们得到的结果是 (1797, 8, 8)。
你可能会想,为什么这个数字如此特别。如果你眼尖的话,可能已经注意到描述中有 5,620 个实例。事实上,这些描述是从归档网页中检索到的。我们加载的数据实际上是完整数据集中的测试部分。你也可以从 archive.ics.uci.edu/ml/machine-learning-databases/optdigits/optdigits.tes 下载其纯文本版本。
如果你有兴趣获取完整的 MNIST 数据集,scikit-learn 也提供了一个 API 来获取它:
from sklearn.datasets import fetch_mldata mnist = fetch_mldata('MNIST original', data_home=custom_data_home)
绘制样本图像
现在我们更了解输入数据的背景了,让我们绘制一些样本图像。
提取每个数字 0-9 的一个样本
为了了解图像的样子,我们希望提取每个数字的一个图像进行检查。为此,我们需要找出数字的位置。我们可以通过跟踪正确的索引(即数据标签)来完成这一任务,使用 digits.target,然后写几行代码来调用 10 张图像:
indices = []
for i in range(10):
for j in digits.target:
if i==j:
indices.append(j)
break
print(indices)
有趣的是,[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 被作为输出返回,这意味着前 10 个样本恰好按数字顺序排列。这是偶然发生的,还是数据是有序的?我们需要检查,因为样本分布可能会影响我们的训练表现。
检查数据集的随机性
由于显示所有 1,797 个数据点会使得图像过于密集,无法进行有意义的解释,我们将绘制前 200 个数据点来检查:
import matplotlib.pyplot as plt
plt.scatter(list(range(200)),digits.target[:200])
plt.show()
在这里,我们得到了一张样本分布的散点图。看起来并不完全随机,对吧?数字 0 到 9 按顺序排列并重复了三次。我们还可以看到从第 125 个样本开始,模式有了重复。数据的结构暗示着在后续机器学习模型训练之前可能进行了随机化处理。现在,我们将按原样继续,继续检查这些图像:

在子图中绘制 10 个数字
我们将图像排列在一个两行五列的网格中。首先通过plt.figure()设置矩形画布。对于每个数字的样本图像,我们通过plt.subplot()函数定义坐标轴,然后调用imshow()显示颜色值数组作为图像。回想一下,颜色映射'gray_r'会将值从零到最大值以灰度形式从白色到黑色进行绘制。由于不需要显示x和y的刻度标签,我们将通过传递一个空列表给plt.xticks()和plt.yticks()来移除这些多余的内容。以下是实现这一操作的代码片段:
import matplotlib.pyplot as plt
nrows, ncols = 2, 5
plt.figure(figsize=(6,3))
for i in range(ncols * nrows):
ax = plt.subplot(nrows, ncols, i + 1)
ax.imshow(digits.images[i],cmap='gray_r')
plt.xticks([])
plt.yticks([])
plt.title(digits.target[i])
如下图所示,图像对人眼来说有些模糊。但信不信由你,算法足够提取出特征并区分每个数字。我们将在我们的工作流程中一起观察这一点:

使用 t-SNE 方法探索数据的性质
在可视化了几个图像并略微了解样本的分布后,我们将深入进行数据探索分析(EDA)。
每个像素都有一个强度值,这样每个 8x8 的图像就有 64 个变量。人类大脑不擅长直观感知超过三维的空间。对于高维数据,我们需要更有效的视觉辅助工具。
降维方法,例如常用的 PCA 和 t-SNE,通过减少考虑的输入变量的数量,同时保留大部分有用的信息,从而使数据的可视化变得更加直观。
在接下来的部分中,我们将重点讨论使用 Python 中的 scikit-learn 库进行 t-SNE 方法的应用。
理解 t-分布随机邻域嵌入
t-SNE 方法由 van der Maaten 和 Hinton 于 2008 年在论文《使用 t-SNE 可视化数据》中提出。它是一种非线性降维方法,旨在有效地可视化高维数据。t-SNE 基于概率分布,通过在邻域图上进行随机游走来发掘数据中的结构。t-SNE 的数学细节超出了本书的范围,建议读者阅读原论文以了解更多细节。
简而言之,t-SNE 是一种捕捉高维数据中非线性关系的方法。当我们试图从高维矩阵中提取特征时,例如图像处理、生物数据和网络信息,它特别有用。它使我们能够将高维数据降维到二维或三维;t-SNE 的一个有趣特点是它是随机的,这意味着它每次显示的最终结果都会不同,但它们都是同样正确的。因此,为了在 t-SNE 降维中获得最佳表现,建议先对大数据集执行 PCA 降维,然后将 PCA 维度引入 t-SNE 进行后续降维。这样,你会得到更一致且可复制的结果。
从 scikit-learn 导入 t-SNE 方法
我们将通过加载 scikit-learn 中的TSNE函数来实现 t-SNE 方法,如下所示:
from sklearn.manifold import TSNE
用户在运行 t-SNE 时需要设置一些超参数,包括:
-
'init':嵌入初始化 -
'method':barnes_hut或精确方法 -
'perplexity':默认值30 -
'n_iter':默认值1000 -
'n_components':默认值2
进入单个超参数的数学细节会是一个单独的章节,但我们确实有一些关于参数设置的一般建议。对于init,推荐使用'pca',原因之前已说明。对于方法,barnes_hut会更快,并且如果提供的数据集本身没有高度相似性,结果非常相似。对于困惑度,它反映了对数据局部和全局子结构的关注。n_iter表示你将运行算法的迭代次数,而n_components = 2表示最终的结果是二维空间。
为了追踪实验轮次的时间,我们可以在 Jupyter Notebook 中使用单元魔法%%timeit来跟踪单元运行所需的时间。
绘制 t-SNE 图来展示我们的数据
让我们首先按手写数字的顺序重新排列数据点:
import numpy as np
X = np.vstack([digits.data[digits.target==i]for i in range(10)])
y = np.hstack([digits.target[digits.target==i] for i in range(10)])
y将变成array([0, 0, 0, ..., 9, 9, 9])。
请注意,t-SNE 变换在普通笔记本电脑上可能需要几分钟的计算时间,tSNE命令可以像下面这样简单地运行。我们首先尝试使用250次迭代来运行 t-SNE:
#Here we run tSNE with 250 iterations and time it
%%timeit
tsne_iter_250 = TSNE(init='pca',method='exact',n_components=2,n_iter=250).fit_transform(X)
让我们绘制一个散点图,看看数据是如何聚集的:
#We import the pandas and matplotlib libraries
import pandas as pd
import matplotlib
matplotlib.style.use('seaborn')
#Here we plot the tSNE results in a reduced two-dimensional space
df = pd.DataFrame(tsne_iter_250)
plt.scatter(df[0],df[1],c=y,cmap=matplotlib.cm.get_cmap('tab10'))
plt.show()
我们可以看到,在250次迭代后,聚类并没有很好地分开:

现在我们尝试使用2000次迭代来运行:
#Here we run tSNE for 2000 iteractions
tsne_iter_2000 = TSNE(init='pca',method='exact',n_components=2,n_iter=2000).fit_transform(X)
#Here we plot the figure
df2 = pd.DataFrame(tsne_iter_2000)
plt.scatter(df2[0],df2[1],c=y,cmap=matplotlib.cm.get_cmap('tab10'))
plt.show()
如下截图所示,样本呈现为10个不同的聚类斑点。通过运行2000次迭代,我们得到了更加满意的结果:

创建一个 CNN 来识别数字
在接下来的部分中,我们将使用 Keras。Keras 是一个用于神经网络的 Python 库,提供了一个高级接口来调用 TensorFlow 库。我们不打算对 Keras 或 CNN 进行完整的教程讲解,但我们希望展示如何使用 Matplotlib 可视化损失函数、准确率以及结果中的异常值。
对于不熟悉机器学习的读者,他们应该能够理解剩余章节的逻辑,并希望能够理解为什么可视化损失函数、准确率和结果中的异常值对于微调 CNN 模型非常重要。
这里是 CNN 代码的一段,最重要的部分是在这一部分之后的评估环节!
# Import sklearn models for preprocessing input data
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelBinarizer
# Import the necessary Keras libraries
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D
from keras import backend as K
from keras.callbacks import History
# Randomize and split data into training dataset with right format to feed to Keras
lb = LabelBinarizer()
X = np.expand_dims(digits.images.T, axis=0).T
y = lb.fit_transform(digits.target)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=100)
# Start a Keras sequential model
model = Sequential()
# Set input format shape as (batch, height, width, channels)
K.set_image_data_format('channels_last') # inputs with shape (batch, height, width, channels)
model.add(Convolution2D(filters=4,kernel_size=(3,3),padding='same',input_shape=(8,8,1),activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
# Drop out 5% of training data in each batch
model.add(Flatten())
model.add(Dropout(0.05))
model.add(Dense(10, activation= 'softmax'))
# Set variable 'history' to store callbacks to track the validation loss
history = History()
# Compile the model
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# Fit the model and save the callbacks of validation loss and accuracy to 'history'
model.fit(X_train,y_train, epochs=100, batch_size= 128, callbacks=[history])
使用可视化评估预测结果
我们已经指定了回调函数,保存每个训练轮次的损失和准确率信息,作为变量history进行保存。我们可以从字典history.history中提取这些数据。让我们查看一下字典的keys:
print(history.history.keys())
这将输出dict_keys(['loss', 'acc'])。
接下来,我们将在折线图中绘制损失函数和准确率随训练轮次变化的情况:
import pandas as pd
import matplotlib
matplotlib.style.use('seaborn')
# Here plots the loss function graph along Epochs
pd.DataFrame(history.history['loss']).plot()
plt.legend([])
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Validation loss across 100 epochs',fontsize=20,fontweight='bold')
plt.show()
# Here plots the percentage of accuracy along Epochs
pd.DataFrame(history.history['acc']).plot()
plt.legend([])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Accuracy loss across 100 epochs',fontsize=20,fontweight='bold')
plt.show()
在训练过程中,我们可以看到损失函数正在减少,准确率也在增加,这是我们很高兴看到的现象。以下是展示损失函数的第一张图:

下一张图展示了准确率在训练轮次中的变化:

从这些截图中,我们可以观察到随着每个训练轮次的进行,损失逐渐减少,准确率逐渐增加,期间有时会上下波动。我们可以观察到最终的准确率是否令人满意,学习率是否合适,并在必要时优化模型。
检查每个数字的预测性能
我们首先将标签从 one-hot 格式还原为整数列表:
y_test1 = model.predict(X_test)
y_test1 = lb.fit_transform(np.round(y_test1))
y_test1 = np.argmax(y_test1, axis=1)
y_test = np.argmax(y_test, axis=1)
我们将提取错误标记图像的索引,并用它们来获取相应的真实标签和预测标签:
import numpy as np
mislabeled_indices = np.arange(len(y_test))[y_test!=y_test1]
true_labels = np.asarray([y_test[i] for i in mislabeled_indices])
predicted_labels = np.asarray([y_test1[i] for i in mislabeled_indices])
print(mislabeled_indices)
print(true_labels)
print(predicted_labels)
输出如下,包含了错误标记图像的索引、真实标签和预测标签的 NumPy 数组:
[ 1 8 56 97 117 186 188 192 198 202 230 260 291 294 323 335 337]
[9 7 8 2 4 4 2 4 8 9 6 9 7 6 8 8 1]
[3 9 5 0 9 1 1 9 1 3 0 3 8 8 1 3 2]
让我们统计每个数字被错误标记的样本数量。我们将把计数结果存储到一个列表中:
mislabeled_digit_counts = [len(true_labels[true_labels==i]) for i in range(10)]
现在,我们将绘制一个条形图,显示每个数字的错误标记样本比例:
# Calculate the ratio of mislabeled samples
total_digit_counts = [len(y_test[y_test==i]) for i in range(10)]
mislabeled_ratio = [mislabeled_digit_counts[i]/total_digit_counts[i] for i in range(10)]
pd.DataFrame(mislabeled_ratio).plot(kind='bar')
plt.xticks(rotation=0)
plt.xlabel('Digit')
plt.ylabel('Mislabeled ratio')
plt.legend([])
plt.show()
这段代码生成一个条形图,显示我们模型错误标记每个数字的比例:

从前面的图中,我们看到数字 8 是模型最容易误识别的数字。我们来看看为什么。
提取错误预测的图像
与我们在本章开头所做的类似,我们将提取出数字图像。这次,我们挑选出被错误标记的图像,因为这些才是我们关注的对象。我们将再次挑选出 10 张图像,并将它们放入子图网格中。我们将真实标签用绿色写在底部作为xlabel,并将预测的错误标签用红色写在顶部作为title,对于每个子图中的图像:
import matplotlib.pyplot as plt
nrows, ncols = 2, 5
plt.figure(figsize=(6,3))
for i in range(ncols * nrows):
j = mislabeled_indices[i]
ax = plt.subplot(nrows, ncols, i + 1)
ax.imshow(X_test[j].reshape(8,8),cmap='gray_r')
plt.xticks([])
plt.yticks([])
plt.title(y_test1[j],color='red')
plt.xlabel(y_test[j],color='green')
plt.show()
让我们看看这些图像的效果。你认为这些手写数字更像真实标签还是错误预测的标签呢?

我们可以观察到,对于某些图像,即使用肉眼看,8x8 分辨率下也很难识别真实标签,比如底部行中间的数字4。然而,同一行最左侧的数字4应该足够清晰,便于人类识别。从这里我们可以估算出通过额外训练和优化模型,准确度最大可能的提升。这将指导我们是否值得投入更多精力去改进模型,或者下一步应该获取或生成哪种训练数据,以获得更好的结果。
同时,请注意,训练和测试数据集通常包含来自不同分布的样本。你可以通过下载 UCI ML 的实际训练数据集,使用更大的 MNIST 数据集(通过 Keras 下载,甚至是抓取或自己生成数据)来重复这一过程,作为练习。
总结
恭喜你!你已经完成了这一章以及整本书。在这一章中,我们整合了各种数据可视化技术,并结合了一个分析项目工作流程,从数据的初步检查和探索性分析,到模型构建和评估。给自己热烈的掌声,准备好跃进数据科学之旅吧!


浙公网安备 33010602011771号