metric_trace.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. #!/usr/bin/env python3
  2. """Trace one (point, angle bin, metric) over the whole available timeline.
  3. Prints the daily medians, splits them into data segments (day gaps), fits a
  4. linear trend per segment and overall, and optionally saves a figure. This is
  5. for checking whether a candidate feature has a slow pre-event evolution.
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import sys
  10. from collections import defaultdict
  11. from datetime import datetime
  12. from pathlib import Path
  13. import numpy as np
  14. BACKEND = Path(__file__).resolve().parents[1]
  15. sys.path.insert(0, str(BACKEND))
  16. import matplotlib
  17. matplotlib.use("Agg")
  18. import matplotlib.pyplot as plt # noqa: E402
  19. matplotlib.rcParams["font.sans-serif"] = ["Arial Unicode MS", "Heiti TC", "Hiragino Sans GB", "DejaVu Sans"]
  20. matplotlib.rcParams["axes.unicode_minus"] = False
  21. import cn_names as CN # noqa: E402
  22. from event_bins import load # noqa: E402
  23. EVENT_DATES = ["2025-08-08", "2025-10-07", "2026-05-21"]
  24. def robust_scale(values):
  25. if len(values) == 0:
  26. return 0.0
  27. values = np.asarray(values)
  28. mad = float(np.median(np.abs(values - np.median(values)))) * 1.4826
  29. return mad if mad > 1e-12 else float(np.std(values))
  30. def main() -> None:
  31. parser = argparse.ArgumentParser(description="单指标全时间轴演化轨迹")
  32. parser.add_argument("--csv", required=True)
  33. parser.add_argument("--unit", required=True)
  34. parser.add_argument("--cylinder", required=True)
  35. parser.add_argument("--side", required=True)
  36. parser.add_argument("--bin-start", type=float, required=True)
  37. parser.add_argument("--metric", required=True)
  38. parser.add_argument("--gap", type=int, default=10, help="数据段切分的天数间隔")
  39. parser.add_argument("--event", action="append", default=[], help="事件日期(画竖线)")
  40. parser.add_argument("--out", default="")
  41. args = parser.parse_args()
  42. rows = load(args.csv)
  43. sel = [r for r in rows if r["unit"] == args.unit and r["cylinder"] == args.cylinder
  44. and r["side"] == args.side and abs(r["bin_start"] - args.bin_start) < 1e-6
  45. and r[args.metric] is not None]
  46. if not sel:
  47. print("没有匹配数据")
  48. return
  49. sel.sort(key=lambda r: r["time"])
  50. by_day = defaultdict(list)
  51. for r in sel:
  52. by_day[r["time"].date()].append(r[args.metric])
  53. days = sorted(by_day)
  54. daily = {d: float(np.median(by_day[d])) for d in days}
  55. print(f"{args.unit}号机 {args.cylinder}缸{args.side} 箱{args.bin_start:g}° {args.metric}"
  56. f" 周期 {len(sel)} 日 {len(days)} {days[0]}~{days[-1]}")
  57. segments = []
  58. for d in days:
  59. if segments and (d - segments[-1][-1]).days <= args.gap:
  60. segments[-1].append(d)
  61. else:
  62. segments.append([d])
  63. all_scale = robust_scale(list(daily.values()))
  64. for seg in segments:
  65. y = np.asarray([daily[d] for d in seg])
  66. span_days = (seg[-1] - seg[0]).days
  67. line = f" 段 {seg[0]}~{seg[-1]} ({len(seg)}天有数据/{span_days}天) 中位 {np.median(y):.5g} 范围 {y.min():.5g}~{y.max():.5g}"
  68. if len(seg) >= 4 and span_days > 0:
  69. x = np.asarray([(d - seg[0]).days for d in seg], dtype=float)
  70. slope = float(np.polyfit(x, y, 1)[0])
  71. drift = slope * span_days
  72. line += f" 趋势 {drift:+.4g} ({drift / all_scale:+.1f}全局MAD)"
  73. print(line)
  74. if len(days) >= 6 and (days[-1] - days[0]).days > 0:
  75. x = np.asarray([(d - days[0]).days for d in days], dtype=float)
  76. y = np.asarray([daily[d] for d in days])
  77. # only fit inside the last contiguous-ish run toward the event
  78. slope = float(np.polyfit(x, y, 1)[0])
  79. print(f" 全程线性趋势 {slope * (days[-1] - days[0]).days:+.4g} / {all_scale:+.4g} = {(slope * (days[-1] - days[0]).days) / all_scale:+.1f} 全局MAD")
  80. print(" 逐日: " + ", ".join(f"{d}:{daily[d]:.4g}" for d in days))
  81. if args.out:
  82. fig, ax = plt.subplots(figsize=(11, 3.6))
  83. xs = [datetime.combine(d, datetime.min.time()) for d in days]
  84. ys = [daily[d] for d in days]
  85. ax.plot(xs, ys, "o-", color="crimson", ms=3, lw=1)
  86. for ev in (args.event or EVENT_DATES):
  87. ax.axvline(datetime.fromisoformat(ev), color="k", ls="--", lw=0.8)
  88. ax.text(datetime.fromisoformat(ev), max(ys), ev, fontsize=7, rotation=90, va="top")
  89. ax.set_title(f"{args.unit}号机 {args.cylinder}缸{args.side} 箱{args.bin_start:g}° {CN.metric_col(args.metric)} 日度演化")
  90. ax.grid(alpha=0.25)
  91. fig.tight_layout()
  92. Path(args.out).parent.mkdir(parents=True, exist_ok=True)
  93. fig.savefig(args.out, dpi=110)
  94. print("图:", args.out)
  95. if __name__ == "__main__":
  96. main()