我如何使用apply()函数的单列?

我有一个有两列的熊猫数据帧。我需要在不影响第二列的情况下改变第一列的值,并返回整个数据帧,只是改变了第一列的值。我如何在熊猫中使用apply来做到这一点?

748211 次浏览

给定一个样本数据框架df,如下:

   a  b
0  1  2
1  2  3
2  3  4
3  4  5

你想要的是:

df['a'] = df['a'].apply(lambda x: x + 1)

返回:

   a  b
0  2  2
1  3  3
2  4  4
3  5  5

对于单列,最好使用map(),如下所示:

df = pd.DataFrame([{'a': 15, 'b': 15, 'c': 5}, {'a': 20, 'b': 10, 'c': 7}, {'a': 25, 'b': 30, 'c': 9}])


a   b  c
0  15  15  5
1  20  10  7
2  25  30  9






df['a'] = df['a'].map(lambda a: a / 2.)


a   b  c
0   7.5  15  5
1  10.0  10  7
2  12.5  30  9

你根本不需要函数。您可以直接处理整个列。

示例数据:

>>> df = pd.DataFrame({'a': [100, 1000], 'b': [200, 2000], 'c': [300, 3000]})
>>> df


a     b     c
0   100   200   300
1  1000  2000  3000

a列中所有值的一半:

>>> df.a = df.a / 2
>>> df


a     b     c
0   50   200   300
1  500  2000  3000

虽然给出的响应是正确的,但它们会修改初始数据帧,这并不总是可取的(并且,鉴于OP要求示例“使用apply”,可能他们想要一个返回新数据帧的版本,就像apply那样)。

这可以使用assign实现:对于现有列,assign是有效的,正如文档所述(重点是我的):

为数据框架分配新列。

返回一个包含所有原始列和新列的新对象重新分配的现有列将被覆盖

简而言之:

In [1]: import pandas as pd


In [2]: df = pd.DataFrame([{'a': 15, 'b': 15, 'c': 5}, {'a': 20, 'b': 10, 'c': 7}, {'a': 25, 'b': 30, 'c': 9}])


In [3]: df.assign(a=lambda df: df.a / 2)
Out[3]:
a   b  c
0   7.5  15  5
1  10.0  10  7
2  12.5  30  9


In [4]: df
Out[4]:
a   b  c
0  15  15  5
1  20  10  7
2  25  30  9

注意,函数将传递整个数据帧,而不仅仅是要修改的列,因此需要确保在lambda中选择了正确的列。

如果你真的很关心你的apply函数的执行速度,并且你有一个巨大的数据集要处理,你可以使用swifter来使执行速度更快,这里有一个在pandas数据框架上的swifter的例子:

import pandas as pd
import swifter


def fnc(m):
return m*3+4


df = pd.DataFrame({"m": [1,2,3,4,5,6], "c": [1,1,1,1,1,1], "x":[5,3,6,2,6,1]})


# apply a self created function to a single column in pandas
df["y"] = df.m.swifter.apply(fnc)

这将使您的所有CPU核心计算结果,因此它将比普通应用函数快得多。试着让我知道它是否对你有用。

让我使用datetime并考虑null或空格来尝试一个复杂的计算。我在一个datetime列上减少30年,并使用apply方法以及lambda和转换datetime格式。行if x != '' else x将相应地处理所有空格或null。

df['Date'] = df['Date'].fillna('')
df['Date'] = df['Date'].apply(lambda x : ((datetime.datetime.strptime(str(x), '%m/%d/%Y') - datetime.timedelta(days=30*365)).strftime('%Y%m%d')) if x != '' else x)

给定以下数据帧df和函数complex_function

import pandas as pd


def complex_function(x, y=0):
if x > 5 and x > y:
return 1
else:
return 2


df = pd.DataFrame(data={'col1': [1, 4, 6, 2, 7], 'col2': [6, 7, 1, 2, 8]})
   col1  col2
0     1     6
1     4     7
2     6     1
3     2     2
4     7     8

有几个解决方案只能在一列上使用apply()。下面我将详细解释它们。

一、简单解决方案

直截了当的解决方案来自@Fabio Lamanna:

df['col1'] = df['col1'].apply(complex_function)

输出:

   col1  col2
0     2     6
1     2     7
2     1     1
3     2     2
4     1     8

只修改了第一列,第二列保持不变。解决方案很漂亮。它只有一行代码,读起来几乎像英语:取“col1”并对其应用函数complex_function。"

然而,如果您需要来自另一列的数据,例如。col2,行不通。如果你想将'col2'的值传递给complex_function的变量y,你需要其他东西。

2使用整个数据框架的解决方案

或者,你也可以像在这篇SO文章中这一个那样使用整个数据帧:

df['col1'] = df.apply(lambda x: complex_function(x['col1']), axis=1)

或者如果你喜欢(像我一样)没有lambda函数的解决方案:

def apply_complex_function(x):
return complex_function(x['col1'])
df['col1'] = df.apply(apply_complex_function, axis=1)

这个解决方案中有很多需要解释的地方。apply()函数作用于pd.Series 而且 pd.DataFrame。但是你不能使用df['col1'] = df.apply(complex_function).loc[:, 'col1'],因为它会抛出ValueError

因此,您需要给出要使用哪一列的信息。更复杂的是,apply()函数只接受来电吗. c。要解决这个问题,你需要定义一个(lambda)函数,将列x['col1']作为参数;也就是说,我们将列信息包装在另一个函数中。

不幸的是,axis参数的默认值是0 (axis=0),这意味着它将尝试按列而不是按行执行。这在第一个解决方案中不是问题,因为我们给了apply()一个pd.Series。但现在输入是一个数据帧,我们必须显式(axis=1)。(我很惊讶我经常忘记这一点。)

你是否喜欢带有函数的版本是主观的。在我看来,即使没有lambda函数,这行代码也足够复杂。您只需要(lambda)函数作为包装器。它只是一个样板代码。读者不应该为此烦恼。

现在,你可以很容易地修改这个解决方案来考虑第二列:

def apply_complex_function(x):
return complex_function(x['col1'], x['col2'])
df['col1'] = df.apply(apply_complex_function, axis=1)

输出:

   col1  col2
0     2     6
1     2     7
2     1     1
3     2     2
4     2     8

在索引4处,值从1变为2,因为第一个条件7 > 5为真,而第二个条件7 > 8为假。

注意,你只需要改变第一行代码(即函数),而不是第二行。


边注

从来没有将列信息放入函数中。

def bad_idea(x):
return x['col1'] ** 2

通过这样做,您可以使一个通用函数依赖于列名!这是一个坏主意,因为下次想要使用这个函数时,就不能使用了。更糟糕的是:也许你重命名了不同数据框架中的一列,只是为了让它与你现有的函数一起工作。(我也经历过。这是一个滑坡!)


3不使用apply()的替代解决方案

尽管OP特别要求使用apply()的解决方案,但建议了其他解决方案。例如,@George Petrov的回答建议使用map();@Thibaut Dubernet的回答提出了assign()

我完全同意apply()很少是最好的解决方案,因为apply()不是矢量化。它是一个基于元素的操作,具有昂贵的函数调用和pd.Series的开销。

使用apply()的一个原因是你想使用一个现有的函数,性能不是问题。或者你的函数太复杂以至于没有向量化的版本存在。

使用apply()的另一个原因是在与groupby()的组合中。请注意__ABC1和GroupBy.apply()是不同的函数。

所以考虑一些替代方案是有意义的:

  • map()只适用于pd.Series,但接受dict和pd.Series作为输入。在函数中使用map()几乎可以与使用apply()互换。它可以比apply()更快。更多细节见这篇SO文章

    df['col1'] = df['col1'].map(complex_function)
    
  • applymap()对于数据帧来说几乎是相同的。它不支持pd.Series,它总是返回一个数据框架。然而,它可以更快。文档状态: " __abc5;;但如果业绩真的很重要,你应该寻找另一条路线。

    df['col1'] = df.applymap(complex_function).loc[:, 'col1']
    
  • assign()不是apply()的可行替代。它仅在最基本的用例中具有类似的行为。它不适用于complex_function。你仍然需要apply(),你可以在下面的例子中看到。assign()的主要用例是方法链接,因为它在不改变原始数据框架的情况下返回了数据框架。

    df['col1'] = df.assign(col1=df.col1.apply(complex_function))
    

附录:如何加速apply()?

我只是在这里提到它,因为它是由其他答案建议的,例如@durjoy。这份清单并不详尽:

  1. 这可不是开玩笑的。对于大多数数值操作,在pandas中存在向量化方法。If/else块通常可以用布尔索引.loc的组合进行重构。我的例子complex_function可以这样重构。

  2. 如果你有一个复杂的方程,方程的参数在你的数据帧中,这可能是一个好主意。查看熊猫官方使用指南获取更多信息。

  3. 理论上,这应该会提高apply() 如果你只是应用NumPy缩减函数的性能,因为pd.Series的开销被删除了。当然,你的函数必须接受ndarray。您必须将函数重构为NumPy。通过这样做,您将获得巨大的性能提升。

  4. 你应该尝试的第一件事是Numba。我不知道@durjoy提到的更快;这里可能还有许多其他软件包值得一提。

  5. 如上所述,map()applymap()可以更快——这取决于用例。计时不同的版本,选择最快的。这种方法是最乏味的,性能提升最小。