前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Pandas图鉴(四):MultiIndex

Pandas图鉴(四):MultiIndex

作者头像
数据STUDIO
发布于 2023-09-04 05:06:09
发布于 2023-09-04 05:06:09
1.1K00
代码可运行
举报
文章被收录于专栏:数据STUDIO数据STUDIO
运行总次数:0
代码可运行
Pandas[1]是用Python分析数据的工业标准。只需敲几下键盘,就可以加载、过滤、重组和可视化数千兆字节的异质信息。它建立在NumPy库的基础上,借用了它的许多概念和语法约定,所以如果你对NumPy很熟悉,你会发现Pandas是一个相当熟悉的工具。即使你从未听说过NumPy,Pandas也可以让你在几乎没有编程背景的情况下轻松拿捏数据分析问题。

Pandas 给 NumPy 数组带来的两个关键特性是:

  1. 异质类型 —— 每一列都允许有自己的类型
  2. 索引 —— 提高指定列的查询速度

事实证明,这些功能足以使Pandas成为Excel和数据库的强大竞争者。

Polars[2]是Pandas最近的转世(用Rust编写,因此速度更快,它不再使用NumPy的引擎,但语法却非常相似,所以学习 Pandas 后对学习 Polars 帮助非常大。

Pandas 图鉴系列文章由四个部分组成:

我们将拆分成四个部分,依次呈现~建议关注和星标@公众号:数据STUDIO,精彩内容等你来~

Part 4. MultiIndex

剖析 MultiIndex

对于没有听说过Pandas的人来说,MultiIndex最直接的用法是使用第二个索引列作为第一个索引列的补充,可以更加独特地识别每一行。例如,为了区分不同州的城市,州名通常被附加到城市名上。(你知道美国有大约40个斯普林菲尔德吗?)在关系型数据库中,它被称为复合主键。

你可以在DataFrame从CSV解析出来后指定要包含在索引中的列,也可以直接作为read_csv的参数。

你也可以在事后用append=True将现有的级别追加到MultiIndex中,正如你在下图中看到的那样:

其实更典型的是Pandas,当有一些具有某种属性的对象时,特别是当它们随着时间的推移而演变时,就会代表多个维度。比如说:

  • 一个社会学调查的结果
  • 泰坦尼克号的数据集
  • 历史气象观测
  • 冠军排名的年表

这也被称为 "Panel data",而Pandas的名字就来源于此。

现在增加这样一个层面:

现在有一个四维空间,其中

  • 年形成一个(几乎连续的)维度
  • 城市名称沿第二条放置
  • 沿着第三条的州名,以及
  • 特定的城市属性("人口"、"密度"、"面积" 等)作为第四维度上的 "刻度线" 发挥作用。

下图说明了这一概念:

为了给对应列的维度名称留出空间,Pandas将整个标题向上移动:

rename_axis

Grouping

关于MultiIndex,首先要注意它并不是简单的分组。在其内部,它只是一个扁平的标签序列,如下图所示:

还可以通过对行标签进行排序来获得同样的groupby效果:

sort_index

你甚至可以通过设置一个相应的Pandas option 来完全禁用可视化分组:pd.options.display.multi_sparse=False

类型转换

Pandas (以及Python本身)对数字和字符串有区别,所以在数据类型没有被自动检测到的情况下,可以将数字转换为字符串:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pdi.set_level(df.columns, 0, pdi.get_level(df.columns, 0).astype('int'))

其实也可以用标准工具做同样的事情:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.columns = df.columns.set_levels(df.columns.level[0].astype(int), level=0)

在正确使用这些工具,我们首先需要了解什么是 levelscodes,而pdi允许你使用MultiIndex,就像level是普通的列表或NumPy数组一样。

levelscodes 是通过将某一级别的常规标签列表分解成,以加快像透视、连接等操作:

  • pdi.get_level(df, 0) == Int64Index([2010, 2010, 2020, 2020])
  • df.columns.level[0] == Int64Index([2010, 2020])
  • df.columns.codes[0] == Int64Index([0, 1, 0, 1] )

用多指标建立一个DataFrame

除了从CSV文件中读取和从现有的列中建立外,还有一些方法来创建MultiIndex。这些方法不太常用--主要用于测试和调试。

由于历史原因,使用Pandas自己表示的MultiIndex的最直观的方式并不可行。

这里的 levelscodes(现在)被认为是实施细节,不应该暴露给最终用户。

也许,建立MultiIndex的最简单的方法是如下:

rename_axis

这里也有个缺点,需要在单独的一行或单独的链式方法中分配层次的名称。有几个替代的构造函数将名字和标签捆绑在一起。

from_arrays, from_tupes

当层次形成有规律的结构时,可以指定关键元素,让Pandas自动交错,如下图:

from_product

上面列出的所有方法也适用于列。比如说:

用MultiIndex编制索引

通过MultiIndex访问DataFrame的好处是,可以很容易地一次引用所有层次(可能会省略内部层次),而且语法很好,很熟悉。

Columns - 通过常规方括号
行和单元格--使用.loc[]

现在,如果想选择俄勒冈州的所有城市,或者只留下有人口的那一列怎么办?Python的语法在这里施加了两个限制:

  1. 没有办法区分df['a', 'b']df[('a', 'b')]--它的处理方式是一样的,所以你不能只写df[:, 'Oregon']。否则,Pandas将永远不知道你指的是Oregon这一列还是Oregon第二层行。
  2. Python 只允许在方括号内使用冒号,不允许在小括号内使用,所以你不能写df.loc[(:, 'Oregon'), :]

警告! 这里不是一个有效的Pandas语法!只有在pdi.patch_mi_co()之后才有效。

这种语法的唯一缺点是,当使用两个索引器时,它会返回一个副本,所以你不能写df.mi[:, 'Oregon'].co['population'] = 10。有许多替代的索引器,其中一些允许这样的分配,但它们都有自己的奇怪的规则:

  1. 你可以将内层与外层互换,并使用括号。

swaplevel

因此,df[:,'population']可以用以下方法实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.swaplevel(axis=1)['population']

注意,这里不方便超过两层

  1. 你可以使用xs方法:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.xs('population', level=1, axis=1)

它感觉不够Pythonic,尤其是在选择多个层次时。

这个方法无法同时过滤行和列,所以名字xs(代表 "cross-section")背后的原因并不完全清楚。它不能用于设置值。

  1. 处理这种情况的首选方法是为pd.IndexSlice创建一个别名,并在.loc中使用它:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
idx=pd.IndexSlice; df.loc[:, idx[:, 'population']]

这更像是Pythonic的做法,但为了访问一个元素而必须使用别名,这多少是个负担(而且没有别名就太长了)。你可以同时选择行和列。

  1. 你可以学习如何使用slice来代替冒号。如果你知道a[3:10:2]==a[slice(3,10,2)],那么你可能也会理解下面的内容:df.loc[:, (slice(None), 'population')],但无论如何,它几乎无法阅读。它可以同时选择行和列。可写。

Pandas有很多方法可以用大括号来访问DataFrame的元素,但都不够方便,所以这里推荐采用另一种索引语法:

  1. .query方法的小型语言(它是唯一能够做'or'的方法,而不仅仅是'and'):
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.query('state=="Oregon" or city=="Portland"')

它既方便又快速,但缺乏IDE的支持(没有自动完成,没有语法高亮等),而且它只过滤行,不过滤列。这意味着你不能用它来实现df[:, 'population'],而不需要转置DataFrame(除非所有列都是相同的类型,否则会丢失类型)。

这里有一个所有MultiIndex索引方法的汇总表:

rw=读/写,ro=只读;'mi[]'和'co[]'是pdi的扩展。

它们中没有一个是完美的,但有些接近了。

Stacking and unstacking

Pandas并没有为列提供set_index。为列增加层次的一个常见方法是将现有的层次从索引中 "unstacking"出来:

tack, unstack

Pandas的stack与NumPy的stack非常不同。我们看看文档中对命名规则的描述:

"这个函数是通过类比来命名的,即一个集合被重新组织,从水平位置上的并排(DataFrame的列)到垂直方向上的堆叠(DataFrame的索引中)。"

Series有unstack,但没有stack,因为它已经被 stack 了。作为一维的,Series在不同情况下可以作为行向量或列向量,但通常被认为是列向量(例如DataFrame的列)。

比如说:

也可以通过名称或位置索引来指定要堆叠/取消堆叠的级别。在这个例子中,df.stack()df.stack(1)df.stack('year')产生了相同的结果,df1.unstack()df1.unstack(2)和df1.unstack('year')也是如此。目的地总是 "在最后一个级别之后",并且不可配置。如果需要把级别放在其他地方,可以使用df.swaplevel().sort_index()或者pdi.swap_level(df, sort=True)

列必须不包含重复的值才有资格进行 stackunstack时同样适用于索引):

如何防止 stack/unstack 的排序

stackunstack都有一个缺点,就是对结果的索引进行不可预知的排序。这有时可能会让人恼火,但这是在有大量缺失值时给出可预测结果的唯一方法。

考虑一下下面的例子。你希望一周中的哪几天以何种顺序出现在右表中?

你可以推测,如果约翰的周一站在约翰的周五的左边,那么'Mon'< 'Fri',同样,对西尔维娅来说,'Fri'<'Sun',所以结果应该是'Mon'<'Fri'<'Sun'。这是合法的,但是如果剩下的列是不同的顺序,例如'Mon'<'Fri''Tue'<'Fri'呢?或者'Mon'<'Fri''Wed'<'Sat'

好吧,一周并没有那么多天,Pandas可以根据先前的知识推断出顺序。但是,对于星期天应该站在一周的末尾还是开头,人类还没有得出决定性的结论。Pandas应该默认使用哪个顺序?阅读区域设置?而对于不那么琐碎的顺序,比如说,中国各省市的顺序,又该如何处理?

在这种情况下,Pandas所做的只是简单地按字母顺序排序,你可以看到下面:

虽然这是一个合理的默认值,但它仍然感觉不对。应该有一个解决方案!现在有了一个。它被称为CategoricalIndex。即使有些标签丢失了,它也会记住顺序。它最近被顺利地集成到Pandas工具链中。它唯一缺乏的是基础设施。它很难构建;它很脆弱(在某些操作中会退回到对象dtype),但它是完全可用的,而且pdi库有一些帮助工具来提高学习曲线。

例如,要告诉Pandas,比如说,持有产品的简单Index(如果需要把星期几解开,就不可避免地会被排序)的顺序,你需要写一些像df.index = pd.CategoricalIndex(df.index, df.index, sorted=True)这样可怕的东西。而对于MultiIndex来说,这就更显得矫情了。

pdi库有一个辅助函数locked(以及一个默认为inplace=True的别名lock),用于锁定某个MultiIndex级别的顺序,将该level提升到CategoricalIndex

level名称旁边的复选标记意味着该level被锁定。它可以通过pdi.vis(df)手动实现可视化,也可以通过pdi.vis_patch()对DataFrame的HTML表示进行猴子修补来自动实现。应用补丁后,只要在Jupyter单元格中写上df,就会显示所有锁定的level的复选标记。

locklocked在简单的情况下自动工作(如客户名称),但在更复杂的情况下需要用户的提示(如缺少日子的星期)。

level转换为CategoricalIndex后,在sort_index、stack、unstack、pivot、pivot_table等操作中保持原来的顺序。

不过,即使是通过df['new_col'] = 1添加一个列这样的简单操作也会破坏它。使用pdi.insert(df.columns, 0, 'new_col', 1)可以正确处理带有CategoricalIndex的级别。

操作levels

除了已经提到的方法之外,还有一些其他的方法:

  • pdi.get_level(obj, level_id)返回一个通过数字或名称引用的特定级别,适用于DataFrames、Series和MultiIndex,是df.columns.get_level_values的别名;
  • pdi.set_level(obj, level_id, labels) 用给定的数组(列表、NumPy数组、系列、索引等)替换一个关卡的标签,--在纯Pandas中没有直接的对应关系:
  • pdi.insert_level(obj, pos, labels, name)用给定的值添加一个关卡(必要时适当广播),--在纯Pandas中不容易做到;
  • pdi.drop_level(obj, level_id)从MultiIndex中删除指定的level(向df.droplevel添加inplace参数):
  • pdi.swap_levels(obj, src=-2, dst=-1) 交换两个level(默认为最里面的两个级别),将inplacesort参数添加到df.swaplevel
  • pdi.move_level(obj, src, dst)将一个特定的级别src移动到指定的位置dst(在纯Pandas中不能轻易完成):

除了上面提到的参数外,本节的所有函数都有以下参数:

  • axis=None,其中None表示DataFrame的 "" 和Series的 "index"(又称 "info"轴);
  • sort=False,可选择在操作后对相应的MultiIndex进行排序;
  • inplace=False,可选择执行原地操作(对单个索引不起作用,因为它是不可变的)。

上面的所有操作都是在传统意义上理解level这个词(level标签数与DataFrame中的列数相同),向最终用户隐藏index.label和index.code的机制。

在极少数情况下,当移动和交换单独的level是不够的,可以通过这个纯粹的Pandas调用,一次性重新排序所有的级别:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.columns = df.columns.reorder_levels(['M', 'L', 'K'] )

其中['M', 'L', 'K']是所需的level顺序。

一般来说,使用get_levelset_level来对标签进行必要的修正就足够了,但是如果想一次性对MultiIndex的所有层次进行转换,Pandas有一个(名字不明确的)函数rename,它接受一个dict或者一个函数:

rename

至于重命名level,它们的名字被存储在.names字段中。这个字段不支持直接赋值(为什么不呢):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.index.names[1] = 'x' # TypeErrorbut

可以作为一个整体被替换:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.index.names = ['z', 'x'] # ok

另外,也可以使用一个可连锁的rename_axis

当只需要重命名一个特定level时,语法如下:

或者如果想通过数字而不是名字来引用级别,可以使用df.index = df.index.set_names('z', level=0)pdi.rename_level(df, 'z', 0, axis=0)(这两种方法也可以通过名字来工作)。

将MultiIndex转换为flat的索引并将其恢复

方便的查询方法只解决了处理行中MultiIndex的复杂性。而且,尽管有所有的辅助函数,当一些棘手的Pandas函数返回列中的MultiIndex时,对初学者来说也会倍感厉害。所以,pdi库有以下内容:

  • join_levels(obj, sep='_', name=None)将所有的MultiIndex级别连接成一个索引。
  • split_level(obj, sep='_', names=None)将索引分割成一个多索引。

两者都有可选的 axisinplace 参数。

至于纯粹的Pandas解决方案,如下代码所示:

  • join levels:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.columns = ['_'.join(k) for k in df.columns.to_flat_index()]
  • split levels:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.columns = pd.MultiIndex.from_tuples(k.split('_') for k in df.columns)

多指标排序

由于MultiIndex是由多个层次组成的,所以排序比单个Index的排序要复杂一些。它仍然可以用sort_index方法来完成,但是可以通过以下参数来进一步微调:

要对列级进行排序,请指定 axis=1

将多索引DataFrame读入和写入磁盘

Pandas可以以完全自动化的方式将一个带有MultiIndex的DataFrame写入CSV文件:df.to_csv('df.csv')。然而,在读取这样的文件时,Pandas无法自动解析MultiIndex,需要用户提供一些提示。例如,要读取一个有三层高的列和四层宽的索引的DataFrame,你需要指定

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pd.read_csv('df.csv', header=[0,1,2], index_col=[0,1,2,3])

这意味着前三行包含了列的信息,后面每行的前四个字段包含了索引level(如果列中有多于一个level,你不能在 read_csv 中通过名字引用行级别,只能通过数字)。

手动解读MultiIndex列的层数并不方便,所以更好的办法是在将DataFrame保存为CSV之前,将所有的列头层数stack(),而在读取之后再将其unstack()

如果需要一个即用即走的解决方案,来研究一下二进制格式,比如Python pickle格式:

  • 直接:df.to_pickle('df.pkl'), pd.read_pickle('df.pkl')
  • 在Jupyter中使用魔法命令 %store df%store -r df(存储在 $HOME/.ipython/profile_default/db/autorestore)

这种格式小而快,但它只能从Python中访问。如果你需要与其他生态系统的互操作性,请关注更多的标准格式,如Excel格式(在读取MultiIndex时需要与read_csv一样的提示)。下面是代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
!pip install openpyxl
df.to_excel('df.xlsx')
df1 = pd.read_excel('df.xlsx', header=[0,1,2], index_col=[0,1,2,3])

Parquet[3]文件格式支持多索引DataFrame,没有任何提示(唯一的限制是所有列的标签必须是字符串),产生的文件更小,而且工作速度更快(见基准):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df.to_parquet('df.parquet')。
df1 = pd.read_parquet('df.parquet')

官方Pandas文档有一个表格[4],列出了所有~20种支持的格式。

多指标算术

在整体使用多索引DataFrame的操作中,适用与普通DataFrame相同的规则(见第三部分)。但处理单元格的子集有其自身的一些特殊性。

可以像下面这样简单地更新通过外部MultiIndex level引用的列的子集:

或者如果想保持原始数据的完整性

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
df1 = df.assign(population=df.population*10)

也可以用density=df.population/df.area来轻松获得人口密度。

但并不能用df.assign将结果分配到原始DataFrame中。

一种方法是将所有不相关的列索引层层叠加到行索引中,进行必要的计算,然后再将它们解叠回来(使用pdi.lock来保持原来的列顺序)。

或者,你也可以使用pdi.assign

pdi.assign有锁定顺序的意识,所以如果你给它提供一个锁定level的DataFrame这不会解锁它们,这样后续的stack/unstack等操作将保持原来的列和行的顺序。

[在这里](https://github.com/ZaxR/pandas_multiindex_tutorial/blob/master/Pandas MultiIndex Tutorial.ipynb "在这里")可以找到一个用巨大的MultiIndex处理现实生活中的销售数据集的好例子。

总而言之,Pandas是一个分析和处理数据的伟大工具。希望这篇文章能帮助你理解解决典型问题的 "方法" 和 "原因",并体会到Pandas库的真正价值和魅力。

参考资料

[1]

Pandas: https://pandas.pydata.org/

[2]

Polars: https://www.pola.rs/

[3]

Parquet: https://en.wikipedia.org/wiki/Apache_Parquet

[4]

表格: https://pandas.pydata.org/docs/user_guide/io.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据STUDIO 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Part 4. MultiIndex
  • Grouping
  • 类型转换
  • 用多指标建立一个DataFrame
  • 用MultiIndex编制索引
    • Columns - 通过常规方括号
    • 行和单元格--使用.loc[]
  • Stacking and unstacking
  • 如何防止 stack/unstack 的排序
  • 操作levels
  • 将MultiIndex转换为flat的索引并将其恢复
  • 多指标排序
  • 将多索引DataFrame读入和写入磁盘
  • 多指标算术
    • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档