matplotlib 日期时间 x 轴问题
我在使用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 个回答
你需要把locator.MAXTICKS的值调大,这样才能避免出现错误:超过了Locator.MAXTICKS * 2(2000)
比如:
alldays = DayLocator() # minor ticks on the days
alldays.MAXTICKS = 2000
这确实感觉像是个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自动生成的刻度数量。
这个问题是因为 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字典来决定刻度间隔。
当 maxticks
是 None
时,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
。所以 freq
是 DAILY
,而 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()