首页 > 解决方案 > 如何加快这个嵌套循环(按日期索引?)

问题描述

数据集如下所示:

    email               event_date                  event_type 
0   4867784685125632    2015-10-26 21:38:03.911350  delivered   
1   5352432066363392    2015-10-26 21:37:57.871980  delivered   
2   6024938649550848    2015-10-26 21:37:57.853210  purchase    
3   6191500064980992    2015-10-26 21:37:58.867800  delivered   
4   4867784685125632    2015-10-28 21:37:56.331130  purchase    

本质上,有许多行共享一个散列的电子邮件(电子邮件)值。对于 event type = 已交付的每一行,我需要计算 event_type = purchase 共享相同电子邮件地址的行数,并在原始行中的日期之后的 5 天内发生。

我已经找到了一种方法来做到这一点,我将这个 df 拆分为单独的交付和购买的数据帧,然后使用嵌套循环来搜索两者——但它确实效率低下并且需要永远。

attributed_purchases = []

count = 0

for idx_e, row_e in delivered.iterrows():
    purch = 0
    rev = 0

    for idx_p, row_p, in purchased.iterrows():

        if delivered.loc[idx_e, 'email'] != purchased.loc[idx_p, 'email']:
            pass

        elif (purchased.loc[idx_p, 'event_date'] >= delivered.loc[idx_e, 'event_date']) and purchased.loc[idx_p, 'event_date'] <= (delivered.loc[idx_e, 'event_date'] + timedelta(days=5)):
            purch += 1

            print('I just found a purchase')

    attributed_purchases.append(purch)

    count += 1

    print(f'Completed iteration {count}')

delivered['attributed_purchases'] = attributed_purchases

第一个循环遍历传递的数据帧中的行。对于每一行,它会遍历购买的数据框并首先检查电子邮件是否匹配。如果有,它会检查日期是否在 5 天之内,并增加计数器。遍历购买的数据帧后,它将计数附加到列表并跳转到第一个 for 循环中的下一次迭代。

这行得通,但是我正在处理大量数据并且花费的时间太长了。

我相信有很多方法可以加快速度。也许如果我根据日期索引?任何帮助表示赞赏!

如果您需要更多信息,请与我们联系。

标签: pythondatetime

解决方案


很难不知道更具体的要求,但是一些高级建议-

  1. 在内部循环中找到购买数据后使用break,这样您就不会不必要地处理所有剩余的项目。
  2. 在交付完成时清理购买列表,以便它随着时间的推移而缩小,并且外部循环的未来迭代不会处理已经归属的项目。

不过,根据我的评论,我仍然认为简单的单循环方法会更有效,并且要处理单个电子邮件的多个可能重叠的购买,您只需将它们分别存储为一个列表(defaultdict(list)为方便起见),并且随时随地管理这些列表。这也确保了一次交付不会满足多次购买,尽管如果需要,只需将整个try块更改为类似count += bool(pending[ehash])

import datetime
from collections import defaultdict

emails = ((e[0], datetime.datetime.strptime(e[1], '%Y-%m-%d'), e[2])
    for e in (
      (1, '2019-01-01', 'delivered'),  # ignored, no prior purchase
      (1, '2019-01-02', 'purchase'),
      (1, '2019-01-03', 'purchase'),
      (1, '2019-01-04', 'delivered'),  # matches [1], count == 1
      (1, '2019-01-05', 'purchase'),
      (1, '2019-01-06', 'delivered'),  # matches [2], count == 2
      (1, '2019-01-20', 'delivered')   # ignored, too long since last purchase
    )
)
count = 0
pending = defaultdict(list)

for (ehash, date, status) in sorted(emails, key=lambda e: e[1]):

    # record a purchase awaiting delivery
    if status == 'purchase':
        pending[ehash].append(date)

    elif status == 'delivered':
        # purge any purchases for this email > 5days old
        pending[ehash] = [p_date for p_date in pending[ehash]
                         if p_date > date - datetime.timedelta(days=5)]

        # then the next oldest (<5days) also deleted, and increments the count
        try:
            del pending[ehash][0]
            count += 1
        except IndexError:
            pass # No valid purchase for this delivery

print(count)

推荐阅读