5.层次化索引

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5.层次化索引相关的知识,希望对你有一定的参考价值。

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

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

假设你想要追踪两个不同年份的州数据。使用我们已经讲过的Pandas工具,你可能首先想到的是简单的使用Python元组作为键值:

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

但是便利也就到此为止了。例如,如果你需要选取所有2010年的值,你需要使用混乱(有可能缓慢的)方法才能实现:

这种方法得到了期待的结果,但它并不像我们喜爱的Pandas切片操作那样简洁(在数据很大时,效率也不高)。

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

注意MultiIndex包含多个索引级别,在这个例子中,有州名称和年份,以及多个为每个数据点都记录层级的标签。

这里Series结果的头两列显示的是多索引值,第三行显示的是数据。注意第一列里面一些条目是空的:在多索引表达是,空位表示那里面的值与上一行的值一样。

现在访问第二个索引是2010的所有数据,我们就可以简单的使用Pandas切片记号:

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

这里你也许会注意到一点别的东西:我们可以很容易的使用带有索引和列标签的DataFrame来存储同样的数据。实际上,Pandas在设计时也想到了这种对等性。unstack()方法会快速的将多层索引Series转换成常用的DataFrame:

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

看到这,您可能奇怪为什么我们还要麻烦的使用层级索引。原因很简单:就如我们可以用一维Series的多层级索引来表示二维数据一样,我们也能使用Series或DataFrame来表示三维或多维数据。多级索引中每个额外的层级代表数据的一个维度;这个属性为我们可以表示的数据类型带来许多灵活性。具体来讲,我们想要添加一列来表示每年的人口统计数据(比如,小于18岁的人口数);使用多级索引,就是简单的在DataFrame中添加一列:

另外,所有ufuncs和其他在Operating on Data in Pandas 讨论的功能都能在层次化索引上工作的很好。我们用上面的数据,来计算每年低于18岁的人口比例:

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

为Series和DataFrame创建多级索引最直接的方法就是传递两个或多个索引数组给构造器。例如:

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

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

如何构造索引有更多的灵活性,你可以使用类构造方法pd.MultiIndex。例如,如我们之前做的,可以通过给出了各级索引值的数组列表来构建多级索引:

也可以通过已经给出每个点多级索引值的元组列表来构建:

甚至可通过单索引的笛卡尔积来创建:

同样的,可以直接通过传递内部编码levels(包含每级可用索引值的列表)和lables(指代这些标签的列表)来构造多级索引:

所有这些对象,在创建Series或DataFrame时,都可用作为index参数传进去,或者传递给已经存在了的Series或DataFrame对象的reindex方法。

有时,给多级索引的层级命名时很有用的。命名可以通过names参数给MultiIndex构造器来实现,或者设置已有索引的names属性:

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

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

我们很容易的得到了行和列的多级索引。这基本上是四维数据,访问对象,检查类型,年份和访问次数。有了这个,我们可以通过最上层人的名称来检索,并且能够得到只包含那个人信息的完整DataFrame:

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

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

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

借助于多条目索引,我们可以访问单个数据元素:

MultiIndex也支持部分索引,或者只是检索索引层级中的一个。结果是另一个Series,保留着较低层的索引。

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

对于排过序的索引,将前级索引置为空,基于低层级索引的检索操作也可以执行:

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

基于花式索引的筛选也可以工作:

DataFrame的多层索引的行为与Series类似。考虑前面用到的医疗数据DataFrame:

要记住列在DataFrame中是主要元素,用于Series的多级索引语法也适用于列。例如我们可以使用简单的方式获得Guido的心率:

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

这些检索器在二维数据上需要提供数组类似输入,但可以把多索引元组传递给loc和iloc。例如:

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

可以通过显示使用Python内置的slice()函数构建期望的切片来绕过上面的限制。但更好的办法是使用IndexSlice对象,它是Pandas专门用来处理这种情形的。例如:

有许多方法可以同多索引Series和DataFrame进行交互,如同本书中的许多工具一样,最好的熟悉方法是多尝试!

使用多索引数据的一个关键是知道如何有效的转换数据。有许多操作会保留数据集的所有信息,但为了不同的目的而对它进行重拍。我们看过简短的例子:stack()和unstack()方法;但是有更多方法可以精细的控制数据在层级索引和列直接进行转换,让我们来探索它们:

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

如果对这个索引进行部分切片的话,它将导致一个错误:

尽管错误信息不是特别清楚,原因是多级索引没有排序。因为各种原因,部分切片和其它类似操作要求多级索引的各个层级是排序了的。Pandas提供了几种方法的函数来执行这类排序;例如DataFrame的sort_index()和sortlevel()方法。这儿我们使用最简单的sort_index():

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

我们前面简要的见过,可以将聚集的多索引转换未简单的二维表现,所使用的层级是可以选的:

unstack()的反向操作是stack(),它可以用来恢复原始的Series:

另一种重排层级化数据的方法是将索引标签变成列;这可以通过reset_index方法实现。在人口字典上调用这个方法会导致原来index里面的state和year信息变成DataFrame对应的列。为清晰起见,我们可以指定数据列显示的名称:

通常在现实世界中,原始的输入数据就是这个样子的。通过列名称来构建多索引非常有用。可以同DataFrame的set_index方法来实现,结果返回的就是多级索引DataFrame:

在实践中,我发现这类重置索引的方法是处理真实数据集最有用的模式之一。

我们之前看到过Pandas的内部数据聚合方法,例如mean(),sum()和max()。对于层级索引数据,可以传递一个level参数来控制对那个数据子集进行计算。例如,我们回到健康数据:

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

借助于使用关键字axis,我们也可以获取列中某层级的均值:

只用了两行,我们就能够发现访问对象的每年平均心率和体温测量值。这个语法实际是Groupby功能的快捷方法,见 Aggregation and Grouping
虽然这只是一个小例子,许多真实的数据集由相似的层级结构。

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

数据规整:聚合合并和重塑

目录

一、层次化索引

层次化索引(hierarchical indexing)是pandas的一项重要功能,它使你能在一个轴上拥有多个(两个以上)索引级别。抽象点说,它使你能以低维度形式处理高维度数据。我们先来看一个简单的例子:创建一个Series,并用一个由列表或数组组成的列表作为索引:

In [9]: data = pd.Series(np.random.randn(9),
   ...:                  index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],
   ...:                         [1, 2, 3, 1, 3, 1, 2, 2, 3]])

In [10]: data
Out[10]: 
a  1   -0.204708
   2    0.478943
   3   -0.519439
b  1   -0.555730
   3    1.965781
c  1    1.393406
   2    0.092908
d  2    0.281746
   3    0.769023
dtype: float64

看到的结果是经过美化的带有MultiIndex索引的Series的格式。索引之间的“间隔”表示“直接使用上面的标签”:

In [11]: data.index
Out[11]: 
MultiIndex(levels=[['a', 'b', 'c', 'd'], [1, 2, 3]],
           labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 2, 0, 2, 0, 1, 1, 2]])

对于一个层次化索引的对象,可以使用所谓的部分索引,使用它选取数据子集的操作更简单:

In [12]: data['b']
Out[12]: 
1   -0.555730
3    1.965781
dtype: float64

In [13]: data['b':'c']
Out[13]: 
b  1   -0.555730
   3    1.965781
c  1    1.393406
   2    0.092908
dtype: float64

In [14]: data.loc[['b', 'd']]
Out[14]: 
b  1   -0.555730
   3    1.965781
d  2    0.281746
   3    0.769023
dtype: float64

有时甚至还可以在“内层”中进行选取

In [15]: data.loc[:, 2]
Out[15]: 
a    0.478943
c    0.092908
d    0.281746
dtype: float64

对于一个DataFrame,每条轴都可以有分层索引:

In [18]: frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
   ....:                      index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
   ....:                      columns=[['Ohio', 'Ohio', 'Colorado'],
   ....:                               ['Green', 'Red', 'Green']])

In [19]: frame
Out[19]: 
     Ohio     Colorado
    Green Red    Green
a 1     0   1        2
  2     3   4        5
b 1     6   7        8
  2     9  10       11

各层都可以有名字(可以是字符串,也可以是别的Python对象)。如果指定了名称,它们就会显示在控制台输出中:

In [20]: frame.index.names = ['key1', 'key2']

In [21]: frame.columns.names = ['state', 'color']

In [22]: frame
Out[22]: 
state      Ohio     Colorado
color     Green Red    Green
key1 key2                   
a    1        0   1        2
     2        3   4        5
b    1        6   7        8
     2        9  10       11

重排与分级排序

有时,你需要重新调整某条轴上各级别的顺序,或根据指定级别上的值对数据进行排序。swaplevel接受两个级别编号或名称,并返回一个互换了级别的新对象(但数据不会发生变化):

In [24]: frame.swaplevel('key1', 'key2')
Out[24]: 
state      Ohio     Colorado
color     Green Red    Green
key2 key1                   
1    a        0   1        2
2    a        3   4        5
1    b        6   7        8
2    b        9  10       11

而sort_index则根据单个级别中的值对数据进行排序。交换级别时,常常也会用到sort_index,这样最终结果就是按照指定顺序进行字母排序了:

In [25]: frame.sort_index(level=1)
Out[25]: 
state      Ohio     Colorado
color     Green Red    Green
key1 key2                   
a    1        0   1        2
b    1        6   7        8
a    2        3   4        5
b    2        9  10       11

In [26]: frame.swaplevel(0, 1).sort_index(level=0)
Out[26]: 
state      Ohio     Colorado
color     Green Red    Green
key2 key1                   
1    a        0   1        2
     b        6   7        8
2    a        3   4        5
     b        9  10       11

根据级别汇总统计

许多对DataFrame和Series的描述和汇总统计都有一个level选项,它用于指定在某条轴上求和的级别。再以上面那个DataFrame为例,我们可以根据行或列上的级别来进行求和:

In [27]: frame.sum(level='key2')
Out[27]: 
state  Ohio     Colorado
color Green Red    Green
key2                    
1         6   8       10
2        12  14       16

In [28]: frame.sum(level='color', axis=1)
Out[28]: 
color      Green  Red
key1 key2            
a    1         2    1
     2         8    4
b    1        14    7
     2        20   10

DataFrame的set_index函数会将其一个或多个列转换为行索引,并创建一个新的DataFrame:

In [29]: frame = pd.DataFrame('a': range(7), 'b': range(7, 0, -1),
   ....:                       'c': ['one', 'one', 'one', 'two', 'two',
   ....:                             'two', 'two'],
   ....:                       'd': [0, 1, 2, 0, 1, 2, 3])

In [30]: frame
Out[30]: 
   a  b    c  d
0  0  7  one  0
1  1  6  one  1
2  2  5  one  2
3  3  4  two  0
4  4  3  two  1
5  5  2  two  2
6  6  1  two  3

默认情况下,那些列会从DataFrame中移除,但也可以将其保留下来:

In [33]: frame.set_index(['c', 'd'], drop=False)
Out[33]: 
       a  b    c  d
c   d              
one 0  0  7  one  0
    1  1  6  one  1
    2  2  5  one  2
two 0  3  4  two  0
    1  4  3  two  1
    2  5  2  two  2
    3  6  1  two  3

reset_index的功能跟set_index刚好相反,层次化索引的级别会被转移到列里面:


In [34]: frame2.reset_index()
Out[34]:
c  d  a  b
0  one  0  0  7
1  one  1  1  6
2  one  2  2  5
3  two  0  3  4
4  two  1  4  3
5  two  2  5  2
6  two  3  6  1

二、合并数据集

pandas对象中的数据可以通过一些方式进行合并:

  • pandas.merge可根据一个或多个键将不同DataFrame中的行连接起来。SQL或其他关系型数据库的用户对此应该会比较熟悉,因为它实现的就是数据库的join操作。
  • pandas.concat可以沿着一条轴将多个对象堆叠到一起。
  • 实例方法combine_first可以将重复数据拼接在一起,用一个对象中的值填充另一个对象中的缺失值。
    我将分别对它们进行讲解,并给出一些例子。本书剩余部分的示例中将经常用到它们。

数据库风格的DataFrame合并

数据集的合并(merge)或连接(join)运算是通过一个或多个键将行连接起来的。这些运算是关系型数据库(基于SQL)的核心。pandas的merge函数是对数据应用这些算法的主要切入点。

以一个简单的例子开始:

In [35]: df1 = pd.DataFrame('key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
   ....:                     'data1': range(7))

In [36]: df2 = pd.DataFrame('key': ['a', 'b', 'd'],
   ....:                     'data2': range(3))

In [37]: df1
Out[37]: 
   data1 key
0      0   b
1      1   b
2      2   a
3      3   c
4      4   a
5      5   a
6      6   b

In [38]: df2
Out[38]: 
   data2 key
0      0   a
1      1   b
2      2   d

这是一种多对一的合并。df1中的数据有多个被标记为a和b的行,而df2中key列的每个值则仅对应一行。对这些对象调用merge即可得到:

In [39]: pd.merge(df1, df2)
Out[39]: 
   data1 key  data2
0      0   b      1
1      1   b      1
2      6   b      1
3      2   a      0
4      4   a      0
5      5   a      0

注意,我并没有指明要用哪个列进行连接。如果没有指定,merge就会将重叠列的列名当做键。不过,最好明确指定一下:

In [40]: pd.merge(df1, df2, on='key')
Out[40]: 
   data1 key  data2
0      0   b      1
1      1   b      1
2      6   b      1
3      2   a      0
4      4   a      0
5      5   a      0

如果两个对象的列名不同,也可以分别进行指定:

In [41]: df3 = pd.DataFrame('lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
   ....:                     'data1': range(7))

In [42]: df4 = pd.DataFrame('rkey': ['a', 'b', 'd'],
   ....:                     'data2': range(3))

In [43]: pd.merge(df3, df4, left_on='lkey', right_on='rkey')
Out[43]: 
   data1 lkey  data2 rkey
0      0    b      1    b
1      1    b      1    b
2      6    b      1    b
3      2    a      0    a
4      4    a      0    a
5      5    a      0    a

可能你已经注意到了,结果里面c和d以及与之相关的数据消失了。默认情况下,merge做的是“内连接”;结果中的键是交集。其他方式还有"left"、“right"以及"outer”。外连接求取的是键的并集,组合了左连接和右连接的效果:

In [44]: pd.merge(df1, df2, how='outer')
Out[44]: 
   data1 key  data2
0    0.0   b    1.0
1    1.0   b    1.0
2    6.0   b    1.0
3    2.0   a    0.0
4    4.0   a    0.0
5    5.0   a    0.0
6    3.0   c    NaN
7    NaN   d    2.0


多键合并

In [51]: left = pd.DataFrame('key1': ['foo', 'foo', 'bar'],
   ....:                      'key2': ['one', 'two', 'one'],
   ....:                      'lval': [1, 2, 3])

In [52]: right = pd.DataFrame('key1': ['foo', 'foo', 'bar', 'bar'],
   ....:                       'key2': ['one', 'one', 'one', 'two'],
   ....:                       'rval': [4, 5, 6, 7])

In [53]: pd.merge(left, right, on=['key1', 'key2'], how='outer')
Out[53]: 
  key1 key2  lval  rval
0  foo  one   1.0   4.0
1  foo  one   1.0   5.0
2  foo  two   2.0   NaN
3  bar  one   3.0   6.0
4  bar  two   NaN   7.0

对于合并运算需要考虑的最后一个问题是对重复列名的处理。虽然你可以手工处理列名重叠的问题(查看前面介绍的重命名轴标签),但merge有一个更实用的suffixes选项,用于指定附加到左右两个DataFrame对象的重叠列名上的字符串:

In [54]: pd.merge(left, right, on='key1')
Out[54]: 
  key1 key2_x  lval key2_y  rval
0  foo    one     1    one     4
1  foo    one     1    one     5
2  foo    two     2    one     4
3  foo    two     2    one     5
4  bar    one     3    one     6
5  bar    one     3    two     7

In [55]: pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
Out[55]: 
  key1 key2_left  lval key2_right  rval
0  foo       one     1        one     4
1  foo       one     1        one     5
2  foo       two     2        one     4
3  foo       two     2        one     5
4  bar       one     3        one     6
5  bar       one     3        two     7


索引上的合并

有时候,DataFrame中的连接键位于其索引中。在这种情况下,你可以传入left_index=True或right_index=True(或两个都传)以说明索引应该被用作连接键:

In [56]: left1 = pd.DataFrame('key': ['a', 'b', 'a', 'a', 'b', 'c'],
   ....:                       'value': range(6))

In [57]: right1 = pd.DataFrame('group_val': [3.5, 7], index=['a', 'b'])

In [58]: left1
Out[58]:

  key  value
0   a      0
1   b      1
2   a      2
3   a      3
4   b      4
5   c      5

In [59]: right1
Out[59]: 
   group_val
a        3.5
b        7.0

In [60]: pd.merge(left1, right1, left_on='key', right_index=True)
Out[60]: 
  key  value  group_val
0   a      0        3.5
以上是关于5.层次化索引的主要内容,如果未能解决你的问题,请参考以下文章

pandas层次化索引

层次化索引

Pandas基本功能之层次化索引及层次化汇总

python pandas 如何去掉层次化索引

pandas层次化索引和拼接

pandas层次化索引