# -*- coding: utf-8 -*- """learn_loop.py — 柱1 P2「判据 n+1 离线学习回路」(RULE-3: 只提案不改阈值, 人裁+版本化+重部署). 飞轮右半的学习件: 修复反馈(故障记录当代理) → 给 agent 候选贴 TP/FP → 每判据代理精度 → **阈值调整提案**(禁自动应用)。补 backfill 的上游 — backfill 需 CLOSED/REFUTED 结局才出提案, 本模块从故障记录**代理标注**产生这些结局(无真修复台账时的 bootstrap)。 ★纪律 (验证程序五次实证 + RULE-3): - **故障记录=代理非真修复验证**: 故障发生≈候选materialized(proxy-TP), 无故障≈proxy-FP; 弱于真"修完回验"。 - **口径对齐**: 只用与判据同域的故障 (温度判据 vs 主传动链热故障, 剔叶片/电气, 复用 validate_pair.is_thermal)。 - **min-evidence 闸 + RULE-3**: 证据不足/精度不明确 → **不提改阈值**(防过拟合弱反馈); 提案全【参考】人裁。 - **欠功效诚实**: 单场/n小 → 提案标"suggestive 非定论"; 跨场需 n≥3 (同 Tier-1 门槛)。 纯核心 (label_precision / threshold_proposals) 无 I/O; per-farm 加载复用 validate_pair。 """ import numpy as np def label_precision(red_turbines, thermal_counts, min_fault=1): """代理精度: 候选红台里 有热故障(proxy-TP) vs 无(proxy-FP)。纯。 red_turbines: set[tid] agent温度红台; thermal_counts: {tid: 严格热故障数} 全台。 返回 {n_candidates, n_proxy_tp, n_proxy_fp, precision_proxy, base_rate}。""" red = {str(t) for t in red_turbines} tp = sum(1 for t in red if thermal_counts.get(t, 0) >= min_fault) fp = len(red) - tp prec = round(tp / len(red), 3) if red else None # base rate: 全台有热故障占比 (判精度是否只是撞base rate) allt = list(thermal_counts) br = round(sum(1 for t in allt if thermal_counts.get(t, 0) >= min_fault) / len(allt), 3) if allt else None return {'n_candidates': len(red), 'n_proxy_tp': tp, 'n_proxy_fp': fp, 'precision_proxy': prec, 'base_rate': br} def threshold_proposals(judge_stats, precision_floor=0.5, min_candidates=8, lift_min=0.15): """判据统计 → 阈值调整提案 (RULE-3: 只【参考】提案, 人裁+版本化+重部署)。纯。 judge_stats: {judge_id: {precision_proxy, n_candidates, base_rate, farms}}。 提案闸: 候选≥min_candidates ∧ 精度