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 matplotlib3 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"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.confidence7 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 == predicted9 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
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
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
Lecture : la courbe est descriptive. Sur un petit échantillon, elle ne prouve pas une 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.py11 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.