- Créer une arborescence propre (src/, scripts/, config/, data/, docs/, tests/) - Déplacer les modules Python dans src/ - Déplacer les scripts autonomes dans scripts/ - Nettoyer les fichiers temporaires et __pycache__ - Mettre à jour le README.md avec documentation complète - Mettre à jour les imports dans les scripts pour la nouvelle structure - Configurer le .gitignore pour ignorer les données et logs - Organiser les données dans data/ (courses, resultats, clubs, exports) Structure du projet: - src/: Modules principaux (ffa_scraper, ffa_analyzer) - scripts/: Scripts CLI et utilitaires - config/: Configuration (config.env) - data/: Données générées - docs/: Documentation - tests/: Tests unitaires 💘 Generated with Crush Assisted-by: GLM-4.7 via Crush <crush@charm.land>
361 lines
12 KiB
Python
Executable File
361 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Script pour générer un récapitulatif complet d'un athlète
|
|
Utilise les fichiers CSV pour extraire toutes les informations
|
|
"""
|
|
|
|
import pandas as pd
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import logging
|
|
from datetime import datetime
|
|
from collections import defaultdict
|
|
import re
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
def parse_time(time_str):
|
|
"""Convertir un temps en secondes pour comparaison"""
|
|
if not time_str or pd.isna(time_str):
|
|
return None
|
|
|
|
# Format HH:MM:SS
|
|
if ':' in time_str:
|
|
parts = time_str.split(':')
|
|
if len(parts) == 3:
|
|
try:
|
|
h, m, s = parts
|
|
return int(h) * 3600 + int(m) * 60 + float(s)
|
|
except:
|
|
pass
|
|
elif len(parts) == 2:
|
|
try:
|
|
m, s = parts
|
|
return int(m) * 60 + float(s)
|
|
except:
|
|
pass
|
|
|
|
# Format en secondes ou minutes
|
|
try:
|
|
# Enlever les non-numériques
|
|
clean = re.sub(r'[^\d.,]', '', str(time_str))
|
|
return float(clean.replace(',', '.'))
|
|
except:
|
|
return None
|
|
|
|
def format_time(seconds):
|
|
"""Formater des secondes en HH:MM:SS"""
|
|
if not seconds:
|
|
return "N/A"
|
|
|
|
hours = int(seconds // 3600)
|
|
minutes = int((seconds % 3600) // 60)
|
|
secs = seconds % 60
|
|
|
|
if hours > 0:
|
|
return f"{hours}:{minutes:02d}:{secs:05.2f}"
|
|
elif minutes > 0:
|
|
return f"{minutes}:{secs:05.2f}"
|
|
else:
|
|
return f"{secs:.2f}s"
|
|
|
|
def get_athlete_summary(nom, prenom=None, data_dir="data"):
|
|
"""Générer un récapitulatif complet d'un athlète"""
|
|
results_path = os.path.join(data_dir, 'resultats', 'results.csv')
|
|
courses_path = os.path.join(data_dir, 'courses', 'courses_list.csv')
|
|
|
|
if not os.path.exists(results_path):
|
|
logging.error(f"Fichier de résultats introuvable: {results_path}")
|
|
return None
|
|
|
|
try:
|
|
df_results = pd.read_csv(results_path, encoding='utf-8-sig')
|
|
|
|
# Filtre par nom
|
|
mask = df_results['nom'].str.contains(nom, case=False, na=False)
|
|
if prenom:
|
|
mask &= df_results['prenom'].str.contains(prenom, case=False, na=False)
|
|
|
|
results = df_results[mask]
|
|
|
|
if results.empty:
|
|
return None
|
|
|
|
# Charger les courses pour plus d'info
|
|
courses_df = None
|
|
if os.path.exists(courses_path):
|
|
courses_df = pd.read_csv(courses_path, encoding='utf-8-sig')
|
|
|
|
# Créer le récapitulatif
|
|
summary = {
|
|
'nom': results.iloc[0]['nom'],
|
|
'prenom': results.iloc[0]['prenom'],
|
|
'club': results.iloc[0]['club'],
|
|
'total_courses': len(results),
|
|
'categories': sorted(results['categorie'].dropna().unique().tolist()),
|
|
'results': [],
|
|
'statistics': {}
|
|
}
|
|
|
|
# Calculer les statistiques
|
|
places = []
|
|
times = []
|
|
podiums = 0
|
|
victoires = 0
|
|
|
|
courses_by_year = defaultdict(int)
|
|
courses_by_type = defaultdict(int)
|
|
|
|
for idx, result in results.iterrows():
|
|
# Extraire les places
|
|
try:
|
|
place = int(result['place'])
|
|
places.append(place)
|
|
if place == 1:
|
|
victoires += 1
|
|
podiums += 1
|
|
elif place <= 3:
|
|
podiums += 1
|
|
except:
|
|
pass
|
|
|
|
# Extraire les temps
|
|
time_seconds = parse_time(result.get('temps', result.get('resultat', '')))
|
|
if time_seconds:
|
|
times.append(time_seconds)
|
|
|
|
# Extraire les infos de course si disponible
|
|
course_info = {}
|
|
if courses_df is not None and 'course_url' in result:
|
|
course_match = courses_df[courses_df['lien'] == result['course_url']]
|
|
if not course_match.empty:
|
|
course_info = course_match.iloc[0].to_dict()
|
|
|
|
# Extraire l'année de la course
|
|
if 'date' in result and pd.notna(result['date']):
|
|
try:
|
|
year = str(result['date']).split('-')[0]
|
|
courses_by_year[year] += 1
|
|
except:
|
|
pass
|
|
elif course_info.get('date'):
|
|
try:
|
|
year = str(course_info['date']).split('-')[0]
|
|
courses_by_year[year] += 1
|
|
except:
|
|
pass
|
|
|
|
# Extraire le type de course
|
|
if course_info.get('type'):
|
|
courses_by_type[course_info['type']] += 1
|
|
|
|
# Ajouter le résultat détaillé
|
|
detailed_result = result.to_dict()
|
|
detailed_result['course_details'] = course_info
|
|
summary['results'].append(detailed_result)
|
|
|
|
# Calculer les statistiques finales
|
|
summary['statistics'] = {
|
|
'victoires': victoires,
|
|
'podiums': podiums,
|
|
'places_moyenne': sum(places) / len(places) if places else 0,
|
|
'meilleure_place': min(places) if places else None,
|
|
'pire_place': max(places) if places else None,
|
|
'meilleur_temps': min(times) if times else None,
|
|
'temps_moyen': sum(times) / len(times) if times else None,
|
|
'courses_par_annee': dict(courses_by_year),
|
|
'courses_par_type': dict(courses_by_type),
|
|
'categories': summary['categories']
|
|
}
|
|
|
|
return summary
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur lors de la génération du récapitulatif: {e}")
|
|
return None
|
|
|
|
def display_athlete_summary(summary, show_full_results=False):
|
|
"""Afficher le récapitulatif de l'athlète"""
|
|
if not summary:
|
|
print("\n❌ Impossible de générer le récapitulatif")
|
|
return
|
|
|
|
stats = summary['statistics']
|
|
|
|
print(f"\n{'='*80}")
|
|
print(f"📊 RÉCAPITULATIF DE {summary['prenom']} {summary['nom']}")
|
|
print(f"{'='*80}\n")
|
|
|
|
# Informations générales
|
|
print(f"🏟️ Club: {summary['club']}")
|
|
print(f"🏃 Total des courses: {summary['total_courses']}")
|
|
print()
|
|
|
|
# Statistiques de performance
|
|
print(f"🏆 STATISTIQUES DE PERFORMANCE")
|
|
print(f"{'─'*40}")
|
|
print(f"Victoires: {stats['victoires']}")
|
|
print(f"Podiums (top 3): {stats['podiums']}")
|
|
print(f"Meilleure place: {stats['meilleure_place']}")
|
|
print(f"Place moyenne: {stats['places_moyenne']:.2f}")
|
|
print()
|
|
|
|
# Statistiques de temps
|
|
if stats['meilleur_temps']:
|
|
print(f"⏱️ STATISTIQUES DE TEMPS")
|
|
print(f"{'─'*40}")
|
|
print(f"Meilleur temps: {format_time(stats['meilleur_temps'])}")
|
|
print(f"Temps moyen: {format_time(stats['temps_moyen'])}")
|
|
print()
|
|
|
|
# Répartition par année
|
|
if stats['courses_par_annee']:
|
|
print(f"📅 RÉPARTITION PAR ANNÉE")
|
|
print(f"{'─'*40}")
|
|
for year, count in sorted(stats['courses_par_annee'].items()):
|
|
print(f"{year}: {count} course(s)")
|
|
print()
|
|
|
|
# Répartition par type
|
|
if stats['courses_par_type']:
|
|
print(f"🏷️ RÉPARTITION PAR TYPE DE COURSE")
|
|
print(f"{'─'*40}")
|
|
for course_type, count in sorted(stats['courses_par_type'].items(), key=lambda x: x[1], reverse=True):
|
|
print(f"{course_type}: {count} course(s)")
|
|
print()
|
|
|
|
# Catégories
|
|
if stats['categories']:
|
|
print(f"📊 CATÉGORIES")
|
|
print(f"{'─'*40}")
|
|
print(", ".join(cat for cat in stats['categories'] if cat))
|
|
print()
|
|
|
|
print(f"{'='*80}\n")
|
|
|
|
# Résultats détaillés
|
|
if show_full_results:
|
|
print(f"📋 LISTE COMPLÈTE DES RÉSULTATS")
|
|
print(f"{'='*80}\n")
|
|
|
|
for i, result in enumerate(summary['results'], 1):
|
|
print(f"{i}. {result.get('course_details', {}).get('nom', 'Inconnu')}")
|
|
|
|
if result.get('course_details', {}).get('date'):
|
|
print(f" 📅 {result['course_details']['date']}")
|
|
|
|
if result.get('course_details', {}).get('lieu'):
|
|
print(f" 📍 {result['course_details']['lieu']}")
|
|
|
|
print(f" 🏆 Place: {result.get('place', 'N/A')}")
|
|
|
|
temps = result.get('temps', result.get('resultat', 'N/A'))
|
|
print(f" ⏱️ Temps: {temps}")
|
|
|
|
if result.get('categorie'):
|
|
print(f" 🏷️ Catégorie: {result['categorie']}")
|
|
|
|
if result.get('points'):
|
|
print(f" 🎯 Points: {result['points']}")
|
|
|
|
print()
|
|
|
|
print(f"{'='*80}\n")
|
|
|
|
def export_summary_txt(summary, output_dir="data"):
|
|
"""Exporter le récapitulatif en fichier texte"""
|
|
os.makedirs(os.path.join(output_dir, 'exports'), exist_ok=True)
|
|
|
|
filename = f"summary_{summary['nom']}_{summary['prenom']}.txt"
|
|
filepath = os.path.join(output_dir, 'exports', filename.replace(" ", "_"))
|
|
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
f.write("="*80 + "\n")
|
|
f.write(f"RÉCAPITULATIF DE {summary['prenom']} {summary['nom']}\n")
|
|
f.write("="*80 + "\n\n")
|
|
|
|
stats = summary['statistics']
|
|
|
|
# Informations générales
|
|
f.write(f"Club: {summary['club']}\n")
|
|
f.write(f"Total des courses: {summary['total_courses']}\n\n")
|
|
|
|
# Statistiques
|
|
f.write("STATISTIQUES DE PERFORMANCE\n")
|
|
f.write("-"*40 + "\n")
|
|
f.write(f"Victoires: {stats['victoires']}\n")
|
|
f.write(f"Podiums: {stats['podiums']}\n")
|
|
f.write(f"Meilleure place: {stats['meilleure_place']}\n")
|
|
f.write(f"Place moyenne: {stats['places_moyenne']:.2f}\n\n")
|
|
|
|
# Temps
|
|
if stats['meilleur_temps']:
|
|
f.write("STATISTIQUES DE TEMPS\n")
|
|
f.write("-"*40 + "\n")
|
|
f.write(f"Meilleur temps: {format_time(stats['meilleur_temps'])}\n")
|
|
f.write(f"Temps moyen: {format_time(stats['temps_moyen'])}\n\n")
|
|
|
|
# Répartition
|
|
if stats['courses_par_annee']:
|
|
f.write("RÉPARTITION PAR ANNÉE\n")
|
|
f.write("-"*40 + "\n")
|
|
for year, count in sorted(stats['courses_par_annee'].items()):
|
|
f.write(f"{year}: {count}\n")
|
|
f.write("\n")
|
|
|
|
if stats['courses_par_type']:
|
|
f.write("RÉPARTITION PAR TYPE\n")
|
|
f.write("-"*40 + "\n")
|
|
for course_type, count in sorted(stats['courses_par_type'].items(), key=lambda x: x[1], reverse=True):
|
|
f.write(f"{course_type}: {count}\n")
|
|
f.write("\n")
|
|
|
|
# Liste des courses
|
|
f.write("LISTE DES COURSES\n")
|
|
f.write("="*80 + "\n\n")
|
|
|
|
for i, result in enumerate(summary['results'], 1):
|
|
f.write(f"{i}. {result.get('course_details', {}).get('nom', 'Inconnu')}\n")
|
|
f.write(f" Date: {result.get('course_details', {}).get('date', 'N/A')}\n")
|
|
f.write(f" Place: {result.get('place', 'N/A')}\n")
|
|
f.write(f" Temps: {result.get('temps', result.get('resultat', 'N/A'))}\n")
|
|
f.write("\n")
|
|
|
|
logging.info(f"Exporté le récapitulatif dans {filepath}")
|
|
return filepath
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Générer un récapitulatif complet d\'un athlète')
|
|
parser.add_argument('nom', help='Nom de l\'athlète')
|
|
parser.add_argument('--prenom', help='Prénom de l\'athlète')
|
|
parser.add_argument('--data-dir', default='data',
|
|
help='Répertoire des données CSV')
|
|
parser.add_argument('--full', action='store_true',
|
|
help='Afficher la liste complète des résultats')
|
|
parser.add_argument('--export', action='store_true',
|
|
help='Exporter le récapitulatif en fichier texte')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Générer le récapitulatif
|
|
print(f"\n📊 Génération du récapitulatif pour {args.prenom or ''} {args.nom}...")
|
|
summary = get_athlete_summary(args.nom, args.prenom, args.data_dir)
|
|
|
|
if summary:
|
|
display_athlete_summary(summary, show_full_results=args.full)
|
|
|
|
if args.export:
|
|
filepath = export_summary_txt(summary, args.data_dir)
|
|
print(f"💾 Exporté dans: {filepath}")
|
|
else:
|
|
print("\n❌ Aucun résultat trouvé pour cet athlète")
|
|
print("💡 Vérifiez que les données ont été scrapées avec:")
|
|
print(" python ffa_cli.py scrape --fetch-details")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|