到目前为止我们关注的是保存在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.中的参考文献。