以小倍数突出显示周末

Highlighting weekends in small multiples

如何以小倍数突出显示周末?

我已经阅读了不同的主题(例如 and (2)),但无法弄清楚如何将其实现到我的案例中,因为我使用的是小倍数,我在其中迭代 DateTimeIndex 到每个月(见下图代码)。对于这种情况,我的数据 Profiles 是 2 年的时间序列,间隔为 15 分钟(即 70080 个数据点)。

但是,周末出现在月底,因此会产生错误;在这种情况下:IndexError: index 2972 is out of bounds for axis 0 with size 2972

我的尝试: [编辑 - 根据 的建议]

In [10]:
class highlightWeekend:
    '''Class object to highlight weekends'''
    def __init__(self, period):
        self.ranges= period.index.dayofweek >= 5
        self.res = [x for x, (i , j) in enumerate(zip( [2] + list(self.ranges), list(self.ranges) + [2])) if i != j]
        if self.res[0] == 0 and self.ranges[0] == False:
            del self.res[0]
        if self.res[-1] == len(self.ranges) and self.ranges[-1] == False:
            del self.res[-1]

months= Profiles.loc['2018'].groupby(lambda x: x.month)
fig, axs= plt.subplots(4,3, figsize= (16, 12), sharey=True)
axs= axs.flatten()
for i, j in months:
    axs[i-1].plot(j.index, j)
    if i < len(months):
        k= 0
        while k < len(highlightWeekend(j).res):
            axs[i-1].axvspan(j.index[highlightWeekend(j).res[k]], j.index[highlightWeekend(j).res[k+1]], alpha=.2)
            k+=2
    i+=1
plt.show()

[Out 10]:

问题 如何解决月底出现周末的问题?

TL;DR 跳至 方法 2 的解决方案以查看最佳解决方案,或跳至最后一个示例以获取解决方案单个 pandas 线图。在所有三个示例中,周末仅使用 4-6 行代码 突出显示,其余用于格式化和再现。



方法和工具

我知道有两种方法可以在时间序列图上突出显示周末,它们既可以应用于单个图,也可以通过遍历子图数组来应用于小的倍数。此答案提供了突出显示周末的解决方案,但可以轻松调整它们以在任何重复出现的时间段内工作。


方法一:根据dataframe索引高亮显示

此方法遵循问题中代码的逻辑以及链接主题中的答案。不幸的是,当月底出现周末时,就会出现问题,绘制周末的整个跨度所需的索引号超出了产生错误的索引范围。这个问题在下面进一步显示的解决方案中通过计算两个时间戳之间的时间差并将其添加到 DatetimeIndex 的每个时间戳时在它们上循环以突出显示周末来解决。

但仍然存在两个问题,i) 此方法不适用于频率超过一天的时间序列,以及 ii) 基于频率小于每小时(如 15 分钟)的时间序列将需要绘制许多多边形会影响性能。由于这些原因,此方法在此处出于文档目的而提供,我建议改用方法 2。


方法二:根据x轴单位高亮显示

此方法使用 x 轴单位,即自时间原点 (1970-01-01) 以来的天数,独立于所绘制的时间序列数据来识别周末,这使其更加灵活比方法 1 高亮显示仅在每个完整的周末绘制,这使得下面给出的示例比方法 1 快两倍(根据 Jupyter Notebook 中的 %%timeit 测试)。这是我推荐使用的方法。


matplotlib中可用于实现这两种方法的工具

axvspan link demo, link API(用于方法1的解决方案

broken_barh link demo, link API

fill_between link demo, link API(用于方法2的解决方案

BrokenBarHCollection.span_where link demo, link API

在我看来,fill_betweenBrokenBarHCollection.span_where 本质上是一样的。两者都提供方便的 where 参数,该参数在下面进一步介绍的方法 2 的解决方案中使用。



解决方案

这是一个可重现的示例数据集,用于说明这两种方法,使用频率为 6 小时。请注意,数据框仅包含一年的数据,这使得 select 月度数据只需使用 df[df.index.month == month] 即可绘制每个子图。如果您要处理多年 DatetimeIndex,则需要对此进行调整。

导入用于所有 3 个示例的包并为前 2 个示例创建数据集

import numpy as np                   # v 1.19.2
import pandas as pd                  # v 1.1.3
import matplotlib.pyplot as plt      # v 3.3.2
import matplotlib.dates as mdates  # used only for method 2

# Create sample dataset
rng = np.random.default_rng(seed=1) # random number generator
dti = pd.date_range('2018-01-01 00:00', '2018-12-31 23:59', freq='6H')
consumption = rng.integers(1000, 2000, size=dti.size)
df = pd.DataFrame(dict(consumption=consumption), index=dti)

方法一的解决方法:根据dataframe索引高亮显示

在此解决方案中,周末使用 axvspan 和每月数据帧的 DatetimeIndex df_month 突出显示。周末时间戳被 select 编辑为 df_month.index[df_month.weekday>=5].to_series() 并且超出索引范围的问题通过从 DatetimeIndex 的频率计算 timedelta 并将其添加到每个时间戳来解决。

当然,axvspan 也可以以比这里显示的更智能的方式使用,以便一次绘制每个周末的亮点,但我相信这会导致解决方案的灵活性降低和代码增多比 方法 2 的解决方案 .

中介绍的内容
# Draw and format subplots by looping through months and flattened array of axes
fig, axs = plt.subplots(4, 3, figsize=(10, 9), sharey=True)
for month, ax in zip(df.index.month.unique(), axs.flat):
    # Select monthly data and plot it
    df_month = df[df.index.month == month]
    ax.plot(df_month.index, df_month['consumption'])
    ax.set_ylim(0, 2500) # set limit similar to plot shown in question
    
    # Draw vertical spans for weekends: computing the timedelta and adding it
    # to the date solves the problem of exceeding the df_month.index
    timedelta = pd.to_timedelta(df_month.index.freq)
    weekends = df_month.index[df_month.index.weekday>=5].to_series()
    for date in weekends:
        ax.axvspan(date, date+timedelta, facecolor='k', edgecolor=None, alpha=.1)
    
    # Format tick labels
    ax.set_xticks(ax.get_xticks())
    tk_labels = [pd.to_datetime(tk, unit='D').strftime('%d') for tk in ax.get_xticks()]
    ax.set_xticklabels(tk_labels, rotation=0, ha='center')
    
    # Add x labels for months
    ax.set_xlabel(df_month.index[0].month_name().upper(), labelpad=5)
    ax.xaxis.set_label_position('top')

# Add title and edit spaces between subplots
year = df.index[0].year
freq = df_month.index.freqstr
title = f'{year} consumption displayed for each month with a {freq} frequency'
fig.suptitle(title.upper(), y=0.95, fontsize=12)
fig.subplots_adjust(wspace=0.1, hspace=0.5)

fig.text(0.5, 0.99, 'Weekends are highlighted by using the DatetimeIndex',
         ha='center', fontsize=14, weight='semibold');

如您所见,周末亮点在数据结束的地方结束,如 3 月份所示。如果使用 DatetimeIndex 设置 x 轴限制,这当然不会引起注意。



方法二的解决方法:根据x轴单位高亮显示

此解决方案使用 x 轴限制以天为单位计算绘图覆盖的时间范围,这是用于 matplotlib dates 的单位。计算 weekends 掩码,然后传递给 fill_between 绘图函数的 where 参数。掩码的 True 值被处理为右排他的,因此在这种情况下,必须包括星期一,以便绘制到星期一 00:00 的高光。因为当周末出现在限制附近时,绘制这些突出显示可能会改变 x 轴限制,因此在绘制后 x 轴限制将设置回原始值。

请注意,对于 fill_between,必须给出 y1y2 参数。出于某种原因,使用默认的 y 轴限制会在图框与周末亮点的顶部和底部之间留下一个小间隙。在这里,y 限制设置为 0 和 2500 只是为了创建一个类似于问题中的示例,但对于一般情况应该使用以下内容:ax.set_ylim(*ax.get_ylim()).

# Draw and format subplots by looping through months and flattened array of axes
fig, axs = plt.subplots(4, 3, figsize=(10, 9), sharey=True)
for month, ax in zip(df.index.month.unique(), axs.flat):
    # Select monthly data and plot it
    df_month = df[df.index.month == month]
    ax.plot(df_month.index, df_month['consumption'])
    ax.set_ylim(0, 2500) # set limit like plot shown in question, or use next line
#     ax.set_ylim(*ax.get_ylim())
    
    # Highlight weekends based on the x-axis units, regardless of the DatetimeIndex
    xmin, xmax = ax.get_xlim()
    days = np.arange(np.floor(xmin), np.ceil(xmax)+2)
    weekends = [(dt.weekday()>=5)|(dt.weekday()==0) for dt in mdates.num2date(days)]
    ax.fill_between(days, *ax.get_ylim(), where=weekends, facecolor='k', alpha=.1)
    ax.set_xlim(xmin, xmax) # set limits back to default values
     
    # Create appropriate ticks with matplotlib date tick locator and formatter
    tick_loc = mdates.MonthLocator(bymonthday=np.arange(1, 31, step=5))
    ax.xaxis.set_major_locator(tick_loc)
    tick_fmt = mdates.DateFormatter('%d')
    ax.xaxis.set_major_formatter(tick_fmt)
    
    # Add x labels for months
    ax.set_xlabel(df_month.index[0].month_name().upper(), labelpad=5)
    ax.xaxis.set_label_position('top')

# Add title and edit spaces between subplots
year = df.index[0].year
freq = df_month.index.freqstr
title = f'{year} consumption displayed for each month with a {freq} frequency'
fig.suptitle(title.upper(), y=0.95, fontsize=12)
fig.subplots_adjust(wspace=0.1, hspace=0.5)
fig.text(0.5, 0.99, 'Weekends are highlighted by using the x-axis units',
         ha='center', fontsize=14, weight='semibold');

如您所见,无论数据从何处开始和结束,周末始终突出显示。



方法 2 的解决方案的其他示例,其中包含每月时间序列和 pandas 图

该图可能没有多大意义,但它可以说明方法 2 的灵活性以及如何使其与 pandas 线图兼容。请注意,示例数据集使用月份开始频率,以便默认刻度与数据点对齐。

# Create sample dataset with a month start frequency
rng = np.random.default_rng(seed=1) # random number generator
dti = pd.date_range('2018-01-01 00:00', '2018-06-30 23:59', freq='MS')
consumption = rng.integers(1000, 2000, size=dti.size)
df = pd.DataFrame(dict(consumption=consumption), index=dti)

# Draw pandas plot: x_compat=True converts the pandas x-axis units to matplotlib
# date units
ax = df.plot(x_compat=True, figsize=(10, 4), legend=None)
ax.set_ylim(0, 2500) # set limit similar to plot shown in question, or use next line
# ax.set_ylim(*ax.get_ylim())
    
# Highlight weekends based on the x-axis units, regardless of the DatetimeIndex
xmin, xmax = ax.get_xlim()
days = np.arange(np.floor(xmin), np.ceil(xmax)+2)
weekends = [(dt.weekday()>=5)|(dt.weekday()==0) for dt in mdates.num2date(days)]
ax.fill_between(days, *ax.get_ylim(), where=weekends, facecolor='k', alpha=.1)
ax.set_xlim(xmin, xmax) # set limits back to default values

# Additional formatting
ax.figure.autofmt_xdate(rotation=0, ha='center')
ax.set_title('2018 consumption by month'.upper(), pad=15, fontsize=12)

ax.figure.text(0.5, 1.05, 'Weekends are highlighted by using the x-axis units',
               ha='center', fontsize=14, weight='semibold');



您可以在我发布的答案中找到此解决方案的更多示例 and 。 参考资料:this answer by Nipun Batra, , matplotlib.dates