Skip to content

索引

当目前为止,我们接触的都是一维数据和二维数据,用 Pandas 的 Series 和 DataFrame 对象就可以存储。但我们也经常会遇到存储多维数据的需求,数据索引超过一两个键。因此,Pandas 提供了 Panel 和 Panel4D 对象解决三维数据与四维数据。而在实践中,更直观的形式是通过层级索引(hierarchical indexing,也被称为多级索引,multi-indexing)配合多个有不同等级(level)的一级索引一起使用,这样就可以将高维数组转换成类似一维 Series 和二维 DataFrame 对象的形式。

接下来我们将介绍创建 MultiIndex 对象的方法,多级索引数据的取值、切片和统计值的计算,以及普通索引与层级索引的转换方法。

多级索引-Series

让我们看看如何用一维的 Series 对象表示二维数据——用一系列包含特征与数值的数据点来简单演示。

原始办法

假设你想要分析美国各州在两个不同年份的数据。如果你用前面介绍的 Pandas 工具来处理,那么可能会用一个 Python 元组来表示索引:

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 上取值或用切片查询数据:

python
print(pop[('California', 2010):('Texas', 2000)])

但是这么做很不方便。假如你想要选择所有 2000 年的数据,那么就得用一些比较复杂的(可能也比较慢的)清理方法了:

python
# 选择所有 2000 年的数据
print(pop[[i for i in pop.index if i[1] == 2010]])

这么做虽然也能得到需要的结果,但是与 Pandas 令人爱不释手的切片语法相比,这种方法确实不够简洁(在处理较大的数据时也不够高效)。

Pandas 多级索引

好在 Pandas 提供了更好的解决方案。用元组表示索引其实是多级索引的基础,Pandas 的 MultiIndex 类型提供了更丰富的操作方法。我们可以用元组创建一个多级索引,如下所示:

python
index = pd.MultiIndex.from_tuples(index)
print('index:\t', index)

你会发现 MultiIndex 里面有一个 levels 属性表示索引的等级

这样做可以将州名和年份作为每个数据点的不同标签。 如果将前面创建的 pop 的索引重置(reindex)为 MultiIndex,就会看到层级索引:

python
pop = pop.reindex(index)
print(pop)

其中前两列表示 Series 的多级索引值,第三列是数据。你会发现有些行仿佛缺失了第一列数据——这其实是多级索引的表现形式,每个空格与上面的索引相同。现在可以直接用第二个索引获取 2010 年的全部数据,与 Pandas 的切片查询用法一致:

python
print(pop[:, 2010])

结果是单索引的数组,正是我们需要的。与之前的元组索引相比,多级索引的语法更简洁。(操作也更方便!)下面继续介绍层级索引的取值操作方法。

高维数据的多级索引

你可能已经注意到,我们其实完全可以用一个带行列索引的简单 DataFrame 代替前面的多级索引。其实 Pandas 已经实现了类似的功能。unstack() 方法可以快速将一个多级索引的 Series 转化为普通索引的 DataFrame:

python
"""高维数据的多级索引"""
pop_df = pop.unstack()
print('unstack:\n', pop_df)

# 当然了,也有 stack() 方法实现相反的效果
print('stack:\n', pop_df.stack())

你可能会纠结于为什么要费时间研究层级索引。其实理由很简单: 如果我们可以用含多级索引的一维 Series 数据表示二维数据,那么我们就可以用 Series 或 DataFrame 表示三维甚至更高维度的数据。多级索引每增加一级,就表示数据增加一维,利用这一特点就可以轻松表示任意维度的数据了。假如要增加一列显示每一年各州的人口统计指标(例如 18 岁以下的人口),那么对于这种带有 MultiIndex 的对象,增加一列就像 DataFrame 的操作一样简单:

python
pop_df = pd.DataFrame({
    'total': pop,
    'under18': [9267089, 9284094, 4687374, 4318033, 5906301, 6879014]
})

print('pop_df:\n', pop_df)

通用函数和其他功能也同样适用于层级索引。我们可以计算上面数据中 18 岁以下的人口占总人口的比例:

python
f_u18 = pop_df['under18'] / pop_df['total']
f_u18.unstack()

多级索引创建方法

为 Series 或 DataFrame 创建多级索引最直接的办法就是将 index 参数设置为至少二维的索引数组,如下所示:

python
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:

python
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:

python
"""显示创建多级索引"""
index = pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
print(index)

也可以通过包含多个索引值的元组构成的列表创建 MultiIndex:

python
index2 = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
print(index2)

还可以用两个索引的笛卡尔积(Cartesian product)创建 MultiIndex:

python
index3 = pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
print(index3)

多级索引的等级名称

给 MultiIndex 的等级加上名称会为一些操作提供便利。你可以在前面任何一个 MultiIndex 构造器中通过 names 参数设置等级名称,也可以在创建之后通过索引的 names 属性来修改名称:

python
"""多级索引的等级名称"""
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 的行与列都是对称的,也就是说既然有多级行索引,那么同样可以有多级列索引。让我们通过一份医学报告的模拟数据来演示:

python
"""多级列索引"""
# 多级行列索引
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:

python
print(health_data['Guido'])
print(health_data['Guido', 'HR'])
print(health_data['Guido', 'HR'][2013])

如果想获取包含多种标签的数据,需要通过对多个维度(姓名、国家、城市等标签)的多次查询才能实现,这时使用多级行列索引进行查询会非常方便。

多级索引的取值与切片

对 MultiIndex 的取值和切片操作很直观,你可以直接把索引看成额外增加的维度。我们先来介绍 Series 多级索引的取值与切片方法,再介绍 DataFrame 的用法。

Series 多级索引

看看下面由各州历年人口数量创建的多级索引 Series

可以通过对多个级别索引值获取单个元素

MultiIndex 也支持局部取值(partial indexing),即只取索引的某一个层级。假如只取最高级的索引,获得的结果是一个新的 Series,未被选中的低层索引值会被保留:

python
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 是按顺序排列的

python
pop.sort_index(inplace=True)
print(pop.loc['California':'New York'])

如果索引已经排序,那么可以用较低层级的索引取值,第一层级的 索引可以用空切片:

python
print(pop[:, 2000])

其他取值与数据选择的方法也都起作用。下面的例子是通过布尔掩码选择数据:

python
print('布尔掩码取值:\n', pop[pop > 22000000])

也可以用花哨的索引选择数据:

python
print('花哨索引取值:\n', pop[['California', 'Texas']])

DataFrame 多级索引

DataFrame 多级索引的用法与 Series 类似。还用之前的体检报告数据来演示

由于 DataFrame 的基本索引是列索引,因此 Series 中多级索引的用法到了 DataFrame 中就应用在列上了。例如,可以通过简单的操作获取 Guido 的心率数据:

python
print(health_data['Guido', 'HR'])

与单索引类似,loc、iloc 和 ix 索引器都可以使 用,例如:

python
print(health_data.iloc[:2, :2])

虽然这些索引器将多维数据当作二维数据处理,但是在 loc 和 iloc 中可以传递多个层级的索引元组,例如:

python
print(health_data.loc[:, ('Bob', 'HR')])

多级索引行列转换

使用多级索引的关键是掌握有效数据转换的方法。Pandas 提供了许多操作,可以让数据在内容保持不变的同时,按照需要进行行列转换。之前我们用一个简短的例子演示过 stack() 和 unstack() 的用法,但其实还有许多合理控制层级行列索引的方法,让我们来一探究竟。

索引 stackunstack

前文曾提过,我们可以将一个多级索引数据集转换成简单的二维形

式,可以通过 level 参数设置转换的索引层级:

python
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() 的逆操作,同时使用这两种方法让数据保持不变:

python
print(pop.unstack().stack())

索引的设置与重置

层级数据维度转换的另一种方法是行列标签转换,可以通过 reset_index 方法实现。如果在上面的人口数据 Series 中使用该方法,则会生成一个列标签中包含之前行索引标签 stateyear 的 DataFrame。也可以用数据的 name 属性为列设置名称:

python
"""索引的设置与重置"""
pop_flat = pop.reset_index(name='population')
print(pop_flat)

在解决实际问题的时候,如果能将类似这样的原始输入数据的列直接转换成 MultiIndex,通常将大有裨益。其实可以通过 DataFrame 的 set_index 方法实现,返回结果就会是一个带多级索引的 DataFrame:

python
print(pop_flat.set_index(['state', 'year']))

在实践中,我发现用这种重建索引的方法处理数据集非常好用。