ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Pandas MultiIndex构造方法详解:from_tuples、from_arrays、from_product、from_frame实战指南

Pandas MultiIndex构造方法详解:from_tuples、from_arrays、from_product、from_frame实战指南 做数据处理时间长了你会发现真正让 pandas 从“Excel 替代品”变成“数据处理利器”的不是眼花缭乱的 API而是它对于索引Index的设计。尤其是多层索引 MultiIndex当你的数据维度从一维升到二维、三维时它能让表格结构既清晰又灵活。而构造 MultiIndex 的方法有四条路可以走from_tuples、from_arrays、from_product、from_frame。这四个函数名字简单但用起来细节很多选错方式轻则多写几行代码重则让你的索引顺序、层级名称全部乱套。这篇文章我会把这四种方式的适用场景、参数细节、避坑点一次讲透读完你就能根据手头的数据形态直接选对方案。如果你是刚接触 pandas 的初学者或者已经在用 set_index 但总觉得 MultiIndex 很绕的老手这篇文章都适合你。我会从 MultiIndex 到底能解决什么问题讲起再用真实数据演示四种构造方法最后整理我在实际项目中遇到过的经典坑帮你少走弯路。这四种方法本质上是在回答同一个问题你手头的原始数据是什么形状就选什么方式把它变成分层索引。1. MultiIndex 解决了什么问题四种构造方式该怎么选1.1 从一维索引到分层索引表格的维度变了大多数人对 pandas 的认知是从 Series 和 DataFrame 开始的索引就是一个由 0 到 N-1 或一组标签组成的序列。但现实中的数据往往是多层次的比如你有一份销售记录需要按“年份地区”来定位数据或者你有一份实验数据需要按“批次温度浓度”三个条件来索引。如果只用一列做索引剩下的维度只能放在数据列里要么用 groupby 临时聚合要么每次都要 set_index 切换。而 MultiIndex 做的事情就是把多个维度直接“升级”为索引层每一层都有自己的名字和取值。举个生活化的例子单层索引就像书架上的一层格子你只能定义一个分类MultiIndex 则是把书架分成“大类-子类-编号”三级每个格子都有一条唯一的定位路径。这种分层结构让数据在透视、切片、聚合时都更加顺手尤其是处理时间序列加分类变量这种复合维度时MultiIndex 几乎是标配。1.2 四种构造方法的选型逻辑对比pandas 官方提供了四个构造 MultiIndex 的类方法它们不是互斥的而是针对不同的“原料形态”。from_tuples 接收元组的列表每个元组对应索引的每一行from_arrays 接收多个数组每个数组代表一个层级from_product 接收多个可迭代对象自动求笛卡尔积生成全组合索引from_frame 则直接把一个 DataFrame 的列转换为索引层级。从数据形态来看它们分别对应“已有成对元组”“已有并列的多列数据”“需要全排列组合”“已有表格但想把某些列当索引”四种场景。为了让你一眼看清差别我把它们放到一起对比方法输入形态典型场景输出效果from_tuples元组列表数据库查询结果、手工构造的索引组合每个元组变成一行索引from_arrays多个数组/列表多列数据同时存在按位置对齐每个数组对应一个索引层级from_product多个可迭代对象实验设计、全因子的组合索引自动生成所有排列组合from_frameDataFrame已有表格希望若干列成为索引层级按列顺序生成多层索引选型有个很简单的原则你现有的数据是什么结构就选择参数形式最接近它的那个方法。如果你手里已经有了一份包含多列数据的 DataFrame却非要手动拼成元组列表再用 from_tuples那纯粹是给自己增加没有意义的代码量反过来如果你只是有两个列表想生成所有组合用 from_product 一行搞定完全没必要先展开成元组。这里还要提一个所有方法共有的“细节”它们的参数里都有 names用于指定每一层索引的名称。很多人构造完索引后忘了设置 names结果后续用 xs、unstack 的时候才发现层级没有名字切片和列名处理都会变得很别扭。我建议构造索引的同时就把 names 传好这是成本最低的规范操作。2. from_tuples从元组列表构建分层索引2.1 基础用法把每一对标签变成索引行from_tuples 的输入是最直观的一个由元组组成的列表每个元组代表一条索引。比如你要构造一个两级的索引一级是组别 A/B二级是序号 1/2代码是这样写的import pandas as pd idx pd.MultiIndex.from_tuples( [(A, 1), (A, 2), (B, 1), (B, 2)], names[组别, 序号] ) print(idx)输出结果MultiIndex([(A, 1), (A, 2), (B, 1), (B, 2)], names[组别, 序号])这个方法最适合的场景是你已经有一批现成的“对偶数据”。比如你在做爬虫时每一条记录本身就是一个 (分类, 子分类) 的结构或者你在对数据库查询结果做后处理查询出的每一行本身就是若干关键字段的组合。此时把结果转成列表套元组的形式直接丢进 from_tuples几乎不用动脑子。2.2 三个维度以上的元组也能处理很多教程喜欢拿二元组举例子容易让人误以为 from_tuples 只支持两层索引。其实元组的长度决定了索引的层数你甚至可以做四层、五层索引。比如空间数据里常用“时间站点高度”三层索引idx_3d pd.MultiIndex.from_tuples( [ (2024-01-01, 站点A, 10), (2024-01-01, 站点A, 20), (2024-01-01, 站点B, 10), (2024-01-01, 站点B, 20), ], names[时间, 站点, 高度] )这里要注意一个问题from_tuples 不会帮你检查层级之间是否等长因为每个元组本身长度一致即可。但如果你传入的元组列表里有元组长度不一致的情况pandas 会直接报错。这个错误信息有点隐蔽实测报的是ValueError: Input must be a list of tuples很容易让人误以为是类型问题实际是内部某个元组长度不同。遇到这种报错先检查元组的长度。2.3 参数 sortorder 值得你重视from_tuples 的完整签名是pd.MultiIndex.from_tuples(tuples, sortorderNone, namesNone)。第三个参数 names 比较常用但 sortorder 很少被人讲清楚。它的作用是声明索引是否已经排序以及排序到第几层。比如你传入的 tups 已经是按第一层排序好的就可以设置sortorder0这能避免后续某些排序操作重复执行节省时间。从我个人的实践经验看如果你只是构造一个几百行的索引sortorder 的设置与否对性能几乎无感。但当你处理的是上亿行的大数据时提前告诉 pandas “索引已经按某一层排好”绝对能省下可观的排序时间。我建议在构造时就明确设置 sortorder 为 -1表示不确定或不需要排序等实际排序时再交给 sort_index 统一处理避免模棱两可的状态。2.4 与 groupby 结果互相转换的实用技巧from_tuples 一个很常见但容易被忽略的用法是把 groupby 产生的 MultiIndex 重新利用起来。groupby 一个包含多个键的 DataFrame 时结果本身就是 MultiIndex例如df pd.DataFrame({ 年份: [2023, 2023, 2024, 2024], 地区: [华东, 华北, 华东, 华北], 销售额: [100, 120, 130, 140], }) grouped df.groupby([年份, 地区])[销售额].sum() print(grouped.index) # MultiIndex([(2023, 华东), (2023, 华北), (2024, 华东), (2024, 华北)])很多人习惯在 groupby 后立刻 reset_index 把索引展开成普通列但有些场合你就是需要索引本身。如果你手里已经拿到了一个元组列表比如从某个接口返回来的[(2023, 华东), ...]直接喂给 from_tuples 就能得到一个可复用的 MultiIndex 对象再跟原 DataFrame 做 reindex 对齐效果比用 for 循环一条条赋值快得多。这也是我经常用来做数据对齐的套路。3. from_arrays多列数组组合最适合观测数据3.1 核心用法每个数组代表一个层级from_arrays 的逻辑和 from_tuples 恰好是“转置”的。from_tuples 把每一行索引当作一个元组而 from_arrays 把每一层索引当作一个数组。比如上面的 A/1、A/2、B/1、B/2 四行索引用 from_arrays 写就是idx pd.MultiIndex.from_arrays( [[A, A, B, B], [1, 2, 1, 2]], names[组别, 序号] )这种方式特别适合你已经有多个等长的列表或数组并且它们天然按位置一一对应的情况。比如你从一个 CSV 读出了“城市”“月份”“销量”三列现在想把这些列变成索引层级直接把这个三列的列表传给 from_arrays 即可。它在数据预处理阶段的价值非常大因为你不需要先 zip 成元组再传给 from_tuples而是保持数据原有的“纵向列”形态直接使用。3.2 从 DataFrame 抽取列构造索引的实战场景在实际项目中from_arrays 最常见的用法之一是从 DataFrame 的若干列中抽出多列作为索引。比如有这么一份订单数据df pd.DataFrame({ 订单号: [1001, 1002, 1003, 1004], 客户类型: [企业, 个人, 企业, 个人], 支付方式: [支付宝, 微信, 微信, 支付宝], 金额: [500, 200, 800, 150], })现在你要按“客户类型支付方式”这两列构建一个 MultiIndex然后用它去索引或对齐另一份数据。直接的做法是idx_from_cols pd.MultiIndex.from_arrays( [df[客户类型], df[支付方式]], names[客户类型, 支付方式] )这里有个关键点from_arrays 是按位置对齐的也就是说两个数组第 i 个元素组合成第 i 行索引。这种方式保证了索引顺序和你原始数据的行顺序完全一致不会因为排序或去重而错位。如果你用 from_product 去生成同样两个列的所有组合得到的是笛卡尔积顺序就会和 df 的行对不上这一点在选型时要特别注意。3.3 names 参数与后续索引操作的联动from_arrays 构造出的 MultiIndex每一层级默认是没有名字的。如果你没有在构造时指定 names后续操作就要靠位置的“层级序号”来取比如index.get_level_values(0)非常不直观。我强烈建议凡是构造 MultiIndex 的地方都显式传 names。一旦 names 设置好你就可以用df.loc[idx, :]做切片或者用df.xs(A, level组别)按层级名取值。而且 MultiIndex 的 names 是可以在后期修改的比如idx idx.set_names([group, number])这会直接改动原索引对象的命名比重新构造一次要方便得多。实际调试代码时发现索引没名字可以用这一行补救不用回头改构造逻辑。3.4 两个数组长度不一致会怎样from_arrays 对数组长度是强校验的传入的每个数组长度必须相等否则会直接报错。这个报错很友好信息明确是长度不匹配。但有的时候你会遇到一种隐蔽情况某个数组里混入了缺失值长度不变但索引层级里出现了 NaN。这并不影响构造却会在后续合并、groupby 时引发很多怪异行为。我建议构造索引前先检查层级列是否有缺失值比如df[客户类型].isna().sum()有缺失值先决定好是填充还是删除别等索引建完才发现 loc 取值取不出来。这属于典型的“前期省事、后期填坑”踩过一次就不会忘了。4. from_product笛卡尔积生成全组合实验设计的最爱4.1 自动生成所有组合不用手动展开from_product 是我个人认为“魔法感”最强的一个方法。它接收多个可迭代对象然后自动计算笛卡尔积生成所有可能的组合。比如你有两组水平值想得到它们的完整组合索引idx pd.MultiIndex.from_product( [[A, B], [1, 2, 3]], names[组别, 序号] ) print(idx) # MultiIndex([(A, 1), (A, 2), (A, 3), # (B, 1), (B, 2), (B, 3)])这个方法的典型应用是实验设计。比如你在做一个 6 因子全因子实验每个因子有多个水平手工写组合列表会写到手软用 from_product 一行代码就能生成全部水平组合。再比如做网格搜索调参超参组合的索引也天然是笛卡尔积结构用它来构造索引做结果汇总非常合适。4.2 结合 reindex 补全缺失组合的经典套路我在实际业务中用到 from_product 最多的地方是数据补全。很多时候你拿到的明细数据并不是所有组合都有记录比如某些地区在某个月份没有销售数据但做报表时需要把缺失的组合也展示出来缺失值填 0 或 NaN。这时候 from_product reindex 就是绝配。举例sales pd.Series( [100, 120, 130], indexpd.MultiIndex.from_tuples( [(A, 1), (A, 2), (B, 1)], names[组别, 序号] ) ) full_index pd.MultiIndex.from_product([[A, B], [1, 2]], names[组别, 序号]) sales_complete sales.reindex(full_index, fill_value0) print(sales_complete)输出结果组别 序号 A 1 100 2 120 B 1 130 2 0 dtype: int64补全后报表里的行数就是你预先定义的全组合数量透视和画图时不用再担心某个组合的缺失导致图形断档。这种做法在生成周报、月报时尤其常见相当于先搭好索引骨架再把数据填充进去。4.3 顺序、重复值与层级的三个细节用 from_product 时有三个细节值得你注意。第一个是顺序默认情况下第一个可迭代对象是最外层变化最慢第二个是第二层变化最快。这与我们平时写嵌套循环的顺序一致但如果你习惯了 from_arrays 的“按位置对应”一开始会觉得顺序不可控实际上它是完全确定的。第二个是重复值from_product 不会自动去重。如果你传入的列表里有重复元素比如[A, A, B]生成的结果里会出现重复的索引组合。这在有些场景下是合理的比如同一批次做了两次实验但如果你本意是要全组合去重建议先pd.unique()或set()处理一下再传入否则后续通过索引取数据时会遇到多个相同标签很容易取出一条 Series 而不是标量。第三个是层级排序from_product 生成的是“行的逻辑顺序”这个顺序不一定是字典序。如果你想让索引按第一层字母序、第二层数字序重新排序可以用idx.sortlevel()或idx.sort_index()。注意 sortlevel 在某些 pandas 版本里有弃用警告推荐用df.sort_index(level[...])来处理避免兼容性问题。5. from_frame从 DataFrame 一键转索引最贴合真实数据5.1 从查询结果或清洗后的表格直接构造索引from_frame 是这四个方法里最“高维”的一个它的输入不是数组或元组而是一个 DataFrame。它的作用是把传入 DataFrame 的每一列当作一个层级生成对应的 MultiIndex。这个方法在处理表格型数据时非常省事你不必手动把列拆成列表再组装直接挑出需要的列传进去就行。举个例子df_meta pd.DataFrame({ 年份: [2023, 2023, 2024, 2024], 地区: [华东, 华北, 华东, 华北] }) idx_from_df pd.MultiIndex.from_frame(df_meta) print(idx_from_df)输出会是一个两层的 MultiIndex层级顺序就是传入 DataFrame 的列顺序。这里有个很多人忽略的地方from_frame 传进去的 DataFrame 会保持原来的列顺序如果你传入的列顺序和希望的层级顺序不一致要先做一次列重排再传。5.2 from_frame 与 set_index 的关键区别很多人会问既然 from_frame 能把 DataFrame 的列变成索引那和df.set_index([年份, 地区])有什么区别区别在于set_index 会修改原 DataFrame 的索引并且把这几列从数据列中挪走默认情况下你可以通过 drop 参数控制而 from_frame 只是“提取”列生成一个新的 MultiIndex 对象它不会修改传入的 DataFrame也不会改变原数据的行数。这两个方法对应的场景不同。如果你需要的是一个用于对齐、合并的独立索引对象用 from_frame如果你直接希望 df 这个表格变成按某几列索引的“主表”用 set_index 更自然。我在实际工作中经常遇到的情况是两份数据各自有一些关键字段需要构造一个 MultiIndex 来做对齐又不想改变原始表的索引结构这时候 from_frame 是比 set_index 安全得多的选择。5.3 列顺序、重复列名与数据类型的影响from_frame 虽然方便但也有几个不那么显眼的坑。第一传入的 DataFrame 如果有重复列名pandas 只会提取其中一列而且不会提示结果很容易懵。建议先df.columns.duplicated().sum()检查一下列名是否唯一。第二层级 dtype 会直接沿用原列的类型如果某列是 object 类型索引层级也是 object如果你希望它是 category 类型或者时间类型最好先转换列类型再构造索引这能显著提升后续分组排序的性能。第三也是比较实用的一点from_frame 支持传入一个已经被索引化的 DataFrame 吗可以但它会忽略原 DataFrame 的索引只提取列值。如果你希望把原索引也作为新索引的一部分需要先用reset_index()把索引变成普通列再调用 from_frame。这个操作在重新组合多个来源的数据时很常见。6. 完整案例从原始数据到多层索引透视6.1 一个销售数据的完整处理流程光讲 API 不落地没有意义我用一份模拟的销售明细数据把四种构造方法串起来演示一个完整流程。现有数据如下raw pd.DataFrame({ 日期: [2024-01-05, 2024-01-07, 2024-02-01, 2024-02-15, 2024-03-10, 2024-03-22], 城市: [北京, 上海, 北京, 上海, 杭州, 杭州], 品类: [数码, 家电, 数码, 家电, 数码, 家电], 销售额: [200, 450, 320, 680, 500, 300], })目标按“月份城市品类”做三层索引并统计每个月每个城市每个品类的销售额总和。第一步把日期转换为月份。第二步提取城市和品类列用 from_arrays 构造 MultiIndex因为这是观测数据不是全组合不能用 from_product。第三步拼接索引和销售额生成一个带 MultiIndex 的 DataFrame。第四步因为并不是每个月都有所有城市品类的组合用 from_product 生成全组合索引reindex 补全缺失记录。第五步用 sum(level...) 或 groupby(level...) 做统计。实现代码如下raw[月份] pd.to_datetime(raw[日期]).dt.to_period(M).astype(str) idx_obs pd.MultiIndex.from_arrays( [raw[月份], raw[城市], raw[品类]], names[月份, 城市, 品类] ) df_multi pd.DataFrame({销售额: raw[销售额].values}, indexidx_obs) full_idx pd.MultiIndex.from_product( [sorted(raw[月份].unique()), [北京, 上海, 杭州], [数码, 家电]], names[月份, 城市, 品类] ) df_complete df_multi.reindex(full_idx, fill_value0) print(df_complete)补全之后每个月份都会有 3 个城市 × 2 个品类共 6 条记录缺失的销售额自动填 0。这样在后续透视、画图、导出报表时结构非常规整不再有“缺行”的问题。这种“from_arrays 构建观测数据索引 from_product 构建全组合索引并 reindex 补全”的组合套路我几乎在每个报表项目里都会用到效率远高于手工维护一个完整组合列表。6.2 常用操作速查排序、交换层级、删除层级构造完 MultiIndex 之后后续操作才是重头戏。这里整理几个我几乎每天都会用到的操作做成速查表方便你直接抄操作目标推荐方式设置某一层索引名称df.index df.index.set_names(月份, level0)获取某层所有值df.index.get_level_values(城市)交换两层顺序df.swaplevel(城市, 品类)或df.index.swaplevel(0, 1)删除一层索引df.droplevel(品类)按某层排序df.sort_index(level[月份, 城市])按层级聚合求和df.groupby(level城市).sum()检查索引是否有重复df.index.is_unique这里要特别提一下is_unique。MultiIndex 和普通索引一样允许重复值但重复值存在时df.loc[key]返回的是 DataFrame 而不是 Series稍不留神就会把下游逻辑写错。所以我在写数据处理流程时会先检查这个属性确保索引唯一性再放心做 loc 取值。6.3 我踩过的几个坑和最终建议第一个坑是 names 没设置。以前我图省事构造 MultiIndex 时不传 names结果后面写xs(level城市)直接报错只能回头补 set_names。现在我把“构造索引必须带 names”当成肌肉记忆。第二个坑是 from_product 与 set_index 混用导致的顺序错乱。from_product 生成的是全组合顺序而 set_index 会保留原数据的行顺序两者如果都叫 MultiIndex但层级顺序和行顺序完全不同一旦把它们赋值到同一个变量上后续就很容易出现数据明明没变但绘图和透视结果对不上的诡异情况。我现在习惯在数据处理链路的每一步都打印一下 index 的前几行确认索引长什么样再继续。第三个坑是层级排序对性能的影响。pandas 在按 MultiIndex 取值时如果索引是无序的会退化成遍历匹配如果索引有序可以用二分查找速度天差地别。所以在数据量达到几十万行以上时我通常会做完构造后马上sort_index()一次让索引有序。最后给你一个实用建议建立一个“索引设计”的小习惯。每次在构造 DataFrame 之前先想清楚哪些列是维度哪些列是度量值然后直接构造好 MultiIndex 再填充数据。这样写出来的代码逻辑更清晰也不会在中途反复 set_index 打散结构再去恢复。MultiIndex 的四种构造方法各有擅长场景关键是先看手头数据的形状再决定用哪一把钥匙开门。
返回列表