Pandas-数据分析实用指南第二版-三-

78 阅读49分钟

Pandas 数据分析实用指南第二版(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:第五章:使用 Pandas 和 Matplotlib 可视化数据

到目前为止,我们一直在处理严格的表格格式数据。然而,人类大脑擅长识别视觉模式;因此,我们的自然下一步是学习如何将数据可视化。可视化使得我们更容易发现数据中的异常,并向他人解释我们的发现。然而,我们不应将数据可视化仅仅保留给向他人呈现结论的人,因为在我们的探索性数据分析中,数据可视化对于帮助我们迅速且全面地理解数据至关重要。

有许多种类的可视化远超我们之前可能见过的内容。在本章中,我们将介绍最常见的图表类型,如折线图、直方图、散点图和条形图,以及基于这些类型的一些其他图表。我们不会讨论饼图——它们通常很难正确阅读,而且有更好的方式传达我们的观点。

Python 有许多用于创建可视化的库,但主要用于数据分析(以及其他目的)的是 matplotlibmatplotlib 库刚开始学习时可能有点棘手,但幸运的是,pandas 为一些 matplotlib 功能提供了自己的封装,使我们能够创建许多不同类型的可视化,而不需要写一行 matplotlib 代码(或者至少,写得非常少)。对于那些不在 pandasmatplotlib 内置的更复杂图表类型,我们有 seaborn 库,我们将在下一章讨论。有了这三个工具,我们应该能够创建大多数(如果不是全部)我们所需要的可视化。动画和交互式图表超出了本书的范围,但你可以在进一步阅读部分找到更多信息。

在本章中,我们将覆盖以下主题:

  • matplotlib 简介

  • 使用 pandas 绘图

  • pandas.plotting 模块

本章材料

本章的材料可以在 GitHub 上找到,链接:github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/tree/master/ch_05。我们将使用三个数据集,这些数据集都可以在 data/ 目录中找到。在 fb_stock_prices_2018.csv 文件中,我们有 Facebook 股票从 2018 年 1 月到 12 月的每日开盘价、最高价、最低价和收盘价,以及交易量。这些数据是通过 stock_analysis 包获取的,我们将在第七章金融分析 - 比特币与股票市场中构建该包。股市在周末休市,因此我们只有交易日的数据。

earthquakes.csv文件包含从mag列收集的地震数据,包括震级(mag列)、震中测量的尺度(magType列)、发生的时间(time列)和地点(place列),以及标明地震发生所在州或国家的parsed_place列(我们在第二章,《使用 Pandas DataFrame》中添加了此列)。其他不必要的列已被删除。

covid19_cases.csv文件中,我们有来自欧洲疾病预防控制中心ECDC)提供的全球各国每日新增 COVID-19 确诊病例数数据集的导出文件,可以通过www.ecdc.europa.eu/en/publications-data/download-todays-data-geographic-distribution-covid-19-cases-worldwide获取。对于脚本化或自动化收集此数据,ECDC 通过opendata.ecdc.europa.eu/covid19/casedistribution/csv提供当天的 CSV 文件。我们将使用的快照数据收集日期为 2020 年 9 月 19 日,包含了 2019 年 12 月 31 日至 2020 年 9 月 18 日各国新增 COVID-19 病例数,并包含 2020 年 9 月 19 日的部分数据。在本章中,我们将查看 2020 年 1 月 18 日至 2020 年 9 月 18 日这 8 个月的数据。

在本章中,我们将通过三个笔记本进行学习。这些笔记本按使用顺序编号——每个笔记本对应本章的一个主要部分。我们将在1-introducing_matplotlib.ipynb笔记本中介绍 Python 绘图,首先介绍matplotlib。然后,我们将在2-plotting_with_pandas.ipynb笔记本中学习如何使用pandas创建可视化。最后,我们将在3-pandas_plotting_module.ipynb笔记本中探索pandas提供的一些额外绘图选项。在需要切换笔记本时,系统会提示您。

matplotlib 介绍

pandasseaborn的绘图功能由matplotlib提供支持:这两个包为matplotlib中的底层功能提供了封装。因此,我们只需编写最少的代码,就能拥有丰富的可视化选项;然而,这也有代价:我们在创作时的灵活性有所降低。

我们可能会发现pandasseaborn的实现无法完全满足我们的需求,实际上,使用它们创建图形后可能无法覆盖某些特定设置,这意味着我们将不得不使用matplotlib来完成一些工作。此外,许多对最终可视化效果的微调将通过matplotlib命令来处理,我们将在下一章中讨论这些内容。因此,了解matplotlib的工作原理将对我们大有裨益。

基础知识

matplotlib包相当大,因为它涵盖了很多功能。幸运的是,对于我们大多数的绘图任务,我们只需要pyplot模块,它提供了类似 MATLAB 的绘图框架。偶尔,我们会需要导入其他模块来处理其他任务,比如动画、改变样式或修改默认参数;我们将在下一章看到一些示例。

我们将只导入pyplot模块,而不是导入整个matplotlib包,使用点(.)符号;这样可以减少输入量,并且避免在内存中占用不需要的代码空间。请注意,pyplot通常会被别名为plt

import matplotlib.pyplot as plt

在我们查看第一个图形之前,先来了解如何实际查看它们。Matplotlib 将通过绘图命令来创建我们的可视化;然而,直到我们请求查看它之前,我们是看不到可视化的。这样做是为了让我们在准备好最终版本之前,能够不断通过额外的代码调整可视化效果。除非我们保存对图形的引用,否则一旦它被显示出来,我们将需要重新创建它以进行更改。这是因为对上一个图形的引用已经被销毁,以释放内存资源。

Matplotlib 使用plt.show()函数来显示可视化。每创建一次可视化都必须调用它。当使用 Python Shell 时,它还会阻止其他代码的执行,直到窗口被关闭,因为它是一个阻塞函数。在 Jupyter Notebooks 中,我们只需使用一次%matplotlib inline,我们的可视化将在执行包含可视化代码的单元时自动显示。魔法命令(或简称magics)在 Jupyter Notebook 单元内作为常规代码执行。如果到目前为止你还不热衷于使用 Jupyter Notebooks,并希望现在设置它,你可以参考第一章数据分析入门

重要提示

%matplotlib inline 魔法命令将图表的静态图像嵌入到我们的笔记本中。另一个常见的选项是 %matplotlib notebook 魔法命令。它通过允许进行缩放和调整大小等操作,为图表提供了一定程度的交互性,但请注意,如果你使用 JupyterLab,还需要进行一些额外的设置,而且可能会出现一些困惑的错误,具体取决于笔记本中运行的代码。欲了解更多信息,请查阅此文章:medium.com/@1522933668924/using-matplotlib-in-jupyter-notebooks-comparing-methods-and-some-tips-python-c38e85b40ba1

让我们在 1-introducing_matplotlib.ipynb 笔记本中创建我们的第一个图表,使用本章仓库中的 fb_stock_prices_2018.csv 文件中的 Facebook 股票价格数据。首先,我们需要导入 pyplotpandas(在这个示例中,我们将使用 plt.show(),因此不需要在这里运行魔法命令):

>>> import matplotlib.pyplot as plt
>>> import pandas as pd

接下来,我们读取 CSV 文件,并将 date 列指定为索引,因为我们已经知道数据的格式,来自前面的章节:

>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv', 
...     index_col='date',
...     parse_dates=True
... )

为了理解 Facebook 股票随时间的变化,我们可以创建一条显示每日开盘价的折线图。对于这个任务,我们将使用plt.plot()函数,分别提供用于 x 轴和 y 轴的数据。接着,我们会调用plt.show()来显示图表:

>>> plt.plot(fb.index, fb.open)
>>> plt.show()

结果如下图所示:

图 5.1 – 使用 matplotlib 绘制的第一个图表

图 5.1 – 使用 matplotlib 绘制的第一个图表

如果我们想要展示这个可视化图表,我们需要返回并添加轴标签、图表标题、图例(如果适用),并可能调整 y 轴的范围;这些内容将在下一章讨论如何格式化和自定义图表外观时进行讲解。至少,Pandas 和 seaborn 会为我们处理一些部分。

在本书的其余部分,我们将使用 %matplotlib inline 魔法命令(记住,这个命令仅在 Jupyter Notebook 中有效),所以在绘图代码后,我们将不再调用 plt.show()。以下代码与前面的代码块产生相同的输出:

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>> import pandas as pd
>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv', 
...     index_col='date',
...     parse_dates=True
... )
>>> plt.plot(fb.index, fb.open)

重要提示

如果你在使用 Jupyter Notebook,请确保现在运行 %matplotlib inline 魔法命令。这将确保本章其余部分的绘图代码能够自动显示输出。

我们还可以使用plt.plot()函数生成散点图,只要在绘图时指定格式字符串作为第三个参数。格式字符串的形式为'[marker][linestyle][color]';例如,'--k'表示黑色虚线。由于我们不希望散点图中显示线条,所以省略了linestyle部分。我们可以使用'or'格式字符串来绘制红色圆形散点图;其中,o代表圆形,r代表红色。以下代码生成了一个高价与低价的散点图。请注意,我们可以将数据框传递给data参数,然后使用列名字符串,而不是将序列作为xy传递:

>>> plt.plot('high', 'low', 'or', data=fb.head(20))

除了大幅波动的日子,我们期望这些点呈现出一条直线的形式,因为高价和低价不会相差太远。这在大多数情况下是正确的,但要小心自动生成的刻度——x 轴和 y 轴并未完全对齐:

图 5.2 – 使用 matplotlib 创建散点图

图 5.2 – 使用 matplotlib 创建散点图

请注意,指定格式字符串时有一定的灵活性。例如,形如'[color][marker][linestyle]'的格式字符串是有效的,除非它具有歧义。下表展示了如何为各种绘图样式构建格式字符串的示例;完整的选项列表可以在文档的注释部分找到,地址为matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html

图 5.3 – matplotlib 的样式快捷方式

图 5.3 – matplotlib 的样式快捷方式

格式字符串是一种方便的方式,可以一次性指定多个选项,幸运的是,正如我们在使用 pandas 绘图部分中将看到的,它同样适用于pandas中的plot()方法。然而,如果我们宁愿单独指定每个选项,也可以使用colorlinestylemarker参数;可以在文档中查看我们可以作为关键字参数传递给plt.plot()的值——pandas会将这些参数传递给matplotlib

提示

作为为每个绘制变量定义样式的替代方案,可以尝试使用matplotlib团队提供的cycler,来指定matplotlib应当在哪些组合之间循环(matplotlib.org/gallery/color/color_cycler.html)。我们将在第七章中看到这个例子的应用,金融分析 – 比特币与股市

要使用matplotlib创建直方图,我们需要使用hist()函数。让我们使用ml震级类型测量的earthquakes.csv文件中的地震震级数据,制作一个直方图:

>>> quakes = pd.read_csv('data/earthquakes.csv')
>>> plt.hist(quakes.query('magType == "ml"').mag)

由此产生的直方图可以帮助我们了解使用ml测量技术时,预期地震震级的范围:

图 5.4 – 使用 matplotlib 绘制直方图

](tos-cn-i-73owjymdk6/ef589361cbf94271898d7d0365669e9a)

图 5.4 – 使用 matplotlib 绘制直方图

正如我们可以猜到的那样,震级通常较小,分布看起来相对正常。然而,关于直方图,有一点需要注意——箱子大小很重要。在某些情况下,我们可以通过改变数据被分成的箱子数量,来改变直方图所表示的分布。例如,如果我们使用不同数量的箱子制作两个直方图,分布会看起来不同:

>>> x = quakes.query('magType == "ml"').mag
>>> fig, axes = plt.subplots(1, 2, figsize=(10, 3))
>>> for ax, bins in zip(axes, [7, 35]):
...     ax.hist(x, bins=bins)
...     ax.set_title(f'bins param: {bins}')

请注意,左侧子图中的分布似乎是单峰的,而右侧子图中的分布看起来是双峰的:

图 5.5 – 不同的箱子大小会大幅改变直方图

图 5.5 – 不同的箱子大小会大幅改变直方图

提示

关于选择箱子数量的一些常见经验法则,可以参考en.wikipedia.org/wiki/Histogram#Number_of_bins_and_width。然而,请注意,在某些情况下,蜜蜂群图可能比直方图更容易解释;这一点可以通过seaborn来实现,我们将在第六章中看到,使用 Seaborn 进行绘图及自定义技巧

从这个例子中,还有几个额外的注意事项,我们将在下一节的图形组件中讨论:

  • 我们可以制作子图。

  • pyplot中绘制函数也可以作为matplotlib对象的方法使用,例如FigureAxes对象。

关于基本用法的最后一件事,我们会发现保存图像为图片非常方便——我们不应该仅限于在 Python 中展示图形。我们可以通过传递保存图像的路径,使用plt.savefig()函数来保存最后一张图像;例如,plt.savefig('my_plot.png')。请注意,如果在保存之前调用了plt.show(),那么文件将是空的,因为在调用plt.show()之后,最后一张图形的引用将会消失(matplotlib会关闭Figure对象以释放内存中的资源)。通过使用%matplotlib inline魔法命令,我们可以在同一个单元格中同时查看并保存图像。

图形组件

在之前使用plt.plot()的例子中,我们不需要创建Figure对象——matplotlib在后台为我们处理了它。然而,正如我们在创建图 5.5时看到的那样,任何超出基本图形的内容都需要稍微更多的工作,包括我们自己创建Figure对象。Figure类是matplotlib可视化的顶层对象。它包含Axes对象,而Axes对象本身又包含其他绘图对象,如线条和刻度。对于子图来说,Figure对象包含具有附加功能的Axes对象。

我们使用plt.figure()函数来创建Figure对象;这些对象在添加图表之前不会有任何Axes对象:

>>> fig = plt.figure()
<Figure size 432x288 with 0 Axes>

plt.subplots()函数创建一个带有Axes对象的Figure对象,用于指定排列方式的子图。如果我们请求plt.subplots()创建一行一列,它将返回一个包含一个Axes对象的Figure对象。这在编写根据输入生成子图布局的函数时非常有用,因为我们不需要为单个子图处理特殊情况。这里,我们将指定一行两列的排列方式;这将返回一个(Figure, Axes)元组,我们可以对其进行解包:

>>> fig, axes = plt.subplots(1, 2)

使用%matplotlib inline魔法命令时,我们会看到创建的图表:

图 5.6 – 创建子图

图 5.6 – 创建子图

使用plt.subplots()的替代方法是,在运行plt.figure()之后,使用Figure对象上的add_axes()方法。add_axes()方法接受一个列表,形式为[left, bottom, width, height],表示子图在图形中占据的区域,它是图形维度的比例:

>>> fig = plt.figure(figsize=(3, 3))
>>> outside = fig.add_axes([0.1, 0.1, 0.9, 0.9])
>>> inside = fig.add_axes([0.7, 0.7, 0.25, 0.25])

这使得可以在图表内部创建子图:

图 5.7 – 使用 matplotlib 绘制带有嵌套图的图表

图 5.7 – 使用 matplotlib 绘制带有嵌套图的图表

如果我们的目标是将所有图表分开但不一定是相同大小的,我们可以使用Figure对象上的add_gridspec()方法来创建子图的网格。然后,我们可以运行add_subplot(),传入网格中给定子图应该占据的区域:

>>> fig = plt.figure(figsize=(8, 8))
>>> gs = fig.add_gridspec(3, 3)
>>> top_left = fig.add_subplot(gs[0, 0])
>>> mid_left = fig.add_subplot(gs[1, 0])
>>> top_right = fig.add_subplot(gs[:2, 1:])
>>> bottom = fig.add_subplot(gs[2,:])

这将导致以下布局:

图 5.8 – 使用 matplotlib 构建自定义图表布局

图 5.8 – 使用 matplotlib 构建自定义图表布局

在上一节中,我们讨论了如何使用plt.savefig()保存可视化,但我们也可以使用Figure对象上的savefig()方法:

>>> fig.savefig('empty.png')

这一点非常重要,因为使用plt.<func>()时,我们只能访问最后一个Figure对象;然而,如果我们保存对Figure对象的引用,我们就可以操作其中任何一个,不管它们是在什么时候创建的。此外,这也预示了本章中你会注意到的一个重要概念:FigureAxes对象具有与其pyplot函数对应项相似或相同的方法名称。

尽管能够引用我们创建的所有Figure对象非常方便,但在完成工作后关闭它们是一个好习惯,这样我们就不会浪费任何资源。这可以通过plt.close()函数来实现。如果我们不传入任何参数,它将关闭最后一个Figure对象;但是,我们可以传入特定的Figure对象,以便仅关闭该对象,或者传入'all'来关闭我们打开的所有Figure对象:

>>> plt.close('all')

直接操作FigureAxes对象非常重要,因为这可以让你对结果的可视化进行更精细的控制。下一章中会更加明显地体现这一点。

其他选项

我们的一些可视化图表看起来有些压缩。为了解决这个问题,我们可以在调用plt.figure()plt.subplots()时传入figsize的值。我们用一个(宽度, 高度)的元组来指定尺寸,单位是英寸。我们将会看到的pandasplot()方法也接受figsize参数,所以请记住这一点:

>>> fig = plt.figure(figsize=(10, 4))
<Figure size 720x288 with 0 Axes>
>>> fig, axes = plt.subplots(1, 2, figsize=(10, 4))

注意,这些子图比我们没有指定figsize时的图 5.6中的子图更接近正方形:

图 5.9 – 指定绘图大小

图 5.9 – 指定绘图大小

为我们的每个图表单独指定figsize参数还不算太麻烦。然而,如果我们发现每次都需要调整为相同的尺寸,有一个更好的替代方法。Matplotlib 将其默认设置保存在rcParams中,rcParams像一个字典一样运作,这意味着我们可以轻松覆盖会话中的某些设置,并在重启 Python 会话时恢复默认值。由于该字典中有许多选项(写作时超过 300 个),让我们随便选择一些,以了解可用的选项:

>>> import random
>>> import matplotlib as mpl
>>> rcparams_list = list(mpl.rcParams.keys())
>>> random.seed(20) # make this repeatable
>>> random.shuffle(rcparams_list)
>>> sorted(rcparams_list[:20])
['axes.axisbelow',
 'axes.formatter.limits',
 'boxplot.vertical',
 'contour.corner_mask',
 'date.autoformatter.month',
 'legend.labelspacing',
 'lines.dashed_pattern',
 'lines.dotted_pattern',
 'lines.scale_dashes',
 'lines.solid_capstyle',
 'lines.solid_joinstyle',
 'mathtext.tt',
 'patch.linewidth',
 'pdf.fonttype',
 'savefig.jpeg_quality',
 'svg.fonttype',
 'text.latex.preview',
 'toolbar',
 'ytick.labelright',
 'ytick.minor.size'] 

如你所见,这里有许多选项可以调整。让我们检查一下当前figsize的默认值是什么:

>>> mpl.rcParams['figure.figsize']
[6.0, 4.0]

要为当前会话更改此设置,只需将其设置为新值:

>>> mpl.rcParams['figure.figsize'] = (300, 10)
>>> mpl.rcParams['figure.figsize']
[300.0, 10.0]

在继续之前,让我们使用mpl.rcdefaults()函数恢复默认设置。figsize的默认值实际上与我们之前的不同;这是因为%matplotlib inline在首次运行时会为一些与绘图相关的参数设置不同的值(github.com/ipython/ipykernel/blob/master/ipykernel/pylab/config.py#L42-L56):

>>> mpl.rcdefaults()
>>> mpl.rcParams['figure.figsize']
[6.8, 4.8]

请注意,如果我们知道其组(在本例中是figure)和参数名称(figsize),也可以使用plt.rc()函数更新特定的设置。正如我们之前所做的,我们可以使用plt.rcdefaults()来重置默认值:

# change `figsize` default to (20, 20)
>>> plt.rc('figure', figsize=(20, 20)) 
>>> plt.rcdefaults() # reset the default

提示

如果我们发现每次启动 Python 时都需要做相同的更改,那么我们应该考虑读取配置文件,而不是每次更新默认值。有关更多信息,请参考mpl.rc_file()函数。

使用 pandas 进行绘图

SeriesDataFrame 对象都有一个 plot() 方法,可以让我们创建几种不同类型的图表,并控制一些格式方面的内容,如子图布局、图形大小、标题以及是否共享子图之间的坐标轴。这使得绘制数据变得更加方便,因为通过一次方法调用就可以完成大部分用于创建可展示图表的工作。实际上,pandas 在背后调用了多个 matplotlib 方法来生成图表。plot() 方法中一些常用的参数包括以下内容:

图 5.10 – 常用的 pandas 绘图参数

图 5.10 – 常用的 pandas 绘图参数

与我们在讨论 matplotlib 时看到的每种图表类型都有单独的函数不同,pandasplot() 方法允许我们使用 kind 参数来指定我们想要的图表类型。图表类型的选择将决定哪些其他参数是必需的。我们可以使用 plot() 方法返回的 Axes 对象进一步修改图表。

让我们在 2-plotting_with_pandas.ipynb 笔记本中探索这个功能。在我们开始之前,我们需要处理本节的导入,并读取将要使用的数据(Facebook 股票价格、地震数据和 COVID-19 病例数据):

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> import pandas as pd
>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv', 
...     index_col='date',
...     parse_dates=True
... )
>>> quakes = pd.read_csv('data/earthquakes.csv')
>>> covid = pd.read_csv('data/covid19_cases.csv').assign(
...     date=lambda x: \
...         pd.to_datetime(x.dateRep, format='%d/%m/%Y')
... ).set_index('date').replace(
...     'United_States_of_America', 'USA'
... ).sort_index()['2020-01-18':'2020-09-18']

在接下来的几节中,我们将讨论如何为特定的分析目标生成合适的可视化图表,比如展示随时间的变化或数据中变量之间的关系。请注意,在可能的情况下,图表已被样式化,使其可以在本书中以黑白形式进行解读。

随时间演变

在处理时间序列数据时(例如存储在 fb 变量中的 Facebook 股票数据),我们通常希望展示数据随时间的变化。为此,我们使用折线图,在某些情况下使用条形图(在计数与频率部分中介绍)。对于折线图,我们只需在 plot() 中提供 kind='line',并指定哪些列作为 xy。请注意,我们实际上不需要为 x 提供列,因为 pandas 默认使用索引(这也使得生成 Series 对象的折线图成为可能)。此外,注意我们可以像在 matplotlib 图表中那样,为 style 参数提供格式字符串:

>>> fb.plot(
...     kind='line', y='open', figsize=(10, 5), style='-b',
...     legend=False, title='Evolution of Facebook Open Price'
... )

这给我们一个与 matplotlib 类似的图表;然而,在这次方法调用中,我们只为这个图表指定了图形的大小,关闭了图例,并为其设置了标题:

图 5.11 – 使用 pandas 绘制的第一个图表

图 5.11 – 使用 pandas 绘制的第一个图表

matplotlib 一样,我们不必使用样式格式字符串——相反,我们可以将每个组件与其关联的关键字分开传递。例如,以下代码给出的结果与之前的结果相同:

fb.plot(
    kind='line', y='open', figsize=(10, 5),
    color='blue', linestyle='solid',
    legend=False, title='Evolution of Facebook Open Price'
)

我们在使用 plot() 方法时并不局限于一次绘制一条线;我们也可以传递一个列列表来绘制,并单独设置样式。请注意,实际上我们不需要指定 kind='line',因为这是默认值:

>>> fb.first('1W').plot(
...     y=['open', 'high', 'low', 'close'], 
...     style=['o-b', '--r', ':k', '.-g'],
...     title='Facebook OHLC Prices during '
...           '1st Week of Trading 2018'
... ).autoscale() # add space between data and axes

这将生成以下图形,其中每条线的样式不同:

图 5.12 – 绘制多个列

图 5.12 – 绘制多个列

此外,我们可以轻松地让 pandas 在同一调用中绘制所有列。xy 参数可以接受单个列名或它们的列表;如果我们不提供任何内容,pandas 将使用所有列。请注意,当 kind='line' 时,列必须作为 y 参数传递;然而,其他绘图类型也支持将列列表传递给 x。在这种情况下,要求子图而不是将所有线条绘制在同一图中可能会更有帮助。让我们将 Facebook 数据中的所有列作为折线图进行可视化:

>>> fb.plot(
...     kind='line', subplots=True, layout=(3, 2),
...     figsize=(15, 10), title='Facebook Stock 2018'
... )

使用 layout 参数,我们告诉 pandas 如何排列我们的子图(三行两列):

图 5.13 – 使用 pandas 创建子图

图 5.13 – 使用 pandas 创建子图

请注意,子图自动共享 x 轴,因为它们共享一个索引。y 轴没有共享,因为 volume 时间序列的尺度不同。我们可以通过在某些绘图类型中将 sharexsharey 参数与布尔值一起传递给 plot() 来改变这种行为。默认情况下会渲染图例,因此对于每个子图,我们在图例中有一个单独的项目,表示其包含的数据。在这种情况下,我们没有通过 title 参数提供子图标题列表,因为图例已起到了这个作用;然而,我们为整个图形传递了一个单一字符串作为标题。总结一下,当处理子图时,我们在标题方面有两种选择:

  • 传递一个字符串作为整个图形的标题。

  • 传递一个字符串列表,用作每个子图的标题。

有时,我们希望制作子图,每个子图包含一些变量供比较。可以通过首先使用 plt.subplots() 创建子图,然后将 Axes 对象提供给 ax 参数来实现这一点。为了说明这一点,让我们来看一下中国、西班牙、意大利、美国、巴西和印度的 COVID-19 每日新增病例数据。这是长格式数据,因此我们必须首先将其透视,使日期(我们在读取 CSV 文件时设置为索引)成为透视表的索引,国家(countriesAndTerritories)成为列。由于这些值波动较大,我们将使用 第四章 中介绍的 rolling() 方法绘制新增病例的 7 天移动平均值,聚合 Pandas 数据框

>>> new_cases_rolling_average = covid.pivot_table(
...     index=covid.index,
...     columns='countriesAndTerritories',
...     values='cases'
... ).rolling(7).mean()

我们不会为每个国家创建单独的图表(这会使比较变得更困难),也不会将它们全部绘制在一起(这样会使较小的值难以看到),而是将病例数量相似的国家绘制在同一个子图中。我们还将使用不同的线条样式,以便在黑白图中区分它们:

>>> fig, axes = plt.subplots(1, 3, figsize=(15, 5))
>>> new_cases_rolling_average[['China']]\
...     .plot(ax=axes[0], style='-.c')
>>> new_cases_rolling_average[['Italy', 'Spain']].plot(
...     ax=axes[1], style=['-', '--'],
...     title='7-day rolling average of new '
...           'COVID-19 cases\n(source: ECDC)'
... )
>>> new_cases_rolling_average[['Brazil', 'India', 'USA']]\ 
...     .plot(ax=axes[2], style=['--', ':', '-'])

通过直接使用matplotlib生成每个子图的Axes对象,我们获得了更多的布局灵活性:

图 5.14 – 控制每个子图中绘制的数据

图 5.14 – 控制每个子图中绘制的数据

在前面的图中,我们能够比较病例数量相似的国家,但由于比例问题,我们无法将所有国家都绘制在同一个子图中。解决这个问题的一种方法是使用面积图,这样我们就能在可视化整体 7 天滚动平均的新增 COVID-19 病例的同时,看到每个国家对总数的贡献。为了提高可读性,我们将意大利和西班牙归为一组,并为美国、巴西和印度以外的国家创建另一个类别:

>>> cols = [
...     col for col in new_cases_rolling_average.columns 
...     if col not in [
...         'USA', 'Brazil', 'India', 'Italy & Spain'
...     ]
... ]
>>> new_cases_rolling_average.assign(
...     **{'Italy & Spain': lambda x: x.Italy + x.Spain}
... ).sort_index(axis=1).assign(
...     Other=lambda x: x[cols].sum(axis=1)
... ).drop(columns=cols).plot(
...     kind='area', figsize=(15, 5), 
...     title='7-day rolling average of new '
...           'COVID-19 cases\n(source: ECDC)'
... )

对于那些以黑白查看结果图的人,巴西是底层,印度在其上方,以此类推。图表区域的合计高度表示总体值,而给定阴影区域的高度表示该国的值。这表明,超过一半的每日新增病例集中在巴西、印度、意大利、西班牙和美国:

图 5.15 – 创建面积图

图 5.15 – 创建面积图

另一种可视化随时间演变的方法是查看随时间累积的和。我们将绘制中国、西班牙、意大利、美国、巴西和印度的 COVID-19 累计病例数,使用ax参数再次创建子图。为了计算随时间的累计和,我们按位置(countriesAndTerritories)和日期分组,日期是我们的索引,因此我们使用pd.Grouper();这次,我们将使用groupby()unstack()将数据透视为宽格式,用于绘图:

>>> fig, axes = plt.subplots(1, 3, figsize=(15, 3))
>>> cumulative_covid_cases = covid.groupby(
...     ['countriesAndTerritories', pd.Grouper(freq='1D')]
... ).cases.sum().unstack(0).apply('cumsum')
>>> cumulative_covid_cases[['China']]\
...     .plot(ax=axes[0], style='-.c')
>>> cumulative_covid_cases[['Italy', 'Spain']].plot(
...     ax=axes[1], style=['-', '--'], 
...     title='Cumulative COVID-19 Cases\n(source: ECDC)'
... )
>>> cumulative_covid_cases[['Brazil', 'India', 'USA']]\ 
...     .plot(ax=axes[2], style=['--', ':', '-'])

观察累计 COVID-19 病例数据表明,尽管中国和意大利似乎已经控制了 COVID-19 病例,但西班牙、美国、巴西和印度仍在挣扎:

图 5.16 – 随时间绘制累计和

图 5.16 – 随时间绘制累计和

重要说明

我们在这一部分多次使用了虚线和点线,以确保生成的图表可以在黑白模式下进行解读;然而,请注意,当以彩色方式展示这些图表时,接受默认的颜色和线条样式就足够了。通常,不同的线条样式表示数据类型的差异——例如,我们可以使用实线来表示时间演变,使用虚线来表示滚动平均值。

变量之间的关系

当我们想要可视化变量之间的关系时,我们通常从散点图开始,散点图展示了不同 x 变量值下的 y 变量值。这使我们非常容易发现相关性和可能的非线性关系。在上一章,当我们查看 Facebook 股票数据时,我们看到高交易量的天数似乎与股价的大幅下跌相关。我们可以使用散点图来可视化这种关系:

>>> fb.assign(
...     max_abs_change=fb.high - fb.low
... ).plot(
...     kind='scatter', x='volume', y='max_abs_change',
...     title='Facebook Daily High - Low vs. Volume Traded'
... )

似乎存在某种关系,但它似乎不是线性的:

图 5.17 – 使用 pandas 绘制散点图

图 5.17 – 使用 pandas 绘制散点图

让我们试着取交易量的对数。为此,我们有几个选择:

  • 创建一个新的列,将交易量取对数,使用 np.log()

  • 通过将 logx=True 传递给 plot() 方法或调用 plt.xscale('log') 来对 x 轴使用对数刻度。

在这种情况下,最有意义的是仅仅改变数据的显示方式,因为我们并不会使用新的列:

>>> fb.assign(
...     max_abs_change=fb.high - fb.low
... ).plot(
...     kind='scatter', x='volume', y='max_abs_change',
...     title='Facebook Daily High - '
...           'Low vs. log(Volume Traded)',
...     logx=True
... )

修改 x 轴刻度后,我们得到如下散点图:

图 5.18 – 对 x 轴应用对数刻度

图 5.18 – 对 x 轴应用对数刻度

提示

pandas 中的 plot() 方法有三个参数用于对数刻度:logx/logy 用于单轴调整,loglog 用于同时设置两个轴为对数刻度。

散点图的一个问题是,很难分辨给定区域内点的集中程度,因为它们只是简单地叠加在一起。我们可以使用 alpha 参数来控制点的透明度;这个参数的值范围从 01,其中 0 表示完全透明,1 表示完全不透明。默认情况下,它们是完全不透明的(值为 1);然而,如果我们使它们更透明,我们应该能够看到一些重叠:

>>> fb.assign(
...     max_abs_change=fb.high - fb.low
... ).plot(
...     kind='scatter', x='volume', y='max_abs_change',
...     title='Facebook Daily High - '
...           'Low vs. log(Volume Traded)', 
...     logx=True, alpha=0.25
... )

现在我们可以开始看出图表左下区域的点密度,但仍然相对较难:

图 5.19 – 修改透明度以可视化重叠

图 5.19 – 修改透明度以可视化重叠

幸运的是,我们还有另一种可用的图表类型:hexbin六边形图通过将图表划分为一个六边形网格,并根据每个六边形内的点密度来进行着色,形成一个二维直方图。让我们将数据以六边形图的形式展示:

>>> fb.assign(
...     log_volume=np.log(fb.volume),
...     max_abs_change=fb.high - fb.low
... ).plot(
...     kind='hexbin', 
...     x='log_volume', 
...     y='max_abs_change', 
...     title='Facebook Daily High - '
...           'Low vs. log(Volume Traded)', 
...     colormap='gray_r', 
...     gridsize=20,
...     sharex=False # bug fix to keep the x-axis label
... )

侧边的颜色条表示颜色与该 bin 中点数之间的关系。我们选择的色图(gray_r)使得高密度的 bins 颜色较深(趋向黑色),低密度的 bins 颜色较浅(趋向白色)。通过传入 gridsize=20,我们指定在 x 轴上使用 20 个六边形,并让 pandas 确定在 y 轴上使用多少个,以使它们大致呈规则形状;不过,我们也可以传入一个元组来选择两个方向上的数量。增大 gridsize 的值会使得 bins 更难以辨识,而减小则会导致 bins 更满,占用更多的空间——我们需要找到一个平衡点:

图 5.20 – 使用 pandas 绘制 hexbins

图 5.20 – 使用 pandas 绘制 hexbins

最后,如果我们只想可视化变量之间的相关性,我们可以绘制一个相关矩阵。使用 pandasmatplotlib 中的 plt.matshow()plt.imshow() 函数。由于需要在同一单元格中运行大量代码,我们将在代码块后立即讨论每个部分的目的:

>>> fig, ax = plt.subplots(figsize=(20, 10))
# calculate the correlation matrix
>>> fb_corr = fb.assign(
...     log_volume=np.log(fb.volume),
...     max_abs_change=fb.high - fb.low
... ).corr()
# create the heatmap and colorbar
>>> im = ax.matshow(fb_corr, cmap='seismic')
>>> im.set_clim(-1, 1)
>>> fig.colorbar(im)
# label the ticks with the column names
>>> labels = [col.lower() for col in fb_corr.columns]
>>> ax.set_xticks(ax.get_xticks()[1:-1])
>>> ax.set_xtickabels(labels, rotation=45)
>>> ax.set_yticks(ax.get_yticks()[1:-1])
>>> ax.set_yticklabels(labels)
# include the value of the correlation coefficient in the boxes
>>> for (i, j), coef in np.ndenumerate(fb_corr):
...     ax.text(
...         i, j, fr'$\rho$ = {coef:.2f}', 
...         ha='center', va='center', 
...         color='white', fontsize=14
...     )

使用 seismic 色图,然后将颜色刻度的限制设置为[-1, 1],因为相关系数的范围就是这些:

im = ax.matshow(fb_corr, cmap='seismic')
im.set_clim(-1, 1) # set the bounds of the color scale
fig.colorbar(im) # add the colorbar to the figure

为了能够读取生成的热图,我们需要用数据中变量的名称标记行和列:

labels = [col.lower() for col in fb_corr.columns]
ax.set_xticks(ax.get_xticks()[1:-1]) # to handle matplotlib bug
ax.set_xticklabels(labels, rotation=45)
ax.set_yticks(ax.get_yticks()[1:-1]) # to handle matplotlib bug
ax.set_yticklabels(labels)

虽然颜色刻度可以帮助我们区分弱相关和强相关,但通常也很有帮助的是在热图上注释实际的相关系数。这可以通过在包含图形的 Axes 对象上使用 text() 方法来实现。对于这个图形,我们放置了白色、居中对齐的文本,表示每对变量组合的皮尔逊相关系数的值:

# iterate over the matrix 
for (i, j), coef in np.ndenumerate(fb_corr): 
    ax.text(
        i, j, 
        fr'$\rho$ = {coef:.2f}', # raw (r), format (f) string
        ha='center', va='center', 
        color='white', fontsize=14
    )

这将生成一个带注释的热图,展示 Facebook 数据集中的变量之间的相关性:

图 5.21 – 将相关性可视化为热图

图 5.21 – 将相关性可视化为热图

图 5.21中,我们可以清楚地看到 OHLC 时间序列之间存在较强的正相关性,以及交易量和最大绝对变化值之间的正相关性。然而,这些组之间存在较弱的负相关性。此外,我们还可以看到,对交易量取对数确实增加了与max_abs_change的相关系数,从 0.64 增加到 0.73。在下一章讨论 seaborn 时,我们将学习一种更简单的生成热图的方法,并更详细地讲解注释。

分布

通常,我们希望可视化数据的分布,以了解数据所呈现的值。根据数据类型的不同,我们可能会选择使用直方图、核密度估计KDEs)、箱型图或经验累积分布函数ECDFs)。在处理离散数据时,直方图是一个很好的起点。让我们来看一下 Facebook 股票的每日交易量直方图:

>>> fb.volume.plot(
...     kind='hist', 
...     title='Histogram of Daily Volume Traded '
...           'in Facebook Stock'
... )
>>> plt.xlabel('Volume traded') # label x-axis (see ch 6)

这是一个很好的实际数据示例,数据显然不是正态分布的。交易量偏右,右侧有一个长尾。回想一下在第四章聚合 Pandas DataFrames 中,我们讨论了分箱并查看了低、中、高交易量时,几乎所有数据都落在低交易量区间,这与我们在此直方图中看到的情况一致:

图 5.22 – 使用 pandas 创建直方图

图 5.22 – 使用 pandas 创建直方图

提示

matplotlib 中的 plt.hist() 函数类似,我们可以通过 bins 参数为箱数提供自定义值。但是,我们必须小心,确保不会误导数据分布。

我们还可以在同一图表上绘制多个直方图,以比较不同的分布,方法是使用 ax 参数为每个图表指定相同的 Axes 对象。在这种情况下,我们必须使用 alpha 参数以便看到任何重叠。由于我们有许多不同的地震测量方法(magType 列),我们可能会想要比较它们所产生的不同震级范围:

>>> fig, axes = plt.subplots(figsize=(8, 5))
>>> for magtype in quakes.magType.unique():
...     data = quakes.query(f'magType == "{magtype}"').mag
...     if not data.empty:
...         data.plot(
...             kind='hist', 
...             ax=axes, 
...             alpha=0.4, 
...             label=magtype, 
...             legend=True, 
...             title='Comparing histograms '
...                   'of earthquake magnitude by magType'
...         )
>>> plt.xlabel('magnitude') # label x-axis (discussed in ch 6)

这表明 ml 是最常见的 magType,其次是 md,它们的震级范围相似;然而,第三常见的 mb 震级更高:

图 5.23 – 使用 pandas 绘制重叠直方图

图 5.23 – 使用 pandas 绘制重叠直方图

在处理连续数据(如股票价格)时,我们可以使用 KDE。让我们看看 Facebook 股票的日最高价的 KDE。请注意,我们可以传递 kind='kde'kind='density'

>>> fb.high.plot(
...     kind='kde', 
...     title='KDE of Daily High Price for Facebook Stock'
... )
>>> plt.xlabel('Price ($)') # label x-axis (discussed in ch 6)

得到的密度曲线有一些左偏:

图 5.24 – 使用 pandas 可视化 KDE

图 5.24 – 使用 pandas 可视化 KDE

我们可能还想将 KDE 可视化叠加在直方图上。Pandas 允许我们传递希望绘制的 Axes 对象,并且在创建可视化后会返回该对象,这使得操作变得非常简单:

>>> ax = fb.high.plot(kind='hist', density=True, alpha=0.5)
>>> fb.high.plot(
...     ax=ax, kind='kde', color='blue', 
...     title='Distribution of Facebook Stock\'s '
...           'Daily High Price in 2018'
... )
>>> plt.xlabel('Price ($)') # label x-axis (discussed in ch 6)

请注意,当我们生成直方图时,必须传入density=True,以确保直方图和 KDE 的y轴处于相同的尺度。否则,KDE 会变得太小,无法看到。然后,直方图会以密度作为y轴进行绘制,这样我们就可以更好地理解 KDE 是如何形成其形状的。我们还增加了直方图的透明度,以便能够看到上面叠加的 KDE 线。请注意,如果我们移除 KDE 调用中的color='blue'部分,我们就不需要更改直方图调用中的alpha值,因为 KDE 和直方图会使用不同的颜色;我们将它们都绘制为蓝色,因为它们表示的是相同的数据:

图 5.25 – 使用 pandas 结合 KDE 和直方图

](tos-cn-i-73owjymdk6/7bebe49e5ed24474a945bde5b6a04573)

图 5.25 – 使用 pandas 结合 KDE 和直方图

KDE 展示了一个估计的概率密度函数PDF),它告诉我们概率是如何在数据值上分布的。然而,在某些情况下,我们更关心的是获取某个值以下(或以上)的概率,我们可以通过累积分布函数CDF)来查看。

重要提示

使用 CDF 时,x变量的值沿着x轴分布,而获取最多某个x值的累积概率沿着y轴分布。这个累积概率介于 0 和 1 之间,并写作P(X ≤ x),其中小写(x)是用于比较的值,大写(X)是随机变量X。更多信息请参考www.itl.nist.gov/div898/handbook/eda/section3/eda362.htm

使用statsmodels包,我们可以估算 CDF 并得到ml震级类型:

>>> from statsmodels.distributions.empirical_distribution \
...     import ECDF
>>> ecdf = ECDF(quakes.query('magType == "ml"').mag)
>>> plt.plot(ecdf.x, ecdf.y)
# axis labels (we will cover this in chapter 6)
>>> plt.xlabel('mag') # add x-axis label 
>>> plt.ylabel('cumulative probability') # add y-axis label
# add title (we will cover this in chapter 6)
>>> plt.title('ECDF of earthquake magnitude with magType ml')

这会产生以下 ECDF:

图 5.26 – 可视化 ECDF

](tos-cn-i-73owjymdk6/ad813ea0ead04341b39a04055bc2a925)

图 5.26 – 可视化 ECDF

这在我们进行 EDA 时非常有用,可以帮助我们更好地理解数据。然而,我们必须小心如何解释这些结果以及如何向他人解释,特别是如果我们选择这么做的话。在这里,我们可以看到,如果该分布确实代表了总体,使用该测量技术测得的地震ml震级小于或等于3的概率为98%

图 5.27 – 解释 ECDF

](github.com/OpenDocCN/f…)

图 5.27 – 解释 ECDF

最后,我们可以使用箱线图来可视化潜在的离群值和通过四分位数描述的分布。举个例子,我们来可视化 Facebook 股票在整个数据集中的 OHLC 价格:

>>> fb.iloc[:,:4].plot(
...     kind='box', 
...     title='Facebook OHLC Prices Box Plot'
... )
>>> plt.ylabel('price ($)') # label x-axis (discussed in ch 6)

请注意,我们确实失去了一些在其他图中得到的信息。我们不再能够了解分布中点的密度;通过箱线图,我们更关注的是五数概括:

图 5.28 – 使用 pandas 创建箱线图

](tos-cn-i-73owjymdk6/7cae0420e6a14f05a12c06a1de667f7f)

图 5.28 – 使用 pandas 创建箱线图

小贴士

我们可以通过传递notch=True来创建带缺口的箱线图。缺口标记了中位数的 95%置信区间,这在比较组之间的差异时很有帮助。笔记本中有一个示例。

我们还可以在调用groupby()之后调用boxplot()方法。让我们来看一下在根据交易量计算时,箱线图如何变化:

>>> fb.assign(
...     volume_bin=\
...         pd.cut(fb.volume, 3, labels=['low', 'med', 'high']) 
... ).groupby('volume_bin').boxplot(
...     column=['open', 'high', 'low', 'close'], 
...     layout=(1, 3), figsize=(12, 3)
... )
>>> plt.suptitle(
...     'Facebook OHLC Box Plots by Volume Traded', y=1.1
... )

记得从第四章,“汇总 Pandas 数据框”中,我们知道大多数天数都落在低交易量范围内,因此我们可以预期在这里会看到更多的波动,因为股票数据随时间变化的情况:

图 5.29 – 使用 pandas 绘制每组箱线图

图 5.29 – 使用 pandas 绘制每组箱线图

我们还可以使用这种方法查看根据使用的magType,地震震级的分布,并与 USGS 网站上预期的范围进行比较(www.usgs.gov/natural-hazards/earthquake-hazards/science/magnitude-types):

>>> quakes[['mag', 'magType']]\
...     .groupby('magType')\
...     .boxplot(figsize=(15, 8), subplots=False)
# formatting (covered in chapter 6)
>>> plt.title('Earthquake Magnitude Box Plots by magType')
>>> plt.ylabel('magnitude')

美国地质调查局(USGS)网站提到了一些情况下无法使用某些测量技术,以及每种测量技术适用的幅度范围(当超出该范围时,使用其他技术)。在这里,我们可以看到这些技术一起覆盖了广泛的幅度范围,但没有一种技术能够覆盖所有的情况:

图 5.30 – 单一图表中每组的箱线图

图 5.30 – 单一图表中每组的箱线图

重要提示

虽然直方图、KDE、ECDF 和箱线图都是展示数据分布的方式,但我们看到每种可视化方法展示了数据的不同方面。在得出结论之前,从多个角度可视化数据是很重要的。

计数和频率

在处理分类数据时,我们可以创建条形图来显示数据的计数或特定值的频率。条形图可以是垂直的(kind='bar')或水平的(kind='barh')。当我们有许多类别或类别之间有某种顺序时(例如,随着时间的演变),垂直条形图非常有用。水平条形图便于比较每个类别的大小,并为长类别名称提供足够的空间(无需旋转它们)。

我们可以使用水平条形图来查看quakes数据框中哪些地方发生了最多的地震。首先,我们对parsed_place系列调用value_counts()方法,提取出发生地震次数最多的前 15 个地方。接下来,我们反转顺序,以便在条形图中将最小的值排在上面,这样我们将得到按地震次数排序的条形图。注意,我们也可以将反转排序作为value_counts()的参数,但由于我们仍然需要提取前 15 名,因此我们将两者结合在一个iloc调用中:

>>> quakes.parsed_place.value_counts().iloc[14::-1,].plot(
...     kind='barh', figsize=(10, 5), 
...     title='Top 15 Places for Earthquakes '
...           '(September 18, 2018 - October 13, 2018)'
... )
>>> plt.xlabel('earthquakes') # label x-axis (see ch 6)

请记住,切片表示法是[start:stop:step],在本例中,由于步长是负数,顺序被反转;我们从索引14(第 15 个条目)开始,每次都朝着索引0靠近。通过传递kind='barh',我们可以得到水平条形图,显示出该数据集中大多数地震发生在阿拉斯加。也许看到在如此短的时间内发生的地震数量令人惊讶,但许多地震的震级很小,以至于人们根本感觉不到:

图 5.31 – 使用 pandas 绘制水平条形图

图 5.31 – 使用 pandas 绘制水平条形图

我们的数据还包含了地震是否伴随海啸的信息。我们可以使用groupby()来绘制一个条形图,展示在我们数据中时间段内遭遇海啸的前 10 个地方:

>>> quakes.groupby(
...     'parsed_place'
... ).tsunami.sum().sort_values().iloc[-10:,].plot(
...     kind='barh', figsize=(10, 5), 
...     title='Top 10 Places for Tsunamis '
...           '(September 18, 2018 - October 13, 2018)'
... )
>>> plt.xlabel('tsunamis') # label x-axis (discussed in ch 6)

请注意,这次我们使用了iloc[-10:,],它从第 10 大值开始(因为sort_values()默认按升序排序),一直到最大值,从而得到前 10 个数据。这里我们可以看到,在这段时间内,印尼发生的海啸数量远远超过其他地区:

图 5.32 – 按组计算结果的绘制

图 5.32 – 按组计算结果的绘制

看到这样的数据后,我们可能会想进一步探究印尼每天发生的海啸数量。我们可以通过线图或使用kind='bar'的垂直条形图来可视化这种随时间变化的情况。这里我们将使用条形图,以避免插值点:

>>> indonesia_quakes = quakes.query(
...     'parsed_place == "Indonesia"'
... ).assign(
...     time=lambda x: pd.to_datetime(x.time, unit='ms'),
...     earthquake=1
... ).set_index('time').resample('1D').sum()
# format the datetimes in the index for the x-axis
>>> indonesia_quakes.index = \
...     indonesia_quakes.index.strftime('%b\n%d')
>>> indonesia_quakes.plot(
...     y=['earthquake', 'tsunami'], kind='bar', rot=0, 
...     figsize=(15, 3), label=['earthquakes', 'tsunamis'], 
...     title='Earthquakes and Tsunamis in Indonesia '
...           '(September 18, 2018 - October 13, 2018)'
... )
# label the axes (discussed in chapter 6)
>>> plt.xlabel('date')
>>> plt.ylabel('count')

2018 年 9 月 28 日,我们可以看到印尼的地震和海啸出现了激增;在这一天,发生了一次 7.5 级地震,引发了毁灭性的海啸:

图 5.33 – 随时间变化的计数比较

图 5.33 – 随时间变化的计数比较

我们还可以通过使用groupby()unstack()从单列的值中创建分组条形图。这使得我们能够为列中的每个独特值生成条形图。让我们用这种方法查看海啸与地震同时发生的频率,作为一个百分比。我们可以使用apply()方法,如我们在第四章《聚合 Pandas DataFrames》中所学,沿着axis=1(逐行应用)。为了说明,我们将查看海啸伴随地震发生比例最高的七个地方:

>>> quakes.groupby(['parsed_place', 'tsunami']).mag.count()\
...     .unstack().apply(lambda x: x / x.sum(), axis=1)\
...     .rename(columns={0: 'no', 1: 'yes'})\
...     .sort_values('yes', ascending=False)[7::-1]\
...     .plot.barh(
...         title='Frequency of a tsunami accompanying '
...               'an earthquake'
...     )
# move legend to the right of the plot; label axes
>>> plt.legend(title='tsunami?', bbox_to_anchor=(1, 0.65))
>>> plt.xlabel('percentage of earthquakes')
>>> plt.ylabel('')

圣诞岛在这段时间内发生了 1 次地震,但伴随了海啸。相比之下,巴布亚新几内亚约 40%的地震都伴随了海啸:

图 5.34 – 按组绘制的条形图

图 5.34 – 按组绘制的条形图

提示

在保存前面的图表时,较长的类别名称可能会被截断;如果是这种情况,尝试在保存之前运行plt.tight_layout()

现在,让我们使用垂直条形图来查看哪些地震震级测量方法最为常见,方法是使用kind='bar'

>>> quakes.magType.value_counts().plot(
...     kind='bar', rot=0,
...     title='Earthquakes Recorded per magType'
... )
# label the axes (discussed in ch 6)
>>> plt.xlabel('magType')
>>> plt.ylabel('earthquakes')

看起来ml是测量地震震级时最常用的方法。这是有道理的,因为它是由理查德·里希特和古滕贝格在 1935 年定义的原始震级关系,用于测量局部地震,这一点可以参考我们使用的数据集中关于magType字段的 USGS 页面(www.usgs.gov/natural-hazards/earthquake-hazards/science/magnitude-types):

图 5.35 – 比较类别计数

图 5.35 – 比较类别计数

假设我们想要查看某一震级的地震数量,并按magType区分它们。这样一个图表可以在一个图中展示多个信息:

  • 哪些震级在magType中最常出现。

  • 每种magType对应的震级的相对范围。

  • magType的最常见值。

为此,我们可以制作一个堆叠条形图。首先,我们将所有震级四舍五入到最接近的整数。这意味着所有地震都会标记为小数点前的震级部分(例如,5.5 被标记为 5,就像 5.7、5.2 和 5.0 一样)。接下来,我们需要创建一个透视表,将震级放入索引,将震级类型放入列中;我们将计算各个值对应的地震数量:

>>> pivot = quakes.assign(
...     mag_bin=lambda x: np.floor(x.mag)
... ).pivot_table(
...     index='mag_bin', 
...     columns='magType', 
...     values='mag', 
...     aggfunc='count'
... )

一旦我们有了透视表,就可以通过在绘制时传入stacked=True来创建堆叠条形图:

>>> pivot.plot.bar(
...     stacked=True,
...     rot=0, 
...     title='Earthquakes by integer magnitude and magType'
... )
>>> plt.ylabel('earthquakes') # label axes (discussed in ch 6)

这将产生以下图表,显示大多数地震使用ml震级类型,并且震级低于 4:

图 5.36 – 堆叠条形图

图 5.36 – 堆叠条形图

其他条形图相比于ml显得较小,这使得我们很难看清哪些震级类型将较高的震级赋给了地震。为了解决这个问题,我们可以制作一个标准化堆叠条形图。我们将不再显示每种震级和magType组合的地震数量,而是显示每种震级下,使用每种magType的地震百分比:

>>> normalized_pivot = \
...     pivot.fillna(0).apply(lambda x: x / x.sum(), axis=1)
... 
>>> ax = normalized_pivot.plot.bar(
...     stacked=True, rot=0, figsize=(10, 5),
...     title='Percentage of earthquakes by integer magnitude '
...           'for each magType'
... )
>>> ax.legend(bbox_to_anchor=(1, 0.8)) # move legend
>>> plt.ylabel('percentage') # label axes (discussed in ch 6)

现在,我们可以轻松看到mww产生较高的震级,而ml似乎分布在震级范围的较低端:

图 5.37 – 标准化堆叠条形图

图 5.37 – 标准化堆叠条形图

请注意,我们也可以使用groupby()方法和unstack()方法来实现这个策略。让我们重新查看伴随地震的海啸频率图,但这次我们不使用分组条形图,而是将其堆叠显示:

>>> quakes.groupby(['parsed_place', 'tsunami']).mag.count()\
...     .unstack().apply(lambda x: x / x.sum(), axis=1)\
...     .rename(columns={0: 'no', 1: 'yes'})\
...     .sort_values('yes', ascending=False)[7::-1]\
...     .plot.barh(
...         title='Frequency of a tsunami accompanying '
...               'an earthquake',
...         stacked=True
...     )
# move legend to the right of the plot
>>> plt.legend(title='tsunami?', bbox_to_anchor=(1, 0.65))
# label the axes (discussed in chapter 6)
>>> plt.xlabel('percentage of earthquakes')
>>> plt.ylabel('')

这个堆叠条形图使得我们很容易比较不同地方的海啸频率:

图 5.38 – 按组归类的标准化堆叠条形图

图 5.38 – 按组归类的标准化堆叠条形图

类别数据限制了我们可以使用的图表类型,但也有一些替代方案可以替代条形图。在下一章的利用 seaborn 进行高级绘图部分,我们将详细介绍它们;现在,让我们先来看看 pandas.plotting 模块。

pandas.plotting 模块

使用 pandas 绘图部分,我们讲解了 pandas 提供的标准图表类型。 然而,pandas 也有一个模块(名为 plotting),其中包含一些可以在数据上使用的特殊图表。请注意,由于它们的构成和返回方式,这些图表的自定义选项可能更为有限。

在这一部分,我们将在 3-pandas_plotting_module.ipynb 笔记本中进行操作。像往常一样,我们将从导入库和读取数据开始;这里只使用 Facebook 数据:

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> import pandas as pd
>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv', 
...     index_col='date', 
...     parse_dates=True
... )

现在,让我们来了解一下 pandas.plotting 模块中提供的一些图表,学习如何将这些可视化结果应用于我们的探索性数据分析(EDA)。

散点矩阵

在本章的前面,我们讨论了如何使用散点图来展示变量之间的关系。通常,我们希望查看数据中每一对变量的散点图,这可能会很繁琐。pandas.plotting 模块包含了 scatter_matrix() 函数,这使得这个过程变得更加容易。我们可以用它来查看 Facebook 股票价格数据中各列组合的散点图:

>>> from pandas.plotting import scatter_matrix
>>> scatter_matrix(fb, figsize=(10, 10))

这样会得到以下的绘图矩阵,这在机器学习中通常用于查看哪些变量在构建模型时可能有用。我们可以很容易地看到,开盘价、最高价、最低价和收盘价之间有强烈的正相关关系:

图 5.39 – Pandas 散点矩阵

图 5.39 – Pandas 散点矩阵

默认情况下,在对角线上,列与自己配对时,我们会得到它的直方图。或者,我们可以通过传入 diagonal='kde' 来请求 KDE:

>>> scatter_matrix(fb, figsize=(10, 10), diagonal='kde')

这样得到的散点矩阵在对角线处是 KDE,而不是直方图:

图 5.40 – 带有 KDE 的散点矩阵

图 5.40 – 带有 KDE 的散点矩阵

尽管散点矩阵可以方便地检查变量之间的关系,但有时我们更关心自相关性,即时间序列与其滞后版本之间的相关性。可视化自相关性的一种方法是使用滞后图。

滞后图

我们可以使用 data[:-1](去掉最后一项)和 data[1:](从第二项到最后一项)。

如果我们的数据是随机的,这个图表将没有任何模式。让我们用 NumPy 生成一些随机数据来测试这一点:

>>> from pandas.plotting import lag_plot
>>> np.random.seed(0) # make this repeatable
>>> lag_plot(pd.Series(np.random.random(size=200)))

随机数据点并未显示出任何模式,只有随机噪声:

图 5.41 – 随机噪声的滞后图

图 5.41 – 随机噪声的滞后图

对于我们的股票数据,我们知道某一天的价格是由前一天的情况决定的;因此,我们预计在滞后图中会看到一种模式。让我们使用 Facebook 股票的收盘价来测试我们的直觉是否正确:

>>> lag_plot(fb.close)

正如预期的那样,这导致了一个线性模式:

图 5.42 – Facebook 股票价格的滞后图

图 5.42 – Facebook 股票价格的滞后图

我们还可以指定用于滞后的周期数。默认滞后为 1,但我们可以通过 lag 参数更改它。例如,我们可以使用 lag=5 比较每个值与前一周的值(记住,股票数据仅包含工作日的数据,因为市场在周末关闭):

>>> lag_plot(fb.close, lag=5)

这仍然产生了强相关性,但与图 5.42相比,它看起来明显较弱:

图 5.43 – 自定义滞后图的周期数

图 5.43 – 自定义滞后图的周期数

虽然滞后图帮助我们可视化自相关,但它们并不能告诉我们数据包含多少个自相关周期。为此,我们可以使用自相关图。

自相关图

Pandas 提供了一种额外的方法来查找我们数据中的自相关,使用 autocorrelation_plot() 函数,它通过滞后的数量来显示自相关。随机数据的自相关值接近零。

正如我们讨论滞后图时所做的那样,让我们首先检查一下使用 NumPy 生成的随机数据是什么样子的:

>>> from pandas.plotting import autocorrelation_plot
>>> np.random.seed(0) # make this repeatable
>>> autocorrelation_plot(pd.Series(np.random.random(size=200)))

确实,自相关接近零,且该线位于置信带内(99% 是虚线;95% 是实线):

图 5.44 – 随机数据的自相关图

图 5.44 – 随机数据的自相关图

让我们探索一下 Facebook 股票收盘价的自相关图,因为滞后图显示了几个自相关周期:

>>> autocorrelation_plot(fb.close)

在这里,我们可以看到,在变为噪声之前,许多滞后周期存在自相关:

图 5.45 – Facebook 股票价格的自相关图

图 5.45 – Facebook 股票价格的自相关图

提示

回想一下 第一章数据分析导论,ARIMA 模型中的一个组成部分是自回归成分。自相关图可以帮助确定要使用的时间滞后数。我们将在 第七章金融分析 – 比特币与股市 中构建一个 ARIMA 模型。

自助法图

Pandas 还提供了一个绘图功能,用于评估常见汇总统计量的不确定性,通过 samplessize 参数分别计算汇总统计量。然后,它将返回结果的可视化图像。

让我们看看交易量数据的汇总统计的不确定性情况:

>>> from pandas.plotting import bootstrap_plot
>>> fig = bootstrap_plot(
...     fb.volume, fig=plt.figure(figsize=(10, 6))
... )

这将生成以下子图,我们可以用它来评估均值、中位数和中值范围(区间中点)的不确定性:

图 5.46 – Pandas bootstrap 图

图 5.46 – Pandas bootstrap 图

这是pandas.plotting模块中一些函数的示例。完整列表请查看pandas.pydata.org/pandas-docs/stable/reference/plotting.html

总结

完成本章后,我们已经能够使用pandasmatplotlib快速创建各种可视化图表。我们现在理解了matplotlib的基本原理以及图表的主要组成部分。此外,我们讨论了不同类型的图表以及在什么情况下使用它们——数据可视化的一个关键部分是选择合适的图表。请务必参考附录中的选择合适的可视化部分以备将来参考。

请注意,最佳的可视化实践不仅适用于图表类型,还适用于图表的格式设置,我们将在下一章讨论此内容。除此之外,我们将在此基础上进一步讨论使用seaborn的其他图表,以及如何使用matplotlib自定义我们的图表。请务必完成章节末的练习,以便在继续前进之前练习绘图,因为我们将在此章节的内容上进行扩展。

练习

使用到目前为止在本书中所学的内容创建以下可视化。请使用本章data/目录中的数据:

  1. 使用pandas绘制 Facebook 收盘价的 20 天滚动最低值。

  2. 创建 Facebook 股票开盘到收盘价变化的直方图和 KDE。

  3. 使用地震数据,为印度尼西亚使用的每个magType绘制箱线图。

  4. 绘制一条线图,表示 Facebook 每周的最高价和最低价之间的差异。这应为单条线。

  5. 绘制巴西、中国、印度、意大利、西班牙和美国每日新增 COVID-19 病例的 14 天移动平均值:

    a) 首先,使用diff()方法,该方法在《第四章》的处理时间序列数据部分介绍,用于计算每日新增病例的变化。然后,使用rolling()计算 14 天的移动平均值。

    b) 创建三个子图:一个显示中国;一个显示西班牙和意大利;另一个显示巴西、印度和美国。

  6. 使用matplotlibpandas,创建并排显示的两个子图,展示盘后交易对 Facebook 股票价格的影响:

    a) 第一个子图将包含每日开盘价与前一天收盘价之间的差值线图(请务必查看第四章中的处理时间序列数据部分,聚合 Pandas 数据框,以便轻松实现)。

    b) 第二个子图将是一个条形图,显示这一变动的月度净效应,使用resample()

    c) 奖励 #1:根据股价是上涨(绿色)还是下跌(红色)来为条形图着色。

    d) 奖励 #2:修改条形图的 x 轴,以显示月份的三字母缩写。

进一步阅读

请查看以下资源,获取本章讨论概念的更多信息:

第七章:第六章:使用 Seaborn 绘图及定制技巧

在上一章中,我们学习了如何使用matplotlibpandas在宽格式数据上创建多种不同的可视化。在本章中,我们将看到如何使用seaborn从长格式数据中制作可视化,并如何定制我们的图表以提高它们的可解释性。请记住,人类大脑擅长在视觉表示中发现模式;通过制作清晰且有意义的数据可视化,我们可以帮助他人(更不用说我们自己)理解数据所传达的信息。

Seaborn 能够绘制我们在上一章中创建的许多相同类型的图表;然而,它也可以快速处理长格式数据,使我们能够使用数据的子集将额外的信息编码到可视化中,如不同类别的面板和/或颜色。我们将回顾上一章中的一些实现,展示如何使用seaborn使其变得更加简便(或更加美观),例如热图和配对图(seaborn的散点矩阵图等价物)。此外,我们将探索seaborn提供的一些新图表类型,以解决其他图表类型可能面临的问题。

之后,我们将转换思路,开始讨论如何定制我们数据可视化的外观。我们将逐步讲解如何创建注释、添加参考线、正确标注图表、控制使用的调色板,并根据需求调整坐标轴。这是我们使可视化准备好呈现给他人的最后一步。

在本章中,我们将涵盖以下主题:

  • 利用 seaborn 进行更高级的绘图类型

  • 使用 matplotlib 格式化图表

  • 定制可视化

本章材料

本章的资料可以在 GitHub 上找到,网址是github.com/stefmolin/Hands-On-Data-Analysis-with-Pandas-2nd-edition/tree/master/ch_06。我们将再次使用三个数据集,所有数据集都可以在data/目录中找到。在fb_stock_prices_2018.csv文件中,我们有 Facebook 在 2018 年所有交易日的股价数据。这些数据包括 OHLC 数据(开盘价、最高价、最低价和收盘价),以及成交量。这些数据是通过stock_analysis包收集的,我们将在第七章中构建该包,金融分析 - 比特币与股市。股市在周末休市,因此我们只有交易日的数据。

earthquakes.csv 文件包含从 mag 列提取的地震数据,包括它的震级(magType 列)、发生时间(time 列)和地点(place 列);我们还包含了 parsed_place 列,表示地震发生的州或国家(我们在 第二章《使用 Pandas 数据框架》时添加了这个列)。其他不必要的列已被删除。

covid19_cases.csv 文件中,我们有一个来自欧洲疾病预防控制中心ECDC)提供的 全球各国每日新增 COVID-19 报告病例数 数据集的导出,数据集可以在 www.ecdc.europa.eu/en/publications-data/download-todays-data-geographic-distribution-covid-19-cases-worldwide 找到。为了实现此数据的脚本化或自动化收集,ECDC 提供了当天的 CSV 文件下载链接:opendata.ecdc.europa.eu/covid19/casedistribution/csv。我们将使用的快照是 2020 年 9 月 19 日收集的,包含了 2019 年 12 月 31 日至 2020 年 9 月 18 日的每个国家新增 COVID-19 病例数,并包含部分 2020 年 9 月 19 日的数据。在本章中,我们将查看 2020 年 1 月 18 日至 2020 年 9 月 18 日这 8 个月的数据。

在本章中,我们将使用三个 Jupyter 笔记本。它们按使用顺序进行编号。我们将首先在 1-introduction_to_seaborn.ipynb 笔记本中探索 seaborn 的功能。接下来,我们将在 2-formatting_plots.ipynb 笔记本中讨论如何格式化和标记我们的图表。最后,在 3-customizing_visualizations.ipynb 笔记本中,我们将学习如何添加参考线、阴影区域、包括注释,并自定义我们的可视化效果。文本会提示我们何时切换笔记本。

提示

附加的 covid19_cases_map.ipynb 笔记本通过一个示例演示了如何使用全球 COVID-19 病例数据在地图上绘制数据。它可以帮助你入门 Python 中的地图绘制,并在一定程度上构建了我们将在本章讨论的格式化内容。

此外,我们有两个 Python(.py)文件,包含我们将在本章中使用的函数:viz.pycolor_utils.py。让我们首先通过探索 seaborn 开始。

使用 seaborn 进行高级绘图

如我们在上一章中看到的,pandas 提供了大多数我们想要创建的可视化实现;然而,还有一个库 seaborn,它提供了更多的功能,可以创建更复杂的可视化,并且比 pandas 更容易处理长格式数据的可视化。这些可视化通常比 matplotlib 生成的标准可视化效果要好看得多。

本节内容我们将使用 1-introduction_to_seaborn.ipynb notebook。首先,我们需要导入 seaborn,通常将其别名为 sns

>>> import seaborn as sns

我们还需要导入 numpymatplotlib.pyplotpandas,然后读取 Facebook 股票价格和地震数据的 CSV 文件:

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> import pandas as pd 
>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv',
...     index_col='date', 
...     parse_dates=True
... )
>>> quakes = pd.read_csv('data/earthquakes.csv')

虽然 seaborn 提供了许多我们在上一章中讨论的图表类型的替代方案,但大多数情况下,我们将仅介绍 seaborn 使得可能的新的图表类型,其余的学习可以作为练习。更多使用 seaborn API 的可用函数可以参考 seaborn.pydata.org/api.html

类别数据

2018 年 9 月 28 日,印尼发生了一次毁灭性的海啸;它是在印尼帕卢附近发生了 7.5 级地震后发生的 (www.livescience.com/63721-tsunami-earthquake-indonesia.html)。让我们创建一个可视化图表,了解印尼使用了哪些震级类型,记录的震级范围,以及有多少地震伴随了海啸。为此,我们需要一种方法来绘制一个变量是类别型(magType),另一个是数值型(mag)的关系图。

重要提示

有关不同震级类型的信息,请访问 www.usgs.gov/natural-hazards/earthquake-hazards/science/magnitude-types

当我们在第五章“使用 Pandas 和 Matplotlib 可视化数据”中讨论散点图时,我们限制了两个变量都必须是数值型;然而,使用 seaborn,我们可以使用另外两种图表类型,使得一个变量是类别型,另一个是数值型。第一个是 stripplot() 函数,它将数据点绘制成代表各类别的条带。第二个是 swarmplot() 函数,我们稍后会看到。

让我们使用 stripplot() 创建这个可视化。我们将发生在印尼的地震子集传递给 data 参数,并指定将 magType 放置在 x 轴(x),将震级放置在 y 轴(y),并根据地震是否伴随海啸(hue)为数据点上色:

>>> sns.stripplot(
...     x='magType', 
...     y='mag', 
...     hue='tsunami',
...     data=quakes.query('parsed_place == "Indonesia"')
... )

通过查看生成的图表,我们可以看到该地震是 mww 列中最高的橙色点(如果没有使用提供的 Jupyter Notebook,别忘了调用 plt.show()):

图 6.1 – Seaborn 的条形图

图 6.1 – Seaborn 的条形图

大部分情况下,海啸发生在较高震级的地震中,正如我们所预期的那样;然而,由于在较低震级区域有大量点的集中,我们无法清晰地看到所有的点。我们可以尝试调整 jitter 参数,它控制要添加多少随机噪声来减少重叠,或者调整 alpha 参数以控制透明度,正如我们在上一章所做的那样;幸运的是,还有一个函数 swarmplot(),它会尽可能减少重叠,因此我们将使用这个函数:

>>> sns.swarmplot(
...     x='magType', 
...     y='mag', 
...     hue='tsunami',
...     data=quakes.query('parsed_place == "Indonesia"'),
...     size=3.5 # point size
... )

mb 列:

图 6.2 – Seaborn 的蜂群图

图 6.2 – Seaborn 的蜂群图

在上一章的 使用 pandas 绘图 部分中,我们讨论了如何可视化分布,并介绍了箱形图。Seaborn 为大数据集提供了增强型箱形图,它展示了更多的分位数,以便提供关于分布形状的更多信息,特别是尾部部分。让我们使用增强型箱形图来比较不同震级类型的地震震中,就像我们在 第五章 中所做的那样,使用 Pandas 和 Matplotlib 可视化数据

>>> sns.boxenplot(
...     x='magType', y='mag', data=quakes[['magType', 'mag']]
... )
>>> plt.title('Comparing earthquake magnitude by magType')

这将产生以下图表:

图 6.3 – Seaborn 的增强型箱形图

图 6.3 – Seaborn 的增强型箱形图

提示

增强型箱形图首次出现在 Heike Hofmann、Karen Kafadar 和 Hadley Wickham 合著的论文 Letter-value plots: Boxplots for large data 中,您可以在 vita.had.co.nz/papers/letter-value-plot.html 找到该文。

箱形图非常适合可视化数据的分位数,但我们失去了关于分布的信息。正如我们所见,增强型箱形图是解决这个问题的一种方法——另一种策略是使用小提琴图,它结合了核密度估计(即基础分布的估计)和箱形图:

>>> fig, axes = plt.subplots(figsize=(10, 5))
>>> sns.violinplot(
...     x='magType', y='mag', data=quakes[['magType', 'mag']], 
...     ax=axes, scale='width' # all violins have same width
... )
>>> plt.title('Comparing earthquake magnitude by magType')

箱形图部分穿过每个小提琴图的中心;然后,在以箱形图作为 x 轴的基础上,分别在两侧绘制 核密度估计KDE)。由于它是对称的,我们可以从箱形图的任一侧读取 KDE:

图 6.4 – Seaborn 的小提琴图

图 6.4 – Seaborn 的小提琴图

seaborn 文档还根据绘图数据类型列出了不同的绘图函数;完整的分类图表列表可以在seaborn.pydata.org/api.html#categorical-plots找到。一定要查看 countplot()barplot() 函数,它们是我们在上一章使用 pandas 创建条形图的变体。

相关性和热图

如约定的那样,今天我们将学习一个比在 第五章 中使用 Pandas 和 Matplotlib 可视化数据时更简单的热图生成方法。这一次,我们将使用 seaborn,它提供了 heatmap() 函数,帮助我们更轻松地生成这种可视化图表:

>>> sns.heatmap(
...     fb.sort_index().assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low
...     ).corr(), 
...     annot=True, 
...     center=0, 
...     vmin=-1, 
...     vmax=1
... )

提示

在使用 seaborn 时,我们仍然可以使用 matplotlib 中的函数,如 plt.savefig()plt.tight_layout()。请注意,如果 plt.tight_layout() 存在问题,可以改为将 bbox_inches='tight' 传递给 plt.savefig()

我们传入 center=0,这样 seaborn 会将 0(无相关性)放置在它使用的色图的中心。为了将色标的范围设置为相关系数的范围,我们还需要提供 vmin=-1vmax=1。注意,我们还传入了 annot=True,这样每个框内会显示相关系数——我们可以通过一次函数调用,既获得数值数据又获得可视化数据:

图 6.5 – Seaborn 的热图

图 6.5 – Seaborn 的热图

Seaborn 还为我们提供了 pandas.plotting 模块中提供的 scatter_matrix() 函数的替代方案,叫做 pairplot()。我们可以使用这个函数将 Facebook 数据中各列之间的相关性以散点图的形式展示,而不是热图:

>>> sns.pairplot(fb)

这个结果使我们能够轻松理解 OHLC 各列之间在热图中几乎完美的正相关关系,同时还展示了沿对角线的每一列的直方图:

图 6.6 – Seaborn 的配对图

图 6.6 – Seaborn 的配对图

Facebook 在 2018 年下半年表现显著不如上半年,因此我们可能想了解数据在每个季度的分布变化情况。与 pandas.plotting.scatter_matrix() 函数类似,我们可以使用 diag_kind 参数来指定对角线的处理方式;然而,与 pandas 不同的是,我们可以轻松地通过 hue 参数基于其他数据为图形着色。为此,我们只需要添加 quarter 列,并将其提供给 hue 参数:

>>> sns.pairplot(
...     fb.assign(quarter=lambda x: x.index.quarter), 
...     diag_kind='kde', hue='quarter'
... )

我们现在可以看到,OHLC 各列的分布在第一季度的标准差较小(因此方差也较小),而股价在第四季度大幅下跌(分布向左偏移):

图 6.7 – 利用数据来确定绘图颜色

图 6.7 – 利用数据来确定绘图颜色

提示

我们还可以将 kind='reg' 传递给 pairplot() 来显示回归线。

如果我们只想比较两个变量,可以使用jointplot(),它会给我们一个散点图,并在两侧展示每个变量的分布。让我们再次查看交易量的对数与 Facebook 股票的日内最高价和最低价差异之间的关联,就像我们在第五章中所做的那样,使用 Pandas 和 Matplotlib 可视化数据

>>> sns.jointplot(
...     x='log_volume', 
...     y='max_abs_change', 
...     data=fb.assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low
...     )
... )

使用kind参数的默认值,我们会得到分布的直方图,并在中心显示一个普通的散点图:

图 6.8 – Seaborn 的联合图

图 6.8 – Seaborn 的联合图

Seaborn 为kind参数提供了许多替代选项。例如,我们可以使用 hexbins,因为当我们使用散点图时,会有显著的重叠:

>>> sns.jointplot(
...     x='log_volume', 
...     y='max_abs_change', 
...     kind='hex',
...     data=fb.assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low
...     )
... )

我们现在可以看到左下角有大量的点集中:

图 6.9 – 使用 hexbins 的联合图

图 6.9 – 使用 hexbins 的联合图

另一种查看值集中度的方法是使用kind='kde',这会给我们一个等高线图,以表示联合密度估计,并同时展示每个变量的 KDEs:

>>> sns.jointplot(
...     x='log_volume', 
...     y='max_abs_change', 
...     kind='kde',
...     data=fb.assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low
...     )
... )

等高线图中的每条曲线包含给定密度的点:

图 6.10 – 联合分布图

图 6.10 – 联合分布图

此外,我们还可以在中心绘制回归图,并在两侧获得 KDEs 和直方图:

>>> sns.jointplot(
...     x='log_volume', 
...     y='max_abs_change', 
...     kind='reg',
...     data=fb.assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low
...     )
... )

这导致回归线通过散点图绘制,并且在回归线周围绘制了一个较浅颜色的置信带:

图 6.11 – 带有线性回归和 KDEs 的联合图

图 6.11 – 带有线性回归和 KDEs 的联合图

关系看起来是线性的,但我们应该查看kind='resid'

>>> sns.jointplot(
...     x='log_volume', 
...     y='max_abs_change', 
...     kind='resid',
...     data=fb.assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low
...     )
... )
# update y-axis label (discussed next section)
>>> plt.ylabel('residuals')

注意,随着交易量的增加,残差似乎越来越远离零,这可能意味着这不是建模这种关系的正确方式:

图 6.12 – 显示线性回归残差的联合图

图 6.12 – 显示线性回归残差的联合图

我们刚刚看到,我们可以使用jointplot()来生成回归图或残差图;自然,seaborn提供了直接生成这些图形的函数,无需创建整个联合图。接下来我们来讨论这些。

回归图

regplot()函数会计算回归线并绘制它,而residplot()函数会计算回归并仅绘制残差。我们可以编写一个函数将这两者结合起来,但首先需要一些准备工作。

我们的函数将绘制任意两列的所有排列(与组合不同,排列的顺序很重要,例如,(open, close)不等同于(close, open))。这使我们能够将每一列作为回归变量和因变量来看待;由于我们不知道关系的方向,因此在调用函数后让查看者自行决定。这会生成许多子图,因此我们将创建一个只包含来自 Facebook 数据的少数几列的新数据框。

我们将查看交易量的对数(log_volume)和 Facebook 股票的日最高价与最低价之间的差异(max_abs_change)。我们使用assign()来创建这些新列,并将它们保存在一个名为fb_reg_data的新数据框中:

>>> fb_reg_data = fb.assign(
...     log_volume=np.log(fb.volume), 
...     max_abs_change=fb.high - fb.low
... ).iloc[:,-2:]

接下来,我们需要导入itertools,它是 Python 标准库的一部分(docs.python.org/3/library/itertools.html)。在编写绘图函数时,itertools非常有用;它可以非常轻松地创建高效的迭代器,用于排列、组合和无限循环或重复等操作:

>>> import itertools

可迭代对象是可以被迭代的对象。当我们启动一个循环时,会从可迭代对象中创建一个迭代器。每次迭代时,迭代器提供它的下一个值,直到耗尽;这意味着,一旦我们完成了一次对所有项的迭代,就没有剩余的元素,它不能再次使用。迭代器是可迭代对象,但并非所有可迭代对象都是迭代器。不是迭代器的可迭代对象可以被重复使用。

使用itertools时返回的迭代器只能使用一次:

>>> iterator = itertools.repeat("I'm an iterator", 1)
>>> for i in iterator:
...     print(f'-->{i}')
-->I'm an iterator
>>> print(
...     'This printed once because the iterator '
...     'has been exhausted'
... )
This printed once because the iterator has been exhausted
>>> for i in iterator:
...     print(f'-->{i}')

另一方面,列表是一个可迭代对象;我们可以编写一个循环遍历列表中的所有元素,之后仍然可以得到一个列表用于后续使用:

>>> iterable = list(itertools.repeat("I'm an iterable", 1))
>>> for i in iterable:
...     print(f'-->{i}')
-->I'm an iterable
>>> print('This prints again because it\'s an iterable:')
This prints again because it's an iterable:
>>> for i in iterable:
...     print(f'-->{i}')
-->I'm an iterable

现在我们对itertools和迭代器有了一些了解,接下来我们来编写回归和残差排列图的函数:

def reg_resid_plots(data):
    """
    Using `seaborn`, plot the regression and residuals plots 
    side-by-side for every permutation of 2 columns in data.
    Parameters:
        - data: A `pandas.DataFrame` object
    Returns:
        A matplotlib `Axes` object.
    """
    num_cols = data.shape[1]
    permutation_count = num_cols * (num_cols - 1)
    fig, ax = \
        plt.subplots(permutation_count, 2, figsize=(15, 8))
    for (x, y), axes, color in zip(
        itertools.permutations(data.columns, 2), 
        ax,
        itertools.cycle(['royalblue', 'darkorange'])
    ):
        for subplot, func in zip(
            axes, (sns.regplot, sns.residplot)
        ):
            func(x=x, y=y, data=data, ax=subplot, color=color)
            if func == sns.residplot:
                subplot.set_ylabel('residuals')
    return fig.axes

在这个函数中,我们可以看到到目前为止本章以及上一章中涉及的所有内容都已融合在一起;我们计算需要多少个子图,并且由于每种排列会有两个图表,我们只需要排列的数量来确定行数。我们利用了zip()函数,它可以一次性从多个可迭代对象中获取值并以元组形式返回,再通过元组解包轻松地遍历排列元组和二维的Axes对象数组。花些时间确保你理解这里发生了什么;本章末尾的进一步阅读部分也有关于zip()和元组解包的资源。

重要提示

如果我们提供不同长度的可迭代对象给zip(),我们将只得到与最短长度相等数量的元组。因此,我们可以使用无限迭代器,如使用itertools.repeat()时获得的,它会无限次重复相同的值(当我们没有指定重复次数时),以及itertools.cycle(),它会在所有提供的值之间无限循环。

调用我们的函数非常简单,只需要一个参数:

>>> from viz import reg_resid_plots
>>> reg_resid_plots(fb_reg_data)

第一行的子集是我们之前在联合图中看到的,而第二行则是翻转xy变量时的回归:

图 6.13 – Seaborn 线性回归和残差图

图 6.13 – Seaborn 线性回归和残差图

提示

regplot()函数通过orderlogistic参数分别支持多项式回归和逻辑回归。

Seaborn 还使得在数据的不同子集上绘制回归变得简单,我们可以使用lmplot()来分割回归图。我们可以使用huecolrow来分割回归图,分别通过给定列的值进行着色、为每个值创建一个新列以及为每个值创建一个新行。

我们看到 Facebook 的表现因每个季度而异,因此让我们使用 Facebook 股票数据计算每个季度的回归,使用交易量和每日最高与最低价格之间的差异,看看这种关系是否也发生变化:

>>> sns.lmplot(
...     x='log_volume', 
...     y='max_abs_change', 
...     col='quarter',
...     data=fb.assign(
...         log_volume=np.log(fb.volume), 
...         max_abs_change=fb.high - fb.low,
...         quarter=lambda x: x.index.quarter
...     )
... )

请注意,第四季度的回归线比前几个季度的斜率要陡得多:

图 6.14 – Seaborn 带有子集的线性回归图

图 6.14 – Seaborn 带有子集的线性回归图

请注意,运行lmplot()的结果是一个FacetGrid对象,这是seaborn的一个强大功能。接下来,我们将讨论如何在其中直接使用任何图形进行绘制。

分面

分面允许我们在子图上绘制数据的子集(分面)。我们已经通过一些seaborn函数看到了一些分面;然而,我们也可以轻松地为自己制作分面,以便与任何绘图函数一起使用。让我们创建一个可视化,比较印尼和巴布亚新几内亚的地震震级分布,看看是否发生了海啸。

首先,我们使用将要使用的数据创建一个FacetGrid对象,并通过rowcol参数定义如何对子集进行划分:

>>> g = sns.FacetGrid(
...     quakes.query(
...         'parsed_place.isin('
...         '["Indonesia", "Papua New Guinea"]) '
...         'and magType == "mb"'
...     ),   
...     row='tsunami',
...     col='parsed_place',
...     height=4
... )

然后,我们使用FacetGrid.map()方法对每个子集运行绘图函数,并传递必要的参数。我们将使用sns.histplot()函数为位置和海啸数据子集制作带有 KDE 的直方图:

>>> g = g.map(sns.histplot, 'mag', kde=True)

对于这两个位置,我们可以看到,当地震震级达到 5.0 或更大时,发生了海啸:

图 6.15 – 使用分面网格绘图

图 6.15 – 使用分面网格绘图

这结束了我们关于seaborn绘图功能的讨论;不过,我鼓励你查看 API(seaborn.pydata.org/api.html)以了解更多功能。此外,在绘制数据时,务必查阅附录中的选择合适的可视化方式部分作为参考。

使用 matplotlib 格式化图表

使我们的可视化图表具有表现力的一个重要部分是选择正确的图表类型,并且为其添加清晰的标签,以便易于解读。通过精心调整最终的可视化外观,我们使其更容易阅读和理解。

现在,让我们转到2-formatting_plots.ipynb笔记本,运行设置代码导入所需的包,并读取 Facebook 股票数据和 COVID-19 每日新增病例数据:

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> import pandas as pd 
>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv', 
...     index_col='date', 
...     parse_dates=True
... ) 
>>> covid = pd.read_csv('data/covid19_cases.csv').assign(
...     date=lambda x: \
...         pd.to_datetime(x.dateRep, format='%d/%m/%Y')
... ).set_index('date').replace(
...     'United_States_of_America', 'USA'
... ).sort_index()['2020-01-18':'2020-09-18']

在接下来的几个章节中,我们将讨论如何为图表添加标题、坐标轴标签和图例,以及如何自定义坐标轴。请注意,本节中的所有内容需要在运行plt.show()之前调用,或者如果使用%matplotlib inline魔法命令,则需要在同一个 Jupyter Notebook 单元格中调用。

标题和标签

迄今为止,我们创建的某些可视化图表没有标题或坐标轴标签。我们知道图中的内容,但如果我们要向他人展示这些图表,可能会引起一些混淆。为标签和标题提供明确的说明是一种良好的做法。

我们看到,当使用pandas绘图时,可以通过将title参数传递给plot()方法来添加标题,但我们也可以通过matplotlibplt.title()来实现这一点。请注意,我们可以将x/y值传递给plt.title()以控制文本的位置。我们还可以更改字体及其大小。为坐标轴添加标签也同样简单;我们可以使用plt.xlabel()plt.ylabel()。让我们绘制 Facebook 的收盘价,并使用matplotlib添加标签:

>>> fb.close.plot()
>>> plt.title('FB Closing Price')
>>> plt.xlabel('date')
>>> plt.ylabel('price ($)')

这将导致以下图表:

图 6.16 – 使用 matplotlib 为图表添加标签

图 6.16 – 使用 matplotlib 为图表添加标签

在处理子图时,我们需要采取不同的方法。为了直观地了解这一点,让我们绘制 Facebook 股票的 OHLC 数据的子图,并使用plt.title()为整个图表添加标题,同时使用plt.ylabel()为每个子图的y-轴添加标签:

>>> fb.iloc[:,:4]\
...     .plot(subplots=True, layout=(2, 2), figsize=(12, 5))
>>> plt.title('Facebook 2018 Stock Data')
>>> plt.ylabel('price ($)')

使用plt.title()将标题放置在最后一个子图上,而不是像我们预期的那样为整个图表添加标题。y-轴标签也会出现同样的问题:

图 6.17 – 为子图添加标签可能会引起混淆

图 6.17 – 为子图添加标签可能会引起混淆

在子图的情况下,我们希望给整个图表添加标题;因此,我们使用 plt.suptitle()。相反,我们希望给每个子图添加 y-轴标签,因此我们在 plot() 返回的每个 Axes 对象上使用 set_ylabel() 方法。请注意,Axes 对象会以与子图布局相同维度的 NumPy 数组返回,因此为了更方便地迭代,我们调用 flatten()

>>> axes = fb.iloc[:,:4]\
...     .plot(subplots=True, layout=(2, 2), figsize=(12, 5))
>>> plt.suptitle('Facebook 2018 Stock Data')
>>> for ax in axes.flatten():
...     ax.set_ylabel('price ($)')

这样会为整个图表添加一个标题,并为每个子图添加y-轴标签:

图 6.18 – 标注子图

图 6.18 – 标注子图

请注意,Figure 类也有一个 suptitle() 方法,而 Axes 类的 set() 方法允许我们标注坐标轴、设置图表标题等,所有这些都可以通过一次调用来完成,例如,set(xlabel='…', ylabel='…', title='…', …)。根据我们想做的事情,我们可能需要直接调用 FigureAxes 对象的方法,因此了解这些方法很重要。

图例

Matplotlib 使得可以通过 plt.legend() 函数和 Axes.legend() 方法控制图例的许多方面。例如,我们可以指定图例的位置,并格式化图例的外观,包括自定义字体、颜色等。plt.legend() 函数和 Axes.legend() 方法也可以用于在图表最初没有图例的情况下显示图例。以下是一些常用参数的示例:

图 6.19 – 图例格式化的有用参数

图 6.19 – 图例格式化的有用参数

图例将使用每个绘制对象的标签。如果我们不希望某个对象显示图例,可以将它的标签设置为空字符串。但是,如果我们只是想修改某个对象的显示名称,可以通过 label 参数传递它的显示名称。我们来绘制 Facebook 股票的收盘价和 20 天移动平均线,使用 label 参数为图例提供描述性名称:

>>> fb.assign(
...     ma=lambda x: x.close.rolling(20).mean()
... ).plot(
...     y=['close', 'ma'], 
...     title='FB closing price in 2018',
...     label=['closing price', '20D moving average'],
...     style=['-', '--']
... )
>>> plt.legend(loc='lower left')
>>> plt.ylabel('price ($)')

默认情况下,matplotlib 会尝试为图表找到最佳位置,但有时它会遮挡图表的部分内容,就像在这个例子中一样。因此,我们选择将图例放在图表的左下角。请注意,图例中的文本是我们在 plot()label 参数中提供的内容:

图 6.20 – 移动图例

图 6.20 – 移动图例

请注意,我们传递了一个字符串给 loc 参数来指定图例的位置;我们也可以传递代码作为整数或元组,表示图例框左下角的 (x, y) 坐标。下表包含了可能的位置信息字符串:

图 6.21 – 常见图例位置

图 6.21 – 常见图例位置

现在我们来看看如何使用 framealphancoltitle 参数来设置图例的样式。我们将绘制 2020 年 1 月 18 日至 2020 年 9 月 18 日期间,巴西、中国、意大利、西班牙和美国的世界每日新增 COVID-19 病例占比。此外,我们还会移除图表的顶部和右侧框线,使其看起来更简洁:

>>> new_cases = covid.reset_index().pivot(
...     index='date',
...     columns='countriesAndTerritories',
...     values='cases'
... ).fillna(0)
>>> pct_new_cases = new_cases.apply(
...     lambda x: x / new_cases.apply('sum', axis=1), axis=0
... )[
...     ['Italy', 'China', 'Spain', 'USA', 'India', 'Brazil']
... ].sort_index(axis=1).fillna(0)
>>> ax = pct_new_cases.plot(
...     figsize=(12, 7),
...     style=['-'] * 3 + ['--', ':', '-.'],
...     title='Percentage of the World\'s New COVID-19 Cases'
...           '\n(source: ECDC)'
... )
>>> ax.legend(title='Country', framealpha=0.5, ncol=2)
>>> ax.set_xlabel('')
>>> ax.set_ylabel('percentage of the world\'s COVID-19 cases')
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

我们的图例已整齐地排列为两列,并且包含了一个标题。我们还增加了图例边框的透明度:

图 6.22 – 格式化图例

图 6.22 – 格式化图例

提示

不要被试图记住所有可用选项而感到不知所措。如果我们不试图学习每一种可能的自定义,而是根据需要查找与我们视觉化目标相匹配的功能,反而会更容易。

格式化轴

第一章《数据分析简介》中,我们讨论了如果我们不小心,轴的限制可能会导致误导性的图表。我们可以通过将轴的限制作为元组传递给 xlim/ylim 参数来使用 pandasplot() 方法。或者,使用 matplotlib 时,我们可以通过 plt.xlim()/plt.ylim() 函数或 Axes 对象上的 set_xlim()/set_ylim() 方法调整每个轴的限制。我们分别传递最小值和最大值;如果我们想保持自动生成的限制,可以传入 None。让我们修改之前的图表,将世界各国每日新增 COVID-19 病例的百分比的 y 轴从零开始:

>>> ax = pct_new_cases.plot(
...     figsize=(12, 7),
...     style=['-'] * 3 + ['--', ':', '-.'],
...     title='Percentage of the World\'s New COVID-19 Cases'
...           '\n(source: ECDC)'
... )
>>> ax.legend(framealpha=0.5, ncol=2)
>>> ax.set_xlabel('')
>>> ax.set_ylabel('percentage of the world\'s COVID-19 cases')
>>> ax.set_ylim(0, None)
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

请注意,y 轴现在从零开始:

图 6.23 – 使用 matplotlib 更新轴限制

图 6.23 – 使用 matplotlib 更新轴限制

如果我们想改变轴的刻度,可以使用 plt.xscale()/plt.yscale() 并传入我们想要的刻度类型。例如,plt.yscale('log') 将会为 y 轴使用对数刻度;我们在前一章中已经学过如何使用 pandas 实现这一点。

我们还可以通过将刻度位置和标签传递给 plt.xticks()plt.yticks() 来控制显示哪些刻度线以及它们的标签。请注意,我们也可以调用这些函数来获取刻度位置和标签。例如,由于我们的数据从每个月的 18 日开始和结束,让我们将前一个图表中的刻度线移到每个月的 18 日,然后相应地标记刻度:

>>> ax = pct_new_cases.plot(
...     figsize=(12, 7),
...     style=['-'] * 3 + ['--', ':', '-.'],
...     title='Percentage of the World\'s New COVID-19 Cases'
...           '\n(source: ECDC)'
... )
>>> tick_locs = covid.index[covid.index.day == 18].unique()
>>> tick_labels = \
...     [loc.strftime('%b %d\n%Y') for loc in tick_locs]
>>> plt.xticks(tick_locs, tick_labels)
>>> ax.legend(framealpha=0.5, ncol=2)
>>> ax.set_xlabel('')
>>> ax.set_ylabel('percentage of the world\'s COVID-19 cases')
>>> ax.set_ylim(0, None)
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

移动刻度线后,图表的第一个数据点(2020 年 1 月 18 日)和最后一个数据点(2020 年 9 月 18 日)都有刻度标签:

图 6.24 – 编辑刻度标签

图 6.24 – 编辑刻度标签

我们当前将百分比表示为小数,但可能希望将标签格式化为使用百分号。请注意,不需要使用plt.yticks()函数来做到这一点;相反,我们可以使用matplotlib.ticker模块中的PercentFormatter类:

>>> from matplotlib.ticker import PercentFormatter
>>> ax = pct_new_cases.plot(
...     figsize=(12, 7),
...     style=['-'] * 3 + ['--', ':', '-.'],
...     title='Percentage of the World\'s New COVID-19 Cases'
...           '\n(source: ECDC)'
... )
>>> tick_locs = covid.index[covid.index.day == 18].unique()
>>> tick_labels = \
...     [loc.strftime('%b %d\n%Y') for loc in tick_locs]
>>> plt.xticks(tick_locs, tick_labels)
>>> ax.legend(framealpha=0.5, ncol=2)
>>> ax.set_xlabel('')
>>> ax.set_ylabel('percentage of the world\'s COVID-19 cases')
>>> ax.set_ylim(0, None)
>>> ax.yaxis.set_major_formatter(PercentFormatter(xmax=1))
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

通过指定xmax=1,我们表示我们的值应该先除以 1(因为它们已经是百分比),然后乘以 100 并附加百分号。这将导致y轴上显示百分比:

图 6.25 – 将刻度标签格式化为百分比

图 6.25 – 将刻度标签格式化为百分比

另一个有用的格式化器是EngFormatter类,它会自动将数字格式化为千位、百万位等,采用工程计数法。让我们用它来绘制每个大洲的累计 COVID-19 病例(单位:百万):

>>> from matplotlib.ticker import EngFormatter
>>> ax = covid.query('continentExp != "Other"').groupby([
...     'continentExp', pd.Grouper(freq='1D')
... ]).cases.sum().unstack(0).apply('cumsum').plot(
...     style=['-', '-', '--', ':', '-.'],
...     title='Cumulative COVID-19 Cases per Continent'
...           '\n(source: ECDC)'
... )
>>> ax.legend(title='', loc='center left')
>>> ax.set(xlabel='', ylabel='total COVID-19 cases')
>>> ax.yaxis.set_major_formatter(EngFormatter())
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

请注意,我们不需要将累计病例数除以 100 万来得到这些数字——我们传递给set_major_formatter()EngFormatter对象自动计算出应该使用百万(M)单位来表示数据:

图 6.26 – 使用工程计数法格式化刻度标签

图 6.26 – 使用工程计数法格式化刻度标签

PercentFormatterEngFormatter类都可以格式化刻度标签,但有时我们希望更改刻度的位置,而不是格式化它们。实现这一点的一种方法是使用MultipleLocator类,它可以轻松地将刻度设置为我们选择的倍数。为了演示我们如何使用它,来看一下 2020 年 4 月 18 日至 2020 年 9 月 18 日新西兰的每日新增 COVID-19 病例:

>>> ax = new_cases.New_Zealand['2020-04-18':'2020-09-18'].plot(
...     title='Daily new COVID-19 cases in New Zealand'
...           '\n(source: ECDC)'
... )
>>> ax.set(xlabel='', ylabel='new COVID-19 cases')
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

如果不干预刻度位置,matplotlib将以 2.5 为间隔显示刻度。我们知道没有半个病例,因此最好以整数刻度显示该数据:

图 6.27 – 默认刻度位置

图 6.27 – 默认刻度位置

我们通过使用MultipleLocator类来修正这个问题。在这里,我们并没有格式化轴标签,而是控制显示哪些标签;因此,我们必须调用set_major_locator()方法,而不是set_major_formatter()

>>> from matplotlib.ticker import MultipleLocator
>>> ax = new_cases.New_Zealand['2020-04-18':'2020-09-18'].plot(
...     title='Daily new COVID-19 cases in New Zealand'
...           '\n(source: ECDC)'
... )
>>> ax.set(xlabel='', ylabel='new COVID-19 cases') 
>>> ax.yaxis.set_major_locator(MultipleLocator(base=3))
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

由于我们传入了base=3,因此我们的y轴现在包含每隔三的整数:

图 6.28 – 使用整数刻度位置

图 6.28 – 使用整数刻度位置

这些只是matplotlib.ticker模块提供的三个功能,因此我强烈建议你查看文档以获取更多信息。在本章末尾的进一步阅读部分中也有相关链接。

自定义可视化

到目前为止,我们学到的所有创建数据可视化的代码都是为了制作可视化本身。现在我们已经打下了坚实的基础,准备学习如何添加参考线、控制颜色和纹理,以及添加注释。

3-customizing_visualizations.ipynb笔记本中,让我们处理导入库并读取 Facebook 股票价格和地震数据集:

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>> import pandas as pd
>>> fb = pd.read_csv(
...     'data/fb_stock_prices_2018.csv', 
...     index_col='date', 
...     parse_dates=True
... )
>>> quakes = pd.read_csv('data/earthquakes.csv')

提示

更改绘图样式是改变其外观和感觉的一种简单方法,无需单独设置每个方面。要设置seaborn的样式,可以使用sns.set_style()。对于matplotlib,我们可以使用plt.style.use()来指定我们要使用的样式表。这些样式会应用于该会话中创建的所有可视化。如果我们只想为某个单一图表设置样式,可以使用sns.set_context()plt.style.context()。可以在前述函数的文档中找到seaborn的可用样式,或在matplotlib中查看plt.style.available中的值。

添加参考线

很常见,我们希望在图表上突出显示某个特定的值,可能是一个边界或转折点。我们可能关心这条线是否被突破,或是否作为一个分界线。在金融领域,可能会在股票价格的折线图上绘制水平参考线,标记出支撑位和阻力位。

支撑位是一个预期下行趋势将会反转的价格水平,因为股票此时处于一个买家更倾向于购买的价格区间,推动价格向上并远离此点。相对地,阻力位是一个预期上行趋势将会反转的价格水平,因为该价格是一个吸引人的卖出点;因此,价格会下跌并远离此点。当然,这并不意味着这些水平永远不会被突破。由于我们有 Facebook 的股票数据,让我们在收盘价的折线图上添加支撑位和阻力位参考线。

重要提示

计算支撑位和阻力位的方法超出了本章的范围,但在第七章财务分析——比特币与股票市场中,将包括一些使用枢轴点计算这些的代码。此外,请务必查看进一步阅读部分,以获得关于支撑位和阻力位的更深入介绍。

我们的两条水平参考线将分别位于支撑位124.46和阻力位124.46 和阻力位138.53。这两个数字是通过使用stock_analysis包计算得出的,我们将在第七章财务分析——比特币与股票市场中构建该包。我们只需要创建StockAnalyzer类的一个实例来计算这些指标:

>>> from stock_analysis import StockAnalyzer
>>> fb_analyzer = StockAnalyzer(fb)
>>> support, resistance = (
...     getattr(fb_analyzer, stat)(level=3)
...     for stat in ['support', 'resistance']
... )
>>> support, resistance
(124.4566666666667, 138.5266666666667)

我们将使用 plt.axhline() 函数来完成这项任务,但请注意,这也适用于 Axes 对象。记住,我们提供给 label 参数的文本将会出现在图例中:

>>> fb.close['2018-12']\
...     .plot(title='FB Closing Price December 2018')
>>> plt.axhline(
...     y=resistance, color='r', linestyle='--',
...     label=f'resistance (${resistance:,.2f})'
... )
>>> plt.axhline(
...     y=support, color='g', linestyle='--',
...     label=f'support (${support:,.2f})'
... )
>>> plt.ylabel('price ($)')
>>> plt.legend()

我们应该已经熟悉之前章节中的 f-string 格式,但请注意这里在变量名之后的额外文本(:,.2f)。支持位和阻力位分别以浮动点存储在 supportresistance 变量中。冒号(:)位于 format_spec 前面,它告诉 Python 如何格式化该变量;在这种情况下,我们将其格式化为小数(f),以逗号作为千位分隔符(,),并且小数点后保留两位精度(.2)。这种格式化也适用于 format() 方法,在这种情况下,它将类似于 '{:,.2f}'.format(resistance)。这种格式化使得图表中的图例更加直观:

图 6.29 – 使用 matplotlib 创建水平参考线

图 6.29 – 使用 matplotlib 创建水平参考线

重要提示

拥有个人投资账户的人在寻找基于股票达到某一价格点来下限价单或止损单时,可能会发现一些关于支撑位和阻力位的文献,因为这些可以帮助判断目标价格的可行性。此外,交易者也可能使用这些参考线来分析股票的动能,并决定是否是时候买入/卖出股票。

回到地震数据,我们将使用 plt.axvline() 绘制垂直参考线,用于表示印尼地震震级分布中的标准差个数。位于 GitHub 仓库中 viz.py 模块的 std_from_mean_kde() 函数使用 itertools 来轻松生成我们需要绘制的颜色和值的组合:

import itertools
def std_from_mean_kde(data):
    """
    Plot the KDE along with vertical reference lines
    for each standard deviation from the mean.
    Parameters:
        - data: `pandas.Series` with numeric data
    Returns:
        Matplotlib `Axes` object.
    """
    mean_mag, std_mean = data.mean(), data.std()
    ax = data.plot(kind='kde')
    ax.axvline(mean_mag, color='b', alpha=0.2, label='mean')
    colors = ['green', 'orange', 'red']
    multipliers = [1, 2, 3]
    signs = ['-', '+']
    linestyles = [':', '-.', '--']
    for sign, (color, multiplier, style) in itertools.product(
        signs, zip(colors, multipliers, linestyles)
    ):
        adjustment = multiplier * std_mean
        if sign == '-':
            value = mean_mag – adjustment
            label = '{} {}{}{}'.format(
                r'$\mu$', r'$\pm$', multiplier, r'$\sigma$'
            )
        else:
            value = mean_mag + adjustment
            label = None # label each color only once
        ax.axvline(
            value, color=color, linestyle=style, 
            label=label, alpha=0.5
        )
    ax.legend()
    return ax

itertools 中的 product() 函数将为我们提供来自任意数量可迭代对象的所有组合。在这里,我们将颜色、乘数和线型打包在一起,因为我们总是希望乘数为 1 时使用绿色虚线;乘数为 2 时使用橙色点划线;乘数为 3 时使用红色虚线。当 product() 使用这些元组时,我们得到的是正负符号的所有组合。为了避免图例过于拥挤,我们仅使用 ± 符号为每种颜色标注一次。由于在每次迭代中字符串和元组之间有组合,我们在 for 语句中解包元组,以便更容易使用。

提示

我们可以使用 LaTeX 数学符号(www.latex-project.org/)为我们的图表标注,只要我们遵循一定的模式。首先,我们必须通过在字符串前加上 r 字符来将其标记为 raw。然后,我们必须用 $ 符号将 LaTeX 包围。例如,我们在前面的代码中使用了 r'$\mu$' 来表示希腊字母 μ。

我们将使用std_from_mean_kde()函数,看看印度尼西亚地震震级的估算分布中哪些部分位于均值的一个、两个或三个标准差内:

>>> from viz import std_from_mean_kde
>>> ax = std_from_mean_kde(
...     quakes.query(
...         'magType == "mb" and parsed_place == "Indonesia"'
...     ).mag
... )
>>> ax.set_title('mb magnitude distribution in Indonesia')
>>> ax.set_xlabel('mb earthquake magnitude')

请注意,KDE 呈右偏分布——右侧的尾部更长,均值位于众数的右侧:

图 6.30 – 包含垂直参考线

图 6.30 – 包含垂直参考线

小提示

要绘制任意斜率的直线,只需将线段的两个端点作为两个x值和两个y值(例如,[0, 2][2, 0])传递给plt.plot(),使用相同的Axes对象。对于非直线,np.linspace()可以用来创建在start, stop)区间内均匀分布的点,这些点可以作为x值并计算相应的y值。作为提醒,指定范围时,方括号表示包含端点,圆括号表示不包含端点,因此[0, 1)表示从 0 到接近 1 但不包括 1。我们在使用pd.cut()pd.qcut()时,如果不命名桶,就会看到这种情况。

填充区域

在某些情况下,参考线本身并不那么有趣,但两条参考线之间的区域更有意义;为此,我们有axvspan()axhspan()。让我们重新审视 Facebook 股票收盘价的支撑位和阻力位。我们可以使用axhspan()来填充两者之间的区域:

>>> ax = fb.close.plot(title='FB Closing Price')
>>> ax.axhspan(support, resistance, alpha=0.2)
>>> plt.ylabel('Price ($)')

请注意,阴影区域的颜色由facecolor参数决定。在这个例子中,我们接受了默认值:

![图 6.31 – 添加一个水平阴影区域

图 6.31 – 添加一个水平阴影区域

当我们感兴趣的是填充两条曲线之间的区域时,可以使用plt.fill_between()plt.fill_betweenx()函数。plt.fill_between()函数接受一组x值和两组y值;如果需要相反的效果,可以使用plt.fill_betweenx()。让我们使用plt.fill_between()填充 Facebook 每个交易日的高价和低价之间的区域:

>>> fb_q4 = fb.loc['2018-Q4']
>>> plt.fill_between(fb_q4.index, fb_q4.high, fb_q4.low)
>>> plt.xticks([
...     '2018-10-01', '2018-11-01', '2018-12-01', '2019-01-01'
... ])
>>> plt.xlabel('date')
>>> plt.ylabel('price ($)')
>>> plt.title(
...     'FB differential between high and low price Q4 2018'
... )

这能让我们更清楚地了解某一天价格的波动情况;垂直距离越高,波动越大:

图 6.32 – 在两条曲线之间填充阴影

图 6.32 – 在两条曲线之间填充阴影

通过为where参数提供布尔掩码,我们可以指定何时填充曲线之间的区域。让我们只填充上一个例子中的 12 月。我们将在整个时间段内为高价曲线和低价曲线添加虚线,以便查看发生了什么:

>>> fb_q4 = fb.loc['2018-Q4']
>>> plt.fill_between(
...     fb_q4.index, fb_q4.high, fb_q4.low, 
...     where=fb_q4.index.month == 12, 
...     color='khaki', label='December differential'
... )
>>> plt.plot(fb_q4.index, fb_q4.high, '--', label='daily high')
>>> plt.plot(fb_q4.index, fb_q4.low, '--', label='daily low') 
>>> plt.xticks([
...     '2018-10-01', '2018-11-01', '2018-12-01', '2019-01-01'
... ])
>>> plt.xlabel('date')
>>> plt.ylabel('price ($)')
>>> plt.legend()
>>> plt.title(
...     'FB differential between high and low price Q4 2018'
... )

这将产生以下图表:

图 6.33 – 在两条曲线之间选择性地填充阴影

图 6.33 – 在两条曲线之间选择性地填充阴影

通过参考线和阴影区域,我们能够引起对特定区域的注意,甚至可以在图例中标注它们,但在用文字解释这些区域时,我们的选择有限。现在,让我们讨论如何为我们的图表添加更多的上下文注释。

注释

我们经常需要在可视化中标注特定的点,以便指出事件,例如 Facebook 股票因某些新闻事件而下跌的日期,或者标注一些重要的值以供比较。例如,让我们使用 plt.annotate() 函数标注支撑位和阻力位:

>>> ax = fb.close.plot(
...     title='FB Closing Price 2018',
...     figsize=(15, 3)
... )
>>> ax.set_ylabel('price ($)')
>>> ax.axhspan(support, resistance, alpha=0.2)
>>> plt.annotate(
...     f'support\n(${support:,.2f})',
...     xy=('2018-12-31', support),
...     xytext=('2019-01-21', support),
...     arrowprops={'arrowstyle': '->'}
... )
>>> plt.annotate(
...     f'resistance\n(${resistance:,.2f})',
...     xy=('2018-12-23', resistance)
... ) 
>>> for spine in ['top', 'right']:
...     ax.spines[spine].set_visible(False)

请注意,注释有所不同;当我们注释阻力位时,只提供了注释文本和通过 xy 参数注释的点的坐标。然而,当我们注释支撑位时,我们还为 xytextarrowprops 参数提供了值;这使得我们可以将文本放置在不同于数据出现位置的地方,并添加箭头指示数据出现的位置。通过这种方式,我们避免了将标签遮挡在最后几天的数据上:

图 6.34 – 包含注释

图 6.34 – 包含注释

arrowprops 参数为我们提供了相当多的定制选项,可以选择我们想要的箭头类型,尽管要做到完美可能有些困难。举个例子,让我们用百分比的下降幅度标注出 Facebook 在七月价格的大幅下跌:

>>> close_price = fb.loc['2018-07-25', 'close']
>>> open_price = fb.loc['2018-07-26', 'open']
>>> pct_drop = (open_price - close_price) / close_price
>>> fb.close.plot(title='FB Closing Price 2018', alpha=0.5)
>>> plt.annotate(
...     f'{pct_drop:.2%}', va='center',
...     xy=('2018-07-27', (open_price + close_price) / 2),
...     xytext=('2018-08-20', (open_price + close_price) / 2),
...     arrowprops=dict(arrowstyle='-,widthB=4.0,lengthB=0.2')
... )
>>> plt.ylabel('price ($)')

请注意,我们能够通过在 f-string 的格式说明符中使用 .2%pct_drop 变量格式化为具有两位精度的百分比。此外,通过指定 va='center',我们告诉 matplotlib 将我们的注释垂直居中显示在箭头的中间:

![图 6.35 – 自定义注释的箭头

图 6.35 – 自定义注释的箭头

Matplotlib 提供了高度灵活的选项来定制这些标注——我们可以传递任何 matplotlibText 类所支持的选项 (matplotlib.org/api/text_api.html#matplotlib.text.Text)。要改变颜色,只需在 color 参数中传递所需的颜色。我们还可以通过 fontsizefontweightfontfamilyfontstyle 参数分别控制字体大小、粗细、家族和样式。

颜色

为了保持一致性,我们制作的可视化图表应该遵循一个颜色方案。公司和学术机构通常会为演示文稿制定定制的调色板。我们也可以轻松地在可视化中采用相同的调色板。

到目前为止,我们要么使用单个字符名称为 color 参数提供颜色,例如 'b' 表示蓝色,'k' 表示黑色,或者使用它们的名称('blue''black')。我们还看到 matplotlib 有许多可以用名称指定的颜色;完整列表可以在文档中找到,地址是 matplotlib.org/examples/color/named_colors.html

重要提示

请记住,如果我们使用 style 参数提供颜色,我们只能使用具有单个字符缩写的颜色。

另外,我们可以提供一个十六进制的颜色码来指定我们想要的颜色;那些之前在 HTML 或 CSS 中工作过的人无疑会熟悉这种方式,它可以精确指定颜色(无论不同的地方称其为何种颜色)。对于不熟悉十六进制颜色码的人来说,它指定了用于制作所需颜色的红色、绿色和蓝色的数量,格式为 #RRGGBB。黑色是 #000000,白色是 #FFFFFF(大小写不敏感)。这可能会令人困惑,因为 F 显然不是一个数字;但这些是十六进制数(基数为 16,而不是我们传统使用的十进制数),其中 0-9 仍然表示 0-9,但 A-F 表示 10-15

Matplotlib 将十六进制码作为字符串接受到 color 参数中。为了说明这一点,让我们以 #8000FF 绘制 Facebook 的开盘价:

>>> fb.plot(
...     y='open',
...     figsize=(5, 3),
...     color='#8000FF',
...     legend=False,
...     title='Evolution of FB Opening Price in 2018'
... )
>>> plt.ylabel('price ($)')

这导致了一个紫色线图:

图 6.36 – 改变线条颜色

图 6.36 – 改变线条颜色

或者,我们可以将值以 RGB 或 color 参数的元组给出。如果我们不提供 alpha 值,默认值为不透明的 1。这里需要注意的一件事是,虽然这些数值以 [0, 255] 范围呈现,但 matplotlib 要求它们在 [0, 1] 范围内,因此我们必须将每个值除以 255。以下代码与前面的示例相同,只是我们使用 RGB 元组而不是十六进制码:

fb.plot(
    y='open',
    figsize=(5, 3),
    color=(128 / 255, 0, 1),
    legend=False,
    title='Evolution of FB Opening Price in 2018'
)
plt.ylabel('price ($)')

在前一章中,我们看到了几个示例,我们在绘制变化数据时需要许多不同的颜色,但这些颜色从哪里来?嗯,matplotlib 有许多颜色映射用于此目的。

颜色映射

而不是必须预先指定我们要使用的所有颜色,matplotlib 可以使用一个颜色映射并循环遍历其中的颜色。在前一章中讨论热图时,我们考虑了根据给定任务使用适当的颜色映射类别的重要性。以下表格显示了三种类型的颜色映射,每种都有其自己的用途:

图 6.37 – 颜色映射类型

图 6.37 – 颜色映射类型

提示

浏览颜色名称、十六进制和 RGB 值,请访问 www.color-hex.com/,并在 matplotlib.org/gallery/color/colormap_reference.html 上找到颜色映射的完整颜色光谱。

在 Python 中,我们可以通过运行以下代码获取所有可用色图的列表:

>>> from matplotlib import cm
>>> cm.datad.keys()
dict_keys(['Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 
           'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 
           'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 
           'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 
           'Reds', ..., 'Blues_r', 'BrBG_r', 'BuGn_r', ...])

注意,有些色图出现了两次,其中一个是反向的,名称后缀带有 _r。这非常有用,因为我们无需将数据反转,就能将值映射到我们想要的颜色。Pandas 接受这些色图作为字符串或 matplotlib 色图,可以通过 plot() 方法的 colormap 参数传入 'coolwarm_r'cm.get_cmap('coolwarm_r')cm.coolwarm_r,得到相同的结果。

让我们使用 coolwarm_r 色图来展示 Facebook 股票的收盘价如何在 20 天滚动最小值和最大值之间波动:

>>> ax = fb.assign(
...     rolling_min=lambda x: x.low.rolling(20).min(),
...     rolling_max=lambda x: x.high.rolling(20).max()
... ).plot(
...     y=['rolling_max', 'rolling_min'], 
...     colormap='coolwarm_r', 
...     label=['20D rolling max', '20D rolling min'],
...     style=[':', '--'],
...     figsize=(12, 3),
...     title='FB closing price in 2018 oscillating between '
...           '20-day rolling minimum and maximum price'
... )
>>> ax.plot(
...     fb.close, 'purple', alpha=0.25, label='closing price'
... )
>>> plt.legend()
>>> plt.ylabel('price ($)')

注意,使用反转的色图将红色表示为热性能(滚动最大值),蓝色表示为冷性能(滚动最小值)是多么简单,而不是试图确保 pandas 首先绘制滚动最小值:

图 6.38 – 使用色图

图 6.38 – 使用色图

colormap 对象是一个可调用的,这意味着我们可以传递[0, 1]范围内的值,它会告诉我们该点在色图上的 RGBA 值,我们可以将其用于 color 参数。这使得我们能更精确地控制从色图中使用的颜色。我们可以使用这种技巧来控制色图如何在我们的数据上展开。例如,我们可以请求 ocean 色图的中点,并将其用于 color 参数:

>>> cm.get_cmap('ocean')(.5)
(0.0, 0.2529411764705882, 0.5019607843137255, 1.0)

提示

covid19_cases_map.ipynb 笔记本中有一个示例,展示了如何将色图作为可调用对象使用,在该示例中,COVID-19 的病例数被映射到颜色上,颜色越深表示病例数越多。

尽管有大量的色图可供选择,我们可能还是需要创建自己的色图。也许我们有自己喜欢使用的颜色调色板,或者有某些需求需要使用特定的色彩方案。我们可以使用 matplotlib 创建自己的色图。让我们创建一个混合色图,它从紫色(#800080)到黄色(#FFFF00),中间是橙色(#FFA500)。我们所需要的所有功能都在 color_utils.py 中。如果我们从与该文件相同的目录运行 Python,我们可以这样导入这些函数:

>>> import color_utils

首先,我们需要将这些十六进制颜色转换为 RGB 等效值,这正是 hex_to_rgb_color_list() 函数所做的。请注意,这个函数还可以处理当 RGB 值的两个数字使用相同的十六进制数字时的简写十六进制代码(例如,#F1D#FF11DD 的简写形式):

import re
def hex_to_rgb_color_list(colors):
    """
    Take color or list of hex code colors and convert them 
    to RGB colors in the range [0,1].
    Parameters:
        - colors: Color or list of color strings as hex codes
    Returns:
        The color or list of colors in RGB representation.
    """
    if isinstance(colors, str):
        colors = [colors]
    for i, color in enumerate(
        [color.replace('#', '') for color in colors]
    ):
        hex_length = len(color)
        if hex_length not in [3, 6]:
            raise ValueError(
                'Colors must be of the form #FFFFFF or #FFF'
            )
        regex = '.' * (hex_length // 3)
        colors[i] = [
            int(val * (6 // hex_length), 16) / 255
            for val in re.findall(regex, color)
        ]
    return colors[0] if len(colors) == 1 else colors

提示

看一下 enumerate() 函数;它允许我们在迭代时获取索引和值,而不必在循环中查找值。另外,注意 Python 如何通过 int() 函数指定基数,轻松地将十进制数转换为十六进制数。(记住 // 是整数除法——我们必须这样做,因为 int() 期望的是整数,而不是浮点数。)

我们需要的下一个函数是将这些 RGB 颜色转换为色图值的函数。此函数需要执行以下操作:

  1. 创建一个具有 256 个槽位的 4D NumPy 数组用于颜色定义。请注意,我们不想改变透明度,因此我们将保持第四维(alpha)不变。

  2. 对于每个维度(红色、绿色和蓝色),使用 np.linspace() 函数在目标颜色之间创建均匀过渡(即,从颜色 1 的红色分量过渡到颜色 2 的红色分量,再到颜色 3 的红色分量,以此类推,然后重复此过程处理绿色分量,最后是蓝色分量)。

  3. 返回一个 ListedColormap 对象,我们可以在绘图时使用它。

这就是 blended_cmap() 函数的功能:

from matplotlib.colors import ListedColormap
import numpy as np
def blended_cmap(rgb_color_list):
    """
    Create a colormap blending from one color to the other.
    Parameters:
        - rgb_color_list: List of colors represented as 
          [R, G, B] values in the range [0, 1], like 
          [[0, 0, 0], [1, 1, 1]], for black and white.
    Returns: 
        A matplotlib `ListedColormap` object
    """
    if not isinstance(rgb_color_list, list):
        raise ValueError('Colors must be passed as a list.')
    elif len(rgb_color_list) < 2:
        raise ValueError('Must specify at least 2 colors.')
    elif (
        not isinstance(rgb_color_list[0], list)
        or not isinstance(rgb_color_list[1], list)
    ) or (
        (len(rgb_color_list[0]) != 3 
        or len(rgb_color_list[1]) != 3)
    ):
        raise ValueError(
            'Each color should be a list of size 3.'
        )
    N, entries = 256, 4 # red, green, blue, alpha
    rgbas = np.ones((N, entries))
    segment_count = len(rgb_color_list) – 1
    segment_size = N // segment_count
    remainder = N % segment_count # need to add this back later
    for i in range(entries - 1): # we don't alter alphas
        updates = []
        for seg in range(1, segment_count + 1):
            # handle uneven splits due to remainder
            offset = 0 if not remainder or seg > 1 \
                     else remainder
            updates.append(np.linspace(
                start=rgb_color_list[seg - 1][i], 
                stop=rgb_color_list[seg][i], 
                num=segment_size + offset
            ))
        rgbas[:,i] = np.concatenate(updates)
    return ListedColormap(rgbas)

我们可以使用 draw_cmap() 函数绘制色条,帮助我们可视化我们的色图:

import matplotlib.pyplot as plt
def draw_cmap(cmap, values=np.array([[0, 1]]), **kwargs):
    """
    Draw a colorbar for visualizing a colormap.
    Parameters:
        - cmap: A matplotlib colormap
        - values: Values to use for the colormap
        - kwargs: Keyword arguments to pass to `plt.colorbar()`
    Returns:
        A matplotlib `Colorbar` object, which you can save 
        with: `plt.savefig(<file_name>, bbox_inches='tight')`
    """
    img = plt.imshow(values, cmap=cmap)
    cbar = plt.colorbar(**kwargs)
    img.axes.remove()
    return cbar

这个函数使我们可以轻松地为任何可视化添加一个带有自定义色图的色条;covid19_cases_map.ipynb 笔记本中有一个示例,展示了如何使用 COVID-19 病例在世界地图上绘制。现在,让我们使用这些函数来创建并可视化我们的色图。我们将通过导入模块来使用它们(我们之前已经做过了):

>>> my_colors = ['#800080', '#FFA500', '#FFFF00']
>>> rgbs = color_utils.hex_to_rgb_color_list(my_colors)
>>> my_cmap = color_utils.blended_cmap(rgbs)
>>> color_utils.draw_cmap(my_cmap, orientation='horizontal')

这将导致显示我们的色图的色条:

图 6.39 – 自定义混合色图

图 6.39 – 自定义混合色图

提示

Seaborn 还提供了额外的颜色调色板,以及一些实用工具,帮助用户选择色图并交互式地为 matplotlib 创建自定义色图,可以在 Jupyter Notebook 中使用。更多信息请查看 选择颜色调色板 教程(seaborn.pydata.org/tutorial/color_palettes.html),该笔记本中也包含了一个简短的示例。

正如我们在创建的色条中看到的,这些色图能够显示不同的颜色渐变,以捕捉连续值。如果我们仅希望每条线在折线图中显示为不同的颜色,我们很可能希望在不同的颜色之间进行循环。为此,我们可以使用 itertools.cycle() 与一个颜色列表;它们不会被混合,但我们可以无限循环,因为它是一个无限迭代器。我们在本章早些时候使用了这种技术来为回归残差图定义自己的颜色:

>>> import itertools
>>> colors = itertools.cycle(['#ffffff', '#f0f0f0', '#000000'])
>>> colors
<itertools.cycle at 0x1fe4f300>
>>> next(colors)
'#ffffff'

更简单的情况是,我们在某个地方有一个颜色列表,但与其将其放入我们的绘图代码并在内存中存储另一个副本,不如写一个简单的 return,它使用 yield。以下代码片段展示了这种情况的一个模拟示例,类似于 itertools 解决方案;然而,它并不是无限的。这只是说明了我们可以在 Python 中找到多种方式来做同一件事;我们必须找到最适合我们需求的实现:

from my_plotting_module import master_color_list
def color_generator():
    yield from master_color_list

使用matplotlib时,另一种选择是实例化一个ListedColormap对象,并传入颜色列表,同时为N定义一个较大的值,以确保颜色足够多次重复(如果不提供,它将只经过一次颜色列表):

>>> from matplotlib.colors import ListedColormap
>>> red_black = ListedColormap(['red', 'black'], N=2000)
>>> [red_black(i) for i in range(3)]
[(1.0, 0.0, 0.0, 1.0), 
 (0.0, 0.0, 0.0, 1.0), 
 (1.0, 0.0, 0.0, 1.0)]

注意,我们还可以使用matplotlib团队的cycler,它通过允许我们定义颜色、线条样式、标记、线宽等的组合来增加额外的灵活性,能够循环使用这些组合。API 文档详细介绍了可用功能,您可以在matplotlib.org/cycler/找到。我们将在第七章《金融分析——比特币与股市》中看到一个例子。

条件着色

颜色映射使得根据数据中的值变化颜色变得简单,但如果我们只想在特定条件满足时使用特定颜色该怎么办?在这种情况下,我们需要围绕颜色选择构建一个函数。

我们可以编写一个生成器,根据数据确定绘图颜色,并且仅在请求时计算它。假设我们想要根据年份(从 1992 年到 200018 年,没错,这不是打字错误)是否为闰年来分配颜色,并区分哪些年份不是闰年(例如,我们希望为那些能被 100 整除但不能被 400 整除的年份指定特殊颜色,因为它们不是闰年)。显然,我们不想在内存中保留如此庞大的列表,所以我们创建一个生成器按需计算颜色:

def color_generator():
    for year in range(1992, 200019): # integers [1992, 200019)
        if year % 100 == 0 and year % 400 != 0: 
            # special case (divisible by 100 but not 400)
            color = '#f0f0f0'
        elif year % 4 == 0:
            # leap year (divisible by 4)
            color = '#000000'
        else:
            color = '#ffffff'
        yield color

重要提示

取余运算符(%)返回除法操作的余数。例如,4 % 2 等于 0,因为 4 可以被 2 整除。然而,由于 4 不能被 3 整除,4 % 3 不为 0,它是 1,因为我们可以将 3 放入 4 一次,剩下 1(4 - 3)。取余运算符可以用来检查一个数字是否能被另一个数字整除,通常用于判断数字是奇数还是偶数。这里,我们使用它来查看是否满足闰年的条件(这些条件依赖于能否被整除)。

由于我们将year_colors定义为生成器,Python 将记住我们在此函数中的位置,并在调用next()时恢复执行:

>>> year_colors = color_generator()
>>> year_colors
<generator object color_generator at 0x7bef148dfed0>
>>> next(year_colors)
'#000000'

更简单的生成器可以通过生成器表达式来编写。例如,如果我们不再关心特殊情况,可以使用以下代码:

>>> year_colors = (
...     '#ffffff'
...     if (not year % 100 and year % 400) or year % 4
...     else '#000000' for year in range(1992, 200019)
... )
>>> year_colors
<generator object <genexpr> at 0x7bef14415138>
>>> next(year_colors)
'#000000'

对于不来自 Python 的人来说,我们之前代码片段中的布尔条件实际上是数字(year % 400 的结果是一个整数),这可能会让人感到奇怪。这是利用了 Python 的真值/假值,即具有零值(例如数字0)或为空(如[]'')的值被视为假值。因此,在第一个生成器中,我们写了 year % 400 != 0 来准确显示发生了什么,而 year % 400 的更多含义是:如果没有余数(即结果为 0),语句将被评估为 False,反之亦然。显然,在某些时候,我们必须在可读性和 Pythonic 之间做出选择,但了解如何编写 Pythonic 代码是很重要的,因为它通常会更高效。

提示

在 Python 中运行 import this 来查看Python 之禅,它给出了关于如何写 Pythonic 代码的一些思路。

现在我们已经了解了一些在 matplotlib 中使用颜色的方法,让我们考虑另一种让数据更突出的方法。根据我们要绘制的内容或可视化的使用场景(例如黑白打印),使用纹理与颜色一起或代替颜色可能会更有意义。

纹理

除了定制我们在可视化中使用的颜色,matplotlib 还使得在各种绘图函数中包含纹理成为可能。这是通过 hatch 参数实现的,pandas 会为我们传递该参数。让我们绘制一个 2018 年 Q4 Facebook 股票每周交易量的条形图,并使用纹理条形图:

>>> weekly_volume_traded = fb.loc['2018-Q4']\
...     .groupby(pd.Grouper(freq='W')).volume.sum()
>>> weekly_volume_traded.index = \
...     weekly_volume_traded.index.strftime('W %W')
>>> ax = weekly_volume_traded.plot(
...     kind='bar',
...     hatch='*',
...     color='lightgray',
...     title='Volume traded per week in Q4 2018'
... )
>>> ax.set(
...     xlabel='week number', 
...     ylabel='volume traded'
... )

使用 hatch='*',我们的条形图将填充星号。请注意,我们还为每个条形图设置了颜色,因此这里有很多灵活性:

图 6.40 – 使用纹理条形图

图 6.40 – 使用纹理条形图

纹理还可以组合起来形成新的图案,并通过重复来增强效果。让我们回顾一下 plt.fill_between() 的示例,其中我们仅为 12 月部分上色(图 6.33)。这次我们将使用纹理来区分每个月,而不仅仅是为 12 月添加阴影;我们将用环形纹理填充 10 月,用斜线填充 11 月,用小点填充 12 月:

>>> import calendar
>>> fb_q4 = fb.loc['2018-Q4']
>>> for texture, month in zip(
...     ['oo', '/\\/\\', '...'], [10, 11, 12]
... ):
...     plt.fill_between(
...         fb_q4.index, fb_q4.high, fb_q4.low,
...         hatch=texture, facecolor='white',
...         where=fb_q4.index.month == month,
...         label=f'{calendar.month_name[month]} differential'
...     )
>>> plt.plot(fb_q4.index, fb_q4.high, '--', label='daily high')
>>> plt.plot(fb_q4.index, fb_q4.low, '--', label='daily low')
>>> plt.xticks([
...     '2018-10-01', '2018-11-01', '2018-12-01', '2019-01-01'
... ])
>>> plt.xlabel('date')
>>> plt.ylabel('price ($)')
>>> plt.title(
...     'FB differential between high and low price Q4 2018'
... )
>>> plt.legend()

使用 hatch='o' 会生成细环,因此我们使用 'oo' 来为 10 月生成更粗的环形纹理。对于 11 月,我们希望得到交叉图案,因此我们结合了两个正斜杠和两个反斜杠(我们实际上用了四个反斜杠,因为它们需要转义)。为了在 12 月实现小点纹理,我们使用了三个句点——添加得越多,纹理就越密集:

图 6.41 – 结合纹理

图 6.41 – 结合纹理

这就是我们对图表定制化的讨论总结。这并非完整的讨论,因此请确保探索 matplotlib API,了解更多内容。

总结

呼,真多啊!我们学习了如何使用matplotlibpandasseaborn创建令人印象深刻且自定义的可视化图表。我们讨论了如何使用seaborn绘制其他类型的图表,并清晰地展示一些常见图表。现在,我们可以轻松地创建自己的颜色映射、标注图表、添加参考线和阴影区域、调整坐标轴/图例/标题,并控制可视化外观的大部分方面。我们还体验了使用itertools并创建我们自己的生成器。

花些时间练习我们讨论的内容,完成章节末的练习。在下一章中,我们将把所学的知识应用到金融领域,创建自己的 Python 包,并将比特币与股票市场进行比较。

练习

使用我们迄今为止在本书中学到的知识和本章的数据,创建以下可视化图表。确保为图表添加标题、轴标签和图例(适当时)。

  1. 使用seaborn创建热图,显示地震震级与是否发生海啸之间的相关系数,地震测量使用mb震级类型。

  2. 创建一个 Facebook 交易量和收盘价格的箱线图,并绘制 Tukey 围栏范围的参考线,乘数为 1.5。边界将位于Q1 − 1.5 × IQRQ3 + 1.5 × IQR。确保使用数据的quantile()方法以简化这一过程。(选择你喜欢的图表方向,但确保使用子图。)

  3. 绘制全球累计 COVID-19 病例的变化趋势,并在病例超过 100 万的日期上添加一条虚线。确保y轴的刻度标签相应地格式化。

  4. 使用axvspan()在收盘价的折线图中,从'2018-07-25''2018-07-31'标记 Facebook 价格的大幅下降区域。

  5. 使用 Facebook 股价数据,在收盘价的折线图上标注以下三个事件:

    a) 2018 年 7 月 25 日收盘后宣布用户增长令人失望

    b) 剑桥分析公司丑闻爆发 2018 年 3 月 19 日(当时影响了市场)

    c) FTC 启动调查 2018 年 3 月 20 日

  6. 修改reg_resid_plots()函数,使用matplotlib的颜色映射,而不是在两种颜色之间循环。记住,在这种情况下,我们应该选择定性颜色映射或创建自己的颜色映射。

进一步阅读

查看以下资源,了解更多关于本章所涉及主题的信息: