$user->id, 'nis' => $user->nis ?? null, 'kelompok_asal' => $user->kelompok_asal ?? null, 'foto' => $user->foto ?? null, ]; } // Ambil riwayat rekomendasi user (limit 10 terakhir, diurutkan dari terbaru) $recommendations = Recommendation::where('user_id', $user->id) ->orderBy('created_at', 'desc') ->limit(10) ->get(); return view('rekomendasi.input', compact('student', 'recommendations')); } /** * Generate textual explanation untuk setiap kriteria * Menjelaskan "mengapa jurusan ini cocok" berdasarkan scoring detail */ private function generateExplanation($jurusanNama, $detail, $katNilai, $kategoriMinat, $prefStudi, $prestasiRaw, array $prestasiAnalysis = []) { $explanations = []; // 1. Penjelasan Nilai Akademik (Kriteria 1) $skorNilai = $detail['nilai'] ?? 0; if ($skorNilai >= 0.8) { $explanations['nilai'] = "✅ Nilai akademik Anda ($katNilai: avg score tinggi) 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 (Kriteria 2) $skorMinat = $detail['minat'] ?? 0; if ($skorMinat >= 0.8) { $explanations['minat'] = "✅ Minat Anda ($kategoriMinat) sangat sesuai dan cocok dengan fokus kurikulum $jurusanNama. Anda akan mempelajari hal-hal yang Anda sukai."; } elseif ($skorMinat >= 0.6) { $explanations['minat'] = "✓ Minat Anda ($kategoriMinat) cukup relevan dan sesuai dengan area pembelajaran di $jurusanNama."; } else { $explanations['minat'] = "ℹ️ Minat Anda ($kategoriMinat) memiliki kesamaan dan relevansi dengan aspek-aspek tertentu di $jurusanNama."; } // 3. Penjelasan Preferensi Studi (Kriteria 3) $skorPref = $detail['pref'] ?? 0; if ($skorPref >= 0.8) { $explanations['pref'] = "✅ Preferensi studi \"$prefStudi\" Anda sangat sesuai dengan karakter jurusan $jurusanNama."; } elseif ($skorPref >= 0.6) { $explanations['pref'] = "✓ Preferensi studi \"$prefStudi\" Anda cukup relevan dengan jurusan ini."; } else { $explanations['pref'] = "ℹ️ Jurusan ini masih memiliki keterkaitan dengan preferensi studi \"$prefStudi\" Anda."; } // 4. Penjelasan Cita-cita (Kriteria 4) - IMPROVED with more detail $skorCita = $detail['cita'] ?? 0; if ($skorCita >= 0.8) { $explanations['cita'] = "✅ Cita-cita karir Anda sangat sesuai dan aligned dengan standar lulusan $jurusanNama. Jurusan ini secara langsung mempersiapkan Anda untuk mencapai cita-cita tersebut."; } elseif ($skorCita >= 0.6) { $explanations['cita'] = "✓ Cita-cita Anda memiliki potensi besar untuk dicapai melalui pendidikan di $jurusanNama. Kurikulum akan membekali skills yang relevan."; } else { $explanations['cita'] = "ℹ️ Jurusan ini membuka jalur karir yang sesuai dengan cita-cita dan aspirasi Anda, meski tidak secara langsung target-nya."; } // 5. Penjelasan Prestasi (Kriteria 5) - IMPROVED with more detail $skorPrestasi = $detail['prestasi'] ?? 0; if (!($prestasiAnalysis['provided'] ?? false)) { $explanations['prestasi'] = "ℹ️ Prestasi tidak diisi. Jika Anda memiliki prestasi atau achievement, itu dapat meningkatkan score untuk jurusan ini."; return $explanations; } $levelPrestasi = $prestasiAnalysis['level'] ?? 'minimal'; $rawPrestasi = $prestasiAnalysis['raw'] ?? ''; $labelLevel = [ 'tinggi' => 'TINGGI (Juara/Winner)', 'sedang' => 'MENENGAH (Finalis/Medalist)', 'cukup' => 'DASAR (Peserta/Sertifikat)', 'minimal' => 'MINIMAL', ]; if ($skorPrestasi >= 0.8) { $explanations['prestasi'] = "✅ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" sangat relevan dengan $jurusanNama. Ini menunjukkan Anda memiliki dedication dan capability."; } elseif ($skorPrestasi >= 0.6) { $explanations['prestasi'] = "✓ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" cukup relevan dan menunjukkan potensi di bidang ini."; } else { $explanations['prestasi'] = "ℹ️ Prestasi Anda ($labelLevel[$levelPrestasi]): \"$rawPrestasi\" menunjukkan usaha yang dapat dikembangkan lebih lanjut di $jurusanNama."; } return $explanations; } public function proses(Request $request) { // Pastikan perhitungan Naive Bayes selalu berbasis 5 atribut input rekomendasi: // 1) Nilai akademik, 2) Minat, 3) Preferensi studi, 4) Cita-cita, 5) Prestasi. $user = Auth::user(); $kelompokAsal = strtoupper($user->kelompok_asal ?? 'IPA'); // Enhanced validation rules dengan lebih strict untuk non-akademik fields $rules = [ '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|min:3|max:255', 'pref_studi' => 'required|string|in:Sains & Teknologi,Pertanian & Lingkungan,Kesehatan & Ilmu Hayat,Bisnis & Manajemen,Sosial & Humaniora', 'cita_cita' => 'required|string|min:3|max:255', 'prestasi' => 'nullable|string|min:3|max:255', ]; if ($kelompokAsal === 'IPA') { $rules['mtk'] = 'required|numeric|min:0|max:100'; $rules['fisika'] = 'required|numeric|min:0|max:100'; $rules['kimia'] = 'required|numeric|min:0|max:100'; $rules['biologi'] = 'required|numeric|min:0|max:100'; } else { $rules['ekonomi'] = 'required|numeric|min:0|max:100'; $rules['geografi'] = 'required|numeric|min:0|max:100'; $rules['sosiologi'] = 'required|numeric|min:0|max:100'; $rules['sejarah'] = 'required|numeric|min:0|max:100'; } // Custom error messages untuk lebih informatif $messages = [ 'minat.required' => 'Minat harus diisi (minimal 3 karakter)', 'minat.min' => 'Minat terlalu pendek, jelaskan lebih detail', 'cita_cita.required' => 'Cita-cita harus diisi (minimal 3 karakter)', 'cita_cita.min' => 'Cita-cita terlalu pendek, jelaskan lebih detail', 'prestasi.min' => 'Prestasi terlalu pendek, jelaskan lebih detail', 'pref_studi.required' => 'Pilih salah satu preferensi studi', 'pref_studi.in' => 'Preferensi studi tidak valid', ]; $validated = $request->validate($rules, $messages); // --- 1. PREPROCESSING NILAI (Kriteria 1: Akademik) --- $scores = $request->only(['mtk', 'fisika', 'kimia', 'biologi', 'ekonomi', 'geografi', 'sosiologi', 'sejarah']); $validScores = array_filter($scores); $average = count($validScores) > 0 ? array_sum($validScores) / count($validScores) : 0; // Kategorisasi Nilai berdasarkan config $nilaiCategories = config('polije.nilai_category', []); $katNilai = 'Rendah'; foreach ($nilaiCategories as $category => $range) { if ($average >= $range['min'] && $average <= $range['max']) { $katNilai = $category; break; } } // --- 2. ANALISIS MINAT (Kriteria 2) --- $minatInput = trim((string) ($validated['minat'] ?? '')); // Validasi minat tidak hanya satu kata if (strlen($minatInput) < 3) { return response()->json([ 'success' => false, 'message' => 'Minat harus diisi dengan minimal 3 karakter untuk analisis yang akurat', ])->setStatusCode(422); } $minatRaw = strtolower($minatInput); $minatMapped = $this->mapMinat($minatRaw); // Log untuk audit trail \Log::debug('Minat Analysis', [ 'input' => $minatInput, 'normalized' => $minatRaw, 'mapped' => $minatMapped, ]); // --- 3. PREFERENSI STUDI LANJUTAN (Kriteria 3) --- $prefStudi = $validated['pref_studi']; // --- 4. ANALISIS CITA-CITA (Kriteria 4) --- $citaInput = trim((string) ($validated['cita_cita'] ?? '')); // Validasi cita-cita tidak hanya satu kata if (strlen($citaInput) < 3) { return response()->json([ 'success' => false, 'message' => 'Cita-cita harus diisi dengan minimal 3 karakter untuk analisis yang akurat', ])->setStatusCode(422); } $citaRaw = strtolower($citaInput); $citaMapped = $this->mapCitaCita($citaRaw); // Log untuk audit trail \Log::debug('Cita-cita Analysis', [ 'input' => $citaInput, 'normalized' => $citaRaw, 'mapped' => $citaMapped, ]); // --- 5. ANALISIS PRESTASI (Kriteria 5) --- $prestasiInput = trim((string) ($validated['prestasi'] ?? '')); $isPrestasiFilled = $prestasiInput !== '' && strlen($prestasiInput) >= 3; $prestasiRaw = strtolower($prestasiInput); $prestasiAnalysis = $this->analyzePrestasi($prestasiRaw); $prestasiScore = $prestasiAnalysis['score']; // Log untuk audit trail \Log::debug('Prestasi Analysis', [ 'input' => $prestasiInput, 'is_filled' => $isPrestasiFilled, 'normalized' => $prestasiRaw, 'level' => $prestasiAnalysis['level'] ?? 'not provided', 'score' => $prestasiScore, ]); // --- 6. PERHITUNGAN NAIVE BAYES BERBOBOT --- $cfg = config('polije.criteria', []); $majorMap = PolijeMajor::all()->keyBy('nama_jurusan'); $logPosteriors = []; $detailPerJurusan = []; $epsilon = 1e-9; // Validate config exists if (empty($cfg)) { return response()->json([ 'success' => false, 'message' => 'Konfigurasi sistem rekomendasi tidak ditemukan', ])->setStatusCode(500); } foreach ($cfg as $jurusan => $c) { // Prior: uniform dengan safety check $cfgCount = max(1, count($cfg)); // Prevent division by zero $prior = 1 / $cfgCount; $logPrior = log(max($prior, $epsilon)); // Use global ROC weights (override any per-jurusan editable weights) // ROC-based: nilai 15.6%, minat 45.6%, pref 25.6%, cita 9%, prestasi 4% $globalWeights = ['nilai' => 0.156, 'minat' => 0.456, 'pref' => 0.256, 'cita_cita' => 0.090, 'prestasi' => 0.040]; $weights = $globalWeights; // Jika prestasi kosong, atribut prestasi tidak dihitung dan lakukan normalisasi ulang pada atribut lain if (!$isPrestasiFilled) { $weights['prestasi'] = 0.0; $sumNonPrestasi = ($weights['nilai'] ?? 0) + ($weights['minat'] ?? 0) + ($weights['pref'] ?? 0) + ($weights['cita_cita'] ?? 0); // Normalize weights dengan safety check if ($sumNonPrestasi > $epsilon) { $weights['nilai'] = ($weights['nilai'] ?? 0) / $sumNonPrestasi; $weights['minat'] = ($weights['minat'] ?? 0) / $sumNonPrestasi; $weights['pref'] = ($weights['pref'] ?? 0) / $sumNonPrestasi; $weights['cita_cita'] = ($weights['cita_cita'] ?? 0) / $sumNonPrestasi; } else { // Fallback ke global jika normalisasi gagal $weights = $globalWeights; } } $matchProb = $c['match_prob'] ?? ['nilai' => 0.80, 'minat' => 0.90, 'prestasi' => 0.65, 'cita_cita' => 0.85]; // Ensure matchProb is array if (!is_array($matchProb)) { $matchProb = ['nilai' => 0.80, 'minat' => 0.90, 'prestasi' => 0.65, 'cita_cita' => 0.85]; } // 1. Likelihood untuk Nilai $p_nilai_category = ($katNilai == ($c['nilai'] ?? 'Sedang')) ? ($matchProb['nilai'] ?? 0.80) : max(1 - ($matchProb['nilai'] ?? 0.80), $epsilon); // Safe access to majorMap with null check $majorRecord = $majorMap[$jurusan] ?? null; $bobotMapel = $majorRecord ? $this->getBobotMapelForKelompok($majorRecord->bobot_mapel ?? [], $kelompokAsal) : []; $p_nilai_subject = $this->scoreSubjectFitLikelihood( $bobotMapel, $scores, $p_nilai_category ); $p_nilai = max(0.05, min(0.98, (0.6 * $p_nilai_category) + (0.4 * $p_nilai_subject))); // 2. Likelihood untuk Minat $p_minat = $this->scoreMinatLikelihood( $minatRaw, $minatMapped, $c['minat'] ?? 'Umum', $matchProb['minat'] ?? 0.90 ); // 3. Likelihood untuk Preferensi Studi $prefList = $c['pref'] ?? ['Sains & Teknologi', 'Pertanian & Lingkungan', 'Kesehatan & Ilmu Hayat', 'Bisnis & Manajemen', 'Sosial & Humaniora']; if (!is_array($prefList)) { $prefList = [$prefList]; } $p_pref = in_array($prefStudi, $prefList) ? ($matchProb['pref'] ?? 0.85) : max(1 - ($matchProb['pref'] ?? 0.85), $epsilon); // 4. Likelihood untuk Cita-cita $citaCitaKeywords = $c['cita_cita_keywords'] ?? []; $p_cita_cita = $this->scoreKeywordLikelihood($citaMapped, $citaCitaKeywords, $matchProb['cita_cita'] ?? 0.85); // 5. Likelihood untuk Prestasi (bertingkat: tinggi/menengah/dasar/minimal) $p_prestasi = $this->scorePrestasiLikelihood( $prestasiAnalysis, $citaCitaKeywords, $matchProb['prestasi'] ?? 0.65 ); // Simpan detail per kriteria untuk tampilan $detailPerJurusan[$jurusan] = [ 'nilai' => round($p_nilai, 4), 'minat' => round($p_minat, 4), 'pref' => round($p_pref, 4), 'cita' => round($p_cita_cita, 4), 'prestasi' => $isPrestasiFilled ? round($p_prestasi, 4) : null, ]; // Hitung log-likelihood dengan bobot $logLikelihood = ($weights['nilai'] ?? 0) * log(max($p_nilai, $epsilon)) + ($weights['minat'] ?? 0) * log(max($p_minat, $epsilon)) + ($weights['pref'] ?? 0) * log(max($p_pref, $epsilon)) + ($weights['cita_cita'] ?? 0) * log(max($p_cita_cita, $epsilon)); if (($weights['prestasi'] ?? 0) > 0) { $logLikelihood += ($weights['prestasi'] ?? 0) * log(max($p_prestasi, $epsilon)); } $logPosteriors[$jurusan] = $logPrior + $logLikelihood; } // Convert log-posteriors ke probabilitas (softmax) $maxLog = max($logPosteriors); $expVals = []; $sumExp = 0.0; foreach ($logPosteriors as $jurusan => $lv) { $expVals[$jurusan] = exp($lv - $maxLog); $sumExp += $expVals[$jurusan]; } $hasilAkhir = []; foreach ($expVals as $jurusan => $val) { $prob = $val / max($sumExp, $epsilon); $detail = $detailPerJurusan[$jurusan] ?? []; $explanations = $this->generateExplanation( $jurusan, $detail, $katNilai, $minatMapped, $prefStudi, $prestasiRaw, $prestasiAnalysis ); $hasilAkhir[] = [ 'jurusan' => $jurusan, 'skor' => round($prob, 4), 'detail' => $detail, 'explanation' => $explanations, 'kecocokan_nilai' => $katNilai, 'kecocokan_minat' => $minatMapped, 'kecocokan_pref' => $prefStudi, ]; } // Sort hasil berdasarkan skor (tertinggi dulu) usort($hasilAkhir, fn($a, $b) => $b['skor'] <=> $a['skor']); // Simpan data rekomendasi ke database $user = Auth::user(); $recommendationId = null; if ($user) { $recommendation = Recommendation::create([ 'user_id' => $user->id, 'mtk' => $request->mtk ?? null, 'fisika' => $request->fisika ?? null, 'kimia' => $request->kimia ?? null, 'biologi' => $request->biologi ?? null, 'ekonomi' => $request->ekonomi ?? null, 'geografi' => $request->geografi ?? null, 'sosiologi' => $request->sosiologi ?? null, 'sejarah' => $request->sejarah ?? null, 'minat' => $minatInput, 'preferensi_studi' => $prefStudi, 'cita_cita' => $citaInput, // Kolom prestasi di DB bersifat NOT NULL, jadi simpan string kosong jika tidak diisi. 'prestasi' => $prestasiInput, 'hasil_rekomendasi' => $hasilAkhir, ]); $recommendationId = $recommendation->id; } // Simpan data rekomendasi ke session untuk chatbot if (count($hasilAkhir) > 0) { $topResult = $hasilAkhir[0]; session([ 'recomendation_data' => [ 'jurusan' => $topResult['jurusan'], 'skor' => $topResult['skor'], 'nilai' => $katNilai, 'minat' => $minatMapped, 'pref_studi' => $prefStudi, ] ]); } // Ambil data jurusan teratas dari database untuk deskripsi/prospek pada halaman hasil $topJurusan = null; if (count($hasilAkhir) > 0) { $topJurusan = PolijeMajor::where('nama_jurusan', $hasilAkhir[0]['jurusan'] ?? '')->first(); } return view('rekomendasi.hasil', compact('hasilAkhir', 'katNilai', 'average', 'minatRaw', 'minatMapped', 'citaRaw', 'citaMapped', 'prefStudi', 'prestasiScore', 'topJurusan', 'isPrestasiFilled', 'recommendationId')); } /** * Pemetaan minat ke kategori yang dipahami sistem */ /** * Normalize text untuk better keyword matching * Menangani variasi kata seperti programmer/programming, coding/code */ private function normalizeText(string $text): string { $text = strtolower(trim($text)); // Simple stemming untuk common variations $replacements = [ 'programmer' => 'programming', 'coder' => 'coding', 'code' => 'coding', 'codes' => 'coding', 'develop' => 'development', 'developer' => 'development', 'develops' => 'development', 'manager' => 'manajemen', 'manages' => 'manajemen', 'doctor' => 'dokter', 'doctors' => 'dokter', 'nurse' => 'perawat', 'nurses' => 'perawat', 'engineer' => 'teknik', 'engineers' => 'teknik', 'farming' => 'pertanian', 'farmer' => 'pertanian', 'farmers' => 'pertanian', 'business' => 'bisnis', 'businessmen' => 'bisnis', ]; foreach ($replacements as $from => $to) { $text = str_replace($from, $to, $text); } return $text; } private function mapMinat(string $minatRaw): string { // Normalize text untuk better matching $minatNormalized = $this->normalizeText($minatRaw); // Use coverage-based scoring untuk handle ambiguous inputs $categoryKeywords = [ 'Logika & Komputer' => ['coding', 'komputer', 'laptop', 'web', 'aplikasi', 'logika', 'programming', 'software', 'development', 'developer', 'it', 'data', 'ai', 'teknologi', 'sistem', 'cloud', 'database', 'network', 'cybersecurity', 'analyst', 'scientist', 'algorithm', 'machine learning', 'app', 'digital'], 'Alam & Tanaman' => ['tanam', 'kebun', 'sawah', 'hewan', 'ternak', 'alam', 'pertanian', 'agri', 'panen', 'tani', 'hortikultura', 'lingkungan', 'berkelanjutan', 'farm', 'farming', 'plantation', 'crops', 'conservation', 'breeding', 'agribusiness', 'agroforestry', 'horticulture', 'cultivate', 'harvest', 'livestock management', 'animal husbandry', 'sustainable agriculture', 'crop science', 'soil', 'botanical'], 'Pelayanan & Kesehatan' => ['obat', 'sakit', 'rawat', 'medis', 'gizi', 'sehat', 'kesehatan', 'perawat', 'dokter', 'rumah sakit', 'klinik', 'farmasi', 'keperawatan', 'terapis', 'nursing', 'therapy', 'wellness', 'nutrition', 'healing', 'caring', 'clinical', 'patient care', 'rehabilitation', 'surgery', 'diagnostic', 'laboratory', 'medical technician', 'health educator', 'public health', 'epidemiology', 'preventive care'], 'Manajemen & Bisnis' => ['bisnis', 'uang', 'jual', 'kantor', 'hitung', 'ekonomi', 'dagang', 'usaha', 'entrepreneur', 'manager', 'marketing', 'akuntan', 'finance', 'keuangan', 'sales', 'trading', 'commerce', 'leadership', 'startup', 'corporate', 'organization', 'administration', 'strategic planning', 'operations', 'budget', 'investment', 'capital', 'supply chain', 'logistics', 'human resources'], 'Mesin & Listrik' => ['mesin', 'bengkel', 'listrik', 'las', 'robot', 'motor', 'teknik', 'otomasi', 'elektronik', 'maintenance', 'industri', 'manufaktur', 'mechanical', 'electrical', 'automation', 'construction', 'repair', 'welding', 'hydraulic', 'pneumatic', 'power generation', 'circuit', 'transformer', 'machinery operation', 'fabrication', 'installation', 'troubleshooting'], ]; // Score setiap kategori berdasarkan keyword coverage $scores = []; foreach ($categoryKeywords as $category => $keywords) { $scores[$category] = $this->keywordCoverage($minatNormalized, $keywords); } // Return kategori dengan coverage tertinggi $bestCategory = 'Umum'; $maxScore = 0; foreach ($scores as $category => $score) { if ($score > $maxScore) { $maxScore = $score; $bestCategory = $category; } } // Jika tidak ada keyword match, return Umum return $maxScore > 0 ? $bestCategory : 'Umum'; } /** * Pemetaan cita-cita ke kategori jurusan yang relevan * Mengevaluasi input cita-cita dengan lebih detail */ private function mapCitaCita(string $citaRaw): string { // Normalize text untuk better matching $citaNormalized = $this->normalizeText($citaRaw); // Map cita-cita ke category berdasarkan keywords $careerCategories = [ 'IT & Software' => ['programmer', 'developer', 'software', 'coding', 'web', 'database', 'it', 'scientist', 'analyst', 'data', 'cloud', 'architect', 'cybersecurity', 'security', 'devops', 'backend', 'frontend', 'fullstack', 'sysadmin', 'network admin', 'cto', 'tech lead', 'ai', 'machine learning'], 'Agriculture' => ['petani', 'pertanian', 'agribisnis', 'kebun', 'ternak', 'peternak', 'agronomi', 'farming', 'livestock', 'agronomist', 'farmer', 'farm manager', 'plantation', 'crops specialist', 'agritech', 'horticultural', 'agricultural scientist', 'soil scientist', 'breeding specialist', 'extension officer', 'crop consultant', 'forestry', 'fishery manager'], 'Healthcare' => ['dokter', 'perawat', 'medis', 'gizi', 'terapis', 'farmasi', 'kesehatan', 'nursing', 'therapist', 'pharmacist', 'nutritionist', 'clinician', 'public health', 'midwife', 'radiologist', 'dentist', 'nurse', 'surgeon', 'diagnostician', 'laboratory technician', 'paramedic', 'health educator', 'epidemiologist', 'wellness coach'], 'Business' => ['entrepreneur', 'manager', 'marketing', 'sales', 'akuntan', 'keuangan', 'bisnis', 'accountant', 'consultant', 'finance', 'cfo', 'ceo', 'director', 'treasurer', 'auditor', 'trader', 'investor', 'controller', 'operations manager', 'strategic planner', 'business analyst', 'supply chain manager', 'hr manager', 'corporate executive'], 'Engineering' => ['teknik', 'engineer', 'mesin', 'listrik', 'bengkel', 'maintenance', 'industri', 'technician', 'constructor', 'mechanical engineer', 'electrical engineer', 'automation', 'supervisor', 'foreman', 'technologist', 'specialist', 'civil engineer', 'welding specialist', 'hydraulics engineer', 'power engineer', 'manufacturing engineer', 'maintenance supervisor'], 'Communication' => ['jurnalis', 'komunikator', 'presenter', 'content', 'pariwisata', 'hospitality', 'tour', 'guide', 'public relations', 'ambassador', 'interpreter', 'diplomat', 'broadcaster', 'event organizer', 'marketing specialist', 'pr specialist', 'copywriter', 'social media manager', 'travel consultant', 'hospitality manager', 'cultural ambassador', 'media producer'], ]; // Score setiap kategori $scores = []; foreach ($careerCategories as $category => $keywords) { $scores[$category] = $this->keywordCoverage($citaNormalized, $keywords); } // Return kategori dengan coverage tertinggi $bestCategory = 'Umum'; $maxScore = 0; foreach ($scores as $category => $score) { if ($score > $maxScore) { $maxScore = $score; $bestCategory = $category; } } return $maxScore > 0 ? $bestCategory : 'Umum'; } /** * Scoring prestasi berdasarkan keyword */ private function scorePrestasiScore(string $prestasiRaw): float { if (empty($prestasiRaw)) { return 0.0; } $prestasiRaw = strtolower(trim($prestasiRaw)); $prestasiScore = 0.0; // Berbagai tingkat prestasi if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $prestasiRaw)) { $prestasiScore = 0.90; // Prestasi tinggi } elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|silver|perak)/', $prestasiRaw)) { $prestasiScore = 0.75; // Prestasi sedang } elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $prestasiRaw)) { $prestasiScore = 0.60; // Prestasi cukup } else { $prestasiScore = 0.30; // Prestasi minimal } return $prestasiScore; } /** * Ringkas level prestasi dari input teks agar lebih transparan. */ private function analyzePrestasi(string $prestasiRaw): array { if (empty(trim($prestasiRaw))) { return [ 'provided' => false, 'level' => 'minimal', 'score' => 0.0, 'raw' => '', ]; } $text = strtolower(trim($prestasiRaw)); $level = 'minimal'; if (preg_match('/(juara|menang|champion|first|gold|emas|terbaik)/', $text)) { $level = 'tinggi'; } elseif (preg_match('/(finalis|semifinal|peringkat|ranking|podium|medali|silver|perak)/', $text)) { $level = 'sedang'; } elseif (preg_match('/(sertifikat|training|kursus|workshop|peserta|mengikuti)/', $text)) { $level = 'cukup'; } return [ 'provided' => true, 'level' => $level, 'score' => $this->scorePrestasiScore($text), 'raw' => $text, ]; } /** * Skor atribut teks berdasarkan coverage keyword (0..1) lalu dipetakan menjadi likelihood. * Digunakan untuk cita-cita dan prestasi scoring. */ private function scoreKeywordLikelihood(string $text, array $keywords, float $matchProb): float { if (empty($keywords)) { return 0.50; } $coverage = $this->keywordCoverage($text, $keywords); // Log untuk debugging if ($coverage > 0) { \Log::debug('Keyword Coverage', [ 'text' => $text, 'keywords_count' => count($keywords), 'coverage' => $coverage, 'match_prob' => $matchProb, ]); } // Base 0.2 agar tidak 0 total, lalu naik proporsional coverage. $likelihood = 0.20 + ($coverage * ($matchProb - 0.20)); return max(0.05, min(0.98, $likelihood)); } private function scoreMinatLikelihood(string $minatRaw, string $minatMapped, string $targetMinat, float $matchProb): float { // Expanded keyword bank dengan lebih banyak variasi $keywordBank = [ 'Logika & Komputer' => ['coding', 'programming', 'komputer', 'software', 'web', 'data', 'ai', 'digital', 'aplikasi', 'developer', 'coding', 'programer', 'it', 'database', 'network'], 'Alam & Tanaman' => ['pertanian', 'tanaman', 'kebun', 'sawah', 'alam', 'peternakan', 'agribisnis', 'hewan', 'ternak', 'panen', 'tani', 'petani', 'hortikultura'], 'Pelayanan & Kesehatan' => ['kesehatan', 'medis', 'gizi', 'perawat', 'dokter', 'klinik', 'rumah sakit', 'farmasi', 'terapis', 'kesehatan masyarakat', 'kesehatan', 'sehat', 'rawat'], 'Manajemen & Bisnis' => ['bisnis', 'usaha', 'marketing', 'keuangan', 'manajemen', 'akuntansi', 'entrepreneur', 'sales', 'marketing', 'penjualan', 'perbankan', 'akuntan'], 'Mesin & Listrik' => ['mesin', 'listrik', 'teknik', 'otomasi', 'elektronik', 'mekanik', 'industri', 'bengkel', 'las', 'motor', 'teknis'], ]; $targetKeywords = $keywordBank[$targetMinat] ?? []; $coverage = $this->keywordCoverage($minatRaw, $targetKeywords); // Perfect match jika mapped minat sama dengan target $categoryMatch = ($minatMapped === $targetMinat) ? 1.0 : 0.0; // Weighted combination: kategori match lebih penting (60%) daripada coverage (40%) $combined = (0.6 * $categoryMatch) + (0.4 * $coverage); $likelihood = 0.20 + ($combined * ($matchProb - 0.20)); return max(0.05, min(0.98, $likelihood)); } private function scorePrestasiLikelihood(array $prestasiAnalysis, array $jurusanKeywords, float $matchProb): float { $baseScore = $prestasiAnalysis['score'] ?? 0.0; // 0..1 if ($baseScore <= 0.0) { return 0.20; } // Relevansi prestasi terhadap konteks jurusan (dari teks prestasi vs keyword jurusan) $relevance = 0.0; if (!empty($jurusanKeywords)) { $relevance = $this->keywordCoverage($prestasiAnalysis['level'] . ' ' . ($prestasiAnalysis['raw'] ?? ''), $jurusanKeywords); } // Prestasi level dominan, relevance sebagai penguat. $combined = (0.75 * $baseScore) + (0.25 * $relevance); $likelihood = 0.20 + ($combined * ($matchProb - 0.20)); return max(0.05, min(0.98, $likelihood)); } private function keywordCoverage(string $text, array $keywords): float { $text = strtolower(trim($text)); if ($text === '' || empty($keywords)) { return 0.0; } $matched = 0; foreach (array_unique($keywords) as $keyword) { if ($keyword !== '' && str_contains($text, strtolower($keyword))) { $matched++; } } // Normalisasi agar tidak terlalu menghukum list keyword yang panjang. $denominator = max(1, min(count(array_unique($keywords)), 6)); return min(1.0, $matched / $denominator); } /** * Hitung kecocokan nilai mapel terhadap bobot mapel jurusan (0..1) lalu ubah ke likelihood. * Ini tetap bagian dari atribut "Nilai Akademik" agar tidak keluar dari 5 atribut. */ private function scoreSubjectFitLikelihood(array $bobotMapel, array $scores, float $fallback): float { if (empty($bobotMapel)) { return max(0.05, min(0.98, $fallback)); } $weighted = 0.0; $weightSum = 0.0; foreach ($bobotMapel as $mapel => $w) { $nilai = $scores[$mapel] ?? null; if ($nilai === null || $nilai === '') { continue; } $num = (float) $nilai; $normalized = max(0.0, min(1.0, $num / 100)); $weighted += $normalized * (float) $w; $weightSum += (float) $w; } if ($weightSum <= 0) { return max(0.05, min(0.98, $fallback)); } $fitScore = $weighted / $weightSum; // 0..1 // Map ke likelihood agar tidak terlalu ekstrem. return max(0.05, min(0.98, 0.25 + (0.70 * $fitScore))); } private function getBobotMapelForKelompok(array $bobotMapel, string $kelompokAsal): array { $kelompokKey = strtoupper($kelompokAsal) === 'IPS' ? 'ips' : 'ipa'; $subjects = $kelompokKey === 'ipa' ? ['mtk', 'fisika', 'kimia', 'biologi'] : ['ekonomi', 'geografi', 'sosiologi', 'sejarah']; if (isset($bobotMapel['ipa']) || isset($bobotMapel['ips'])) { $groupValues = $bobotMapel[$kelompokKey] ?? []; return $this->normalizeBobotGroup(is_array($groupValues) ? $groupValues : [], $subjects); } $legacyValues = array_intersect_key($bobotMapel, array_flip($subjects)); return $this->normalizeBobotGroup($legacyValues, $subjects); } private function normalizeBobotGroup(array $values, array $subjects): array { $normalized = []; foreach ($subjects as $subject) { $value = $values[$subject] ?? null; $normalized[$subject] = is_numeric($value) ? (float) $value : 0.0; } return $normalized; } /** * 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')); } }