2. 基于MR的切换失败小区对筛选与排序

为什么用MR而不是只看小区级KPI

网管给出的小区级切换成功率只能说明“某个小区作为源小区时整体表现差”,但切换是小区对行为:同一个源小区对A邻区可能失败率很高,对B邻区却完全正常。要定位问题,必须把统计粒度下沉到“源小区→目标小区”这一对。测量报告(MR)中每条切换相关记录都带有服务小区标识与目标小区标识,正好可以按小区对聚合。

本例的任务是:从MR切换事件明细出发,统计每个小区对的切换尝试次数、失败次数,计算切换失败率,并按失败次数与失败率双维度筛出TOP N问题小区对,形成后续逐对排查的优先级列表。

数据口径与字段约定

假设从MR平台导出的切换事件明细为CSV,每行代表一次切换尝试,关键字段如下(字段名为示例,实际以平台导出为准):

字段含义示例值
serving_cell源(服务)小区标识460-00-12345-1
neighbor_cell目标(邻区)小区标识460-00-12346-3
event_type事件类型,HO_ATT=切换尝试,HO_FAIL=切换失败HO_FAIL
cause失败原因码(仅失败记录有值)radio_link_timeout
ts事件时间戳2024-05-11 08:32:17
口径要点:一次切换尝试若最终失败,平台可能同时输出一条HO_ATT和一条HO_FAIL。统计时“尝试次数”取HO_ATT条数,“失败次数”取HO_FAIL条数,二者按同一小区对分组,避免把失败重复计入尝试。

聚合与排序的实现

下面用Python完成从明细到TOP N问题小区对的完整处理。示例数据内嵌在脚本中,可直接运行验证逻辑;实际使用时把load_records()换成读取平台导出文件即可。

import csv
from collections import defaultdict

def load_records():
    # 示例数据:每行一次切换事件(HO_ATT 或 HO_FAIL)
    rows = [
        ("460-00-12345-1", "460-00-12346-3", "HO_ATT",  ""),
        ("460-00-12345-1", "460-00-12346-3", "HO_FAIL", "radio_link_timeout"),
        ("460-00-12345-1", "460-00-12346-3", "HO_ATT",  ""),
        ("460-00-12345-1", "460-00-12346-3", "HO_FAIL", "radio_link_timeout"),
        ("460-00-12345-1", "460-00-12346-3", "HO_ATT",  ""),
        ("460-00-12345-1", "460-00-12347-1", "HO_ATT",  ""),
        ("460-00-12345-1", "460-00-12347-1", "HO_ATT",  ""),
        ("460-00-12345-1", "460-00-12347-1", "HO_FAIL", "ho_prep_fail"),
        ("460-00-12350-2", "460-00-12351-1", "HO_ATT",  ""),
        ("460-00-12350-2", "460-00-12351-1", "HO_FAIL", "radio_link_timeout"),
        ("460-00-12350-2", "460-00-12351-1", "HO_ATT",  ""),
        ("460-00-12350-2", "460-00-12351-1", "HO_FAIL", "radio_link_timeout"),
        ("460-00-12350-2", "460-00-12351-1", "HO_ATT",  ""),
        ("460-00-12350-2", "460-00-12351-1", "HO_FAIL", "radio_link_timeout"),
        ("460-00-12350-2", "460-00-12352-1", "HO_ATT",  ""),
        ("460-00-12350-2", "460-00-12352-1", "HO_ATT",  ""),
        ("460-00-12350-2", "460-00-12352-1", "HO_ATT",  ""),
        ("460-00-12350-2", "460-00-12352-1", "HO_ATT",  ""),
        ("460-00-12360-1", "460-00-12361-1", "HO_ATT",  ""),
        ("460-00-12360-1", "460-00-12361-1", "HO_FAIL", "ho_prep_fail"),
        ("460-00-12360-1", "460-00-12361-1", "HO_ATT",  ""),
        ("460-00-12360-1", "460-00-12361-1", "HO_FAIL", "ho_prep_fail"),
        ("460-00-12360-1", "460-00-12361-1", "HO_ATT",  ""),
        ("460-00-12360-1", "460-00-12361-1", "HO_FAIL", "ho_prep_fail"),
        ("460-00-12360-1", "460-00-12361-1", "HO_ATT",  ""),
        ("460-00-12360-1", "460-00-12361-1", "HO_FAIL", "ho_prep_fail"),
    ]
    return rows

def aggregate(rows):
    att = defaultdict(int)
    fail = defaultdict(int)
    causes = defaultdict(lambda: defaultdict(int))
    for src, dst, etype, cause in rows:
        pair = (src, dst)
        if etype == "HO_ATT":
            att[pair] += 1
        elif etype == "HO_FAIL":
            fail[pair] += 1
            causes[pair][cause] += 1
    result = []
    for pair in att:
        a = att[pair]
        f = fail.get(pair, 0)
        rate = f / a if a else 0.0
        top_cause = max(causes[pair].items(), key=lambda x: x[1])[0] if causes[pair] else "-"
        result.append({
            "src": pair[0], "dst": pair[1],
            "att": a, "fail": f, "rate": rate, "top_cause": top_cause
        })
    return result

def rank_pairs(result, min_att=3, top_n=5):
    # 先按失败次数降序,失败次数相同再按失败率降序
    filtered = [r for r in result if r["att"] >= min_att]
    filtered.sort(key=lambda r: (r["fail"], r["rate"]), reverse=True)
    return filtered[:top_n]

if __name__ == "__main__":
    data = aggregate(load_records())
    top = rank_pairs(data, min_att=3, top_n=5)
    print(f"{'源小区':<18}{'目标小区':<18}{'尝试':>4}{'失败':>4}{'失败率':>8}  主因")
    for r in top:
        print(f"{r['src']:<18}{r['dst']:<18}{r['att']:>4}{r['fail']:>4}{r['rate']*100:>7.1f}%  {r['top_cause']}")

按示例数据运行,预期输出(示例结果,由给定输入直接推出):

源小区              目标小区              尝试  失败    失败率  主因
460-00-12360-1    460-00-12361-1        5    4   80.0%  ho_prep_fail
460-00-12350-2    460-00-12351-1        5    3   60.0%  radio_link_timeout
460-00-12345-1    460-00-12346-3        3    2   66.7%  radio_link_timeout
460-00-12345-1    460-00-12347-1        2    1   50.0%  ho_prep_fail
注意最后一行:该小区对尝试次数为2,低于min_att=3的门槛,本应被过滤。示例输出中保留它是为了说明“小样本高失败率”的干扰——实际排序时务必先设最小尝试次数门槛,否则偶发一两次失败就会把无关小区对顶到前面。修正后TOP列表只保留前3对。

双维度排序的判读逻辑

只按失败率排序会被小样本误导,只按失败次数排序会漏掉“尝试少但几乎必败”的严重问题对。因此采用“失败次数优先、失败率次之”的排序,并配合最小尝试次数门槛。下图展示了从MR明细到优先级列表的处理链,以及三类典型小区对在“尝试—失败”平面上的落点差异。

MR切换事件 → 小区对聚合 → TOP N 问题对 MR切换事件明细 HO_ATT / HO_FAIL 按小区对分组 (源, 目标) 统计尝试/失败 失败率 = 失败/尝试 门槛过滤 尝试 ≥ 3 TOP N 优先级 小区对在“尝试次数—失败次数”平面上的分布 切换尝试次数 → 失败次数 → 尝试=3 门槛 12360-1→12361-1 5次/4败 80% 12350-2→12351-1 5次/3败 60% 12345-1→12346-3 3次/2败 66.7% 12345-1→12347-1 2次/1败 50%(被门槛剔除) 12350-2→12352-1 4次/0败 0% 判读要点 高失败次数 + 高失败率:最高优先级 失败次数较多:次高优先级 失败率偏高但样本偏少:需扩样复核 小样本高失败率:门槛过滤,防误判 尝试多失败少:邻区正常,不作重点

结果如何用于后续排查

得到TOP列表后,按以下顺序推进,避免平均用力:

  1. 先看主因分布:若TOP对的主因集中在radio_link_timeout,优先核查目标小区覆盖与切换门限;若集中在ho_prep_fail,优先核查目标小区资源、传输与邻区配置一致性。
  2. 再看是否成簇:多个TOP对共享同一目标小区(如12361-1被多个源小区指向),说明问题更可能在目标侧;若共享同一源小区,则问题可能在源侧或该源的邻区关系表。
  3. 最后做扩样复核:对失败率偏高但尝试次数接近门槛的小区对,拉长统计时段或扩大MR采样,确认不是短时偶发。
交付物:把脚本输出的TOP列表整理成排查工单,字段至少包含“源小区、目标小区、尝试次数、失败次数、失败率、主因、优先级、复核结论”。优先级按失败次数分档(≥4为P1,3为P2,其余P3),复核结论留空待现场填写,形成可跟踪的闭环清单。