A Python CLI application that simulates the 2026 FIFA World Cup end-to-end, from the group stage through the final. Features a Poisson-based match engine with upset mechanics, extra time, penalty shootouts, and full FIFA 2026 regulations compliance (head-to-head tiebreakers, 495-combination third-place bracket table).
Win probabilities are calibrated against real betting market odds from Sportium, Betway, and Retabet (May 2026). After 100,000 simulations, champion distributions match market expectations within ~2 percentage points.
- 48 teams, 12 groups, 104 matches per complete simulation
- Poisson-based match engine with realistic goal distributions
- Upset mechanics: 7% chance of underdog heroics per match
- FIFA 2026 tiebreakers: Head-to-head → GD → GF → FIFA ranking → random draw
- 495-combination bracket table: Exact match per FIFA Annex C for Round of 32
- Two simulation modes: Batch (default: 107 sims) and interactive (live step-by-step)
- SQLite storage: Persist and query thousands of simulations
- Team advancement tracking: Per-team phase-exit probabilities (Group → R32 → R16 → QF → SF → Final → Champion)
- HTML export: Self-contained report with champion distribution, advancement stats, and match details
- 100% offline: No network dependencies at runtime
- Calibrated coefficients: Real betting market odds, power-law strength curve
# Clone the repository
git clone https://github.com/<your-username>/mundial-predictor.git
cd mundial-predictor
# Create a virtual environment (recommended)
python -m venv .venv
# Activate the virtual environment
# On macOS/Linux:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate
# Install dependencies
pip install -r requirements.txtRequires Python 3.10+.
# Run 107 batch simulations (default)
python main.py simular
# Run 1000 simulations with seed and HTML report
python main.py simular --cantidad 1000 --seed 42 --export-html
# Interactive step-by-step simulation
python main.py live
# Show champion distribution
python main.py stats --top 10
# Show all teams' advancement probabilities
python main.py stats --avance
# Show advancement stats for a specific team
python main.py stats --avance --equipo "España"python main.py simular --cantidad 1000 --seed 42 --export-htmlOptions:
--cantidad N: Number of simulations to run (default: 107)--seed S: Random seed for reproducibility--export-html: Generate HTML report after simulation--db-path PATH: Custom SQLite database path (default: mundial.db)
python main.py live --seed 42 --velocidad pasoOptions:
--seed S: Random seed for reproducibility--velocidad [rapido|paso]: Auto-advance or step-by-step (default: paso)
python main.py stats --top 10 --equipo "España" --sorpresas --avanceOptions:
--top N: Show top N champions by frequency--equipo X: Detailed stats for a specific team--sorpresas: Show underdog champion surprises (wins outside top 8 by probability)--avance: Show team advancement statistics (phase-exit probabilities for all 48 teams)--desde-sim N --hasta-sim M: Filter by simulation ID range--db-path PATH: Custom SQLite database path
python main.py migrar-avancePopulates the avance_equipos table from existing partidos data. Only needed for databases created before advancement tracking was added. New simulations automatically record advancement data.
Options:
--db-path PATH: Custom SQLite database path
mundial-predictor/
├── main.py # CLI entry point (Click)
├── data/
│ ├── teams.py # 48 teams, groups, strengths
│ ├── probabilities.py # Win probabilities, attack/defense derivation, constants
│ ├── schedule.py # Match calendar and venues
│ └── bracket_table.py # 495-combination R32 bracket table
├── engine/
│ ├── match.py # Poisson match simulation
│ ├── group_stage.py # Group stage, tiebreakers, 3rd place qualification
│ └── knockout.py # Knockout bracket, advancement computation
├── storage/
│ ├── db.py # SQLite persistence, stats queries, migrations
│ └── models.py # Dataclasses for database rows and tournament results
├── cli/
│ ├── commands.py # Command implementations
│ ├── display.py # Rich terminal UI helpers
│ └── export_html.py # Jinja2 HTML report generation
├── tests/
│ ├── test_match.py # Match engine tests (13 tests)
│ ├── test_group_stage.py # Group stage tests (15 tests)
│ └── test_advancement.py # Advancement computation and storage tests (22 tests)
├── requirements.txt
└── README.md
Team strengths are derived from real betting market odds:
strength = (win_probability * 100) ** 0.24- Win probabilities sourced from Sportium, Betway, Retabet averages (May 2026)
- Overround removed (~22.3% normalization to sum to 1.0)
- Pure betting-based: no FIFA ranking factor, no Elo — market odds already price in all relevant information
- Power exponent 0.24 (between fourth-root 0.25 and cube-root 0.33) produces the best fit against real odds
- Expected goals: λ = attack_strength × defense_strength × home_advantage
attack = strength × deterministic_variationdefense = 2.5 / (strength + 1.5) × deterministic_variationdeterministic_variation = 0.97 + hash(team, seed) % 7 / 100(±3% per team, per simulation)
- Lambda cap: λ clamped to [0.1, 4.0] after all modifiers to prevent extreme scores
- Home advantage: ×1.1 for host nations (USA, Mexico, Canada) in group stage only
- Upset mechanism: 7% chance per match (underdog λ ×1.8, favorite λ ×0.6)
- Extra time: λ ×0.4 for 30 minutes in knockout ties
- Penalty shootout: Binomial distribution per kick (~75% conversion), sudden death
- 12 groups of 4 teams (A–L), round-robin (72 matches)
- Points: Win=3, Draw=1, Loss=0
- FIFA 2026 tiebreakers (in order): Points → Head-to-head → Goal difference → Goals for → FIFA ranking → Random draw
- Top 2 per group + 8 best third-placed teams advance (32 teams total)
- 32 teams, single-elimination bracket
- Round of 32 matchups determined by 495-combination FIFA Annex C table (pre-parsed from Wikipedia)
- R16 → Quarterfinals → Semifinals → Third-place match → Final
| Team | Simulated | Target (market) | Δ |
|---|---|---|---|
| Spain | 14.5% | 14.4% | +0.1 |
| France | 13.2% | 12.8% | +0.4 |
| England | 11.3% | 12.0% | -0.7 |
| Argentina | 11.2% | 10.5% | +0.7 |
| Brazil | 10.0% | 10.3% | -0.3 |
| Portugal | 7.9% | 9.1% | -1.2 |
| Germany | 6.4% | 6.7% | -0.3 |
| Netherlands | 4.5% | 5.2% | -0.7 |
All top 8 within ~1.2 percentage points of market odds.
The simulator produces several interrelated statistics. Here is what each metric means and how to interpret it.
Shows how often each team wins the tournament across all simulations.
- count: Number of simulations where this team was champion
- percentage:
(count / total_simulations) × 100 - Percentages shown with 2 decimal places (so 22/100,000 = 0.02%, not rounded to 0.0%)
- Underdog champions (
stats --sorpresas): Teams outside the top 8 by pre-tournament win probability that won at least once
Interpretation: A team at 10% is expected to win 1 in 10 World Cups. The sum of all percentages = 100%. This is the most intuitive metric but also the noisiest at low counts — 1 win in 1,000 simulations = 0.10%.
Shows each team's probability of reaching every knockout phase:
| Column | Meaning |
|---|---|
| Best | Highest phase reached on average (e.g., "Group Stage", "Round of 32", "Quarterfinals", "Semifinals", "Final", "Champion") |
| Group Exit | Probability of advancing from the group stage (top 2 + best 3rd) |
| R32 | Probability of winning the Round of 32 match |
| R16 | Probability of reaching the Quarterfinals |
| QF | Probability of reaching the Semifinals |
| SF | Probability of reaching the Final |
| Final | Probability of reaching the championship match |
| 🏆 | Probability of winning the tournament (same as champion distribution) |
How to read it: Each column is conditional on reaching the previous phase. So SF = 30% means "30% chance of reaching the Final given the simulation started." The cascade naturally shrinks — even strong teams have ~5–10% chance of being upset in any single knockout match.
Specific team filter (stats --avance --equipo "España"): Shows all 8 columns for one team plus the distribution of which phase it reached most frequently as its "best" result.
Why phases are tracked per simulation: The data is stored at record time (not derived at query time), so filters like --desde-sim N --hasta-sim M work efficiently on advancement data.
| Phase | Teams | Description |
|---|---|---|
| Group Stage | 48 | 12 groups of 4, round-robin |
| Round of 32 | 32 | Group winners + runners-up + 8 best third-placed teams |
| Round of 16 | 16 | Knockout winners from R32 |
| Quarterfinals | 8 | Knockout winners from R16 |
| Semifinals | 4 | Knockout winners from QF |
| Final | 2 | Semifinal winners |
| Champion | 1 | Final winner |
The third-place match is played between semifinal losers but is not tracked as a separate phase — those teams are recorded as "Semifinals" best phase.
The generated HTML report (--export-html) includes:
- Summary card: Total simulations, most common champion, average goals, underdog champion percentage
- Champion distribution: Horizontal bar chart for all 48 teams (top 8 highlighted, underdogs in red)
- Team Advancement table: All 8 columns for every team
- Underdog Champions table: Teams outside top 8 that won at least once
- Last simulation details: Full group standings + all knockout match results
# Who has the best final appearance rate outside the favorites?
python main.py stats --avance --top 5 --equipo "Noruega"
# Compare two teams' knockout stage performance
python main.py stats --avance --equipo "Marruecos"
python main.py stats --avance --equipo "Japón"
# See the full picture in one HTML file
python main.py simular --cantidad 10000 --seed 42 --export-html
open report.htmlpytest tests/ -v50 tests across 3 test files:
tests/test_match.py (13 tests):
- Match engine convergence, determinism, upset mechanics
- Penalty shootout correctness (binomial distribution, no score corruption)
- Home advantage, seed reproducibility, extra time logic
tests/test_group_stage.py (15 tests):
- Group stage integrity, points calculation, tiebreaker ordering
- Head-to-head tiebreaker before goal difference (FIFA 2026 rule)
- Third-place team ranking and qualification logic
- Full tournament group simulation
tests/test_advancement.py (22 tests):
- Advancement computation correctness across all phases
- Edge cases: 0 simulations, single simulation, all teams same phase
- Storage and retrieval from SQLite
- Migration from existing
partidosdata - Display formatting for Rich tables
Make sure you've activated the virtual environment and installed dependencies:
source .venv/bin/activate
pip install -r requirements.txtThe mundial.db file grows with each simulation. You can safely delete it to start fresh:
rm mundial.db
python main.py simular --cantidad 100Ensure you're using the --export-html flag. The report is saved as report.html in the project root:
python main.py simular --cantidad 1000 --export-html
open report.html # macOS
xdg-open report.html # Linux
start report.html # WindowsCheck your Python version:
python --versionIf it shows Python 3.9 or lower, try python3 instead of python, or install Python 3.10+.
- <0.5s per single simulation (104 matches)
- <5 minutes for 10,000 simulations
- SQLite queries on 100K rows return in <100ms
MIT