使用自定义函数检查列中是否存在一组字符串

Checking if a set of strings exist in a column with a custom function

其他贡献者,

我想检查分组的 pandas DataFrame 上是否存在一组特定的关键字。我想检查的词是 startpendingfinishedalmost_finished。我想为此定义一个自定义函数,apply 到 pandas groupby,因为与我们解决的按行操作相比,定义一个应用于列的函数对我来说有点不清楚每一行都有 (row[colname])。 在此示例中,如果存在所需单词的序列,我希望将每个 IDnumber 中的最后一个值复制到新列中,并且之前的其他值是否无关紧要空字符串。这是一个可重现的例子:

import pandas as pd

df = pd.DataFrame({'ID' : [1100, 1100, 1100, 1200, 1200, 1200, 1300, 1300],
                  'number' : ['Yes', 'No', 'No', 'Yes', 'No', 'No', 'Yes', 'No'],
                  'status' : ['start', 'pending', 'finished', 'start', 'pending', 'partially_finished', 'start', 'pending']})

在这种情况下,最后一组 ID == 1300 没有 return 值。 基本上我问这个问题是为了学习解决这些需要检查列中某些值的问题的最佳方法,因为我来自 R 我需要熟悉我在 [=26= 中做同样事情的方式].如果您提出任何更好的解决方案,我也将不胜感激。 非常感谢您。

df.groupby.apply 可能是您要查找的内容。您可以将一个函数应用于每个组和 return 单个值、系列或数据框。结果将汇总在一起。

例如以下函数

def return_last_num(df):
    if df.status.str.contains('start').any() & df.status.str.contains('pending').any() \
        & (df.status.str.contains('finished').any() | df.status.str.contains('partically_finished').any()):
        df['last_number'] = df.number.values[-1]
    else:
        df['last_number'] = str()
    return df

应用时:df.groupby('ID').apply(return_last_num).

Returns:

     ID number              status last_number
0  1100    Yes               start          No
1  1100     No             pending          No
2  1100     No            finished          No
3  1200    Yes               start          No
4  1200     No             pending          No
5  1200     No  partially_finished          No
6  1300    Yes               start            
7  1300     No             pending            

或者:return获取单个值

def return_last_num(df):
    if df.status.str.contains('start').any() & df.status.str.contains('pending').any() \
        & (df.status.str.contains('finished').any() | df.status.str.contains('partically_finished').any()):
        return df.number.values[-1]
    else:
        return str()

应用时:df.groupby('ID').apply(return_last_num).

Returns 如果序列条件匹配,则只是每个 ID 的最后一个 'number' 值:

ID
1100    No
1200    No
1300           

我在考虑矢量化方法。

首先,如果finishedalmost finished具有相同的效果,我会将它们“合并”,并使它们成为一个易于检查的唯一编号:

>>> df['status2'] = df['status'].map({'finished':1,'partially_finished':1,'pending':10,'start':100})
>>> df
     ID number              status  status2
0  1100    Yes               start      100
1  1100     No             pending       10
2  1100     No            finished        1
3  1200    Yes               start      100
4  1200     No             pending       10
5  1200     No  partially_finished        1
6  1300    Yes               start      100
7  1300     No             pending       10

这让我可以“提取”所需的状态 (100+10+1):

idstatus=df.groupby('ID', sort=False).sum('status2')==111
      status2
ID
1100     True
1200     True
1300    False

以及实际数值:如果您不需要匹配 所有 个单词,那么仅此行就足够了。

valuenumber=df.query('status2==1').set_index('ID')
     number              status  status2
ID
1100     No            finished        1
1200     No  partially_finished        1

最后合并:

idstatus.merge(valuenumber, left_index=True, right_index=True, how='left')
>>> idstatus.merge(valuenumber, left_index=True, right_index=True, how='left')
      status2_x number              status  status2_y
ID
1100       True     No            finished        1.0
1200       True     No  partially_finished        1.0
1300      False    NaN                 NaN        NaN

可以合并较少的条目:

>>> idstatus.merge(valuenumber[['number','status']], left_index=True, right_index=True, how='left')
      status2 number              status
ID
1100     True     No            finished
1200     True     No  partially_finished
1300    False    NaN                 NaN
>>>

编辑: 如果你只想得到成品的输出,你也可以用这个 oneliner 得到它:

>>> valuenumber.loc[idstatus[idstatus.status2].index][['number','status']]
     number              status
ID
1100     No            finished
1200     No  partially_finished
>>>

Edit2:刚刚进行了基准测试 有趣的是,相交解决方案更快,至少对于示例数据而言:

  • groubpy-apply: 2.86 毫秒
  • 聚合与合并:3.90 毫秒
  • 矢量和位置:3.96 毫秒
  • set-intersect:2.4 毫秒,尽管使用 lambda

Edit3: 昨天太沉了。使用Emma的“一个groupby中的两个操作”的想法,并假设项目是有序的:

df2['status'] = df2.status.map({'finished':1,'partially_finished':1,'pending':10,'start':100})
x = df2.groupby('ID').agg({'number': 'last', 'status': 'sum'})
x[x.status==111]

在 2.1 毫秒,它比带有 apply / lambda 的版本稍快

可以用set聚合,用intersection查。

但首先,我会将 partially_finishedalmost_finished 映射到 finished,如果它们应该被平等对待的话。

df['status'] = df.status.replace('partially_finished|almost_finished', 'finished', regex=True)

接下来,将 number 聚合到最后一个值,将 status 聚合到 set,然后我使用 intersect 检查 status 中是否存在所有值.

checkcriteria = {'start', 'pending', 'finished'}
df = df.groupby('ID').agg({'number': 'last', 'status': set})
df['check'] = df.status.transform(lambda x: len(x.intersection(checkcriteria)) == 3)

这应该给出一个结果,

     number                      status  check
ID
1100     No  {start, pending, finished}   True
1200     No  {start, pending, finished}   True
1300     No            {start, pending}  False

您可以按 checkmask 过滤并删除 number 的值。

# This will only return ID == 1100, 1200
df[df.check]

# OR mask to remove the number value for when check == False
df.loc[~df.check, 'number'] = None