2-3 多层索引:分诊台的科室分区 本节摘要:MultiIndex给行(或列)挂上多级标签,让"门店-月份"这类天然两级结构住进一张表。本节讲setindex多列升格、元组取数、IndexSlice跨层切片、getlevelvalues按层取值,以及swaplevel与xs这两个解层器械。 分诊台的桌子原来只有一列抽屉(单层索引),现在病人多了,按"门店-月份"两级分区才放得下。多层索引是Pandas从"一张平面表"升级到"结构化数据"的第一道门槛,也是第05章透视表、第06章stack操作的地基。 造一张两级分诊表 五条记录如今由(门店, 月份)这样的元组唯一标识。这就是多层索引的本质:挂号条从单个值变成元组。
本节摘要:MultiIndex给行(或列)挂上多级标签,让"门店-月份"这类天然两级结构住进一张表。本节讲set_index多列升格、元组取数、IndexSlice跨层切片、get_level_values按层取值,以及swaplevel与xs这两个解层器械。
分诊台的桌子原来只有一列抽屉(单层索引),现在病人多了,按"门店-月份"两级分区才放得下。多层索引是Pandas从"一张平面表"升级到"结构化数据"的第一道门槛,也是第05章透视表、第06章stack操作的地基。
import pandas as pd df = pd.DataFrame({ '门店': ['A', 'A', 'B', 'B', 'C'], '月份': ['1月', '2月', '1月', '2月', '1月'], '金额': [1280, 1350, 960, 1020, 880], }) mi = df.set_index(['门店', '月份']) # 两列同时升格,挂号条变成两级 print(mi) # 金额 # 门店 月份 # A 1月 1280 # 2月 1350 # B 1月 960 # 2月 1020 # C 1月 880 print(mi.index) # MultiIndex([('A', '1月'), ('A', '2月'), ('B', '1月'), # ('B', '2月'), ('C', '1月')], names=['门店', '月份'])
五条记录如今由(门店, 月份)这样的元组唯一标识。这就是多层索引的本质:挂号条从单个值变成元组。
# 完整元组:定位单行 print(mi.loc[('A', '1月')]) # 金额 1280 # Name: (A, 1月), dtype: int64 # 只给外层:叫出该门店整层 print(mi.loc['B']) # 金额 # 月份 # 1月 960 # 2月 1020 # 外层切片:双闭规则在层内仍然生效 print(mi.loc['A':'B']) # 金额 # 门店 月份 # A 1月 1280 # 2月 1350 # B 1月 960 # 2月 1020 # 内层切片必须借助IndexSlice,直接写loc['A', '1月':'2月']会歧义 idx = pd.IndexSlice print(mi.loc[idx[:, '2月'], :]) # 所有门店的2月 # 金额 # 门店 月份 # A 2月 1350 # B 2月 1020
# get_level_values:拿到整层标签做布尔条件 mask = mi.index.get_level_values('月份') == '2月' print(mi[mask].shape) # (2, 1) # xs:按某一层直接抽片,默认丢弃该层 print(mi.xs('2月', level='月份')) # 金额 # 门店 # A 1350 # B 1020 # swaplevel:两层互换,方便按另一层切片 swapped = mi.swaplevel() print(swapped.loc['2月']) # 金额 # 门店 # A 1350 # B 1020
⚠️ 常见坑:xs默认drop_level=True,抽出来的表少了那一层。想让层保留着继续多层操作,加drop_level=False。另外未排序的MultiIndex上做局部切片会抛UnsortedIndexError,先sort_index()再切,几乎总能解决。
# 报错现场与处置 # mi.loc['A':'C'] 若索引乱序 → UnsortedIndexError mi_sorted = mi.sort_index() print(mi_sorted.loc['A':'B'].shape) # (4, 1)

多层索引强大但语法税不轻,很多场景有更朴素的替代:groupby的聚合结果天然带层次索引,但加一句reset_index就回到普通宽表,后续布尔筛选、merge都更顺手。我的判断标准是层次索引是中间形态,普通列是协作形态:分析过程中用多层加速切片,交付给别人或接下游管线前摊平。一张表"长期住在多层索引里"通常说明结构选错了。
# 多层的聚合结果,两种归宿 agg = df.groupby(['门店', '月份'])['金额'].sum() pivoted = agg.unstack('月份').fillna(0) # 归宿一:透视成宽表 flat = agg.reset_index() # 归宿二:摊平成长表 print(flat) # 门店 月份 金额 # 0 A 1月 1280 # 1 A 2月 1350
一条完整的链路感受一下分区的价值:构造半年的门店月度数据,按两级分区后回答"B店3月到5月的走势"。
import pandas as pd months = [f'{m}月' for m in range(1, 7)] data = pd.DataFrame({ '门店': ['A'] * 6 + ['B'] * 6, '月份': months * 2, '金额': [1000+m*50 for m in range(6)] + [800+m*120 for m in range(6)], }) zone = data.set_index(['门店', '月份']).sort_index() idx = pd.IndexSlice print(zone.loc[idx['B', '3月':'5月'], '金额'].tolist()) # [1160, 1280, 1400]
建好分区再取数,一行完成"B店、3到5月、金额"的立体查询。平面表上等价的写法需要两层布尔条件,行数一多差距更明显。
分诊台的工作到此完备。下一章病人进入内科检查室:缺失值与重复记录,那才是脏数据真正的主战场。