trend.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. #!/usr/bin/env python3
  2. """Long-term indicator-feature deviation vs the scanner's anomaly segments.
  3. For each cylinder/side it reduces the feature CSV to daily medians, scores them
  4. as robust z against the point's own baseline (median/MAD over its valid days),
  5. and marks days that fall inside a rule anomaly segment from cache/pks_scanner/
  6. evolution (pressure 阶段段 / 振动异常段). Read-only.
  7. """
  8. from __future__ import annotations
  9. import argparse
  10. import csv
  11. from collections import defaultdict
  12. from datetime import datetime
  13. from pathlib import Path
  14. import numpy as np
  15. UNIT_BATCH = {"7": 30, "8": 31, "9": 32}
  16. TREND_FEATURES = ("p_discharge", "pressure_ratio", "n_comp", "efficiency")
  17. def parse_dt(value):
  18. return datetime.strptime(value[:19], "%Y-%m-%d %H:%M:%S")
  19. def load_features(path):
  20. rows = list(csv.DictReader(open(path, encoding="utf-8-sig")))
  21. for row in rows:
  22. row["day"] = row["sample_time"][:10]
  23. for name in TREND_FEATURES:
  24. try:
  25. row[name] = float(row[name])
  26. except (TypeError, ValueError):
  27. row[name] = None
  28. return rows
  29. def load_segments(evolution_dir, unit):
  30. """Rule anomaly segments for a unit from the two stage CSVs."""
  31. segments = []
  32. for name, kind in (("润滑油压力小时阶段段.csv", "压力"), ("振动异常段.csv", "振动")):
  33. path = Path(evolution_dir) / name
  34. if not path.is_file():
  35. continue
  36. for row in csv.DictReader(open(path, encoding="utf-8-sig")):
  37. if row["机组"] != f"{unit}号机":
  38. continue
  39. segments.append({
  40. "kind": kind, "level": row.get("压力阶段") or row.get("振动阶段"),
  41. "start": parse_dt(row["开始小时"]), "end": parse_dt(row["结束小时"]),
  42. })
  43. return segments
  44. def mark_segments(day: str, segments):
  45. moment = datetime.strptime(day, "%Y-%m-%d")
  46. return [s["kind"] + "/" + str(s["level"]) for s in segments if s["start"] <= moment <= s["end"]]
  47. def main():
  48. parser = argparse.ArgumentParser(description="功图特征长期偏离检测(对齐 evolution 异常段)")
  49. parser.add_argument("csv_path")
  50. parser.add_argument("--evolution-dir", default=str(Path(__file__).resolve().parents[2] / "cache" / "pks_scanner" / "evolution"))
  51. parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
  52. args = parser.parse_args()
  53. rows = load_features(args.csv_path)
  54. segments = load_segments(args.evolution_dir, args.unit)
  55. by_series = defaultdict(list)
  56. for row in rows:
  57. by_series[(row["cylinder"], row["side"])].append(row)
  58. print(f"文件 {args.csv_path} 行 {len(rows)} evolution段 {len(segments)}")
  59. for (cylinder, side), series in sorted(by_series.items()):
  60. days = sorted({r["day"] for r in series})
  61. print(f"\n== {cylinder}缸{side} ({len(days)} 天)")
  62. for feature in TREND_FEATURES:
  63. daily = {}
  64. for day in days:
  65. values = [r[feature] for r in series if r["day"] == day and r[feature] is not None]
  66. if values:
  67. daily[day] = float(np.median(values))
  68. if len(daily) < 5:
  69. continue
  70. values = np.array([daily[d] for d in days if d in daily], dtype=float)
  71. center = float(np.median(values))
  72. mad = float(np.median(np.abs(values - center)))
  73. scale = mad * 1.4826 or float(np.std(values))
  74. if scale <= 1e-9:
  75. continue
  76. print(f" {feature:<16} 基线 {center:.3f}±{scale:.3f}")
  77. for day in days:
  78. if day not in daily:
  79. continue
  80. z = (daily[day] - center) / scale
  81. flags = mark_segments(day, segments)
  82. if abs(z) >= 2.0 or flags:
  83. tag = " ".join(flags)
  84. print(f" {day} {daily[day]:>8.3f} z={z:+.2f} {tag}")
  85. if __name__ == "__main__":
  86. main()