【问题标题】:How to calculate the number of days since a given event in each group如何计算每组中给定事件以来的天数
【发布时间】:2021-11-18 17:20:40
【问题描述】:

下面是一个示例数据框:

df = pd.DataFrame({'StudentName': ['Anil','Ramu','Ramu','Anil','Peter','Peter','Anil','Ramu','Peter','Anil'],
                   'ExamDate': ['2021-01-10','2021-01-20','2021-02-22','2021-03-30','2021-01-04','2021-06-06','2021-04-30','2021-07-30','2021-07-08','2021-09-07'],
                   'Result': ['Fail','Pass','Fail','Pass','Pass','Pass','Pass','Pass','Fail','Pass']})

  StudentName    ExamDate Result
0        Anil  2021-01-10   Fail
1        Ramu  2021-01-20   Pass
2        Ramu  2021-02-22   Fail
3        Anil  2021-03-30   Pass
4       Peter  2021-01-04   Pass
5       Peter  2021-06-06   Pass
6        Anil  2021-04-30   Pass
7        Ramu  2021-07-30   Pass
8       Peter  2021-07-08   Fail
9        Anil  2021-09-07   Pass

对于每一行,我想计算自该学生上次未通过测试以来的天数:

df = pd.DataFrame({'StudentName': ['Anil','Ramu','Ramu','Anil','Peter','Peter','Anil','Ramu','Peter','Anil'],
                   'ExamDate': ['2021-01-10','2021-01-20','2021-02-22','2021-03-30','2021-01-04','2021-06-06','2021-04-30','2021-07-30','2021-07-08','2021-09-07'],
                   'Result': ['Fail','Pass','Fail','Pass','Pass','Pass','Pass','Pass','Fail','Pass'],
                   'LastFailedDays': [0, 0, 0, 79, 0, 0, 110, 158, 0, 240]})

  StudentName    ExamDate Result  LastFailedDays
0        Anil  2021-01-10   Fail               0
1        Ramu  2021-01-20   Pass               0
2        Ramu  2021-02-22   Fail               0
3        Anil  2021-03-30   Pass              79
4       Peter  2021-01-04   Pass               0
5       Peter  2021-06-06   Pass               0
6        Anil  2021-04-30   Pass             110
7        Ramu  2021-07-30   Pass             158
8       Peter  2021-07-08   Fail               0
9        Anil  2021-09-07   Pass             240

例如:

  • Anil 在 2021 年 1 月 10 日失败,因此该行将是零天。
  • Anil 的下一个成功记录是在 2021-03-30,因此该行的天数将是从他之前的失败日期 2021-01-10 到 2021-03-30 的天数,即 79 天。
  • 阿尼尔的第三条记录,也是成功的,是在2021-04-30,所以天数会再次出现,天数2021-01-10(他最后失败的日期)到2021-04- 30,即 110 天。

常规循环是可行的,但我正在寻找更传统的 Pandas 解决方案。我猜groupby 是可能的。

【问题讨论】:

  • 无法理解您如何计算 LastFailedDays。例如,anil 在 21-01-10、21-03-30、21-04-30 和 21-09-07 失败了,最后一天怎么算 240 天?第一次失败后的几天?
  • 我已经用一个例子解释了这个场景。 @UlisesBussi

标签: python pandas pandas-groupby


【解决方案1】:

TL;DR

使用Series.wheregroupby.ffill 生成每个学生的最后失败日期,并从ExamDate 中减去LastFailedDays

df['ExamDate'] = pd.to_datetime(df['ExamDate'])

df['LastFailedDays'] = (df['ExamDate'].sub(
    df['ExamDate'].where(df['Result'] == 'Fail').groupby(df['StudentName']).ffill()
).dt.days.fillna(0))

#   StudentName    ExamDate  Result  LastFailedDays
# 0        Anil  2021-01-10    Fail             0.0
# 1        Ramu  2021-01-20    Pass             0.0
# 2        Ramu  2021-02-22    Fail             0.0
# 3        Anil  2021-03-30    Pass            79.0
# 4       Peter  2021-01-04    Pass             0.0
# 5       Peter  2021-06-06    Pass             0.0
# 6        Anil  2021-04-30    Pass           110.0
# 7        Ramu  2021-07-30    Pass           158.0
# 8       Peter  2021-07-08    Fail             0.0
# 9        Anil  2021-09-07    Pass           240.0

Re:cmets,按多列分组,例如StudentClassStudentName,使用列表作为分组:

...groupby([df['StudentClass'], df['StudentName']]).ffill()

详情

  1. 转换to_datetime:

    df['ExamDate'] = pd.to_datetime(df['ExamDate'])
    
  2. 使用Series.where 生成每个学生的最后失败日期(这里我将其设为一列以便于可视化):

    df['LastFailedDate'] = df['ExamDate'].where(df['Result'] == 'Fail')
    
    #   StudentName    ExamDate  Result  LastFailedDate
    # 0        Anil  2021-01-10    Fail      2021-01-10
    # 1        Ramu  2021-01-20    Pass             NaT
    # 2        Ramu  2021-02-22    Fail      2021-02-22
    # 3        Anil  2021-03-30    Pass             NaT
    # 4       Peter  2021-01-04    Pass             NaT
    # 5       Peter  2021-06-06    Pass             NaT
    # 6        Anil  2021-04-30    Pass             NaT
    # 7        Ramu  2021-07-30    Pass             NaT
    # 8       Peter  2021-07-08    Fail      2021-07-08
    # 9        Anil  2021-09-07    Pass             NaT
    
  3. 使用groupby.ffill 转发每个学生的最后失败日期(NaT,如果之前没有失败的考试):

    df['LastFailedDate'] = df['LastFailedDate'].groupby(df['StudentName']).ffill()
    
    #   StudentName    ExamDate  Result  LastFailedDate
    # 0        Anil  2021-01-10    Fail      2021-01-10
    # 1        Ramu  2021-01-20    Pass             NaT
    # 2        Ramu  2021-02-22    Fail      2021-02-22
    # 3        Anil  2021-03-30    Pass      2021-01-10
    # 4       Peter  2021-01-04    Pass             NaT
    # 5       Peter  2021-06-06    Pass             NaT
    # 6        Anil  2021-04-30    Pass      2021-01-10
    # 7        Ramu  2021-07-30    Pass      2021-02-22
    # 8       Peter  2021-07-08    Fail      2021-07-08
    # 9        Anil  2021-09-07    Pass      2021-01-10
    
  4. 最后用最后失败的日期减去考试日期,并使用dt.days提取天数:

    df['LastFailedDays'] = df['ExamDate'].sub(df['LastFailedDate']).dt.days.fillna(0)
    
    #   StudentName    ExamDate  Result  LastFailedDate  LastFailedDays
    # 0        Anil  2021-01-10    Fail      2021-01-10             0.0
    # 1        Ramu  2021-01-20    Pass             NaT             0.0
    # 2        Ramu  2021-02-22    Fail      2021-02-22             0.0
    # 3        Anil  2021-03-30    Pass      2021-01-10            79.0
    # 4       Peter  2021-01-04    Pass             NaT             0.0
    # 5       Peter  2021-06-06    Pass             NaT             0.0
    # 6        Anil  2021-04-30    Pass      2021-01-10           110.0
    # 7        Ramu  2021-07-30    Pass      2021-02-22           158.0
    # 8       Peter  2021-07-08    Fail      2021-07-08             0.0
    # 9        Anil  2021-09-07    Pass      2021-01-10           240.0
    

【讨论】:

  • 在第 3 步中,使用groupby.ffill 是否可以对多个列进行分组?像这样的东西,df['LastFailedDate'] = df['LastFailedDate'].groupby(df[['StudentClass', 'StudentName']]).ffill() 它正在抛出错误,所以我尝试了df['LastFailedDate'] = df.groupby(['StudentClass', 'StudentName'])['LastFailedDate'].ffill() 但现在我想知道如何将它包含在你写在上面的 pythonic 单行代码中。
  • @AmeyYadav 您可以通过list 2 系列作为石斑鱼:groupby([df.StudentClass, df.StudentName]).ffill()
  • 所以 oneliner 将是:df['LastFailedDays'] = df.ExamDate.where(df.Result == 'Fail').groupby([df.StudentClass, df.StudentName]).ffill().pipe(lambda fail_date: df.ExamDate.sub(fail_date).dt.days.fillna(0))
【解决方案2】:

我终于想出了一个可行的解决方案。

# Process the data a bit
df['Tmp_Result'] = df['Result'].map({'Pass': 1, 'Fail': 0})
df['ExamDate'] = pd.to_datetime(df['ExamDate'])

# Create a mask that will be used to group the rows by StudentName + consecutive passed tests after a failed test (including the failed test)
sorted_df = df.sort_values(['StudentName', 'ExamDate']) 
mask = sorted_df.groupby('StudentName')['Tmp_Result'].diff().ne(0).cumsum()
mask[(sorted_df['Tmp_Result'].eq(0) & ~(pd.isna(sorted_df.groupby('StudentName')['Tmp_Result'].shift(-1))))] += 1

df['LastFailedDays'] = df.groupby(mask)['ExamDate'].diff().fillna(pd.Timedelta(0))
df['LastFailedDays'] = df.groupby(mask)['LastFailedDays'].cumsum()

# Cleanup
df = df.drop('Tmp_Result', axis=1)

输出:

>>> df
  StudentName   ExamDate Result LastFailedDays
0        Anil 2021-01-10   Fail         0 days
1        Ramu 2021-01-20   Pass         0 days
2        Ramu 2021-02-22   Fail         0 days
3        Anil 2021-03-30   Pass        79 days
4       Peter 2021-01-04   Pass         0 days
5       Peter 2021-06-06   Pass       153 days
6        Anil 2021-04-30   Pass       110 days
7        Ramu 2021-07-30   Pass       158 days
8       Peter 2021-07-08   Fail         0 days
9        Anil 2021-09-07   Pass       240 days

>>> df.sort_values(['StudentName', 'ExamDate'])
  StudentName   ExamDate Result LastFailedDays
0        Anil 2021-01-10   Fail         0 days
3        Anil 2021-03-30   Pass        79 days
6        Anil 2021-04-30   Pass       110 days
9        Anil 2021-09-07   Pass       240 days
4       Peter 2021-01-04   Pass         0 days
5       Peter 2021-06-06   Pass       153 days
8       Peter 2021-07-08   Fail         0 days
1        Ramu 2021-01-20   Pass         0 days
2        Ramu 2021-02-22   Fail         0 days
7        Ramu 2021-07-30   Pass       158 days

这看起来有点毛骨悚然,但因为它是矢量化的,它应该比任何使用循环的解决方案都要快很多。

【讨论】:

  • 我认为您走在正确的道路上,但最终结果并不完全正确。对于学生 Peter,如果结果错误。他的所有记录都应该为零。
  • 哦,糟糕,这只是一个错误。让我们解决这个问题。
  • 你知道如何修复这个错误了吗!
猜你喜欢
  • 1970-01-01
  • 2019-08-16
  • 1970-01-01
  • 2015-08-04
  • 1970-01-01
  • 1970-01-01
  • 2018-09-10
  • 1970-01-01
  • 2016-05-07
相关资源
最近更新 更多