索引
当目前为止,我们接触的都是一维数据和二维数据,用 Pandas 的 Series 和 DataFrame 对象就可以存储。但我们也经常会遇到存储多维数据的需求,数据索引超过一两个键。因此,Pandas 提供了 Panel 和 Panel4D 对象解决三维数据与四维数据。而在实践中,更直观的形式是通过层级索引(hierarchical indexing,也被称为多级索引,multi-indexing)配合多个有不同等级(level)的一级索引一起使用,这样就可以将高维数组转换成类似一维 Series 和二维 DataFrame 对象的形式。
接下来我们将介绍创建 MultiIndex 对象的方法,多级索引数据的取值、切片和统计值的计算,以及普通索引与层级索引的转换方法。
多级索引-Series
让我们看看如何用一维的 Series 对象表示二维数据——用一系列包含特征与数值的数据点来简单演示。
原始办法
假设你想要分析美国各州在两个不同年份的数据。如果你用前面介绍的 Pandas 工具来处理,那么可能会用一个 Python 元组来表示索引:
import pandas as pd
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)
print(pop)
通过元组构成的多级索引,你可以直接在 Series 上取值或用切片查询数据:
print(pop[('California', 2010):('Texas', 2000)])
但是这么做很不方便。假如你想要选择所有 2000 年的数据,那么就得用一些比较复杂的(可能也比较慢的)清理方法了:
# 选择所有 2000 年的数据
print(pop[[i for i in pop.index if i[1] == 2010]])
这么做虽然也能得到需要的结果,但是与 Pandas 令人爱不释手的切片语法相比,这种方法确实不够简洁(在处理较大的数据时也不够高效)。
Pandas 多级索引
好在 Pandas 提供了更好的解决方案。用元组表示索引其实是多级索引的基础,Pandas 的 MultiIndex 类型提供了更丰富的操作方法。我们可以用元组创建一个多级索引,如下所示:
index = pd.MultiIndex.from_tuples(index)
print('index:\t', index)
你会发现 MultiIndex 里面有一个 levels 属性表示索引的等级
这样做可以将州名和年份作为每个数据点的不同标签。 如果将前面创建的 pop 的索引重置(reindex)为 MultiIndex,就会看到层级索引:
pop = pop.reindex(index)
print(pop)
其中前两列表示 Series 的多级索引值,第三列是数据。你会发现有些行仿佛缺失了第一列数据——这其实是多级索引的表现形式,每个空格与上面的索引相同。现在可以直接用第二个索引获取 2010 年的全部数据,与 Pandas 的切片查询用法一致:
print(pop[:, 2010])
结果是单索引的数组,正是我们需要的。与之前的元组索引相比,多级索引的语法更简洁。(操作也更方便!)下面继续介绍层级索引的取值操作方法。
高维数据的多级索引
你可能已经注意到,我们其实完全可以用一个带行列索引的简单 DataFrame 代替前面的多级索引。其实 Pandas 已经实现了类似的功能。unstack() 方法可以快速将一个多级索引的 Series 转化为普通索引的 DataFrame:
"""高维数据的多级索引"""
pop_df = pop.unstack()
print('unstack:\n', pop_df)
# 当然了,也有 stack() 方法实现相反的效果
print('stack:\n', pop_df.stack())
你可能会纠结于为什么要费时间研究层级索引。其实理由很简单: 如果我们可以用含多级索引的一维 Series 数据表示二维数据,那么我们就可以用 Series 或 DataFrame 表示三维甚至更高维度的数据。多级索引每增加一级,就表示数据增加一维,利用这一特点就可以轻松表示任意维度的数据了。假如要增加一列显示每一年各州的人口统计指标(例如 18 岁以下的人口),那么对于这种带有 MultiIndex 的对象,增加一列就像 DataFrame 的操作一样简单:
pop_df = pd.DataFrame({
'total': pop,
'under18': [9267089, 9284094, 4687374, 4318033, 5906301, 6879014]
})
print('pop_df:\n', pop_df)
通用函数和其他功能也同样适用于层级索引。我们可以计算上面数据中 18 岁以下的人口占总人口的比例:
f_u18 = pop_df['under18'] / pop_df['total']
f_u18.unstack()
多级索引创建方法
为 Series 或 DataFrame 创建多级索引最直接的办法就是将 index 参数设置为至少二维的索引数组,如下所示:
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.rand(4, 2),
index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=['data1', 'data2'])
print(df)
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}
print(pd.Series(data))
但是有时候显式地创建 MultiIndex 也是很有用的,下面来介绍一些创建方法。
显式地创建多级索引
你可以用 pd.MultiIndex 中的类方法更加灵活地构建多级索引。 例如,就像前面介绍的,你可以通过一个有不同等级的若干简单数组组成的列表来构建 MultiIndex:
"""显示创建多级索引"""
index = pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
print(index)
也可以通过包含多个索引值的元组构成的列表创建 MultiIndex:
index2 = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
print(index2)
还可以用两个索引的笛卡尔积(Cartesian product)创建 MultiIndex:
index3 = pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
print(index3)
多级索引的等级名称
给 MultiIndex 的等级加上名称会为一些操作提供便利。你可以在前面任何一个 MultiIndex 构造器中通过 names 参数设置等级名称,也可以在创建之后通过索引的 names 属性来修改名称:
"""多级索引的等级名称"""
index = [('California', 2000), ('California', 2010),
('New York', 2000), ('New York', 2010),
('Texas', 2000), ('Texas', 2010)]
populations = [33871648, 37253956, 18976457, 19378102, 20851820, 25145561]
index = pd.MultiIndex.from_tuples(index)
pop = pd.DataFrame(populations, index=index)
print(pop)
pop.index.names = ['state', 'year']
print(pop)
在处理复杂的数据时,为等级设置名称是管理多个索引值的好办法
多级列索引
每个 DataFrame 的行与列都是对称的,也就是说既然有多级行索引,那么同样可以有多级列索引。让我们通过一份医学报告的模拟数据来演示:
"""多级列索引"""
# 多级行列索引
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)
print(health_data)
多级行列索引的创建非常简单。上面创建了一个简易的四维数据,四个维度分别为被检查人的姓名、检查项目、检查年份和检查次数。可以在列索引的第一级查询姓名,从而获取包含一个人(例如 Guido)全部检查信息的 DataFrame:
print(health_data['Guido'])
print(health_data['Guido', 'HR'])
print(health_data['Guido', 'HR'][2013])
如果想获取包含多种标签的数据,需要通过对多个维度(姓名、国家、城市等标签)的多次查询才能实现,这时使用多级行列索引进行查询会非常方便。
多级索引的取值与切片
对 MultiIndex 的取值和切片操作很直观,你可以直接把索引看成额外增加的维度。我们先来介绍 Series 多级索引的取值与切片方法,再介绍 DataFrame 的用法。
Series 多级索引
看看下面由各州历年人口数量创建的多级索引 Series
可以通过对多个级别索引值获取单个元素
MultiIndex 也支持局部取值(partial indexing),即只取索引的某一个层级。假如只取最高级的索引,获得的结果是一个新的 Series,未被选中的低层索引值会被保留:
import pandas as pd
data = {('California', 2000): 33871648,
('California', 2010): 37253956,
('Texas', 2000): 20851820,
('Texas', 2010): 25145561,
('New York', 2000): 18976457,
('New York', 2010): 19378102}
pop = pd.Series(data)
pop.index.names = ['state', 'year']
print('pop:\n', pop)
print('单个取值:\n', pop['California', 2000])
print('局部取值:\n', pop['California'])
类似的还有局部切片,不过要求 MultiIndex 是按顺序排列的
pop.sort_index(inplace=True)
print(pop.loc['California':'New York'])
如果索引已经排序,那么可以用较低层级的索引取值,第一层级的 索引可以用空切片:
print(pop[:, 2000])
其他取值与数据选择的方法也都起作用。下面的例子是通过布尔掩码选择数据:
print('布尔掩码取值:\n', pop[pop > 22000000])
也可以用花哨的索引选择数据:
print('花哨索引取值:\n', pop[['California', 'Texas']])
DataFrame 多级索引
DataFrame 多级索引的用法与 Series 类似。还用之前的体检报告数据来演示
由于 DataFrame 的基本索引是列索引,因此 Series 中多级索引的用法到了 DataFrame 中就应用在列上了。例如,可以通过简单的操作获取 Guido 的心率数据:
print(health_data['Guido', 'HR'])
与单索引类似,loc、iloc 和 ix 索引器都可以使 用,例如:
print(health_data.iloc[:2, :2])
虽然这些索引器将多维数据当作二维数据处理,但是在 loc 和 iloc 中可以传递多个层级的索引元组,例如:
print(health_data.loc[:, ('Bob', 'HR')])
多级索引行列转换
使用多级索引的关键是掌握有效数据转换的方法。Pandas 提供了许多操作,可以让数据在内容保持不变的同时,按照需要进行行列转换。之前我们用一个简短的例子演示过 stack() 和 unstack() 的用法,但其实还有许多合理控制层级行列索引的方法,让我们来一探究竟。
索引 stack 与 unstack
前文曾提过,我们可以将一个多级索引数据集转换成简单的二维形
式,可以通过 level 参数设置转换的索引层级:
import numpy as np
import pandas as pd
data = {('California', 2000): 33871648,
('California', 2010): 37253956,
('Texas', 2000): 20851820,
('Texas', 2010): 25145561,
('New York', 2000): 18976457,
('New York', 2010): 19378102}
pop = pd.Series(data)
pop.index.names = ['state', 'year']
print(pop)
print(pop.unstack(level=0))
unstack() 是 stack() 的逆操作,同时使用这两种方法让数据保持不变:
print(pop.unstack().stack())
索引的设置与重置
层级数据维度转换的另一种方法是行列标签转换,可以通过 reset_index 方法实现。如果在上面的人口数据 Series 中使用该方法,则会生成一个列标签中包含之前行索引标签 state 和 year 的 DataFrame。也可以用数据的 name 属性为列设置名称:
"""索引的设置与重置"""
pop_flat = pop.reset_index(name='population')
print(pop_flat)
在解决实际问题的时候,如果能将类似这样的原始输入数据的列直接转换成 MultiIndex,通常将大有裨益。其实可以通过 DataFrame 的 set_index 方法实现,返回结果就会是一个带多级索引的 DataFrame:
print(pop_flat.set_index(['state', 'year']))
在实践中,我发现用这种重建索引的方法处理数据集非常好用。