mirror of
https://github.com/K-Dense-AI/claude-scientific-skills.git
synced 2026-03-27 07:09:27 +08:00
Create an all-out demonstration showing how TimesFM forecasts evolve as more historical data is added: - generate_animation_data.py: Runs 25 incremental forecasts (12→36 points) - interactive_forecast.html: Single-file HTML with Chart.js slider - Play/Pause animation control - Shows historical data, forecast, 80%/90% CIs, and actual future data - Live stats: forecast mean, max, min, CI width - generate_gif.py: Creates animated GIF for embedding in markdown - forecast_animation.gif: 25-frame animation (896 KB) Interactive features: - Slider to manually step through forecast evolution - Auto-play with 500ms per frame - Shows how each additional data point changes the forecast - Confidence intervals narrow as more data is added
183 lines
4.5 KiB
Python
183 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate animated GIF showing forecast evolution.
|
|
|
|
Creates a GIF animation showing how the TimesFM forecast changes
|
|
as more historical data points are added.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.dates as mdates
|
|
import numpy as np
|
|
import pandas as pd
|
|
from PIL import Image
|
|
|
|
# Configuration
|
|
EXAMPLE_DIR = Path(__file__).parent
|
|
DATA_FILE = EXAMPLE_DIR / "animation_data.json"
|
|
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."""
|
|
ax.clear()
|
|
|
|
# Parse dates
|
|
historical_dates = pd.to_datetime(step_data["historical_dates"])
|
|
forecast_dates = pd.to_datetime(step_data["forecast_dates"])
|
|
|
|
# Plot historical data
|
|
ax.plot(
|
|
historical_dates,
|
|
step_data["historical_values"],
|
|
color="#3b82f6",
|
|
linewidth=2,
|
|
marker="o",
|
|
markersize=4,
|
|
label="Historical",
|
|
)
|
|
|
|
# Plot 90% CI (outer)
|
|
ax.fill_between(
|
|
forecast_dates,
|
|
step_data["q10"],
|
|
step_data["q90"],
|
|
alpha=0.1,
|
|
color="#ef4444",
|
|
label="90% CI",
|
|
)
|
|
|
|
# Plot 80% CI (inner)
|
|
ax.fill_between(
|
|
forecast_dates,
|
|
step_data["q20"],
|
|
step_data["q80"],
|
|
alpha=0.2,
|
|
color="#ef4444",
|
|
label="80% CI",
|
|
)
|
|
|
|
# Plot forecast
|
|
ax.plot(
|
|
forecast_dates,
|
|
step_data["point_forecast"],
|
|
color="#ef4444",
|
|
linewidth=2,
|
|
marker="s",
|
|
markersize=4,
|
|
label="Forecast",
|
|
)
|
|
|
|
# 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
|
|
ax.axvline(
|
|
x=historical_dates[-1],
|
|
color="#6b7280",
|
|
linestyle="--",
|
|
linewidth=1,
|
|
alpha=0.5,
|
|
)
|
|
|
|
# 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']}",
|
|
fontsize=13,
|
|
fontweight="bold",
|
|
)
|
|
|
|
ax.grid(True, alpha=0.3)
|
|
ax.legend(loc="upper left", fontsize=9)
|
|
ax.set_ylim(0.5, 1.6)
|
|
|
|
# Format x-axis
|
|
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
|
|
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=6))
|
|
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right")
|
|
|
|
|
|
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}")
|
|
|
|
# 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)
|
|
|
|
# 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(
|
|
OUTPUT_FILE,
|
|
save_all=True,
|
|
append_images=frames[1:],
|
|
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")
|
|
print(f"\n✅ Done!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|