2. 基于时间窗口的日志分段与故障事件切分

为什么固定窗口不能直接当事件边界

航次日志是连续时间流,故障事件本身有起止时刻。若只按固定窗口(如 60 s)机械切分,一个跨越窗口边界的故障会被拆成两段,导致后续分类时同一故障出现两个标签。本例的做法是:先用固定窗口做粗分段,再用事件触发规则做合并与边界修正,让每个故障事件片段尽量完整。

核心参数有两个:窗口长度 W 决定粗分段粒度;切分阈值 G(相邻两条日志的最大允许间隔)决定何时判定"事件已结束"。当两条相邻日志的时间差 Δt ≤ G 时视为同一事件延续,Δt > G 时切出新事件。窗口只用于限制单段最大长度,不直接决定事件边界。

固定窗口粗分段 → 事件触发合并 → 独立故障事件片段 0s 30s 60s 90s 120s 150s 180s 窗口 W1 (0–60s) 窗口 W2 (60–120s) 窗口 W3 (120–180s) 事件A:推进器电流异常(跨 W1/W2 边界) 事件B:深度传感器漂移 事件C:通信超时 Δt=30s Δt=60s 切分判定(阈值 G = 45s) • 事件A内相邻日志 Δt ≤ 30s ≤ G → 合并,即使跨越 W1/W2 边界也不拆开 • 事件A末条(300s)与事件B首条(360s) Δt = 60s > G → 切出新事件B • 事件B末条(500s)与事件C首条(620s) Δt = 120s > G → 切出新事件C • 若 G 取 90s,则事件B与C会被错误合并为同一片段

分段与切分的实现

下面代码读取已对齐字段的结构化日志(字段:ts 秒级时间戳、source 来源、level 级别、msg 描述),先按窗口长度 W 打上窗口编号,再按阈值 G 做事件切分,最后输出事件片段表。示例数据为教学构造,用于演示切分逻辑。

import pandas as pd

# 教学示例:已对齐的结构化日志(ts 为航次内相对秒)
logs = pd.DataFrame([
    {"ts": 90,  "source": "sensor",  "level": "WARN",  "msg": "推进器电流偏高"},
    {"ts": 150, "source": "sensor",  "level": "WARN",  "msg": "推进器电流持续偏高"},
    {"ts": 210, "source": "control", "level": "ERROR", "msg": "推进器过流保护"},
    {"ts": 300, "source": "control", "level": "ERROR", "msg": "推进器输出受限"},
    {"ts": 360, "source": "sensor",  "level": "WARN",  "msg": "深度读数漂移"},
    {"ts": 420, "source": "sensor",  "level": "WARN",  "msg": "深度读数漂移扩大"},
    {"ts": 500, "source": "control", "level": "ERROR", "msg": "深度控制偏差超限"},
    {"ts": 620, "source": "comm",    "level": "ERROR", "msg": "通信超时"},
    {"ts": 700, "source": "comm",    "level": "ERROR", "msg": "通信重连失败"},
]).sort_values("ts").reset_index(drop=True)

W = 60   # 窗口长度(秒)
G = 45   # 切分阈值(秒)

# 1) 固定窗口粗分段:仅打标签,不决定事件边界
logs["window_id"] = (logs["ts"] // W).astype(int)

# 2) 事件触发切分:相邻日志间隔 > G 则新事件
logs["gap"] = logs["ts"].diff().fillna(0)
logs["event_id"] = (logs["gap"] > G).cumsum()

# 3) 汇总事件片段
events = logs.groupby("event_id").agg(
    start_ts=("ts", "min"),
    end_ts=("ts", "max"),
    n_logs=("ts", "size"),
    windows=("window_id", lambda s: sorted(set(s))),
    sources=("source", lambda s: sorted(set(s))),
).reset_index()

print(events.to_string(index=False))

预期输出(由上述输入直接推出):

 event_id  start_ts  end_ts  n_logs  windows      sources
        0        90     300       4   [1, 3, 5]  [control, sensor]
        1       360     500       3   [6, 8]     [control, sensor]
        2       620     700       2  [10, 11]    [comm]

窗口长度与阈值的影响

事件 0 的 windows 为 [1,3,5],说明它跨越了多个固定窗口——这正是固定窗口不能直接当事件边界的原因。若把窗口当作事件边界,事件 0 会被拆成 3 段,同一故障出现 3 个标签。

参数组合事件数事件 0 是否完整问题
W=60, G=453完整无(推荐基线)
W=60, G=902完整事件 B 与 C 被错误合并
W=60, G=205被拆开事件 0 内部 Δt=30s 被误切
W=30, G=453完整窗口更细,但事件边界仍由 G 决定

结论:事件完整性主要由 G 决定,W 只影响粗分段粒度。G 应大于同一故障内相邻日志的典型间隔,小于不同故障之间的最小间隔。本例中同一故障内最大间隔为 30s,不同故障间最小间隔为 60s,故 G 取 45s 合适。

选取 G 的实用方法:统计同一故障内相邻日志间隔的分布,取其高分位数(如 P90)作为 G 下限;再统计不同故障间的最小间隔作为 G 上限。两者之间取值即可保证不拆同一故障、不并不同故障。
若日志时间戳存在乱序或重复,需先按 ts 排序并去重,否则 diff() 会产生负间隔或零间隔,导致事件切分错误。本例已用 sort_values 排序。

验证切分结果

验证分两步:一是检查每个事件片段内相邻日志间隔是否都 ≤ G;二是检查不同事件之间的间隔是否都 > G。两条同时成立,说明切分满足"不拆同一故障、不并不同故障"的约束。

# 片段内最大间隔应 ≤ G
inner_max = logs.groupby("event_id")["gap"].max()
assert (inner_max <= G).all(), "存在片段内间隔超过阈值"

# 片段间最小间隔应 > G
starts = events["start_ts"].tolist()
ends = events["end_ts"].tolist()
between = [starts[i+1] - ends[i] for i in range(len(events)-1)]
assert all(b > G for b in between), "存在片段间间隔未超过阈值"

print("切分校验通过:事件数 =", len(events))

对示例数据运行上述校验,inner_max 为 30s(≤45s),片段间间隔为 60s 与 120s(均 >45s),校验通过,事件数为 3。若把 G 改为 90s,片段间间隔 60s 不满足 >90s,校验会失败并提示"存在片段间间隔未超过阈值",从而暴露错误合并。