analyze.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/env python3
  2. """Compare indicator features with three normalizations (read-only).
  3. * peer : within the same stage and side at the same timestamp;
  4. * chamber : cover side vs shaft side of the same cylinder;
  5. * self : the cylinder own trailing baseline (median/MAD) -> z-score.
  6. Absolute levels depend on the process, so only relative quantities are scored.
  7. """
  8. from __future__ import annotations
  9. import argparse
  10. import csv
  11. from collections import defaultdict
  12. import numpy as np
  13. DEAD_DISCHARGE = 0.20
  14. PEER_FEATURES = ("p_discharge", "pressure_ratio", "n_comp", "discharge_frac", "efficiency", "indicated_work")
  15. PROFILE = tuple(f"prof_{i:02d}" for i in range(12))
  16. def to_float(value):
  17. try:
  18. result = float(value)
  19. except (TypeError, ValueError):
  20. return None
  21. return result if np.isfinite(result) else None
  22. def load(path: str) -> list[dict]:
  23. rows = list(csv.DictReader(open(path, encoding="utf-8-sig")))
  24. for row in rows:
  25. for key in PEER_FEATURES + PROFILE + ("p_suction", "p_span"):
  26. row[key] = to_float(row.get(key))
  27. return rows
  28. def healthy(row: dict, exclude: set | None = None) -> bool:
  29. if exclude and (row["cylinder"], row["side"]) in exclude:
  30. return False
  31. pd = row.get("p_discharge")
  32. ps = row.get("p_suction")
  33. span = row.get("p_span")
  34. return pd is not None and pd >= DEAD_DISCHARGE and ps and ps > 0.05 and span and span >= 0.3
  35. def series_medians(rows: list[dict], feature: str) -> dict:
  36. by_series = defaultdict(list)
  37. for row in rows:
  38. value = row.get(feature)
  39. if value is not None:
  40. by_series[(row["cylinder"], row["side"])].append(value)
  41. return {key: float(np.median(values)) for key, values in by_series.items() if values}
  42. def flag_channels(rows: list[dict]) -> dict:
  43. """Flag weak/dead channels relative to the stage-wide expected discharge level."""
  44. medians = series_medians(rows, "p_discharge")
  45. reference = np.median([v for v in medians.values() if v > DEAD_DISCHARGE]) if medians else 0.0
  46. flags = {}
  47. for key, value in medians.items():
  48. if value < DEAD_DISCHARGE:
  49. flags[key] = "dead"
  50. elif reference > 0 and value < 0.6 * reference:
  51. flags[key] = "weak"
  52. elif reference > 0 and value > 1.5 * reference:
  53. flags[key] = "high"
  54. return flags
  55. def peer_deviations(rows: list[dict], exclude: set) -> dict:
  56. by_key = defaultdict(dict)
  57. for row in rows:
  58. if healthy(row, exclude):
  59. by_key[(row["stage"], row["side"], row["sample_time"])][row["cylinder"]] = row
  60. result = defaultdict(list)
  61. for (stage, side, _time), members in by_key.items():
  62. if len(members) < 2:
  63. continue
  64. for feature in PEER_FEATURES:
  65. values = [m[feature] for m in members.values() if m[feature] is not None]
  66. if len(values) < 2:
  67. continue
  68. med = float(np.median(values))
  69. if abs(med) < 1e-9:
  70. continue
  71. for cylinder, member in members.items():
  72. if member[feature] is not None:
  73. result[(cylinder, side, feature)].append((member[feature] - med) / med)
  74. return {key: float(np.mean(values)) for key, values in result.items()}
  75. def chamber_asymmetry(rows: list[dict], exclude: set) -> dict:
  76. by_key = defaultdict(dict)
  77. for row in rows:
  78. by_key[(row["cylinder"], row["sample_time"])][row["side"]] = row
  79. result = defaultdict(list)
  80. for (cylinder, _time), pair in by_key.items():
  81. if "盖侧" not in pair or "轴侧" not in pair:
  82. continue
  83. cover, shaft = pair["盖侧"], pair["轴侧"]
  84. if not (healthy(cover, exclude) and healthy(shaft, exclude)):
  85. continue
  86. for feature in ("p_discharge", "pressure_ratio", "n_comp"):
  87. if cover[feature] is not None and shaft[feature] is not None and abs(shaft[feature]) > 1e-9:
  88. result[(cylinder, feature)].append((cover[feature] - shaft[feature]) / abs(shaft[feature]))
  89. profile_cover = np.array([cover[name] for name in PROFILE], dtype=float)
  90. profile_shaft = np.array([shaft[name] for name in PROFILE], dtype=float)
  91. if np.all(np.isfinite(profile_cover)) and np.all(np.isfinite(profile_shaft)):
  92. result[(cylinder, "profile_l2")].append(float(np.linalg.norm(profile_cover - profile_shaft)))
  93. return {key: float(np.mean(values)) for key, values in result.items()}
  94. def self_baseline(rows: list[dict], exclude: set) -> dict:
  95. by_series = defaultdict(list)
  96. for row in rows:
  97. if healthy(row, exclude):
  98. by_series[(row["cylinder"], row["side"])].append(row)
  99. result = {}
  100. for (cylinder, side), series in by_series.items():
  101. series.sort(key=lambda r: r["sample_time"])
  102. times = [r["sample_time"] for r in series]
  103. for feature in ("p_discharge", "pressure_ratio", "n_comp"):
  104. values = np.array([r[feature] if r[feature] is not None else np.nan for r in series], dtype=float)
  105. valid = values[np.isfinite(values)]
  106. if valid.size < 5:
  107. continue
  108. center = float(np.median(valid))
  109. mad = float(np.median(np.abs(valid - center)))
  110. scale = mad * 1.4826 or float(np.std(valid))
  111. if scale <= 1e-9:
  112. continue
  113. z = (values - center) / scale
  114. result[(cylinder, side, feature)] = {
  115. "center": center, "scale": scale,
  116. "first_half_z": float(np.nanmean(z[: len(z) // 2])),
  117. "second_half_z": float(np.nanmean(z[len(z) // 2:])),
  118. "max_abs_z": float(np.nanmax(np.abs(z))) if np.isfinite(z).any() else None,
  119. "last_time": times[-1],
  120. }
  121. return result
  122. def main() -> None:
  123. parser = argparse.ArgumentParser(description="功图特征三种归一化对照")
  124. parser.add_argument("csv_path")
  125. args = parser.parse_args()
  126. rows = load(args.csv_path)
  127. flags = flag_channels(rows)
  128. exclude = set(flags)
  129. units = sorted({(r["cylinder"], r["side"]) for r in rows})
  130. print(f"总行 {len(rows)};测点 {len(units)}")
  131. print(f"通道标记(排除对照):{flags if flags else '无'}")
  132. print("\n=== 同级缸间偏差(同侧、同刻,相对中位数)===")
  133. for (cylinder, side, feature), value in sorted(peer_deviations(rows, exclude).items()):
  134. print(f" {cylinder}缸{side:<3} {feature:<16} {value * 100:+.2f}%")
  135. print("\n=== 同缸双腔差(盖 - 轴)===")
  136. for (cylinder, feature), value in sorted(chamber_asymmetry(rows, exclude).items()):
  137. if feature == "profile_l2":
  138. print(f" {cylinder}缸 profile L2 {value:.3f}")
  139. else:
  140. print(f" {cylinder}缸 {feature:<16} {value * 100:+.2f}%")
  141. print("\n=== 本缸自身基线 z(前段 vs 后段,沿时间趋势)===")
  142. for (cylinder, side, feature), stat in sorted(self_baseline(rows, exclude).items()):
  143. print(f" {cylinder}缸{side:<3} {feature:<16} 前段z={stat['first_half_z']:+.2f} 后段z={stat['second_half_z']:+.2f} max|z|={stat['max_abs_z']:.2f}")
  144. if __name__ == "__main__":
  145. main()