"""预测式异常模块:FORECAST + 区间外点判定。""" import logging from typing import TypedDict logger = logging.getLogger(__name__) class ForecastPoint(TypedDict): ts: int value: float _flow: float _fhigh: float is_anomaly: bool def run_forecast( conn, stable: str, algo: str = "holtwinters", rows: int = 10, conf: int = 95, ) -> list[tuple[int, float, float]]: """执行 FORECAST 查询。 Args: conn: TDConnection 实例 stable: supertable 名称 algo: 预测算法,默认 holtwinters rows: 预测行数 conf: 置信度百分比 Returns: [(frowts_ms, flow, fhigh), ...] Raises: RuntimeError: SQL 执行失败 """ sql = ( f"SELECT _FROWTS, _FLOW, _FHIGH " f"FROM (FORECAST(value, \"algo={algo},rows={rows},conf={conf}\") " f"FROM ds_{stable})" ) try: raw = conn.execute(sql) except Exception as e: raise RuntimeError(f"FORECAST 执行失败: {e}") from e results = [] for row in raw: frowts = int(row[0]) if row[0] is not None else 0 flow = float(row[1]) if row[1] is not None else 0.0 fhigh = float(row[2]) if row[2] is not None else 0.0 results.append((frowts, flow, fhigh)) return results def forecast_anomaly( conn, stable: str, algo: str = "holtwinters", rows: int = 10, conf: int = 95 ) -> list[ForecastPoint]: """FORECAST 预测并判定区间外异常。 步骤: 1. 执行 FORECAST,获取 [(_flow, _fhigh)] 区间 2. 查询预测区间对应的原始数据 3. 比较判定:value < _flow or value > _fhigh → is_anomaly Args: conn: TDConnection 实例 stable: supertable 名称 algo: 预测算法 rows: 预测行数 conf: 置信度百分比 Returns: ForecastPoint 列表(含 is_anomaly 标记) """ forecasts = run_forecast(conn, stable, algo, rows, conf) if not forecasts: return [] frowtses = [f[0] for f in forecasts] start_ts = min(frowtses) end_ts = max(frowtses) sql = f"SELECT ts, value FROM s_{stable} WHERE ts >= {start_ts} AND ts <= {end_ts}" raw = conn.execute(sql) fmap = {f[0]: (f[1], f[2]) for f in forecasts} results: list[ForecastPoint] = [] for row in raw: ts = int(row[0]) value = float(row[1]) if ts in fmap: flow, fhigh = fmap[ts] is_anom = value < flow or value > fhigh else: flow, fhigh = 0.0, 0.0 is_anom = False results.append(ForecastPoint( ts=ts, value=value, _flow=flow, _fhigh=fhigh, is_anomaly=is_anom )) logger.info( "forecast_anomaly: %d points, %d anomalies", len(results), sum(1 for r in results if r["is_anomaly"]), ) return results