lookback_scan.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #!/usr/bin/env python3
  2. """Look back over the previous 1-2 valid data segments before each event.
  3. Many repairs have no data immediately before them (machine stopped long before
  4. the repair was recorded). This groups the days that actually contain an
  5. indicator cycle into segments and inspects the last one or two segments before
  6. the event: peer-relative levels and within-segment drift, plus a comparison
  7. between the two segments to expose a per-cylinder divergence. Read-only.
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. import csv
  12. from collections import defaultdict
  13. from datetime import datetime
  14. from pathlib import Path
  15. import numpy as np
  16. EVENTS = [
  17. ("7", "2025-01-10", "活塞"),
  18. ("7", "2025-08-08", "气阀"),
  19. ("7", "2025-10-07", "气阀"),
  20. ("7", "2026-05-21", "气阀"),
  21. ("8", "2025-02-17", "气阀"),
  22. ("8", "2025-12-06", "活塞"),
  23. ("8", "2026-03-10", "十字头"),
  24. ("8", "2026-03-29", "气阀"),
  25. ("8", "2026-04-01", "气阀"),
  26. ("9", "2026-01-18", "活塞"),
  27. ]
  28. CYLINDERS = ("一", "二", "三", "四", "五", "六")
  29. SIDES = ("盖侧", "轴侧")
  30. FEATURES = ("p_discharge", "pressure_ratio", "efficiency")
  31. def load(path):
  32. rows = []
  33. for row in csv.DictReader(open(path, encoding="utf-8-sig")):
  34. row["day"] = row["sample_time"][:10]
  35. for name in FEATURES:
  36. try:
  37. row[name] = float(row[name])
  38. except (TypeError, ValueError):
  39. row[name] = None
  40. rows.append(row)
  41. return rows
  42. def segments(days, gap=10):
  43. days = sorted(set(days))
  44. out = []
  45. for day in days:
  46. if out and (datetime.strptime(day, "%Y-%m-%d") - datetime.strptime(out[-1][-1], "%Y-%m-%d")).days <= gap:
  47. out[-1].append(day)
  48. else:
  49. out.append([day])
  50. return out
  51. def peer_relative(rows, feature):
  52. by_ts = defaultdict(dict)
  53. for row in rows:
  54. if row[feature] is not None:
  55. by_ts[(row["day"], row["side"])][row["cylinder"]] = row[feature]
  56. out = defaultdict(dict)
  57. for (day, side), members in by_ts.items():
  58. clean = {c: v for c, v in members.items() if v is not None}
  59. if len(clean) < 2:
  60. continue
  61. med = float(np.median(list(clean.values())))
  62. if abs(med) < 1e-9:
  63. continue
  64. for cylinder, value in clean.items():
  65. out[(cylinder, side)][day] = (value - med) / med
  66. return out
  67. def seg_summary(rows, seg):
  68. seg_rows = [r for r in rows if r["day"] in set(seg)]
  69. peer = {feature: peer_relative(seg_rows, feature) for feature in ("p_discharge", "pressure_ratio")}
  70. summary = {}
  71. drift = {}
  72. for feature, series in peer.items():
  73. for key, daily in series.items():
  74. summary[(key[0], key[1], feature)] = (float(np.median(list(daily.values()))), len(daily))
  75. days = sorted(daily)
  76. if len(days) >= 8:
  77. x = np.array([(datetime.strptime(d, "%Y-%m-%d") - datetime.strptime(days[0], "%Y-%m-%d")).days for d in days], dtype=float)
  78. y = np.array([daily[d] for d in days], dtype=float)
  79. if x[-1] > 0:
  80. slope = float(np.polyfit(x, y, 1)[0])
  81. scale = float(np.median(np.abs(y - np.median(y)))) * 1.4826 or float(np.std(y))
  82. if scale > 1e-9:
  83. drift[(key[0], key[1], feature)] = float(slope * x[-1] / scale) # total drift in MADs
  84. return summary, drift
  85. def main():
  86. parser = argparse.ArgumentParser(description="事件前 1~2 个有效数据段观察")
  87. parser.add_argument("--data-dir", default=str(Path(__file__).resolve().parents[1] / "cache" / "indicator"))
  88. parser.add_argument("--segments", type=int, default=2)
  89. args = parser.parse_args()
  90. cache = {}
  91. for unit in ("7", "8", "9"):
  92. path = Path(args.data_dir) / f"all_{unit}_2025_2026.csv"
  93. cache[unit] = load(path) if path.is_file() else []
  94. for unit, date, kind in EVENTS:
  95. rows = cache[unit]
  96. all_days = sorted({r["day"] for r in rows})
  97. segs = [s for s in segments(all_days) if s[-1] < date]
  98. print(f"\n== {unit}号机 {date} {kind} 事件前有效段 {len(segs)},取最近 {args.segments}")
  99. chosen = segs[-args.segments:]
  100. summaries = []
  101. for i, seg in enumerate(reversed(chosen), start=1):
  102. summary, drift = seg_summary(rows, seg)
  103. summaries.append((seg, summary))
  104. print(f" 段{i} {seg[0]}~{seg[-1]} ({len(seg)}天) peer相对 p_discharge | pressure_ratio")
  105. for cylinder in CYLINDERS:
  106. cells = []
  107. for side in SIDES:
  108. pd = summary.get((cylinder, side, "p_discharge"))
  109. pr = summary.get((cylinder, side, "pressure_ratio"))
  110. if pd is None and pr is None:
  111. continue
  112. pd_s = f"{pd[0]*100:+.0f}%" if pd else "-"
  113. pr_s = f"{pr[0]*100:+.0f}%" if pr else "-"
  114. cells.append(f"{side}:Pd{pd_s}/R{pr_s}")
  115. if cells:
  116. print(f" {cylinder}缸 " + " ".join(cells))
  117. strong = [(k, v) for k, v in sorted(drift.items(), key=lambda kv: -abs(kv[1])) if abs(v) >= 2.0][:4]
  118. if strong:
  119. print(" 段内漂移(>=2MAD): " + "; ".join(f"{k[0]}缸{k[1]} {k[2]} {v:+.1f}" for k, v in strong))
  120. if len(summaries) == 2:
  121. (seg_prev, sum_prev), (seg_last, sum_last) = summaries[1], summaries[0]
  122. changes = []
  123. for key in set(sum_prev) & set(sum_last):
  124. cylinder, side, feature = key
  125. delta = sum_last[key][0] - sum_prev[key][0]
  126. # only peer-relative, ignore tiny absolute
  127. if abs(delta) >= 0.05:
  128. changes.append((abs(delta), f"{cylinder}缸{side} {feature}: {sum_prev[key][0]*100:+.1f}% -> {sum_last[key][0]*100:+.1f}%"))
  129. changes.sort(reverse=True)
  130. if changes:
  131. print(" 两段对比(变化>=5%):")
  132. for _, text in changes[:6]:
  133. print(" " + text)
  134. if __name__ == "__main__":
  135. main()