947 lines
36 KiB
PHP
947 lines
36 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Student;
|
|
use App\Models\StudentValue;
|
|
use App\Models\DistanceCalculation;
|
|
use App\Models\WeightCalculation;
|
|
use App\Models\WeightRatio;
|
|
use App\Models\Prediction;
|
|
use App\Models\GraduationRule;
|
|
use Illuminate\Support\Facades\DB;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class PrediksiController extends Controller
|
|
{
|
|
// ==================== PARAMETER & KONSTANTA ====================
|
|
private const FUZZY_STRENGTH = 2.0; // m parameter untuk fuzzy strength
|
|
private const EPSILON = 0.001; // Small value to avoid division by zero
|
|
private const FEATURE_WEIGHTS = [
|
|
'avg_semester_score' => 0.4,
|
|
'usp_score' => 0.3,
|
|
'kerapian' => 0.1,
|
|
'kerajinan' => 0.1,
|
|
'sikap' => 0.1
|
|
];
|
|
|
|
// ==================== ROUTE HANDLERS ====================
|
|
public function index()
|
|
{
|
|
return view('pages.prediksi');
|
|
}
|
|
|
|
public function showResult($id)
|
|
{
|
|
Log::info("=== MENAMPILKAN HASIL PREDIKSI ===", ['student_id' => $id]);
|
|
|
|
$testStudent = Student::with('studentValues')->findOrFail($id);
|
|
|
|
Log::info("Data siswa yang ditemukan", [
|
|
'student_id' => $testStudent->id,
|
|
'nisn' => $testStudent->nisn,
|
|
'name' => $testStudent->name,
|
|
'student_values_count' => $testStudent->studentValues->count()
|
|
]);
|
|
|
|
// Ambil data hasil distance dan weight yang sudah dihitung sebelumnya
|
|
$distanceData = DistanceCalculation::with(['trainingStudent', 'weightCalculation'])
|
|
->where('test_student_id', $testStudent->id)
|
|
->get();
|
|
|
|
Log::info("Data perhitungan jarak yang ditemukan", [
|
|
'distance_calculations_count' => $distanceData->count()
|
|
]);
|
|
|
|
$results = $distanceData->map(function ($data) {
|
|
return [
|
|
'nisn' => $data->trainingStudent->nisn ?? '-',
|
|
'nama' => $data->trainingStudent->name ?? '-',
|
|
'distance' => $data->distance,
|
|
'weight' => optional($data->weightCalculation)->weight,
|
|
'true_status' => optional($data->trainingStudent)->true_status ?? '-'
|
|
];
|
|
});
|
|
|
|
Log::info("Hasil prediksi yang akan ditampilkan", [
|
|
'results_count' => $results->count(),
|
|
'results_summary' => $results->take(3)->toArray() // Ambil 3 hasil pertama untuk log
|
|
]);
|
|
|
|
// Ambil prediksi final
|
|
$prediction = Prediction::where('test_student_id', $testStudent->id)->first();
|
|
$predictedStatus = $prediction ? $prediction->predicted_status : null;
|
|
if ($prediction) {
|
|
Log::info("Prediksi final", [
|
|
'predicted_status' => $prediction->predicted_status,
|
|
'k_value' => $prediction->k_value
|
|
]);
|
|
}
|
|
|
|
Log::info("=== SELESAI MENAMPILKAN HASIL PREDIKSI ===");
|
|
|
|
return view('pages.prediksi', compact('testStudent', 'results', 'predictedStatus'));
|
|
}
|
|
|
|
public function processAndPredict(Request $request)
|
|
{
|
|
Log::info("=== MULAI PROSES PREDIKSI MANUAL ===");
|
|
|
|
$data = $request->input('data');
|
|
$k = $request->input('k_value', 5); // Default K value to 5 if not provided
|
|
|
|
Log::info("Data input yang diterima", [
|
|
'data' => $data,
|
|
'k_value' => $k
|
|
]);
|
|
|
|
if (!$data || !is_array($data)) {
|
|
Log::error("Input data tidak valid", ['data' => $data]);
|
|
return back()->with('error', 'Input data tidak valid.');
|
|
}
|
|
|
|
// Extract name and NISN
|
|
$name = $data['nama'] ?? '';
|
|
$nisn = $data['nisn'] ?? '';
|
|
|
|
Log::info("Data siswa yang akan diproses", [
|
|
'name' => $name,
|
|
'nisn' => $nisn
|
|
]);
|
|
|
|
if (empty($name) || empty($nisn)) {
|
|
Log::error("Nama atau NISN kosong", ['name' => $name, 'nisn' => $nisn]);
|
|
return back()->with('error', 'Nama dan NISN harus diisi.');
|
|
}
|
|
|
|
Log::info("Membuat record siswa baru");
|
|
$testStudent = Student::create([
|
|
'nisn' => $nisn,
|
|
'name' => $name,
|
|
'jenis_data' => 'testing',
|
|
'true_status' => null
|
|
]);
|
|
|
|
Log::info("Siswa berhasil dibuat", [
|
|
'student_id' => $testStudent->id,
|
|
'nisn' => $testStudent->nisn,
|
|
'name' => $testStudent->name
|
|
]);
|
|
|
|
// Remove name and NISN from data before creating student values
|
|
unset($data['nama'], $data['nisn']);
|
|
|
|
Log::info("Menyimpan nilai-nilai siswa");
|
|
// Create student values for remaining data
|
|
foreach ($data as $key => $value) {
|
|
if (trim($key) !== '' && $value !== null) {
|
|
StudentValue::create([
|
|
'student_id' => $testStudent->id,
|
|
'key' => $key,
|
|
'value' => $value
|
|
]);
|
|
|
|
Log::info("Nilai siswa disimpan", [
|
|
'student_id' => $testStudent->id,
|
|
'key' => $key,
|
|
'value' => $value
|
|
]);
|
|
}
|
|
}
|
|
|
|
Log::info("Memulai proses prediksi untuk siswa", ['student_id' => $testStudent->id]);
|
|
$this->predictForStudent($testStudent, $k);
|
|
|
|
// Ambil hasil prediksi manual
|
|
$prediction = Prediction::where('test_student_id', $testStudent->id)->first();
|
|
$neighbors = \App\Models\DistanceCalculation::with(['trainingStudent', 'weightCalculation'])
|
|
->where('test_student_id', $testStudent->id)
|
|
->orderBy('distance')
|
|
->limit($k)
|
|
->get()
|
|
->map(function ($item) {
|
|
return [
|
|
'nisn' => $item->trainingStudent->nisn ?? '-',
|
|
'nama' => $item->trainingStudent->name ?? '-',
|
|
'true_status' => $item->trainingStudent->true_status ?? '-',
|
|
'distance' => $item->distance,
|
|
'weight' => optional($item->weightCalculation)->weight,
|
|
];
|
|
})->toArray();
|
|
$ratios = \App\Models\WeightRatio::where('test_student_id', $testStudent->id)->get(['class', 'total_weight', 'weight_ratio']);
|
|
$manualPrediction = [
|
|
'nisn' => $testStudent->nisn,
|
|
'name' => $testStudent->name,
|
|
'status' => $prediction ? $prediction->predicted_status : '-',
|
|
'neighbors' => $neighbors,
|
|
'ratios' => $ratios
|
|
];
|
|
$studentValues = $testStudent->studentValues->pluck('value', 'key')->toArray();
|
|
// Hitung rata-rata nilai akademik (semester_1 sampai semester_6)
|
|
$akademik = collect(range(1,6))->map(function($i) use ($studentValues) {
|
|
return isset($studentValues['semester_'.$i]) ? floatval($studentValues['semester_'.$i]) : null;
|
|
})->filter()->avg();
|
|
// Normalisasi ke 0-1 (anggap skala 0-100)
|
|
$akademik_normalized = $akademik !== null ? round($akademik / 100, 4) : 0;
|
|
return view('pages.prediksi', [
|
|
'manualPrediction' => $manualPrediction,
|
|
'studentValues' => $studentValues,
|
|
'activeInputMethod' => 'manual',
|
|
'akademik_normalized' => $akademik_normalized
|
|
])->with('success', 'Prediksi berhasil dilakukan.');
|
|
}
|
|
|
|
// ==================== FUNGSI PENENTUAN STATUS KELULUSAN ====================
|
|
private function tentukanStatusKelulusan($fitur)
|
|
{
|
|
Log::info("=== MULAI PENENTUAN STATUS KELULUSAN ===", ['fitur' => $fitur]);
|
|
|
|
// Ambil semua aturan kelulusan dan urutkan berdasarkan priority
|
|
$rules = GraduationRule::orderBy('priority')->get();
|
|
Log::info("Aturan kelulusan yang ditemukan", ['rules_count' => $rules->count()]);
|
|
|
|
foreach ($rules as $rule) {
|
|
$attribute = $rule->attribute;
|
|
$operator = $rule->operator;
|
|
$value = $rule->value;
|
|
$category = $rule->category;
|
|
|
|
Log::info("Mengevaluasi aturan", [
|
|
'attribute' => $attribute,
|
|
'operator' => $operator,
|
|
'value' => $value,
|
|
'category' => $category
|
|
]);
|
|
|
|
// Cek apakah atribut ada dalam fitur
|
|
if (!isset($fitur[$attribute])) {
|
|
Log::info("Atribut tidak ditemukan dalam fitur", ['attribute' => $attribute]);
|
|
continue;
|
|
}
|
|
|
|
$fiturValue = $this->convertToNumeric($fitur[$attribute]);
|
|
$ruleValue = floatval($value);
|
|
|
|
Log::info("Nilai fitur vs aturan", [
|
|
'fitur_value' => $fiturValue,
|
|
'rule_value' => $ruleValue,
|
|
'operator' => $operator
|
|
]);
|
|
|
|
// Evaluasi aturan berdasarkan operator
|
|
$isRuleMet = match ($operator) {
|
|
'=' => $fiturValue == $ruleValue,
|
|
'>=' => $fiturValue >= $ruleValue,
|
|
'<=' => $fiturValue <= $ruleValue,
|
|
'>' => $fiturValue > $ruleValue,
|
|
'<' => $fiturValue < $ruleValue,
|
|
default => false
|
|
};
|
|
|
|
Log::info("Hasil evaluasi aturan", [
|
|
'is_rule_met' => $isRuleMet,
|
|
'category' => $category
|
|
]);
|
|
|
|
if ($isRuleMet) {
|
|
Log::info("=== ATURAN KELULUSAN DITEMUKAN ===", ['category' => $category]);
|
|
return $category;
|
|
}
|
|
}
|
|
|
|
// Jika tidak ada aturan yang terpenuhi, gunakan hasil Fuzzy KNN
|
|
Log::info("=== TIDAK ADA ATURAN KELULUSAN YANG TERPENUHI ===");
|
|
return null;
|
|
}
|
|
|
|
// ==================== FUNGSI UTAMA FUZZY KNN ====================
|
|
private function predictForStudent($testStudent, $k = 5)
|
|
{
|
|
Log::info("=== MULAI PREDIKSI UNTUK SISWA ===", [
|
|
'student_id' => $testStudent->id,
|
|
'nisn' => $testStudent->nisn,
|
|
'name' => $testStudent->name,
|
|
'k_value' => $k
|
|
]);
|
|
|
|
// 1. Persiapan Data
|
|
Log::info("1. Persiapan Data - Mencari MinMax dari data training");
|
|
$minMax = $this->getMinMaxPerFeatureFromTraining();
|
|
if (empty($minMax)) {
|
|
Log::error('MinMax data kosong, pastikan data latih sudah ada.');
|
|
return;
|
|
}
|
|
Log::info("MinMax data berhasil diperoleh", ['minMax' => $minMax]);
|
|
|
|
// 2. Normalisasi Data Testing
|
|
Log::info("2. Normalisasi Data Testing");
|
|
$testValues = $testStudent->studentValues->pluck('value', 'key');
|
|
Log::info("Data testing sebelum normalisasi", ['testValues' => $testValues->toArray()]);
|
|
|
|
$testValues = $this->processAndNormalizeData($testValues, $minMax);
|
|
Log::info("Data testing setelah normalisasi", ['normalizedTestValues' => $testValues]);
|
|
|
|
// 3. Ambil Data Training
|
|
Log::info("3. Mengambil Data Training");
|
|
$trainingStudents = Student::where('jenis_data', 'training')
|
|
->whereNotNull('true_status')
|
|
->where('id', '!=', $testStudent->id)
|
|
->with('studentValues')
|
|
->get();
|
|
|
|
Log::info("Jumlah data training yang ditemukan", ['count' => $trainingStudents->count()]);
|
|
|
|
// 4. Hitung Jarak ke Setiap Data Training
|
|
Log::info("4. Menghitung Jarak ke Setiap Data Training");
|
|
$distances = [];
|
|
foreach ($trainingStudents as $train) {
|
|
$trainValues = $train->studentValues->pluck('value', 'key');
|
|
$trainValues = $this->processAndNormalizeData($trainValues, $minMax);
|
|
|
|
$distance = $this->calculateFeatureWeightedDistance($testValues, $trainValues);
|
|
$distances[] = [
|
|
'student' => $train,
|
|
'distance' => $distance
|
|
];
|
|
|
|
Log::info("Jarak ke training student", [
|
|
'training_id' => $train->id,
|
|
'training_nisn' => $train->nisn,
|
|
'training_name' => $train->name,
|
|
'distance' => $distance,
|
|
'true_status' => $train->true_status
|
|
]);
|
|
}
|
|
|
|
// 5. Ambil K Tetangga Terdekat
|
|
Log::info("5. Mengambil K Tetangga Terdekat", ['k' => $k]);
|
|
$neighbors = collect($distances)->sortBy('distance')->take($k);
|
|
|
|
Log::info("K tetangga terdekat", [
|
|
'neighbors' => $neighbors->map(function ($item) {
|
|
return [
|
|
'id' => $item['student']->id,
|
|
'nisn' => $item['student']->nisn,
|
|
'name' => $item['student']->name,
|
|
'distance' => $item['distance'],
|
|
'true_status' => $item['student']->true_status
|
|
];
|
|
})->toArray()
|
|
]);
|
|
|
|
// 6. Inisialisasi Bobot Kelas
|
|
Log::info("6. Inisialisasi Bobot Kelas");
|
|
$classWeights = [
|
|
'lulus' => 0,
|
|
'lulus bersyarat' => 0,
|
|
'tidak lulus' => 0,
|
|
];
|
|
Log::info("Bobot kelas awal", $classWeights);
|
|
|
|
// 7. Hitung Bobot Fuzzy untuk Setiap Tetangga
|
|
Log::info("7. Menghitung Bobot Fuzzy untuk Setiap Tetangga");
|
|
foreach ($neighbors as $item) {
|
|
$train = $item['student'];
|
|
$distance = $item['distance'];
|
|
|
|
// Simpan Perhitungan Jarak
|
|
$distanceCalc = DistanceCalculation::create([
|
|
'test_student_id' => $testStudent->id,
|
|
'training_data_id' => $train->id,
|
|
'distance' => $distance
|
|
]);
|
|
|
|
// Hitung Bobot Fuzzy
|
|
$weight = $this->calculateFuzzyWeight($distance);
|
|
Log::info("Perhitungan bobot fuzzy", [
|
|
'training_id' => $train->id,
|
|
'training_nisn' => $train->nisn,
|
|
'distance' => $distance,
|
|
'fuzzy_weight' => $weight,
|
|
'true_status' => $train->true_status
|
|
]);
|
|
|
|
// Simpan Perhitungan Bobot
|
|
WeightCalculation::create([
|
|
'distance_calculation_id' => $distanceCalc->id,
|
|
'weight' => $weight
|
|
]);
|
|
|
|
// Akumulasi Bobot untuk Setiap Kelas
|
|
$status = $train->true_status;
|
|
if ($status && isset($classWeights[$status])) {
|
|
$classWeights[$status] += $weight;
|
|
Log::info("Akumulasi bobot untuk kelas", [
|
|
'status' => $status,
|
|
'weight_added' => $weight,
|
|
'total_weight' => $classWeights[$status]
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 8. Hitung Total Bobot
|
|
Log::info("8. Menghitung Total Bobot");
|
|
$totalWeight = array_sum($classWeights);
|
|
Log::info("Total bobot semua kelas", ['totalWeight' => $totalWeight]);
|
|
|
|
// 9. Simpan Rasio Bobot untuk Setiap Kelas
|
|
Log::info("9. Menyimpan Rasio Bobot untuk Setiap Kelas");
|
|
WeightRatio::where('test_student_id', $testStudent->id)->delete();
|
|
foreach ($classWeights as $status => $weight) {
|
|
$weightRatio = $totalWeight > 0 ? $weight / $totalWeight : 0;
|
|
WeightRatio::create([
|
|
'test_student_id' => $testStudent->id,
|
|
'class' => $status,
|
|
'total_weight' => $weight,
|
|
'weight_ratio' => $weightRatio,
|
|
]);
|
|
|
|
Log::info("Rasio bobot untuk kelas", [
|
|
'status' => $status,
|
|
'total_weight' => $weight,
|
|
'weight_ratio' => $weightRatio
|
|
]);
|
|
}
|
|
|
|
// 10. Tentukan Kelas dengan Bobot Tertinggi (Fuzzy KNN)
|
|
Log::info("10. Menentukan Kelas dengan Bobot Tertinggi (Fuzzy KNN)");
|
|
$fuzzyKnnStatus = collect($classWeights)->sortDesc()->keys()->first();
|
|
Log::info("Hasil Fuzzy KNN", [
|
|
'fuzzyKnnStatus' => $fuzzyKnnStatus,
|
|
'classWeights' => $classWeights
|
|
]);
|
|
|
|
// 11. Cek Aturan Kelulusan
|
|
Log::info("11. Mengecek Aturan Kelulusan");
|
|
$fitur = $testStudent->studentValues->pluck('value', 'key')->toArray();
|
|
Log::info("Fitur untuk pengecekan aturan", ['fitur' => $fitur]);
|
|
|
|
$ruleBasedStatus = $this->tentukanStatusKelulusan($fitur);
|
|
Log::info("Hasil pengecekan aturan kelulusan", ['ruleBasedStatus' => $ruleBasedStatus]);
|
|
|
|
// 12. Simpan Hasil Prediksi (Prioritaskan aturan kelulusan jika ada)
|
|
$finalStatus = $ruleBasedStatus ?? $fuzzyKnnStatus;
|
|
Log::info("12. Status Final Prediksi", [
|
|
'ruleBasedStatus' => $ruleBasedStatus,
|
|
'fuzzyKnnStatus' => $fuzzyKnnStatus,
|
|
'finalStatus' => $finalStatus
|
|
]);
|
|
|
|
Prediction::create([
|
|
'test_student_id' => $testStudent->id,
|
|
'predicted_status' => $finalStatus,
|
|
'k_value' => $k
|
|
]);
|
|
|
|
Log::info("=== SELESAI PREDIKSI UNTUK SISWA ===", [
|
|
'student_id' => $testStudent->id,
|
|
'nisn' => $testStudent->nisn,
|
|
'final_prediction' => $finalStatus,
|
|
'k_value' => $k
|
|
]);
|
|
}
|
|
|
|
// ==================== FUNGSI PENDUKUNG ====================
|
|
private function normalize($value, $min, $max)
|
|
{
|
|
if ($value === null) {
|
|
Log::info("Nilai null, return 0");
|
|
return 0;
|
|
}
|
|
|
|
$value = floatval($value);
|
|
$min = floatval($min);
|
|
$max = floatval($max);
|
|
|
|
if ($max - $min == 0) {
|
|
Log::info("Min dan max sama, return 0 untuk menghindari pembagian nol", [
|
|
'value' => $value,
|
|
'min' => $min,
|
|
'max' => $max
|
|
]);
|
|
return 0; // Hindari pembagian dengan nol
|
|
}
|
|
|
|
// Normalisasi dan batasi hasil antara 0 dan 1
|
|
$normalized = ($value - $min) / ($max - $min);
|
|
$result = max(0, min(1, $normalized));
|
|
|
|
Log::info("Normalisasi nilai", [
|
|
'original_value' => $value,
|
|
'min' => $min,
|
|
'max' => $max,
|
|
'normalized' => $normalized,
|
|
'final_result' => $result
|
|
]);
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function processAndNormalizeData($studentValues, $minMax)
|
|
{
|
|
Log::info("=== MULAI PROSES NORMALISASI DATA ===");
|
|
Log::info("Data yang akan dinormalisasi", ['studentValues' => $studentValues->toArray()]);
|
|
Log::info("MinMax yang digunakan", ['minMax' => $minMax]);
|
|
|
|
$normalizedValues = [];
|
|
|
|
foreach ($studentValues as $key => $value) {
|
|
$val = $this->convertToNumeric($value);
|
|
|
|
if (isset($minMax[$key])) {
|
|
$min = $minMax[$key]['min'];
|
|
$max = $minMax[$key]['max'];
|
|
$normalizedValues[$key] = $this->normalize($val, $min, $max);
|
|
|
|
Log::info("Normalisasi fitur", [
|
|
'feature' => $key,
|
|
'original_value' => $value,
|
|
'numeric_value' => $val,
|
|
'min' => $min,
|
|
'max' => $max,
|
|
'normalized_value' => $normalizedValues[$key]
|
|
]);
|
|
} else {
|
|
// Jika min/max tidak ditemukan, gunakan bobot default
|
|
$normalizedValues[$key] = 0;
|
|
Log::warning("Feature {$key} tidak memiliki min/max values, menggunakan 0");
|
|
}
|
|
}
|
|
|
|
Log::info("=== SELESAI PROSES NORMALISASI DATA ===", ['normalizedValues' => $normalizedValues]);
|
|
return $normalizedValues;
|
|
}
|
|
|
|
private function getMinMaxPerFeatureFromTraining()
|
|
{
|
|
Log::info("=== MULAI MENCARI MINMAX DARI DATA TRAINING ===");
|
|
|
|
// Ambil semua atribut dari data siswa uji
|
|
$testStudent = Student::where('jenis_data', 'testing')
|
|
->latest()
|
|
->first();
|
|
|
|
if (!$testStudent) {
|
|
Log::error('Tidak ada data siswa uji yang ditemukan.');
|
|
return [];
|
|
}
|
|
|
|
Log::info("Siswa uji yang digunakan untuk referensi", [
|
|
'student_id' => $testStudent->id,
|
|
'nisn' => $testStudent->nisn,
|
|
'name' => $testStudent->name
|
|
]);
|
|
|
|
$testAttributes = $testStudent->studentValues->pluck('key')->unique()->toArray();
|
|
Log::info("Atribut yang akan dicari minmax", ['attributes' => $testAttributes]);
|
|
|
|
$minMax = [];
|
|
|
|
// Ambil data latih
|
|
$dataLatih = Student::where('jenis_data', 'training')
|
|
->whereNotNull('true_status')
|
|
->with('studentValues')
|
|
->get();
|
|
|
|
Log::info("Data latih yang ditemukan", [
|
|
'training_count' => $dataLatih->count()
|
|
]);
|
|
|
|
foreach ($testAttributes as $attribute) {
|
|
Log::info("Mencari minmax untuk atribut", ['attribute' => $attribute]);
|
|
|
|
if ($dataLatih->isNotEmpty()) {
|
|
// Jika ada data latih, gunakan min dan max dari data latih
|
|
$values = $dataLatih->flatMap(function ($student) use ($attribute) {
|
|
return $student->studentValues
|
|
->where('key', $attribute)
|
|
->pluck('value')
|
|
->map(function ($value) {
|
|
return $this->convertToNumeric($value);
|
|
});
|
|
})->filter()->values();
|
|
|
|
if ($values->isNotEmpty()) {
|
|
$min = $values->min();
|
|
$max = $values->max();
|
|
$minMax[$attribute] = [
|
|
'min' => $min,
|
|
'max' => $max
|
|
];
|
|
|
|
Log::info("Minmax dari data latih", [
|
|
'attribute' => $attribute,
|
|
'min' => $min,
|
|
'max' => $max,
|
|
'values_count' => $values->count()
|
|
]);
|
|
} else {
|
|
Log::warning("Tidak ada nilai untuk atribut dalam data latih", ['attribute' => $attribute]);
|
|
}
|
|
} else {
|
|
// Jika tidak ada data latih, gunakan nilai default berdasarkan atribut
|
|
Log::info("Tidak ada data latih, menggunakan nilai default untuk atribut", ['attribute' => $attribute]);
|
|
|
|
switch ($attribute) {
|
|
case 'rata_rata':
|
|
case 'usp':
|
|
$minMax[$attribute] = ['min' => 0, 'max' => 100];
|
|
break;
|
|
case 'sikap':
|
|
case 'kerajinan':
|
|
case 'kerapian':
|
|
$minMax[$attribute] = ['min' => 0, 'max' => 1];
|
|
break;
|
|
default:
|
|
$minMax[$attribute] = ['min' => 0, 'max' => 100];
|
|
}
|
|
|
|
Log::info("Nilai default yang digunakan", [
|
|
'attribute' => $attribute,
|
|
'minmax' => $minMax[$attribute]
|
|
]);
|
|
}
|
|
}
|
|
|
|
Log::info("=== SELESAI MENCARI MINMAX DARI DATA TRAINING ===", ['minMax' => $minMax]);
|
|
return $minMax;
|
|
}
|
|
|
|
private function calculateFuzzyMembership($distance, $m = self::FUZZY_STRENGTH)
|
|
{
|
|
// Fungsi keanggotaan fuzzy menggunakan Euclidean distance
|
|
// μ(x) = 1 / (1 + ε + d²)
|
|
// Dimana d adalah jarak Euclidean dan ε adalah nilai kecil untuk menghindari pembagian nol
|
|
$adjustedDistance = $distance + self::EPSILON;
|
|
$membership = 1 / (1 + pow($adjustedDistance, 2));
|
|
|
|
Log::info("Perhitungan Fuzzy Membership", [
|
|
'distance' => $distance,
|
|
'epsilon' => self::EPSILON,
|
|
'adjusted_distance' => $adjustedDistance,
|
|
'membership' => $membership
|
|
]);
|
|
|
|
return $membership;
|
|
}
|
|
|
|
private function calculateFuzzyWeight($distance, $m = self::FUZZY_STRENGTH)
|
|
{
|
|
// Perhitungan bobot fuzzy menggunakan Euclidean distance
|
|
// w = 1 / (1 + ε + d²)
|
|
$weight = $this->calculateFuzzyMembership($distance, $m);
|
|
|
|
Log::info("Perhitungan Fuzzy Weight", [
|
|
'distance' => $distance,
|
|
'fuzzy_weight' => $weight
|
|
]);
|
|
|
|
return $weight;
|
|
}
|
|
|
|
private function calculateFeatureWeightedDistance($testValues, $trainValues)
|
|
{
|
|
Log::info("=== MULAI PERHITUNGAN JARAK BERBOBOT ===");
|
|
Log::info("Data test", ['testValues' => $testValues]);
|
|
Log::info("Data training", ['trainValues' => $trainValues]);
|
|
|
|
$sum = 0;
|
|
$totalWeight = 0;
|
|
$featureDetails = [];
|
|
|
|
foreach ($testValues as $key => $value) {
|
|
if (isset($trainValues[$key])) {
|
|
$testVal = $this->convertToNumeric($value);
|
|
$trainVal = $this->convertToNumeric($trainValues[$key]);
|
|
|
|
// Pembobotan fitur sesuai kaidah
|
|
$weight = self::FEATURE_WEIGHTS[$key] ?? 0.1;
|
|
$squaredDiff = pow($testVal - $trainVal, 2);
|
|
$weightedDiff = $weight * $squaredDiff;
|
|
|
|
$sum += $weightedDiff;
|
|
$totalWeight += $weight;
|
|
|
|
$featureDetails[] = [
|
|
'feature' => $key,
|
|
'test_value' => $testVal,
|
|
'train_value' => $trainVal,
|
|
'difference' => $testVal - $trainVal,
|
|
'squared_diff' => $squaredDiff,
|
|
'feature_weight' => $weight,
|
|
'weighted_diff' => $weightedDiff
|
|
];
|
|
|
|
Log::info("Perhitungan fitur", [
|
|
'feature' => $key,
|
|
'test_value' => $testVal,
|
|
'train_value' => $trainVal,
|
|
'difference' => $testVal - $trainVal,
|
|
'squared_diff' => $squaredDiff,
|
|
'feature_weight' => $weight,
|
|
'weighted_diff' => $weightedDiff
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Normalisasi jarak dengan total bobot
|
|
$finalDistance = $totalWeight > 0 ? sqrt($sum / $totalWeight) : 0;
|
|
|
|
Log::info("=== HASIL PERHITUNGAN JARAK BERBOBOT ===", [
|
|
'sum' => $sum,
|
|
'total_weight' => $totalWeight,
|
|
'final_distance' => $finalDistance,
|
|
'feature_details' => $featureDetails
|
|
]);
|
|
|
|
return $finalDistance;
|
|
}
|
|
|
|
private function convertToNumeric($value)
|
|
{
|
|
$originalValue = $value;
|
|
|
|
$map = [
|
|
'baik' => 1,
|
|
'cukup' => 0.5,
|
|
'kurang' => 0
|
|
];
|
|
|
|
if (is_numeric($value)) {
|
|
$result = floatval($value);
|
|
Log::info("Konversi nilai numerik", [
|
|
'original_value' => $originalValue,
|
|
'converted_value' => $result
|
|
]);
|
|
return $result;
|
|
}
|
|
|
|
$value = strtolower(trim($value));
|
|
$result = $map[$value] ?? 0;
|
|
|
|
Log::info("Konversi nilai non-numerik", [
|
|
'original_value' => $originalValue,
|
|
'cleaned_value' => $value,
|
|
'converted_value' => $result,
|
|
'mapping_used' => $map[$value] ?? 'default (0)'
|
|
]);
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function uploadExcelDanPrediksi(Request $request)
|
|
{
|
|
Log::info("=== MULAI PROSES UPLOAD EXCEL DAN PREDIKSI ===");
|
|
|
|
try {
|
|
if (!$request->hasFile('excel_file')) {
|
|
Log::error("File tidak ditemukan pada request");
|
|
return response()->json(['status' => 'error', 'message' => 'File tidak ditemukan pada request!']);
|
|
}
|
|
|
|
$file = $request->file('excel_file');
|
|
$k = 5; // Fixed K value
|
|
|
|
Log::info("File Excel yang diupload", [
|
|
'filename' => $file->getClientOriginalName(),
|
|
'size' => $file->getSize(),
|
|
'mime_type' => $file->getMimeType(),
|
|
'k_value' => $k
|
|
]);
|
|
|
|
// Validate file extension
|
|
$extension = $file->getClientOriginalExtension();
|
|
if (!in_array($extension, ['xlsx', 'xls', 'csv'])) {
|
|
Log::error("Format file tidak didukung", ['extension' => $extension]);
|
|
return back()->with('error', 'Format file tidak didukung. Gunakan file Excel (.xlsx, .xls) atau CSV.');
|
|
}
|
|
|
|
Log::info("Memulai proses membaca file Excel");
|
|
$spreadsheet = IOFactory::load($file->getPathname());
|
|
$worksheet = $spreadsheet->getActiveSheet();
|
|
|
|
$rows = [];
|
|
foreach ($worksheet->getRowIterator() as $index => $row) {
|
|
$cellIterator = $row->getCellIterator();
|
|
$cellIterator->setIterateOnlyExistingCells(false);
|
|
$cells = [];
|
|
|
|
foreach ($cellIterator as $cell) {
|
|
$cells[] = $cell->getFormattedValue();
|
|
}
|
|
|
|
if (array_filter($cells)) {
|
|
$rows[] = $cells;
|
|
}
|
|
}
|
|
|
|
Log::info("Data yang dibaca dari Excel", [
|
|
'total_rows' => count($rows),
|
|
'first_row' => $rows[0] ?? null
|
|
]);
|
|
|
|
if (count($rows) < 2) {
|
|
Log::error("Data tidak cukup", ['row_count' => count($rows)]);
|
|
return back()->with('error', 'Data tidak cukup (minimal header dan satu baris data).');
|
|
}
|
|
|
|
$headers = $rows[0];
|
|
$dataRows = array_slice($rows, 1);
|
|
$formattedRows = [];
|
|
|
|
Log::info("Header yang ditemukan", ['headers' => $headers]);
|
|
|
|
// Validate required headers
|
|
$requiredHeaders = ['nama', 'nisn', 'semester_1', 'semester_2', 'semester_3', 'semester_4', 'semester_5', 'semester_6', 'usp', 'sikap', 'kerapian', 'kerajinan'];
|
|
$missingHeaders = array_diff($requiredHeaders, array_map('strtolower', $headers));
|
|
|
|
if (!empty($missingHeaders)) {
|
|
Log::error("Header yang diperlukan tidak ditemukan", ['missing_headers' => $missingHeaders]);
|
|
return back()->with('error', 'Format Excel tidak sesuai. Header yang diperlukan: ' . implode(', ', $missingHeaders));
|
|
}
|
|
|
|
Log::info("Memformat data dari Excel");
|
|
foreach ($dataRows as $index => $dataRow) {
|
|
$formatted = [];
|
|
foreach ($headers as $i => $header) {
|
|
$formatted[strtolower(trim($header))] = trim($dataRow[$i] ?? '');
|
|
}
|
|
$formattedRows[] = $formatted;
|
|
|
|
Log::info("Data baris " . ($index + 1), ['formatted_data' => $formatted]);
|
|
}
|
|
|
|
Log::info("Memulai transaksi database");
|
|
DB::beginTransaction();
|
|
$insertedStudents = [];
|
|
$existingNISNs = [];
|
|
|
|
foreach ($formattedRows as $index => $row) {
|
|
$name = trim($row['nama'] ?? '');
|
|
$nisn = trim($row['nisn'] ?? '');
|
|
|
|
Log::info("Memproses baris " . ($index + 1), [
|
|
'name' => $name,
|
|
'nisn' => $nisn
|
|
]);
|
|
|
|
if ($name === '' || $nisn === '') {
|
|
Log::warning("Baris " . ($index + 1) . " dilewati karena nama atau NISN kosong");
|
|
continue;
|
|
}
|
|
|
|
// Check for duplicate NISN
|
|
if (in_array($nisn, $existingNISNs)) {
|
|
Log::error("NISN duplikat ditemukan", ['nisn' => $nisn]);
|
|
DB::rollback();
|
|
return back()->with('error', "NISN duplikat ditemukan: $nisn");
|
|
}
|
|
$existingNISNs[] = $nisn;
|
|
|
|
// Check if NISN already exists as training data
|
|
if (Student::where('nisn', $nisn)->where('jenis_data', 'training')->exists()) {
|
|
Log::error("NISN sudah terdaftar sebagai data latih", ['nisn' => $nisn]);
|
|
DB::rollback();
|
|
return back()->with('error', "NISN sudah terdaftar sebagai data latih: $nisn");
|
|
}
|
|
|
|
// Simpan siswa sebagai testing
|
|
$student = Student::create([
|
|
'nisn' => $nisn,
|
|
'name' => $name,
|
|
'true_status' => null,
|
|
'jenis_data' => 'testing',
|
|
]);
|
|
|
|
Log::info("Siswa berhasil dibuat", [
|
|
'student_id' => $student->id,
|
|
'nisn' => $student->nisn,
|
|
'name' => $student->name
|
|
]);
|
|
|
|
// Simpan nilai-nilai atribut (kecuali nama dan NISN)
|
|
$excludedFields = ['nama', 'nisn', 'status', 'jenis_data'];
|
|
foreach ($row as $key => $value) {
|
|
if (!in_array(strtolower(trim($key)), $excludedFields) && trim($key) !== '' && $value !== null) {
|
|
StudentValue::create([
|
|
'student_id' => $student->id,
|
|
'key' => trim($key),
|
|
'value' => trim($value),
|
|
]);
|
|
|
|
Log::info("Nilai siswa disimpan", [
|
|
'student_id' => $student->id,
|
|
'key' => trim($key),
|
|
'value' => trim($value)
|
|
]);
|
|
}
|
|
}
|
|
|
|
$insertedStudents[] = $student;
|
|
}
|
|
|
|
if (empty($insertedStudents)) {
|
|
Log::error("Tidak ada data valid yang dapat diproses");
|
|
DB::rollback();
|
|
return back()->with('error', 'Tidak ada data valid yang dapat diproses.');
|
|
}
|
|
|
|
Log::info("Commit transaksi database", ['inserted_count' => count($insertedStudents)]);
|
|
DB::commit();
|
|
|
|
// Jalankan prediksi untuk setiap siswa yang baru diinsert
|
|
Log::info("Memulai prediksi untuk semua siswa yang diinsert");
|
|
foreach ($insertedStudents as $index => $testStudent) {
|
|
Log::info("Prediksi untuk siswa " . ($index + 1) . " dari " . count($insertedStudents), [
|
|
'student_id' => $testStudent->id,
|
|
'nisn' => $testStudent->nisn
|
|
]);
|
|
$this->predictForStudent($testStudent, $k);
|
|
}
|
|
|
|
// Ambil hasil prediksi untuk semua siswa
|
|
$excelPredictions = [];
|
|
foreach ($insertedStudents as $student) {
|
|
$prediction = Prediction::where('test_student_id', $student->id)->first();
|
|
$neighbors = \App\Models\DistanceCalculation::with(['trainingStudent', 'weightCalculation'])
|
|
->where('test_student_id', $student->id)
|
|
->orderBy('distance')
|
|
->limit($k)
|
|
->get()
|
|
->map(function ($item) {
|
|
return [
|
|
'nisn' => $item->trainingStudent->nisn ?? '-',
|
|
'nama' => $item->trainingStudent->name ?? '-',
|
|
'true_status' => $item->trainingStudent->true_status ?? '-',
|
|
'distance' => $item->distance,
|
|
'weight' => optional($item->weightCalculation)->weight,
|
|
];
|
|
})->toArray();
|
|
$ratios = \App\Models\WeightRatio::where('test_student_id', $student->id)->get(['class', 'total_weight', 'weight_ratio']);
|
|
$excelPredictions[] = [
|
|
'nisn' => $student->nisn,
|
|
'name' => $student->name,
|
|
'status' => $prediction ? $prediction->predicted_status : '-',
|
|
'neighbors' => $neighbors,
|
|
'ratios' => $ratios
|
|
];
|
|
}
|
|
return view('pages.prediksi', [
|
|
'excelPredictions' => $excelPredictions,
|
|
'activeInputMethod' => 'excel'
|
|
])->with('success', 'Data Excel berhasil diproses dan diprediksi.');
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error("Error dalam upload Excel dan prediksi", [
|
|
'error_message' => $e->getMessage(),
|
|
'error_trace' => $e->getTraceAsString()
|
|
]);
|
|
DB::rollback();
|
|
Log::error('Upload & Prediksi Error: ' . $e->getMessage());
|
|
return back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|