Skip to content
Tableau de bord football

FrançaisEnglishEspañol

Tester maintenant dans Google Colab

Construisez un tableau de bord football en Python

Un parcours guidé, du client authentifié aux trois graphiques, avec du code court et copiable à chaque étape et le script complet à télécharger.

1 Résultat final

Construisez un tableau de bord football avec les prochains matchs, les marchés disponibles et l’historique vérifié de votre compétition.

Vous allez produire, en local, trois vues à partir d’une seule compétition :

  • un tableau des probabilités 1X2 avec le pick principal et la confiance ;
  • les marchés BTTS, Over/Under, double chance et DNB lorsqu’ils existent dans la réponse ;
  • un historique vérifié avec la réussite cumulée du pick publié avant le match.

Le parcours principal n’utilise que deux appels API lorsque FORES_LEAGUE_CODE est configuré : un pour les prochains matchs, un pour l’historique.

2 Prérequis

Installez le SDK officiel et matplotlib. foresportia est le SDK Python officiel de l’API ; matplotlib sert uniquement aux visualisations locales.

python -m pip install --upgrade foresportia matplotlib

3 Ajouter la clé API

Windows PowerShell (session courante) :

$env:FORES_API_KEY = "your_key"
$env:FORES_LEAGUE_CODE = "SUE"

Variable Windows persistante :

[Environment]::SetEnvironmentVariable(
    "FORES_API_KEY",
    "your_key",
    "User"
)

macOS / Linux :

export FORES_API_KEY="your_key"
export FORES_LEAGUE_CODE="SUE"
Ne placez jamais votre clé dans Git, dans un notebook public ou dans du JavaScript exécuté dans le navigateur. Gardez-la dans une variable d’environnement ou un gestionnaire de secrets.

4 Initialiser le client

ForesportiaClient.from_env() lit la clé depuis l’environnement. Le SDK gère l’authentification, les réponses typées, les quotas et les erreurs HTTP.

from foresportia import ForesportiaClient

client = ForesportiaClient.from_env()

5 Choisir la compétition

Avec une clé Developer, une seule compétition est liée à la clé. Avec Starter, plusieurs compétitions peuvent être accessibles. La fonction ci-dessous utilise FORES_LEAGUE_CODE si présent, sinon détecte l’unique compétition accessible avec list_leagues().

def choose_league(client: ForesportiaClient) -> str:
    """Use FORES_LEAGUE_CODE or detect the only accessible competition."""
    if LEAGUE_CODE:
        return LEAGUE_CODE.upper()

    leagues = client.list_leagues().data
    if len(leagues) == 1:
        return leagues[0].code
    if not leagues:
        raise RuntimeError("No competition is accessible with this key.")

    codes = ", ".join(league.code for league in leagues)
    raise RuntimeError(
        "Several competitions are accessible. "
        f"Set FORES_LEAGUE_CODE to one of: {codes}"
    )

6 Charger les prochains matchs

Un seul appel renvoie les prochains matchs de la compétition. Ici, UPCOMING_DAYS vaut 14 et UPCOMING_LIMIT vaut 5.

upcoming_response = client.list_league_matches(
    league_code,
    include="upcoming",
    days=UPCOMING_DAYS,
    limit=UPCOMING_LIMIT,
)

Chaque match expose les champs utiles au tableau de bord :

match.probabilities
match.pick
match.markets
match.confidence

7 Charger l’historique

La méthode dédiée list_league_history() renvoie l’historique vérifié de la compétition. La fenêtre s’adapte automatiquement au contrat.

history_response = client.list_league_history(league_code)
history = history_response.data
  • Developer : 7 jours ; Starter : jusqu’à 31 jours par requête sur un entitlement de 90 jours.
  • Une période peut légitimement être vide : c’est une réponse normale, pas une erreur.
  • Les probabilités et le pick sont ceux publiés avant le match.
  • status="final" et result_score fournissent le résultat vérifié.

8 Évaluer le pick 1X2

Deux fonctions courtes suffisent : actual_outcome() convertit un score final en home/draw/away, et pick_is_correct() compare le pick publié au résultat vérifié.

def actual_outcome(score: str | None) -> str | None:
    """Turn a final score such as '2-1' into home/draw/away."""
    if not score:
        return None
    try:
        home, away = map(int, score.split("-"))
    except ValueError:
        return None
    return "home" if home > away else "away" if away > home else "draw"


def pick_is_correct(match: MatchSummary) -> bool | None:
    """Compare the published 1X2 pick with the verified final score."""
    actual = actual_outcome(match.result_score) if match.is_final else None
    predicted = match.predicted_outcome
    return None if actual is None or predicted is None else actual == predicted
Cet exemple convient directement aux compétitions de ligue. Pour les coupes, vérifiez la convention de règlement utilisée pour les prolongations et les tirs au but avant de réutiliser cette comparaison.

9 Construire les trois graphiques

Probabilités 1X2

But : lire d’un coup d’œil la répartition domicile / nul / extérieur, le pick principal et la confiance.

Données : match.probabilities, match.pick, match.predicted_outcome et match.confidence des prochains matchs.

def plot_upcoming(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
    """1X2 probabilities, main pick and confidence."""
    figure, axis = plt.subplots(figsize=(13, max(4.8, len(matches) * 0.9)))
    if not matches:
        axis.axis("off")
        axis.text(0.5, 0.5, "No upcoming published match.", ha="center", va="center")
        return figure

    y = list(range(len(matches)))
    home = [m.probabilities["home"] * 100 for m in matches]
    draw = [m.probabilities["draw"] * 100 for m in matches]
    away = [m.probabilities["away"] * 100 for m in matches]

    axis.barh(y, home, label="1 · Home")
    axis.barh(y, draw, left=home, label="X · Draw")
    axis.barh(y, away, left=[h + d for h, d in zip(home, draw)], label="2 · Away")
    axis.set_yticks(y, [f"{date_label(m)} · {m.home_team} – {m.away_team}" for m in matches])
    axis.set_xlim(0, 140)
    axis.set_xticks((0, 20, 40, 60, 80, 100))
    axis.set_xlabel("1X2 probability (%)")
    axis.set_title(f"{league_name} — upcoming matches")
    axis.grid(axis="x", alpha=0.25)
    axis.invert_yaxis()
    axis.legend(ncol=3, loc="upper center", bbox_to_anchor=(0.42, -0.12))

    for index, match in enumerate(matches):
        probability = (match.pick or {}).get("probability")
        probability_text = f"{probability:.0%}" if isinstance(probability, (int, float)) else ""
        axis.text(
            102,
            index,
            f"★ {match.predicted_outcome or 'no pick'} {probability_text} · {confidence_text(match)}",
            va="center",
            fontsize=9,
        )

    figure.tight_layout()
    return figure
Probabilités 1X2 et prédictions principales pour cinq matchs de football
Probabilités 1X2 des prochains matchs, pick principal et confiance. Exemple réel généré à partir d’une compétition brésilienne. Les valeurs dépendent de la compétition et de la période accessibles avec votre clé.

Lecture : une barre longue côté domicile signale un favori net ; le pick étoilé rappelle le choix publié et sa confiance.

Marchés disponibles

But : visualiser, sous forme de matrice, les marchés réellement présents dans la réponse.

Données : match.markets, filtré sur les clés effectivement renvoyées (BTTS, Over/Under, double chance, DNB).

def plot_markets(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
    """Matrix of the markets actually present in the response."""
    available = {key for match in matches for key in (match.markets or {})}
    columns = [(key, label) for key, label in MARKETS if key in available]
    figure, axis = plt.subplots(
        figsize=(max(10, len(columns) * 1.4), max(4.8, len(matches) * 0.9))
    )

    if not matches or not columns:
        axis.axis("off")
        axis.text(0.5, 0.5, "No additional market available.", ha="center", va="center")
        return figure

    matrix = [[(m.markets or {}).get(key, float("nan")) for key, _ in columns] for m in matches]
    image = axis.imshow(matrix, aspect="auto", vmin=0, vmax=1)
    axis.set_xticks(range(len(columns)), [label for _, label in columns])
    axis.set_yticks(range(len(matches)), [f"{m.home_team} – {m.away_team}" for m in matches])
    axis.set_title(f"{league_name} — available markets")

    for row_index, row in enumerate(matrix):
        for column_index, probability in enumerate(row):
            if probability == probability:  # NaN-safe
                axis.text(column_index, row_index, f"{probability:.0%}", ha="center", va="center")

    figure.colorbar(image, ax=axis, label="Probability")
    figure.tight_layout()
    return figure
Matrice des probabilités BTTS, Over Under, double chance et DNB
Marchés réellement disponibles par match, en pourcentage. Exemple réel généré à partir d’une compétition brésilienne. Les valeurs dépendent de la compétition et de la période accessibles avec votre clé.

Lecture : une case vide signifie que le marché n’est pas fourni pour ce match, pas qu’il vaut zéro.

Historique vérifié

But : suivre la réussite cumulée du pick sur les matchs déjà joués.

Données : l’historique renvoyé par list_league_history(), limité aux matchs dont le résultat est vérifié.

def plot_history(league_name: str, history: list[MatchSummary]) -> plt.Figure:
    """Cumulative success curve with result and confidence."""
    decided = [match for match in history if pick_is_correct(match) is not None]
    figure, axis = plt.subplots(figsize=(12, 6))

    if not decided:
        axis.axis("off")
        axis.text(0.5, 0.5, "No verified result in the history window.", ha="center", va="center")
        return figure

    successes = 0
    cumulative = []
    for index, match in enumerate(decided, start=1):
        successes += int(pick_is_correct(match) is True)
        cumulative.append(successes / index)

    x = list(range(1, len(decided) + 1))
    axis.plot(x, cumulative, marker="o")
    axis.axhline(0.5, linestyle="--", linewidth=1)
    axis.set_ylim(0, 1)
    axis.set_xlabel("Historical match")
    axis.set_ylabel("Cumulative success rate")
    axis.set_title(
        f"{league_name} — history: {successes}/{len(decided)} correct "
        f"({cumulative[-1]:.1%})"
    )
    axis.grid(alpha=0.25)

    for index, match in enumerate(decided):
        verdict = "✓" if pick_is_correct(match) else "✗"
        axis.annotate(
            f"{verdict} {confidence_text(match)}\n{match.result_score}",
            (x[index], cumulative[index]),
            xytext=(0, 10),
            textcoords="offset points",
            ha="center",
            fontsize=8,
        )

    if len(decided) < 20:
        axis.text(0.01, 0.03, "Small sample: descriptive reading only.", transform=axis.transAxes)

    figure.tight_layout()
    return figure
Courbe du taux de réussite cumulé sur les résultats historiques vérifiés
Taux de réussite cumulé du pick sur l’historique vérifié. Exemple réel généré à partir d’une compétition brésilienne. Les valeurs dépendent de la compétition et de la période accessibles avec votre clé.

Lecture : la courbe est descriptive. Sur un petit échantillon, elle ne prouve pas une performance générale.

Un historique réduit reste descriptif : n’affichez jamais une métrique globale comme une preuve de performance générale.

10 Lancer le script

Exécutez le script depuis le dossier où il se trouve. SAVE_PNG contrôle l’enregistrement des images : à True, les trois graphiques sont écrits dans le dossier foresportia_charts.

python foresportia_tutorial.py

11 Code complet

Le bloc ci-dessous est le contenu intégral du fichier canonique, identique au script téléchargeable. Copiez-le ou téléchargez le .py.

Afficher le code complet
#!/usr/bin/env python3
"""Foresportia tutorial: upcoming matches, markets and verified history.

This script is the canonical source used by the Foresportia documentation
tutorial at /api/docs/tutorials/football-dashboard.html. The page code blocks
and the downloadable file are both generated from this exact file, so they
never diverge. The ``# region`` / ``# endregion`` markers are non-functional
comments used by the documentation generator to slice out each step.

Install:
    python -m pip install --upgrade foresportia matplotlib

API key — Windows PowerShell:
    $env:FORES_API_KEY = "your_key"                          # current session
    [Environment]::SetEnvironmentVariable(
        "FORES_API_KEY", "your_key", "User"
    )                                                        # persistent

API key — macOS / Linux:
    export FORES_API_KEY="your_key"                          # current session
    echo 'export FORES_API_KEY="your_key"' >> ~/.zshrc       # macOS/zsh
    echo 'export FORES_API_KEY="your_key"' >> ~/.bashrc      # Linux/bash

To keep the main path at two API calls, also set the competition:
    $env:FORES_LEAGUE_CODE = "SUE"       # Windows PowerShell
    export FORES_LEAGUE_CODE="SUE"       # macOS / Linux

Without FORES_LEAGUE_CODE, a single-competition Developer key can be detected
with list_leagues(), at the cost of one extra request.

Run:
    python foresportia_tutorial.py
"""

from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path

import matplotlib.pyplot as plt
from foresportia import ForesportiaAPIError, ForesportiaClient, ForesportiaRateLimitError
from foresportia.models import MatchSummary

LEAGUE_CODE = os.getenv("FORES_LEAGUE_CODE")
UPCOMING_DAYS = 14
UPCOMING_LIMIT = 5
SAVE_PNG = False
OUTPUT_DIR = Path("foresportia_charts")

MARKETS = (
    ("btts", "BTTS"),
    ("over_2_5", "Over 2.5"),
    ("under_2_5", "Under 2.5"),
    ("double_chance_1x", "1X"),
    ("double_chance_x2", "X2"),
    ("double_chance_12", "12"),
    ("dnb_home", "DNB 1"),
    ("dnb_away", "DNB 2"),
)


# region choose-league
def choose_league(client: ForesportiaClient) -> str:
    """Use FORES_LEAGUE_CODE or detect the only accessible competition."""
    if LEAGUE_CODE:
        return LEAGUE_CODE.upper()

    leagues = client.list_leagues().data
    if len(leagues) == 1:
        return leagues[0].code
    if not leagues:
        raise RuntimeError("No competition is accessible with this key.")

    codes = ", ".join(league.code for league in leagues)
    raise RuntimeError(
        "Several competitions are accessible. "
        f"Set FORES_LEAGUE_CODE to one of: {codes}"
    )
# endregion choose-league


# region evaluate
def actual_outcome(score: str | None) -> str | None:
    """Turn a final score such as '2-1' into home/draw/away."""
    if not score:
        return None
    try:
        home, away = map(int, score.split("-"))
    except ValueError:
        return None
    return "home" if home > away else "away" if away > home else "draw"


def pick_is_correct(match: MatchSummary) -> bool | None:
    """Compare the published 1X2 pick with the verified final score."""
    actual = actual_outcome(match.result_score) if match.is_final else None
    predicted = match.predicted_outcome
    return None if actual is None or predicted is None else actual == predicted
# endregion evaluate


def confidence_text(match: MatchSummary) -> str:
    """Show the badge when present, otherwise the confidence score."""
    confidence = match.confidence or {}
    if confidence.get("label"):
        return str(confidence["label"])
    if confidence.get("badge"):
        return str(confidence["badge"])
    score = confidence.get("score")
    return f"confidence {score:.2f}" if isinstance(score, (int, float)) else "not provided"


def date_label(match: MatchSummary) -> str:
    kickoff = match.kickoff_local or match.kickoff
    try:
        return datetime.fromisoformat(kickoff.replace("Z", "+00:00")).strftime("%d/%m")
    except (AttributeError, ValueError):
        return "--/--"


# region plot-upcoming
def plot_upcoming(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
    """1X2 probabilities, main pick and confidence."""
    figure, axis = plt.subplots(figsize=(13, max(4.8, len(matches) * 0.9)))
    if not matches:
        axis.axis("off")
        axis.text(0.5, 0.5, "No upcoming published match.", ha="center", va="center")
        return figure

    y = list(range(len(matches)))
    home = [m.probabilities["home"] * 100 for m in matches]
    draw = [m.probabilities["draw"] * 100 for m in matches]
    away = [m.probabilities["away"] * 100 for m in matches]

    axis.barh(y, home, label="1 · Home")
    axis.barh(y, draw, left=home, label="X · Draw")
    axis.barh(y, away, left=[h + d for h, d in zip(home, draw)], label="2 · Away")
    axis.set_yticks(y, [f"{date_label(m)} · {m.home_team} – {m.away_team}" for m in matches])
    axis.set_xlim(0, 140)
    axis.set_xticks((0, 20, 40, 60, 80, 100))
    axis.set_xlabel("1X2 probability (%)")
    axis.set_title(f"{league_name} — upcoming matches")
    axis.grid(axis="x", alpha=0.25)
    axis.invert_yaxis()
    axis.legend(ncol=3, loc="upper center", bbox_to_anchor=(0.42, -0.12))

    for index, match in enumerate(matches):
        probability = (match.pick or {}).get("probability")
        probability_text = f"{probability:.0%}" if isinstance(probability, (int, float)) else ""
        axis.text(
            102,
            index,
            f"★ {match.predicted_outcome or 'no pick'} {probability_text} · {confidence_text(match)}",
            va="center",
            fontsize=9,
        )

    figure.tight_layout()
    return figure
# endregion plot-upcoming


# region plot-markets
def plot_markets(league_name: str, matches: list[MatchSummary]) -> plt.Figure:
    """Matrix of the markets actually present in the response."""
    available = {key for match in matches for key in (match.markets or {})}
    columns = [(key, label) for key, label in MARKETS if key in available]
    figure, axis = plt.subplots(
        figsize=(max(10, len(columns) * 1.4), max(4.8, len(matches) * 0.9))
    )

    if not matches or not columns:
        axis.axis("off")
        axis.text(0.5, 0.5, "No additional market available.", ha="center", va="center")
        return figure

    matrix = [[(m.markets or {}).get(key, float("nan")) for key, _ in columns] for m in matches]
    image = axis.imshow(matrix, aspect="auto", vmin=0, vmax=1)
    axis.set_xticks(range(len(columns)), [label for _, label in columns])
    axis.set_yticks(range(len(matches)), [f"{m.home_team} – {m.away_team}" for m in matches])
    axis.set_title(f"{league_name} — available markets")

    for row_index, row in enumerate(matrix):
        for column_index, probability in enumerate(row):
            if probability == probability:  # NaN-safe
                axis.text(column_index, row_index, f"{probability:.0%}", ha="center", va="center")

    figure.colorbar(image, ax=axis, label="Probability")
    figure.tight_layout()
    return figure
# endregion plot-markets


# region plot-history
def plot_history(league_name: str, history: list[MatchSummary]) -> plt.Figure:
    """Cumulative success curve with result and confidence."""
    decided = [match for match in history if pick_is_correct(match) is not None]
    figure, axis = plt.subplots(figsize=(12, 6))

    if not decided:
        axis.axis("off")
        axis.text(0.5, 0.5, "No verified result in the history window.", ha="center", va="center")
        return figure

    successes = 0
    cumulative = []
    for index, match in enumerate(decided, start=1):
        successes += int(pick_is_correct(match) is True)
        cumulative.append(successes / index)

    x = list(range(1, len(decided) + 1))
    axis.plot(x, cumulative, marker="o")
    axis.axhline(0.5, linestyle="--", linewidth=1)
    axis.set_ylim(0, 1)
    axis.set_xlabel("Historical match")
    axis.set_ylabel("Cumulative success rate")
    axis.set_title(
        f"{league_name} — history: {successes}/{len(decided)} correct "
        f"({cumulative[-1]:.1%})"
    )
    axis.grid(alpha=0.25)

    for index, match in enumerate(decided):
        verdict = "✓" if pick_is_correct(match) else "✗"
        axis.annotate(
            f"{verdict} {confidence_text(match)}\n{match.result_score}",
            (x[index], cumulative[index]),
            xytext=(0, 10),
            textcoords="offset points",
            ha="center",
            fontsize=8,
        )

    if len(decided) < 20:
        axis.text(0.01, 0.03, "Small sample: descriptive reading only.", transform=axis.transAxes)

    figure.tight_layout()
    return figure
# endregion plot-history


def main() -> int:
    try:
        with ForesportiaClient.from_env(timeout=20.0, max_retries=0) as client:
            league_code = choose_league(client)
            # region upcoming-call
            upcoming_response = client.list_league_matches(
                league_code,
                include="upcoming",
                days=UPCOMING_DAYS,
                limit=UPCOMING_LIMIT,
            )
            # endregion upcoming-call
            # region history-call
            history_response = client.list_league_history(league_code)
            # endregion history-call

        upcoming = upcoming_response.data[:UPCOMING_LIMIT]
        history = history_response.data
        reference = upcoming[0] if upcoming else history[0] if history else None
        league_name = f"{reference.league.name} ({reference.league.code})" if reference else league_code

        decided = [match for match in history if pick_is_correct(match) is not None]
        successes = sum(pick_is_correct(match) is True for match in decided)
        print(f"\n{league_name}")
        print(f"Upcoming matches: {len(upcoming)}")
        print(f"History received: {len(history)}")
        if decided:
            print(f"Success: {successes}/{len(decided)} ({successes / len(decided):.1%})")
        print("History window:", history_response.payload.get("date_range"))
        print("Entitlement:", history_response.history_entitlement_days, "day(s)")

        figures = [
            ("01_upcoming_1x2.png", plot_upcoming(league_name, upcoming)),
            ("02_markets.png", plot_markets(league_name, upcoming)),
            ("03_history.png", plot_history(league_name, history)),
        ]

        if SAVE_PNG:
            OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
            for filename, figure in figures:
                path = OUTPUT_DIR / filename
                figure.savefig(path, dpi=170, bbox_inches="tight", pad_inches=0.2)
                print("Saved:", path.resolve())

        plt.show()
        return 0

    except ForesportiaRateLimitError as error:
        print("Temporary quota reached. Retry-After:", error.retry_after)
        return 2
    except ForesportiaAPIError as error:
        print("API error:", error.status_code, error.error_code, error.endpoint)
        return 1
    except RuntimeError as error:
        print("Error:", error)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

Créez votre clé Developer

Developer est gratuit et utilise les mêmes endpoints que Starter. Activez une clé, définissez FORES_API_KEY, puis relancez le script.

Pour aller plus loin