hf_validation.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """Validate high-frequency candidates with 5-second raw data.
  2. The hourly ``STDDEV`` used by the first pass also reacts to a slow one-sided
  3. ramp inside an hour. This module removes a rolling slow trend and counts
  4. short reversals, then keeps only candidates that look like repeated up/down
  5. movement rather than a load ramp.
  6. """
  7. from __future__ import annotations
  8. import re
  9. from datetime import datetime, timedelta
  10. import numpy as np
  11. import pandas as pd
  12. from scipy.signal import find_peaks
  13. from models.common.db import connect
  14. DEFAULT_POINT = "YSJ_21"
  15. TREND_WINDOW = 121 # 10-minute rolling median at 5s resolution
  16. MIN_PROMINENCE = 0.3 # degC; prevent sensor noise from counting
  17. MIN_REVERSALS_PER_HOUR = 5.0
  18. MIN_RESIDUAL_STD = 0.5 # degC
  19. def _parse(value: str) -> datetime:
  20. return datetime.strptime(str(value).strip(), "%Y-%m-%d %H:%M:%S")
  21. def _point(row: dict) -> str:
  22. point = str(row.get("阶段点位") or DEFAULT_POINT).strip()
  23. return point if re.fullmatch(r"YSJ_\d+", point) else DEFAULT_POINT
  24. def _metrics(row: dict) -> dict:
  25. batch = int(row["批次"])
  26. start = _parse(row["开始小时"])
  27. end = _parse(row["结束小时"]) + timedelta(hours=1)
  28. point = _point(row)
  29. query_start = start - timedelta(hours=1)
  30. query_end = end + timedelta(hours=1)
  31. with connect() as connection:
  32. with connection.cursor() as cursor:
  33. cursor.execute(
  34. f"SELECT sample_time, `{point}` AS value "
  35. "FROM pks_long_sample "
  36. "WHERE import_batch_id = %s AND sample_time >= %s AND sample_time <= %s "
  37. "ORDER BY sample_time",
  38. (batch, query_start, query_end),
  39. )
  40. rows = cursor.fetchall()
  41. if not rows:
  42. return {"去趋势残差标准差": "", "每小时反复升降次数": "", "是否确认": "否"}
  43. times = pd.to_datetime([item["sample_time"] for item in rows])
  44. values = np.asarray([float(item["value"] or 0.0) for item in rows], dtype=float)
  45. values[values <= 0] = np.nan
  46. valid = ~np.isnan(values)
  47. times = times[valid]
  48. values = values[valid]
  49. if len(values) < 30:
  50. return {"去趋势残差标准差": "", "每小时反复升降次数": "", "是否确认": "否"}
  51. series = pd.Series(values)
  52. trend = series.rolling(TREND_WINDOW, center=True, min_periods=30).median().to_numpy()
  53. residual = values - trend
  54. window = (times >= start) & (times < end)
  55. residual = residual[window]
  56. segment_times = times[window]
  57. if len(residual) < 30:
  58. return {"去趋势残差标准差": "", "每小时反复升降次数": "", "是否确认": "否"}
  59. residual_std = float(np.nanstd(residual))
  60. prominence = max(MIN_PROMINENCE, residual_std * 0.7)
  61. peaks, _ = find_peaks(residual, prominence=prominence, distance=12)
  62. troughs, _ = find_peaks(-residual, prominence=prominence, distance=12)
  63. hours = max((segment_times[-1] - segment_times[0]).total_seconds() / 3600.0, 1e-9)
  64. reversals = float(len(peaks) + len(troughs)) / hours
  65. confirmed = residual_std >= MIN_RESIDUAL_STD and reversals >= MIN_REVERSALS_PER_HOUR
  66. row["阶段触发依据"] = (
  67. f"{row.get('阶段触发依据') or ''};去趋势残差{residual_std:.2f}℃、"
  68. f"反复升降{reversals:.1f}次/小时"
  69. ).strip(";")
  70. return {
  71. "去趋势残差标准差": round(residual_std, 2),
  72. "每小时反复升降次数": round(reversals, 1),
  73. "是否确认": "是" if confirmed else "否",
  74. }
  75. def annotate(hf_rows: list[dict]) -> list[dict]:
  76. for row in hf_rows:
  77. row.update(_metrics(row))
  78. return hf_rows
  79. def filter_hf(hf_rows: list[dict]) -> tuple[list[dict], list[dict]]:
  80. """Return ``(confirmed, candidates_with_metrics)``.
  81. ``candidates_with_metrics`` keeps every first-pass candidate and its
  82. validation metrics; ``confirmed`` only keeps candidates passing the
  83. residual/reversal test.
  84. """
  85. candidates = annotate(hf_rows)
  86. confirmed = [row for row in candidates if row.get("是否确认") == "是"]
  87. return confirmed, candidates
  88. __all__ = ["annotate", "filter_hf", "MIN_RESIDUAL_STD", "MIN_REVERSALS_PER_HOUR"]