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);
|
$current = $this->scoreService->getCachedScore($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$currentDistanceKm = $current !== null ? $current['distance'] : null;
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'spotId' => (string) $spot->getId(),
|
'spotId' => (string) $spot->getId(),
|
||||||
'name' => $spot->getName(),
|
'name' => $spot->getName(),
|
||||||
'type' => $spot->getType(),
|
'type' => $spot->getType(),
|
||||||
'region' => $spot->getRegion(),
|
'region' => $spot->getRegion(),
|
||||||
'score' => $current,
|
'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.
|
* Scores estimés par horizon — même formule 4 composantes que computeScore() :
|
||||||
* Retourne null si aucun forecast disponible.
|
* 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
|
* @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(
|
$rows = $this->connection->fetchAllAssociative(
|
||||||
'SELECT
|
'SELECT
|
||||||
f.time_horizon AS horizon,
|
f.time_horizon AS horizon,
|
||||||
ROUND(CAST(ST_Distance(
|
ROUND(CAST(ST_Distance(
|
||||||
cp.geometry::geography,
|
cp.geometry::geography,
|
||||||
f.geometry::geography
|
f.geometry::geography
|
||||||
) / 1000 AS numeric), 2) AS distance_km,
|
) / 1000 AS numeric), 2) AS distance_km,
|
||||||
f.confidence,
|
f.confidence,
|
||||||
f.valid_at
|
f.valid_at,
|
||||||
|
COALESCE(o.coverage_area, 0) AS coverage_area
|
||||||
FROM sargassum_forecast f
|
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 = (
|
WHERE f.source_observation_id = (
|
||||||
SELECT o.id FROM sargassum_observation o
|
SELECT o2.id FROM sargassum_observation o2
|
||||||
INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o.id
|
INNER JOIN sargassum_forecast ff ON ff.source_observation_id = o2.id
|
||||||
ORDER BY o.detected_at DESC LIMIT 1
|
ORDER BY o2.detected_at DESC LIMIT 1
|
||||||
)
|
)
|
||||||
ORDER BY f.time_horizon ASC',
|
ORDER BY f.time_horizon ASC',
|
||||||
['spotId' => $spotId]
|
['spotId' => $spotId]
|
||||||
@@ -113,20 +117,51 @@ class SpotScoreController extends AbstractController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$horizons = [];
|
$horizons = [];
|
||||||
|
$prevDistKm = $currentDistanceKm; // distance de référence = "now"
|
||||||
|
$prevHorizon = 0;
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$distKm = (float) $row['distance_km'];
|
$distKm = (float) $row['distance_km'];
|
||||||
$confidence = (float) $row['confidence'];
|
$distM = $distKm * 1000;
|
||||||
// Score pondéré par la confiance : un horizon lointain moins certain
|
$confidence = (float) $row['confidence'];
|
||||||
// ne peut pas afficher un risque supérieur à un horizon proche plus fiable
|
$coverageKm2 = (float) $row['coverage_area'];
|
||||||
$score = max(0, (int) round((1 - min($distKm / 50, 1)) * 100 * $confidence));
|
$horizon = (int) $row['horizon'];
|
||||||
$horizons['h' . $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,
|
'score' => $score,
|
||||||
'level' => $this->scoreToLevel($score),
|
'level' => $this->scoreToLevel($score),
|
||||||
'distanceKm' => $distKm,
|
'distanceKm' => $distKm,
|
||||||
'confidence' => $confidence,
|
'confidence' => $confidence,
|
||||||
'validAt' => $row['valid_at'],
|
'validAt' => $row['valid_at'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$prevDistKm = $distKm;
|
||||||
|
$prevHorizon = $horizon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $horizons;
|
return $horizons;
|
||||||
|
|||||||
Reference in New Issue
Block a user