【问题标题】:Highlighting weekends in small multiples以小倍数突出显示周末
【发布时间】:2021-05-06 17:43:12
【问题描述】:

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

我已经阅读了不同的线程(例如 (1)(2)),但无法弄清楚如何在我的案例中实现它,因为我使用的是小倍数,我每个月都会遍历 DateTimeIndex (见下图代码)。我的数据Profiles 在这种情况下是 2 年的时间序列,间隔为 15 分钟(即 70080 个数据点)。

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

我的尝试: [已编辑 - 由 @Patrick FitzGerald 提出建议]

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]:

问题 如何解决月末周末的问题?

【问题讨论】:

    标签: python datetime matplotlib highlight


    【解决方案1】:

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



    方法和工具

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


    方法一:根据数据框索引高亮

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

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


    方法二:基于x轴单位高亮

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


    ma​​tplotlib 中可用于实现这两种方法的工具

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

    broken_barhlink demo,link API

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

    BrokenBarHCollection.span_wherelink demo,link API

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



    解决方案

    这是一个可重复的示例数据集,用于说明这两种方法,使用频率为 6 小时。请注意,数据框仅包含一年的数据,因此可以简单地使用 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)
    

    方法一的解决方案:基于数据框索引的高亮显示

    在此解决方案中,使用 axvspan 和每月数据帧的 DatetimeIndex df_month 突出显示周末。周末时间戳用df_month.index[df_month.weekday&gt;=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 轴范围,这当然不会引起注意。



    方法 2 的解决方案:基于 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 的解决方案的附加示例,包含每月时间序列和熊猫图

    此图可能没有多大意义,但它说明了方法 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');
    



    您可以在我发布的herehere 的答案中找到此解决方案的更多示例。 参考文献:this answer by Nipun Batrathis answer by BenBmatplotlib.dates

    【讨论】:

    • 感谢您的回答,我会记住您的方法,因为它看起来更容易处理几个月。我已将您的建议实施到我的脚本部分,但仍然无法弄清楚如何应对月底发生的周末错误:IndexError: index ... is out of bounds .. 任何建议,因为我不使用 @ 987654369@ ?或者这是唯一的方法
    • 我不知道有任何其他方便的方法来解决索引错误,因为axvspan 需要从某个地方获取xmax 值。现在,在您处理频率为 15 分钟的特定情况下,您可以简单地决定不包括导致错误的周末的最后 15 分钟,这在与我的示例相反的图中不明显 6 -小时频率。在上面的代码中,axvspan 循环可以像这样更改/中断:for idx in indices_weekends: if idx != len(df_month)-1: ax.axvspan(df_month.index[idx], df_month.index[idx+1],...)
    • 虽然我必须补充一点,除非有任何特殊原因阻止您使用 matplotlib 日期模块,如果我在你的鞋子里,我只会使用上面共享的代码,只需将 df 替换为Profiles.loc['2018']。通过将这种方法与mdates 一起使用,可以将代码重用于直到“day”的任何频率的时间序列,而不会出现周末垂直跨度不均匀的风险,除此之外,可以方便地格式化刻度.
    • @rclee 我已经更新了我的答案以提供更好的解决方案(参见方法 2)并使其更加规范。如果我的回答还没有帮助您解决问题,我很乐意提供更多解释。
    • 完美!我已经在我的脚本中实现了您之前的解决方案,因为它准确地再现了所需的结果。其他解决方案确实更短,这有利于计算时间。 [出于好奇,我仍然想知道我的代码中缺少什么以获得所需的结果......但我会把它留在我的待办事项列表中)再次感谢您的明确解释,非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-31
    • 1970-01-01
    • 2020-08-13
    • 2019-12-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多