Introduction
penaltyblog v1.11.0 adds batch predictions across the goal-model API, neutral venue support for international tournaments and cup finals, a pre-trained xT model, faster Weibull Copula fitting, and Context7 MCP documentation support.
Neutral Venue Support
With the 2026 World Cup approaching, goal models in penaltyblog now support neutral venue predictions.
Historically, models such as PoissonGoalsModel, DixonColesGoalModel, and WeibullCopulaGoalsModel include an estimated home-advantage parameter. That’s usually what you want for league football, but it can be inappropriate for tournaments, cup finals, or matches played at neutral sites.
v1.11.0 adds a neutral_venue option to prediction methods, allowing forecasts to be generated without applying home advantage:
prediction = model.predict(
"England",
"France",
neutral_venue=True,
)
print(prediction.home_draw_away)
The same option is also available when using predict_many(), making it straightforward to generate neutral-site forecasts for an entire tournament schedule.
This keeps the underlying model fit unchanged while producing probabilities that better reflect matches where neither team enjoys a true home advantage.
Batch Predictions with predict_many()
By popular request, another addition in v1.11.0 is predict_many(), available across the goal-model API.
Previously, predicting a set of fixtures meant looping over predict() one match at a time. That works fine, but it repeats validation and team-index lookups on every call. predict_many() consolidates all of that into a single pass.
Here's a minimal example:
import pandas as pd
from penaltyblog.models import PoissonGoalsModel
df = pd.DataFrame(
{
"goals_home": [2, 1, 0, 3, 1, 2, 0, 1],
"goals_away": [1, 1, 2, 0, 0, 2, 1, 3],
"team_home": [
"Arsenal",
"Chelsea",
"Liverpool",
"Arsenal",
"Chelsea",
"Liverpool",
"Arsenal",
"Liverpool",
],
"team_away": [
"Chelsea",
"Liverpool",
"Arsenal",
"Liverpool",
"Arsenal",
"Chelsea",
"Liverpool",
"Chelsea",
],
}
)
model = PoissonGoalsModel(
df["goals_home"],
df["goals_away"],
df["team_home"],
df["team_away"],
)
model.fit()
single = model.predict("Arsenal", "Chelsea")
batch = model.predict_many(
home_teams=["Arsenal", "Liverpool"],
away_teams=["Chelsea", "Arsenal"],
max_goals=10,
)
print(single.home_draw_away)
print(batch[0].home_draw_away)
Two things worth noting:
predict_many()takes parallel home and away team arrays, not a list of fixture tuples.- It returns a list of
FootballProbabilityGridobjects, so the output is identical in shape topredict(), just in bulk.
That means moving from a one-off prediction to a full matchday or backtesting run requires almost no changes to how you consume the results.
Faster Internals for Supported Models
Under the hood, predict_many() hooks into a new internal method, _compute_probabilities_many(), which gives each model the option to provide a batch-optimized implementation.
In v1.11.0, fast paths were added for:
PoissonGoalsModelDixonColesGoalModelBivariatePoissonGoalModelZeroInflatedPoissonGoalsModelNegativeBinomialGoalModelWeibullCopulaGoalsModel
This matters because a generic loop, while fine as a fallback, still pays Python overhead on every iteration. These optimized paths skip a lot of repeated work and lean more heavily on vectorized computation.
From a user perspective, there's nothing new to learn. The speedup comes through the same model classes you're already using.
A Bundled Pretrained xT Model
v1.11.0 also ships a bundled pre-trained expected threat model via penaltyblog.xt.load_pretrained_xt().
It's built on the methodology from my earlier post, Calculating Expected Threat in Python Using Linear Algebra, where xT is solved as a linear system rather than through brute force iteration.
The real value here is that it's ready to drop straight into an event-data workflow. Using canonical xT column names, a basic scoring pass looks like this:
import pandas as pd
import penaltyblog as pb
events = pd.DataFrame(
{
"minute": [12, 12, 13, 13],
"player": ["Rodri", "Rodri", "De Bruyne", "Haaland"],
"event_type": ["pass", "carry", "pass", "shot"],
"x": [50.8, 65.0, 76.7, 90.0],
"y": [52.5, 50.0, 37.5, 50.0],
"end_x": [65.0, 76.7, 90.0, None],
"end_y": [50.0, 37.5, 50.0, None],
"is_success": [True, True, True, False],
}
)
model = pb.xt.load_pretrained_xt()
scored = model.score(events)
print(
scored.sort_values("xt_added", ascending=False)[
["minute", "player", "event_type", "xt_start", "xt_end", "xt_added"]
]
)
Take an event table, score the possession actions and see which passes or carries added the most threat. That's the typical xT workflow, and now it works out of the box.
If your data provider uses different column names, coordinate ranges, or event labels, you can map everything once using XTEventSchema and pass it into model.score(...). And if you need a competition-specific surface later on, you can fit your own XTModel.
Faster Weibull Copula Fitting
The other major improvement in this release is inside WeibullCopulaGoalsModel.
Analytical derivatives have been added for the Weibull Copula negative log-likelihood gradient's shape and kappa parameters, replacing the previous numerical approach. The result is roughly a 4x speedup in fitting time.
The API hasn't changed:
import pandas as pd
from penaltyblog.models import WeibullCopulaGoalsModel
df = pd.DataFrame(
{
"goals_home": [2, 1, 0, 3, 1, 2, 0, 1],
"goals_away": [1, 1, 2, 0, 0, 2, 1, 3],
"team_home": [
"Arsenal",
"Chelsea",
"Liverpool",
"Arsenal",
"Chelsea",
"Liverpool",
"Arsenal",
"Liverpool",
],
"team_away": [
"Chelsea",
"Liverpool",
"Arsenal",
"Liverpool",
"Arsenal",
"Chelsea",
"Liverpool",
"Chelsea",
],
}
)
model = WeibullCopulaGoalsModel(
df["goals_home"],
df["goals_away"],
df["team_home"],
df["team_away"],
)
model.fit() # analytical gradients are used by default
prediction = model.predict("Arsenal", "Chelsea")
print(prediction.home_draw_away)
Same model, same interface, noticeably less waiting.
Documentation via Context7 MCP
A smaller addition, but worth mentioning: penaltyblog documentation is now available through Context7 MCP. If you use MCP-aware tools or editor integrations, they can pull package docs directly into your development environment. For a library with several modelling families and a fairly wide API surface, it's a handy quality-of-life improvement.
Summary
v1.11.0 is a focused release. Batch forecasting is now a first-class workflow via predict_many(), goal models can generate neutral-site forecasts when home advantage should be removed,fitting WeibullCopulaGoalsModel is meaningfully faster, and expected threat is usable straight out of the box. Context7 MCP support rounds it out on the tooling side.
If you're already using penaltyblog, most of this lands for free - same interfaces, better performance.
Full documentation and examples are available in the official docs here.