【问题标题】:How to apply a function to two columns of Pandas dataframe如何将函数应用于两列 Pandas 数据框
【发布时间】:2012-10-31 04:01:08
【问题描述】:

假设我有一个df,其中包含'ID', 'col_1', 'col_2' 列。我定义了一个函数:

f = lambda x, y : my_function_expression.

现在我想将f 应用到df 的两列'col_1', 'col_2' 以逐元素计算一个新列'col_3',有点像:

df['col_3'] = df[['col_1','col_2']].apply(f)  
# Pandas gives : TypeError: ('<lambda>() takes exactly 2 arguments (1 given)'

怎么办?

** 添加详细示例如下***

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

  ID  col_1  col_2            col_3
0  1      0      1       ['a', 'b']
1  2      2      4  ['c', 'd', 'e']
2  3      3      5  ['d', 'e', 'f']

【问题讨论】:

标签: python pandas dataframe


【解决方案1】:

这是一个更快的解决方案:

def func_1(a,b):
    return a + b

df["C"] = func_1(df["A"].to_numpy(),df["B"].to_numpy())

这比 @Aman 的 df.apply(f, axis=1) 快 380 倍,比 @ajrwhite 的 df['col_3'] = df.apply(lambda x: f(x.col_1, x.col_2), axis=1) 快 310 倍。

我也添加了一些基准:

结果:

  FUNCTIONS   TIMINGS   GAIN
apply lambda    0.7     x 1
apply           0.56    x 1.25
map             0.3     x 2.3
np.vectorize    0.01    x 70
f3 on Series    0.0026  x 270
f3 on np arrays 0.0018  x 380
f3 numba        0.0018  x 380

简而言之:

使用 apply 很慢。我们可以非常简单地加快速度,只需使用一个可以直接在 Pandas Series 上运行的函数(或者在 numpy 数组上更好)。因为我们将在 Pandas Series 或 numpy 数组上进行操作,所以我们将能够对操作进行矢量化。该函数将返回一个 Pandas 系列或 numpy 数组,我们将其分配为新列。

这是基准代码:

import timeit

timeit_setup = """
import pandas as pd
import numpy as np
import numba

np.random.seed(0)

# Create a DataFrame of 10000 rows with 2 columns "A" and "B" 
# containing integers between 0 and 100
df = pd.DataFrame(np.random.randint(0,10,size=(10000, 2)), columns=["A", "B"])

def f1(a,b):
    # Here a and b are the values of column A and B for a specific row: integers
    return a + b

def f2(x):
    # Here, x is pandas Series, and corresponds to a specific row of the DataFrame
    # 0 and 1 are the indexes of columns A and B
    return x[0] + x[1]  

def f3(a,b):
    # Same as f1 but we will pass parameters that will allow vectorization
    # Here, A and B will be Pandas Series or numpy arrays
    # with df["C"] = f3(df["A"],df["B"]): Pandas Series
    # with df["C"] = f3(df["A"].to_numpy(),df["B"].to_numpy()): numpy arrays
    return a + b

@numba.njit('int64[:](int64[:], int64[:])')
def f3_numba_vectorize(a,b):
    # Here a and b are 2 numpy arrays with dtype int64
    # This function must return a numpy array whith dtype int64
    return a + b

"""

test_functions = [
'df["C"] = df.apply(lambda row: f1(row["A"], row["B"]), axis=1)',
'df["C"] = df.apply(f2, axis=1)',
'df["C"] = list(map(f3,df["A"],df["B"]))',
'df["C"] = np.vectorize(f3) (df["A"].to_numpy(),df["B"].to_numpy())',
'df["C"] = f3(df["A"],df["B"])',
'df["C"] = f3(df["A"].to_numpy(),df["B"].to_numpy())',
'df["C"] = f3_numba_vectorize(df["A"].to_numpy(),df["B"].to_numpy())'
]


for test_function in test_functions:
    print(min(timeit.repeat(setup=timeit_setup, stmt=test_function, repeat=7, number=10)))

输出:

0.7
0.56
0.3
0.01
0.0026
0.0018
0.0018

最后说明:也可以使用 Cython 和其他 numba 技巧来优化事情。

【讨论】:

    【解决方案2】:

    另一个选项是df.itertuples()(通常比df.iterrows() 更快,由docs 和user testing 推荐):

    import pandas as pd
    
    df = pd.DataFrame([range(4) for _ in range(4)], columns=list("abcd"))
    
    df
        a   b   c   d
    0   0   1   2   3
    1   0   1   2   3
    2   0   1   2   3
    3   0   1   2   3
    
    
    df["e"] = [sum(row) for row in df[["b", "d"]].itertuples(index=False)]
    
    df
        a   b   c   d   e
    0   0   1   2   3   4
    1   0   1   2   3   4
    2   0   1   2   3   4
    3   0   1   2   3   4
    

    由于itertuples 返回namedtuples 的Iterable,您可以通过列名(也称为点表示法)和索引作为属性访问元组元素:

    b, d = row
    b = row.b
    d = row[1]
    

    【讨论】:

    • 根据我的经验,itertuples 有时比df.apply(..., axis=1) 快得多。对于大型表,我看到时间从大约 3 分钟(使用 apply)减少到 10 秒(使用 itertuples。我个人也认为 itertuples 有时更具可读性;它读起来像伪代码。请注意,元组可以通过名称或位置访问(即,在上面的答案中,index=False、row.b 等价于row[0])。
    【解决方案3】:

    在 Pandas 中有一种简洁的单行方式:

    df['col_3'] = df.apply(lambda x: f(x.col_1, x.col_2), axis=1)
    

    这允许f 成为具有多个输入值的用户定义函数,并使用(安全)列名而不是(不安全)数字索引来访问列。

    数据示例(基于原始问题):

    import pandas as pd
    
    df = pd.DataFrame({'ID':['1', '2', '3'], 'col_1': [0, 2, 3], 'col_2':[1, 4, 5]})
    mylist = ['a', 'b', 'c', 'd', 'e', 'f']
    
    def get_sublist(sta,end):
        return mylist[sta:end+1]
    
    df['col_3'] = df.apply(lambda x: get_sublist(x.col_1, x.col_2), axis=1)
    

    print(df) 的输出:

      ID  col_1  col_2      col_3
    0  1      0      1     [a, b]
    1  2      2      4  [c, d, e]
    2  3      3      5  [d, e, f]
    

    如果您的列名包含空格或与现有数据框属性共享名称,则可以使用方括号进行索引:

    df['col_3'] = df.apply(lambda x: f(x['col 1'], x['col 2']), axis=1)
    

    【讨论】:

    • 注意,如果使用axis=1 并且您的列被称为name,它实际上不会返回您的列数据,而是index。类似于在groupby() 中获取name。我通过重命名我的专栏解决了这个问题。
    • 就是这样!我只是没有意识到您可以将具有多个输入参数的用户定义函数插入到 lambdas 中。重要的是要注意(我认为)您使用的是 DF.apply() 而不是 Series.apply()。这使您可以使用所需的两列对 df 进行索引,并将整个列传递给函数,但是因为您使用的是 apply(),所以它以元素方式将函数应用到整个列。杰出的!感谢您发布!
    • 终于!你拯救了我的一天!
    • 我相信建议的方法是 df.loc[:, 'new col'] = df.apply.....
    • @Mez13 如有必要,您还可以使用f(x['col 1'], x['col 2']) 样式索引(例如,如果您的列名包含空格或受保护的名称)。
    【解决方案4】:

    如果你有一个庞大的数据集,那么你可以使用一种简单但更快(执行时间)的方式来使用 swifter:

    import pandas as pd
    import swifter
    
    def fnc(m,x,c):
        return m*x+c
    
    df = pd.DataFrame({"m": [1,2,3,4,5,6], "c": [1,1,1,1,1,1], "x":[5,3,6,2,6,1]})
    df["y"] = df.swifter.apply(lambda x: fnc(x.m, x.x, x.c), axis=1)
    

    【讨论】:

      【解决方案5】:

      我想你不想改变get_sublist 函数,只想使用DataFrame 的apply 方法来完成这项工作。为了得到你想要的结果,我写了两个帮助函数:get_sublist_list 和unlist。正如函数名所暗示的那样,首先获取子列表的列表,然后从该列表中提取该子列表。最后,我们需要调用apply函数将这两个函数随后应用到df[['col_1','col_2']] DataFrame。

      import pandas as pd
      
      df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
      mylist = ['a','b','c','d','e','f']
      
      def get_sublist(sta,end):
          return mylist[sta:end+1]
      
      def get_sublist_list(cols):
          return [get_sublist(cols[0],cols[1])]
      
      def unlist(list_of_lists):
          return list_of_lists[0]
      
      df['col_3'] = df[['col_1','col_2']].apply(get_sublist_list,axis=1).apply(unlist)
      
      df
      

      如果您不使用[] 来封装get_sublist 函数,那么get_sublist_list 函数将返回一个普通列表,它将引发ValueError: could not broadcast input array from shape (3) into shape (2),正如@Ted Petrou 所提到的。

      【讨论】:

        【解决方案6】:

        从apply 返回一个列表是一个危险的操作,因为结果对象不能保证是 Series 或 DataFrame。在某些情况下可能会引发异常。我们来看一个简单的例子:

        df = pd.DataFrame(data=np.random.randint(0, 5, (5,3)),
                          columns=['a', 'b', 'c'])
        df
           a  b  c
        0  4  0  0
        1  2  0  1
        2  2  2  2
        3  1  2  2
        4  3  0  0
        

        从apply返回列表有三种可能的结果

        1)如果返回列表的长度不等于列数,则返回一系列列表。

        df.apply(lambda x: list(range(2)), axis=1)  # returns a Series
        0    [0, 1]
        1    [0, 1]
        2    [0, 1]
        3    [0, 1]
        4    [0, 1]
        dtype: object
        

        2) 当返回列表的长度等于 columns 然后返回一个 DataFrame 并且每一列都得到 列表中的对应值。

        df.apply(lambda x: list(range(3)), axis=1) # returns a DataFrame
           a  b  c
        0  0  1  2
        1  0  1  2
        2  0  1  2
        3  0  1  2
        4  0  1  2
        

        3) 如果返回列表的长度等于第一行的列数,但至少有一行列表的元素数与列数不同,则 ValueError 为提出来。

        i = 0
        def f(x):
            global i
            if i == 0:
                i += 1
                return list(range(3))
            return list(range(4))
        
        df.apply(f, axis=1) 
        ValueError: Shape of passed values is (5, 4), indices imply (5, 3)
        

        不应用就回答问题

        将apply 与axis=1 一起使用非常慢。使用基本的迭代方法可以获得更好的性能(尤其是在较大的数据集上)。

        创建更大的数据框

        df1 = df.sample(100000, replace=True).reset_index(drop=True)
        

        时间

        # apply is slow with axis=1
        %timeit df1.apply(lambda x: mylist[x['col_1']: x['col_2']+1], axis=1)
        2.59 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
        
        # zip - similar to @Thomas
        %timeit [mylist[v1:v2+1] for v1, v2 in zip(df1.col_1, df1.col_2)]  
        29.5 ms ± 534 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
        

        @Thomas 回答

        %timeit list(map(get_sublist, df1['col_1'],df1['col_2']))
        34 ms ± 459 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
        

        【讨论】:

        • 很高兴从可以学习的地方看到如此详细的答案。
        • 对于最新的 pandas 版本(1.3.1),返回的列表被保留,以上三个示例都可以正常工作。所有结果将是 dtype='object' 的 pd.Series。但是 pd.apply(f, axis=0) 的工作方式与上述类似。奇怪的是 pd.DataFrame.apply 打破了对称性,这意味着 df.T.apply(f,axis=0).T 并不总是与 df.apply(f,axis=1) 相同。比如f = lambda x: list(range(2))、df.T.apply(f, axis=0).T和df.apply(f, axis=1)不一样的时候。
        【解决方案7】:

        我对你的问题的例子:

        def get_sublist(row, col1, col2):
            return mylist[row[col1]:row[col2]+1]
        df.apply(get_sublist, axis=1, col1='col_1', col2='col_2')
        

        【讨论】:

          【解决方案8】:

          一个简单的解决方案是:

          df['col_3'] = df[['col_1','col_2']].apply(lambda x: f(*x), axis=1)
          

          【讨论】:

          • 这个答案与问题中的方法有何不同:df['col_3'] = df[['col_1','col_2']].apply(f) 只是为了确认,方法在这个问题没有解决,因为发帖人没有指定这个axis=1,默认是axis=0?
          • 这个答案与@Anman 的答案相当,但有点圆滑。他正在构建一个匿名函数,该函数接受一个可迭代对象,并在将其传递给函数 f 之前对其进行解包。
          • 这种方法在我的情况下要快两倍,有 100k 行(与 df.apply(lambda x: f(x.col_1, x.col_2), axis=1) 相比)
          • 这里最优雅的答案。
          【解决方案9】:

          我确信这不如使用 Pandas 或 Numpy 操作的解决方案快,但如果你不想重写你的函数,你可以使用 map.使用原始示例数据 -

          import pandas as pd
          
          df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
          mylist = ['a','b','c','d','e','f']
          
          def get_sublist(sta,end):
              return mylist[sta:end+1]
          
          df['col_3'] = list(map(get_sublist,df['col_1'],df['col_2']))
          #In Python 2 don't convert above to list
          

          我们可以通过这种方式将任意数量的参数传递给函数。输出是我们想要的

          ID  col_1  col_2      col_3
          0  1      0      1     [a, b]
          1  2      2      4  [c, d, e]
          2  3      3      5  [d, e, f]
          

          【讨论】:

          • 这实际上比使用apply 和axis=1 的答案要快得多
          • 这是 4 年后的事了,但是这么快的成语比起 apply 来!来自未来的感谢。
          【解决方案10】:

          我将为 np.vectorize 投票。它允许您只拍摄 x 列而不处理函数中的数据帧,因此对于您无法控制的函数或执行诸如将 2 列和一个常量发送到函数(即 col_1、col_2、 'foo')。

          import numpy as np
          import pandas as pd
          
          df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
          mylist = ['a','b','c','d','e','f']
          
          def get_sublist(sta,end):
              return mylist[sta:end+1]
          
          #df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
          # expect above to output df as below 
          
          df.loc[:,'col_3'] = np.vectorize(get_sublist, otypes=["O"]) (df['col_1'], df['col_2'])
          
          
          df
          
          ID  col_1   col_2   col_3
          0   1   0   1   [a, b]
          1   2   2   4   [c, d, e]
          2   3   3   5   [d, e, f]
          

          【讨论】:

          • 这并不能真正回答使用熊猫的问题。
          • 问题是“如何将函数应用于 Pandas 数据帧的两列”而不是“如何仅使用 Pandas 方法将函数应用于两列 Pandas 数据帧”并且 numpy 是 Pandas 的依赖项,所以无论如何你都必须安装它,所以这似乎是一个奇怪的反对意见。
          【解决方案11】:

          一个有趣的问题!我的回答如下:

          import pandas as pd
          
          def sublst(row):
              return lst[row['J1']:row['J2']]
          
          df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
          print df
          lst = ['a','b','c','d','e','f']
          
          df['J3'] = df.apply(sublst,axis=1)
          print df
          

          输出:

            ID  J1  J2
          0  1   0   1
          1  2   2   4
          2  3   3   5
            ID  J1  J2      J3
          0  1   0   1     [a]
          1  2   2   4  [c, d]
          2  3   3   5  [d, e]
          

          我把列名改成ID,J1,J2,J3,保证ID

          一个更简短的版本:

          import pandas as pd
          
          df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
          print df
          lst = ['a','b','c','d','e','f']
          
          df['J3'] = df.apply(lambda row:lst[row['J1']:row['J2']],axis=1)
          print df
          

          【讨论】:

            【解决方案12】:

            您正在寻找的方法是 Series.combine。 但是,似乎必须注意数据类型。 在您的示例中,您会(就像我在测试答案时所做的那样)天真地调用

            df['col_3'] = df.col_1.combine(df.col_2, func=get_sublist)
            

            但是,这会引发错误:

            ValueError: setting an array element with a sequence.
            

            我最好的猜测是,它似乎期望结果与调用该方法的系列的类型相同(此处为 df.col_1)。但是,以下工作:

            df['col_3'] = df.col_1.astype(object).combine(df.col_2, func=get_sublist)
            
            df
            
               ID   col_1   col_2   col_3
            0   1   0   1   [a, b]
            1   2   2   4   [c, d, e]
            2   3   3   5   [d, e, f]
            

            【讨论】:

              【解决方案13】:

              您编写 f 的方式需要两个输入。如果您查看错误消息,它说您没有为 f 提供两个输入,只有一个。错误信息是正确的。
              不匹配是因为 df[['col1','col2']] 返回具有两列的单个数据帧,而不是两个单独的列。

              您需要更改您的 f 以便它接受单个输入,将上述数据框保留为输入,然后将其分解为 x,y inside 函数体。然后做任何你需要的事情并返回一个值。

              你需要这个函数签名,因为语法是 .apply(f) 因此, f 需要采用单一事物 = 数据框,而不是您当前 f 期望的两件事。

              由于您没有提供 f 的主体,我无法提供更多详细信息 - 但这应该提供出路,而无需从根本上更改您的代码或使用其他方法而不是应用

              【讨论】:

                【解决方案14】:

                这是一个在数据帧上使用apply 的示例,我使用axis = 1 调用它。

                注意不同之处在于,不是尝试将两个值传递给函数f,而是重写函数以接受熊猫系列对象,然后索引系列以获取所需的值。

                In [49]: df
                Out[49]: 
                          0         1
                0  1.000000  0.000000
                1 -0.494375  0.570994
                2  1.000000  0.000000
                3  1.876360 -0.229738
                4  1.000000  0.000000
                
                In [50]: def f(x):    
                   ....:  return x[0] + x[1]  
                   ....:  
                
                In [51]: df.apply(f, axis=1) #passes a Series object, row-wise
                Out[51]: 
                0    1.000000
                1    0.076619
                2    1.000000
                3    1.646622
                4    1.000000
                

                根据您的用例,有时创建一个熊猫group 对象,然后在组上使用apply 会很有帮助。

                【讨论】:

                • 是的,我尝试使用 apply,但找不到有效的语法表达式。而如果df的每一行都是唯一的,还用groupby吗?
                • 在我的答案中添加了一个示例,希望这能满足您的需求。如果不是,请提供更具体的示例函数,因为sum 已通过目前建议的任何方法成功解决。
                • 您能粘贴您的代码吗?我重写了函数: def get_sublist(x): return mylist[x[1]:x[2] + 1] 和 df['col_3'] = df.apply(get_sublist, axis=1) 给出'ValueError:操作数可以不与形状一起广播 (2) (3)'
                • @Aman:对于 Pandas 版本 0.14.1(可能更早),使用也可以使用 lambda 表达式。给出您定义的df 对象,另一种方法(具有等效结果)是df.apply(lambda x: x[0] + x[1], axis = 1)。
                • @CanCeylan 您可以只使用函数中的列名而不是索引,那么您不必担心顺序更改,或者按名称获取索引,例如见stackoverflow.com/questions/13021654/…
                猜你喜欢
                • 2021-02-03
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2016-06-04
                • 1970-01-01
                相关资源
                最近更新 更多