✨ Changements majeurs: - Suppression complète du code Flask legacy - Migration backend FastAPI vers racine /backend - Migration frontend Vue.js vers racine /frontend - Suppression de notytex-v2/ (code monté à la racine) ✅ Validations: - Backend démarre correctement (port 8000) - API /api/v2/health répond healthy - 99/99 tests unitaires passent - Frontend configuré avec proxy Vite 📝 Documentation: - README.md réécrit pour v2 - Instructions de démarrage mises à jour - .gitignore adapté pour backend/frontend/ 🎯 Architecture finale: notytex/ ├── backend/ # FastAPI + SQLAlchemy + Pydantic ├── frontend/ # Vue 3 + Vite + TailwindCSS ├── docs/ # Documentation └── school_management.db # Base de données (inchangée) Jalon 6 complété: Application v2 prête pour utilisation!
200 lines
5.7 KiB
Python
200 lines
5.7 KiB
Python
"""
|
|
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
|
|
]
|