【问题标题】:How to vectorize (make use of pandas/numpy) instead of using a nested for loop如何矢量化(利用 pandas/numpy)而不是使用嵌套的 for 循环
【发布时间】:2018-11-29 17:57:48
【问题描述】:

我希望有效地使用pandas(或numpy)而不是带有if 语句的嵌套for 循环来解决特定问题。这是一个玩具版本:

假设我有以下两个 DataFrames

import pandas as pd
import numpy as np

dict1 = {'vals': [100,200], 'in': [0,1], 'out' :[1,3]}
df1 = pd.DataFrame(data=dict1)

dict2 = {'vals': [500,800,300,200], 'in': [0.1,0.5,2,4], 'out' :[0.5,2,4,5]}
df2 = pd.DataFrame(data=dict2)

现在我希望遍历每个数据帧的每一行,并在满足特定条件时将 vals 相乘。这段代码适用于我想要的

ans = []

for i in range(len(df1)):
    for j in range(len(df2)):
        if (df1['in'][i] <= df2['out'][j] and df1['out'][i] >= df2['in'][j]):
            ans.append(df1['vals'][i]*df2['vals'][j])

np.sum(ans)

但是,显然这非常效率低下,实际上我的 DataFrame 可能有数百万个条目,这使得它无法使用。我也没有让我们使用pandasnumpy 高效的向量实现。有谁知道如何有效地矢量化这个嵌套循环?

我觉得这段代码类似于矩阵乘法,所以使用outer 可以取得进展吗?这是if 条件,我发现很难融入其中,因为if 逻辑需要将df1 中的每个条目与df2 中的所有条目进行比较。

【问题讨论】:

  • 这可以实现为矩阵形式,但如果如你所说的数据帧有数百万行,那么交叉矩阵将是数百万。在这种情况下,它始终是内存和 CPU 之间的补偿。您可以以内存为代价对代码进行矢量化处理,或者使用itertooples 获得更高效的代码。当您有许多较小的向量而不是两个巨大的向量时,向量化是最有效的。

标签: python pandas numpy vectorization


【解决方案1】:

您也可以使用像 Numba 这样的编译器来完成这项工作。这也将优于矢量化解决方案,并且不需要临时数组。

示例

import numba as nb
import numpy as np
import pandas as pd
import time

@nb.njit(fastmath=True,parallel=True,error_model='numpy')
def your_function(df1_in,df1_out,df1_vals,df2_in,df2_out,df2_vals):
  sum=0.
  for i in nb.prange(len(df1_in)):
      for j in range(len(df2_in)):
          if (df1_in[i] <= df2_out[j] and df1_out[i] >= df2_in[j]):
              sum+=df1_vals[i]*df2_vals[j]
  return sum

测试

dict1 = {'vals': np.random.randint(1, 100, 1000),
         'in': np.random.randint(1, 10, 1000),
         'out': np.random.randint(1, 10, 1000)}
df1 = pd.DataFrame(data=dict1)
dict2 = {'vals': np.random.randint(1, 100, 1500),
         'in': 5*np.random.random(1500),
         'out': 5*np.random.random(1500)}
df2 = pd.DataFrame(data=dict2)

# First call has some compilation overhead
res=your_function(df1['in'].values, df1['out'].values, df1['vals'].values,
                  df2['in'].values, df2['out'].values, df2['vals'].values)

t1 = time.time()
for i in range(1000):
  res = your_function(df1['in'].values, df1['out'].values, df1['vals'].values,
                      df2['in'].values, df2['out'].values, df2['vals'].values)

print(time.time() - t1)

时间

vectorized solution @AGN Gazer: 9.15ms
parallelized Numba Version: 0.7ms

【讨论】:

  • 感谢您向我介绍numba。这个解决方案效果很好,而且速度足够快。
【解决方案2】:
m1 = np.less_equal.outer(df1['in'], df2['out']) 
m2 = np.greater_equal.outer(df1['out'], df2['in'])
m = np.logical_and(m1, m2)
v12 = np.outer(df1['vals'], df2['vals'])
print(v12[m].sum())

或者,用这个长行替换前三行:

m = np.less_equal.outer(df1['in'], df2['out']) & np.greater_equal.outer(df1['out'], df2['in'])
s = np.outer(df1['vals'], df2['vals'])[m].sum()

对于非常大的问题,推荐dask

计时测试:

这是使用 1000 和 1500 长数组时的时序比较:

In [166]: dict1 = {'vals': np.random.randint(1,100,1000), 'in': np.random.randint(1,10,1000), 'out': np.random.randint(1,10,1000)}
     ...: df1 = pd.DataFrame(data=dict1)
     ...: 
     ...: dict2 = {'vals': np.random.randint(1,100,1500), 'in': 5*np.random.random(1500), 'out': 5*np.random.random(1500)}
     ...: df2 = pd.DataFrame(data=dict2)

作者原创方法(Python循环):

In [167]: def f(df1, df2):
     ...:     ans = []
     ...:     for i in range(len(df1)):
     ...:         for j in range(len(df2)):
     ...:             if (df1['in'][i] <= df2['out'][j] and df1['out'][i] >= df2['in'][j]):
     ...:                 ans.append(df1['vals'][i]*df2['vals'][j])
     ...:     return np.sum(ans)
     ...: 
     ...: 

In [168]: %timeit f(df1, df2)
47.3 s ± 1.02 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

@Ben.T 方法:

In [170]: %timeit df2['ans']= df2.apply(lambda row: df1['vals'][(df1['in'] <= row['out']) & (df1['out'] >= row['in'])].sum()*row['vals'],1); df2['a
     ...: ns'].sum()
2.22 s ± 40.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

这里提出的矢量化解决方案:

In [171]: def g(df1, df2):
     ...:     m = np.less_equal.outer(df1['in'], df2['out']) & np.greater_equal.outer(df1['out'], df2['in'])
     ...:     return np.outer(df1['vals'], df2['vals'])[m].sum()
     ...: 
     ...: 

In [172]: %timeit g(df1, df2)

7.81 ms ± 127 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

【讨论】:

  • 如果 df1/df2 真的有数百万行长,m1 将太大而无法实现。
  • @DSM 可能,但这是一个矢量化问题的解决方案(至少对于较少的数据)。真正的替代解决方案是什么?也许Numba 来编译 OP 循环?或者尝试dask,如果它可以处理非常大的数据。
【解决方案3】:

你的答案:

471 µs ± 35.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

方法1(慢3倍以上):

df1.apply(lambda row: list((df2['vals'][(row['in'] <= df2['out']) & (row['out'] >= df2['in'])] * row['vals'])), axis=1).sum()

1.56 ms ± 7.56 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

方法2(慢2倍以上):

ans = []
for name, row in df1.iterrows():
    _in = row['in']
    _out = row['out']
    _vals = row['vals']
    ans.append(df2['vals'].loc[(df2['in'] <= _out) & (df2['out'] >= _in)].values * _vals)

1.01 ms ± 8.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

方法3(快3倍以上):

df1_vals = df1.values
ans = np.zeros(shape=(len(df1_vals), len(df2.values)))
for i in range(df1_vals.shape[0]):
    df2_vals = df2.values
    df2_vals[:, 2][~np.logical_and(df1_vals[i, 1] >= df2_vals[:, 0], df1_vals[i, 0] <= df2_vals[:, 1])] = 0
    ans[i, :] = df2_vals[:, 2] * df1_vals[i, 2]

144 µs ± 3.11 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

在方法 3 中,您可以通过执行以下操作查看解决方案:

ans[ans.nonzero()]

Out[]: array([ 50000.,  80000., 160000.,  60000.]

我想不出一种方法来删除底层循环:(但我在这个过程中学到了很多关于 numpy 的知识!(学习了)

【讨论】:

  • 在进行时序比较时,需要使用大数据:否则您将测量数组创建等开销。
  • @AGNGazer 感谢您的建议,这很有道理!一个快速的问题,当我引起你的注意时,在我的方法 3 中,我分配了一个具有最大可能形状的零数组,然后如果仍然存在任何零,则在最后将其缩小。目的是让 numpy 数组在内存中只占用该空间一次,并使用ans[i, :] = ... 来填充结果。这是“numpythonic”吗?
  • 我不是“numthonics”或“pythonics”方面的专家。我认为代码(至少是我在工作中编写的)必须高效且易于阅读/维护。其他一切对我来说都不重要(太多了;我当然喜欢优雅的解决方案)。我看不出预分配更多内存有什么问题你无法准确猜出你需要多少。
  • @AGNGazer 这是一种很好的态度!为了让我学习 pandas/numpy 的有效解决方案,您是否可以指出任何资源,或者您认为从这些答案中学习是一个良好的开端?
  • 老实说,我没有使用任何其他资源,也不推荐任何其他资源。我认为,如果您尝试了解其他人的解决方案尝试为人们的问题提供有关 SO 的答案(即使一开始会花费您更长的时间-不要注意时间:最终结果和“到达那里”是最重要的事情)一般来说是学习 programmig 和诸如 pandas、numpy 等包的最好的东西。
【解决方案4】:

一种方法是使用apply。在 df2 中创建一列,其中包含 df1 中的 val 总和,满足您的输入和输出标准,乘以 df2 行的 vals

df2['ans']= df2.apply(lambda row: df1['vals'][(df1['in'] <= row['out']) & 
                                              (df1['out'] >= row['in'])].sum()*row['vals'],1)

那么就对这一列求和

df2['ans'].sum()

【讨论】:

  • 我不确定这会发生什么变化。 apply 只是引擎盖下的一个循环,方法仍然是 O(N^2)。
  • @DSM 当然,但我认为它仍然比两个循环快for?
  • 我认为你是对的,部分矢量化的比较会更快,但我认为它在 ~1M 行规模上没有足够的帮助,unfort。
  • 所以我已经尝试过了,即使 DataFrame 只有 10000 个条目,它仍然很慢。我认为 @DSM 关于 apply 只是在底层隐藏了一个 for 循环是正确的?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-02-01
  • 1970-01-01
  • 2021-04-06
  • 2018-10-04
  • 1970-01-01
  • 1970-01-01
  • 2015-11-01
相关资源
最近更新 更多