2. 近岸SST数据质量检查与缺测标记

为什么近岸SST必须先做质量检查

近岸SST数据在进入异常识别流程前,必须先经过质量检查。原因在于近岸环境本身存在大量干扰:卫星反演在岸边受陆地污染和混合像元影响,浮标数据可能因生物附着、传感器漂移或通信中断产生缺测与跳变,再分析产品在浅水与岸线附近也可能出现不合理的插值值。若直接计算距平,这些缺陷会被放大成虚假的“异常事件”。因此本案例的任务是:对一份近岸SST数据完成缺测识别、重复记录剔除、物理越界判定、时间跳变检测,输出缺测掩膜与质量标记,并统计各站位的有效数据比例。

数据对象与检查规则

假设我们拿到一份近岸SST观测表,字段包括:station_id(站位编号)、time(观测时间,逐日)、sst(海表温度,℃)、lon、lat。检查规则按以下顺序执行,顺序本身很重要:先剔除重复,再判缺测,再判越界,最后判跳变,避免重复记录干扰跳变判断。

  • 重复:同一 station_id 与 time 出现多条记录,保留第一条,其余标记为重复。
  • 缺测:sst 为空、NaN 或等于填充值(如 -999)。
  • 越界:近岸SST物理合理范围取 -2 ℃ 至 35 ℃,超出即判越界。
  • 跳变:同一站位相邻两日SST差值绝对值超过 5 ℃/天,判为可疑跳变(近岸日变化通常远小于此值)。
阈值不是固定真理。-2~35 ℃ 与 5 ℃/天 是本例的教学示例参数,实际业务中应结合海域气候、观测方式和时间分辨率调整,并在文档中记录所用阈值。

质量检查与标记的处理链

下图展示从原始记录到质量标记与有效比例统计的处理链,以及每条规则对应的判定条件与输出字段。注意跳变检测依赖“前一个有效值”,因此它必须排在缺测与越界剔除之后。

原始SST表 station_id / time sst / lon / lat ① 重复剔除 同站位同时间多条 保留首条 ② 缺测判定 空值 / NaN / -999 → 缺测掩膜 ③ 物理越界 sst < -2 或 > 35 单位 ℃ ④ 时间跳变 |Δsst| > 5 ℃/天 对比前一有效值 质量标记表 qc_flag 字段 0有效 1重复 2缺测 3越界 4跳变 站位有效比例 有效数 / 总记录数 逐站位统计 缺测掩膜(布尔) 顺序约束:重复→缺测→越界→跳变;跳变检测只与前一“有效”值比较,避免被缺测或越界值污染。 qc_flag 为整型编码,便于后续按标记筛选或统计;缺测掩膜单独输出,供距平计算时直接屏蔽。

用Python实现检查与标记

下面给出一个可直接运行的实现。它读取一份示例CSV,按上述顺序生成 qc_flag 与 missing_mask,并输出各站位有效比例。示例数据在代码中直接构造,便于验证逻辑。

import pandas as pd
import numpy as np

# 构造教学示例数据(含重复、缺测、越界、跳变)
rows = [
    ("S01", "2023-01-01", 18.2), ("S01", "2023-01-02", 18.5),
    ("S01", "2023-01-02", 18.5),          # 重复
    ("S01", "2023-01-03", np.nan),        # 缺测
    ("S01", "2023-01-04", 19.0),
    ("S01", "2023-01-05", 40.0),          # 越界
    ("S01", "2023-01-06", 19.4),
    ("S02", "2023-01-01", 16.0), ("S02", "2023-01-02", 16.3),
    ("S02", "2023-01-03", 16.1), ("S02", "2023-01-04", 25.0),  # 跳变
    ("S02", "2023-01-05", 16.8),
]
df = pd.DataFrame(rows, columns=["station_id", "time", "sst"])
df["time"] = pd.to_datetime(df["time"])
df = df.sort_values(["station_id", "time"]).reset_index(drop=True)

# ① 重复:同站位同时间保留首条
df["qc_flag"] = 0
dup = df.duplicated(subset=["station_id", "time"], keep="first")
df.loc[dup, "qc_flag"] = 1

# ② 缺测:空值 / NaN / -999
miss = df["sst"].isna() | (df["sst"] == -999)
df.loc[miss & (df["qc_flag"] == 0), "qc_flag"] = 2

# ③ 越界:-2 ~ 35 ℃
out = (df["sst"] < -2) | (df["sst"] > 35)
df.loc[out & (df["qc_flag"] == 0), "qc_flag"] = 3

# ④ 跳变:与前一有效值比较,|Δsst| > 5
df["prev_valid"] = df.groupby("station_id")["sst"].transform(
    lambda s: s.where(df.loc[s.index, "qc_flag"] == 0).ffill().shift(1)
)
jump = (df["qc_flag"] == 0) & (df["prev_valid"].notna()) & \
       ((df["sst"] - df["prev_valid"]).abs() > 5)
df.loc[jump, "qc_flag"] = 4

# 缺测掩膜与有效比例
df["missing_mask"] = df["qc_flag"].isin([2, 3, 4])
valid_ratio = df.groupby("station_id").apply(
    lambda g: (g["qc_flag"] == 0).sum() / len(g)
).rename("valid_ratio")

print(df[["station_id", "time", "sst", "qc_flag", "missing_mask"]])
print(valid_ratio)

运行后应观察到:S01 的 2023-01-02 第二条记录 qc_flag=1;2023-01-03 为 qc_flag=2;2023-01-05 的 40.0 ℃ 为 qc_flag=3;S02 的 2023-01-04 的 25.0 ℃ 相对前一有效值 16.1 ℃ 跳变 8.9 ℃,为 qc_flag=4。有效比例 S01 为 4/7≈0.571,S02 为 4/5=0.8(示例数据下的预期结果,实际运行以输出为准)。

跳变检测中“前一有效值”的取法容易出错。若直接用 shift(1) 而不屏蔽缺测与越界值,跳变判断会被无效值污染,产生误报或漏报。本例通过 where(qc_flag==0) 先屏蔽无效值再前向填充,保证比较对象是真正的有效观测。

结果判读与后续使用

质量标记表是后续所有步骤的入口。计算距平时,应只使用 qc_flag==0 的记录;缺测掩膜用于在空间分布图上直接留白,避免把缺测误画成异常。各站位有效比例则用于筛选:若某站位有效比例过低(例如低于 0.6),其距平序列的统计意义不足,应在异常识别前剔除或单独说明。下表给出本例的判读要点。

qc_flag含义后续处理
0有效观测参与距平计算与异常识别
1重复记录剔除,不参与统计
2缺测写入缺测掩膜,距平留空
3物理越界视为无效,检查传感器或反演问题
4时间跳变标记可疑,人工复核后再决定是否保留

完成这一步后,数据已具备进入气候态基准构建的条件:有效观测清晰、缺测位置明确、异常值有据可查。质量标记应随数据一起保存,而不是丢弃,以便在后续发现异常事件时能回溯到原始观测的质量状态。