首页 > 解决方案 > 使用 np.vectorize 将函数应用于 Pandas 数据框中所有列的正确方法

问题描述

我有一个带有任意列数的 Pandas 数据框。我想对每一列应用一个函数。从关于这个 SO POst的讨论来看,与 pandas apply 函数相比,使用 np.vectorize 更好。

但是,我将如何使用 np.vectorize 对每一列执行操作?

我能想出的最好的主意是带有 for 循环的 np.vectorize ,但这会在我的机器上在一个虚拟数据帧上花费 2 倍的时间。是最优applyraw=True,然后就更快的选择而言,我们只能利用numba.

test_df = pd.DataFrame({'a': np.arange(5), 'b': np.arange(5), 'c': np.arange(5)})

def myfunc(a, b):
    return a+b

start_time = time.time()
test_df.apply(lambda x: x + 3, raw = True)
print("--- %s seconds ---" % (time.time() - start_time))

start_time = time.time()
for i in range(test_df.shape[1]):
    np.vectorize(myfunc)(test_df.iloc[:,i], 3)
print("--- %s seconds ---" % (time.time() - start_time))

标签: pythonpandasnumpy

解决方案


正确的使用方法np.vectorize不要使用它——除非你正在处理一个只接受标量值的函数,而且你不关心速度。当我测试过它时,显式 Python 迭代速度更快。

至少在使用numpy数组时是这种情况。使用DataFrames时,事情变得更加复杂,因为提取 Series 和重新创建帧会大大扭曲时间。


但是,让我们更详细地看一下您的示例。

您的示例框架:

In [177]: test_df = pd.DataFrame({'a': np.arange(5), 'b': np.arange(5), 'c': np.arange(5)})
     ...: 
In [178]: test_df
Out[178]: 
   a  b  c
0  0  0  0
1  1  1  1
2  2  2  2
3  3  3  3
4  4  4  4
In [179]: def myfunc(a, b):
     ...:     return a+b
     ...: 

您的申请:

In [180]: test_df.apply(lambda x: x+3, raw=True)
Out[180]: 
   a  b  c
0  3  3  3
1  4  4  4
2  5  5  5
3  6  6  6
4  7  7  7
In [181]: timeit test_df.apply(lambda x: x+3, raw=True)
186 µs ± 524 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
# 1.23 ms ± 13.9 µs per loop without **raw**

只需使用框架自己的加法运算符,我就能得到同样的结果——而且速度更快。好的,对于一个不起作用的更通用的功能。您使用applywith default axisraw意味着您有一个一次只能处理一列的函数。

In [182]: test_df+3
Out[182]: 
   a  b  c
0  3  3  3
1  4  4  4
2  5  5  5
3  6  6  6
4  7  7  7
In [183]: timeit test_df+3
114 µs ± 3.02 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

随着raw您将 numpy 数组传递给lambda. 整个帧的数组是:

In [184]: test_df.to_numpy()
Out[184]: 
array([[0, 0, 0],
       [1, 1, 1],
       [2, 2, 2],
       [3, 3, 3],
       [4, 4, 4]])
In [185]: test_df.to_numpy()+3
Out[185]: 
array([[3, 3, 3],
       [4, 4, 4],
       [5, 5, 5],
       [6, 6, 6],
       [7, 7, 7]])
In [186]: timeit test_df.to_numpy()+3
13.1 µs ± 119 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

那要快得多。但是返回一个框架需要时间。

In [188]: timeit pd.DataFrame(test_df.to_numpy()+3, columns=test_df.columns)
91.1 µs ± 769 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [189]: 

测试vectorize

In [218]: f=np.vectorize(myfunc)

f迭代地应用于myfunc输入数组的每个元素。它有一个明确的性能免责声明。

即使对于这个小数组,与将函数直接应用于数组相比,它也很慢:

In [219]: timeit f(test_df.to_numpy(),3)
42.3 µs ± 123 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

传递框架本身

In [221]: timeit f(test_df,3)
69.8 µs ± 1.98 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [223]: timeit pd.DataFrame(f(test_df,3), columns=test_df.columns)
154 µs ± 2.11 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

并迭代地应用于列 - 慢得多:

In [226]: [f(test_df.iloc[:,i], 3) for i in range(test_df.shape[1])]
Out[226]: [array([3, 4, 5, 6, 7]), array([3, 4, 5, 6, 7]), array([3, 4, 5, 6, 7])]
In [227]: timeit [f(test_df.iloc[:,i], 3) for i in range(test_df.shape[1])]
477 µs ± 17.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

但是很多额外的时间来自“提取”列:

In [228]: timeit [f(test_df.to_numpy()[:,i], 3) for i in range(test_df.shape[1])]
127 µs ± 357 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

引用np.vectorize文档:

Notes
-----
The `vectorize` function is provided primarily for convenience, not for
performance. The implementation is essentially a for loop.

推荐阅读