我有一个带有两列的熊猫数据框。我需要在不影响第二列的情况下更改第一列的值,并在仅更改第一列值的情况下取回整个数据框。我如何在熊猫中使用 apply 来做到这一点?
apply
。而是直接对列进行操作。
apply
。如果您不确定是否需要使用它,则可能不需要。我建议看看 When should I ever want to use pandas apply() in my code?。
给定一个示例数据框 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
map()
优于 apply()
?
df['file_name'] = df['Path'].map(lambda a: os.path.basename(a))
中的路径中提取文件名
给定以下数据帧 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
,则需要其他内容。
二、使用整个数据框的解决方案
或者,您可以按照 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。为了解决这个问题,您需要定义一个以列 x['col1']
作为参数的 (lambda) 函数;即我们将列信息包装在另一个函数中。
不幸的是,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() 更快。有关更多详细信息,请参阅此 SO 帖子。
df['col1'] = df['col1'].map(complex_function)
applymap() 对于数据帧几乎相同。它不支持 pd.Series 并且它总是会返回一个数据框。但是,它可以更快。文档指出:“在当前实现中,applymap 在第一列/行上调用 func 两次,以决定它是否可以采用快速或慢速代码路径。”。但是,如果性能真的很重要,您应该寻找替代路线。
df['col1'] = df.applymap(complex_function).loc[:, 'col1']
assign() 不是 apply() 的可行替代品。它仅在最基本的用例中具有类似的行为。它不适用于 complex_function。您仍然需要 apply() ,如下例所示。 assign() 的主要用例是方法链接,因为它在不更改原始数据帧的情况下返回数据帧。
df['col1'] = df.assign(col1=df.col1.apply(complex_function))
附件:如何加快申请?
我只在这里提到它,因为它是由其他答案建议的,例如@durjoy。该列表并不详尽:
不要使用 apply()。这不是开玩笑。对于大多数数值运算,pandas 中存在矢量化方法。 If/else 块通常可以使用布尔索引和 .loc 的组合来重构。我的示例 complex_function 可以通过这种方式重构。重构为 Cython。如果您有一个复杂的方程并且方程的参数在您的数据框中,这可能是一个好主意。查看官方 pandas 用户指南了解更多信息。使用 raw=True 参数。从理论上讲,如果您只是应用 NumPy 缩减函数,这应该会提高 apply() 的性能,因为 pd.Series 的开销被删除了。当然,你的函数必须接受一个 ndarray。您必须将您的函数重构为 NumPy。通过这样做,您将获得巨大的性能提升。使用第 3 方包。您应该尝试的第一件事是 Numba。我不知道@durjoy 提到的更快;可能还有很多其他的包在这里值得一提。尝试/失败/重复。如上所述,map() 和 applymap() 可以更快 - 取决于用例。只需为不同的版本计时并选择最快的。这种方法是最繁琐且性能提升最少的方法。
col1
更复杂的切片怎么办?如何避免重复切片表达式?比如说:df[:, ~df.columns.isin(skip_cols)]
。在等式两边写两次这似乎是不合规矩的。
你根本不需要函数。您可以直接处理整个列。
示例数据:
>>> 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
df['a'].str.split('/')
产生一个 Series
对象,对吧?那么 df['a'].str.split('/')[0]
不会从该 Series
中生成单个元素吗?我认为您不能将其分配给这样的整个列。
尽管给定的响应是正确的,但它们会修改初始数据帧,这并不总是可取的(并且,鉴于 OP 要求提供“使用 apply
”的示例,他们可能想要一个返回新数据帧的版本,如apply
确实如此)。
这可以使用 assign
:对现有列有效,如文档所述(重点是我的):
将新列分配给 DataFrame。返回一个包含所有原始列以及新列的新对象。重新分配的现有列将被覆盖。
简而言之:
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 内核能够计算结果,因此它将比正常的应用函数快得多。尝试让我知道它是否对您有用。
让我尝试使用日期时间并考虑空值或空格的复杂计算。我在日期时间列上减少 30 年,并使用 apply
方法以及 lambda
并转换日期时间格式。第 if x != '' else x
行将相应地处理所有空格或空值。
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)
apply
绝不应该在这种情况下使用apply
在行上使用比矢量化函数慢得多的内部循环,例如df.a = df.a / 2
(请参阅 Mike Muller 的回答)。