1. 环境数据获取与预处理

相关分析与回归分析对数据质量极为敏感。相关系数会被一个离群点显著拉偏,回归系数会因量纲不一致或时间错位而失去解释力。因此,在进入建模之前,必须先把来自不同来源的环境监测数据整理成一份“干净、对齐、可解释”的数据集。本节围绕三个环节展开:读取常见格式的环境数据、处理缺失值与异常值、完成时间序列对齐,并给出可直接复用的代码骨架。

一、环境数据的三种常见来源与读取方式

环境监测数据通常来自三类载体。第一类是CSV 文本文件,常见于自动监测站导出的小时均值数据,字段如 station_id、datetime、PM2.5、PM10、NO2、O3、temp、RH。第二类是Excel 工作簿,多见于人工整理的月报或实验室检测记录,可能存在合并单元格、多表头、单位写在表头里的情况。第三类是API 接口,例如空气质量开放平台按站点和时间段返回 JSON,需要解析嵌套结构后再转成表格。

读取阶段的核心原则是:先原样读入,再统一字段名与类型。不要一边读一边改,否则出错时难以定位。下面是一段示例代码,演示三种来源的读取与字段规范化(示例数据为合理假设,非实测结果):

import pandas as pd
import requests

# 1) CSV:指定时间列解析,避免被当成字符串
df_csv = pd.read_csv("station_hourly.csv",
                     parse_dates=["datetime"],
                     na_values=["", "NA", "-999", "—"])

# 2) Excel:跳过说明行,指定真实表头行
df_xls = pd.read_excel("monthly_report.xlsx",
                       sheet_name="监测数据", header=2)

# 3) API:解析 JSON 中的 data 列表
resp = requests.get("https://api.example.com/air",
                    params={"station": "S001",
                            "start": "2024-01-01",
                            "end": "2024-01-31"},
                    timeout=10)
records = resp.json()["data"]
df_api = pd.DataFrame(records)

# 统一字段名与类型
rename_map = {"PM2.5": "pm25", "PM10": "pm10",
              "NO2": "no2", "O3": "o3",
              "temp": "temperature", "RH": "humidity"}
for df in (df_csv, df_xls, df_api):
    df.rename(columns=rename_map, inplace=True)
    df["datetime"] = pd.to_datetime(df["datetime"])
    for col in ["pm25", "pm10", "no2", "o3",
                "temperature", "humidity"]:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors="coerce")

这里有两个容易忽略的细节。其一,na_values 要显式列出监测系统常用的缺失标记,如 -999、-9999、空串和破折号,否则它们会被当作真实浓度参与计算。其二,errors="coerce" 会把无法转换的值变成 NaN,这比直接报错更适合处理脏数据,但转换后必须统计各列缺失比例,确认没有整列失效。

二、缺失值:先判断机制,再选择策略

缺失值处理不能一律“填均值”。应先看缺失比例和缺失模式。若某污染物整月缺失超过 40%,通常不建议插补后直接用于回归,而应剔除该变量或缩短分析时段;若缺失是零散的单点缺失,可用时间插值;若缺失成段出现(如仪器停机 6 小时),线性插值会人为制造平滑趋势,应限制插补跨度。

常用做法是:对时间序列按时间索引重采样到统一频率,再用 interpolate 做有限跨度插值,最后用 ffill 兜底短缺口。示例:

df = df_csv.set_index("datetime").sort_index()

# 统一为小时频率,统计缺失
df = df.asfreq("1h")
print(df.isna().mean().round(3))

# 仅对连续缺口不超过 2 小时的做线性插值
df["pm25"] = df["pm25"].interpolate(
    method="time", limit=2, limit_direction="both")

# 温湿度允许稍长跨度,但不超过 3 小时
df["temperature"] = df["temperature"].interpolate(
    method="time", limit=3)

判断标准可以写得很具体:插补后检查插补点占比与插补前后均值、标准差的变化。若某列插补比例超过 10%,或插补后标准差明显缩小,说明插补过度,应回退并考虑剔除该时段。

三、异常值:区分物理越界与统计离群

环境数据的异常值有两类。第一类是物理越界,如 PM2.5 为负值、相对湿度大于 100%、温度出现 -200℃。这类值可直接判定为无效,置为 NaN 后按缺失处理。第二类是统计离群,如某小时 PM2.5 突然达到平日的 8 倍,可能是真实污染过程,也可能是仪器故障,不能简单删除。

推荐用“物理阈值 + 稳健统计”双重筛查。稳健统计用中位数和四分位距(IQR),比均值标准差更抗离群点干扰:

# 物理越界
df.loc[df["pm25"] < 0, "pm25"] = pd.NA
df.loc[df["humidity"] > 100, "humidity"] = pd.NA

# 稳健离群标记(不直接删除)
q1, q3 = df["pm25"].quantile([0.25, 0.75])
iqr = q3 - q1
lower, upper = q1 - 3 * iqr, q3 + 3 * iqr
df["pm25_outlier"] = ~df["pm25"].between(lower, upper)
print(df["pm25_outlier"].sum())

注意这里用 3 倍 IQR 而非 1.5 倍,是因为污染过程本身就会产生长尾高值,1.5 倍会误删真实污染事件。标记出来后,应结合时间上下文人工判断:若高值与风向突变、周边站点同步升高一致,则保留;若单站孤立尖峰且前后骤降,则视为故障并置为缺失。

四、时间序列对齐:让不同来源的数据“站在同一时刻”

相关性分析要求同一时间戳上的变量真正对应。实际数据中常见三种错位:时区不一致(UTC 与本地时间混用)、采样频率不一致(气象为 1 小时、水质为 4 小时)、时间戳代表“时段起点”还是“时段终点”不统一。处理顺序是:统一时区 → 统一频率 → 明确时间戳语义 → 对齐合并。

# 统一到本地时区并转为无时区时间
df["datetime"] = (df["datetime"]
                  .dt.tz_localize("UTC")
                  .dt.tz_convert("Asia/Shanghai")
                  .dt.tz_localize(None))

# 气象数据重采样到小时,取时段均值
met = met.set_index("datetime").resample("1h").mean()

# 按时间索引内连接,只保留双方都有的时刻
merged = df.join(met, how="inner", lsuffix="_air", rsuffix="_met")
print(len(df), len(met), len(merged))

合并后必须核对行数变化。若 inner 连接后行数骤减,说明两套数据的时间覆盖范围或时间戳语义不一致,此时应回到原始数据检查时间字段,而不是直接改用 outer 填充。对齐完成后,建议再画一张双变量时间序列叠加图,肉眼确认峰值是否同步,这是发现“差一小时”类错位最直接的方法。

环境数据预处理:从多源原始数据到可分析数据集 CSV 监测站数据 小时均值 / 多污染物 Excel 月报 多表头 / 单位混写 API 接口 JSON 嵌套结构 读取与规范化 统一字段名 时间列解析 数值类型转换 缺失标记识别 缺失值处理 有限跨度时间插值 插补比例 >10% 回退 异常值处理 物理越界置缺失 3×IQR 标记不删除 时间序列对齐 统一时区 统一采样频率 明确时间戳语义 按索引内连接 干净数据集 时间对齐 · 类型一致 · 缺失可控 · 异常可解释 对齐后核对 行数变化 + 峰值同步性

五、交付前的质量核对

预处理完成后,建议用一张简短的核对表确认数据集可用。以下字段与阈值属于示例性建议,实际项目应按监测规范调整:

检查项判断依据不通过时的处理
时间索引唯一且有序index.is_unique 为真、单调递增去重、排序后重建索引
关键变量缺失比例单列缺失 ≤ 10%缩短时段或剔除该变量
插补点占比插补点 ≤ 总样本 10%减小 limit 或改用剔除
物理越界残留浓度非负、湿度 ≤ 100%复查单位与量纲
对齐后样本量内连接后保留 ≥ 原数据 70%检查时区与时间戳语义

最后强调一点:预处理不是“把数据洗干净就完事”,而是要让每一步处理都可追溯。建议保留原始列、新增 pm25_outlier 这类标记列,并在分析报告中写明插补方法与阈值。这样在后续相关分析发现某两个变量高度相关时,才能判断这是真实的环境规律,还是插补与对齐方式人为制造出来的假象。