Python Pandas:尝试在 date_range 操作中加快每个日期的每行

Python Pandas: Trying to speed-up a per row per date in date_range operation

我有一个如下形式的数据框,其中每一行对应机器上的一个作业运行:

import pandas as pd
df = pd.DataFrame({
    'MachineID': [4, 3, 2, 2, 1, 1, 5, 3],
    'JobStartDate': ['2020-01-01', '2020-01-01', '2020-01-01', '2020-01-01', '2020-01-02', '2020-01-03', '2020-01-01', '2020-01-03'], 
    'JobEndDate': ['2020-01-03', '2020-01-03', '2020-01-04', '2020-01-02', '2020-01-04', '2020-01-05', '2020-01-02', '2020-01-04'], 
    'IsTypeAJob': [1, 1, 0, 1, 0, 0, 1, 1]
    })
    
df
>>>    MachineID JobStartDate JobEndDate  IsTypeAJob
0          4      2020-01-01  2020-01-03      1
1          3      2020-01-01  2020-01-03      1
2          2      2020-01-01  2020-01-04      0
3          2      2020-01-01  2020-01-02      1
4          1      2020-01-02  2020-01-04      0
5          1      2020-01-03  2020-01-05      0
6          5      2020-01-01  2020-01-02      1
7          3      2020-01-03  2020-01-04      1

在我的数据中,有两种类型的作业可以在机器上 运行,类型 A 或类型 B。我的目标是计算每台机器每天类型 A 和类型 B 的作业数量。因此,期望的结果看起来像

        MachineID    Date         TypeAJobs  TypeBJobs
0           1     2020-01-02          0          1
1           1     2020-01-03          0          2
2           1     2020-01-04          0          2
3           1     2020-01-05          0          1
4           2     2020-01-01          1          1
5           2     2020-01-02          1          1
6           2     2020-01-03          0          1
7           2     2020-01-04          0          1
8           3     2020-01-01          1          0
9           3     2020-01-02          1          0
10          3     2020-01-03          2          0
11          3     2020-01-04          1          0
12          4     2020-01-01          1          0
13          4     2020-01-02          1          0
14          4     2020-01-03          1          0
15          5     2020-01-01          1          0
16          5     2020-01-02          1          0

我已尝试使用 resample()apply() 方法找到 and here 的方法,但计算时间太慢。这与我的集合中某些日期范围跨越多年的事实有关,这意味着在重新采样期间一行可能会爆炸成 2000 多行新行(我的数据开始时包含大约一百万行)。因此,像为某个工作范围内的每个日期创建一个新的 machine/date 行这样的事情太慢了(目标是在最后做一个 group_by(['MachineID', 'Date']).sum())。

我目前正在考虑一种新方法,我首先按 MachineID 分组,然后找到该机器的最早作业开始日期和最晚作业结束日期。然后我可以在这两个日期之间创建一个日期范围(按天递增),我将使用它来为每台机器的新数据帧建立索引。然后,对于 MachineID 的每项工作,我可能会对一系列日期求和,即在伪代码中:

df['TypeAJobs'][row['JobStartDate']:row['JobEndDate']] += 1 如果它是 A 类型的工作或

df['TypeBJobs'][row['JobStartDate']:row['JobEndDate']] += 1否则。

这似乎可以避免为每个作业创建一堆额外的行,因为现在我们正在为每台机器创建额外的行。此外,加法操作看起来很快,因为我们是一次添加到一个系列的整个切片中。但是,我不知道在 Pandas 中是否可以进行类似的操作(按日期索引)。也许可以先进行一些转换?完成上述操作后,理想情况下我会有许多与所需结果相似的数据帧,但只有一个 MachineID,然后我将连接这些数据帧以获得结果。

我很想听听有关此方法或其他潜在算法的 feasibility/effectiveness 的任何建议。非常感谢阅读!

IIUC,尝试使用 pd.date_rangeexplode 创建 'daily' 行,然后按日期和 IsTypeAJob 和 rename 列分组:

df_out = df.assign(JobDates=df.apply(lambda x: pd.date_range(x['JobStartDate'], 
                                                             x['JobEndDate'], freq='D'), 
                                     axis=1))\
           .explode('JobDates')

df_out = df_out.groupby([df_out['MachineID'], 
                         df_out['JobDates'].dt.floor('D'), 
                         'IsTypeAJob'])['MachineID'].count()\
               .unstack()\
               .rename(columns={0:'TypeBJobs', 1:'TypeAJobs'})\
               .fillna(0).reset_index()

df_out

输出:

IsTypeAJob  MachineID   JobDates  TypeBJobs  TypeAJobs
0                   1 2020-01-02        1.0        0.0
1                   1 2020-01-03        2.0        0.0
2                   1 2020-01-04        2.0        0.0
3                   1 2020-01-05        1.0        0.0
4                   2 2020-01-01        1.0        1.0
5                   2 2020-01-02        1.0        1.0
6                   2 2020-01-03        1.0        0.0
7                   2 2020-01-04        1.0        0.0
8                   3 2020-01-01        0.0        1.0
9                   3 2020-01-02        0.0        1.0
10                  3 2020-01-03        0.0        2.0
11                  3 2020-01-04        0.0        1.0
12                  4 2020-01-01        0.0        1.0
13                  4 2020-01-02        0.0        1.0
14                  4 2020-01-03        0.0        1.0
15                  5 2020-01-01        0.0        1.0
16                  5 2020-01-02        0.0        1.0

pd.concat([pd.DataFrame({'JobDates':pd.date_range(r.JobStartDate, r.JobEndDate, freq='D'),
                        'MachineID':r.MachineID, 
                        'IsTypeAJob':r.IsTypeAJob}) for i, r in df.iterrows()])

这是完成这项工作的另一种方法,其想法类似于在开始和结束的两列上使用 str.get_dummies,但使用数组广播完成。使用 cumsum 在开始和结束之间取 1,否则取 0。创建一个数据框,其中列为日期,索引为机器和类型。然后执行与 的答案类似的操作以获得预期的输出形状。

#get all possible dates
dr = pd.date_range(df['JobStartDate'].min(), 
                   df['JobEndDate'].max()).strftime("%Y-%m-%d").to_numpy()

df_ = (pd.DataFrame(
            np.cumsum((df['JobStartDate'].to_numpy()[:, None] == dr).astype(int) 
                       - np.pad(df['JobEndDate'].to_numpy()[:, None]==dr,((0,0),(1,False)), 
                                mode='constant')[:, :-1],  # pad is equivalent to shift along columns
                      axis=1), 
            index=pd.MultiIndex.from_frame(df[['MachineID', 'IsTypeAJob']]), 
            columns=dr,)
         .sum(level=['MachineID', 'IsTypeAJob']) #equivalent to groupby(['MachineID', 'IsTypeAJob']).sum()
         .replace(0, np.nan) #to remove extra dates per original row during the stack
         .stack()
         .unstack(level='IsTypeAJob', fill_value=0)
         .astype(int)
         .reset_index()
         .rename_axis(columns=None)
         .rename(columns={'level_1':'Date', 0:'TypeBJobs', 1:'TypeAJobs'})
      )

你得到

    MachineID        Date  TypeBJobs  TypeAJobs
0           1  2020-01-02          1          0
1           1  2020-01-03          2          0
2           1  2020-01-04          2          0
3           1  2020-01-05          1          0
4           2  2020-01-01          1          1
5           2  2020-01-02          1          1
6           2  2020-01-03          1          0
7           2  2020-01-04          1          0
8           3  2020-01-01          0          1
9           3  2020-01-02          0          1
10          3  2020-01-03          0          2
11          3  2020-01-04          0          1
12          4  2020-01-01          0          1
13          4  2020-01-02          0          1
14          4  2020-01-03          0          1
15          5  2020-01-01          0          1
16          5  2020-01-02          0          1