intervals.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """第二轮:把单点候选扩展成连续时间段,并做事件验证。"""
  2. from __future__ import annotations
  3. from datetime import timedelta
  4. import numpy as np
  5. import pandas as pd
  6. from . import config
  7. def _level(max_score: float, active_ratio: float, duration_hours: float) -> str:
  8. if max_score >= 6.0 and active_ratio >= 0.5 and duration_hours >= 2.0:
  9. return "严重"
  10. if max_score >= 4.5 and active_ratio >= 0.3:
  11. return "异常"
  12. return "轻微"
  13. def _confidence(max_score: float, active_ratio: float, duration_hours: float) -> str:
  14. if max_score >= 6.0 and active_ratio >= 0.5 and duration_hours >= 2.0:
  15. return "高"
  16. if max_score >= 4.5 and active_ratio >= 0.3:
  17. return "中"
  18. return "低"
  19. def build_intervals(
  20. frame: pd.DataFrame,
  21. point_labels: pd.DataFrame,
  22. before_hours: float = 1.0,
  23. after_hours: float = 1.0,
  24. merge_gap_hours: float = 2.0,
  25. min_duration_hours: float = 0.75,
  26. ) -> pd.DataFrame:
  27. """每个单点候选向左右扩展 before/after 小时,再按缸合并相邻段。"""
  28. if point_labels.empty or frame.empty:
  29. return pd.DataFrame()
  30. frame = frame.copy()
  31. frame["sample_time"] = pd.to_datetime(frame["sample_time"])
  32. rows: list[dict] = []
  33. for cylinder, group in frame.groupby("cylinder", sort=False):
  34. group = group.sort_values("sample_time").reset_index(drop=True)
  35. seeds = point_labels[point_labels["cylinder"] == cylinder]
  36. if seeds.empty:
  37. continue
  38. candidates: list[tuple[pd.Timestamp, pd.Timestamp, pd.Series]] = []
  39. for seed in seeds.itertuples(index=False):
  40. seed_time = pd.Timestamp(getattr(seed, "max_time"))
  41. start = seed_time - timedelta(hours=before_hours)
  42. end = seed_time + timedelta(hours=after_hours)
  43. part = group[(group["sample_time"] >= start) & (group["sample_time"] <= end)]
  44. if part.empty:
  45. part = group.iloc[[int((group["sample_time"] - seed_time).abs().argmin())]]
  46. # 不足 1 小时时,扩大到前后 1.5 小时,保证形成可解释的时间段。
  47. if (pd.Timestamp(part["sample_time"].iloc[-1]) - pd.Timestamp(part["sample_time"].iloc[0])).total_seconds() / 3600.0 < 1.0:
  48. part = group[
  49. (group["sample_time"] >= seed_time - timedelta(hours=1.5))
  50. & (group["sample_time"] <= seed_time + timedelta(hours=1.5))
  51. ]
  52. if part.empty:
  53. continue
  54. candidates.append((pd.Timestamp(part["sample_time"].iloc[0]), pd.Timestamp(part["sample_time"].iloc[-1]), seed))
  55. if not candidates:
  56. continue
  57. candidates.sort(key=lambda item: item[0])
  58. merged: list[tuple[pd.Timestamp, pd.Timestamp, list[pd.Series]]] = []
  59. for start, end, seed in candidates:
  60. if merged and start <= merged[-1][1] + timedelta(hours=merge_gap_hours):
  61. merged[-1] = (merged[-1][0], max(merged[-1][1], end), merged[-1][2] + [seed])
  62. else:
  63. merged.append((start, end, [seed]))
  64. for start, end, seed_rows in merged:
  65. part = group[(group["sample_time"] >= start) & (group["sample_time"] <= end)]
  66. if part.empty:
  67. continue
  68. actual_start = pd.Timestamp(part["sample_time"].iloc[0])
  69. actual_end = pd.Timestamp(part["sample_time"].iloc[-1])
  70. duration = (actual_end - actual_start).total_seconds() / 3600.0
  71. if duration < min_duration_hours:
  72. continue
  73. max_row = part.loc[part["score"].idxmax()]
  74. max_score = float(max_row["score"])
  75. mean_score = float(part["score"].mean())
  76. active_points = int((part["score"] >= 3.0).sum())
  77. active_ratio = active_points / max(len(part), 1)
  78. seed_best = max(seed_rows, key=lambda item: float(item.max_score))
  79. rows.append(
  80. {
  81. "cylinder": cylinder,
  82. "stage": config.CYLINDERS[cylinder]["stage"],
  83. "start_time": actual_start,
  84. "end_time": actual_end,
  85. "duration_hours": round(duration, 2),
  86. "points": int(len(part)),
  87. "mean_score": round(mean_score, 3),
  88. "max_score": round(max_score, 3),
  89. "active_points": active_points,
  90. "active_ratio": round(active_ratio, 2),
  91. "max_time": pd.Timestamp(max_row["sample_time"]),
  92. "driver": str(max_row["driver"]),
  93. "seed_max_score": round(float(seed_best.max_score), 3),
  94. "seed_max_time": pd.Timestamp(seed_best.max_time),
  95. "level": _level(max_score, active_ratio, duration),
  96. "confidence": _confidence(max_score, active_ratio, duration),
  97. },
  98. )
  99. if not rows:
  100. return pd.DataFrame()
  101. return pd.DataFrame(rows).sort_values(["start_time", "cylinder"]).reset_index(drop=True)
  102. def validate_events(intervals: pd.DataFrame, temperature: pd.DataFrame) -> pd.DataFrame:
  103. rows: list[dict] = []
  104. event_windows: list[tuple[pd.Timestamp, pd.Timestamp]] = []
  105. for event in config.EVENTS:
  106. event_time = pd.Timestamp(event["date"])
  107. window_start = event_time - pd.Timedelta(days=config.VALIDATION_BEFORE_DAYS)
  108. window_end = event_time + pd.Timedelta(days=config.VALIDATION_AFTER_DAYS)
  109. event_windows.append((window_start, window_end))
  110. cylinder = event["cylinder"]
  111. candidate = intervals[
  112. (intervals["cylinder"] == cylinder)
  113. & (intervals["start_time"] <= window_end)
  114. & (intervals["end_time"] >= window_start)
  115. ] if not intervals.empty else pd.DataFrame()
  116. if candidate.empty:
  117. hit = False
  118. interval = None
  119. else:
  120. interval = candidate.loc[candidate["max_score"].idxmax()]
  121. hit = True
  122. temp_row = pd.DataFrame()
  123. if not temperature.empty:
  124. temp_row = temperature[
  125. (temperature["cylinder"] == cylinder)
  126. & (temperature["start_time"] <= window_end)
  127. & (temperature["end_time"] >= window_start)
  128. ].sort_values("start_time")
  129. if interval is None:
  130. rows.append(
  131. {
  132. "event_id": event["event_id"],
  133. "event_date": event["date"],
  134. "cylinder": cylinder,
  135. "description": event["description"],
  136. "vibration_hit": False,
  137. "interval_start": None,
  138. "interval_end": None,
  139. "duration_hours": None,
  140. "mean_score": None,
  141. "max_score": None,
  142. "driver": None,
  143. "level": None,
  144. "confidence": None,
  145. "lead_days": None,
  146. "temp_reference_start": None if temp_row.empty else temp_row["start_time"].iloc[0],
  147. "temp_reference_end": None if temp_row.empty else temp_row["end_time"].iloc[0],
  148. "temp_reference_stage": None if temp_row.empty else " / ".join(temp_row["温度阶段"].astype(str).unique()),
  149. },
  150. )
  151. continue
  152. if pd.Timestamp(interval["start_time"]) <= event_time:
  153. lead_days = round((event_time - pd.Timestamp(interval["start_time"])).total_seconds() / 86400.0, 2)
  154. else:
  155. lead_days = -round((pd.Timestamp(interval["start_time"]) - event_time).total_seconds() / 86400.0, 2)
  156. rows.append(
  157. {
  158. "event_id": event["event_id"],
  159. "event_date": event["date"],
  160. "cylinder": cylinder,
  161. "description": event["description"],
  162. "vibration_hit": True,
  163. "interval_start": interval["start_time"],
  164. "interval_end": interval["end_time"],
  165. "duration_hours": interval["duration_hours"],
  166. "mean_score": interval["mean_score"],
  167. "max_score": interval["max_score"],
  168. "driver": interval["driver"],
  169. "level": interval["level"],
  170. "confidence": interval["confidence"],
  171. "lead_days": lead_days,
  172. "temp_reference_start": None if temp_row.empty else temp_row["start_time"].iloc[0],
  173. "temp_reference_end": None if temp_row.empty else temp_row["end_time"].iloc[0],
  174. "temp_reference_stage": None if temp_row.empty else " / ".join(temp_row["温度阶段"].astype(str).unique()),
  175. },
  176. )
  177. result = pd.DataFrame(rows)
  178. if not intervals.empty:
  179. related = intervals.apply(
  180. lambda row: any(
  181. pd.Timestamp(row["start_time"]) <= end and pd.Timestamp(row["end_time"]) >= start
  182. for start, end in event_windows
  183. ),
  184. axis=1,
  185. )
  186. false_alarms = intervals[~related]
  187. false_hours = float(false_alarms["duration_hours"].sum()) if not false_alarms.empty else 0.0
  188. else:
  189. false_alarms = pd.DataFrame()
  190. false_hours = 0.0
  191. result.attrs["false_alarm_intervals"] = int(len(false_alarms))
  192. result.attrs["false_alarm_hours"] = round(false_hours, 2)
  193. return result