【问题标题】:pandas apply function that returns multiple values to rows in pandas dataframe熊猫应用函数将多个值返回到熊猫数据框中的行
【发布时间】:2014-07-04 14:40:56
【问题描述】:

我有一个带有时间索引和 3 列的数据框,其中包含 3D 矢量的坐标:

                         x             y             z
ts
2014-05-15 10:38         0.120117      0.987305      0.116211
2014-05-15 10:39         0.117188      0.984375      0.122070
2014-05-15 10:40         0.119141      0.987305      0.119141
2014-05-15 10:41         0.116211      0.984375      0.120117
2014-05-15 10:42         0.119141      0.983398      0.118164

我想对也返回向量的每一行应用一个转换

def myfunc(a, b, c):
    do something
    return e, f, g

但如果我这样做:

df.apply(myfunc, axis=1)

我最终得到了一个以元组为元素的 Pandas 系列。这是因为 apply 将获取 myfunc 的结果而不解包它。如何更改 myfunc 以获得具有 3 列的新 df?

编辑:

以下所有解决方案均有效。 Series 解决方案确实允许列名,List 解决方案似乎执行得更快。

def myfunc1(args):
    e=args[0] + 2*args[1]
    f=args[1]*args[2] +1
    g=args[2] + args[0] * args[1]
    return pd.Series([e,f,g], index=['a', 'b', 'c'])

def myfunc2(args):
    e=args[0] + 2*args[1]
    f=args[1]*args[2] +1
    g=args[2] + args[0] * args[1]
    return [e,f,g]

%timeit df.apply(myfunc1 ,axis=1)

100 loops, best of 3: 4.51 ms per loop

%timeit df.apply(myfunc2 ,axis=1)

100 loops, best of 3: 2.75 ms per loop

【问题讨论】:

  • “将函数返回的元组(/list)解包成多列”很有用。而不是 “这是因为 apply 将获取 myfunc 的结果而不对其进行解包。如何更改 myfunc 以便获得具有 3 列的新 df?”* 标记为 tuple-unpacking/iterable-unpacking

标签: python pandas dataframe apply iterable-unpacking


【解决方案1】:

其他人的一些答案包含错误,因此我在下面对其进行了总结。完美答案如下。

准备数据集。 pandas 版本使用1.1.5

import numpy as np
import pandas as pd
import timeit

# check pandas version
print(pd.__version__)
# 1.1.5

# prepare DataFrame
df = pd.DataFrame({
    'x': [0.120117, 0.117188, 0.119141, 0.116211, 0.119141],
    'y': [0.987305, 0.984375, 0.987305, 0.984375, 0.983398],
    'z': [0.116211, 0.122070, 0.119141, 0.120117, 0.118164]},
    index=[
        '2014-05-15 10:38',
        '2014-05-15 10:39',
        '2014-05-15 10:40',
        '2014-05-15 10:41',
        '2014-05-15 10:42'],
    columns=['x', 'y', 'z'])
df.index.name = 'ts'
#                          x         y         z
# ts                                            
# 2014-05-15 10:38  0.120117  0.987305  0.116211
# 2014-05-15 10:39  0.117188  0.984375  0.122070
# 2014-05-15 10:40  0.119141  0.987305  0.119141
# 2014-05-15 10:41  0.116211  0.984375  0.120117
# 2014-05-15 10:42  0.119141  0.983398  0.118164

解决方案 01。

在应用函数中返回pd.Series

def myfunc1(args):
    e = args[0] + 2*args[1]
    f = args[1]*args[2] + 1
    g = args[2] + args[0] * args[1]
    return pd.Series([e, f, g])

df[['e', 'f', 'g']] = df.apply(myfunc1, axis=1)
#                          x         y         z         e         f         g
# ts
# 2014-05-15 10:38  0.120117  0.987305  0.116211  2.094727  1.114736  0.234803
# 2014-05-15 10:39  0.117188  0.984375  0.122070  2.085938  1.120163  0.237427
# 2014-05-15 10:40  0.119141  0.987305  0.119141  2.093751  1.117629  0.236770
# 2014-05-15 10:41  0.116211  0.984375  0.120117  2.084961  1.118240  0.234512
# 2014-05-15 10:42  0.119141  0.983398  0.118164  2.085937  1.116202  0.235327

t1 = timeit.timeit(
    'df.apply(myfunc1, axis=1)',
    globals=dict(df=df, myfunc1=myfunc1), number=10000)
print(round(t1, 3), 'seconds')
# 14.571 seconds

解决方案 02。

申请时使用result_type ='expand'

def myfunc2(args):
    e = args[0] + 2*args[1]
    f = args[1]*args[2] + 1
    g = args[2] + args[0] * args[1]
    return [e, f, g]

df[['e', 'f', 'g']] = df.apply(myfunc2, axis=1, result_type='expand')
#                          x         y         z         e         f         g
# ts                                                                          
# 2014-05-15 10:38  0.120117  0.987305  0.116211  2.094727  1.114736  0.234803
# 2014-05-15 10:39  0.117188  0.984375  0.122070  2.085938  1.120163  0.237427
# 2014-05-15 10:40  0.119141  0.987305  0.119141  2.093751  1.117629  0.236770
# 2014-05-15 10:41  0.116211  0.984375  0.120117  2.084961  1.118240  0.234512
# 2014-05-15 10:42  0.119141  0.983398  0.118164  2.085937  1.116202  0.235327

t2 = timeit.timeit(
    "df.apply(myfunc2, axis=1, result_type='expand')",
    globals=dict(df=df, myfunc2=myfunc2), number=10000)
print(round(t2, 3), 'seconds')
# 9.907 seconds

解决方案 03。

如果您想让它更快,请使用np.vectorize。请注意,使用np.vectorize 时,args 不能是单个参数。

def myfunc3(args0, args1, args2):
    e = args0 + 2*args1
    f = args1*args2 + 1
    g = args2 + args0 * args1
    return [e, f, g]

df[['e', 'f', 'g']] = pd.DataFrame(np.row_stack(np.vectorize(myfunc3, otypes=['O'])(df['x'], df['y'], df['z'])), index=df.index)
#                          x         y         z         e         f         g
# ts                                                                          
# 2014-05-15 10:38  0.120117  0.987305  0.116211  2.094727  1.114736  0.234803
# 2014-05-15 10:39  0.117188  0.984375  0.122070  2.085938  1.120163  0.237427
# 2014-05-15 10:40  0.119141  0.987305  0.119141  2.093751  1.117629  0.236770
# 2014-05-15 10:41  0.116211  0.984375  0.120117  2.084961  1.118240  0.234512
# 2014-05-15 10:42  0.119141  0.983398  0.118164  2.085937  1.116202  0.235327

t3 = timeit.timeit(
    "pd.DataFrame(np.row_stack(np.vectorize(myfunc3, otypes=['O'])(df['x'], df['y'], df['z'])), index=df.index)",
    globals=dict(pd=pd, np=np, df=df, myfunc3=myfunc3), number=10000)
print(round(t3, 3), 'seconds')
# 1.598 seconds

【讨论】:

    【解决方案2】:

    Pandas 1.0.5 的 DataFrame.apply 带有参数 result_type 可以在这里提供帮助。 来自文档:

    These only act when axis=1 (columns):
    
    ‘expand’ : list-like results will be turned into columns.
    
     ‘reduce’ : returns a Series if possible rather than expanding list-like results. This 
     is the opposite of ‘expand’.
    
    ‘broadcast’ : results will be broadcast to the original shape of the DataFrame, the 
    original index and columns will be retained.
    

    【讨论】:

      【解决方案3】:

      我尝试返回一个元组(我使用像 scipy.stats.pearsonr 这样返回这种结构的函数)但它返回了一个 1D Series 而不是我预期的 Dataframe。如果我手动创建一个系列,性能会更差,所以我使用result_type 修复它,如official API documentation 中所述:

      在函数内部返回一个Series类似于传递 结果类型='展开'。结果列名将是系列 索引。

      所以你可以这样编辑你的代码:

      def myfunc(a, b, c):
          # do something
          return (e, f, g)
      
      df.apply(myfunc, axis=1,  result_type='expand')
      

      【讨论】:

      • 我喜欢这个,它似乎是最熊猫的,虽然只兼容pandas >= 0.0.23 (per Genarito's link to the api documentation)
      • 如果您希望在数据框中创建两个或三个(或 n 个)新列,您可以使用:df['e'], d['f'], d['g'] = df.apply(myfunc, axis=1, result_type='expand').T.values
      • 我们可以使用 .apply 返回比 df 更多的行数来创建稀释副本吗?假设 df 有 100 行,函数为每行返回 100 行,结果数据框应该有 100*100 行。可能吗?
      • 说真的,我不知道。也许您能做的最好的事情是另一个 Stack Overflow 问题,以获得自定义最佳答案
      • 我不得不使用 df['e'], d['f'], d['g'] = df.apply(myfunc, axis=1, result_type='expand')。 @spen.smith 建议的 T.values。没有它,直接分配列的值是 0 和 1(例如 df["A"], df["B"] = df.apply(foo, axis=1, result_type="expand") 与 foo 返回 [" A"、"B"] 或 ("A","B") 将分别为 A 和 B 列赋予值 0 和 1。
      【解决方案4】:

      基于@U2EF1 出色的answer,我创建了一个方便的函数,该函数应用指定的函数将元组返回到数据帧字段,并将结果扩展回数据帧。

      def apply_and_concat(dataframe, field, func, column_names):
          return pd.concat((
              dataframe,
              dataframe[field].apply(
                  lambda cell: pd.Series(func(cell), index=column_names))), axis=1)
      

      用法:

      df = pd.DataFrame([1, 2, 3], index=['a', 'b', 'c'], columns=['A'])
      print df
         A
      a  1
      b  2
      c  3
      
      def func(x):
          return x*x, x*x*x
      
      print apply_and_concat(df, 'A', func, ['x^2', 'x^3'])
      
         A  x^2  x^3
      a  1    1    1
      b  2    4    8
      c  3    9   27
      

      希望对某人有所帮助。

      【讨论】:

      • 这很棒。为我节省了很多时间。谢谢!
      【解决方案5】:

      只返回一个列表而不是元组。

      In [81]: df
      Out[81]: 
                                  x         y         z
      ts                                               
      2014-05-15 10:38:00  0.120117  0.987305  0.116211
      2014-05-15 10:39:00  0.117188  0.984375  0.122070
      2014-05-15 10:40:00  0.119141  0.987305  0.119141
      2014-05-15 10:41:00  0.116211  0.984375  0.120117
      2014-05-15 10:42:00  0.119141  0.983398  0.118164
      
      [5 rows x 3 columns]
      
      In [82]: def myfunc(args):
         ....:        e=args[0] + 2*args[1]
         ....:        f=args[1]*args[2] +1
         ....:        g=args[2] + args[0] * args[1]
         ....:        return [e,f,g]
         ....: 
      
      In [83]: df.apply(myfunc ,axis=1)
      Out[83]: 
                                  x         y         z
      ts                                               
      2014-05-15 10:38:00  2.094727  1.114736  0.234803
      2014-05-15 10:39:00  2.085938  1.120163  0.237427
      2014-05-15 10:40:00  2.093751  1.117629  0.236770
      2014-05-15 10:41:00  2.084961  1.118240  0.234512
      2014-05-15 10:42:00  2.085937  1.116202  0.235327
      

      【讨论】:

      • 这不起作用。它返回一个系列,其元素是列表。我在熊猫 0.18.1
      • 请参阅下面的 U2EF1 响应 - 将结果列表包装到 pd.Series()
      【解决方案6】:

      返回Series,它会将它们放入一个DataFrame中。

      def myfunc(a, b, c):
          do something
          return pd.Series([e, f, g])
      

      这样做的好处是您可以为每个结果列添加标签。如果您返回一个 DataFrame,它只会为该组插入多行。

      【讨论】:

      • flexible apply查看更多示例
      • 系列答案似乎是规范答案。但是,在 0.18.1 版本上,系列解决方案所需的时间大约是运行 apply 多次的 4 倍。
      • 在每次迭代中创建一个完整的pd.Series 会不会非常低效?
      • 我在尝试这种方法时收到“AttributeError: 'float' object has no attribute 'index'”,但不确定为什么它试图从其中一个 vales (float) 获取索引? (编辑)问题是我有两个返回语句,一个只有 Nan,也需要包装在 pd.Series() 中。
      • 为这个不错的答案添加一点,可以进一步做new_vars = ['e', 'f', 'g']df[new_vars] = df.apply(my_func, axis=1)
      【解决方案7】:

      找到一个可能的解决方案,将 myfunc 更改为返回一个 np.array,如下所示:

      import numpy as np
      
      def myfunc(a, b, c):
          do something
          return np.array((e, f, g))
      

      有更好的解决方案吗?

      【讨论】:

      • 返回 numpy 数组在性能方面似乎是最好的。对于 100K 行,返回 numpy 数组以获取 DataFrame 列需要 1.55 秒;使用 return Series 需要 39.7 秒。此处的性能差异显着
      猜你喜欢
      • 2018-05-13
      • 2022-01-21
      • 1970-01-01
      • 1970-01-01
      • 2022-07-22
      • 2020-06-03
      • 1970-01-01
      • 1970-01-01
      • 2018-11-13
      相关资源
      最近更新 更多