1. 环境监测数据异常初筛与分类

为什么“初筛”不能只看一个阈值

环境实验室的原始监测数据往往同时存在三类问题:离群点(数值偏离总体分布)、缺失值(未采样、仪器掉线、记录漏填)、逻辑矛盾(数值本身合法但违反业务约束,如“检出浓度 > 采样体积对应上限”“pH=9.6 却标注为中性样品”“平行样相对偏差超限”)。如果只用一个固定阈值筛,会把正常的浓度波动误判为离群,也会漏掉“数值正常但逻辑错误”的记录。因此初筛的核心不是“找最大值”,而是建立一条可复用的分类判断链:先判缺失,再判逻辑,最后用分布方法判离群,并对每条异常给出类型标签与处置建议。

判断链的四个判定层

下面这张图给出本节要落地的分类流程。注意判定顺序很关键:缺失值必须先处理,否则后续统计量(均值、标准差、四分位距)会被空值污染;逻辑矛盾优先于统计离群,因为一条“浓度 500 mg/L 但采样体积 0 mL”的记录在统计上可能并不极端,却一定是错的。

原始监测数据 点位·指标·浓度 采样体积·平行样 第1层 缺失判定 浓度/体积/时间 是否为空或占位符 空串 / NA / “-” / 0体积 第2层 逻辑校验 浓度≤方法上限 平行样RD≤20% pH∈[0,14] 且与标签一致 第3层 分布离群 IQR 法:Q1−1.5IQR Q3+1.5IQR 按指标分组计算 初筛报告 异常类型标签 处置建议 复测/复核清单 标签:缺失值 补采 / 标记剔除 不参与统计计算 标签:逻辑矛盾 核对原始记录 重算 / 重采样 标签:统计离群 查仪器/采样过程 保留并标注待复核 标签:正常 进入统计 纳入均值/评价 判定顺序原则:缺失 → 逻辑 → 离群。缺失值先剔除,避免污染 Q1/Q3;逻辑矛盾优先于统计离群,因为“合法数值 + 错误业务关系”在分布上可能并不极端。 离群判定按指标分组进行(如 COD、氨氮、pH 各自算 IQR),不可跨指标混算。

用一组示例数据走完判断链

假设某批次 8 条 COD 监测记录(单位 mg/L),并附带采样体积与平行样信息。为便于演示,数据为教学示例,非实测结果。

编号COD采样体积(mL)平行样RD备注
S142503%—
S245505%—
S3—50—浓度空值
S44804%体积为0
S5445028%平行样超限
S646506%—
S743502%—
S8210507%数值偏高

第1层(缺失):S3 的 COD 为空,直接标“缺失值”,不参与后续统计。第2层(逻辑):S4 采样体积为 0,浓度却存在,属于逻辑矛盾;S5 平行样 RD=28% 超过常用 20% 控制限,也归为逻辑矛盾(精密度不达标)。第3层(离群):对剩余有效值 {42,45,46,43,210} 排序为 42,43,45,46,210。Q1=43,Q3=46,IQR=3,上界=46+1.5×3=50.5。S8 的 210 远超上界,标“统计离群”。

初筛结论:S3→缺失值(补采);S4→逻辑矛盾(核对体积记录);S5→逻辑矛盾(平行样超限,重测);S8→统计离群(查消解过程与试剂空白,保留待复核);S1、S2、S6、S7→正常。

可复用的初筛脚本(Python)

把上面的判断链固化成函数,输入一张 DataFrame,输出带 异常类型 与 处置建议 两列的结果表。关键点:缺失判定要覆盖空串、NA、占位符;逻辑校验用业务阈值参数化;离群按指标分组计算 IQR。

import pandas as pd
import numpy as np

def screen_anomalies(df, value_col, volume_col=None,
                     rd_col=None, rd_limit=20.0,
                     method_upper=None, group_col=None):
    """
    df: 原始数据表
    value_col: 浓度列名
    volume_col: 采样体积列名(可选)
    rd_col: 平行样相对偏差列名(可选)
    rd_limit: 平行样RD控制限(%)
    method_upper: 方法检出上限(可选)
    group_col: 按指标分组计算IQR的列名(可选)
    返回:新增 异常类型 / 处置建议 两列
    """
    out = df.copy()
    out["异常类型"] = "正常"
    out["处置建议"] = "纳入统计"

    # 第1层:缺失
    raw = out[value_col]
    missing_mask = raw.isna() | raw.astype(str).str.strip().isin(["", "-", "NA", "N/A", "null"])
    out.loc[missing_mask, "异常类型"] = "缺失值"
    out.loc[missing_mask, "处置建议"] = "补采或标记剔除,不参与统计"

    # 第2层:逻辑矛盾(仅对非缺失行)
    valid = ~missing_mask
    if volume_col is not None:
        vol_bad = valid & (pd.to_numeric(out[volume_col], errors="coerce") <= 0)
        out.loc[vol_bad, "异常类型"] = "逻辑矛盾"
        out.loc[vol_bad, "处置建议"] = "核对采样体积原始记录"
    if rd_col is not None:
        rd_bad = valid & (pd.to_numeric(out[rd_col], errors="coerce") > rd_limit)
        out.loc[rd_bad, "异常类型"] = "逻辑矛盾"
        out.loc[rd_bad, "处置建议"] = f"平行样RD超{rd_limit}%,重测"
    if method_upper is not None:
        up_bad = valid & (pd.to_numeric(out[value_col], errors="coerce") > method_upper)
        out.loc[up_bad, "异常类型"] = "逻辑矛盾"
        out.loc[up_bad, "处置建议"] = "超出方法上限,稀释复测"

    # 第3层:分布离群(仅对仍为“正常”的行)
    still_ok = out["异常类型"] == "正常"
    num = pd.to_numeric(out[value_col], errors="coerce")
    if group_col is not None:
        groups = out[group_col]
    else:
        groups = pd.Series(["all"] * len(out), index=out.index)

    for g, idx in groups.groupby(groups).groups.items():
        sub = num.loc[idx][still_ok.loc[idx]].dropna()
        if len(sub) < 4:
            continue
        q1, q3 = sub.quantile(0.25), sub.quantile(0.75)
        iqr = q3 - q1
        lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
        flag = (num.loc[sub.index] < lo) | (num.loc[sub.index] > hi)
        out.loc[sub.index[flag], "异常类型"] = "统计离群"
        out.loc[sub.index[flag], "处置建议"] = "查仪器/消解/空白,保留待复核"

    return out

# 教学示例数据
df = pd.DataFrame({
    "编号": ["S1","S2","S3","S4","S5","S6","S7","S8"],
    "指标": ["COD"]*8,
    "COD": [42,45,np.nan,48,44,46,43,210],
    "采样体积": [50,50,50,0,50,50,50,50],
    "平行样RD": [3,5,np.nan,4,28,6,2,7],
})
res = screen_anomalies(df, "COD", volume_col="采样体积",
                       rd_col="平行样RD", rd_limit=20,
                       group_col="指标")
print(res[["编号","COD","异常类型","处置建议"]])

运行后预期输出(教学示例,非实测):S3 为缺失值,S4、S5 为逻辑矛盾,S8 为统计离群,其余为正常。若把 rd_limit 改为 30,则 S5 会回到正常——这说明阈值是业务约定,必须在报告中写明所用控制限,否则初筛结论不可复现。

初筛报告至少应包含:所用判定规则与阈值(IQR 系数、RD 限、方法上限)、每条异常的编号与类型、处置建议、以及“正常”记录数。这样后续复核人员才能判断某条记录是“被规则筛掉”还是“被人工改判”。

不要把“统计离群”直接等同于“错误数据”。离群只说明该值偏离同组分布,可能是真实污染事件,也可能是仪器漂移。初筛阶段应保留原值并标注,交由复测或过程核查确认,避免误删有效证据。