瀏覽代碼

功图:事件前1-2个有效段追溯(lookback_scan),更新观察记录(过渡段无变化/六缸盖侧传感器在先)

18922397810 3 周之前
父節點
當前提交
d8e2b910fa
共有 2 個文件被更改,包括 171 次插入 和 1 次删除
  1. 153 0
      backend/indicator_features/lookback_scan.py
  2. 18 1
      backend/indicator_features/观察记录.md

+ 153 - 0
backend/indicator_features/lookback_scan.py

@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""Look back over the previous 1-2 valid data segments before each event.
+
+Many repairs have no data immediately before them (machine stopped long before
+the repair was recorded).  This groups the days that actually contain an
+indicator cycle into segments and inspects the last one or two segments before
+the event: peer-relative levels and within-segment drift, plus a comparison
+between the two segments to expose a per-cylinder divergence.  Read-only.
+"""
+from __future__ import annotations
+
+import argparse
+import csv
+from collections import defaultdict
+from datetime import datetime
+from pathlib import Path
+
+import numpy as np
+
+EVENTS = [
+    ("7", "2025-01-10", "活塞"),
+    ("7", "2025-08-08", "气阀"),
+    ("7", "2025-10-07", "气阀"),
+    ("7", "2026-05-21", "气阀"),
+    ("8", "2025-02-17", "气阀"),
+    ("8", "2025-12-06", "活塞"),
+    ("8", "2026-03-10", "十字头"),
+    ("8", "2026-03-29", "气阀"),
+    ("8", "2026-04-01", "气阀"),
+    ("9", "2026-01-18", "活塞"),
+]
+CYLINDERS = ("一", "二", "三", "四", "五", "六")
+SIDES = ("盖侧", "轴侧")
+FEATURES = ("p_discharge", "pressure_ratio", "efficiency")
+
+
+def load(path):
+    rows = []
+    for row in csv.DictReader(open(path, encoding="utf-8-sig")):
+        row["day"] = row["sample_time"][:10]
+        for name in FEATURES:
+            try:
+                row[name] = float(row[name])
+            except (TypeError, ValueError):
+                row[name] = None
+        rows.append(row)
+    return rows
+
+
+def segments(days, gap=10):
+    days = sorted(set(days))
+    out = []
+    for day in days:
+        if out and (datetime.strptime(day, "%Y-%m-%d") - datetime.strptime(out[-1][-1], "%Y-%m-%d")).days <= gap:
+            out[-1].append(day)
+        else:
+            out.append([day])
+    return out
+
+
+def peer_relative(rows, feature):
+    by_ts = defaultdict(dict)
+    for row in rows:
+        if row[feature] is not None:
+            by_ts[(row["day"], row["side"])][row["cylinder"]] = row[feature]
+    out = defaultdict(dict)
+    for (day, side), members in by_ts.items():
+        clean = {c: v for c, v in members.items() if v is not None}
+        if len(clean) < 2:
+            continue
+        med = float(np.median(list(clean.values())))
+        if abs(med) < 1e-9:
+            continue
+        for cylinder, value in clean.items():
+            out[(cylinder, side)][day] = (value - med) / med
+    return out
+
+
+def seg_summary(rows, seg):
+    seg_rows = [r for r in rows if r["day"] in set(seg)]
+    peer = {feature: peer_relative(seg_rows, feature) for feature in ("p_discharge", "pressure_ratio")}
+    summary = {}
+    drift = {}
+    for feature, series in peer.items():
+        for key, daily in series.items():
+            summary[(key[0], key[1], feature)] = (float(np.median(list(daily.values()))), len(daily))
+            days = sorted(daily)
+            if len(days) >= 8:
+                x = np.array([(datetime.strptime(d, "%Y-%m-%d") - datetime.strptime(days[0], "%Y-%m-%d")).days for d in days], dtype=float)
+                y = np.array([daily[d] for d in days], dtype=float)
+                if x[-1] > 0:
+                    slope = float(np.polyfit(x, y, 1)[0])
+                    scale = float(np.median(np.abs(y - np.median(y)))) * 1.4826 or float(np.std(y))
+                    if scale > 1e-9:
+                        drift[(key[0], key[1], feature)] = float(slope * x[-1] / scale)  # total drift in MADs
+    return summary, drift
+
+
+def main():
+    parser = argparse.ArgumentParser(description="事件前 1~2 个有效数据段观察")
+    parser.add_argument("--data-dir", default=str(Path(__file__).resolve().parents[1] / "cache" / "indicator"))
+    parser.add_argument("--segments", type=int, default=2)
+    args = parser.parse_args()
+
+    cache = {}
+    for unit in ("7", "8", "9"):
+        path = Path(args.data_dir) / f"all_{unit}_2025_2026.csv"
+        cache[unit] = load(path) if path.is_file() else []
+
+    for unit, date, kind in EVENTS:
+        rows = cache[unit]
+        all_days = sorted({r["day"] for r in rows})
+        segs = [s for s in segments(all_days) if s[-1] < date]
+        print(f"\n== {unit}号机 {date} {kind}  事件前有效段 {len(segs)},取最近 {args.segments}")
+        chosen = segs[-args.segments:]
+        summaries = []
+        for i, seg in enumerate(reversed(chosen), start=1):
+            summary, drift = seg_summary(rows, seg)
+            summaries.append((seg, summary))
+            print(f"   段{i} {seg[0]}~{seg[-1]} ({len(seg)}天)   peer相对 p_discharge | pressure_ratio")
+            for cylinder in CYLINDERS:
+                cells = []
+                for side in SIDES:
+                    pd = summary.get((cylinder, side, "p_discharge"))
+                    pr = summary.get((cylinder, side, "pressure_ratio"))
+                    if pd is None and pr is None:
+                        continue
+                    pd_s = f"{pd[0]*100:+.0f}%" if pd else "-"
+                    pr_s = f"{pr[0]*100:+.0f}%" if pr else "-"
+                    cells.append(f"{side}:Pd{pd_s}/R{pr_s}")
+                if cells:
+                    print(f"      {cylinder}缸  " + "  ".join(cells))
+            strong = [(k, v) for k, v in sorted(drift.items(), key=lambda kv: -abs(kv[1])) if abs(v) >= 2.0][:4]
+            if strong:
+                print("      段内漂移(>=2MAD): " + "; ".join(f"{k[0]}缸{k[1]} {k[2]} {v:+.1f}" for k, v in strong))
+        if len(summaries) == 2:
+            (seg_prev, sum_prev), (seg_last, sum_last) = summaries[1], summaries[0]
+            changes = []
+            for key in set(sum_prev) & set(sum_last):
+                cylinder, side, feature = key
+                delta = sum_last[key][0] - sum_prev[key][0]
+                # only peer-relative, ignore tiny absolute
+                if abs(delta) >= 0.05:
+                    changes.append((abs(delta), f"{cylinder}缸{side} {feature}: {sum_prev[key][0]*100:+.1f}% -> {sum_last[key][0]*100:+.1f}%"))
+            changes.sort(reverse=True)
+            if changes:
+                print("   两段对比(变化>=5%):")
+                for _, text in changes[:6]:
+                    print("      " + text)
+
+
+if __name__ == "__main__":
+    main()

+ 18 - 1
backend/indicator_features/观察记录.md

@@ -55,6 +55,22 @@
 
 **结论:全事件扫描仍未发现气阀/活塞的缸级故障漂移;找到的异常几乎都是传感器故障。**
 
+### 4.7 往前追溯 1~2 个有效段(`lookback_scan.py`)
+把“有功图周期”的日期聚成段(相邻间隔>10 天分段),对每个事件取之前最近的 1~2 段,比较段间 peer 相对值,并在长段(≥8 天)内做漂移。
+
+| 事件 | 追溯到的段(最近→更早) | 观察 |
+|---|---|---|
+| 7 号机 2025-08-08 气阀 | 2025-07-25~27、2025-06-11~13 | 两段 peer 几乎一致 → **无变化** |
+| 7 号机 2025-10-07 气阀 | 2025-07-25~27、2025-06-11~13(2025-10 无数据) | 同上,**无变化** |
+| 7 号机 2026-05-21 气阀 | 2026-03-12~04-04(22天)、2026-02-10~16 | **六缸盖侧**:段内漂移(pressure_ratio −3.4MAD)× 段间 +4.8%→−23.0% → **传感器退化,事件前两段即可见** |
+| 8 号机 2025-12-06 活塞 | 2025-03-13~19、2025-04-29~05-03 | 多缸同步变(一缸 Pd、四/六压比)→ 工况/启停;且距事件 8~9 个月 |
+| 8 号机 2026-03-10 十字头 | 同上 | 同上 |
+| 8 号机 2026-03-29 / 04-01 气阀 | 2026-03-15~20、2025-04-29~05-03 | 六缸盖侧已死(−99%,传感器);四轴压比 +24%→+79%(配置) |
+| 9 号机 2026-01-18 活塞 | 2025-10-13~16、2025-09-04~09 | 二/三缸一致 → **无变化** |
+| 7 号机 2025-01-10 活塞、8 号机 2025-02-17 气阀 | 无 | 该时段**完全无 data** |
+
+结论:往前追溯 1~2 段后,**仍未发现气阀/活塞的缸级特征**;唯一在事件前两段内可见的是 **7 号机六缸盖侧的传感器退化**。
+
 ### 4.5 长窗口慢趋势(`trend.py` 的 slow_trends,数月、每点每天4条)
 | 机组 | 最主要的慢变化 | 判断 |
 |---|---|---|
@@ -158,7 +174,7 @@
 |---|---|---|---|
 | 7 号机 | 2025-01-10 | 活塞(一级3#缸,笔误) | ❌ 无 |
 | 7 号机 | 2025-08-08 | 气阀(污染) | ✅ 2025-07-25~27 |
-| 7 号机 | 2025-10-07 | 气阀(一级排气阀阀片) | ❌ 2025-10 无,最近 07-27 |
+| 7 号机 | 2025-10-07 | 气阀(一级排气阀阀片) | ❌ 2025-10 无;已追溯 07-25~27、06-11~13 两段,无变化 |
 | 7 号机 | 2026-05-21 | 气阀(进气阀组油压管) | ✅ 2026-05-07~21(连续) |
 | 8 号机 | 2025-02-17 | 气阀(阀片) | ❌ 数据 2025-03 才开始 |
 | 8 号机 | 2025-12-06 | 活塞(开裂) | ❌ 故障前无 |
@@ -223,4 +239,5 @@
 - 方法对**缓慢/阶跃变化是敏感的**:稳定抓出了两处**传感器故障**(7 号机六缸盖侧骤降、二缸盖侧卡死 13.3 MPa)以及一处**年度配置变化**(8 号机四缸轴侧排序反转)。
 - 但在**现有功图数据**上,**没有找到可归因于气阀/活塞、且只在单缸出现的故障特征**;已有较大变化均被解释为工况、配置或传感器问题。
 - 主要制约:活塞事件故障前无 `wave_sample_one`;气阀事件多为突发或数据有长缺口。
+- 补充:对“故障前缺失”的事件已**往前追溯 1~2 个有效段**(`lookback_scan.py`,见 4.7);追溯后仍无功图故障特征,唯一在事件前两段内可见的是 7 号机六缸盖侧的传感器退化。