031aa35604
- schema: rename label -> is_anomaly, escape `value` as backtick - detection/forecast: wrap subquery with SELECT ts, `value` AS v so ANOMALY_WINDOW/FORECAST receive non-keyword column name - resolves [0x2600] syntax error and unblocks E2E pipeline on community tsdb 3.4.1
109 lines
2.9 KiB
Python
109 lines
2.9 KiB
Python
"""预测式异常模块: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 (SELECT ts, `value` AS v FROM ds_{stable}) "
|
|
f"FORECAST(v, 'algo={algo},rows={rows},conf={conf}')"
|
|
)
|
|
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
|