☰
merge 一下,金额凭空多了三分之一:pandas 对账翻车实测
2026/10/15 7:30:06 网站建设 项目流程

文章目录

    • 一、先看一个真实的翻车
    • 二、事故一:一对多,行数守恒破灭
    • 三、对账三件套:validate、indicator、键体检
    • 四、安全 merge 清单
    • 五、30 秒带走版

一、先看一个真实的翻车

两份表对账:左边 3 行销售记录,右边 4 行单价表(其中 001 号有 3 条记录)。一个 left join,看起来人畜无害:

importpandasaspd sales=pd.DataFrame({"口岸编号":["001","002","003"],"金额":[100,200,300]})price=pd.DataFrame({"口岸编号":["001","001","001","002"],"单价":[10,99,20,30]})bad=sales.merge(price,on="口岸编号",how="left")print(len(bad))# 5——left 只有 3 行print(bad["金额"].sum())# 800——原始合计是 600

3 行变 5 行,金额合计从 600 变 800,凭空多出三分之一。没有任何报错,没有任何警告,DataFrame 长得干干净净。这类事故在对账脚本里是最贵的一种:报表数字变了,但没人知道是哪一行开始错的。本篇把 merge 的五类坑和一整套对账方法全部实测一遍(pandas 3.0.3)。

二、事故一:一对多,行数守恒破灭

行数放大的机制很直白:left 的每一行,会与 right 中所有同键行配对。right 的 001 有 3 条,left 的 001 就被复制成 3 行(图一左)。金额列原样跟着复制,sum()直接把重复部分累加进去——join 是行级操作,sum 是列级操作,中间的放大没人替你看着。

更扎心的是同一份数据的另一种写法。price 表里没有 003 的单价,把 how 换成默认的 inner:4 行,sum 500——003 连行带金额被静默丢掉;而 left 保留 003(单价列明明白白是 NaN), sum 800。

三个数字放在一起:真值 600,inner 给 500,left 给 800。丢行和放大,两个方向都错,而中间那个正确的 600,两种默认姿势一个都给不出。很多人不知道 merge 的默认 how 是 inner——不写参数,就等于让 pandas 替你决定丢不丢数据;而对账场景里,显式写 how 只是及格线,后面两节的三件套才是真正的保险。

三、对账三件套:validate、indicator、键体检

行数放大和静默丢行,都有标准的拦截手段,加起来三件(图二):

第一件,merge 前 validate。一个参数声明你期望的基数,pandas 帮你验:

importpandasaspd sales=pd.DataFrame({"口岸编号":["001","002","003"],"金额":[100,200,300]})price=pd.DataFrame({"口岸编号":["001","001","001","002"],"单价":[10,99,20,30]})sales.merge(price,on="口岸编号",how="left",validate="one_to_many")# 放行:right 可多sales.merge(price,on="口岸编号",how="left",validate="one_to_one")# 抛 MergeError

实测声明 one_to_one 直接抛MergeError,文案点名哪个键重复——一行参数换一次事前拦截。基数声明本身就是文档:看到 one_to_many,读代码的人立刻知道 right 故意有重复。它还有个隐藏好处:把「预期」写进代码。多数对账 bug 不是工具错,是上游数据悄悄变了——昨天还唯一的键,今天渠道表重修了一遍。validate 让这种变化在 merge 那一刻就炸,而不是等到月度报表差出三万块。

顺带把 3.0 的新玩具用对地方:反连接left_anti取「只在左边」的行,以前得 left join 完再过滤_merge列,现在一步到位,语义还更直白(SQL 的 NOT EXISTS)。找孤儿记录、找缺失覆盖,都比以前干净。

第二件,merge 后 indicator。indicator=True加一列_merge,每行标来源:

_merge含义
both两边都有
left_only只在左边
right_only只在右边

实测样本 both 2 / left_only 1 / right_only 1。它还自带一个对账恒等式:both + left_only = left 行数,both + right_only = right 行数——两边一验,平不平一目了然。对账不平先看这列,不用猜。

这列在真实工作流里是这么用的:月度对账跑完,先把 _merge 列的三个计数打出来;left_only 突增,说明上游开始漏 Key;right_only 从零变正,说明有新渠道没进主表;both 占比掉一个点,说明匹配质量在退化。把对账从「数字对不上再回头查」变成「三类行数先说话」,这就是 indicator 的真正价值——它不修 bug,它让 bug 在汇总之前现形。

第三件,键体检。三类问题按顺序查:

  • 键类型:一边 int64 一边 str。pandas 3.0 实测直接抛ValueError(老版本会静默产出匹配不上的结果)——这是 3.0 少有的「把静默故障改成显式报错」的改进,升级后老代码反而更安全;
  • 键唯一性:duplicated().sum()或直接交给 validate;
  • NaN 语义:这条最反直觉——两个 NaN 键会互相匹配。和 SQL 反着来(SQL 里 NULL 不等于 NULL),pandas 官方文档明确警告过这个行为。实测 left 的 NaN 键行成功匹配到了 right 的 NaN 键行。后果:主键缺失的行会悄悄跨表认亲,dropna()或填哨兵值,看你的业务。

四、安全 merge 清单

把上面的东西收成一张按顺序过的清单(图三):

  1. 键类型对齐:两侧键 dtype 一致;ID 列在read_csv时就用dtype=str读进来(上一篇 read_csv 的坑在这里兑现——前导零丢了的键,join 必然对不上);
  2. 键唯一性检查:duplicated().sum()心里有数,或validate=声明基数;
  3. how 显式写:对账不用默认 inner,left/outer 保行,丢失可见;
  4. indicator 收尾:_merge三分类 + 恒等式验平,再进汇总;
  5. NaN 键心里有数:会互相匹配,必要时先dropna或填哨兵值。

版本注脚:pandas 3.0 起 int64×str 键直接报错、新增left_anti/right_anti反连接(SQL 的 NOT EXISTS,实测反连接写法干净取出只在左边的行)、copy参数已弃用——同一份 merge 代码,2.x 与 3.x 的成败表现并不一样。

五、30 秒带走版

  1. 一对多 merge 复制行:3 行变 5 行、sum 600 变 700,零报错——join 是行级,sum 是列级,中间没人看着;
  2. 默认的 inner 姿势会静默丢行:对账一律显式写 how,丢失必须可见;
  3. validate=事前声明基数,重复键当场 MergeError;
  4. indicator=True事后三分类,both+left_only=left 行数恒等式验平;
  5. 键类型不一致:3.0 直接 ValueError(老版本静默)——升级后老代码反而更安全;
  6. 两个 NaN 键会互相匹配,与 SQL 的 NULL 语义相反,官方文档明示;
  7. ID 键在read_csv时dtype=str,前导零丢了 join 必错;
  8. 3.0 新玩具:left_anti反连接,取「只在左边」不用再 left join + 过滤。

本文全部实验基于 pandas 3.0.3 本地实测,样本为构造的小表;2.x 与 3.x 行为差异处已注明版本,读者请以自己环境的实测为准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询