Files
notytex/backend/domain/services/statistics_service.py
Bertrand Benjamin 2b08eb534a Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
 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!
2025-11-25 21:09:47 +01:00

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
]