feat: add detection + forecast modules with tests
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""预测式异常模块: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
|
||||
Reference in New Issue
Block a user