MIF_E31222307/app/Services/ValidasiRekomendasiService.php

268 lines
9.6 KiB
PHP

<?php
namespace App\Services;
use App\Models\RekomendasiAhli;
use App\Models\ValidasiRekomendasi;
use App\Models\Rekomendasi;
use App\Models\Makanan;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ValidasiRekomendasiService
{
/**
* Bandingkan rekomendasi pakar dengan hasil AHP
* @param int $topN Jumlah makanan teratas yang akan dibandingkan (default: 4)
* @return array Hasil validasi dan persentase kecocokan
*/
public function bandingkanRekomendasi(int $topN = 4): array
{
try {
// Ambil semua rekomendasi pakar
$rekomendasiPakar = RekomendasiAhli::with(['makanan', 'waktuMakan', 'komponen'])->get();
\Log::info('Rekomendasi pakar count: ' . $rekomendasiPakar->count());
// Ambil hasil AHP terbaru untuk setiap waktu makan dan komponen
$hasilAHP = $this->ambilHasilAHP($topN);
\Log::info('Hasil AHP count: ' . $hasilAHP->count());
\Log::info('Hasil AHP structure:', $hasilAHP->toArray());
$hasilValidasi = [];
$totalLebihBaik = 0;
$totalSetara = 0;
$totalLebihBuruk = 0;
foreach ($rekomendasiPakar as $pakar) {
// Cari hasil AHP yang sesuai dengan waktu makan dan komponen
$hasilAHPUntukKomponen = $hasilAHP
->where('waktu_makan_id', $pakar->waktu_makan_id)
->where('komponen_id', $pakar->komponen_id)
->first();
if (!$hasilAHPUntukKomponen) {
\Log::info('Tidak ada hasil AHP untuk waktu_makan_id: ' . $pakar->waktu_makan_id . ', komponen_id: ' . $pakar->komponen_id);
continue;
}
$makananPakar = $pakar->makanan;
$makananSistem = collect($hasilAHPUntukKomponen['makanans']);
$makananSistemIds = $makananSistem->pluck('id')->toArray();
\Log::info('Processing pakar:', [
'pakar_id' => $pakar->id,
'makanan_pakar_id' => $pakar->makanan_id,
'makanan_sistem_ids' => $makananSistemIds
]);
// Cek kecocokan
$status = $this->tentukanStatusKecocokan($makananPakar, $makananSistem);
// Update counter
switch ($status) {
case 'lebih_baik':
$totalLebihBaik++;
break;
case 'setara':
$totalSetara++;
break;
case 'lebih_buruk':
$totalLebihBuruk++;
break;
}
// Simpan hasil validasi
$hasilValidasi[] = [
'hari' => $pakar->hari,
'waktu_makan_id' => $pakar->waktu_makan_id,
'komponen_id' => $pakar->komponen_id,
'makanan_pakar_id' => $pakar->makanan_id,
'makanan_sistem_ids' => $makananSistemIds,
'status_kecocokan' => $status
];
}
// Hitung persentase kecocokan
// $total = count($hasilValidasi);
$total = $totalLebihBaik + $totalSetara + $totalLebihBuruk;
$persentaseCocok = $total > 0
? (($totalLebihBaik + $totalSetara) / $total) * 100 : 0;
// Simpan hasil ke database
$this->simpanHasilValidasi($hasilValidasi);
return [
'hasil_validasi' => $hasilValidasi,
'statistik' => [
'total' => $total,
'lebih_baik' => $totalLebihBaik,
'setara' => $totalSetara,
'lebih_buruk' => $totalLebihBuruk,
'persentase_cocok' => round($persentaseCocok, 2)
]
];
} catch (\Exception $e) {
\Log::error('Error in bandingkanRekomendasi: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Ambil hasil AHP terbaru untuk setiap waktu makan dan komponen
*/
private function ambilHasilAHP(int $topN): Collection
{
try {
// Ambil data rekomendasi dengan eager loading
$rekomendasis = Rekomendasi::with(['makanan', 'waktuMakan', 'komponen'])
->orderBy('waktu_makan_id')
->orderBy('komponen_id')
->orderBy('nilai_akhir', 'desc')
->get();
\Log::info('Total rekomendasi found: ' . $rekomendasis->count());
// Group by waktu makan dan komponen
$groupedData = $rekomendasis->groupBy(['waktu_makan_id', 'komponen_id']);
// Transform data structure
$transformedData = collect();
foreach ($groupedData as $waktuMakanId => $komponenGroups) {
foreach ($komponenGroups as $komponenId => $items) {
// Ambil top N makanan berdasarkan nilai_akhir
$topMakanans = $items->take($topN)
->map(function($item) {
return [
'id' => $item->makanan_id,
'nama' => $item->makanan->nama,
'nilai_akhir' => $item->nilai_akhir,
'energi' => $item->makanan->energi,
'lemak' => $item->makanan->lemak,
'karbohidrat' => $item->makanan->karbohidrat,
'natrium' => $item->makanan->natrium
];
});
$transformedData->push([
'waktu_makan_id' => $waktuMakanId,
'komponen_id' => $komponenId,
'makanans' => $topMakanans
]);
\Log::info("Added data for waktu_makan_id: $waktuMakanId, komponen_id: $komponenId, makanan_count: " . $topMakanans->count());
}
}
\Log::info('Transformed data count: ' . $transformedData->count());
return $transformedData;
} catch (\Exception $e) {
\Log::error('Error in ambilHasilAHP: ' . $e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* Tentukan status kecocokan antara makanan pakar dan sistem
*/
private function tentukanStatusKecocokan($makananPakar, Collection $makananSistem): string
{
$toleransi = 0.10; // 10%
$lebihBaik = 0;
$setara = 0;
$lebihBuruk = 0;
foreach ($makananSistem as $makanan) {
$lemakPakar = $makananPakar->lemak;
$natriumPakar = $makananPakar->natrium;
$lemakAHP = $makanan['lemak'];
$natriumAHP = $makanan['natrium'];
$lemakSelisih = $this->hitungSelisihPersen($lemakPakar, $lemakAHP);
$natriumSelisih = $this->hitungSelisihPersen($natriumPakar, $natriumAHP);
if ($lemakAHP < $lemakPakar && $natriumAHP < $natriumPakar) {
$lebihBaik++;
} elseif ($lemakSelisih <= $toleransi && $natriumSelisih <= $toleransi) {
$setara++;
} else {
$lebihBuruk++;
}
}
// Tentukan status dominan
// Logika khusus: Jika lebih_baik = 1 dan setara = 1, maka dianggap lebih_baik
if ($lebihBaik === 1 && $setara === 1) {
return 'lebih_baik';
}
// Tentukan status dominan (logika umum)
if ($lebihBaik >= max($setara, $lebihBuruk)) {
return 'lebih_baik';
} elseif ($setara >= max($lebihBaik, $lebihBuruk)) {
return 'setara';
} else {
return 'lebih_buruk';
}
}
private function hitungSelisihPersen($nilai1, $nilai2): float
{
if ($nilai1 == 0 && $nilai2 == 0) return 0;
if ($nilai1 == 0 || $nilai2 == 0) return 1; // 100%
return abs($nilai1 - $nilai2) / max($nilai1, $nilai2);
}
/**
* Cek apakah kandungan gizi dua makanan mirip (±10%)
*/
private function isGiziMirip($makanan1, $makanan2): bool
{
$toleransi = 0.10; // 10%
$giziMirip = true;
$giziMirip &= $this->isDalamToleransi($makanan1->energi, $makanan2['energi'], $toleransi);
$giziMirip &= $this->isDalamToleransi($makanan1->lemak, $makanan2['lemak'], $toleransi);
$giziMirip &= $this->isDalamToleransi($makanan1->karbohidrat, $makanan2['karbohidrat'], $toleransi);
$giziMirip &= $this->isDalamToleransi($makanan1->natrium, $makanan2['natrium'], $toleransi);
return $giziMirip;
}
/**
* Cek apakah dua nilai dalam toleransi yang ditentukan
*/
private function isDalamToleransi($nilai1, $nilai2, $toleransi): bool
{
if ($nilai1 == 0 && $nilai2 == 0) return true;
if ($nilai1 == 0 || $nilai2 == 0) return false;
$selisih = abs($nilai1 - $nilai2) / max($nilai1, $nilai2);
return $selisih <= $toleransi;
}
/**
* Simpan hasil validasi ke database
*/
private function simpanHasilValidasi(array $hasilValidasi): void
{
// Hapus data validasi lama
ValidasiRekomendasi::truncate();
// Simpan data baru
foreach ($hasilValidasi as $validasi) {
ValidasiRekomendasi::create($validasi);
}
}
}