选择pandas MultiIndex DataFrame中的行
选择pandas MultiIndex DataFrame中的行
在MultiIndex索引的pandas数据框中,选择/过滤行的最常见方式是什么?
- 基于单个值/标签的切片
- 基于一个或多个级别的多个标签的切片
- 基于布尔条件和表达式的过滤
- 哪些方法适用于什么情况
为简单起见,假设:
- 输入数据框没有重复索引键
- 以下输入数据框只有两个级别。 (这里显示的大多数解决方案可推广到N个级别)
示例输入:
mux = pd.MultiIndex.from_arrays([ list('aaaabbbbbccddddd'), list('tuvwtuvwtuvwtuvw') ], names=['one', 'two']) df = pd.DataFrame({'col': np.arange(len(mux))}, mux) col one two a t 0 u 1 v 2 w 3 b t 4 u 5 v 6 w 7 t 8 c u 9 v 10 d w 11 t 12 u 13 v 14 w 15
问题1:选择单个项目
如何选择具有一级别中的\"a\"的行?
col one two a t 0 u 1 v 2 w 3
此外,如何能在输出中删除一级别?
col two t 0 u 1 v 2 w 3
问题1b
如何切片所有在二级别上为\"value = t\"的行?
col one two a t 0 b t 4 t 8 d t 12
问题2:选择一个级别中的多个值
如何选择级别\"one\"中为\"b\"和\"d\"的项目所在的行?
col one two b t 4 u 5 v 6 w 7 t 8 d w 11 t 12 u 13 v 14 w 15
问题2b
如何获得对应于级别\"two\"中\"value = \'t\'\"和\"value = \'w\'\"的所有值?
col one two a t 0 w 3 b t 4 w 7 t 8 d w 11 t 12 w 15
问题3:切片单个交叉点(x, y)
如何检索交叉点,即从df
检索具有特定索引值的单个行? 特别地,给定(\'c\', \'u\')
的交叉点如何检索?
col one two c u 9
问题4:切片多个交叉部分[(a,b),(c,d),...]
如何选择与(\'c\',\'u\')
和(\'a\',\'w\')
对应的两行?
col one two c u 9 a w 3
问题5:每个级别切片一个项目
如何检索所有与级别 \"one\" 中的 \"a\" 或级别 \"two\" 中的 \"t\" 对应的行?
col one two a t 0 u 1 v 2 w 3 b t 4 t 8 d t 12
问题6:任意切片
如何切片特定的交叉部分?对于 \"a\" 和 \"b\",我想选择所有子级别为 \"u\" 和 \"v\" 的行,对于 \"d\",我想选择子级别为 \"w\" 的行。
col one two a u 1 v 2 b u 5 v 6 d w 11 w 15
问题7将使用数字级别的独特设置:
np.random.seed(0) mux2 = pd.MultiIndex.from_arrays([ list('aaaabbbbbccddddd'), np.random.choice(10, size=16) ], names=['one', 'two']) df2 = pd.DataFrame({'col': np.arange(len(mux2))}, mux2) col one two a 5 0 0 1 3 2 3 3 b 7 4 9 5 3 6 5 7 2 8 c 4 9 7 10 d 6 11 8 12 8 13 1 14 6 15
问题7:过滤多层索引中各级别的数字不平等
如何获取所有级别 \"two\" 中值大于5的行?
col one two b 7 4 9 5 c 7 10 d 6 11 8 12 8 13 6 15
注意:本篇文章不会介绍如何创建多级索引,如何在它们上执行赋值操作或任何性能相关讨论(这些是另一个时候的独立话题)。
最近我遇到了一个使用案例,其中我有一个三级以上的多重索引数据帧,在其中我不能使任何上面的解决方案产生我要寻找的结果。当然,上述解决方案很可能适用于我的使用情况,我尝试了几种方案,但由于我可用的时间,我无法使它们正常工作。
我远非专家,但我发现了一个解决方案,它没有列在上面详细的答案中。我不保证解决方案在任何方面都是最优的。
这是一种获取与上面的问题#6略微不同的结果的不同方法。(可能也适用于其他问题)
具体而言,我要找的是:
- 一种选择一个索引级别中的两个以上值和另一个索引级别中的单个值的方法,以及
- 一种在数据帧输出中保留前一个操作的索引值的方法。
作为一个恶作剧(但是完全可修复):
- 索引没有命名。
在下面的玩具数据帧上:
index = pd.MultiIndex.from_product([['a','b'], ['stock1','stock2','stock3'], ['price','volume','velocity']]) df = pd.DataFrame([1,2,3,4,5,6,7,8,9, 10,11,12,13,14,15,16,17,18], index) 0 a stock1 price 1 volume 2 velocity 3 stock2 price 4 volume 5 velocity 6 stock3 price 7 volume 8 velocity 9 b stock1 price 10 volume 11 velocity 12 stock2 price 13 volume 14 velocity 15 stock3 price 16 volume 17 velocity 18
当然,使用下面的方法是可行的:
df.xs(('stock1', 'velocity'), level=(1,2)) 0 a 3 b 12
但我想要一个不同的结果,所以我获取该结果的方法是:
df.iloc[df.index.isin(['stock1'], level=1) & df.index.isin(['velocity'], level=2)] 0 a stock1 velocity 3 b stock1 velocity 12
如果我想要一个索引级别中的两个或多个值,以及另一个索引级别中的单个(或2+)值:
df.iloc[df.index.isin(['stock1','stock3'], level=1) & df.index.isin(['velocity'], level=2)] 0 a stock1 velocity 3 stock3 velocity 9 b stock1 velocity 12 stock3 velocity 18
上述方法可能有点笨拙,但我发现它满足了我的需求,并且作为奖励更容易理解和阅读。
MultiIndex /高级索引
注意
本文将以下列方式结构化:
- 将针对原始问题逐一回答
- 为每个问题演示一个或多个解决此问题并获得预期结果的方法。
附注(像这个一样)将包括对想学习有关附加功能、实现细节和与手头主题不相关的其他信息感兴趣的读者。这些附注是通过查阅文档和发现各种晦涩功能编制而成的,并基于我自己的(有限)经验。
所有代码示例都是在pandas v0.23.4,python3.7上创建和测试的。如果某些内容不清楚,或事实有误,或者您没有找到适用于您的用例的解决方案,请随时建议修改,在评论中请求澄清或打开新的问题...如适用。
以下是我们将经常重新访问的一些常见习语(以下简称“四个习语”)的介绍
-
DataFrame.loc
- 根据标签进行选择的通用解决方案(+pd.IndexSlice
用于更复杂的涉及切片的应用) -
DataFrame.xs
- 从Series/DataFrame中提取特定的交叉部分。 -
DataFrame.query
- 动态指定切片和/或过滤操作(即通过动态评估的表达式。比某些场景更适用。还请参阅文档中的此部分了解有关在MultiIndexes上进行查询的内容。 ) -
使用由
MultiIndex.get_level_values
生成的掩码进行布尔索引(通常与Index.isin
结合使用,特别是在使用多个值进行过滤时)。这在某些情况下也非常有用。 -
使用
loc
进行切片,可使用df3.loc[:, ....] # Notice how we slice across the index with `:`.
或
df3.loc[:, pd.IndexSlice[...]]
-
要适当使用
xs
,只需传递一个参数axis = 1
。 -
您可以直接使用
df.columns.get_level_values
访问列级值。然后需要进行类似以下的操作:df.loc[:, {condition}]
其中
{condition}
表示使用columns.get_level_values
构建的某些条件。 -
要使用
query
,您唯一的选择是转置,对索引进行查询,然后再次转置:df3.T.query(...).T
不推荐使用,使用其他三个选项之一。
通过四种常用方法来看待切片和过滤问题,可以更好地了解在特定情况下可以应用哪种方法。非常重要的是要理解并不是所有的方法都能在每种情况下同样有效(如果有效)。如果一个方法没有被列为以下问题的潜在解决方案,那就意味着这种方法不能有效地应用于该问题。
问题1
如何选取level "one"中含有"a"的行?
col one two a t 0 u 1 v 2 w 3
你可以使用loc
,作为一个通用的解决方案适用于大多数情况:
df.loc[['a']]
此时,如果你看到:
TypeError: Expected tuple, got str
那么这意味着你正在使用较旧的 pandas 版本,请考虑升级!否则,请使用 df.loc[('a', slice(None)), :]
。
另外,你也可以在这里使用xs
,因为我们正在提取单个交叉部分。请注意 levels
和 axis
参数(这里可以假定采用合理的默认值)。
df.xs('a', level=0, axis=0, drop_level=False) # df.xs('a', drop_level=False)
这里,需要使用 drop_level=False
参数来防止 xs
在结果中删除“one”级别(我们进行切片的级别)。
在这里还有另一种选择,那就是使用query
:
df.query("one == 'a'")
如果索引没有名称,那么您需要更改查询字符串为 "ilevel_0 == 'a'"
。
最后,使用get_level_values
:
df[df.index.get_level_values('one') == 'a'] # If your levels are unnamed, or if you need to select by position (not label), # df[df.index.get_level_values(0) == 'a']
此外,我如何能够在输出中删除“one”级别?
col two t 0 u 1 v 2 w 3
这可以很容易地通过使用以下方法之一来完成:
df.loc['a'] # Notice the single string argument instead the list.
或者,
df.xs('a', level=0, axis=0, drop_level=True) # df.xs('a')
请注意,我们可以省略 drop_level
参数(默认情况下它被认为是True
)。
注意
你可能会注意到,筛选后的 DataFrame 可能仍然具有所有级别,即使也看不到它们。例如:v = df.loc[['a']] print(v) col one two a t 0 u 1 v 2 w 3 print(v.index) MultiIndex(levels=[['a', 'b', 'c', 'd'], ['t', 'u', 'v', 'w']], labels=[[0, 0, 0, 0], [0, 1, 2, 3]], names=['one', 'two'])
你可以使用
MultiIndex.remove_unused_levels
来去除这些级别:v.index = v.index.remove_unused_levels()
print(v.index) MultiIndex(levels=[['a'], ['t', 'u', 'v', 'w']], labels=[[0, 0, 0, 0], [0, 1, 2, 3]], names=['one', 'two'])
问题1b
如何在“two”级别上选取所有值为“t”的行?
col one two a t 0 b t 4 t 8 d t 12
直观地说,你希望使用slice()
来完成:
df.loc[(slice(None), 't'), :]
它可以正常工作!但是很笨重。我们可以使用 pd.IndexSlice
API 来简化一些语法
idx = pd.IndexSlice df.loc[idx[:, 't'], :]
这样写起来就更加简洁了。
注意
为什么需要在列上加上结束的 slice:
这是因为可以使用loc
在两个轴上选择和切片 (axis=0
或axis=1
)。如果不明确地指出在哪个轴上进行切片,操作就会变得模糊。参见在slicing文档中显示的大红色框。
如果您想消除任何模棱两可的情况,可以使用 loc
接受一个 axis
参数:
df.loc(axis=0)[pd.IndexSlice[:, 't']]
如果没有 axis
参数(即仅使用 df.loc[pd.IndexSlice[:, 't']]
),则假定切片是在列上进行的,在此情况下会引发 KeyError
异常。
这在 切片器 中有记录。但对于本文章的目的,我们将明确指定所有轴。
使用 xs
,如下所示:
df.xs('t', axis=0, level=1, drop_level=False)
使用 query
,如下所示:
df.query("two == 't'") # Or, if the first level has no name, # df.query("ilevel_1 == 't'")
最后,可以使用 get_level_values
进行如下操作:
df[df.index.get_level_values('two') == 't'] # Or, to perform selection by position/integer, # df[df.index.get_level_values(1) == 't']
所有操作都会产生相同的效果。
问题2
如何选择级别“one”中对应于项目“b”和“d”的行?
col one two b t 4 u 5 v 6 w 7 t 8 d w 11 t 12 u 13 v 14 w 15
使用 loc,可以通过指定列表来完成类似的操作。
df.loc[['b', 'd']]
要解决上述选择“b”和“d”的问题,还可以使用 query
:
items = ['b', 'd'] df.query("one in @items") # df.query("one == @items", parser='pandas') # df.query("one in ['b', 'd']") # df.query("one == ['b', 'd']", parser='pandas')
注意
是的,默认解析器是'pandas'
,但重要的是要强调这种语法通常不是 Python 的惯常用法。Pandas 解析器从表达式生成略微不同的解析树。这是为了使一些操作更容易指定。有关更多信息,请阅读我的文章
Dynamic Expression Evaluation in pandas using pd.eval()。
使用 get_level_values
+ Index.isin
:
df[df.index.get_level_values("one").isin(['b', 'd'])]
问题2b
如何获取级别“two”中对应于“t”和“w”的所有值?
col one two a t 0 w 3 b t 4 w 7 t 8 d w 11 t 12 w 15
使用 loc
,只能与 pd.IndexSlice
一起使用。
df.loc[pd.IndexSlice[:, ['t', 'w']], :]
在 pd.IndexSlice[:, ['t', 'w']]
中,第一个冒号 :
意味着在第一级上进行切片。随着所查询的级别深度的增加,您将需要指定更多的切片,一个切片对应一个被切片的级别。但您不需要指定超过被切片的级别以外的其他级别。
使用 query
,如下所示:
items = ['t', 'w'] df.query("two in @items") # df.query("two == @items", parser='pandas') # df.query("two in ['t', 'w']") # df.query("two == ['t', 'w']", parser='pandas')
使用 get_level_values
和 Index.isin
(与上述类似):
df[df.index.get_level_values('two').isin(['t', 'w'])]
问题3
如何检索横截面,即从
df
中检索具有特定索引值的单个行?具体而言,如何检索由('c','u')
组成的横截面:col one two c u 9
使用 loc
,通过指定键的元组来实现:
df.loc[('c', 'u'), :]
或者,
df.loc[pd.IndexSlice[('c', 'u')]]
注意
此时,您可能会遇到一个看起来像这样的PerformanceWarning
:PerformanceWarning: indexing past lexsort depth may impact performance.
这只是意味着您的索引未排序。pandas 依赖于索引排序(在此情况下按字典顺序排列,因为我们处理字符串值)以实现最优的搜索和检索。一个快速修复方法是使用
DataFrame.sort_index
在先对 DataFrame 进行排序。这在性能上尤其可取,如果您计划同时进行多个这样的查询:df_sort = df.sort_index() df_sort.loc[('c', 'u')]
您还可以使用
MultiIndex.is_lexsorted()
检查索引是否已排序。此函数相应地返回True
或False
。您可以调用此函数确定是否需要进行附加排序步骤。
对于 xs
,这仍然只需将单个元组作为第一个参数传递,其余参数设置为其适当的默认值:
df.xs(('c', 'u'))
对于 query
,事情变得有些别扭:
df.query("one == 'c' and two == 'u'")
现在你可以看到这将相对难以概括。但对于这个特定问题来说还可以。
对于跨越多个级别的访问,仍然可以使用 get_level_values
,但不建议这样做:
m1 = (df.index.get_level_values('one') == 'c') m2 = (df.index.get_level_values('two') == 'u') df[m1 & m2]
问题 4
如何选择与
('c', 'u')
和('a', 'w')
对应的两行?col one two c u 9 a w 3
对于 loc
,这仍然很简单:
df.loc[[('c', 'u'), ('a', 'w')]] # df.loc[pd.IndexSlice[[('c', 'u'), ('a', 'w')]]]
对于 query
,您将需要通过迭代您的交叉部分和级别来动态生成查询字符串:
cses = [('c', 'u'), ('a', 'w')] levels = ['one', 'two'] # This is a useful check to make in advance. assert all(len(levels) == len(cs) for cs in cses) query = '(' + ') or ('.join([ ' and '.join([f"({l} == {repr(c)})" for l, c in zip(levels, cs)]) for cs in cses ]) + ')' print(query) # ((one == 'c') and (two == 'u')) or ((one == 'a') and (two == 'w')) df.query(query)
100% 不推荐!但是这是可能的。
如果我有多个级别怎么办?
在这种情况下,一个选项是使用droplevel
删除您不检查的级别,然后使用isin
测试成员资格,然后在最终结果上进行布尔索引。
df[df.index.droplevel(unused_level).isin([('c', 'u'), ('a', 'w')])]
问题 5
如何检索属于“一级”中的“a”或“二级”中的“t”的所有行?
col one two a t 0 u 1 v 2 w 3 b t 4 t 8 d t 12
使用loc
选取特定交叉部分并确保正确性并同时保持代码清晰是非常困难的。 df.loc[pd.IndexSlice ['a','t']]
是不正确的,它被解释为df.loc[pd.IndexSlice [( 'a','t')]]
(即选择交叉部分)。 您可以考虑使用pd.concat
分别处理每个标签的解决方案:
pd.concat([ df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:] ]) col one two a t 0 u 1 v 2 w 3 t 0 # Does this look right to you? No, it isn't! b t 4 t 8 d t 12
但是您将注意到,其中一个行被复制了。 这是因为该行同时满足两个切片条件,因此出现了两次。 您需要执行以下操作:
v = pd.concat([ df.loc[['a'],:], df.loc[pd.IndexSlice[:, 't'],:] ]) v[~v.index.duplicated()]
但是,如果您的DataFrame包含重复的索引(您想要的索引),那么它将无法保留它们。 极其小心地使用。
使用query
,这非常简单:
df.query("one == 'a' or two == 't'")
使用get_level_values
,这仍然很简单,但不太优雅:
m1 = (df.index.get_level_values('one') == 'a') m2 = (df.index.get_level_values('two') == 't') df[m1 | m2]
问题6
如何切片特定的交叉部分? 对于“a”和“b”,我想选择所有具有子级“u”和“v”的行,对于“d”,我想选择具有子级“w”的行。
col one two a u 1 v 2 b u 5 v 6 d w 11 w 15
这是我添加的一个特殊案例,以帮助理解四个习语的适用性——这是一个案例,其中没有哪一个习语将有效地起作用,因为切片非常特定,没有遵循任何实际的模式。
通常,这样的切片问题需要显式地将关键字列表传递给loc
。 一种方法是:
keys = [('a', 'u'), ('a', 'v'), ('b', 'u'), ('b', 'v'), ('d', 'w')] df.loc[keys, :]
如果您想节省一些键入,您将会认识到在对“a”,“b”及其子级进行切片时存在一种模式,因此我们可以将切片任务分成两部分并concat
结果:
pd.concat([ df.loc[(('a', 'b'), ('u', 'v')), :], df.loc[('d', 'w'), :] ], axis=0)
用于“a”和“b”的切片规范稍微干净一些(('a','b'),('u','v'))
,因为所索引的同一子级对于每个级别都是相同的。
问题7
如何获取级别“two”中值大于5的所有行?
col one two b 7 4 9 5 c 7 10 d 6 11 8 12 8 13 6 15
这可以使用query
进行操作,
df2.query("two > 5")
还可以使用get_level_values
。
df2[df2.index.get_level_values('two') > 5]
注意
类似于此示例,我们可以基于任何任意条件过滤使用这些结构。一般来说,有用的是记住,loc
和xs
专门用于基于标签的索引,而query
和get_level_values
有助于构建通用的过滤条件掩码。
奖励问题
如果需要切片
MultiIndex
列怎么办?
实际上,这里的大多数解决方案也适用于列,只需进行小幅更改。 考虑:
np.random.seed(0) mux3 = pd.MultiIndex.from_product([ list('ABCD'), list('efgh') ], names=['one','two']) df3 = pd.DataFrame(np.random.choice(10, (3, len(mux))), columns=mux3) print(df3) one A B C D two e f g h e f g h e f g h e f g h 0 5 0 3 3 7 9 3 5 2 4 7 6 8 8 1 6 1 7 7 8 1 5 9 8 9 4 3 0 3 5 0 2 3 2 8 1 3 3 3 7 0 1 9 9 0 4 7 3 2 7
以下是将“四个成语”与列一起使用所需进行的更改。