关于python:对于带有pandas的循环 – 我什么时候应该关心?

For loops with pandas - When should I care?

在我熟悉的概念,以及如何"矢量化"的大熊猫对UPS技术employs computation速度矢量。矢量函数飞越整个系列广播业务实现他们对多帧迭代速度比大conventionally飞越的日期。 >

然而,我相当惊讶地看到很多代码(包括在线提供从答案到问题解决方案栈overflow),involve环通环和列表中使用for日期。该文件有读和理解,有体面的API,我相信这是有"坏"的循环,这是一个"不应该"系列,在iterate翼,他们dataframes。那么,怎么会看到用户的解决方案,然后每一个现在suggesting疯狂吗? >

所以,对summarise…我的问题是: 真的有for环"坏"?如果不是在什么情况(S),他们会使用更多的比传统的"矢量"的做法吗?1 >

<下> 1,而它是真实的问题somewhat声大,事实上,那个时候有非常具体的形势有for通常比conventionally循环迭代结束日期。这个论坛的目的posterity捕捉到这个职位。<下> >


tldr;不,for环不是"坏"的,至少,不总是这样。也许更准确的说法是,某些矢量化操作比迭代慢,而迭代比某些矢量化操作快。知道何时以及为什么是从代码中获得最大性能的关键。简言之,在这种情况下,有必要考虑一种替代矢量化熊猫功能的方法:好的。

  • 当你的数据很小时(取决于你在做什么),
  • 处理object/混合数据类型时
  • 使用str/regex访问函数时
  • 让我们单独检查一下这些情况。好的。小数据的迭代V/S矢量化

    PANDAS在其API设计中采用了"配置约定"方法。这意味着已经安装了相同的API来满足广泛的数据和用例。好的。

    当调用panda函数时,函数必须在内部处理以下内容(除其他外),以确保工作好的。

  • 索引/轴对齐
  • 处理混合数据类型
  • 处理丢失的数据
  • 几乎每个函数都必须在不同程度上处理这些问题,这会带来开销。数值函数(例如,Series.add的开销较小,而字符串函数(例如,Series.str.replace的开销更明显)。好的。

    另一方面,for循环比你想象的要快。更好的是列表理解(通过for循环创建列表)更快,因为它们是用于列表创建的优化迭代机制。好的。

    列表理解遵循模式好的。

    1
    [f(x) for x in seq]

    其中,seq是熊猫系列或数据帧列。或者,在多列上操作时,好的。

    1
    [f(x, y) for x, y in zip(seq1, seq2)]

    其中,seq1seq2是列。好的。

    数值比较考虑一个简单的布尔索引操作。列表理解方法针对Series.ne(!=)和query。功能如下:好的。

    1
    2
    3
    4
    # Boolean indexing with Numeric value comparison.
    df[df.A != df.B]                            # vectorized !=
    df.query('A != B')                          # query (numexpr)
    df[[x != y for x, y in zip(df.A, df.B)]]    # list comp

    为了简单起见,我使用了perfplot包来运行本文中的所有timeit测试。上述操作的时间安排如下:好的。

    enter image description here好的。

    对于中等大小的n,列表理解优于query,甚至优于矢量化的与极小的n不相等的比较。不幸的是,列表理解是线性的,因此对于较大的n,它不提供太多的性能增益。好的。

    Note
    It is worth mentioning that much of the benefit of list comprehension come from not having to worry about the index alignment,
    but this means that if your code is dependent on indexing alignment,
    this will break. In some cases, vectorised operations over the
    underlying NumPy arrays can be considered as bringing in the"best of
    both worlds", allowing for vectorisation without all the unneeded overhead of the pandas functions. This means that you can rewrite the operation above as

    Ok.

    1
    df[df.A.values != df.B.values]

    Which outperforms both the pandas and list comprehension equivalents:
    enter image description here
    NumPy vectorization is out of the scope of this post, but it is definitely worth considering, if performance matters.

    Ok.

    价值计数举另一个例子——这次,使用另一个比for循环更快的普通python构造——collections.Counter。一个常见的要求是计算值计数并将结果作为字典返回。这是通过value_countsnp.uniqueCounter完成的:好的。

    1
    2
    3
    4
    # Value Counts comparison.
    ser.value_counts(sort=False).to_dict()           # value_counts
    dict(zip(*np.unique(ser, return_counts=True)))   # np.unique
    Counter(ser)                                     # Counter

    enter image description here好的。

    结果更为明显,对于较大范围的小N(~3500),Counter优于两种矢量化方法。好的。

    Note
    More trivia (courtesy @user2357112). The Counter is implemented with a C
    accelerator,
    so while it still has to work with python objects instead of the
    underlying C datatypes, it is still faster than a for loop. Python
    power!

    Ok.

    当然,这里要考虑的是性能取决于您的数据和用例。这些例子的要点是说服您不要将这些解决方案排除在合法选项之外。如果这些仍然不能给你你所需要的性能,总是有赛通和纽巴。让我们把这个测试加入到混合中。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from numba import njit, prange

    @njit(parallel=True)
    def get_mask(x, y):
        result = [False] * len(x)
        for i in prange(len(x)):
            result[i] = x[i] != y[i]

        return np.array(result)

    df[get_mask(df.A.values, df.B.values)] # numba

    enter image description here好的。

    numba为非常强大的矢量化代码提供了循环的python代码的JIT编译。了解如何使numba工作涉及到一个学习曲线。好的。混合/object数据类型的操作

    基于字符串的比较重新访问第一节中的筛选示例,如果要比较的列是字符串呢?考虑上面相同的3个函数,但是输入数据帧转换为字符串。好的。

    1
    2
    3
    4
    # Boolean indexing with string value comparison.
    df[df.A != df.B]                            # vectorized !=
    df.query('A != B')                          # query (numexpr)
    df[[x != y for x, y in zip(df.A, df.B)]]    # list comp

    enter image description here好的。

    那么,发生了什么变化?这里需要注意的是,字符串操作本质上很难向量化。pandas将字符串视为对象,对象上的所有操作都会返回到缓慢、循环的实现中。好的。

    现在,因为这个循环的实现被上面提到的所有开销所包围,所以这些解决方案之间存在一个恒定的量级差异,即使它们的规模相同。好的。

    当涉及到可变/复杂对象的操作时,没有比较。列表理解优于所有涉及听写和列表的操作。好的。

    按键访问字典值下面是从字典列中提取值的两个操作的时间安排:map和列表理解。设置在附录的"代码段"标题下。好的。

    1
    2
    3
    # Dictionary value extraction.
    ser.map(operator.itemgetter('value'))     # map
    pd.Series([x.get('value') for x in ser])  # list comprehension

    enter image description here好的。

    位置列表索引从列列表中提取第0个元素(处理异常)、mapstr.get访问方法和列表理解的3个操作的计时:好的。

    1
    2
    3
    4
    5
    6
    7
    # List positional indexing.
    def get_0th(lst):
        try:
            return lst[0]
        # Handle empty lists and NaNs gracefully.
        except (IndexError, TypeError):
            return np.nan

    好的。

    1
    2
    3
    4
    ser.map(get_0th)                                          # map
    ser.str[0]                                                # str accessor
    pd.Series([x[0] if len(x) > 0 else np.nan for x in ser])  # list comp
    pd.Series([get_0th(x) for x in ser])                      # list comp safe

    Note
    If the index matters, you would want to do:

    Ok.

    1
    pd.Series([...], index=ser.index)

    When reconstructing the series.

    Ok.

    enter image description here好的。

    列表平坦化最后一个例子是扁平化列表。这是另一个常见问题,并演示了纯Python的强大功能。好的。

    1
    2
    3
    4
    # Nested list flattening.
    pd.DataFrame(ser.tolist()).stack().reset_index(drop=True)  # stack
    pd.Series(list(chain.from_iterable(ser.tolist())))         # itertools.chain
    pd.Series([y for x in ser for y in x])                     # nested list comp

    enter image description here好的。

    itertools.chain.from_iterable和嵌套列表理解都是纯python结构,其伸缩性比stack解决方案要好得多。好的。

    这些时间是一个强有力的迹象,表明熊猫不具备与混合数据类型一起工作的能力,并且您可能应该避免使用它来完成这项工作。在可能的情况下,数据应该作为标量值(ints/floats/strings)出现在单独的列中。好的。

    最后,这些解决方案的适用性很大程度上取决于您的数据。所以,最好的做法是在决定使用什么之前对数据测试这些操作。注意,我没有在这些解决方案上对apply进行计时,因为它会扭曲图表(是的,速度太慢)。好的。regex操作和.str访问方法

    pandas可以在字符串列上应用regex操作,如str.containsstr.extractstr.extractall,以及其他"矢量化"字符串操作(如str.split、str.fin,str.translate`,等等)。这些函数比列表理解慢,并且比其他任何函数都更方便。好的。

    预编译regex模式并使用re.compile迭代数据通常要快得多(另请参见使用python的re.compile值得吗?).相当于str.contains的列表比较如下:好的。

    1
    2
    p = re.compile(...)
    ser2 = pd.Series([x for x in ser if p.search(x)])

    或者,好的。

    1
    ser2 = ser[[bool(p.search(x)) for x in ser]]

    如果你需要照顾奶奶,你可以做些类似的事情好的。

    1
    ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]

    相当于str.extract的list comp(无组)将如下所示:好的。

    1
    df['col2'] = [p.search(x).group(0) for x in df['col']]

    如果不需要处理任何匹配项和nan,则可以使用自定义函数(速度更快!):好的。

    1
    2
    3
    4
    5
    6
    7
    def matcher(x):
        m = p.search(str(x))
        if m:
            return m.group(0)
        return np.nan

    df['col2'] = [matcher(x) for x in df['col']]

    matcher函数是非常可扩展的。它可以根据需要返回每个捕获组的列表。只需提取matcher对象的groupgroups属性。好的。

    对于str.extractall,将p.search改为p.findall。好的。

    字符串提取考虑一个简单的过滤操作。如果前面是大写字母,则提取4位数字。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # Extracting strings.
    p = re.compile(r'(?<=[A-Z])(\d{4})')
    def matcher(x):
        m = p.search(x)
        if m:
            return m.group(0)
        return np.nan

    ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False)   #  str.extract
    pd.Series([matcher(x) for x in ser])                  #  list comprehension

    enter image description here好的。

    更多例子完全披露-我是下面列出的这些文章的作者(部分或全部)。好的。

    • 熊猫快速删除标点符号好的。

    • 两个panda列的字符串连接好的。

    • 从列中的字符串中删除不需要的部分好的。

    • 替换数据帧中除最后一次出现的字符以外的所有字符好的。

    结论

    如上面的例子所示,迭代在处理小的数据帧行、混合数据类型和正则表达式时会发光。好的。

    你得到的加速取决于你的数据和你的问题,所以你的里程数可能会有所不同。最好的做法是仔细运行测试,看看付出的代价是否值得。好的。

    "矢量化"函数以其简单性和可读性著称,因此,如果性能不是关键的,您肯定会更喜欢这些函数。好的。

    另一个注意事项是,某些字符串操作处理有利于使用numpy的约束。下面是两个例子,其中谨慎的numpy矢量化优于python:好的。

    • 以更快、高效的方式使用增量值创建新列-Divakar回答好的。

    • 用pandas快速删除标点符号-Paul Panzer回答好的。

    此外,有时仅仅通过.values在底层阵列上操作,而不是在系列或数据帧上操作,可以为大多数常见情况提供足够健康的加速(请参见上面的数字比较部分中的注释)。因此,例如,df[df.A.values != df.B.values]将显示出比df[df.A != df.B]更快的性能提升。使用.values可能并不适用于所有情况,但它是一个有用的黑客知道。好的。

    如上所述,由您决定这些解决方案是否值得麻烦地实现。好的。附录:代码段

    1
    2
    3
    4
    5
    6
    7
    8
    import perfplot  
    import operator
    import pandas as pd
    import numpy as np
    import re

    from collections import Counter
    from itertools import chain

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Boolean indexing with Numeric value comparison.
    perfplot.show(
        setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']),
        kernels=[
            lambda df: df[df.A != df.B],
            lambda df: df.query('A != B'),
            lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
            lambda df: df[get_mask(df.A.values, df.B.values)]
        ],
        labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N'
    )

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Value Counts comparison.
    perfplot.show(
        setup=lambda n: pd.Series(np.random.choice(1000, n)),
        kernels=[
            lambda ser: ser.value_counts(sort=False).to_dict(),
            lambda ser: dict(zip(*np.unique(ser, return_counts=True))),
            lambda ser: Counter(ser),
        ],
        labels=['value_counts', 'np.unique', 'Counter'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=lambda x, y: dict(x) == dict(y)
    )

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Boolean indexing with string value comparison.
    perfplot.show(
        setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str),
        kernels=[
            lambda df: df[df.A != df.B],
            lambda df: df.query('A != B'),
            lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
        ],
        labels=['vectorized !=', 'query (numexpr)', 'list comp'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Dictionary value extraction.
    ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}])
    perfplot.show(
        setup=lambda n: pd.concat([ser1] * n, ignore_index=True),
        kernels=[
            lambda ser: ser.map(operator.itemgetter('value')),
            lambda ser: pd.Series([x.get('value') for x in ser]),
        ],
        labels=['map', 'list comprehension'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # List positional indexing.
    ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []])        
    perfplot.show(
        setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
        kernels=[
            lambda ser: ser.map(get_0th),
            lambda ser: ser.str[0],
            lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]),
            lambda ser: pd.Series([get_0th(x) for x in ser]),
        ],
        labels=['map', 'str accessor', 'list comprehension', 'list comp safe'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # Nested list flattening.
    ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']])
    perfplot.show(
        setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
        kernels=[
            lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True),
            lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))),
            lambda ser: pd.Series([y for x in ser for y in x]),
        ],
        labels=['stack', 'itertools.chain', 'nested list comp'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',    
        equality_check=None

    )

    好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Extracting strings.
    ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz'])
    perfplot.show(
        setup=lambda n: pd.concat([ser4] * n, ignore_index=True),
        kernels=[
            lambda ser: ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False),
            lambda ser: pd.Series([matcher(x) for x in ser])
        ],
        labels=['str.extract', 'list comprehension'],
        n_range=[2**k for k in range(0, 15)],
        xlabel='N',
        equality_check=None
    )

    好啊。