bitsofchris Claude Opus 4.7 (1M context) commited on
Commit
4cfd118
·
1 Parent(s): 57e3799

Overlay past Toto and NWS forecasts on the historical chart

Browse files

Every refresh already logs each forecast (p10/p50/p90) keyed by both
forecast_made_at and target_ts. Use that history to plot, for each past
hour, the most-recent forecast that was issued *before* that hour — for
both Toto and NWS — alongside the Ecowitt actuals on every metric panel.

- src/forecast_log.py: historical_predictions(source, metric, since_unix)
joins the forecast table back onto itself per (source, target_ts) to
pick the latest pre-target snapshot, returns a tidy ts-indexed DF.
- app.py: build past_toto / past_nws dicts per metric over the visible
history window, pass them into combined_figure.
- src/weather_ui.py: render two new dimmer overlay traces — a solid
semi-transparent blue line for past Toto p50, dashed semi-transparent
red for past NWS — so the viewer can see at a glance how each model
has been doing on this exact station.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +22 -1
  2. src/forecast_log.py +39 -0
  3. src/weather_ui.py +26 -0
app.py CHANGED
@@ -186,12 +186,33 @@ def refresh(cycle_label: str = "Hourly", horizon_label: str = "24 h"):
186
  forecast_log.record_nws(log_conn, m["col"], ns)
187
 
188
  now = pd.Timestamp.now(tz="UTC").floor("h")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  fig = combined_figure(
190
- history=history.tail(int(hist_days * 24 / step_hours)),
191
  totos=totos,
192
  nws_df=nws_future,
193
  metrics=METRICS,
194
  now=now,
 
 
195
  )
196
 
197
  hero = hero_markdown(PLACE_NAME, history, nws_first, DISPLAY_TZ, realtime=realtime)
 
186
  forecast_log.record_nws(log_conn, m["col"], ns)
187
 
188
  now = pd.Timestamp.now(tz="UTC").floor("h")
189
+
190
+ # Look back: for each metric, build a series of past predictions issued
191
+ # for actual hours that have already passed. Lets us overlay what each
192
+ # model thought vs what happened.
193
+ visible_history = history.tail(int(hist_days * 24 / step_hours))
194
+ since_unix = (
195
+ int(visible_history.index.min().timestamp()) if not visible_history.empty else None
196
+ )
197
+ past_toto: dict[str, pd.DataFrame] = {}
198
+ past_nws: dict[str, pd.DataFrame] = {}
199
+ for m in METRICS:
200
+ col = m["col"]
201
+ pt = forecast_log.historical_predictions(log_conn, "toto", col, since_unix=since_unix)
202
+ if not pt.empty:
203
+ past_toto[col] = pt
204
+ pn = forecast_log.historical_predictions(log_conn, "nws", col, since_unix=since_unix)
205
+ if not pn.empty:
206
+ past_nws[col] = pn
207
+
208
  fig = combined_figure(
209
+ history=visible_history,
210
  totos=totos,
211
  nws_df=nws_future,
212
  metrics=METRICS,
213
  now=now,
214
+ past_toto=past_toto,
215
+ past_nws=past_nws,
216
  )
217
 
218
  hero = hero_markdown(PLACE_NAME, history, nws_first, DISPLAY_TZ, realtime=realtime)
src/forecast_log.py CHANGED
@@ -160,6 +160,45 @@ def scoreboard(
160
  return df
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  def scoreboard_summary(
164
  conn: sqlite3.Connection,
165
  metric: str = "temp_f",
 
160
  return df
161
 
162
 
163
+ def historical_predictions(
164
+ conn: sqlite3.Connection,
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 >= ?"
179
+ params.append(since_unix)
180
+ sql = f"""
181
+ WITH latest AS (
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
190
+ FROM forecast_snapshots f
191
+ JOIN latest l USING (source, target_ts, metric, forecast_made_at)
192
+ ORDER BY f.target_ts
193
+ """
194
+ df = pd.read_sql_query(sql, conn, params=params)
195
+ if df.empty:
196
+ return df
197
+ df.index = pd.to_datetime(df["target_ts"], unit="s", utc=True)
198
+ df = df.drop(columns=["target_ts"])
199
+ return df
200
+
201
+
202
  def scoreboard_summary(
203
  conn: sqlite3.Connection,
204
  metric: str = "temp_f",
src/weather_ui.py CHANGED
@@ -187,6 +187,8 @@ 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 +214,30 @@ 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
+ past_nws: dict[str, pd.DataFrame] | None = None,
192
  ) -> go.Figure:
193
  """Three stacked subplots sharing the x-axis."""
194
  fig = make_subplots(
 
214
  ),
215
  row=i, col=1,
216
  )
217
+ # Past Toto forecasts vs the same hours' actuals.
218
+ if past_toto and col in past_toto:
219
+ pt = past_toto[col]
220
+ fig.add_trace(
221
+ go.Scatter(
222
+ x=pt.index, y=pt["p50"].values,
223
+ name="🤖 Toto (past forecasts)", mode="lines",
224
+ line=dict(color="rgba(31,119,180,0.55)", width=1.5),
225
+ showlegend=showlegend, legendgroup="toto-past",
226
+ ),
227
+ row=i, col=1,
228
+ )
229
+ # Past NWS forecasts vs the same hours' actuals.
230
+ if past_nws and col in past_nws:
231
+ pn = past_nws[col]
232
+ fig.add_trace(
233
+ go.Scatter(
234
+ x=pn.index, y=pn["p50"].values,
235
+ name="🌎 NWS (past forecasts)", mode="lines",
236
+ line=dict(color="rgba(214,39,40,0.55)", width=1.5, dash="dash"),
237
+ showlegend=showlegend, legendgroup="nws-past",
238
+ ),
239
+ row=i, col=1,
240
+ )
241
  if toto is not None:
242
  fig.add_trace(
243
  go.Scatter(