对MultiIndex的pandas.DataFrame列应用函数
我有一个多重索引的 pandas 数据框(DataFrame),我想对其中一列应用一个函数,然后把结果放回那一列。
In [1]:
import numpy as np
import pandas as pd
cols = ['One', 'Two', 'Three', 'Four', 'Five']
df = pd.DataFrame(np.array(list('ABCDEFGHIJKLMNO'), dtype='object').reshape(3,5), index = list('ABC'), columns=cols)
df.to_hdf('/tmp/test.h5', 'df')
df = pd.read_hdf('/tmp/test.h5', 'df')
df
Out[1]:
One Two Three Four Five
A A B C D E
B F G H I J
C K L M N O
3 rows × 5 columns
In [2]:
df.columns = pd.MultiIndex.from_arrays([list('UUULL'), ['One', 'Two', 'Three', 'Four', 'Five']])
df['L']['Five'] = df['L']['Five'].apply(lambda x: x.lower())
df
-c:2: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
Out[2]:
U L
One Two Three Four Five
A A B C D E
B F G H I J
C K L M N O
3 rows × 5 columns
In [3]:
df.columns = ['One', 'Two', 'Three', 'Four', 'Five']
df
Out[3]:
One Two Three Four Five
A A B C D E
B F G H I J
C K L M N O
3 rows × 5 columns
In [4]:
df['Five'] = df['Five'].apply(lambda x: x.upper())
df
Out[4]:
One Two Three Four Five
A A B C D E
B F G H I J
C K L M N O
3 rows × 5 columns
如你所见,这个函数并没有成功应用到那一列,我猜是因为我收到了这个警告:
-c:2: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
奇怪的是,这个错误有时候会出现,有时候又不会,我一直搞不清楚到底是什么原因。
我通过使用 .loc
来切片数据框,成功地应用了这个函数,正如警告所建议的那样:
In [5]:
df.columns = pd.MultiIndex.from_arrays([list('UUULL'), ['One', 'Two', 'Three', 'Four', 'Five']])
df.loc[:,('L','Five')] = df.loc[:,('L','Five')].apply(lambda x: x.lower())
df
Out[5]:
U L
One Two Three Four Five
A A B C D e
B F G H I j
C K L M N o
3 rows × 5 columns
但我想搞明白,为什么在使用类似字典的切片(比如 df['L']['Five']
)时会出现这种情况,而使用 .loc
切片时就不会。
注意:这个数据框是从一个不是多重索引的 HDF 文件中来的,这可能是导致奇怪行为的原因吗?
编辑:我使用的是 Pandas v.0.13.1
和 NumPy v.1.8.0
1 个回答
df['L']['Five']
这个操作是先选择数据框中第0级的值为'L'的部分,然后再选择其中的'Five'这一列,最后返回的是一个系列。
在数据框中,__getitem__
这个访问器(就是用 []
这个符号)会尽量做正确的选择,给你返回正确的列。不过,这种方式叫做链式索引,可以看看这里。
如果你想访问多重索引,可以用元组的方式,比如 ('a','b')
和 .loc
,这样就不会产生歧义,比如 df.loc[:,('a','b')]
。而且,这样可以同时对多维度进行索引(比如同时选择行和列)。
那么,为什么链式索引和赋值操作不管用,比如 df['L']['Five'] = value
这样?
df['L']
返回的是一个单层索引的数据框。接着,另一个操作 df_with_L['Five']
选择了'Five'这一列。为了说明这一点,我用了另一个变量。因为 pandas 会把这些操作看作是独立的事件(比如分别调用 __getitem__
),所以它必须把这些操作当作线性操作,一个接一个地执行。
对比一下 df.loc[:,('L','Five')]
,这个操作是把一个嵌套的元组 (:,('L','Five'))
传给 __getitem__
的一次调用。这样 pandas 就可以把它当作一个整体来处理(顺便说一下,这样会快很多,因为它可以直接索引到数据框中)。
这有什么重要性呢?因为链式索引是两次调用,所以可能会有其中一次返回的是数据的副本,这和切片的方式有关。因此,当你进行赋值时,实际上是在设置一个副本,而不是原始的数据框。pandas 无法判断这一点,因为这两次操作是独立的。
SettingWithCopy
的警告是一种“启发式”方法,用来检测这种情况(意思是它能捕捉到大多数情况,但只是一个轻量级的检查)。真正搞清楚这一点是非常复杂的。
.loc
操作是一次完整的 python 操作,因此可以选择一个切片(虽然这仍然可能是副本),但允许 pandas 在修改后将这个切片重新赋值回数据框,从而按照你的想法设置值。
警告的原因就是这样。有时候,当你切片一个数组时,你会得到一个视图,这样你就可以随意设置了。然而,即使是一个单一的数据类型数组,在特定的切片方式下也可能生成一个副本。一个多数据类型的数据框(比如同时包含浮点数和对象数据)几乎总是会返回一个副本。是否创建视图取决于数组的内存布局。
注意:这与数据的来源无关。