Pandas按时间窗口分组

8 投票
3 回答
10492 浏览
提问于 2025-04-18 00:43

编辑:通过分析日志文件生成会话的内容似乎正是我想要的。

我有一个数据框,里面有重复的时间戳,我想按时间段把它们分组。基本的逻辑是:

1) 从每个时间戳开始,向前和向后各加n分钟,创建一个时间范围。

2) 将重叠的时间范围进行分组。最终的效果是,时间窗口可以小到一个时间戳加减时间缓冲,但只要多个事件之间的距离小于时间缓冲,时间窗口就没有上限。

我觉得使用 df.groupby(pd.TimeGrouper(minutes=n)) 可能是正确的答案,但我不知道如何让 TimeGrouper 在看到事件在时间缓冲内时创建动态的时间范围。

举个例子,如果我对一组事件使用 TimeGrouper('20s'),这些事件是:10:34:00、10:34:08、10:34:08、10:34:15、10:34:28 和 10:34:54,那么 pandas 会给我三个组(事件分别在 10:34:00 - 10:34:20、10:34:20 - 10:34:40 和 10:34:40 - 10:35:00 之间)。我希望只得到两个组,10:34:00 - 10:34:28,因为在这个时间范围内事件之间的间隔不超过 20 秒,第二组是 10:34:54。

找到不固定的时间范围的最佳方法是什么?

假设有一个类似于以下的序列:

      time
0     2013-01-01 10:34:00+00:00
1     2013-01-01 10:34:12+00:00
2     2013-01-01 10:34:28+00:00
3     2013-01-01 10:34:54+00:00
4     2013-01-01 10:34:55+00:00
5     2013-01-01 10:35:19+00:00
6     2013-01-01 10:35:30+00:00

如果我对这个序列使用 df.groupby(pd.TimeGrouper('20s')),我会得到 5 个组,10:34:00-:20、:20-:40、:40-10:35:00 等等。我想要的是有一个函数可以创建弹性的时间范围……只要事件在 20 秒内,就扩展时间范围。所以我希望得到:

2013-01-01 10:34:00 - 2013-01-01 10:34:48 
    0 2013-01-01 10:34:00+00:00
    1 2013-01-01 10:34:12+00:00
    2 2013-01-01 10:34:28+00:00

2013-01-01 10:34:54 - 2013-01-01 10:35:15
    3 2013-01-01 10:34:54+00:00
    4 2013-01-01 10:34:55+00:00

2013-01-01 10:35:19 - 2013-01-01 10:35:50
    5 2013-01-01 10:35:19+00:00
    6 2013-01-01 10:35:30+00:00

谢谢。

3 个回答

0

试试这个:

  • 创建一个叫 tsdiff 的列,用来存放连续时间之间的差值(可以用 shift 函数)
  • df['new_group'] = df.tsdiff > timedelta 来判断差值是否大于某个时间段
  • new_group 列使用 fillna 来填补空值
  • 根据这个新列进行分组 groupby

这只是一个很粗略的伪代码,但解决方案就在里面某个地方...

1

你可能想考虑使用 apply 这个功能:

def my_grouper(datetime_value):
    return some_group(datetime_value)

df.groupby(df['date_time'].apply(my_grouper))

在你的分组函数中,你可以自由实现任何分组逻辑。顺便提一下,合并重叠的时间范围其实是一个需要反复处理的任务:比如,A = (0, 10),B = (20, 30),C = (10, 20)。当C出现后,A、B和C这三个范围应该被合并在一起。

更新:

这是我写的一个不太优雅的合并算法:

groups = {}

def in_range(val, begin, end):
    return begin <= val <= end

global max_group_id
max_group_id = 1

def find_merged_group(begin, end):
    global max_group_id
    found_common_group = None
    full_wraps = []

    for (group_start, group_end), group in groups.iteritems():
        begin_inclusion = in_range(begin, group_start, group_end)
        end_inclusion = in_range(end, group_start, group_end)
        full_inclusion = begin_inclusion and end_inclusion
        full_wrap = not begin_inclusion and not end_inclusion and in_range(group_start, begin, end) and in_range(group_end, begin, end)
        if full_inclusion:
            groups[(begin, end)] = group
            return group
        if full_wrap:
            full_wraps.append(group)
        elif begin_inclusion or end_inclusion:
            if not found_common_group:
                 found_common_group = group
            else:  # merge
                for range, g in groups.iteritems():
                    if g == group:
                        groups[range] = found_common_group

    if not found_common_group:
        found_common_group = max_group_id
        max_group_id += 1
    groups[(begin, end)] = found_common_group
    return found_common_group

def my_grouper(date_time):
    return find_merged_group(date_time - 1, date_time + 1)

df['datetime'].apply(my_grouper) # first run to fill groups dict
grouped = df.groupby(df['datetime'].apply(my_grouper))  # this run is using already merged groups
9

下面是如何创建一个自定义分组器的方法。(需要使用 pandas 版本大于等于 0.13)来进行时间差的计算,但在其他版本中也能工作。

首先,创建你的数据序列。

In [31]: s = Series(range(6),pd.to_datetime(['20130101 10:34','20130101 10:34:08', '20130101 10:34:08', '20130101 10:34:15', '20130101 10:34:28', '20130101 10:34:54','20130101 10:34:55','20130101 10:35:12']))

In [32]: s
Out[32]: 
2013-01-01 10:34:00    0
2013-01-01 10:34:08    1
2013-01-01 10:34:08    2
2013-01-01 10:34:15    3
2013-01-01 10:34:28    4
2013-01-01 10:34:54    5
2013-01-01 10:34:55    6
2013-01-01 10:35:12    7
dtype: int64

这段代码只是计算了连续元素之间的时间差,单位是秒,但实际上可以计算任何东西。

In [33]: indexer = s.index.to_series().order().diff().fillna(0).astype('timedelta64[s]')

In [34]: indexer
Out[34]: 
2013-01-01 10:34:00     0
2013-01-01 10:34:08     8
2013-01-01 10:34:08     0
2013-01-01 10:34:15     7
2013-01-01 10:34:28    13
2013-01-01 10:34:54    26
2013-01-01 10:34:55     1
2013-01-01 10:35:12    17
dtype: float64

随意将时间差小于 20 秒的分到组 0,其他的分到组 1。这种分组方式也可以更随意。如果与前一个元素的时间差小于 0,但从第一个元素到现在的总时间差大于 50 秒,就把它分到组 2。

In [35]: grouper = indexer.copy()

In [36]: grouper[indexer<20] = 0

In [37]: grouper[indexer>20] = 1

In [95]: grouper[(indexer<20) & (indexer.cumsum()>50)] = 2

In [96]: grouper
Out[96]: 
2013-01-01 10:34:00    0
2013-01-01 10:34:08    0
2013-01-01 10:34:08    0
2013-01-01 10:34:15    0
2013-01-01 10:34:28    0
2013-01-01 10:34:54    1
2013-01-01 10:34:55    2
2013-01-01 10:35:12    2
dtype: float64

进行分组(这里也可以使用 apply 方法)。

In [97]: s.groupby(grouper).sum()
Out[97]: 
0    10
1     5
2    13
dtype: int64

撰写回答