DEV Community

Cover image for Quels clubs de foot dépensent le mieux ?
Maxim Cruz
Maxim Cruz

Posted on

Quels clubs de foot dépensent le mieux ?

Chaque été, les clubs de foot européens battent des records de dépenses. Et pourtant, certains finissent devant des géants en ayant investi dix fois moins. Le classement final ne dit jamais qui a bien dépensé son argent, il dit juste qui a gagné.

Alors on s'est posé la question autrement, et on en a fait un projet data complet :

Quels clubs investissent le mieux leur argent ?

Pour y répondre, on a construit un pipeline de bout en bout : ingestion, data lake en 3 couches, orchestration Airflow, calcul métier en Spark, clustering ML et dashboard Kibana. Voici comment, et surtout ce que les données racontent.

👉 Code source : https://github.com/Nassim821/football-roi-pipeline


La métrique : le ROI sportif

Tout le projet tourne autour d'une métrique qu'on a définie nous-mêmes :

ROI = points du championnat / valeur de l'effectif (en M€)
Enter fullscreen mode Exit fullscreen mode

L'idée est simple : plus un club obtient de points avec un petit effectif, plus il est efficace. À l'inverse, un club avec un effectif énorme mais peu de points gaspille.

On a appliqué ça à :

  • 5 championnats majeurs : Ligue 1, Premier League, La Liga, Bundesliga, Serie A
  • La Champions League (avec un ROI adapté basé sur le stade atteint)
  • 3 saisons : 2022, 2023, 2024

Et pour aller plus loin que la moyenne, on a ajouté du clustering K-Means afin de découvrir automatiquement des profils de clubs : Efficaces, Dominants, Standards, Gaspilleurs.


L'architecture

Le projet suit le pattern Medallion, classique en data engineering : on organise le data lake en trois couches successives (raw → formatted → usage).

Voici la stack et le rôle de chaque brique :

Étape Technologie Rôle
Ingestion requests + cloudscraper API REST + scraping HTML
Stockage JSON puis Parquet Brut traçable / Parquet typé ~10× plus rapide
Compute PySpark + pandas Spark pour les jointures, pandas pour le formatage
Orchestration Apache Airflow 3.2.1 Planification + retries + observabilité
ML scikit-learn (K-Means) Clustering non supervisé
Indexation Elasticsearch 9.0.0 Stockage analytique et recherche
Visualisation Kibana 9.0.0 Dashboard interactif
Conteneurisation Docker Compose Reproductibilité de l'environnement

Quelques choix qu'on revendique :

  • Airflow plutôt qu'un cron : pour gérer les dépendances entre tâches, relancer automatiquement en cas d'erreur, et visualiser l'avancement.
  • Parquet plutôt que CSV/JSON : format colonnaire compressé, beaucoup plus rapide à lire et plus léger.
  • Spark pour les JOINs : meilleure gestion des volumes, et le code reste lisible.
  • Elasticsearch + Kibana plutôt que Tableau/PowerBI : open source, performant pour la recherche, intégré nativement.

Les données : 3 sources combinées

Pour calculer le ROI, il nous fallait deux types d'infos : les performances (points, rang) et les valeurs marchandes (budget de l'effectif).

Source Type Donnée Volume / run
API-Football (standings) API REST Classements des 5 ligues 15 appels
Transfermarkt Scraping HTML Valeurs marchandes des effectifs 15 scrapes
API-Football (CL) API REST Matchs Champions League 3 appels

Le data lake respecte une convention de nommage stricte — chaque entité a sa place et sa version datée :

data/<layer>/<group>/<dataEntity>/<dateVersion>/
Enter fullscreen mode Exit fullscreen mode

Concrètement :

data/
├── raw/          # JSON brut, immuable
│   ├── football/standings/2022/
│   ├── transfermarkt/squad_values/2022/
│   └── champions_league/fixtures/2022/
├── formatted/    # Parquet nettoyé et typé
│   └── ...
└── usage/        # Métriques business
    ├── roi_sportif/
    ├── cl_roi/
    └── roi_clusters/
Enter fullscreen mode Exit fullscreen mode

Pourquoi 3 couches ? Parce que chacune a un rôle distinct :

  • Raw est immuable. Si l'API change ou tombe demain, on garde nos données originales pour tout recalculer.
  • Formatted est propre techniquement (bons types, dates UTC, doublons supprimés) mais sans logique métier.
  • Usage porte le cas d'usage spécifique (le calcul du ROI). D'autres cas d'usage pourraient cohabiter à ce niveau.

Au final : 312 lignes business indexées dans Elasticsearch (254 pour les championnats, 58 pour la CL, le tout enrichi par le clustering).


Le pipeline Airflow

Le DAG football_roi_pipeline enchaîne 10 tâches en 5 étapes, et tourne automatiquement chaque jour à 6h UTC.

Tâche Couche Rôle
source_to_raw_1 source → raw Classements via API-Football
source_to_raw_2 source → raw Scraping Transfermarkt
source_to_raw_3 source → raw Matchs Champions League
raw_to_formatted_1 raw → formatted JSON football → Parquet typé
raw_to_formatted_2 raw → formatted Nettoyage Transfermarkt ("€45.20m"45.2)
raw_to_formatted_3 raw → formatted Stade max atteint en CL par club
produce_usage formatted → usage JOIN Spark standings × valeurs, calcul du ROI
produce_usage_cl formatted → usage JOIN CL × valeurs, calcul du cl_roi
ml_clustering usage → usage K-Means 4 clusters + labels
index_to_elastic usage → ES Push des 3 datasets vers Elasticsearch

Un peu de machine learning

Pour le clustering, on a choisi K-Means parce que le problème est non supervisé (on n'a pas de labels au départ, on veut découvrir des groupes naturels), qu'il est simple à interpréter (un cluster = un centroïde), et qu'il est rapide (254 lignes traitées en moins d'une seconde).

Trois features, normalisées avec StandardScaler pour éviter que le budget en M€ n'écrase tout :

  • squad_value_m — la valeur de l'effectif
  • roi — le retour sur investissement sportif
  • points — les points obtenus

On a fixé k = 4 pour coller aux 4 quadrants du plan (budget, ROI) :

  • petit budget + gros ROI → Efficaces
  • gros budget + gros ROI → Dominants
  • gros budget + petit ROI → Gaspilleurs
  • petit budget + petit ROI → Standards

Le résultat :

Cluster Budget moyen ROI moyen Points moyens Nb clubs
Efficaces 85 M€ 0.53 43 119
Dominants 155 M€ 0.27 40 31
Standards 423 M€ 0.15 59 26
Gaspilleurs 980 M€ 0.08 79 104

Ce que les données racontent

Le ROI inverse complètement le palmarès. En tête : Clermont Foot (ROI ~0.82), Rayo Vallecano, Holstein Kiel, Cadiz. Que des clubs à petit budget. Ils marquent beaucoup de points par rapport à ce qu'ils ont investi — c'est ça, l'efficacité.

La ligue la plus riche est la moins efficiente. La Liga et la Ligue 1 mènent (ROI ~0.28-0.29), et la Premier League ferme la marche avec 0.10. L'inflation des salaires anglais ne se traduit pas par une supériorité proportionnelle en points.

La relation budget → points est non linéaire. Plus le budget monte, plus les points montent… mais la courbe sature au-delà de ~600 M€. C'est un argument économique fort : le rendement marginal de chaque euro investi décroît vite.

Et c'est là que le ML apporte sa vraie valeur. L'algorithme a découvert tout seul qu'il existe des seuils d'efficience : au-delà d'un certain budget, l'argent ne se transforme quasiment plus en points. Un résultat invisible sur une simple moyenne globale.


Parce qu'un pipeline qui marche du premier coup, ça n'existe pas :

1. Deadlock virtioFS sur macOS. OSError: [Errno 35] Resource deadlock avoided à chaque écriture Parquet dans Docker. Bug connu de virtioFS : le memory-mapping de PyArrow sur un bind mount ne libère jamais les locks. Fix : remplacer le bind mount de data/ par un volume Docker nommé, qui ne passe pas par virtioFS.

2. Conflit de namespace package dans Airflow 3. ModuleNotFoundError: No module named 'config.config'. Le dossier config/ était monté à deux endroits, Python le traitait comme un namespace package PEP 420 mal géré par le task runner. Fix : un simple __init__.py vide pour forcer un package régulier.

3. Timeout Elasticsearch à la création d'index. Par défaut ES crée les indices avec 1 réplique ; sur un cluster single-node, elle ne peut pas être allouée, le cluster passe en YELLOW et la création boucle jusqu'au timeout. Fix : "number_of_replicas": 0 dans les mappings.

4. Labels de clustering qui s'effondrent. 4 clusters détectés, mais seulement 2 labels attribués. Notre première logique utilisait des seuils de rangs qui se chevauchaient. Fix : une assignation 1-pour-1 basée sur la distance euclidienne aux 4 archétypes dans le plan normalisé — chaque label utilisé exactement une fois.


Bilan

Côté technique, le pipeline est complet et reproductible : 3 sources hétérogènes ingérées chaque jour, un data lake en 3 couches, 10 tâches Airflow orchestrées, un calcul métier en PySpark, un enrichissement ML, et une visualisation Kibana — le tout containerisé avec Docker.

Côté insights, trois choses ressortent :

  1. Le ROI inverse le palmarès : les « petits » clubs sont souvent les plus efficaces.
  2. La relation budget-points est non linéaire : au-delà de ~600 M€, dépenser plus ne rapporte plus grand-chose.
  3. Le ML révèle des archétypes économiques : certains clubs sont structurellement condamnés à un ROI faible par leur taille.

Limites connues : batch quotidien (pas de temps réel), plan API gratuit limité à 100 requêtes/jour, Elasticsearch en single-node.

Pistes futures : Kafka pour l'ingestion temps réel, monitoring Prometheus/Grafana, tests pytest, et extension à d'autres sports (basket, F1) pour tester la généralité de la métrique.


Le projet est entièrement sur GitHub, avec un guide d'installation et le dashboard Kibana exporté en .ndjson :

👉 https://github.com/Nassim821/football-roi-pipeline

Si vous bossez sur de la data sportive ou des pipelines Airflow, on est preneurs de retours — surtout sur la métrique ROI et la façon dont vous l'auriez définie.

Top comments (3)

Collapse
 
stiiwann_35eb8bb2cf8dc53e profile image
StiiWann

Sujet très intéressant, bravo

Collapse
 
remi_foures_036456c35af85 profile image
remi foures

super poste a partager a l'OM cette année c est la notre droit au but
🥵🥶⛈️

Collapse
 
stiiwann_35eb8bb2cf8dc53e profile image
StiiWann

T'as pas lu les dashboard toi visiblement