diff --git a/scientific-skills/timesfm-forecasting/examples/global-temperature/forecast_animation.gif b/scientific-skills/timesfm-forecasting/examples/global-temperature/forecast_animation.gif index 070a39f..f2fcfe3 100644 Binary files a/scientific-skills/timesfm-forecasting/examples/global-temperature/forecast_animation.gif and b/scientific-skills/timesfm-forecasting/examples/global-temperature/forecast_animation.gif differ diff --git a/scientific-skills/timesfm-forecasting/examples/global-temperature/generate_gif.py b/scientific-skills/timesfm-forecasting/examples/global-temperature/generate_gif.py index d1310e2..d6ef828 100644 --- a/scientific-skills/timesfm-forecasting/examples/global-temperature/generate_gif.py +++ b/scientific-skills/timesfm-forecasting/examples/global-temperature/generate_gif.py @@ -3,9 +3,8 @@ Generate animated GIF showing forecast evolution. Creates a GIF animation showing how the TimesFM forecast changes -as more historical data points are added. +as more historical data points are added. Shows the full actual data as a background layer. """ - from __future__ import annotations import json @@ -24,107 +23,134 @@ OUTPUT_FILE = EXAMPLE_DIR / "forecast_animation.gif" DURATION_MS = 500 # Time per frame in milliseconds -def create_frame(ax, step_data: dict, actual_data: dict, total_steps: int) -> None: - """Create a single frame of the animation.""" +def create_frame( + ax, + step_data: dict, + actual_data: dict, + final_forecast: dict, + total_steps: int, + x_min, + x_max, + y_min, + y_max, +) -> None: + """Create a single frame of the animation with fixed axes.""" ax.clear() # Parse dates historical_dates = pd.to_datetime(step_data["historical_dates"]) forecast_dates = pd.to_datetime(step_data["forecast_dates"]) + + # Get final forecast dates for full extent + final_forecast_dates = pd.to_datetime(final_forecast["forecast_dates"]) + + # All actual dates for full background + all_actual_dates = pd.to_datetime(actual_data["dates"]) + all_actual_values = np.array(actual_data["values"]) - # Plot historical data + # ========== BACKGROUND LAYER: Full actual data (faded) ========== + ax.plot( + all_actual_dates, + all_actual_values, + color="#9ca3af", + linewidth=1, + marker="o", + markersize=2, + alpha=0.3, + label="All observed data", + zorder=1, + ) + + # ========== BACKGROUND LAYER: Final forecast (faded) ========== + ax.plot( + final_forecast_dates, + final_forecast["point_forecast"], + color="#fca5a5", + linewidth=1, + linestyle="--", + marker="s", + markersize=2, + alpha=0.3, + label="Final forecast", + zorder=2, + ) + + # ========== FOREGROUND LAYER: Historical data used (bright) ========== ax.plot( historical_dates, step_data["historical_values"], color="#3b82f6", - linewidth=2, + linewidth=2.5, marker="o", - markersize=4, - label="Historical", + markersize=5, + label="Data used", + zorder=10, ) - # Plot 90% CI (outer) + # ========== FOREGROUND LAYER: Current forecast (bright) ========== + # 90% CI (outer) ax.fill_between( forecast_dates, step_data["q10"], step_data["q90"], - alpha=0.1, + alpha=0.15, color="#ef4444", - label="90% CI", + zorder=5, ) - - # Plot 80% CI (inner) + + # 80% CI (inner) ax.fill_between( forecast_dates, step_data["q20"], step_data["q80"], - alpha=0.2, + alpha=0.25, color="#ef4444", - label="80% CI", + zorder=6, ) - - # Plot forecast + + # Forecast line ax.plot( forecast_dates, step_data["point_forecast"], color="#ef4444", - linewidth=2, + linewidth=2.5, marker="s", - markersize=4, + markersize=5, label="Forecast", + zorder=7, ) - # Plot actual future data if available - actual_dates = pd.to_datetime(actual_data["dates"]) - actual_values = actual_data["values"] - - # Find which actual points fall in forecast period - forecast_start = forecast_dates[0] - forecast_end = forecast_dates[-1] - - future_mask = (actual_dates >= forecast_start) & (actual_dates <= forecast_end) - future_dates = actual_dates[future_mask] - future_values = np.array(actual_values)[future_mask] - - if len(future_dates) > 0: - ax.plot( - future_dates, - future_values, - color="#10b981", - linewidth=1, - linestyle="--", - marker="o", - markersize=3, - alpha=0.7, - label="Actual (future)", - ) - - # Add vertical line at forecast boundary + # ========== Vertical line at forecast boundary ========== ax.axvline( x=historical_dates[-1], color="#6b7280", linestyle="--", - linewidth=1, - alpha=0.5, + linewidth=1.5, + alpha=0.7, + zorder=8, ) - # Formatting + # ========== Formatting ========== ax.set_xlabel("Date", fontsize=11) ax.set_ylabel("Temperature Anomaly (°C)", fontsize=11) ax.set_title( f"TimesFM Forecast Evolution\n" - f"Step {step_data['step']}/{total_steps}: {step_data['n_points']} points → {step_data['last_historical_date']}", + f"Step {step_data['step']}/{total_steps}: {step_data['n_points']} points → " + f"forecast from {step_data['last_historical_date']}", fontsize=13, fontweight="bold", ) - - ax.grid(True, alpha=0.3) - ax.legend(loc="upper left", fontsize=9) - ax.set_ylim(0.5, 1.6) - + + ax.grid(True, alpha=0.3, zorder=0) + ax.legend(loc="upper left", fontsize=8) + + # FIXED AXES - same for all frames + ax.set_xlim(x_min, x_max) + ax.set_ylim(y_min, y_max) + # Format x-axis ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) - ax.xaxis.set_major_locator(mdates.MonthLocator(interval=6)) + ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4)) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right") @@ -132,36 +158,76 @@ def main() -> None: print("=" * 60) print(" GENERATING ANIMATED GIF") print("=" * 60) - + # Load data with open(DATA_FILE) as f: data = json.load(f) - + total_steps = len(data["animation_steps"]) print(f"\n📊 Total frames: {total_steps}") - + + # Get the final forecast step for reference + final_forecast = data["animation_steps"][-1] + + # Calculate fixed axis extents from ALL data + all_actual_dates = pd.to_datetime(data["actual_data"]["dates"]) + all_actual_values = np.array(data["actual_data"]["values"]) + + final_forecast_dates = pd.to_datetime(final_forecast["forecast_dates"]) + final_forecast_values = np.array(final_forecast["point_forecast"]) + + # X-axis: from first actual date to last forecast date + x_min = all_actual_dates[0] + x_max = final_forecast_dates[-1] + + # Y-axis: min/max across all actual + all forecasts with CIs + all_forecast_q10 = np.array(final_forecast["q10"]) + all_forecast_q90 = np.array(final_forecast["q90"]) + + all_values = np.concatenate([ + all_actual_values, + final_forecast_values, + all_forecast_q10, + all_forecast_q90, + ]) + y_min = all_values.min() - 0.05 + y_max = all_values.max() + 0.05 + + print(f" X-axis: {x_min.strftime('%Y-%m')} to {x_max.strftime('%Y-%m')}") + print(f" Y-axis: {y_min:.2f}°C to {y_max:.2f}°C") + # Create figure fig, ax = plt.subplots(figsize=(12, 6)) - + # Generate frames frames = [] - + for i, step in enumerate(data["animation_steps"]): print(f" Frame {i + 1}/{total_steps}...") - - create_frame(ax, step, data["actual_data"], total_steps) - + + create_frame( + ax, + step, + data["actual_data"], + final_forecast, + total_steps, + x_min, + x_max, + y_min, + y_max, + ) + # Save frame to buffer fig.canvas.draw() - + # Convert to PIL Image buf = fig.canvas.buffer_rgba() width, height = fig.canvas.get_width_height() img = Image.frombytes("RGBA", (width, height), buf) frames.append(img.convert("RGB")) - + plt.close() - + # Save as GIF print(f"\n💾 Saving GIF: {OUTPUT_FILE}") frames[0].save( @@ -171,7 +237,7 @@ def main() -> None: duration=DURATION_MS, loop=0, # Loop forever ) - + # Get file size size_kb = OUTPUT_FILE.stat().st_size / 1024 print(f" File size: {size_kb:.1f} KB") diff --git a/scientific-skills/timesfm-forecasting/examples/global-temperature/interactive_forecast.html b/scientific-skills/timesfm-forecasting/examples/global-temperature/interactive_forecast.html index a94025f..56aede3 100644 --- a/scientific-skills/timesfm-forecasting/examples/global-temperature/interactive_forecast.html +++ b/scientific-skills/timesfm-forecasting/examples/global-temperature/interactive_forecast.html @@ -5,31 +5,20 @@ TimesFM Interactive Forecast Animation -
-

TimesFM Zero-Shot Forecast Animation

-

Watch the forecast evolve as more data is added

+

TimesFM Forecast Evolution

+

Watch the forecast evolve as more data is added — with full context always visible

@@ -246,13 +139,13 @@
- Historical Data Points + Data Points Used 12 / 36
- 2022-01 - 2022-12 → Forecast to 2023-12 + 2022-01 + Using data through 2022-12
@@ -266,6 +159,10 @@
Forecast Mean
0.86°C
+
+
vs Final Forecast
+
--
+
Forecast Max
--
@@ -274,47 +171,49 @@
Forecast Min
--
-
-
80% CI Width
-
--
-
+
+
+ All Observed Data +
+
+
+ Final Forecast (reference) +
- Historical Data + Data Used
- Forecast + Current Forecast
-
- 80% Confidence Interval -
-
-
- 90% Confidence Interval +
+ 80% CI