MultiIndex / 高级索引#

本节涵盖 indexing with a MultiIndexother advanced indexing features

有关通用索引文档,请参阅 Indexing and Selecting Data

警告

对于设置操作,返回副本还是引用可能取决于上下文。这有时被称为“链式赋值”,应避免使用。请参阅 Returning a View versus Copy

有关一些高级策略,请参阅 cookbook

分层索引 (MultiIndex)#

分层/多级索引非常令人兴奋,因为它为一些相当复杂的数据分析和操作打开了大门,尤其是在处理更高维度数据时。本质上,它使您能够将任意维度的数据存储和操作在更低维的数据结构中,如 Series (1d) 和 DataFrame (2d)。

在本节中,我们将展示“分层”索引的确切含义,以及它如何与上面和先前各节中描述的所有 pandas 索引功能集成。稍后,在讨论 group bypivoting and reshaping data 时,我们将展示非平凡的应用,以说明它如何帮助构建用于分析的数据。

有关一些高级策略,请参阅 cookbook

创建 MultiIndex (分层索引) 对象#

MultiIndex 对象是标准 Index 对象的层级对应物,它通常在 pandas 对象中存储轴标签。您可以将 MultiIndex 视为一个元组数组,其中每个元组都是唯一的。MultiIndex 可以从一系列数组(使用 MultiIndex.from_arrays() )、一个元组数组(使用 MultiIndex.from_tuples() )、一组可迭代对象的交叉(使用 MultiIndex.from_product() )或 DataFrame (使用 MultiIndex.from_frame() )创建。当传递一个元组列表时,Index 构造函数会尝试返回一个 MultiIndex。下面的示例演示了初始化 MultiIndex 的不同方法。

当您想要两个可迭代对象中的所有配对时,使用 MultiIndex.from_product() 方法可能更简单:

您也可以直接从 DataFrame 构建 MultiIndex,使用 MultiIndex.from_frame() 方法。这是 MultiIndex.to_frame() 的一个补充方法。

作为一种方便,您可以直接将数组列表传递给 SeriesDataFrame,以自动构造 MultiIndex

所有 MultiIndex 构造函数都接受一个 names 参数,该参数存储级别本身的字符串名称。如果未提供名称,则分配 None

此索引可以支持 pandas 对象的任何轴,并且索引的 级别 数由您决定:

我们已经“稀疏化”了索引的较高级别,以使控制台输出更易于查看。请注意,索引的显示方式可以通过 pandas.set_options() 中的 multi_sparse 选项进行控制:

值得记住的是,没有任何东西可以阻止您将元组用作轴上的原子标签:

MultiIndex 之所以重要,是因为它可以让您执行分组、选择和重塑操作,正如我们将在下面和文档后续部分中所述。正如您将在后面的部分中看到的,您可能会发现自己在使用分层索引数据,而无需自己显式创建 MultiIndex。但是,在从文件加载数据时,您可能希望在准备数据集时生成自己的 MultiIndex

重建级别标签#

方法 get_level_values() 将返回特定级别上每个位置的标签向量:

带有 MultiIndex 的轴上的基本索引#

分层索引的一个重要特性是,您可以按标识数据中子组的“部分”标签选择数据。部分 选择以完全类似于在常规 DataFrame 中选择列的方式“删除”分层索引的级别:

有关如何在更深的级别上选择数据,请参阅 Cross-section with hierarchical index

定义的级别#

The MultiIndex keeps all the defined levels of an index, even if they are not actually used. When slicing an index, you may notice this. For example:

这样做是为了避免重新计算级别,以使切片变得非常高效。如果您只想查看使用的级别,可以使用 get_level_values() 方法。

要使用仅使用的级别重新构造 MultiIndex,可以使用 remove_unused_levels() 方法。

数据对齐和使用 reindex#

在具有 MultiIndex 的不同索引对象之间进行的运算将按您的预期工作;数据对齐将与元组索引的工作方式相同:

Series/DataFramesreindex() 方法可以与另一个 MultiIndex,甚至一个元组或数组列表一起调用:

使用分层索引的高级索引#

在语法上将 MultiIndex 集成到使用 .loc 的高级索引中有些挑战,但我们已尽一切努力做到这一点。通常,MultiIndex 键采用元组的形式。例如,以下内容如您所料:

请注意,在此示例中 df.loc['bar', 'two'] 也会起作用,但这种简写表示法通常可能导致歧义。

如果您还想使用 .loc 索引特定列,则必须使用如下元组:

您不必通过仅传递元组的第一个元素来指定 MultiIndex 的所有级别。例如,您可以使用“部分”索引来获取第一个级别中包含 bar 的所有元素,如下所示:

这是 df.loc[('bar',),]``(在此示例中等同于 ``df.loc['bar',])的更简洁的写法。

“部分”切片也效果很好。

您可以通过提供元组的切片来按值“范围”进行切片。

传递标签或元组列表的操作类似于重新索引:

备注

需要注意的是,在 pandas 中,元组和列表在索引方面的处理方式不同。元组被解释为一个多级键,而列表用于指定多个键。换句话说,元组是水平移动(遍历级别),列表是垂直移动(扫描级别)。

重要的是,元组列表索引了几个完整的 MultiIndex 键,而元组列表则引用了级别内的多个值:

使用切片器#

您可以通过提供多个索引器来切片 MultiIndex

您可以提供任何选择器,就像通过标签进行索引一样,请参阅 Selection by Label ,包括切片、标签列表、标签和布尔索引器。

您可以使用 slice(None) 选择*该*级别的所有内容。您无需指定所有*更深*级别,它们将默认为 slice(None)

一如既往,切片器的**两侧**都包含在内,因为这是标签索引。

警告

您应该在 .loc 说明符中指定所有轴,即**索引**和**列**的索引器。在某些模糊的情况下,传递的索引器可能会被误解为索引*两个*轴,而不是索引例如行中的 MultiIndex

您应该这样做:

df.loc[(slice("A1", "A3"), ...), :]  # noqa: E999

您**不**应该这样做:

df.loc[(slice("A1", "A3"), ...)]  # noqa: E999

使用切片、列表和标签进行基本的多索引切片。

您可以使用 pandas.IndexSlice 来方便地使用 : 而不是 slice(None) 实现更自然的语法。

使用此方法可以同时在多个轴上执行相当复杂的选择。

使用布尔索引器,您可以提供与*值*相关的选择。

您还可以为 .loc 指定 axis 参数,以在单个轴上解释传递的切片器。

此外,您还可以*使用*以下方法设置值。

您也可以使用可对齐对象的右侧。

交叉选择#

DataFramexs() 方法另外接受一个 level 参数,以便更轻松地选择 MultiIndex 特定级别的数据。

您还可以通过提供 axis 参数,使用 xs 选择列。

xs 还允许使用多个键进行选择。

您可以将 drop_level=False 传递给 xs,以保留所选的级别。

将上面的结果与使用 ``drop_level=True``(默认值)的结果进行比较。

高级重新索引和对齐#

使用 pandas 对象中的 reindex()align() 方法的 level 参数,可以方便地跨级别广播值。例如:

使用 swaplevel 交换级别#

swaplevel() 方法可以切换两个级别的顺序:

使用 reorder_levels 重新排序级别#

reorder_levels() 方法将 swaplevel 方法进行了泛化,允许您一步置换层次索引的级别:

重命名 IndexMultiIndex 的名称#

rename() 方法用于重命名 MultiIndex 的标签,通常用于重命名 DataFrame 的列。renamecolumns 参数允许指定一个字典,其中仅包含您希望重命名的列。

此方法也可用于重命名 DataFrame 主索引的特定标签。

rename_axis() 方法用于重命名 IndexMultiIndex 的名称。特别地,可以指定 MultiIndex 级别的名称,这在之后使用 reset_index()MultiIndex 的值移到列中时非常有用。

请注意,DataFrame 的列是一个索引,因此使用带有 columns 参数的 rename_axis 将会更改该索引的名称。

renamerename_axis 都支持指定字典、Series 或映射函数来将标签/名称映射到新值。

当直接使用 Index 对象而不是通过 DataFrame 时,可以使用 Index.set_names() 来更改名称。

您无法通过级别设置 MultiIndex 的名称。

请改用 Index.set_names()

MultiIndex 进行排序#

为了有效地索引和切片 MultiIndex ed 对象,需要对它们进行排序。与任何索引一样,您可以使用 sort_index()

如果 MultiIndex 级别已命名,您也可以将级别名称传递给 sort_index

在更高维度的对象上,您可以按级别对其他轴进行排序,前提是它们具有 MultiIndex

即使数据未排序,索引也会工作,但效率会相对低下(并显示 PerformanceWarning)。它还将返回数据的副本而不是视图:

此外 ,如果您尝试对未完全按词汇顺序排序的内容进行索引 ,这可能会引发 :

MultiIndex 上的 is_monotonic_increasing() 方法用于显示索引是否已排序 :

现在 ,选择就能按预期工作了 。

Take 方法#

与 NumPy ndarrays 类似 ,pandas 的 IndexSeriesDataFrame 也提供了 take() 方法 ,该方法沿指定轴在给定索引处检索元素 。给定的索引必须是整数索引位置的列表或 ndarray 。take 也接受负整数作为相对于对象末尾的位置 。

对于 DataFrame ,给定的索引应该是指定行或列位置的一维列表或 ndarray 。

需要注意的是 ,pandas 对象上的 take 方法不适用于布尔索引 ,并可能返回意外结果 。

最后 ,关于性能的一个小说明 ,由于 take 方法处理的输入范围较窄 ,因此它的性能可能比花式索引要好得多 。

索引类型#

我们在前面的章节中已经详细讨论了 MultiIndexDatetimeIndexPeriodIndex 的文档显示在 hereTimedeltaIndex 的文档显示在 here

在接下来的子部分中 ,我们将重点介绍其他一些索引类型 。

CategoricalIndex#

CategoricalIndex 是一种索引类型 ,用于支持带有重复项的索引 。它是一个围绕 Categorical 的容器 ,能够高效地存储和索引具有大量重复元素的索引 。

设置索引将创建一个 CategoricalIndex

使用 __getitem__/.iloc/.loc 进行索引的操作方式类似于带有重复项的 Index 。索引器 必须 存在于类别中 ,否则操作将引发 KeyError

CategoricalIndex 在索引后 得以保留

对索引进行排序将根据类别的顺序进行排序 (回想一下 ,我们使用 CategoricalDtype(list('cab')) 创建了索引 ,因此排序后的顺序是 cab )。

对索引进行分组操作也将保留索引的性质 。

重新索引操作将返回基于传入索引程序类型的索引 。传递列表将返回普通的 Index ;使用 Categorical 进行索引将返回 CategoricalIndex ,并根据 传入Categorical 数据类型的类别进行索引 。这允许我们对这些内容进行任意索引 ,即使是使用 不在 类别中的值 ,这与重新索引 任何 pandas 索引的方式类似 。

警告

CategoricalIndex 进行重塑和比较操作时 ,必须具有相同的类别 ,否则将引发 TypeError

RangeIndex#

RangeIndexIndex 的一个子类 ,它为所有 DataFrameSeries 对象提供了默认索引 。RangeIndexIndex 的优化版本 ,可以表示单调有序集 。这些类似于 Python 的 range typesRangeIndex 始终具有 int64 数据类型 。

RangeIndex 是所有 DataFrameSeries 对象的默认索引 :

RangeIndex 的行为类似于具有 int64 数据类型的 Index ,并且对于结果无法用 RangeIndex 表示但应具有整数数据类型的 RangeIndex 上的操作 ,将转换为 int64Index 。例如 :

IntervalIndex#

IntervalIndex 及其自身的数据类型 IntervalDtype 以及 Interval 标量类型 ,在 pandas 中为区间表示法提供了第一类支持 。

IntervalIndex 允许一些独特的索引 ,并且也用作 cut()qcut() 的类别的返回类型 。

使用 IntervalIndex 进行索引#

IntervalIndex 可用作 SeriesDataFrame 中的索引 。

通过 .loc 在区间边缘进行的基于标签的索引将按预期工作 ,选择该特定区间 。

如果您选择一个*包含*在区间内的标签,同样也会选中该区间。

使用 Interval 进行选择将只返回精确匹配。

尝试选择一个没有被精确包含在 IntervalIndex 中的 Interval 将会引发 KeyError

可以使用 overlaps() 方法来选择与给定 Interval 重叠的所有 Interval,从而创建一个布尔索引器。

使用 cutqcut 进行分箱#

cut()qcut() 都返回一个 Categorical 对象,并且它们创建的分箱会存储在 IntervalIndex.categories 属性中。

cut() 也接受将 IntervalIndex 作为其 bins 参数,这使得一个实用的 pandas 惯用法成为可能。首先,我们调用 cut() 处理一些数据,并将 bins 设置为一个固定的数字,以生成分箱。然后,我们将 .categories 的值作为 bins 参数传递给后续对 cut() 的调用,提供将要被分到相同分箱的新数据。

任何落在所有分箱之外的值都将被赋值为 NaN

生成区间范围#

如果我们需要的区间是规则频率的,我们可以使用 interval_range() 函数,通过 startendperiods 的各种组合来创建一个 IntervalIndexinterval_range 的默认频率对于数值区间是 1,对于类似日期的区间是日历日:

freq 参数可用于指定非默认频率,并且可以利用各种 frequency aliases 与类日期的区间一起使用:

此外,closed 参数可用于指定区间的闭合端。区间默认是右侧闭合的。

指定 startendperiods 将会生成一个从 startend``(包含两端)的等距区间范围,最终的 ``IntervalIndex 中包含 periods 个元素:

杂项索引 FAQ#

整数索引#

具有整数轴标签的基于标签的索引是一个棘手的问题。它在邮件列表和科学 Python 社区的成员中被广泛讨论过。在 pandas 中,我们的普遍观点是标签比整数位置更重要。因此,仅具有整数轴索引时,才可能使用标准工具(如 .loc)进行基于标签的索引。以下代码将引发异常:

这一刻意的决定是为了防止歧义和细微的错误(许多用户在 API 更改为停止“回退”到基于位置的索引时报告发现了错误)。

非单调索引需要精确匹配#

如果 SeriesDataFrame 的索引是单调递增或递减的,那么基于标签的切片边界可以超出索引的范围,这与切片普通 Python list 类似。可以通过 is_monotonic_increasing()is_monotonic_decreasing() 属性来测试索引的单调性。

另一方面,如果索引不是单调的,则切片边界必须是索引的*唯一*成员。

Index.is_monotonic_increasingIndex.is_monotonic_decreasing 只检查索引是否为弱单调。要检查严格单调,可以将其中一个与 is_unique() 属性结合使用。

端点是包含的#

与不包含切片端点的标准 Python 序列切片相比,pandas 中的基于标签的切片**是包含的**。最主要的原因是,在索引中,通常无法轻松确定特定标签的“后继”或下一个元素。例如,考虑以下 Series

假设我们希望从 c 切片到 e,使用整数可以这样完成:

然而,如果您只有 ce,确定索引中的下一个元素可能会比较复杂。例如,以下方法无效:

一个非常常见的用例是将时间序列限制在两个特定日期之间开始和结束。为了实现这一点,我们做出了基于标签的切片包含两个端点的设计选择:

这绝对是一种“实用性胜过纯粹性”的权衡,但如果你期望基于标签的切片能够像标准的 Python 整数切片那样工作,那么这是需要注意的一点。

索引可能会更改基础 Series 的 dtype#

不同的索引操作可能会更改 Series 的 dtype。

这是因为上述的(重新)索引操作会默默地插入 NaNs,并且 dtype 会相应地改变。在使用 numpy ufuncs``(如 ``numpy.logical_and)时,这可能会导致一些问题。

有关更详细的讨论,请参阅 GH 2388