7.8 分层索引

原文:Hierarchical Indexing

译者:飞龙

协议:CC BY-NC-SA 4.0

本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。

到目前为止,我们主要关注一维和二维数据,分别存储在 Pandas SeriesDataFrame对象中。通常,超出此范围并存储更高维度的数据(即由多于一个或两个键索引的数据)是有用的。

虽然 Pandas 确实提供了PanelPanel4D对象,这些对象原生地处理三维和四维数据(参见“旁注:面板数据”),实践中的更常见模式是利用分层索引(也称为多重索引),在单个索引中合并多个索引层次。通过这种方式,可以在熟悉的一维Series和二维DataFrame对象中,紧凑地表示高维数据。

在本节中,我们将探索MultiIndex对象的直接创建,在对多重索引数据执行索引,切片和计算统计数据时的注意事项,以及在数据的简单和分层索引表示之间进行转换的有用例程。

我们以标准导入开始:

import pandas as pd
import numpy as np

多重索引的序列

让我们首先考虑如何在一维Series中表示二维数据。具体而言,我们将考虑数据序列,其中每个点都有一个字符和数字键。

不好的方式

假设你想跟踪两个不同年份的州的数据。使用我们已经介绍过的 Pandas 工具,你可能只想使用 Python 元组作为键:

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
'''

使用此索引方案,你可以根据这个多重索引,直接对序列索引或切片:

pop[('California', 2010):('Texas', 2000)]

'''
(California, 2010)    37253956
(New York, 2000)      18976457
(New York, 2010)      19378102
(Texas, 2000)         20851820
dtype: int64
'''

但也就这样了。 例如,如果你需要选择 2010 年的所有值,则需要进行一些混乱(并且可能很慢)的调整来实现它:

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 切片表示法:

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
'''

看到这个,你可能想知道为什么我们会纠结于分层索引。原因很简单:就像我们能够使用多重索引,在一维序列中表示二维数据一样,我们也可以用它在SeriesDataFrame中表示更多维的数据。

多重索引中的每个额外层次表示数据的额外维度;利用这个属性,我们可以更灵活地处理我们可以表示的数据类型。具体而言,我们可能希望,每年为每个州添加另一列人口统计数据(例如,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的创建方法

SeriesDataFrame构造多重索引的最简单方法,是简单地将两个或多个索引数组的列表传递给构造器。 例如:

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]])
'''

你可以从元组列表构造它,提供每个点的多重索引值:

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]])
'''

你甚至可以从单个索引的笛卡尔积中构造它:

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]])
'''

当创建SeriesDataframe时,这些对象中的任何一个都可以作为index参数传递,或者传递给现有SeriesDataFramereindex方法。

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 Temp HR Temp
year visit
2013 1 31.0 38.7 32.0 36.7 35.0 37.2
2 44.0 37.7 50.0 35.0 29.0 36.7
2014 1 30.0 37.4 39.0 37.8 61.0 36.9
2 47.0 37.8 48.0 37.3 51.0 36.5

在这里,我们看到行和列的多重索引可以非常方便。这基本上是四维数据,其中维度是受试者,测量类型,年份和就诊次数。有了这个,我们就可以通过人名来索引顶级列,并得到一个完整的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

多重索引的序列

考虑一下我们之前看到的,州人口的多重索引的序列:

pop

'''
state       year
California  2000    33871648
            2010    37253956
New York    2000    18976457
            2010    19378102
Texas       2000    20851820
            2010    25145561
dtype: int64
'''

我们可以通过索引多项来访问单个元素:

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
'''

对于有序索引,可以通过在第一个索引中传递空切片,来在较低层次上执行部分索引:

pop[:, 2000]

'''
state
California    33871648
New York      18976457
Texas         20851820
dtype: int64
'''

其他类型的索引和选择(在“数据索引和选择”中讨论)也可以使用;例如,基于布尔掩码的选择:

pop[pop > 22000000]

'''
state       year
California  2000    33871648
            2010    37253956
Texas       2010    25145561
dtype: int64
'''

基于花式索引的选择也有效:

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 Temp HR Temp
year visit
2013 1 31.0 38.7 32.0 36.7 35.0 37.2
2 44.0 37.7 50.0 35.0 29.0 36.7
2014 1 30.0 37.4 39.0 37.8 61.0 36.9
2 47.0 37.8 48.0 37.3 51.0 36.5

请记住,列在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
'''

此外,与单值索引情况一样,我们可以使用“数据索引和选择”中介绍的locilocix索引器。例如:

health_data.iloc[:2, :2]
subject Bob
type HR Temp
year visit
2013 1 31.0 38.7
2 44.0 37.7

这些索引器提供了底层二维数据的数组式的视图,但是可以向lociloc中的每个索引器,传递多个索引的元组。例如:

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
'''

在这些索引元组中使用切片并不是特别方便;尝试在元组中创建切片将导致语法错误:

health_data.loc[(:, 1), (:, 'HR')]

'''
  File "<ipython-input-32-8e3cc151e316>", line 1
    health_data.loc[(:, 1), (:, 'HR')]
                     ^
SyntaxError: invalid syntax
'''

你可以通过使用 Python 的内置slice()函数,显式构建所需的切片,来解决这个问题,但在这种情况下,更好的方法是使用IndexSlice对象,正是由 Pandas 为这种情况提供的。

例如:

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切片操作将失败。在这里我们来看看。

我们首先创建一些简单的多重索引数据,其中索引不是按字母顺序排序:

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
'''

如果我们尝试对此索引进行部分切片,则会导致错误:

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 提供了许多便利的例程来执行这种排序;例如DataFramesort_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
'''

通过以这种方式排序索引,部分切片将按预期工作:

data['a':'b']

'''
char  int
a     1      0.003001
      2      0.164974
b     1      0.001693
      2      0.526226
dtype: float64
'''

索引堆叠和解除堆叠

正如我们之前简要介绍的那样,可以将数据集从堆叠的多索引转换为简单的二维表示,可选择指定要使用的层次:

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方法完成。 在人口字典上调用它将产生一个带有stateyear列的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会有用。这可以通过DataFrameset_index方法完成,它返回一个多重索引的DataFrame

pop_flat.set_index(['state', 'year'])
population
state year
California 2000 33871648
2010 37253956
New York 2000 18976457
2010 19378102
Texas 2000 20851820
2010 25145561

在实践中,遇到真实数据集时,我发现这种类型的重索引,是更有用的模式之一。

多重索引上的数据聚合

我们以前看到,Pandas 有内置的数据聚合方法,比如mean()``,sum()max()。对于分层索引数据,可以传递level``参数,该参数控制聚合在上面计算的数据子集。

例如,让我们回到我们的健康数据:

health_data
subject Bob Guido Sue
type HR Temp HR Temp HR Temp
year visit
2013 1 31.0 38.7 32.0 36.7 35.0 37.2
2 44.0 37.7 50.0 35.0 29.0 36.7
2014 1 30.0 37.4 39.0 37.8 61.0 36.9
2 47.0 37.8 48.0 37.3 51.0 36.5

也许我们想对每年的两次就诊的测量值求平均。 我们可以通过指定我们想要探索的索引层次来实现,在这种情况下是年份:

data_mean = health_data.mean(level='year')
data_mean
subject Bob Guido Sue
type HR Temp HR Temp HR Temp
year
2013 37.5 38.2 41.0 35.85 32.0 36.95
2014 38.5 37.6 43.5 37.55 56.0 36.70

通过进一步利用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.Panelpd.Panel4D对象。这些可以分别认为是(一维)Series和(二维)DataFrame结构的三维和四维扩展。

一旦熟悉了SeriesDataFrame中的数据索引和操作,PanelPanel4D就相对简单易用了。特别是,“数据索引和选择”中讨论的ixlociloc索引器,很容易扩展到这些更高维的结构。

我们将不会在本文中进一步介绍这些面板结构,因为我在大多数情况下发现,对于更高维数据来说,多重索引是更有用且概念上更简单的表示。另外,面板数据基本上是密集数据表示,而多索引基本上是稀疏数据表示。

随着维度数量的增加,对于大多数真实世界的数据集,密集表示可能变得非常低效。然而,对于特定场合下的应用,这些结构可能是有用的。如果你想了解PanelPanel4D结构的更多信息,请参考“更多资源”中列出的参考资料。

results matching ""

    No results matching ""