数据科学 IPython 笔记本 7.8 分层索引

2022-06-03 17:21:12 浏览数 (1)

7.8 分层索引

原文:Hierarchical Indexing 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。

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

虽然 Pandas 确实提供了PanelPanel4D对象,这些对象原生地处理三维和四维数据(参见“旁注:面板数据”),实践中的更常见模式是利用分层索引(也称为多重索引),在单个索引中合并多个索引层次。通过这种方式,可以在熟悉的一维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类型为我们提供了我们希望拥有的操作类型。我们可以从元组创建多重索引,如下所示:

代码语言:javascript复制
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重新索引我们的序列,我们会看到数据的分层表示:

代码语言:javascript复制
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

代码语言:javascript复制
pop_df = pop.unstack()
pop_df

2000

2010

California

33871648

37253956

New York

18976457

19378102

Texas

20851820

25145561

当然,stack()方法提供了相反的操作:

代码语言:javascript复制
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中添加另一列一样简单:

代码语言:javascript复制
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 岁以下人口的比例:

代码语言:javascript复制
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构造多重索引的最简单方法,是简单地将两个或多个索引数组的列表传递给构造器。 例如:

代码语言:javascript复制
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

代码语言:javascript复制
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,提供每个层次中的索引值:

代码语言:javascript复制
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

代码语言:javascript复制
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属性来实现:

代码语言:javascript复制
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中,行和列是完全对称的,就像行可以有多个索引层次一样,列也可以有多个层次。考虑以下内容,这是一些(有些现实的)医学数据的模型:

代码语言:javascript复制
# 分层索引和列
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,其中只包含该人的信息:

代码语言:javascript复制
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,持有较低层次的索引:

代码语言:javascript复制
pop['California']

'''
year
2000    33871648
2010    37253956
dtype: int64
'''

MultiIndex是有序的,就可以执行部分切片(参见“排序和未排序索引”中的讨论):

代码语言:javascript复制
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

代码语言: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

请记住,列在DataFrame中是主要的,并且用于多重索引的Series的语法适用于列。例如,我们可以通过简单的操作恢复 Guido 的心率数据:

代码语言:javascript复制
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索引器。例如:

代码语言:javascript复制
health_data.iloc[:2, :2]

subject

Bob

type

HR

year

visit

2013

1

31.0

2

44.0

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

代码语言:javascript复制
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 提供了许多便利的例程来执行这种排序;例如DataFramesort_index()sortlevel()方法。我们这里将使用最简单的sort_index()

代码语言:javascript复制
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

代码语言:javascript复制
pop.unstack(level=1)

year

2000

2010

state

California

33871648

37253956

New York

18976457

19378102

Texas

20851820

25145561

unstack()的反面是stack(),这里可以用来恢复原始序列:

代码语言:javascript复制
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,包含以前在索引中的信息。 为清楚起见,我们可以为列表示指定数据名称:

代码语言:javascript复制
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

代码语言:javascript复制
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关键字,我们可以在列上的层次中取平均值:

代码语言:javascript复制
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索引器,很容易扩展到这些更高维的结构。

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

随着维度数量的增加,对于大多数真实世界的数据集,密集表示可能变得非常低效。然而,对于特定场合下的应用,这些结构可能是有用的。

0 人点赞