[criteria_id=>1-5, ...]] * @return Collection */ // public function getRecommendations(array $input): Collection // { // $ageMin = (int) ($input['age_min'] ?? 0); // $ageMax = (int) ($input['age_max'] ?? 99); // $budgetMin = (float) ($input['budget_min'] ?? 0); // $budgetMax = (float) ($input['budget_max'] ?? 999999999); // $priorities = $input['priorities'] ?? []; // $products = $this->getFilteredProducts($ageMin, $ageMax, $budgetMin, $budgetMax,); // if ($products->isEmpty()) { // return collect(); // } // $criterias = Criteria::orderBy('weight_order')->get(); // if ($criterias->isEmpty()) { // return $products->take(5)->map(fn($p, $i) => [ // 'rank' => $i + 1, // 'product' => $p, // 'score' => 0.0, // 'explanation' => 'Produk ini tersedia dalam rentang usia dan budget Anda.', // ]); // } // $weights = $this->buildWeightsFromPriorities($priorities, $criterias); // $matrix = $this->buildMatrix($products, $criterias); // $normalized = $this->normalizeMatrix($matrix, $criterias); // $scores = $this->calculatePreferenceScores($normalized, $weights, $criterias); // $ranked = $scores->sortByDesc('score')->take(5)->values(); // $criteriaNames = $criterias->keyBy('id')->map->name; // return $ranked->map(function ($item, $index) use ($products, $criteriaNames, $priorities) { // $product = $products->firstWhere('id', $item['product_id']); // if (!$product) { // return null; // } // $explanation = $this->buildExplanation($product, $item['score'], $priorities, $criteriaNames); // return [ // 'rank' => $index + 1, // 'product' => $product, // 'score' => round($item['score'], 4), // 'explanation' => $explanation, // ]; // })->filter()->values(); // } public function getRecommendations(array $input): Collection { $ageMin = (int) ($input['age_min'] ?? 0); $ageMax = (int) ($input['age_max'] ?? 99); $budgetMin = (float) ($input['budget_min'] ?? 0); $budgetMax = (float) ($input['budget_max'] ?? 999999999); $priorities = $input['priorities'] ?? []; $category = $input['category'] ?? null; $products = $this->getFilteredProducts( $ageMin, $ageMax, $budgetMin, $budgetMax, $category ); if ($products->isEmpty()) { return collect(); } $criterias = Criteria::orderBy('weight_order')->get(); if ($criterias->isEmpty()) { return $products->take(5)->map(fn($p, $i) => [ 'rank' => $i + 1, 'product' => $p, 'score' => 0.0, 'explanation' => 'Produk ini tersedia dalam rentang usia dan budget Anda.', ]); } $weights = $this->buildWeightsFromPriorities($priorities, $criterias); $matrix = $this->buildMatrix($products, $criterias); $normalized = $this->normalizeMatrix($matrix, $criterias); $scores = $this->calculatePreferenceScores($normalized, $weights, $criterias); $ranked = $scores->sortByDesc('score')->take(5)->values(); $criteriaNames = $criterias->keyBy('id')->map->name; return $ranked->map(function ($item, $index) use ($products, $criteriaNames, $priorities) { $product = $products->firstWhere('id', $item['product_id']); if (!$product) { return null; } $explanation = $this->buildExplanation( $product, $item['score'], $priorities, $criteriaNames ); return [ 'rank' => $index + 1, 'product' => $product, 'score' => round($item['score'], 4), 'explanation' => $explanation, ]; })->filter()->values(); } /** * Hitung rekomendasi SAW beserta data matriks untuk tampilan (skripsi). * * @return array{recommendations: Collection, criterias: array, decision_matrix: array, normalized_matrix: array, weights: array, preference_scores: array} */ public function getRecommendationsWithMatrix(array $input): array { $ageMin = (int) ($input['age_min'] ?? 0); $ageMax = (int) ($input['age_max'] ?? 99); $budgetMin = (float) ($input['budget_min'] ?? 0); $budgetMax = (float) ($input['budget_max'] ?? 999999999); $priorities = $input['priorities'] ?? []; $category = $input['category'] ?? null; $products = $this->getFilteredProducts( $ageMin, $ageMax, $budgetMin, $budgetMax, $category ); if ($products->isEmpty()) { return [ 'recommendations' => collect(), 'criterias' => [], 'decision_matrix' => [], 'normalized_matrix' => [], 'weights' => [], 'preference_scores' => [], ]; } $criterias = Criteria::orderBy('weight_order')->get(); if ($criterias->isEmpty()) { $recommendations = $products->take(5)->map(fn($p, $i) => [ 'rank' => $i + 1, 'product' => $p, 'score' => 0.0, 'explanation' => 'Produk ini tersedia dalam rentang usia dan budget Anda.', ]); return [ 'recommendations' => $recommendations, 'criterias' => [], 'decision_matrix' => [], 'normalized_matrix' => [], 'weights' => [], 'preference_scores' => [], ]; } $weights = $this->buildWeightsFromPriorities($priorities, $criterias); $matrix = $this->buildMatrix($products, $criterias); $normalized = $this->normalizeMatrix($matrix, $criterias); $scores = $this->calculatePreferenceScores($normalized, $weights, $criterias); $ranked = $scores->sortByDesc('score')->take(5)->values(); $topProductIds = $ranked->pluck('product_id')->all(); $rankedScoresByProductId = $ranked ->mapWithKeys(fn($item) => [$item['product_id'] => round($item['score'], 4)]) ->all(); $criteriasWithWeight = []; foreach ($criterias as $c) { $criteriasWithWeight[] = [ 'name' => $c->name, 'type' => $c->type, 'weight' => $weights[$c->name] ?? 0, ]; } $decisionMatrix = []; foreach ($topProductIds as $productId) { $row = $matrix[$productId] ?? null; if (!$row) { continue; } $product = $products->firstWhere('id', $productId); $r = ['product_id' => $productId, 'product_name' => $product ? $product->name : '-']; foreach ($criterias as $c) { $r[$c->name] = $row[$c->name] ?? 0; } $decisionMatrix[] = $r; } $normalizedMatrix = []; foreach ($topProductIds as $productId) { $row = $normalized[$productId] ?? null; if (!$row) { continue; } $product = $products->firstWhere('id', $productId); $r = ['product_id' => $productId, 'product_name' => $product ? $product->name : '-']; foreach ($criterias as $c) { $val = $row[$c->name] ?? 0; $r[$c->name] = round((float) $val, 4); } $normalizedMatrix[] = $r; } $preferenceScores = $rankedScoresByProductId; $criteriaNames = $criterias->keyBy('id')->map->name; $recommendations = $ranked->map(function ($item, $index) use ($products, $criteriaNames, $priorities) { $product = $products->firstWhere('id', $item['product_id']); if (!$product) { return null; } $explanation = $this->buildExplanation($product, $item['score'], $priorities, $criteriaNames); return [ 'rank' => $index + 1, 'product' => $product, 'score' => round($item['score'], 4), 'explanation' => $explanation, ]; })->filter()->values(); return [ 'recommendations' => $recommendations, 'criterias' => $criteriasWithWeight, 'decision_matrix' => $decisionMatrix, 'normalized_matrix' => $normalizedMatrix, 'weights' => $weights, 'preference_scores' => $preferenceScores, ]; } protected function getFilteredProducts( int $ageMin, int $ageMax, float $budgetMin, float $budgetMax, ?string $category = null ): Collection { $query = Product::with('criterias') ->where('stock', '>', 0) ->whereBetween('price', [$budgetMin, $budgetMax]); // Filter kategori jika dipilih if (!empty($category)) { $query->where('category', $category); } return $query->get() ->filter(function ($product) use ($ageMin, $ageMax) { if (empty($product->age_range)) { return true; } return $this->ageRangeOverlaps( $product->age_range, $ageMin, $ageMax ); }) ->values(); } protected function ageRangeOverlaps(string $ageRange, int $childMin, int $childMax): bool { if (preg_match('/^(\d+)\s*-\s*(\d+)$/', trim($ageRange), $m)) { $pMin = (int) $m[1]; $pMax = (int) $m[2]; return $childMax >= $pMin && $childMin <= $pMax; } return true; } protected function buildWeightsFromPriorities(array $priorities, Collection $criterias): array { $raw = []; $sum = 0.0; foreach ($criterias as $c) { $input = (int) ($priorities[(string) $c->id] ?? $priorities[$c->id] ?? 3); if ($input < 1) $input = 1; if ($input > 5) $input = 5; // Jika Cost, dibalik if ($c->type === 'cost') { $w = 6 - $input; } else { $w = $input; } $raw[$c->name] = $w; $sum += $w; } $weights = []; foreach ($raw as $name => $value) { $weights[$name] = $value / $sum; } return $weights; } // protected function buildWeightsFromPriorities(array $priorities, Collection $criterias): array // { // // priorities: [criteria_id => 1..5] // // return weights by criteria name: ['Harga' => 0.2, 'Kualitas' => 0.2, ...] // $raw = []; // $sum = 0.0; // foreach ($criterias as $c) { // $w = (float) ($priorities[(string) $c->id] ?? $priorities[$c->id] ?? 1); // if ($w < 1) $w = 1; // if ($w > 5) $w = 5; // $raw[$c->name] = $w; // $sum += $w; // } // $count = max(1, $criterias->count()); // $weights = []; // foreach ($raw as $name => $v) { // $weights[$name] = $sum > 0 ? $v / $sum : 1 / $count; // } // return $weights; // } protected function buildMatrix(Collection $products, Collection $criterias): array { $matrix = []; foreach ($products as $product) { $row = ['product_id' => $product->id]; foreach ($product->criterias as $pc) { $criteria = $criterias->firstWhere('id', $pc->id); if ($criteria) { $row[$criteria->name] = (float) $pc->pivot->value; } } foreach ($criterias as $c) { if (!isset($row[$c->name])) { $row[$c->name] = $c->name === 'Harga' ? (float) $product->price : 0; } } $matrix[$product->id] = $row; } return $matrix; } protected function normalizeMatrix(array $matrix, Collection $criterias): array { if (empty($matrix)) { return []; } $normalized = []; foreach (array_keys($matrix) as $pid) { $normalized[$pid] = ['product_id' => $pid]; } foreach ($criterias as $criteria) { $key = $criteria->name; $values = array_column($matrix, $key); $max = max($values); $min = min($values); $isCost = $criteria->type === 'cost'; if ($isCost) { foreach ($matrix as $pid => $row) { $val = (float) ($row[$key] ?? 0); $normalized[$pid][$key] = $min > 0 && $val > 0 ? $min / $val : 0; } } else { foreach ($matrix as $pid => $row) { $val = (float) ($row[$key] ?? 0); $normalized[$pid][$key] = $max > 0 ? $val / $max : 0; } } } return $normalized; } protected function calculatePreferenceScores(array $normalized, array $weights, Collection $criterias): Collection { $result = collect(); foreach ($normalized as $productId => $row) { $vi = 0; foreach ($criterias as $c) { $w = (float) ($weights[$c->name] ?? (1 / max(1, $criterias->count()))); $r = (float) ($row[$c->name] ?? 0); $vi += $w * $r; } $result->push(['product_id' => $productId, 'score' => $vi]); } return $result; } protected function buildExplanation($product, float $score, array $priorities, $criteriaNames): string { return 'Produk ini direkomendasikan karena memiliki nilai tinggi pada kriteria yang Anda prioritaskan.'; } }