3. 数据表结构统一与字段对齐

污染源监测数据往往来自多个渠道:排污单位自行监测的Excel报表、在线监控系统导出的CSV、第三方检测机构提供的汇总表,以及历史归档的旧版台账。这些表在业务上描述的是同一件事——某个排口在某段时间的污染物浓度与排放量,但落到文件层面,列名、列序、缺失情况和数据类型几乎不可能天然一致。批量清洗的第二步,就是把这些“各说各话”的表结构,收敛成一套统一的字段规范,让后续的合并、计算和校验有稳定的输入。

这一步的核心不是简单改个列名,而是建立“标准字段集 + 映射规则 + 对齐执行”三层结构。标准字段集定义清洗后每张表必须长什么样;映射规则描述每个来源表的列如何对应到标准字段;对齐执行则按规则批量重排、补列、改名并统一类型。三者缺一,批量处理就会退化成逐表手工调整。

多源监测表 → 标准字段集 的对齐结构 来源A:自行监测报表 排口编号 | 监测日期 | 污染物 浓度(mg/L) | 流量(m³/h) 缺:排放量、数据来源 列序:编号在前,日期在后 来源B:在线监控导出 OUTLET_ID | TIME | ITEM VALUE | FLOW | UNIT 列名为英文缩写,需翻译 时间含时分秒,需截断 来源C:第三方检测汇总 点位名称 | 采样时间 | 项目 结果 | 单位 | 检测方法 点位名称需映射为排口编号 多出“检测方法”列 字段映射规则 排口编号 ← OUTLET_ID 排口编号 ← 点位名称(查表) 监测日期 ← TIME[:10] 污染物 ← ITEM / 项目 浓度 ← VALUE / 结果 流量 ← FLOW 排放量 ← 浓度×流量(计算) 数据来源 ← 常量标记 标准字段集(清洗后统一结构) 排口编号 string 必填 监测日期 date 必填 污染物 string 必填 浓度 float 必填 流量 float 可空 排放量 float 计算列 数据来源 string 常量 单位 string 默认mg/L 列序固定,缺失列补空值,多余列丢弃 按规则 重排/补列

先定义标准字段集,再谈对齐

标准字段集是整个清洗流程的“契约”。它应当由业务需求反推:后续要做浓度超标判断,就必须有污染物、浓度、限值关联字段;要做排放量核算,就必须有流量和浓度;要追溯数据可信度,就必须保留数据来源。以本课程使用的示例为例,标准字段集可定义为:排口编号、监测日期、污染物、浓度、流量、排放量、数据来源、单位,共8列,列序固定。其中排口编号、监测日期、污染物、浓度为必填,流量允许为空,排放量为计算列,单位默认mg/L。

字段类型也要一并约定。监测日期统一为YYYY-MM-DD的日期类型,浓度和流量为浮点数,排口编号为字符串(避免前导零丢失)。这些约定写进一个配置字典,后续所有来源表都向它看齐,而不是每张表单独判断。

用映射字典描述列名差异

列名不一致是最常见的障碍。来源A叫“排口编号”,来源B叫“OUTLET_ID”,来源C叫“点位名称”。处理方式不是写一堆if分支,而是为每个来源维护一份映射字典,键是来源列名,值是标准字段名。例如来源B的映射可写成:

MAP_B = {
    "OUTLET_ID": "排口编号",
    "TIME": "监测日期",
    "ITEM": "污染物",
    "VALUE": "浓度",
    "FLOW": "流量",
    "UNIT": "单位",
}

来源C的“点位名称”不能直接改名,因为它是名称而非编号,需要先经过一张点位对照表转换为排口编号,再进入映射。这类“值映射”与“列映射”要分开处理:列映射解决列名,值映射解决列内取值口径。来源C多出的“检测方法”列不在标准字段集内,对齐时直接丢弃;来源A缺少的“排放量”和“数据来源”,则分别用计算和常量补齐。

对齐执行的四个动作

有了标准字段集和映射规则,批量对齐可以归纳为四个动作,按固定顺序执行:

  1. 改名:按映射字典把来源列名替换为标准字段名,未出现在字典中的列标记为待丢弃。
  2. 补列:对照标准字段集,缺失的列补为空值(None或NaN),计算列和常量列在此步填充。
  3. 重排:按标准字段集的列序重新索引,保证所有表列序完全一致。
  4. 转类型:按约定把日期、数值列转换为目标类型,无法转换的值置为缺失并记录。

这四个动作对每张表执行同一套逻辑,差异只体现在映射字典和来源标记上。这样,新增一个数据来源时,只需增加一份映射配置,而不必改动对齐代码本身。

一个可执行的批量对齐函数

把上述逻辑封装成函数,输入是原始表、映射字典和来源名称,输出是符合标准结构的表:

import pandas as pd

STANDARD_COLS = ["排口编号","监测日期","污染物","浓度",
                 "流量","排放量","数据来源","单位"]

def align_table(df, mapping, source_name):
    df = df.rename(columns=mapping)
    # 丢弃非标准列
    df = df[[c for c in df.columns if c in STANDARD_COLS]]
    # 补缺失列
    for col in STANDARD_COLS:
        if col not in df.columns:
            df[col] = None
    # 固定列序
    df = df[STANDARD_COLS]
    # 类型转换
    df["监测日期"] = pd.to_datetime(df["监测日期"], errors="coerce").dt.date
    for col in ["浓度","流量","排放量"]:
        df[col] = pd.to_numeric(df[col], errors="coerce")
    df["数据来源"] = source_name
    df["单位"] = df["单位"].fillna("mg/L")
    return df

注意两个细节:一是errors="coerce"让无法解析的值变成缺失而不是抛错,保证批量处理不中断;二是“数据来源”在补列之后统一赋常量,避免来源表自带的值干扰。排放量若来源表未提供,可在对齐后按“浓度×流量×换算系数”计算,换算系数依单位而定,示例中假设浓度mg/L、流量m³/h,则排放量(kg/h)= 浓度×流量÷1000,该系数需在配置中显式声明,不能硬编码在函数里。

对齐结果如何判断

批量执行后,不能只看“没报错”就认为成功。应检查三项:所有输出表的列名与列序是否与标准字段集完全一致;必填字段的缺失比例是否在可接受范围;类型转换失败的数量是否被记录。例如对来源B执行后,若“监测日期”出现大量NaT,往往说明原始时间格式与预期不符,需要回到映射规则中补充格式说明,而不是在对齐阶段强行填充。对齐的目标是结构一致且问题可见,而不是把异常悄悄抹平。

建议把每个来源的映射字典、来源标记和换算系数集中放在一个配置文件或配置字典中,与对齐函数分离。这样字段规范调整时只改配置,批量任务无需重写代码,也便于多人协作时核对口径。

结构统一完成后,各表已经具备相同的“骨架”,下一步才能安全地纵向合并、按排口与日期关联限值,并进入缺失值与异常值的处理。字段对齐看似机械,实则是整个批量清洗流程能否稳定复现的分水岭。