【问题标题】:Pandas sum time interval in a group excluding overlaps熊猫在一个组中求和时间间隔,不包括重叠
【发布时间】:2020-02-22 11:44:47
【问题描述】:

我有一个数据框,其中每一行都有一个开始时间和一个结束时间。每行也有一个他们所属的组。我想要一个新列,以秒为单位给出该组中所有时间的总和。

例如,如果我们有一个如下所示的组:

         id1:    |----|
         id2:       |-----|
         id3:                      |--------|
                 .  .  .  .  .  .  .  .  .  .  .
time ->        12:00    12:04    12:07    12:10

那么对于属于该组的所有行,总时间为 4+3 min = 420 秒

如果它们完全重叠,那么我们会得到这样的场景:

         id1:    |--------|
         id2:    |--------|
                 .  .  .  .  .  .  .  .  .  .  .
time ->        12:00    12:04    12:07    12:10

这将为我们提供 4 分钟 = 240 秒的结果。

以下是一些虚拟数据:


import pandas as pd

ids = [x for x in range(10)]
group = [0, 1, 1, 2, 2, 3, 4, 4, 4, 4]

start = pd.to_datetime(["2019-10-21-16:20:00", "2019-10-21-16:22:00", "2019-10-21-16:22:00", "2019-10-21-16:15:00",
         "2019-10-21-16:22:00", "2019-10-21-16:58:00", "2019-10-21-17:02:00", "2019-10-21-17:03:00",
         "2019-10-21-17:04:00", "2019-10-21-17:20:00"])

end = pd.to_datetime(["2019-10-21-16:25:00", "2019-10-21-16:24:00", "2019-10-21-16:24:00", "2019-10-21-16:18:00",
       "2019-10-21-16:26:00", "2019-10-21-17:02:00", "2019-10-21-17:06:00", "2019-10-21-17:07:00",
       "2019-10-21-17:08:00", "2019-10-21-17:22:00"])

cols = ["id", "group", "start", "end"]


df = pd.DataFrame(dict(zip(cols, [ids, group, start, end])))

到目前为止,我尝试过的方法显然不正确。我尝试分组,然后找到该组的每个开始和结束的最小值和最大值,然后将该间隔设置为总和。这种方法是不正确的,因为它还会在间隔中包含间隙。

gr = df.groupby("group").apply(lambda x : x.end.max() - x.start.min())
df['total_time'] = df.group.map(gr)

【问题讨论】:

  • 到目前为止你自己尝试过什么?
  • 我尝试先按“组”对它们进行分组,然后找到该组的每个开始和结束的最小值和最大值,然后将该间隔设置为总和。这种方法不会涵盖不重叠的情况,因为它也包括“空”。

标签: python python-3.x pandas pandas-groupby


【解决方案1】:

首先,添加一个跟踪迄今为止看到的最新结束时间的列(但仅考虑同一组):

df['notbefore'] = df.groupby('group').end.shift().cummax()

它被移动了 1 以反映前几行(不包括同一行)上看到的最晚结束时间。在cummax() 之前有shift() 很重要,否则转换会在组之间“泄漏”值。

然后添加一个包含“有效”开始时间的列:

df['effstart'] = df[['start', 'notbefore']].max(1)

这是修改的开始时间,使其不早于任何先前的结束时间(以避免重叠)。

然后计算覆盖的总秒数:

df['effsec'] = (df.end - df.effstart).clip(np.timedelta64(0))

df 现在是:

   id  group               start                 end           notbefore            effstart   effsec
0   0      0 2019-10-21 16:20:00 2019-10-21 16:25:00                 NaT 2019-10-21 16:20:00 00:05:00
1   1      1 2019-10-21 16:22:00 2019-10-21 16:24:00                 NaT 2019-10-21 16:22:00 00:02:00
2   2      1 2019-10-21 16:22:00 2019-10-21 16:24:00 2019-10-21 16:24:00 2019-10-21 16:24:00 00:00:00
3   3      2 2019-10-21 16:15:00 2019-10-21 16:18:00                 NaT 2019-10-21 16:15:00 00:03:00
4   4      2 2019-10-21 16:22:00 2019-10-21 16:26:00 2019-10-21 16:24:00 2019-10-21 16:24:00 00:02:00
5   5      3 2019-10-21 16:58:00 2019-10-21 17:02:00                 NaT 2019-10-21 16:58:00 00:04:00
6   6      4 2019-10-21 17:02:00 2019-10-21 17:06:00                 NaT 2019-10-21 17:02:00 00:04:00
7   7      4 2019-10-21 17:03:00 2019-10-21 17:07:00 2019-10-21 17:06:00 2019-10-21 17:06:00 00:01:00
8   8      4 2019-10-21 17:04:00 2019-10-21 17:08:00 2019-10-21 17:07:00 2019-10-21 17:07:00 00:01:00
9   9      4 2019-10-21 17:20:00 2019-10-21 17:22:00 2019-10-21 17:08:00 2019-10-21 17:20:00 00:02:00

要获得最终结果:

df.groupby('group').effsec.sum()

这给了你:

group
0   00:05:00
1   00:02:00
2   00:05:00
3   00:04:00
4   00:08:00

【讨论】:

    【解决方案2】:

    使用-

    def merge_intervals(intervals):
        sorted_by_lower_bound = sorted(intervals, key=lambda tup: tup[0])
        merged = []
    
        for higher in sorted_by_lower_bound:
            if not merged:
                merged.append(higher)
            else:
                lower = merged[-1]
                # test for intersection between lower and higher:
                # we know via sorting that lower[0] <= higher[0]
                if higher[0] <= lower[1]:
                    upper_bound = max(lower[1], higher[1])
                    merged[-1] = (lower[0], upper_bound)  # replace by merged interval
                else:
                    merged.append(higher)
        return merged
    
    df['dt'] = df[['start', 'end']].apply(tuple, axis=1)
    op = df.groupby(['group'])['dt'].apply(list)
    f_op = op.apply(merge_intervals)
    
    op_d = f_op.apply(lambda x: sum([(y[1]-y[0]).seconds for y in x]))
    

    输出

    group
    0    300
    1    120
    2    420
    3    240
    4    480
    

    【讨论】:

    • 这在 Python 中有多个嵌套的“for”循环,因此与我发布的原生 Pandas 解决方案相比,它会非常慢。
    【解决方案3】:

    作为源数据,我采用了以下 DataFrame:

      group             start               end
    0    G1  2019-09-01 12:00  2019-09-01 12:02
    1    G1  2019-09-01 12:01  2019-09-01 12:04
    2    G1  2019-09-01 12:07  2019-09-01 12:10
    3    G2  2019-09-01 12:05  2019-09-01 12:12
    4    G2  2019-09-01 12:10  2019-09-01 12:15
    

    第一步是在一组行中定义一个计算秒数的函数:

    def getSecs(grp):
        return pd.DatetimeIndex([]).union_many([ pd.date_range(
            row.start, row.end, freq='s', closed='left')
                for _, row in grp.iterrows() ]).size
    

    然后将这个函数应用到每个组,按group分组:

    secs = df.groupby('group').apply(getSecs).rename('secs')
    

    对于我的测试数据,结果是:

    group
    G1    420
    G2    600
    Name: secs, dtype: int64
    

    最后一步是通过与secs合并在df中创建一个新列:

    df = df.merge(secs, left_on='Grp', right_index=True)
    

    结果是:

      group             start               end  secs
    0    G1  2019-09-01 12:00  2019-09-01 12:02   420
    1    G1  2019-09-01 12:01  2019-09-01 12:04   420
    2    G1  2019-09-01 12:07  2019-09-01 12:10   420
    3    G2  2019-09-01 12:05  2019-09-01 12:12   600
    4    G2  2019-09-01 12:10  2019-09-01 12:15   600
    

    一个相当简洁的解决方案,仅仅6行代码,大大少于 其他一些解决方案。

    另请注意,我的解决方案会创建一个新列,等于 每个组中所有行的值(其他解决方案之一失败 这个细节)。 所有其他解决方案都停止计算每组的秒数。

    【讨论】:

      【解决方案4】:

      假设您的数据框已排序,那么这样的事情呢?

      In [1]: import datetime 
              def calc_periods(x):
                  time_delt = datetime.timedelta()
                  for i in x.index:
                      if (i > x.index[0]):
                          if x.loc[i].start < x.loc[i-1].end:
                              time_delt += x.loc[i].end - x.loc[i-1].end
                          else:
                              time_delt += x.loc[i].end - x.loc[i].start
                      else:
                          time_delt += x.loc[i].end - x.loc[i].start
                  return time_delt.seconds
      
      
      In [2]: df.groupby('group')[['start', 'end']].apply(calc_periods)
      Out[2]: group
              0    300
              1    120
              2    420
              3    240
              4    480
              dtype: int64
      

      【讨论】:

      • 非常干净整洁!
      • 现在编辑返回秒而不是分钟
      猜你喜欢
      • 1970-01-01
      • 2021-12-01
      • 2014-11-17
      • 2023-01-10
      • 1970-01-01
      • 1970-01-01
      • 2016-10-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多