import os import json import joblib import datetime import warnings import requests import numpy as np import pandas as pd from io import StringIO from tqdm import tqdm from xgboost import XGBClassifier from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score, brier_score_loss, confusion_matrix from sklearn.pipeline import Pipeline from sklearn.multioutput import MultiOutputClassifier from sklearn.base import clone from sklearn.calibration import CalibratedClassifierCV from imblearn.over_sampling import SMOTE, ADASYN, BorderlineSMOTE from imblearn.combine import SMOTEENN, SMOTETomek from imblearn.pipeline import Pipeline as ImbPipeline import matplotlib.pyplot as plt import seaborn as sns # Ignorer les avertissements spécifiques warnings.filterwarnings("ignore", category=UserWarning) # Constantes LEAGUES = { 'F1': 'France Ligue 1', 'F2': 'France Ligue 2', 'E0': 'England Premier League', 'E1': 'Championship', 'D1': 'Germany Bundesliga', 'D2': '2. Bundesliga', 'I1': 'Italy Serie A', 'I2': 'Serie B', 'SP1': 'Spain La Liga', 'SP2': 'Segunda Division', } REQUIRED_COLS = ['Date', 'HomeTeam', 'AwayTeam', 'FTHG', 'FTAG', 'FTR'] BASE_URL = "https://www.football-data.co.uk/mmz4281" TARGET_COLUMNS = ['Home_Win', 'Away_Win', 'Draw', 'Over2.5', 'BTTS'] def fetch_football_data(): """Télécharge les données de football des 5 dernières saisons pour les ligues majeures européennes""" current_year = datetime.datetime.now().year seasons = [f"{str(y-1)[-2:]}{str(y)[-2:]}" for y in range(current_year-6, current_year)] all_data = [] for season in tqdm(seasons, desc="Chargement des saisons"): season_code = season[-4:] for league_code, league_name in LEAGUES.items(): try: url = f"{BASE_URL}/{season_code[:2]}{season_code[2:]}/{league_code}.csv" response = requests.get(url, timeout=15) response.raise_for_status() # Essayer différents encodages for encoding in ['utf-8', 'latin1', 'iso-8859-1']: try: df = pd.read_csv(StringIO(response.text), encoding=encoding, parse_dates=['Date'], dayfirst=True, on_bad_lines='warn') break except Exception: continue else: print(f"⚠️ Erreur d'encodage : {league_code} {season}") continue if not all(col in df.columns for col in REQUIRED_COLS): print(f"⚠️ Colonnes manquantes : {league_code} {season}") continue df['Season'] = season df['League'] = league_name df['League_Code'] = league_code all_data.append(df) except Exception as e: print(f"⚠️ Erreur {league_code} {season}: {str(e)}") continue if not all_data: raise ValueError("Aucune donnée valide chargée.") result_df = pd.concat(all_data, ignore_index=True).sort_values('Date') print(f"📊 Données chargées : {len(result_df)} matchs de {len(seasons)} saisons") return result_df def preprocess_data(df): """Prétraite les données et calcule des caractéristiques additionnelles""" # Nettoyer les noms de colonnes df.columns = [col.strip() for col in df.columns] # Mapping pour gérer différentes notations entre saisons mapping = { 'HST': ['HST', 'HS', 'HSTS'], # Plus de variantes pour shots 'AST': ['AST', 'AS', 'ASTS'], 'HF': ['HF', 'HomeF', 'HFauls'], # Nouvelles métriques: fautes 'AF': ['AF', 'AwayF', 'AFauls'], 'HY': ['HY', 'HomeY'], # Cartes jaunes 'AY': ['AY', 'AwayY'], 'HR': ['HR', 'HomeR'], # Cartes rouges 'AR': ['AR', 'AwayR'], 'HC': ['HC'], 'AC': ['AC'], 'B365H': ['B365H', 'BbHwin'], 'B365D': ['B365D', 'BbDwin'], 'B365A': ['B365A', 'BbAwin'], 'B365O2.5': ['B365O2.5', 'BbOver'], 'B365U2.5': ['B365U2.5', 'BbUnder'], 'B365GG': ['B365GG', 'BBBTS'] } # Harmonisation des colonnes basée sur le mapping for target, sources in mapping.items(): for col in sources: if col in df.columns: df[target] = pd.to_numeric(df[col], errors='coerce') break # Caractéristiques de base des matchs df['Goal_Diff'] = df['FTHG'] - df['FTAG'] df['Total_Goals'] = df['FTHG'] + df['FTAG'] df['Shot_Diff'] = df.get('HST', 0) - df.get('AST', 0) df['Corners_Diff'] = df.get('HC', 0) - df.get('AC', 0) df['Fouls_Diff'] = df.get('HF', 0) - df.get('AF', 0) df['Yellow_Diff'] = df.get('HY', 0) - df.get('AY', 0) df['Red_Diff'] = df.get('HR', 0) - df.get('AR', 0) # Variables cibles df['BTTS'] = ((df['FTHG'] > 0) & (df['FTAG'] > 0)).astype(int) df['Over2.5'] = (df['Total_Goals'] > 2.5).astype(int) # Caractéristiques avancées: forme récente (3 et 5 derniers matchs) for team in ['Home', 'Away']: team_col = f'{team}Team' # Moyenne des 3 derniers matchs pour diverses statistiques for stat in ['FTHG', 'FTAG', 'Total_Goals', 'BTTS', 'Over2.5']: if stat in df.columns: df[f'{team}_{stat}_Last3'] = df.groupby(team_col)[stat].transform( lambda x: x.rolling(3, min_periods=1).mean().shift(1)) # Ajouter moyenne sur 5 matchs df[f'{team}_{stat}_Last5'] = df.groupby(team_col)[stat].transform( lambda x: x.rolling(5, min_periods=1).mean().shift(1)) # Statistiques défensives if team == 'Home': df[f'{team}_Goals_Conceded_Last3'] = df.groupby(team_col)['FTAG'].transform( lambda x: x.rolling(3, min_periods=1).mean().shift(1)) df[f'{team}_Goals_Conceded_Last5'] = df.groupby(team_col)['FTAG'].transform( lambda x: x.rolling(5, min_periods=1).mean().shift(1)) else: df[f'{team}_Goals_Conceded_Last3'] = df.groupby(team_col)['FTHG'].transform( lambda x: x.rolling(3, min_periods=1).mean().shift(1)) df[f'{team}_Goals_Conceded_Last5'] = df.groupby(team_col)['FTHG'].transform( lambda x: x.rolling(5, min_periods=1).mean().shift(1)) # Statistiques de cartes for card_type in ['HY', 'AY', 'HR', 'AR']: if card_type in df.columns: prefix = card_type[0] # H ou A if (prefix == 'H' and team == 'Home') or (prefix == 'A' and team == 'Away'): df[f'{team}_Cards_Last3'] = df.groupby(team_col)[card_type].transform( lambda x: x.rolling(3, min_periods=1).mean().shift(1)) # Probabilités implicites depuis les cotes odds_columns = { 'Implied_Prob_Home': 'B365H', 'Implied_Prob_Draw': 'B365D', 'Implied_Prob_Away': 'B365A', 'Implied_Prob_Over2.5': 'B365O2.5', 'Implied_Prob_BTTS': 'B365GG' } for prob_col, odds_col in odds_columns.items(): if odds_col in df.columns: df[prob_col] = 1 / df[odds_col] else: df[prob_col] = 0.5 # Valeur par défaut si cote non disponible # Normalisation des probabilités (somme = 1 pour HDA) if all(col in df.columns for col in ['Implied_Prob_Home', 'Implied_Prob_Draw', 'Implied_Prob_Away']): total_prob = (df['Implied_Prob_Home'].fillna(0) + df['Implied_Prob_Draw'].fillna(0) + df['Implied_Prob_Away'].fillna(0)) for prob_col in ['Implied_Prob_Home', 'Implied_Prob_Draw', 'Implied_Prob_Away']: df[prob_col] = df[prob_col] / total_prob # Points et forme df['Points_Home'] = df['FTR'].map({'H': 3, 'D': 1, 'A': 0}) df['Points_Away'] = df['FTR'].map({'A': 3, 'D': 1, 'H': 0}) # Forme sur différentes périodes for period in [3, 5, 10]: df[f'Home_Form{period}'] = df.groupby('HomeTeam')['Points_Home'].transform( lambda x: x.rolling(period, min_periods=1).mean().shift(1)) df[f'Away_Form{period}'] = df.groupby('AwayTeam')['Points_Away'].transform( lambda x: x.rolling(period, min_periods=1).mean().shift(1)) # Nettoyage final df = df.dropna(subset=['FTHG', 'FTAG', 'FTR', 'Date']) df['Date'] = pd.to_datetime(df['Date'], errors='coerce') df = df.dropna(subset=['Date']).reset_index(drop=True) # Ajouter jour de la semaine et mois df['DayOfWeek'] = df['Date'].dt.dayofweek df['Month'] = df['Date'].dt.month # Classements au moment du match # Simuler un classement simple en fonction des points accumulés seasons = df['Season'].unique() leagues = df['League'].unique() for season in seasons: for league in leagues: season_league_mask = (df['Season'] == season) & (df['League'] == league) season_league_data = df[season_league_mask].sort_values('Date') # Initialiser dictionnaires pour les points et matchs joués team_points = {} team_matches = {} # Parcourir les matchs chronologiquement for idx, row in season_league_data.iterrows(): home_team = row['HomeTeam'] away_team = row['AwayTeam'] # Initialiser si équipes pas encore rencontrées for team in [home_team, away_team]: if team not in team_points: team_points[team] = 0 team_matches[team] = 0 # Enregistrer le classement avant le match df.loc[idx, 'Home_Rank'] = sorted( [(team, pts/max(1, matches)) for team, pts, matches in zip(team_points.keys(), team_points.values(), team_matches.values())], key=lambda x: x[1], reverse=True ).index((home_team, team_points[home_team]/max(1, team_matches[home_team]))) + 1 df.loc[idx, 'Away_Rank'] = sorted( [(team, pts/max(1, matches)) for team, pts, matches in zip(team_points.keys(), team_points.values(), team_matches.values())], key=lambda x: x[1], reverse=True ).index((away_team, team_points[away_team]/max(1, team_matches[away_team]))) + 1 # Mettre à jour les points après le match if row['FTR'] == 'H': team_points[home_team] += 3 elif row['FTR'] == 'A': team_points[away_team] += 3 else: # Draw team_points[home_team] += 1 team_points[away_team] += 1 # Mettre à jour les matchs joués team_matches[home_team] += 1 team_matches[away_team] += 1 # Différence de classement df['Rank_Diff'] = df['Home_Rank'] - df['Away_Rank'] return df def prepare_features(df): """Prépare les features et les cibles pour l'entraînement""" # Sélection des 5 caractéristiques à utiliser features = [ 'Shot_Diff', # Donne une idée du rapport de force offensif 'Home_Total_Goals_Last5', # Forme récente (attaque) 'Away_Total_Goals_Last5', # Forme récente (attaque) 'Home_BTTS_Last5', # Tendance à marquer + encaisser 'Implied_Prob_Home' # Sentiment des bookmakers ] # Filtrer pour ne garder que les colonnes disponibles available_features = [f for f in features if f in df.columns] print(f"Features disponibles: {len(available_features)}/{len(features)}") if len(available_features) < len(features): print(f"Features manquantes: {set(features) - set(available_features)}") X = df[available_features].copy() # Conversion et imputation X = X.apply(pd.to_numeric, errors='coerce') # Imputation plus robuste for col in X.columns: # Utiliser la médiane pour l'imputation (plus robuste que la moyenne) X[col] = X[col].fillna(X[col].median()) # Préparation des variables cibles y = pd.DataFrame({ 'Home_Win': (df['FTR'] == 'H').astype(int), 'Away_Win': (df['FTR'] == 'A').astype(int), 'Draw': (df['FTR'] == 'D').astype(int), 'Over2.5': (df['Total_Goals'] > 2.5).astype(int), 'BTTS': ((df['FTHG'] > 0) & (df['FTAG'] > 0)).astype(int) }) # Analyse des distributions des cibles for col in y.columns: positive_rate = y[col].mean() * 100 print(f"{col}: {positive_rate:.1f}% des cas") return X, y def plot_feature_importance(model, feature_names, targets, save_dir="ml/plots"): """Génère des graphiques de l'importance des features pour chaque cible""" os.makedirs(save_dir, exist_ok=True) estimators = model.named_steps['model'].estimators_ for i, (target, estimator) in enumerate(zip(targets, estimators)): # Vérifier si l'estimateur est un CalibratedClassifierCV if hasattr(estimator, 'base_estimator'): # Accéder à l'estimateur sous-jacent base_estimator = estimator.base_estimator if hasattr(base_estimator, 'feature_importances_'): importance = base_estimator.feature_importances_ else: print(f"L'estimateur pour {target} n'a pas d'attribut feature_importances_") continue elif hasattr(estimator, 'feature_importances_'): importance = estimator.feature_importances_ else: print(f"L'estimateur pour {target} n'a pas d'attribut feature_importances_") continue indices = np.argsort(importance)[::-1] # Graphique des 15 features les plus importantes plt.figure(figsize=(10, 8)) plt.title(f'Importance des features pour {target}') plt.barh(range(min(15, len(feature_names))), importance[indices][:15], align='center') plt.yticks(range(min(15, len(feature_names))), [feature_names[i] for i in indices[:15]]) plt.xlabel('Importance relative') plt.tight_layout() plt.savefig(f"{save_dir}/feature_importance_{target}.png") plt.close() def train_model(X, y): """Entraîne des modèles spécifiques pour chaque cible en utilisant XGBoost""" X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y['Draw']) # Définir un modèle de base base_model = XGBClassifier( use_label_encoder=False, eval_metric='logloss', random_state=42, n_jobs=-1 ) # Hyperparamètres à optimiser pour chaque cible param_grids = { 'Home_Win': { 'n_estimators': [300, 400, 500], 'max_depth': [3, 4, 5], 'learning_rate': [0.01, 0.03], 'subsample': [0.8, 0.9], 'colsample_bytree': [0.8, 0.9] }, 'Away_Win': { 'n_estimators': [300, 400, 500], 'max_depth': [3, 4, 5], 'learning_rate': [0.01, 0.03], 'subsample': [0.8, 0.9], 'colsample_bytree': [0.8, 0.9] }, 'Draw': { # Paramètres plus robustes pour la classe minoritaire 'n_estimators': [400, 500, 600], 'max_depth': [4, 5, 6], # Légèrement plus profond pour capturer patterns complexes 'learning_rate': [0.005, 0.01, 0.02], # Taux d'apprentissage plus faible 'subsample': [0.8, 0.9], 'colsample_bytree': [0.8, 0.9], 'scale_pos_weight': [3] # Donner plus de poids aux exemples minoritaires }, 'Over2.5': { 'n_estimators': [300, 400, 500], 'max_depth': [4, 5, 6], 'learning_rate': [0.01, 0.03], 'subsample': [0.8, 0.9], 'colsample_bytree': [0.8, 0.9] }, 'BTTS': { 'n_estimators': [300, 400, 500], 'max_depth': [4, 5, 6], 'learning_rate': [0.01, 0.03], 'subsample': [0.8, 0.9], 'colsample_bytree': [0.8, 0.9] } } # Techniques de rééquilibrage pour chaque cible resampling_methods = { 'Home_Win': SMOTE(random_state=42), 'Away_Win': SMOTE(random_state=42), 'Draw': SMOTETomek(random_state=42), # Méthode plus agressive pour la classe la plus déséquilibrée 'Over2.5': SMOTEENN(random_state=42), 'BTTS': SMOTEENN(random_state=42) } # Préparation des pipelines et modèles optimisés pour chaque cible best_params = {} estimators = [] scaler = StandardScaler() # Utiliser StandardScaler au lieu de MinMaxScaler X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) print("\n🔍 Optimisation des hyperparamètres pour chaque cible...") for target in y_train.columns: print(f"\nOptimisation pour {target}...") # Recherche en grille avec validation croisée stratifiée grid_search = GridSearchCV( estimator=base_model, param_grid=param_grids[target], cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42), scoring='roc_auc', n_jobs=-1, verbose=0 ) # Appliquer le rééquilibrage et entraîner resampler = resampling_methods[target] X_train_res, y_train_res = resampler.fit_resample(X_train_scaled, y_train[target]) # Entraîner la recherche en grille grid_search.fit(X_train_res, y_train_res) # Récupérer les meilleurs paramètres best_params[target] = grid_search.best_params_ print(f"Meilleurs paramètres pour {target}: {best_params[target]}") print(f"Meilleur score: {grid_search.best_score_:.4f}") # Créer et entraîner le modèle avec les meilleurs paramètres best_model = XGBClassifier(**best_params[target], random_state=42, use_label_encoder=False) # Entraîner avec les données rééquilibrées best_model.fit( X_train_res, y_train_res, eval_set=[(X_test_scaled, y_test[target])], early_stopping_rounds=20, verbose=False ) # Pour Draw, Over2.5 et BTTS, ajouter une calibration de probabilité if target in ['Draw', 'Over2.5', 'BTTS']: print(f"Calibration des probabilités pour {target}...") calibrated_model = CalibratedClassifierCV( best_model, method='isotonic', # ou 'sigmoid' selon le cas cv='prefit' # le modèle est déjà entraîné ) calibrated_model.fit(X_train_scaled, y_train[target]) estimators.append(calibrated_model) else: estimators.append(best_model) # Construire le pipeline complet avec les modèles optimisés multi_model = MultiOutputClassifier(base_model) multi_model.estimators_ = estimators pipeline = Pipeline([ ('scaler', scaler), ('model', multi_model) ]) # Générer les graphiques d'importance des features plot_feature_importance(pipeline, X.columns, y.columns) return pipeline, X_test, y_test, best_params def evaluate_model(model, X_test, y_test): """Évalue la performance du modèle et génère des visualisations""" try: y_pred = model.predict(X_test) y_proba = [est.predict_proba(X_test)[:, 1] for est in model.named_steps['model'].estimators_] # Créer des dossiers pour les visualisations os.makedirs("ml/plots2", exist_ok=True) # Résultats par cible proba_results = {} print("\n==== RÉSULTATS D'ÉVALUATION ====") plt.figure(figsize=(12, 10)) for i, target in enumerate(y_test.columns): proba = y_proba[i] proba_results[target] = proba print(f"\n=== Évaluation pour {target} ===") print(classification_report(y_test[target], y_pred[:, i])) # Métriques supplémentaires roc_auc = roc_auc_score(y_test[target], proba) brier = brier_score_loss(y_test[target], proba) print(f"ROC AUC: {roc_auc:.3f}") print(f"Brier Score: {brier:.3f}") # Confusion matrix cm = confusion_matrix(y_test[target], y_pred[:, i]) print(f"Matrice de confusion:\n{cm}") # Plot de la matrice de confusion plt.subplot(2, 3, i+1) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False) plt.title(f'Matrice de confusion: {target}\nAUC: {roc_auc:.3f}') plt.ylabel('Réel') plt.xlabel('Prédit') plt.tight_layout() plt.savefig("ml/plots2/confusion_matrices.png") plt.close() # Dataframe avec les probabilités prédites proba_df = pd.DataFrame(proba_results, index=X_test.index) # Identification de la prédiction avec la probabilité la plus élevée proba_df['Highest_Proba'] = proba_df[y_test.columns].idxmax(axis=1) proba_df['Highest_Proba_Value'] = proba_df[y_test.columns].max(axis=1) # Analyse des distributions de probabilités print("\n=== Probabilités moyennes ===") mean_probas = proba_df[y_test.columns].mean().sort_values(ascending=False) print(mean_probas) # Distribution des probabilités par classe plt.figure(figsize=(12, 8)) for i, target in enumerate(y_test.columns): plt.subplot(2, 3, i+1) sns.histplot(proba_df[target], bins=20, kde=True) plt.axvline(0.5, color='r', linestyle='--') plt.title(f'Distribution des probabilités: {target}') plt.xlabel('Probabilité') plt.ylabel('Fréquence') plt.tight_layout() plt.savefig("ml/plots2/probability_distributions.png") plt.close() # Calibration des probabilités plt.figure(figsize=(12, 8)) for i, target in enumerate(y_test.columns): plt.subplot(2, 3, i+1) # Regrouper les prédictions en buckets n_bins = 10 bins = np.linspace(0, 1, n_bins + 1) binned_preds = np.digitize(proba_df[target], bins) - 1 bin_accs = np.zeros(n_bins) bin_confs = np.zeros(n_bins) bin_sizes = np.zeros(n_bins) for j in range(n_bins): bin_mask = binned_preds == j if np.sum(bin_mask) > 0: bin_accs[j] = np.mean(y_test[target].values[bin_mask]) bin_confs[j] = np.mean(proba_df[target].values[bin_mask]) bin_sizes[j] = np.sum(bin_mask) # Tracer la courbe de calibration plt.plot(bin_confs, bin_accs, marker='o', linewidth=2, label='Calibration') plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Parfaite calibration') plt.title(f'Courbe de calibration: {target}') plt.xlabel('Probabilité prédite') plt.ylabel('Fréquence observée') plt.legend(loc='lower right') # Ajouter la taille des bins for j in range(n_bins): if bin_sizes[j] > 0: plt.text(bin_confs[j], bin_accs[j], f' {int(bin_sizes[j])}', ha='left', va='center', fontsize=8) plt.tight_layout() plt.savefig("ml/plots2/calibration_curves.png") plt.close() # Afficher les prédictions les plus confiantes print("\n=== Top 5 des prédictions les plus confiantes ===") print(proba_df.sort_values('Highest_Proba_Value', ascending=False).head(5)) # Analyse des erreurs les plus importantes print("\n=== Analyse des erreurs ===") errors = pd.DataFrame() for target in y_test.columns: # Identifier les faux positifs les plus confiants false_positives = proba_df[ (y_test[target] == 0) & (proba_df[target] > 0.75) ].sort_values(target, ascending=False) if not false_positives.empty: print(f"\nFaux positifs les plus confiants pour {target}:") print(false_positives.head(3)) # Identifier les faux négatifs les plus confiants false_negatives = proba_df[ (y_test[target] == 1) & (proba_df[target] < 0.25) ].sort_values(target) if not false_negatives.empty: print(f"\nFaux négatifs les plus confiants pour {target}:") print(false_negatives.head(3)) return proba_df except Exception as e: print(f"\n❌ Erreur lors de l'évaluation : {str(e)}") import traceback traceback.print_exc() return None def save_model_info(model, X, best_params, targets): """Sauvegarde le modèle et les informations associées""" os.makedirs("ml", exist_ok=True) # Sauvegarder le modèle joblib.dump(model, "ml/multi_output_model_5.joblib") # Sauvegarder les caractéristiques et les paramètres model_info = { "features": list(X.columns), "targets": targets, "best_params": best_params, "created_at": datetime.datetime.now().isoformat() } with open("ml/model_info_5.json", "w") as f: json.dump(model_info, f, indent=2) print("\n✅ Modèle sauvegardé dans ml/multi_output_model_5.joblib") print("✅ Informations du modèle sauvegardées dans ml/model_info_5.json") def predict_new_matches(model, features_df, feature_names): """Réalise des prédictions sur de nouveaux matchs""" # S'assurer que les features sont dans le bon ordre X_new = features_df[feature_names].copy() # Appliquer le modèle pour obtenir les probabilités y_proba = [est.predict_proba(X_new)[:, 1] for est in model.named_steps['model'].estimators_] # Créer le DataFrame de résultats results = pd.DataFrame() for i, target in enumerate(TARGET_COLUMNS): results[target] = y_proba[i] # Ajouter la prédiction avec la plus haute probabilité results['Highest_Proba'] = results[TARGET_COLUMNS].idxmax(axis=1) results['Highest_Proba_Value'] = results[TARGET_COLUMNS].max(axis=1) return results def main(): """Fonction principale""" try: print("🚀 Démarrage du processus d'analyse et modélisation...") # Étape 1: Téléchargement des données print("\n⏳ Téléchargement des données...") df = fetch_football_data() # Étape 2: Prétraitement print("\n🧹 Prétraitement des données...") df = preprocess_data(df) print(f"\n📊 {len(df)} matchs prêts à l'analyse.") # Étape 3: Préparation des features print("\n🔧 Préparation des features...") X, y = prepare_features(df) print(f"\nFeatures utilisées ({len(X.columns)}):") print(", ".join(X.columns)) # Étape 4: Entraînement du modèle print("\n🤖 Entraînement du modèle...") model, X_test, y_test, best_params = train_model(X, y) # Étape 5: Évaluation finale print("\n🔍 Évaluation finale...") evaluate_model(model, X_test, y_test) # Étape 6: Sauvegarde save_model_info(model, X, best_params, list(y.columns)) # Bonus: Préparer un modèle pour de futures prédictions latest_data = df.sort_values('Date').tail(100) print(f"\n📝 Les {len(latest_data)} matchs les plus récents sont disponibles pour des prédictions futures.") # Démonstration: prédire les 5 prochains matchs (simulation) print("\n🔮 Démonstration de prédiction pour les 5 derniers matchs:") sample_matches = X_test.head(5) predictions = predict_new_matches(model, sample_matches, X.columns) # Afficher HomeTeam et AwayTeam s'ils sont disponibles dans l'index if 'HomeTeam' in df.columns and 'AwayTeam' in df.columns: sample_info = df.loc[sample_matches.index, ['HomeTeam', 'AwayTeam']] predictions = pd.concat([sample_info, predictions], axis=1) print(predictions) print("\n✅ Processus terminé avec succès!") except Exception as e: print(f"\n❌ Erreur: {str(e)}") import traceback traceback.print_exc() if __name__ == "__main__": main()