6-3 stack与unstack:宽长之间的床位调整 本节摘要:unstack把层次索引的某一层掀到列上变宽表,stack把列名压回索引变长表,两者是纯搬运不聚合的形态转换。本节讲清层次索引这个操作台、层号与层名的指定法、以及与第05章melt的分工边界。 归并室的收官器械。5-1的分组结果和5-3的透视表都产出过层次索引,病人躺在"多层床"上,有时要把上铺的掀下来(unstack),有时要把摊开的收回去(stack)。它和melt一样做宽长转换,区别在操作对象:stack与unstack以层次索引为舞台,melt以普通列名为舞台。 unstack:掀上铺 unstack('渠道')用层名,unstack(0)或unstack(1)用层号(0最外层)。
本节摘要:unstack把层次索引的某一层掀到列上变宽表,stack把列名压回索引变长表,两者是纯搬运不聚合的形态转换。本节讲清层次索引这个操作台、层号与层名的指定法、以及与第05章melt的分工边界。
归并室的收官器械。5-1的分组结果和5-3的透视表都产出过层次索引,病人躺在"多层床"上,有时要把上铺的掀下来(unstack),有时要把摊开的收回去(stack)。它和melt一样做宽长转换,区别在操作对象:stack与unstack以层次索引为舞台,melt以普通列名为舞台。
import pandas as pd # 一张两层病床:门店-渠道双层索引的金额 s = pd.Series( [1280, 960, 1732, 880, 2100], index=pd.MultiIndex.from_product([['A', 'B', 'C'], ['线上', '线下', '']], names=['门店', '渠道'])) s = s[s.index.get_level_values('渠道') != ''] print(s) # 门店 渠道 # A 线上 1280 # 线下 960 # B 线上 1732 # C 线下 2100 ← 层次索引:数据是长形态 # unstack把内层(渠道)掀到列上 wide = s.unstack('渠道') print(wide) # 渠道 线上 线下 # 门店 # A 1280 960.0 # B 1732 NaN # C NaN 2100.0
unstack('渠道')用层名,unstack(0)或unstack(1)用层号(0最外层)。掀床的结果:行变少、列变多、没数据的格子自动补NaN。
# stack是逆操作:把列名压回索引层 long = wide.stack() print(long) # 门店 渠道 # A 线下 960.0 # 线上 1280.0 # B 线上 1732.0 # C 线下 2100.0 # 注意两个细节: # 一、stack默认丢弃NaN格(dropna=True),A线上B线下那种空格直接消失 # 二、值列因NaN的存在已是float,来回转换后dtype不还原 filled = wide.fillna(0).stack() print(filled) # A 线下 960.0 # 线上 1280.0 # B 线上 1732.0 # 线下 0.0 ← 0补位后保留 # C 线下 2100.0 # 线上 0.0
⚠️ 常见坑:stack默认悄悄丢掉NaN格子,且不发任何提示。想要"每个组合都占一格"的完整长表,先fillna再stack;反过来,想保持稀疏(没开张就是没记录),就利用这个默认行为。
df = pd.DataFrame({ '门店': ['A', 'A', 'B', 'B'], '渠道': ['线上', '线下', '线上', '线下'], '金额': [1280, 960, 1732, 880], }) pivot = df.pivot_table(index='门店', columns='渠道', values='金额', aggfunc='sum') # 层次索引版的长表:索引是门店-渠道两层 hi = pivot.stack().rename('金额').reset_index() print(hi) # 门店 渠道 金额 # 0 A 线下 960.0 # 1 A 线上 1280.0 # 2 B 线上 1732.0 # 3 B 线下 880.0 # 列也可以是多层的:unstack两次得到二维排版 multi = df.pivot_table(index='门店', columns='渠道', values='金额', aggfunc='sum') swapped = multi.T # 行列互换的快捷方式 print(swapped.index.name) # 渠道
| 维度 | stack与unstack | melt |
|---|---|---|
| 操作台 | 层次索引 | 普通列名 |
| 聚合 | 无,纯搬运 | 无,纯搬运 |
| 典型入口 | 分组或透视的产物 | 人手维护的宽报表 |
| NaN行为 | stack默认丢弃 | 保留为行 |
| 配套出口 | unstack | pivot_table |

床铺调整不是炫技,三类真实需求撑起它的出场率。其一,人看的对比表天然是宽的(门店对比渠道),机器吃的建模表天然是长的(每行一个观测),一份分析常常两头都要。其二,多指标相关性分析:各渠道金额在宽形态下才能一句corr算出相关矩阵。其三,入库与出库:数据库范式偏爱长形态,报表平台偏爱宽形态,ETL的中段就是在两种形态间来回摆渡。
# 宽形态的corr:一句话看出渠道间的联动 wide2 = df.pivot_table(index='门店', columns='渠道', values='金额', aggfunc='sum') print(wide2.corr().round(2)) # 渠道 线上 线下 # 渠道 # 线上 1.00 NaN # 样本太小,相关系数无意义,仅示意流程 # 线下 NaN 1.00
样本量放大后这张表才有诊断价值,但管道就是这两行。工具的意义在于规模变化时管道不用重写。
三台器械各自的验收点:merge——口径想清楚、indicator看了、行数对上了;concat——索引处理决定了、结构差异是个决定而不是意外;stack与unstack——NaN的丢弃与保留是知情选择。归并室输出的宽表,就是下一章时间轴要接手的病史正本。
归并室收工。下一章给病史装上时间轴,进入长期随访。