当前位置: 首页 > news >正文

Pandas 分层索引

文章目录

  • Pandas 分层索引
    • 多重索引的 Series
      • 糟糕的方式
      • 更好的方式:Pandas 的 MultiIndex
      • 多重索引作为额外的维度
    • 多重索引的创建方法
      • 显式 MultiIndex 构造器
      • MultiIndex 层级命名
      • 列的 MultiIndex
    • 多重索引的索引与切片
      • 多重索引的 Series
      • 多重索引的 DataFrame
    • 重排多重索引
      • 已排序与未排序的索引
      • 索引的堆叠与反堆叠
      • 索引的设置与重置
    • 总结

Pandas 分层索引

conver
到目前为止,我们主要关注的是一维和二维数据,分别存储在 Pandas 的 SeriesDataFrame 对象中。
但很多时候,存储更高维度的数据——即由多个键索引的数据——是很有用的。
早期的 Pandas 版本提供了 PanelPanel4D 对象,可以被视为二维 DataFrame 的三维或四维类似物,但在实际使用中它们有些笨重。处理高维数据更常见的模式是利用分层索引(也称为多重索引),在单个索引中包含多个索引层级
通过这种方式,更高维度的数据可以紧凑地表示在熟悉的一维 Series 和二维 DataFrame 对象中。
(如果你对具有 Pandas 风格灵活索引的真正N维数组感兴趣,可以了解优秀的 Xarray 包。)

在本章中,我们将探讨 MultiIndex 对象的直接创建;在多重索引数据上进行索引、切片和统计计算时的注意事项;以及在简单和分层索引数据表示之间转换的实用方法。

我们从标准导入开始:

import pandas as pd
import numpy as np

多重索引的 Series

让我们从思考如何在一维 Series 中表示二维数据开始。
为了具体说明,我们将考虑一个数据序列,其中每个数据点都有一个字符和一个数字键。

糟糕的方式

假设你想跟踪两个不同年份的各州数据。利用我们已经介绍过的 Pandas 工具,你可能会倾向于直接使用 Python 元组作为键:

index = [('California', 2010), ('California', 2020),('New York', 2010), ('New York', 2020),('Texas', 2010), ('Texas', 2020)]
populations = [37253956, 39538223,19378102, 20201249,25145561, 29145505]
pop = pd.Series(populations, index=index)
pop
(California, 2010)    37253956
(California, 2020)    39538223
(New York, 2010)      19378102
(New York, 2020)      20201249
(Texas, 2010)         25145561
(Texas, 2020)         29145505
dtype: int64

使用这种索引方案,你可以直接根据元组索引对 Series 进行索引或切片:

pop[('California', 2020):('Texas', 2010)]
(California, 2020)    39538223
(New York, 2010)      19378102
(New York, 2020)      20201249
(Texas, 2010)         25145561
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 的 MultiIndex

幸运的是,Pandas 提供了更好的方法。
我们基于元组的索引本质上就是一种初级的多重索引,而 Pandas 的 MultiIndex 类型为我们提供了所需的各种操作。
我们可以如下通过元组创建一个多重索引:

index = pd.MultiIndex.from_tuples(index)

MultiIndex 表示多个索引层级——在本例中为州名和年份——以及为每个数据点编码这些层级的多个标签

如果我们用这个 MultiIndex 重新索引我们的 series,就可以看到数据的分层表示:

pop = pop.reindex(index)
pop
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
Texas       2010    251455612020    29145505
dtype: int64

在 Series 的表示中,前两列显示了多重索引的值,第三列显示了数据。
请注意,第一列中有些条目是空白的:在这种多重索引的表示中,任何空白条目都表示与上一行相同的值。

现在,如果我们想访问第二层索引为 2020 的所有数据,可以使用 Pandas 的切片语法:

pop[:, 2020]
California    39538223
New York      20201249
Texas         29145505
dtype: int64

结果是一个只包含我们感兴趣键的单层索引 Series。
这种语法比我们一开始用元组实现的多重索引方案要方便得多(操作效率也高得多)。
接下来我们将进一步讨论这种针对分层索引数据的索引操作。

多重索引作为额外的维度

你可能还注意到:我们其实可以用带有索引和列标签的普通 DataFrame 来存储相同的数据。
事实上,Pandas 的设计正是基于这种等价性。unstack 方法可以迅速将一个多重索引的 Series 转换为常规索引的 DataFrame

pop_df = pop.unstack()
pop_df
20102020
California3725395639538223
New York1937810220201249
Texas2514556129145505

自然而然,stack 方法则提供了相反的操作:

pop_df.stack()
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
Texas       2010    251455612020    29145505
dtype: int64

看到这里,你可能会想,为什么我们还要费心使用分层索引呢?
原因很简单:正如我们能够利用多重索引在一维的 Series 中操作二维数据一样,我们也可以用它在 SeriesDataFrame 中操作三维或更多维度的数据。
多重索引中的每增加一个层级,就代表数据的一个额外维度;利用这一特性,我们可以更灵活地表示各种类型的数据。具体来说,我们可能希望为每个州的每一年添加另一列人口统计数据(比如 18 岁以下人口);有了 MultiIndex,只需在 DataFrame 中添加一列即可轻松实现:

pop_df = pd.DataFrame({'total': pop,'under18': [9284094, 8898092,4318033, 4181528,6879014, 7432474]})
pop_df
totalunder18
California2010372539569284094
2020395382238898092
New York2010193781024318033
2020202012494181528
Texas2010251455616879014
2020291455057432474

此外,Pandas中的数据操作中讨论的所有 ufuncs 和其他功能同样适用于分层索引。
下面我们根据上述数据,按年份计算 18 岁以下人口所占比例:

f_u18 = pop_df['under18'] / pop_df['total']
f_u18.unstack()
20102020
California0.2492110.225050
New York0.2228310.206994
Texas0.2735680.255013

这使我们能够轻松快速地操作和探索高维数据。

多重索引的创建方法

构建多重索引的 SeriesDataFrame 最直接的方法,就是将两个或更多索引数组的列表直接传递给构造函数。例如:

df = pd.DataFrame(np.random.rand(4, 2),index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],columns=['data1', 'data2'])
df
data1data2
a10.6268330.939326
20.8249480.823318
b10.9538310.173091
20.3713340.779618

创建 MultiIndex 的工作是在后台完成的。

类似地,如果你传递一个以合适元组为键的字典,Pandas 会自动识别并默认使用 MultiIndex

data = {('California', 2010): 37253956,('California', 2020): 39538223,('New York', 2010): 19378102,('New York', 2020): 20201249,('Texas', 2010): 25145561,('Texas', 2020): 29145505}
pd.Series(data)
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
Texas       2010    251455612020    29145505
dtype: int64

尽管如此,有时显式创建一个 MultiIndex 也是很有用的;接下来我们将介绍几种实现这一目标的方法。

显式 MultiIndex 构造器

为了更灵活地构建索引,你可以使用 pd.MultiIndex 类中提供的构造方法。
例如,正如我们之前所做的,可以通过简单的数组列表(每个层级的索引值)来构造一个 MultiIndex

pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
MultiIndex([('a', 1),('a', 2),('b', 1),('b', 2)],)

或者,你可以通过一个元组列表(每个元组给出每个数据点的多重索引值)来构造它:

pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)])
MultiIndex([('a', 1),('a', 2),('b', 1),('b', 2)],)

你甚至可以通过单个索引的笛卡尔积来构造它:

pd.MultiIndex.from_product([['a', 'b'], [1, 2]])
MultiIndex([('a', 1),('a', 2),('b', 1),('b', 2)],)

类似地,你可以通过直接传递 levels(每个层级可用索引值的列表)和 codes(引用这些标签的编码列表)来使用其内部编码方式构造一个 MultiIndex

pd.MultiIndex(levels=[['a', 'b'], [1, 2]],codes=[[0, 0, 1, 1], [0, 1, 0, 1]])
MultiIndex([('a', 1),('a', 2),('b', 1),('b', 2)],)

这些对象中的任何一个都可以作为 index 参数传递给 SeriesDataFrame 的构造函数,或者传递给已有 SeriesDataFramereindex 方法。

MultiIndex 层级命名

有时候,为 MultiIndex 的各层级命名会很方便。
这可以通过向前面讨论过的任意一个 MultiIndex 构造器传递 names 参数来实现,或者事后设置索引的 names 属性也可以:

pop.index.names = ['state', 'year']
pop
state       year
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
Texas       2010    251455612020    29145505
dtype: int64

对于更复杂的数据集,这是一种有助于追踪各索引值含义的实用方法。

列的 MultiIndex

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)
health_data
subjectBobGuidoSue
typeHRTempHRTempHRTemp
yearvisit
2013139.036.136.037.325.037.4
243.036.133.038.337.037.7
2014160.036.69.036.937.037.5
243.037.324.037.641.034.8

这本质上是一个四维数据,其中维度分别是受试者、测量类型、年份和访问次数。
有了这样的结构,我们可以按人的名字索引顶层列,从而获得只包含该人信息的完整 DataFrame

health_data['Guido']
typeHRTemp
yearvisit
2013136.037.3
233.038.3
201419.036.9
224.037.6

多重索引的索引与切片

MultiIndex 进行索引和切片的设计非常直观,如果你将索引看作是增加的维度会更容易理解。
我们将首先介绍对多重索引的 Series 进行索引的方法,然后再介绍对多重索引的 DataFrame 对象进行索引的方法。

多重索引的 Series

来看我们之前见过的州人口多重索引 Series 示例:

pop
state       year
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
Texas       2010    251455612020    29145505
dtype: int64

我们可以通过使用多个索引项来访问单个元素:

pop['California', 2010]
np.int64(37253956)

MultiIndex 还支持部分索引,即只索引索引中的某一层级。
结果是另一个 Series,并保留低层级的索引:

pop['California']
year
2010    37253956
2020    39538223
dtype: int64

只要 MultiIndex 已排序,也可以进行部分切片:

pop.loc['California':'New York']
state       year
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
dtype: int64

对于已排序的索引,可以通过在第一个索引位置传递一个空切片,实现对低层级的部分索引:

pop[:, 2010]
state
California    37253956
New York      19378102
Texas         25145561
dtype: int64

其他类型的索引和选择(详见数据索引与选择)同样适用;例如,可以使用布尔掩码进行选择:

pop[pop > 22000000]
state       year
California  2010    372539562020    39538223
Texas       2010    251455612020    29145505
dtype: int64

花式索引(fancy indexing)同样适用:

pop[['California', 'Texas']]
state       year
California  2010    372539562020    39538223
Texas       2010    251455612020    29145505
dtype: int64

多重索引的 DataFrame

多重索引的 DataFrame 具有类似的行为。
来看我们之前的医疗数据玩具 DataFrame 示例:

health_data
subjectBobGuidoSue
typeHRTempHRTempHRTemp
yearvisit
2013139.036.136.037.325.037.4
243.036.133.038.337.037.7
2014160.036.69.036.937.037.5
243.037.324.037.641.034.8

请记住,在 DataFrame 中列是主要的索引轴,对于多重索引的 Series,所用的语法同样适用于列。
例如,我们可以通过一个简单的操作获取 Guido 的心率数据:

health_data['Guido', 'HR']
year  visit
2013  1        36.02        33.0
2014  1         9.02        24.0
Name: (Guido, HR), dtype: float64

同样地,和单层索引的情况一样,我们可以使用在数据索引与选择中介绍的 locilocix 索引器。例如:

health_data.iloc[:2, :2]
subjectBob
typeHRTemp
yearvisit
2013139.036.1
243.036.1

这些索引器为底层的二维数据提供了类似数组的视图,但在 lociloc 中,每个单独的索引都可以传递一个包含多个索引的元组。例如:

health_data.loc[:, ('Bob', 'HR')]
year  visit
2013  1        39.02        43.0
2014  1        60.02        43.0
Name: (Bob, HR), dtype: float64

在这些索引元组中使用切片并不是特别方便;尝试在元组中创建切片会导致语法错误:

health_data.loc[(:, 1), (:, 'HR')]
  Cell In[32], line 1health_data.loc[(:, 1), (:, 'HR')]^
SyntaxError: invalid syntax

你可以通过显式构建所需的切片(使用 Python 内置的 slice 函数)来实现这一点,但在这种情况下,更好的方法是使用 Pandas 专门为此场景提供的 IndexSlice 对象。
例如:

idx = pd.IndexSlice
health_data.loc[idx[:, 1], idx[:, 'HR']]
subjectBobGuidoSue
typeHRHRHR
yearvisit
2013139.036.025.0
2014160.09.037.0

如你所见,有许多方法可以与多重索引的 SeriesDataFrame 数据进行交互。正如本书中的许多工具一样,最好的熟悉方式就是亲自尝试!

重排多重索引

处理多重索引数据的关键之一是学会如何有效地转换数据。
有许多操作可以在不丢失任何信息的前提下,重新排列数据集,以便进行各种计算。
我们在 stackunstack 方法中已经简单见过这种操作,但实际上还有更多方法可以精细地控制分层索引与列之间的数据重排,下面我们将进行详细探讨。

已排序与未排序的索引

前面我曾简要提到一个注意事项,这里需要特别强调:
许多 MultiIndex 的切片操作如果索引未排序会失败。
让我们仔细看看这个问题。

我们先创建一些简单的多重索引数据,其中索引不是按字典序排序的:

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.2528572      0.342139
c     1      0.7324842      0.740275
b     1      0.4783202      0.624116
dtype: float64

如果我们尝试对这个索引进行部分切片,将会导致错误:

try:data['a':'b']
except KeyError as e:print("KeyError", e)
KeyError 'Key length (1) was greater than MultiIndex lexsort depth (0)'

虽然从错误信息中并不十分清楚,但这实际上是由于 MultiIndex 没有排序造成的。
出于多种原因,部分切片和其他类似操作要求 MultiIndex 的各层级必须是有序(即字典序)的。
Pandas 提供了许多便捷方法来进行这种排序,比如 DataFramesort_indexsortlevel 方法。
这里我们将使用最简单的 sort_index 方法:

data = data.sort_index()
data
char  int
a     1      0.2528572      0.342139
b     1      0.4783202      0.624116
c     1      0.7324842      0.740275
dtype: float64

通过这种方式对索引进行排序后,部分切片将如预期般工作:

data['a':'b']
char  int
a     1      0.2528572      0.342139
b     1      0.4783202      0.624116
dtype: float64

索引的堆叠与反堆叠

如前所述,我们可以将数据集从堆叠的多重索引转换为简单的二维表示,并可选指定要使用的层级:

pop.unstack(level=0)
stateCaliforniaNew YorkTexas
year
2010372539561937810225145561
2020395382232020124929145505
pop.unstack(level=1)
year20102020
state
California3725395639538223
New York1937810220201249
Texas2514556129145505

unstack 的反操作是 stack,在这里可以用来恢复原始的 series:

pop.unstack().stack()
state       year
California  2010    372539562020    39538223
New York    2010    193781022020    20201249
Texas       2010    251455612020    29145505
dtype: int64

索引的设置与重置

重排分层数据的另一种方式是将索引标签转换为列,这可以通过 reset_index 方法实现。
对人口字典调用该方法会得到一个包含 stateyear 列的 DataFrame,这些信息原本存储在索引中。
为了更清晰,我们还可以为数据列指定名称,使其在列表示中更易理解:

pop_flat = pop.reset_index(name='population')
pop_flat
stateyearpopulation
0California201037253956
1California202039538223
2New York201019378102
3New York202020201249
4Texas201025145561
5Texas202029145505

一种常见的模式是根据列的值构建 MultiIndex(多重索引)。
这可以通过 DataFrameset_index 方法实现,该方法会返回一个具有多级索引的 DataFrame

pop_flat.set_index(['state', 'year'])
population
stateyear
California201037253956
202039538223
New York201019378102
202020201249
Texas201025145561
202029145505

在实际操作中,这种类型的重新索引是探索真实世界数据集时最有用的模式之一。

总结

本笔记系统介绍了 Pandas 的分层索引(MultiIndex)及其在 Series 和 DataFrame 中的应用。内容涵盖了分层索引的创建方法(如 from_tuples、from_arrays、from_product 等)、索引和切片操作、索引排序、堆叠与反堆叠(stack/unstack)、索引与列之间的转换(reset_index/set_index),以及多重索引在实际数据分析中的优势。通过丰富的代码示例,展示了如何灵活、高效地处理高维数据,并强调了分层索引在数据探索和处理中的重要作用。

http://www.dtcms.com/a/323272.html

相关文章:

  • AI 大模型企业级应用落地挑战与解决方案
  • 机器翻译:需要了解的数学基础详解
  • BPMN编辑器技术实现总结AI时代的工作流编辑器
  • Ubuntu系统忘记密码怎么办?
  • 【机器学习深度学习】模型选型:如何根据现有设备选择合适的训练模型
  • 安全合规3--防火墙
  • 知识蒸馏 - 大语言模型知识蒸馏LLM-KD-Trainer 源码分析 KnowledgeDistillationTrainer类
  • 【动态数据源】⭐️@DS注解实现项目中多数据源的配置
  • 【QT】常⽤控件详解(六)多元素控件 QListWidget Table Widget Tree Widget
  • 【Avalonia】无开发者账号使用iOS真机调试跨平台应用
  • C++四种类型转换
  • Tiger任务管理系统-12
  • SpringBoot学习日记(二)
  • Day38 Dataset和Dataloader类
  • Git 核心概念与操作全指南(含工作区、暂存区、版本库详解)
  • VisionMoE本地部署的创新设计:从架构演进到高效实现
  • python的format易混淆的细节
  • Java 实现企业级服务器资源监控系统(含 SSH 执行 + 邮件通知 + Excel 报表)
  • 欧拉公式的意义
  • 202506 电子学会青少年等级考试机器人六级器人理论真题
  • 通用AGI到来,记忆仍需要一点旧颜色
  • 【狂飙AGI】2025年上半年中文大模型综合性测评
  • [已解决]VSCode右键菜单消失恢复
  • 用户需求调研后的信息如何整理
  • 大语言模型提示工程与应用:LLMs文本生成与数据标注实践
  • 需求管理流程规范
  • 强化学习概论(1)
  • Android 锁屏图标的大小修改
  • android15哪些广播可以会走冷启动或者用于保活呢?
  • 探索Trae:使用Trae CN爬取 Gitbook 电子书