bin_plot.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. #!/usr/bin/env python3
  2. """Plot the per-angular-bin statistics before/after a chosen event (read-only).
  3. Uses the same running-segment selection as ``event_bins.py`` (rpm>0 segments, at
  4. least ``--pre-days`` / ``--post-days`` running days). Two figures per event:
  5. * profile: for every measurement point, the pre-event median and post-event
  6. median of a chosen metric across the bins (whole profile shift vs shape);
  7. * series : for every point, the time trace of the bin whose local (shape) change
  8. is the largest for a chosen metric, pre and post coloured differently.
  9. """
  10. from __future__ import annotations
  11. import argparse
  12. import sys
  13. from collections import defaultdict
  14. from datetime import datetime
  15. from pathlib import Path
  16. import numpy as np
  17. import matplotlib
  18. matplotlib.use("Agg")
  19. import matplotlib.pyplot as plt # noqa: E402
  20. matplotlib.rcParams["font.sans-serif"] = ["Arial Unicode MS", "Heiti TC", "Hiragino Sans GB", "DejaVu Sans"]
  21. matplotlib.rcParams["axes.unicode_minus"] = False
  22. BACKEND = Path(__file__).resolve().parents[1]
  23. sys.path.insert(0, str(BACKEND))
  24. from event_bins import load as load_rows, collect_before, collect_after, robust_scale # noqa: E402
  25. from run_segments import unit_segments # noqa: E402
  26. CYLINDERS = ("一", "二", "三", "四", "五", "六")
  27. SIDES = ("盖侧", "轴侧")
  28. VOLATILE = ("std", "mad", "range", "roughness")
  29. def median_at(rows, metric, bin_index):
  30. values = [r[metric] for r in rows if r["bin"] == bin_index and r[metric] is not None]
  31. return float(np.median(values)) if values else None
  32. def main() -> None:
  33. parser = argparse.ArgumentParser(description="每角度箱 前后剖面 / 时间序列图(运行段口径)")
  34. parser.add_argument("--csv", required=True, help="angle_bins.py 长表 CSV")
  35. parser.add_argument("--unit", required=True, choices=("7", "8", "9"))
  36. parser.add_argument("--date", required=True, help="事件日期 YYYY-MM-DD")
  37. parser.add_argument("--pre-days", type=int, default=15)
  38. parser.add_argument("--post-days", type=int, default=15)
  39. parser.add_argument("--merge-gap", type=int, default=2)
  40. parser.add_argument("--max-gap", type=int, default=30)
  41. parser.add_argument("--profile-metric", default="mean")
  42. parser.add_argument("--series-metric", default="std")
  43. parser.add_argument("--outdir", default="cache/indicator/bins/plots")
  44. args = parser.parse_args()
  45. rows = load_rows(args.csv)
  46. t0 = datetime.fromisoformat(args.date)
  47. segments = unit_segments(args.unit, args.merge_gap)
  48. bins = sorted({r["bin"] for r in rows})
  49. bin_start = {r["bin"]: r["bin_start"] for r in rows}
  50. bin_deg = round(bin_start[bins[1]] - bin_start[bins[0]], 6) if len(bins) > 1 else 10.0
  51. points = [(c, s) for c in CYLINDERS for s in SIDES if any(r["cylinder"] == c and r["side"] == s for r in rows)]
  52. outdir = Path(args.outdir)
  53. outdir.mkdir(parents=True, exist_ok=True)
  54. # pre/post rows per point via running segments
  55. selected = {}
  56. for cylinder, side in points:
  57. point_rows = [r for r in rows if r["cylinder"] == cylinder and r["side"] == side]
  58. pre, _ = collect_before(point_rows, t0, segments, args.pre_days)
  59. post, _ = collect_after(point_rows, t0, segments, args.post_days, args.max_gap)
  60. selected[(cylinder, side)] = (pre, post)
  61. cols = 3
  62. rows_n = (len(points) + cols - 1) // cols
  63. # ---- profile figure ----
  64. fig, axes = plt.subplots(rows_n, cols, figsize=(4.2 * cols, 2.6 * rows_n), squeeze=False)
  65. for ax, key in zip(axes.ravel(), points):
  66. pre, post = selected[key]
  67. xs = [bin_start[b] + bin_deg / 2 for b in bins]
  68. prof_pre = [median_at(pre, args.profile_metric, b) for b in bins]
  69. prof_post = [median_at(post, args.profile_metric, b) for b in bins]
  70. ax.plot(xs, prof_pre, color="crimson", lw=1.4, label="pre")
  71. ax.plot(xs, prof_post, color="royalblue", lw=1.4, label="post")
  72. ax.set_title(f"{key[0]}缸{key[1]}", fontsize=10)
  73. ax.set_xlim(0, 360)
  74. ax.set_xticks([0, 90, 180, 270, 360])
  75. ax.tick_params(labelsize=8)
  76. ax.grid(alpha=0.25)
  77. for ax in axes.ravel()[len(points):]:
  78. ax.axis("off")
  79. axes.ravel()[0].legend(fontsize=7)
  80. fig.suptitle(f"{args.unit}号机 {args.date} 每角度箱 {args.profile_metric}(pre红 post蓝)")
  81. fig.tight_layout()
  82. p1 = outdir / f"{args.unit}_{args.date}_profile_{args.profile_metric}.png"
  83. fig.savefig(p1, dpi=110)
  84. plt.close(fig)
  85. print("图:", p1)
  86. # ---- series figure: strongest local shape change per point ----
  87. fig, axes = plt.subplots(rows_n, cols, figsize=(4.2 * cols, 2.6 * rows_n), squeeze=False)
  88. for ax, key in zip(axes.ravel(), points):
  89. pre, post = selected[key]
  90. best_bin, best_score = None, 0.0
  91. for b in bins:
  92. a = [r[args.series_metric] for r in pre if r["bin"] == b and r[args.series_metric] is not None]
  93. c = [r[args.series_metric] for r in post if r["bin"] == b and r[args.series_metric] is not None]
  94. if len(a) < 3 or len(c) < 3:
  95. continue
  96. scale = robust_scale(np.asarray(a))
  97. if scale <= 1e-12:
  98. continue
  99. score = abs(float(np.median(c)) - float(np.median(a))) / scale
  100. if score > best_score:
  101. best_bin, best_score = b, score
  102. if best_bin is None:
  103. ax.set_title(f"{key[0]}缸{key[1]} 无数据", fontsize=9)
  104. ax.axis("off")
  105. continue
  106. trace = sorted([r for r in rows if r["cylinder"] == key[0] and r["side"] == key[1]
  107. and r["bin"] == best_bin and r[args.series_metric] is not None], key=lambda r: r["time"])
  108. for r in trace:
  109. color = "royalblue" if r["time"] >= t0 else "crimson"
  110. ax.plot(r["time"], r[args.series_metric], ".", color=color, markersize=3)
  111. ax.axvline(t0, color="k", lw=0.8, ls="--")
  112. start = bin_start[best_bin]
  113. ax.set_title(f"{key[0]}缸{key[1]} 箱{start:g}-{start + bin_deg:g}° ({best_score:.1f}MAD)", fontsize=9)
  114. ax.tick_params(labelsize=7, rotation=30)
  115. ax.grid(alpha=0.25)
  116. for ax in axes.ravel()[len(points):]:
  117. ax.axis("off")
  118. fig.suptitle(f"{args.unit}号机 {args.date} 各点局部变化最大的箱 {args.series_metric} 时间序列")
  119. fig.tight_layout()
  120. p2 = outdir / f"{args.unit}_{args.date}_series_{args.series_metric}.png"
  121. fig.savefig(p2, dpi=110)
  122. plt.close(fig)
  123. print("图:", p2)
  124. if __name__ == "__main__":
  125. main()