""" Service de calculs statistiques. Fournit les fonctions de calcul de statistiques descriptives et de génération d'histogrammes pour les résultats d'évaluations. """ import statistics from typing import List, Union from domain.value_objects import StatisticsResult, HistogramBin, HistogramData # Type pour les scores numériques NumericScore = Union[int, float] class StatisticsService: """ Service de calculs statistiques pour les évaluations. Calcule les statistiques descriptives (moyenne, médiane, écart-type, etc.) et génère les données pour les histogrammes de distribution. """ def calculate_statistics(self, scores: List[NumericScore]) -> StatisticsResult: """ Calcule les statistiques descriptives pour une liste de scores. Args: scores: Liste des scores numériques Returns: StatisticsResult avec toutes les métriques """ if not scores: return StatisticsResult.empty() count = len(scores) mean = round(statistics.mean(scores), 2) median = round(statistics.median(scores), 2) min_score = round(min(scores), 2) max_score = round(max(scores), 2) # Écart-type nécessite au moins 2 valeurs if count > 1: std_dev = round(statistics.stdev(scores), 2) else: std_dev = 0.0 return StatisticsResult( count=count, mean=mean, median=median, min=min_score, max=max_score, std_dev=std_dev ) def create_histogram( self, scores: List[NumericScore], max_points: float, bin_size: float = 1.0 ) -> HistogramData: """ Crée un histogramme de distribution des scores. Args: scores: Liste des scores max_points: Score maximum possible bin_size: Taille de chaque intervalle (défaut: 1 point) Returns: HistogramData avec les bins de l'histogramme """ if not scores or max_points <= 0: return HistogramData(bins=[]) # Calculer le nombre de bins num_bins = int(max_points / bin_size) + 1 bin_counts = [0] * num_bins # Remplir les bins for score in scores: bin_index = min(int(score / bin_size), num_bins - 1) bin_counts[bin_index] += 1 # Créer les objets HistogramBin bins = [] for i in range(num_bins): range_start = i * bin_size range_end = (i + 1) * bin_size if i < num_bins - 1 else float('inf') bins.append(HistogramBin( range_start=range_start, range_end=range_end, count=bin_counts[i] )) return HistogramData(bins=bins) def create_simple_histogram( self, scores: List[NumericScore], max_points: float ) -> List[int]: """ Crée un histogramme simple (liste de counts). Format compatible avec Chart.js pour l'affichage direct. Args: scores: Liste des scores max_points: Score maximum possible Returns: Liste des counts par bin (de 0 à max_points inclus) """ if not scores or max_points <= 0: return [] # Créer les bins (de 0 à max_points inclus) num_bins = int(max_points) + 1 bins = [0] * num_bins for score in scores: bin_index = min(int(score), num_bins - 1) bins[bin_index] += 1 return bins def calculate_percentile(self, scores: List[NumericScore], percentile: float) -> float: """ Calcule un percentile donné. Args: scores: Liste des scores percentile: Percentile à calculer (0-100) Returns: Valeur du percentile """ if not scores: return 0.0 sorted_scores = sorted(scores) index = (percentile / 100) * (len(sorted_scores) - 1) # Interpolation linéaire lower = int(index) upper = min(lower + 1, len(sorted_scores) - 1) fraction = index - lower return round( sorted_scores[lower] * (1 - fraction) + sorted_scores[upper] * fraction, 2 ) def calculate_quartiles(self, scores: List[NumericScore]) -> dict: """ Calcule les quartiles Q1, Q2 (médiane), Q3. Args: scores: Liste des scores Returns: Dict avec q1, q2, q3 """ return { "q1": self.calculate_percentile(scores, 25), "q2": self.calculate_percentile(scores, 50), # Médiane "q3": self.calculate_percentile(scores, 75) } def normalize_scores( self, scores: List[NumericScore], original_max: float, target_max: float = 20.0 ) -> List[float]: """ Normalise les scores vers une échelle cible. Utile pour comparer des évaluations avec des barèmes différents. Args: scores: Liste des scores original_max: Maximum de l'échelle originale target_max: Maximum de l'échelle cible (défaut: 20) Returns: Liste des scores normalisés """ if original_max <= 0: return [0.0] * len(scores) return [ round((score / original_max) * target_max, 2) for score in scores ]