【问题标题】:How to Optimize Performance in Nested Loops Iterating a Pandas Dataframe如何在迭代 Pandas 数据框的嵌套循环中优化性能
【发布时间】:2021-11-05 05:02:45
【问题描述】:

假设以下数据框 df:

import pandas as pd
data = {"Time":["2021-01-10 21:00:00", "2021-01-10 22:00:00", 
                "2021-01-10 21:30:01", "2021-01-10 21:45:00",
                "2021-01-12 09:00:00", "2021-01-12 09:30:00"],
        "ID":["1","1","2","2","2","2"],
        "Event":["cut","cut", "smooth","smooth","cut","cut"],
        "Status":["start", "complete", "start", "complete","start", "complete",]}
df = pd.DataFrame(data)  
df["Time"] = pd.to_datetime(df["Time"])  
df["ID"] = df["ID"].astype("int")  
df

我的最终目标是计算每个唯一“ID”的总制作时间,不考虑每个时间间隔之间的任何潜在时间间隔。每个 ID 的开始时间是“开始”状态的第一个实例,结束生产时间是每个 ID 的“完成”状态的最后一个实例。例如,对于 ID==1,这是 1 小时(3600 秒),而对于 ID==2,大约是 45 分钟(第一个时间间隔为 15 分钟,第二个时间间隔为 30 分钟)。

因为我也有兴趣捕获每个唯一 ID 的时间间隔(例如,ID==1 只有 1 个与其总生产时间一致的时间间隔,ID==2 有 2 对开始-完成状态,并且因此2个间隔),我想做的是创建两个字典:'time_diff_dict'和'cumulativeSumId':

  • 'time_diff_dict':key:唯一 ID,values:时间间隔
  • 'cumulativeSumId':key:唯一ID,values:上述时间间隔的累计和

这样,在“cumulativeSumId”字典中,每个键 (ID) 的最后一个键值将等于其总生产时间。

但是,假设真正的 df 有大约 180,000 行和大约 3000 个唯一 ID,并且终止以下代码大约需要 10 分钟。可能我将不得不使用像here 描述的迭代方法或更有效的嵌套循环,但是,我需要一些帮助来为这种特殊情况实现更好的性能。我当前的代码是:

# dictionary containing the time intervals of IDs (delta of complete-start)
# key: ID, value: time interval for every unique complete-start completion per unique ID
time_diff_dict = {key: [] for key in list(df.ID.unique())}

# same structure as time_diff_dict, but here storing the time_diff_dict values per ID
cumulativeSumId = {key: [] for key in list(df.ID.unique())} 

# initialise time difference to 0 before calculating time interval
time_diff = 0

for unique_ID in df.ID.unique():
  for row in df.itertuples(index=True, name="Pandas"):
    if row.ID == unique_ID:
      if row.Status == "start":
        start = row.Time
        cumulativeSumId[unique_ID].append(sum(time_diff_dict[unique_ID]))
      elif row.Status == "complete":
        end = row.Time
        delta = end - start
        time_diff = delta.total_seconds()
        time_diff_dict[unique_ID].append(time_diff)
        cumulativeSumId[unique_ID].append(sum(time_diff_dict[unique_ID]))

dict_values = list(cumulativeSumId.values())
df["Cumulative_Time"] = [item for sublist in dict_values for item in sublist]

df 现在的结果:

,例如对于 ID==1,总生产时间是 3600 秒,对于 ID==2 是 2699 秒,因为这是其累积时间字典中的最后一个实例。

之后,我创建了一个新的 df,其中包含:唯一 ID、“totalTimeId”和“timeIntervals”:

''' 
* create list of lists 
* every sublist is a dataframe per unique ID
'''
lists_of_IDdfs =[]

for id, df_id in df.groupby("ID"):
  lists_of_IDdfs.append(df_id)

data = []
for df in range(len(lists_of_IDdfs)):
  data.append((lists_of_IDdfs[df].ID.iloc[-1], lists_of_IDdfs[df].Cumulative_Time.iloc[-1]))
df_ID_TotalTime = pd.DataFrame(data, columns= ["ID", "totalTimeId"])

'''add the respective time interval data points per unique ID'''
df_ID_TotalTime["timeIntervals"] = df_ID_TotalTime["ID"].map(time_diff_dict)
df_ID_TotalTime

最终想要的结果:

如果有任何想法和帮助,我将不胜感激!谢谢!

【问题讨论】:

    标签: python pandas dataframe performance loops


    【解决方案1】:

    您可以使用pivot 重塑您的数据框,计算两个日期时间之间的差异,并使用groupby“ID”来聚合数据:

    # pre-requisite ensure that Time is of datetime type
    df['Time'] = pd.to_datetime(df['Time'])
    
    (df.pivot(index=['ID', 'Event'], columns='Status', values='Time')
       .assign(time=lambda d: d['complete']-d['start'])
       .groupby('ID')['time'].sum()
    )
    

    输出:

    ID
    1   0 days 00:30:00
    2   0 days 00:24:58
    

    要在几秒钟内获得输出:

    (df.pivot(index=['ID', 'Event'], columns='Status', values='Time')
       .assign(time=lambda d: d['complete']-d['start'])
       .groupby('ID')['time'].sum()
       .dt.total_seconds()
    )
    

    输出:

    ID
    1    1800.0
    2    1498.0
    

    替代输出:

    (df.pivot(index=['ID', 'Event'], columns='Status', values='Time')
       .assign(time=lambda d: (d['complete']-d['start']).dt.total_seconds())
       .groupby('ID')['time'].agg(totalTimeId='sum', timeIntervals=list)
    )
    

    输出:

        totalTimeId    timeIntervals
    ID                              
    1        3600.0         [3600.0]
    2        2699.0  [1800.0, 899.0]
    

    编辑如何处理重复:

    您需要添加一个唯一的二级索引(ID2)

    (df.assign(ID2=df.groupby(['ID', 'Event', 'Status']).cumcount())
       .pivot(index=['ID', 'ID2', 'Event'], columns='Status', values='Time')
       .assign(time=lambda d: (d['complete']-d['start']).dt.total_seconds())
       .groupby('ID')['time'].agg(totalTimeId='sum', timeIntervals=list)
    )
    

    输入:

                     Time  ID   Event    Status
    0 2021-01-10 21:00:00   1     cut     start
    1 2021-01-10 22:00:00   1     cut  complete
    2 2021-01-10 21:30:01   2  smooth     start
    3 2021-01-10 21:45:00   2  smooth  complete
    4 2021-01-12 09:00:00   2     cut     start
    5 2021-01-12 09:30:00   2     cut  complete
    6 2021-01-12 09:30:00   2     cut     start
    7 2021-01-12 09:35:00   2     cut  complete
    

    中级:

    Status                   complete               start
    ID ID2 Event                                         
    1  0   cut    2021-01-10 22:00:00 2021-01-10 21:00:00
    2  0   cut    2021-01-12 09:30:00 2021-01-12 09:00:00
           smooth 2021-01-10 21:45:00 2021-01-10 21:30:01
       1   cut    2021-01-12 09:35:00 2021-01-12 09:30:00
    

    输出:

        totalTimeId           timeIntervals
    ID                                     
    1        3600.0                [3600.0]
    2        2999.0  [1800.0, 899.0, 300.0]
    

    【讨论】:

    • 谢谢@mozway。我收到了一个关于重塑索引的错误,我将对其进行调查,但是,请再次查看我的帖子。我不得不重新编辑以指出 df 示例中未捕获的细节
    • 您需要确保时间是日期时间类型:df['Time'] = pd.to_datetime(df['Time']),查看更新后的答案
    • 完成,如果您现在复制粘贴所有代码 sn-ps,您应该可以毫无问题地运行它们。
    • @dimi_fn 所以,它工作正常吗?我得到 1: 3600.0 / 2: 2699.0 更新的数据集
    • 非常感谢@mozway,在这个 df 示例中它工作得很好!在我的原件中,我收到“索引包含重复条目,无法重塑”,我必须对其进行调查。但是,即使对于这个 df 示例,您是否可能对我如何达到最后一个 df_ID_TotalTime df 中所示的最终结果有所了解?
    【解决方案2】:

    你可以按ID分组,然后计算timedeltas:

    df['Cumulative_Time'] = df.groupby('ID')['Time'].apply(lambda x: x - x.min()).dt.total_seconds()
    

    为了获得您想要的输出,您可以在@mozway 的回答的启发下执行以下操作。

    (df.groupby(['ID','Event'])['Time']
     .apply(lambda x: x.max() - x.min()).dt.total_seconds()
     .groupby('ID')
     .agg(totalTimeId='sum', timeIntervals=list))
    

    【讨论】:

    • 我很抱歉@joAschauer,这很有效,谢谢你,但我应该指出我不应该在每个时间间隔之间捕捉任何潜在的时间间隔。我原来的 df 没有帮助捕捉到那个场景,所以我再次编辑了这篇文章!感谢您的帮助,希望您再看看!
    • 嗨@dimi_fn,我相应地编辑了答案,也许可以解决您的重复索引问题?
    • 感谢 @joAschauer 再看一眼!查看this notebook可以看到cum time考虑了时间间隔,时间间隔的len没有被正确捕获
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-04
    • 1970-01-01
    • 2015-05-16
    • 2021-01-21
    • 2018-10-04
    • 2014-10-22
    相关资源
    最近更新 更多