给定以下数据框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
只有第一列被修改,第二列不变。解决方案很漂亮。它只是一行代码,读起来几乎像英语:"Take 'col1' and apply the function complex_function to it."
但是,如果您需要来自另一列的数据,例如'col2',它不工作。如果你想将'col2'的值传递给complex_function的变量y,你需要别的东西。
二。使用整个数据框的解决方案
或者,您可以按照in this 或this SO post 的描述使用整个数据框:
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() 函数does only accept callables。要解决这个问题,您需要定义一个 (lambda) 函数,并将列 x['col1'] 作为参数;即我们将列信息包装在另一个函数中。
不幸的是,axis 参数的默认值为零(axis=0),这意味着它将尝试按列而不是按行执行。这在第一个解决方案中不是问题,因为我们给 apply() 一个 pd.Series。但是现在输入是一个数据框,我们必须是明确的(axis=1)。 (我惊讶于我忘记这一点的频率。)
您是否更喜欢带有 lambda 函数的版本是主观的。在我看来,这行代码足够复杂,即使没有引入 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
通过这样做,您可以创建一个依赖于列名的通用函数!这是一个坏主意,因为下次您想使用此功能时,您将无法使用。更糟糕的是:也许您重命名不同数据框中的列只是为了使其与您现有的函数一起使用。 (去过那里,做到了。这是一个滑坡!)
三。不使用 apply() 的替代解决方案
虽然 OP 专门要求使用 apply() 提供解决方案,但还是建议了替代解决方案。比如@George Petrov 的回答建议使用map(),@Thibaut Dubernet 的回答建议使用assign()。
我完全同意 apply() 是 seldom the best solution,因为 apply() 是 not vectorized。它是一个元素操作,具有昂贵的函数调用和 pd.Series 的开销。
使用 apply() 的一个原因是您想使用现有函数并且性能不是问题。或者你的函数太复杂以至于不存在矢量化版本。
使用 apply() 的另一个原因是在combination with groupby()。 请注意 DataFrame.apply() 和 GroupBy.apply() 是不同的函数。
因此考虑一些替代方案确实有意义:
-
map() 仅适用于 pd.Series,但接受 dict 和 pd.Series 作为输入。使用带有函数的 map() 几乎可以与使用 apply() 互换。它可以比 apply() 更快。详情请见this SO post。
df['col1'] = df['col1'].map(complex_function)
-
applymap() 对于数据帧几乎相同。它不支持 pd.Series 并且它总是会返回一个数据框。但是,它可以更快。 documentation states:“在当前的实现中,applymap 在第一列/行上调用 func 两次,以决定它是否可以采用快速或慢速代码路径。”。但如果性能真的很重要,您应该寻找替代路线。
df['col1'] = df.applymap(complex_function).loc[:, 'col1']
df['col1'] = df.assign(col1=df.col1.apply(complex_function))
附件:如何加快申请速度?
我只在这里提到它,因为它是由其他答案建议的,例如@durjoy。该列表并不详尽:
-
不要使用 apply()。 这不是开玩笑。对于大多数数值运算,pandas 中存在矢量化方法。 if/else 块通常可以使用boolean indexing 和
.loc 的组合来重构。我的示例complex_function 可以通过这种方式重构。
-
重构为 Cython。 如果您有一个复杂的方程并且方程的参数在您的数据框中,这可能是一个好主意。查看the official pandas user guide 了解更多信息。
-
使用
raw=True 参数。理论上,这应该会提高apply() if you are just applying a NumPy reduction function 的性能,因为pd.Series 的开销被移除了。当然,你的函数必须接受一个 ndarray。您必须将您的函数重构为 NumPy。通过这样做,您将获得巨大的性能提升。
-
使用 3rd 方包。您应该尝试的第一件事是 Numba。我不知道@durjoy 提到的swifter;可能还有许多其他软件包值得一提。
-
尝试/失败/重复。 如上所述,map() 和 applymap() 可以更快 - 取决于用例。只需为不同的版本计时并选择最快的。这种方法是最繁琐且性能提升最少的方法。