数据清洗后怎么检查字段类型有没有问题

文章导读
数据清洗后检查字段类型,核心不是看 dtype 那行输出好不好看,而是确认「声明的类型」和「列里实际承载的值」是否还对得上。清洗过程里只要做过去空格、空值填充、拼接、格式转换或导出再导入,类型就可能被悄悄改掉:数字列混进了空串,日期列一半是 2024/01/02 一半是 2024-01-02,ID 列的前导零被读成整数后消失。判断方向是:先拿到期望类型清单,再对每一列做值级别的可转换性检查,最后用
📋 目录
  1. A 先确认当前场景和目标
  2. B 按这个顺序处理
  3. C 改完后怎么验证
  4. D 容易忽略的边界
A A

数据清洗后检查字段类型,核心不是看 dtype 那行输出好不好看,而是确认「声明的类型」和「列里实际承载的值」是否还对得上。清洗过程里只要做过去空格、空值填充、拼接、格式转换或导出再导入,类型就可能被悄悄改掉:数字列混进了空串,日期列一半是 2024/01/02 一半是 2024-01-02,ID 列的前导零被读成整数后消失。判断方向是:先拿到期望类型清单,再对每一列做值级别的可转换性检查,最后用目标端写入和抽样对照来收口。

检查字段类型的可行路径是三步对照:期望 schema(建表语句或数据字典)、清洗后全量 dtype 快照、逐列值级可转换率。判断一项是否有问题,看三件事——非空值能否整体转成目标类型、转换失败的行能否逐条解释、极端值(最大最小、最长最短)是否落在业务区间。适用前提是手上有一份明确的期望类型定义;如果只有清洗后的文件而没有任何目标 schema,先补定义再谈检查,否则查出来的只是读取端推断结果,不能作为结论。

先确认当前场景和目标

动手之前要先确认清洗后的数据往哪里落地,因为落地方式的约束强弱不同,检查重点也不同。

  • 落进强类型库(数仓表、关系库):类型由建表语句决定,清洗结果里不合规的值会在写入时报错或被隐式转换。检查重点是写入前的可转换率和失败样例。
  • 落成 CSV、Parquet 等文件:文件本身约束弱,类型靠读取端推断。检查重点是读取参数的稳定性,比如 dtype=str 读进来再自己转,避免依赖默认推断。
  • 作为模型或报表输入:关注的是数值列不能有字符串、日期列要能排序、分类列取值集合是否稳定。

还需要先分清清洗动作改了什么。如果只是过滤行、没有改值,类型问题基本来自源头,排查源头即可;如果做过类型转换、去空格、空值填充、字符串拼接,就必须重新校验,因为这些动作最容易把空串和 NULL 混为一谈,或者把数字变成带说明文字的新列。判断条件可以先用一句话写下来:这次清洗后,每个字段的期望类型、是否允许为空、取值约束分别是什么。写不出来说明缺数据字典,需要先补。

按这个顺序处理

建议按「定标准 → 看快照 → 查值 → 出清单」的顺序走,避免一上来就盯着 dtype 猜。

  1. 整理期望类型清单:从建表语句、schema 文件或数据字典里抄出字段名、期望类型、可空性、约束(如金额两位小数、ID 定长)。这是后面所有比对的基准。
  2. 取全量 dtype 快照:pandas 用 df.dtypes,SQL 侧查 information_schema.columns,Parquet 读 schema 元数据。快照要和期望清单逐字段对照,标出不一致项。
  3. 做逐列值级探查:dtype 一致不代表值干净,比如 object 列里全是数字字符串。要看非空数、去重数、长度分布,以及「强制转成目标类型后有多少变成空」。
  4. 盯高发陷阱字段:数字列里的 ''、'NULL'、'N/A'、千分位逗号;ID 类字段的前导零;日期列的多种分隔符和时区;布尔列的 Y/N、1/0、是/否 混用;金额用浮点存导致小数抖动。
  5. 产出一张问题清单:字段名、问题类型、样例值、影响行数(用 count 算出来,不要估)。清单里每一项都要能对应一个修改动作或一个「确认可接受」的判断。

下面是一个 pandas 的检查骨架,用途是先把所有列按字符串读入,再逐列试转,避免读取阶段就把问题掩盖掉:

import pandas as pd

# 全部按字符串读入,保留原始形态
df = pd.read_csv("cleaned.csv", dtype=str, keep_default_na=False)

expected = {
    "order_id": "string",
    "amount": "float",
    "created_at": "datetime",
    "is_paid": "bool",
}

for col, target in expected.items():
    s = df[col]
    if target in ("float", "int"):
        conv = pd.to_numeric(s, errors="coerce")
        bad = s[(conv.isna()) & (s.str.strip() != "")]
        print(col, "转数字失败行数:", len(bad), "样例:", bad.head(5).tolist())
    elif target == "datetime":
        conv = pd.to_datetime(s, errors="coerce")
        bad = s[(conv.isna()) & (s.str.strip() != "")]
        print(col, "转日期失败行数:", len(bad), "样例:", bad.head(5).tolist())
    elif target == "bool":
        print(col, "取值分布:", s.value_counts().head(10).to_dict())
    elif target == "string":
        print(col, "长度极值:", s.str.len().min(), s.str.len().max())

如果数据落在库里,可以用 TRY_CAST 做同样的检查,把失败行数一次算出来:

数据清洗后怎么检查字段类型有没有问题
SELECT
  COUNT(*) AS total_rows,
  SUM(CASE WHEN TRY_CAST(amount AS DECIMAL(18,2)) IS NULL
            AND amount IS NOT NULL AND TRIM(amount) <> '' THEN 1 ELSE 0 END) AS amount_bad,
  SUM(CASE WHEN TRY_CAST(created_at AS TIMESTAMP) IS NULL
            AND created_at IS NOT NULL AND TRIM(created_at) <> '' THEN 1 ELSE 0 END) AS created_at_bad,
  SUM(CASE WHEN order_id LIKE '0%' THEN 1 ELSE 0 END) AS leading_zero_rows
FROM cleaned_tmp;

要注意 TRY_CAST 并非所有引擎都提供,部分方言需要换成正则匹配或先 CAST 到临时表看报错。这条要结合自己的数据库确认,不能直接照搬。

改完后怎么验证

类型调整之后不要只看一眼转换成功就结束,建议做下面几组验证动作,并对照预期信号判断:

  • 重跑同一套探查脚本:预期信号是各列「转目标类型失败行数」降为 0,或等于已确认的例外清单(例如确实存在的历史脏数据被单独标记)。两次结果要能逐列对比差异。
  • 核对行数和主键唯一性:清洗和类型转换不应该无故改变行数。预期信号是总行数符合预期过滤后的数量、主键去重数等于总行数,不相等就要回头查是不是转换过程中产生了重复或丢行。
  • 看极值而不是均值:每列取最大最小、最长最短、最早最晚,预期信号是这些边界值落在业务合理区间。日期列出现 1970 或 9999、金额出现负数或超大值,通常说明转换时把错误值兜底成了默认值。
  • 让目标端兜底一次:把结果写进目标表,预期信号是写入过程没有隐式转换告警或截断提示。数据库报错比脚本里的 coerce 更可信。
  • 抽样人工对照:从转换失败清单和极值附近各抽若干行,回原表看原始值,确认是解释得清的脏数据,而不是自己转换写错了。

容易忽略的边界

有几类问题不会报错,但会静默改变数据含义,需要单独留意。第一类是精度和范围:浮点存金额会出现小数抖动,用 int32 存超出范围的 ID 会被截断,这类问题在转换时不抛异常,只能靠范围检查发现。第二类是零和空:前导零在整数化后丢掉,NULL、''、'NULL' 三种写法语义不同,统一填充成 0 或空串会污染后续统计。第三类是时区和格式:同一列混着带时区和不带时区的时间戳,转完后排序会错乱,建议统一到一个明确的时区再落地。

还有几项限制要提前说清。CSV 读取端默认只抽样若干行做类型推断,同一个文件在不同读取参数下可能得到不同 dtype,所以建议显式指定 dtype 或全按字符串读入;pandas 的日期解析参数在不同版本间行为有差异,老版本可能不支持 format="mixed" 这类写法,使用前要结合本地版本确认。若清洗后的数据要回流给下游,改类型属于破坏性变更,回退建议是:改动前保留原表和清洗前快照,把类型变更写成显式 CAST 落成新字段而不是原地覆盖,同时在字段清单里记录变更点,出问题时能快速退回到旧字段对比。不建议在没有对照基线的情况下直接原地改类型。