6-2 concat:按轴堆叠的病史叠加 本节摘要:concat沿指定轴把多张表堆成一张:axis=0纵向摞行、axis=1横向拼列,join参数决定不对齐的列与索引怎么处理,ignoreindex重排挂号条。本节顺带交代append的退役始末。 merge是"按内容配对",concat是"按位置堆叠"。十二个月的分表摞成年度总表、订单表旁边拼上气温对照列,都是concat的活。它是堆叠,不问内容:键不参与任何匹配,这一点与merge划清界限。 纵向叠加:月表摞成年表 重号挂号条是纵向叠加的第一事故:后续loc[0]一次叫出两行。摞表默认带上ignoreindex,除非你要用keys追溯来源。 结构不一致时的取舍 outer保信息、inner保整齐。
本节摘要:concat沿指定轴把多张表堆成一张:axis=0纵向摞行、axis=1横向拼列,join参数决定不对齐的列与索引怎么处理,ignore_index重排挂号条。本节顺带交代append的退役始末。
merge是"按内容配对",concat是"按位置堆叠"。十二个月的分表摞成年度总表、订单表旁边拼上气温对照列,都是concat的活。它是堆叠,不问内容:键不参与任何匹配,这一点与merge划清界限。
import pandas as pd jan = pd.DataFrame({'门店': ['A', 'B'], '金额': [1280, 960]}) feb = pd.DataFrame({'门店': ['A', 'B'], '金额': [1150, 990]}) # axis=0 默认:行摞行,挂号条保留原样 stacked = pd.concat([jan, feb]) print(stacked) # 门店 金额 # 0 A 1280 # 1 B 960 # 0 A 1150 ← 挂号条重号了 # 1 B 990 # ignore_index=True:重发挂号条 print(pd.concat([jan, feb], ignore_index=True)) # 门店 金额 # 0 A 1280 # 1 B 960 # 2 A 1150 # 3 B 990 # keys参数:给每张来源表挂"上级标签",做多级索引 tagged = pd.concat([jan, feb], keys=['1月', '2月']) print(tagged.loc['2月']) # 门店 金额 # 0 A 1150 # 1 B 990
重号挂号条是纵向叠加的第一事故:后续loc[0]一次叫出两行。摞表默认带上ignore_index,除非你要用keys追溯来源。
# 2月表多了一列"渠道" feb2 = pd.DataFrame({'门店': ['A', 'B'], '金额': [1150, 990], '渠道': ['线上', '线下']}) # join='outer'默认:列取并集,缺的补NaN print(pd.concat([jan, feb2], ignore_index=True)) # 门店 金额 渠道 # 0 A 1280 NaN # 1 B 960 NaN # 2 A 1150 线上 # 3 B 990 线下 # join='inner':只留公共列 print(pd.concat([jan, feb2], ignore_index=True, join='inner')) # 门店 金额 # 0 A 1280 # 1 B 960 # 2 A 1150 # 3 B 990
outer保信息、inner保整齐。报表口径求整齐用inner,原始归档怕丢字段用outer——同一对参数,两种档案观。
# axis=1:列挨着列放,按挂号条对齐! weather = pd.DataFrame({'气温': [3, 8]}, index=[0, 1]) print(pd.concat([jan, weather], axis=1)) # 门店 金额 气温 # 0 A 1280 3 # 1 B 960 8 # 挂号条不同就对不上,这是axis=1的第一坑 odd = pd.DataFrame({'湿度': [40]}, index=[1]) print(pd.concat([jan, odd], axis=1)) # 门店 金额 湿度 # 0 A 1280 NaN ← 0号没有湿度 # 1 B 960 40.0
axis=1的"对齐键"是索引本身。日期对不上、重号没重排,拼出来的表就错位。拼列前先确认两边的索引含义一致——时间序列场景里这一条尤其要命,第07章会反复遇到。
老代码里常见的df.append(other)在Pandas 1.4被标记废弃、2.0彻底移除。它每次调用都复制整表,循环里append一百次等于复制一百次全量数据。迁移方案一行:
# 旧:result = df1.append(df2) result = pd.concat([df1, df2], ignore_index=True) # 循环攒表的正确姿势:先攒列表,最后一次concat frames = [] for month in range(1, 13): frames.append(load_month(month)) # 各月分表 year = pd.concat(frames, ignore_index=True) print(len(frames), '张月表摞成1张年表')
⚠️ 常见坑:循环里逐次concat虽然比append好些,仍是每次都复制。列表攒齐再一次concat,千张分表能从分钟级降到秒级——这也是第08章性能话题的前菜。
| 维度 | merge | concat |
|---|---|---|
| 匹配依据 | 键的内容 | 位置或索引 |
| 方向 | 横向为主 | 两轴皆可 |
| 典型场景 | 订单接会员属性 | 月表摞年表 |
| 行数风险 | 多对多膨胀 | 几乎不涨(axis=0) |
| 对齐失控点 | 脏键静默丢行 | 索引错位错列 |

十二张月表逐月人工产出,结构漂移几乎必然:某月加了备注列、某月列名改了中文名。摞表前先做一次结构对账,把各表的列集合亮出来比对:
frames = [jan, feb2] # 假设共两张 all_cols = {} for i, f in enumerate(frames): all_cols[i] = set(f.columns) common = set.intersection(*all_cols.values()) union = set.union(*all_cols.values()) print('公共列:', common, '差异列:', union - common) # 公共列: {'门店', '金额'} 差异列: {'渠道'}
差异列的存在不代表必须丢弃,但必须是一个决定而不是一个意外:要么outer保信息、要么inner求整齐,选完在注释里写一句为什么。数据工程的纪律感,多半体现在这些"多写两行让未来的人看懂"的地方。
下一节是归并室的最后一把器械:stack与unstack的床位调整。