【问题标题】:Return multiple columns from pandas apply()从 pandas apply() 返回多列
【发布时间】:2014-06-28 11:40:09
【问题描述】:

我有一个熊猫数据框,df_test。它包含一个“大小”列,它表示以字节为单位的大小。我使用以下代码计算了 KB、MB 和 GB:

df_test = pd.DataFrame([
    {'dir': '/Users/uname1', 'size': 994933},
    {'dir': '/Users/uname2', 'size': 109338711},
])

df_test['size_kb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0, grouping=True) + ' KB')
df_test['size_mb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 2, grouping=True) + ' MB')
df_test['size_gb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 3, grouping=True) + ' GB')

df_test


             dir       size       size_kb   size_mb size_gb
0  /Users/uname1     994933      971.6 KB    0.9 MB  0.0 GB
1  /Users/uname2  109338711  106,776.1 KB  104.3 MB  0.1 GB

[2 rows x 5 columns]

我已经运行了超过 120,000 行,每列需要大约 2.97 秒 * 3 = ~9 秒,根据 %timeit。

无论如何我可以让这更快吗?例如,我是否可以一次返回一列,而不是从 apply 一次返回一列并运行 3 次,我可以一次返回所有三列以插入回原始数据帧吗?

我发现的其他问题都希望采用多个值并返回单个值。我想获取单个值并返回多个列

【问题讨论】:

标签: python pandas dataframe apply


【解决方案1】:

如果您改为使用 numpy 进行数学运算,则可以比此处的最佳答案快 40 倍以上。改编@Rocky K 的前两个答案。主要区别在于在 120k 行的实际 df 上运行。当您以数组方式应用函数(而不是以值方式应用函数)时,Numpy 在数学方面的速度要快得多。到目前为止,最好的答案是第三个,因为它使用 numpy 进行数学运算。另请注意,它只计算 1024**2 和 1024**3 一次,而不是每行一次,节省 240k 计算。这是我机器上的时间:

Tuples (pass value, return tuple then zip, new columns dont exist):
Runtime: 10.935037851333618 

Tuples (pass value, return tuple then zip, new columns exist):
Runtime: 11.120025157928467 

Use numpy for math portions:
Runtime: 0.24799370765686035

这是我用来计算这些时间的脚本(改编自 Rocky K):

import numpy as np
import pandas as pd
import locale
import time

size = np.random.random(120000) * 1000000000
data = pd.DataFrame({'Size': size})

def sizes_pass_value_return_tuple(value):
    a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c

print('\nTuples (pass value, return tuple then zip, new columns dont exist):')
df1 = data.copy()
start = time.time()
df1['size_kb'],  df1['size_mb'], df1['size_gb'] = zip(*df1['Size'].apply(sizes_pass_value_return_tuple))
end = time.time()
print('Runtime:', end - start, '\n')

print('Tuples (pass value, return tuple then zip, new columns exist):')
df2 = data.copy()
start = time.time()
df2 = pd.concat([df2, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
df2['size_kb'],  df2['size_mb'], df2['size_gb'] = zip(*df2['Size'].apply(sizes_pass_value_return_tuple))
end = time.time()
print('Runtime:', end - start, '\n')

print('Use numpy for math portions:')
df3 = data.copy()
start = time.time()
df3['size_kb'] = (df3.Size.values / 1024).round(1)
df3['size_kb'] = df3.size_kb.astype(str) + ' KB'
df3['size_mb'] = (df3.Size.values / 1024 ** 2).round(1)
df3['size_mb'] = df3.size_mb.astype(str) + ' MB'
df3['size_gb'] = (df3.Size.values / 1024 ** 3).round(1)
df3['size_gb'] = df3.size_gb.astype(str) + ' GB'
end = time.time()
print('Runtime:', end - start, '\n')

【讨论】:

    【解决方案2】:

    简单易行:

    def func(item_df):
      return [1,'Label 1'] if item_df['col_0'] > 0 else [0,'Label 0']
     
    my_df[['col_1','col2']] = my_df.apply(func, axis=1,result_type='expand')
    

    【讨论】:

      【解决方案3】:

      您可以从包含新数据的应用函数中返回一个 Series,从而无需迭代 3 次。将axis=1 传递给apply 函数将函数sizes 应用于数据帧的每一行,返回一个系列以添加到新数据帧。该系列 s 包含新值以及原始数据。

      def sizes(s):
          s['size_kb'] = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
          s['size_mb'] = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
          s['size_gb'] = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
          return s
      
      df_test = df_test.append(rows_list)
      df_test = df_test.apply(sizes, axis=1)
      

      【讨论】:

      • 我很惊讶它花了将近 2 年没有正确的答案。我在寻找别的东西,偶然发现了这个。希望现在有用还为时不晚!
      • 这个答案中的rows_list 是什么?
      • 只是构建Dataframe的Series列表。
      • 如果 pd.Series 需要索引,您需要提供 pd.Series(data, index=...)。否则,当您尝试将结果分配回父数据帧时,您会遇到神秘错误。
      • @Nelz11 我建议您使用问题中提供的相同示例而不是rows_list 公式,以便您的答案编译时不会出现任何问题(另请参阅@David Stansby 评论)。我建议将其作为编辑以避免您的麻烦,但显然版主更喜欢 cmets 而不是编辑。
      【解决方案4】:

      使用 apply 和 lambda 是一种相当快速的方法。只需将多个值作为列表返回,然后使用 to_list()

      import pandas as pd
      
      dat = [ [i, 10*i] for i in range(100000)]
      
      df = pd.DataFrame(dat, columns = ["a","b"])
      
      def add_and_div(x):
          add = x + 3
          div = x / 3
          return [add, div]
      
      start = time.time()
      df[['c','d']] = df['a'].apply(lambda x: add_and_div(x)).to_list()
      end = time.time()
      
      print(end-start) # output: 0.27606
      

      【讨论】:

        【解决方案5】:

        当前的一些回复工作正常,但我想提供另一个,也许更“pandified”的选项。这适用于当前的 pandas 0.23(不确定它是否适用于以前的版本):

        import pandas as pd
        
        df_test = pd.DataFrame([
          {'dir': '/Users/uname1', 'size': 994933},
          {'dir': '/Users/uname2', 'size': 109338711},
        ])
        
        def sizes(s):
          a = locale.format_string("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
          b = locale.format_string("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
          c = locale.format_string("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
          return a, b, c
        
        df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes, axis=1, result_type="expand")
        

        请注意,诀窍在于applyresult_type 参数,它将其结果扩展为可以直接分配给新/旧列的DataFrame

        【讨论】:

        • 缺点是这适用于 .apply() 在 DataFrames 上,而不是在 Series 上。此外,对于 pandas 1.1.5,这根本不起作用。
        • @MERose +1 提到它不适用于系列(apply() 没有 axis 参数),但它适用于 1.1.5。跨度>
        • 对我来说,它是最优雅和内置支持的解决方案。在 pandas 1.3.0 上运行良好
        【解决方案6】:

        我相信 1.1 版本打破了此处最佳答案中建议的行为。

        import pandas as pd
        def test_func(row):
            row['c'] = str(row['a']) + str(row['b'])
            row['d'] = row['a'] + 1
            return row
        
        df = pd.DataFrame({'a': [1, 2, 3], 'b': ['i', 'j', 'k']})
        df.apply(test_func, axis=1)
        

        以上代码在 pandas 1.1.0 上运行返回:

           a  b   c  d
        0  1  i  1i  2
        1  1  i  1i  2
        2  1  i  1i  2
        

        在 pandas 1.0.5 中返回:

           a   b    c  d
        0  1   i   1i  2
        1  2   j   2j  3
        2  3   k   3k  4
        

        我认为这是你所期望的。

        不确定the release notes 是如何解释这种行为的,但是正如here 解释的那样,通过复制原始行来避免它们的突变会恢复旧的行为。即:

        def test_func(row):
            row = row.copy()   #  <---- Avoid mutating the original reference
            row['c'] = str(row['a']) + str(row['b'])
            row['d'] = row['a'] + 1
            return row
        

        【讨论】:

          【解决方案7】:

          热门答案之间的表现差异很大,Jesse 和 famaral42 已经讨论过这一点,但值得分享热门答案之间的公平比较,并详细说明 Jesse 答案的一个微妙但重要的细节:传入函数的参数,也会影响性能

          (Python 3.7.4,Pandas 1.0.3)

          import pandas as pd
          import locale
          import timeit
          
          
          def create_new_df_test():
              df_test = pd.DataFrame([
                {'dir': '/Users/uname1', 'size': 994933},
                {'dir': '/Users/uname2', 'size': 109338711},
              ])
              return df_test
          
          
          def sizes_pass_series_return_series(series):
              series['size_kb'] = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
              series['size_mb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
              series['size_gb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
              return series
          
          
          def sizes_pass_series_return_tuple(series):
              a = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
              b = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
              c = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
              return a, b, c
          
          
          def sizes_pass_value_return_tuple(value):
              a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
              b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
              c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
              return a, b, c
          

          结果如下:

          # 1 - Accepted (Nels11 Answer) - (pass series, return series):
          9.82 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          # 2 - Pandafied (jaumebonet Answer) - (pass series, return tuple):
          2.34 ms ± 48.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          # 3 - Tuples (pass series, return tuple then zip):
          1.36 ms ± 62.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
          
          # 4 - Tuples (Jesse Answer) - (pass value, return tuple then zip):
          752 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
          
          

          注意返回元组是最快的方法,但是作为参数传递的in 也会影响性能。代码的差异是细微的,但性能提升是显着的。

          测试#4(传递单个值)比测试#3(传递一系列)快两倍,尽管执行的操作表面上是相同的。

          但还有更多...

          # 1a - Accepted (Nels11 Answer) - (pass series, return series, new columns exist):
          3.23 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          # 2a - Pandafied (jaumebonet Answer) - (pass series, return tuple, new columns exist):
          2.31 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          # 3a - Tuples (pass series, return tuple then zip, new columns exist):
          1.36 ms ± 58.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
          
          # 4a - Tuples (Jesse Answer) - (pass value, return tuple then zip, new columns exist):
          694 µs ± 3.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
          
          

          在某些情况下(#1a 和 #4a),将函数应用于已经存在输出列的 DataFrame 比从函数创建它们要快。

          这是运行测试的代码:

          # Paste and run the following in ipython console. It will not work if you run it from a .py file.
          print('\nAccepted Answer (pass series, return series, new columns dont exist):')
          df_test = create_new_df_test()
          %timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
          print('Accepted Answer (pass series, return series, new columns exist):')
          df_test = create_new_df_test()
          df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
          %timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
          
          print('\nPandafied (pass series, return tuple, new columns dont exist):')
          df_test = create_new_df_test()
          %timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
          print('Pandafied (pass series, return tuple, new columns exist):')
          df_test = create_new_df_test()
          df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
          %timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
          
          print('\nTuples (pass series, return tuple then zip, new columns dont exist):')
          df_test = create_new_df_test()
          %timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
          print('Tuples (pass series, return tuple then zip, new columns exist):')
          df_test = create_new_df_test()
          df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
          %timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
          
          print('\nTuples (pass value, return tuple then zip, new columns dont exist):')
          df_test = create_new_df_test()
          %timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
          print('Tuples (pass value, return tuple then zip, new columns exist):')
          df_test = create_new_df_test()
          df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
          %timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
          

          【讨论】:

          • 这真的很有趣...而且确实值得深思。我总是偏爱使用图书馆提供的解决方案,但她的性能差异不容忽视。我现在想知道如果只通过值而不是系列,我的解决方案将如何运行。感谢您的精彩分析!
          • 我会很感激一个简单的顶线“这种方法是最快的”,而不是几段不同单位(ms vs. us)之间的夸大和比较。是的,要确定自己是微不足道的,但首先要考虑所有可怜的 Google 员工和 Stack Overflow 的目的。
          • 欣赏代码 - 明确测量的内容,在我的机器上重新运行并测试不同的情况。我发现如果使用 1000 行进行测试,结果会大不相同:#3 和 #4 仅相差 50%,但 #1 比 1a 慢 x8。
          【解决方案8】:

          它提供了一个新数据框,其中包含原始数据框的两列。

          import pandas as pd
          df = ...
          df_with_two_columns = df.apply(lambda row:pd.Series([row['column_1'], row['column_2']], index=['column_1', 'column_2']),axis = 1)
          

          【讨论】:

            【解决方案9】:

            只是另一种可读方式。此代码将添加三个新列及其值,在 apply 函数中返回不使用参数的系列。

            def sizes(s):
            
                val_kb = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
                val_mb = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
                val_gb = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
                return pd.Series([val_kb,val_mb,val_gb],index=['size_kb','size_mb','size_gb'])
            
            df[['size_kb','size_mb','size_gb']] = df.apply(lambda x: sizes(x) , axis=1)
            

            来自https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.apply.html的一般示例

            df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)
            
            #foo  bar
            #0    1    2
            #1    1    2
            #2    1    2
            

            【讨论】:

            • 只需一个参数,就不需要 lambda 转换:df.apply(x, axis=1)。还有,和Jesse的解决方案基本一样。
            【解决方案10】:

            真的很酷的答案!谢谢杰西和jaumebonet!只是关于以下方面的一些观察:

            • zip(* ...
            • ... result_type="expand")

            虽然 expand 更优雅(pandified),zip 至少快 **2 倍。在下面这个简单的示例中,我的速度提高了 4 倍

            import pandas as pd
            
            dat = [ [i, 10*i] for i in range(1000)]
            
            df = pd.DataFrame(dat, columns = ["a","b"])
            
            def add_and_sub(row):
                add = row["a"] + row["b"]
                sub = row["a"] - row["b"]
                return add, sub
            
            df[["add", "sub"]] = df.apply(add_and_sub, axis=1, result_type="expand")
            # versus
            df["add"], df["sub"] = zip(*df.apply(add_and_sub, axis=1))
            

            【讨论】:

            • "expand" 不适用于 pandas 1.3,但 zip 很神奇!谢谢
            【解决方案11】:

            使用 apply 和 zip 将比 Series 方式快 3 倍。

            def sizes(s):    
                return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \
                    locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \
                    locale.format("%.1f", s / 1024.0 ** 3, grouping=True) + ' GB'
            df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes))
            

            测试结果为:

            Separate df.apply(): 
            
                100 loops, best of 3: 1.43 ms per loop
            
            Return Series: 
            
                100 loops, best of 3: 2.61 ms per loop
            
            Return tuple:
            
                1000 loops, best of 3: 819 µs per loop
            

            【讨论】:

            • @Jesse 这在 pandas 1.1 中不再有效。* 如果我在整个框架而不是特定列上执行 apply,我会收到形状错误
            • 请注意zip 方法不会保留正确的索引。 result_type=expand 不过会的。
            • 这是一个不错的答案,因为它避免了典型的ValueError: Columns must be same length as key
            【解决方案12】:

            一般来说,要返回多个值,我就是这样做的

            def gimmeMultiple(group):
                x1 = 1
                x2 = 2
                return array([[1, 2]])
            def gimmeMultipleDf(group):
                x1 = 1
                x2 = 2
                return pd.DataFrame(array([[1,2]]), columns=['x1', 'x2'])
            df['size'].astype(int).apply(gimmeMultiple)
            df['size'].astype(int).apply(gimmeMultipleDf)
            

            返回一个数据框肯定有它的好处,但有时不是必需的。您可以查看apply() 返回的内容,并尝试使用这些函数;)

            【讨论】:

            • 感谢您提供此示例。但是,这不会为所有结果输出单个数据帧。当我尝试将其添加回原始数据帧时,我得到“ValueError:数组不可广播以纠正形状”。
            • 你能提供代码来生成一些小数据样本吗?
            • 当然可以。我刚刚更新了原始帖子中的代码以包含示例数据和输出。
            猜你喜欢
            • 2021-03-16
            • 2017-09-09
            • 1970-01-01
            • 1970-01-01
            • 2022-01-26
            • 1970-01-01
            • 2015-06-04
            • 2016-08-04
            • 1970-01-01
            相关资源
            最近更新 更多