feat: add versioned DEB bias backtesting
This commit is contained in:
+3
-3
@@ -9,7 +9,7 @@
|
|||||||
########################################
|
########################################
|
||||||
ENV=production
|
ENV=production
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
POLYWEATHER_MAP_URL=https://polyweather-pro.vercel.app/
|
POLYWEATHER_MAP_URL=https://polyweather.top/
|
||||||
POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather
|
POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather
|
||||||
POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db
|
POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db
|
||||||
OPEN_METEO_DISK_CACHE_PATH=/var/lib/polyweather/open_meteo_cache.json
|
OPEN_METEO_DISK_CACHE_PATH=/var/lib/polyweather/open_meteo_cache.json
|
||||||
@@ -21,7 +21,7 @@ GID=1000
|
|||||||
POLYWEATHER_STATE_STORAGE_MODE=sqlite
|
POLYWEATHER_STATE_STORAGE_MODE=sqlite
|
||||||
# Backend CORS allowlist. Add your Vercel production/preview domains when
|
# Backend CORS allowlist. Add your Vercel production/preview domains when
|
||||||
# NEXT_PUBLIC_POLYWEATHER_API_BASE_URL points browsers directly at this backend.
|
# NEXT_PUBLIC_POLYWEATHER_API_BASE_URL points browsers directly at this backend.
|
||||||
WEB_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,https://polyweather-pro.vercel.app
|
WEB_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,https://polyweather.top,https://www.polyweather.top,https://api.polyweather.top
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# 2) Telegram bot minimal
|
# 2) Telegram bot minimal
|
||||||
@@ -39,7 +39,7 @@ TELEGRAM_QUERY_TOPIC_CHAT_ID=
|
|||||||
TELEGRAM_QUERY_TOPIC_ID=
|
TELEGRAM_QUERY_TOPIC_ID=
|
||||||
TELEGRAM_QUERY_TOPIC_MAP=
|
TELEGRAM_QUERY_TOPIC_MAP=
|
||||||
POLYWEATHER_BOT_GROUP_INVITE_URL=
|
POLYWEATHER_BOT_GROUP_INVITE_URL=
|
||||||
POLYWEATHER_APP_URL=https://polyweather-pro.vercel.app
|
POLYWEATHER_APP_URL=https://polyweather.top
|
||||||
# Global Telegram auto-push copy: both, en, or zh. Module-specific vars can override it.
|
# Global Telegram auto-push copy: both, en, or zh. Module-specific vars can override it.
|
||||||
TELEGRAM_PUSH_LANGUAGE=both
|
TELEGRAM_PUSH_LANGUAGE=both
|
||||||
# High-frequency airport push loop. Keep workers at 1 on shared VPS.
|
# High-frequency airport push loop. Keep workers at 1 on shared VPS.
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ jobs:
|
|||||||
NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL || 'https://polyweather.top' }}
|
NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL || 'https://polyweather.top' }}
|
||||||
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=${{ secrets.NEXT_PUBLIC_POLYWEATHER_API_BASE_URL || '' }}
|
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=${{ secrets.NEXT_PUBLIC_POLYWEATHER_API_BASE_URL || '' }}
|
||||||
NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false
|
NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=${{ secrets.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || '' }}
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=${{ secrets.NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com' }}
|
||||||
|
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=${{ secrets.NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS || 'polyweather.top,www.polyweather.top' }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data/*.db
|
|||||||
data/*.db-*
|
data/*.db-*
|
||||||
data/*.db.*
|
data/*.db.*
|
||||||
data/*.json
|
data/*.json
|
||||||
|
data/*backtest*.csv
|
||||||
data/logs/
|
data/logs/
|
||||||
data/historical/
|
data/historical/
|
||||||
data/cache/
|
data/cache/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Production weather-intelligence stack for temperature settlement markets.
|
Production weather-intelligence stack for temperature settlement markets.
|
||||||
|
|
||||||
Official dashboard: [polyweather-pro.vercel.app](https://polyweather-pro.vercel.app/)
|
Official dashboard: [polyweather.top](https://polyweather.top/)
|
||||||
中文说明: [README_ZH.md](README_ZH.md)
|
中文说明: [README_ZH.md](README_ZH.md)
|
||||||
|
|
||||||
Public docs center: `/docs/intro` on the main site (bilingual product documentation, including intraday analysis, calibrated probability, model stack, TAF, settlement sources, history, and extension).
|
Public docs center: `/docs/intro` on the main site (bilingual product documentation, including intraday analysis, calibrated probability, model stack, TAF, settlement sources, history, and extension).
|
||||||
@@ -162,7 +162,7 @@ curl http://127.0.0.1:8000/metrics
|
|||||||
### Frontend cache headers
|
### Frontend cache headers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/validate_frontend_cache.sh "https://polyweather-pro.vercel.app"
|
./scripts/validate_frontend_cache.sh "https://polyweather.top"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Payment auto-reconciliation logs
|
### Payment auto-reconciliation logs
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
面向温度结算市场的生产级气象情报系统。
|
面向温度结算市场的生产级气象情报系统。
|
||||||
|
|
||||||
官方看板:[polyweather-pro.vercel.app](https://polyweather-pro.vercel.app/)
|
官方看板:[polyweather.top](https://polyweather.top/)
|
||||||
|
|
||||||
## 产品截图
|
## 产品截图
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ curl http://127.0.0.1:8000/metrics
|
|||||||
### 外部监控栈
|
### 外部监控栈
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/validate_frontend_cache.sh "https://polyweather-pro.vercel.app"
|
./scripts/validate_frontend_cache.sh "https://polyweather.top"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 支付自动补单日志
|
### 支付自动补单日志
|
||||||
@@ -187,7 +187,7 @@ curl http://127.0.0.1:8000/api/payments/runtime
|
|||||||
|
|
||||||
### 运营后台
|
### 运营后台
|
||||||
|
|
||||||
- 前端入口:`https://polyweather-pro.vercel.app/ops`
|
- 前端入口:`https://polyweather.top/ops`
|
||||||
- 后端需配置:
|
- 后端需配置:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
|
|||||||
@@ -60,9 +60,12 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL: ${NEXT_PUBLIC_POLYWEATHER_API_BASE_URL:-}
|
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL: ${NEXT_PUBLIC_POLYWEATHER_API_BASE_URL:-}
|
||||||
NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS: 'false'
|
NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS: 'false'
|
||||||
|
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS: ${NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS:-polyweather.top,www.polyweather.top}
|
||||||
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://polyweather.top}
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://polyweather.top}
|
||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
|
||||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL: ${NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL:-https://polygon-bor-rpc.publicnode.com}
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: ${NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID:-}
|
||||||
POLYWEATHER_API_BASE_URL: ${POLYWEATHER_API_BASE_URL:-http://polyweather_web:8000}
|
POLYWEATHER_API_BASE_URL: ${POLYWEATHER_API_BASE_URL:-http://polyweather_web:8000}
|
||||||
POLYWEATHER_AUTH_ENABLED: ${POLYWEATHER_AUTH_ENABLED:-true}
|
POLYWEATHER_AUTH_ENABLED: ${POLYWEATHER_AUTH_ENABLED:-true}
|
||||||
POLYWEATHER_AUTH_REQUIRED: ${POLYWEATHER_AUTH_REQUIRED:-true}
|
POLYWEATHER_AUTH_REQUIRED: ${POLYWEATHER_AUTH_REQUIRED:-true}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本文档记录 PolyWeather 当前开放模型接入、区域覆盖差异,以及 DEB 在新增模型后的计权规则。
|
本文档记录 PolyWeather 当前开放模型接入、区域覆盖差异,以及 DEB 在新增模型后的计权规则。
|
||||||
|
|
||||||
最后更新:`2026-04-18`
|
最后更新:`2026-05-27`
|
||||||
|
|
||||||
## 1. 接入方式
|
## 1. 接入方式
|
||||||
|
|
||||||
@@ -165,11 +165,47 @@ raw current_forecasts
|
|||||||
-> 按模型家族去重
|
-> 按模型家族去重
|
||||||
-> 历史 MAE 统计
|
-> 历史 MAE 统计
|
||||||
-> MAE 倒数权重
|
-> MAE 倒数权重
|
||||||
-> 输出 blended_high + weights_info
|
-> 输出 raw blended_high
|
||||||
|
-> recent signed-bias correction
|
||||||
|
-> 输出 production prediction + raw_prediction + version
|
||||||
```
|
```
|
||||||
|
|
||||||
当 `weights_info` 出现 `家族去重`,表示当前输入模型数量多于 DEB 实际入模数量,系统已先折叠同家族模型。
|
当 `weights_info` 出现 `家族去重`,表示当前输入模型数量多于 DEB 实际入模数量,系统已先折叠同家族模型。
|
||||||
|
|
||||||
|
### 5.1 版本化预测与偏差校正
|
||||||
|
|
||||||
|
DEB 原始融合逻辑不推倒重写,`calculate_dynamic_weights(...)` 仍作为 raw baseline。线上生产入口使用 `calculate_deb_prediction(...)` 包装 raw baseline,并在有足够历史样本时追加城市级 recent signed-bias correction。
|
||||||
|
|
||||||
|
当前版本:
|
||||||
|
|
||||||
|
- `deb_v1_raw`:原始 DEB,最近模型误差倒数加权后的融合值。
|
||||||
|
- `deb_v1_recent_bias_corrected`:在 raw DEB 上叠加最近已结算样本的有符号偏差校正。偏差使用 shrinkage,样本少时自动收缩,避免单日异常过拟合。
|
||||||
|
|
||||||
|
API `deb` payload 会保留:
|
||||||
|
|
||||||
|
- `prediction`:当前生产使用值。
|
||||||
|
- `raw_prediction`:未经 recent-bias correction 的原始 DEB。
|
||||||
|
- `version`:生产值对应的 DEB 版本。
|
||||||
|
- `bias_adjustment` / `bias_samples`:城市级偏差校正幅度与训练样本数。
|
||||||
|
- `intraday_adjustment`:网页 full detail 中额外的日内观测路径修正,仅用于当前日实时展示。
|
||||||
|
|
||||||
|
版本化回测命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/backtest_deb_versions.py \
|
||||||
|
--output-json data/deb_backtest_latest.json \
|
||||||
|
--output-csv data/deb_backtest_latest.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
本地 `data/polyweather.db` 于 2026-05-27 的回测样本显示:
|
||||||
|
|
||||||
|
| 版本 | 样本 | MAE | RMSE | Bias | 结算桶命中率 |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| `deb_v1_raw` | 1272 | 1.626 | 2.602 | -0.633 | 25.8% |
|
||||||
|
| `deb_v1_recent_bias_corrected` | 1272 | 1.499 | 2.542 | +0.263 | 31.4% |
|
||||||
|
|
||||||
|
该结果只证明当前历史样本上的离线表现改善;后续仍需要持续用版本化回测追踪不同城市、季节与结算源下的漂移。
|
||||||
|
|
||||||
## 6. 前端展示
|
## 6. 前端展示
|
||||||
|
|
||||||
网页的模型展示读取:
|
网页的模型展示读取:
|
||||||
@@ -228,6 +264,7 @@ raw current_forecasts
|
|||||||
|
|
||||||
- `tests/test_multi_model_sources.py`
|
- `tests/test_multi_model_sources.py`
|
||||||
- `tests/test_deb_model_family.py`
|
- `tests/test_deb_model_family.py`
|
||||||
|
- `tests/test_deb_evaluation_upgrade.py`
|
||||||
|
|
||||||
重点覆盖:
|
重点覆盖:
|
||||||
|
|
||||||
@@ -237,3 +274,4 @@ raw current_forecasts
|
|||||||
- DEB 家族去重
|
- DEB 家族去重
|
||||||
- 历史不足时的去重等权
|
- 历史不足时的去重等权
|
||||||
- 有历史 MAE 时的去重动态权重
|
- 有历史 MAE 时的去重动态权重
|
||||||
|
- DEB raw/corrected 版本化评估、recent-bias correction、JSON/CSV 回测输出
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|||||||
|
|
||||||
# 必填:生产环境站点 URL(OAuth 回调强制使用此域名)
|
# 必填:生产环境站点 URL(OAuth 回调强制使用此域名)
|
||||||
# 设置后,所有登录回调将始终跳转到此域名,而非当前浏览器地址。
|
# 设置后,所有登录回调将始终跳转到此域名,而非当前浏览器地址。
|
||||||
# 生产环境必须设为 https://polyweather-pro.vercel.app
|
# 生产环境必须设为 https://polyweather.top
|
||||||
NEXT_PUBLIC_SITE_URL=https://polyweather-pro.vercel.app
|
NEXT_PUBLIC_SITE_URL=https://polyweather.top
|
||||||
|
|
||||||
# 常用:前端鉴权开关
|
# 常用:前端鉴权开关
|
||||||
# true: 启用 Supabase 登录
|
# true: 启用 Supabase 登录
|
||||||
@@ -37,7 +37,7 @@ POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=
|
|||||||
# 可选:钱包支付 / Telegram 入口
|
# 可选:钱包支付 / Telegram 入口
|
||||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
||||||
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
||||||
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather-pro.vercel.app
|
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather.top,www.polyweather.top
|
||||||
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
|
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
|
||||||
NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group
|
NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group
|
||||||
NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/polyyuanbot
|
NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/polyyuanbot
|
||||||
|
|||||||
+3
-3
@@ -18,7 +18,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_1z0DR7nZ1Juf_HGTASA8WA_uxcHJnby
|
|||||||
|
|
||||||
# 必填:生产环境站点 URL(OAuth 回调强制使用此域名)
|
# 必填:生产环境站点 URL(OAuth 回调强制使用此域名)
|
||||||
# 设置后,所有登录回调将始终跳转到此域名,而非当前浏览器地址。
|
# 设置后,所有登录回调将始终跳转到此域名,而非当前浏览器地址。
|
||||||
# 生产环境必须设为 https://polyweather-pro.vercel.app
|
# 生产环境必须设为 https://polyweather.top
|
||||||
NEXT_PUBLIC_SITE_URL=https://polyweather.top
|
NEXT_PUBLIC_SITE_URL=https://polyweather.top
|
||||||
|
|
||||||
# 常用:前端鉴权开关
|
# 常用:前端鉴权开关
|
||||||
@@ -43,9 +43,9 @@ POLYWEATHER_DASHBOARD_ACCESS_TOKEN=
|
|||||||
POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=
|
POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=
|
||||||
|
|
||||||
# 可选:钱包支付 / Telegram 入口
|
# 可选:钱包支付 / Telegram 入口
|
||||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=8ce6332b57a42e606cf28224e82d1e02
|
||||||
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
||||||
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather-pro.vercel.app
|
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather.top,www.polyweather.top
|
||||||
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
|
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
|
||||||
NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group
|
NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group
|
||||||
NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/polyyuanbot
|
NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/polyyuanbot
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_1z0DR7nZ1Juf_HGTASA8WA_uxcHJnby
|
|||||||
NEXT_PUBLIC_SITE_URL=https://polyweather.top
|
NEXT_PUBLIC_SITE_URL=https://polyweather.top
|
||||||
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=https://api.polyweather.top
|
NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=https://api.polyweather.top
|
||||||
NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false
|
NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=false
|
||||||
|
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather.top,www.polyweather.top
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=8ce6332b57a42e606cf28224e82d1e02
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|||||||
ARG NEXT_PUBLIC_SITE_URL
|
ARG NEXT_PUBLIC_SITE_URL
|
||||||
ARG NEXT_PUBLIC_POLYWEATHER_API_BASE_URL
|
ARG NEXT_PUBLIC_POLYWEATHER_API_BASE_URL
|
||||||
ARG NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS
|
ARG NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS
|
||||||
|
ARG NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
|
||||||
|
ARG NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL
|
||||||
|
ARG NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS
|
||||||
|
|
||||||
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
|
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
|
||||||
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
|
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||||
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
||||||
ENV NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=$NEXT_PUBLIC_POLYWEATHER_API_BASE_URL
|
ENV NEXT_PUBLIC_POLYWEATHER_API_BASE_URL=$NEXT_PUBLIC_POLYWEATHER_API_BASE_URL
|
||||||
ENV NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=$NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS
|
ENV NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS=$NEXT_PUBLIC_POLYWEATHER_LOCAL_FULL_ACCESS
|
||||||
|
ENV NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=$NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
|
||||||
|
ENV NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=$NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL
|
||||||
|
ENV NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=$NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
COPY --from=deps /app/frontend/node_modules ./node_modules
|
COPY --from=deps /app/frontend/node_modules ./node_modules
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
PolyWeather Pro 的生产前端工程。
|
PolyWeather Pro 的生产前端工程。
|
||||||
|
|
||||||
线上地址:
|
线上地址:
|
||||||
- [https://polyweather-pro.vercel.app/](https://polyweather-pro.vercel.app/)
|
- [https://polyweather.top/](https://polyweather.top/)
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=
|
|||||||
# 钱包支付
|
# 钱包支付
|
||||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
||||||
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com
|
||||||
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather-pro.vercel.app
|
NEXT_PUBLIC_PAYMENT_ALLOWED_HOSTS=polyweather.top,www.polyweather.top
|
||||||
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
|
POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com
|
||||||
|
|
||||||
# 社群入口
|
# 社群入口
|
||||||
@@ -144,7 +144,7 @@ Ops:
|
|||||||
|
|
||||||
当前前端已内置轻量管理页:
|
当前前端已内置轻量管理页:
|
||||||
|
|
||||||
- [https://polyweather-pro.vercel.app/ops](https://polyweather-pro.vercel.app/ops)
|
- [https://polyweather.top/ops](https://polyweather.top/ops)
|
||||||
|
|
||||||
页面当前支持:
|
页面当前支持:
|
||||||
|
|
||||||
|
|||||||
@@ -206,7 +206,12 @@ export interface ForecastData {
|
|||||||
|
|
||||||
export interface DebForecast {
|
export interface DebForecast {
|
||||||
prediction: number | null;
|
prediction: number | null;
|
||||||
|
raw_prediction?: number | null;
|
||||||
|
version?: string | null;
|
||||||
weights_info?: string | null;
|
weights_info?: string | null;
|
||||||
|
bias_adjustment?: number | null;
|
||||||
|
bias_samples?: number | null;
|
||||||
|
intraday_adjustment?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CitySummary {
|
export interface CitySummary {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run versioned DEB backtests from runtime SQLite daily records."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.analysis.deb_evaluation import ( # noqa: E402
|
||||||
|
backtest_deb_versions,
|
||||||
|
flatten_daily_records,
|
||||||
|
write_backtest_report,
|
||||||
|
)
|
||||||
|
from src.database.runtime_state import DailyRecordRepository, RuntimeStateDB # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--db", default=str(ROOT / "data" / "polyweather.db"))
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-json",
|
||||||
|
default=str(ROOT / "data" / "deb_backtest_latest.json"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-csv",
|
||||||
|
default=str(ROOT / "data" / "deb_backtest_latest.csv"),
|
||||||
|
)
|
||||||
|
parser.add_argument("--train-lookback-days", type=int, default=30)
|
||||||
|
parser.add_argument("--min-train-samples", type=int, default=2)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db = RuntimeStateDB(args.db)
|
||||||
|
daily_records = DailyRecordRepository(db).load_all()
|
||||||
|
history = flatten_daily_records(daily_records)
|
||||||
|
report = backtest_deb_versions(
|
||||||
|
history,
|
||||||
|
train_lookback_days=args.train_lookback_days,
|
||||||
|
min_train_samples=args.min_train_samples,
|
||||||
|
)
|
||||||
|
write_backtest_report(
|
||||||
|
report,
|
||||||
|
json_path=args.output_json,
|
||||||
|
csv_path=args.output_csv,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": report.get("schema_version"),
|
||||||
|
"versions": report.get("versions"),
|
||||||
|
"rows": len(report.get("rows") or []),
|
||||||
|
"output_json": args.output_json,
|
||||||
|
"output_csv": args.output_csv,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not report.get("rows"):
|
||||||
|
return 2
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1209,6 +1209,82 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7, dec
|
|||||||
return round(blended_high, 1), " | ".join(weight_str_parts)
|
return round(blended_high, 1), " | ".join(weight_str_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_deb_prediction(
|
||||||
|
city_name,
|
||||||
|
current_forecasts,
|
||||||
|
*,
|
||||||
|
lookback_days=7,
|
||||||
|
decay_factor=0.85,
|
||||||
|
bias_lookback_days=30,
|
||||||
|
bias_min_samples=3,
|
||||||
|
raw_calculator=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Return the production DEB prediction with versioned recent-bias correction.
|
||||||
|
|
||||||
|
The raw DEB path remains `calculate_dynamic_weights`; this wrapper keeps the
|
||||||
|
raw value in the payload and only adds a conservative city-level signed-bias
|
||||||
|
adjustment when enough settled samples exist.
|
||||||
|
"""
|
||||||
|
from src.analysis.deb_evaluation import (
|
||||||
|
DEB_RAW_VERSION,
|
||||||
|
DEB_RECENT_BIAS_CORRECTED_VERSION,
|
||||||
|
build_recent_bias_corrector,
|
||||||
|
flatten_daily_records,
|
||||||
|
)
|
||||||
|
|
||||||
|
calculate_raw = raw_calculator or calculate_dynamic_weights
|
||||||
|
raw_prediction, weights_info = calculate_raw(
|
||||||
|
city_name,
|
||||||
|
current_forecasts,
|
||||||
|
lookback_days=lookback_days,
|
||||||
|
decay_factor=decay_factor,
|
||||||
|
)
|
||||||
|
if raw_prediction is None:
|
||||||
|
return {
|
||||||
|
"prediction": None,
|
||||||
|
"raw_prediction": None,
|
||||||
|
"version": DEB_RAW_VERSION,
|
||||||
|
"weights_info": weights_info,
|
||||||
|
"bias_adjustment": 0.0,
|
||||||
|
"bias_samples": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = load_history(_get_history_file_path())
|
||||||
|
history_rows = flatten_daily_records(data)
|
||||||
|
corrected = build_recent_bias_corrector(
|
||||||
|
history_rows,
|
||||||
|
lookback_days=bias_lookback_days,
|
||||||
|
min_samples=bias_min_samples,
|
||||||
|
).apply(city_name, raw_prediction)
|
||||||
|
bias_adjustment = float(corrected.get("bias_adjustment") or 0.0)
|
||||||
|
bias_samples = int(corrected.get("samples") or 0)
|
||||||
|
if bias_samples <= 0:
|
||||||
|
return {
|
||||||
|
"prediction": raw_prediction,
|
||||||
|
"raw_prediction": raw_prediction,
|
||||||
|
"version": DEB_RAW_VERSION,
|
||||||
|
"weights_info": weights_info,
|
||||||
|
"bias_adjustment": 0.0,
|
||||||
|
"bias_samples": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
next_weights_info = weights_info
|
||||||
|
if abs(bias_adjustment) >= 0.05:
|
||||||
|
next_weights_info = (
|
||||||
|
f"{weights_info or 'DEB'} | "
|
||||||
|
f"recent_bias({bias_adjustment:+.1f},n={bias_samples})"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"prediction": corrected["corrected_prediction"],
|
||||||
|
"raw_prediction": corrected["raw_prediction"],
|
||||||
|
"version": DEB_RECENT_BIAS_CORRECTED_VERSION,
|
||||||
|
"weights_info": next_weights_info,
|
||||||
|
"bias_adjustment": bias_adjustment,
|
||||||
|
"bias_samples": bias_samples,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_deb_accuracy(city_name):
|
def get_deb_accuracy(city_name):
|
||||||
"""
|
"""
|
||||||
计算 DEB 融合预测的历史准确率
|
计算 DEB 融合预测的历史准确率
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import statistics
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
from src.analysis.settlement_rounding import apply_city_settlement
|
||||||
|
|
||||||
|
DEB_RAW_VERSION = "deb_v1_raw"
|
||||||
|
DEB_RECENT_BIAS_CORRECTED_VERSION = "deb_v1_recent_bias_corrected"
|
||||||
|
DEB_BACKTEST_SCHEMA_VERSION = "deb_backtest_report.v1"
|
||||||
|
|
||||||
|
|
||||||
|
def _sf(value: Any) -> float | None:
|
||||||
|
try:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
parsed = float(value)
|
||||||
|
return parsed if math.isfinite(parsed) else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _round3(value: float | None) -> float | None:
|
||||||
|
return None if value is None else round(float(value), 3)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_record(row: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
city = str(row.get("city") or "").strip().lower()
|
||||||
|
target_date = str(row.get("target_date") or row.get("date") or "").strip()
|
||||||
|
prediction = _sf(row.get("prediction", row.get("deb_prediction")))
|
||||||
|
actual = _sf(row.get("actual", row.get("actual_high")))
|
||||||
|
if not city or not target_date or prediction is None or actual is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"city": city,
|
||||||
|
"target_date": target_date,
|
||||||
|
"prediction": prediction,
|
||||||
|
"actual": actual,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_prediction_records(
|
||||||
|
records: Iterable[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
version: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
rows = [row for record in records if (row := _normalise_record(record))]
|
||||||
|
if not rows:
|
||||||
|
return {
|
||||||
|
"version": version,
|
||||||
|
"samples": 0,
|
||||||
|
"mae": None,
|
||||||
|
"rmse": None,
|
||||||
|
"bias": None,
|
||||||
|
"bucket_hit_rate": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
signed_errors = [row["prediction"] - row["actual"] for row in rows]
|
||||||
|
abs_errors = [abs(error) for error in signed_errors]
|
||||||
|
sq_errors = [error * error for error in signed_errors]
|
||||||
|
bucket_hits = 0
|
||||||
|
bucket_total = 0
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
pred_bucket = apply_city_settlement(row["city"], row["prediction"])
|
||||||
|
actual_bucket = apply_city_settlement(row["city"], row["actual"])
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if pred_bucket is None or actual_bucket is None:
|
||||||
|
continue
|
||||||
|
bucket_total += 1
|
||||||
|
if pred_bucket == actual_bucket:
|
||||||
|
bucket_hits += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": version,
|
||||||
|
"samples": len(rows),
|
||||||
|
"mae": _round3(statistics.mean(abs_errors)),
|
||||||
|
"rmse": _round3(math.sqrt(statistics.mean(sq_errors))),
|
||||||
|
"bias": _round3(statistics.mean(signed_errors)),
|
||||||
|
"bucket_hit_rate": (
|
||||||
|
_round3(bucket_hits / bucket_total) if bucket_total else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BiasCorrectionResult:
|
||||||
|
version: str
|
||||||
|
raw_prediction: float
|
||||||
|
corrected_prediction: float
|
||||||
|
bias_adjustment: float
|
||||||
|
samples: int
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"version": self.version,
|
||||||
|
"raw_prediction": self.raw_prediction,
|
||||||
|
"corrected_prediction": self.corrected_prediction,
|
||||||
|
"bias_adjustment": self.bias_adjustment,
|
||||||
|
"samples": self.samples,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RecentBiasCorrector:
|
||||||
|
def __init__(self, bias_by_city: dict[str, tuple[float, int]]) -> None:
|
||||||
|
self._bias_by_city = bias_by_city
|
||||||
|
|
||||||
|
def apply(self, city: str, raw_prediction: float) -> dict[str, Any]:
|
||||||
|
city_key = str(city or "").strip().lower()
|
||||||
|
raw = float(raw_prediction)
|
||||||
|
bias, samples = self._bias_by_city.get(city_key, (0.0, 0))
|
||||||
|
adjustment = round(bias, 1)
|
||||||
|
return BiasCorrectionResult(
|
||||||
|
version=DEB_RECENT_BIAS_CORRECTED_VERSION,
|
||||||
|
raw_prediction=round(raw, 1),
|
||||||
|
corrected_prediction=round(raw + adjustment, 1),
|
||||||
|
bias_adjustment=adjustment,
|
||||||
|
samples=samples,
|
||||||
|
).to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def build_recent_bias_corrector(
|
||||||
|
history: Iterable[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
lookback_days: int = 30,
|
||||||
|
min_samples: int = 3,
|
||||||
|
shrinkage_samples: int = 5,
|
||||||
|
max_adjustment: float = 3.0,
|
||||||
|
) -> RecentBiasCorrector:
|
||||||
|
by_city: dict[str, list[dict[str, Any]]] = {}
|
||||||
|
for record in history:
|
||||||
|
row = _normalise_record(record)
|
||||||
|
if row is None:
|
||||||
|
continue
|
||||||
|
by_city.setdefault(row["city"], []).append(row)
|
||||||
|
|
||||||
|
bias_by_city: dict[str, tuple[float, int]] = {}
|
||||||
|
for city, rows in by_city.items():
|
||||||
|
rows.sort(key=lambda row: row["target_date"], reverse=True)
|
||||||
|
recent = rows[: max(int(lookback_days or 0), 1)]
|
||||||
|
signed_actual_minus_prediction = [
|
||||||
|
row["actual"] - row["prediction"] for row in recent
|
||||||
|
]
|
||||||
|
samples = len(signed_actual_minus_prediction)
|
||||||
|
if samples < min_samples:
|
||||||
|
continue
|
||||||
|
raw_bias = statistics.mean(signed_actual_minus_prediction)
|
||||||
|
shrink = min(1.0, samples / max(float(shrinkage_samples), 1.0))
|
||||||
|
adjusted = raw_bias * shrink
|
||||||
|
adjusted = max(-abs(max_adjustment), min(abs(max_adjustment), adjusted))
|
||||||
|
bias_by_city[city] = (adjusted, samples)
|
||||||
|
|
||||||
|
return RecentBiasCorrector(bias_by_city)
|
||||||
|
|
||||||
|
|
||||||
|
def backtest_deb_versions(
|
||||||
|
history: Iterable[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
train_lookback_days: int = 30,
|
||||||
|
min_train_samples: int = 2,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
rows = [row for record in history if (row := _normalise_record(record))]
|
||||||
|
rows.sort(key=lambda row: (row["city"], row["target_date"]))
|
||||||
|
|
||||||
|
report_rows: list[dict[str, Any]] = []
|
||||||
|
raw_eval_rows: list[dict[str, Any]] = []
|
||||||
|
corrected_eval_rows: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
by_city: dict[str, list[dict[str, Any]]] = {}
|
||||||
|
for row in rows:
|
||||||
|
previous = by_city.setdefault(row["city"], [])
|
||||||
|
if len(previous) >= min_train_samples:
|
||||||
|
corrector = build_recent_bias_corrector(
|
||||||
|
previous,
|
||||||
|
lookback_days=train_lookback_days,
|
||||||
|
min_samples=min_train_samples,
|
||||||
|
)
|
||||||
|
corrected = corrector.apply(row["city"], row["prediction"])
|
||||||
|
raw_prediction = round(row["prediction"], 1)
|
||||||
|
corrected_prediction = corrected["corrected_prediction"]
|
||||||
|
|
||||||
|
raw_eval_rows.append(
|
||||||
|
{
|
||||||
|
"city": row["city"],
|
||||||
|
"target_date": row["target_date"],
|
||||||
|
"prediction": raw_prediction,
|
||||||
|
"actual": row["actual"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
corrected_eval_rows.append(
|
||||||
|
{
|
||||||
|
"city": row["city"],
|
||||||
|
"target_date": row["target_date"],
|
||||||
|
"prediction": corrected_prediction,
|
||||||
|
"actual": row["actual"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report_rows.append(
|
||||||
|
{
|
||||||
|
"city": row["city"],
|
||||||
|
"target_date": row["target_date"],
|
||||||
|
"actual": row["actual"],
|
||||||
|
"versions": {
|
||||||
|
DEB_RAW_VERSION: {
|
||||||
|
"prediction": raw_prediction,
|
||||||
|
"error": round(raw_prediction - row["actual"], 3),
|
||||||
|
},
|
||||||
|
DEB_RECENT_BIAS_CORRECTED_VERSION: {
|
||||||
|
"prediction": corrected_prediction,
|
||||||
|
"error": round(corrected_prediction - row["actual"], 3),
|
||||||
|
"bias_adjustment": corrected["bias_adjustment"],
|
||||||
|
"train_samples": corrected["samples"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
previous.append(row)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"schema_version": DEB_BACKTEST_SCHEMA_VERSION,
|
||||||
|
"versions": {
|
||||||
|
DEB_RAW_VERSION: evaluate_prediction_records(
|
||||||
|
raw_eval_rows,
|
||||||
|
version=DEB_RAW_VERSION,
|
||||||
|
),
|
||||||
|
DEB_RECENT_BIAS_CORRECTED_VERSION: evaluate_prediction_records(
|
||||||
|
corrected_eval_rows,
|
||||||
|
version=DEB_RECENT_BIAS_CORRECTED_VERSION,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"rows": report_rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_daily_records(
|
||||||
|
daily_records: dict[str, dict[str, dict[str, Any]]],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for city, by_date in (daily_records or {}).items():
|
||||||
|
if not isinstance(by_date, dict):
|
||||||
|
continue
|
||||||
|
for target_date, record in by_date.items():
|
||||||
|
if not isinstance(record, dict):
|
||||||
|
continue
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"city": city,
|
||||||
|
"target_date": target_date,
|
||||||
|
"deb_prediction": record.get("deb_prediction"),
|
||||||
|
"actual_high": record.get("actual_high"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def write_backtest_report(
|
||||||
|
report: dict[str, Any],
|
||||||
|
*,
|
||||||
|
json_path: str | Path,
|
||||||
|
csv_path: str | Path | None = None,
|
||||||
|
) -> None:
|
||||||
|
json_target = Path(json_path)
|
||||||
|
json_target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
json_target.write_text(
|
||||||
|
json.dumps(report, ensure_ascii=False, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
if csv_path is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
csv_target = Path(csv_path)
|
||||||
|
csv_target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with csv_target.open("w", newline="", encoding="utf-8") as handle:
|
||||||
|
writer = csv.DictWriter(
|
||||||
|
handle,
|
||||||
|
fieldnames=[
|
||||||
|
"city",
|
||||||
|
"target_date",
|
||||||
|
"actual",
|
||||||
|
f"{DEB_RAW_VERSION}_prediction",
|
||||||
|
f"{DEB_RAW_VERSION}_error",
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_prediction",
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_error",
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_bias_adjustment",
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_train_samples",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
writer.writeheader()
|
||||||
|
for row in report.get("rows") or []:
|
||||||
|
versions = row.get("versions") or {}
|
||||||
|
raw = versions.get(DEB_RAW_VERSION) or {}
|
||||||
|
corrected = versions.get(DEB_RECENT_BIAS_CORRECTED_VERSION) or {}
|
||||||
|
writer.writerow(
|
||||||
|
{
|
||||||
|
"city": row.get("city"),
|
||||||
|
"target_date": row.get("target_date"),
|
||||||
|
"actual": row.get("actual"),
|
||||||
|
f"{DEB_RAW_VERSION}_prediction": raw.get("prediction"),
|
||||||
|
f"{DEB_RAW_VERSION}_error": raw.get("error"),
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_prediction": corrected.get(
|
||||||
|
"prediction"
|
||||||
|
),
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_error": corrected.get(
|
||||||
|
"error"
|
||||||
|
),
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_bias_adjustment": corrected.get(
|
||||||
|
"bias_adjustment"
|
||||||
|
),
|
||||||
|
f"{DEB_RECENT_BIAS_CORRECTED_VERSION}_train_samples": corrected.get(
|
||||||
|
"train_samples"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -11,6 +11,7 @@ from typing import List, Optional, Tuple, Dict, Any
|
|||||||
|
|
||||||
from src.analysis.deb_algorithm import (
|
from src.analysis.deb_algorithm import (
|
||||||
calculate_dynamic_weights,
|
calculate_dynamic_weights,
|
||||||
|
calculate_deb_prediction,
|
||||||
get_deb_accuracy,
|
get_deb_accuracy,
|
||||||
update_daily_record,
|
update_daily_record,
|
||||||
_is_excluded_model_name,
|
_is_excluded_model_name,
|
||||||
@@ -313,22 +314,32 @@ def analyze_weather_trend(
|
|||||||
|
|
||||||
# === DEB ===
|
# === DEB ===
|
||||||
deb_prediction = None
|
deb_prediction = None
|
||||||
|
deb_raw_prediction = None
|
||||||
|
deb_version = None
|
||||||
|
deb_bias_adjustment = 0.0
|
||||||
|
deb_bias_samples = 0
|
||||||
deb_weights = ""
|
deb_weights = ""
|
||||||
if city_name and current_forecasts:
|
if city_name and current_forecasts:
|
||||||
blended_high, weight_info = calculate_dynamic_weights(
|
deb_result = calculate_deb_prediction(
|
||||||
city_name, current_forecasts
|
city_name,
|
||||||
|
current_forecasts,
|
||||||
|
raw_calculator=calculate_dynamic_weights,
|
||||||
)
|
)
|
||||||
if blended_high is not None:
|
if deb_result.get("prediction") is not None:
|
||||||
deb_prediction = blended_high
|
deb_prediction = deb_result.get("prediction")
|
||||||
deb_weights = weight_info
|
deb_raw_prediction = deb_result.get("raw_prediction")
|
||||||
|
deb_version = deb_result.get("version")
|
||||||
|
deb_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||||
|
deb_bias_samples = deb_result.get("bias_samples") or 0
|
||||||
|
deb_weights = deb_result.get("weights_info") or ""
|
||||||
insights.insert(
|
insights.insert(
|
||||||
0,
|
0,
|
||||||
f"🧬 <b>DEB 融合预测</b>:<b>{blended_high}{temp_symbol}</b> ({weight_info})",
|
f"🧬 <b>DEB 融合预测</b>:<b>{deb_prediction}{temp_symbol}</b> ({deb_weights})",
|
||||||
)
|
)
|
||||||
ai_features.append(
|
ai_features.append(
|
||||||
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}。"
|
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {deb_prediction}{temp_symbol}。"
|
||||||
)
|
)
|
||||||
_deb_to_save = blended_high
|
_deb_to_save = deb_prediction
|
||||||
|
|
||||||
# === METAR trend ===
|
# === METAR trend ===
|
||||||
recent_temps = metar.get("recent_temps", [])
|
recent_temps = metar.get("recent_temps", [])
|
||||||
@@ -417,7 +428,7 @@ def analyze_weather_trend(
|
|||||||
# DEB blending uses the already-computed set of model forecasts
|
# DEB blending uses the already-computed set of model forecasts
|
||||||
if ai_features and "DEB系统已通过历史偏差矫正算出期待点是" in ai_features[0]:
|
if ai_features and "DEB系统已通过历史偏差矫正算出期待点是" in ai_features[0]:
|
||||||
ai_features[0] = (
|
ai_features[0] = (
|
||||||
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}。"
|
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {deb_prediction}{temp_symbol}。"
|
||||||
)
|
)
|
||||||
|
|
||||||
if trend_direction == "stagnant":
|
if trend_direction == "stagnant":
|
||||||
@@ -825,6 +836,10 @@ def analyze_weather_trend(
|
|||||||
"peak_status": peak_status,
|
"peak_status": peak_status,
|
||||||
"peak_hours": peak_hours,
|
"peak_hours": peak_hours,
|
||||||
"deb_prediction": deb_prediction,
|
"deb_prediction": deb_prediction,
|
||||||
|
"deb_raw_prediction": deb_raw_prediction,
|
||||||
|
"deb_version": deb_version,
|
||||||
|
"deb_bias_adjustment": deb_bias_adjustment,
|
||||||
|
"deb_bias_samples": deb_bias_samples,
|
||||||
"deb_weights": deb_weights,
|
"deb_weights": deb_weights,
|
||||||
"current_forecasts": current_forecasts,
|
"current_forecasts": current_forecasts,
|
||||||
"ens_data": ens_data,
|
"ens_data": ens_data,
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.analysis.deb_evaluation import (
|
||||||
|
DEB_RAW_VERSION,
|
||||||
|
DEB_RECENT_BIAS_CORRECTED_VERSION,
|
||||||
|
backtest_deb_versions,
|
||||||
|
build_recent_bias_corrector,
|
||||||
|
evaluate_prediction_records,
|
||||||
|
write_backtest_report,
|
||||||
|
)
|
||||||
|
from src.database.runtime_state import DailyRecordRepository, RuntimeStateDB
|
||||||
|
|
||||||
|
|
||||||
|
def test_deb_evaluation_reports_mae_rmse_bias_and_bucket_hits():
|
||||||
|
records = [
|
||||||
|
{"city": "ankara", "target_date": "2026-05-20", "prediction": 20.0, "actual": 21.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-21", "prediction": 22.0, "actual": 21.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-22", "prediction": 23.0, "actual": 23.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
metrics = evaluate_prediction_records(records, version=DEB_RAW_VERSION)
|
||||||
|
|
||||||
|
assert metrics["version"] == DEB_RAW_VERSION
|
||||||
|
assert metrics["samples"] == 3
|
||||||
|
assert metrics["mae"] == 0.667
|
||||||
|
assert metrics["rmse"] == 0.816
|
||||||
|
assert metrics["bias"] == 0.0
|
||||||
|
assert metrics["bucket_hit_rate"] == 0.333
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_bias_corrector_uses_signed_error_without_rewriting_raw_deb():
|
||||||
|
history = [
|
||||||
|
{"city": "ankara", "target_date": "2026-05-20", "deb_prediction": 20.0, "actual_high": 22.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-21", "deb_prediction": 21.0, "actual_high": 23.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-22", "deb_prediction": 24.0, "actual_high": 25.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
corrector = build_recent_bias_corrector(history, lookback_days=30, min_samples=2)
|
||||||
|
corrected = corrector.apply("ankara", raw_prediction=24.0)
|
||||||
|
|
||||||
|
assert corrected["version"] == DEB_RECENT_BIAS_CORRECTED_VERSION
|
||||||
|
assert corrected["raw_prediction"] == 24.0
|
||||||
|
assert corrected["corrected_prediction"] > corrected["raw_prediction"]
|
||||||
|
assert corrected["bias_adjustment"] == 1.0
|
||||||
|
assert corrected["samples"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_deb_versions_compares_raw_and_bias_corrected_versions():
|
||||||
|
history = [
|
||||||
|
{"city": "ankara", "target_date": "2026-05-20", "deb_prediction": 20.0, "actual_high": 22.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-21", "deb_prediction": 21.0, "actual_high": 23.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-22", "deb_prediction": 24.0, "actual_high": 25.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-23", "deb_prediction": 24.0, "actual_high": 26.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
report = backtest_deb_versions(history, train_lookback_days=30)
|
||||||
|
|
||||||
|
assert report["schema_version"] == "deb_backtest_report.v1"
|
||||||
|
assert report["versions"][DEB_RAW_VERSION]["samples"] == 2
|
||||||
|
assert report["versions"][DEB_RECENT_BIAS_CORRECTED_VERSION]["samples"] == 2
|
||||||
|
assert (
|
||||||
|
report["versions"][DEB_RECENT_BIAS_CORRECTED_VERSION]["mae"]
|
||||||
|
< report["versions"][DEB_RAW_VERSION]["mae"]
|
||||||
|
)
|
||||||
|
assert report["rows"][0]["versions"][DEB_RAW_VERSION]["prediction"] == 24.0
|
||||||
|
assert report["rows"][0]["versions"][DEB_RECENT_BIAS_CORRECTED_VERSION]["prediction"] == 24.8
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_backtest_report_persists_versioned_json_and_csv(tmp_path):
|
||||||
|
history = [
|
||||||
|
{"city": "ankara", "target_date": "2026-05-20", "deb_prediction": 20.0, "actual_high": 22.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-21", "deb_prediction": 21.0, "actual_high": 23.0},
|
||||||
|
{"city": "ankara", "target_date": "2026-05-22", "deb_prediction": 24.0, "actual_high": 25.0},
|
||||||
|
]
|
||||||
|
report = backtest_deb_versions(history)
|
||||||
|
json_path = tmp_path / "deb-backtest.json"
|
||||||
|
csv_path = tmp_path / "deb-backtest.csv"
|
||||||
|
|
||||||
|
write_backtest_report(report, json_path=json_path, csv_path=csv_path)
|
||||||
|
|
||||||
|
assert json_path.read_text(encoding="utf-8").startswith("{\n \"schema_version\": \"deb_backtest_report.v1\"")
|
||||||
|
csv_text = csv_path.read_text(encoding="utf-8")
|
||||||
|
assert "deb_v1_raw_prediction" in csv_text
|
||||||
|
assert "deb_v1_recent_bias_corrected_prediction" in csv_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_deb_versions_cli_reads_sqlite_and_writes_outputs(tmp_path):
|
||||||
|
db_path = tmp_path / "polyweather.db"
|
||||||
|
db = RuntimeStateDB(str(db_path))
|
||||||
|
repo = DailyRecordRepository(db)
|
||||||
|
repo.upsert_record("ankara", "2026-05-20", {"deb_prediction": 20.0, "actual_high": 22.0})
|
||||||
|
repo.upsert_record("ankara", "2026-05-21", {"deb_prediction": 21.0, "actual_high": 23.0})
|
||||||
|
repo.upsert_record("ankara", "2026-05-22", {"deb_prediction": 24.0, "actual_high": 25.0})
|
||||||
|
json_path = tmp_path / "report.json"
|
||||||
|
csv_path = tmp_path / "report.csv"
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(Path("scripts") / "backtest_deb_versions.py"),
|
||||||
|
"--db",
|
||||||
|
str(db_path),
|
||||||
|
"--output-json",
|
||||||
|
str(json_path),
|
||||||
|
"--output-csv",
|
||||||
|
str(csv_path),
|
||||||
|
],
|
||||||
|
cwd=Path(__file__).resolve().parents[1],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||||
|
assert payload["schema_version"] == "deb_backtest_report.v1"
|
||||||
|
assert payload["versions"][DEB_RAW_VERSION]["samples"] == 1
|
||||||
|
assert csv_path.exists()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from src.analysis.deb_algorithm import (
|
from src.analysis.deb_algorithm import (
|
||||||
_collapse_forecasts_for_deb,
|
_collapse_forecasts_for_deb,
|
||||||
|
calculate_deb_prediction,
|
||||||
calculate_dynamic_weights,
|
calculate_dynamic_weights,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,6 +92,42 @@ def test_deb_weighted_path_uses_deduped_family_values(monkeypatch):
|
|||||||
assert "家族去重" in info
|
assert "家族去重" in info
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_deb_prediction_keeps_raw_and_adds_versioned_bias_correction(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"src.analysis.deb_algorithm.load_history",
|
||||||
|
lambda _: {
|
||||||
|
"ankara": {
|
||||||
|
"2026-04-14": {
|
||||||
|
"actual_high": 22.0,
|
||||||
|
"deb_prediction": 20.0,
|
||||||
|
"forecasts": {"ECMWF": 20.0, "GFS": 20.0},
|
||||||
|
},
|
||||||
|
"2026-04-15": {
|
||||||
|
"actual_high": 23.0,
|
||||||
|
"deb_prediction": 21.0,
|
||||||
|
"forecasts": {"ECMWF": 21.0, "GFS": 21.0},
|
||||||
|
},
|
||||||
|
"2026-04-16": {
|
||||||
|
"actual_high": 25.0,
|
||||||
|
"deb_prediction": 24.0,
|
||||||
|
"forecasts": {"ECMWF": 24.0, "GFS": 24.0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = calculate_deb_prediction(
|
||||||
|
"ankara",
|
||||||
|
{"ECMWF": 24.0, "GFS": 24.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["raw_prediction"] == 24.0
|
||||||
|
assert result["prediction"] == 25.0
|
||||||
|
assert result["version"] == "deb_v1_recent_bias_corrected"
|
||||||
|
assert result["bias_adjustment"] == 1.0
|
||||||
|
assert result["bias_samples"] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_compute_hourly_model_errors_basic():
|
def test_compute_hourly_model_errors_basic():
|
||||||
from src.analysis.deb_algorithm import compute_hourly_model_errors
|
from src.analysis.deb_algorithm import compute_hourly_model_errors
|
||||||
|
|
||||||
|
|||||||
+67
-16
@@ -24,7 +24,7 @@ from web.core import (
|
|||||||
_sf,
|
_sf,
|
||||||
_weather,
|
_weather,
|
||||||
)
|
)
|
||||||
from src.analysis.deb_algorithm import calculate_dynamic_weights
|
from src.analysis.deb_algorithm import calculate_deb_prediction
|
||||||
from src.analysis.settlement_rounding import apply_city_settlement
|
from src.analysis.settlement_rounding import apply_city_settlement
|
||||||
from src.data_collection.country_networks import build_country_network_snapshot
|
from src.data_collection.country_networks import build_country_network_snapshot
|
||||||
from src.data_collection.city_registry import ALIASES, CITY_REGISTRY
|
from src.data_collection.city_registry import ALIASES, CITY_REGISTRY
|
||||||
@@ -1082,11 +1082,18 @@ def _analyze(
|
|||||||
|
|
||||||
# ── 6. DEB fusion ──
|
# ── 6. DEB fusion ──
|
||||||
deb_val, deb_weights = None, ""
|
deb_val, deb_weights = None, ""
|
||||||
|
deb_raw_val, deb_version = None, None
|
||||||
|
deb_bias_adjustment, deb_bias_samples = 0.0, 0
|
||||||
|
deb_intraday_adjustment = 0.0
|
||||||
if current_forecasts:
|
if current_forecasts:
|
||||||
blended, winfo = calculate_dynamic_weights(city, current_forecasts)
|
deb_result = calculate_deb_prediction(city, current_forecasts)
|
||||||
if blended is not None:
|
if deb_result.get("prediction") is not None:
|
||||||
deb_val = blended
|
deb_val = deb_result.get("prediction")
|
||||||
deb_weights = winfo
|
deb_raw_val = deb_result.get("raw_prediction")
|
||||||
|
deb_version = deb_result.get("version")
|
||||||
|
deb_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||||
|
deb_bias_samples = deb_result.get("bias_samples") or 0
|
||||||
|
deb_weights = deb_result.get("weights_info") or ""
|
||||||
|
|
||||||
# ── 7. Ensemble stats ──
|
# ── 7. Ensemble stats ──
|
||||||
ens_data = {
|
ens_data = {
|
||||||
@@ -1230,6 +1237,10 @@ def _analyze(
|
|||||||
# Use shared DEB if not already set
|
# Use shared DEB if not already set
|
||||||
if deb_val is None and sd.get("deb_prediction") is not None:
|
if deb_val is None and sd.get("deb_prediction") is not None:
|
||||||
deb_val = sd["deb_prediction"]
|
deb_val = sd["deb_prediction"]
|
||||||
|
deb_raw_val = sd.get("deb_raw_prediction") or deb_val
|
||||||
|
deb_version = sd.get("deb_version")
|
||||||
|
deb_bias_adjustment = sd.get("deb_bias_adjustment") or 0.0
|
||||||
|
deb_bias_samples = sd.get("deb_bias_samples") or 0
|
||||||
deb_weights = sd.get("deb_weights", "")
|
deb_weights = sd.get("deb_weights", "")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1288,10 +1299,11 @@ def _analyze(
|
|||||||
max_correction_clamped = max(-max_correction, min(max_correction, max_so_far_excess * max(0.3, weight)))
|
max_correction_clamped = max(-max_correction, min(max_correction, max_so_far_excess * max(0.3, weight)))
|
||||||
|
|
||||||
blended_correction = hourly_correction * 0.6 + max_correction_clamped * 0.4
|
blended_correction = hourly_correction * 0.6 + max_correction_clamped * 0.4
|
||||||
|
deb_intraday_adjustment = round(blended_correction, 1)
|
||||||
deb_val = round(deb_val + blended_correction, 1)
|
deb_val = round(deb_val + blended_correction, 1)
|
||||||
if mu is not None:
|
if mu is not None:
|
||||||
mu = round(mu + blended_correction, 1)
|
mu = round(mu + blended_correction, 1)
|
||||||
deb_weights = f"{deb_weights or 'DEB'} + intraday_bias({blended_correction:+.1f})"
|
deb_weights = f"{deb_weights or 'DEB'} + intraday_bias({deb_intraday_adjustment:+.1f})"
|
||||||
|
|
||||||
# ── 12b. Next 48h hourly block for future-date analysis modal ──
|
# ── 12b. Next 48h hourly block for future-date analysis modal ──
|
||||||
next_48h_hourly = {
|
next_48h_hourly = {
|
||||||
@@ -1490,6 +1502,10 @@ def _analyze(
|
|||||||
if i == 0:
|
if i == 0:
|
||||||
day_m = current_forecasts.copy()
|
day_m = current_forecasts.copy()
|
||||||
d_val, d_winfo = deb_val, deb_weights
|
d_val, d_winfo = deb_val, deb_weights
|
||||||
|
d_raw_val = deb_raw_val
|
||||||
|
d_version = deb_version
|
||||||
|
d_bias_adjustment = deb_bias_adjustment
|
||||||
|
d_bias_samples = deb_bias_samples
|
||||||
else:
|
else:
|
||||||
day_m = mm_daily_raw.get(d_str, {}).copy()
|
day_m = mm_daily_raw.get(d_str, {}).copy()
|
||||||
if i < len(maxtemps) and maxtemps[i] is not None:
|
if i < len(maxtemps) and maxtemps[i] is not None:
|
||||||
@@ -1505,14 +1521,20 @@ def _analyze(
|
|||||||
}
|
}
|
||||||
|
|
||||||
d_val, d_winfo = None, ""
|
d_val, d_winfo = None, ""
|
||||||
|
d_raw_val, d_version = None, None
|
||||||
|
d_bias_adjustment, d_bias_samples = 0.0, 0
|
||||||
d_probs = []
|
d_probs = []
|
||||||
d_probs_all = []
|
d_probs_all = []
|
||||||
if day_m:
|
if day_m:
|
||||||
try:
|
try:
|
||||||
blended, winfo = calculate_dynamic_weights(city, day_m)
|
deb_result = calculate_deb_prediction(city, day_m)
|
||||||
if blended is not None:
|
if deb_result.get("prediction") is not None:
|
||||||
d_val = blended
|
d_val = deb_result.get("prediction")
|
||||||
d_winfo = winfo
|
d_raw_val = deb_result.get("raw_prediction")
|
||||||
|
d_version = deb_result.get("version")
|
||||||
|
d_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||||
|
d_bias_samples = deb_result.get("bias_samples") or 0
|
||||||
|
d_winfo = deb_result.get("weights_info") or ""
|
||||||
|
|
||||||
# Calculate future probability based on model divergence
|
# Calculate future probability based on model divergence
|
||||||
m_vals = [v for v in day_m.values() if v is not None]
|
m_vals = [v for v in day_m.values() if v is not None]
|
||||||
@@ -1532,7 +1554,14 @@ def _analyze(
|
|||||||
if day_m:
|
if day_m:
|
||||||
multi_model_daily[d_str] = {
|
multi_model_daily[d_str] = {
|
||||||
"models": day_m,
|
"models": day_m,
|
||||||
"deb": {"prediction": d_val, "weights_info": d_winfo},
|
"deb": {
|
||||||
|
"prediction": d_val,
|
||||||
|
"raw_prediction": d_raw_val,
|
||||||
|
"version": d_version,
|
||||||
|
"weights_info": d_winfo,
|
||||||
|
"bias_adjustment": d_bias_adjustment,
|
||||||
|
"bias_samples": d_bias_samples,
|
||||||
|
},
|
||||||
"probabilities": d_probs if i > 0 else probabilities, # Use today's real prob for today
|
"probabilities": d_probs if i > 0 else probabilities, # Use today's real prob for today
|
||||||
"probabilities_all": d_probs_all if i > 0 else probabilities_all,
|
"probabilities_all": d_probs_all if i > 0 else probabilities_all,
|
||||||
}
|
}
|
||||||
@@ -1689,7 +1718,15 @@ def _analyze(
|
|||||||
"forecasts": {k: v for k, v in current_forecasts.items() if v is not None},
|
"forecasts": {k: v for k, v in current_forecasts.items() if v is not None},
|
||||||
},
|
},
|
||||||
"multi_model_daily": multi_model_daily,
|
"multi_model_daily": multi_model_daily,
|
||||||
"deb": {"prediction": deb_val, "weights_info": deb_weights},
|
"deb": {
|
||||||
|
"prediction": deb_val,
|
||||||
|
"raw_prediction": deb_raw_val,
|
||||||
|
"version": deb_version,
|
||||||
|
"weights_info": deb_weights,
|
||||||
|
"bias_adjustment": deb_bias_adjustment,
|
||||||
|
"bias_samples": deb_bias_samples,
|
||||||
|
"intraday_adjustment": deb_intraday_adjustment,
|
||||||
|
},
|
||||||
"deviation_monitor": deviation_monitor,
|
"deviation_monitor": deviation_monitor,
|
||||||
"ensemble": ens_data,
|
"ensemble": ens_data,
|
||||||
"probabilities": {
|
"probabilities": {
|
||||||
@@ -1998,10 +2035,18 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
deb_val = None
|
deb_val = None
|
||||||
|
deb_raw_val = None
|
||||||
|
deb_version = None
|
||||||
|
deb_bias_adjustment = 0.0
|
||||||
|
deb_bias_samples = 0
|
||||||
if current_forecasts:
|
if current_forecasts:
|
||||||
blended, _weights_info = calculate_dynamic_weights(city, current_forecasts)
|
deb_result = calculate_deb_prediction(city, current_forecasts)
|
||||||
if blended is not None:
|
if deb_result.get("prediction") is not None:
|
||||||
deb_val = blended
|
deb_val = deb_result.get("prediction")
|
||||||
|
deb_raw_val = deb_result.get("raw_prediction")
|
||||||
|
deb_version = deb_result.get("version")
|
||||||
|
deb_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||||
|
deb_bias_samples = deb_result.get("bias_samples") or 0
|
||||||
if deb_val is None:
|
if deb_val is None:
|
||||||
deb_val = om_today
|
deb_val = om_today
|
||||||
|
|
||||||
@@ -2071,7 +2116,13 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
"obs_age_min": obs_age_min,
|
"obs_age_min": obs_age_min,
|
||||||
"observation_status": "live" if cur_temp is not None else "missing",
|
"observation_status": "live" if cur_temp is not None else "missing",
|
||||||
},
|
},
|
||||||
"deb": {"prediction": _sf(deb_val)},
|
"deb": {
|
||||||
|
"prediction": _sf(deb_val),
|
||||||
|
"raw_prediction": _sf(deb_raw_val),
|
||||||
|
"version": deb_version,
|
||||||
|
"bias_adjustment": deb_bias_adjustment,
|
||||||
|
"bias_samples": deb_bias_samples,
|
||||||
|
},
|
||||||
"deviation_monitor": deviation_monitor or {},
|
"deviation_monitor": deviation_monitor or {},
|
||||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user