Python Pandas:如何在不编写辅助函数的情况下使用 apply 广播操作
Posted
技术标签:
【中文标题】Python Pandas:如何在不编写辅助函数的情况下使用 apply 广播操作【英文标题】:Python Pandas: How to broadcast an operation using apply without writing a secondary function 【发布时间】:2012-09-05 18:44:42 【问题描述】:我有一列包含字符串的数据,我想创建一个新列,该列只取相应数据字符串中的前两个字符。
为此使用apply
函数似乎是合乎逻辑的,但它并不像预期的那样工作。它甚至似乎与apply
的其他用途不一致。见下文。
In [205]: dfrm_test = pandas.DataFrame("A":np.repeat("the", 10))
In [206]: dfrm_test
Out[206]:
A
0 the
1 the
2 the
3 the
4 the
5 the
6 the
7 the
8 the
9 the
In [207]: dfrm_test["A"].apply(lambda x: x+" cat")
Out[207]:
0 the cat
1 the cat
2 the cat
3 the cat
4 the cat
5 the cat
6 the cat
7 the cat
8 the cat
9 the cat
Name: A
In [208]: dfrm_test["A"].apply(lambda x: x[0:2])
Out[208]:
0 the
1 the
Name: A
基于此,apply
似乎除了执行与内部调用的 NumPy 等效的操作外,什么也不做。也就是说,apply
似乎与第一个示例中的arr + " cat"
执行相同的操作。如果 NumPy 恰好广播了它,那么它将起作用。如果没有,那就不会了。
但这似乎与文档中apply
的承诺有所不同。以下是 pandas.Series.apply 应该期待的报价:
对 Series 的值调用函数。可以是只需要单个值的 ufunc 或 Python 函数 (link)
它明确表示它可以接受只需要单个值的 Python 函数。而不起作用的功能(lambda x: x[0:2]
)绝对可以满足这一点。它并不是说单个参数必须是一个数组。鉴于像 numpy.sqrt
这样的东西通常用于单个输入(因此不只是数组),期待 Pandas 使用任何此类函数似乎很自然。
有什么方法可以使用我在这里缺少的apply
?
注意:我确实在下面编写了自己的额外函数:
def ix2(arr):
return np.asarray([x[0:2] for x in arr])
我验证了这个版本确实适用于 Pandas apply
。但这是无关紧要的。编写在 Series 对象之上进行外部操作的东西比必须不断编写使用列表推导来有效循环 Series 内容的包装器要容易得多。这不是apply
应该从用户那里抽象出来的具体内容吗?
我使用的是 Pandas 0.7.3 版,它位于工作场所共享网络上,因此无法升级到最新版本。
添加:
我能够确认此行为从 0.7.3 版更改为 0.8.1 版。在 0.8.1 中,它可以在没有 NumPy ufunc 包装器的情况下按预期工作。
我的猜测是,在代码中,有人试图在 try-except 语句中使用 numpy.vectorize
或 numpy.frompyfunc
。也许它不能与我正在使用的特定 lambda 函数一起正常工作,因此在代码的 except
部分中,它默认仅依赖于通用 NumPy 广播。
如果可能的话,最好能从 Pandas 开发人员那里得到一些确认。但与此同时,ufunc 解决方法就足够了。
【问题讨论】:
Pandas 0.8 返回dfrm_test["A"].apply(lambda x: x[0:2])
十次 th
。
您确认这只是 7.2-1 版中的一个错误吗?请注意,我在问题的底部提到我无法摆脱使用此版本。
我不知道,我现在无法检查。如果我在 0.8 上遇到同样的问题,我可以尝试找到解决方案,但如果没有 7.2,我就不能。
更正:我有 0.7.3 版。这是我之前报道的 Enthought 发行版。但是仍然会出现相同的错误。
【参考方案1】:
我能想到的一种解决方法是将 Python 函数转换为 numpy.ufunc
和 numpy.frompyfunc
:
numpy.frompyfunc((lambda x: x[0:2]), 1, 1)
并在apply
中使用它:
In [50]: dfrm_test
Out[50]:
A
0 the
1 the
2 the
3 the
4 the
5 the
6 the
7 the
8 the
9 the
In [51]: dfrm_test["A"].apply(np.frompyfunc((lambda x: x[0:2]), 1, 1))
Out[51]:
0 th
1 th
2 th
3 th
4 th
5 th
6 th
7 th
8 th
9 th
Name: A
In [52]: pandas.version.version
Out[52]: '0.7.3'
In [53]: dfrm_test["A"].apply(lambda x: x[0:2])
Out[53]:
0 the
1 the
Name: A
【讨论】:
【参考方案2】:试试dfrm_test.A.map(lambda x: x[0:2])
【讨论】:
这可行,但我认为它也是一种解决方法,因为它没有解决apply
没有按承诺工作的事实。您能否验证map
是否可以在apply
可以工作的所有相同情况下工作?我也不喜欢从系列的map
到DataFrame 的applymap
的不一致。
我不确定我是否同意这是一种“解决方法”,因为Series.map
是按元素操作的预期方法。 Series.apply
将首先尝试将整个系列传递给输入函数,并且只有在引发异常时才会退回到元素操作。
这与 apply
的文档及其 0.8.1 行为相矛盾,其中它成功执行了我上面示例的元素版本,而版本 0.7.3 似乎使用了你的逻辑描述。由于apply
应该像在 0.8.1 中一样在 0.7.3 中工作(根据文档),这就是为什么我认为这是一种解决方法。 map
很好,但 apply
应该可以。
我在github master上,它不起作用;它可能偶然在 0.8.1 中工作。 apply
旨在让您可以应用 ufunc 并返回索引完整的系列。查看源代码,它尝试调用 func(self) 并将其包装在 try/except 块中,然后在 except 中调用 map_infer。在您的示例中,您提供的函数 can 接受一个系列并返回一个系列,但不执行逐元素操作,因此代码无法知道触发逐元素情况。要明确表示您希望按元素应用输入函数,您必须使用Series.map
。
虽然我同意你的观点,但 apply 的文档字符串在这方面非常不清楚。我们可以改进申请的文档。【参考方案3】:
这适用于 pandas 0.8.1:
In [47]: dfrm_test.A.str[:2]
Out[47]:
0 th
1 th
2 th
3 th
4 th
5 th
6 th
7 th
8 th
9 th
Name: A
【讨论】:
以上是关于Python Pandas:如何在不编写辅助函数的情况下使用 apply 广播操作的主要内容,如果未能解决你的问题,请参考以下文章
Python 3:如何在不保存在磁盘上的情况下将 pandas 数据帧作为 csv 流上传?
如何在不使用外部库(例如 Numpy、Pandas)的情况下读取 CSV 文件?
Python Pandas - 编写新的 CSV 标题行而不读取/重写整个文件