减轻 pandas 的性能警告(DataFrame 高度碎片化)

Jer*_*nej 13 python dataframe pandas

假设我们有一个函数,它返回给定 dataframebar(df)的长度的 numpy 数组。len(df)df

\n

现在考虑这个习语

\n
def foo(df):\n    for i in range(N):\n        df['FOO_' + str(i)] = bar(df)\n    return df\n
Run Code Online (Sandbox Code Playgroud)\n

最近的 pandas 更新开始导致以下警告

\n
\n

性能警告:DataFrame 高度碎片化。这通常是\n多次调用的结果frame.insert,\n性能较差。考虑使用 pd.concat(axis=1) 一次连接所有列。要获取碎片整理的帧,请使用\nnewframe = frame.copy()

\n
\n

据我了解,缓解这种情况的一种方法是将上面的代码更改为以下习惯用法

\n
def foo2(df):\n    frames = [df]\n    for i in range(N):\n        frames += [pd.Series(bar(df), index=df.index)]\n    return pd.concat(frames, axis=1)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
In [110]: %timeit foo()\n1.73 s \xc2\xb1 11 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n\nIn [111]: %timeit foo2()\n2.51 s \xc2\xb1 25.1 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n
Run Code Online (Sandbox Code Playgroud)\n

抑制引入额外开销的性能警告的修复似乎很愚蠢。因此我的问题是

\n
\n

如何修复警告,同时获得更好的性能。换句话说,有没有办法改进函数 foo2 以提供比 foo 更好的性能?

\n
\n

Jon*_*sch 6

从 numpy 数组按列构建数据帧的一种非常有效且优雅的方法是构建列的字典,然后立即将它们转换为所需的数据帧,因为这意味着合并数据和管理索引的成本已降低只支付一次。

如果我用一个变体扩展@SultanOrazbayev 的示例foo2_dict

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)
Run Code Online (Sandbox Code Playgroud)

foo2_opt我看到从(390ms) 到(58ms)的额外因子 6 改进foo2_dict,但这当然高度依赖于底层bar函数的实际实现。

另请注意,通过使用字典理解来提高速度对于 numpy -> pandas 转换来说是次要的,即。afoo2_incremental_dict对我来说需要 59 毫秒。

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)
Run Code Online (Sandbox Code Playgroud)


Sul*_*yev 2

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

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)
Run Code Online (Sandbox Code Playgroud)

在我的机器上,我看到性能提高了 2 倍,尽管我不确定 100 行和 5000 列是否适合您的情况。加速的原因是列表理解更加高效

更新:

如果列名称列表已知 ( ),则可以使用kwarglist_col_names为各个系列分配自定义列名称:name

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)
Run Code Online (Sandbox Code Playgroud)

  • 我更新了答案,添加了一个链接,该链接可以更好地解释效率提升和列的自定义命名。 (2认同)