matplotlib 日期时间 x 轴问题

11 投票
3 回答
8096 浏览
提问于 2025-04-17 17:37

我在使用matplotlib绘制日期的x轴标签时,遇到了一些奇怪的情况。当我输入以下命令时:

from datetime import datetime as dt
plot( [ dt(2013, 1, 1), dt(2013, 5, 17)], [ 1 , 1 ], linestyle='None', marker='.')

我得到了一个标签看起来很合理的图表:

在这里输入图片描述

但是如果我把结束日期增加一天:

plot( [ dt(2013, 1, 1), dt(2013, 5, 18)], [ 1 , 1 ], linestyle='None', marker='.')

我得到的结果是这个:

在这里输入图片描述

我在不同的日期范围(2012年)中复现了这个问题,每次触发这个问题的天数大约是140天(在这个例子中是136/137天)。有人知道这是怎么回事吗?这是一个已知的bug吗?如果是的话,有什么解决办法吗?

几点说明:在上面的命令中,我使用的是IPython的--pylab模式来创建图表,但我第一次遇到这个问题时是直接使用matplotlib,并且在脚本中也能复现(也就是说,我认为这不是IPython的问题)。此外,我在matplotlib 1.1.0和1.2.X版本中都观察到了这个问题。

更新:

看起来有一个窗口,如果你向前推得足够远,标签又会正常显示。对于上面的例子,标签在5月18日至5月31日之间仍然是混乱的,但在6月1日,标签又开始正常绘制了。所以,

(labels are garbled)
plot( [ dt(2013, 1, 1), dt(2013, 5, 31)], [ 1 , 1 ], linestyle='None', marker='.')

(labels are fine)
plot( [ dt(2013, 1, 1), dt(2013, 6, 1)], [ 1 , 1 ], linestyle='None', marker='.')

3 个回答

0

你需要把locator.MAXTICKS的值调大,这样才能避免出现错误:超过了Locator.MAXTICKS * 2(2000)

比如:

    alldays = DayLocator()              # minor ticks on the days
    alldays.MAXTICKS = 2000
2

这确实感觉像是个bug;我建议你去matplotlib的邮件列表问问那里的朋友们,他们可能会给你一些建议。

我可以提供一个解决方法,具体如下:

from datetime import datetime as dt
from matplotlib import pylab as pl

fig = pl.figure()
axes = fig.add_subplot(111)
axes.plot( [ dt(2013, 1, 1), dt(2013, 5, 18)], [ 1 , 1 ], linestyle='None', marker='.')
ticks = axes.get_xticks()
n = len(ticks)//6
axes.set_xticks(ticks[::n])
fig.savefig('dateticks.png')

抱歉使用了面向对象的方法(这不是你用的方式),但这样可以更方便地将刻度和图表关联起来。这里的数字 6 只是我想要在x轴上显示的标签数量,然后我通过计算得出的 n 来减少matplotlib自动生成的刻度数量。

9

这个问题是因为 AutoDateLocator 里面有个bug。看起来这个bug还没有被报告到问题追踪系统上。
之所以看起来奇怪,是因为绘制了太多的标签和刻度。

在用日期数据绘图时,默认情况下,matplotlib会使用 matplotlib.dates.AutoDateLocator 作为主要的定位器。也就是说,AutoDateLocator 用来决定刻度的间隔和位置。

假设数据序列是 [datetime(2013, 1, 1), datetime(2013, 5, 18)]
时间间隔是4个月17天。月数间隔是4,天数间隔是4*31+17=141。

根据 matplotlib的文档

class matplotlib.dates.AutoDateLocator(tz=None, minticks=5, maxticks=None, interval_multiples=False)

minticks是希望的最小刻度数量,用来选择刻度的类型(每年、每月等)。

maxticks是希望的最大刻度数量,用来控制刻度之间的间隔(比如每隔一个、每隔三个等)。如果需要更精细的控制,可以用一个字典来映射每种频率(如每年、每月等)对应的最大刻度数量。这样可以确保刻度数量适合在 AutoDateFormatter 中选择的格式。字典中没有指定的频率将使用默认值。

AutoDateLocator有一个间隔字典,用来映射刻度的频率(来自dateutil.rrule的常量)和允许的倍数。默认的看起来是这样的:

    self.intervald = {
      YEARLY  : [1, 2, 4, 5, 10],
      MONTHLY : [1, 2, 3, 4, 6],
      DAILY   : [1, 2, 3, 7, 14],
      HOURLY  : [1, 2, 3, 4, 6, 12],
      MINUTELY: [1, 5, 10, 15, 30],
      SECONDLY: [1, 5, 10, 15, 30]
      }

这个间隔用来指定适合刻度频率的倍数。例如,每7天适合日刻度,但对于分钟/秒,15或30比较合适。你可以通过以下方式自定义这个字典:

因为月数间隔是4,小于5,而天数间隔是141,不小于5。所以刻度的类型将是每日的。
确定了刻度类型后,AutoDateLocator 会使用间隔字典和maxticks字典来决定刻度间隔。

maxticksNone 时,AutoDateLocator 会使用它的默认maxticks字典。 文档告诉我们默认的间隔字典,但没有说明默认的maxticks字典是什么样的。
我们可以在 dates.py 中找到它。

self.maxticks = {YEARLY : 16, MONTHLY : 12, DAILY : 11, HOURLY : 16,
            MINUTELY : 11, SECONDLY : 11}

确定刻度间隔的算法

# Find the first available interval that doesn't give too many ticks
for interval in self.intervald[freq]:
    if num <= interval * (self.maxticks[freq] - 1):
        break
else:
    # We went through the whole loop without breaking, default to 1
    interval = 1

现在刻度类型是 DAILY。所以 freqDAILY,而 num 是141,也就是天数间隔。上面的代码等价于

for interval in [1, 2, 3, 7, 14]:
    if 141 <= interval * (11 - 1):
        break
else:
    interval = 1

141太大了。所有的日间隔都会产生太多的刻度。else 语句会被执行,刻度间隔会被设置为1。
这意味着会绘制140个以上的标签和刻度。我们可以预期x轴会很难看。

如果数据序列是 [datetime(2013, 1, 1), datetime(2013, 5, 17)],只少一天,天数间隔是140。 那么 AutoDateLocator 会选择14作为刻度间隔,只会绘制10个标签。 这样你的第一个图看起来就不错。

其实我不明白为什么matplotlib选择在无法满足 maxticks 限制时将间隔设置为1。 如果间隔是1,只会导致刻度数量大幅增加。我更倾向于使用最长的间隔。

结论:
对于任何日期序列,其范围大于或等于4个月18天且小于5个月,AutoDateLocator 会选择1作为刻度间隔。 当用默认的主要定位器 AutoDateLocator 绘制这样的日期序列时,你会看到x轴或y轴会出现一些难看的情况。

解决方案:
最简单的解决方案是将每日的maxticks增加到12。 例如:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.dates import DAILY
from datetime import datetime

ax = plt.subplot(111)
plt.plot_date([datetime(2013, 1, 1), datetime(2013, 5, 31)],
              [datetime(2013, 1, 1), datetime(2013, 5, 10)])

loc = ax.xaxis.get_major_locator()
loc.maxticks[DAILY] = 12

plt.show()

撰写回答