Skip to main content
Inside the Nixtla Engine: Agentic Forecasting, Live · Nov 19. Save my spot
Nixtla Blog

Forecasting Championship Results using Time Series and Nixtla

Learn how to forecast championship standings using Nixtla's StatsForecast library.

Introduction

In the real world, we often face forecasting problems in environments where several teams, departments, or companies are competing and building up their performance over time. Think about how sales branches stack up revenue throughout the year, how factories compare production outputs, or how bids play out during a procurement cycle. In all these settings, we’re not just interested in the numbers themselves, but in how each competitor measures up against the rest as the results accumulate.

Championship tournaments provide an excellent case study for this pattern. Unlike simple time series where we forecast a single variable in isolation, championship-style data creates unique challenges:

  • Panel structure: Multiple entities (teams, plants, branches) tracked simultaneously
  • Cumulative metrics: Performance compounds over time (points, sales, production)
  • Fixed horizon: A predetermined endpoint where final rankings matter
  • Historical patterns: Entities establish consistent performance trajectories

These characteristics appear across industries. In manufacturing, production lines accumulate defect rates or output volumes over quarters. In finance, regional offices accumulate sales targets. In logistics, distribution centers accumulate delivery performance metrics. Understanding how to forecast these cumulative, competitive time series has broad applications beyond sports.

In this blog post, we'll use a championship tournament as our example system to demonstrate how StatsForecast, Nixtla's very powerful statistical forecasting library, can predict final outcomes by analyzing cumulative performance time series. The same methodology applies whenever you need to forecast how multiple entities will perform relative to each other over a defined period.

To accomplish this, we'll follow a systematic approach:

  1. Prepare the Data: Generate a simulated championship with cumulative points time series for each team
  2. Hold Out Last N Matches: Keep final matches for evaluation
  3. Train Forecast Model: Fit the model on matches 1 to T−N using StatsForecast and AutoARIMA
  4. Predict Last N Outcomes: Generate forecasts for the remaining matches
  5. Evaluate and Visualize Results: Compare predictions with actual outcomes and assess forecast accuracy

The setup is summarized in the following chart:

It seems like we have a lot to cover. Let's get to it!

Setup Championship Teams and Matches

To generate realistic championship data, we need to model teams with different strengths and simulate match outcomes. The key concepts are:

  1. Team strength parameters: Each team gets a strength value that influences their scoring ability
  2. Poisson match model: Goals are generated using a Poisson distribution based on team strengths
  3. Home advantage: Home teams get a slight boost in expected goals

The core logic uses a Poisson process where expected goals depend on:

  • Team strength differential
  • Home advantage (typically ~0.3 goals)
  • Base scoring rate (~1.35 goals per team)

Match outcomes translate to points: Win = 3 points, Draw = 1 point, Loss = 0 points.

Generate Championship Schedule

For a valid championship, each team must play every other team exactly twice (once home, once away). We use the circle method algorithm:

  1. First half of season: N-1 rounds with rotating pairings
  2. Second half: Mirror of first half (swap home/away)
  3. Validation: Each team plays N-1 home games and N-1 away games

For 20 teams, this creates 38 matchdays with 380 total matches.

Sample Output:

Text
Rounds: 38; Matches total: 380 (should be 38 & 380)

Matchday 1
Team12 vs Team08
Team19 vs Team06
Team17 vs Team03
Team02 vs Team13
Team11 vs Team07
...

Simulate Results and Build Time Series

Now we put everything together: simulate matches, track cumulative statistics, and transform the data into a panel time series ready for forecasting.

The key transformation is converting match-by-match results into a cumulative points time series for each team:

  • Panel structure: unique_id (team), ds (matchday), y (cumulative points)
  • Cumulative metrics: Points, goals for/against, wins/draws/losses accumulate over time
  • Train/test split: Hold out final matchdays for evaluation

This structure is exactly what Nixtla's forecasting libraries expect and is analogous to tracking cumulative sales across branches, production output across facilities, or any competitive metric across entities.

Full implementation: For the complete code covering team setup, calendar generation, match simulation, and data transformation, see the championship_forecasting.ipynb notebook, external link.

Running the simulation:

Python
teams = [f"Team{i:02d}" for i in range(1, 21)]
season = generate_calendar(teams, seed=2025, shuffle_rounds=True)
strengths = make_tiered_strengths(teams)

# 1) Full season → dataframes for plots + forecasting
full_season_results = prepare_forecasting_data(teams, season, strengths, seed=777)
matches_df = full_season_results["matches_df"]
full_season_ts = full_season_results["ts_df"]  # (unique_id, ds, y) ready for StatsForecast/TimeGPT
standings_df = full_season_results["standings_df"]

# 2) Train on first 35 matchdays, forecast remaining 3
train_data = prepare_forecasting_data(teams, season, strengths, seed=777, cutoff_matchday=35)
train_ts = train_data["ts_df"]  # ds ∈ [1..35]
forecast_horizon = train_data["h"]  # 3 matchdays remaining

The following assumptions are made:

  1. We are considering 20 teams (so 38 matchdays per team, 380 matches total).
  2. We are training on the first 35 matchdays and predicting the last 3.
  3. Thanks to the structure of the output, we can train on part of the championship and predict the final championship results and standings.
Row labelunique_iddsyptsopponenthagoals_forgoals_againstresultcum_gfcum_gacum_gdcum_wcum_dcum_l
0Team01133Team20H60W606100
20Team01263Team09H41W1019200
40Team01393Team11H51W15213300
60Team014101Team07H00D15213310
80Team015133Team12H53W20515410
117Team016163Team19A71W27621510
120Team017160Team10H12L28820511
140Team018193Team14H31W31922611
160Team019223Team04H52W361125711
180Team0110253Team05H21W381226811
200Team0111283Team13H41W421329911
220Team0112313Team06H60W4813351011
240Team0113343Team17H60W5413411111
273Team0114373Team16A50W5913461211
280Team0115403Team03H31W6214481311
315Team0116433Team18A41W6615511411
320Team0117463Team02H20W6815531511
347Team0118471Team08A00D6815531521
373Team0119503Team15A30W7115561621
380Team0120533Team08H21W7316571721
415Team0121563Team12A10W7416581821
439Team0122593Team20A51W7917621921
447Team0123623Team06A21W8118632021
................................................
637Team2032180Team14A14L2691-654622
657Team2033180Team13A24L2895-674623
678Team2034191Team15H00D2895-674723
683Team2035190Team04A15L29100-714724

Predict and Forecast with StatsForecast

Now that we have all the data, we can let StatsForecast do the magic. In particular, we will use the AutoARIMA feature to train and forecast the last three matches for the entire championship.

The whole thing can be done in literally three lines of code:

Python
from statsforecast import StatsForecast
from statsforecast.models import AutoARIMA

sf = StatsForecast(models=[AutoARIMA()], freq=1)
sf.fit(train_ts)

forecast_raw = sf.predict(h=forecast_horizon, level=[95])

Evaluate the Results

The championship forecast outputs are stored in forecast_raw. To properly evaluate and visualize our predictions, we need two key steps:

Step 1: Round forecasts to valid integer points

Since championship points can only be integers (0, 1, or 3 per match), we need to round all forecast values:

Python
def round_forecast_to_valid_points(forecast_df: pd.DataFrame) -> pd.DataFrame:
    """
    Round forecast values to integers since points must be whole numbers.
    """
    df = forecast_df.copy()
    for col in df.columns:
        if col not in ['unique_id', 'ds']:
            df[col] = df[col].round().astype(int)
    return df

Step 2: Visualize forecasts with actual results

For visualization, we'll use helper functions that plot cumulative points over time with prediction intervals. The plotting logic handles:

  • Extracting team-specific data from panel forecasts
  • Overlaying actual vs. predicted cumulative points
  • Displaying 95% prediction intervals
  • Marking the train/test split point

Plotting utilities: For the complete plotting functions (plot_team_cumpoints_with_forecast and helpers), see the championship_forecasting.ipynb notebook, external link.

And display the results using the following block of code:

Python
# Round to valid integer points (football only allows 0, 1, or 3 points per match)
forecast = round_forecast_to_valid_points(forecast_raw)

# Add actual values to compare with predictions
full_season_results = prepare_forecasting_data(teams, season, strengths, seed=777)
full_season_ts = full_season_results["ts_df"][["unique_id", "ds", "y"]]

# Merge actual values into the forecast dataframe
forecast = forecast.merge(
    full_season_ts.rename(columns={"y": "actual"}),
    on=["unique_id", "ds"],
    how="left"
)
plot_team_cumpoints_with_forecast(
    ts_df=full_season_results["ts_df"],  # full actuals for context
    team="Team01",
    fcst_df=forecast,
    model_name="AutoARIMA",  # tell the helper how to read the wide columns
    level=95
)

This is the output for Team01:

Loading

And this is how the predictions look (forecast for the full championship):

Row labelunique_iddsAutoARIMAAutoARIMA-lo-95AutoARIMA-hi-95actualerrorabs_errorsquared_error
0Team013689879290-111
1Team013792899593-111
2Team013894919894000
3Team023683818584-111
4Team023786838987-111
5Team023888859287111
6Team033678768179-111
7Team033781778482-111
8Team033883788885-224
9Team043685838886-111
10Team043788859189-111
11Team043890869490000
12Team053672697570224
13Team053775707973224
14Team053878728476224
15Team063667647068-111
16Team063769657371-224
17Team063871667574-339
18Team073673707671224
19Team073775717974111
20Team073877728277000
21Team083671687470111
22Team083773697773000
..............................
56Team193823192721224
57Team203620182119111
58Team203720172319111
59Team203821172419224

Thanks to the power of StatsForecast and AutoARIMA, we are able to predict the full championship in a few seconds, together with the prediction intervals and the average prediction for each team in the championship.

Conclusions

Let's recap what we covered in this post:

  • Forecast many entities at once with panel data structure: Instead of building separate models for each team, we organize our data so that all 20 teams are stacked together with shared columns (unique_id, ds, y).

  • Tracked cumulative metrics which create predictable patterns: When performance accumulates over time (points, sales, production output), historical trajectories become informative for future outcomes.

  • AutoARIMA automates model selection: Rather than manually tuning ARIMA parameters for each entity, StatsForecast's AutoARIMA automatically identifies the optimal model configuration per team. This automation is crucial when forecasting across many entities simultaneously, saving time while maintaining forecast accuracy.

  • Prediction intervals quantify uncertainty: The 95% prediction intervals generated by our model provide not just point forecasts but also confidence ranges. This is essential for decision-making: knowing that a team will finish with 85-90 points is more actionable than a single-point estimate of 87 points.

  • Historical holdout validation demonstrates practical performance: By training on matchdays 1-35 and predicting the final 3 matchdays, we simulated a realistic forecasting scenario, validating that this approach works when you need to forecast competitive outcomes before a period ends.

This forecasting methodology extends beyond sports to any scenario where multiple entities compete on cumulative metrics over a fixed horizon: quarterly sales targets across regions, monthly production goals across facilities, or seasonal performance metrics across departments. The combination of panel data structure, cumulative metric tracking, and automated model selection with StatsForecast provides a powerful framework for forecasting competitive, multi-entity systems in any industry.