4. 时间维度解析与监测点标识处理

在多监测点指标对比任务中,原始数据往往只提供一列“采集时间”字符串和一列“监测点编号”。如果直接拿这两列做分组统计,会遇到三个典型问题:时间字符串无法按小时、星期聚合;同一监测点在不同数据源中编码不一致;编码与中文名称之间缺少稳定映射。本节围绕这三个问题展开,目标是把“原始时间 + 原始编码”整理成“可分组的时间特征 + 统一的监测点标识”,为后续的对比与分组统计打好基础。

一、时间列转换为 datetime 并提取派生特征

假设原始数据来自三个监测点,字段为 point_code、point_name、ts、value,其中 ts 形如 "2024-03-11 08:15:00"。第一步是用 pd.to_datetime 转换类型,并显式指定格式,避免解析歧义:

import pandas as pd

df = pd.read_csv("monitor_raw.csv")
df["ts"] = pd.to_datetime(df["ts"], format="%Y-%m-%d %H:%M:%S", errors="coerce")

# 检查解析失败的行
bad = df[df["ts"].isna()]
print("解析失败行数:", len(bad))

df["date"]  = df["ts"].dt.date          # 日期
df["hour"]  = df["ts"].dt.hour          # 小时 0-23
df["weekday"] = df["ts"].dt.dayofweek   # 星期一=0,星期日=6
df["is_weekend"] = df["weekday"] >= 5  # 是否周末
df["week_label"] = df["ts"].dt.day_name()  # 英文星期名,便于展示

这里有两个细节值得注意。其一,errors="coerce" 会把无法解析的值置为 NaT,而不是直接抛异常,便于先统计再决定丢弃还是修正;实际项目中应先打印失败行,确认是格式不统一还是脏数据。其二,dayofweek 的约定是星期一为 0、星期日为 6,这与部分人直觉中的“周日为 0”不同,全文和图示都按此约定,后续判断周末用 >= 5 即可。

派生特征的选择取决于分析目标。若关注日内波动,用 hour 分组;若关注工作日与周末差异,用 is_weekend;若关注周期性,可再加 weekday。不建议一次性堆砌十几个时间字段,字段越多,后续透视表的维度越难读。一个实用做法是先明确“对比什么问题”,再决定提取哪几个特征。

二、监测点编码与名称映射的统一

真实数据中,同一监测点常出现多种写法,例如 "A01"、"a-01"、"A1" 可能指向同一设备,而中文名称又可能是“一号站”“1号监测点”。如果直接按原始编码分组,同一监测点会被拆成多组,对比结果失真。处理思路是建立一张编码映射表,把各种别名收敛到唯一的标准编码,再挂接标准名称。

# 别名到标准编码的映射
alias_map = {
    "A01": "P_A", "a-01": "P_A", "A1": "P_A",
    "B02": "P_B", "b-02": "P_B",
    "C03": "P_C", "c_03": "P_C",
}

# 标准编码到标准名称的映射
name_map = {"P_A": "东区一号站", "P_B": "西区二号站", "P_C": "北区三号站"}

df["point_code_std"] = df["point_code"].str.strip().map(alias_map)
df["point_name_std"] = df["point_code_std"].map(name_map)

# 未匹配到的编码单独列出,人工确认
unmapped = df[df["point_code_std"].isna()]["point_code"].unique()
print("未映射编码:", unmapped)

映射表应作为独立配置文件维护,而不是散落在代码里。这样新增监测点时只改配置,不改统计逻辑。映射完成后,务必检查 unmapped 是否为空;若不为空,说明存在未登记的别名,需要补充映射或确认是否为异常数据。这一步是保证后续分组统计“组数正确”的关键。

建议在映射后增加一行校验:df.groupby("point_code_std")["point_name_std"].nunique(),正常情况下每个标准编码只对应一个名称,若出现大于 1,说明名称映射存在冲突。

三、时间与标识的联合校验

时间特征和监测点标识处理完后,还需要做一次联合校验,确认数据在“时间 × 监测点”两个维度上是否完整。常用方法是构造透视表,观察每个监测点在每个小时是否有记录:

check = df.pivot_table(
    index="point_code_std", columns="hour",
    values="value", aggfunc="count", fill_value=0
)
print(check)

如果某监测点在某些小时计数为 0,可能是设备离线、采集缺失或时区问题。此时不要急于填充,应先判断缺失是“真实无数据”还是“解析错误”。例如时间列若混入了 UTC 与本地时间,小时分布会出现整体偏移,这类问题在转换阶段就应通过统一时区解决,而不是留到统计阶段猜测。

时间维度解析与监测点标识处理 原始数据 point_code: "a-01" ts: "2024-03-11 08:15" value: 42.6 时间解析 to_datetime(format=...) errors="coerce" 检查 NaT date / hour / weekday is_weekend(周一=0) 监测点标识统一 alias_map 别名→标准编码 name_map 编码→标准名称 strip() 去空格 未映射编码单独列出 联合校验:时间 × 监测点 pivot_table(index=标准编码, columns=hour, aggfunc="count") 计数为 0 → 判断离线 / 缺失 / 时区偏移 可分组的时间特征 + 统一监测点标识

四、处理结果如何判断是否达标

完成上述步骤后,可以用三条标准判断处理是否到位。第一,df["ts"].isna().sum() 应为 0,或剩余缺失行已明确标记为待处理;第二,df["point_code_std"].isna().sum() 应为 0,说明所有编码都已映射;第三,按 point_code_std 分组后,每组对应的 point_name_std 唯一。三条都满足,才说明时间维度和监测点标识已经可以放心用于后续的分组统计。

需要强调的是,时间特征提取和编码映射不是一次性的清洗动作,而是后续所有对比分析的基础。若这一步留下歧义,例如小时字段混用了不同时区、编码映射遗漏了别名,后面无论用多复杂的透视表或图表,结论都不可靠。因此建议把映射表、时间格式和校验逻辑固化为可复用的处理函数,每次导入新数据时自动执行,并在日志中输出未映射编码和解析失败行数,形成可追溯的数据准备记录。

不要用 df["ts"].dt.hour 直接对未转换的字符串列操作,否则会报错或得到错误结果;转换与派生特征必须按顺序执行,且转换后先校验再派生。

至此,原始数据已经具备“按日期、小时、星期观察”和“按统一监测点分组”的能力。下一步即可在此基础上构造透视表,比较不同监测点在同一时间粒度下的指标差异,并进一步做分组聚合与异常识别。