commit 84fc5b1b07ed5492e3282dd9f2e9897c5515d561 Author: pauli Date: Tue May 19 23:27:34 2026 +0200 add some parts of the app diff --git a/FWC26_regulations_EN.pdf b/FWC26_regulations_EN.pdf new file mode 100644 index 0000000..5d23b91 Binary files /dev/null and b/FWC26_regulations_EN.pdf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b39950 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# WM 2026 KO-Simulator (Deutschland) + +Kleine Streamlit-App zur Simulation der K.-o.-Runden der FIFA WM 2026, mit Fokus darauf, welche Gegner Deutschland in den einzelnen Runden treffen kann und wie die Zusammensetzung der KO-Runden zustande kommt. + +Installation: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Starten: + +```bash +streamlit run app.py +``` + +Beschreibung: +- Lade oder bearbeite die Gruppen (12 Gruppen à 4 Teams). +- Wähle die Anzahl der Simulationen. +- Die App simuliert Gruppenphase und K.-o.-Phase mehrfach und zeigt für Deutschland, welche Gegner in jeder KO-Runde möglich sind und mit welchen Häufigkeiten. + +Hinweis: Die App verwendet ein Standard-Paarungsschema für die Runde der letzten 32: Gruppen werden paarweise verbunden (A↔B, C↔D, ...). Diese Zuordnung kann im Code angepasst werden. diff --git a/__pycache__/app.cpython-314.pyc b/__pycache__/app.cpython-314.pyc new file mode 100644 index 0000000..52c7a21 Binary files /dev/null and b/__pycache__/app.cpython-314.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..3e7c15b --- /dev/null +++ b/app.py @@ -0,0 +1,1141 @@ +import streamlit as st +import pandas as pd +import numpy as np +from collections import defaultdict, Counter +import random +import urllib.parse +from pypdf import PdfReader + +LANGUAGES = { + 'de': 'Deutsch', + 'en': 'English', + 'fr': 'Français', + 'es': 'Español', + 'ru': 'Русский', +} + +GERMANY_DISPLAY = { + 'de': 'Deutschland', + 'en': 'Germany', + 'fr': 'Allemagne', + 'es': 'Alemania', + 'ru': 'Германия', +} + +TRANSLATIONS = { + 'title': { + 'de': 'WM 2026 KO-Simulator — Wer kann Deutschland treffen?', + 'en': 'World Cup 2026 KO Simulator — Who can meet Germany?', + 'fr': 'Simulateur KO Coupe du Monde 2026 — Qui peut rencontrer l’Allemagne ?', + 'es': 'Simulador KO Mundial 2026 — ¿Quién puede encontrarse con Alemania?', + 'ru': 'Симулятор плей-офф ЧМ 2026 — Кто может встретиться с Германией?', + }, + 'sidebar_header': { + 'de': 'Einstellungen', + 'en': 'Settings', + 'fr': 'Paramètres', + 'es': 'Configuración', + 'ru': 'Настройки', + }, + 'sidebar_n': { + 'de': 'Anzahl Simulationen', + 'en': 'Number of simulations', + 'fr': 'Nombre de simulations', + 'es': 'Número de simulaciones', + 'ru': 'Количество симуляций', + }, + 'sidebar_seed': { + 'de': 'Zufalls-Seed (0 = variabel)', + 'en': 'Random seed (0 = variable)', + 'fr': 'Graine aléatoire (0 = variable)', + 'es': 'Semilla aleatoria (0 = variable)', + 'ru': 'Случайное зерно (0 = переменное)', + }, + 'sidebar_groups_description': { + 'de': 'Gruppen werden in 12 Gruppen (A–L) mit je 4 Teams verteilt.', + 'en': 'Teams are distributed into 12 groups (A–L) of 4 teams each.', + 'fr': 'Les équipes sont réparties en 12 groupes (A–L) de 4 équipes chacun.', + 'es': 'Los equipos se distribuyen en 12 grupos (A–L) de 4 equipos cada uno.', + 'ru': 'Команды распределяются на 12 групп (A–L) по 4 команды.', + }, + 'sidebar_odds_description': { + 'de': 'Die Simulation verwendet Wettquoten für Achtelfinale, Viertelfinale, Halbfinale, Finale und Sieg, um Teamstärke und Matchausgänge zu gewichten.', + 'en': 'The simulator uses odds for Round of 16, Quarterfinal, Semifinal, Final and Winner to weight team strength and match outcomes.', + 'fr': 'Le simulateur utilise les cotes pour les 8es, quarts, demis, finale et victoire afin de pondérer la force des équipes et les résultats.', + 'es': 'El simulador utiliza probabilidades para octavos, cuartos, semifinales, final y victoria para ponderar la fuerza de los equipos y los resultados.', + 'ru': 'Симулятор использует котировки на 1/8, 1/4, 1/2, финал и победу для взвешивания силы команд и результатов.', + }, + 'groups_header': { + 'de': 'Gruppen (Standardverteilung)', + 'en': 'Groups (default distribution)', + 'fr': 'Groupes (distribution par défaut)', + 'es': 'Grupos (distribución por defecto)', + 'ru': 'Группы (стандартное распределение)', + }, + 'round32_scheme': { + 'de': '**Paarungsschema für die Runde der letzten 32**: Gruppen werden paarweise verbunden: (A↔B), (C↔D), ..., (K↔L). Für jedes Paar gibt es zwei Spiele: A1 vs B2 und B1 vs A2.', + 'en': '**Round of 32 layout**: groups are paired (A↔B), (C↔D), ..., (K↔L). Each pair yields two matches: A1 vs B2 and B1 vs A2.', + 'fr': '**Schéma des 32es de finale** : les groupes sont appariés (A↔B), (C↔D), ..., (K↔L). Chaque paire donne deux matches : A1 vs B2 et B1 vs A2.', + 'es': '**Esquema de octavos de final**: los grupos se emparejan (A↔B), (C↔D), ..., (K↔L). Cada pareja genera dos partidos: A1 vs B2 y B1 vs A2.', + 'ru': '**Сетка 1/8 финала**: группы соединяются парами (A↔B), (C↔D), ..., (K↔L). Для каждой пары два матча: A1 vs B2 и B1 vs A2.', + }, + 'button_start': { + 'de': 'Simulation starten', + 'en': 'Start simulation', + 'fr': 'Lancer la simulation', + 'es': 'Iniciar simulación', + 'ru': 'Запустить симуляцию', + }, + 'start_info': { + 'de': 'Simulation läuft — bitte kurz warten...', + 'en': 'Simulation running — please wait...', + 'fr': 'Simulation en cours — veuillez patienter...', + 'es': 'Simulación en curso — espere por favor...', + 'ru': 'Симуляция запущена — подождите, пожалуйста...', + }, + 'done_message': { + 'de': 'Fertig', + 'en': 'Done', + 'fr': 'Terminé', + 'es': 'Listo', + 'ru': 'Готово', + }, + 'tab_opponent_details': { + 'de': 'Gegnerdetails', + 'en': 'Opponent details', + 'fr': 'Détails adversaires', + 'es': 'Detalles del rival', + 'ru': 'Детали соперника', + }, + 'tab_results': { + 'de': 'Ergebnisse', + 'en': 'Results', + 'fr': 'Résultats', + 'es': 'Resultados', + 'ru': 'Результаты', + }, + 'opponent_details_header': { + 'de': 'Direkte KO-Paarungen mit Deutschland', + 'en': 'Direct knockout pairings with Germany', + 'fr': 'Rencontres directes en KO avec l’Allemagne', + 'es': 'Emparejamientos directos de eliminación con Alemania', + 'ru': 'Прямые пары плей-офф с Германией', + }, + 'select_opponent': { + 'de': 'Wähle einen Gegner', + 'en': 'Choose an opponent', + 'fr': 'Choisissez un adversaire', + 'es': 'Elige un rival', + 'ru': 'Выберите соперника', + }, + 'germany_vs': { + 'de': '**Deutschland vs. {opponent}**', + 'en': '**Germany vs. {opponent}**', + 'fr': '**Allemagne vs. {opponent}**', + 'es': '**Alemania vs. {opponent}**', + 'ru': '**Германия vs. {opponent}**', + }, + 'no_encounter': { + 'de': 'In den Simulationen sind diese beiden Teams nicht aufeinander getroffen.', + 'en': 'These two teams did not meet in the simulations.', + 'fr': 'Ces deux équipes ne se sont pas rencontrées dans les simulations.', + 'es': 'Estos dos equipos no se enfrentaron en las simulaciones.', + 'ru': 'Эти две команды не встретились в симуляциях.', + }, + 'total_encounters': { + 'de': '- Insgesamt {count} Begegnungen mit Deutschland ({prob} der Simulationen)', + 'en': '- Total {count} meetings with Germany ({prob} of simulations)', + 'fr': '- Au total {count} rencontres avec l’Allemagne ({prob} des simulations)', + 'es': '- En total {count} enfrentamientos con Alemania ({prob} de las simulaciones)', + 'ru': '- Всего {count} встреч с Германией ({prob} симуляций)', + }, + 'both_group_winners': { + 'de': '- Beide Teams als Gruppensieger in **{count}** Begegnungen ({prob})', + 'en': '- Both teams were group winners in **{count}** meetings ({prob})', + 'fr': '- Les deux équipes étaient vainqueurs de groupe dans **{count}** rencontres ({prob})', + 'es': '- Ambos equipos fueron primeros de grupo en **{count}** encuentros ({prob})', + 'ru': '- Обе команды были победителями группы в **{count}** встречах ({prob})', + }, + 'stage_count': { + 'de': '- {stage}: **{count}** Mal ({prob})', + 'en': '- {stage}: **{count}** times ({prob})', + 'fr': '- {stage} : **{count}** fois ({prob})', + 'es': '- {stage}: **{count}** veces ({prob})', + 'ru': '- {stage}: **{count}** раз ({prob})', + }, + 'no_both_group_winners': { + 'de': '- Keine Begegnungen, in denen beide Teams Gruppensieger waren.', + 'en': '- No meetings where both teams were group winners.', + 'fr': '- Aucune rencontre où les deux équipes étaient vainqueurs de groupe.', + 'es': '- Ningún encuentro en el que ambos equipos fueran primeros de grupo.', + 'ru': '- Нет встреч, в которых обе команды были победителями групп.', + }, + 'possible_paths_header': { + 'de': 'Mögliche Verläufe für die Paarung', + 'en': 'Possible paths for the matchup', + 'fr': 'Parcours possibles pour la confrontation', + 'es': 'Posibles rutas para el enfrentamiento', + 'ru': 'Возможные пути для пары', + }, + 'flows_first_both_winners': { + 'de': '**Verläufe, zuerst beide Gruppensieger**', + 'en': '**Paths, both group winners first**', + 'fr': '**Parcours, d’abord les deux vainqueurs de groupe**', + 'es': '**Rutas, primero ambos primeros de grupo**', + 'ru': '**Маршруты, сначала оба победителя группы**', + }, + 'meeting_by_group_place': { + 'de': 'Treffen nach Gruppenplatzierung', + 'en': 'Meetings by group placement', + 'fr': 'Rencontres selon la place en groupe', + 'es': 'Encuentros por posición de grupo', + 'ru': 'Встречи по месту в группе', + }, + 'stage_meeting_phase': { + 'de': 'Typische Turnierphase bei Begegnung', + 'en': 'Typical tournament phase at meeting', + 'fr': 'Phase de tournoi typique lors de la rencontre', + 'es': 'Fase típica del torneo en el enfrentamiento', + 'ru': 'Типичная стадия турнира при встрече', + }, + 'team_phase_line': { + 'de': '{team}: Achtelfinale **{r16}**, Viertelfinale **{qf}**, Halbfinale **{sf}**, Finale **{final}**, Sieg **{winner}**', + 'en': '{team}: Round of 16 **{r16}**, Quarterfinal **{qf}**, Semifinal **{sf}**, Final **{final}**, Win **{winner}**', + 'fr': '{team} : 8es **{r16}**, quarts **{qf}**, demis **{sf}**, finale **{final}**, victoire **{winner}**', + 'es': '{team}: Octavos **{r16}**, Cuartos **{qf}**, Semifinales **{sf}**, Final **{final}**, Victoria **{winner}**', + 'ru': '{team}: 1/8 **{r16}**, 1/4 **{qf}**, 1/2 **{sf}**, финал **{final}**, победа **{winner}**', + }, + 'this_window_text': { + 'de': 'Dieses Fenster zeigt, wie oft die jeweilige KO-Paarung zustande kommt und auf welchem Spielverlauf Deutschland und der Gegner dazu wahrscheinlich kommen.', + 'en': 'This view shows how often the knockout matchup occurs and which path Germany and the opponent are likely to take.', + 'fr': 'Cette vue montre la fréquence de cette confrontation en KO et le parcours probable de l’Allemagne et de l’adversaire.', + 'es': 'Esta vista muestra con qué frecuencia ocurre el enfrentamiento eliminatorio y qué ruta es probable para Alemania y el rival.', + 'ru': 'Этот вид показывает, как часто происходит пара плей-офф и какой путь, скорее всего, пройдут Германия и соперник.', + }, + 'please_start': { + 'de': 'Bitte starte zuerst die Simulation, damit dir die Gegnerdetails angezeigt werden.', + 'en': 'Please start the simulation first to see opponent details.', + 'fr': 'Veuillez d’abord lancer la simulation pour voir les détails des adversaires.', + 'es': 'Por favor, inicia primero la simulación para ver los detalles del rival.', + 'ru': 'Пожалуйста, сначала запустите симуляцию, чтобы увидеть детали соперника.', + }, + 'final_note': { + 'de': 'Die Simulation verwendet Wettquoten, um die Teamstärke zu gewichten. So können die Gegnerverteilungen besser den Bookmaker-Erwartungen folgen und liefern zugleich Gruppenergebnisse (Siege/Unentschieden/Niederlagen) für alle Teams.', + 'en': 'The simulation uses odds to weight team strength. That way opponent distributions better match bookmaker expectations and also deliver group-stage results (wins/draws/losses) for all teams.', + 'fr': 'La simulation utilise les cotes pour pondérer la force des équipes. Ainsi les distributions des adversaires correspondent mieux aux attentes des bookmakers et fournissent des résultats de phase de groupes (victoires/nuls/défaites) pour toutes les équipes.', + 'es': 'La simulación usa probabilidades para ponderar la fuerza de los equipos. Así, las distribuciones de rivales se ajustan mejor a las expectativas de las casas de apuestas y ofrecen resultados de la fase de grupos (victorias/empates/derrotas) para todos los equipos.', + 'ru': 'Симуляция использует котировки для взвешивания силы команд. Так распределение соперников лучше соответствует ожиданиям букмекеров и дает результаты группового этапа (победы/ничьи/поражения) для всех команд.', + }, + 'germany_comparison_header': { + 'de': 'Deutschland im Vergleich', + 'en': 'Germany compared', + 'fr': 'L’Allemagne en comparaison', + 'es': 'Alemania en comparación', + 'ru': 'Германия в сравнении', + }, + 'germany_reaches': { + 'de': '{team} erreicht das {stage} in **{value}** der Simulationen.', + 'en': '{team} reaches the {stage} in **{value}** of simulations.', + 'fr': '{team} atteint les {stage} dans **{value}** des simulations.', + 'es': '{team} llega a {stage} en **{value}** de las simulaciones.', + 'ru': '{team} выходит в {stage} в **{value}** симуляциях.', + }, + 'possible_opponents': { + 'de': 'Mögliche Gegner von {team}', + 'en': 'Possible opponents for {team}', + 'fr': 'Adversaires possibles pour {team}', + 'es': 'Posibles rivales para {team}', + 'ru': 'Возможные соперники для {team}', + }, + 'team_summary_header': { + 'de': 'Team-Zusammenfassung nach Simulation', + 'en': 'Team summary from simulation', + 'fr': 'Résumé des équipes après simulation', + 'es': 'Resumen del equipo tras la simulación', + 'ru': 'Сводка по командам после симуляции', + }, + 'detailed_overview': { + 'de': '{team} — Detaillierte Übersicht', + 'en': '{team} — Detailed overview', + 'fr': '{team} — Aperçu détaillé', + 'es': '{team} — Resumen detallado', + 'ru': '{team} — Подробный обзор', + }, + 'group_place_line': { + 'de': '- Gruppenplatz 1: **{p1}**, Platz 2: **{p2}**, Platz 3: **{p3}**, Platz 4: **{p4}**', + 'en': '- Group place 1: **{p1}**, place 2: **{p2}**, place 3: **{p3}**, place 4: **{p4}**', + 'fr': '- Place de groupe 1 : **{p1}**, place 2 : **{p2}**, place 3 : **{p3}**, place 4 : **{p4}**', + 'es': '- Puesto en grupo 1: **{p1}**, puesto 2: **{p2}**, puesto 3: **{p3}**, puesto 4: **{p4}**', + 'ru': '- Место в группе 1: **{p1}**, место 2: **{p2}**, место 3: **{p3}**, место 4: **{p4}**', + }, + 'average_group_results': { + 'de': '- Durchschnittliche Gruppenergebnisse: {wins} Siege, {draws} Unentschieden, {losses} Niederlagen', + 'en': '- Average group results: {wins} wins, {draws} draws, {losses} losses', + 'fr': '- Résultats moyens de groupe : {wins} victoires, {draws} nuls, {losses} défaites', + 'es': '- Resultados medios de grupo: {wins} victorias, {draws} empates, {losses} derrotas', + 'ru': '- Средние результаты в группе: {wins} побед, {draws} ничьих, {losses} поражений', + }, + 'stage_stats_line': { + 'de': '- Achtelfinale: **{r16}**, Viertelfinale: **{qf}**, Halbfinale: **{sf}**, Finale: **{final}**, Sieg: **{winner}**', + 'en': '- Round of 16: **{r16}**, Quarterfinal: **{qf}**, Semifinal: **{sf}**, Final: **{final}**, Win: **{winner}**', + 'fr': '- 8es : **{r16}**, quarts : **{qf}**, demis : **{sf}**, finale : **{final}**, victoire : **{winner}**', + 'es': '- Octavos: **{r16}**, Cuartos: **{qf}**, Semifinales: **{sf}**, Final: **{final}**, Victoria: **{winner}**', + 'ru': '- 1/8: **{r16}**, 1/4: **{qf}**, 1/2: **{sf}**, финал: **{final}**, победа: **{winner}**', + }, +} + +ROUND_NAMES = { + 'de': {'R32': 'Runde der letzten 32', 'R16': 'Achtelfinale', 'QF': 'Viertelfinale', 'SF': 'Halbfinale', 'Final': 'Finale'}, + 'en': {'R32': 'Round of 32', 'R16': 'Round of 16', 'QF': 'Quarterfinal', 'SF': 'Semifinal', 'Final': 'Final'}, + 'fr': {'R32': '8es', 'R16': '8es', 'QF': 'Quarts', 'SF': 'Démis', 'Final': 'Finale'}, + 'es': {'R32': 'Octavos', 'R16': 'Octavos', 'QF': 'Cuartos', 'SF': 'Semifinales', 'Final': 'Final'}, + 'ru': {'R32': '1/8', 'R16': '1/8', 'QF': '1/4', 'SF': '1/2', 'Final': 'Финал'}, +} + +COLUMN_LABELS = { + 'de': { + 'team': 'Team', + 'avg_group_wins': 'Avg. Gruppen-Siege', + 'avg_group_draws': 'Avg. Gruppen-Unentschieden', + 'avg_group_losses': 'Avg. Gruppen-Niederlagen', + 'place_1': '1. Platz', + 'place_2': '2. Platz', + 'place_3': '3. Platz', + 'place_4': '4. Platz', + 'R16': 'Achtelfinale', + 'QF': 'Viertelfinale', + 'SF': 'Halbfinale', + 'Final': 'Finale', + 'Winner': 'Sieg', + 'Stage': 'Phase', + 'Round': 'Runde', + 'Germany group place': 'Deutschland Gruppenplatz', + 'Opponent group place': 'Gegner Gruppenplatz', + 'Both group winners': 'Beide Gruppensieger', + 'Count': 'Anzahl', + 'Share of round': 'Anteil in der Runde', + 'Share of sims': 'Anteil der Simulationen', + 'Germany path': 'Deutschland Pfad', + 'Opponent path': 'Gegner Pfad', + 'Probability of meeting': 'Wahrscheinlichkeit des Treffens', + }, + 'en': { + 'team': 'Team', + 'avg_group_wins': 'Avg. group wins', + 'avg_group_draws': 'Avg. group draws', + 'avg_group_losses': 'Avg. group losses', + 'place_1': 'Place 1', + 'place_2': 'Place 2', + 'place_3': 'Place 3', + 'place_4': 'Place 4', + 'R16': 'Round of 16', + 'QF': 'Quarterfinal', + 'SF': 'Semifinal', + 'Final': 'Final', + 'Winner': 'Win', + 'Stage': 'Stage', + 'Round': 'Round', + 'Germany group place': 'Germany group place', + 'Opponent group place': 'Opponent group place', + 'Both group winners': 'Both group winners', + 'Count': 'Count', + 'Share of round': 'Share of round', + 'Share of sims': 'Share of sims', + 'Germany path': 'Germany path', + 'Opponent path': 'Opponent path', + 'Probability of meeting': 'Probability of meeting', + }, + 'fr': { + 'team': 'Équipe', + 'avg_group_wins': 'Moy. victoires de groupe', + 'avg_group_draws': 'Moy. nuls de groupe', + 'avg_group_losses': 'Moy. défaites de groupe', + 'place_1': '1ᵉʳ', + 'place_2': '2ᵉ', + 'place_3': '3ᵉ', + 'place_4': '4ᵉ', + 'R16': '8es', + 'QF': 'Quarts', + 'SF': 'Démis', + 'Final': 'Finale', + 'Winner': 'Victoire', + 'Stage': 'Phase', + 'Round': 'Tour', + 'Germany group place': 'Place Allemagne', + 'Opponent group place': 'Place adversaire', + 'Both group winners': 'Deux vainqueurs de groupe', + 'Count': 'Nombre', + 'Share of round': 'Part de tour', + 'Share of sims': 'Part des simulations', + 'Germany path': 'Parcours Allemagne', + 'Opponent path': 'Parcours adversaire', + 'Probability of meeting': 'Probabilité de rencontre', + }, + 'es': { + 'team': 'Equipo', + 'avg_group_wins': 'Prom. victorias de grupo', + 'avg_group_draws': 'Prom. empates de grupo', + 'avg_group_losses': 'Prom. derrotas de grupo', + 'place_1': 'Puesto 1', + 'place_2': 'Puesto 2', + 'place_3': 'Puesto 3', + 'place_4': 'Puesto 4', + 'R16': 'Octavos', + 'QF': 'Cuartos', + 'SF': 'Semifinales', + 'Final': 'Final', + 'Winner': 'Victoria', + 'Stage': 'Fase', + 'Round': 'Ronda', + 'Germany group place': 'Puesto grupo Alemania', + 'Opponent group place': 'Puesto grupo rival', + 'Both group winners': 'Ambos primeros de grupo', + 'Count': 'Cantidad', + 'Share of round': 'Proporción de ronda', + 'Share of sims': 'Proporción de simulaciones', + 'Germany path': 'Ruta Alemania', + 'Opponent path': 'Ruta rival', + 'Probability of meeting': 'Probabilidad de encuentro', + }, + 'ru': { + 'team': 'Команда', + 'avg_group_wins': 'Ср. победы в группе', + 'avg_group_draws': 'Ср. ничьи в группе', + 'avg_group_losses': 'Ср. поражения в группе', + 'place_1': '1-е место', + 'place_2': '2-е место', + 'place_3': '3-е место', + 'place_4': '4-е место', + 'R16': '1/8', + 'QF': '1/4', + 'SF': '1/2', + 'Final': 'Финал', + 'Winner': 'Победа', + 'Stage': 'Стадия', + 'Round': 'Раунд', + 'Germany group place': 'Место Германии', + 'Opponent group place': 'Место соперника', + 'Both group winners': 'Оба победителя группы', + 'Count': 'Количество', + 'Share of round': 'Доля раунда', + 'Share of sims': 'Доля симуляций', + 'Germany path': 'Путь Германии', + 'Opponent path': 'Путь соперника', + 'Probability of meeting': 'Вероятность встречи', + }, +} + + +def translate(key, lang='de', **kwargs): + value = TRANSLATIONS.get(key, {}) + text = value.get(lang, value.get('en', key)) + return text.format(**kwargs) + + +def label(key, lang='de'): + return COLUMN_LABELS.get(lang, {}).get(key, key) + + +def stage_label(stage, lang='de'): + return ROUND_NAMES.get(lang, ROUND_NAMES['en']).get(stage, stage) + + +def display_team_name(team, lang='de'): + return GERMANY_DISPLAY.get(lang, team) if team == 'Germany' else team + + +def format_stage_line(team, r16, qf, sf, final, winner, lang='de'): + return translate('team_phase_line', lang, team=team, r16=r16, qf=qf, sf=sf, final=final, winner=winner) + + +def format_reaches_line(team, stage, value, lang='de'): + return translate('germany_reaches', lang, team=team, stage=stage, value=value) + + +def format_possible_opponents(team, lang='de'): + return translate('possible_opponents', lang, team=team) + + + +def load_teams(path="teams.csv"): + df = pd.read_csv(path) + teams = [urllib.parse.unquote(t) for t in df['Team'].dropna().tolist()] + return teams + + +def make_default_groups(teams): + # 12 groups A-L, 4 teams each + groups = {} + letters = [chr(ord('A') + i) for i in range(12)] + idx = 0 + for g in letters: + groups[g] = teams[idx:idx+4] + idx += 4 + return groups + + +TEAM_STAGE_ODDS = { + "Spain": {"win": 5.50, "final": 3.30, "semi": 2.20, "quarter": 1.70, "round16": 1.27}, + "France": {"win": 5.50, "final": 3.50, "semi": 2.35, "quarter": 1.75, "round16": 1.30}, + "England": {"win": 8.00, "final": 4.00, "semi": 2.70, "quarter": 1.80, "round16": 1.28}, + "Brazil": {"win": 9.00, "final": 4.80, "semi": 3.00, "quarter": 1.90, "round16": 1.40}, + "Argentina": {"win": 10.00, "final": 5.00, "semi": 3.10, "quarter": 2.00, "round16": 1.40}, + "Portugal": {"win": 12.00, "final": 6.00, "semi": 3.40, "quarter": 2.25, "round16": 1.40}, + "Germany": {"win": 15.00, "final": 6.00, "semi": 3.80, "quarter": 2.35, "round16": 1.40}, + "Netherlands": {"win": 25.00, "final": 10.00, "semi": 4.80, "quarter": 2.80, "round16": 1.75}, + "Norway": {"win": 30.00, "final": 13.00, "semi": 5.00, "quarter": 2.80, "round16": 1.80}, + "Belgium": {"win": 40.00, "final": 15.00, "semi": 6.50, "quarter": 3.00, "round16": 1.80}, + "Colombia": {"win": 40.00, "final": 15.00, "semi": 7.00, "quarter": 3.20, "round16": 1.95}, + "Japan": {"win": 50.00, "final": 25.00, "semi": 10.00, "quarter": 4.50, "round16": 2.30}, + "Morocco": {"win": 50.00, "final": 20.00, "semi": 9.00, "quarter": 4.50, "round16": 2.00}, + "Uruguay": {"win": 60.00, "final": 20.00, "semi": 9.00, "quarter": 4.30, "round16": 2.00}, + "United States": {"win": 60.00, "final": 20.00, "semi": 9.00, "quarter": 4.30, "round16": 2.10}, + "Mexico": {"win": 80.00, "final": 25.00, "semi": 11.00, "quarter": 5.00, "round16": 2.10}, + "Croatia": {"win": 80.00, "final": 25.00, "semi": 12.00, "quarter": 5.00, "round16": 2.30}, + "Switzerland": {"win": 80.00, "final": 25.00, "semi": 12.00, "quarter": 5.00, "round16": 1.95}, + "Turkey": {"win": 80.00, "final": 25.00, "semi": 12.00, "quarter": 5.00, "round16": 2.20}, + "Ecuador": {"win": 100.00, "final": 35.00, "semi": 13.00, "quarter": 6.00, "round16": 2.40}, + "Senegal": {"win": 100.00, "final": 40.00, "semi": 15.00, "quarter": 7.00, "round16": 3.20}, + "Sweden": {"win": 100.00, "final": 40.00, "semi": 17.00, "quarter": 7.50, "round16": 3.30}, + "Austria": {"win": 125.00, "final": 40.00, "semi": 18.00, "quarter": 7.00, "round16": 3.20}, + "Paraguay": {"win": 150.00, "final": 50.00, "semi": 20.00, "quarter": 7.50, "round16": 3.20}, + "Canada": {"win": 150.00, "final": 50.00, "semi": 17.00, "quarter": 7.00, "round16": 3.10}, + "Bosnia and Herzegovina": {"win": 250.00, "final": 80.00, "semi": 25.00, "quarter": 9.00, "round16": 3.50}, + "Scotland": {"win": 250.00, "final": 70.00, "semi": 25.00, "quarter": 9.00, "round16": 3.40}, + "Czech Republic": {"win": 250.00, "final": 70.00, "semi": 30.00, "quarter": 9.00, "round16": 3.40}, + "Ivory Coast": {"win": 300.00, "final": 80.00, "semi": 30.00, "quarter": 9.00, "round16": 3.30}, + "Egypt": {"win": 300.00, "final": 80.00, "semi": 30.00, "quarter": 9.00, "round16": 3.30}, + "Ghana": {"win": 400.00, "final": 100.00, "semi": 30.00, "quarter": 10.00, "round16": 4.00}, + "South Korea": {"win": 400.00, "final": 100.00, "semi": 35.00, "quarter": 10.00, "round16": 4.00}, + "Algeria": {"win": 400.00, "final": 100.00, "semi": 30.00, "quarter": 10.00, "round16": 4.00}, + "Tunisia": {"win": 500.00, "final": 150.00, "semi": 40.00, "quarter": 12.00, "round16": 4.50}, + "Australia": {"win": 500.00, "final": 150.00, "semi": 40.00, "quarter": 12.00, "round16": 4.50}, + "Iran": {"win": 500.00, "final": 150.00, "semi": 40.00, "quarter": 12.00, "round16": 4.50}, + "DR Congo": {"win": 750.00, "final": 200.00, "semi": 50.00, "quarter": 15.00, "round16": 6.00}, + "South Africa": {"win": 1000.00, "final": 250.00, "semi": 60.00, "quarter": 15.00, "round16": 5.50}, + "Qatar": {"win": 1000.00, "final": 250.00, "semi": 60.00, "quarter": 15.00, "round16": 5.50}, + "Saudi Arabia": {"win": 1000.00, "final": 250.00, "semi": 60.00, "quarter": 15.00, "round16": 5.50}, + "Panama": {"win": 1500.00, "final": 300.00, "semi": 70.00, "quarter": 20.00, "round16": 8.00}, + "New Zealand": {"win": 1500.00, "final": 300.00, "semi": 70.00, "quarter": 20.00, "round16": 7.00}, + "Iraq": {"win": 1500.00, "final": 300.00, "semi": 70.00, "quarter": 20.00, "round16": 9.00}, + "Cape Verde": {"win": 2000.00, "final": 400.00, "semi": 100.00, "quarter": 40.00, "round16": 15.00}, + "Curacao": {"win": 2000.00, "final": 400.00, "semi": 100.00, "quarter": 40.00, "round16": 17.00}, + "Uzbekistan": {"win": 2000.00, "final": 400.00, "semi": 100.00, "quarter": 40.00, "round16": 15.00}, + "Jordan": {"win": 2500.00, "final": 500.00, "semi": 150.00, "quarter": 50.00, "round16": 15.00}, + "Haiti": {"win": 3000.00, "final": 750.00, "semi": 350.00, "quarter": 100.00, "round16": 25.00}, +} + + +def compute_team_strengths(stage_odds): + strengths = {} + for team, odds in stage_odds.items(): + p = { + 'win': 1.0 / odds['win'], + 'final': 1.0 / odds['final'], + 'semi': 1.0 / odds['semi'], + 'quarter': 1.0 / odds['quarter'], + 'round16': 1.0 / odds['round16'], + } + strengths[team] = ( + p['win'] * 0.30 + + p['final'] * 0.25 + + p['semi'] * 0.20 + + p['quarter'] * 0.15 + + p['round16'] * 0.10 + ) + mean_strength = np.mean(list(strengths.values())) + if mean_strength <= 0: + mean_strength = 1.0 + for team in strengths: + strengths[team] /= mean_strength + return strengths + + +def simulate_group(group_teams, rng, strengths): + # round-robin for 4 teams: each pair plays once + pts = {t: 0 for t in group_teams} + gd = {t: 0 for t in group_teams} + gf = {t: 0 for t in group_teams} + cards = {t: 0 for t in group_teams} # conduct penalty points (negative) + match_records = {t: {'wins': 0, 'draws': 0, 'losses': 0} for t in group_teams} + matches = [] + for i in range(len(group_teams)): + for j in range(i + 1, len(group_teams)): + a = group_teams[i] + b = group_teams[j] + pa = strengths.get(a, 1.0) + pb = strengths.get(b, 1.0) + total = pa + pb + a_lambda = 1.3 * (pa / total) + 0.6 + b_lambda = 1.3 * (pb / total) + 0.6 + sa = int(rng.poisson(a_lambda)) + sb = int(rng.poisson(b_lambda)) + sa = min(max(sa, 0), 6) + sb = min(max(sb, 0), 6) + # simulate simple card events + ya = int(rng.integers(0, 3)) + yb = int(rng.integers(0, 3)) + ra = int(rng.random() < 0.02) + rb = int(rng.random() < 0.02) + + def penalty(y, r): + pen = -1 * y + if r: + pen += -4 + return pen + + gf[a] += sa + gf[b] += sb + gd[a] += sa - sb + gd[b] += sb - sa + if sa > sb: + pts[a] += 3 + match_records[a]['wins'] += 1 + match_records[b]['losses'] += 1 + elif sb > sa: + pts[b] += 3 + match_records[b]['wins'] += 1 + match_records[a]['losses'] += 1 + else: + pts[a] += 1 + pts[b] += 1 + match_records[a]['draws'] += 1 + match_records[b]['draws'] += 1 + cards[a] += penalty(ya, ra) + cards[b] += penalty(yb, rb) + matches.append((a, b, sa, sb, ya, ra, yb, rb)) + + teams_sorted = list(group_teams) + return teams_sorted, {'pts': pts, 'gd': gd, 'gf': gf, 'cards': cards, 'matches': matches}, match_records + + +def run_single_tournament(groups, rng, strengths, annex_mapping=None, team_rankings=None): + if team_rankings is None: + team_rankings = strengths + + group_winners = {} + group_runners = {} + group_thirds = {} + group_stats = {} + group_records = {} + team_positions = {} + all_teams = [t for team_list in groups.values() for t in team_list] + team_stage_reached = {t: {'R16': False, 'QF': False, 'SF': False, 'Final': False, 'Winner': False} for t in all_teams} + + for g, teams in groups.items(): + _, stats, match_records = simulate_group(teams, rng, strengths) + order = rank_group(teams, stats, team_rankings=team_rankings) + group_winners[g] = order[0] + group_runners[g] = order[1] + group_thirds[g] = order[2] + group_stats[g] = stats + group_records.update(match_records) + for idx, team in enumerate(order, start=1): + team_positions[team] = idx + + team_groups = {team: g for g, team_list in groups.items() for team in team_list} + team_info = { + team: { + 'group': team_groups[team], + 'position': team_positions[team], + 'record': group_records[team], + 'route': [], + 'stage_reached': {'R32': False, 'QF': False, 'SF': False, 'Final': False}, + } + for team in all_teams + } + + third_list = list(group_thirds.items()) + def third_key(item): + g, t = item + s = group_stats[g] + return (s['pts'][t], s['gd'][t], s['gf'][t], s['cards'][t], team_rankings.get(t, 0)) + third_list.sort(key=third_key, reverse=True) + best3 = third_list[:8] + best3_teams = {g: t for g, t in best3} + + r32 = {1: (group_winners['A'], group_runners['B'])} + r32[2] = (group_winners['E'], None) + r32[3] = (group_winners['F'], group_runners['C']) + r32[4] = (group_winners['C'], group_winners['F']) + r32[5] = (group_winners['I'], None) + r32[6] = (group_runners['E'], group_runners['I']) + r32[7] = (group_winners['A'], None) + r32[8] = (group_winners['L'], None) + r32[9] = (group_winners['D'], None) + r32[10] = (group_winners['G'], None) + r32[11] = (group_runners['K'], group_runners['L']) + r32[12] = (group_winners['H'], group_runners['J']) + r32[13] = (group_winners['B'], None) + r32[14] = (group_winners['J'], group_runners['H']) + r32[15] = (group_winners['K'], None) + r32[16] = (group_runners['D'], group_runners['G']) + + if annex_mapping is None: + annex_mapping = {} + key = frozenset([g for g, _ in best3]) + ordered = annex_mapping.get(key, sorted([g for g, _ in best3])) + match_place_indices = {2: 0, 5: 1, 7: 2, 8: 3, 9: 4, 10: 5, 13: 6, 15: 7} + for matchno, idx in match_place_indices.items(): + grp_letter = ordered[idx] + team = best3_teams.get(grp_letter) + a, b = r32[matchno] + r32[matchno] = (a, team) + + r32_matches = [r32[i] for i in range(1, 17)] + def play_knockout_round(matches): + winners = [] + for a, b in matches: + if a is None or b is None: + winners.append(a or b) + continue + pa = strengths.get(a, 1.0) + pb = strengths.get(b, 1.0) + prob_a = pa / (pa + pb) + winners.append(a if rng.random() < prob_a else b) + return winners + + r16_teams = play_knockout_round(r32_matches) + r16_matches = [(r16_teams[i], r16_teams[i+1]) for i in range(0, len(r16_teams), 2)] + qf_teams = play_knockout_round(r16_matches) + qf_matches = [(qf_teams[i], qf_teams[i+1]) for i in range(0, len(qf_teams), 2)] + sf_teams = play_knockout_round(qf_matches) + sf_matches = [(sf_teams[i], sf_teams[i+1]) for i in range(0, len(sf_teams), 2)] + final_teams = play_knockout_round(sf_matches) + final_match = (final_teams[0], final_teams[1]) if len(final_teams) == 2 else (None, None) + champion = play_knockout_round([final_match])[0] if final_match[0] is not None and final_match[1] is not None else final_match[0] + + for team in set(group_winners.values()).union(group_runners.values()).union(best3_teams.values()): + if team is not None: + team_stage_reached[team]['R16'] = False + for team in r16_teams: + if team is not None: + team_stage_reached[team]['R16'] = True + for a, b in r32_matches: + if a is not None and b is not None: + desc = f'R32: {a} ({team_info[a]["group"]}{team_info[a]["position"]}) vs {b} ({team_info[b]["group"]}{team_info[b]["position"]})' + team_info[a]['route'].append(('R32', desc)) + team_info[b]['route'].append(('R32', desc)) + team_info[a]['stage_reached']['R32'] = True + team_info[b]['stage_reached']['R32'] = True + + for a, b in r16_matches: + if a is not None and b is not None: + desc = f'R16: {a} vs {b}' + team_info[a]['route'].append(('R16', desc)) + team_info[b]['route'].append(('R16', desc)) + team_info[a]['stage_reached']['R16'] = True + team_info[b]['stage_reached']['R16'] = True + + for a, b in qf_matches: + if a is not None and b is not None: + desc = f'QF: {a} vs {b}' + team_info[a]['route'].append(('QF', desc)) + team_info[b]['route'].append(('QF', desc)) + team_info[a]['stage_reached']['QF'] = True + team_info[b]['stage_reached']['QF'] = True + + for a, b in sf_matches: + if a is not None and b is not None: + desc = f'SF: {a} vs {b}' + team_info[a]['route'].append(('SF', desc)) + team_info[b]['route'].append(('SF', desc)) + team_info[a]['stage_reached']['SF'] = True + team_info[b]['stage_reached']['SF'] = True + + if final_match[0] is not None and final_match[1] is not None: + a, b = final_match + desc = f'Final: {a} vs {b}' + team_info[a]['route'].append(('Final', desc)) + team_info[b]['route'].append(('Final', desc)) + team_info[a]['stage_reached']['Final'] = True + team_info[b]['stage_reached']['Final'] = True + + for team in qf_teams: + if team is not None: + team_stage_reached[team]['QF'] = True + for team in sf_teams: + if team is not None: + team_stage_reached[team]['SF'] = True + for team in final_teams: + if team is not None: + team_stage_reached[team]['Final'] = True + if champion is not None: + team_stage_reached[champion]['Winner'] = True + + rounds = {} + for a, b in r32_matches: + if a == 'Germany' or b == 'Germany': + opp = b if a == 'Germany' else a + rounds['R32'] = opp + for a, b in r16_matches: + if a == 'Germany' or b == 'Germany': + opp = b if a == 'Germany' else a + rounds['R16'] = opp + for a, b in qf_matches: + if a == 'Germany' or b == 'Germany': + opp = b if a == 'Germany' else a + rounds['QF'] = opp + for a, b in sf_matches: + if a == 'Germany' or b == 'Germany': + opp = b if a == 'Germany' else a + rounds['SF'] = opp + if 'Germany' in final_teams: + opp = final_teams[1] if final_teams[0] == 'Germany' else final_teams[0] if final_teams[1] == 'Germany' else None + rounds['Final'] = opp + + return { + 'rounds': rounds, + 'group_records': group_records, + 'positions': team_positions, + 'stages': team_stage_reached, + 'champion': champion, + 'team_info': team_info, + } + + +def summarize_meeting_route(info, meeting_round): + lines = [f'Gruppe {info["group"]} Platz {info["position"]} ({info["record"]["wins"]}S/{info["record"]["draws"]}U/{info["record"]["losses"]}N)'] + for stage, desc in info['route']: + lines.append(desc) + if stage == meeting_round: + break + return tuple(lines) + + +def is_group_winner_route(route): + return any('Platz 1' in line for line in route[:1]) + + +def parse_group_place(route): + first_line = route[0] if route else '' + for num in ['1', '2', '3', '4']: + if f'Platz {num}' in first_line: + return int(num) + return None + + +def run_simulations(groups, n, seed=0, annex_mapping=None, team_rankings=None, strengths=None): + rng = np.random.default_rng(seed) + counters = {r: Counter() for r in ['R32', 'R16', 'QF', 'SF', 'Final']} + teams = [t for team_list in groups.values() for t in team_list] + meeting_stats = {t: defaultdict(Counter) for t in teams} + meeting_paths = {t: {r: Counter() for r in ['R32', 'R16', 'QF', 'SF', 'Final']} for t in teams} + both_group_winner_meetings = {t: 0 for t in teams} + team_stats = { + t: { + 'group_positions': Counter(), + 'group_wins': 0, + 'group_draws': 0, + 'group_losses': 0, + 'R16': 0, + 'QF': 0, + 'SF': 0, + 'Final': 0, + 'Winner': 0, + } + for t in teams + } + + for _ in range(n): + result = run_single_tournament(groups, rng, strengths, annex_mapping=annex_mapping, team_rankings=team_rankings) + for r, opp in result['rounds'].items(): + if opp is not None: + counters[r][opp] += 1 + meeting_stats['Germany'][opp][r] += 1 + meeting_stats[opp]['Germany'][r] += 1 + if result['positions']['Germany'] == 1 and result['positions'][opp] == 1: + both_group_winner_meetings[opp] += 1 + germany_route = summarize_meeting_route(result['team_info']['Germany'], r) + opp_route = summarize_meeting_route(result['team_info'][opp], r) + meeting_paths[opp][r][(germany_route, opp_route)] += 1 + for team, pos in result['positions'].items(): + team_stats[team]['group_positions'][pos] += 1 + for team, record in result['group_records'].items(): + team_stats[team]['group_wins'] += record['wins'] + team_stats[team]['group_draws'] += record['draws'] + team_stats[team]['group_losses'] += record['losses'] + for team, stage in result['stages'].items(): + if stage['R16']: + team_stats[team]['R16'] += 1 + if stage['QF']: + team_stats[team]['QF'] += 1 + if stage['SF']: + team_stats[team]['SF'] += 1 + if stage['Final']: + team_stats[team]['Final'] += 1 + if stage['Winner']: + team_stats[team]['Winner'] += 1 + + return counters, team_stats, meeting_stats, meeting_paths, both_group_winner_meetings + + +def parse_annex_c(pdf_path='FWC26_regulations_EN.pdf'): + # parse Annex C combinations into mapping: frozenset(groups)->ordered list + reader = PdfReader(pdf_path) + full = '\n'.join([p.extract_text() or '' for p in reader.pages]) + lines = full.splitlines() + comb_lines = [l.strip() for l in lines if l.strip() and l.strip().split()[0].isdigit() and '3' in l] + mapping = {} + for l in comb_lines: + parts = l.split() + # first token may be index + toks = [p for p in parts if p.startswith('3')] + groups = [t[1:] for t in toks] + mapping[frozenset(groups)] = groups + return mapping + + +def rank_group(group_teams, stats, team_rankings=None): + # Apply FIFA tie-breakers for ordering teams in a group + pts = stats['pts']; gd = stats['gd']; gf = stats['gf']; cards = stats.get('cards', {}) + + def cmp_key(team): + # We implement steps: head-to-head (simplified: not fully recomputed here), + # but to be faithful we must implement head-to-head among tied teams. + return (pts[team], gd[team], gf[team], cards.get(team,0), team_rankings.get(team,0) if team_rankings else 0) + + # For correctness we perform iterative head-to-head resolution for tied sets + teams = list(group_teams) + # sort by overall points/gd/gf first as baseline + teams.sort(key=lambda t: (pts[t], gd[t], gf[t]), reverse=True) + # handle ties by applying head-to-head between tied teams + i = 0 + final = [] + while i < len(teams): + j = i+1 + tied = [teams[i]] + while j < len(teams) and pts[teams[j]]==pts[teams[i]] and gd[teams[j]]==gd[teams[i]] and gf[teams[j]]==gf[teams[i]]: + tied.append(teams[j]); j+=1 + if len(tied)>1: + # compute head-to-head among tied teams + h_pts = {t:0 for t in tied} + h_gd = {t:0 for t in tied} + h_gf = {t:0 for t in tied} + for a,b,sa,sb, *rest in stats['matches']: + if a in tied and b in tied: + h_gf[a]+=sa; h_gf[b]+=sb + h_gd[a]+=sa-sb; h_gd[b]+=sb-sa + if sa>sb: h_pts[a]+=3 + elif sb>sa: h_pts[b]+=3 + else: h_pts[a]+=1; h_pts[b]+=1 + # sort tied by head-to-head criteria + tied.sort(key=lambda t: (h_pts[t], h_gd[t], h_gf[t], gd[t], gf[t], cards.get(t,0), team_rankings.get(t,0) if team_rankings else 0), reverse=True) + final.extend(tied) + i = j + return final + + + +def main(): + lang = st.sidebar.selectbox( + 'Language / Sprache / Langue / Idioma / Язык', + list(LANGUAGES.keys()), + format_func=lambda code: LANGUAGES[code], + index=0, + ) + st.title(translate('title', lang)) + + teams = load_teams() + strengths = compute_team_strengths(TEAM_STAGE_ODDS) + + st.sidebar.header(translate('sidebar_header', lang)) + n = st.sidebar.slider(translate('sidebar_n', lang), min_value=100, max_value=20000, value=2000, step=100) + seed = st.sidebar.number_input(translate('sidebar_seed', lang), value=0, step=1) + st.sidebar.markdown(translate('sidebar_groups_description', lang)) + st.sidebar.markdown(translate('sidebar_odds_description', lang)) + + # default groups + default_groups = make_default_groups(teams) + + st.header(translate('groups_header', lang)) + grp_df = pd.DataFrame({g: default_groups[g] for g in default_groups}) + st.dataframe(grp_df) + + st.markdown(translate('round32_scheme', lang)) + + if 'simulation_results' not in st.session_state: + st.session_state.simulation_results = None + st.session_state.simulation_seed = None + st.session_state.simulation_params = None + + run_sim = st.button(translate('button_start', lang)) + if run_sim: + actual_seed = seed if seed != 0 else random.randint(1, 2**30) + st.session_state.simulation_seed = actual_seed + st.session_state.simulation_params = {'n': n, 'seed': actual_seed} + st.session_state.simulation_results = run_simulations( + default_groups, + n, + actual_seed, + annex_mapping=None, + team_rankings=strengths, + strengths=strengths, + ) + + if st.session_state.simulation_results is not None: + if run_sim: + st.success(translate('done_message', lang)) + + counters, team_stats, meeting_stats, meeting_paths, both_group_winner_meetings = st.session_state.simulation_results + germany = team_stats['Germany'] + tab1, tab2 = st.tabs([translate('tab_opponent_details', lang), translate('tab_results', lang)]) + + with tab1: + st.subheader(translate('opponent_details_header', lang)) + possible_opponents = sorted( + [t for t in teams if t != 'Germany'], + key=lambda x: sum(meeting_stats['Germany'][x].values()), + reverse=True, + ) + opponent_options = ['Germany'] + [t for t in sorted(teams) if t != 'Germany'] + opponent = st.selectbox( + translate('select_opponent', lang), + opponent_options, + format_func=lambda t: display_team_name(t, lang), + ) + + if opponent: + meet = meeting_stats['Germany'][opponent] + total_meet = sum(meet.values()) + st.markdown(translate('germany_vs', lang, opponent=display_team_name(opponent, lang))) + if total_meet == 0: + st.write(translate('no_encounter', lang)) + else: + st.markdown( + translate('total_encounters', lang, count=total_meet, prob=f'{total_meet / n:.1%}') + ) + if both_group_winner_meetings[opponent] > 0: + st.markdown( + translate( + 'both_group_winners', + lang, + count=both_group_winner_meetings[opponent], + prob=f'{both_group_winner_meetings[opponent] / n:.1%}', + ) + ) + else: + st.markdown(translate('no_both_group_winners', lang)) + for r in ['R32', 'R16', 'QF', 'SF', 'Final']: + st.markdown( + translate( + 'stage_count', + lang, + stage=stage_label(r, lang), + count=meet[r], + prob=f'{meet[r] / n:.1%}', + ) + ) + + st.subheader(translate('possible_paths_header', lang)) + placement_rows = [] + for r in ['R32', 'R16', 'QF', 'SF', 'Final']: + if meet[r] == 0: + continue + for (germany_route, opp_route), path_count in meeting_paths[opponent][r].most_common(): + germany_winner = is_group_winner_route(germany_route) + opp_winner = is_group_winner_route(opp_route) + germany_place = parse_group_place(germany_route) + opp_place = parse_group_place(opp_route) + placement_rows.append({ + 'Round': r, + 'Germany group place': germany_place, + f'{opponent} group place': opp_place, + 'Both group winners': germany_winner and opp_winner, + 'Count': path_count, + 'Share of round': f'{path_count / meet[r]:.1%}', + 'Share of sims': f'{path_count / n:.1%}', + 'Germany path': ' → '.join(germany_route), + f'{opponent} path': ' → '.join(opp_route), + }) + + if placement_rows: + placement_df = pd.DataFrame(placement_rows) + stage_order = {'R32': 0, 'R16': 1, 'QF': 2, 'SF': 3, 'Final': 4} + placement_df['Stage'] = placement_df['Round'].map(lambda r: stage_label(r, lang)) + placement_df['Round order'] = placement_df['Round'].map(stage_order) + placement_df = placement_df[ + [ + 'Stage', + 'Round', + 'Germany group place', + f'{opponent} group place', + 'Both group winners', + 'Count', + 'Share of round', + 'Share of sims', + 'Germany path', + f'{opponent} path', + 'Round order', + ] + ] + placement_df = placement_df.sort_values( + ['Both group winners', 'Round order', 'Count'], + ascending=[False, True, False], + ) + placement_df = placement_df.drop(columns=['Round order']) + placement_df = placement_df.rename(columns=COLUMN_LABELS[lang]) + st.markdown(translate('flows_first_both_winners', lang)) + def highlight_row(row): + return [ + 'background-color: rgba(255, 0, 0, 0.5)' if row[label('Both group winners', lang)] else '' + for _ in row + ] + st.dataframe(placement_df.style.apply(highlight_row, axis=1)) + + grouping = pd.DataFrame(placement_rows).groupby( + ['Germany group place', f'{opponent} group place'] + )['Count'].sum().reset_index() + grouping['Probability of meeting'] = grouping['Count'] / total_meet + grouping = grouping.rename( + columns={ + 'Germany group place': label('Germany group place', lang), + f'{opponent} group place': label(f'{opponent} group place', lang), + 'Count': label('Count', lang), + 'Probability of meeting': label('Probability of meeting', lang), + } + ) + st.subheader(translate('meeting_by_group_place', lang)) + grouping = grouping.sort_values( + [label('Germany group place', lang), label(f'{opponent} group place', lang)] + ) + st.dataframe(grouping) + + st.subheader(translate('stage_meeting_phase', lang)) + st.markdown( + format_stage_line( + display_team_name('Germany', lang), + f'{team_stats['Germany']['R16'] / n:.1%}', + f'{team_stats['Germany']['QF'] / n:.1%}', + f'{team_stats['Germany']['SF'] / n:.1%}', + f'{team_stats['Germany']['Final'] / n:.1%}', + f'{team_stats['Germany']['Winner'] / n:.1%}', + lang=lang, + ) + ) + opp_stats = team_stats[opponent] + st.markdown( + format_stage_line( + display_team_name(opponent, lang), + f'{opp_stats['R16'] / n:.1%}', + f'{opp_stats['QF'] / n:.1%}', + f'{opp_stats['SF'] / n:.1%}', + f'{opp_stats['Final'] / n:.1%}', + f'{opp_stats['Winner'] / n:.1%}', + lang=lang, + ) + ) + st.markdown(translate('this_window_text', lang)) + + with tab2: + st.subheader(translate('germany_comparison_header', lang)) + st.markdown( + translate( + 'germany_reaches', + lang, + team=display_team_name('Germany', lang), + stage=stage_label('R16', lang), + value=f'{germany[ + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6fe0514 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +streamlit +pandas +numpy diff --git a/teams.csv b/teams.csv new file mode 100644 index 0000000..b0917db --- /dev/null +++ b/teams.csv @@ -0,0 +1,49 @@ +Team +Mexico +South Africa +South Korea +Czech Republic +Canada +Bosnia and Herzegovina +Qatar +Switzerland +Brazil +Morocco +Haiti +Scotland +United States +Paraguay +Australia +Turkey +Germany +Curacao +Ivory Coast +Ecuador +Netherlands +Japan +Sweden +Tunisia +Belgium +Egypt +Iran +New Zealand +Spain +Cape Verde +Saudi Arabia +Uruguay +France +Senegal +Iraq +Norway +Argentina +Algeria +Austria +Jordan +Portugal +DR Congo +Uzbekistan +Colombia +England +Croatia +Ghana +Panama