Pandas 的 merge 一次通常只能连接两个 DataFrame,遇到多张表时不断嵌套 merge 或写循环会显著降低可读性。functools.reduce 能把一个接受两个参数的函数依次作用到序列上,将 merge 这种二元操作推广到多张表。本文先说明基本用法,再通过典型场景分析连接类型、列名冲突和性能问题。

一、为什么 reduce 比多次 merge 更合适
多次 merge 写起来像 df1.merge(df2, on='id').merge(df3, on='id'),表一多括号就会越堆越长,而且每次都要重复传入连接键和连接方式。reduce 的思路是把 merge 的公共逻辑集中到一个函数里,让代码只描述“两两合并”的规则,剩下的按顺序自动完成。reduce 接收一个函数和一个可迭代对象,第一次从序列中取前两个元素执行函数,得到结果后再与下一个元素继续执行,直到所有元素都处理完。对 DataFrame 列表来说,这就是一个逐步合并的过程。
reduce 并不是什么特殊优化,它底层仍然是一次次调用 merge,每次都会生成新的 DataFrame。与 for 循环手动维护中间变量相比,reduce 把状态隐藏起来,避免先初始化 result 再反复赋值的模板代码。新增一张表时,通常只需要往列表里追加 DataFrame,再调整 merge 的参数即可。下面是最简单的三表内连接示例。
import pandas as pd
from functools import reduce
df1 = pd.DataFrame({'id': [1, 2, 3], 'a': ['A1', 'A2', 'A3']})
df2 = pd.DataFrame({'id': [1, 2, 3], 'b': ['B1', 'B2', 'B3']})
df3 = pd.DataFrame({'id': [1, 2, 3], 'c': ['C1', 'C2', 'C3']})
dfs = [df1, df2, df3]
result = reduce(lambda left, right: pd.merge(left, right, on='id'), dfs)
print(result)
这段代码里没有显式保存中间状态,df 列表就是唯一需要维护的数据源。当表数量较多时,这种写法的优势更明显,也便于把多表合并封装成通用函数。reduce 与 map、filter 一样属于函数式编程工具,配合使用可以让数据清洗流程更紧凑。
二、连接方式与多键关联的写法
多表整合并不总是默认的内连接,很多场景需要以主表为基准做左连接,保留主表的全部记录。reduce 方案同样支持连接方式控制,只要在 lambda 里给 merge 传入 how='left' 即可。需要注意,左连接的主表是当前累积结果,因此通常应把主表放在列表首位。下面示例中主表 id 比较多,其他表缺失部分 id,最终结果会保留 1 到 4 四条记录,缺失列自动填 NaN。
main = pd.DataFrame({'id': [1, 2, 3, 4], 'name': ['a', 'b', 'c', 'd']})
score = pd.DataFrame({'id': [1, 2, 3], 'score': [90, 85, 88]})
level = pd.DataFrame({'id': [2, 3, 4], 'level': ['B', 'A', 'C']})
dfs = [main, score, level]
result = reduce(lambda left, right: pd.merge(left, right, on='id', how='left'), dfs)
print(result)
实际业务里关联键经常不止一列,比如订单数据需要按照用户和日期同时匹配。reduce 会把 lambda 中的参数原样传给 merge,所以 on=['user', 'day'] 这样的多键写法同样有效。只要所有 DataFrame 都有相同名称的键列,把列表交给 reduce 就能得到多键合并结果。下面是一个三张订单相关表按复合键合并的示例。
o1 = pd.DataFrame({'user': ['u1', 'u2'], 'day': ['2024-01-01', '2024-01-02'], 'amount': [100, 200]})
o2 = pd.DataFrame({'user': ['u1', 'u2'], 'day': ['2024-01-01', '2024-01-02'], 'tax': [10, 20]})
o3 = pd.DataFrame({'user': ['u1', 'u2'], 'day': ['2024-01-01', '2024-01-02'], 'qty': [5, 7]})
dfs = [o1, o2, o3]
merged = reduce(lambda left, right: pd.merge(left, right, on=['user', 'day']), dfs)
print(merged)
执行后会得到 user、day、amount、tax、qty 五列。如果希望保留所有用户和日期的组合,可以把 how='outer' 一并传给 merge。为了避免每处都重复写连接参数,可以提前定义合并函数,或者使用 functools.partial 绑定固定参数,再交给 reduce 执行。这样连接键、连接方式发生变化时,只需修改一处。
三、列名冲突与 suffixes 处理
多张表里出现相同名字的非键列很常见,比如每张销售表都有 value。merge 遇到列名冲突时会自动添加 _x、_y 后缀,但在 reduce 多轮合并中,后缀命名可能变得混乱。第一轮合并得到 value_x 和 value_y,第二轮再与含有 value 的表合并时,Pandas 会根据命名冲突继续加后缀,最终可能产生 value_x、value_y、value_x_x 这类难以辨别的列名。
一种直观做法是在 merge 中设置 suffixes 参数,但 suffixes 只对当前一轮合并生效,不能保证多轮之后仍然保持整洁。下面示例用 suffixes=('', '_drop') 让左侧结果保留原列名,右侧冲突列带上 _drop 后缀。
a = pd.DataFrame({'id': [1, 2], 'value': [10, 20]})
b = pd.DataFrame({'id': [1, 2], 'value': [30, 40]})
c = pd.DataFrame({'id': [1, 2], 'value': [50, 60]})
dfs = [a, b, c]
merged = reduce(
lambda left, right: pd.merge(left, right, on='id', suffixes=('', '_drop')),
dfs
)
print(merged.columns.tolist())
即便如此,当第三张表再次带来相同列名时,仍可能产生新的 _drop 或与旧列冲突。更稳妥的办法是在合并前就为每张表的业务列加上有含义的前缀,例如把各表的 value 改成 base_value、tax_value。如果只是想纵向追加行,而不是根据键横向对齐,应该改用 pd.concat,merge 和 concat 的目的完全不同,混用会带来错误结果。
遇到列名冲突已经造成混乱的情况,可以在合并后通过 DataFrame.filter 找到带 _x、_y 后缀的列,再用 rename 统一整理。不过源头规范列名仍然是最省事的方案。因为 reduce 的中间结果不直接暴露,一旦列名混乱,排查起来会非常耗时,尤其是列数量达到几十列时。
四、性能与内存注意事项
reduce 合并多表时,每两张表合并都会产生一个中间 DataFrame,10 张表会产生 9 个中间结果。数据规模较大时,这些中间对象会占用不少内存。一个有效的优化是在合并前只保留关联键和必要字段,避免把无关的大文本列带进 merge。还可以通过分组聚合把明细表压缩成较小粒度,再执行多表关联,从而减少中间数据量。
从执行速度上看,reduce 不会比手动循环 merge 快多少,因为底层本质上都是多次调用同一个算法。它的价值主要体现在代码易读和易维护上。如果表非常大,且每次连接都有复杂条件,可以考虑把数据导入 SQL 数据库,利用数据库的 join 优化器完成多表关联。Pandas 在单机内存中处理这类操作,容易受内存大小限制。
多表内连接还可能带来行数膨胀。比如主表某个 id 出现 3 次,关联表同一个 id 出现 4 次,这两表合并后该 id 会产生 12 行。多张表叠加之后,数据量可能呈乘数增长。合并前最好用 df['id'].is_unique 检查键是否唯一,或者用 df.groupby('id').size().max() 查看最大重复次数。理解每张表的键粒度,是避免多表合并产生意外大数据量的关键。
另一个容易忽略的问题是关联键类型不一致。一个表的 id 是 int64,另一个表读入后变成 object 字符串,merge 时可能出现类型转换或匹配失败。建议合并前统一类型,例如 df['id'] = df['id'].astype('int64')。相比 lambda 本身的一点点调用开销,这类数据质量问题对性能和时间的影响要大得多。先把基础数据整理好,再使用 reduce 合并多表,通常能得到简洁又可靠的代码。
Pandas合并多表reducemerge修改时间:2026-10-01 12:06:47