#!/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()