#!/usr/bin/env python3 """ Analyse REX - Corrélations et calibration des poids du scoring Lit historical_data + performance pour améliorer le modèle de prédiction """ import sqlite3 import json import os import math from datetime import datetime, timedelta DB_PATH = os.environ.get("DB_PATH", "/home/h3r7/turf_scraper/turf.db") # ============================================================ # UTILITAIRES STATISTIQUES # ============================================================ def moyenne(valeurs): v = [x for x in valeurs if x is not None] return sum(v) / len(v) if v else 0 def ecart_type(valeurs): v = [x for x in valeurs if x is not None] if len(v) < 2: return 0 m = moyenne(v) return math.sqrt(sum((x - m)**2 for x in v) / len(v)) def correlation_pearson(x_list, y_list): """Corrélation de Pearson entre deux listes""" pairs = [(x, y) for x, y in zip(x_list, y_list) if x is not None and y is not None] if len(pairs) < 5: return 0 xs = [p[0] for p in pairs] ys = [p[1] for p in pairs] mx, my = moyenne(xs), moyenne(ys) num = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) den = math.sqrt(sum((x - mx)**2 for x in xs) * sum((y - my)**2 for y in ys)) return round(num / den, 4) if den else 0 def taux_top5_par_segment(conn, feature, nb_segments=5): """ Découpe une feature en segments et calcule le taux top5 de chaque segment. Permet de voir si la feature est discriminante. """ c = conn.cursor() c.execute(f""" SELECT {feature}, top5 FROM historical_data WHERE {feature} IS NOT NULL AND {feature} > 0 ORDER BY {feature} ASC """) rows = c.fetchall() if len(rows) < nb_segments: return [] segment_size = len(rows) // nb_segments segments = [] for i in range(nb_segments): debut = i * segment_size fin = debut + segment_size if i < nb_segments - 1 else len(rows) seg = rows[debut:fin] vals = [r[0] for r in seg] top5s = [r[1] for r in seg] segments.append({ 'segment': i + 1, 'min': round(min(vals), 2), 'max': round(max(vals), 2), 'nb': len(seg), 'taux_top5': round(sum(top5s) / len(top5s) * 100, 1) }) return segments # ============================================================ # ANALYSES PRINCIPALES # ============================================================ def analyse_volume(conn): """Statistiques générales sur les données disponibles""" c = conn.cursor() c.execute(""" SELECT COUNT(DISTINCT date) as jours, COUNT(*) as lignes, MIN(date) as debut, MAX(date) as fin, AVG(top1) as taux_gagnant, AVG(top5) as taux_top5 FROM historical_data """) row = c.fetchone() c.execute("SELECT COUNT(DISTINCT race_name) FROM historical_data") nb_courses = c.fetchone()[0] c.execute(""" SELECT discipline, COUNT(DISTINCT date) as nb FROM historical_data GROUP BY discipline ORDER BY nb DESC """) disciplines = c.fetchall() return { 'nb_jours': row[0], 'nb_lignes': row[1], 'nb_courses': nb_courses, 'debut': row[2], 'fin': row[3], 'taux_gagnant': round((row[4] or 0) * 100, 1), 'taux_top5': round((row[5] or 0) * 100, 1), 'disciplines': disciplines, } def analyse_correlations(conn): """ Calcule la corrélation de chaque feature avec top5 et top1. Features négatives avec top5 = meilleur prédicteur (ex: cote basse → top5 élevé) """ c = conn.cursor() c.execute(""" SELECT cote_directe, cote_reference, reduction_km, forme_recente, tendance_forme, tx_victoire, tx_place, gains_carriere, gains_annee, rang_cote, ratio_cote_field, nb_courses, nb_victoires, age, driver_change, indicateur_tendance, nb_disq, top1, top5 FROM historical_data WHERE cote_directe > 0 """) rows = c.fetchall() features = [ 'cote_directe', 'cote_reference', 'reduction_km', 'forme_recente', 'tendance_forme', 'tx_victoire', 'tx_place', 'gains_carriere', 'gains_annee', 'rang_cote', 'ratio_cote_field', 'nb_courses', 'nb_victoires', 'age', 'driver_change', 'indicateur_tendance', 'nb_disq' ] top1_vals = [r[17] for r in rows] top5_vals = [r[18] for r in rows] correlations = [] for i, feat in enumerate(features): feat_vals = [r[i] for r in rows] corr_top1 = correlation_pearson(feat_vals, top1_vals) corr_top5 = correlation_pearson(feat_vals, top5_vals) correlations.append({ 'feature': feat, 'corr_top1': corr_top1, 'corr_top5': corr_top5, 'abs_top5': abs(corr_top5), }) # Trier par corrélation absolue avec top5 correlations.sort(key=lambda x: x['abs_top5'], reverse=True) return correlations def analyse_cote(conn): """Analyse détaillée de la cote comme prédicteur""" c = conn.cursor() # Taux de réussite par tranche de cote tranches = [ (0, 3, "Très favori (< 3)"), (3, 6, "Favori (3-6)"), (6, 10, "Second favori (6-10)"), (10, 20, "Outsider (10-20)"), (20, 50, "Longshot (20-50)"), (50, 999, "Outsider extrême (50+)"), ] resultats = [] for cmin, cmax, label in tranches: c.execute(""" SELECT COUNT(*) as nb, AVG(top1) as taux_gagnant, AVG(top5) as taux_top5, AVG(ordre_arrivee) as pos_moy FROM historical_data WHERE cote_directe >= ? AND cote_directe < ? AND ordre_arrivee > 0 """, (cmin, cmax)) row = c.fetchone() if row[0] > 0: resultats.append({ 'tranche': label, 'nb': row[0], 'taux_gagnant': round((row[1] or 0) * 100, 1), 'taux_top5': round((row[2] or 0) * 100, 1), 'position_moy': round(row[3] or 0, 1), }) return resultats def analyse_forme(conn): """Analyse de la forme récente comme prédicteur""" c = conn.cursor() tranches = [ (0, 1.5, "Excellente (< 1.5)"), (1.5, 3, "Bonne (1.5-3)"), (3, 5, "Moyenne (3-5)"), (5, 8, "Mauvaise (5-8)"), (8, 99, "Très mauvaise (8+)"), ] resultats = [] for fmin, fmax, label in tranches: c.execute(""" SELECT COUNT(*) as nb, AVG(top1) as taux_gagnant, AVG(top5) as taux_top5 FROM historical_data WHERE forme_recente >= ? AND forme_recente < ? AND ordre_arrivee > 0 """, (fmin, fmax)) row = c.fetchone() if row[0] > 0: resultats.append({ 'tranche': label, 'nb': row[0], 'taux_gagnant': round((row[1] or 0) * 100, 1), 'taux_top5': round((row[2] or 0) * 100, 1), }) return resultats def analyse_avis_entraineur(conn): """Impact de l'avis entraîneur""" c = conn.cursor() c.execute(""" SELECT avis_entraineur, COUNT(*) as nb, AVG(top1) as taux_gagnant, AVG(top5) as taux_top5, AVG(cote_directe) as cote_moy FROM historical_data WHERE ordre_arrivee > 0 GROUP BY avis_entraineur ORDER BY AVG(top5) DESC """) rows = c.fetchall() return [{ 'avis': r[0], 'nb': r[1], 'taux_gagnant': round((r[2] or 0) * 100, 1), 'taux_top5': round((r[3] or 0) * 100, 1), 'cote_moy': round(r[4] or 0, 1), } for r in rows] def analyse_driver_change(conn): """Impact du changement de driver""" c = conn.cursor() c.execute(""" SELECT driver_change, COUNT(*) as nb, AVG(top1) as taux_gagnant, AVG(top5) as taux_top5 FROM historical_data WHERE ordre_arrivee > 0 GROUP BY driver_change """) rows = c.fetchall() return [{ 'driver_change': 'Oui' if r[0] else 'Non', 'nb': r[1], 'taux_gagnant': round((r[2] or 0) * 100, 1), 'taux_top5': round((r[3] or 0) * 100, 1), } for r in rows] def analyse_top_chevaux(conn): """Chevaux les plus performants dans l'historique""" c = conn.cursor() c.execute(""" SELECT horse_name, COUNT(*) as nb_courses, SUM(top1) as nb_gagnant, SUM(top5) as nb_top5, AVG(cote_directe) as cote_moy FROM historical_data WHERE ordre_arrivee > 0 GROUP BY horse_name HAVING COUNT(*) >= 3 ORDER BY AVG(top5) DESC, SUM(top1) DESC LIMIT 15 """) rows = c.fetchall() return [{ 'cheval': r[0], 'nb_courses': r[1], 'nb_gagnant': r[2], 'nb_top5': r[3], 'tx_top5': round(r[3] / r[1] * 100, 1), 'cote_moy': round(r[4] or 0, 1), } for r in rows] # ============================================================ # CALIBRATION DES POIDS # ============================================================ def calibrer_poids(correlations): """ Recalcule les pondérations du scoring basé sur les corrélations REX. Les features avec plus forte corrélation absolue reçoivent plus de poids. """ # Mapping feature → critère scoring actuel feature_to_critere = { 'cote_directe': 'cote', 'forme_recente': 'forme', 'tx_victoire': 'tx_victoire', 'tx_place': 'tx_place', 'reduction_km': 'reduction_km', 'tendance_forme': 'tendance', 'rang_cote': 'cote', # corrélé à cote } # Agréger les corrélations par critère criteres = { 'cote': [], 'forme': [], 'tx_victoire': [], 'tx_place': [], 'reduction_km':[], 'tendance': [], 'avis': [0.05], # valeur fixe (avis entraîneur difficile à corréler) } for corr in correlations: critere = feature_to_critere.get(corr['feature']) if critere and critere in criteres: criteres[critere].append(corr['abs_top5']) # Moyenne par critère scores = {} for critere, vals in criteres.items(): scores[critere] = moyenne(vals) if vals else 0.01 # Normaliser pour que la somme = 100% total = sum(scores.values()) poids_calibres = {k: round(v / total * 100, 1) for k, v in scores.items()} # Poids actuels (référence) poids_actuels = { 'cote': 20.0, 'forme': 25.0, 'tx_victoire': 15.0, 'tx_place': 15.0, 'reduction_km': 10.0, 'tendance': 10.0, 'avis': 5.0, } return { 'poids_actuels': poids_actuels, 'poids_calibres': poids_calibres, 'delta': {k: round(poids_calibres.get(k, 0) - poids_actuels.get(k, 0), 1) for k in poids_actuels} } # ============================================================ # RAPPORT # ============================================================ def print_rapport(volume, correlations, cote_analyse, forme_analyse, avis_analyse, driver_analyse, top_chevaux, poids): print(f"\n{'='*65}") print(f"📊 ANALYSE REX — {datetime.now().strftime('%d/%m/%Y %H:%M')}") print(f"{'='*65}") # Volume print(f"\n📦 DONNÉES DISPONIBLES") print(f" Jours : {volume['nb_jours']}") print(f" Courses : {volume['nb_courses']}") print(f" Partants : {volume['nb_lignes']}") print(f" Période : {volume['debut']} → {volume['fin']}") print(f" Taux top5 moyen : {volume['taux_top5']}%") for disc, nb in volume['disciplines']: print(f" {disc:<20} : {nb} courses") # Corrélations print(f"\n🔗 CORRÉLATIONS FEATURES → TOP5 (triées par force)") print(f" {'FEATURE':<22} {'CORR TOP5':>10} {'CORR TOP1':>10} {'FORCE'}") print(f" {'─'*55}") for c in correlations[:10]: force = '●●●' if c['abs_top5'] > 0.15 else '●●' if c['abs_top5'] > 0.08 else '●' direction = '▼ (inverse)' if c['corr_top5'] < 0 else '▲ (direct) ' print(f" {c['feature']:<22} {c['corr_top5']:>10.4f} {c['corr_top1']:>10.4f} {force} {direction}") # Analyse cote print(f"\n🎰 TAUX DE RÉUSSITE PAR TRANCHE DE COTE") print(f" {'TRANCHE':<28} {'NB':>5} {'GAGNANT':>8} {'TOP5':>6} {'POS MOY':>8}") print(f" {'─'*60}") for t in cote_analyse: print(f" {t['tranche']:<28} {t['nb']:>5} {t['taux_gagnant']:>7.1f}% {t['taux_top5']:>5.1f}% {t['position_moy']:>8.1f}") # Analyse forme print(f"\n🏃 TAUX DE RÉUSSITE PAR FORME RÉCENTE") print(f" {'FORME':<28} {'NB':>5} {'GAGNANT':>8} {'TOP5':>6}") print(f" {'─'*50}") for t in forme_analyse: print(f" {t['tranche']:<28} {t['nb']:>5} {t['taux_gagnant']:>7.1f}% {t['taux_top5']:>5.1f}%") # Avis entraîneur print(f"\n👨‍🏫 IMPACT AVIS ENTRAÎNEUR") print(f" {'AVIS':<20} {'NB':>5} {'GAGNANT':>8} {'TOP5':>6} {'COTE MOY':>9}") print(f" {'─'*52}") for a in avis_analyse: print(f" {a['avis']:<20} {a['nb']:>5} {a['taux_gagnant']:>7.1f}% {a['taux_top5']:>5.1f}% {a['cote_moy']:>9.1f}") # Driver change print(f"\n🔄 IMPACT CHANGEMENT DE DRIVER") for d in driver_analyse: print(f" Changement {d['driver_change']:<4} : {d['nb']} courses · " f"gagnant {d['taux_gagnant']}% · top5 {d['taux_top5']}%") # Top chevaux if top_chevaux: print(f"\n🏆 TOP CHEVAUX (≥3 courses)") print(f" {'CHEVAL':<28} {'COURSES':>8} {'GAGNANT':>8} {'TOP5':>6} {'TX TOP5':>8} {'COTE MOY':>9}") print(f" {'─'*70}") for h in top_chevaux[:10]: print(f" {h['cheval']:<28} {h['nb_courses']:>8} {h['nb_gagnant']:>8} " f"{h['nb_top5']:>6} {h['tx_top5']:>7.1f}% {h['cote_moy']:>9.1f}") # Calibration poids print(f"\n⚖️ CALIBRATION DES POIDS DU SCORING") print(f" {'CRITÈRE':<16} {'ACTUEL':>8} {'CALIBRÉ':>8} {'DELTA':>8} RECOMMANDATION") print(f" {'─'*60}") for critere in poids['poids_actuels']: actuel = poids['poids_actuels'][critere] calibre = poids['poids_calibres'].get(critere, actuel) delta = poids['delta'].get(critere, 0) if delta > 2: reco = "↑ AUGMENTER" elif delta < -2: reco = "↓ RÉDUIRE" else: reco = "→ OK" print(f" {critere:<16} {actuel:>7.1f}% {calibre:>7.1f}% {delta:>+7.1f}% {reco}") nb_jours = volume['nb_jours'] if nb_jours < 30: print(f"\n⚠️ Données insuffisantes ({nb_jours} jours) — attendez 30+ jours avant d'appliquer la calibration") elif nb_jours < 100: print(f"\n✅ Données suffisantes pour calibration Phase 2 ({nb_jours} jours)") print(f" Recommandation : appliquer les nouveaux poids dans scoring.py") else: print(f"\n🚀 Données suffisantes pour ML Phase 3 ({nb_jours} jours)") print(f" Recommandation : entraîner un modèle XGBoost") print(f"\n{'='*65}\n") # ============================================================ # MAIN # ============================================================ def main(): print(f"\n{'='*65}") print(f"🧠 ANALYSE REX — {datetime.now().strftime('%d/%m/%Y %H:%M')}") print(f"{'='*65}\n") conn = sqlite3.connect(DB_PATH) # Vérifier les données c = conn.cursor() c.execute("SELECT COUNT(*) FROM historical_data") nb = c.fetchone()[0] if nb == 0: print("❌ Aucune donnée dans historical_data.") print(" Lancez d'abord : python3 historical_loader.py --days 365") conn.close() return print(f"✅ {nb} lignes trouvées dans historical_data\n") # Analyses print("📊 Calcul des statistiques...") volume = analyse_volume(conn) correlations= analyse_correlations(conn) cote_analyse= analyse_cote(conn) forme_analyse= analyse_forme(conn) avis_analyse= analyse_avis_entraineur(conn) driver_analyse= analyse_driver_change(conn) top_chevaux = analyse_top_chevaux(conn) poids = calibrer_poids(correlations) conn.close() # Afficher le rapport print_rapport(volume, correlations, cote_analyse, forme_analyse, avis_analyse, driver_analyse, top_chevaux, poids) # Sauvegarder le rapport JSON rapport = { 'date': datetime.now().isoformat(), 'volume': volume, 'correlations': correlations, 'cote_analyse': cote_analyse, 'forme_analyse': forme_analyse, 'avis_analyse': avis_analyse, 'driver_analyse': driver_analyse, 'top_chevaux': top_chevaux, 'poids_calibres': poids, } turf_dir = os.environ.get('TURF_DIR', '/home/h3r7/turf_scraper') path = f"{turf_dir}/rex_analyse_{datetime.now().strftime('%Y%m%d')}.json" with open(path, 'w', encoding='utf-8') as f: json.dump(rapport, f, indent=2, ensure_ascii=False) print(f"📁 Rapport sauvegardé : {path}") if __name__ == "__main__": main()