Matplotlib时间序列

🔖 python
🔖 visualization
Author

Guangyao Zhao

Published

May 1, 2023

Matplotlib 中画折线图用 ax.plot(x, y),当横坐标 x 是时间数组时,例如 datetime 或 np.datetime64 构成的列表,x 和 y 的组合即一条时间序列。Matplotlib 能直接画出时间序列,并自动设置刻度。下面以一条长三年的气温时间序列为例:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

tmp = 100
series = pd.Series(np.random.randn(tmp),
                   index=pd.date_range(start="2023/01/01",
                                       periods=tmp,
                                       freq="D"))

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(series.index, series)
ax.set_ylabel("Temperature")
ax.tick_params(axis="both", which="major", direction="inout")

print(ax.xaxis.get_major_locator())
print(ax.xaxis.get_major_formatter())
<matplotlib.dates.AutoDateLocator object at 0x16bb6bf70>
<matplotlib.dates.AutoDateFormatter object at 0x16bb82130>

打印 x 轴的属性发现,Matplotlib 默认为时间序列设置了 AutoDateLocatorAutoDateFormatter,前者会自动根据 ax 的时间范围在 x 轴上选出位置、数量和间隔都比较合适的刻度,后者会自动根据主刻度的间隔,将刻度标签格式化为合适的样式。

虽然自动刻度很方便,但如果想像上图一样调整刻度间隔,追加小刻度,并修改刻度标签格式,就需要手动设置刻度。本文的目的就是介绍手动修改时间刻度的方法,内容主要分为三点:

1 matplotlib 处理时间的机制

matplotlib.dates(后简称 mdates)模块里有两个函数:date2numnum2date。前者能将一个 datetimenp.datetime64 对象转换成该对象离 1970-01-01T00:00:00 以来的天数(注意不是秒数),后者则是反过来转换。当 ax.plot 接受时间类型的 x 时,会在内部创建一个 mdates.DateConverter 对象,对 x 的每个元素调用 date2num,将其转换成表示天数的浮点型一维数组。Matplotlib 在内部便是以这种浮点数的形式存储时间的。下面验证一下这点:

x0, x1 = ax.get_xlim()
origin = "1970-01-01 00:00"
t0 = pd.to_datetime(x0, unit="D", origin=origin)
t1 = pd.to_datetime(x1, unit="D", origin=origin)
print("x0 =", x0, "t0 =", t0)
print("x1 =", x1, "t1 =", t1)
x0 = 19353.05 t0 = 2022-12-27 01:12:00
x1 = 19461.95 t1 = 2023-04-14 22:48:00

其中 pd.to_datetime 可以直接换成 num2date。所以后续在 ax 上画新线条时,使用时间类型或浮点类型的 x 都可以。

2 使用 matplotlib.dates 提供的工具

除引言里提到的 AutoDateLocatorAutoDateFormatter 外,mdates 还提供其它规则的 LocatorFormatter。以设置月份刻度的 MonthLocator 为例:

dates.MonthLocator(bymonth=None, bymonthday=1, interval=1, tz=None)

其中 bymonth 参数可以是表示月份的整数,或整数构成的列表,默认值是 1 - 12 月。MonthLocator会在 axx 轴显示范围间生成一系列间隔为 interval 个月的 datetime 对象,它们的日由 bymonthday 指定,时分秒都为 0。从中挑选出月份跟 bymonth 匹配的对象,调用 date2num 函数作为最后的刻度值。因为内部实现用的是 dateutil.rrule.rrule,所以参数也是与之同名的。例如 MonthLocator() 的效果就是在每年每月 1 号 00:00:00 的位置设置一个刻度,那么一年就会有 12 个刻度。MonthLocator(bymonth=[1, 4, 7, 10]) 就是在每年 1、4、7 和 10 月设置刻度。

除此之外 mdates 里还有 YearLocator, DayLocator, WeekDayLocator, HourLocator 等,原理和参数跟 MonthLocator 类似,就不多介绍了。

接着以 DateFormatter 为例:class matplotlib.dates.DateFormatter(fmt, tz=None, *, usetex=None)。原理非常简单,就是对刻度值 x 调用 num2date(x).strftime(fmt),得到刻度标签。例如取 DateFormatter(fmt='%Y-%m'),就能让刻度标签呈 YYYY-MM 的格式。此外我们知道,如果直接向 ax.xaxis.get_major_formatter 传入一个参数为 xpos 的函数,就相当于用这个函数构造了一个 FuncFormatter。所以可以简单自制一个只在每年 1 月标出年份的 Formatter

def format_func(x, pos=None):
    x = mdates.num2date(x)
    if x.month == 1:
        fmt = "%m\n%Y"
    else:
        fmt = "%m"
    label = x.strftime(fmt)

    return label
import matplotlib.dates as mdates

ax.xaxis.set_major_locator(mdates.MonthLocator([1, 4, 7, 10]))
ax.xaxis.set_minor_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(format_func)