Pandas索引机制
pandas 相比 numpy ,一个很重要的特点就在于它引入了显式的索引机制。显式的索引在方便数据获取的同时,也可能造成学习上的困惑。接下来首先详细介绍 pandas 的索引机制。
Series与索引
之前说过,Series
对象可以看作一种字典,它提供了索引与值对的映射,因此可以使用字典一样的方式获取值:
Series
的许多操作都和 Python 字典很像,例如可以通过 item assignment 增加新的索引-值对,这等价于向 Series
添加新的一项:
这种自定义的索引是显式的,它是真实存在的,因此可以向字典一样获取所有的索引-值对:
Series
不仅有着和字典一样的索引操作,还具备和 numpy 数组一样的数组数据选择功能,例如数组索引:
这么看来,Series
的索引除了允许自定义外,和 ndarray
的索引好像没什么区别。不过注意,Series
的索引是允许重复的,这可能会导致一次性获取到多个值:
此外,这种索引在用作切片时,得到的结果将包含后端的值:
这样做的好处是不用明白它后一项的索引是什么。但如果索引有重复的话,将不能用于切片操作:
这种索引机制可能会导致数据获取的不便。但实际上,Series
依然保留了 numpy 数组从零开始、切片时前闭后开的隐式索引:
这两种索引方式很容易造成混淆,尤其是使用自定义整数作索引时,它可能会覆盖隐式索引,使得某些操作失效:
因此 pandas 提供了一些索引器作为取值的方法,它们是 Series
对象暴露取值与切片接口的属性。
第一种索引器是 .loc
属性,表示用的是自定义、可重复、类型不限、切片时包含两端的显式索引:
第二种索引器是 .iloc
属性,表示用的是从 0 开始、切片前闭后开的整数隐式(implicit)索引:
这两种索引器独立工作,不能混用,因此可以各自用于需要的场景中。
DataFrame与索引
之前说过 DataFrame
也可以看作一种字典,它提供了列索引与 Series
对的映射,因此可以使用字典一样的方式由列索引获取一个 Series
:
units | unitcost | |
---|---|---|
pencil | 95 | 1.99 |
binder | 30 | 19.99 |
paperclip | 81 | 4.99 |
和前面介绍的 Series
对象一样,也可以用 item assignment 增加一列:
units | unitcost | total | |
---|---|---|---|
pencil | 95 | 1.99 | 189.05 |
binder | 30 | 19.99 | 599.70 |
paperclip | 81 | 4.99 | 404.19 |
因此对列索引而言,它和 Series
的索引机制比较像。但是由于 DataFrame
行列都有索引,因此单级的显式索引只能作用于列,否则操作很容易引起歧义。
除此之外,直接对行或列应用隐式索引会引起错误。从概念上来说,对行和列的隐式索引容易存在误解:如果将 DataFrame
看作结构数组,那么一列就代表一个结构成员,列与列之间并没有严格的先后关系,直接取第几列这种操作无法让人明白其意图。而行虽然没有这种误解,但是会产生一个更关键的问题:直接取某一行使得行索引不再被用到而丢弃,返回一个 Series
,但是 Series
要求所有元素的类型一致,而一个结构各成员间往往有着各自各样的类型,强行统一它们的类型会造成类型提升,为后续操作带来更多问题。
一种特殊的情况是切片。切片将会保留行索引,得到的仍然是一个 DataFrame
。如果切片涉及的范围只有一行,那么就基本等价于获取 DataFrame
的某一行(虽然得到的仍然是一个二维数组):
units | unitcost | total | |
---|---|---|---|
pencil | 95 | 1.99 | 189.05 |
因此,除了对列应用显式索引外,其它形式的索引不仅应该使用索引器,而且应该使用 numpy 高维数组的索引方式。
例如,以下使用隐式索引器获取 DataFrame
的元素。这里在代表取值的方括号内传入了一个元组,第一个元素指代行的隐式索引,第二个元素指代列的隐式索引:
根据隐式索引的规则,获取的应该是第 3 行第 2 列位置的元素。
再如,以下使用显式索引器得到指定几行的元素。这里对行应用数组索引,对列使用单个冒号 :
表示全部切片:
units | unitcost | total | |
---|---|---|---|
pencil | 95 | 1.99 | 189.05 |
paperclip | 81 | 4.99 | 404.19 |
最后,下图总结了 DataFrame
的索引:
层级索引
层级索引的概念
通过之前的介绍可以认识到,DataFrame
是一种二维的结构。但有些时候,处理的数据可能不止两个维度。例如,在操作 Excel 时,经常可以看到这样的表格:
2021 | 2022 | ||||
---|---|---|---|---|---|
mid term | end of term | mid term | end of term | ||
grade 1 | class 1 | 86 | 88 | 89 | 90 |
class 2 | 88 | 87 | 91 | 89 | |
class 3 | 84 | 86 | 86 | 85 | |
grade 2 | class 1 | 86 | 94 | 90 | 91 |
class 2 | 85 | 84 | 87 | 91 | |
class 3 | 87 | 91 | 90 | 90 |
这种数据可以从四个维度聚合:对列来说,可以得出每个年度的得分平均值,也可以得出历年期中和期末的得分平均值;对行也是同理。只凭借二维数据无法实现这样的关系,这时就需要使用层级索引。层级索引可以从多个角度来描述数据的分组。
pandas 中的索引类型不仅限于数值和字符串,甚至还能使用元组,例如:
元组表示存储了多个值,是多级索引的基础。pandas 的 MultiIndex
类提供了更丰富的操作方法。可以用它的类方法从元组创建一个多级索引:
通过 names
参数可以为这两个层级指定名称,方面区分各索引层。层级名称会保存到索引对象的 .names
属性中。
如果将前面创建的 Series
对象使用 .reindex()
方法将它的索引重置为 MultiIndex
对象,就会看到一个层级索引结构:
关于层级索引,需要记住的是:层级索引可以看作一个元素对应多个索引,或者说一个索引元组。如果检查层级索引的 .values
属性,会发现每个索引都使用多个值来描述:
因此在获取元素的时候,也需要通过多个值,或者说一个元组来获取:
多个值或一个元组构成的索引也可以用于切片。除了索引由一个值变成一个元组外,均遵循一维 Series
的切片规则,例如可以使用显式索引器 .loc
:
显式索引器使切片包含两端的元素。返回检查层级索引的 .values
属性可以发现,包含两端的元素确实是 3 个。
这里需要注意,如果层级索引不是有序的,那么大多数切片操作都会失败。以下演示一种会导致错误的操作:
问题出在切片和许多其它相似的操作都要求 MultiIndex
的各级索引是有序的。为此,pandas 提供了一些操作可以实现对索引的排序,最简单的方法是 .sort_index()
:
经过索引排序后的切片结果就正常了。这里再次使用 inplace
参数来提醒默认情况下排序后得到的是一个新的对象,而不是在原有对象的基础上做修改。
层级索引相比普通的索引,索引类型由一个值变为多个值(或者说一个元组)。这看似多此一举,但是它允许从不同层面来处理一维的数据。如果访问层级索引的 .level
属性,可以得到:
这说明该层级索引有两层:从索引的角度看,第一层有 2 种不同的索引,第二层有 3 种不同的索引;从数据的角度看,根据第一层索引可以将数据分为 2 类,根据第二层可以将数据分为 3 类。因此数据在聚合、变换时,可以根据不同的索引层级,从不同的角度处理。例如,对于以上具有层级索引的 Series
,可以统计每个 "class"
的数值平均值:
新版本的 pandas 可能已经弃用了这种使用方式,或者抛出 FutureWarning
,提示说应该使用对表作分组计算后再合并,这就是以后介绍的内容了。
具有层级索引的 Series
很像一个 DataFrame
。事实上,使用对象的 .unstack()
方法可以将一个多级索引的 Series
转化为普通索引的 DataFrame
:
group | 1 | 2 | 3 | |
---|---|---|---|---|
class | ||||
A | 1341 | 1412 | 1263 | |
B | 643 | 632 | 685 |
或者使用 .stack()
方法实现相反的效果,将一个 DataFrame
变成具有多级索引的 Series
。 既然可以用含多级索引的一维 Series
数据表示二维数据,那么就可以用 Series
或 DataFrame
表示三维甚至更高维度的数据。借助多级索引,可以使三维及以上的数据以一种较为易读的形式表示出来。层级索引每增加一层,就表示数据增加一维,使得 DataFrame
可以表示任意维度的数据。因此 pandas 并没有提供三维及以上的数量类型。
DataFrame与层级索引
在 DataFrame
使用层级索引和在 Series
上使用层级索引是一致的,只不过列索引和行索引都可以设置为层级索引。
以下创建一个较为复杂的、行列都具有两级索引的 DataFrame
用于演示:
name | Tim | Mary | John | ||||
---|---|---|---|---|---|---|---|
subject | math | physics | math | physics | math | physics | |
year | term | ||||||
2020 | 1 | 92 | 74 | 53 | 58 | 50 | 71 |
2 | 69 | 60 | 93 | 91 | 60 | 71 | |
2021 | 1 | 88 | 82 | 70 | 94 | 79 | 89 |
2 | 64 | 76 | 67 | 76 | 72 | 52 |
对 DataFrame
索引和 Series
基本一致,需要通过元组形式的索引来获取一个 Series
,并会保留行的层级索引:
索引器和切片的用法都是一致的:
name | John | Mary | ||
---|---|---|---|---|
subject | math | math | ||
year | term | |||
2020 | 1 | 50 | 53 | |
2 | 60 | 93 |
不过这种索引元组的用法不是很方便,因为这个 DataFrame
实际上可以看作四维数据,但是只能在两个维度上切片。如果想获取所有人在第 1 学期的数学成绩,那么可能需要这样的索引:
这是错误的用法,它会直接导致解释出错。为此,pandas 提供了 IndexSlice
对象,专门用来解决高维 DataFrame
的切片问题,例如:
name | Tim | Mary | John | |
---|---|---|---|---|
subject | math | math | math | |
year | term | |||
2020 | 1 | 92 | 53 | 50 |
2021 | 1 | 88 | 70 | 79 |
下图总结了 DataFrame
的层级索引: