| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- #!/usr/bin/env python3
- """Long-term indicator-feature deviation vs the scanner's anomaly segments.
- For each cylinder/side it reduces the feature CSV to daily medians, scores them
- as robust z against the point's own baseline (median/MAD over its valid days),
- and marks days that fall inside a rule anomaly segment from cache/pks_scanner/
- evolution (pressure 阶段段 / 振动异常段). 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
- UNIT_BATCH = {"7": 30, "8": 31, "9": 32}
- TREND_FEATURES = ("p_discharge", "pressure_ratio", "n_comp", "efficiency")
- def parse_dt(value):
- return datetime.strptime(value[:19], "%Y-%m-%d %H:%M:%S")
- def load_features(path):
- rows = list(csv.DictReader(open(path, encoding="utf-8-sig")))
- for row in rows:
- row["day"] = row["sample_time"][:10]
- for name in TREND_FEATURES:
- try:
- row[name] = float(row[name])
- except (TypeError, ValueError):
- row[name] = None
- return rows
- def load_segments(evolution_dir, unit):
- """Rule anomaly segments for a unit from the two stage CSVs."""
- segments = []
- for name, kind in (("润滑油压力小时阶段段.csv", "压力"), ("振动异常段.csv", "振动")):
- path = Path(evolution_dir) / name
- if not path.is_file():
- continue
- for row in csv.DictReader(open(path, encoding="utf-8-sig")):
- if row["机组"] != f"{unit}号机":
- continue
- segments.append({
- "kind": kind, "level": row.get("压力阶段") or row.get("振动阶段"),
- "start": parse_dt(row["开始小时"]), "end": parse_dt(row["结束小时"]),
- })
- return segments
- def mark_segments(day: str, segments):
- moment = datetime.strptime(day, "%Y-%m-%d")
- return [s["kind"] + "/" + str(s["level"]) for s in segments if s["start"] <= moment <= s["end"]]
- def main():
- parser = argparse.ArgumentParser(description="功图特征长期偏离检测(对齐 evolution 异常段)")
- parser.add_argument("csv_path")
- parser.add_argument("--evolution-dir", default=str(Path(__file__).resolve().parents[2] / "cache" / "pks_scanner" / "evolution"))
- parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
- args = parser.parse_args()
- rows = load_features(args.csv_path)
- segments = load_segments(args.evolution_dir, args.unit)
- by_series = defaultdict(list)
- for row in rows:
- by_series[(row["cylinder"], row["side"])].append(row)
- print(f"文件 {args.csv_path} 行 {len(rows)} evolution段 {len(segments)}")
- for (cylinder, side), series in sorted(by_series.items()):
- days = sorted({r["day"] for r in series})
- print(f"\n== {cylinder}缸{side} ({len(days)} 天)")
- for feature in TREND_FEATURES:
- daily = {}
- for day in days:
- values = [r[feature] for r in series if r["day"] == day and r[feature] is not None]
- if values:
- daily[day] = float(np.median(values))
- if len(daily) < 5:
- continue
- values = np.array([daily[d] for d in days if d in daily], dtype=float)
- center = float(np.median(values))
- mad = float(np.median(np.abs(values - center)))
- scale = mad * 1.4826 or float(np.std(values))
- if scale <= 1e-9:
- continue
- print(f" {feature:<16} 基线 {center:.3f}±{scale:.3f}")
- for day in days:
- if day not in daily:
- continue
- z = (daily[day] - center) / scale
- flags = mark_segments(day, segments)
- if abs(z) >= 2.0 or flags:
- tag = " ".join(flags)
- print(f" {day} {daily[day]:>8.3f} z={z:+.2f} {tag}")
- if __name__ == "__main__":
- main()
|