177 lines
7.8 KiB
PHP
177 lines
7.8 KiB
PHP
<?php
|
||
// ════════════════════════════════════════════════════════════════
|
||
// app/Services/CertaintyFactorService.php
|
||
//
|
||
// KEGUNAAN: Inti dari sistem pakar — mesin inferensi Certainty
|
||
// Factor. Bertugas menghitung nilai CF untuk setiap penyakit
|
||
// berdasarkan gejala yang dipilih user, lalu menyimpan hasilnya
|
||
// ke database. Semua logika perhitungan CF ada di sini,
|
||
// sehingga Controller tetap bersih dan mudah dibaca.
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Rule;
|
||
use App\Models\Konsultasi;
|
||
use App\Models\KonsultasiGejala;
|
||
use App\Models\HasilDiagnosa;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class CertaintyFactorService
|
||
{
|
||
// Nilai minimum CF agar penyakit dianggap terdeteksi
|
||
private float $threshold = 0.2;
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// FORMULA CF KOMBINASI
|
||
// Rumus: CF = CF_lama + CF_baru × (1 - CF_lama)
|
||
// ─────────────────────────────────────────────────────────
|
||
private function kombinasiCF(float $cfLama, float $cfBaru): float
|
||
{
|
||
return $cfLama + $cfBaru * (1 - $cfLama);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// INTERPRETASI NILAI CF
|
||
// Mengubah angka CF menjadi kalimat yang mudah dipahami.
|
||
// ─────────────────────────────────────────────────────────
|
||
public function interpretasi(float $cf): string
|
||
{
|
||
if ($cf >= 0.81) return 'Sangat Yakin';
|
||
if ($cf >= 0.61) return 'Yakin';
|
||
if ($cf >= 0.41) return 'Cukup Yakin';
|
||
if ($cf >= 0.21) return 'Mungkin';
|
||
if ($cf >= 0.01) return 'Tidak Yakin';
|
||
return 'Tidak Terdeteksi';
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// PROSES UTAMA: HITUNG CF
|
||
// Menerima array ID gejala yang dipilih user, lalu
|
||
// menghitung nilai CF gabungan untuk setiap penyakit.
|
||
// ─────────────────────────────────────────────────────────
|
||
public function hitung(array $gejalaIds, array $cfUserMap = []): array
|
||
{
|
||
if (empty($gejalaIds)) {
|
||
return ['error' => 'Minimal 1 gejala harus dipilih.'];
|
||
}
|
||
|
||
foreach ($gejalaIds as $id) {
|
||
if (!isset($cfUserMap[$id])) {
|
||
$cfUserMap[$id] = 1.0;
|
||
}
|
||
}
|
||
|
||
$ruleCocok = Rule::with(['penyakit', 'gejala'])
|
||
->whereIn('id_gejala', $gejalaIds)
|
||
->get();
|
||
|
||
if ($ruleCocok->isEmpty()) {
|
||
return ['error' => 'Tidak ada rule yang cocok dengan gejala yang dipilih.'];
|
||
}
|
||
|
||
$hasilCF = [];
|
||
$detailLog = [];
|
||
$penyakitIds = $ruleCocok->pluck('id_penyakit')->unique();
|
||
|
||
foreach ($penyakitIds as $idPenyakit) {
|
||
$rulesPerPenyakit = $ruleCocok->where('id_penyakit', $idPenyakit);
|
||
$cfGabungan = 0.0;
|
||
$log = [];
|
||
$step = 1;
|
||
|
||
foreach ($rulesPerPenyakit as $rule) {
|
||
$cfPakar = (float) $rule->nilai_cf;
|
||
$cfUser = (float) ($cfUserMap[$rule->id_gejala] ?? 1.0);
|
||
$cfIndividu = $cfPakar * $cfUser;
|
||
$cfSebelum = $cfGabungan;
|
||
|
||
if ($cfGabungan == 0.0) {
|
||
$cfGabungan = $cfIndividu;
|
||
$rumus = number_format($cfIndividu, 4);
|
||
} else {
|
||
$cfGabungan = $this->kombinasiCF($cfGabungan, $cfIndividu);
|
||
$rumus = number_format($cfSebelum, 4)
|
||
. ' + ' . number_format($cfIndividu, 4)
|
||
. ' × (1 - ' . number_format($cfSebelum, 4) . ')'
|
||
. ' = ' . number_format($cfGabungan, 4);
|
||
}
|
||
|
||
$log[] = [
|
||
'step' => $step++,
|
||
'rule' => $rule->kode_rule,
|
||
'kode_gejala' => $rule->gejala->kode ?? '-',
|
||
'nama_gejala' => $rule->gejala->nama ?? '-',
|
||
'cf_pakar' => $cfPakar,
|
||
'cf_user' => $cfUser,
|
||
'cf_individu' => round($cfIndividu, 4),
|
||
'cf_sebelum' => round($cfSebelum, 4),
|
||
'cf_sesudah' => round($cfGabungan, 4),
|
||
'rumus' => $rumus,
|
||
];
|
||
}
|
||
|
||
$hasilCF[$idPenyakit] = round($cfGabungan, 6);
|
||
$detailLog[$idPenyakit] = $log;
|
||
}
|
||
|
||
$terdeteksi = array_filter($hasilCF, fn($cf) => $cf >= $this->threshold);
|
||
arsort($terdeteksi);
|
||
|
||
return [
|
||
'gejala_ids' => $gejalaIds,
|
||
'cf_user_map' => $cfUserMap,
|
||
'semua_cf' => $hasilCF,
|
||
'terdeteksi' => $terdeteksi,
|
||
'total_diperiksa' => count($hasilCF),
|
||
'total_terdeteksi' => count($terdeteksi),
|
||
'detail_log' => $detailLog,
|
||
];
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────
|
||
// SIMPAN HASIL KE DATABASE
|
||
// Menyimpan sesi konsultasi, gejala yang dipilih, dan
|
||
// hasil CF ke tiga tabel sekaligus dalam satu transaksi.
|
||
// ─────────────────────────────────────────────────────────
|
||
public function simpan(int $userId, array $gejalaIds, array $cfUserMap, array $hasilInferensi): Konsultasi
|
||
{
|
||
return DB::transaction(function () use ($userId, $gejalaIds, $cfUserMap, $hasilInferensi) {
|
||
|
||
$kode = 'KON-' . date('Ymd') . '-' . str_pad(
|
||
Konsultasi::whereDate('created_at', today())->count() + 1,
|
||
4, '0', STR_PAD_LEFT
|
||
);
|
||
|
||
$status = empty($hasilInferensi['terdeteksi']) ? 'tidak_terdeteksi' : 'selesai';
|
||
|
||
$konsultasi = Konsultasi::create([
|
||
'kode_konsultasi' => $kode,
|
||
'user_id' => $userId,
|
||
'tanggal' => now(),
|
||
'status' => $status,
|
||
]);
|
||
|
||
foreach ($gejalaIds as $idGejala) {
|
||
KonsultasiGejala::create([
|
||
'id_konsultasi' => $konsultasi->id,
|
||
'id_gejala' => $idGejala,
|
||
'cf_user' => $cfUserMap[$idGejala] ?? 1.0,
|
||
]);
|
||
}
|
||
|
||
$ranking = 1;
|
||
foreach ($hasilInferensi['terdeteksi'] as $idPenyakit => $cfAkhir) {
|
||
HasilDiagnosa::create([
|
||
'id_konsultasi' => $konsultasi->id,
|
||
'id_penyakit' => $idPenyakit,
|
||
'nilai_cf_akhir' => $cfAkhir,
|
||
'persentase' => round($cfAkhir * 100, 2),
|
||
'ranking' => $ranking++,
|
||
]);
|
||
}
|
||
|
||
return $konsultasi;
|
||
});
|
||
}
|
||
} |