按天重新采样并分类具有日期时间开始和日期时间结束的 DataFrame

Resampling by day and category a DataFrame that have datetime start and datetime end

问题

给定一个 table (DataFrame) 事件,其中每个事件(行)都有其开始日期时间和停止日期时间以及事件类别。

如何将此 table 转换为 table,其中每一行都是所有日期和类别的组合,以及这一天事件类别的相关小时数?

例子

也许看例子比解释问题更容易:

我想转换这个DataFrame

datetime_start datetime_end event_category
2021-01-0110:30:00 2021-01-0316:30:00 'A'
2021-01-0109:00:00 2021-01-0115:30:00 'B'
2021-01-0122:00:00 2021-01-0123:00:00 'B'

进入这个DataFrame

日期 event_category sum_of_hours_with_event_active
2021-01-01 'A' 13.5
2021-01-01 'B' 7.5
2021-01-02 'A' 24
2021-01-02 'B' 0
2021-01-03 'A' 16.5
2021-01-03 'B' 0

如果您确定同一事件类别中的同一天没有重叠的时间段(或者您想重复计算这些时间段),那么您可以按事件类别创建所有日期的基础并合并您的时间跨度到该 DataFrame。

然后通过剪裁减去,我们可以计算出该事件仅在当天贡献的总时间(生成的负值与当天不对应,因此它们被剪裁为 0)。最后,我们可以在一天内按事件 sum

import pandas as pd

# Enumerate all categories for every day. 
dfb = pd.merge(pd.DataFrame({'event_category': df['event_category'].unique()}),
               pd.DataFrame({'date': pd.date_range(df.datetime_start.dt.normalize().min(),
                                                   df.datetime_end.dt.normalize().max(), freq='D')}),
               how='cross')

# Merge timespans 
m = dfb.merge(df, on='event_category')

# Calculate time for that day
m['sum_hours'] = ((m['datetime_end'].clip(upper=m['date']+pd.offsets.DateOffset(days=1))
                   - m['datetime_start'].clip(lower=m['date']))
                   .clip(lower=pd.Timedelta(0)))

# Sum of hours for event by day
m = (m.groupby(['event_category', 'date'])['sum_hours']
      .sum().dt.total_seconds().div(3600)
      .reset_index())

print(m)
  event_category       date  sum_hours
0              A 2021-01-01       13.5
1              A 2021-01-02       24.0
2              A 2021-01-03       16.5
3              B 2021-01-01        7.5
4              B 2021-01-02        0.0
5              B 2021-01-03        0.0

数据

import pandas as pd

start_times = pd.DatetimeIndex(['2021-01-01 10:30:00', '2021-01-01 09:00:00', '2021-01-01 22:00:00'])
end_times = pd.DatetimeIndex(['2021-01-03 16:30:00', '2021-01-01 15:30:00', '2021-01-01 23:00:00'])
categories = ['A', 'B', 'B']
df = pd.DataFrame({'datetime_start': start_times, 'datetime_end': end_times, 'event_category': categories})

回答

首先我们 groupby event_category 这样 apply 每个类别都起作用。两个系列的串联表示事件的变化,即事件的开始和结束。如果同一类别中有多个事件同时开始或结束,则需要 apply 内的 groupbysum。累积总和 (cumsum) 给出发生变化时的事件总数,即一个或多个事件开始或结束时的事件总数。接下来我们使用 asfreq 上采样到所需的频率。这应该至少等于数据的时间粒度。最后我们再次重新采样(使用 groupbyGrouper 对象实现)和 sum.

本质上,我们是在计算每个类别中所有事件占用的时间段数,然后乘以时间段的长度(示例中为半小时),然后按天分组。 DateOffset 对象用于参数化周期。

step = pd.DateOffset(hours=0.5)  # Half hour steps
df.groupby('event_category') \
  .apply(lambda x: pd.concat([pd.Series(1, x['datetime_start']),
                              pd.Series(-1, x['datetime_end'])]) \
         .groupby(level=0) \
         .sum() \
         .cumsum() \
         .asfreq(step, method='ffill')
        ) \
  .groupby([pd.Grouper(level=0), pd.Grouper(level=1, freq='D')]) \
  .sum() * step.hours

这将适用于同一类别中的重叠事件。

结果

event_category
A               2021-01-01    13.5
                2021-01-02    24.0
                2021-01-03    16.5
B               2021-01-01     7.5
dtype: float64