3. 轨道检测数据清洗基础

轨道检测车、便携式轨检仪和人工检查记录汇总到一起后,得到的原始数据往往"能用但不干净":某一段里程的轨距值缺失、同一检测日期被重复导入两次、个别高低偏差出现明显不合理的跳变、里程列被读成了字符串。这些问题如果不处理,后续的统计指标、超限判断和趋势分析都会失真。数据清洗就是把这些"脏"的部分识别出来、按规则处理,并保存成一份可复用的干净数据集。

本节以一份典型的轨道几何检测数据为例展开。假设原始文件 track_raw.csv 包含以下字段:line_name(线路名)、mileage(里程,单位 km)、detect_date(检测日期)、gauge(轨距,mm)、level(水平,mm)、twist(三角坑,mm)、align(轨向,mm)、high_low(高低,mm)。下面按"识别—处理—校验—保存"的实际工作顺序讲解,但每一步都给出可执行的具体做法。

缺失值:先看清缺在哪、缺多少

缺失值处理的第一步不是急着填充,而是先量化。用 pandas 读入后,先看整体缺失比例,再看缺失是否集中在某些线路或某些检测批次:

import pandas as pd

df = pd.read_csv("track_raw.csv")
print(df.isna().sum())                 # 各列缺失数量
print(df.isna().mean().round(3))       # 各列缺失比例
print(df.groupby("line_name")["gauge"].apply(lambda s: s.isna().mean()))

假设输出显示 gauge 缺失约 2.1%、twist 缺失约 5.4%,且 twist 的缺失主要集中在某一条线路的早期批次。这时就要判断缺失性质:如果缺失是随机的、比例很低,可以用中位数填充;如果缺失成片出现,说明该批次可能根本没测三角坑,填充反而会制造虚假数据,应当标记为无效而不是补值。

对轨道几何数据,一个实用的原则是:单点偶发缺失用邻近里程插值,成段缺失保留为空并记录原因。例如按里程排序后对 gauge 做线性插值,但限制只对连续缺失不超过 3 个点的位置插值:

df = df.sort_values(["line_name", "mileage"])
df["gauge"] = (df.groupby("line_name")["gauge"]
                 .transform(lambda s: s.interpolate(limit=3, limit_area="inside")))

这里 limit=3 表示最多连续补 3 个点,limit_area="inside" 表示只补两端都有实测值之间的空缺,不会向外 extrapolate。这样既补上了零星漏测,又不会在数据边界外凭空造值。

重复记录:按业务主键去重

重复记录常见于多次导入同一文件、或不同检测方式对同一里程重复记录。判断重复不能只看整行是否完全相同,而应结合业务主键。对轨道检测数据,合理的唯一标识是"线路 + 里程 + 检测日期":

key = ["line_name", "mileage", "detect_date"]
print(df.duplicated(subset=key).sum())          # 主键重复条数
df = df.drop_duplicates(subset=key, keep="last")

keep="last" 表示保留后导入的那条,通常后导入的是修正版。如果两条记录主键相同但测量值不同,不能简单丢弃,应先比对差异:若差异在测量误差范围内(例如轨距差小于 0.5 mm),保留任一条即可;若差异明显,说明存在数据冲突,应保留并打上"待复核"标记,交由业务人员确认,而不是在清洗阶段擅自取舍。

异常值:用物理范围和统计规则双重筛查

轨道几何量都有明确的物理合理范围。轨距标准值 1435 mm,检测值一般落在 1420~1455 mm 之间;水平、三角坑、轨向、高低的偏差量通常在 ±20 mm 以内。超出这个范围的值,要么是传感器故障,要么是单位或小数点错误。先用固定阈值做第一道筛查:

limits = {"gauge": (1420, 1455), "level": (-20, 20),
          "twist": (-20, 20), "align": (-20, 20), "high_low": (-20, 20)}

for col, (lo, hi) in limits.items():
    bad = df[(df[col] < lo) | (df[col] > hi)]
    print(col, "越界条数:", len(bad))

固定阈值能抓住明显的录入错误,但抓不住"在范围内却明显偏离本段整体水平"的跳变点。第二道用统计规则,例如对每公里区段计算中位数和 MAD(中位数绝对偏差),把偏离超过 6 倍 MAD 的点标为可疑:

def flag_outlier(s, k=6):
    med = s.median()
    mad = (s - med).abs().median()
    if mad == 0:
        return pd.Series(False, index=s.index)
    return (s - med).abs() > k * 1.4826 * mad

df["gauge_flag"] = (df.groupby(df["mileage"] // 1)["gauge"]
                      .transform(flag_outlier))

注意异常值不等于错误值。轨道上真实存在的个别大偏差恰恰是需要重点关注的对象,因此清洗阶段只做"标记"而不直接删除,把 gauge_flag 为 True 的记录单独导出复核,确认是测量错误后再剔除。这是轨道检测数据清洗与一般业务数据清洗的重要区别。

数据类型转换:让字段可计算、可比较

从 CSV 读入的列常被识别为 object 类型,尤其是里程、日期和带单位的数值。转换时要显式指定,并处理转换失败的情况:

df["mileage"] = pd.to_numeric(df["mileage"], errors="coerce")
df["detect_date"] = pd.to_datetime(df["detect_date"], errors="coerce")
for col in ["gauge", "level", "twist", "align", "high_low"]:
    df[col] = pd.to_numeric(df[col], errors="coerce")

print(df.dtypes)
print(df[df["detect_date"].isna()])   # 转换失败的日期行

errors="coerce" 会把无法转换的值变成 NaN,而不是直接报错中断。转换后必须回查:日期转换失败的行往往对应格式混乱的记录(如"2024/3/5"与"2024-03-05"混用),数值转换失败的行可能是混入了"<0.1"这类带符号的文本。这些行要么统一格式后重转,要么按缺失值规则处理,不能放任 NaN 悄悄进入后续计算。

清洗结果校验与保存

清洗完成后不要直接覆盖原文件,而应保存为新文件并保留清洗日志。保存前做一次整体校验:行数变化、各列缺失率、异常标记数量、里程是否单调、日期范围是否合理。

df.to_csv("track_clean.csv", index=False, encoding="utf-8-sig")
df.to_parquet("track_clean.parquet", index=False)   # 便于后续快速读取

下面的图示概括了本节各处理环节之间的数据流与判断关系,可作为实际操作时的对照检查:

轨道检测数据清洗流程与判断关系 原始数据 track_raw.csv 缺失值识别 isna / 分组缺失率 重复记录去除 线路+里程+日期 异常值筛查 物理阈值 + MAD 偶发缺失→插值 成段缺失→保留空值 值一致→保留一条 值冲突→标记待复核 仅标记不删除 复核后决定剔除 数据类型转换 to_numeric / to_datetime,errors="coerce" 后回查 清洗结果校验 行数变化 · 缺失率 · 异常标记数 · 里程单调性 保存干净数据 track_clean.csv / .parquet 清洗日志 记录每步处理条数与原因

保存时建议同时输出一份清洗日志,记录每一步处理前后的行数、缺失率变化和异常标记数量。例如"缺失值插值:gauge 补 37 点;重复去除:删除 12 条;异常标记:gauge 标记 5 条、twist 标记 8 条"。这份日志是数据可追溯性的依据,也是后续复核和向他人说明数据质量的基础。清洗后的数据应保持原始字段含义不变,只新增标记列(如 gauge_flag),不要随意改名或合并字段,以免下游分析脚本无法对接。

清洗顺序建议为:先做类型转换,再做缺失与重复处理,最后做异常筛查。因为类型不对时,缺失判断和数值比较都可能出错;而异常筛查放在最后,可以基于已经去重、补缺后的数据计算统计量,结果更稳定。
不要用全局均值填充轨道几何量。轨道偏差沿里程呈区段性分布,全局均值会抹平区段差异,使后续的超限统计偏乐观。需要填充时优先用同线路邻近里程值,并限制插值跨度。