MIF_E31230745/app/Http/Controllers/RekomendasiController.php

521 lines
21 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers;
use App\Models\Student;
use App\Models\PolijeMajor;
use App\Models\Recommendation;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RekomendasiController extends Controller
{
public function index()
{
$user = Auth::user();
$student = null;
if ($user) {
$student = (object) [
'user_id' => $user->id,
'nis' => $user->nis ?? null,
'kelompok_asal' => $user->kelompok_asal ?? null,
'foto' => $user->foto ?? null,
];
}
return view('rekomendasi.input', compact('student'));
}
/**
* Generate textual explanation untuk setiap kriteria
* Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail
*/
private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasi)
{
$explanations = [];
// 1. Penjelasan Nilai Akademik
$skorNilai = $detail['nilai'] ?? 0;
if ($skorNilai >= 0.8) {
$explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai) sangat sesuai dengan jalur pendidikan yang dibutuhkan jurusan ini.";
} elseif ($skorNilai >= 0.6) {
$explanations['nilai'] = "✓ Nilai akademik Anda ($katNilai) cukup sesuai dengan persyaratan jurusan ini.";
} else {
$explanations['nilai'] = "⚠️ Nilai akademik Anda ($katNilai) masih perlu ditingkatkan untuk optimal di jurusan ini, namun tetap relevan.";
}
// 2. Penjelasan Minat
$skorMinat = $detail['minat'] ?? 0;
if ($skorMinat >= 0.8) {
$explanations['minat'] = "✅ Minat Anda sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama.";
} elseif ($skorMinat >= 0.6) {
$explanations['minat'] = "✓ Minat Anda cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama.";
} else {
$explanations['minat'] = " Minat Anda memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama.";
}
// 3. Penjelasan Preferensi Studi
$skorPref = $detail['pref'] ?? 0;
if ($skorPref >= 0.8) {
$explanations['pref'] = "✅ Metode pembelajaran \"$prefStudi\" yang Anda pilih sangat sesuai dengan pendekatan pembelajaran $jurusanNama.";
} elseif ($skorPref >= 0.6) {
$explanations['pref'] = " Preferensi studi \"$prefStudi\" Anda cocok dengan sistem pembelajaran yang diterapkan.";
} else {
$explanations['pref'] = " Jurusan ini menawarkan elemen pembelajaran \"$prefStudi\" yang relevan dengan preferensi Anda.";
}
// 4. Penjelasan Cita-cita
$skorCita = $detail['cita'] ?? 0;
if ($skorCita >= 0.8) {
$explanations['cita'] = " Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan bidang ini.";
} elseif ($skorCita >= 0.6) {
$explanations['cita'] = " Cita-cita Anda memiliki potensi besar untuk dicapai melalui jurusan ini.";
} else {
$explanations['cita'] = " Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda.";
}
// 5. Penjelasan Prestasi
$skorPrestasi = $detail['prestasi'] ?? 0;
if ($skorPrestasi >= 0.7) {
$explanations['prestasi'] = " Prestasi Anda mencerminkan potensi kuat untuk sukses dan berkembang di jurusan ini.";
} elseif ($skorPrestasi >= 0.4) {
$explanations['prestasi'] = " Prestasi Anda menunjukkan kemampuan dasar yang memadai dan relevan.";
} else {
$explanations['prestasi'] = " Prestasi tidak menjadi hambatan untuk mengembangkan diri dan berkembang di jurusan ini.";
}
return $explanations;
}
/**
* ============================================================
* ALGORITMA NAIVE BAYES UNTUK REKOMENDASI JURUSAN
* Sesuai flowchart:
* 1. Input Data
* 2. Preprocessing Data
* 3. Tentukan Hipotesis (H)
* 4. Hitung Probabilitas Awal (Prior) P(H)
* 5. Hitung Likelihood P(X|H) per fitur
* 6. Hitung Probabilitas Gabungan (Rumus Naive Bayes)
* P(H|X) ∝ P(H) × P(X1|H) × P(X2|H) × ... × P(Xn|H)
* 7. Klasifikasi (Hasil Rekomendasi)
* ============================================================
*/
public function proses(Request $request)
{
$validated = $request->validate([
'mtk' => ['nullable', 'numeric', 'min:0', 'max:100'],
'fisika' => ['nullable', 'numeric', 'min:0', 'max:100'],
'kimia' => ['nullable', 'numeric', 'min:0', 'max:100'],
'biologi' => ['nullable', 'numeric', 'min:0', 'max:100'],
'ekonomi' => ['nullable', 'numeric', 'min:0', 'max:100'],
'geografi' => ['nullable', 'numeric', 'min:0', 'max:100'],
'sosiologi' => ['nullable', 'numeric', 'min:0', 'max:100'],
'sejarah' => ['nullable', 'numeric', 'min:0', 'max:100'],
'minat' => ['required', 'string', 'max:255'],
'pref_studi' => ['required', 'string', 'in:Sains & Teknologi,Pertanian & Lingkungan,Kesehatan & Ilmu Hayat,Bisnis & Manajemen,Sosial & Humaniora,Praktikum,Teori'],
'cita_cita' => ['required', 'string', 'max:255'],
'prestasi' => ['nullable', 'string', 'max:255'],
]);
$kelompokAsal = Auth::user()?->kelompok_asal;
if ($kelompokAsal === 'IPA') {
$request->validate([
'mtk' => ['required', 'numeric', 'min:0', 'max:100'],
'fisika' => ['required', 'numeric', 'min:0', 'max:100'],
'kimia' => ['required', 'numeric', 'min:0', 'max:100'],
'biologi' => ['required', 'numeric', 'min:0', 'max:100'],
]);
} elseif ($kelompokAsal === 'IPS') {
$request->validate([
'ekonomi' => ['required', 'numeric', 'min:0', 'max:100'],
'geografi' => ['required', 'numeric', 'min:0', 'max:100'],
'sosiologi' => ['required', 'numeric', 'min:0', 'max:100'],
'sejarah' => ['required', 'numeric', 'min:0', 'max:100'],
]);
}
$epsilon = 1e-9;
// ================================================================
// LANGKAH 1: INPUT DATA
// ================================================================
$scores = [
'mtk' => $validated['mtk'] ?? null,
'fisika' => $validated['fisika'] ?? null,
'kimia' => $validated['kimia'] ?? null,
'biologi' => $validated['biologi'] ?? null,
'ekonomi' => $validated['ekonomi'] ?? null,
'geografi' => $validated['geografi'] ?? null,
'sosiologi' => $validated['sosiologi'] ?? null,
'sejarah' => $validated['sejarah'] ?? null,
];
$minatRaw = strtolower(trim($validated['minat'] ?? ''));
$prefStudi = $validated['pref_studi'] ?? 'Sains & Teknologi';
$citaRaw = strtolower(trim($validated['cita_cita'] ?? ''));
$prestasiRaw = strtolower(trim($validated['prestasi'] ?? ''));
// ================================================================
// LANGKAH 2: PREPROCESSING DATA
// ================================================================
// 2a. Hitung rata-rata nilai
$validScores = array_filter($scores, fn($v) => $v !== null && $v !== '');
$average = count($validScores) > 0 ? array_sum($validScores) / count($validScores) : 0;
// 2b. Kategorisasi nilai
if ($average >= 85) {
$katNilai = 'Tinggi';
} elseif ($average >= 70) {
$katNilai = 'Sedang';
} else {
$katNilai = 'Rendah';
}
// 2c. Skor prestasi
$prestasiScore = $this->hitungSkorPrestasi($prestasiRaw);
// ================================================================
// LANGKAH 3: TENTUKAN HIPOTESIS (H)
// H = {Jurusan1, Jurusan2, ..., JurusanN} dari database
// ================================================================
$jurusanList = PolijeMajor::all();
if ($jurusanList->isEmpty()) {
return back()->with('error', 'Data jurusan belum tersedia di database.');
}
$jumlahJurusan = $jurusanList->count();
// ================================================================
// LANGKAH 4: HITUNG PROBABILITAS AWAL (PRIOR) P(H)
// Prior uniform: P(H) = 1 / jumlah_jurusan
// ================================================================
$prior = 1 / $jumlahJurusan;
// ================================================================
// LANGKAH 5 & 6: HITUNG LIKELIHOOD DAN PROBABILITAS GABUNGAN
// Rumus Naive Bayes:
// P(H|X) ∝ P(H) × P(X1|H) × P(X2|H) × P(X3|H) × P(X4|H) × P(X5|H)
//
// Fitur (Xi):
// X1 = Nilai Akademik → P(nilai|H)
// X2 = Minat → P(minat|H)
// X3 = Preferensi Studi → P(pref|H)
// X4 = Cita-cita → P(cita|H)
// X5 = Prestasi → P(prestasi|H)
//
// Weighted Naive Bayes (log-space):
// log P(H|X) = log P(H) + Σ wi × log P(Xi|H)
//
// Bobot (wi):
// w1 = 0.40 (Nilai), w2 = 0.35 (Minat), w3 = 0.15 (Pref),
// w4 = 0.05 (Cita-cita), w5 = 0.05 (Prestasi)
// ================================================================
$weights = [
'nilai' => 0.40,
'minat' => 0.35,
'pref' => 0.15,
'cita' => 0.05,
'prestasi' => 0.05,
];
$logPosteriors = [];
$detailPerJurusan = [];
foreach ($jurusanList as $jurusan) {
// --- Log Prior ---
$logPrior = log(max($prior, $epsilon));
// --- X1: Likelihood Nilai Akademik P(nilai|H) ---
$pNilai = $this->hitungLikelihoodNilai($scores, $jurusan->bobot_mapel);
// --- X2: Likelihood Minat P(minat|H) ---
$pMinat = $this->hitungLikelihoodMinat($minatRaw, $jurusan->keywords);
// --- X3: Likelihood Preferensi Studi P(pref|H) ---
$pPref = $this->hitungLikelihoodPref($prefStudi, $jurusan->preferensi_studi);
// --- X4: Likelihood Cita-cita P(cita|H) ---
$pCita = $this->hitungLikelihoodCitaCita($citaRaw, $jurusan->keywords);
// --- X5: Likelihood Prestasi P(prestasi|H) ---
$pPrestasi = $this->hitungLikelihoodPrestasi($prestasiScore);
// --- Probabilitas Gabungan (Weighted Naive Bayes) ---
// log P(H|X) = log P(H) + w1·log P(X1|H) + w2·log P(X2|H) + ... + w5·log P(X5|H)
$logPosterior = $logPrior
+ $weights['nilai'] * log(max($pNilai, $epsilon))
+ $weights['minat'] * log(max($pMinat, $epsilon))
+ $weights['pref'] * log(max($pPref, $epsilon))
+ $weights['cita'] * log(max($pCita, $epsilon))
+ $weights['prestasi'] * log(max($pPrestasi, $epsilon));
$logPosteriors[$jurusan->nama_jurusan] = $logPosterior;
// Simpan detail per kriteria untuk tampilan
$detailPerJurusan[$jurusan->nama_jurusan] = [
'nilai' => round($pNilai, 4),
'minat' => round($pMinat, 4),
'pref' => round($pPref, 4),
'cita' => round($pCita, 4),
'prestasi' => round($pPrestasi, 4),
];
}
// ================================================================
// LANGKAH 7: KLASIFIKASI (HASIL REKOMENDASI)
// Konversi log-posterior ke probabilitas menggunakan softmax
// P(Hk|X) = exp(log Pk) / Σ exp(log Pi)
// ================================================================
$maxLog = max($logPosteriors);
$expVals = [];
$sumExp = 0.0;
foreach ($logPosteriors as $namaJurusan => $lv) {
$expVals[$namaJurusan] = exp($lv - $maxLog);
$sumExp += $expVals[$namaJurusan];
}
$hasilAkhir = [];
foreach ($expVals as $namaJurusan => $val) {
$prob = $val / max($sumExp, $epsilon);
$detail = $detailPerJurusan[$namaJurusan];
$explanations = $this->generateExplanation(
$namaJurusan,
$detail,
$katNilai,
$minatRaw,
$prefStudi,
$prestasiRaw
);
$hasilAkhir[] = [
'jurusan' => $namaJurusan,
'skor' => round($prob, 4),
'detail' => $detail,
'explanation' => $explanations,
'kecocokan_nilai' => $katNilai,
'kecocokan_minat' => $minatRaw,
'kecocokan_pref' => $prefStudi,
];
}
// Urutkan berdasarkan skor tertinggi
usort($hasilAkhir, fn($a, $b) => $b['skor'] <=> $a['skor']);
// Ambil data jurusan teratas untuk detail view
$topJurusan = !empty($hasilAkhir) ? PolijeMajor::where('nama_jurusan', $hasilAkhir[0]['jurusan'] ?? '')->first() : null;
// Simpan ke database
$user = Auth::user();
$savedRec = null;
if ($user) {
$savedRec = Recommendation::create([
'user_id' => $user->id,
'mtk' => $validated['mtk'] ?? null,
'fisika' => $validated['fisika'] ?? null,
'kimia' => $validated['kimia'] ?? null,
'biologi' => $validated['biologi'] ?? null,
'ekonomi' => $validated['ekonomi'] ?? null,
'geografi' => $validated['geografi'] ?? null,
'sosiologi' => $validated['sosiologi'] ?? null,
'sejarah' => $validated['sejarah'] ?? null,
'minat' => $validated['minat'],
'preferensi_studi' => $validated['pref_studi'],
'cita_cita' => $validated['cita_cita'],
'prestasi' => $validated['prestasi'] ?? '',
'hasil_rekomendasi' => $hasilAkhir,
]);
}
// Simpan recommendation_id ke session agar bisa dipakai link chatbot
$recId = $savedRec ? $savedRec->id : null;
session(['last_recommendation_id' => $recId]);
// Simpan ke session untuk chatbot
if (count($hasilAkhir) > 0) {
$topResult = $hasilAkhir[0];
// Ambil top 3 untuk konteks chatbot
$top3 = array_slice($hasilAkhir, 0, 3);
session([
'recomendation_data' => [
'jurusan' => $topResult['jurusan'],
'skor' => $topResult['skor'], // Sudah 0-1, jangan ×100
'detail' => $topResult['detail'] ?? [],
'nilai' => $katNilai,
'rata_rata' => round($average, 1),
'minat' => $minatRaw,
'pref_studi' => $prefStudi,
'cita_cita' => $citaRaw,
'prestasi' => $prestasiRaw,
'top3' => array_map(fn($r) => [
'jurusan' => $r['jurusan'],
'skor' => $r['skor'],
], $top3),
]
]);
}
return view('rekomendasi.hasil', compact(
'hasilAkhir', 'katNilai', 'average', 'prefStudi', 'prestasiScore', 'topJurusan'
));
}
// ==================================================================
// FUNGSI LIKELIHOOD — P(Xi | H)
// ==================================================================
/**
* P(nilai | H) — Likelihood nilai akademik terhadap jurusan
* Menggunakan bobot_mapel dari database untuk menghitung
* weighted average yang dinormalisasi ke range probabilitas.
*/
private function hitungLikelihoodNilai(array $scores, ?array $bobotMapel): float
{
// Jika tidak ada bobot, gunakan rata-rata biasa
if (empty($bobotMapel)) {
$valid = array_filter($scores, fn($v) => $v !== null && $v !== '');
if (empty($valid)) return 0.3;
$avg = array_sum($valid) / count($valid);
return $this->normalisasiProbabilitas($avg / 100, 0.10, 0.95);
}
$weightedSum = 0;
$totalWeight = 0;
foreach ($bobotMapel as $subject => $weight) {
$nilai = floatval($scores[$subject] ?? 0);
if ($nilai > 0 && $weight > 0) {
$weightedSum += $weight * ($nilai / 100);
$totalWeight += $weight;
}
}
if ($totalWeight == 0) return 0.3;
$weightedAvg = $weightedSum / $totalWeight;
return $this->normalisasiProbabilitas($weightedAvg, 0.10, 0.95);
}
/**
* P(minat | H) — Likelihood minat terhadap jurusan
* Menggunakan keyword matching terhadap keywords jurusan dari database.
*/
private function hitungLikelihoodMinat(string $minatRaw, ?array $keywords): float
{
if (empty($keywords) || empty($minatRaw)) {
return 0.20; // probabilitas dasar jika tidak ada data
}
$matchCount = 0;
foreach ($keywords as $keyword) {
if (stripos($minatRaw, strtolower($keyword)) !== false) {
$matchCount++;
}
}
// Rasio kecocokan keyword
$matchRatio = $matchCount / count($keywords);
// Konversi ke range probabilitas: 0 match → 0.10, full match → 0.95
return $this->normalisasiProbabilitas($matchRatio, 0.10, 0.95);
}
/**
* P(pref | H) — Likelihood preferensi studi terhadap jurusan
* Membandingkan preferensi siswa dengan preferensi_studi jurusan dari database.
*/
private function hitungLikelihoodPref(string $prefStudi, ?array $jurusanPref): float
{
if (empty($jurusanPref)) {
return 0.40; // probabilitas netral
}
// Cek apakah preferensi siswa ada di list preferensi jurusan
if (in_array($prefStudi, $jurusanPref)) {
return 0.85; // cocok
}
return 0.15; // tidak cocok
}
/**
* P(cita_cita | H) — Likelihood cita-cita terhadap jurusan
* Menggunakan keyword matching dari cita-cita siswa terhadap keywords jurusan.
*/
private function hitungLikelihoodCitaCita(string $citaRaw, ?array $keywords): float
{
if (empty($keywords) || empty($citaRaw)) {
return 0.25; // probabilitas dasar
}
$matchCount = 0;
foreach ($keywords as $keyword) {
if (stripos($citaRaw, strtolower($keyword)) !== false) {
$matchCount++;
}
}
$matchRatio = $matchCount / count($keywords);
return $this->normalisasiProbabilitas($matchRatio, 0.10, 0.90);
}
/**
* P(prestasi | H) — Likelihood prestasi
* Prestasi bersifat umum (tidak spesifik per jurusan), sehingga
* memberikan boost yang sama untuk semua jurusan.
*/
private function hitungLikelihoodPrestasi(float $prestasiScore): float
{
// Konversi skor prestasi (0-1) ke range probabilitas
return $this->normalisasiProbabilitas($prestasiScore, 0.20, 0.90);
}
// ==================================================================
// FUNGSI HELPER
// ==================================================================
/**
* Normalisasi nilai (0-1) ke range probabilitas [min, max]
* Agar tidak ada likelihood 0 atau 1 (menghindari dominasi)
*/
private function normalisasiProbabilitas(float $value, float $min = 0.10, float $max = 0.95): float
{
return $min + ($value * ($max - $min));
}
/**
* Hitung skor prestasi berdasarkan keyword
*/
private function hitungSkorPrestasi(string $prestasiRaw): float
{
$prestasiRaw = strtolower(trim($prestasiRaw));
if (empty($prestasiRaw)) {
return 0.0;
}
if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $prestasiRaw)) {
return 0.90;
} elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|perak)/', $prestasiRaw)) {
return 0.75;
} elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $prestasiRaw)) {
return 0.60;
}
return 0.30;
}
/**
* Tampilkan history rekomendasi
*/
public function historyRekomendasi()
{
$user = Auth::user();
$recommendations = Recommendation::where('user_id', $user->id)
->orderBy('created_at', 'desc')
->get();
return view('history.rekomendasi', compact('recommendations'));
}
}