feat(animation): extend forecasts to final date with dynamic horizon

- Each forecast now extends to 2025-12 regardless of historical data length
- Step 1 (12 points): forecasts 36 months ahead to 2025-12
- Step 25 (36 points): forecasts 12 months ahead to 2025-12
- GIF shows full forecast horizon at every animation step
This commit is contained in:
Clayton Young
2026-02-21 17:25:05 -05:00
parent 7b7110eebb
commit 96372cee99
3 changed files with 2003 additions and 161 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

After

Width:  |  Height:  |  Size: 776 KiB

View File

@@ -3,7 +3,7 @@
Generate animation data for interactive forecast visualization. Generate animation data for interactive forecast visualization.
This script runs TimesFM forecasts incrementally, starting with minimal data This script runs TimesFM forecasts incrementally, starting with minimal data
and adding one point at a time, saving all forecasts for an interactive slider. and adding one point at a time. Each forecast extends to the final date (2025-12).
Output: animation_data.json with all forecast steps Output: animation_data.json with all forecast steps
""" """
@@ -19,7 +19,10 @@ import timesfm
# Configuration # Configuration
MIN_CONTEXT = 12 # Minimum points to start forecasting MIN_CONTEXT = 12 # Minimum points to start forecasting
HORIZON = 12 # Always forecast 12 months ahead MAX_HORIZON = (
36 # Max forecast length (when we have 12 points, forecast 36 months to 2025-12)
)
TOTAL_MONTHS = 48 # Total months from 2022-01 to 2025-12 (graph extent)
INPUT_FILE = Path(__file__).parent / "temperature_anomaly.csv" INPUT_FILE = Path(__file__).parent / "temperature_anomaly.csv"
OUTPUT_FILE = Path(__file__).parent / "animation_data.json" OUTPUT_FILE = Path(__file__).parent / "animation_data.json"
@@ -27,6 +30,7 @@ OUTPUT_FILE = Path(__file__).parent / "animation_data.json"
def main() -> None: def main() -> None:
print("=" * 60) print("=" * 60)
print(" TIMESFM ANIMATION DATA GENERATOR") print(" TIMESFM ANIMATION DATA GENERATOR")
print(" Dynamic horizon - forecasts always reach 2025-12")
print("=" * 60) print("=" * 60)
# Load data # Load data
@@ -42,9 +46,9 @@ def main() -> None:
) )
print(f" Animation steps: {len(all_values) - MIN_CONTEXT + 1}") print(f" Animation steps: {len(all_values) - MIN_CONTEXT + 1}")
# Load TimesFM # Load TimesFM with max horizon (will truncate output for shorter forecasts)
print("\n🤖 Loading TimesFM 1.0 (200M) PyTorch...") print(f"\n🤖 Loading TimesFM 1.0 (200M) PyTorch (horizon={MAX_HORIZON})...")
hparams = timesfm.TimesFmHparams(horizon_len=HORIZON) hparams = timesfm.TimesFmHparams(horizon_len=MAX_HORIZON)
checkpoint = timesfm.TimesFmCheckpoint( checkpoint = timesfm.TimesFmCheckpoint(
huggingface_repo_id="google/timesfm-1.0-200m-pytorch" huggingface_repo_id="google/timesfm-1.0-200m-pytorch"
) )
@@ -57,23 +61,32 @@ def main() -> None:
step_num = n_points - MIN_CONTEXT + 1 step_num = n_points - MIN_CONTEXT + 1
total_steps = len(all_values) - MIN_CONTEXT + 1 total_steps = len(all_values) - MIN_CONTEXT + 1
print(f"\n📈 Step {step_num}/{total_steps}: Using {n_points} points...") # Calculate dynamic horizon: forecast enough to reach 2025-12
horizon = TOTAL_MONTHS - n_points
print(
f"\n📈 Step {step_num}/{total_steps}: Using {n_points} points, forecasting {horizon} months..."
)
# Get historical data up to this point # Get historical data up to this point
historical_values = all_values[:n_points] historical_values = all_values[:n_points]
historical_dates = all_dates[:n_points] historical_dates = all_dates[:n_points]
# Run forecast # Run forecast (model outputs MAX_HORIZON, we truncate to actual horizon)
point, quantiles = model.forecast( point, quantiles = model.forecast(
[historical_values], [historical_values],
freq=[0], freq=[0],
) )
# Truncate to actual horizon
point = point[0][:horizon]
quantiles = quantiles[0, :horizon, :]
# Determine forecast dates # Determine forecast dates
last_date = historical_dates[-1] last_date = historical_dates[-1]
forecast_dates = pd.date_range( forecast_dates = pd.date_range(
start=last_date + pd.DateOffset(months=1), start=last_date + pd.DateOffset(months=1),
periods=HORIZON, periods=horizon,
freq="MS", freq="MS",
) )
@@ -81,22 +94,24 @@ def main() -> None:
step_data = { step_data = {
"step": step_num, "step": step_num,
"n_points": n_points, "n_points": n_points,
"horizon": horizon,
"last_historical_date": historical_dates[-1].strftime("%Y-%m"), "last_historical_date": historical_dates[-1].strftime("%Y-%m"),
"historical_dates": [d.strftime("%Y-%m") for d in historical_dates], "historical_dates": [d.strftime("%Y-%m") for d in historical_dates],
"historical_values": historical_values.tolist(), "historical_values": historical_values.tolist(),
"forecast_dates": [d.strftime("%Y-%m") for d in forecast_dates], "forecast_dates": [d.strftime("%Y-%m") for d in forecast_dates],
"point_forecast": point[0].tolist(), "point_forecast": point.tolist(),
"q10": quantiles[0, :, 0].tolist(), "q10": quantiles[:, 0].tolist(),
"q20": quantiles[0, :, 1].tolist(), "q20": quantiles[:, 1].tolist(),
"q80": quantiles[0, :, 7].tolist(), "q80": quantiles[:, 7].tolist(),
"q90": quantiles[0, :, 8].tolist(), "q90": quantiles[:, 8].tolist(),
} }
animation_steps.append(step_data) animation_steps.append(step_data)
# Show summary # Show summary
print(f" Last date: {historical_dates[-1].strftime('%Y-%m')}") print(f" Last date: {historical_dates[-1].strftime('%Y-%m')}")
print(f" Forecast mean: {point[0].mean():.3f}°C") print(f" Forecast to: {forecast_dates[-1].strftime('%Y-%m')}")
print(f" Forecast mean: {point.mean():.3f}°C")
# Create output # Create output
output = { output = {
@@ -104,7 +119,8 @@ def main() -> None:
"model": "TimesFM 1.0 (200M) PyTorch", "model": "TimesFM 1.0 (200M) PyTorch",
"total_steps": len(animation_steps), "total_steps": len(animation_steps),
"min_context": MIN_CONTEXT, "min_context": MIN_CONTEXT,
"horizon": HORIZON, "max_horizon": MAX_HORIZON,
"total_months": TOTAL_MONTHS,
"data_source": "NOAA GISTEMP Global Temperature Anomaly", "data_source": "NOAA GISTEMP Global Temperature Anomaly",
"full_date_range": f"{all_dates[0].strftime('%Y-%m')} to {all_dates[-1].strftime('%Y-%m')}", "full_date_range": f"{all_dates[0].strftime('%Y-%m')} to {all_dates[-1].strftime('%Y-%m')}",
}, },
@@ -124,7 +140,7 @@ def main() -> None:
print("=" * 60) print("=" * 60)
print(f"\n📁 Output: {OUTPUT_FILE}") print(f"\n📁 Output: {OUTPUT_FILE}")
print(f" Total steps: {len(animation_steps)}") print(f" Total steps: {len(animation_steps)}")
print(f" Each step shows forecast as one more data point is added") print(f" Each forecast extends to 2025-12")
if __name__ == "__main__": if __name__ == "__main__":