【问题标题】:What is the Big O Complexity of Reversing the Order of Columns in Pandas DataFrame?在 Pandas DataFrame 中反转列顺序的大 O 复杂度是多少?
【发布时间】:2018-12-31 08:02:09
【问题描述】:

假设我在 pandas 中有一个具有 m 行和 n 列的 DataFrame。还假设我想反转列的顺序,可以使用以下代码完成:

df_reversed = df[df.columns[::-1]]

这个操作的大 O 复杂度是多少?我假设这将取决于列数,但它也取决于行数吗?

【问题讨论】:

  • 自我注意:在设置此类测试时,请使用最低数量级:) 导致笔记本电脑崩溃。
  • 如果您要提高性能,请使用切片df.iloc[:,::-1],它返回一个视图,因此应该几乎是免费的,而不是df[df.columns[::-1]],它会在您在后一个索引中创建一个副本.
  • @Divakar,作为一般规则,这仅适用于iloc,还是loc 也返回视图?可能超出了单个评论的范围,但我也对为什么通过df[col_list] 直接索引应该返回一个副本感兴趣(这是一种设计选择/副作用/有什么好处吗? )。
  • @divakar 如果我返回一个视图,我是否仍然可以对此进行操作,然后再次反转列的顺序并最终得到应用了操作的原始数据框?
  • @TimHoldsworth 执行操作后,您将创建一个副本。

标签: python algorithm pandas numpy big-o


【解决方案1】:

我不知道 Pandas 是如何实现这一点的,但我确实根据经验对其进行了测试。我运行了以下代码(在 Jupyter 笔记本中)来测试操作的速度:

def get_dummy_df(n):
    return pd.DataFrame({'a': [1,2]*n, 'b': [4,5]*n, 'c': [7,8]*n})

df = get_dummy_df(100)
print df.shape
%timeit df_r = df[df.columns[::-1]]

df = get_dummy_df(1000)
print df.shape
%timeit df_r = df[df.columns[::-1]]

df = get_dummy_df(10000)
print df.shape
%timeit df_r = df[df.columns[::-1]]

df = get_dummy_df(100000)
print df.shape
%timeit df_r = df[df.columns[::-1]]

df = get_dummy_df(1000000)
print df.shape
%timeit df_r = df[df.columns[::-1]]

df = get_dummy_df(10000000)
print df.shape
%timeit df_r = df[df.columns[::-1]]

输出是:

(200, 3)
1000 loops, best of 3: 419 µs per loop
(2000, 3)
1000 loops, best of 3: 425 µs per loop
(20000, 3)
1000 loops, best of 3: 498 µs per loop
(200000, 3)
100 loops, best of 3: 2.66 ms per loop
(2000000, 3)
10 loops, best of 3: 25.2 ms per loop
(20000000, 3)
1 loop, best of 3: 207 ms per loop

如您所见,在前 3 种情况下,操作的开销是大部分时间(400-500μs)所花费的,但从第 4 种情况开始,它所花费的时间开始与数据,每次增加一个数量级。

所以,假设对n也一定有比例,看来我们处理的是O(m*n)

【讨论】:

    【解决方案2】:

    Big O 复杂度(截至 Pandas 0.24)为 m*n,其中 m 是列数,n 是行数。请注意,这是在使用 DataFrame.__getitem__ 方法(又名 [])和 Indexsee relevant code, with other types that would trigger a copy)时。

    这是一个有用的堆栈跟踪:

     <ipython-input-4-3162cae03863>(2)<module>()
          1 columns = df.columns[::-1]
    ----> 2 df_reversed = df[columns]
    
      pandas/core/frame.py(2682)__getitem__()
       2681             # either boolean or fancy integer index
    -> 2682             return self._getitem_array(key)
       2683         elif isinstance(key, DataFrame):
    
      pandas/core/frame.py(2727)_getitem_array()
       2726             indexer = self.loc._convert_to_indexer(key, axis=1)
    -> 2727             return self._take(indexer, axis=1)
       2728 
    
      pandas/core/generic.py(2789)_take()
       2788                                    axis=self._get_block_manager_axis(axis),
    -> 2789                                    verify=True)
       2790         result = self._constructor(new_data).__finalize__(self)
    
      pandas/core/internals.py(4539)take()
       4538         return self.reindex_indexer(new_axis=new_labels, indexer=indexer,
    -> 4539                                     axis=axis, allow_dups=True)
       4540 
    
      pandas/core/internals.py(4421)reindex_indexer()
       4420             new_blocks = self._slice_take_blocks_ax0(indexer,
    -> 4421                                                      fill_tuple=(fill_value,))
       4422         else:
    
      pandas/core/internals.py(1254)take_nd()
       1253             new_values = algos.take_nd(values, indexer, axis=axis,
    -> 1254                                        allow_fill=False)
       1255         else:
    
    > pandas/core/algorithms.py(1658)take_nd()
       1657     import ipdb; ipdb.set_trace()
    -> 1658     func = _get_take_nd_function(arr.ndim, arr.dtype, out.dtype, axis=axis,
       1659                                  mask_info=mask_info)
       1660     func(arr, indexer, out, fill_value)
    

    pandas/core/algorithms 中 L1660 上的 func 调用最终调用了具有O(m * n) 复杂性的 cython 函数。这是将原始数据中的数据复制到out 的位置。 out 包含以相反顺序排列的原始数据的副本。

        inner_take_2d_axis0_template = """\
        cdef:
            Py_ssize_t i, j, k, n, idx
            %(c_type_out)s fv
    
        n = len(indexer)
        k = values.shape[1]
    
        fv = fill_value
    
        IF %(can_copy)s:
            cdef:
                %(c_type_out)s *v
                %(c_type_out)s *o
    
            #GH3130
            if (values.strides[1] == out.strides[1] and
                values.strides[1] == sizeof(%(c_type_out)s) and
                sizeof(%(c_type_out)s) * n >= 256):
    
                for i from 0 <= i < n:
                    idx = indexer[i]
                    if idx == -1:
                        for j from 0 <= j < k:
                            out[i, j] = fv
                    else:
                        v = &values[idx, 0]
                        o = &out[i, 0]
                        memmove(o, v, <size_t>(sizeof(%(c_type_out)s) * k))
                return
    
        for i from 0 <= i < n:
            idx = indexer[i]
            if idx == -1:
                for j from 0 <= j < k:
                    out[i, j] = fv
            else:
                for j from 0 <= j < k:
                    out[i, j] = %(preval)svalues[idx, j]%(postval)s
    """
    

    请注意,在上面的模板函数中,有一个使用memmove的路径(这是本例中采用的路径,因为我们正在从int64映射到int64,并且输出的维度与我们只是在切换索引)。请注意,memmove is still O(n) 与它必须复制的字节数成正比,尽管可能比直接写入索引要快。

    【讨论】:

    • 我继续添加了一些示例,这些示例显示了在调用 __getitem__ 和调用 cython 函数之后所采用的路径中的更多上下文,最终大部分时间都花在了大值上。 __getitem__ 中的逻辑并不总是直观的,但我发现这个 GH 问题有助于解释 github.com/pandas-dev/pandas/issues/9595 背后不同输入的作用。
    • 谢谢,这对解释我们看到的行为大有帮助。
    【解决方案3】:

    我使用big_O拟合库here进行了实证测试

    注意:所有测试均在自变量扫描 6 个数量级(

    • rows1010^63 的常量 column 大小,
    • columns1010^6 与常量 row 大小为 10

    结果显示columns逆向运算.columns[::-1]DataFrame中的复杂度为

    1. 立方O(n^3) 其中 n 是 rows 的数量
    2. 立方O(n^3) 其中 n 是 columns 的数量

    先决条件:您需要使用终端命令pip install big_o安装big_o()

    代码

    import big_o
    import pandas as pd
    import numpy as np
    
    SWEAP_LOG10 = 6
    COLUMNS = 3
    ROWS = 10
    
    def build_df(rows, columns):
        # To isolated the creation of the DataFrame from the inversion operation.
        narray = np.zeros(rows*columns).reshape(rows, columns)
        df = pd.DataFrame(narray)
        return df
    
    def flip_columns(df):
        return df[df.columns[::-1]]
    
    def get_row_df(n, m=COLUMNS):
        return build_df(1*10**n, m)
    
    def get_column_df(n, m=ROWS):
        return build_df(m, 1*10**n)
    
    
    # infer the big_o on columns[::-1] operation vs. rows
    best, others = big_o.big_o(flip_columns, get_row_df, min_n=1, max_n=SWEAP_LOG10,n_measures=SWEAP_LOG10, n_repeats=10)
    
    # print results
    print('Measuring .columns[::-1] complexity against rapid increase in # rows')
    print('-'*80 + '\nBig O() fits: {}\n'.format(best) + '-'*80)
    
    for class_, residual in others.items():
        print('{:<60s}  (res: {:.2G})'.format(str(class_), residual))
    
    print('-'*80)
    
    # infer the big_o on columns[::-1] operation vs. columns
    best, others = big_o.big_o(flip_columns, get_column_df, min_n=1, max_n=SWEAP_LOG10,n_measures=SWEAP_LOG10, n_repeats=10)
    
    # print results
    print()
    print('Measuring .columns[::-1] complexity against rapid increase in # columns')
    print('-'*80 + '\nBig O() fits: {}\n'.format(best) + '-'*80)
    
    for class_, residual in others.items():
        print('{:<60s}  (res: {:.2G})'.format(str(class_), residual))
        
    print('-'*80)
    

    结果

    Measuring .columns[::-1] complexity against rapid increase in # rows
    --------------------------------------------------------------------------------
    Big O() fits: Cubic: time = -0.017 + 0.00067*n^3
    --------------------------------------------------------------------------------
    Constant: time = 0.032                                        (res: 0.021)
    Linear: time = -0.051 + 0.024*n                               (res: 0.011)
    Quadratic: time = -0.026 + 0.0038*n^2                         (res: 0.0077)
    Cubic: time = -0.017 + 0.00067*n^3                            (res: 0.0052)
    Polynomial: time = -6.3 * x^1.5                               (res: 6)
    Logarithmic: time = -0.026 + 0.053*log(n)                     (res: 0.015)
    Linearithmic: time = -0.024 + 0.012*n*log(n)                  (res: 0.0094)
    Exponential: time = -7 * 0.66^n                               (res: 3.6)
    --------------------------------------------------------------------------------
    
    
    Measuring .columns[::-1] complexity against rapid increase in # columns
    --------------------------------------------------------------------------------
    Big O() fits: Cubic: time = -0.28 + 0.009*n^3
    --------------------------------------------------------------------------------
    Constant: time = 0.38                                         (res: 3.9)
    Linear: time = -0.73 + 0.32*n                                 (res: 2.1)
    Quadratic: time = -0.4 + 0.052*n^2                            (res: 1.5)
    Cubic: time = -0.28 + 0.009*n^3                               (res: 1.1)
    Polynomial: time = -6 * x^2.2                                 (res: 16)
    Logarithmic: time = -0.39 + 0.71*log(n)                       (res: 2.8)
    Linearithmic: time = -0.38 + 0.16*n*log(n)                    (res: 1.8)
    Exponential: time = -7 * 1^n                                  (res: 9.7)
    --------------------------------------------------------------------------------
    

    【讨论】:

    • 您忽略了小数量级的开销
    • 是的,从技术上讲,如果它是 O(n),O(n^3) 也适用,但它不是很有用。
    • @MadPhysicist 你能详细说明小数量级的开销
    • 另一个答案在这方面做得很好。
    • 您只是在有限的点数上进行拟合,而低尾却让您望而却步。另一个答案实际上是看大 O。我严重怀疑 pandas 中的任何基本操作都是 O(n^3)
    猜你喜欢
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 2019-07-27
    • 1970-01-01
    • 2016-01-14
    • 1970-01-01
    • 1970-01-01
    • 2015-03-05
    相关资源
    最近更新 更多