【问题标题】:How to efficiently iterate a pandas DataFrame and increment a NumPy array on these values?如何有效地迭代 pandas DataFrame 并在这些值上增加 NumPy 数组?
【发布时间】:2019-02-05 15:43:56
【问题描述】:

我的 pandas/numpy 生锈了,我写的代码感觉效率低下。

我在 Python3.x 中初始化一个 numpy 零数组,长度为 1000。就我的目的而言,这些只是整数:

import numpy as np
array_of_zeros =  np.zeros((1000, ), )

我还有以下DataFrame(比我的实际数据小很多)

import pandas as pd
dict1 = {'start' : [100, 200, 300], 'end':[400, 500, 600]}
df = pd.DataFrame(dict1)
print(df)
##
##    start     end
## 0    100     400
## 1    200     500
## 2    300     600

DataFrame 有两列,startend。这些值表示一个值范围,即start 将始终是小于end 的整数。上面,我们看到第一行的范围是100-400,接下来是200-500,然后是300-600

我的目标是逐行遍历 pandas DataFrame,并根据这些索引位置递增 numpy 数组 array_of_zeros。因此,如果在1020 的数据框中有一行,我想将索引 10-20 的零增加 +1。

这是我想做的代码:

import numpy as np
array_of_zeros =  np.zeros((1000, ), )

import pandas as pd
dict1 = {'start' : [100, 200, 300], 'end':[400, 500, 600]}
df = pd.DataFrame(dict1)
print(df)

for idx, row in df.iterrows():
    for i in range(int(row.start), int(row.end)+1):
        array_of_zeros[i]+=1

而且它有效!

print(array_of_zeros[15])
## output: 0.0
print(array_of_zeros[600])
## output: 1.0
print(array_of_zeros[400])
## output: 3.0
print(array_of_zeros[100])
## output: 1.0
print(array_of_zeros[200])
## output: 2.0

我的问题:这是非常笨拙的代码!我不应该在 numpy 数组中使用这么多 for 循环!如果输入数据帧非常大,此解决方案将非常低效

是否有更有效(即更基于 numpy)的方法来避免这种 for 循环?

for i in range(int(row.start), int(row.end)+1):
    array_of_zeros[i]+=1

也许有面向 pandas 的解决方案?

【问题讨论】:

    标签: python python-3.x pandas numpy


    【解决方案1】:

    您可以使用 NumPy 数组索引来避免内部循环,即res[np.arange(A[i][0], A[i][1]+1)] += 1,但这并不高效,因为它涉及创建新数组和使用高级索引。

    相反,您可以使用numba1 来优化您的算法,就像它现在的样子一样。下面的示例显示了通过将性能关键逻辑移至 JIT 编译代码来显着提高性能。

    from numba import jit
    
    @jit(nopython=True)
    def jpp(A):
        res = np.zeros(1000)
        for i in range(A.shape[0]):
            for j in range(A[i][0], A[i][1]+1):
                res[j] += 1
        return res
    

    一些基准测试结果:

    # Python 3.6.0, NumPy 1.11.3
    
    # check result the same
    assert (jpp(df[['start', 'end']].values) == original(df)).all()
    assert (pir(df) == original(df)).all()
    assert (pir2(df) == original(df)).all()
    
    # time results
    df = pd.concat([df]*10000)
    
    %timeit jpp(df[['start', 'end']].values)  # 64.6 µs per loop
    %timeit original(df)                      # 8.25 s per loop
    %timeit pir(df)                           # 208 ms per loop
    %timeit pir2(df)                          # 1.43 s per loop
    

    用于基准测试的代码:

    def original(df):
        array_of_zeros = np.zeros(1000)
        for idx, row in df.iterrows():
            for i in range(int(row.start), int(row.end)+1):
                array_of_zeros[i]+=1   
        return array_of_zeros
    
    def pir(df):
        return np.bincount(np.concatenate([np.arange(a, b + 1) for a, b in \
                           zip(df.start, df.end)]), minlength=1000)
    
    def pir2(df):
        a = np.zeros((1000,), np.int64)
        for b, c in zip(df.start, df.end):
            np.add.at(a, np.arange(b, c + 1), 1)
        return a
    

    1 对于后人,我将 @piRSquared 的精彩评论包括在为什么 numba 在这里有帮助:

    numba 的优点是循环非常高效。虽然可以 了解 NumPy 的大部分 API,通常最好避免创建 循环内的 NumPy 对象。我的代码正在为 数据框中的每一行。然后在使用之前将它们连接起来 二进制计数。 @jpp 的 numba 代码创建的额外对象非常少,并且 利用了很多已经存在的东西。我和我的区别 NumPy 解决方案和@jpp 的numba 解决方案大约是4-5 倍。两者都是 线性的,应该很快。

    【讨论】:

    • 你知道我喜欢numba。还有from numba import njit -> njitjit(nopython=True)
    • @piRSquared,谢谢!我希望你不介意我从你的解决方案中添加时间。是的,我知道njit,但我认为有时对于numba 的新手来说更明确。
    • 谢谢! jpp 的实现非常令人印象深刻!我剩下的问题:是否可以使用numba 优化@piRSquared 代码?
    • @ShanZhengYang numba的优点是循环非常高效。虽然它可以理解 Numpy 的大部分 api,但通常最好避免在循环中创建 Numpy 对象。我的代码正在为数据框中的每一行创建一个 Numpy 数组。然后在使用 bincount 之前将它们连接起来。 @jpp 的 numba 代码创建的额外对象非常少,并利用了很多已经存在的东西。我的 Numpy 解决方案和 jpp 的 numba 解决方案之间的差异大约是 4-5 倍。两者都是线性的,应该很快。
    • 你为什么用 res[np.arange(A[i][0], A[i][1]+1)] += 1 (分配一个临时数组,填入一个使用值循环,使用它在另一个循环中索引数组 res)?是否有应纠正的误导性教程?一个简单的嵌套循环是 aprox。由于非常明显的原因(如上所述),速度快了 2 倍。尽管如此,即使对于这种方法,您的时间也很慢。在使用 timeit 之前,您是否调用过该函数一次?如果不是,您第一次测量了恒定的编译开销。 0.2s,也可以通过 cache=True 来最小化
    【解决方案2】:

    numpy.bincount

    np.bincount(np.concatenate(
        [np.arange(a, b + 1) for a, b in zip(df.start, df.end)]
    ), minlength=1000)
    

    numpy.add.at

    a = np.zeros((1000,), np.int64)
    for b, c in zip(df.start, df.end):
      np.add.at(a, np.arange(b, c + 1), 1)
    

    【讨论】:

      【解决方案3】:

      我的解决方案

      for x, y in zip(df.start, df.end):
          array_of_zeros[x:y+1]+=1
      

      【讨论】:

      • array_of_zeros[x:y] += 1?
      • @piRSquared 我之前试过,好像不行..让我再测试一下
      • 不应该包括y 吗?即x:y不包括y,不应该是x:(y+1)吗??
      • @Onyambu hi double ??固定:-)
      • 使用循环是确保这样做的方法。但效率不高,需要更多时间。有关迭代 DataFrame 的不同方法及其效率和时间估计的分析,请参阅此博客文章。 towardsdatascience.com/…
      猜你喜欢
      • 2016-07-14
      • 2020-12-17
      • 2019-11-27
      • 2019-10-22
      • 2016-03-11
      • 1970-01-01
      • 2014-01-19
      • 2022-01-22
      • 1970-01-01
      相关资源
      最近更新 更多