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经验公式换算,不能简单线性缩放;深度与压力之间也需按纬度做近似换算。凡是涉及非线性换算的量,都应在数据字典中注明公式来源与适用范围。
质量控制结果如何判读
清洗完成后不要只看“还剩多少行”,而要看质量结构。建议输出一张按变量汇总的质控表:各qc等级占比、插值填补点数、剔除点数、每小时有效样本中位数。若某变量qc=3占比超过10%,说明传感器可能系统性故障,应回溯原始记录而非继续调参;若n_valid在夜间集中下降,可能是供电或通信问题,这类信息对后续解释曲线缺口至关重要。
| 检查项 | 示例判据 | 处理动作 |
|---|---|---|
| 时间索引 | 无重复、UTC、单调递增 | 去重、排序、时区转换 |
| 缺失率 | 单变量 < 5% | 短缺口时间插值,长缺口保留NaN |
| 物理越界 | 温度 -2~40℃ | 置NaN,qc=3 |
| 统计异常 | 偏离滑动中位数 > 4×MAD | qc=2,交叉验证后决定 |
| 重采样有效性 | 每小时有效样本 ≥ 50% | 不足则小时均值降级为可疑 |
| 单位一致性 | 同列单一量纲 | 换算并更新单位字段 |
完成上述处理后,得到的是一份时间连续、量纲统一、每列带质量标记的数据集。它未必“完美无缺”,但每个缺口和可疑点都有据可查,这正是海洋监测可视化可信度的基础。