【问题标题】:Better way to add the result of apply (multiple outputs) to an existing DataFrame with column names将应用(多个输出)的结果添加到具有列名的现有 DataFrame 的更好方法
【发布时间】:2020-02-04 10:57:12
【问题描述】:

我正在对 pandas 中数据框的行应用一个函数。该函数返回四个值(意思是每行四个值)。实际上,这意味着从 apply 函数返回的对象是一个包含元组的 Series。我想将这些添加到他们自己的列中。我知道我可以将该输出转换为 DataFrame,然后与旧 DataFrame 连接,如下所示:

import pandas as pd


def some_func(i):
    return i+1, i+2, i+3, i+4

df = pd.DataFrame(range(10), columns=['start'])
res = df.apply(lambda row: some_func(row['start']), axis=1)

# convert to df and add column names
res_df = res.apply(pd.Series)
res_df.columns = ['label_1', 'label_2', 'label_3', 'label_4']

# concatenate with old df
df = pd.concat([df, res_df], axis=1)
print(df)

我的问题是是否有更好的方法来做到这一点?特别是res.apply(pd.Series) 似乎是多余的,但我不知道更好的选择。性能对我来说是一个重要因素。


根据要求,示例输入 DataFrame 可能如下所示

   start
0      0
1      1
2      2
3      3
4      4
5      5
6      6
7      7
8      8
9      9

以及预期的输出,添加了四列:

   start  label_1  label_2  label_3  label_4
0      0        1        2        3        4
1      1        2        3        4        5
2      2        3        4        5        6
3      3        4        5        6        7
4      4        5        6        7        8
5      5        6        7        8        9
6      6        7        8        9       10
7      7        8        9       10       11
8      8        9       10       11       12
9      9       10       11       12       13

【问题讨论】:

  • 示例初始数据框和预期输出可能会有所帮助
  • @sammywemmy 好吧,代码是一个 MCVE,所以你可以直接运行它。但我已经添加了示例数据,以便给您一个直观的想法。

标签: python pandas dataframe apply


【解决方案1】:

直接给DataFrame赋值会比concating快。

这是一种分配方式

df = pd.DataFrame(range(10), columns=['start'])

df['label_1'], df['label_2'], df['label_3'], df['label_4'] = zip(*[some_func(x) for x in df['start']])

这比res.apply(pd.Series) 快。

请参阅adding multiple columns to pandas simultaneously 了解更多分配多列的方法。

【讨论】:

  • 这是最快的解决方案:)
【解决方案2】:

这里有几件事要尝试,而不是两次 apply 方法,使用生成器和 pandas.DataFrame 构造函数,以及 DataFrame.join 方法:

import pandas as pd

def some_func(i):
    return i+1, i+2, i+3, i+4

df = pd.DataFrame(range(10), columns=['start'])

df = df.join(pd.DataFrame(some_func(x) for x in df['start'])
             .rename(columns=lambda x: f'label_{x+1}'))

[出]

   start  label_1  label_2  label_3  label_4
0      0        1        2        3        4
1      1        2        3        4        5
2      2        3        4        5        6
3      3        4        5        6        7
4      4        5        6        7        8
5      5        6        7        8        9
6      6        7        8        9       10
7      7        8        9       10       11
8      8        9       10       11       12
9      9       10       11       12       13

时间

这应该提供~144x 加速

使用df = df = pd.DataFrame(range(100000), columns=['start']) 进行测试。

原方法:

22.2 s ± 246 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

这个解决方案:

152 ms ± 3.31 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

@Andrea 解决方案:

1.66 s ± 17.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

@Keval Dave 解决方案: ~230 x 加速

95.2 ms ± 857 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

【讨论】:

  • 你也可以测试我的吗?我怀疑join 可能更快
  • 1.61 s ± 46.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) - 仍然是一个显着的加速 :)
  • 是的,确实如此。我也测试了它。顺便说一句,@Keval Dave 方法似乎更快
  • 不确定它是否对时间很重要,但您没有使用相同数量的循环测试所有实现。否则,一个很好的答案。谢谢!
  • 我注意到一些奇怪的事情。如果我使用%%time(仅运行一次)测试时间,我会发现@Chris A 版本始终比@Keval Dave 解决方案更快。你能复制这种行为吗?
【解决方案3】:

如果您想要单线,可以尝试:

df[['label_1', 'label_2', 'label_3', 'label_4']] = pd.DataFrame(
    df.apply(lambda row: some_func(row['start']), axis=1).tolist())

这似乎是here建议的一种非常快速的方法

【讨论】:

    【解决方案4】:

    如果你从你的函数中返回一个pd.Series,那么当调用apply() 时,Pandas 将把它的元素转换成结果 DataFrame 的列。

    如果您对 Series 使用索引,则索引中的元素将成为生成的 DataFrame 的列的名称。

    在你的情况下:

    res_index = pd.Index([
        'label_1',
        'label_2',
        'label_3',
        'label_4',
    ])
    res_df = df.apply(
        lambda row: pd.Series(some_func(row['start']), index=res_index),
        axis=1,
    )
    df = pd.concat([df, res_df], axis=1)
    print(df)
    

    res_df 可能稍微干净一点的是仅将其应用于系列:

    res_df = df['start'].apply(
        lambda i: pd.Series(some_func(i), index=res_index),
    )
    

    如果您愿意重写您的some_func 以直接返回pd.Series

    def some_func(i, index=None):
        return pd.Series(
            [i+1, i+2, i+3, i+4],
            index=index,
        )
    
    res_index = pd.Index([
        'label_1',
        'label_2',
        'label_3',
        'label_4',
    ])
    res_df = df['start'].apply(some_func, index=res_index)
    df = pd.concat([df, res_df], axis=1)
    print(df)
    

    以上都将返回预期的结果:

       start  label_1  label_2  label_3  label_4
    0      0        1        2        3        4
    1      1        2        3        4        5
    2      2        3        4        5        6
    3      3        4        5        6        7
    4      4        5        6        7        8
    5      5        6        7        8        9
    6      6        7        8        9       10
    7      7        8        9       10       11
    8      8        9       10       11       12
    9      9       10       11       12       13
    

    【讨论】:

    • 看起来这不会比原来快多少,因为每行创建一个系列仍然是瓶颈。很多时候zip() 真的夺冠了!
    猜你喜欢
    • 2016-01-06
    • 1970-01-01
    • 2018-06-10
    • 2019-12-18
    • 2020-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多