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': 'Выберите соперника', }, 'select_focus_team_label': { 'de': 'Land für KO-Analyse wählen', 'en': 'Choose country for KO analysis', 'fr': 'Choisir le pays pour l’analyse KO', 'es': 'Elige país para análisis KO', 'ru': 'Выберите страну для анализа плей-офф', }, 'group_place_phrase': { 'de': 'Gruppenplatz', 'en': 'group place', 'fr': 'place en groupe', 'es': 'puesto de grupo', 'ru': 'место в группе', }, 'path_phrase': { 'de': 'Pfad', 'en': 'path', 'fr': 'parcours', 'es': 'ruta', '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': 'Эти две команды не встретились в симуляциях.', }, 'did_not_reach_stage': { 'de': '{team} hat das {stage} in den Simulationen nicht erreicht.', 'en': '{team} did not reach the {stage} in the simulations.', 'fr': '{team} n’a pas atteint le {stage} dans les simulations.', 'es': '{team} no alcanzó {stage} en las simulaciones.', 'ru': '{team} не вышла в {stage} в симуляциях.', }, '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}** симуляциях.', }, 'select_team': { 'de': 'Team auswählen', 'en': 'Select team', 'fr': 'Choisir une équipe', 'es': 'Selecciona un equipo', 'ru': 'Выберите команду', }, 'column_opponent': { 'de': 'Gegner', 'en': 'Opponent', 'fr': 'Adversaire', 'es': 'Rival', 'ru': 'Соперник', }, 'Probability': { 'de': 'Wahrscheinlichkeit', 'en': 'Probability', 'fr': 'Probabilité', 'es': 'Probabilidad', 'ru': 'Вероятность', }, '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', 'Winner': 'Sieg'}, 'en': {'R32': 'Round of 32', 'R16': 'Round of 16', 'QF': 'Quarterfinal', 'SF': 'Semifinal', 'Final': 'Final', 'Winner': 'Winner'}, 'fr': {'R32': '8es', 'R16': '8es', 'QF': 'Quarts', 'SF': 'Démis', 'Final': 'Finale', 'Winner': 'Victoire'}, 'es': {'R32': 'Octavos', 'R16': 'Octavos', 'QF': 'Cuartos', 'SF': 'Semifinales', 'Final': 'Final', 'Winner': 'Victoria'}, 'ru': {'R32': '1/8', 'R16': '1/8', 'QF': '1/4', 'SF': '1/2', 'Final': 'Финал', 'Winner': 'Победа'}, } 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, focal_team='Germany'): 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 == focal_team or b == focal_team: opp = b if a == focal_team else a rounds['R32'] = opp for a, b in r16_matches: if a == focal_team or b == focal_team: opp = b if a == focal_team else a rounds['R16'] = opp for a, b in qf_matches: if a == focal_team or b == focal_team: opp = b if a == focal_team else a rounds['QF'] = opp for a, b in sf_matches: if a == focal_team or b == focal_team: opp = b if a == focal_team else a rounds['SF'] = opp if focal_team in final_teams: opp = final_teams[1] if final_teams[0] == focal_team else final_teams[0] if final_teams[1] == focal_team 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, focal_team='Germany'): 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, focal_team=focal_team) for r, opp in result['rounds'].items(): if opp is not None: counters[r][opp] += 1 meeting_stats[focal_team][opp][r] += 1 meeting_stats[opp][focal_team][r] += 1 if result['positions'][focal_team] == 1 and result['positions'][opp] == 1: both_group_winner_meetings[opp] += 1 focal_route = summarize_meeting_route(result['team_info'][focal_team], r) opp_route = summarize_meeting_route(result['team_info'][opp], r) meeting_paths[opp][r][(focal_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, key='lang_select', ) st.title(translate('title', lang)) teams = load_teams() strengths = compute_team_strengths(TEAM_STAGE_ODDS) # choose focal team for KO analysis (defaults to Germany if present) focus_default = 'Germany' if 'Germany' in teams else teams[0] focus_team = st.sidebar.selectbox( translate('select_focus_team_label', lang), teams, index=teams.index(focus_default), format_func=lambda t: display_team_name(t, lang), key=f'focus_team_{lang}', ) 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, focal_team=focus_team, ) 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 focal_stats = team_stats[focus_team] 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 != focus_team], key=lambda x: sum(meeting_stats[focus_team][x].values()), reverse=True, ) opponent_options = [focus_team] + [t for t in sorted(teams) if t != focus_team] opponent = st.selectbox( translate('select_opponent', lang), opponent_options, format_func=lambda t: display_team_name(t, lang), key=f'opponent_select_{lang}', ) if opponent: meet = meeting_stats[focus_team][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(): focal_winner = is_group_winner_route(germany_route) opp_winner = is_group_winner_route(opp_route) focal_place = parse_group_place(germany_route) opp_place = parse_group_place(opp_route) placement_rows.append({ 'Round': r, 'Focus group place': focal_place, 'Opponent group place': opp_place, 'Both group winners': focal_winner and opp_winner, 'Count': path_count, 'Share of round': f'{path_count / meet[r]:.1%}', 'Share of sims': f'{path_count / n:.1%}', 'Focus path': ' → '.join(germany_route), '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', 'Focus group place', 'Opponent group place', 'Both group winners', 'Count', 'Share of round', 'Share of sims', 'Focus path', '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']) # prepare dynamic labels for the focus team col_map = COLUMN_LABELS.get(lang, {}).copy() # dynamic focus labels col_map['Focus group place'] = f"{display_team_name(focus_team, lang)} {translate('group_place_phrase', lang)}" col_map['Focus path'] = f"{display_team_name(focus_team, lang)} {translate('path_phrase', lang)}" placement_df = placement_df.rename(columns=col_map) 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( ['Focus group place', 'Opponent group place'] )['Count'].sum().reset_index() grouping['Probability of meeting'] = grouping['Count'] / total_meet grouping = grouping.rename( columns={ 'Focus group place': f"{display_team_name(focus_team, lang)} {translate('group_place_phrase', lang)}", 'Opponent group place': label('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( [f"{display_team_name(focus_team, lang)} {translate('group_place_phrase', lang)}", label('Opponent group place', lang)] ) st.dataframe(grouping) st.subheader(translate('stage_meeting_phase', lang)) st.markdown( format_stage_line( display_team_name(focus_team, lang), f"{team_stats[focus_team]['R16'] / n:.1%}", f"{team_stats[focus_team]['QF'] / n:.1%}", f"{team_stats[focus_team]['SF'] / n:.1%}", f"{team_stats[focus_team]['Final'] / n:.1%}", f"{team_stats[focus_team]['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)) for r in ['R16', 'QF', 'SF', 'Final', 'Winner']: st.markdown( translate( 'germany_reaches', lang, team=display_team_name(focus_team, lang), stage=stage_label(r, lang), value=f"{team_stats[focus_team][r] / n:.1%}", ) ) st.subheader(translate('possible_opponents', lang, team=display_team_name(focus_team, lang))) for r in ['R32', 'R16', 'QF', 'SF', 'Final']: cnt = counters[r] total = sum(cnt.values()) st.markdown(f"**{stage_label(r, lang)}**") if total == 0: st.write( translate( 'did_not_reach_stage', lang, team=display_team_name('Germany', lang), stage=stage_label(r, lang), ) ) continue df = pd.DataFrame([ {'Opponent': k, 'Count': v, 'Probability': v / total} for k, v in cnt.most_common() ]) df = df.rename(columns={ 'Opponent': translate('column_opponent', lang), 'Count': label('Count', lang), 'Probability': translate('Probability', lang), }) st.dataframe(df) st.subheader(translate('team_summary_header', lang)) team_rows = [] for team, stats in team_stats.items(): team_rows.append({ label('team', lang): display_team_name(team, lang), label('avg_group_wins', lang): stats['group_wins'] / n, label('avg_group_draws', lang): stats['group_draws'] / n, label('avg_group_losses', lang): stats['group_losses'] / n, label('place_1', lang): stats['group_positions'][1] / n, label('place_2', lang): stats['group_positions'][2] / n, label('place_3', lang): stats['group_positions'][3] / n, label('place_4', lang): stats['group_positions'][4] / n, label('R16', lang): stats['R16'] / n, label('QF', lang): stats['QF'] / n, label('SF', lang): stats['SF'] / n, label('Final', lang): stats['Final'] / n, label('Winner', lang): stats['Winner'] / n, }) summary_df = pd.DataFrame(team_rows).sort_values(label('R16', lang), ascending=False) st.dataframe(summary_df) team_options = [focus_team] + [t for t in sorted(teams) if t != focus_team] selected_team = st.selectbox( translate('select_team', lang), team_options, format_func=lambda t: display_team_name(t, lang), key=f'team_select_{lang}', ) if selected_team: selected = team_stats[selected_team] st.subheader( translate('detailed_overview', lang, team=display_team_name(selected_team, lang)) ) st.markdown( translate( 'group_place_line', lang, p1=f"{selected['group_positions'][1] / n:.1%}", p2=f"{selected['group_positions'][2] / n:.1%}", p3=f"{selected['group_positions'][3] / n:.1%}", p4=f"{selected['group_positions'][4] / n:.1%}", ) ) st.markdown( translate( 'average_group_results', lang, wins=f"{selected['group_wins'] / n:.2f}", draws=f"{selected['group_draws'] / n:.2f}", losses=f"{selected['group_losses'] / n:.2f}", ) ) st.markdown( translate( 'stage_stats_line', lang, r16=f"{selected['R16'] / n:.1%}", qf=f"{selected['QF'] / n:.1%}", sf=f"{selected['SF'] / n:.1%}", final=f"{selected['Final'] / n:.1%}", winner=f"{selected['Winner'] / n:.1%}", ) ) else: st.warning(translate('please_start', lang)) st.markdown(translate('final_note', lang)) if __name__ == '__main__': main()