代码之家  ›  专栏  ›  技术社区  ›  dumbledad

慢得离谱的简单熊猫应用功能[复制]

  •  1
  • dumbledad  · 技术社区  · 6 年前

    这是一个自我回答的qna,旨在指导用户使用apply的陷阱和好处。

    我已经看到许多关于堆栈溢出的问题的答案,其中涉及到apply的使用。我也看到用户在他们下面评论说 apply 是缓慢的“,应该避免”。

    我读过很多关于表演的文章 应用 是慢的。我在文件中也看到了一个免责声明 应用 只是传递udf的一个方便函数(现在似乎找不到)。所以,普遍的共识是 应用 如果可能的话应该避免。然而,这提出了以下问题:

    1. 如果 应用 很糟糕,那为什么在api中呢?
    2. 我应该如何以及何时编写代码 应用 -免费?
    3. 有没有什么情况 应用 好的 (比其他可能的解决方案更好)?
    0 回复  |  直到 6 年前
        1
  •  39
  •   cs95 abhishek58g    6 年前

    apply ,你永远不需要的便利功能

    我们从一个接一个的回答OP中的问题开始。

    如果 应用 很糟糕,那为什么在api中呢?

    DataFrame.apply Series.apply 便利功能 分别在dataframe和series对象上定义。 应用 接受对数据帧应用转换/聚合的任何用户定义函数。 应用 它实际上是一颗银弹,可以做任何现有熊猫功能所不能做的事情。

    一些事情 应用 可以做到:

    • 在数据帧或序列上运行任何用户定义的函数
    • 按行应用函数( axis=1 )或按列( axis=0 )在数据帧上
    • 应用函数时执行索引对齐
    • 使用用户定义的函数执行聚合(但是,我们通常更喜欢 agg transform 在这些情况下)
    • 执行元素转换
    • 将聚合结果广播到原始行(请参见 result_type 争论)。
    • 接受要传递给用户定义函数的位置/关键字参数。

    ……以及其他。有关详细信息,请参见 Row or Column-wise Function Application 在文档中。

    所以,有了这些特性,为什么 应用 不好?它是 因为 应用 缓慢的 . 熊猫对你的功能没有任何假设,所以 迭代应用函数 必要时发送到每行/每列。另外,处理 全部的 在上述情况下 应用 在每次迭代中都会产生一些主要的开销。此外, 应用 消耗更多的内存,这对内存有限的应用程序来说是一个挑战。

    很少有情况下 应用 适合使用(更多信息见下文)。 如果你不确定是否应该使用 应用 ,你可能不应该。


    我们来回答下一个问题。

    我应该如何以及何时编写代码 应用 -免费?

    数值数据
    如果您使用的是数字数据,那么很可能已经有一个矢量化的cython函数正是您想要做的(如果没有,请询问堆栈溢出问题或在github上打开一个功能请求)。

    对比 应用 一个简单的加法运算。

    df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
    df
    
       A   B
    0  9  12
    1  4   7
    2  2   5
    3  1   4
    

    df.apply(np.sum)
    
    A    16
    B    28
    dtype: int64
    
    df.sum()
    
    A    16
    B    28
    dtype: int64
    

    就性能而言,没有可比性,cythonized等价物要快得多。不需要图表,因为即使是玩具数据,这种差异也是显而易见的。

    %timeit df.apply(np.sum)
    %timeit df.sum()
    2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    

    即使您使用 raw 争论,还是慢了一倍。

    %timeit df.apply(np.sum, raw=True)
    840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    另一个例子:

    df.apply(lambda x: x.max() - x.min())
    
    A    8
    B    8
    dtype: int64
    
    df.max() - df.min()
    
    A    8
    B    8
    dtype: int64
    
    %timeit df.apply(lambda x: x.max() - x.min())
    %timeit df.max() - df.min()
    
    2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    

    一般来说,如果可能的话,寻找矢量化的替代方案。


    字符串/正则表达式
    pandas在大多数情况下提供“矢量化”字符串函数,但很少有情况下这些函数不…可以这么说。

    一个常见的问题是检查列中的值是否存在于同一行的另一列中。

    df = pd.DataFrame({
        'Name': ['mickey', 'donald', 'minnie'],
        'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
        'Value': [20, 10, 86]})
    df
    
         Name  Value                       Title
    0  mickey     20                  wonderland
    1  donald     10  welcome to donald's castle
    2  minnie     86      Minnie mouse clubhouse
    

    这将返回第二行和第三行,因为“donald”和“minnie”分别出现在各自的“title”列中。

    使用apply,可以使用

    df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
    
    0    False
    1     True
    2     True
    dtype: bool
    
    df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
    
         Name                       Title  Value
    1  donald  welcome to donald's castle     10
    2  minnie      Minnie mouse clubhouse     86
    

    然而,使用列表理解有更好的解决方案。

    df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
    
         Name                       Title  Value
    1  donald  welcome to donald's castle     10
    2  minnie      Minnie mouse clubhouse     86
    

    %timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
    %timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
    
    2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    

    这里要注意的是,迭代例程碰巧比 应用 ,因为开销较低。如果需要处理NANS和无效DType,可以使用自定义函数来构建此函数,然后可以在列表理解中调用参数。

    有关何时应将清单理解视为一个好的选择的更多信息,请参阅我的写作: For loops with pandas - When should I care? .

    注释
    日期和日期时间操作也有矢量化版本。例如,你应该 pd.to_datetime(df['date']) ,结束, 说, df['date'].apply(pd.to_datetime) .

    阅读更多 docs .


    分解列表列

    s = pd.Series([[1, 2]] * 3)
    s
    
    0    [1, 2]
    1    [1, 2]
    2    [1, 2]
    dtype: object
    

    人们很想使用 apply(pd.Series) . 这是 好可怕 在性能方面。

    s.apply(pd.Series)
    
       0  1
    0  1  2
    1  1  2
    2  1  2
    

    更好的选择是列出列并将其传递给pd.dataframe。

    pd.DataFrame(s.tolist())
    
       0  1
    0  1  2
    1  1  2
    2  1  2
    

    %timeit s.apply(pd.Series)
    %timeit pd.DataFrame(s.tolist())
    
    2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    

    最后,

    有没有什么情况 应用 好的 ?

    apply是一个方便的功能,所以 在这种情况下,开销是微不足道的,足以原谅。这实际上取决于函数被调用的次数。

    为序列而不是数据帧矢量化的函数
    如果要对多个列应用字符串操作怎么办?如果要将多个列转换为datetime怎么办?这些函数仅对序列进行矢量化,因此它们必须 应用 在要转换/操作的每个列上。

    df = pd.DataFrame(
             pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2), 
             columns=['date1', 'date2'])
    df
    
           date1      date2
    0 2018-12-31 2019-01-02
    1 2019-01-04 2019-01-06
    2 2019-01-08 2019-01-10
    3 2019-01-12 2019-01-14
    4 2019-01-16 2019-01-18
    5 2019-01-20 2019-01-22
    6 2019-01-24 2019-01-26
    7 2019-01-28 2019-01-30
    
    df.dtypes
    
    date1    object
    date2    object
    dtype: object
    

    这是一个可以受理的案件 应用 :

    df.apply(pd.to_datetime, errors='coerce').dtypes
    
    date1    datetime64[ns]
    date2    datetime64[ns]
    dtype: object
    

    请注意,这对 stack ,或者只使用显式循环。所有这些选项都比使用 应用 ,但差别很小,足以让人原谅。

    %timeit df.apply(pd.to_datetime, errors='coerce')
    %timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
    %timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
    %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
    
    5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    您可以对其他操作进行类似的操作,例如字符串操作,或转换为类别。

    u = df.apply(lambda x: x.str.contains(...))
    v = df.apply(lambda x: x.astype(category))
    

    V/S

    u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
    v = df.copy()
    for c in df:
        v[c] = df[c].astype(category)
    

    等等…


    将序列转换为 str D-型使用 astype V/S 应用
    这似乎是api的一个特性。使用 应用 将序列中的整数转换为字符串比使用 阿斯泰普 .

    enter image description here 图是用 perfplot 图书馆。

    import perfplot
    
    perfplot.show(
        setup=lambda n: pd.Series(np.random.randint(0, n, n)),
        kernels=[
            lambda s: s.astype(str),
            lambda s: s.apply(str)
        ],
        labels=['astype', 'apply'],
        n_range=[2**k for k in range(1, 20)],
        xlabel='N',
        logx=True,
        logy=True,
        equality_check=lambda x, y: (x == y).all()
    )
    

    有了漂浮物,我看到了 阿斯泰普 始终与 应用 . 所以这与测试中的数据是整数类型有关。


    GroupBy 涉及两个功能的操作
    GroupBy.apply 直到现在还没有讨论过,但是 groupby.apply组 也是一个迭代便利函数,用于处理 分组 功能没有。

    一个常见的要求是先执行groupby,然后执行两个基本操作,如“lagged cumsum”:

    df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
    df
    
       A   B
    0  a  12
    1  a   7
    2  b   5
    3  c   4
    4  c   5
    5  c   4
    6  d   3
    7  d   2
    8  e   1
    9  e  10
    

    这里需要连续两次Groupby呼叫:

    df.groupby('A').B.cumsum().groupby(df.A).shift()
    
    0     NaN
    1    12.0
    2     NaN
    3     NaN
    4     4.0
    5     9.0
    6     NaN
    7     3.0
    8     NaN
    9     1.0
    Name: B, dtype: float64
    

    使用 应用 你可以把它缩短为一个单一的呼叫。

    df.groupby('A').B.apply(lambda x: x.cumsum().shift())
    
    0     NaN
    1    12.0
    2     NaN
    3     NaN
    4     4.0
    5     9.0
    6     NaN
    7     3.0
    8     NaN
    9     1.0
    Name: B, dtype: float64
    

    很难量化性能,因为它依赖于数据。但总的来说, 应用 如果目标是减少 groupby 打电话(因为 子句 也很贵)。

        2
  •  20
  •   jpp    6 年前

    所有 apply S不一样

    下表建议何时考虑 应用 . 绿色表示可能有效;红色表示避免。

    enter image description here

    一些 这是直觉的: pd.Series.apply 是一个python级别的行循环,同上 pd.DataFrame.apply 按行排列(行) axis=1 )这些错误的使用是多方面的。另一篇文章则更深入地探讨了这些问题。流行的解决方案是使用矢量化方法、列表理解(假设数据是干净的)或高效的工具,如 pd.DataFrame 构造器(例如避免 apply(pd.Series) )

    如果你正在使用 pd.dataframe.apply文件 按行,指定 raw=True (在可能的情况下)通常是有益的。在这个阶段, numba 通常是更好的选择。

    GroupBy.apply :一般受欢迎

    重复 groupby 避免的操作 应用 会影响表演。 groupby.apply组 在这里通常很好,只要您在自定义函数中使用的方法本身是矢量化的。有时,对于要应用的分组聚合,没有本地pandas方法。在这种情况下,对于少数组 应用 使用自定义函数仍然可以提供合理的性能。

    pd.dataframe.apply文件 纵列:混合袋

    pd.dataframe.apply文件 按列( axis=0 )是个有趣的案例。对于少量行和大量列,它几乎总是很昂贵的。对于相对于列的大量行,更常见的情况是 有时 使用 应用 :

    # Python 3.7, Pandas 0.23.4
    np.random.seed(0)
    df = pd.DataFrame(np.random.random((10**7, 3)))     # Scenario_1, many rows
    df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns
    
                                                   # Scenario_1  | Scenario_2
    %timeit df.sum()                               # 800 ms      | 109 ms
    %timeit df.apply(pd.Series.sum)                # 568 ms      | 325 ms
    
    %timeit df.max() - df.min()                    # 1.63 s      | 314 ms
    %timeit df.apply(lambda x: x.max() - x.min())  # 838 ms      | 473 ms
    
    %timeit df.mean()                              # 108 ms      | 94.4 ms
    %timeit df.apply(pd.Series.mean)               # 276 ms      | 233 ms
    

    有例外,但这些通常是边缘或不常见的。举几个例子:

    1. df['col'].apply(str) 5月小幅跑赢 df['col'].astype(str) .
    2. df.apply(pd.to_datetime) 与常规字符串相比,使用行处理字符串的伸缩性不好 for 循环。
        3
  •  1
  •   astro123    6 年前

    我想加上我的两分钱:

    有没有什么情况下申请是好的? 是的,有时候。

    任务:解码Unicode字符串。

    import numpy as np
    import pandas as pd
    import unidecode
    
    s = pd.Series(['mañana','Ceñía'])
    s.head()
    0    mañana
    1     Ceñía
    
    
    s.apply(unidecode.unidecode)
    0    manana
    1     Cenia
    

    更新
    我决不提倡使用。 apply ,只是在想 numpy 无法处理上述情况,它可能是 pandas apply . 但我忘记了普通的清单理解,感谢JPP的提醒。

        4
  •  1
  •   Pete Cacioppi    6 年前

    为了 axis=1 (即行函数),您可以只使用以下函数代替 apply . 我想知道为什么这不是 pandas 行为。(未使用复合索引进行测试,但速度似乎比 应用 )

    def faster_df_apply(df, func):
        cols = list(df.columns)
        data, index = [], []
        for row in df.itertuples(index=True):
            row_dict = {f:v for f,v in zip(cols, row[1:])}
            data.append(func(row_dict))
            index.append(row[0])
        return pd.Series(data, index=index)