使用 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.uint8 或np.int16)因为计算主要是内存限制。