Initial commit: Reorganiser le projet FFA Calendar Scraper
- 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>
This commit is contained in:
360
scripts/athlete_summary.py
Executable file
360
scripts/athlete_summary.py
Executable file
@@ -0,0 +1,360 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user