Matplotlib 进阶


主讲人:李显祥

大气科学学院

from matplotlib import pyplot as plt
import numpy as np
%matplotlib inline
# 创建一些示例数据
import numpy as np
x = np.linspace(-np.pi, np.pi, 100)
y = np.cos(x)
z = np.sin(6*x)

Matplotlib 的几种调用方法

参考 https://matplotlib.org/faq/usage_faq.html#coding-styles:

  1. pyplot 方法

 import matplotlib.pyplot as plt
 plt.plot(x,y)
  1. 面向对象方法 (Object oriented)

 import matplotlib.pyplot as plt
 fig,ax = plt.subplots()
 ax.plot(x,y)
  1. 嵌入图形界面的调用方法

plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f91e4537110>]
../_images/Matplotlib_advanced_5_1.png

Figure 和 Axes

Figurematplotlib 对象架构中的最高一级。 它可以用来直接显式创建一幅图。

fig = plt.figure() 
<Figure size 432x288 with 0 Axes>
fig = plt.figure(figsize=(13, 5))
<Figure size 936x360 with 0 Axes>

Axes 是次一级的对象,它需要在 Figure 上创建。

fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
../_images/Matplotlib_advanced_12_0.png

我们可以指定 Axes 占据 Figure 的多少空间 (左下角 x 坐标, 左下角 y 坐标, 宽度,高度)

fig = plt.figure()
ax = fig.add_axes([0, 0, 0.5, 1.0])
../_images/Matplotlib_advanced_14_0.png
fig = plt.figure()
ax1 = fig.add_axes([0, 0, 0.5, 1])
ax2 = fig.add_axes([0.6, 0, 0.3, 0.5], facecolor='g')
../_images/Matplotlib_advanced_15_0.png
fig = plt.figure()
ax1 = fig.add_axes([0.1,0.1,0.8,0.8])
ax1.tick_params(left=False, bottom=False, labelbottom=False, labelleft=False)
ax2 = fig.add_axes([0.2,0.2,0.3,0.3])
ax2.tick_params(left=False, bottom=False, labelbottom=False, labelleft=False)
../_images/Matplotlib_advanced_16_0.png
fig = plt.figure()
for i in range(4):
    ax = fig.add_axes([0.1*(i+1),0.1*(i+1),0.5,0.5])
    ax.tick_params(left=False, bottom=False,labelbottom=False, labelleft=False)
../_images/Matplotlib_advanced_17_0.png

Subplots

plt.subplots 可以一次性创建多个 Axes

fig = plt.figure()
axes = fig.subplots(nrows=2, ncols=3)
../_images/Matplotlib_advanced_20_0.png
fig = plt.figure(figsize=(12, 6))
axes = fig.subplots(nrows=2, ncols=3)
../_images/Matplotlib_advanced_21_0.png
fig = plt.figure()
axes = fig.subplots(nrows=2, ncols=3, sharex=True, sharey=True)
../_images/Matplotlib_advanced_22_0.png
axes
array([[<AxesSubplot:>, <AxesSubplot:>, <AxesSubplot:>],
       [<AxesSubplot:>, <AxesSubplot:>, <AxesSubplot:>]], dtype=object)

我们可以同时创建 figureAxes

这也是我们推荐的画图方式!

fig, ax = plt.subplots()
../_images/Matplotlib_advanced_25_0.png
ax
<AxesSubplot:>
fig, axes = plt.subplots(ncols=2, figsize=(8, 4), subplot_kw={'facecolor': 'g'})
../_images/Matplotlib_advanced_27_0.png
axes
array([<AxesSubplot:>, <AxesSubplot:>], dtype=object)

Axes 中绘图

所有的绘图操作都发生在 Axes 对象上. 如果你采用面向对象的方式来调用 matploblib,事情就会变得很容易理解.

fig, ax = plt.subplots()
ax.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f91e6343650>]
../_images/Matplotlib_advanced_31_1.png

我们也可以用 pyplot 来画出同样的图 (见前面的示例)。

但是当我们要画多个子图的时候,面向对象的方法就显示出威力了。

#fig, axes = plt.subplots(figsize=(10, 4), ncols=2)
#ax0, ax1 = axes
fig,[ax0,ax1] = plt.subplots(figsize=(10, 4), ncols=2)
ax0.plot(x, y)
ax1.plot(x, z)
[<matplotlib.lines.Line2D at 0x7f91e6435590>]
../_images/Matplotlib_advanced_33_1.png

坐标轴标注 Labels

fig, (ax0, ax1) = plt.subplots(figsize=(9, 4), ncols=2)

ax0.plot(x, y)
ax0.set_xlabel('x')
ax0.set_ylabel('y')
ax0.set_title('x vs. y')

ax1.plot(x, z)
ax1.set_xlabel('x')
ax1.set_ylabel('z')
ax1.set_title('x vs. z')

# squeeze everything in
plt.tight_layout()
../_images/Matplotlib_advanced_35_0.png

线型

fig, axes = plt.subplots(figsize=(20, 5), ncols=4)
axes[0].plot(x, y, linestyle='-')
axes[0].plot(x, z, linestyle='solid')

axes[1].plot(x, y, linestyle='dashed')
axes[1].plot(x, z, linestyle='--')

axes[2].plot(x, y, linestyle='dotted')
axes[2].plot(x, z, linestyle=':')

axes[3].plot(x, y, linestyle='dashdot', linewidth=5)
axes[3].plot(x, z, linestyle='-.', linewidth=0.5)
[<matplotlib.lines.Line2D at 0x7f91e4a7ac90>]
../_images/Matplotlib_advanced_37_1.png

颜色

根据 colors documentation,有一常用些颜色有专门的名字:

  • b: blue

  • g: green

  • r: red

  • c: cyan

  • m: magenta

  • y: yellow

  • k: black

  • w: white

fig, ax = plt.subplots()
ax.plot(x, y, color='k')
ax.plot(x, z, color='r')
[<matplotlib.lines.Line2D at 0x7f91e4ad07d0>]
../_images/Matplotlib_advanced_40_1.png

指定颜色的其它方式:

fig, axes = plt.subplots(figsize=(16, 5), ncols=3)

# grayscale
axes[0].plot(x, y, color='0.8')
axes[0].plot(x, z, color='0.2')

# RGB tuple
axes[1].plot(x, y, color=(1, 0, 0.7))
axes[1].plot(x, z, color=(0, 0.4, 0.3))

# HTML hex code
axes[2].plot(x, y, color='#00dcba')
axes[2].plot(x, z, color='#b029ee') 
[<matplotlib.lines.Line2D at 0x7f91e4fb7290>]
../_images/Matplotlib_advanced_42_1.png

如果不指定颜色,matplotlib 内部会循环使用默认的颜色列表:

plt.rcParams['axes.prop_cycle']
'color'
'#1f77b4'
'#ff7f0e'
'#2ca02c'
'#d62728'
'#9467bd'
'#8c564b'
'#e377c2'
'#7f7f7f'
'#bcbd22'
'#17becf'
fig, ax = plt.subplots(figsize=(12, 10))
for factor in np.linspace(0.2, 1, 11):
    ax.plot(x, factor*y)
../_images/Matplotlib_advanced_45_0.png

Markers

matplolib 里有很多不同的 markers 供选择!

fig, axes = plt.subplots(figsize=(12, 5), ncols=2)

axes[0].plot(x[:20], y[:20], marker='.')
axes[0].plot(x[:20], z[:20], marker='o')

axes[1].plot(x[:20], z[:20], marker='^',
             markersize=10, markerfacecolor='r',
             markeredgecolor='k')
[<matplotlib.lines.Line2D at 0x7f91e78f2350>]
../_images/Matplotlib_advanced_48_1.png

标注、刻度和网格 Label, Ticks, and Gridlines

ax.set_xlabel()        ax.set_ylabel()

ax.set_xticks()        ax.set_yticks() 

ax.set_xticklabels()   ax.set_yticklabels()
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, y)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title(r'A complicated math function: $f(x) = \cos(x)$')

ax.set_xticks(np.pi * np.array([-1, -0.5, 0, 0.5, 1]))
ax.set_xticklabels([r'$-\pi$', r'$-\frac{\pi}{2}$', '0', r'$\frac{\pi}{2}$', r'$\pi$'])
ax.set_yticks([-1, 0, 1])

ax.set_yticks(np.arange(-1, 1.1, 0.2), minor=True)
#ax.set_xticks(np.arange(-3, 3.1, 0.2), minor=True)

ax.grid(which='minor', linestyle='--')
ax.grid(which='major', linewidth=2)
../_images/Matplotlib_advanced_51_0.png

Axes.tick_params 可以用来调整坐标轴(包括刻度、标注)的属性,其参数包括

  • axis {‘x’, ‘y’, ‘both’} 指定操作的坐标轴; 默认值 ‘both’.

  • reset 布尔值, 默认值: False 如果是 True, 先将所有参数值重置为默认值,然后再处理其它参数.

  • which {‘major’, ‘minor’, ‘both’} 默认值 ‘major’; 指定是操作主刻度还是次刻度.

  • direction {‘in’, ‘out’, ‘inout’} 刻度的方向:朝内、朝外或者同时朝内外.

  • length 浮点数: 刻度的长度(单位:points).

  • width 浮点数: 刻度的宽度(单位:points).

  • color,colors 刻度的颜色

  • pad 浮点数: 刻度和标注之间的距离(单位:points).

  • labelsize, labelcolor:标注的大小,颜色,

  • bottom, top, left, right 布尔值: 是否绘制相应的刻度.

  • labelbottom, labeltop, labelleft, labelright 布尔值:是否绘制相应的标注

  • labelrotation 浮点数:标注的旋转度数(逆时针方向为正)

  • zorder:绘图元素的层数,数值越大,绘制的顺序越靠后(不容易被其它元素遮挡)

import pandas as pd

dates = pd.date_range('2020-03-01','2020-03-03',freq='H')
temp = 15+np.random.rand(len(dates))*10

fig,ax=plt.subplots()
ax.plot(dates,temp,color='b')

ax.tick_params(axis='x',labelrotation=45)
ax.tick_params(axis='y',color='b',labelcolor='b')
ax.spines['left'].set_color('b')
../_images/Matplotlib_advanced_53_0.png

Spines

Axes 有四个 spines (即连接 ticks的直线),分别为 'left', 'right', 'bottom''top', 存放在 Axes.spines 这个有序字典(OrderedDict)里。

我们可以改变这些 spines 的属性,包括颜色和位置。

X = np.linspace(-np.pi, np.pi, 256,endpoint=True)
C,S = np.cos(X), np.sin(X)

fig,ax=plt.subplots(figsize=(10,6))
ax.plot(X, C, color="blue", linewidth=1.0, linestyle="-")
ax.plot(X, S, color="green", linewidth=1.0, linestyle="-")
ax.set_xlim(-4.0,4.0)
#ax.set_xticks(np.linspace(-4,4,9,endpoint=True))
ax.set_ylim(-1.10,1.10)
ax.set_xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi])
ax.set_xticklabels([r'$-\pi$', r'$-\pi/2$', r'$0$', r'$+\pi/2$', r'$+\pi$'])
ax.set_yticks([-1, 0, +1])
ax.set_yticklabels([r'$-1$', r'$0$', r'$+1$'])
ax.tick_params(axis='both',labelsize=16)

ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.xaxis.set_ticks_position('bottom')
ax.spines['bottom'].set_position(('data',0))
ax.yaxis.set_ticks_position('left')
ax.spines['left'].set_position(('data',0))

ax.plot(1, 0, ">k", transform=ax.get_yaxis_transform(), clip_on=False)
ax.plot(0, 1, "^k", transform=ax.get_xaxis_transform(), clip_on=False)
[<matplotlib.lines.Line2D at 0x7f91ea1ef290>]
../_images/Matplotlib_advanced_55_1.png
# https://matplotlib.org/3.3.3/gallery/ticks_and_spines/spines_bounds.html
# Fixing random state for reproducibility
np.random.seed(19680801)

x = np.linspace(0, 2*np.pi, 50)
y = np.sin(x)
y2 = y + 0.1 * np.random.normal(size=x.shape)

fig, ax = plt.subplots()
ax.plot(x, y)
ax.plot(x, y2)

# set ticks and tick labels
ax.set_xlim((0, 2*np.pi))
ax.set_xticks([0, np.pi, 2*np.pi])
ax.set_xticklabels(['0', r'$\pi$', r'2$\pi$'])
ax.set_ylim((-1.5, 1.5))
ax.set_yticks([-1, 0, 1])

# Only draw spine between the y-ticks
ax.spines['left'].set_bounds((-1, 1))
# Hide the right and top spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
../_images/Matplotlib_advanced_56_0.png

fill_between

x = np.arange(-5, 5, 0.01)
y1 = -5*x*x + x + 10
y2 = 5*x*x + x

fig, ax = plt.subplots()
ax.plot(x, y1, x, y2, color='black')
ax.fill_between(x, y1, y2, where=y2>y1, facecolor='yellow', alpha=0.5)
ax.fill_between(x, y1, y2, where=y2<=y1, facecolor='red', alpha=0.5)
ax.set_title('Fill Between')
Text(0.5, 1.0, 'Fill Between')
../_images/Matplotlib_advanced_58_1.png
x = np.arange(0.0, 2, 0.01)
y1 = np.sin(2 * np.pi * x)
y2 = 1.2 * np.sin(4 * np.pi * x)

fig,ax = plt.subplots(2,2,sharex=True,sharey=True,figsize=(9, 8))
ax[0,0].fill_between(x,0,y1)
ax[0,0].axhline(0,color='grey',alpha=0.5,lw=1)
ax[0,0].set_title('Between y1 and 0')

ax[0,1].fill_between(x,0,y1,where=y1>0,facecolor='r')
ax[0,1].fill_between(x,0,y1,where=y1<0,facecolor='b')
ax[0,1].axhline(0,color='grey',alpha=0.5,lw=1)
ax[0,1].set_title('Between y1 and 0 with different colors')

ax[1,0].fill_between(x,y1,1)
ax[1,0].axhline(0,color='grey',alpha=0.5,lw=1)
ax[1,0].set_title('Between y1 and 1')

ax[1,1].fill_between(x,y1,y2,where=y1>y2,color='b')
ax[1,1].fill_between(x,y1,y2,where=y1<y2,color='r')
ax[1,1].axhline(0,color='grey',alpha=0.5,lw=1)
ax[1,1].set_title('Between y1 and y2')
Text(0.5, 1.0, 'Between y1 and y2')
../_images/Matplotlib_advanced_59_1.png
  • fill_between 是对于 y 轴数据进行填充

  • 对应的也有 fill_betweenx 来对 x 轴数据进行填充。

y = np.arange(0.0, 2, 0.01)
x1 = np.sin(2 * np.pi * y)
x2 = 1.2 * np.sin(4 * np.pi * y)

fig, [ax1, ax2, ax3] = plt.subplots(1, 3, sharey=True, figsize=(10, 6))
ax1.plot(x1,y)
ax1.fill_betweenx(y, 0, x1)
ax1.set_title('between (x1, 0)')
ax2.plot(x1,y)
ax2.fill_betweenx(y, x1, 1)
ax2.set_title('between (x1, 1)')
ax2.set_xlabel('x')
ax3.plot(x1,y,x2,y)
ax3.fill_betweenx(y, x1, x2,where=x1<x2,color='r')
ax3.fill_betweenx(y, x1, x2,where=x1>x2,color='b')
ax3.set_title('between (x1, x2)')
Text(0.5, 1.0, 'between (x1, x2)')
../_images/Matplotlib_advanced_61_1.png

填充整个坐标轴

transform 参数:参照系的选择

  • ax.get_xaxis_transform(): 对 y 轴使用坐标轴参照系,0 表示 bottom, 1 表示 top

  • ax.get_yaxis_transform(): 对 x 轴使用坐标轴参照系,0 表示 left, 1 表示 right

比如,我们对晚上的时间用灰色来填充。

fig,ax=plt.subplots()
ax.plot(dates,temp)
ax.set_ylim([14,25])
ax.tick_params(axis='x',labelrotation=45)
ax.fill_between(dates,0,1,where=(dates.hour<=7)|(dates.hour>=19),
               color='grey',alpha=0.2, transform=ax.get_xaxis_transform())
<matplotlib.collections.PolyCollection at 0x7f91ea1e6ed0>
../_images/Matplotlib_advanced_64_1.png

Matplotlib styles

import matplotlib.pyplot as plt

plt.style.use('ggplot')

# 或者临时改变 style
with plt.style.context('ggplot'):
    fig, ax = plt.subplots()

Matplotlib style sheets reference

总有一款适合你!

def plot_fig(style):  
  x = np.linspace(0, 10)

  # Fixing random state for reproducibility
  np.random.seed(19680801)
  
  fig, ax = plt.subplots()

  ax.plot(x, np.sin(x) + x + np.random.randn(50))
  ax.plot(x, np.sin(x) + 0.5 * x + np.random.randn(50))
  ax.plot(x, np.sin(x) + 2 * x + np.random.randn(50))
  ax.plot(x, np.sin(x) - 0.5 * x + np.random.randn(50))
  ax.plot(x, np.sin(x) - 2 * x + np.random.randn(50))
  ax.plot(x, np.sin(x) + np.random.randn(50))
  ax.set_title(style)
    
for s in ['seaborn-darkgrid','seaborn-whitegrid','ggplot']:
    with plt.style.context(s):
        plot_fig(s)
../_images/Matplotlib_advanced_68_0.png ../_images/Matplotlib_advanced_68_1.png ../_images/Matplotlib_advanced_68_2.png

图例 Legend

图例一般由以下部分组成

  • legend entry

A legend is made up of one or more legend entries. An entry is made up of exactly one key and one label.

  • legend key

The colored/patterned marker to the left of each legend label.

  • legend label

The text which describes the handle represented by the key.

  • legend handle

The original object which is used to generate an appropriate entry in the legend.

plt.legend, fig.legend

plt.legend()                     ax.legend()
plt.legend(labels)               ax.legend(labels)
plt.legend(handles, labels)      ax.legend(handles, labels)
  • 如果不传递任何参数,则根据之前 plot 生成的对象的 label 来产生图例

  • 图例的放置位置:loc 参数,默认为 best,即自动选择最优位置(不保证是最优的!)。

  • 其它位置为

    位置

    代号

    ‘best’

    0

    ‘upper right’

    1

    ‘upper left’

    2

    ‘lower left’

    3

    ‘lower right’

    4

    ‘right’

    5

    ‘center left’

    6

    ‘center right’

    7

    ‘lower center’

    8

    ‘upper center’

    9

    ‘center’

    10

  • 以下划线开头的 label 将被legend 自动排除

fig, ax = plt.subplots()
line_up, = ax.plot([1, 2, 3], label='Line 2')
line_down, = ax.plot([3, 2, 1], label='Line 1')
ax.legend(frameon=False)
<matplotlib.legend.Legend at 0x7f91eb013610>
../_images/Matplotlib_advanced_73_1.png
fig, ax = plt.subplots()
line_up, = ax.plot([1, 2, 3])
line_down, = ax.plot([3, 2, 1])
# plt.legend(handles, labels)
ax.legend([line_down, line_up], ['Line 1', 'Line 2'], loc='upper center', frameon=False) 
<matplotlib.legend.Legend at 0x7f91ea7daad0>
../_images/Matplotlib_advanced_74_1.png

我们也可以用 bbox_to_anchor 来指定 legend 位置,其形式为 [x,y,width,height], 前两个为相对于 ax 的位置,后两个为其大小(可省略)。

# Create plots with pre-defined labels.
fig, ax = plt.subplots()
ax.plot(x, y1, 'k--', label='Model length')
ax.plot(x, y2, 'k:', label='Data length')
ax.plot(x, y1 + y2, 'k', label='Total message length')

legend = ax.legend(bbox_to_anchor=(1.01,0.6), shadow=True, fontsize='x-large')

# Put a nicer background color on the legend.
legend.get_frame().set_facecolor('C6')
../_images/Matplotlib_advanced_76_0.png

插图 Inset

很多时候我们需要放置一张小图来表现一些细节(比如中国地图的南海小图),这时我们需要一个插图 (inset)。

可以用 fig.add_axes 来实现,其第一个参数为四个 float 组成的 list,分别表示插图的位置和大小 [x,y,width,height]

np.random.seed(19680801)
# create some data to use for the plot
dt = 0.001
t = np.arange(0.0, 10.0, dt)
r = np.exp(-t[:1000] / 0.05)  # impulse response
x = np.random.randn(len(t))
s = np.convolve(x, r)[:len(x)] * dt  # colored noise

fig, main_ax = plt.subplots()
main_ax.plot(t, s)
main_ax.set_xlim(0, 1)
main_ax.set_ylim(1.1 * np.min(s), 2 * np.max(s))
main_ax.set_xlabel('time (s)')
main_ax.set_ylabel('current (nA)')
main_ax.set_title('Gaussian colored noise')

# this is an inset axes over the main axes
right_inset_ax = fig.add_axes([.65, .6, .2, .2], facecolor='k')
right_inset_ax.hist(s, 400, density=True)
right_inset_ax.set_title('Probability')
right_inset_ax.set_xticks([])
right_inset_ax.set_yticks([])

# this is another inset axes over the main axes
left_inset_ax = fig.add_axes([.2, .6, .2, .2], facecolor='k')
left_inset_ax.plot(t[:len(r)], r)
left_inset_ax.set_title('Impulse response')
left_inset_ax.set_xlim(0, 0.2)
left_inset_ax.set_xticks([])
left_inset_ax.set_yticks([])
[]
../_images/Matplotlib_advanced_80_1.png

xy

第一种情况

两个坐标轴绘制同一个数据,但显示不同的坐标数据,比如一个显示角度,一个显示弧度,或一个显示摄氏度,一个显示华氏度。

我们可以用 axes.Axes.secondary_xaxisaxes.Axes.secondary_yaxis 来实现这个效果.

我们需要提供一个 正向 和一个 反向 转换函数作为参数。

import datetime
import matplotlib.dates as mdates
from matplotlib.transforms import Transform
from matplotlib.ticker import AutoLocator, AutoMinorLocator

fig, ax = plt.subplots(constrained_layout=True)
dx = np.arange(0, 360, 1)
dy = np.sin(2 * dx * np.pi / 180)
ax.plot(dx, dy)
ax.set_xlabel('angle [degrees]') 
ax.set_ylabel('signal')
ax.set_title('Sine wave')
ax.axhline(0, color='grey', linewidth=1)

def deg2rad(x):
    return x * np.pi / 180

def rad2deg(x):
    return x * 180 / np.pi

#xticks = [0, 0.5*np.pi, np.pi, 1.5*np.pi, 2*np.pi]
#labels = ['0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$']
secax = ax.secondary_xaxis('top', functions=(deg2rad, rad2deg)) #, xticks = xticks, xticklabels = labels)
secax.set_xlabel('angle [rad]')
Text(0.5, 0, 'angle [rad]')
../_images/Matplotlib_advanced_83_1.png
fig, ax = plt.subplots(constrained_layout=True)
dx = np.arange(0.02, 1, 0.02)
np.random.seed(196808)
dy = np.random.randn(len(dx)) ** 2
ax.loglog(dx, dy)
ax.set_xlabel('f [Hz]')
ax.set_ylabel('PSD')
ax.set_title('Random spectrum')

def forward(x):
    return 1 / x

def inverse(x):
    return 1 / x

secax = ax.secondary_xaxis('top', functions=(forward, inverse))
secax.set_xlabel('period [s]') 
Text(0.5, 0, 'period [s]')
/opt/miniconda3/lib/python3.7/site-packages/ipykernel_launcher.py:14: RuntimeWarning: divide by zero encountered in true_divide
  
../_images/Matplotlib_advanced_84_2.png
dates = [datetime.datetime(2018, 1, 1) + datetime.timedelta(hours=k * 6) for k in range(240)]
temperature = np.random.randn(len(dates))
fig, ax = plt.subplots(constrained_layout=True)

ax.plot(dates, temperature)
ax.set_ylabel(r'$T\ [^oC]$')
#plt.xticks(rotation=70)
#labels = ax.get_xticklabels()
#plt.setp(labels,rotation=70)
ax.tick_params('x',labelrotation=45)

def date2yday(x):
    """Convert matplotlib datenum to days since 2018-01-01."""
    y = x - mdates.date2num(datetime.datetime(2018, 1, 1))
    return y

def yday2date(x):
    """Return a matplotlib datenum for *x* days after 2018-01-01."""
    y = x + mdates.date2num(datetime.datetime(2018, 1, 1))
    return y

secaxx = ax.secondary_xaxis('top', functions=(date2yday, yday2date))
secaxx.set_xlabel('yday [2018]')

def CtoF(x):
    return x * 1.8 + 32

def FtoC(x):
    return (x - 32) / 1.8

secaxy = ax.secondary_yaxis('right', functions=(CtoF, FtoC))
secaxy.set_ylabel(r'$T\ [^oF]$')
Text(0, 0.5, '$T\\ [^oF]$')
../_images/Matplotlib_advanced_85_1.png

第二种情况

两个坐标轴来绘制不同的数据

  • axis.Axes.twinx: 两个 y 轴 (共享 x 轴)

  • axis.Axes.twiny:两个 x 轴 (共享 y 轴)

这两个函数可以调用多次,以生成更多的 xy 轴,但是注意它们会叠加在一起,需要挪动它们的位置来同时显示它们。

t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)

fig, ax1 = plt.subplots()

color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)

ax2 = ax1.twinx()  # instantiate a second axes that shares the same x-axis

color = 'tab:blue'
ax2.set_ylabel('sin', color=color)  # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)

fig.tight_layout()  # otherwise the right y-label is slightly clipped
../_images/Matplotlib_advanced_87_0.png
fig,ax1 = plt.subplots()
ax2 = ax1.twinx()   # 第二个 y 轴
ax3 = ax1.twinx()   # 第三个 y 轴

ax3.spines['right'].set_position(('outward',60))
ax3.spines['right'].set_color('red')
ax3.tick_params(axis='y',color='red',labelcolor='red')
../_images/Matplotlib_advanced_88_0.png

时间坐标轴

插入标注文本 Annotations

  • ax.text

  • ax.annotate

  • transform 参数:参照系的选择 ax.transData,ax.transAxes

fig, ax = plt.subplots()

x = np.linspace(-np.pi, np.pi, 100)
y = np.cos(x)

ax.plot(x, y)
ax.text(-3, 0.3, 'hello world')
ax.annotate('the maximum', xy=(0, 1),
             xytext=(0, 0), arrowprops={'facecolor': 'k'})
Text(0, 0, 'the maximum')
../_images/Matplotlib_advanced_92_1.png
fig, ax = plt.subplots()

x = np.linspace(-np.pi, np.pi, 100)
y = np.cos(x)

ax.plot(x, y)
ax.text(0.01, 0.95, 'hello world', transform=ax.transAxes) # 注意参照系的选择 ax.transAxes
ax.annotate('the maximum', xy=(0, 1),
             xytext=(1.2, 0.95), arrowprops={'facecolor': 'k','arrowstyle': '->'})
Text(1.2, 0.95, 'the maximum')
../_images/Matplotlib_advanced_93_1.png

不同 subplot 间绘制辅助线

  • matplotlib.patches.ConnectionPath 可以在不同子图间绘制连接线

  • 同样需要选择不同的参照系

    • ‘data’

    • ‘axes fraction’: 左下角(0,0),右上角(1,1)

from matplotlib.patches import ConnectionPatch

fig = plt.figure(figsize=(10,5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)

np.random.seed(12345)
datax, datay = np.random.rand(100),np.random.rand(100)

ax1.plot(datax,datay,'ko')
ax2.plot(datax,datay,'ko')

xy=(datax[13],datay[13])
con1 = ConnectionPatch(xyA=xy, xyB=(0,0),coordsA="data", coordsB="axes fraction",
                      arrowstyle='->', axesA=ax1, axesB=ax2, color='red')
con2 = ConnectionPatch(xyA=xy, xyB=(0,1),coordsA="data", coordsB="axes fraction",
                      axesA=ax1, axesB=ax2, color='red')

ax2.add_artist(con1)
ax2.add_artist(con2)

ax1.plot(datax[13],datay[13],'ro',markersize=10)
[<matplotlib.lines.Line2D at 0x7f91cce27450>]
../_images/Matplotlib_advanced_96_1.png
#mport numpy as np
#from matplotlib.patches import ConnectionPatch
from matplotlib.patches import Rectangle

fig = plt.figure(figsize=(10,5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)

np.random.seed(12345)
datax, datay = np.random.rand(100),np.random.rand(100)

ax1.plot(datax,datay,'ko')
ax2.plot(datax,datay,'ko')

xy=(datax[13],datay[13])
rect = Rectangle(xy,width=0.3,height=0.3,hatch='x',fill=False,edgecolor='b')
ax1.add_patch(rect)
[[x0,y0],[x1,y1]] = rect.get_bbox().get_points()  # vertices of the rectangle

con1 = ConnectionPatch(xyA=(x1,y0), xyB=(0,0),coordsA="data", coordsB="axes fraction",
                      arrowstyle='->', axesA=ax1, axesB=ax2, color='red',zorder=10)
con2 = ConnectionPatch(xyA=(x1,y1), xyB=(0,1),coordsA="data", coordsB="axes fraction",
                      axesA=ax1, axesB=ax2, color='red',zorder=10)

ax2.add_artist(con1)
ax2.add_artist(con2)
<matplotlib.patches.ConnectionPatch at 0x7f91cd16e710>
../_images/Matplotlib_advanced_97_1.png

输出文件

plt.savefig, fig.savefig

参数:

  • 文件名:根据后缀名决定保存的文件格式,png,jpg,gif,eps,pdf,svg

  • dpidots per inch,图片的精度,出版的图片一般要求 dpi > 300

  • bbox_inches="tight":让保存的图片更加紧凑,去掉周边的空白

动画

Matplotlib.animation 子模块可以用来生成动画。主要函数是 FuncAnimationArtistAnimation

FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, *, cache_frame_data=True, **kwargs)

  • fig: 绘制动画的 Figure 对象

  • func: callable,每个帧(frame)调用的绘图函数,其参数从 frames 获得

  • frames: iterable, int, generator function, or None, 可省略。传给 func 的数据,每次传一个,如果省略,则默认为100.

  • save_count: int, default: 100。保存的帧数。

  • interval: number, 可省略,默认200。两帧之间的延时,单位是毫秒(milliseconds)。

import matplotlib.animation as animation

fig, ax = plt.subplots(constrained_layout=True)

x = np.arange(0, 2*np.pi, 0.01)
line, = ax.plot(x, np.sin(x))

def init():  # only required for blitting to give a clean slate.
    line.set_ydata([np.nan] * len(x))
    return line,

def animate(i):
    line.set_ydata(np.sin(x + i / 10))  # update the data.
    return line,

ani = animation.FuncAnimation( 
    fig, animate, init_func=init, interval=2, blit=True)

ani.save("movie.gif" , dpi=80)

# from matplotlib.animation import FFMpegWriter
# writer = FFMpegWriter(fps=15, metadata=dict(artist='Me'), bitrate=1800)
# ani.save("movie.mp4", writer=writer)
MovieWriter ffmpeg unavailable; using Pillow instead.
../_images/Matplotlib_advanced_103_1.png

或者,我们也可以直接生成一些图片,把它们串起来。

import matplotlib.animation as animation

fig = plt.figure(constrained_layout=True)

def f(x, y):
    return np.sin(x) + np.cos(y)

x = np.linspace(0, 2 * np.pi, 120)
y = np.linspace(0, 2 * np.pi, 100).reshape(-1, 1)

ims = []
for i in range(60):
    x += np.pi / 15.
    y += np.pi / 20.
    im = plt.imshow(f(x, y), animated=True)
    ims.append([im])

ani = animation.ArtistAnimation(fig, ims, interval=50, blit=True, repeat_delay=1000)
ani.save('movie2.gif',dpi=80)
MovieWriter ffmpeg unavailable; using Pillow instead.
../_images/Matplotlib_advanced_106_1.png

绘制复杂的 subplot

import matplotlib.gridspec as gridspec

fig2 = plt.figure(constrained_layout=True)
spec2 = gridspec.GridSpec(ncols=2, nrows=2, figure=fig2)
f2_ax1 = fig2.add_subplot(spec2[0, 0])
f2_ax2 = fig2.add_subplot(spec2[0, 1])
f2_ax3 = fig2.add_subplot(spec2[1, 0])
f2_ax4 = fig2.add_subplot(spec2[1, 1])
../_images/Matplotlib_advanced_109_0.png
fig3 = plt.figure(constrained_layout=True)
gs = fig3.add_gridspec(3, 3)
f3_ax1 = fig3.add_subplot(gs[0, :])
f3_ax1.set_title('gs[0, :]')
f3_ax2 = fig3.add_subplot(gs[1, :-1])
f3_ax2.set_title('gs[1, :-1]')
f3_ax3 = fig3.add_subplot(gs[1:, -1])
f3_ax3.set_title('gs[1:, -1]')
f3_ax4 = fig3.add_subplot(gs[-1, 0])
f3_ax4.set_title('gs[-1, 0]')
f3_ax5 = fig3.add_subplot(gs[-1, -2])
f3_ax5.set_title('gs[-1, -2]')
Text(0.5, 1.0, 'gs[-1, -2]')
../_images/Matplotlib_advanced_110_1.png

def format_axes(fig):
    for i, ax in enumerate(fig.axes):
        ax.text(0.5, 0.5, "ax%d" % (i+1), va="center", ha="center")
        ax.tick_params(labelbottom=False, labelleft=False)

# gridspec inside gridspec
f = plt.figure()

gs0 = gridspec.GridSpec(1, 2, figure=f)

gs00 = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=gs0[0])

ax1 = f.add_subplot(gs00[:-1, :])
ax2 = f.add_subplot(gs00[-1, :-1])
ax3 = f.add_subplot(gs00[-1, -1])

# the following syntax does the same as the GridSpecFromSubplotSpec call above:
gs01 = gs0[1].subgridspec(3, 3)

ax4 = f.add_subplot(gs01[:, :-1])
ax5 = f.add_subplot(gs01[:-1, -1])
ax6 = f.add_subplot(gs01[-1, -1])

plt.suptitle("GridSpec Inside GridSpec")
format_axes(f)
../_images/Matplotlib_advanced_112_0.png

以下是另外一种实现方法

# gridspec inside gridspec
f = plt.figure()

gs0 = gridspec.GridSpec(3, 6, figure=f, wspace=0.2)

ax1 = f.add_subplot(gs0[:-1, 0:3])
ax2 = f.add_subplot(gs0[-1, 0:2])
ax3 = f.add_subplot(gs0[-1, 2])

ax4 = f.add_subplot(gs0[:, 3:-1])
ax5 = f.add_subplot(gs0[:-1, -1])
ax6 = f.add_subplot(gs0[-1, -1])

plt.suptitle("GridSpec Inside GridSpec")
format_axes(f)
../_images/Matplotlib_advanced_114_0.png

矢量箭头 quiver

x1d = np.linspace(-2*np.pi, 2*np.pi, 100)
y1d = np.linspace(-np.pi, np.pi, 50)
xx, yy = np.meshgrid(x1d, y1d)
f = np.cos(xx) * np.sin(yy)
clevels = np.arange(-1, 1, 0.2) + 0.1

u = -np.cos(xx) * np.cos(yy)
v = -np.sin(xx) * np.sin(yy)

fig, ax = plt.subplots(figsize=(12, 7))
ax.contour(xx, yy, f, clevels, cmap='RdBu_r', extend='both', zorder=0)
ax.quiver(xx[::4, ::4], yy[::4, ::4],
           u[::4, ::4], v[::4, ::4], zorder=1)
<matplotlib.quiver.Quiver at 0x7f91ce31ec50>
../_images/Matplotlib_advanced_116_1.png

流线图 streamplot

fig, ax = plt.subplots(figsize=(12, 7))
ax.streamplot(xx, yy, u, v, density=2, color=(u**2 + v**2))
<matplotlib.streamplot.StreamplotSet at 0x7f91ceb0ef90>
../_images/Matplotlib_advanced_118_1.png

中文字体

Matplotlib 默认的字体不支持中文;我们必须指定支持中文的字体。

matplotlib.font_manager 可以用来管理字体

import matplotlib as mpl

# 请使用你的电脑上的字体文件
myfont = mpl.font_manager.FontProperties(fname='/System/Library/Fonts/STHeiti Light.ttc') # Mac 字体文件
#myfont = mpl.font_manager.FontProperties(fname='C:\Windows\Fonts\STXIHEI.TTF') # Windows 字体文件

fig, ax = plt.subplots()

x = np.linspace(-np.pi, np.pi, 100)
y = np.cos(x)

ax.plot(x, y)
ax.text(0.01, 0.95, '你好', transform=ax.transAxes, fontproperties=myfont, fontsize=12) # 参照系选择
ax.annotate('最大值', xy=(0, 1),
             xytext=(1.2, 0.95), arrowprops={'facecolor': 'k','arrowstyle': '->'},
             fontproperties=myfont, fontsize=12) 
ax.set_title('中文标注',fontproperties=myfont, fontsize=16)
Text(0.5, 1.0, '中文标注')
../_images/Matplotlib_advanced_121_1.png

Pandas 绘图

import pandas as pd
air_quality = pd.read_csv("air_quality_no2.csv", index_col=0, parse_dates=True)
air_quality.head()
station_antwerp station_paris station_london
datetime
2019-05-07 02:00:00 NaN NaN 23.0
2019-05-07 03:00:00 50.5 25.0 19.0
2019-05-07 04:00:00 45.0 27.7 19.0
2019-05-07 05:00:00 NaN 50.4 16.0
2019-05-07 06:00:00 NaN 61.9 NaN
air_quality.plot()
<AxesSubplot:xlabel='datetime'>
../_images/Matplotlib_advanced_126_1.png
air_quality["station_paris"].plot()
<AxesSubplot:xlabel='datetime'>
../_images/Matplotlib_advanced_127_1.png
air_quality.plot.scatter(x="station_london", y="station_paris", alpha=0.5)
<AxesSubplot:xlabel='station_london', ylabel='station_paris'>
../_images/Matplotlib_advanced_128_1.png
air_quality.plot.box()  # 箱线图
<AxesSubplot:>
../_images/Matplotlib_advanced_129_1.png
axs = air_quality.plot.area(figsize=(12, 4), subplots=True)
../_images/Matplotlib_advanced_130_0.png
axs = air_quality.plot.area(figsize=(12, 4), subplots=True, layout=(2,2))
../_images/Matplotlib_advanced_131_0.png
# 参考 https://matplotlib.org/3.3.3/api/dates_api.html
import matplotlib.dates as mdates

fig, axs = plt.subplots(figsize=(12, 4))
air_quality.plot.area(ax=axs)
axs.set_ylabel("NO$_2$ concentration")

dayLocator = mdates.DayLocator(bymonthday=(1,15))
#dayLocator = mdates.MonthLocator(bymonthday=(1,15))
#dayFormatter = mdates.DateFormatter('%Y-%m-%d')
axs.xaxis.set_major_locator(mdates.AutoDateLocator(interval_multiples=False)) #(dayLocator)
axs.xaxis.set_minor_locator(mdates.DayLocator(interval=1))
#axs.xaxis.set_major_formatter(dayFormatter)
../_images/Matplotlib_advanced_132_0.png
air_quality['station_london'].plot()
air_quality['station_antwerp'].plot(secondary_y=True, style='g')
<AxesSubplot:label='ddea9f5f-d870-4902-ab9b-89703a9156db'>
../_images/Matplotlib_advanced_133_1.png
air_quality.plot(secondary_y=['station_london']) #, mark_right=False)
<AxesSubplot:xlabel='datetime'>
../_images/Matplotlib_advanced_134_1.png
from pandas.plotting import scatter_matrix

scatter_matrix(air_quality, alpha=0.2, figsize=(6, 6), diagonal='kde');
../_images/Matplotlib_advanced_135_0.png

Colorbar 和 colormap

Colormap 参考 https://matplotlib.org/tutorials/colors/colormaps.html#sphx-glr-tutorials-colors-colormaps-py

多图共用一个 colorbar

fig, axs = plt.subplots(2, 2, figsize=(8,5))
cm = ['RdBu_r', 'viridis']
for col in range(2):
    for row in range(2):
        ax = axs[row, col]
        pcm = ax.pcolormesh(np.random.random((20, 20)) * (col + 1), cmap=cm[col])
        fig.colorbar(pcm, ax=ax)
../_images/Matplotlib_advanced_139_0.png
fig, axs = plt.subplots(2, 2, figsize=(8,5))
fig.subplots_adjust(wspace=0.2, hspace=0.3)
cm = ['RdBu_r', 'viridis']

for col in range(2):
    for row in range(2):
        ax = axs[row, col]
        pcm = ax.pcolormesh(np.random.random((20, 20)) * (col + 1), cmap=cm[col])
    fig.colorbar(pcm, ax=axs[:, col], shrink=0.6)
../_images/Matplotlib_advanced_140_0.png

思考:如果上面4幅图要使用同一个 colorbar,该怎么办?

fig, axs = plt.subplots(3, 3, constrained_layout=True)
for ax in axs.flat:
    pcm = ax.pcolormesh(np.random.random((20, 20)))

fig.colorbar(pcm, ax=axs[0, :2], shrink=0.6, location='bottom')
fig.colorbar(pcm, ax=[axs[0, 2]], location='bottom')
fig.colorbar(pcm, ax=axs[1:, :], location='right', shrink=0.6)
fig.colorbar(pcm, ax=[axs[2, 1]], location='left')
<matplotlib.colorbar.Colorbar at 0x7f91d1a8c650>
../_images/Matplotlib_advanced_142_1.png

References

  1. https://rabernat.github.io/research_computing_2018/more-matplotlib.html

  2. https://matplotlib.org/tutorials/

  3. https://matplotlib.org/gallery/index.html

  4. https://github.com/rougier/matplotlib-tutorial

  5. https://pandas.pydata.org/pandas-docs/stable/user_guide/visualization.html