【问题标题】:Python - For loop millions of rowsPython - For 循环数百万行
【发布时间】:2019-02-16 04:21:13
【问题描述】:

我有一个数据框 c 有很多不同的列。此外,arr 是一个数据帧,对应于c 的子集:arr = c[c['A_D'] == 'A']

我的代码的主要思想是遍历c-dataframe 中的所有行并搜索所有可能的情况(在arr 数据框中),其中一些特定条件应该发生:

  • 只需要遍历c['A_D'] == Dc['Already_linked'] == 0的行
  • arr 数据帧中的hour 必须小于c 数据帧中的hour_aux
  • arr 数据框的Already_linked 列必须为零:arr.Already_linked == 0
  • TerminalOperator 在 c 和 arr 数据帧中需要相同

现在,使用布尔索引和 groupby get_group 存储条件:

  • arr 数据框分组以选择相同的操作员和终端:g = groups.get_group((row.Operator, row.Terminal))
  • 仅选择小时小于 c 数据框中的小时并且 Already_linked==0: vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)] 的到达

对于验证所有条件的c 数据帧中的每一行,都会创建一个vb 数据帧。自然地,这个数据帧在每次迭代中都有不同的长度。创建vb 数据框后,我的目标是选择vb 数据框的索引,以最小化vb.START 和c[x] 之间的时间。与该索引对应的FightID 然后存储在a 列上的c 数据框中。此外,由于到达与出发相关联,arr 数据框中的列 Already_linked 从 0 更改为 1。

请务必注意,arr 数据框的 Already_linked 列可能会在每次迭代中发生变化(arr.Already_linked == 0 是创建 vb 数据框的条件之一)。因此,无法并行化此代码。

我已经使用c.itertuples() 来提高效率,但是由于c 有数百万行,这段代码仍然太耗时。

其他选项也将pd.apply 用于每一行。尽管如此,这并不是很简单,因为在每个循环中,carr 的值都会发生变化(另外,我相信即使使用 pd.apply 也会非常慢)。

是否有任何可能的方法可以在矢量化解决方案中将此 for 循环转换(或将运行时间减少 10 倍(如果可能的话甚至更多))?

初始数据帧:

START     END       A_D     Operator     FlightID    Terminal   TROUND_ID   tot
0   2017-03-26 16:55:00 2017-10-28 16:55:00 A   QR  QR001   4   QR002       70
1   2017-03-26 09:30:00 2017-06-11 09:30:00 D   DL  DL001   3   "        "  84
2   2017-03-27 09:30:00 2017-10-28 09:30:00 D   DL  DL001   3   "        "  78
3   2017-10-08 15:15:00 2017-10-22 15:15:00 D   VS  VS001   3   "        "  45
4   2017-03-26 06:50:00 2017-06-11 06:50:00 A   DL  DL401   3   "        "  9
5   2017-03-27 06:50:00 2017-10-28 06:50:00 A   DL  DL401   3   "        "  19
6   2017-03-29 06:50:00 2017-04-19 06:50:00 A   DL  DL401   3   "        "  3
7   2017-05-03 06:50:00 2017-10-25 06:50:00 A   DL  DL401   3   "        "  32
8   2017-06-25 06:50:00 2017-10-22 06:50:00 A   DL  DL401   3   "        "  95
9   2017-03-26 07:45:00 2017-10-28 07:45:00 A   DL  DL402   3   "        "  58

所需的输出(以下数据框中排除了某些列。只有 aAlready_linked 列是相关的):

    START                    END             A_D  Operator  a   Already_linked
0   2017-03-26 16:55:00 2017-10-28 16:55:00 A   QR  0               1
1   2017-03-26 09:30:00 2017-06-11 09:30:00 D   DL  DL402           1
2   2017-03-27 09:30:00 2017-10-28 09:30:00 D   DL  DL401           1
3   2017-10-08 15:15:00 2017-10-22 15:15:00 D   VS  No_link_found   0
4   2017-03-26 06:50:00 2017-06-11 06:50:00 A   DL  0               0
5   2017-03-27 06:50:00 2017-10-28 06:50:00 A   DL  0               1
6   2017-03-29 06:50:00 2017-04-19 06:50:00 A   DL  0               0
7   2017-05-03 06:50:00 2017-10-25 06:50:00 A   DL  0               0
8   2017-06-25 06:50:00 2017-10-22 06:50:00 A   DL  0               0
9   2017-03-26 07:45:00 2017-10-28 07:45:00 A   DL  0               1

代码:

groups = arr.groupby(['Operator', 'Terminal'])
for row in c[(c.A_D == "D") & (c.Already_linked == 0)].itertuples():
    try:
        g = groups.get_group((row.Operator, row.Terminal))
        vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]
        aux = (vb.START - row.x).abs().idxmin()
        c.loc[row.Index, 'a'] = vb.loc[aux].FlightID
        arr.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

c['Already_linked'] = np.where((c.a != 0) & (c.a != 'No_link_found') & (c.A_D == 'D'), 1, c['Already_linked'])
c.Already_linked.loc[arr.Already_linked.index] = arr.Already_linked
c['a'] = np.where((c.Already_linked  == 0) & (c.A_D == 'D'),'No_link_found',c['a'])

初始c 数据帧的代码:

import numpy as np
import pandas as pd
import io

s = '''
 A_D     Operator     FlightID    Terminal   TROUND_ID   tot
 A   QR  QR001   4   QR002       70
 D   DL  DL001   3   "        "  84
 D   DL  DL001   3   "        "  78
 D   VS  VS001   3   "        "  45
 A   DL  DL401   3   "        "  9
 A   DL  DL401   3   "        "  19
 A   DL  DL401   3   "        "  3
 A   DL  DL401   3   "        "  32
 A   DL  DL401   3   "        "  95
 A   DL  DL402   3   "        "  58
'''

data_aux = pd.read_table(io.StringIO(s), delim_whitespace=True)
data_aux.Terminal = data_aux.Terminal.astype(str)
data_aux.tot= data_aux.tot.astype(str)

d = {'START': ['2017-03-26 16:55:00', '2017-03-26 09:30:00','2017-03-27 09:30:00','2017-10-08 15:15:00',
           '2017-03-26 06:50:00','2017-03-27 06:50:00','2017-03-29 06:50:00','2017-05-03 06:50:00',
           '2017-06-25 06:50:00','2017-03-26 07:45:00'], 'END': ['2017-10-28 16:55:00' ,'2017-06-11 09:30:00' ,
           '2017-10-28 09:30:00' ,'2017-10-22 15:15:00','2017-06-11 06:50:00' ,'2017-10-28 06:50:00', 
           '2017-04-19 06:50:00' ,'2017-10-25 06:50:00','2017-10-22 06:50:00' ,'2017-10-28 07:45:00']}    

aux_df = pd.DataFrame(data=d)
aux_df.START = pd.to_datetime(aux_df.START)
aux_df.END = pd.to_datetime(aux_df.END)
c = pd.concat([aux_df, data_aux], axis = 1)
c['A_D'] = c['A_D'].astype(str)
c['Operator'] = c['Operator'].astype(str)
c['Terminal'] = c['Terminal'].astype(str)

c['hour'] = pd.to_datetime(c['START'], format='%H:%M').dt.time
c['hour_aux'] = pd.to_datetime(c['START'] - pd.Timedelta(15, unit='m'), 
format='%H:%M').dt.time
c['start_day'] = c['START'].astype(str).str[0:10]
c['end_day'] = c['END'].astype(str).str[0:10]
c['x'] = c.START -  pd.to_timedelta(c.tot.astype(int), unit='m')
c["a"] = 0
c["Already_linked"] = np.where(c.TROUND_ID != "        ", 1 ,0)

arr = c[c['A_D'] == 'A']

【问题讨论】:

标签: python python-3.x pandas performance vectorization


【解决方案1】:

您的问题是是否有办法对 for 循环进行矢量化,但我认为这个问题隐藏了您真正想要的内容,这是一种加快代码速度的简单方法。对于性能问题,一个好的起点总是分析。但是,我强烈怀疑您的代码中的主要操作是.query(row.query_string)。如果arr 很大,则为每一行运行它会很昂贵。

对于任意查询,如果不消除迭代之间的依赖关系并并行化昂贵的步骤,则根本无法真正改善运行时间。不过你可能会幸运一些。您的查询字符串始终检查两个不同的列,以查看它们是否等于您关心的内容。但是,对于需要遍历整个arr 切片的每一行。由于切片每次都会更改,这可能会导致问题,但这里有一些想法:

  • 由于您每次都在对arr 进行切片,因此只保留arr.Already_Linked==0 行的视图,以便您迭代更小的对象。
  • 更好的是,在进行任何循环之前,您应该首先将arrTerminalOperator 分组。然后,不要遍历所有arr,而是首先选择您想要的组,然后进行切片和过滤。这需要稍微重新考虑query_string 的确切实现,但优点是如果您有很多终端和运算符,您通常会处理比arr 小得多的对象。此外,您甚至不必查询该对象,因为这是由 groupby 隐式完成的。
  • 根据aux.hour 通常与row.hour_aux 的关系,您可以通过在开头对aux 相对于hour 进行排序来获得改进。仅使用不等式运算符您可能看不到任何收益,但您可以将其与截止点的对数搜索配对,然后切分到该截止点。
  • 等等。同样,我怀疑任何重组您在 all of arr 上为 每一行 执行的查询的方法都将提供比仅仅切换框架或矢量化位和件。

稍微扩展其中一些点并稍微调整@DJK的代码,看看当我们进行以下更改时会发生什么。

groups = arr.groupby(['Operator', 'Terminal'])

for row in c[(c.A_D == 'D') & (c.Already_linked == 0)].itertuples():
    g = groups.get_group((row.Operator, row.Terminal))
    vb = g[(g.Already_linked==0) & (g.hour<row.hour_aux)]
    try:
        aux = (vb.START - row.x).abs().idxmin()
        print(row.x)
        c.loc[row.Index, 'a'] = vb.loc[aux,'FlightID']
        g.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

您的查询如此缓慢的部分原因是因为它每次都在搜索所有arr。相比之下,.groupby() 与一个查询的执行时间大致相同,但是对于每次后续迭代,您只需使用.get_group() 即可有效地找到您关心的数据的微小子集。

在进行基准测试时,一个有用的(非常粗略的)经验法则是 10 亿件事情需要一秒钟。如果您看到的时间比以数百万计的事物(例如数百万行)要长得多,这意味着对于这些行中的每一行,您都在做大量的事情以进行数十亿次操作。这为更好的算法减少操作数量留下了巨大的潜力,而矢量化实际上只会产生恒定的因子改进(对于许多字符串/查询操作来说甚至没有很大的改进)。

【讨论】:

  • 是的。 .query(row.query_string) 是迄今为止最耗时的。问题是arr.Already_Linked 在每次迭代中都会发生变化......因此我不知道如何不使用 for 循环(小时的排序可能会有所帮助)。谢谢。
  • 它会发生变化,但是如果您执行groups = arr.groupby('Terminal') 之类的操作,那么与运行查询相比,选择group = groups.get_group('my_terminal') 非常快。然后按arr.Already_Linked==0 过滤该组,而不是过滤所有arr。如果按 Terminal 和 Operator 分组会更好,因为分组会更小,而且在遍历所有行之前,您只需执行一次 groupby 操作。
  • 我不确定我是否完全理解它。你认为你可以修改我发送的代码吗?我真的很感激。我还根据 DJK 的建议编辑了代码。但这仍然很耗时。
  • @MiguelLambelho 抱歉花了这么长时间。我不得不去上班。对我提出的更改进行基准测试,看看它是否对您来说更快。除非你有一些奇怪的东西,比如每个终端和运营商都是一样的,否则它应该会提供巨大的改进。
  • 感谢@Hans Musgrave 的帮助。 groupby 绝对有帮助。不幸的是,这仍然比我所拥有的时间限制要多。你知道还有更多的可能以指数方式减少运行时间吗?
【解决方案2】:

虽然这不是一个矢量化的解决方案,但如果您的样本数据集模仿您的真实数据集,它应该会加快速度。目前,您正在浪费时间循环遍历每一行,但您只关心循环遍历 ['A_D'] == 'D'['Already_linked'] ==0 的行。而是删除 if 并遍历仅占初始数据帧 30% 的截断数据帧

for row in c[(c.A_D == 'D') & (c.Already_linked == 0)].itertuples():

    vb = arr[(arr.Already_linked == 0) & (arr.hour < row.hour_aux)].copy().query(row.query_string)
    try:
        aux = (vb.START - row.x).abs().idxmin()
        print(row.x)
        c.loc[row.Index, 'a'] = vb.loc[aux,'FlightID']
        arr.loc[aux, 'Already_linked'] = 1
        continue
    except:
        continue

【讨论】:

  • 感谢您的评论@DJK。这绝对有帮助。但是,数据框仍然太大。不过谢谢!!
  • 已经根据您的评论编辑了答案。谢谢你。您是否知道任何其他选项可以以指数方式减少运行时间?
  • 稍后将尝试更新答案,但尽量不要使用query() 它比布尔索引慢,并且由于过滤器是静态的并且依赖于当前行,只需将其添加到已经完成的过滤器中循环
  • 好的。从现在开始将使用vb = arr[(arr.Already_linked == 0) &amp; (arr.hour &lt; row.hour_aux) &amp; (arr.Operator == row.Operator) &amp; (arr.Terminal == row.Terminal)]。非常感谢你!将等待您的更新。我真的很感激。
  • Hans Musgrave 提出的 groupby 确实很有帮助。我用你的建议更新了这个问题。如果您知道更多使其更快的其他方法,我将不胜感激。
【解决方案3】:

您的问题看起来是数据库操作中最常见的问题之一。我不完全理解你想要得到什么,因为你没有制定任务。现在来看看可能的解决方案 - 完全避免循环

您有一个很长的table,其中包含time, FlightID, Operator, Terminal, A_D 列。如果我理解正确,其他列和日期无关紧要。此外,start_timeend_time 在每一行中都是相同的。顺便说一句,您可能会得到带有代码table.loc[:, 'time'] = table.loc[:, 'START'].dt.timetime 列。

  1. table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal'])。而且你的table 会显着变短。

  2. 根据A_D值将table拆分为table_arrtable_deptable_arr = table.loc[table.loc[:, 'A_D'] == 'A', ['FlightID', 'Operator', 'Terminal', 'time']]table_dep = table.loc[table.loc[:, 'A_D'] == 'D', ['FlightID', 'Operator', 'Terminal', 'time']]

  3. 似乎您尝试使用循环获得的所有内容都可以通过一行获得:table_result = table_arr.merge(table_dep, how='right', on=['Operator', 'Terminal'], suffixes=('_arr', '_dep'))。与SQL中JOIN的操作基本相同。

根据我对您的问题的理解以及您提供的一小部分数据,您将获得所需的输出(所有FlightID_dep 值的FlightID_depFlightID_arr 之间的对应关系),没有任何循环这么快。 table_result 是:

  FlightID_arr Operator  Terminal  time_arr FlightID_dep  time_dep
0        DL401       DL         3  06:50:00        DL001  09:30:00
1        DL402       DL         3  07:45:00        DL001  09:30:00
2          NaN       VS         3       NaN        VS001  15:15:00

当然,在一般情况下(使用实际数据),您将需要多一步 - 在条件 time_arr &lt; time_dep 或您拥有的任何其他条件下过滤 table_result。很遗憾,您提供的数据不足以完全解决您的问题。

完整代码为:

import io
import pandas as pd

data = '''
START,END,A_D,Operator,FlightID,Terminal,TROUND_ID,tot
2017-03-26 16:55:00,2017-10-28 16:55:00,A,QR,QR001,4,QR002,70
2017-03-26 09:30:00,2017-06-11 09:30:00,D,DL,DL001,3,,84
2017-03-27 09:30:00,2017-10-28 09:30:00,D,DL,DL001,3,,78
2017-10-08 15:15:00,2017-10-22 15:15:00,D,VS,VS001,3,,45
2017-03-26 06:50:00,2017-06-11 06:50:00,A,DL,DL401,3,,9
2017-03-27 06:50:00,2017-10-28 06:50:00,A,DL,DL401,3,,19
2017-03-29 06:50:00,2017-04-19 06:50:00,A,DL,DL401,3,,3
2017-05-03 06:50:00,2017-10-25 06:50:00,A,DL,DL401,3,,32
2017-06-25 06:50:00,2017-10-22 06:50:00,A,DL,DL401,3,,95
2017-03-26 07:45:00,2017-10-28 07:45:00,A,DL,DL402,3,,58
'''

table = pd.read_csv(io.StringIO(data), parse_dates=[0, 1])
table.loc[:, 'time'] = table.loc[:, 'START'].dt.time
table = table.drop_duplicates(subset=['time', 'FlightID', 'Operator', 'Terminal'])
table_arr = table.loc[table.loc[:, 'A_D'] == 'A', ['FlightID', 'Operator', 'Terminal', 'time']]
table_dep = table.loc[table.loc[:, 'A_D'] == 'D', ['FlightID', 'Operator', 'Terminal', 'time']]

table_result = table_arr.merge(
    table_dep,
    how='right',
    on=['Operator', 'Terminal'],
    suffixes=('_arr', '_dep'))
print(table_result)

【讨论】:

  • 非常感谢您的回答 Poolka。我仍然不确定我是否完全理解它。你认为你可以在这里发布引导你到table_result的整个代码吗?你能解释一下为什么在这个例子中你不过滤 time_arr
  • @MiguelLambelho 所有代码都已发布,所有循环都替换为一行。我没有过滤结果,因为样本数据太小 - 提供的 10 行数据不需要过滤。
  • 我测试了你的解决方案,发现了一个小问题。通过使用合并方法,您将有多个出发航班链接到同一个到达。这就是我使用Already_linked 列的原因,并且在每次迭代中该列都会被更新。有什么办法可以解决这个问题吗?
  • @MiguelLambelho 您的问题不包含您的任务的完整描述。所以我显然不能知道。根据您如何陈述问题,您可以为每次到达选择随机出发。如果是这样,则使用table_result.drop_duplicates(subset=['FlightID_dep', 'FlightID_arr']) 执行额外的过滤。您的示例数据不允许尝试处理此类情况。
  • 是的。我明白那个。但是,我提供的代码包括选择最接近特定时间的到达的可能性。那么,有什么办法可以使用这种合并方法,使time 最接近x 列中的特定时间?
【解决方案4】:

此解决方案使用 pd.DataFrame.isin,它使用 numpy.in1d

显然 'isin' 对于小型数据集(如本示例)不一定更快,但对于大型数据集则要快得多。您必须针对您的数据运行它以确定性能。

flight_record_linkage.ipynb

使用c = pd.concat([c] * 10000, ignore_index=True)扩展数据集

  • 将数据集长度增加 3 个数量级(总共 10000 行)。
    • 原方法:挂墙时间:8.98s
    • 新方法:挂墙时间:16.4s
  • 将数据集长度增加 4 个数量级(总共 100000 行)。
    • 原方法:挂墙时间:8分17秒
    • 新方法:挂墙时间:1分14秒
  • 将数据集长度增加 5 个数量级(总共 1000000 行)。
    • 新方法:挂墙时间:11分33秒

新方法:使用 isin 和 apply

def apply_do_g(it_row):
    """
    This is your function, but using isin and apply
    """

    keep = {'Operator': [it_row.Operator], 'Terminal': [it_row.Terminal]}  # dict for isin combined mask

    holder1 = arr[list(keep)].isin(keep).all(axis=1)  # create boolean mask
    holder2 = arr.Already_linked.isin([0])  # create boolean mask
    holder3 = arr.hour < it_row.hour_aux  # create boolean mask

    holder = holder1 & holder2 & holder3  # combine the masks

    holder = arr.loc[holder]

    if not holder.empty:

        aux = np.absolute(holder.START - it_row.x).idxmin()

        c.loc[it_row.name, 'a'] = holder.loc[aux].FlightID  # use with apply 'it_row.name'

        arr.loc[aux, 'Already_linked'] = 1


def new_way_2():
    keep = {'A_D': ['D'], 'Already_linked': [0]}
    df_test = c[c[list(keep)].isin(keep).all(axis=1)].copy()  # returns the resultant df
    df_test.apply(lambda row: apply_do_g(row), axis=1)  # g is multiple DataFrames"


#call the function
new_way_2()

【讨论】:

  • 非常感谢您的评论!今天晚些时候将测试这两种方法。会告诉你它当时的表现。谢谢!
猜你喜欢
  • 2019-09-16
  • 1970-01-01
  • 1970-01-01
  • 2016-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多