unified_labels.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. """把振动小时标签映射为与压力/温度可共用的统一字段。"""
  2. from __future__ import annotations
  3. from pathlib import Path
  4. import numpy as np
  5. import pandas as pd
  6. from . import config
  7. CORE_FIELDS = [
  8. "标签ID", "标签类型", "机组", "批次", "周期编号", "阶段",
  9. "阶段部位", "阶段点位", "开始小时", "结束小时",
  10. "持续自然小时数", "有效运行小时数",
  11. "基线值", "正常波动带", "阶段开始值", "阶段结束值", "阶段最大值",
  12. "相对基线变化", "阶段最大连续异常有效小时数",
  13. "阶段高报警小时数", "阶段高高报警小时数",
  14. "方向", "特征类型", "置信度", "是否形成确认等级",
  15. "阶段触发依据", "事件关联", "启动过渡比例", "局部基线可用比例",
  16. "数据来源", "算法版本",
  17. ]
  18. VIBRATION_FIELDS = CORE_FIELDS + [
  19. "近期基线RMS", "RMS正常波动带", "阶段开始RMS", "阶段结束RMS",
  20. "阶段最大RMS", "阶段最大峭度", "阶段最大峭度/基线",
  21. "阶段最大残差峭度z", "触发特征", "多特征确认",
  22. ]
  23. def _event_hits(cylinder: str, start: pd.Timestamp, end: pd.Timestamp) -> str:
  24. hits = []
  25. for event in config.EVENTS:
  26. if event["cylinder"] != cylinder:
  27. continue
  28. center = pd.Timestamp(event["date"])
  29. if start <= center + pd.Timedelta(days=1) and end >= center - pd.Timedelta(days=7):
  30. hits.append(event["event_id"])
  31. return " / ".join(hits)
  32. def _max_consecutive_hours(frame: pd.DataFrame) -> int:
  33. if frame.empty:
  34. return 0
  35. hours = frame["sample_time"].dt.floor("h").drop_duplicates().sort_values()
  36. if hours.empty:
  37. return 0
  38. gaps = hours.diff().dt.total_seconds().fillna(3600)
  39. best = current = 1
  40. for gap in gaps.iloc[1:]:
  41. if gap <= 3600:
  42. current += 1
  43. best = max(best, current)
  44. else:
  45. current = 1
  46. return int(best)
  47. def build_vibration_unified(hourly: pd.DataFrame, frame: pd.DataFrame, review: pd.DataFrame) -> pd.DataFrame:
  48. frame = frame.copy()
  49. frame["sample_time"] = pd.to_datetime(frame["sample_time"])
  50. review_index = review.set_index(["cylinder", "start_hour"])
  51. rows = []
  52. for index, item in enumerate(hourly.itertuples(index=False), start=1):
  53. part = frame[
  54. (frame["cylinder"] == item.cylinder)
  55. & (frame["sample_time"] >= item.start_hour)
  56. & (frame["sample_time"] < item.end_hour)
  57. ].sort_values("sample_time")
  58. if part.empty:
  59. continue
  60. peak = part.loc[part["score"].idxmax()]
  61. base_log = float(part["base_rms_log"].median())
  62. scale_log = float(part["scale_rms_log"].median())
  63. base_rms = float(10 ** base_log) if np.isfinite(base_log) else float(part["rms"].median())
  64. low = float(10 ** (base_log - 0.6745 * scale_log)) if np.isfinite(scale_log) else base_rms * 0.9
  65. high = float(10 ** (base_log + 0.6745 * scale_log)) if np.isfinite(scale_log) else base_rms * 1.1
  66. rms_max = float(part["rms"].max())
  67. kurt_max = float(part["kurt"].max())
  68. kurt_ratio_max = float(part["kurt_ratio"].max())
  69. z_kurt = float(part["z_resid_kurt"].abs().max())
  70. review_row = review_index.loc[(item.cylinder, item.start_hour)].to_dict() if (item.cylinder, item.start_hour) in review_index.index else {}
  71. transition_ratio = review_row.get("transition_ratio", item.transition_ratio)
  72. baseline_ok_ratio = review_row.get("baseline_ok_ratio", 1.0)
  73. confirmed = bool(item.confirmed)
  74. event_hits = _event_hits(item.cylinder, item.start_hour, item.end_hour)
  75. relative = (rms_max / base_rms - 1.0) if base_rms else 0.0
  76. trigger = (
  77. f"5号机{item.cylinder} {item.start_hour:%Y-%m-%d %H:%M}~{item.end_hour:%Y-%m-%d %H:%M}:"
  78. f"{item.direction};RMS峰值{rms_max:.2f}(近期基线{base_rms:.2f},{relative:+.1%}),"
  79. f"实际峭度最大{kurt_max:.2f}(局部基线{float(part['base_kurt'].median()):.2f},"
  80. f"{kurt_ratio_max:.2f}倍),残差峭度z={z_kurt:.2f},持续{item.hour_count}小时;"
  81. f"多特征确认={'是' if confirmed else '否'}"
  82. + (f";事件关联={event_hits}" if event_hits else "")
  83. )
  84. high_hours = item.hour_count if item.level in ("异常", "严重异常") else 0
  85. high_high_hours = item.hour_count if item.level == "严重异常" else 0
  86. rows.append(
  87. {
  88. "标签ID": f"5号机-VIB-{index:04d}",
  89. "标签类型": "振动",
  90. "机组": "5号机",
  91. "批次": "全量",
  92. "周期编号": f"5号机-VIB-{index:04d}",
  93. "阶段": item.level,
  94. "阶段部位": f"5号机{item.cylinder}",
  95. "阶段点位": str(peak["point_name"]),
  96. "开始小时": item.start_hour,
  97. "结束小时": item.end_hour,
  98. "持续自然小时数": item.hour_count,
  99. "有效运行小时数": int(part["sample_time"].dt.floor("h").nunique()),
  100. "基线值": round(base_rms, 4),
  101. "正常波动带": f"{low:.4f}~{high:.4f}",
  102. "阶段开始值": round(float(part["rms"].iloc[0]), 4),
  103. "阶段结束值": round(float(part["rms"].iloc[-1]), 4),
  104. "阶段最大值": round(rms_max, 4),
  105. "相对基线变化": round(relative, 4),
  106. "阶段最大连续异常有效小时数": _max_consecutive_hours(part[part["hit"]]),
  107. "阶段高报警小时数": high_hours,
  108. "阶段高高报警小时数": high_high_hours,
  109. "方向": item.direction,
  110. "特征类型": "振动",
  111. "置信度": "高" if item.level == "严重异常" else ("中" if item.level == "异常" else "低"),
  112. "是否形成确认等级": "是" if confirmed else "否",
  113. "阶段触发依据": trigger,
  114. "事件关联": event_hits,
  115. "启动过渡比例": round(float(transition_ratio or 0), 2),
  116. "局部基线可用比例": round(float(baseline_ok_ratio or 1), 2),
  117. "数据来源": "wave_file 首周期波形",
  118. "算法版本": "cylinder_vibration_v1",
  119. "近期基线RMS": round(base_rms, 4),
  120. "RMS正常波动带": f"{low:.4f}~{high:.4f}",
  121. "阶段开始RMS": round(float(part["rms"].iloc[0]), 4),
  122. "阶段结束RMS": round(float(part["rms"].iloc[-1]), 4),
  123. "阶段最大RMS": round(rms_max, 4),
  124. "阶段最大峭度": round(kurt_max, 3),
  125. "阶段最大峭度/基线": round(kurt_ratio_max, 3),
  126. "阶段最大残差峭度z": round(z_kurt, 3),
  127. "触发特征": str(peak["driver"]),
  128. "多特征确认": "是" if confirmed else "否",
  129. }
  130. )
  131. return pd.DataFrame(rows, columns=VIBRATION_FIELDS)
  132. def _map_external(path: Path, label_type: str, prefix: str, unit: str | None = None) -> pd.DataFrame:
  133. if not path.exists():
  134. return pd.DataFrame(columns=CORE_FIELDS)
  135. frame = pd.read_csv(path, encoding="utf-8-sig")
  136. if unit and "机组" in frame.columns:
  137. frame = frame[frame["机组"].astype(str).str.strip() == unit].copy().reset_index(drop=True)
  138. if frame.empty:
  139. return pd.DataFrame(columns=CORE_FIELDS)
  140. stage_column = "压力阶段" if label_type == "润滑油压力" else "温度阶段"
  141. if stage_column not in frame:
  142. stage_column = "振动阶段"
  143. result = pd.DataFrame()
  144. result["标签ID"] = [f"{prefix}-{i:04d}" for i in range(1, len(frame) + 1)]
  145. result["标签类型"] = label_type
  146. result["机组"] = frame.get("机组", "")
  147. result["批次"] = frame.get("批次", "")
  148. result["周期编号"] = frame.get("周期编号", "")
  149. result["阶段"] = frame.get(stage_column, "")
  150. result["阶段部位"] = frame.get("阶段部位", frame.get("级", ""))
  151. result["阶段点位"] = frame.get("阶段点位", "")
  152. result["开始小时"] = frame.get("开始小时", "")
  153. result["结束小时"] = frame.get("结束小时", "")
  154. result["持续自然小时数"] = frame.get("持续自然小时数", "")
  155. result["有效运行小时数"] = frame.get("有效运行小时数", "")
  156. result["方向"] = frame.get("方向", "")
  157. result["特征类型"] = frame.get("特征类型", "")
  158. result["置信度"] = frame.get("置信度", "")
  159. result["是否形成确认等级"] = frame.get("是否形成确认等级", "")
  160. result["阶段触发依据"] = frame.get("阶段触发依据", "")
  161. result["数据来源"] = str(path.name)
  162. for column in CORE_FIELDS:
  163. if column not in result:
  164. result[column] = ""
  165. return result[CORE_FIELDS]
  166. def build_combined_preview(out_dir: Path) -> pd.DataFrame:
  167. pressure = _map_external(
  168. Path("/Users/zhouhao/Code/Python/Compressor/models/results/pressure/润滑油压力小时阶段段.csv"),
  169. "润滑油压力",
  170. "P",
  171. )
  172. temperature = _map_external(
  173. Path("/Users/zhouhao/Code/Python/Compressor/_tmp_temperature/out/温度异常段.csv"),
  174. "温度异常",
  175. "T",
  176. )
  177. vibration = pd.read_csv(out_dir / "cylinder_vibration_label_unified.csv", parse_dates=["开始小时", "结束小时"])
  178. vibration_core = vibration.reindex(columns=CORE_FIELDS)
  179. return pd.concat([pressure, temperature, vibration_core], ignore_index=True)