event_shape_audit.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. #!/usr/bin/env python3
  2. """Audit event coverage and normalized whole-cycle shape changes from sampled features."""
  3. from __future__ import annotations
  4. import argparse
  5. import csv
  6. import sys
  7. from collections import defaultdict
  8. from datetime import datetime, timedelta
  9. from pathlib import Path
  10. import numpy as np
  11. BACKEND = Path(__file__).resolve().parents[1]
  12. sys.path.insert(0, str(BACKEND))
  13. from event_bins import EVENTS # noqa: E402
  14. def load(path: str) -> list[dict]:
  15. with open(path, encoding="utf-8-sig", newline="") as handle:
  16. rows = list(csv.DictReader(handle))
  17. for row in rows:
  18. row["time"] = datetime.fromisoformat(row["sample_time"])
  19. row["span"] = float(row.get("p_span") or 0)
  20. for key in row:
  21. if key.startswith("prof_"):
  22. row[key] = float(row[key]) if row[key] else np.nan
  23. return rows
  24. def median_profile(rows: list[dict], names: list[str]) -> np.ndarray | None:
  25. values = [[r[n] for n in names] for r in rows]
  26. if not values:
  27. return None
  28. result = np.nanmedian(np.asarray(values, dtype=float), axis=0)
  29. return result if np.all(np.isfinite(result)) else None
  30. def main() -> None:
  31. parser = argparse.ArgumentParser(description="事件样本覆盖和整条功图形状审计")
  32. parser.add_argument("--csv", nargs="+", required=True)
  33. parser.add_argument("--unit", nargs="+", required=True)
  34. parser.add_argument("--pre-days", type=int, default=30)
  35. parser.add_argument("--post-days", type=int, default=30)
  36. parser.add_argument("--out", required=True)
  37. args = parser.parse_args()
  38. by_unit = {unit: load(path) for unit, path in zip(args.unit, args.csv)}
  39. output = []
  40. for unit, date_text, kind in EVENTS:
  41. rows = [r for r in by_unit.get(unit, []) if r["span"] >= 0.3]
  42. t0 = datetime.fromisoformat(date_text)
  43. pre_start = t0 - timedelta(days=args.pre_days)
  44. post_end = t0 + timedelta(days=args.post_days)
  45. pre = [r for r in rows if pre_start <= r["time"] < t0]
  46. post = [r for r in rows if t0 <= r["time"] <= post_end]
  47. points = sorted({(r["cylinder"], r["side"]) for r in rows})
  48. profile_names = sorted(k for k in rows[0] if k.startswith("prof_")) if rows else []
  49. for cylinder, side in points:
  50. a = [r for r in pre if r["cylinder"] == cylinder and r["side"] == side]
  51. b = [r for r in post if r["cylinder"] == cylinder and r["side"] == side]
  52. pa = median_profile(a, profile_names)
  53. pb = median_profile(b, profile_names)
  54. distance = float(np.linalg.norm(pa - pb)) if pa is not None and pb is not None else None
  55. output.append({
  56. "机组": unit,
  57. "事件": date_text,
  58. "故障类型": kind,
  59. "测点": f"{cylinder}缸{side}",
  60. "修前有效周期": len(a),
  61. "修前日期数": len({r["time"].date() for r in a}),
  62. "修后有效周期": len(b),
  63. "修后日期数": len({r["time"].date() for r in b}),
  64. "形状距离": "" if distance is None else round(distance, 4),
  65. })
  66. fields = list(output[0]) if output else []
  67. out = Path(args.out)
  68. out.parent.mkdir(parents=True, exist_ok=True)
  69. with out.open("w", encoding="utf-8-sig", newline="") as handle:
  70. writer = csv.DictWriter(handle, fieldnames=fields)
  71. writer.writeheader()
  72. writer.writerows(output)
  73. print(f"事件审计 -> {out}")
  74. for event in EVENTS:
  75. unit, date_text, kind = event
  76. selected = [r for r in output if r["机组"] == unit and r["事件"] == date_text]
  77. comparable = [r for r in selected if r["修前有效周期"] and r["修后有效周期"]]
  78. print(f"{unit}号 {date_text} {kind}: 可比较测点 {len(comparable)}/{len(selected)}")
  79. for row in sorted(comparable, key=lambda r: float(r["形状距离"] or 0), reverse=True)[:3]:
  80. print(f" {row['测点']}: {row['修前有效周期']}->{row['修后有效周期']} 个周期,形状距离 {row['形状距离']}")
  81. if __name__ == "__main__":
  82. main()