缓解Pandas性能警告(DataFrame高度分散)

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

现在考虑这个用法:
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更好的性能?

在你的例子中,N是否等于df的长度?此外,我认为foo不会运行,因为它在你的例子中是这样写的:df[i]查找的是第i列,而不是第i行。另一个问题是foo会覆盖传入的DataFrame df - Peter Leimbigler
1
@PeterLeimbigler N是任意的,通常是一个很大的数。我已经编辑了代码以反映您关于df[i]的评论。至于覆盖,我可以接受,但好奇的是:这样做的礼貌方式是什么? - Jernej
@Jernej,你能分享一些数据和 bar() 函数体吗? - Danila Ganchar
@DanilaGanchar 这种习语被用于多种情况。我希望看到的解决方案不会与特定的数据实现或bar函数绑定。 - Jernej
2个回答

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)

我看到从foo2_opt(390毫秒)到foo2_dict(58毫秒)有额外的6倍改进,但这当然高度依赖于底层bar函数的实际实现。
另请注意,使用字典推导式带来的速度改进次于从numpy到pandas的转换,即对我来说,foo2_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)

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)

在我的机器上,我看到了2倍的性能提升,尽管我不确定100行和5000列是否适用于您的情况。加速的原因是列表推导更有效率
更新:
如果列名列表已知(`list_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)

我可以确认这似乎运行得非常好。两个后续问题 - 为什么第二个版本更有效?而且,是否可以为新创建的列设置自定义名称?我们可以假设给定一个长度为N的字符串列表。 - Jernej
2
我更新了答案,并附上了一个更好的解释链接,用于提高效率和自定义列名。 - SultanOrazbayev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接