【问题标题】:Pandas DataFrame groupby based on conditionPandas DataFrame groupby 基于条件
【发布时间】:2019-04-10 22:47:58
【问题描述】:

我发现的最相似的问题是here,但没有正确答案。

基本上我有一个问题,我试图在数据帧上使用 groupby 来为公交路线生成唯一的 ID。问题是,有时(尽管很少)我拥有的数据对于我的 groupby 列具有相同的值,因此即使它们不是,它们也被视为同一总线。

我能想到的唯一另一种方法是根据另一个名为“停止类型”的列对巴士进行分组,其中有一个指示开始、中间和结束。我想使用 groupby 基于此列创建组,其中每个组从“停止类型”= 开始的位置开始,在“停止类型”= 结束的位置结束。

考虑以下数据:

df = pd.DataFrame({'Vehicle_ID': ['A']*18,
    'Position': ['START', 'MID', 'MID', 'END', 'MID', 'START']*3)})

   Cond   Position
0     A   START
1     A   MID  
2     A   MID   
3     A   END    
4     A   MID    
5     A   START   
6     A   START   
7     A   MID    
8     A   MID    
9     A   END    
10    A   MID   
11    A   START    
12    A   START    
13    A   MID    
14    A   MID    
15    A   END     
16    A   MID    
17    A   START

我想出的将这些总线准确分组在一起的唯一方法是生成一个带有总线序列 ID 的附加列,但鉴于我正在处理大量数据,这不是一个非常有效的解决方案。如果可能的话,我希望能够管理我想要使用单个 groupby 执行的操作,以便生成以下输出

   Cond   Position   Group
0     A   START      1
1     A   MID        1
2     A   MID        1
3     A   END        1
4     A   MID        
5     A   START      2
6     A   START      2
7     A   MID        2
8     A   MID        2
9     A   END        2 
10    A   MID        
11    A   START      3
12    A   START      3 
13    A   MID        3
14    A   MID        3
15    A   END        3 
16    A   MID        
17    A   START      4

【问题讨论】:

  • 为什么4 A MID 没有组号?

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


【解决方案1】:

一个想法是通过np.select 分解,然后通过numba 使用自定义循环:

from numba import njit

df = pd.DataFrame({'Vehicle_ID': ['A']*18,
                   'Position': ['START', 'MID', 'MID', 'END', 'MID', 'START']*3})

@njit
def grouper(pos):
    res = np.empty(pos.shape)
    num = 1
    started = 0
    for i in range(len(res)):
        current_pos = pos[i]
        if (started == 0) and (current_pos == 0):
            started = 1
            res[i] = num
        elif (started == 1) and (current_pos == 1):
            started = 0
            res[i] = num
            num += 1
        elif (started == 1) and (current_pos in [-1, 0]):
            res[i] = num
        else:
            res[i] = 0
    return res

arr = np.select([df['Position'].eq('START'), df['Position'].eq('END')], [0, 1], -1)

df['Group'] = grouper(arr).astype(int)

结果:

print(df)

   Position Vehicle_ID  Group
0     START          A      1
1       MID          A      1
2       MID          A      1
3       END          A      1
4       MID          A      0
5     START          A      2
6     START          A      2
7       MID          A      2
8       MID          A      2
9       END          A      2
10      MID          A      0
11    START          A      3
12    START          A      3
13      MID          A      3
14      MID          A      3
15      END          A      3
16      MID          A      0
17    START          A      4

在我看来,您应该包含“空白”值,因为这会强制您的系列为object dtype,对于任何后续处理都效率低下。如上,您可以改用0

性能基准测试

numba 比一种纯 Pandas 方法快约 10 倍:-

import pandas as pd, numpy as np
from numba import njit

df = pd.DataFrame({'Vehicle_ID': ['A']*18,
                   'Position': ['START', 'MID', 'MID', 'END', 'MID', 'START']*3})


df = pd.concat([df]*10, ignore_index=True)

assert joz(df.copy()).equals(jpp(df.copy()))

%timeit joz(df.copy())  # 18.6 ms per loop
%timeit jpp(df.copy())  # 1.95 ms per loop

基准函数:

def joz(df):
    # identification of sequences
    df['Position_Prev'] = df['Position'].shift(1)
    df['Sequence'] = 0
    df.loc[(df['Position'] == 'START') & (df['Position_Prev'] != 'START'), 'Sequence'] = 1
    df.loc[df['Position'] == 'END', 'Sequence'] = -1
    df['Sequence_Sum'] = df['Sequence'].cumsum()
    df.loc[df['Sequence'] == -1, 'Sequence_Sum'] = 1

    # take only items between START and END and generate Group number
    df2 = df[df['Sequence_Sum'] == 1].copy()
    df2.loc[df['Sequence'] == -1, 'Sequence'] = 0
    df2['Group'] = df2['Sequence'].cumsum()

    # merge results to one dataframe
    df = df.merge(df2[['Group']], left_index=True, right_index=True, how='left')
    df['Group'] = df['Group'].fillna(0)
    df['Group'] = df['Group'].astype(int)
    df.drop(['Position_Prev', 'Sequence', 'Sequence_Sum'], axis=1, inplace=True)    
    return df

@njit
def grouper(pos):
    res = np.empty(pos.shape)
    num = 1
    started = 0
    for i in range(len(res)):
        current_pos = pos[i]
        if (started == 0) and (current_pos == 0):
            started = 1
            res[i] = num
        elif (started == 1) and (current_pos == 1):
            started = 0
            res[i] = num
            num += 1
        elif (started == 1) and (current_pos in [-1, 0]):
            res[i] = num
        else:
            res[i] = 0
    return res

def jpp(df):
    arr = np.select([df['Position'].eq('START'), df['Position'].eq('END')], [0, 1], -1)
    df['Group'] = grouper(arr).astype(int)
    return df

【讨论】:

  • 这不是非常有效的解决方案。你应该避免循环。尝试将您的代码与数百万行一起使用
  • @JozefCechovsky,你自己计时了吗?你会发现numba + for 循环通常比 Pandas 内置方法快得多。
  • @JozefCechovsky,我在我的解决方案中添加了一些基准测试代码,您可以将其用于测试。 numba 大约快 10 倍。
  • 非常有趣的方法。看起来远远超出了我的水平,但我肯定会收藏它以供将来参考,以前从未听说过 numba,谢谢!
【解决方案2】:

我有一些解决方案。您必须避免循环并尝试使用滑动、切片和合并。

这是我的第一个原型(应该重构)

# identification of sequences
df['Position_Prev'] = df['Position'].shift(1)
df['Sequence'] = 0
df.loc[(df['Position'] == 'START') & (df['Position_Prev'] != 'START'), 'Sequence'] = 1
df.loc[df['Position'] == 'END', 'Sequence'] = -1
df['Sequence_Sum'] = df['Sequence'].cumsum()
df.loc[df['Sequence'] == -1, 'Sequence_Sum'] = 1

# take only items between START and END and generate Group number
df2 = df[df['Sequence_Sum'] == 1].copy()
df2.loc[df['Sequence'] == -1, 'Sequence'] = 0
df2['Group'] = df2['Sequence'].cumsum()

# merge results to one dataframe
df = df.merge(df2[['Group']], left_index=True, right_index=True, how='left')
df['Group'] = df['Group'].fillna(0)
df['Group'] = df['Group'].astype(int)
df.drop(columns=['Position_Prev', 'Sequence', 'Sequence_Sum'], inplace=True)
df

结果:

Vehicle_ID Position  Group
0           A    START      1
1           A      MID      1
2           A      MID      1
3           A      END      1
4           A      MID      0
5           A    START      2
6           A    START      2
7           A      MID      2
8           A      MID      2
9           A      END      2
10          A      MID      0
11          A    START      3
12          A    START      3
13          A      MID      3
14          A      MID      3
15          A      END      3
16          A      MID      0
17          A    START      4

【讨论】:

    猜你喜欢
    • 2017-04-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-23
    • 2020-04-26
    • 2022-01-16
    • 2021-10-19
    • 2019-02-08
    相关资源
    最近更新 更多