Pandas apply 与 np.vectorize 从现有列创建新列的性能对比

2025-01-15 08:46:00
admin
原创
101
摘要:问题描述:我正在使用 Pandas 数据框,并希望根据现有列创建一个新列。我还没有看到关于df.apply()和之间的速度差异的很好的讨论np.vectorize(),所以我想在这里问一下。Pandasapply()函数很慢。根据我的测量结果(如下所示的一些实验),使用np.vectorize()比使用 Da...

问题描述:

我正在使用 Pandas 数据框,并希望根据现有列创建一个新列。我还没有看到关于df.apply()和之间的速度差异的很好的讨论np.vectorize(),所以我想在这里问一下。

Pandasapply()函数很慢。根据我的测量结果(如下所示的一些实验),使用np.vectorize()比使用 DataFrame 函数快 25 倍(或更多)apply(),至少在我的 2016 MacBook Pro 上是如此。这是预期的结果吗?为什么?

例如,假设我有以下包含N行的数据框:

N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
#     A   B
# 0  78  50
# 1  23  91
# 2  55  62
# 3  82  64
# 4  99  80

进一步假设我想创建一个新列作为两列A和的函数B。在下面的例子中,我将使用一个简单的函数divide()。要应用该函数,我可以使用df.apply()np.vectorize()

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)

df['result2'] = np.vectorize(divide)(df['A'], df['B'])

df.head()
#     A   B    result   result2
# 0  78  50  1.560000  1.560000
# 1  23  91  0.252747  0.252747
# 2  55  62  0.887097  0.887097
# 3  82  64  1.281250  1.281250
# 4  99  80  1.237500  1.237500

如果我增加到N真实世界的规模,比如 100 万或更多,那么我发现它np.vectorize()比 快 25 倍或更多df.apply()

以下是一些完整的基准测试代码:

import pandas as pd
import numpy as np
import time

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

for N in [1000, 10000, 100000, 1000000, 10000000]:    

    print ''
    A_list = np.random.randint(1, 100, N)
    B_list = np.random.randint(1, 100, N)
    df = pd.DataFrame({'A': A_list, 'B': B_list})

    start_epoch_sec = int(time.time())
    df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
    end_epoch_sec = int(time.time())
    result_apply = end_epoch_sec - start_epoch_sec

    start_epoch_sec = int(time.time())
    df['result2'] = np.vectorize(divide)(df['A'], df['B'])
    end_epoch_sec = int(time.time())
    result_vectorize = end_epoch_sec - start_epoch_sec


    print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \n            (N, result_apply, result_vectorize)

    # Make sure results from df.apply and np.vectorize match.
    assert(df['result'].equals(df['result2']))

结果如下所示:

N=1000, df.apply: 0 sec, np.vectorize: 0 sec

N=10000, df.apply: 1 sec, np.vectorize: 0 sec

N=100000, df.apply: 2 sec, np.vectorize: 0 sec

N=1000000, df.apply: 24 sec, np.vectorize: 1 sec

N=10000000, df.apply: 262 sec, np.vectorize: 4 sec

如果np.vectorize()总体上总是比 快df.apply(),那么为什么np.vectorize()没有更多地提及 ?我只看到与 相关的 StackOverflow 帖子df.apply(),例如:

pandas 根据其他列的值创建新列

如何将 Pandas 的“应用”函数应用于多列?

如何将函数应用于 Pandas 数据框的两列


解决方案 1:

首先我想说的是,Pandas 和 NumPy 数组的强大功能源自对数值数组的高性能矢量化计算。1矢量化计算的全部意义在于通过将计算转移到高度优化的 C 代码并利用连续的内存块来避免 Python 级循环。2

Python 级循环

现在我们可以看看一些时间。下面是所有Python 级循环,它们产生pd.Seriesnp.ndarraylist包含相同值的对象。为了分配给数据框内的系列,结果是可比较的。

# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)
N = 10**5

%timeit list(map(divide, df['A'], df['B']))                                   # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B'])                                # 48.1 ms
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]                      # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)]     # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True)                  # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1)              # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()]  # 11.6 s

以下是一些要点:

  1. 基于的方法(前 4 种)比基于的方法(后 3 种)tuple更有效。pd.Series

  2. np.vectorize、列表推导 +zipmap方法(即前 3 个)的性能大致相同。这是因为它们使用tuple 绕过了 的一些 Pandas 开销pd.DataFrame.itertuples

  3. raw=True与不使用相比,使用时速度有显著提升pd.DataFrame.apply。此选项将 NumPy 数组而不是对象提供给自定义函数pd.Series

pd.DataFrame.apply:又一个循环

要准确查看Pandas 传递的对象,您可以稍微修改您的函数:

def foo(row):
    print(type(row))
    assert False  # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)

输出:<class 'pandas.core.series.Series'>。与 NumPy 数组相比,创建、传递和查询 Pandas 系列对象会产生大量开销。这并不奇怪:Pandas 系列包含大量用于保存索引、值、属性等的支架。

用 再次进行同样的练习raw=True,你会看到<class 'numpy.ndarray'>。所有这些都在文档中描述,但亲眼看到更有说服力。

np.vectorize:假矢量化

的文档np.vectorize有以下说明:

矢量化函数pyfunc像 python map 函数一样对输入数组的连续元组进行评估,但它使用 numpy 的广播规则。

“广播规则”在这里无关紧要,因为输入数组具有相同的维度。与的比较很有map启发性,因为map上面的版本具有几乎相同的性能。源代码显示了正在发生的事情:通过将np.vectorize您的输入函数转换为通用函数(“ufunc”)np.frompyfunc。有一些优化,例如缓存,可以带来一些性能改进。

简而言之,np.vectorize它执行 Python 级循环执行的操作,但pd.DataFrame.apply会增加大量开销。没有您看到的 JIT 编译numba(见下文)。它只是一种便利。

真正的矢量化:您应该使用什么

为什么没有在任何地方提到上述差异? 因为真正矢量化计算的性能使它们变得无关紧要:

%timeit np.where(df['B'] == 0, 0, df['A'] / df['B'])       # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0)  # 1.96 ms

是的,这比上述最快的循环解决方案快约 40 倍。这两种方法都可以接受。在我看来,第一种方法简洁、易读且高效。只有numba当性能至关重要并且这是瓶颈的一部分时,才考虑其他方法,例如下面的方法。

numba.njit:提高效率

当循环认为可行时,它们通常通过numba底层 NumPy 数组进行优化,以尽可能多地转移到 C。

确实,numba将性能提升到了微秒级。如果不做一些繁琐的工作,很难获得比这更高的效率。

from numba import njit

@njit
def divide(a, b):
    res = np.empty(a.shape)
    for i in range(len(a)):
        if b[i] != 0:
            res[i] = a[i] / b[i]
        else:
            res[i] = 0
    return res

%timeit divide(df['A'].values, df['B'].values)  # 717 µs

使用@njit(parallel=True)可能会为更大的阵列提供进一步的推动力。


1数字类型包括:intfloatdatetimeboolcategory。它们不包括 objectdtype 并且可以保存在连续的内存块中。

2
NumPy 操作比 Python 更高效至少有两个原因:

  • Python 中的一切都是对象。与 C 不同,这包括数字。因此,Python 类型具有原生 C 类型不存在的开销。

  • NumPy 方法通常基于 C。此外,尽可能使用优化算法。

解决方案 2:

您的函数越复杂(即,numpy可以移动到其自身内部的函数越少),您就会发现性能差异越小。例如:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))

def parse_name(name):
    if name.lower().startswith('a'):
        return 'A'
    elif name.lower().startswith('e'):
        return 'E'
    elif name.lower().startswith('i'):
        return 'I'
    elif name.lower().startswith('o'):
        return 'O'
    elif name.lower().startswith('u'):
        return 'U'
    return name

parse_name_vec = np.vectorize(parse_name)

进行一些计时:

使用 Apply

%timeit name_series.apply(parse_name)

结果:

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

使用np.vectorize

%timeit parse_name_vec(name_series)

结果:

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

当您调用时,Numpy 会尝试将 Python 函数转换为 numpyufunc对象np.vectorize。它是如何做到这一点的,我实际上并不知道 - 您必须比我愿意 ATM 更多地挖掘 numpy 的内部结构。也就是说,它在简单的数值函数上似乎比这里的基于字符串的函数做得更好。

将大小增加到 1,000,000:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))

apply

%timeit name_series.apply(parse_name)

结果:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

结果:

794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

更好的(矢量化)方法是np.select

cases = [
    name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
    name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
    name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()

时间安排:

%timeit np.select(cases, replacements, default=name_series)

结果:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用