From 55caca001cf7132a463794b9c244f993bb3c3eda Mon Sep 17 00:00:00 2001 From: Gwadaking Date: Fri, 3 Apr 2026 21:51:56 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20scores=20horizons=20=E2=80=94=20m=C3=AAm?= =?UTF-8?q?e=20formule=204=20composantes=20que=20le=20score=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avant : horizons = (1 - dist/50km) × 100 × confidence — échelle incomparable avec le score now qui utilise distance(40) + densité(30) + vitesse(20) + tendance(10). Après : même décomposition pour les 4 horizons, en passant currentDistanceKm pour calculer la vitesse d'approche réelle par rapport à la position courante. La confiance s'applique sur le score total (incertitude du modèle). Résultat : si la sargasse s'éloigne à H+6, le score baisse. Si elle approche, le score monte — mais atténué par la confiance du modèle (0.85 à H+6). Co-Authored-By: Claude Sonnet 4.6 --- .../src/Controller/SpotScoreController.php | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/backend/src/Controller/SpotScoreController.php b/backend/src/Controller/SpotScoreController.php index 1b7f045..e6c7976 100644 --- a/backend/src/Controller/SpotScoreController.php +++ b/backend/src/Controller/SpotScoreController.php @@ -49,13 +49,15 @@ class SpotScoreController extends AbstractController $current = $this->scoreService->getCachedScore($id); } + $currentDistanceKm = $current !== null ? $current['distance'] : null; + return $this->json([ 'spotId' => (string) $spot->getId(), 'name' => $spot->getName(), 'type' => $spot->getType(), 'region' => $spot->getRegion(), 'score' => $current, - 'horizons' => $this->loadHorizonScores($id), + 'horizons' => $this->loadHorizonScores($id, $currentDistanceKm), ]); } @@ -81,29 +83,31 @@ class SpotScoreController extends AbstractController } /** - * Scores estimés par horizon à partir des forecasts les plus récents. - * Retourne null si aucun forecast disponible. + * Scores estimés par horizon — même formule 4 composantes que computeScore() : + * distance (40pts) + densité observation (30pts) + vitesse d'approche (20pts) + tendance (10pts) + * Le tout multiplié par la confiance du modèle (décroissante avec l'horizon). * * @return array|null */ - private function loadHorizonScores(string $spotId): ?array + private function loadHorizonScores(string $spotId, ?float $currentDistanceKm): ?array { - // Dernière observation avec forecasts $rows = $this->connection->fetchAllAssociative( 'SELECT - f.time_horizon AS horizon, + f.time_horizon AS horizon, ROUND(CAST(ST_Distance( cp.geometry::geography, f.geometry::geography - ) / 1000 AS numeric), 2) AS distance_km, + ) / 1000 AS numeric), 2) AS distance_km, f.confidence, - f.valid_at + f.valid_at, + COALESCE(o.coverage_area, 0) AS coverage_area FROM sargassum_forecast f - JOIN coastal_point cp ON cp.id = :spotId + JOIN coastal_point cp ON cp.id = :spotId + JOIN sargassum_observation o ON o.id = f.source_observation_id WHERE f.source_observation_id = ( - SELECT o.id FROM sargassum_observation o - INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o.id - ORDER BY o.detected_at DESC LIMIT 1 + SELECT o2.id FROM sargassum_observation o2 + INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o2.id + ORDER BY o2.detected_at DESC LIMIT 1 ) ORDER BY f.time_horizon ASC', ['spotId' => $spotId] @@ -113,20 +117,51 @@ class SpotScoreController extends AbstractController return null; } - $horizons = []; + $horizons = []; + $prevDistKm = $currentDistanceKm; // distance de référence = "now" + $prevHorizon = 0; + foreach ($rows as $row) { - $distKm = (float) $row['distance_km']; - $confidence = (float) $row['confidence']; - // Score pondéré par la confiance : un horizon lointain moins certain - // ne peut pas afficher un risque supérieur à un horizon proche plus fiable - $score = max(0, (int) round((1 - min($distKm / 50, 1)) * 100 * $confidence)); - $horizons['h' . $row['horizon']] = [ + $distKm = (float) $row['distance_km']; + $distM = $distKm * 1000; + $confidence = (float) $row['confidence']; + $coverageKm2 = (float) $row['coverage_area']; + $horizon = (int) $row['horizon']; + + // Composante distance (même seuil 50km que computeScore) + $distanceScore = max(0.0, 1.0 - $distM / 50_000) * 40; + + // Composante densité : même observation source, coverage identique + $densityScore = min($coverageKm2 / 100, 1.0) * 30; + + // Composante vitesse d'approche par rapport à l'horizon précédent + $hoursElapsed = max($horizon - $prevHorizon, 1); + $approachKmh = $prevDistKm !== null + ? ($prevDistKm - $distKm) / $hoursElapsed + : 0.0; + $velocityScore = min(max($approachKmh, 0.0) / 5.0, 1.0) * 20; + + // Composante tendance + $trendScore = match (true) { + $approachKmh > 0.2 => 10, // approche + $approachKmh < -0.2 => 0, // s'éloigne + default => 5, // stable + }; + + // Confiance du modèle appliquée à l'ensemble (l'observation "now" est certaine, pas la prévision) + $raw = $distanceScore + $densityScore + $velocityScore + $trendScore; + $score = max(0, min(100, (int) round($raw * $confidence))); + + $horizons['h' . $horizon] = [ 'score' => $score, 'level' => $this->scoreToLevel($score), 'distanceKm' => $distKm, 'confidence' => $confidence, 'validAt' => $row['valid_at'], ]; + + $prevDistKm = $distKm; + $prevHorizon = $horizon; } return $horizons;