缓解pandas的性能警告(DataFrame高度碎片化)

ipakzgxi  于 2023-06-20  发布在  其他
关注(0)|答案(2)|浏览(238)

假设我们有一个函数bar(df),它返回一个长度为len(df)的numpy数组,给定一个数据框架df
现在考虑一下这个成语

def foo(df):
    for i in range(N):
        df['FOO_' + str(i)] = bar(df)
    return df

最近的pandas更新开始导致以下警告
性能警告:DataFrame是高度碎片化的。这通常是多次调用frame.insert的结果,它的性能很差。考虑使用pd.concat(axis = 1)一次性连接所有列。要获得碎片整理帧,请使用newframe = frame.copy()
据我所知,缓解这种情况的一种方法是将上述代码更改为以下习惯用法

def foo2(df):
    frames = [df]
    for i in range(N):
        frames += [pd.Series(bar(df), index=df.index)]
    return pd.concat(frames, axis=1)

上面的代码修复了警告,但是导致了更糟糕的执行时间。

In [110]: %timeit foo()
1.73 s ± 11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [111]: %timeit foo2()
2.51 s ± 25.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

抑制性能警告的修复会引入额外的开销,这似乎很愚蠢。所以我的问题是
如何在修复警告的同时获得更好的性能。换句话说,有没有一种方法可以改进函数foo2,使其性能比foo更好?

6mzjoqzu

6mzjoqzu1#

从numpy数组构建一个列式数据框架的非常有效和优雅的方法是构建一个列的字典,然后将它们立即转换为所需的数据框架,因为这意味着整合数据和管理索引的成本只需支付一次。
如果我用foo2_dict变体扩展@SultanOrazbayev的example

def foo2_dict(df):
    new_columns = pd.DataFrame({f"FOO_{i}": bar(df) for i in range(N)}, index=df.index)
    return pd.concat([df, new_columns], axis=1)

foo2_opt(390ms)到foo2_dict(58ms),我看到了一个额外的因子6改进,但这当然高度依赖于底层bar函数的实际实现。
还要注意的是,通过使用字典理解的速度提高是次要的,而不是bunch numpy-> pandas转换,即。一个foo2_incremental_dict对我来说需要59ms。

def foo2_incremental_dict(df):
    new_columns = {}
    for i in range(N):
        new_columns[f"FOO_{i}"] = bar(df)
    new_columns = pd.DataFrame(new_columns, index=df.index)

    return pd.concat([df, new_columns], axis=1)
axr492tv

axr492tv2#

优化代码的一个机会是将+=重新构建为列表理解:

import pandas as pd

N = 5000
df = pd.DataFrame(index=[_ for _ in range(100)])

def bar(df):
    return np.random.rand(len(df))

def foo2_orig(df):
    frames = [df]
    for i in range(N):
        frames += [pd.Series(bar(df), index=df.index)]
    return pd.concat(frames, axis=1)

def foo2_opt(df):
    frames = pd.concat([pd.Series(bar(df), index=df.index) for i in range(N)], axis=1)
    return pd.concat([df, frames], axis=1)

在我的机器上,我看到性能提高了2倍,尽管我不确定100行和5000列是否适合您的情况。加速的原因是list comprehensions are more efficient
更新:
如果列名列表已知(list_col_names),则可以使用name kwarg为各个系列分配自定义列名:

def foo2_opt(df):
    frames = pd.concat([pd.Series(bar(df), index=df.index, name=col_name) for i, col_name in zip(range(N), list_col_names)], axis=1)
    return pd.concat([df, frames], axis=1)

相关问题