5.层次化索引

到目前为止我们关注的是保存在Pandas Series和DataFrame中的一维和二维数据。
通常超越二维的数据,即用两个以上键值索引的数据也是很有用的。
尽管Pandas提供了“Panel”和“Panel4D”对象来本地处理3维和4维数据,实践中更常用的方式是在单个索引中使用层次化索引(也叫多级索引)来包含多个索引。
以这种方式,高维数据可以被紧凑的表示在我们熟悉的一维Series和二维DataFrame对象中。
在这部分,我们将探索直接创建多级索引对象,仔细考虑对多级索引数据的索引,切片和计算统计,还会介绍对数据进行简单和层次化索引转换有用的函数。
我们以标准的导入开始:

import pandas as pd
import numpy as np

多级索引Series

让我们由考虑如果使用一维Series表达二维数据开始。具体讲,我们考虑那种带有字母和数字键值数据的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

使用这种索引方案,你可以直接基于多索引进行Series的索引和切片操作:

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多级索引

幸运的是,Pandas提供了一种更好的方法。我们基于元组的索引本质上来说是初级的多索引,Pandas的多级索引类型带来我们所希望的操作类型。我们可以像下面这样通过元组创建多索引:

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包含多个索引级别,在这个例子中,有州名称和年份,以及多个为每个数据点都记录层级的标签。

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

结果是带有我们想要键值的单索引数组。跟我们开始时自制的基于元组的解决方法相比,现在的语法更简洁(执行效率也更高)。我们将会更深入的讨论这类在层级索引数据上面索引操作。

多级索引作为另外的维度

这里你也许会注意到一点别的东西:我们可以很容易的使用带有索引和列标签的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的多层级索引来表示二维数据一样,我们也能使用Series或DataFrame来表示三维或多维数据。多级索引中每个额外的层级代表数据的一个维度;这个属性为我们可以表示的数据类型带来许多灵活性。具体来讲,我们想要添加一列来表示每年的人口统计数据(比如,小于18岁的人口数);使用多级索引,就是简单的在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

另外,所有ufuncs和其他在Operating on Data in Pandas 讨论的功能都能在层次化索引上工作的很好。我们用上面的数据,来计算每年低于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

这使我们可以方便和快捷操纵高维数据。

多级索引的创建方法

为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

创建索引的工作由后台去做。
类似的,如果你传递一个带有适当元组作为键值的字典的话,Pandas将会自动识别并且使用多级索引:

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

然而,有时候明确的创建多级索引也是很有用的;我们来看几个用法。

显示索引构造器

如何构造索引有更多的灵活性,你可以使用类构造方法pd.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(包含每级可用索引值的列表)和lables(指代这些标签的列表)来构造多级索引:

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方法。

多级索引层级名称

有时,给多级索引的层级命名时很有用的。命名可以通过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

随着参与的数据集变多,给索引命名来记录不同索引的意义是非常有用的。

列的多级索引

在DataFrame中,行和列是完全对称的,正如行有多级索引,列也可以有多级索引。考虑如下模拟的医疗数据:

# hierarchical indices and columns
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'])

# mock some data
data = np.round(np.random.randn(4, 6), 1)
data[:, ::2] *= 10
data += 37

# create the DataFrame
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

对于包含多个标签涵盖多次,多个主题(人口,国家,城市等)的复杂数据记录,使用层级化的行和列索引将会极其方便!

多索引的检索和切片

多索引上面的检索和切片被设计的很直观,把索引当作是增加的维度将会很有帮助。
我们首先来看Series上的多索引检索,然后再看DataFrame上的。

多索引Series

考虑我们之前看到的州人口的多索引Series:

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

部分切片也是可用的,只要多级索引是排过序的(参见 Sorted and Unsorted Indices):

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

其他类型的检索和筛选操作(见 Data Indexing and Selection) 工作的也很好;例如,基于布尔过滤筛选:

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

DataFrames 的多层索引

DataFrame的多层索引的行为与Series类似。考虑前面用到的医疗数据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

同样,与单一索引情况一样,我们可以使用loc,iloc和ix 见 Data Indexing and Selection。例如:

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

这些检索器在二维数据上需要提供数组类似输入,但可以把多索引元组传递给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

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

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()方法;但是有更多方法可以精细的控制数据在层级索引和列直接进行转换,让我们来探索它们:

排序索引和未排序索引

之前我们曾经有警告,但在这里应该强调一下.如果索引是未排序的话,许多多索引切片操作将会失败。
我们由创建一些简单的未排序多索引数据开始:

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

尽管错误信息不是特别清楚,原因是多级索引没有排序。因为各种原因,部分切片和其它类似操作要求多级索引的各个层级是排序了的。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

索引经过这样的排序,部分切片就会按期望的工作了:

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(),它可以用来恢复原始的Series:

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方法实现。在人口字典上调用这个方法会导致原来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

通常在现实世界中,原始的输入数据就是这个样子的。通过列名称来构建多索引非常有用。可以同DataFrame的set_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

也许我们想平均一下每年两次来访的测量值。我们可以通过指定想要的索引层级名称来实现,本例使用的是year:

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功能的快捷方法,见Aggregation and Grouping
虽然这只是一个小例子,许多真实的数据集由相似的层级结构。

旁白:面板数据

Pandas还有几种我们没有讲到的数据类型,即pd.Panel和pd.Panel4D对象。它们分别可以被看做是泛化的3维和4维结构,就像Serie是一维,DataFrame是二维一样。一旦熟悉了Series和DataFrame的索引和数据操作,对Panel和Panel4D基本可以直接使用。特别是索引器ix,loc和iloc,它们完全适用于这些高维数据结构。
我们不会再覆盖这些面板结构,因为我发现在大多数情况下,多级索引是非常有用的,并且可以在概念上非常简洁的表示高维数据。另外面板数据是密数据表达,而多级索引基本上是稀数据表达。随着维度的增加,密表达方式对于真实数据集来说变得效率很低。但对于一些特殊的应用,这些结构也很有用。如果想要知道更多关于Panel和Panel4D结构,请看列在Further Resources.中的参考文献。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,050评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,538评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,673评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,622评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,047评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,974评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,129评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,893评论 0 209
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,654评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,828评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,297评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,619评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,326评论 3 243
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,176评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,975评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,118评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,909评论 2 278

推荐阅读更多精彩内容