Files
SkinbaseNova/app/Services/Recommendations/FeedOfflineEvaluationService.php
2026-02-14 15:14:12 +01:00

140 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Recommendations;
use Illuminate\Support\Facades\DB;
final class FeedOfflineEvaluationService
{
/**
* @return array<string, mixed>
*/
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<int, array<string, mixed>>
*/
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<string, mixed>
*/
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),
],
];
}
}