2. 铁路沿线邻区关系数据采集与清洗
数据来源与字段约定
铁路沿线邻区关系数据通常从网管系统按“小区—邻区”粒度导出,一条记录代表一个服务小区与其一个邻区的配置关系。为保证后续清洗可复现,先固定导出字段与主键约定:
- 主键:
serving_cell_id + neighbor_cell_id,即“服务小区—邻区”对,同一对只应出现一次。 - 关键字段:服务小区标识、邻区标识、邻区所属线路/区段、邻区经纬度、切换门限、迟滞、小区偏置、邻区优先级、配置状态。
- 方向约定:记录 A→B 表示“A 的邻区表中包含 B”,与 B→A 是两条独立记录,单向邻区即只有其中一条。
导出后先做一次字段级体检:统计总记录数、空值分布、主键重复数,作为清洗前的基线。
异常类型与判定规则
本例需要识别的四类异常,判定条件必须写成可执行的规则,而不是靠人工目测:
| 异常类型 | 判定条件 | 处理动作 |
|---|---|---|
| 缺失值 | 邻区标识、经纬度、门限/迟滞等关键字段为空 | 关键字段缺失则整条剔除;非关键字段缺失标记待补 |
| 重复邻区 | 同一 serving_cell_id + neighbor_cell_id 出现多次 | 保留配置状态有效、更新时间最新的一条 |
| 单向邻区 | 存在 A→B 但不存在 B→A | 标记为单向,输出待补录清单,不直接删除 |
| 越界邻区 | 邻区经纬度落在本线路区段缓冲区之外,或距离超过设定阈值 | 标记越界,人工确认后剔除或转交核查 |
越界判定建议用“线路区段缓冲区 + 距离阈值”双重条件:先按区段外扩缓冲判断是否属于本线路,再用服务小区与邻区的球面距离做二次过滤,避免把跨线邻区误判为越界。
清洗实现(Python 示例)
下面给出可直接运行的清洗脚本,输入为网管导出的 CSV,输出规范化邻区关系表与异常清单。示例数据为教学构造,用于演示规则如何落地。
import pandas as pd
import numpy as np
# 1. 读取导出数据
df = pd.read_csv("neighbor_raw.csv", dtype=str)
df["threshold"] = pd.to_numeric(df["threshold"], errors="coerce")
df["hysteresis"] = pd.to_numeric(df["hysteresis"], errors="coerce")
df["lon"] = pd.to_numeric(df["lon"], errors="coerce")
df["lat"] = pd.to_numeric(df["lat"], errors="coerce")
key = ["serving_cell_id", "neighbor_cell_id"]
critical = ["neighbor_cell_id", "lon", "lat", "threshold", "hysteresis"]
# 2. 缺失值:关键字段缺失整条剔除
mask_missing = df[critical].isna().any(axis=1)
missing_rows = df[mask_missing].copy()
df = df[~mask_missing].copy()
# 3. 重复邻区:按更新时间保留最新一条
df["update_time"] = pd.to_datetime(df["update_time"], errors="coerce")
df = df.sort_values("update_time").drop_duplicates(subset=key, keep="last")
# 4. 单向邻区:构造反向键做存在性匹配
pairs = set(map(tuple, df[key].values))
def is_one_way(row):
return (row["neighbor_cell_id"], row["serving_cell_id"]) not in pairs
df["one_way"] = df.apply(is_one_way, axis=1)
one_way_rows = df[df["one_way"]].copy()
# 5. 越界邻区:区段缓冲 + 距离阈值(示例阈值 15 km)
def haversine(lon1, lat1, lon2, lat2):
r = 6371.0
p1, p2 = np.radians(lat1), np.radians(lat2)
dphi = np.radians(lat2 - lat1)
dlmb = np.radians(lon2 - lon1)
a = np.sin(dphi/2)**2 + np.cos(p1)*np.cos(p2)*np.sin(dlmb/2)**2
return 2 * r * np.arcsin(np.sqrt(a))
df["dist_km"] = haversine(df["serving_lon"], df["serving_lat"], df["lon"], df["lat"])
df["out_of_range"] = (df["dist_km"] > 15) | (df["section_ok"] == "0")
out_rows = df[df["out_of_range"]].copy()
# 6. 输出规范化表与异常清单
clean = df[~df["out_of_range"]].drop(columns=["one_way", "out_of_range"])
clean.to_csv("neighbor_clean.csv", index=False)
missing_rows.to_csv("abnormal_missing.csv", index=False)
one_way_rows.to_csv("abnormal_one_way.csv", index=False)
out_rows.to_csv("abnormal_out_of_range.csv", index=False)
print("原始记录:", len(missing_rows) + len(df))
print("清洗后:", len(clean), "缺失:", len(missing_rows),
"单向:", len(one_way_rows), "越界:", len(out_rows))
脚本中 section_ok 表示邻区是否落在本线路区段缓冲区内(由 GIS 预计算后随导出数据带入),serving_lon/serving_lat 为服务小区坐标。距离阈值 15 km 是示例值,实际应按线路站间距与覆盖半径调整。
清洗流程与结果判读
清洗完成后必须做结果判读,而不是只看脚本跑通:
- 数量守恒:原始记录数 = 清洗后记录数 + 缺失剔除数 + 越界剔除数,重复记录在去重时已合并,需单独核对去重前后差值。
- 单向比例:单向邻区占比过高(例如超过 10%)通常说明导出范围不完整或存在跨系统邻区,应先补全数据再进入优化。
- 越界分布:越界记录若集中在某几个区段,优先核查该区段的线路缓冲区是否设置过窄,而非直接删除。
- 字段完整性:清洗后门限、迟滞、偏置等参数应无空值,否则后续参数计算会失真。
规范化输出与交付物
最终交付一张规范化邻区关系表,字段与约束如下,可直接作为后续优化环节的输入:
| 字段 | 含义 | 约束 |
|---|---|---|
| serving_cell_id | 服务小区标识 | 非空 |
| neighbor_cell_id | 邻区标识 | 非空,与 serving 组合唯一 |
| section_id | 所属线路区段 | 非空 |
| lon / lat | 邻区经纬度 | 非空,落在线路缓冲区内 |
| threshold / hysteresis | 切换门限 / 迟滞 | 非空,数值型 |
| cell_offset | 小区偏置 | 数值型,允许为空待补 |
| priority | 邻区优先级 | 整数 |
| one_way_flag | 单向标记 | 0/1,1 表示待补录 |
不要把单向邻区在清洗阶段直接删除:单向关系可能反映真实的覆盖不对称,也可能是导出遗漏。正确做法是保留并打标,交由后续覆盖交叠分析判断是否需要补录或调整。