《Python数据科学手册》读书笔记
层级索引
当目前为止, 接触的都是一维数据和二维数据, 用 Pandas 的
Series 和 DataFrame 对象就可以存储。 但也经常会遇到存储多维
数据的需求, 数据索引超过一两个键。 因此, Pandas 提供了 Panel 和
Panel4D 对象解决三维数据与四维数据 。 而在实
践中, 更直观的形式是通过层级索引配合多个有不同等级的一级索引一
起使用, 这样就可以将高维数组转换成类似一维 Series 和二维
DataFrame 对象的形式。
import pandas as pd
import numpy as np
## 多级索引Series
用一维的 Series 对象表示二维数据
- 笨办法
分析美国各州在两个不同年份的数据,用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
但是这么做很不方便。 假如想要选择所有 2000 年的数据, 那么
就得用一些比较复杂的清理方法:
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 类型提供了更丰富的操作方
法。 先用元组创建一个多级索引, 如下所示:
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 里面有一个 levels 属性表示索引的等级
——这样做可以将州名和年份作为每个数据点的不同标签。
如果将前面创建的 pop 的索引重置(reindex) 为 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 年的全部数据
pop[:, 2010]
California 37253956
New York 19378102
Texas 25145561
dtype: int64
与之前的元组索引相比,
多级索引的语法更简洁。
- 高维数据的多级索引
其实完全可以用一个带行列索引的简单
DataFrame 代替前面的多级索引。
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 岁以下的人口) , 那么对于这种带有
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 |
另外, 通用函数和其他功能函数也同样适用于层
级索引。 我们可以计算上面数据中 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 创建多级索引最直接的办法就是将 index 参
数设置为至少二维的索引数组, 如下所示:
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.722323 | 0.425514 |
2 | 0.770060 | 0.948675 | |
b | 1 | 0.621423 | 0.603367 |
2 | 0.551296 | 0.462293 |
MultiIndex 的创建工作将在后台完成。
同理, 如果你把将元组作为键的字典传递给 Pandas, 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
Texas 2000 20851820
2010 25145561
New York 2000 18976457
2010 19378102
dtype: int64
- 显式地创建多级索引
可以用 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]])
也可以通过包含多个索引值的元组构成的列表创建 MultiIndex:
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]])
还可以用两个索引的笛卡尔积创建
MultiIndex:
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]])
在创建 Series 或 DataFrame 时, 可以将这些对象作为 index 参
数, 或者通过 reindex 方法更新 Series 或 DataFrame 的索引。
- 多级索引的等级名称
可以在
前面任何一个 MultiIndex 构造器中通过 names 参数设置等级名
称, 也可以在创建之后通过索引的 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
在处理复杂的数据时, 为等级设置名称是管理多个索引值的好办
法。
- 多级列索引
既然有多级行索
引, 那么同样可以有多级列索引。 让我们通过一份医学报告的模拟
数据来演示:
# 多级行列索引
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
# 创建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 | 35.0 | 36.6 | 45.0 | 36.2 | 33.0 | 37.6 |
2 | 29.0 | 37.9 | 31.0 | 38.3 | 37.0 | 38.1 | |
2014 | 1 | 33.0 | 37.6 | 25.0 | 35.9 | 47.0 | 36.6 |
2 | 57.0 | 37.3 | 45.0 | 37.0 | 24.0 | 37.4 |
多级行列索引的创建非常简单。 上面创建了一个简易的四维数据,
四个维度分别为被检查人的姓名、 检查项目、 检查年份和检查次
数。 可以在列索引的第一级查询姓名, 从而获取包含一个人(例如
Guido) 全部检查信息的 DataFrame:
health_data['Guido']
type | HR | Temp | |
---|---|---|---|
year | visit | ||
2013 | 1 | 45.0 | 36.2 |
2 | 31.0 | 38.3 | |
2014 | 1 | 25.0 | 35.9 |
2 | 45.0 | 37.0 |
如果想获取包含多种标签的数据, 需要通过对多个维度(姓名、 国
家、 城市等标签) 的多次查询才能实现, 这时使用多级行列索引进
行查询会非常方便。
多级索引的取值与切片
对 MultiIndex 的取值和切片操作很直观, 你可以直接把索引看成额外
增加的维度。
- 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
类似的还有局部切片, 不过要求 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 多级索引的用法与 Series 类似。 还用之前的体检报告
数据来演示:
由于 DataFrame 的基本索引是列索引, 因此 Series 中多级索引
的用法到了 DataFrame 中就应用在列上了。 例如, 可以通过简单
的操作获取 Guido 的心率数据:
health_data['Guido', 'HR']
year visit
2013 1 45.0
2 31.0
2014 1 25.0
2 45.0
Name: (Guido, HR), dtype: float64
与单索引类似,loc、 iloc 和 ix 索引器都可以使
用, 例如:
health_data.iloc[:2, :2]
subject | Bob | ||
---|---|---|---|
type | HR | Temp | |
year | visit | ||
2013 | 1 | 35.0 | 36.6 |
2 | 29.0 | 37.9 |
虽然这些索引器将多维数据当作二维数据处理, 但是在 loc 和
iloc 中可以传递多个层级的索引元组, 例如:
这种索引元组的用法不是很方便, 如果在元组中使用切片还会导致
语法错误:
health_data.loc[(:, 1), (:, 'HR')]
File "<ipython-input-38-6d10fc475b38>", line 1
health_data.loc[( : , 1), ( : , 'HR')]
^
SyntaxError: invalid syntax
还有一种更好的办法, 就是使用 IndexSlice 对象。 Pandas 专门用
它解决这类问题, 例如:
idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, 'HR']]
subject | Bob | Guido | Sue | |
---|---|---|---|---|
type | HR | HR | HR | |
year | visit | |||
2013 | 1 | 35.0 | 45.0 | 33.0 |
2014 | 1 | 33.0 | 25.0 | 47.0 |
多级索引行列转换
- 有序的索引和无序的索引
如果 MultiIndex 不是有序的索引, 那么大多数切片
操作都会失败。
首先创建一个不按字典顺序排列的多级索引
Series:
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.029188
2 0.386739
c 1 0.273822
2 0.514064
b 1 0.395069
2 0.930861
dtype: float64
如果想对索引使用局部切片, 那么错误就会出现:
try:
data['a':'b']
except KeyError as e:
print(type(e))
print(e)
<class 'pandas.errors.UnsortedIndexError'>
'Key length (1) was greater than MultiIndex lexsort depth (0)'
尽管从错误信息里面看不出具体的细节, 但问题是出在
MultiIndex 无序排列上。 局部切片和许多其他相似的操作都要求
MultiIndex 的各级索引是有序的(即按照字典顺序由 A 至 Z) 。
为此, Pandas 提供了许多便捷的操作完成排序, 如 sort_index()
和 sortlevel() 方法。 用最简单的 sort_index() 方法来演
示:
data = data.sort_index()
data
char int
a 1 0.029188
2 0.386739
b 1 0.395069
2 0.930861
c 1 0.273822
2 0.514064
dtype: float64
索引排序之后, 局部切片就可以正常使用了:
data['a':'b']
char int
a 1 0.029188
2 0.386739
b 1 0.395069
2 0.930861
dtype: float64
- 索引stack与unstack
可以将一个多级索引数据集转换成简单的二维形
式, 可以通过 level 参数设置转换的索引层级:
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 方法实现。 如果在上面的人口数据 Series 中使用该
方法, 则会生成一个列标签中包含之前行索引标签 state 和 year 的
DataFrame。 也可以用数据的 name 属性为列设置名称:
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, 通常将大有裨益。 其实可以通过
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 | 35.0 | 36.6 | 45.0 | 36.2 | 33.0 | 37.6 |
2 | 29.0 | 37.9 | 31.0 | 38.3 | 37.0 | 38.1 | |
2014 | 1 | 33.0 | 37.6 | 25.0 | 35.9 | 47.0 | 36.6 |
2 | 57.0 | 37.3 | 45.0 | 37.0 | 24.0 | 37.4 |
如果需要计算每一年各项指标的平均值, 那么可以将参数 level 设
置为索引 year:
data_mean = health_data.mean(level='year')
data_mean
subject | Bob | Guido | Sue | |||
---|---|---|---|---|---|---|
type | HR | Temp | HR | Temp | HR | Temp |
year | ||||||
2013 | 32.0 | 37.25 | 38.0 | 37.25 | 35.0 | 37.85 |
2014 | 45.0 | 37.45 | 35.0 | 36.45 | 35.5 | 37.00 |
如果再设置 axis 参数, 就可以对列索引进行类似的累计操作了:
data_mean.mean(axis=1, level='type')
type | HR | Temp |
---|---|---|
year | ||
2013 | 35.0 | 37.450000 |
2014 | 38.5 | 36.966667 |
通过这两行数据, 就可以获取每一年所有人的平均心率和体温了。
这里还有一些 Pandas 的基本数据结构没有介绍到, 包括 pd.Panel
对象和 pd.Panel4D 对象。 这两种数据结构可以分别看成是(一维
数组) Series 和(二维数组) DataFrame 的三维与四维形式。 如
果熟悉 Series 和 DataFrame 的使用方法, 那么 Panel 和
Panel4D 使用起来也会很简单, ix、 loc 和 iloc 索引器在高维数据结构上的用法更是完全相同。