数据可视化#

一图胜千言。 —Arthur Brisbane (1911)

本章介绍 matplotlib 和 plotly 包的基本可视化功能。

虽然有更多的可视化包可用,但 matplotlib 已确立了自己作为基准和——在许多情况下——一种稳健可靠的可视化工具的地位。它既易于用于标准绘图,在处理更复杂的绘图和自定义时也非常灵活。此外,它与 NumPy 和 pandas 及其提供的数据结构紧密集成。

matplotlib 只能生成位图形式的图表(例如 PNG 或 JPG 格式)。另一方面,现代 Web 技术——例如基于数据驱动文档(Data-Driven Documents, D3.js)标准的技术——允许创建漂亮的交互式且可嵌入的图表(交互式的意思是,例如,可以放大查看特定区域的更多细节)。plotly 是一个可以方便地用 Python 创建此类 D3.js 图表的包。一个名为 Cufflinks 的小型附加库将 plotly 与 pandas DataFrame 对象紧密集成,并允许创建流行的金融图表(如蜡烛图(candlestick chart))。

本章主要涵盖以下主题:

"静态二维绘图" on page 168

本节介绍 matplotlib,并展示一系列典型的二维绘图,从最简单的到一些更高级的——带有双刻度或不同子图的绘图。

"静态三维绘图" on page 191

基于 matplotlib,本节展示了一些对某些金融应用有用的三维绘图。

"交互式二维绘图" on page 195

本节介绍 plotly 和 Cufflinks 以创建交互式二维绘图。利用 Cufflinks 的 QuantFigure 功能,本节还介绍了技术分析中使用的典型金融图表。

本章无法全面介绍 Python、matplotlib 或 plotly 的数据可视化功能,但提供了一些示例,展示了这些包在金融领域的基本和重要功能。其他示例也出现在后续章节中。例如,第8章更深入地展示了如何使用 pandas 库可视化金融时间序列数据。

静态二维绘图#

在创建示例数据和开始绘图之前,先进行一些导入和自定义设置:

In [1]: import matplotlib as mpl ①
In [2]: mpl.__version__ ②
Out[2]: '3.0.0'
In [3]: import matplotlib.pyplot as plt ③
In [4]: plt.style.use('seaborn') ④
In [5]: mpl.rcParams['font.family'] = 'serif' ⑤
In [6]: %matplotlib inline

① 以常用的缩写 mpl 导入 matplotlib。

② 使用的 matplotlib 版本。

③ 以常用的缩写 plt 导入主要绘图(子)包。

④ 将绘图样式设置为 seaborn。

⑤ 将所有绘图的字体设置为衬线字体(serif)。

一维数据集#

最基本但仍然相当强大的绘图函数是 plt.plot()。原则上,它需要两组数字:

x 值

包含 x 坐标(横坐标值)的列表或数组

y 值

包含 y 坐标(纵坐标值)的列表或数组

当然,提供的 x 和 y 值的数量必须匹配。考虑以下代码,其输出如图7-1所示:

In [7]: import numpy as np

In [8]: np.random.seed(1000)

In [9]: y = np.random.standard_normal(20)

In [10]: x = np.arange(len(y)) plt.plot(x, y);

① 固定随机数生成器的种子以保证可重复性。

② 生成随机数(y 值)。

③ 固定整数(x 值)。

④ 调用 plt.plot() 函数并传入 x 和 y 对象。

图7-1 给定x和y值的绘图

plt.plot() 会检测到传入的是 ndarray 对象。在这种情况下,无需提供"额外"的 x 值信息。如果只提供 y 值,plt.plot() 会将索引值作为相应的 x 值。因此,下面一行代码就能生成完全相同的输出(见图7-2):

In [11]: plt.plot(y);

图7-2 给定 ndarray 对象数据的绘图

NumPy 数组与 matplotlib#

你可以简单地将 NumPy ndarray 对象传递给 matplotlib 函数。matplotlib 能够解释这些数据结构以简化绘图。但注意不要传递过大和/或过于复杂的数组。

由于大多数 ndarray 方法返回一个 ndarray 对象,也可以传递带有方法(在某些情况下甚至多个方法)的对象。通过调用包含示例数据的 ndarray 对象的 cumsum() 方法,可以得到这些数据的累积和,并得到不同的输出(见图7-3):

In [12]: plt.plot(y.cumsum());

图7-3 带方法调用的 ndarray 对象绘图

一般来说,默认的绘图样式不能满足报告、出版物等的典型要求。例如,可能需要自定义使用的字体(例如,为了与 LaTeX 字体兼容)、在轴上添加标签,或绘制网格以提高可读性。这就是绘图样式发挥作用的地方。此外,matplotlib 提供了大量函数来自定义绘图样式。有些函数易于使用;其他一些则需要更深入的了解。例如,易于使用的是那些操作坐标轴以及与网格和标签相关的函数(见图7-4):

In [13]: plt.plot(y.cumsum())
plt.grid(False)
plt.axis('equal');

① 关闭网格。

② 使两个轴等比例缩放。

图7-4 无网格的绘图

plt.axis() 的其他选项见表7-1,其中大部分必须作为 str 对象传递。

表7-1 plt.axis() 的选项

参数描述
空返回当前轴限制
off关闭轴线及标签
equal等比例缩放
scaled通过尺寸变化实现等比例缩放
tight使所有数据可见(收紧限制)
image使所有数据可见(带数据限制)
[xmin, xmax, ymin, ymax]设置限制为给定的(列表)值

此外,还可以直接使用 plt.xlim() 和 plt.ylim() 设置每个轴的最小值和最大值。以下代码提供了一个示例,其输出如图7-5所示:

In [14]: plt.plot(y.cumsum())
plt.xlim(-1, 20)
plt.ylim(np.min(y.cumsum()) - 1,
np.max(y.cumsum()) + 1);

图7-5 自定义轴限制的绘图

为了提高可读性,绘图通常包含一些标签——例如标题和描述 x、y 值性质的标签。这些分别由 plt.title()、plt.xlabel() 和 plt.ylabel() 函数添加。默认情况下,plot() 绘制连续线条,即使提供的是离散数据点。离散点的绘制通过选择不同的样式选项来实现。图7-6 叠加了(红色)点和一条线宽为1.5磅的(蓝色)线:

In [15]: plt.figure(figsize=(10, 6)) ①
plt.plot(y.cumsum(), 'b', lw=1.5) ②
plt.plot(y.cumsum(), 'ro') ③
plt.xlabel('index') ④
plt.ylabel('value') ⑤
plt.title('A Simple Plot'); ⑥

① 增大图形尺寸。

② 以蓝色线条绘制数据,线宽1.5磅。

③ 以红色(粗)点绘制数据。

④ 在 x 轴上放置标签。

⑤ 在 y 轴上放置标签。

⑥ 放置标题。

图7-6 带典型标签的绘图

默认情况下,plt.plot() 支持表7-2中的颜色缩写。

表7-2 标准颜色缩写

字符颜色
b蓝色 (Blue)
g绿色 (Green)
r红色 (Red)
c青色 (Cyan)
m品红 (Magenta)
y黄色 (Yellow)
k黑色 (Black)
w白色 (White)

在线条和/或点样式方面,plt.plot() 支持表7-3中列出的字符。

表7-3 标准样式字符

Character Symbol Solid line style Dashed line style -. Dash-dot line style : Dotted line style Point marker Pixel marker o Circle marker v Triangle_down marker^ Triangle_up marker < Triangle_left marker > Triangle_right marker 1 Tri_down marker 2 Tri_up marker 3 Tri_left marker 4 Tri_right marker s Square marker p Pentagon marker * Star marker h Hexagon1 marker H Hexagon2 marker + Plus marker x X marker D Diamond marker d Thin diamond marker | Vline marker Hline marker 一

任何颜色缩写都可以与任何样式字符组合。这样,可以确保不同的数据集易于区分。绘图样式也会反映在图例中。

二维数据集#

绘制一维数据可以被视为一个特例。通常,数据集会包含多个独立的数据子集。使用 matplotlib 处理此类数据集的规则与处理一维数据相同。然而,在这种情况下可能会出现一些额外的问题。例如,两个数据集的缩放尺度可能差异很大,以至于无法使用相同的 y 轴和/或 x 轴缩放来绘制。另一个问题可能是希望以不同的方式可视化两个不同的数据集,例如一个用线图,另一个用条形图。

以下代码生成一个二维样本数据集,以形状为 20 × 2 的 NumPy ndarray 对象形式,包含标准正态分布的伪随机数。在该数组上,调用 cumsum() 方法沿轴0(即第一个维度)计算样本数据的累积和:

In [16]: y = np.random.standard_normal((20, 2)).cumsum(axis=0)

一般来说,也可以将此类二维数组传递给 plt.plot()。它会自动将包含的数据解释为独立的数据集(沿轴1,即第二个维度)。相应的绘图如图7-7所示:

In [17]: plt.figure(figsize=(10, 6))
    plt.plot(y, lw=1.5)
    plt.plot(y, 'ro')
    plt.xlabel('index')
    plt.ylabel('value')
    plt.title('A Simple Plot');

图7-7 两个数据集的绘图

在这种情况下,额外的注释可能有助于更好地解读图表。可以为每个数据集添加单独的标签,并使其显示在图例中。函数 plt.legend() 接受不同的位置参数。0 代表最佳位置,即尽可能少的数据被图例遮挡。

图7-8 展示了两个数据集的绘图,这次带有图例。在生成代码中,ndarray 对象不是整体传递的,而是分别访问两个数据子集(y[:, 0] 和 y[:, 1]),这允许为它们附加单独的标签:

In [18]: plt.figure(figsize=(10, 6))
plt.plot(y[:, 0], lw=1.5, label='1st')
plt.plot(y[:, 1], lw=1.5, label='2nd')
plt.plot(y, 'ro')
plt.legend(loc=0)
plt.xlabel('index')
plt.ylabel('value')
plt.title('A Simple Plot');

① 为数据子集定义标签。

② 在"最佳"位置放置图例。

图7-8 带标签数据集的绘图

plt.legend() 的其他位置选项包括表7-4中列出的那些。

表7-4 plt.legend() 的选项

Loc描述
默认右上角
0最佳位置
1右上角
2左上角
3左下角
4右下角
5右侧
6左侧居中
7右侧居中
8下方居中
9上方居中
10居中

多个具有相似缩放尺度的数据集(如同一个金融风险因子的模拟路径)可以使用单个 y 轴绘制。然而,数据集通常表现出相当不同的缩放尺度,使用单个 y 尺度绘制此类数据通常会导致视觉信息的大量丢失。为了说明这种效果,以下示例将两个数据子集中的第一个缩放 100 倍,并再次绘制数据(见图7-9):

In [19]: y[:, 0] = y[:, 0] * 100
In [20]: plt.figure(figsize=(10, 6))
plt.plot(y[:, 0], lw=1.5, label='1st')
plt.plot(y[:, 1], lw=1.5, label='2nd')
plt.plot(y, 'ro')
plt.legend(loc=0)
plt.xlabel('index')
plt.ylabel('value')
plt.title('A Simple Plot');

① 重新缩放第一个数据子集。

图7-9 两个不同缩放尺度数据集的绘图

检查图7-9 可以发现,第一个数据集仍然"视觉可读",而第二个数据集在新的 y 轴缩放下现在看起来像一条直线。从某种意义上说,关于第二个数据集的信息现在"视觉上丢失了"。有两种基本方法可以通过绘图手段解决这个问题,而不是调整数据(例如通过重新缩放):

  • 使用两个 y 轴(左/右)
  • 使用两个子图(上/下、左/右)

以下示例为绘图引入了第二个 y 轴。图7-10 现在有两个不同的 y 轴。左侧 y 轴用于第一个数据集,而右侧 y 轴用于第二个数据集。因此,还有两个图例:

In [21]: fig, ax1 = plt.subplots() ①
plt.plot(y[:, 0], 'b', lw=1.5, label='1st')
plt.plot(y[:, 0], 'ro')
plt.legend(loc=8)
plt.xlabel('index')
plt.ylabel('value 1st')
plt.title('A Simple Plot')
ax2 = ax1.twinx() ②
plt.plot(y[:, 1], 'g', lw=1.5, label='2nd')
plt.plot(y[:, 1], 'ro')
plt.legend(loc=0)
plt.ylabel('value 2nd');

① 定义图形和轴对象。

② 创建第二个共享 x 轴的轴对象。

图7-10 两个数据集和两个 y 轴的绘图

关键代码行是那些帮助管理轴的代码行:

fig, ax1 = plt.subplots()
ax2 = ax1.twinx()

通过使用 plt.subplots() 函数,可以直接访问底层绘图对象(图形、子图等)。例如,它允许生成一个与第一个子图共享 x 轴的第二个子图。在图7-10 中,这两个子图实际上是重叠的。

接下来,考虑两个独立子图的情况。这个选项为处理两个数据集提供了更多的自由度,如图7-11 所示:

In [22]: plt.figure(figsize=(10, 6))
    plt.subplot(211) ①
    plt.plot(y[:, 0], lw=1.5, label='1st')
    plt.plot(y[:, 0], 'ro')
    plt.legend(loc=0)
    plt.ylabel('value')
    plt.title('A Simple Plot')
    plt.subplot(212) ②
    plt.plot(y[:, 1], 'g', lw=1.5, label='2nd')
plt.plot(y[:, 1], 'ro')
plt.legend(loc=0)
plt.xlabel('index')
plt.ylabel('value');

① 定义上方的子图1。

② 定义下方的子图2。

图7-11 两个子图的绘图

在 matplotlib 图形对象中放置子图是通过使用特殊的坐标系完成的。plt.subplot() 接受三个整数作为参数:numrows(行数)、numcols(列数)和 fignum(子图编号),可以逗号分隔或不分隔。numrows 指定行数,numcols 指定列数,fignum 指定子图的编号,从1开始,到 numrows * numcols 结束。例如,一个有九个等大小子图的图形将具有 numrows=3、numcols=3 和 fignum=1,2,...,9。右下角的子图将具有以下"坐标":plt.subplot(3, 3, 9)。

有时,可能需要或希望选择两种不同的绘图类型来可视化此类数据。使用子图方法,可以自由组合 matplotlib 提供的任意类型的绘图。¹

图7-12 结合了线/点图和条形图:

In [23]: plt.figure(figsize=(10, 6))
    plt.subplot(121)
    plt.plot(y[:, 0], lw=1.5, label='1st')
    plt.plot(y[:, 0], 'ro')
    plt.legend(loc=0)
    plt.xlabel('index')
    plt.ylabel('value')
    plt.title('1st Data Set')
    plt.subplot(122)
    plt.bar(np.arange(len(y)), y[:, 1], width=0.5, color='g', label='2nd')
    plt.legend(loc=0)
    plt.xlabel('index')
    plt.title('2nd Data Set');

① 创建条形子图。

图7-12 结合线/点子图与条形子图的绘图

其他绘图样式#

在二维绘图方面,线图和点图可能是金融中最重要的绘图类型;这是因为许多数据集包含时间序列数据(time series data),这些数据通常通过此类图表可视化。第8章将详细讨论金融时间序列数据。然而,本节继续使用二维随机数数据集,并展示一些替代的、对金融应用有用的可视化方法。

首先是散点图(scatter plot),其中一个数据集的值作为另一个数据集的 x 值。图7-13 展示了这样的绘图。这种绘图类型可用于,例如,将一个金融时间序列的收益率(returns)与另一个金融时间序列的收益率作图。这个示例使用了一个新的包含更多数据的二维数据集:

In [24]: y = np.random.standard_normal((1000, 2)) ①
In [25]: plt.figure(figsize=(10, 6))
    plt.plot(y[:, 0], y[:, 1], 'ro') ②
    plt.xlabel('1st')
    plt.ylabel('2nd')
    plt.title('Scatter Plot');

① 创建一个更大的随机数数据集。

② 通过 plt.plot() 函数生成散点图。

图7-13 通过 plt.plot() 函数绘制的散点图

matplotlib 也提供了一个特定的函数来生成散点图。它的工作方式基本相同,但提供了一些额外的功能。图7-14 显示了与图7-13 对应的散点图,这次是使用 plt.scatter() 函数生成的:

In [26]: plt.figure(figsize=(10, 6))
plt.scatter(y[:, 0], y[:, 1], marker='o')
plt.xlabel('1st')
plt.ylabel('2nd')
plt.title('Scatter Plot');

① 通过 plt.scatter() 函数生成的散点图。

图7-14 通过 plt.scatter() 函数绘制的散点图

plt.scatter() 绘图函数还允许添加第三维度,可以通过不同颜色进行可视化,并使用颜色条(color bar)进行描述。图7-15 显示了一个散点图,其中第三维度通过各个点的不同颜色和作为颜色图例的颜色条来说明。为此,以下代码生成第三个随机数据集,这次由 0 到 10 之间的整数组成:

In [27]: c = np.random.randint(0, 10, len(y))
In [28]: plt.figure(figsize=(10, 6))
    plt.scatter(y[:, 0], y[:, 1],
    c=c,
    cmap='coolwarm',
    marker='o')
    plt.colorbar()
    plt.xlabel('1st')
    plt.ylabel('2nd')
    plt.title('Scatter Plot');

① 包含第三个数据集。

② 选择颜色映射。

③ 将标记定义为粗点。

图7-15 带第三维度的散点图

另一种常见的绘图类型——直方图(histogram),也常用于金融收益率分析。图7-16 将两个数据集的频率值并排显示在同一个图中:

In [29]: plt.figure(figsize=(10, 6))
plt.hist(y, label=['1st', '2nd'], bins=25)
plt.legend(loc=0)
plt.xlabel('value')
plt.ylabel('frequency')
plt.title('Histogram');

① 通过 plt.hist() 函数生成直方图。

图7-16 两个数据集的直方图

由于直方图是金融应用中如此重要的绘图类型,让我们更仔细地看看 plt.hist() 的使用。以下示例说明了其支持的参数:

plt.hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, hold=None, **kwargs)

表7-5 描述了 plt.hist() 函数的主要参数。

表7-5 plt.hist() 的参数

参数描述
x列表对象、ndarray 对象
bins区间数
range区间的下限和上限
normed归一化使得积分值为1
weightsx 中每个值的权重
cumulative每个区间包含较低区间的计数
histtype选项(字符串):bar, barstacked, step, stepfilled
align选项(字符串):left, mid, right
orientation选项(字符串):horizontal, vertical
rwidth条形的相对宽度
log对数刻度
color每个数据集的颜色(类似数组的)
label标签的字符串或字符串序列
stacked堆叠多个数据集

图7-17 显示了一个类似的绘图;这次,两个数据集的数据在直方图中堆叠:

In [30]: plt.figure(figsize=(10, 6))
plt.hist(y, label=['1st', '2nd'], color=['b', 'g'], stacked=True, bins=20, alpha=0.5)
plt.legend(loc=0)
plt.xlabel('value')
plt.ylabel('frequency')
plt.title('Histogram');

图7-17 两个数据集的堆叠直方图

另一个有用的绘图类型是箱线图(boxplot)。与直方图类似,箱线图既可以简洁地概述数据集的特征,也可以轻松地比较多个数据集。图7-18 显示了我们的数据集的箱线图:

In [31]: fig, ax = plt.subplots(figsize=(10, 6))
plt.boxplot(y)
plt.setp(ax, xticklabels=['1st', '2nd'])
plt.xlabel('data set')

plt.ylabel('value')

plt.title('Boxplot');

① 通过 plt.boxplot() 函数生成箱线图。

② 设置单独的 x 标签。

图7-18 两个数据集的箱线图

最后一个示例使用了函数 plt.setp(),它为一个(或一组)绘图实例设置属性。例如,考虑由以下代码生成的线图:

line = plt.plot(data, 'r')

以下代码将线条样式改为"虚线":

plt.setp(line, linestyle='--')

这样,在生成绘图实例("artist object")后,可以轻松地更改参数。

作为本节的最后演示,考虑一个数学启发的绘图,也可以在 matplotlib 画廊中找到。它绘制一个函数并以图形方式突出显示从下限到上限的函数下方的面积——换句话说,就是函数在下限和上限之间的积分值。要说明的积分(值)是 $\textstyle \int_{a} ^{b} f ( x ) d x$,其中 $f ( x ) = {\textstyle{\frac{1} {2}}} \cdot e^{x} + 1 , a = {\frac{1} {2}}$,且 $\begin{array} {r} {b = \frac{3} {2}} \end{array}$。图7-19 显示了结果图,并展示了 matplotlib 无缝处理 LaTeX 排版以在绘图中包含数学公式的能力。首先,函数定义、积分限作为变量,以及 x 和 y 值的数据集:

In [32]: def func(x):
    return 0.5 * np.exp(x) + 1
    a, b = 0.5, 1.5
    x = np.linspace(0, 2)
    y = func(x)
    Ix = np.linspace(a, b)
    Iy = func(Ix)
    verts = [(a, 0)] + list(zip(Ix, Iy)) + [(b, 0)]

① 函数定义。

② 积分上下限。

③ 用于绘制函数的 x 值。

④ 用于绘制函数的 y 值。

⑤ 积分限内的 x 值。

⑥ 积分限内的 y 值。

⑦ 包含多个元组对象的列表对象,表示要绘制的多边形的坐标。

其次,绘图本身由于要明确放置的众多独立对象而略显复杂:

In [33]: from matplotlib.patches import Polygon
fig, ax = plt.subplots(figsize=(10, 6))
plt.plot(x, y, 'b', linewidth=2) ①
plt.ylim(bottom=0) ②
poly = Polygon(verts, facecolor='0.7', edgecolor='0.5') ③
ax.add_patch(poly) ④
plt.text(0.5 * (a + b), 1, r'\$int_a^b f(x)\mathrm{d}x$', horizontalalignment='center', fontsize=20) ⑤
plt.figtext(0.9, 0.075, '$x') ⑥
plt.figtext(0.075, 0.9, '$f(x)') ⑦
ax.set_xticks((a, b)) ⑧
ax.set_xticklabels(('$a$', '$b'')) ⑨
ax.set_yticks([func(a), func(b)]) ⑩
ax.set_yticklabels(('$f(a)$", '$f(b)')); ⑪

① 将函数值绘制为蓝色线条。

② 定义纵轴的最小 y 值。

③ 以灰色绘制多边形(积分区域)。

④ 在绘图中放置积分公式。

⑤ 放置 x 轴标签。

⑥ 放置 x 标签。

⑦ 放置 y 标签。

图7-19 指数函数、积分区域和 LaTeX 标签

静态三维绘图#

在金融领域,真正能从三维可视化中受益的领域并不多。然而,一个应用领域是波动率曲面(volatility surface),它同时显示不同到期时间和执行价格的隐含波动率(implied volatility)。另见附录B,其中展示了欧式看涨期权的价值和 vega 曲面的可视化示例。下面,代码人工生成一个类似于波动率曲面的绘图。为此,考虑以下参数:

  • 执行价格(strike)值范围在50到150之间
  • 到期时间(time-to-maturity)范围在0.5到2.5年之间
In [39]: iv[:5, :3] ④
Out[39]: array([[1., 0.76695652, 0.58132045],
    [0.85185185, 0.65333333, 0.4951989 ],
    [0.74193548, 0.56903226, 0.43130227],
    [0.65714286, 0.504, 0.38201058],
    [0.58974359, 0.45230769, 0.34283001]])
In [35]: ttm = np.linspace(0.5, 2.5, 24)

这提供了一个二维坐标系。NumPy 的 np.meshgrid() 函数可以从两个一维 ndarray 对象生成这样的坐标系:

In [34]: strike = np.linspace(50, 150, 24)
In [36]: strike, ttm = np.meshgrid(strike, ttm) 3
In [37]: strike[:2].round(1) ③
Out[37]: array([[50., 54.3, 58.7, 63., 67.4, 71.7, 76.1, 80.4, 84.8, 89.1, 93.5, 97.8, 102.2, 106.5, 110.9, 115.2, 119.6, 123.9, 128.3, 132.6, 137., 141.3, 145.7, 150.], [50., 54.3, 58.7, 63., 67.4, 71.7, 76.1, 80.4, 84.8, 89.1, 93.5, 97.8, 102.2, 106.5, 110.9, 115.2, 119 .6 , 123 .9 , 128 .3 , 132 .6 , 137., 141 .3 , 145 .7 , 150.]])
In [38]: iv = (strike - 100) ** 2 / (100 * strike) / ttm

① 包含执行价格值的 ndarray 对象。

② 包含到期时间值的 ndarray 对象。

③ 创建的两个二维 ndarray 对象(网格)。

④ 虚拟的隐含波动率值。

以下代码生成的绘图如图7-20 所示:

In [40]: from mpl_toolkits.mplot3d import Axes3D ①
    fig = plt.figure(figsize=(10, 6))
    ax = fig.gca(projection='3d') ②
    surf = ax.plot_surface(strike, ttm, iv, rstride=2, cstride=2, cmap=plt.cm.coolwarm, linewidth=0.5, antialiased=True) ③
    ax.set_xlabel('strike') ④
    ax.set_ylabel('time-to-maturity') ⑤
    ax.set_zlabel('implied volatility') ⑥
    fig.colorbar(surf, shrink=0.5, aspect=5); ⑦

① 导入相关的三维绘图功能,尽管 Axes3D 没有直接使用,但这是必需的。

② 设置三维绘图的画布。

③ 创建三维绘图。

④ 设置 x 轴标签。

⑤ 设置 y 轴标签。

⑥ 设置 z 轴标签。

⑦ 创建颜色条。

图7-20 虚拟隐含波动率的三维曲面图

表7-6 描述了 plt.plot_surface() 函数可以接受的不同参数。

表7-6 plot_surface() 的参数

参数描述
X, Y, Z数据值,二维数组
rstride数组行步长(步进大小)
cstride数组列步长(步进大小)
color曲面片的颜色
cmap曲面片的颜色映射
facecolors各个面片的颜色
normNormalize 实例,用于将值映射到颜色
vmin要映射的最小值
vmax要映射的最大值
shade是否对面片颜色进行着色

与二维绘图一样,线条样式可以替换为单个点,或者如下面的三角形。图7-21 以三维散点图的形式绘制相同的数据,但这次使用了不同的视角,通过 view_init() 方法进行设置:

In [41]: fig = plt.figure(figsize=(10, 6))
    ax = fig.add_subplot(111, projection='3d')
    ax.view_init(30, 60)
    ax.scatter(strike, ttm, iv, zdir='z', s=25, c='b', marker='^') ①
    ax.set_xlabel('strike')
    ax.set_ylabel('time-to-maturity')
    ax.set_zlabel('implied volatility');

① 设置视角角度。

② 创建三维散点图。

图7-21 虚拟隐含波动率的三维散点图

交互式二维绘图#

matplotlib 允许创建静态位图对象或 PDF 格式的图表。如今,有许多库可以基于 D3.js 标准创建交互式图表。此类图表支持放大缩小、悬停效果以检查数据等。它们通常也可以轻松嵌入到网页中。

plotly 是一个流行的平台和绘图库。它专注于数据科学的可视化,在数据科学社区中广泛使用。plotly 的主要优点包括与 Python 生态系统的紧密集成以及易用性——特别是与 pandas DataFrame 对象和封装包 Cufflinks 结合使用时。

某些功能需要免费账户。获得凭据后,应将其存储在本地以供永久使用。有关详细信息,请参阅"Plotly for Python 入门"指南。

本节仅关注选定的方面,专门使用 Cufflinks 从存储在 DataFrame 对象中的数据创建交互式图表。

基本绘图#

要从 Jupyter Notebook 环境中开始,需要进行一些导入并启用笔记本模式:

In [42]: import pandas as pd

In [43]: import cufflinks as cf In [44]: import plotly.offline as plyo

In [45]: plyo.init_notebook_mode(connected=True)

① 导入 Cufflinks。

② 导入 plotly 的离线绘图功能。

③ 开启笔记本绘图模式。

远程或本地渲染#

使用 plotly,还可以选择在 plotly 服务器上渲染图表。然而,笔记本模式通常要快得多,特别是在处理较大数据集时。不过,某些功能(如 plotly 的流式绘图服务)只能通过与服务器通信来使用。

接下来的示例再次依赖伪随机数,这次存储在带有 DatetimeIndex 的 DataFrame 对象中(即作为时间序列数据):

In [46]: a = np.random.standard_normal((250, 5)).cumsum(axis=0)
In [47]: index = pd.date_range('2019-1-1', freq='B', periods=len(a))
In [48]: df = pd.DataFrame(100 + 5 * a, columns=list('abcde'), index=index)
In [49]: df.head( ) ⑧
Out[49]: a b c d e
2019-01-01109.03753598.693865104.47409496.878857100.6219362019-01-02107.59824297.005738106.78918997.966552100.1753132019-01-03101.639668100.332253103.18350099.747869107.9029012019-01-0498.500363101.208283100.96624294.023898104.3872562019-01-0793.941632103.319168105.67401295.89106286.547934

① 标准正态分布的伪随机数。

② DatetimeIndex 对象的起始日期。

③ 频率("business daily",工作日)。

④ 需要的期数。

⑤ 原始数据的线性变换。

⑥ 列标题为单个字符。

⑦ DatetimeIndex 对象。

⑧ 数据的前五行。

Cufflinks 为 DataFrame 类添加了一个新方法:df.iplot()。该方法在后端使用 plotly 来创建交互式图表。本节中的代码示例都使用了将交互式图表下载为静态位图的选项,然后将其嵌入到文本中。在 Jupyter Notebook 环境中,创建的图表都是交互式的。以下代码的结果如图7-22 所示:

In [50]: plyo.iplot( ①
    df.iplot(asFigure=True), ②
    image='png', ③
    filename='ply_01' ④
)

① 使用 plotly 的离线(笔记本模式)功能。

② 调用 df.iplot() 方法,参数 asFigure=True 以允许本地绘图和嵌入。

③ 图像选项额外提供图表的静态位图版本。

④ 指定要保存的位图文件名(文件扩展名自动添加)。

图7-22 使用 plotly、pandas 和 Cufflinks 绘制的时间序列数据线图

与一般的 matplotlib 和 pandas 绘图功能一样,有多个参数可用于自定义此类图表(见图7-23):

In [51]: plyo.iplot(
    df[['a', 'b']].iplot(asFigure=True,
    theme='polar', ①
    title='A Time Series Plot', ②
    xTitle='date', ③
    yTitle='value', ④
    mode={'a': 'markers', 'b': 'lines+markers'}, ⑤
    symbol={'a': 'circle', 'b': 'diamond'}, ⑥
    size=3.5, ⑦
    colors={'a': 'blue', 'b': 'magenta'}, ⑧},
    image='png',
filename='ply_02'

① 为图表选择主题(绘图样式)。

② 添加标题。

③ 添加 x 轴标签。

④ 添加 y 轴标签。

⑤ 按列定义绘图模式(线条、标记等)。

⑥ 按列定义用作标记的符号。

⑦ 固定所有标记的大小。

⑧ 按列指定绘图颜色。

图7-23 DataFrame 对象两列数据的线图(带自定义设置)

与 matplotlib 类似,plotly 支持多种不同的绘图类型。通过 Cufflinks 可用的绘图类型包括:chart、scatter、bar、box、spread、ratio、heat map、surface、histogram、bubble、bubble3d、scatter3d、scattergeo、ohlc、candle、pie 和 choropleth。作为不同于线图的绘图类型示例,考虑直方图(见图7-24):

In [52]: plyo.iplot(
    df.iplot(kind='hist', ①
    subplots=True, ②
    bins=15, ③
    asFigure=True),
    image='png',
    filename='ply_03'
)

① 指定绘图类型。

② 要求为每一列创建独立的子图。

③ 设置 bins 参数(要使用的桶数 = 要绘制的条形数)。

图7-24 DataFrame 对象每列的直方图

金融图表#

plotly、Cufflinks 和 pandas 的组合在处理金融时间序列数据时特别强大。Cufflinks 提供了专门的功能来创建典型的金融图表并添加典型的金融图表元素,例如相对强弱指数(Relative Strength Index, RSI)。为此,创建一个持久的 QuantFig 对象,其绘图方式与带有 Cufflinks 的 DataFrame 对象相同。

本小节使用一个真实的金融数据集——欧元/美元(EUR/USD)汇率的时间序列数据(来源:FXCM Forex Capital Markets Ltd.):

In [54]: raw = pd.read_csv('../source/fxcm_eur_usd_eod_data.csv', index_col=0, parse_dates=True) ①
In [55]: raw.info() ②
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1547 entries, 2013-01-0122:00:00 to 2017-12-3122:00:00
Data columns (total 8 columns):
BidOpen 1547 non-null float64
BidHigh 1547 non-null float64
BidLow 1547 non-null float64
BidClose 1547 non-null float64
AskOpen 1547 non-null float64
AskHigh 1547 non-null float64
AskLow 1547 non-null float64
AskClose 1547 non-null float64
dtypes: float64(8)
memory usage: 108.8 KB
In [56]: quotes = raw[['AskOpen', 'AskHigh', 'AskLow', 'AskClose']] ③
quotes = quotes.iloc[-60:] ④
quotes.tail() ⑤
Out[56]:
AskOpen AskHigh AskLow AskClose
2017-12-2522:00:001.186671.187911.184671.185872017-12-2622:00:001.185871.191041.185521.188852017-12-2722:00:001.188851.195921.188851.194262017-12-2822:00:001.194261.202561.193691.200922017-12-3122:00:001.200921.201441.199941.20144

① 从 CSV 文件读取金融数据。

② 生成的 DataFrame 对象包含多列和超过1500行数据。

③ 从 DataFrame 对象中选择四列(开盘-最高-最低-收盘,即 OHLC)。

④ 仅使用有限的数据行进行可视化。

⑤ 返回结果数据集 quotes 的最后五行。

在实例化期间,QuantFig 对象将 DataFrame 对象作为输入,并允许一些基本的自定义设置。然后通过 qf.iplot() 方法绘制存储在 QuantFig 对象 qf 中的数据(见图7-25):

In [57]: qf = cf.QuantFig(
    quotes, ①
    title='EUR/USD Exchange Rate', ②
    legend='top', ③
    name='EUR/USD' ④
)

In [58]: plyo.iplot(
    qf.iplot(asFigure=True),
    image='png',
    filename='qf_01'
)

① 将 DataFrame 对象传递给 QuantFig 构造函数。

② 添加图表标题。

③ 图例放置在图表顶部。

④ 为数据集命名。

图7-25 EUR/USD 数据的 OHLC 图

添加典型的金融图表元素,如布林带(Bollinger Bands),可以通过 QuantFig 对象的不同方法来实现(见图7-26):

In [59]: qf.add_bollinger_bands(periods=15, boll_std=2)
In [60]: plyo.iplot(qf.iplot(asFigure=True), image='png', filename='qf_02')

① 布林带的周期数。

② 用于带带宽度的标准差数量。

图7-26 带布林带的 EUR/USD 数据 OHLC 图

某些金融指标,如 RSI,可以添加为子图(见图7-27):

In [61]: qf.add_rsi(periods=14, ① showbands=False) ②
In [62]: plyo.iplot( qf.iplot(asFigure=True),

① 设置 RSI 周期。

② 不显示上轨或下轨。

图7-27 带布林带和 RSI 的 EUR/USD 数据 OHLC 图

结论#

matplotlib 可以被视为 Python 数据可视化的基准和全能选手。它与 NumPy 和 pandas 紧密集成,基本功能可以方便地访问。然而,matplotlib 是一个功能强大的库,其 API 有些复杂。这使得本章无法全面介绍 matplotlib 的所有功能。

本章介绍了 matplotlib 在二维和三维绘图方面的基本功能,这些功能在许多金融环境中都很有用。其他章节提供了更多关于如何使用该包进行可视化的示例。

此外,本章还介绍了 plotly 与 Cufflinks 的组合。这种组合使得创建交互式 D3.js 图表变得非常方便,因为通常只需要在 DataFrame 对象上调用一个方法。所有技术细节都在后端处理。此外,Cufflinks 通过 QuantFig 对象提供了一种创建带有流行金融指标的典型金融图表的简便方法。

延伸资源#

matplotlib 的各种资源可以在网上找到,包括:

  • 主页,可能是最好的起点
  • 包含许多有用示例的画廊
  • 二维绘图教程
  • 三维绘图教程

咨询画廊、寻找合适的可视化示例并从相应的示例代码开始,已经成为一种标准做法。

plotly 和 Cufflinks 包的主要资源也在网上。包括:

  • plotly 主页
  • Plotly for Python 入门教程
  • Cufflinks GitHub 页面