Système d'analyse avancée de matchs de football basé sur la Programmation Orientée Objet (POO) en Python.
Calcule automatiquement les métriques VAEP, xG, xT à partir de données réelles (FUS Rabat vs FAR).
- 📌 Présentation du projet
- 🎯 Objectifs
- 🧠 Concepts OOP utilisés
- 📐 Architecture des classes
- 📊 Diagramme UML
- 🔄 Diagramme de cas d'utilisation
- 📁 Structure du projet
- 📚 Métriques calculées
- ⚡ Installation & Lancement
- 📈 Exemple de sortie
- 👥 Auteurs
Ce projet est un mini-projet académique développé dans le cadre du module Programmation Orientée Objet (POO). Il s'appuie sur le framework VAEP (Valuing Actions by Estimating Probabilities), issu d'un article de recherche présenté à KDD 2019 et IJCAI 2020 par Tom Decroos et al.
Le système analyse les événements d'un vrai match de football (données StatsBomb) pour évaluer objectivement la contribution de chaque joueur via des métriques avancées.
Decroos, T., Bransen, L., Van Haaren, J., & Davis, J. (2020).
VAEP: An Objective Approach to Valuing On-the-Ball Actions in Soccer.
IJCAI-20 Sister Conferences Best Papers Track.
| Objectif | Description |
|---|---|
| 🏗️ Modélisation OOP | Concevoir un système orienté objet complet pour un match de football |
| ⚽ Intervenants | Modéliser tous les intervenants : joueurs, balle, actions, possessions |
| 📊 Métriques | Calculer XG, DXG, XT, DXT, VAEP pour chaque action |
| 🔢 VAEP | Implémenter VAEP = P(marquer en k actions) - P(encaisser en k actions) |
| 📁 Données réelles | Analyser les données du match FUS Rabat (StatsBomb format) |
╔══════════════════════════════════════════════════════════════╗
║ CONCEPTS POO APPLIQUÉS ║
╠══════════════════════════════════════╦═══════════════════════╣
║ Concept ║ Où dans le code ║
╠══════════════════════════════════════╬═══════════════════════╣
║ ✅ Classes & Objets ║ Tous les fichiers ║
║ ✅ Constructeur __init__ ║ Chaque classe ║
║ ✅ Encapsulation (__) ║ Joueur, Match ║
║ ✅ Héritage (super()) ║ Passe, Tir, Dribble ║
║ ✅ Polymorphisme ║ calculer_valeur() ║
║ ✅ Méthodes spéciales (__str__) ║ Toutes les classes ║
║ ✅ isinstance() ║ Possession ║
║ ✅ Abstraction ║ Action (classe mère) ║
╚══════════════════════════════════════╩═══════════════════════╝
Encapsulation — Les attributs sensibles sont privés (double underscore) :
class Joueur:
def __init__(self, id_joueur, nom, equipe):
self.__actions = [] # privé → inaccessible de l'extérieur
def get_actions(self): # getter pour y accéder proprement
return self.__actionsHéritage + Polymorphisme — Chaque type d'action redéfinit calculer_valeur() :
class Action: # classe MÈRE
def calculer_valeur(self):
return self.vaep # comportement par défaut
class Tir(Action): # classe FILLE
def calculer_valeur(self): # POLYMORPHISME : redéfinition
if self.but:
return 1.0 - self.xg # comportement spécifique au Tir
return -self.xgprojet_foot/
│
├── 📄 balle.py → Classe Balle
│ └── Attributs privés : __x, __y
│ └── Méthodes : get_position(), set_position(), en_zone_dangereuse()
│
├── 📄 joueur.py → Classe Joueur
│ └── Attributs : id_joueur, nom, equipe, position
│ └── Privé : __actions (liste des actions du joueur)
│ └── Méthodes : vaep_total(), xg_total(), ajouter_action()
│
├── 📄 action.py → Classes Action + sous-classes
│ ├── Action (classe mère abstraite)
│ ├── Passe(Action) (hérite → héritage)
│ ├── Tir(Action) (hérite → héritage)
│ └── Dribble(Action) (hérite → héritage)
│ └── Chacune redéfinit calculer_valeur() → polymorphisme
│
├── 📄 possession.py → Classe Possession
│ └── Contient une liste d'actions
│ └── Calcule VAEP, XG, probabilité marquer/encaisser
│
├── 📄 match.py → Classe Match (point central)
│ └── Charge le fichier CSV
│ └── Crée automatiquement Joueurs, Actions, Possessions
│ └── Génère les analyses et classements
│
├── 📄 main.py → Point d'entrée
└── 📊 fus_FAR.csv → Données réelles StatsBomb
┌─────────────────────────────────────────────────────────────────────────────┐
│ DIAGRAMME DE CLASSES UML │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────────────────┐
│ Balle │ │ Joueur │
├──────────────┤ ├──────────────────────────┤
│ - __x: float │ │ + id_joueur: str │
│ - __y: float │ │ + nom: str │
├──────────────┤ │ + equipe: str │
│+get_position │ │ + position: str │
│+set_position │ │ - __actions: list │
│+en_zone_dan. │ ├──────────────────────────┤
└──────────────┘ │ + vaep_total(): float │
│ + xg_total(): float │
│ + ajouter_action(action) │
└──────────────────────────┘
▲
│ utilise
┌───────────────────────────────────────┤
│ │
┌─────────┴──────────────────────┐ │
│ «abstract» │ │
│ Action │───────────────┘
├────────────────────────────────┤
│ + joueur: Joueur │
│ + timestamp: str │
│ + loc_x, loc_y: float │
│ + vaep: float │
├────────────────────────────────┤
│ + calculer_valeur(): float │
│ + distance_au_but(): float │
│ + __str__(): str │
└────────────────────────────────┘
△ héritage
┌──────────────┼──────────────────┐
│ │ │
┌───────┴──────┐ ┌─────┴──────┐ ┌────────┴───────┐
│ Passe │ │ Tir │ │ Dribble │
├──────────────┤ ├────────────┤ ├────────────────┤
│+dest_x: float│ │+xg: float │ │+reussi: bool │
│+dest_y: float│ │+but: bool │ ├────────────────┤
│+longueur:flt │ ├────────────┤ │+calculer_valeur│ ← polymorphisme
├──────────────┤ │+calculer_v.│ │+__str__() │
│+calculer_val.│ │+__str__() │ └────────────────┘
│+__str__() │ └────────────┘
└──────────────┘
┌─────────────────────────────┐ ◆ contient ┌──────────────────────────┐
│ Possession │◆────────────────────►│ Match │
├─────────────────────────────┤ ├──────────────────────────┤
│ + id_possession: str │ │ + fichier_csv: str │
│ + equipe: str │ │ - __joueurs: dict │
│ - __actions: list │ │ - __possessions: dict │
├─────────────────────────────┤ ├──────────────────────────┤
│ + calculer_VAEP(): float │ │ + analyser() │
│ + calculer_XG(): float │ │ + top_joueurs_vaep(n) │
│ + probabilite_marquer(k) │ │ + get_joueurs() │
│ + probabilite_encaisser(k) │ │ + get_possessions() │
└─────────────────────────────┘ └──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ SYSTÈME D'ANALYSE FOOTBALL │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ │ │ UC1: Charger données CSV │ │
│ │ │───────►│ UC2: Créer joueurs │ │
│ │ │ │ UC3: Créer possessions │ │
│ │ Analyste / │ │ UC4: Identifier actions │ │
│ │ Entraîneur │ └──────────────────────────────┘ │
│ │ │ │
│ │ (Acteur) │ ┌──────────────────────────────┐ │
│ │ │───────►│ UC5: Calculer VAEP │ │
│ │ │ │ UC6: Calculer xG des tirs │ │
│ │ │ │ UC7: Calculer P(marquer) │ │
│ └──────────────────┘ │ UC8: Calculer P(encaisser) │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ ┌───►│ UC9: Afficher top joueurs │ │
│ │ │ UC10: Afficher possessions │ │
│ │ │ UC11: Analyser le match │ │
│ │ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
mini-project-propre/
│
├── 📄 README.md ← Ce fichier
├── 📊 fus_FAR.csv ← Données match FUS Rabat (StatsBomb)
│
├── 🐍 balle.py ← Classe Balle (encapsulation)
├── 🐍 joueur.py ← Classe Joueur
├── 🐍 action.py ← Action + Passe, Tir, Dribble (héritage)
├── 🐍 possession.py ← Classe Possession (VAEP, xG)
├── 🐍 match.py ← Classe Match (charge CSV, analyse)
├── 🐍 main.py ← Point d'entrée principal
│
├── 🧪 test_balle.py ← Tests unitaires classe Balle
├── 🧪 test_joueur.py ← Tests unitaires classe Joueur
├── 🧪 test_action.py ← Tests unitaires Passe, Tir, Dribble
└── 🧪 test_possession.py ← Tests unitaires classe Possession
La formule fondamentale :
VAEP(action_i) = V(état_i) - V(état_i-1)
où : V(état_i) = P_scores(état_i) - P_concedes(état_i)
P_scores(état_i) = probabilité de marquer dans les k prochaines actions
P_concedes(état_i) = probabilité d'encaisser dans les k prochaines actions
Une action
+0.05→ contribue 0.05 buts en faveur de l'équipe
Une action-0.05→ risque de donner 0.05 buts à l'adversaire
| Métrique | Description | Colonne CSV |
|---|---|---|
| VAEP | Valeur totale de l'action | obv_total_net |
| VAEP offensif | Contribution offensive | obv_for_net |
| VAEP défensif | Contribution défensive | obv_against_net |
| xG | Expected Goals (probabilité de but) | statsbomb_xg |
| P(marquer k) | Probabilité marquer en k actions | calculé dynamiquement |
| P(encaisser k) | Probabilité encaisser en k actions | calculé dynamiquement |
Python 3.10+
Aucune bibliothèque externe requise (stdlib uniquement : csv, math)# 1. Cloner le projet
git clone https://github.com/votre-username/football-analytics-vaep.git
cd football-analytics-vaep
# 2. Vérifier que le CSV est présent
ls fus_FAR.csv
# 3. Lancer l'analyse
python main.pypython test_balle.py # Test classe Balle
python test_joueur.py # Test classe Joueur
python test_action.py # Test Passe, Tir, Dribble
python test_possession.py # Test Possession + VAEP✅ Tous les tests doivent s'exécuter sans erreur et sans warning
=======================================================
ANALYSE DU MATCH - FUS RABAT vs FAR
=======================================================
Joueurs detectes : 24
Possessions totales: 89
TOP 5 JOUEURS par VAEP:
-------------------------------------------------------
1. Reda Hajhouj | VAEP: +0.6461 | xG: 0.7970
2. Naoufel Zerhouni | VAEP: +0.2340 | xG: 0.1200
3. Omar Jerrari | VAEP: +0.1890 | xG: 0.0000
4. Ayoub Lakred | VAEP: -0.0120 | xG: 0.0000
5. ... | VAEP: ... | xG: ...
TOP 5 POSSESSIONS les plus dangereuses:
-------------------------------------------------------
Possession 84 - FUS Rabat: 6 actions | VAEP=0.7100 | XG=0.7900
Possession 31 - FAR : 4 actions | VAEP=0.2300 | XG=0.1500
...
Possession #84 — FUS Rabat (contre-attaque)
Action 1 : Passe (60,40)→(80,42) | VAEP = +0.00 [neutre]
Action 2 : Passe (80,42)→(95,38) | VAEP = -0.01 [légère perte]
Action 3 : Passe (95,38)→(105,36) | VAEP = +0.01 [progression]
Action 4 : Dribble (105,36) | VAEP = +0.05 [pénétration]
Action 5 : Passe (105,36)→(112,40) | VAEP = +0.09 [décisive]
Action 6 : Tir ⚽ (114,35) | VAEP = +0.64 [BUT !]
VAEP total possession : +0.78
xG du tir : 0.797
# ── Héritage ───────────────────────────────────────────────────
class Passe(Action): # Passe hérite de Action
def __init__(self, ...):
super().__init__(...) # appel du constructeur parent
# ── Polymorphisme ───────────────────────────────────────────────
p = Passe(...)
t = Tir(...)
d = Dribble(...)
for action in [p, t, d]:
print(action.calculer_valeur()) # 3 comportements DIFFÉRENTS
# même nom de méthode → polymorphisme
# ── Encapsulation ───────────────────────────────────────────────
j = Joueur("001", "Reda Hajhouj", "FUS Rabat")
# j.__actions → AttributeError : inaccessible !
j.get_actions() # ✅ accès via le getter
# ── isinstance() ────────────────────────────────────────────────
for action in possession.get_actions():
if isinstance(action, Tir):
xg_total += action.xg # traitement spécifique aux tirsLe projet répond aux exigences suivantes :
- ✅ Conception prenant en compte tous les intervenants (joueurs, balle)
- ✅ Méthodes pour calculer XG, DXG, XT, DXT
- ✅ Possession = équipe qui a la balle
- ✅ Action = succession de 2 à 5 passes
- ✅ Pour chaque action :
VAEP = P(marquer k actions) - P(encaisser k actions) - ✅ Programme Python 0 erreurs, 0 warnings
- ✅ POO privilégiée : héritage, polymorphisme, encapsulation
Chaque classe principale a son fichier de test dédié :
| Fichier | Classe testée | Ce qui est vérifié |
|---|---|---|
test_balle.py |
Balle |
Position, zone dangereuse, encapsulation |
test_joueur.py |
Joueur |
Création, liste actions vide, VAEP=0 |
test_action.py |
Passe, Tir, Dribble |
Héritage, polymorphisme, distance au but |
test_possession.py |
Possession |
VAEP total, xG, probabilités |
python test_balle.py
python test_joueur.py
python test_action.py
python test_possession.pyRésultat attendu pour chaque test :
✅ 0 erreur ✅ 0 warning ✅ Résultats corrects affichés
| Nom | Établissement |
|---|---|
| KHALID MORJAN | Faculté polydisciplinaire Khouribga -- SIIA |
Module : Programmation Orientée Objet (POO)
Année : 2025–2026
Encadrant : MOHAMED MOUJABIR