fix: scores horizons — même formule 4 composantes que le score now
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, array{score: int, level: string, distanceKm: float, confidence: float, validAt: string}>|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;
|
||||
|
||||
Reference in New Issue
Block a user