430 lines
16 KiB
JavaScript
430 lines
16 KiB
JavaScript
const { Rule_penyakit, Rule_hama, Gejala, Penyakit, Hama, Histori } = require('../models');
|
||
const moment = require('moment');
|
||
|
||
// Helper function to calculate Bayes probability
|
||
function calculateBayesProbability(rules, entityType) {
|
||
if (!rules || rules.length === 0) return null;
|
||
|
||
const entityData = rules[0][entityType];
|
||
const entityName = entityData.nama;
|
||
const entityId = entityType === 'penyakit' ? rules[0].id_penyakit : rules[0].id_hama;
|
||
|
||
// Mencari nilai semesta P(E|Hi) untuk setiap gejala
|
||
let nilai_semesta = 0;
|
||
const gejalaValues = {};
|
||
|
||
for (const rule of rules) {
|
||
gejalaValues[rule.id_gejala] = rule.nilai_pakar;
|
||
nilai_semesta += rule.nilai_pakar;
|
||
}
|
||
|
||
// Mencari hasil bobot P(Hi) untuk setiap gejala
|
||
const bobotGejala = {};
|
||
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
||
bobotGejala[idGejala] = nilai / nilai_semesta;
|
||
}
|
||
|
||
// Hitung probabilitas H tanpa memandang Evidence P(E|Hi) × P(Hi)
|
||
const probTanpaEvidence = {};
|
||
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
||
probTanpaEvidence[idGejala] = nilai * bobotGejala[idGejala];
|
||
}
|
||
|
||
// Hitung total untuk digunakan di langkah 4
|
||
let totalProbTanpaEvidence = 0;
|
||
for (const nilai of Object.values(probTanpaEvidence)) {
|
||
totalProbTanpaEvidence += nilai;
|
||
}
|
||
|
||
// Hitung probabilitas H dengan memandang Evidence P(Hi|E)
|
||
const probDenganEvidence = {};
|
||
for (const [idGejala, nilai] of Object.entries(probTanpaEvidence)) {
|
||
probDenganEvidence[idGejala] = nilai / totalProbTanpaEvidence;
|
||
}
|
||
|
||
// Hitung Nilai Bayes ∑bayes = ∑(P(E|Hi) × P(Hi|E))
|
||
let nilaiBayes = 0;
|
||
const detailBayes = [];
|
||
|
||
for (const [idGejala, nilai] of Object.entries(gejalaValues)) {
|
||
const bayes = nilai * probDenganEvidence[idGejala];
|
||
nilaiBayes += bayes;
|
||
|
||
detailBayes.push({
|
||
id_gejala: parseInt(idGejala),
|
||
P_E_given_Hi: nilai,
|
||
P_Hi: bobotGejala[idGejala],
|
||
P_E_Hi_x_P_Hi: probTanpaEvidence[idGejala],
|
||
P_Hi_given_E: probDenganEvidence[idGejala],
|
||
bayes_value: bayes
|
||
});
|
||
}
|
||
|
||
// Hasil akhir
|
||
const idField = entityType === 'penyakit' ? 'id_penyakit' : 'id_hama';
|
||
return {
|
||
[idField]: entityId,
|
||
nama: entityName,
|
||
nilai_semesta: nilai_semesta,
|
||
detail_perhitungan: detailBayes,
|
||
nilai_bayes: nilaiBayes,
|
||
probabilitas_persen: nilaiBayes * 100,
|
||
jumlah_gejala_cocok: rules.length // Menambahkan jumlah gejala yang cocok
|
||
};
|
||
}
|
||
|
||
// Helper function untuk mendapatkan total gejala yang tersedia untuk entity
|
||
async function getTotalGejalaForEntity(entityId, entityType, inputGejala) {
|
||
try {
|
||
let totalGejala = 0;
|
||
|
||
if (entityType === 'penyakit') {
|
||
const allRules = await Rule_penyakit.findAll({
|
||
where: { id_penyakit: entityId }
|
||
});
|
||
totalGejala = allRules.length;
|
||
} else if (entityType === 'hama') {
|
||
const allRules = await Rule_hama.findAll({
|
||
where: { id_hama: entityId }
|
||
});
|
||
totalGejala = allRules.length;
|
||
}
|
||
|
||
return totalGejala;
|
||
} catch (error) {
|
||
console.error('Error getting total gejala:', error);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Helper function untuk menyelesaikan ambiguitas
|
||
async function resolveAmbiguity(candidates, inputGejala) {
|
||
// Tambahkan informasi total gejala untuk setiap kandidat
|
||
for (let candidate of candidates) {
|
||
const entityType = candidate.type;
|
||
const entityId = entityType === 'penyakit' ? candidate.id_penyakit : candidate.id_hama;
|
||
|
||
candidate.total_gejala_entity = await getTotalGejalaForEntity(entityId, entityType, inputGejala);
|
||
|
||
// Hitung persentase kesesuaian gejala
|
||
candidate.persentase_kesesuaian = candidate.total_gejala_entity > 0
|
||
? (candidate.jumlah_gejala_cocok / candidate.total_gejala_entity) * 100
|
||
: 0;
|
||
}
|
||
|
||
// Urutkan berdasarkan:
|
||
// 1. Jumlah gejala yang cocok (descending)
|
||
// 2. Persentase kesesuaian (descending)
|
||
// 3. Total gejala entity (ascending - lebih spesifik lebih baik)
|
||
candidates.sort((a, b) => {
|
||
// Prioritas 1: Jumlah gejala cocok
|
||
if (a.jumlah_gejala_cocok !== b.jumlah_gejala_cocok) {
|
||
return b.jumlah_gejala_cocok - a.jumlah_gejala_cocok;
|
||
}
|
||
|
||
// Prioritas 2: Persentase kesesuaian
|
||
if (Math.abs(a.persentase_kesesuaian - b.persentase_kesesuaian) > 0.01) {
|
||
return b.persentase_kesesuaian - a.persentase_kesesuaian;
|
||
}
|
||
|
||
// Prioritas 3: Entity dengan total gejala lebih sedikit (lebih spesifik)
|
||
return a.total_gejala_entity - b.total_gejala_entity;
|
||
});
|
||
|
||
return candidates[0]; // Kembalikan yang terbaik
|
||
}
|
||
|
||
// Helper function untuk memfilter hasil dengan 100% akurasi yang hanya cocok 1 gejala
|
||
function filterSingleSymptomPerfectMatch(results) {
|
||
const filtered = results.filter(result => {
|
||
// Jika probabilitas 100% dan hanya cocok dengan 1 gejala, filter keluar
|
||
const isPerfectMatch = Math.abs(result.probabilitas_persen - 100) < 0.0001;
|
||
const isSingleSymptom = result.jumlah_gejala_cocok === 1;
|
||
|
||
if (isPerfectMatch && isSingleSymptom) {
|
||
console.log(`Memfilter ${result.nama} (${result.type}) - 100% akurasi dengan hanya 1 gejala cocok`);
|
||
return false; // Filter keluar
|
||
}
|
||
|
||
return true; // Tetap masukkan
|
||
});
|
||
|
||
return filtered;
|
||
}
|
||
|
||
// Helper function untuk memprioritaskan berdasarkan jumlah gejala cocok
|
||
function prioritizeBySymptomCount(results) {
|
||
// Kelompokkan hasil berdasarkan jumlah gejala cocok
|
||
const groupedBySymptoms = {};
|
||
results.forEach(result => {
|
||
const symptomCount = result.jumlah_gejala_cocok;
|
||
if (!groupedBySymptoms[symptomCount]) {
|
||
groupedBySymptoms[symptomCount] = [];
|
||
}
|
||
groupedBySymptoms[symptomCount].push(result);
|
||
});
|
||
|
||
// Ambil kelompok dengan jumlah gejala cocok terbanyak
|
||
const maxSymptomCount = Math.max(...Object.keys(groupedBySymptoms).map(Number));
|
||
const topSymptomMatches = groupedBySymptoms[maxSymptomCount];
|
||
|
||
// Jika ada lebih dari 1 hasil dengan gejala cocok terbanyak, urutkan berdasarkan probabilitas
|
||
const sortedTopMatches = topSymptomMatches.sort((a, b) => {
|
||
return b.probabilitas_persen - a.probabilitas_persen;
|
||
});
|
||
|
||
console.log(`Memprioritaskan ${sortedTopMatches.length} hasil dengan ${maxSymptomCount} gejala cocok`);
|
||
|
||
return {
|
||
prioritizedResults: sortedTopMatches,
|
||
maxSymptomCount: maxSymptomCount,
|
||
totalGroups: Object.keys(groupedBySymptoms).length
|
||
};
|
||
}
|
||
|
||
exports.diagnosa = async (req, res) => {
|
||
const { gejala } = req.body;
|
||
const userId = req.user?.id;
|
||
const tanggal_diagnosa = moment().format('YYYY-MM-DD');
|
||
|
||
if (!gejala || !Array.isArray(gejala)) {
|
||
return res.status(400).json({ message: 'Gejala harus berupa array' });
|
||
}
|
||
|
||
try {
|
||
// Mengambil semua data yang dibutuhkan sekaligus
|
||
const allGejala = await Gejala.findAll({
|
||
where: { id: gejala }
|
||
});
|
||
|
||
// ========== PENYAKIT ==========
|
||
const allPenyakitRules = await Rule_penyakit.findAll({
|
||
where: { id_gejala: gejala },
|
||
include: [{ model: Penyakit, as: 'penyakit' }]
|
||
});
|
||
|
||
const uniquePenyakitIds = [...new Set(allPenyakitRules.map(rule => rule.id_penyakit))];
|
||
const hasilPenyakit = [];
|
||
|
||
// Hitung untuk setiap penyakit
|
||
for (const idPenyakit of uniquePenyakitIds) {
|
||
const penyakitRules = allPenyakitRules.filter(rule => rule.id_penyakit === idPenyakit);
|
||
const hasil = calculateBayesProbability(penyakitRules, 'penyakit');
|
||
if (hasil) {
|
||
hasilPenyakit.push(hasil);
|
||
}
|
||
}
|
||
|
||
// ========== HAMA ==========
|
||
const allHamaRules = await Rule_hama.findAll({
|
||
where: { id_gejala: gejala },
|
||
include: [{ model: Hama, as: 'hama' }]
|
||
});
|
||
|
||
const uniqueHamaIds = [...new Set(allHamaRules.map(rule => rule.id_hama))];
|
||
const hasilHama = [];
|
||
|
||
// Hitung untuk setiap hama
|
||
for (const idHama of uniqueHamaIds) {
|
||
const hamaRules = allHamaRules.filter(rule => rule.id_hama === idHama);
|
||
const hasil = calculateBayesProbability(hamaRules, 'hama');
|
||
if (hasil) {
|
||
hasilHama.push(hasil);
|
||
}
|
||
}
|
||
|
||
// Urutkan hasil berdasarkan probabilitas
|
||
const sortedPenyakit = hasilPenyakit.sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
||
const sortedHama = hasilHama.sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
||
|
||
// Gabung hasil dan ambil yang tertinggi (bisa penyakit atau hama)
|
||
const allResults = [
|
||
...sortedPenyakit.map(p => ({ type: 'penyakit', ...p })),
|
||
...sortedHama.map(h => ({ type: 'hama', ...h }))
|
||
].sort((a, b) => b.probabilitas_persen - a.probabilitas_persen);
|
||
|
||
// ========== FILTER HASIL 100% DENGAN 1 GEJALA ==========
|
||
const filteredResults = filterSingleSymptomPerfectMatch(allResults);
|
||
|
||
// Jika semua hasil terfilter, gunakan hasil berdasarkan kecocokan gejala terbanyak
|
||
let finalResults = filteredResults;
|
||
let filterInfo = {
|
||
total_sebelum_filter: allResults.length,
|
||
total_setelah_filter: filteredResults.length,
|
||
hasil_terfilter: allResults.length - filteredResults.length
|
||
};
|
||
|
||
if (filteredResults.length === 0 && allResults.length > 0) {
|
||
console.log('Semua hasil terfilter karena 100% dengan 1 gejala. Menggunakan kecocokan gejala terbanyak.');
|
||
// Urutkan berdasarkan jumlah gejala cocok, lalu probabilitas
|
||
finalResults = allResults.sort((a, b) => {
|
||
if (a.jumlah_gejala_cocok !== b.jumlah_gejala_cocok) {
|
||
return b.jumlah_gejala_cocok - a.jumlah_gejala_cocok;
|
||
}
|
||
return b.probabilitas_persen - a.probabilitas_persen;
|
||
});
|
||
|
||
filterInfo.fallback_to_symptom_count = true;
|
||
filterInfo.fallback_reason = 'Semua hasil memiliki 100% akurasi dengan hanya 1 gejala cocok';
|
||
} else if (filteredResults.length > 0) {
|
||
// ========== PRIORITAS BERDASARKAN JUMLAH GEJALA COCOK ==========
|
||
const priorityAnalysis = prioritizeBySymptomCount(filteredResults);
|
||
|
||
// Jika ada hasil dengan gejala cocok lebih banyak, prioritaskan mereka
|
||
if (priorityAnalysis.totalGroups > 1) {
|
||
console.log(`Menggunakan prioritas gejala: ${priorityAnalysis.maxSymptomCount} gejala cocok diprioritaskan`);
|
||
finalResults = priorityAnalysis.prioritizedResults;
|
||
|
||
filterInfo.symptom_priority_applied = true;
|
||
filterInfo.max_symptom_count = priorityAnalysis.maxSymptomCount;
|
||
filterInfo.prioritized_count = priorityAnalysis.prioritizedResults.length;
|
||
} else {
|
||
// Jika semua hasil memiliki jumlah gejala cocok yang sama, urutkan berdasarkan probabilitas
|
||
finalResults = filteredResults.sort((a, b) => {
|
||
return b.probabilitas_persen - a.probabilitas_persen;
|
||
});
|
||
|
||
filterInfo.sorted_by_probability = true;
|
||
}
|
||
}
|
||
|
||
// ========== PENANGANAN AMBIGUITAS ==========
|
||
let hasilTertinggi = null;
|
||
let isAmbiguous = false;
|
||
let ambiguityResolution = null;
|
||
|
||
if (finalResults.length > 0) {
|
||
const nilaiTertinggi = finalResults[0].probabilitas_persen;
|
||
|
||
// Cari semua hasil dengan nilai probabilitas yang sama dengan yang tertinggi
|
||
const kandidatTertinggi = finalResults.filter(result =>
|
||
Math.abs(result.probabilitas_persen - nilaiTertinggi) < 0.0001 // Toleransi untuk floating point
|
||
);
|
||
|
||
if (kandidatTertinggi.length > 1) {
|
||
// Ada ambiguitas - perlu resolusi
|
||
isAmbiguous = true;
|
||
console.log(`Ditemukan ${kandidatTertinggi.length} kandidat dengan nilai probabilitas sama: ${nilaiTertinggi}%`);
|
||
|
||
// Lakukan resolusi ambiguitas
|
||
hasilTertinggi = await resolveAmbiguity(kandidatTertinggi, gejala);
|
||
|
||
ambiguityResolution = {
|
||
total_kandidat: kandidatTertinggi.length,
|
||
metode_resolusi: 'jumlah_gejala_cocok',
|
||
kandidat: kandidatTertinggi.map(k => ({
|
||
type: k.type,
|
||
nama: k.nama,
|
||
probabilitas_persen: k.probabilitas_persen,
|
||
jumlah_gejala_cocok: k.jumlah_gejala_cocok,
|
||
total_gejala_entity: k.total_gejala_entity,
|
||
persentase_kesesuaian: k.persentase_kesesuaian
|
||
})),
|
||
terpilih: {
|
||
type: hasilTertinggi.type,
|
||
nama: hasilTertinggi.nama,
|
||
alasan: `Memiliki ${hasilTertinggi.jumlah_gejala_cocok} gejala cocok dengan kesesuaian ${hasilTertinggi.persentase_kesesuaian?.toFixed(2)}%`
|
||
}
|
||
};
|
||
} else {
|
||
// Tidak ada ambiguitas
|
||
hasilTertinggi = finalResults[0];
|
||
|
||
// Validasi: Jika hanya cocok 1 gejala dan masih ada kandidat lain dengan kecocokan lebih baik
|
||
if (hasilTertinggi.jumlah_gejala_cocok === 1 && finalResults.length > 1) {
|
||
const alternatif = finalResults
|
||
.filter(result => result.jumlah_gejala_cocok > 1 && result.probabilitas_persen >= hasilTertinggi.probabilitas_persen - 5); // toleransi 5%
|
||
|
||
if (alternatif.length > 0) {
|
||
// Gunakan resolveAmbiguity terhadap alternatif + hasilTertinggi
|
||
const kandidatAmbigu = [hasilTertinggi, ...alternatif];
|
||
hasilTertinggi = await resolveAmbiguity(kandidatAmbigu, gejala);
|
||
|
||
isAmbiguous = true;
|
||
ambiguityResolution = {
|
||
total_kandidat: kandidatAmbigu.length,
|
||
metode_resolusi: 'gejala_minimum_filter',
|
||
kandidat: kandidatAmbigu.map(k => ({
|
||
type: k.type,
|
||
nama: k.nama,
|
||
probabilitas_persen: k.probabilitas_persen,
|
||
jumlah_gejala_cocok: k.jumlah_gejala_cocok,
|
||
total_gejala_entity: k.total_gejala_entity,
|
||
persentase_kesesuaian: k.persentase_kesesuaian
|
||
})),
|
||
terpilih: {
|
||
type: hasilTertinggi.type,
|
||
nama: hasilTertinggi.nama,
|
||
alasan: `Dipilih karena memiliki jumlah gejala cocok lebih banyak dari kandidat yang hanya cocok 1 gejala`
|
||
}
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Simpan histori diagnosa jika ada user yang login dan ada hasil diagnosa
|
||
if (!userId) {
|
||
console.error('ID user tidak ditemukan. Histori tidak dapat disimpan.');
|
||
} else {
|
||
const semuaHasil = [...hasilPenyakit, ...hasilHama];
|
||
|
||
if (semuaHasil.length > 0 && hasilTertinggi) {
|
||
// Dapatkan waktu saat ini dalam zona waktu Indonesia (GMT+7)
|
||
const now = new Date();
|
||
const jakartaTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); // GMT+7 (WIB)
|
||
|
||
const baseHistoriData = {
|
||
userId: userId, // harus ada
|
||
tanggal_diagnosa: jakartaTime, // Menggunakan waktu real-time Indonesia
|
||
hasil: hasilTertinggi.nilai_bayes, // harus ada, harus tipe FLOAT
|
||
};
|
||
|
||
// Tambahkan id_penyakit / id_hama jika ada
|
||
if (hasilTertinggi.id_penyakit) {
|
||
baseHistoriData.id_penyakit = hasilTertinggi.id_penyakit;
|
||
} else if (hasilTertinggi.id_hama) {
|
||
baseHistoriData.id_hama = hasilTertinggi.id_hama;
|
||
}
|
||
|
||
try {
|
||
const historiPromises = gejala.map(gejalaId => {
|
||
return Histori.create({
|
||
...baseHistoriData,
|
||
id_gejala: parseInt(gejalaId)
|
||
});
|
||
});
|
||
|
||
await Promise.all(historiPromises);
|
||
console.log(`Histori berhasil disimpan untuk ${gejala.length} gejala dengan waktu: ${jakartaTime.toISOString()}`);
|
||
} catch (error) {
|
||
console.error('Gagal menyimpan histori:', error.message);
|
||
}
|
||
} else {
|
||
console.log('Tidak ada hasil diagnosa untuk disimpan.');
|
||
}
|
||
}
|
||
|
||
return res.status(200).json({
|
||
success: true,
|
||
message: 'Berhasil melakukan diagnosa',
|
||
data: {
|
||
penyakit: sortedPenyakit,
|
||
hama: sortedHama,
|
||
gejala_input: gejala.map(id => parseInt(id)),
|
||
hasil_tertinggi: hasilTertinggi,
|
||
is_ambiguous: isAmbiguous,
|
||
ambiguity_resolution: ambiguityResolution,
|
||
filter_info: filterInfo // Informasi tentang filtering yang dilakukan
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error diagnosa:', error);
|
||
return res.status(500).json({
|
||
success: false,
|
||
message: 'Gagal melakukan diagnosa',
|
||
error: error.message
|
||
});
|
||
}
|
||
}; |