如何将函数应用于Pandas数据框的两列

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

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']

2
我在下面的网址找到了相关的问答,但我的问题是通过两列现有的列计算一个新列,而不是从1个生成2个。https://dev59.com/lWct5IYBdhLWcg3wF5pF?rq=1 - bigbug
16个回答

653

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)

4
注意,如果使用axis=1并且你的列名叫做name,它实际上不会返回你的列数据而是返回索引。类似于在groupby()中获取name。我通过重命名我的列来解决这个问题。 - Tom Hemmes
17
就是这样!我只是没有意识到您可以将具有多个输入参数的用户定义函数插入lambda中。值得注意(我认为)的是,您使用的是DF.apply()而不是Series.apply()。这使您可以使用您想要的两列索引df并将整个列传递到函数中,但是因为您使用了apply(),它会在整个列中按元素逐个应用该函数。太棒了!谢谢您的发布! - Data-phile
4
@Mez13 如果需要的话,你也可以使用 f(x['col 1'], x['col 2']) 样式的索引(例如,如果你的列名中有空格或受保护的名称)。 - ajrwhite
这是一个非常好的答案!终于!它解决了一个更普遍的问题。也就是说,df['C'] = my_function(df['A']) 不起作用。然而,df['C'] = df.apply(lambda x: my_function(x.A), axis=1) 可以起作用。 - SomJura
一行代码解决了我的问题,谢谢。 - vinsinraw
显示剩余19条评论

477

这里是一个在数据框上使用apply的例子,我使用axis=1进行调用。

请注意,不同之处在于,不要试图向函数f传递两个值,而是重写函数以接受Pandas Series对象,然后对Series进行索引以获取所需的值。

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

根据您的使用情况,有时创建pandas group对象并在组上使用apply会很有帮助。


2
请问您能否粘贴一下您的代码?我重写了这个函数:def get_sublist(x): return mylist[x[1]:x[2] + 1],然后 df['col_3'] = df.apply(get_sublist, axis=1) 报错 'ValueError: operands could not be broadcast together with shapes (2) (3)'。 - bigbug
7
使用Pandas版本0.14.1(可能早期版本)时,也可以使用lambda表达式。对于您定义的“df”对象,另一种方法(具有等效结果)是“df.apply(lambda x: x[0] + x[1], axis = 1)”。 - Jubbles
能否让f()返回一个dict?当我尝试这样做时,会得到<built-in method values of dict object at 0x10...的错误提示。我查看了这个链接,其中的评论“假设pandas可以调用someobj.values”似乎表明这是不可能的? - scharfmn
@Jubbles:是的,说得好。事实上,OP在2012年就使用了lambda表达式!我只是在我的答案中匹配了OP的格式。 - Aman
4
你可以在函数中直接使用列名,而不是索引,这样就不用担心顺序变化了,或者通过列名获取索引,例如请参见https://dev59.com/sWcs5IYBdhLWcg3wPxce。 - Davos
显示剩余9条评论

174

一个简单的解决方法是:

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

3
这个回答和问题中的方法有什么不同?df['col_3'] = df[['col_1','col_2']].apply(f)只是为了确认,问题中的方法没有奏效,因为发布者没有指定axis=1,而默认的是axis=0? - Lost1
3
这个答案与 @Anman 的答案类似,但更加简洁。它构建了一个匿名函数,接受一个可迭代对象并在传递给函数 f 之前对其进行解包。 - tiao
8
在我的情况下,使用这种方法可以使处理100k行数据的速度提升一倍(相对于df.apply(lambda x: f(x.col_1, x.col_2), axis=1))。 - Sylvain
3
这里最优雅的答案。 - user41855
2
@sjm 不错!但是如果x的参数是args和kwargs等混合的呢? - jtlz2

46
一个有趣的问题!我的答案如下:
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 < J1 < J2 < J3,从而使列的显示顺序正确。

简洁版:

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

axis=1 是我想要的,谢谢。 - Quinten C

28

你要寻找的方法是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]

25

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) 当返回的列表长度等于列数时,将返回一个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使用apply较慢。使用基本的迭代方法可以获得更好的性能(特别是在处理大型数据集时)。

创建更大的数据框

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)

4
很高兴看到如此详细的回答,可以从中学到很多。 - Andrea Moro
对于最新的pandas版本(1.3.1),返回的列表被保留,上述三个示例都可以正常工作。所有结果将是pd.Series,dtype ='object'。但是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).Tdf.apply(f,axis = 1)不相同。 - KH Kim

22

以下是更快的解决方案:

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 Series或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技巧可以进一步优化。


19

我相信这种方法并不像使用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]

2
这实际上比那些使用applyaxis=1的答案要快得多。 - Ted Petrou
3
四年过去了,但相比之下这个成语是如此迅速!来自未来的感谢。 - Chris

17

我要为 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]

1
这并没有真正使用pandas回答问题。 - mnky9800n
25
问题是如何将函数应用于Pandas数据框的两列,而不是仅使用Pandas方法将函数应用于两列。Numpy是Pandas的依赖项,因此你必须安装它,所以这似乎是一个奇怪的反对意见。 - Trae Wallace

13

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

您需要更改f,使其接受一个单一的输入参数,将上述数据框保留为输入,在函数体内拆分为 x 和 y。然后执行所需操作,并返回单个值。

您需要使用这个函数签名,因为语法是.apply(f)。 因此,f需要采用单个参数——数据框,并且不能像当前的f那样采用两个参数。

由于您没有提供f的主体,我无法提供更详细的帮助——但这应该为您提供出路,而不必从根本上更改代码或使用其他方法而不是apply。


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