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 填充。对齐完成后,建议再画一张双变量时间序列叠加图,肉眼确认峰值是否同步,这是发现“差一小时”类错位最直接的方法。
五、交付前的质量核对
预处理完成后,建议用一张简短的核对表确认数据集可用。以下字段与阈值属于示例性建议,实际项目应按监测规范调整:
| 检查项 | 判断依据 | 不通过时的处理 |
|---|---|---|
| 时间索引唯一且有序 | index.is_unique 为真、单调递增 | 去重、排序后重建索引 |
| 关键变量缺失比例 | 单列缺失 ≤ 10% | 缩短时段或剔除该变量 |
| 插补点占比 | 插补点 ≤ 总样本 10% | 减小 limit 或改用剔除 |
| 物理越界残留 | 浓度非负、湿度 ≤ 100% | 复查单位与量纲 |
| 对齐后样本量 | 内连接后保留 ≥ 原数据 70% | 检查时区与时间戳语义 |
最后强调一点:预处理不是“把数据洗干净就完事”,而是要让每一步处理都可追溯。建议保留原始列、新增 pm25_outlier 这类标记列,并在分析报告中写明插补方法与阈值。这样在后续相关分析发现某两个变量高度相关时,才能判断这是真实的环境规律,还是插补与对齐方式人为制造出来的假象。