【问题标题】:Python, Pandas: How to club rows rows of dataframe based on Start Time and End TimePython,Pandas:如何根据开始时间和结束时间对数据帧的行进行分组
【发布时间】:2020-09-25 15:22:41
【问题描述】:

所以我有一个这样的数据框:

Start Time  End Time        IDs
15:02:13    15:10:24        BAMB30
19:46:19    19:46:29        BHI110
19:47:01    19:57:04        BHI110
19:47:01    19:56:58        BHI110
19:47:01    19:56:59        BHI110
12:01:46    12:06:30        AKB286

我想将行分组或(行中的 ID)组合成一个名为“events”的东西,其定义如下:

  1. 一个事件的开始时差应该是

  2. 一个事件的结束时间差应该是

  3. 每个事件的 C1、C2 列中的值相同。

  4. 每个事件中的不同/唯一 ID。

  5. 仅当基于 1 和 2 的组大小>5 时,事件才有效。

所以我想要的最终结果是这样的:

Start Time  End Time             IDs             Event
    15:02:13    15:10:24        BAMB30      
    19:46:19    19:46:29        BHI110
    19:47:01    19:57:04        BHI110           1
    19:47:01    19:56:58        BHI110           1
    19:47:01    19:56:59        BHI110           1
    12:01:46    12:06:30        AKB286

我们想为事件提供数字。例如,如果某些行属于某个事件,我们将为事件列中的所有这些行赋予相同的编号。

那么如何进行呢?

编辑:

我按照您的建议尝试了代码:

def tDlt(row):
    st, et, sid, c1, c2 = row[['Start Time (HHMM)', 'End Time (HHMM)', 'IDs', 'C1', 'C2']]
    if(tDlt.start is None):
        tDlt.start, tDlt.end, tDlt.id, tDlt.c1, tDlt.c2, tDlt.ev = st, et, sid, c1, c2, 0
    else:
        if(((st - tDlt.start).total_seconds() > 120) or ((et - tDlt.end).total_seconds() > 120) or (sid == tDlt.id) or  (c1!=tDlt.c1) or (c2!=tDlt.c2)):
            tDlt.start, tDlt.end, tDlt.id, tDlt.c1, tDlt.c2 = st, et, sid, c1, c2
            tDlt.ev += 1
    return tDlt.ev

def gen_events():
    global df
    global sizeLimit
    x = list(df['Start Time (HHMM)'])
    y = list(df['End Time (HHMM)'])
    for i in range(len(x)):
        x[i] = str(x[i])

    for i in range(len(y)):
        y[i] = str(y[i])

    df['Start Time (HHMM)'] = x
    df['End Time (HHMM)'] = y

    df['Start Time (HHMM)'] = df['Start Time (HHMM)'].apply(pd.Timedelta)
    df['End Time (HHMM)'] = df['End Time (HHMM)'].apply(pd.Timedelta)  
    tDlt.start = None
    ev = df.sort_values(['Start Time (HHMM)', 'End Time (HHMM)','IDs']).apply(tDlt, axis=1)
    ev = ev.groupby(ev).transform(lambda grp: str(grp.iloc[0]) if grp.size > sizeLimit else '')
    df['Event'] = ev[ev != ''].groupby(ev, sort=False).ngroup() + 1
    df.Event.replace(np.nan, '', inplace=True)
    df = df.dropna(how='all')
    fs = filedialog.asksaveasfilename(filetypes=[("Excel files", ".xlsx .xls .xlsm"),("CSV files", ".csv")])
    if((str(fs))[-3:] == "csv"):
        df.to_csv(fs)
    else:
        df.to_excel(fs)
    done()

注意:我需要按不同的IDS(不一样)对事件进行分组,这就是我将(sid == tDlt.id)放在代码的else条件中的原因。

但是我在某些部分得到了不正确的结果:

如您所见,我希望单个事件具有不同的 ID,但它仍然使用一些相同的 ID 进行分组。代码哪里出错了?

注意:这些不是我的数据框中唯一的列。但是,在查找事件时,只有上述列很重要。 谢谢!

【问题讨论】:

  • 在您的预期结果中,您没有在组 1 中包含“19:47:01 19:56:59”行。为什么?另一个细节:这个组只有 2 行,但您希望一个事件至少包含 5 行。
  • 请更好地解释您的标准
  • 您的代码仅将组中的每一行与上一个行进行比较。但我认为: 1. 源组应首先按开始时间和结束时间排序(您的数据样本未排序)。 2. 一个组应该在一些“初始”行(不一定是第一个)中包含两个时间都在给定限制内的开始/结束时间的行。还要写出单个组(按ID)是否可以包含多个事件。
  • @Valdi_Bo "19:47:01 19:56:59",是的,抱歉,这也将包括在内。是的,它必须包含至少 5 行才能称为事件。我需要一个大数据框来说明这一点。我已经编辑过了。是的,单个组可以包含多个事件,因为每一行都可以重叠成 >1 个组。
  • @gtomer 我们需要将行分组为事件。每组的长度应大于 5,并且组的每一行的开始时间和结束时间最多相差 2 分钟(绝对差异)。

标签: python pandas


【解决方案1】:

为了提供一个更有启发性的例子,我采用了以下示例 源数据框:

   Start Time End Time     IDs  C1  C2
0    15:02:13 15:10:24  BAMB30  X9  Y9
1    19:46:19 19:46:29  BHI110  X9  Y9
2    19:47:01 19:57:04  BHI110  D2  F2
3    19:47:01 19:56:58  BHI110  D2  E2
4    19:47:01 19:56:59  BHI110  D2  E2
5    20:00:02 20:20:00  BHI110  G3  H3
6    20:01:03 20:21:16  BHI110  G3  H3
7    20:15:00 20:23:20  BHI110  X9  Y9
8    12:01:46 12:06:30  AKB286  A1  B1
9    12:02:48 12:06:50  AKB286  A1  B1
10   12:02:50 12:06:55  AKB286  A1  C1

我添加了 C1 和 C2 列(用于比较 当前组),根据您的评论。

由于 Start Time 和 End Time 列都是 string 类型, 第一步是将它们转换为Timedelta:

df['Start Time'] = df['Start Time'].apply(pd.Timedelta)
df['End Time'] = df['End Time'].apply(pd.Timedelta)

然后我定义了一个组的大小限制以被视为一个事件。 你写了这个限制 == 5,但是因为你和我的数据 样本只包含较小的组,我将此限制设置为 2:

sizeLimit = 2

当然,在您的真实数据上运行我的代码,将此限制更改为 你需要什么。

然后定义一个函数来检查当前之间的“时间增量” 行和“起始行”并生成“事件编号”:

def tDlt(row):
    st, et, c1, c2 = row[['Start Time', 'End Time', 'C1', 'C2']]
    if tDlt.start is None:
        tDlt.start, tDlt.end, tDlt.ev, tDlt.c1, tDlt.c2 = st, et, 0, c1, c2
    else:
        if ((st - tDlt.start).total_seconds() > 120)\
                or ((et - tDlt.end).total_seconds() > 120)\
                or (c1 != tDlt.c1) or (c2 != tDlt.c2):
            tDlt.start, tDlt.end, tDlt.c1, tDlt.c2 = st, et, c1, c2
            tDlt.ev += 1
    return tDlt.ev

由于使用了它的内部属性,它是一个“有记忆的函数”, 在 start 和 end 属性中保持各自的值 上一行和 ev 属性 - 事件编号。

此函数将应用于DataFrame的每一行,但之前 它,它的 start 属性将被设置为 None,以提供适当的处理 第一行。

注意设置了起始行:

  • 在第一行(当tDlt.start 为无时),
  • 在与“开始”行“时间太远”的每一行上或与 C1 或 C2 与“起始”行不同。

此函数生成连续的“事件编号”:

  • 从0开始,
  • 增加任何条件以继续当前组 尚未满足,
  • 适用于所有组,甚至那些低于大小限制的组。

主要处理运行如下:

  1. 在tDlt函数的start属性中设置“初始值”:

     tDlt.start = None
    
  2. 排序 df 并将 tDlt 应用于每一行:

     ev = df.sort_values(['Start Time', 'End Time']).apply(tDlt, axis=1)
    

    结果(对于我的数据样本)是:

     8     0
     9     0
     10    1
     0     2
     1     3
     3     4
     4     4
     2     5
     5     6
     6     6
     7     7
     dtype: int64
    

    当然,行顺序是不同的,由于之前的排序 应用程序。

    检查例如索引为 3、4 和 2 的行。第 3 行是最早的 从这个组。第 4 行在同一组内(所有条件都满足)。 但是行 2 在 C2 列中有不同的值,所以它开始一个新的 组。

  3. 下一步是取消“太小”组的组号:

     ev = ev.groupby(ev).transform(lambda grp: str(grp.iloc[0])
         if grp.size >= sizeLimit else '')
    

    步骤:

    • 获取每个组(按值)并检查其大小,
    • 如果它至少有 sizeLimit 行,则返回原始组 数字,但作为字符串(对于每一行),
    • 否则返回一个空字符串(也适用于每一行)- 实际取消。

    结果是:

     8     0
     9     0
     10     
     0      
     1      
     3     4
     4     4
     2      
     5     6
     6     6
     7      
     dtype: object
    
  4. 现在执行新列的“初始填充”:

     df['Event'] = ev[ev != ''].groupby(ev, sort=False).ngroup() + 1
    

    步骤:

    • 从 ev 中获取非空元素。
    • 对它们进行分组(按它们的值)。
    • 返回“全局”组号,从 1 开始。注意 “初始组号”(到目前为止计算)在这里更改 连续个数字。

    但这还不是最终的内容(在这个阶段打印df), 因为:

    • “太短”组的单元格包含 NaN,
    • 元素是 float 类型的。
  5. 要摆脱上述不足,运行:

     df.Event.replace(np.nan, '', inplace=True)
    

两次排序后的最终结果是:

   Start Time End Time     IDs  C1  C2 Event
8    12:01:46 12:06:30  AKB286  A1  B1     1
9    12:02:48 12:06:50  AKB286  A1  B1     1
10   12:02:50 12:06:55  AKB286  A1  C1      
0    15:02:13 15:10:24  BAMB30  X9  Y9      
1    19:46:19 19:46:29  BHI110  X9  Y9      
3    19:47:01 19:56:58  BHI110  D2  E2     2
4    19:47:01 19:56:59  BHI110  D2  E2     2
2    19:47:01 19:57:04  BHI110  D2  F2      
5    20:00:02 20:20:00  BHI110  G3  H3     3
6    20:01:03 20:21:16  BHI110  G3  H3     3
7    20:15:00 20:23:20  BHI110  X9  Y9      

如你所见:

  • 前 2 行在时间上足够接近,所以是事件 1。
  • 第三行的开始时间与上一行的距离太远, 所以它没有被包括在上面的组中。
  • 离下排也太远了,不能 分组在一个事件中。
  • 行 3 和 4 构成下一组。
  • 行 2 被排除,因为 C2 中的值不同。
  • 等等。

最有可能的是,您要检查是否相等的两列都可以 其他名称,所以将它们的实际名称放在 tDlt 中,而不是 C1 和 C2。

编辑以下关于按 ID 分组的评论

将函数改为:

def tDlt(row):
    id, st, et, c1, c2 = row[['IDs', 'Start Time', 'End Time', 'C1', 'C2']]
    if tDlt.start is None:
        tDlt.id, tDlt.start, tDlt.end, tDlt.ev, tDlt.c1, tDlt.c2 = id, st, et, 0, c1, c2
    else:
        if id != tDlt.id\
                or ((st - tDlt.start).total_seconds() > 120)\
                or ((et - tDlt.end).total_seconds() > 120)\
                or (c1 != tDlt.c1) or (c2 != tDlt.c2):
            tDlt.id, tDlt.start, tDlt.end, tDlt.c1, tDlt.c2 = id, st, et, c1, c2
            tDlt.ev += 1
    return tDlt.ev

tDlt.start = None之后的下一条指令改为:

ev = df.sort_values(['IDs', 'Start Time', 'End Time']).apply(tDlt, axis=1)

我想您为 IDs 添加了一个内部属性,但忘记排序 IDs 上的源 DataFrame。

为了测试这段代码,我添加了 2 行:

11   20:00:02   20:20:00  XXX110  G3  H3
12   20:01:03   20:21:16  XXX110  G3  H3

请注意,它们就像索引为 5 和 6 的行,但是 有不同的ID。

对于此类扩展数据,按ID、开始时间和结束时间排序的结果是:

   Start Time End Time     IDs  C1  C2 Event
8    12:01:46 12:06:30  AKB286  A1  B1     1
9    12:02:48 12:06:50  AKB286  A1  B1     1
10   12:02:50 12:06:55  AKB286  A1  C1      
0    15:02:13 15:10:24  BAMB30  X9  Y9      
1    19:46:19 19:46:29  BHI110  X9  Y9      
3    19:47:01 19:56:58  BHI110  D2  E2     2
4    19:47:01 19:56:59  BHI110  D2  E2     2
2    19:47:01 19:57:04  BHI110  D2  F2      
5    20:00:02 20:20:00  BHI110  G3  H3     3
6    20:01:03 20:21:16  BHI110  G3  H3     3
7    20:15:00 20:23:20  BHI110  X9  Y9      
11   20:00:02 20:20:00  XXX110  G3  H3     4
12   20:01:03 20:21:16  XXX110  G3  H3     4

所以 XXX110 中的行是单独事件的成员。

【讨论】:

  • 我运行了上面的代码。它将具有相同名称的 ID 分组,然后检查开始时间和结束时间条件。我想要的是将行作为一个整体进行分组。事件不需要具有相同名称的 ID。它所拥有的只是 ROWS 具有相同的开始时间和相同的结束时间,+/- 2 分钟。加大码>5。谢谢!
  • 我修改了我的答案,因此不会按 IDs 进行分组。现在更短了,没有“两级”分组,这是一个难以追踪的操作。
  • 您是否也考虑过一行也可以属于多个事件?谢谢。
  • 感谢代码和解释。现在,如果我想根据两个完全相同的附加列值(除了开始时间和结束时间)进一步隔离事件,我该如何修改这段代码?再次感谢
  • 我认为,这对于另一个问题来说相当重要。您还应该更准确地描述这些新列是什么,并包括一些源数据和预期结果的示例。它也不清楚你的意思是写“隔离”。可能它应该是一个新的专栏。在新问题中描述所有细节。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-24
  • 2018-01-03
  • 2010-11-22
相关资源
最近更新 更多