Commit ·
7cc37a1
1
Parent(s): 071f1ee
Past-Toto overlay: re-enable, capped at last Ecowitt actual
Browse filesBring the translucent Toto p50 overlay back on the historical side of
the chart but strictly cap target_ts <= last_actual_ts so it can't bleed
into the forecast window.
historical_predictions() now takes an explicit until_unix parameter
(default = now) and includes it in the WHERE clause.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app.py +19 -0
- src/forecast_log.py +13 -7
- src/weather_ui.py +13 -0
app.py
CHANGED
|
@@ -180,12 +180,31 @@ def _build_view(view: dict, log_conn, log_to_scoreboard: bool) -> dict:
|
|
| 180 |
visible_steps = int(round(hours / step_hours))
|
| 181 |
visible_history = history.tail(visible_steps)
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
fig = combined_figure(
|
| 184 |
history=visible_history,
|
| 185 |
totos=totos,
|
| 186 |
nws_df=nws_future,
|
| 187 |
metrics=METRICS,
|
| 188 |
now=now,
|
|
|
|
| 189 |
)
|
| 190 |
return {
|
| 191 |
"fig": fig,
|
|
|
|
| 180 |
visible_steps = int(round(hours / step_hours))
|
| 181 |
visible_history = history.tail(visible_steps)
|
| 182 |
|
| 183 |
+
# Past Toto forecasts: for each past hour visible on the chart, the
|
| 184 |
+
# most-recent forecast we issued *before* that hour. Strictly capped at
|
| 185 |
+
# the most recent Ecowitt actual so the overlay never bleeds into the
|
| 186 |
+
# future portion of the chart.
|
| 187 |
+
since_unix = (
|
| 188 |
+
int(visible_history.index.min().timestamp()) if not visible_history.empty else None
|
| 189 |
+
)
|
| 190 |
+
until_unix = int(last_actual.timestamp()) if last_actual is not None else None
|
| 191 |
+
past_toto: dict[str, pd.DataFrame] = {}
|
| 192 |
+
for m in METRICS:
|
| 193 |
+
col = m["col"]
|
| 194 |
+
pt = forecast_log.historical_predictions(
|
| 195 |
+
log_conn, "toto", col,
|
| 196 |
+
since_unix=since_unix, until_unix=until_unix,
|
| 197 |
+
)
|
| 198 |
+
if not pt.empty:
|
| 199 |
+
past_toto[col] = pt
|
| 200 |
+
|
| 201 |
fig = combined_figure(
|
| 202 |
history=visible_history,
|
| 203 |
totos=totos,
|
| 204 |
nws_df=nws_future,
|
| 205 |
metrics=METRICS,
|
| 206 |
now=now,
|
| 207 |
+
past_toto=past_toto,
|
| 208 |
)
|
| 209 |
return {
|
| 210 |
"fig": fig,
|
src/forecast_log.py
CHANGED
|
@@ -165,14 +165,18 @@ def historical_predictions(
|
|
| 165 |
source: str,
|
| 166 |
metric: str,
|
| 167 |
since_unix: int | None = None,
|
|
|
|
| 168 |
) -> pd.DataFrame:
|
| 169 |
-
"""For each
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
timestamp with columns p10, p50, p90 (NWS rows have NULL p10/p90).
|
| 174 |
"""
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
| 176 |
where_extra = ""
|
| 177 |
if since_unix is not None:
|
| 178 |
where_extra = " AND target_ts >= ?"
|
|
@@ -182,8 +186,10 @@ def historical_predictions(
|
|
| 182 |
SELECT source, target_ts, metric,
|
| 183 |
MAX(forecast_made_at) AS forecast_made_at
|
| 184 |
FROM forecast_snapshots
|
| 185 |
-
WHERE source = ? AND metric = ?
|
| 186 |
-
|
|
|
|
|
|
|
| 187 |
GROUP BY source, target_ts, metric
|
| 188 |
)
|
| 189 |
SELECT f.target_ts, f.p10, f.p50, f.p90
|
|
|
|
| 165 |
source: str,
|
| 166 |
metric: str,
|
| 167 |
since_unix: int | None = None,
|
| 168 |
+
until_unix: int | None = None,
|
| 169 |
) -> pd.DataFrame:
|
| 170 |
+
"""For each target_ts in [since, until], return the most-recent forecast
|
| 171 |
+
issued *before* that hour.
|
| 172 |
|
| 173 |
+
`until_unix` defaults to now — pass it to cap the overlay so it doesn't
|
| 174 |
+
bleed into the future portion of the chart.
|
|
|
|
| 175 |
"""
|
| 176 |
+
import time as _time # noqa: PLC0415
|
| 177 |
+
if until_unix is None:
|
| 178 |
+
until_unix = int(_time.time())
|
| 179 |
+
params: list = [source, metric, until_unix]
|
| 180 |
where_extra = ""
|
| 181 |
if since_unix is not None:
|
| 182 |
where_extra = " AND target_ts >= ?"
|
|
|
|
| 186 |
SELECT source, target_ts, metric,
|
| 187 |
MAX(forecast_made_at) AS forecast_made_at
|
| 188 |
FROM forecast_snapshots
|
| 189 |
+
WHERE source = ? AND metric = ?
|
| 190 |
+
AND forecast_made_at <= target_ts
|
| 191 |
+
AND target_ts <= ?
|
| 192 |
+
{where_extra}
|
| 193 |
GROUP BY source, target_ts, metric
|
| 194 |
)
|
| 195 |
SELECT f.target_ts, f.p10, f.p50, f.p90
|
src/weather_ui.py
CHANGED
|
@@ -187,6 +187,7 @@ def combined_figure(
|
|
| 187 |
nws_df: pd.DataFrame | None,
|
| 188 |
metrics: list[dict],
|
| 189 |
now: pd.Timestamp | None = None,
|
|
|
|
| 190 |
) -> go.Figure:
|
| 191 |
"""Three stacked subplots sharing the x-axis."""
|
| 192 |
fig = make_subplots(
|
|
@@ -212,6 +213,18 @@ def combined_figure(
|
|
| 212 |
),
|
| 213 |
row=i, col=1,
|
| 214 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
if toto is not None:
|
| 216 |
fig.add_trace(
|
| 217 |
go.Scatter(
|
|
|
|
| 187 |
nws_df: pd.DataFrame | None,
|
| 188 |
metrics: list[dict],
|
| 189 |
now: pd.Timestamp | None = None,
|
| 190 |
+
past_toto: dict[str, pd.DataFrame] | None = None,
|
| 191 |
) -> go.Figure:
|
| 192 |
"""Three stacked subplots sharing the x-axis."""
|
| 193 |
fig = make_subplots(
|
|
|
|
| 213 |
),
|
| 214 |
row=i, col=1,
|
| 215 |
)
|
| 216 |
+
# Past Toto forecasts overlaid on actuals (historical side only).
|
| 217 |
+
if past_toto and col in past_toto:
|
| 218 |
+
pt = past_toto[col]
|
| 219 |
+
fig.add_trace(
|
| 220 |
+
go.Scatter(
|
| 221 |
+
x=pt.index, y=pt["p50"].values,
|
| 222 |
+
name="🤖 Toto (past forecasts)", mode="lines",
|
| 223 |
+
line=dict(color="rgba(31,119,180,0.55)", width=1.5),
|
| 224 |
+
showlegend=showlegend, legendgroup="toto-past",
|
| 225 |
+
),
|
| 226 |
+
row=i, col=1,
|
| 227 |
+
)
|
| 228 |
if toto is not None:
|
| 229 |
fig.add_trace(
|
| 230 |
go.Scatter(
|