finalize_pseudo_online.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #!/usr/bin/env python3
  2. """Merge final pseudo-online event replays and unlabelled controls."""
  3. from __future__ import annotations
  4. import csv
  5. from pathlib import Path
  6. ROOT=Path("cache/indicator")
  7. OUT=ROOT/"伪在线检测"
  8. EVENT_DIRS=[ROOT/"伪在线检测_7号2025final",ROOT/"伪在线检测_7号2026final",
  9. ROOT/"伪在线检测_4号final",ROOT/"伪在线检测_6号final"]
  10. HEALTH=ROOT/"伪在线健康段v2"
  11. def load(path):
  12. with path.open(encoding="utf-8-sig",newline="") as handle:return list(csv.DictReader(handle))
  13. def write(path,rows,fields=None):
  14. fields=list(dict.fromkeys(key for row in rows for key in row)) if rows else list(fields or [])
  15. with path.open("w",encoding="utf-8-sig",newline="") as handle:
  16. writer=csv.DictWriter(handle,fieldnames=fields);writer.writeheader();writer.writerows(rows)
  17. def main():
  18. OUT.mkdir(parents=True,exist_ok=True)
  19. details=[];bands=[];validations=[]
  20. for folder in EVENT_DIRS:
  21. details+=load(folder/"伪在线_窗口检测明细.csv")
  22. bands+=load(folder/"伪在线_角度带告警.csv")
  23. validations+=load(folder/"伪在线_事件验证.csv")
  24. write(OUT/"伪在线_窗口检测明细.csv",details)
  25. write(OUT/"伪在线_角度带告警.csv",bands)
  26. write(OUT/"伪在线_事件验证.csv",validations)
  27. health=[]
  28. for unit in ("7","8","9"):
  29. path=HEALTH/f"伪在线_未标注对照触发明细_{unit}号.csv"
  30. if path.exists():health+=load(path)
  31. write(OUT/"伪在线_未标注对照触发明细.csv",health,["机组","样本标签"])
  32. control_summary=[]
  33. for unit in sorted({row["机组"] for row in health}):
  34. rows=[row for row in health if row["机组"]==unit]
  35. point_days={(row["测点"],row["日期"]) for row in rows}
  36. candidate={(row["测点"],row["日期"]) for row in rows if row["等级"] in ("候选","持续预警","未分类异常")}
  37. persistent={(row["测点"],row["日期"]) for row in rows if row["等级"]=="持续预警"}
  38. control_summary.append({"机组":unit,"标签":"未标注对照,非确认健康","日期数":len({row["日期"] for row in rows}),
  39. "测点数":len({row["测点"] for row in rows}),"测点日数":len(point_days),
  40. "候选测点日数":len(candidate),"候选测点日率":round(len(candidate)/max(1,len(point_days)),4),
  41. "持续预警测点日数":len(persistent),"持续预警测点日率":round(len(persistent)/max(1,len(point_days)),4),
  42. "说明":"7日运行片段;8号片段距维修17天,不保证健康。用于粗略候选压力测试,不是确认误报率。"})
  43. write(OUT/"伪在线_未标注对照统计.csv",control_summary,["机组","标签"])
  44. common_by_angle={}
  45. grouped={}
  46. for row in details:
  47. if row["等级"] not in ("候选","持续预警"):continue
  48. angle=int(row["角度箱"]);grouped.setdefault(angle,[]).append(row)
  49. for angle,rows in grouped.items():
  50. events=sorted({row["事件"] for row in rows})
  51. if len(events)<2:continue
  52. common_by_angle[angle]={"角度范围":f"{angle}-{angle+10}°","事件数":len(events),"事件":";".join(events),
  53. "测点":";".join(sorted({row["测点"] for row in rows})),
  54. "候选窗":sum(row["等级"]=="候选" for row in rows),"持续预警窗":sum(row["等级"]=="持续预警" for row in rows),
  55. "历史签名匹配窗":sum(row.get("历史事件签名匹配")=="1" for row in rows),
  56. "未分类异常窗":sum(row["等级"]=="未分类异常" for row in rows),
  57. "最大快RMS":round(max(float(row["综合RMS"]) for row in rows),3),
  58. "最大慢RMS":round(max(float(row["慢通道综合RMS"]) for row in rows),3)}
  59. common=sorted(common_by_angle.values(),key=lambda row:(-row["事件数"],-row["持续预警窗"],-row["候选窗"]))
  60. write(OUT/"伪在线_跨事件共性.csv",common,["角度范围","事件数"])
  61. lines=["# 伪在线双通道故障检测验证", "", "## 回放设置", "",
  62. "- 快通道:长历史基线,每测点每天粗采最多3周期;24小时检测窗口使用全部周期并分4个6小时子窗。",
  63. "- 慢通道:本次运行段最早3个有效日期建立固定基线;后续有效日检测。",
  64. "- 两路均使用周期平台归一化、测点自身稳健Z和单机公共变化扣除。",
  65. "- 历史签名只用于标记相似性;未匹配异常仍保留为未分类异常,不静默丢弃。",
  66. "- 全周期大范围异常单列为通道质控,不计入局部气阀候选。", ""]
  67. for event in sorted({row["事件"] for row in validations}):
  68. rows=[row for row in validations if row["事件"]==event]
  69. rows.sort(key=lambda row:(-int(row["持续预警窗口数"]),-float(row["最大综合RMS"])))
  70. lines += [f"## {event}","",f"- 验证候选角度带:{len(rows)}。"]
  71. for i,row in enumerate(rows[:12],1):
  72. lines.append(f"{i}. {row['测点']} {row['角度带']}:首次候选{row['首次候选日期']},提前{row['提前日数']}天,持续预警{row['持续预警窗口数']}窗,快通道RMS {float(row['最大综合RMS']):.1f}。")
  73. lines.append("")
  74. lines += ["## 目标特征验证", "",
  75. "- 7号2026一缸盖侧240~270°:5月17日左右相邻箱形成候选,5月19日前后达到持续条件,较5月21日事件提前约2~4天。",
  76. "- 7号2026二缸盖侧140~150°:慢通道能在事件前触发,但长历史健康对照触发率高,当前只能视为慢漂移候选,尚未证明故障特异性。",
  77. "- 6号三缸轴侧160~180°:快通道在事件前约1~3天触发。",
  78. "- 4号出现多测点候选;候选分散,需降低误报后再判断哪些与故障相关。",
  79. "- 7号2025二缸盖侧全周期异常被标为通道质控,不进入局部故障签名;事件附近缺测,不能证明预测了8月或10月故障。", "",
  80. "## 未标注运行段压力测试", ""]
  81. for row in control_summary:
  82. lines.append(f"- {row['机组']}号:{row['日期数']}日、{row['测点数']}测点;候选测点日率{row['候选测点日率']:.1%},持续预警测点日率{row['持续预警测点日率']:.1%}。这是未标注片段触发率,不是真实误报率。")
  83. lines += ["", "## 判定", "",
  84. "当前实现仍不能上线。健康压力测试中7/8/9号触发率偏高,且慢通道会捕捉季节、负荷和运行段状态转换。检测器可以发现事件前的局部变化,但还没有足够证据判断这些变化是否具有故障特异性。",
  85. "正式阈值冻结前,需要更长、确认无维修/故障的负样本;建立事件级而不是10度箱级的误报控制;对传感器异常和跨运行段工况切换设置独立状态分类。", "",
  86. "## 推荐下一版规则", "",
  87. "1. 保留快慢双通道,慢通道只在每个新运行段开始时重置,不能用全历史模板。",
  88. "2. 先做整通道质控:近零幅、异常覆盖超过半周期、异常平台量程,剔除后再做局部检测。",
  89. "3. 对候选角度带要求至少两个特征族、连续时间支持和局部公共变化残差;历史签名匹配只加分,不作为唯一条件。",
  90. "4. 误报按事件/机组/运行段计数,不能把重叠10度箱和相邻24小时窗口当独立误报。",
  91. "5. 目前未标注对照每台仅7日,且8号段距维修17日;需更多独立健康运行段后再估计生产误报率。"]
  92. (OUT/"伪在线检测结论.md").write_text("\n".join(lines)+"\n",encoding="utf-8")
  93. print("merged",len(details),len(bands),len(validations),len(health),len(common))
  94. if __name__=="__main__":main()