Commit Β·
071f1ee
1
Parent(s): cd68687
Drop zoom view, remove past-Toto overlay, dated x-axis
Browse files- app.py: remove VIEW_ZOOM and zoom_plot. Only the weekly view renders.
- app.py + src/weather_ui.py: stop building / passing past_toto into
combined_figure. The 'past forecasts' overlay used MAX(forecast_made_at)
per target hour, which sampled forecasts at wildly different lags
depending on autorefresh timing β that's where the sawtooth came from,
not from Toto. The scoreboard MAE is the honest single number; the
chart overlay was the wrong abstraction. (A fixed-horizon overlay is
a future option.)
- src/weather_ui.py: explicit x-axis tick format 'Month day\nhour AM/PM'
so dates are unambiguous on a 7-day-plus-72h chart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app.py +4 -31
- src/weather_ui.py +8 -15
app.py
CHANGED
|
@@ -30,18 +30,7 @@ CACHE_TTL_SECONDS = AUTO_REFRESH_SECONDS - 60 # so autorefresh always refetches
|
|
| 30 |
DISPLAY_TZ = os.environ.get("DISPLAY_TZ", "America/New_York")
|
| 31 |
PLACE_NAME = os.environ.get("PLACE_NAME", "Yaphank, NY")
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
VIEW_ZOOM = {
|
| 35 |
-
# 24h history is the longest window where Ecowitt reliably keeps 5-min
|
| 36 |
-
# data; beyond ~30h the cloud has already downsampled to 30-min and the
|
| 37 |
-
# zoom view stops being zoomed. 8h forecast keeps the chart ~75% history,
|
| 38 |
-
# ~25% future.
|
| 39 |
-
"label": "Last 24 h Β· 8 h forecast (5-min cadence)",
|
| 40 |
-
"cycle_type": "5min",
|
| 41 |
-
"resample": "5min",
|
| 42 |
-
"history_hours": 24,
|
| 43 |
-
"horizon_hours": 8,
|
| 44 |
-
}
|
| 45 |
VIEW_WEEK = {
|
| 46 |
"label": "Past 7 days Β· 72 h forecast (hourly cadence)",
|
| 47 |
"cycle_type": "30min",
|
|
@@ -190,15 +179,6 @@ def _build_view(view: dict, log_conn, log_to_scoreboard: bool) -> dict:
|
|
| 190 |
now = pd.Timestamp.now(tz="UTC").floor(resample)
|
| 191 |
visible_steps = int(round(hours / step_hours))
|
| 192 |
visible_history = history.tail(visible_steps)
|
| 193 |
-
since_unix = (
|
| 194 |
-
int(visible_history.index.min().timestamp()) if not visible_history.empty else None
|
| 195 |
-
)
|
| 196 |
-
past_toto: dict[str, pd.DataFrame] = {}
|
| 197 |
-
for m in METRICS:
|
| 198 |
-
col = m["col"]
|
| 199 |
-
pt = forecast_log.historical_predictions(log_conn, "toto", col, since_unix=since_unix)
|
| 200 |
-
if not pt.empty:
|
| 201 |
-
past_toto[col] = pt
|
| 202 |
|
| 203 |
fig = combined_figure(
|
| 204 |
history=visible_history,
|
|
@@ -206,7 +186,6 @@ def _build_view(view: dict, log_conn, log_to_scoreboard: bool) -> dict:
|
|
| 206 |
nws_df=nws_future,
|
| 207 |
metrics=METRICS,
|
| 208 |
now=now,
|
| 209 |
-
past_toto=past_toto,
|
| 210 |
)
|
| 211 |
return {
|
| 212 |
"fig": fig,
|
|
@@ -221,10 +200,7 @@ def refresh():
|
|
| 221 |
realtime = fetch_realtime_snapshot()
|
| 222 |
log_conn = forecast_log.connect()
|
| 223 |
|
| 224 |
-
# Weekly view is the canonical one logged to the scoreboard (hourly
|
| 225 |
-
# cadence keeps target_ts aligned with NWS hourly periods).
|
| 226 |
week = _build_view(VIEW_WEEK, log_conn, log_to_scoreboard=True)
|
| 227 |
-
zoom = _build_view(VIEW_ZOOM, log_conn, log_to_scoreboard=False)
|
| 228 |
|
| 229 |
# Hero uses the weekly history + the NWS period containing "now".
|
| 230 |
nws_df_raw = week["nws_df_raw"]
|
|
@@ -250,7 +226,7 @@ def refresh():
|
|
| 250 |
scoreboard = render_scoreboard(log_conn)
|
| 251 |
|
| 252 |
persist.push_db_async()
|
| 253 |
-
return hero, comparison_md,
|
| 254 |
|
| 255 |
|
| 256 |
# --- scoreboard ----------------------------------------------------------
|
|
@@ -370,10 +346,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
|
|
| 370 |
|
| 371 |
scoreboard_md = gr.Markdown()
|
| 372 |
|
| 373 |
-
gr.Markdown(f"###
|
| 374 |
-
zoom_plot = gr.Plot(label="Zoomed-in")
|
| 375 |
-
|
| 376 |
-
gr.Markdown(f"### π
Weekly view β {VIEW_WEEK['label']}")
|
| 377 |
week_plot = gr.Plot(label="Weekly")
|
| 378 |
|
| 379 |
with gr.Accordion("How the scoreboard is calculated", open=False):
|
|
@@ -418,7 +391,7 @@ with gr.Blocks(title="Toto Weather Forecast", theme=gr.themes.Soft()) as demo:
|
|
| 418 |
"Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
|
| 419 |
)
|
| 420 |
|
| 421 |
-
outputs = [hero_md, comparison_md,
|
| 422 |
demo.load(refresh, outputs=outputs)
|
| 423 |
|
| 424 |
|
|
|
|
| 30 |
DISPLAY_TZ = os.environ.get("DISPLAY_TZ", "America/New_York")
|
| 31 |
PLACE_NAME = os.environ.get("PLACE_NAME", "Yaphank, NY")
|
| 32 |
|
| 33 |
+
# Single canonical view β weekly, hourly cadence.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
VIEW_WEEK = {
|
| 35 |
"label": "Past 7 days Β· 72 h forecast (hourly cadence)",
|
| 36 |
"cycle_type": "30min",
|
|
|
|
| 179 |
now = pd.Timestamp.now(tz="UTC").floor(resample)
|
| 180 |
visible_steps = int(round(hours / step_hours))
|
| 181 |
visible_history = history.tail(visible_steps)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
fig = combined_figure(
|
| 184 |
history=visible_history,
|
|
|
|
| 186 |
nws_df=nws_future,
|
| 187 |
metrics=METRICS,
|
| 188 |
now=now,
|
|
|
|
| 189 |
)
|
| 190 |
return {
|
| 191 |
"fig": fig,
|
|
|
|
| 200 |
realtime = fetch_realtime_snapshot()
|
| 201 |
log_conn = forecast_log.connect()
|
| 202 |
|
|
|
|
|
|
|
| 203 |
week = _build_view(VIEW_WEEK, log_conn, log_to_scoreboard=True)
|
|
|
|
| 204 |
|
| 205 |
# Hero uses the weekly history + the NWS period containing "now".
|
| 206 |
nws_df_raw = week["nws_df_raw"]
|
|
|
|
| 226 |
scoreboard = render_scoreboard(log_conn)
|
| 227 |
|
| 228 |
persist.push_db_async()
|
| 229 |
+
return hero, comparison_md, week["fig"], scoreboard
|
| 230 |
|
| 231 |
|
| 232 |
# --- scoreboard ----------------------------------------------------------
|
|
|
|
| 346 |
|
| 347 |
scoreboard_md = gr.Markdown()
|
| 348 |
|
| 349 |
+
gr.Markdown(f"### π
{VIEW_WEEK['label']}")
|
|
|
|
|
|
|
|
|
|
| 350 |
week_plot = gr.Plot(label="Weekly")
|
| 351 |
|
| 352 |
with gr.Accordion("How the scoreboard is calculated", open=False):
|
|
|
|
| 391 |
"Full spec: [`docs/toto-inference.md`](https://huggingface.co/spaces/bitsofchris/time-series-ai-weather-forecast/blob/main/docs/toto-inference.md)."
|
| 392 |
)
|
| 393 |
|
| 394 |
+
outputs = [hero_md, comparison_md, week_plot, scoreboard_md]
|
| 395 |
demo.load(refresh, outputs=outputs)
|
| 396 |
|
| 397 |
|
src/weather_ui.py
CHANGED
|
@@ -187,8 +187,6 @@ def combined_figure(
|
|
| 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,18 +212,6 @@ def combined_figure(
|
|
| 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 |
if toto is not None:
|
| 230 |
fig.add_trace(
|
| 231 |
go.Scatter(
|
|
@@ -268,7 +254,14 @@ def combined_figure(
|
|
| 268 |
fig.update_layout(
|
| 269 |
height=900,
|
| 270 |
hovermode="x unified",
|
| 271 |
-
margin=dict(l=50, r=20, t=50, b=
|
| 272 |
legend=dict(orientation="h", yanchor="bottom", y=1.04, xanchor="right", x=1),
|
| 273 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
return fig
|
|
|
|
| 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 |
),
|
| 213 |
row=i, col=1,
|
| 214 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
if toto is not None:
|
| 216 |
fig.add_trace(
|
| 217 |
go.Scatter(
|
|
|
|
| 254 |
fig.update_layout(
|
| 255 |
height=900,
|
| 256 |
hovermode="x unified",
|
| 257 |
+
margin=dict(l=50, r=20, t=50, b=60),
|
| 258 |
legend=dict(orientation="h", yanchor="bottom", y=1.04, xanchor="right", x=1),
|
| 259 |
)
|
| 260 |
+
# Explicit date + hour on the x-axis so the reader doesn't have to guess
|
| 261 |
+
# what day a tick refers to.
|
| 262 |
+
fig.update_xaxes(
|
| 263 |
+
tickformat="%b %-d\n%-I %p",
|
| 264 |
+
ticklabelmode="instant",
|
| 265 |
+
showgrid=True,
|
| 266 |
+
)
|
| 267 |
return fig
|