基于前一行的 Pandas 数据框列

6
我有一个以下数据框:
         id  action   
         ================
         10   CREATED   
         10   111
         10   222
         10   333
         10   DONE      
         10   222
         10   UPDATED   
         777  CREATED    
         10   333
         10   DONE      

我想创建一个名为"check"的新列,该列将基于数据框中先前行的数据:

  1. 查找动作列中的单元格="DONE"
  2. 在DONE之前的先前行中搜索具有相同id的第一个CREATED或UPDATED。如果是CREATED,则放置C;如果是UPDATED则放置U。

输出:

         id  action   check
         ================
         10   CREATED   
         10   111
         10   222
         10   333
         10   DONE      C
         10   222
         10   UPDATED   
         777  CREATED    
         10   333
         10   DONE      U

我尝试使用多个if条件,但是没能成功。你能帮忙吗?


每个id是否可以有多个DONE值? - Erfan
是的,我们可以有多个相同id的DONE状态,但在每个DONE之前必须有该id的CREATED或UPDATED状态。 - johnt
3个回答

1
考虑以下更复杂的示例数据框来进行说明:
# print(df)
id  action   
10   CREATED   
10   111
10   222
10   333
10   DONE      
10   222
10   UPDATED   
777  CREATED    
10   333
10   DONE
777  DONE
10   CREATED
10   DONE
11   UPDATED
11   DONE     

使用:

transformer = lambda s: s[(s.eq('CREATED') | s.eq('UPDATED')).cumsum().idxmax()]

grouper = (
    lambda g: g.groupby(
        g['action'].eq('DONE').cumsum().shift().fillna(0))['action']
    .transform(transformer)
)

df['check'] = df.groupby('id').apply(grouper).droplevel(0).str[0]
df.loc[df['action'].ne('DONE'), 'check'] = ''

解释:
首先,我们根据“id”对数据框进行分组,并应用“grouper”函数,然后对每个分组的数据框进一步按照动作列中第一次出现的“DONE”进行分组,因此本质上我们将这个分组的数据框分成多个部分,每个部分都由动作列中的“DONE”值分隔开。然后,我们使用“transformer”lambda函数根据在动作列中前面的第一个值(“CREATED”或“UPDATED”)来转换每个分割的数据框。
结果:
# print(df)
     id   action check
0    10  CREATED      
1    10      111      
2    10      222      
3    10      333      
4    10     DONE     C
5    10      222      
6    10  UPDATED      
7   777  CREATED      
8    10      333      
9    10     DONE     U
10  777     DONE     C
11   10  CREATED      
12   10     DONE     C
13   11  UPDATED      
14   11     DONE     U

它将在此处失败:https://justpaste.it/2vkql。如果将逻辑应用于连续的“完成”,则会失败。 - Pygirl
我猜不会,因为在777组中的DONE之前,第一个值应该是C。 - Shubham Sharma
而且根据 OP 的要求,同一 ID 下不能连续出现两个“DONE”。 - Shubham Sharma
我没明白。第四行也要考虑进去吗? - Pygirl
我明白了,你认为第一个值是从顶部开始的,但我是从底部取第一个值。我想这需要由OP澄清。 - Shubham Sharma

0

我不知道这是否是最佳答案,但我尝试创建自己的逻辑来解决这个问题。

1)获取执行操作的行的索引:

m = df.groupby(['id'])['action'].transform(list).eq('DONE')
idx = df[m].index.values.tolist()

df[m]:

    id  action
4   10  DONE
9   10  DONE

idx:

[4, 9]

2) 按ID分组并索引所有Action为CREATED或UPDATED的行

n = df.groupby(['id'])['action'].transform(list).str.contains('CREATED|UPDATED', case=False)

n_idx = df[n].index

df[n]:

    id  action
0   10  CREATED
6   10  UPDATED
7   777 CREATED

n_idx:

Int64Index([0, 6, 7], dtype='int64')

3) 在新的列中填充空字符串"check":

df['check'] = ''

4) 现在你有两个索引,一个是为DONE,另一个是为CREATED/UPDATED。 现在你必须检查之前的行是否有任何CREATED/UPDATED,记住它们应该具有相同的ID。

ix = [0] + idx # <-- [0, 4, 9]
for a in list(zip(ix, ix[1:])): # <--- will create range (0,4), (4,9)
    for j in (n_idx):
        if j in range(a[0], a[1]): # <--- compare if CREATED/UPDATED indexes fall in this range. (checking previous row) and break if get any of them
            if (df.iloc[a[1]].id==df.iloc[j].id): # <--  check for id
                df.loc[a[1],'check'] = df.loc[j,'action'][0] # <--- assign Action
                break

最终输出:

df:

    id  action  check
0   10  CREATED 
1   10  111 
2   10  222 
3   10  333 
4   10  DONE    C
5   10  222 
6   10  UPDATED 
7   777 CREATED 
8   10  333 
9   10  DONE    U

完整代码:

m = df.groupby(['id'])['action'].transform(list).eq('DONE')
idx = df[m].index.values.tolist()
n = df.groupby(['id'])['action'].transform(list).str.contains('CREATED|UPDATED', case=False)
n_idx = df[n].index
ix = [0] + idx
df['check'] = ''

for a in list(zip(ix, ix[1:])):
    for j in (n_idx):
        if (j in range(a[0], a[1]+1)) and (df.iloc[a[1]].id==df.iloc[j].id):
            df.loc[a[1],'check'] = df.loc[j,'action'][0]
            break

带结果的示例数据:

    id  action  check
0   10  CREATED 
1   10  111 
2   10  DONE    C
3   10  333 
4   10  DONE    
5   10  222 
6   10  UPDATED 
7   777 CREATED 
8   777 DONE    C
9   10  DONE    

    id  action  check
0   10  CREATED 
1   10  111 
2   10  DONE    C
3   10  333 
4   777 UPDATED 
5   10  222 
6   10  UPDATED 
7   777 CREATED 
8   777 DONE    U
9   10  DONE    

0

这是一个循环的解决方案,虽然不是最优的,但可以完成任务。

假设您的数据框按时间排序,并且您有一个包含2列['id','action']和整数索引range(N)(其中N是列数)的数据框,则:

df['check'] = ''
for i, action in zip(df.index, df['action']):
    if action == 'DONE':
        action_id = df.loc[i, 'id']
        prev_action = df.iloc[:i].loc[(df['id'] == action_id) & 
                      (df['action'].isin(['CREATED', 'UPDATED'])), 'action'].iloc[-1]
        if prev_action == 'CREATED':
            df.loc[i, 'check'] = 'C'
        elif prev_action == 'UPDATED':
            df.loc[i, 'check'] = 'U'

基本上我们循环执行操作,找到df['action'] == 'DONE'的情况,然后获取与该操作相关联的ID,然后通过调用df.iloc[:i]查看此ID之前的操作历史记录,以查看当前'DONE'事件之前的操作。然后,我们将此列表缩小为属于['CREATED','UPDATED']的操作,然后根据该列表中的最后一个操作分配值给'check'列。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接