analyze.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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 *same stage* expected discharge level."""
  44. medians = series_medians(rows, "p_discharge")
  45. stage_of = {}
  46. for row in rows:
  47. stage_of[(row["cylinder"], row["side"])] = row["stage"]
  48. by_stage = defaultdict(list)
  49. for key, value in medians.items():
  50. by_stage[stage_of.get(key, "?")].append(value)
  51. reference = {stage: float(np.median([v for v in values if v > DEAD_DISCHARGE]))
  52. for stage, values in by_stage.items()
  53. if any(v > DEAD_DISCHARGE for v in values)}
  54. flags = {}
  55. for key, value in medians.items():
  56. ref = reference.get(stage_of.get(key, "?"), 0.0)
  57. if value < DEAD_DISCHARGE:
  58. flags[key] = "dead"
  59. elif ref > 0 and value < 0.6 * ref:
  60. flags[key] = "weak"
  61. elif ref > 0 and value > 1.5 * ref:
  62. flags[key] = "high"
  63. return flags
  64. def peer_deviations(rows: list[dict], exclude: set) -> dict:
  65. by_key = defaultdict(dict)
  66. for row in rows:
  67. if healthy(row, exclude):
  68. by_key[(row["stage"], row["side"], row["sample_time"])][row["cylinder"]] = row
  69. result = defaultdict(list)
  70. for (stage, side, _time), members in by_key.items():
  71. if len(members) < 2:
  72. continue
  73. for feature in PEER_FEATURES:
  74. values = [m[feature] for m in members.values() if m[feature] is not None]
  75. if len(values) < 2:
  76. continue
  77. med = float(np.median(values))
  78. if abs(med) < 1e-9:
  79. continue
  80. for cylinder, member in members.items():
  81. if member[feature] is not None:
  82. result[(cylinder, side, feature)].append((member[feature] - med) / med)
  83. return {key: float(np.mean(values)) for key, values in result.items()}
  84. def chamber_asymmetry(rows: list[dict], exclude: set) -> dict:
  85. by_key = defaultdict(dict)
  86. for row in rows:
  87. by_key[(row["cylinder"], row["sample_time"])][row["side"]] = row
  88. result = defaultdict(list)
  89. for (cylinder, _time), pair in by_key.items():
  90. if "盖侧" not in pair or "轴侧" not in pair:
  91. continue
  92. cover, shaft = pair["盖侧"], pair["轴侧"]
  93. if not (healthy(cover, exclude) and healthy(shaft, exclude)):
  94. continue
  95. for feature in ("p_discharge", "pressure_ratio", "n_comp"):
  96. if cover[feature] is not None and shaft[feature] is not None and abs(shaft[feature]) > 1e-9:
  97. result[(cylinder, feature)].append((cover[feature] - shaft[feature]) / abs(shaft[feature]))
  98. profile_cover = np.array([cover[name] for name in PROFILE], dtype=float)
  99. profile_shaft = np.array([shaft[name] for name in PROFILE], dtype=float)
  100. if np.all(np.isfinite(profile_cover)) and np.all(np.isfinite(profile_shaft)):
  101. result[(cylinder, "profile_l2")].append(float(np.linalg.norm(profile_cover - profile_shaft)))
  102. return {key: float(np.mean(values)) for key, values in result.items()}
  103. def self_baseline(rows: list[dict], exclude: set) -> dict:
  104. by_series = defaultdict(list)
  105. for row in rows:
  106. if healthy(row, exclude):
  107. by_series[(row["cylinder"], row["side"])].append(row)
  108. result = {}
  109. for (cylinder, side), series in by_series.items():
  110. series.sort(key=lambda r: r["sample_time"])
  111. times = [r["sample_time"] for r in series]
  112. for feature in ("p_discharge", "pressure_ratio", "n_comp"):
  113. values = np.array([r[feature] if r[feature] is not None else np.nan for r in series], dtype=float)
  114. valid = values[np.isfinite(values)]
  115. if valid.size < 5:
  116. continue
  117. center = float(np.median(valid))
  118. mad = float(np.median(np.abs(valid - center)))
  119. scale = mad * 1.4826 or float(np.std(valid))
  120. if scale <= 1e-9:
  121. continue
  122. z = (values - center) / scale
  123. result[(cylinder, side, feature)] = {
  124. "center": center, "scale": scale,
  125. "first_half_z": float(np.nanmean(z[: len(z) // 2])),
  126. "second_half_z": float(np.nanmean(z[len(z) // 2:])),
  127. "max_abs_z": float(np.nanmax(np.abs(z))) if np.isfinite(z).any() else None,
  128. "last_time": times[-1],
  129. }
  130. return result
  131. def main() -> None:
  132. parser = argparse.ArgumentParser(description="功图特征三种归一化对照")
  133. parser.add_argument("csv_path")
  134. args = parser.parse_args()
  135. rows = load(args.csv_path)
  136. flags = flag_channels(rows)
  137. exclude = set(flags)
  138. units = sorted({(r["cylinder"], r["side"]) for r in rows})
  139. print(f"总行 {len(rows)};测点 {len(units)}")
  140. print(f"通道标记(排除对照):{flags if flags else '无'}")
  141. print("\n=== 同级缸间偏差(同侧、同刻,相对中位数)===")
  142. for (cylinder, side, feature), value in sorted(peer_deviations(rows, exclude).items()):
  143. print(f" {cylinder}缸{side:<3} {feature:<16} {value * 100:+.2f}%")
  144. print("\n=== 同缸双腔差(盖 - 轴)===")
  145. for (cylinder, feature), value in sorted(chamber_asymmetry(rows, exclude).items()):
  146. if feature == "profile_l2":
  147. print(f" {cylinder}缸 profile L2 {value:.3f}")
  148. else:
  149. print(f" {cylinder}缸 {feature:<16} {value * 100:+.2f}%")
  150. print("\n=== 本缸自身基线 z(前段 vs 后段,沿时间趋势)===")
  151. for (cylinder, side, feature), stat in sorted(self_baseline(rows, exclude).items()):
  152. 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}")
  153. if __name__ == "__main__":
  154. main()