bitsofchris Claude Opus 4.7 (1M context) commited on
Commit
7cc37a1
·
1 Parent(s): 071f1ee

Past-Toto overlay: re-enable, capped at last Ecowitt actual

Browse files

Bring 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>

Files changed (3) hide show
  1. app.py +19 -0
  2. src/forecast_log.py +13 -7
  3. 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 past target_ts, return the most-recent forecast issued for it.
 
170
 
171
- Useful for overlaying 'what we predicted vs what actually happened' on the
172
- historical portion of the chart. Result is a DataFrame indexed by UTC
173
- timestamp with columns p10, p50, p90 (NWS rows have NULL p10/p90).
174
  """
175
- params: list = [source, metric]
 
 
 
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 = ? AND forecast_made_at <= target_ts
186
- {where_extra}
 
 
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(