268 lines
9.6 KiB
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);
|
|
}
|
|
}
|
|
}
|