2. 海洋监测数据清洗与预处理实战

海洋监测数据在进入可视化之前,往往是一份“毛坯数据”:浮标传感器偶发掉线造成整段缺失,CTD剖面在跃层附近出现盐度尖峰,多台仪器时钟不同步导致温盐序列错位,溶解氧单位在mg/L与µmol/L之间混用。若直接把这些数据画成曲线,得到的可能是断裂、抖动或量纲混乱的图,结论自然不可信。本节围绕缺失值、异常值、时间序列对齐、单位转换四条主线,用pandas完成重采样与质量控制,最终产出一份可直接用于绘图的干净数据集。

先建立统一的数据约定

清洗前必须先固定几条约定,否则后续每一步都会产生歧义。第一,时间统一为UTC,并设为DatetimeIndex,避免本地时区与夏令时干扰;第二,物理量统一采用海洋学常用单位:温度℃、盐度(无量纲)、溶解氧mg/L、流速m/s、深度m(向下为正);第三,为每个变量保留一列质量控制标记qc_变量名,取值为0(未检验)、1(通过)、2(可疑)、3(剔除)。这套约定让“数据是否可用”成为可查询的列,而不是靠记忆判断。

import pandas as pd
import numpy as np

df = pd.read_csv("buoy_2024.csv", parse_dates=["time"])
df = df.set_index("time").sort_index()
df = df[~df.index.duplicated(keep="first")]   # 去重复时间戳
df.index = df.index.tz_localize("UTC") if df.index.tz is None else df.index.tz_convert("UTC")
print(df.shape, df.index.min(), df.index.max())

缺失值:先判断成因,再决定填补策略

缺失不是一种情况。短时丢包(如单点缺测)适合插值;传感器故障造成的连续长段缺失,插值只会制造虚假的平滑曲线,应当保留为NaN并在图上断线。实操中先统计缺失比例与连续缺失长度:

miss = df["temp"].isna()
run_id = (miss != miss.shift()).cumsum()
gap_len = miss.groupby(run_id).sum()
print(df.isna().mean().round(3))          # 各变量缺失比例
print(gap_len[gap_len > 0].describe())    # 连续缺失段长度分布

经验规则可设为:连续缺失不超过3个采样间隔且总缺失率低于5%时,用时间插值df["temp"].interpolate(method="time", limit=3);超过阈值则标记为不可用,不填补。对盐度这类变化缓慢的量可用前向填充配合限长,但温度在跃层附近梯度大,前向填充会引入偏差,应优先时间插值。填补后务必在qc_temp中记录处理方式,保证可追溯。

异常值:物理阈值加统计判据双保险

海洋数据异常值检测分两层。第一层是物理合理范围,例如温度-2~40℃、盐度0~42、溶解氧0~20mg/L、流速0~5m/s,越界直接判为剔除。第二层是统计判据,常用滑动中位数绝对偏差(MAD):以窗口内中位数为基准,偏离超过k倍MAD视为可疑。相比均值±3σ,MAD对尖峰不敏感,不会被异常值本身拉偏。

def mad_flag(s, window=25, k=4.0):
    med = s.rolling(window, center=True, min_periods=5).median()
    mad = (s - med).abs().rolling(window, center=True, min_periods=5).median()
    return (s - med).abs() > k * 1.4826 * mad

df.loc[(df.temp < -2) | (df.temp > 40), "qc_temp"] = 3
df.loc[mad_flag(df.temp), "qc_temp"] = 2
df.loc[df.qc_temp == 3, "temp"] = np.nan

注意MAD判据给出的是“可疑”而非“错误”,可疑点应结合相邻时刻与同站其他变量交叉验证,例如盐度尖峰若同时伴随温度跳变,更可能是传感器故障;若仅盐度单点异常而温度平稳,可能是生物附着或气泡干扰。剔除后不要立即插值,先让曲线出现断口,人工确认再决定是否补。

时间序列对齐与重采样

多仪器观测的采样节奏不同:温盐深仪可能1分钟一条,溶解氧10分钟一条,气象站1小时一条。对齐的核心是统一到同一时间网格。上采样(提高频率)用插值,下采样(降低频率)用聚合。海洋监测中常用10分钟或1小时网格:

grid = pd.date_range(df.index.min().floor("h"),
                     df.index.max().ceil("h"), freq="10min", tz="UTC")
aligned = df.reindex(grid)
aligned["temp"] = aligned["temp"].interpolate(method="time", limit=3)
hourly = aligned.resample("1h").agg({"temp":"mean","salt":"mean",
                                     "do":"mean","qc_temp":"max"})
hourly["n_valid"] = aligned["temp"].resample("1h").count()

聚合时质量控制标记取最大值(最差状态),并统计每小时有效样本数n_valid。若某小时有效样本少于应有数量的50%,该小时均值应降级为可疑。这一步是后续画日均曲线、计算统计量时的可靠性依据。

单位转换:换算与标记同步完成

溶解氧常见µmol/L与mg/L两种单位,换算关系为1 mg/L ≈ 31.25 µmol/L(以O₂摩尔质量32 g/mol近似)。单位转换必须成对记录,避免同一列混用两种量纲:

if df["do_unit"].iloc[0] == "umol/L":
    df["do_mgL"] = df["do"] / 31.25
else:
    df["do_mgL"] = df["do"]
df["do_unit"] = "mg/L"

盐度若以电导率形式给出,需按PSS-78经验公式换算,不能简单线性缩放;深度与压力之间也需按纬度做近似换算。凡是涉及非线性换算的量,都应在数据字典中注明公式来源与适用范围。

海洋监测数据清洗与质量控制流程 原始浮标数据 多仪器 / 多频率 时间统一 UTC + DatetimeIndex 去重复时间戳 缺失值诊断 缺失率 / 连续段长 短缺口插值 异常值检测 物理阈值 + MAD 标记可疑/剔除 单位转换 µmol/L → mg/L 电导率 → 盐度 重采样对齐 10min 网格 1h 聚合 + n_valid 质量控制标记 qc = 0/1/2/3 聚合取最差状态 干净数据集(供可视化使用) 统一时间网格 · 统一单位 · 每列带 qc 标记 · 保留缺失断口 绘图前按 qc ≤ 1 过滤,可疑点单独标注

质量控制结果如何判读

清洗完成后不要只看“还剩多少行”,而要看质量结构。建议输出一张按变量汇总的质控表:各qc等级占比、插值填补点数、剔除点数、每小时有效样本中位数。若某变量qc=3占比超过10%,说明传感器可能系统性故障,应回溯原始记录而非继续调参;若n_valid在夜间集中下降,可能是供电或通信问题,这类信息对后续解释曲线缺口至关重要。

检查项示例判据处理动作
时间索引无重复、UTC、单调递增去重、排序、时区转换
缺失率单变量 < 5%短缺口时间插值,长缺口保留NaN
物理越界温度 -2~40℃置NaN,qc=3
统计异常偏离滑动中位数 > 4×MADqc=2,交叉验证后决定
重采样有效性每小时有效样本 ≥ 50%不足则小时均值降级为可疑
单位一致性同列单一量纲换算并更新单位字段
把清洗步骤写成函数并保留中间列(原始值、填补值、qc标记),比直接覆盖原列更利于复现与排错。可视化阶段只需按qc过滤,就能在同一份数据上画出“全部观测”和“质控后序列”两条曲线做对照。

完成上述处理后,得到的是一份时间连续、量纲统一、每列带质量标记的数据集。它未必“完美无缺”,但每个缺口和可疑点都有据可查,这正是海洋监测可视化可信度的基础。