合并之后类型悄悄变了:洹拓科技谈 DataFrame 的 dtype 保全
一、合并这一步为什么总在悄悄改数据
一份订单明细、一份商品主数据,单独打开都正常;一旦 merge,编号列就开始出问题。原本是整数的 10086,出来变成 10086.0;原本是纯数字的长 ID,显示成 1.23e+18 且末几位变成了 0;导出 Excel 之后,”0012345” 这类带前导零的编码直接成了 12345。最麻烦的是这三件事都不报错——pandas 只是”替你”改了类型,等你发现时,报表已经发出去了。
近期技术社区里有这样一组问答:合并两个列名不完全一致的 DataFrame 时,原始数据类型保不住。看起来像 pandas 的坑,实际上同时发生了三件事:join 键没对上,缺失值触发类型上浮,显示层再把上浮的结果放大成精度损失。把它们拆开,就能写出可断言的规则。
| 合并后出现的现象 | 真实原因 | 第一反应该查什么 |
|---|---|---|
整数编号全部带 .0 |
键没对上导致列里混入 NaN,而 np.int64 表示不了 NaN,整列升到 float64 |
该列的 isna().sum(),以及 on= 用的是哪个列 |
长 ID 显示成 1.23e+18,末几位归零 |
float64 有效位约 53 位,装不下 18 位整数,这是精度损失而非显示问题 | 读入阶段有没有指定 dtype |
| 导出 Excel 后前导零丢失 | 在 pandas 里它已经是数值类型,Excel 只是照着数值写 | 源头是否按字符串读取 |
| 结果行数比左表多出一截 | on= 写成了非唯一列,触发笛卡尔式膨胀 |
右表键的重复情况与 validate= |
| 日期列变成 object,无法比较大小 | 对齐过程中 category、timestamp 被降级 | 合并前后的 dtypes 差异 |

二、类型上浮的三条机理
第一,整数列的空值规则。传统整数 dtype 是 np.int64,它没有”缺失”这个概念,NaN 本质上是浮点位模式的哨兵值。于是只要某一行在合并中没匹配上,这一列就被迫整列转成 float64——语义从”编号”变成了”实数”,后续所有下游都跟着变。pandas 的可空整数扩展类型 Int64(注意是大写 I)用独立的掩码记录缺失,可以在保留整数语义的同时容纳空值。区别只在于读入和合并之前有没有显式指定。
第二,列名不一致造成的静默失配。左表叫 order_no,右表叫 order_id,如果 on= 写了一个只在一侧存在的列名,merge 依然返回一个结构完整的表,只是另一侧的所有列在每一行上都对不上,被填满 NaN。列数没变、行数没变,但内容已经空了。类型上浮只是这次失配的副作用,真正的问题是键根本没接上。
第三,扩展类型在对齐中被降级。category 和 datetime64 这类 dtype,在 merge、concat、reindex 的对齐过程中,只要两侧不一致——一边是 category 一边是 object,或者两边都是 category 但 categories 集合不同——结果列往往退化为 object。退化之后,日期比较、有序判断、groupby 观察到的取值都会变,而报错通常要到下游环节才浮现。
三、四条 dtype 保全规则
- 合并前先把列名规范化,并断言两边的键名字与 dtype 完全一致。
import pandas as pd |
- 键统一成字符串,或统一成可空整数 Int64,绝不允许左半边 int64、右半边 object。
# 编码类字段(有前导零、不参与运算)一律按字符串读、字符串合 |
- 用
validate=和indicator=True主动检查行数膨胀,别让静默的一对多毁掉数据。
rows_before = len(left) |
validate="m:1" 声明的是”左多右一”,右表键一旦重复就直接报错;这正是我们想要暴露的情况。如果业务上两侧都可能重复,那说明你合的不是主数据而是明细,得先聚合再合。
- 合并后跑一遍 dtypes 快照比对,把断言留在管道里。
def dtype_snapshot(df: pd.DataFrame) -> pd.Series: |
四、把断言塞进 ETL 的测试
上面四条不是代码风格建议,而是可以固化成测试的约束。判断标准很简单:一次合并如果只靠”人眼看几行数据没变样”来验收,它就不算完成。ETL 脚本里至少留三个测试——第一个测试比对键的 dtype 与名字,专门对付列名改名;第二个测试跑小样本 merge 并断言行数与匹配率,专门对付重复键;第三个测试做 dtypes 快照 diff,专门对付类型降级。三个测试各十几行,跑在 CI 里比任何数据质量平台的看板都便宜。
还有一类边界值得单独造样本:右表缺这个键、左表键为空、键同时存在前导零与纯数字形态。这三类样本一旦进入测试集,多数”合并后类型变了”的问题会在提交阶段就被拦下。
五、洹拓视角:数据合并的验收清单
我们交付数据类项目时,合并环节的验收只看四个指标,缺一个就打回,阈值由业务方定,不由开发方猜。
| 必须记录的指标 | 怎么算 | 不达标时先停什么 |
|---|---|---|
| 键匹配率 | 匹配上的行数 / 左表行数 | 先停报表发布,查列名与口径,而不是补默认值 |
| 行数变化率 | (结果行数 - 左表行数) / 左表行数 | 先停写入,查右表键唯一性 |
| dtype 差异数 | 快照比对中不一致的列数 | 先停下游任务,逐列确认为什么降级 |
| 空值新增数 | 合并后新增的 NaN 单元格数 | 先停指标计算,空值会污染均值与占比 |
四条验收标准写得更直白一些:一,任何人改列名,测试必须先红一次;二,任何一次合并,结果行数与匹配率必须落到日志里可回溯;三,任何一列的 dtype 变化必须显式声明理由,”合并时自动变的”不算理由;四,编码类字段永远按字符串处理,不参与算术就不许是数值类型。做到这四条,”报表一到合并就变样”这件事就不再是玄学,而是一个有明确责任环节的缺陷。