adaptive.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. RMS_HIGH = 1.10
  8. KURT_HIGH = 1.50
  9. ACTIVE_RATIO = 0.30
  10. MAX_GROW_HOURS = 6.0
  11. MAX_GAP_MINUTES = 60
  12. def add_ratios(frame: pd.DataFrame, baseline: dict) -> pd.DataFrame:
  13. frame = frame.copy()
  14. frame["rms_ratio"] = np.nan
  15. frame["kurt_ratio"] = np.nan
  16. for cylinder, group in frame.groupby("cylinder", sort=False):
  17. meta = baseline.get(cylinder, {})
  18. rms_median = float(meta.get("rms_log", [0.0, 1.0])[0])
  19. kurt_median = float(meta.get("kurt", [1.0, 1.0])[0])
  20. frame.loc[group.index, "rms_ratio"] = np.power(10.0, group["rms_log"] - rms_median)
  21. if kurt_median > 0:
  22. frame.loc[group.index, "kurt_ratio"] = group["kurt"] / kurt_median
  23. return frame
  24. def _expand(group: pd.DataFrame, seed_index: int) -> tuple[int, int]:
  25. seed_time = pd.Timestamp(group["sample_time"].iloc[seed_index])
  26. left = right = seed_index
  27. def allowed(index: int) -> bool:
  28. row = group.iloc[index]
  29. gap = abs((pd.Timestamp(row["sample_time"]) - seed_time).total_seconds()) / 60.0
  30. if gap > MAX_GROW_HOURS * 60:
  31. return False
  32. neighbor_gap = abs((pd.Timestamp(row["sample_time"]) - pd.Timestamp(group["sample_time"].iloc[right if index > right else left])).total_seconds()) / 60.0
  33. if index > right:
  34. neighbor_gap = (pd.Timestamp(row["sample_time"]) - pd.Timestamp(group["sample_time"].iloc[right])).total_seconds() / 60.0
  35. elif index < left:
  36. neighbor_gap = (pd.Timestamp(group["sample_time"].iloc[left]) - pd.Timestamp(row["sample_time"])).total_seconds() / 60.0
  37. if neighbor_gap > MAX_GAP_MINUTES:
  38. return False
  39. near_seed = gap <= 60.0
  40. return (
  41. near_seed
  42. or float(row["score"]) >= 3.0
  43. or float(row["kurt_ratio"]) >= KURT_HIGH
  44. or float(row["rms_ratio"]) >= RMS_HIGH
  45. )
  46. while left - 1 >= 0 and allowed(left - 1):
  47. left -= 1
  48. while right + 1 < len(group) and allowed(right + 1):
  49. right += 1
  50. start = pd.Timestamp(group["sample_time"].iloc[left])
  51. end = pd.Timestamp(group["sample_time"].iloc[right])
  52. if (end - start).total_seconds() / 3600.0 < 1.0:
  53. start_time = seed_time - timedelta(hours=1)
  54. end_time = seed_time + timedelta(hours=1)
  55. part = group[(group["sample_time"] >= start_time) & (group["sample_time"] <= end_time)]
  56. if not part.empty:
  57. left = int(part.index[0])
  58. right = int(part.index[-1])
  59. return left, right
  60. def build_intervals(frame: pd.DataFrame, point_labels: pd.DataFrame, baseline: dict) -> pd.DataFrame:
  61. if frame.empty or point_labels.empty:
  62. return pd.DataFrame()
  63. if "rms_ratio" not in frame.columns or "kurt_ratio" not in frame.columns:
  64. frame = add_ratios(frame, baseline)
  65. frame["sample_time"] = pd.to_datetime(frame["sample_time"])
  66. rows: list[dict] = []
  67. for cylinder, group in frame.groupby("cylinder", sort=False):
  68. group = group.sort_values("sample_time").reset_index(drop=True)
  69. seeds = point_labels[point_labels["cylinder"] == cylinder]
  70. if seeds.empty:
  71. continue
  72. candidates: list[tuple[int, int]] = []
  73. for seed in seeds.itertuples(index=False):
  74. seed_time = pd.Timestamp(getattr(seed, "max_time"))
  75. index = int((group["sample_time"] - seed_time).abs().argmin())
  76. candidates.append(_expand(group, index))
  77. candidates.sort()
  78. merged: list[tuple[int, int]] = []
  79. for left, right in candidates:
  80. if merged and pd.Timestamp(group["sample_time"].iloc[left]) <= pd.Timestamp(group["sample_time"].iloc[merged[-1][1]]) + timedelta(hours=2):
  81. merged[-1] = (merged[-1][0], max(merged[-1][1], right))
  82. else:
  83. merged.append((left, right))
  84. for left, right in merged:
  85. part = group.iloc[left:right + 1]
  86. if part.empty:
  87. continue
  88. start = pd.Timestamp(part["sample_time"].iloc[0])
  89. end = pd.Timestamp(part["sample_time"].iloc[-1])
  90. duration = (end - start).total_seconds() / 3600.0
  91. if duration < 0.75:
  92. continue
  93. max_row = part.loc[part["score"].idxmax()]
  94. active_rms = float((part["rms_ratio"] >= RMS_HIGH).mean())
  95. active_kurt = float((part["kurt_ratio"] >= KURT_HIGH).mean())
  96. max_rms_ratio = float(part["rms_ratio"].max())
  97. max_kurt_ratio = float(part["kurt_ratio"].max())
  98. mean_rms_ratio = float(part["rms_ratio"].mean())
  99. mean_kurt_ratio = float(part["kurt_ratio"].mean())
  100. high_rms = max_rms_ratio >= RMS_HIGH
  101. high_kurt = max_kurt_ratio >= KURT_HIGH
  102. confirmed = high_rms and high_kurt
  103. if confirmed:
  104. direction = "高幅高冲击"
  105. elif high_kurt and not high_rms:
  106. direction = "低幅高冲击"
  107. elif high_rms and not high_kurt:
  108. direction = "高幅"
  109. else:
  110. direction = "未确认"
  111. score = float(max_row["score"])
  112. candidate_threshold = float(part["threshold"].iloc[0])
  113. anomaly_threshold = float(part["anomaly_threshold"].iloc[0])
  114. severe_threshold = float(part["severe_threshold"].iloc[0])
  115. if score >= severe_threshold:
  116. level = "严重异常"
  117. confidence = "高"
  118. elif score >= anomaly_threshold:
  119. level = "异常"
  120. confidence = "中"
  121. elif score >= candidate_threshold:
  122. level = "轻微"
  123. confidence = "低"
  124. else:
  125. level = "观察"
  126. confidence = "低"
  127. rows.append(
  128. {
  129. "cylinder": cylinder,
  130. "stage": config.CYLINDERS[cylinder]["stage"],
  131. "start_time": start,
  132. "end_time": end,
  133. "duration_hours": round(duration, 2),
  134. "points": int(len(part)),
  135. "max_score": round(float(max_row["score"]), 3),
  136. "max_time": pd.Timestamp(max_row["sample_time"]),
  137. "driver": str(max_row["driver"]),
  138. "active_rms_ratio": round(active_rms, 2),
  139. "active_kurt_ratio": round(active_kurt, 2),
  140. "max_rms_ratio": round(max_rms_ratio, 3),
  141. "max_kurt_ratio": round(max_kurt_ratio, 3),
  142. "mean_rms_ratio": round(mean_rms_ratio, 3),
  143. "mean_kurt_ratio": round(mean_kurt_ratio, 3),
  144. "multi_feature_confirmed": bool(confirmed),
  145. "direction": direction,
  146. "level": level,
  147. "confidence": confidence,
  148. },
  149. )
  150. if not rows:
  151. return pd.DataFrame()
  152. return pd.DataFrame(rows).sort_values(["start_time", "cylinder"]).reset_index(drop=True)
  153. def validate_events(intervals: pd.DataFrame) -> pd.DataFrame:
  154. if intervals.empty:
  155. return pd.DataFrame()
  156. rows: list[dict] = []
  157. for event in config.EVENTS:
  158. event_time = pd.Timestamp(event["date"])
  159. window_start = event_time - pd.Timedelta(days=config.VALIDATION_BEFORE_DAYS)
  160. window_end = event_time + pd.Timedelta(days=config.VALIDATION_AFTER_DAYS)
  161. cylinder = event["cylinder"]
  162. candidates = intervals[
  163. (intervals["cylinder"] == cylinder)
  164. & (intervals["start_time"] <= window_end)
  165. & (intervals["end_time"] >= window_start)
  166. ]
  167. if candidates.empty:
  168. rows.append(
  169. {
  170. "event_id": event["event_id"],
  171. "event_date": event["date"],
  172. "cylinder": cylinder,
  173. "description": event["description"],
  174. "hit": False,
  175. "confirmed": False,
  176. "start_time": None,
  177. "end_time": None,
  178. "duration_hours": None,
  179. "max_score": None,
  180. "direction": None,
  181. "level": None,
  182. "confidence": None,
  183. "lead_days": None,
  184. },
  185. )
  186. continue
  187. confirmed = candidates[candidates["multi_feature_confirmed"]]
  188. pool = confirmed if not confirmed.empty else candidates
  189. row = pool.loc[pool["max_score"].idxmax()]
  190. lead_days = round((event_time - pd.Timestamp(row["start_time"])).total_seconds() / 86400.0, 2)
  191. rows.append(
  192. {
  193. "event_id": event["event_id"],
  194. "event_date": event["date"],
  195. "cylinder": cylinder,
  196. "description": event["description"],
  197. "hit": True,
  198. "confirmed": bool(row["multi_feature_confirmed"]),
  199. "start_time": row["start_time"],
  200. "end_time": row["end_time"],
  201. "duration_hours": row["duration_hours"],
  202. "max_score": row["max_score"],
  203. "direction": row["direction"],
  204. "level": row["level"],
  205. "confidence": row["confidence"],
  206. "lead_days": lead_days,
  207. },
  208. )
  209. return pd.DataFrame(rows)