*/ public function evaluateAlgo(string $algoVersion, string $from, string $to): array { $row = DB::table('feed_daily_metrics') ->selectRaw('SUM(impressions) as impressions') ->selectRaw('SUM(clicks) as clicks') ->selectRaw('SUM(saves) as saves') ->selectRaw('SUM(dwell_0_5) as dwell_0_5') ->selectRaw('SUM(dwell_5_30) as dwell_5_30') ->selectRaw('SUM(dwell_30_120) as dwell_30_120') ->selectRaw('SUM(dwell_120_plus) as dwell_120_plus') ->where('algo_version', $algoVersion) ->whereBetween('metric_date', [$from, $to]) ->first(); $impressions = (int) ($row->impressions ?? 0); $clicks = (int) ($row->clicks ?? 0); $saves = (int) ($row->saves ?? 0); $dwell05 = (int) ($row->dwell_0_5 ?? 0); $dwell530 = (int) ($row->dwell_5_30 ?? 0); $dwell30120 = (int) ($row->dwell_30_120 ?? 0); $dwell120Plus = (int) ($row->dwell_120_plus ?? 0); $ctr = $impressions > 0 ? $clicks / $impressions : 0.0; $saveRate = $clicks > 0 ? $saves / $clicks : 0.0; $longDwellShare = $clicks > 0 ? ($dwell30120 + $dwell120Plus) / $clicks : 0.0; $bounceRate = $clicks > 0 ? $dwell05 / $clicks : 0.0; $objectiveWeights = (array) config('discovery.evaluation.objective_weights', []); $wCtr = (float) ($objectiveWeights['ctr'] ?? 0.45); $wSave = (float) ($objectiveWeights['save_rate'] ?? 0.35); $wLong = (float) ($objectiveWeights['long_dwell_share'] ?? 0.25); $wBouncePenalty = (float) ($objectiveWeights['bounce_rate_penalty'] ?? 0.15); $saveRateInformational = (bool) config('discovery.evaluation.save_rate_informational', true); if ($saveRateInformational) { $wSave = 0.0; } $normalizationSum = $wCtr + $wSave + $wLong + $wBouncePenalty; if ($normalizationSum > 0.0) { $wCtr /= $normalizationSum; $wSave /= $normalizationSum; $wLong /= $normalizationSum; $wBouncePenalty /= $normalizationSum; } $objectiveScore = ($wCtr * $ctr) + ($wSave * $saveRate) + ($wLong * $longDwellShare) - ($wBouncePenalty * $bounceRate); return [ 'algo_version' => $algoVersion, 'from' => $from, 'to' => $to, 'impressions' => $impressions, 'clicks' => $clicks, 'saves' => $saves, 'ctr' => round($ctr, 6), 'save_rate' => round($saveRate, 6), 'long_dwell_share' => round($longDwellShare, 6), 'bounce_rate' => round($bounceRate, 6), 'dwell_buckets' => [ '0_5' => $dwell05, '5_30' => $dwell530, '30_120' => $dwell30120, '120_plus' => $dwell120Plus, ], 'objective_score' => round($objectiveScore, 6), ]; } /** * @return array> */ public function evaluateAll(string $from, string $to): array { $algoVersions = DB::table('feed_daily_metrics') ->select('algo_version') ->whereBetween('metric_date', [$from, $to]) ->distinct() ->orderBy('algo_version') ->pluck('algo_version') ->map(static fn (mixed $v): string => (string) $v) ->all(); $out = []; foreach ($algoVersions as $algoVersion) { $out[] = $this->evaluateAlgo($algoVersion, $from, $to); } usort($out, static fn (array $a, array $b): int => $b['objective_score'] <=> $a['objective_score']); return $out; } /** * @return array */ public function compareBaselineCandidate(string $baselineAlgoVersion, string $candidateAlgoVersion, string $from, string $to): array { $baseline = $this->evaluateAlgo($baselineAlgoVersion, $from, $to); $candidate = $this->evaluateAlgo($candidateAlgoVersion, $from, $to); $deltaObjective = (float) $candidate['objective_score'] - (float) $baseline['objective_score']; $objectiveLiftPct = (float) $baseline['objective_score'] !== 0.0 ? ($deltaObjective / (float) $baseline['objective_score']) * 100.0 : null; return [ 'from' => $from, 'to' => $to, 'baseline' => $baseline, 'candidate' => $candidate, 'delta' => [ 'objective_score' => round($deltaObjective, 6), 'objective_lift_pct' => $objectiveLiftPct !== null ? round($objectiveLiftPct, 4) : null, 'ctr' => round((float) $candidate['ctr'] - (float) $baseline['ctr'], 6), 'save_rate' => round((float) $candidate['save_rate'] - (float) $baseline['save_rate'], 6), 'long_dwell_share' => round((float) $candidate['long_dwell_share'] - (float) $baseline['long_dwell_share'], 6), 'bounce_rate' => round((float) $candidate['bounce_rate'] - (float) $baseline['bounce_rate'], 6), ], ]; } }