4. 数据清洗与质控的Python基础工具

海洋结构监测系统(如导管架平台应变监测、浮式平台运动监测、海底管道振动监测)通常以固定采样频率连续记录数据,单日数据量可达数十万至数百万条。面对这样的数据规模,手工处理既不现实也不可靠。在正式进入缺失值填补、异常识别、平滑滤波等清洗任务之前,需要先搭建一套稳定的编程环境,并掌握三个核心库的基本操作:NumPy负责数值数组与向量化计算,Pandas负责带时间标签的表格数据组织与聚合,Matplotlib负责把数据形态和清洗效果画出来。本节以一段模拟的海洋平台应变监测数据为例,走通“读取—切片—聚合—可视化”的完整链路。

一、环境准备与数据对象约定

推荐使用 Anaconda 创建独立环境,避免不同项目的库版本互相干扰:

conda create -n marine_qc python=3.11
conda activate marine_qc
conda install numpy pandas matplotlib

本节使用的示例数据为合理假设的模拟数据:某导管架平台在 2024-06-01 00:00 至 2024-06-02 00:00 期间,以 1 Hz 采样频率记录 4 个应变测点(SG01~SG04)的应变值(单位 με),并附带一个环境温度通道。数据以 CSV 文件保存,字段为 timestamp, SG01, SG02, SG03, SG04, temp。需要强调:以下所有数值结果均为示例演示,不代表任何实际工程测量值。

二、NumPy:数值数组与向量化运算

NumPy 的核心是 ndarray,它把同类型数值连续存放在内存中,使整列运算不必写循环。读取 CSV 的数值部分后,可以先做几件清洗前必须确认的事:

import numpy as np

strain = np.loadtxt("platform_strain_20240601.csv",
                    delimiter=",", skiprows=1, usecols=(1, 2, 3, 4))
print(strain.shape)        # (86400, 4)
print(strain.dtype)        # float64

# 基本统计:逐通道均值、标准差、极值
print(np.mean(strain, axis=0))
print(np.std(strain, axis=0))
print(np.min(strain, axis=0), np.max(strain, axis=0))

# 布尔索引定位疑似异常(示例阈值:|应变| > 300 με)
mask = np.abs(strain) > 300
print(mask.sum(axis=0))    # 各通道超阈值点数

这里 axis=0 表示沿时间方向压缩,得到每个测点的统计量;mask 是一个与数据同形状的布尔数组,可直接用于计数或后续替换。向量化写法 np.abs(strain) > 300 一次完成 86400×4 个元素的比较,比 Python 循环快一到两个数量级。此外,np.isnan(strain).sum() 可快速统计缺失数量,np.diff(timestamp_seconds) 可检查采样间隔是否恒为 1 s——这两项是质控前的例行检查。

三、Pandas:时间索引、切片与聚合

NumPy 数组没有时间标签,而监测数据的清洗几乎都围绕时间展开,因此实际工作中以 Pandas 为主入口:

import pandas as pd

df = pd.read_csv("platform_strain_20240601.csv",
                 parse_dates=["timestamp"])
df = df.set_index("timestamp").sort_index()
print(df.index.freq)       # 若为 None,说明存在缺测或时间跳变

切片方面,Pandas 支持按时间字符串直接截取区间,例如 df.loc["2024-06-01 08:00":"2024-06-01 10:00"] 取出两小时数据,df["SG01"] 取出单列,df.iloc[0:3600] 按位置取前 3600 行。若需按固定窗口统计,用 resample:

# 10 分钟均值与标准差,用于观察趋势和波动
stat10 = df.resample("10min").agg(["mean", "std", "count"])
print(stat10["SG01"].head())

# 按小时统计各通道最大值与缺失数
hourly = df.resample("1h").agg({"SG01": "max", "SG02": "max",
                                "SG03": "max", "SG04": "max"})
print(hourly)

count 列尤其重要:若某 10 分钟窗口的 count 明显小于 600,说明该时段存在缺测,这正是后续清洗要处理的对象。agg 支持对不同列指定不同函数,也可传入自定义函数,例如用 lambda x: x.quantile(0.99) 统计高分位数,用于判断冲击型异常。

四、Matplotlib:把数据形态画出来

清洗前先画图,是判断数据“长什么样”的最快方式。下面同时画出原始时序与 10 分钟均值:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(2, 1, figsize=(11, 6), sharex=True)
ax[0].plot(df.index, df["SG01"], lw=0.4, color="#2b7bba")
ax[0].set_ylabel("应变 / με")
ax[0].set_title("SG01 原始时序(示例数据)")

ax[1].plot(stat10.index, stat10[("SG01", "mean")],
           color="#e07b39", marker="o", ms=2)
ax[1].set_ylabel("10 min 均值 / με")
ax[1].set_xlabel("时间")
plt.tight_layout()
plt.savefig("sg01_overview.png", dpi=150)

观察要点:原始曲线若出现孤立尖峰(单点跳变后立即恢复),多为传感器噪声或瞬时干扰;若出现持续平台段或长时间恒定值,可能是信号饱和或采集卡死;若 10 分钟均值曲线在某时段突然抬升,则需回到原始数据核对该时段是否存在阶跃。这些判断都依赖图形,而非单纯统计量。

监测数据处理三件套:分工与数据流 原始 CSV 文件 timestamp + SG01~SG04 1 Hz,86400 行 NumPy ndarray 数值数组 向量化统计 / 布尔掩码 NaN 计数、采样间隔检查 Pandas 时间索引 DataFrame loc 切片 / resample 聚合 count 列暴露缺测窗口 Matplotlib 时序曲线 均值趋势图 形态判读 典型操作链(示例) 读取 read_csv + parse_dates 切片 loc 时间区间 / iloc 行号 聚合 resample("10min").agg 可视化 plot + savefig 判读 尖峰/缺测 图形形态与清洗线索(示例判读) 孤立尖峰 单点跳变后立即恢复 → 疑似噪声/瞬时干扰 平台段/恒定值 长时间数值不变 → 疑似饱和或采集卡死 count 不足 10 min 窗口少于 600 条 → 该时段存在缺测

五、把四步串成一条可复用的检查脚本

将上述操作组织成一个函数,每次拿到新数据先跑一遍,能快速形成对数据质量的初步判断:

def quick_check(path, channels=("SG01", "SG02", "SG03", "SG04")):
    df = pd.read_csv(path, parse_dates=["timestamp"]).set_index("timestamp")
    df = df.sort_index()
    report = {}
    report["行数"] = len(df)
    report["时间跨度"] = df.index[-1] - df.index[0]
    report["缺失数"] = df[list(channels)].isna().sum().to_dict()
    report["超阈值数"] = (df[list(channels)].abs() > 300).sum().to_dict()
    win = df[list(channels)].resample("10min").count()
    report["不完整窗口数"] = int((win < 600).any(axis=1).sum())
    return report

print(quick_check("platform_strain_20240601.csv"))