如何获得格式良好的日期,如熊猫线图
问题在于pandas bar plot 将日期变量作为分类变量处理,其中每个日期都被认为是一个唯一的类别,因此 x 轴单位设置为从 0 开始的整数(就像默认的 DataFrame 索引时none 被分配)并且每个日期的完整字符串都显示出来,没有任何自动格式化。
这里有两种解决方案来格式化时间序列的 pandas(堆叠)条形图的日期刻度标签:
- 第一个是answer by unutbu 的变体,旨在更好地拟合问题中显示的数据;
- 第二个是通用解决方案,可让您使用 matplotlib 日期刻度定位器和格式化程序,为任何频率类型的时间序列生成适当的日期标签。
但首先,让我们看看使用 pandas 线图绘制样本数据时格式良好的刻度标签是什么样的。
默认熊猫线图日期格式
import numpy as np # v 1.19.2
import pandas as pd # v 1.1.3
import matplotlib.dates as mdates # v 3.3.2
# Create sample dataset with a daily frequency and resample it to a weekly frequency
rng = np.random.default_rng(seed=123) # random number generator
idx = pd.date_range(start='2012-01-01', end='2013-12-31', freq='D')
df_raw = pd.DataFrame(rng.random(size=(idx.size, 3)),
index=idx, columns=list('ABC'))
df = df_raw.resample('W').sum() # default is 'W-SUN'
# Create pandas stacked line plot
ax = df.plot(stacked=True, figsize=(10,5))
由于数据按星期分组,带有星期日的时间戳(频率 W-SUN),每月刻度标签不一定放在每月的第一天,每个第一周之间可能有 3 或 4 周月,因此小刻度线的间距不均匀(如果仔细观察会发现)。以下是主要刻度的确切日期:
# Convert major x ticks to date labels
np.array([mdates.num2date(tick*7-4).strftime('%Y-%b-%d') for tick in ax.get_xticks()])
"""
array(['2012-Jan-01', '2012-Apr-01', '2012-Jul-01', '2012-Oct-07',
'2013-Jan-06', '2013-Apr-07', '2013-Jul-07', '2013-Oct-06',
'2014-Jan-05'], dtype='<U11')
"""
挑战在于为每个月的第一周选择刻度,因为它们的间距不相等。其他答案提供了基于固定刻度频率的简单解决方案,这会产生奇怪的间隔标签有时可以重复月份的日期(例如 unutbu 回答中的 7 月份)。或者他们提供了基于每月时间序列而不是每周时间序列的解决方案,这更容易格式化,因为每年总是有 12 个月。 所以这里有一个解决方案,它可以提供格式良好的刻度标签,就像 pandas 线图中一样,并且适用于任何频率的数据。
解决方案 1:基于 DatetimeIndex 的带有刻度标签的 pandas 条形图
# Create pandas stacked bar chart
ax = df.plot.bar(stacked=True, figsize=(10,5))
# Create list of monthly timestamps by selecting the first weekly timestamp of each
# month (in this example, the first Sunday of each month)
monthly_timestamps = [timestamp for idx, timestamp in enumerate(df.index)
if (timestamp.month != df.index[idx-1].month) | (idx == 0)]
# Automatically select appropriate number of timestamps so that x-axis does
# not get overcrowded with tick labels
step = 1
while len(monthly_timestamps[::step]) > 10: # increase number if time range >3 years
step += 1
timestamps = monthly_timestamps[::step]
# Create tick labels from timestamps
labels = [ts.strftime('%b\n%Y') if ts.year != timestamps[idx-1].year
else ts.strftime('%b') for idx, ts in enumerate(timestamps)]
# Set major ticks and labels
ax.set_xticks([df.index.get_loc(ts) for ts in timestamps])
ax.set_xticklabels(labels)
# Set minor ticks without labels
ax.set_xticks([df.index.get_loc(ts) for ts in monthly_timestamps], minor=True)
# Rotate and center labels
ax.figure.autofmt_xdate(rotation=0, ha='center')
据我所知,使用 matplotlib.dates (mdates) 刻度定位器和格式化程序无法获得这种精确的标签格式。不过,如果您更喜欢使用刻度定位器/格式化程序,或者如果您希望在使用 matplotlib 的交互式界面(平移/放大和缩小)时拥有动态刻度,则将 mdates 功能与 pandas 堆叠条形图相结合会派上用场。
此时,考虑直接在 matplotlib 中创建堆积条形图可能很有用,您需要在其中循环变量以创建堆积条形图。下面显示的基于 pandas 的解决方案通过循环遍历条形块的补丁来根据 matplotlib 日期单位重新定位它们。所以它基本上是一个循环而不是另一个循环,由你来看看哪个更方便。
解决方案 2:使用 matplotlib 刻度定位器和格式化程序的 pandas 条形图
此通用解决方案使用 mdates AutoDateLocator 将刻度放在月/年的开头。如果您在 pandas 中使用pd.date_range 生成数据和时间戳(如本例所示),您应该记住,常用的'M' 和'Y' 频率会为周期的结束日期生成时间戳。以下示例中给出的代码将每月/每年的刻度线与 'MS' 和 'YS' 频率对齐。
如果您使用期末日期(或 some other type of pandas frequency 未与 AutoDateLocator 刻度对齐)导入数据集,我不知道有任何方便的方法可以相应地移动 AutoDateLocator 以便标签正确对齐酒吧。我看到两个选项:i)如果这不会导致有关基础数据含义的任何问题,则使用 df.resample('MS').sum() 重新采样数据; ii) 或者使用另一个日期定位器。
这个问题在以下示例中没有问题,因为数据的周末频率为'W-SUN',因此以月/年开始频率放置的月/年标签很好。
# Create pandas stacked bar chart with the default bar width = 0.5
ax = df.plot.bar(stacked=True, figsize=(10,5))
# Compute width of bars in matplotlib date units, 'md' (in days) and adjust it if
# the bar width in df.plot.bar has been set to something else than the default 0.5
bar_width_md_default, = np.diff(mdates.date2num(df.index[:2]))/2
bar_width = ax.patches[0].get_width()
bar_width_md = bar_width*bar_width_md_default/0.5
# Compute new x values in matplotlib date units for the patches (rectangles) that
# make up the stacked bars, adjusting the positions according to the bar width:
# if the frequency is in months (or years), the bars may not always be perfectly
# centered over the tick marks depending on the number of days difference between
# the months (or years) given by df.index[0] and [1] used to compute the bar
# width, this should not be noticeable if the bars are wide enough.
x_bars_md = mdates.date2num(df.index) - bar_width_md/2
nvar = len(ax.get_legend_handles_labels()[1])
x_patches_md = np.ravel(nvar*[x_bars_md])
# Set bars to new x positions and adjust width: this loop works fine with NaN
# values as well because in bar plot NaNs are drawn with a rectangle of 0 height
# located at the foot of the bar, you can verify this with patch.get_bbox()
for patch, x_md in zip(ax.patches, x_patches_md):
patch.set_x(x_md)
patch.set_width(bar_width_md)
# Set major ticks
maj_loc = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(maj_loc)
# Show minor tick under each bar (instead of each month) to highlight
# discrepancy between major tick locator and bar positions seeing as no tick
# locator is available for first-week-of-the-month frequency
ax.set_xticks(x_bars_md + bar_width_md/2, minor=True)
# Set major tick formatter
zfmts = ['', '%b\n%Y', '%b', '%b-%d', '%H:%M', '%H:%M']
fmt = mdates.ConciseDateFormatter(maj_loc, zero_formats=zfmts, show_offset=False)
ax.xaxis.set_major_formatter(fmt)
# Shift the plot frame to where the bars are now located
xmin = min(x_bars_md) - bar_width_md
xmax = max(x_bars_md) + 2*bar_width_md
ax.set_xlim(xmin, xmax)
# Adjust tick label format last, else it may sometimes not be applied correctly
ax.figure.autofmt_xdate(rotation=0, ha='center')
在每个条形下方显示的小刻度 a 以突出显示条形的时间戳通常与 AutoDateLocator 刻度标签标记的月/年开始不一致的事实。我不知道有任何日期定位器可用于选择每个月第一周的刻度并准确重现解决方案 1 中显示的结果。
文档:date format codes、mdates.ConciseDateFormatter