*/ private const array PHRASES = [ 'delta_ulsp_lag_0' => "last week's pump price move", 'delta_ulsp_lag_1' => 'the pump price move two weeks ago', 'delta_ulsp_lag_3' => 'the pump price move four weeks ago', 'delta_ulsd_lag_0' => "last week's diesel move", 'ulsp_minus_ma8' => "the gap between this week's pump price and its 8-week average", 'week_of_year_sin' => 'the seasonal pattern', 'week_of_year_cos' => 'the seasonal pattern', 'is_pre_bank_holiday' => 'an upcoming bank holiday', ]; /** * @param array $features */ public function generate( RidgeRegressionModel $model, WeeklyPrediction $prediction, array $features, CarbonInterface $targetMonday, int $confidence, bool $flaggedDutyChange, ?float $trailingHitRate, ): string { if ($confidence < 40) { return 'Not enough signal in the historical pattern to call this week — staying silent.'; } $coeffs = $model->coefficients() ?? []; $features_meta = $coeffs['features'] ?? []; $contributions = []; foreach ($features as $f) { $name = $f->name(); $meta = $features_meta[$name] ?? null; if ($meta === null) { continue; } $value = $f->valueFor($targetMonday); if ($value === null) { continue; } $z = ($value - $meta['mean']) / ($meta['std_dev'] ?: 1.0); $contributions[$name] = $z * $meta['beta_standardised']; } $headline = $this->headline($prediction); $driver = $this->dominantFeatureSentence($contributions); $duty = $flaggedDutyChange ? ' Recent fuel duty change may skew accuracy for the next several weeks.' : ''; $accuracy = $trailingHitRate !== null ? sprintf(' Last 13 weeks: %d%% hit rate.', (int) round($trailingHitRate * 100)) : ''; return $headline.' '.$driver.$duty.$accuracy; } private function headline(WeeklyPrediction $prediction): string { $absP = round(abs($prediction->magnitudePence) / 100, 1); return match ($prediction->direction) { 'rising' => sprintf('Model expects pump prices to rise by ~%sp/L next week.', number_format($absP, 1)), 'falling' => sprintf('Model expects pump prices to fall by ~%sp/L next week.', number_format($absP, 1)), default => 'Pump prices are likely flat next week.', }; } /** @param array $contributions */ private function dominantFeatureSentence(array $contributions): string { if ($contributions === []) { return 'Drawn from the full feature set with no single dominant signal.'; } uasort($contributions, fn (float $a, float $b): int => abs($b) <=> abs($a)); $topName = array_key_first($contributions); $phrase = self::PHRASES[$topName] ?? $topName; return sprintf('Driver: %s.', $phrase); } }