MiMo AI 能力扩展:TAF解读、概率分布解读、异常检测、市场概览
AI 解读字段扩展:
- 新增 taf_read_zh/en:解读机场预报中影响今日峰值窗口的变化
- 新增 probability_read_zh/en:描述概率分布形态(最高桶、偏左/偏右)
- stream max_tokens 900→1200 容纳新输出字段
- 缓存 key 简化为 METAR原文+观测时间,大幅提升命中率
- 兜底函数补全 TAF 和概率字段的确定性生成
异常检测:
- 纯数学计算,零 AI 延迟:实测温度 vs 全部模型预测上下限
- 三级告警:breakout_above / breakout_below / deviation
市场概览:
- 新增 POST /api/scan/terminal/overview(MiMo 批量解读,缓存10分钟)
- 前端 MarketOverviewBanner 可折叠横幅(顶栏与标签栏之间)
- 移动端适配 640px/768px 断点,暗色/亮色双主题
Scope-risk: MEDIUM — 170 测试通过,TypeScript 零错误,ruff 零告警
Tested: python -m pytest -q (170 passed), npx tsc --noEmit (0 errors), ruff check .
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""Anomaly detection — pure math, no AI call.
|
||||
|
||||
Flags cities where current observations deviate from model predictions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from web.scan_city_ai_helpers import _safe_float
|
||||
|
||||
|
||||
def _check_city_anomaly(
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
high_temp_threshold: float = 2.0,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Return anomaly flag if current observation breaks model cluster bounds."""
|
||||
current = data.get("current") if isinstance(data.get("current"), dict) else {}
|
||||
airport = data.get("airport_current") if isinstance(data.get("airport_current"), dict) else {}
|
||||
multi = data.get("multi_model") if isinstance(data.get("multi_model"), dict) else {}
|
||||
deb = data.get("deb") if isinstance(data.get("deb"), dict) else {}
|
||||
|
||||
observed = _safe_float(current.get("temp") or airport.get("temp"))
|
||||
if observed is None:
|
||||
return None
|
||||
|
||||
model_highs = [
|
||||
_safe_float(v)
|
||||
for v in multi.values()
|
||||
if _safe_float(v) is not None
|
||||
]
|
||||
deb_pred = _safe_float(deb.get("prediction"))
|
||||
if deb_pred is not None:
|
||||
model_highs.append(deb_pred)
|
||||
|
||||
if not model_highs:
|
||||
return None
|
||||
|
||||
model_max = max(model_highs)
|
||||
model_min = min(model_highs)
|
||||
model_median = sorted(model_highs)[len(model_highs) // 2]
|
||||
|
||||
delta_above_max = observed - model_max
|
||||
delta_below_min = model_min - observed
|
||||
delta_from_median = observed - model_median
|
||||
|
||||
anomaly: Optional[Dict[str, Any]] = None
|
||||
|
||||
if delta_above_max > high_temp_threshold:
|
||||
anomaly = {
|
||||
"level": "breakout_above",
|
||||
"observed": observed,
|
||||
"model_max": model_max,
|
||||
"delta": round(delta_above_max, 1),
|
||||
"model_count": len(model_highs),
|
||||
}
|
||||
elif delta_below_min > high_temp_threshold:
|
||||
anomaly = {
|
||||
"level": "breakout_below",
|
||||
"observed": observed,
|
||||
"model_min": model_min,
|
||||
"delta": round(delta_below_min, 1),
|
||||
"model_count": len(model_highs),
|
||||
}
|
||||
elif abs(delta_from_median) > 1.5:
|
||||
anomaly = {
|
||||
"level": "deviation",
|
||||
"observed": observed,
|
||||
"model_median": model_median,
|
||||
"delta": round(delta_from_median, 1),
|
||||
"model_count": len(model_highs),
|
||||
}
|
||||
|
||||
if anomaly:
|
||||
anomaly.update(
|
||||
{
|
||||
"city": data.get("name") or data.get("city"),
|
||||
"local_date": data.get("local_date"),
|
||||
"temp_unit": data.get("temp_symbol", "°C"),
|
||||
"deb_prediction": deb_pred,
|
||||
}
|
||||
)
|
||||
return anomaly
|
||||
|
||||
|
||||
def detect_scan_terminal_anomalies(
|
||||
rows: List[Dict[str, Any]],
|
||||
*,
|
||||
high_temp_threshold: float = 2.0,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Scan all terminal rows and return anomaly flags."""
|
||||
anomalies = []
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
city_data = row.get("city_data") or row
|
||||
flag = _check_city_anomaly(city_data, high_temp_threshold=high_temp_threshold)
|
||||
if flag:
|
||||
flag["row_id"] = row.get("row_id") or row.get("id")
|
||||
anomalies.append(flag)
|
||||
return anomalies
|
||||
Reference in New Issue
Block a user