7.8 分层索引
原文:Hierarchical Indexing 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。
到目前为止,我们主要关注一维和二维数据,分别存储在 Pandas Series
和DataFrame
对象中。通常,超出此范围并存储更高维度的数据(即由多于一个或两个键索引的数据)是有用的。
虽然 Pandas 确实提供了Panel
和Panel4D
对象,这些对象原生地处理三维和四维数据(参见“旁注:面板数据”),实践中的更常见模式是利用分层索引(也称为多重索引),在单个索引中合并多个索引层次。通过这种方式,可以在熟悉的一维Series
和二维DataFrame
对象中,紧凑地表示高维数据。
在本节中,我们将探索MultiIndex
对象的直接创建,在对多重索引数据执行索引,切片和计算统计数据时的注意事项,以及在数据的简单和分层索引表示之间进行转换的有用例程。
我们以标准导入开始:
代码语言:javascript复制import pandas as pd
import numpy as np
多重索引的序列
让我们首先考虑如何在一维Series
中表示二维数据。具体而言,我们将考虑数据序列,其中每个点都有一个字符和数字键。
不好的方式
假设你想跟踪两个不同年份的州的数据。使用我们已经介绍过的 Pandas 工具,你可能只想使用 Python 元组作为键:
代码语言:javascript复制index = [('California', 2000), ('California', 2010),
('New York', 2000), ('New York', 2010),
('Texas', 2000), ('Texas', 2010)]
populations = [33871648, 37253956,
18976457, 19378102,
20851820, 25145561]
pop = pd.Series(populations, index=index)
pop
'''
(California, 2000) 33871648
(California, 2010) 37253956
(New York, 2000) 18976457
(New York, 2010) 19378102
(Texas, 2000) 20851820
(Texas, 2010) 25145561
dtype: int64
'''
使用此索引方案,你可以根据这个多重索引,直接对序列索引或切片:
代码语言:javascript复制pop[('California', 2010):('Texas', 2000)]
'''
(California, 2010) 37253956
(New York, 2000) 18976457
(New York, 2010) 19378102
(Texas, 2000) 20851820
dtype: int64
'''
但也就这样了。 例如,如果你需要选择 2010 年的所有值,则需要进行一些混乱(并且可能很慢)的调整来实现它:
代码语言:javascript复制pop[[i for i in pop.index if i[1] == 2010]]
'''
(California, 2010) 37253956
(New York, 2010) 19378102
(Texas, 2010) 25145561
dtype: int64
'''
这会产生所需的结果,但不像我们所喜欢的 Pandas 中的切片语法那样干净(或对大型数据集有效)。
更好的方式:Pandas MultiIndex
幸运的是,Pandas 提供了一种更好的方式。
我们的基于元组的索引,本质上是一个基本的多重索引,而 Pandas 的MultiIndex
类型为我们提供了我们希望拥有的操作类型。我们可以从元组创建多重索引,如下所示:
index = pd.MultiIndex.from_tuples(index)
index
'''
MultiIndex(levels=[['California', 'New York', 'Texas'], [2000, 2010]],
labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]])
'''
请注意,MultiIndex
包含索引的多个层次 - 在这种情况下,州名称和年份,以及编码这些层次的,每个数据点的多个标签。
如果我们用这个MultiIndex
重新索引我们的序列,我们会看到数据的分层表示:
pop = pop.reindex(index)
pop
'''
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
dtype: int64
'''
这里Series
表示的前两列展示了多个索引值,而第三列展示数据。请注意,第一列中缺少某些条目:在多重索引表示中,任何空白条目都表示与其上方的行相同的值。
现在来访问第二个索引是 2010 的所有数据,我们可以简单地使用 Pandas 切片表示法:
代码语言:javascript复制pop[:, 2010]
'''
California 37253956
New York 19378102
Texas 25145561
dtype: int64
'''
结果是一个单值索引的数组,只带有我们感兴趣的键。与我们开始使用的自制的基于元组的多重索引解决方案相比,这种语法更方便(并且操作更加高效!)。我们现在将进一步讨论分层索引数据上的这种索引操作。
作为额外维度的MultiIndex
你可能会注意到其他内容:我们可以使用带有索引和列标签的简单DataFrame
,来轻松存储相同的数据。事实上,Pandas 的构建具有这种等价关系。 unstack()
方法会快速将多重索引的Series
转换为常规索引的DataFrame
:
pop_df = pop.unstack()
pop_df
2000 | 2010 | |
---|---|---|
California | 33871648 | 37253956 |
New York | 18976457 | 19378102 |
Texas | 20851820 | 25145561 |
当然,stack()
方法提供了相反的操作:
pop_df.stack()
'''
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
dtype: int64
'''
看到这个,你可能想知道为什么我们会纠结于分层索引。原因很简单:就像我们能够使用多重索引,在一维序列中表示二维数据一样,我们也可以用它在Series
或DataFrame
中表示更多维的数据。
多重索引中的每个额外层次表示数据的额外维度;利用这个属性,我们可以更灵活地处理我们可以表示的数据类型。具体而言,我们可能希望,每年为每个州添加另一列人口统计数据(例如,18 岁以下的人口); 使用MultiIndex
就像在DataFrame
中添加另一列一样简单:
pop_df = pd.DataFrame({'total': pop,
'under18': [9267089, 9284094,
4687374, 4318033,
5906301, 6879014]})
pop_df
total | under18 | ||
---|---|---|---|
California | 2000 | 33871648 | 9267089 |
2010 | 37253956 | 9284094 | |
New York | 2000 | 18976457 | 4687374 |
2010 | 19378102 | 4318033 | |
Texas | 2000 | 20851820 | 5906301 |
2010 | 25145561 | 6879014 |
此外,“Pandas 中的数据操作”中讨论的所有ufunc
和其他功能也适用于分层索引。根据以上数据,我们在这里按年计算 18 岁以下人口的比例:
f_u18 = pop_df['under18'] / pop_df['total']
f_u18.unstack()
2000 | 2010 | |
---|---|---|
California | 0.273594 | 0.249211 |
New York | 0.247010 | 0.222831 |
Texas | 0.283251 | 0.273568 |
这使我们能够轻松快速地操作和探索高维数据。
MultiIndex
的创建方法
为Series
或DataFrame
构造多重索引的最简单方法,是简单地将两个或多个索引数组的列表传递给构造器。 例如:
df = pd.DataFrame(np.random.rand(4, 2),
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=['data1', 'data2'])
df
data1 | data2 | ||
---|---|---|---|
a | 1 | 0.554233 | 0.356072 |
2 | 0.925244 | 0.219474 | |
b | 1 | 0.441759 | 0.610054 |
2 | 0.171495 | 0.886688 |
创建MultiIndex
的工作是在后台完成的。
类似地,如果你传递一个带有适当元组作为键的字典,Pandas 会自动识别它并默认使用MultiIndex
:
data = {('California', 2000): 33871648,
('California', 2010): 37253956,
('Texas', 2000): 20851820,
('Texas', 2010): 25145561,
('New York', 2000): 18976457,
('New York', 2010): 19378102}
pd.Series(data)
'''
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
dtype: int64
'''
然而,显式创建一个MultiIndex
有时很有用;我们在这里会看到其中的几种方法。
MultiIndex
显式构造器
为了更灵活地构造索引,你可以使用pd.MultiIndex
中提供的类方法构造器。例如,正如我们之前所做的那样,你可以从一个简单的数组列表中构造MultiIndex
,提供每个层次中的索引值:
pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
'''
MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
'''
你可以从元组列表构造它,提供每个点的多重索引值:
代码语言:javascript复制pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
'''
MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
'''
你甚至可以从单个索引的笛卡尔积中构造它:
代码语言:javascript复制pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
'''
MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
'''
类似地,你可以通过传递levels
(列表的列表,包含每个级别的可用索引值)和labels
(引用这些标签的列表的列表),直接使用其内部编码构造MultiIndex
:
pd.MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
'''
MultiIndex(levels=[['a', 'b'], [1, 2]],
labels=[[0, 0, 1, 1], [0, 1, 0, 1]])
'''
当创建Series
或Dataframe
时,这些对象中的任何一个都可以作为index
参数传递,或者传递给现有Series
或DataFrame
的reindex
方法。
MultiIndex
层次名称
有时命名MultiIndex
的层次很方便。
这可以通过将names
参数传递给上述任何一个MultiIndex
构造器,或者通过在事后设置索引的names
属性来实现:
pop.index.names = ['state', 'year']
pop
'''
state year
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
dtype: int64
'''
在更复杂数据集的情况下,这可能是跟踪各种索引值含义的有用方法。
列的MultiIndex
在DataFrame
中,行和列是完全对称的,就像行可以有多个索引层次一样,列也可以有多个层次。考虑以下内容,这是一些(有些现实的)医学数据的模型:
# 分层索引和列
index = pd.MultiIndex.from_product([[2013, 2014], [1, 2]],
names=['year', 'visit'])
columns = pd.MultiIndex.from_product([['Bob', 'Guido', 'Sue'], ['HR', 'Temp']],
names=['subject', 'type'])
# 生成一些数据
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data = 37
# 创建数据帧
health_data = pd.DataFrame(data, index=index, columns=columns)
health_data
subject | Bob | Guido | Sue | |
---|---|---|---|---|
type | HR | Temp | HR | |
year | visit | |||
2013 | 1 | 31.0 | 38.7 | 32.0 |
2 | 44.0 | 37.7 | 50.0 | |
2014 | 1 | 30.0 | 37.4 | 39.0 |
2 | 47.0 | 37.8 | 48.0 |
在这里,我们看到行和列的多重索引可以非常方便。这基本上是四维数据,其中维度是受试者,测量类型,年份和就诊次数。有了这个,我们就可以通过人名来索引顶级列,并得到一个完整的DataFrame
,其中只包含该人的信息:
health_data['Guido']
type | HR | Temp | |
---|---|---|---|
year | visit | ||
2013 | 1 | 32.0 | 36.7 |
2 | 50.0 | 35.0 | |
2014 | 1 | 39.0 | 37.8 |
2 | 48.0 | 37.3 |
对于一些复杂记录,它包含多个标记的测量值,并多次跨越许多受试者(人,国家,城市等),使用分层的行和列非常方便!
MultiIndex
的索引和切片
MultiIndex
上的索引和切片设计得很直观,如果你将索引视为添加的维度,它会有所帮助。我们首先看一下多重索引的Series
的索引,然后是多重索引的DataFrame
。
多重索引的序列
考虑一下我们之前看到的,州人口的多重索引的序列:
代码语言:javascript复制pop
'''
state year
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
dtype: int64
'''
我们可以通过索引多项来访问单个元素:
代码语言:javascript复制pop['California', 2000]
# 33871648
MultiIndex
也支持部分索引,或仅索引索引中的一个层次。结果是另一个Series
,持有较低层次的索引:
pop['California']
'''
year
2000 33871648
2010 37253956
dtype: int64
'''
MultiIndex
是有序的,就可以执行部分切片(参见“排序和未排序索引”中的讨论):
pop.loc['California':'New York']
'''
state year
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
dtype: int64
'''
对于有序索引,可以通过在第一个索引中传递空切片,来在较低层次上执行部分索引:
代码语言:javascript复制pop[:, 2000]
'''
state
California 33871648
New York 18976457
Texas 20851820
dtype: int64
'''
其他类型的索引和选择(在“数据索引和选择”中讨论)也可以使用;例如,基于布尔掩码的选择:
代码语言:javascript复制pop[pop > 22000000]
'''
state year
California 2000 33871648
2010 37253956
Texas 2010 25145561
dtype: int64
'''
基于花式索引的选择也有效:
代码语言:javascript复制pop[['California', 'Texas']]
'''
state year
California 2000 33871648
2010 37253956
Texas 2000 20851820
2010 25145561
dtype: int64
'''
多重索引数据帧
多重索引的DataFrame
的表现类似。考虑我们之前的玩具医疗DataFrame
:
health_data
subject | Bob | Guido | Sue | |
---|---|---|---|---|
type | HR | Temp | HR | |
year | visit | |||
2013 | 1 | 31.0 | 38.7 | 32.0 |
2 | 44.0 | 37.7 | 50.0 | |
2014 | 1 | 30.0 | 37.4 | 39.0 |
2 | 47.0 | 37.8 | 48.0 |
请记住,列在DataFrame
中是主要的,并且用于多重索引的Series
的语法适用于列。例如,我们可以通过简单的操作恢复 Guido 的心率数据:
health_data['Guido', 'HR']
'''
year visit
2013 1 32.0
2 50.0
2014 1 39.0
2 48.0
Name: (Guido, HR), dtype: float64
'''
此外,与单值索引情况一样,我们可以使用“数据索引和选择”中介绍的loc
,iloc
和ix
索引器。例如:
health_data.iloc[:2, :2]
subject | Bob | |
---|---|---|
type | HR | |
year | visit | |
2013 | 1 | 31.0 |
2 | 44.0 |
这些索引器提供了底层二维数据的数组式的视图,但是可以向loc
或iloc
中的每个索引器,传递多个索引的元组。例如:
health_data.loc[:, ('Bob', 'HR')]
'''
year visit
2013 1 31.0
2 44.0
2014 1 30.0
2 47.0
Name: (Bob, HR), dtype: float64
'''
在这些索引元组中使用切片并不是特别方便;尝试在元组中创建切片将导致语法错误:
代码语言:javascript复制health_data.loc[(:, 1), (:, 'HR')]
'''
File "<ipython-input-32-8e3cc151e316>", line 1
health_data.loc[(:, 1), (:, 'HR')]
^
SyntaxError: invalid syntax
'''
你可以通过使用 Python 的内置slice()
函数,显式构建所需的切片,来解决这个问题,但在这种情况下,更好的方法是使用IndexSlice
对象,正是由 Pandas 为这种情况提供的。
例如:
代码语言:javascript复制idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, 'HR']]
subject | Bob | Guido | Sue | |
---|---|---|---|---|
type | HR | HR | HR | |
year | visit | |||
2013 | 1 | 31.0 | 32.0 | 35.0 |
2014 | 1 | 30.0 | 39.0 | 61.0 |
有很多方法可以在多重索引的Series
和``DataFrame`中与数据进行交互,就像本书中的许多工具一样,熟悉它们的最好方法就是尝试它们!
重排多重索引
处理多重索引数据的关键之一,是知道如何有效地转换数据。有许多操作将保留数据集中的所有信息,但为了各种计算的目的重新排列它。
我们在stack()
和unstack()
方法中看到了一个简短的例子,但是还有很多方法,可以精确控制分层索引和列之间的数据重排,在这里我们将探索他们。
排序和未排序索引
早些时候,我们简要地提到了一个警告,但我们应该在这里强调一下。如果索引未排序,多数MultiIndex
切片操作将失败。在这里我们来看看。
我们首先创建一些简单的多重索引数据,其中索引不是按字母顺序排序:
代码语言:javascript复制index = pd.MultiIndex.from_product([['a', 'c', 'b'], [1, 2]])
data = pd.Series(np.random.rand(6), index=index)
data.index.names = ['char', 'int']
data
'''
char int
a 1 0.003001
2 0.164974
c 1 0.741650
2 0.569264
b 1 0.001693
2 0.526226
dtype: float64
'''
如果我们尝试对此索引进行部分切片,则会导致错误:
代码语言:javascript复制try:
data['a':'b']
except KeyError as e:
print(type(e))
print(e)
'''
<class 'KeyError'>
'Key length (1) was greater than MultiIndex lexsort depth (0)'
'''
虽然从错误消息中并不完全清楚,但这是MultiIndex
未排序的结果。由于各种原因,部分切片和其他类似操作要求MultiIndex
中的层次是(按字母顺序)排序的。
Pandas 提供了许多便利的例程来执行这种排序;例如DataFrame
的sort_index()
和sortlevel()
方法。我们这里将使用最简单的sort_index()
:
data = data.sort_index()
data
'''
char int
a 1 0.003001
2 0.164974
b 1 0.001693
2 0.526226
c 1 0.741650
2 0.569264
dtype: float64
'''
通过以这种方式排序索引,部分切片将按预期工作:
代码语言:javascript复制data['a':'b']
'''
char int
a 1 0.003001
2 0.164974
b 1 0.001693
2 0.526226
dtype: float64
'''
索引堆叠和解除堆叠
正如我们之前简要介绍的那样,可以将数据集从堆叠的多索引转换为简单的二维表示,可选择指定要使用的层次:
代码语言:javascript复制pop.unstack(level=0)
state | California | New York | Texas |
---|---|---|---|
year | |||
2000 | 33871648 | 18976457 | 20851820 |
2010 | 37253956 | 19378102 | 25145561 |
pop.unstack(level=1)
year | 2000 | 2010 |
---|---|---|
state | ||
California | 33871648 | 37253956 |
New York | 18976457 | 19378102 |
Texas | 20851820 | 25145561 |
unstack()
的反面是stack()
,这里可以用来恢复原始序列:
pop.unstack().stack()
'''
state year
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561
dtype: int64
'''
索引设置和重设
重排分层数据的另一种方法是将索引标签转换为列;这可以通过reset_index
方法完成。
在人口字典上调用它将产生一个带有state
和year
列的DataFrame
,包含以前在索引中的信息。
为清楚起见,我们可以为列表示指定数据名称:
pop_flat = pop.reset_index(name='population')
pop_flat
state | year | population | |
---|---|---|---|
0 | California | 2000 | 33871648 |
1 | California | 2010 | 37253956 |
2 | New York | 2000 | 18976457 |
3 | New York | 2010 | 19378102 |
4 | Texas | 2000 | 20851820 |
5 | Texas | 2010 | 25145561 |
通常处理现实世界中的数据时,原始输入数据看起来像这样,从列值构建MultiIndex
会有用。这可以通过DataFrame
的set_index
方法完成,它返回一个多重索引的DataFrame
:
pop_flat.set_index(['state', 'year'])
population | ||
---|---|---|
state | year | |
California | 2000 | |
2010 | 37253956 | |
New York | 2000 | |
2010 | 19378102 | |
Texas | 2000 | |
2010 | 25145561 |
在实践中,遇到真实数据集时,我发现这种类型的重索引,是更有用的模式之一。
多重索引上的数据聚合
我们以前看到,Pandas 有内置的数据聚合方法,比如mean()``,
sum()和
max()。对于分层索引数据,可以传递
level``参数,该参数控制聚合在上面计算的数据子集。
例如,让我们回到我们的健康数据:
代码语言:javascript复制health_data
subject | Bob | Guido | Sue | |
---|---|---|---|---|
type | HR | Temp | HR | |
year | visit | |||
2013 | 1 | 31.0 | 38.7 | 32.0 |
2 | 44.0 | 37.7 | 50.0 | |
2014 | 1 | 30.0 | 37.4 | 39.0 |
2 | 47.0 | 37.8 | 48.0 |
也许我们想对每年的两次就诊的测量值求平均。 我们可以通过指定我们想要探索的索引层次来实现,在这种情况下是年份:
代码语言:javascript复制data_mean = health_data.mean(level='year')
data_mean
subject | Bob | Guido | Sue |
---|---|---|---|
type | HR | Temp | HR |
year | |||
2013 | 37.5 | 38.2 | 41.0 |
2014 | 38.5 | 37.6 | 43.5 |
通过进一步利用axis
关键字,我们可以在列上的层次中取平均值:
data_mean.mean(axis=1, level='type')
type | HR | Temp |
---|---|---|
year | ||
2013 | 36.833333 | 37.000000 |
2014 | 46.000000 | 37.283333 |
因此,在两行中,我们已经能够查询每年所有就诊和所有受试者的平均心率和体温。这个语法实际上是GroupBy
函数的简写,我们将在“聚合和分组”中讨论。虽然这是一个玩具示例,但许多真实世界的数据集具有相似的层次结构。
旁注:面板数据
Pandas 还有一些我们尚未讨论的基本数据结构,即pd.Panel
和pd.Panel4D
对象。这些可以分别认为是(一维)Series
和(二维)DataFrame
结构的三维和四维扩展。
一旦熟悉了Series
和DataFrame
中的数据索引和操作,Panel
和Panel4D
就相对简单易用了。特别是,“数据索引和选择”中讨论的ix
,loc
和iloc
索引器,很容易扩展到这些更高维的结构。
我们将不会在本文中进一步介绍这些面板结构,因为我在大多数情况下发现,对于更高维数据来说,多重索引是更有用且概念上更简单的表示。另外,面板数据基本上是密集数据表示,而多索引基本上是稀疏数据表示。
随着维度数量的增加,对于大多数真实世界的数据集,密集表示可能变得非常低效。然而,对于特定场合下的应用,这些结构可能是有用的。