1. 为什么 concat 是数据合并里最容易被低估的一环刚入行做数据分析那会儿我对pd.concat的态度就是能用就行。反正merge才是重头戏concat不就是把几个表摞在一起吗直到有一次处理一批按月份分文件存储的日志数据12 个 CSV 文件每个大概几十万行我随手写了个循环append跑了将近四分钟才出结果内存还差点爆掉。后来换成一次concat同样的数据量不到两秒。那次之后我才认真去翻文档发现concat远不是摞表格这么简单——它的axis、join、keys、ignore_index这几个参数组合起来能覆盖的场景比我想象的多得多。这篇内容我想把pandas concat从会用讲到用对。核心围绕几个问题展开concat到底在什么维度上做拼接、join参数在行方向和列方向上的行为差异、keys和MultiIndex怎么配合做来源标记、以及实际项目中那些文档不会告诉你的坑。适合已经能写基础 pandas 代码、但在多表合并时经常遇到列错位、索引重复、性能瓶颈的读者。如果你还在用append或者循环拼接这篇应该能帮你省下不少时间。先说一个基本认知concat的本质是沿某个轴对齐后拼接。它不像merge那样基于键做匹配而是把多个对象的行或列直接接在一起对齐的依据是索引行方向或列名列方向。理解这一点后面所有的参数行为都能推导出来。2. concat 的轴选择与对齐逻辑行拼接和列拼接到底差在哪2.1 axis0 与 axis1 的语义差异concat最核心的参数就是axis。默认axis0表示沿行方向拼接也就是把多个 DataFrame 上下摞起来axis1表示沿列方向拼接把多个 DataFrame 左右并排。但这里有个容易混淆的点axis0时对齐的是列名axis1时对齐的是行索引。很多人第一次遇到列错位就是因为没意识到这一点。import pandas as pd df1 pd.DataFrame({A: [1, 2], B: [3, 4]}) df2 pd.DataFrame({B: [5, 6], C: [7, 8]}) # axis0按列名对齐缺失的列补 NaN result pd.concat([df1, df2], axis0) print(result)输出会是A B C 0 1.0 3 NaN 1 2.0 4 NaN 0 NaN 5 7.0 1 NaN 6 8.0注意df1没有C列df2没有A列拼接后各自缺失的位置填了NaN。这就是按列名对齐的含义——concat会取所有列的并集然后逐个填充。axis1的情况类似只不过对齐依据换成了行索引df3 pd.DataFrame({X: [1, 2]}, index[0, 1]) df4 pd.DataFrame({Y: [3, 4]}, index[1, 2]) result pd.concat([df3, df4], axis1)df3的索引是[0, 1]df4是[1, 2]拼接后索引取并集[0, 1, 2]df3在索引 2 处没有数据df4在索引 0 处没有数据对应位置都是NaN。2.2 join 参数inner 和 outer 的实际影响上面默认的行为其实是joinouter取并集。如果你只想保留两边都有的部分用joininner# axis0, joininner只保留公共列 pd.concat([df1, df2], axis0, joininner) # 结果只有 B 列 # axis1, joininner只保留公共索引 pd.concat([df3, df4], axis1, joininner) # 结果只有索引 1这里有个实战经验做数据清洗时joininner往往比outer更安全。因为outer会悄悄引入大量NaN如果后续没做缺失值检查很容易在聚合或建模时出问题。我一般会在拼接后立刻跑一次result.isnull().sum()看看每列缺了多少心里有数再决定怎么处理。2.3 索引重复问题与 ignore_index行拼接时如果两个 DataFrame 的索引都是默认的0, 1, 2...拼完之后索引会重复。这在后续用loc取值或者做groupby时会造成困扰。df5 pd.DataFrame({A: [1, 2]}) df6 pd.DataFrame({A: [3, 4]}) pd.concat([df5, df6]) # 索引是 0, 1, 0, 1 pd.concat([df5, df6], ignore_indexTrue) # 索引重置为 0, 1, 2, 3ignore_indexTrue会丢弃原索引生成新的连续整数索引。什么时候该用我的判断标准是如果原索引没有业务含义就重置。比如从多个 CSV 读进来的数据索引只是行号重置掉更干净。但如果索引是时间戳或者用户 ID 这类有意义的标识就不能随便丢得用keys或者保留原索引。注意ignore_indexTrue和keys参数不能同时用因为keys会生成 MultiIndex而ignore_index要丢弃索引两者逻辑冲突。3. keys 与 MultiIndex给拼接结果打上来源标签3.1 keys 的基本用法当你要拼接多个来源的数据并且希望保留这条数据来自哪个表的信息时keys参数就派上用场了df_a pd.DataFrame({value: [1, 2]}) df_b pd.DataFrame({value: [3, 4]}) result pd.concat([df_a, df_b], keys[source_a, source_b]) print(result)输出value source_a 0 1 1 2 source_b 0 3 1 4结果是一个带 MultiIndex 的 DataFrame第一层索引就是keys里指定的标签。这样后续可以用result.loc[source_a]直接取出某个来源的数据非常方便。3.2 用 keys 做分组聚合keys生成的 MultiIndex 在groupby时特别好用。比如你拼接了多个渠道的销售数据想按渠道汇总result pd.concat([df_a, df_b], keys[channel_1, channel_2]) result.groupby(level0)[value].sum()level0表示按 MultiIndex 的第一层分组也就是渠道标签。这比先加一列来源字段再groupby要简洁而且不污染原始列结构。3.3 keys 与 names 的配合默认情况下keys生成的那层索引没有名字。可以用names参数给它命名pd.concat([df_a, df_b], keys[a, b], names[source, row_id])这样索引层级就有明确的名字导出 CSV 或者做reset_index时更清晰。我个人的习惯是只要用了keys就顺手把names也写上避免后面忘了这层索引是干嘛的。3.4 列方向拼接时 keys 的行为axis1时keys的作用是给列加一层标签pd.concat([df3, df4], axis1, keys[left, right])结果的列会变成 MultiIndex第一层是left和right。这个场景在对比两组特征时很有用比如把训练集特征和验证集特征并排放在一起用keys区分来源。4. 实战中最常踩的五个坑与排查思路4.1 列顺序不一致导致的隐性错位concat按列名对齐所以列顺序不一致不会导致数据错位——这是好事。但如果你用的是axis1且两个 DataFrame 的索引顺序不同concat会按索引对齐也不会错位。真正会出问题的是列名相同但含义不同的情况。比如两个表都有id列但一个是用户 ID一个是订单 ID。concat不会报错会直接把它们当成同一列拼在一起。这种错误极其隐蔽排查起来很痛苦。我的做法是拼接前先打印两边的columns.tolist()肉眼确认一遍列名语义是否一致。4.2 dtype 隐式转换拼接时如果两边的同一列 dtype 不同pandas 会做隐式转换。最常见的是int和float混拼结果整列变成float。更隐蔽的是int和object混拼比如一边是数字一边是字符串1拼完变成object后续做数值计算直接报错。df7 pd.DataFrame({num: [1, 2]}) df8 pd.DataFrame({num: [3, 4]}) pd.concat([df7, df8])[num].dtype # dtype(O)也就是 object排查方法拼接后立刻跑result.dtypes对比拼接前的 dtype。如果发现异常用astype显式转换。4.3 空 DataFrame 参与拼接空 DataFrame 的列名如果是RangeIndex或者空拼接时可能不报错但结果不符合预期。比如empty pd.DataFrame() pd.concat([df7, empty])结果还是df7看起来没问题。但如果empty有列名但没数据拼接后会多出那些列全是NaN。实战中从数据库查询返回空结果集时容易遇到这种情况建议拼接前用if not df.empty过滤一下。4.4 性能问题循环 concat 是反模式这是我最想强调的一点。很多人写代码时习惯这样result pd.DataFrame() for df in df_list: result pd.concat([result, df])这段代码在df_list很长时性能极差因为每次concat都会创建一个新的 DataFrame内存拷贝次数是 O(n²) 级别的。正确做法是一次性传入列表result pd.concat(df_list, ignore_indexTrue)我实测过一个场景100 个各 1 万行的 DataFrame循环拼接耗时约 12 秒一次性拼接不到 0.3 秒。差距是数量级的。如果数据量再大循环拼接还可能触发内存不足。4.5 MultiIndex 后续操作的复杂性用了keys之后索引变成 MultiIndex很多操作需要调整。比如reset_index会把两层索引都变成列可能不是你想要的。如果只想保留来源标签作为一列可以result pd.concat([df_a, df_b], keys[a, b], names[source, row]) result.reset_index(level0) # 只把 source 层变成列或者用result.index.get_level_values(0)直接取第一层索引的值。这些操作在文档里都有但实际用时容易忘建议写代码时随手加注释。5. 从真实场景出发三种典型拼接任务的完整写法5.1 多文件日志合并与来源标记假设你有一批按日期命名的日志文件需要合并成一张大表并保留日期信息import glob import pandas as pd files sorted(glob.glob(logs/*.csv)) dfs [] keys [] for f in files: df pd.read_csv(f) dfs.append(df) keys.append(f.split(/)[-1].replace(.csv, )) result pd.concat(dfs, keyskeys, names[log_date, row_id]) result result.reset_index(level0)这里用keys记录文件名names给索引起名最后reset_index(level0)把日期变成普通列。这样既保留了来源信息又不会让索引层级太复杂。5.2 特征表横向拼接与列去重做机器学习时经常需要把多组特征横向拼在一起features pd.concat([feat_a, feat_b, feat_c], axis1) # 检查是否有重复列名 duplicated features.columns[features.columns.duplicated()] if len(duplicated) 0: features features.loc[:, ~features.columns.duplicated()]axis1拼接时如果列名重复pandas 不会报错但后续用列名取值会返回多列。所以拼接后检查columns.duplicated()是个好习惯。5.3 增量数据追加与去重定时任务中经常需要把新数据追加到历史数据里history pd.read_parquet(history.parquet) new_data pd.read_parquet(new_batch.parquet) combined pd.concat([history, new_data], ignore_indexTrue) combined combined.drop_duplicates(subset[order_id], keeplast)这里keeplast保证新数据覆盖旧数据。注意ignore_indexTrue在这里是必要的否则历史和新数据的索引可能冲突drop_duplicates的行为会受影响。6. 几个让 concat 更稳的工程习惯6.1 拼接前做 schema 校验我现在养成了一个习惯写一个简单的函数在拼接前检查所有 DataFrame 的列名集合是否一致def check_schema(dfs, expected_cols): for i, df in enumerate(dfs): missing set(expected_cols) - set(df.columns) extra set(df.columns) - set(expected_cols) if missing or extra: print(fdf[{i}] missing: {missing}, extra: {extra})这个函数不复杂但能提前拦住大部分列错位问题。尤其是在多人协作的项目里上游数据格式变动时这个检查能帮你快速定位是哪一批数据出了问题。6.2 用 copy 参数控制内存行为concat有个copy参数默认是True表示总是拷贝数据。如果确定后续不会修改原数据可以设copyFalse来节省内存。但这个参数要慎用因为一旦原数据被修改拼接结果也会跟着变。我的建议是数据量不大时保持默认数据量大且内存紧张时再考虑关掉同时确保原数据不再被改动。6.3 拼接后立刻做一次完整性检查不管什么场景拼接完我都会跑这几行print(result.shape) print(result.dtypes) print(result.isnull().sum().sort_values(ascendingFalse).head(10))shape确认行数是否符合预期比如多个文件行数之和dtypes看有没有意外的类型转换isnull().sum()看缺失值分布。这三行花不了几秒但能发现大部分拼接问题。6.4 关于 append 的弃用早期 pandas 有DataFrame.append方法用起来很直观但它在 1.4 版本被标记弃用2.0 版本已经移除。如果你还在用append迁移到concat时注意append默认ignore_indexFalse而concat也是但append不支持keys和join参数。迁移时把df.append(other)改成pd.concat([df, other])即可行为基本一致。7. 一些边界情况与个人体会concat在处理时间序列数据时有个细节值得注意如果两个 DataFrame 的索引都是DatetimeIndexaxis1拼接会按时间戳对齐这比手动merge方便很多。但如果时区不一致pandas 会报错或者产生意外结果拼接前统一时区是个好习惯。另一个边界情况是Categorical类型。如果两边的分类列类别集合不同拼接后可能变成object类型。可以用pd.Categorical显式指定类别集合来避免。我个人在实际操作中的体会是concat的参数不多但每个参数在不同axis下的行为差异很大光看文档容易记混。最好的办法是拿几个小 DataFrame 在 Jupyter 里手动试一遍把axis、join、keys、ignore_index的组合效果都跑出来看。试过一次之后后面用起来基本不会出错。最后分享一个小技巧如果你经常需要拼接结构相似的数据可以封装一个工具函数把 schema 校验、拼接、完整性检查串在一起。这样每次调用只需要传 DataFrame 列表省去重复写检查代码的麻烦。我在几个项目里都这么干维护成本低很多。 SEO 优化官网定制响应式建站教育培训建站