Configure Maven repository for unionflow-server-api dependency
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO;
|
||||
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
|
||||
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service d'analyse des tendances et prédictions pour les KPI
|
||||
*
|
||||
* <p>Ce service calcule les tendances, effectue des analyses statistiques et génère des prédictions
|
||||
* basées sur l'historique des données.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class TrendAnalysisService {
|
||||
|
||||
@Inject AnalyticsService analyticsService;
|
||||
|
||||
@Inject KPICalculatorService kpiCalculatorService;
|
||||
|
||||
/**
|
||||
* Calcule la tendance d'un KPI sur une période donnée
|
||||
*
|
||||
* @param typeMetrique Le type de métrique à analyser
|
||||
* @param periodeAnalyse La période d'analyse
|
||||
* @param organisationId L'ID de l'organisation (optionnel)
|
||||
* @return Les données de tendance du KPI
|
||||
*/
|
||||
public KPITrendDTO calculerTendance(
|
||||
TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
|
||||
log.info(
|
||||
"Calcul de la tendance pour {} sur la période {} et l'organisation {}",
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId);
|
||||
|
||||
LocalDateTime dateDebut = periodeAnalyse.getDateDebut();
|
||||
LocalDateTime dateFin = periodeAnalyse.getDateFin();
|
||||
|
||||
// Génération des points de données historiques
|
||||
List<KPITrendDTO.PointDonneeDTO> pointsDonnees =
|
||||
genererPointsDonnees(typeMetrique, dateDebut, dateFin, organisationId);
|
||||
|
||||
// Calculs statistiques
|
||||
StatistiquesDTO stats = calculerStatistiques(pointsDonnees);
|
||||
|
||||
// Analyse de tendance (régression linéaire simple)
|
||||
TendanceDTO tendance = calculerTendanceLineaire(pointsDonnees);
|
||||
|
||||
// Prédiction pour la prochaine période
|
||||
BigDecimal prediction = calculerPrediction(pointsDonnees, tendance);
|
||||
|
||||
// Détection d'anomalies
|
||||
detecterAnomalies(pointsDonnees, stats);
|
||||
|
||||
return KPITrendDTO.builder()
|
||||
.typeMetrique(typeMetrique)
|
||||
.periodeAnalyse(periodeAnalyse)
|
||||
.organisationId(organisationId)
|
||||
.nomOrganisation(obtenirNomOrganisation(organisationId))
|
||||
.dateDebut(dateDebut)
|
||||
.dateFin(dateFin)
|
||||
.pointsDonnees(pointsDonnees)
|
||||
.valeurActuelle(stats.valeurActuelle)
|
||||
.valeurMinimale(stats.valeurMinimale)
|
||||
.valeurMaximale(stats.valeurMaximale)
|
||||
.valeurMoyenne(stats.valeurMoyenne)
|
||||
.ecartType(stats.ecartType)
|
||||
.coefficientVariation(stats.coefficientVariation)
|
||||
.tendanceGenerale(tendance.pente)
|
||||
.coefficientCorrelation(tendance.coefficientCorrelation)
|
||||
.pourcentageEvolutionGlobale(calculerEvolutionGlobale(pointsDonnees))
|
||||
.predictionProchainePeriode(prediction)
|
||||
.margeErreurPrediction(calculerMargeErreur(tendance))
|
||||
.seuilAlerteBas(calculerSeuilAlerteBas(stats))
|
||||
.seuilAlerteHaut(calculerSeuilAlerteHaut(stats))
|
||||
.alerteActive(verifierAlertes(stats.valeurActuelle, stats))
|
||||
.intervalleRegroupement(periodeAnalyse.getIntervalleRegroupement())
|
||||
.formatDate(periodeAnalyse.getFormatDate())
|
||||
.dateDerniereMiseAJour(LocalDateTime.now())
|
||||
.frequenceMiseAJourMinutes(determinerFrequenceMiseAJour(periodeAnalyse))
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Génère les points de données historiques pour la période */
|
||||
private List<KPITrendDTO.PointDonneeDTO> genererPointsDonnees(
|
||||
TypeMetrique typeMetrique,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
UUID organisationId) {
|
||||
List<KPITrendDTO.PointDonneeDTO> points = new ArrayList<>();
|
||||
|
||||
// Déterminer l'intervalle entre les points
|
||||
ChronoUnit unite = determinerUniteIntervalle(dateDebut, dateFin);
|
||||
long intervalleValeur = determinerValeurIntervalle(dateDebut, dateFin, unite);
|
||||
|
||||
LocalDateTime dateCourante = dateDebut;
|
||||
int index = 0;
|
||||
|
||||
while (!dateCourante.isAfter(dateFin)) {
|
||||
LocalDateTime dateFinIntervalle = dateCourante.plus(intervalleValeur, unite);
|
||||
if (dateFinIntervalle.isAfter(dateFin)) {
|
||||
dateFinIntervalle = dateFin;
|
||||
}
|
||||
|
||||
// Calcul de la valeur pour cet intervalle
|
||||
BigDecimal valeur =
|
||||
calculerValeurPourIntervalle(
|
||||
typeMetrique, dateCourante, dateFinIntervalle, organisationId);
|
||||
|
||||
KPITrendDTO.PointDonneeDTO point =
|
||||
KPITrendDTO.PointDonneeDTO.builder()
|
||||
.date(dateCourante)
|
||||
.valeur(valeur)
|
||||
.libelle(formaterLibellePoint(dateCourante, unite))
|
||||
.anomalie(false) // Sera déterminé plus tard
|
||||
.prediction(false)
|
||||
.build();
|
||||
|
||||
points.add(point);
|
||||
dateCourante = dateCourante.plus(intervalleValeur, unite);
|
||||
index++;
|
||||
}
|
||||
|
||||
log.info("Généré {} points de données pour la tendance", points.size());
|
||||
return points;
|
||||
}
|
||||
|
||||
/** Calcule les statistiques descriptives des points de données */
|
||||
private StatistiquesDTO calculerStatistiques(List<KPITrendDTO.PointDonneeDTO> points) {
|
||||
if (points.isEmpty()) {
|
||||
return new StatistiquesDTO();
|
||||
}
|
||||
|
||||
List<BigDecimal> valeurs = points.stream().map(KPITrendDTO.PointDonneeDTO::getValeur).toList();
|
||||
|
||||
BigDecimal valeurActuelle = points.get(points.size() - 1).getValeur();
|
||||
BigDecimal valeurMinimale = valeurs.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
|
||||
BigDecimal valeurMaximale = valeurs.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
|
||||
|
||||
// Calcul de la moyenne
|
||||
BigDecimal somme = valeurs.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal moyenne = somme.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
|
||||
|
||||
// Calcul de l'écart-type
|
||||
BigDecimal sommeDifferencesCarrees =
|
||||
valeurs.stream()
|
||||
.map(v -> v.subtract(moyenne).pow(2))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal variance =
|
||||
sommeDifferencesCarrees.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
|
||||
BigDecimal ecartType =
|
||||
new BigDecimal(Math.sqrt(variance.doubleValue())).setScale(4, RoundingMode.HALF_UP);
|
||||
|
||||
// Coefficient de variation
|
||||
BigDecimal coefficientVariation =
|
||||
moyenne.compareTo(BigDecimal.ZERO) != 0
|
||||
? ecartType.divide(moyenne, 4, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
return new StatistiquesDTO(
|
||||
valeurActuelle, valeurMinimale, valeurMaximale, moyenne, ecartType, coefficientVariation);
|
||||
}
|
||||
|
||||
/** Calcule la tendance linéaire (régression linéaire simple) */
|
||||
private TendanceDTO calculerTendanceLineaire(List<KPITrendDTO.PointDonneeDTO> points) {
|
||||
if (points.size() < 2) {
|
||||
return new TendanceDTO(BigDecimal.ZERO, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
int n = points.size();
|
||||
BigDecimal sommeX = BigDecimal.ZERO;
|
||||
BigDecimal sommeY = BigDecimal.ZERO;
|
||||
BigDecimal sommeXY = BigDecimal.ZERO;
|
||||
BigDecimal sommeX2 = BigDecimal.ZERO;
|
||||
BigDecimal sommeY2 = BigDecimal.ZERO;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal x = new BigDecimal(i); // Index comme variable X
|
||||
BigDecimal y = points.get(i).getValeur(); // Valeur comme variable Y
|
||||
|
||||
sommeX = sommeX.add(x);
|
||||
sommeY = sommeY.add(y);
|
||||
sommeXY = sommeXY.add(x.multiply(y));
|
||||
sommeX2 = sommeX2.add(x.multiply(x));
|
||||
sommeY2 = sommeY2.add(y.multiply(y));
|
||||
}
|
||||
|
||||
// Calcul de la pente (coefficient directeur)
|
||||
BigDecimal nBD = new BigDecimal(n);
|
||||
BigDecimal numerateur = nBD.multiply(sommeXY).subtract(sommeX.multiply(sommeY));
|
||||
BigDecimal denominateur = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
|
||||
|
||||
BigDecimal pente =
|
||||
denominateur.compareTo(BigDecimal.ZERO) != 0
|
||||
? numerateur.divide(denominateur, 6, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
|
||||
// Calcul du coefficient de corrélation R²
|
||||
BigDecimal numerateurR = numerateur;
|
||||
BigDecimal denominateurR1 = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
|
||||
BigDecimal denominateurR2 = nBD.multiply(sommeY2).subtract(sommeY.multiply(sommeY));
|
||||
|
||||
BigDecimal coefficientCorrelation = BigDecimal.ZERO;
|
||||
if (denominateurR1.compareTo(BigDecimal.ZERO) != 0
|
||||
&& denominateurR2.compareTo(BigDecimal.ZERO) != 0) {
|
||||
BigDecimal denominateurR =
|
||||
new BigDecimal(Math.sqrt(denominateurR1.multiply(denominateurR2).doubleValue()));
|
||||
|
||||
if (denominateurR.compareTo(BigDecimal.ZERO) != 0) {
|
||||
BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP);
|
||||
coefficientCorrelation = r.multiply(r); // R²
|
||||
}
|
||||
}
|
||||
|
||||
return new TendanceDTO(pente, coefficientCorrelation);
|
||||
}
|
||||
|
||||
/** Calcule une prédiction pour la prochaine période */
|
||||
private BigDecimal calculerPrediction(
|
||||
List<KPITrendDTO.PointDonneeDTO> points, TendanceDTO tendance) {
|
||||
if (points.isEmpty()) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
|
||||
BigDecimal prediction = derniereValeur.add(tendance.pente);
|
||||
|
||||
// S'assurer que la prédiction est positive
|
||||
return prediction.max(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
/** Détecte les anomalies dans les points de données */
|
||||
private void detecterAnomalies(List<KPITrendDTO.PointDonneeDTO> points, StatistiquesDTO stats) {
|
||||
BigDecimal seuilAnomalie = stats.ecartType.multiply(new BigDecimal("2")); // 2 écarts-types
|
||||
|
||||
for (KPITrendDTO.PointDonneeDTO point : points) {
|
||||
BigDecimal ecartMoyenne = point.getValeur().subtract(stats.valeurMoyenne).abs();
|
||||
if (ecartMoyenne.compareTo(seuilAnomalie) > 0) {
|
||||
point.setAnomalie(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
private ChronoUnit determinerUniteIntervalle(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
long joursTotal = ChronoUnit.DAYS.between(dateDebut, dateFin);
|
||||
|
||||
if (joursTotal <= 7) return ChronoUnit.DAYS;
|
||||
if (joursTotal <= 90) return ChronoUnit.DAYS;
|
||||
if (joursTotal <= 365) return ChronoUnit.WEEKS;
|
||||
return ChronoUnit.MONTHS;
|
||||
}
|
||||
|
||||
private long determinerValeurIntervalle(
|
||||
LocalDateTime dateDebut, LocalDateTime dateFin, ChronoUnit unite) {
|
||||
long dureeTotal = unite.between(dateDebut, dateFin);
|
||||
|
||||
// Viser environ 10-20 points de données
|
||||
if (dureeTotal <= 20) return 1;
|
||||
if (dureeTotal <= 40) return 2;
|
||||
if (dureeTotal <= 100) return 5;
|
||||
return dureeTotal / 15; // Environ 15 points
|
||||
}
|
||||
|
||||
private BigDecimal calculerValeurPourIntervalle(
|
||||
TypeMetrique typeMetrique,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
UUID organisationId) {
|
||||
// Utiliser le service KPI pour calculer la valeur
|
||||
return switch (typeMetrique) {
|
||||
case NOMBRE_MEMBRES_ACTIFS -> {
|
||||
// Calcul direct via le service KPI
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, BigDecimal.ZERO);
|
||||
}
|
||||
case TOTAL_COTISATIONS_COLLECTEES -> {
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, BigDecimal.ZERO);
|
||||
}
|
||||
case NOMBRE_EVENEMENTS_ORGANISES -> {
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, BigDecimal.ZERO);
|
||||
}
|
||||
case NOMBRE_DEMANDES_AIDE -> {
|
||||
var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
|
||||
yield kpis.getOrDefault(TypeMetrique.NOMBRE_DEMANDES_AIDE, BigDecimal.ZERO);
|
||||
}
|
||||
default -> BigDecimal.ZERO;
|
||||
};
|
||||
}
|
||||
|
||||
private String formaterLibellePoint(LocalDateTime date, ChronoUnit unite) {
|
||||
return switch (unite) {
|
||||
case DAYS -> date.toLocalDate().toString();
|
||||
case WEEKS -> "S" + date.get(java.time.temporal.WeekFields.ISO.weekOfYear());
|
||||
case MONTHS -> date.getMonth().toString() + " " + date.getYear();
|
||||
default -> date.toString();
|
||||
};
|
||||
}
|
||||
|
||||
private BigDecimal calculerEvolutionGlobale(List<KPITrendDTO.PointDonneeDTO> points) {
|
||||
if (points.size() < 2) return BigDecimal.ZERO;
|
||||
|
||||
BigDecimal premiereValeur = points.get(0).getValeur();
|
||||
BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
|
||||
|
||||
if (premiereValeur.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;
|
||||
|
||||
return derniereValeur
|
||||
.subtract(premiereValeur)
|
||||
.divide(premiereValeur, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"));
|
||||
}
|
||||
|
||||
private BigDecimal calculerMargeErreur(TendanceDTO tendance) {
|
||||
// Marge d'erreur basée sur le coefficient de corrélation
|
||||
BigDecimal precision = tendance.coefficientCorrelation;
|
||||
BigDecimal margeErreur = BigDecimal.ONE.subtract(precision).multiply(new BigDecimal("100"));
|
||||
return margeErreur.min(new BigDecimal("50")); // Plafonnée à 50%
|
||||
}
|
||||
|
||||
private BigDecimal calculerSeuilAlerteBas(StatistiquesDTO stats) {
|
||||
return stats.valeurMoyenne.subtract(stats.ecartType.multiply(new BigDecimal("1.5")));
|
||||
}
|
||||
|
||||
private BigDecimal calculerSeuilAlerteHaut(StatistiquesDTO stats) {
|
||||
return stats.valeurMoyenne.add(stats.ecartType.multiply(new BigDecimal("1.5")));
|
||||
}
|
||||
|
||||
private Boolean verifierAlertes(BigDecimal valeurActuelle, StatistiquesDTO stats) {
|
||||
BigDecimal seuilBas = calculerSeuilAlerteBas(stats);
|
||||
BigDecimal seuilHaut = calculerSeuilAlerteHaut(stats);
|
||||
|
||||
return valeurActuelle.compareTo(seuilBas) < 0 || valeurActuelle.compareTo(seuilHaut) > 0;
|
||||
}
|
||||
|
||||
private Integer determinerFrequenceMiseAJour(PeriodeAnalyse periode) {
|
||||
return switch (periode) {
|
||||
case AUJOURD_HUI, HIER -> 15; // 15 minutes
|
||||
case CETTE_SEMAINE, SEMAINE_DERNIERE -> 60; // 1 heure
|
||||
case CE_MOIS, MOIS_DERNIER -> 240; // 4 heures
|
||||
default -> 1440; // 24 heures
|
||||
};
|
||||
}
|
||||
|
||||
private String obtenirNomOrganisation(UUID organisationId) {
|
||||
// À implémenter avec le repository
|
||||
return null;
|
||||
}
|
||||
|
||||
// === CLASSES INTERNES ===
|
||||
|
||||
private static class StatistiquesDTO {
|
||||
final BigDecimal valeurActuelle;
|
||||
final BigDecimal valeurMinimale;
|
||||
final BigDecimal valeurMaximale;
|
||||
final BigDecimal valeurMoyenne;
|
||||
final BigDecimal ecartType;
|
||||
final BigDecimal coefficientVariation;
|
||||
|
||||
StatistiquesDTO() {
|
||||
this(
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO,
|
||||
BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
StatistiquesDTO(
|
||||
BigDecimal valeurActuelle,
|
||||
BigDecimal valeurMinimale,
|
||||
BigDecimal valeurMaximale,
|
||||
BigDecimal valeurMoyenne,
|
||||
BigDecimal ecartType,
|
||||
BigDecimal coefficientVariation) {
|
||||
this.valeurActuelle = valeurActuelle;
|
||||
this.valeurMinimale = valeurMinimale;
|
||||
this.valeurMaximale = valeurMaximale;
|
||||
this.valeurMoyenne = valeurMoyenne;
|
||||
this.ecartType = ecartType;
|
||||
this.coefficientVariation = coefficientVariation;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TendanceDTO {
|
||||
final BigDecimal pente;
|
||||
final BigDecimal coefficientCorrelation;
|
||||
|
||||
TendanceDTO(BigDecimal pente, BigDecimal coefficientCorrelation) {
|
||||
this.pente = pente;
|
||||
this.coefficientCorrelation = coefficientCorrelation;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user