event_scan.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. #!/usr/bin/env python3
  2. """Scan every repair event for a single-cylinder slow drift that leads it.
  3. For each event it looks back --days before the repair, builds the daily
  4. peer-relative series (pressure_ratio / p_discharge / efficiency within the same
  5. stage and side) and the self series, fits a linear drift, and ranks by strength.
  6. Read-only; uses feature CSVs produced by extract_from_db.py.
  7. """
  8. from __future__ import annotations
  9. import argparse
  10. import csv
  11. from collections import defaultdict
  12. from datetime import datetime, timedelta
  13. from pathlib import Path
  14. import numpy as np
  15. EVENTS = [
  16. ("7", "2025-08-08", "气阀"),
  17. ("7", "2025-10-07", "气阀"),
  18. ("7", "2026-05-21", "气阀"),
  19. ("8", "2025-02-17", "气阀"),
  20. ("8", "2025-12-06", "活塞"),
  21. ("8", "2026-03-10", "十字头"),
  22. ("8", "2026-03-29", "气阀"),
  23. ("8", "2026-04-01", "气阀"),
  24. ("9", "2026-01-18", "活塞"),
  25. ]
  26. STAGE_MEMBERS = {"一级": ("二", "四", "六"), "二级": ("三", "五"), "三级": ("一",)}
  27. CYL_STAGE = {"一": "三级", "二": "一级", "三": "二级", "四": "一级", "五": "二级", "六": "一级"}
  28. FEATURES = ("pressure_ratio", "p_discharge", "efficiency")
  29. def load(path):
  30. rows = []
  31. for row in csv.DictReader(open(path, encoding="utf-8-sig")):
  32. row["day"] = row["sample_time"][:10]
  33. for name in FEATURES:
  34. try:
  35. row[name] = float(row[name])
  36. except (TypeError, ValueError):
  37. row[name] = None
  38. rows.append(row)
  39. return rows
  40. def daily_median(rows, cylinder, side, feature):
  41. groups = defaultdict(list)
  42. for row in rows:
  43. if row["cylinder"] == cylinder and row["side"] == side and row[feature] is not None:
  44. groups[row["day"]].append(row[feature])
  45. return {day: float(np.median(v)) for day, v in groups.items()}
  46. def peer_relative(rows, feature):
  47. by_key = defaultdict(dict)
  48. for row in rows:
  49. if row[feature] is not None:
  50. by_key[(row["day"], row["side"])][row["cylinder"]] = row[feature]
  51. series = defaultdict(dict)
  52. for (day, side), members in by_key.items():
  53. clean = {c: v for c, v in members.items() if v is not None}
  54. if len(clean) < 2:
  55. continue
  56. med = float(np.median(list(clean.values())))
  57. if abs(med) < 1e-9:
  58. continue
  59. for cylinder, value in clean.items():
  60. series[(cylinder, side)][day] = (value - med) / med
  61. return series
  62. def fit(daily):
  63. days = sorted(daily)
  64. if len(days) < 10:
  65. return None
  66. dates = [datetime.strptime(d, "%Y-%m-%d") for d in days]
  67. span = (dates[-1] - dates[0]).days
  68. if span <= 0 or span / (len(dates) - 1) > 15:
  69. return None
  70. xd = np.array([(d - dates[0]).days for d in dates], dtype=float)
  71. y = np.array([daily[d] for d in days], dtype=float)
  72. if xd[-1] <= 0:
  73. return None
  74. slope, intercept = np.polyfit(xd, y, 1)
  75. pred = slope * xd + intercept
  76. ss_res = float(np.sum((y - pred) ** 2))
  77. ss_tot = float(np.sum((y - y.mean()) ** 2))
  78. r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0
  79. scale = float(np.median(np.abs(y - np.median(y)))) * 1.4826 or float(np.std(y))
  80. if scale <= 1e-9:
  81. return None
  82. total_mads = abs(slope) * xd[-1] / scale # total drift over the window in MADs
  83. return total_mads, r2, len(days), float(y[0]), float(y[-1])
  84. def main():
  85. parser = argparse.ArgumentParser(description="扫描所有维修事件的单缸慢漂移")
  86. parser.add_argument("--data-dir", default=str(Path(__file__).resolve().parents[1] / "cache" / "indicator"))
  87. parser.add_argument("--days", type=int, default=180, help="维修前回看天数")
  88. args = parser.parse_args()
  89. cache = {}
  90. for unit in ("7", "8", "9"):
  91. path = Path(args.data_dir) / f"all_{unit}_2025_2026.csv"
  92. cache[unit] = load(path) if path.is_file() else []
  93. for unit, date, kind in EVENTS:
  94. event_day = datetime.strptime(date, "%Y-%m-%d")
  95. start = (event_day - timedelta(days=args.days)).strftime("%Y-%m-%d")
  96. rows = [r for r in cache[unit] if start <= r["day"] <= date]
  97. days = sorted({r["day"] for r in rows})
  98. print(f"\n== {unit}号机 {date} {kind} 回看{args.days}天 数据日 {len(days)} ({days[0] if days else '-'}~{days[-1] if days else '-'})")
  99. if len(days) < 8:
  100. print(" 数据不足,跳过")
  101. continue
  102. ranked = []
  103. for feature in FEATURES:
  104. for (cylinder, side), daily in peer_relative(rows, feature).items():
  105. result = fit(daily)
  106. if result and result[0] >= 2.0 and result[1] >= 0.4:
  107. ranked.append((result[0], f"peer {cylinder}缸{side} {feature}", result))
  108. for cylinder in ("一", "二", "三", "四", "五", "六"):
  109. for side in ("盖侧", "轴侧"):
  110. result = fit(daily_median(rows, cylinder, side, feature))
  111. if result and result[0] >= 3.0 and result[1] >= 0.5:
  112. ranked.append((result[0], f"self {cylinder}缸{side} {feature}", result))
  113. ranked.sort(reverse=True)
  114. if not ranked:
  115. print(" 无显著慢漂移")
  116. for _, label, (mads, r2, n, first, last) in ranked[:5]:
  117. print(f" {label:<34} 漂移{mads:4.1f}MAD R2={r2:.2f} (n={n}, {first:.3f}->{last:.3f})")
  118. if __name__ == "__main__":
  119. main()