【问题标题】:Fast removal of only zero columns in pandas dataframe快速删除熊猫数据框中的零列
【发布时间】:2021-11-25 12:04:28
【问题描述】:

使用:

import pandas as pd
import numpy as np
df = pd.DataFrame(np.random.randint(0,3,(100000,5000)))

df = df.loc[:, (df != 0).any(axis=0)]

对于非常大的 (1000000x2000) 数据帧来说,删除仅包含零的列太慢了。有什么建议可以加快速度吗?

谢谢

【问题讨论】:

  • 你能展示你的数据框样本吗?见minimal reproducible exampleHow to Ask
  • 确定我添加了一个示例 df,尽管我认为它没有任何区别
  • 如果所有整数在你的轴上求和可能更快df.loc[:,df.sum(0).gt(0)]
  • 现在我明白你为什么要问了 :) 谢谢你的提示。我会尽快试一试
  • 也许用 nan 替换 0 并用 how = all 替换 dropna ?我希望总和 agg 会好得多

标签: python pandas dataframe performance


【解决方案1】:

使用 Numba 可以更快地实现这一点。

确实,大多数 Numpy 实现都会创建 巨大的临时数组,这些数组的填充和读取速度都很慢。此外,Numpy 将遍历整个数据帧,而这通常不是必需的(至少在您的示例中)。关键是您可以通过迭代检查列值并如果有任何 0(通常在开始时)尽早停止当前列的计算,就可以非常快速地知道是否需要保留列。此外,不需要总是复制整个数据帧(使用大约 1.9 GiB 的内存):当所有列都保留时。最后,您可以并行执行计算。

但是,存在对性能至关重要的低级捕获。首先,Numba 无法处理 Pandas 数据帧,但是使用df.values 转换为 Numpy 数组几乎是免费的(同样的事情适用于创建新数据帧)。此外,关于数组的内存布局,在最内层循环中遍历行或列可能会更好。

可以通过检查输入数据帧 Numpy 数组的 strides 来获取此布局。

请注意,由于(不寻常的)Numpy 随机初始化,该示例使用行主数据帧,但大多数数据帧往往是列主数据。

这是一个优化的实现:

import numba as nb

@nb.njit('int_[:,:](int_[:,:])', parallel=True)
def filterNullColumns(dfValues):
    n, m = dfValues.shape
    s0, s1 = dfValues.strides
    columnMajor = s0 < s1
    toKeep = np.full(m, False, dtype=np.bool_)

    # Find the columns to keep
    # Only-optimized for column-major dataframes (quite complex otherwise)
    for colId in nb.prange(m):
        for rowId in range(n):
            if dfValues[rowId, colId] != 0:
                toKeep[colId] = True
                break

    # Optimization: no columns are discarded
    if np.all(toKeep):
        return dfValues

    # Create a new dataframe
    newColCount = np.sum(toKeep)
    res = np.empty((n,newColCount), dtype=dfValues.dtype)
    if columnMajor:
        newColId = 0
        for colId in nb.prange(m):
            if toKeep[colId]:
                for rowId in range(n):
                    res[rowId, newColId] = dfValues[rowId, colId]
                newColId += 1
    else:
        for rowId in nb.prange(n):
            newColId = 0
            for colId in range(m):
                res[rowId, newColId] = dfValues[rowId, colId]
                newColId += toKeep[colId]
    return res

result = pd.DataFrame(filterNullColumns(df.values))

这是我的 6 核机器上的结果:

Reference:           1094 ms
Valdi_Bo answer:     1262 ms
This implementation:    0.056 ms  (300 ms with discarded columns)

这个实现比提供的示例上的参考实现快大约20 000倍(没有丢弃的列)并且快4.2倍 更多病理病例(仅丢弃一列)。

如果您想获得更快的性能,那么您可以就地执行计算(危险,尤其是由于 Pandas)或使用更小的数据类型(如np.uint8np.int16)因为计算主要是内存限制

【讨论】:

  • 令我惊讶的是,如果要丢弃一列,它的速度只有 4.2 倍。我在我这边实现了第一部分 # Find the columns to keep 并发现了非常好的性能,我想知道 # Create a new dataframe 部分是否不是这里的问题,return dfValues[:, toKeep] 会不会是一个更快的解决方案?公平地说,如果你丢弃超过 1 列,那么增益应该会增加,不是吗?
  • 我认为为了进行公平的比较,我们应该分开案例 1) 没有什么要删除,2) 有什么要删除。 Valdi_Bo 和参考版本默默地假设总有一些东西要删除。虚拟示例在我们的生活中永远不会产生 2000/5000 个零,并且整个检查毫无意义:)
  • 我用一个只包含零列的数据框对此进行了测试,它仍然比我的初始解决方案快 3k 倍!感谢这个很棒的代码 sn-p
【解决方案2】:

Numpy 的运行速度比 Pandas 快得多。

所以要发现哪些列只包含零,请使用:

np.all(np.equal(df.values, 0), axis=0)

但你的任务是将这些列放到 DataFrame 中, 正如我想的那样,保留源列名。

所以实际的drop必须在源Dataframe上进行, 使用 loc 和上述检查的否定结果。

类似:

df = df.loc[:, ~np.all(np.equal(df.values, 0), axis=0)]

【讨论】:

  • 我测试了这个答案,令我惊讶的是它并没有为上述示例提供加速。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-04
  • 1970-01-01
  • 1970-01-01
  • 2019-08-12
  • 2017-03-28
  • 1970-01-01
  • 2020-05-28
相关资源
最近更新 更多