LearnMood/app/Services/RecommendationCalculator.php

458 lines
15 KiB
PHP

<?php
// app/Services/RecommendationCalculator.php
namespace App\Services;
use App\Models\ActivityLog;
use App\Models\Recommendation;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class RecommendationCalculator
{
/**
* Hitung durasi rekomendasi berdasarkan kategori dan data historis
*/
public function calculateDuration($userId, $category, $currentMood, $currentSleep)
{
// Base duration per kategori (dibuat variatif)
$baseDurations = [
'Ringan' => 30,
'Sedang' => 45,
'Intensif' => 80 // Base 80, bukan 90, agar lebih bervariasi
];
$baseDuration = $baseDurations[$category];
// Ambil data 7 hari terakhir untuk analisis
$recentActivities = ActivityLog::where('user_id', $userId)
->with('recommendation')
->orderBy('activity_date', 'desc')
->take(7)
->get();
// Jika belum punya history, return base duration dengan variasi ringan
if ($recentActivities->isEmpty()) {
$initialVariation = $this->getInitialVariation($category);
$finalDuration = $baseDuration + $initialVariation;
$finalDuration = $this->ensureInRange($finalDuration, $category);
return [
'minutes' => $finalDuration,
'notes' => $this->generateSimpleNotes($category, $finalDuration),
'factors' => ['new_user'],
'adjustment' => $initialVariation,
'base_duration' => $baseDuration
];
}
// ===========================================
// FAKTOR-FAKTOR PENYESUAIAN
// ===========================================
$adjustment = 0;
$factors = [];
// 1. FAKTOR KONSISTENSI
$consistencyScore = $this->calculateConsistency($recentActivities);
if ($consistencyScore > 0.7) {
$adjustment += 5;
$factors[] = 'konsisten';
} elseif ($consistencyScore < 0.3) {
$adjustment -= 5;
$factors[] = 'tidak konsisten';
}
// 2. FAKTOR TREND
$trend = $this->calculateTrend($recentActivities);
if ($trend == 'meningkat') {
$adjustment += 4;
$factors[] = 'trend meningkat';
} elseif ($trend == 'menurun') {
$adjustment -= 4;
$factors[] = 'trend menurun';
}
// 3. FAKTOR MOOD
$moodScore = $this->getMoodWeight($currentMood);
if ($moodScore >= 1.5) {
$adjustment += 3;
$factors[] = 'mood positif';
} elseif ($moodScore <= 0.5) {
$adjustment -= 5;
$factors[] = 'mood negatif';
}
// 4. FAKTOR TIDUR
if ($currentSleep >= 7 && $currentSleep <= 9) {
$adjustment += 3;
$factors[] = 'tidur ideal';
} elseif ($currentSleep < 5) {
$adjustment -= 5;
$factors[] = 'kurang tidur';
} elseif ($currentSleep > 9) {
$adjustment += 1;
$factors[] = 'tidur panjang';
}
// 5. FAKTOR BEBAN KOGNITIF
$intensiveDays = $recentActivities->take(3)
->filter(function($activity) {
return $activity->recommendation && $activity->recommendation->category == 'Intensif';
})->count();
if ($intensiveDays >= 2) {
$adjustment -= 5;
$factors[] = 'perlu recovery';
}
// 6. FAKTOR ADAPTASI (Individual Differences)
$avgActual = $recentActivities->avg('duration_minutes');
$avgRecommended = $recentActivities->filter(function($a) {
return $a->recommendation;
})->avg(function($a) {
return $a->recommendation->recommended_minutes ?? 0;
});
if ($avgActual > $avgRecommended * 1.2) {
$adjustment += 5;
$factors[] = 'kapasitas tinggi';
} elseif ($avgActual < $avgRecommended * 0.7) {
$adjustment -= 5;
$factors[] = 'target terlalu tinggi';
}
// 7. FAKTOR STREAK (Hari beruntun)
$streak = $this->calculateStreak($recentActivities);
if ($streak >= 5) {
$adjustment += 5;
$factors[] = 'streak 5+ hari';
} elseif ($streak >= 3) {
$adjustment += 3;
$factors[] = 'streak 3+ hari';
}
// 8. FAKTOR KESESUAIAN KATEGORI
$sameCategory = $recentActivities->take(5)
->filter(function($a) use ($category) {
return $a->recommendation && $a->recommendation->category == $category;
})->count();
if ($sameCategory >= 4) {
$adjustment += 3;
$factors[] = 'konsisten di kategori';
}
// ===========================================
// VARIASI KHUSUS PER KATEGORI
// ===========================================
if ($category == 'Intensif') {
// Variasi untuk Intensif (75-115)
// Berdasarkan durasi aktual
if ($avgActual > 100) {
$adjustment += rand(5, 10);
$factors[] = 'biasa belajar panjang';
} elseif ($avgActual > 80) {
$adjustment += rand(0, 5);
$factors[] = 'cukup terbiasa';
} else {
$adjustment -= rand(0, 5);
$factors[] = 'baru masuk intensif';
}
// Variasi berdasarkan mood
if ($currentMood == 'Bagus') {
$adjustment += rand(2, 5);
} elseif ($currentMood == 'Lumayan') {
$adjustment += rand(0, 3);
}
// Random factor untuk variasi alami (±5)
$adjustment += rand(-5, 5);
} elseif ($category == 'Sedang') {
// Variasi untuk Sedang (35-65)
// Berdasarkan trend
if ($trend == 'meningkat') {
$adjustment += rand(3, 7);
$factors[] = 'menuju intensif';
} elseif ($trend == 'menurun') {
$adjustment -= rand(3, 7);
$factors[] = 'menuju ringan';
}
// Random factor kecil
$adjustment += rand(-3, 3);
} elseif ($category == 'Ringan') {
// Variasi untuk Ringan (20-40)
// Jika mood jelek, kurangi
if ($currentMood == 'Jenuh' || $currentMood == 'Cukup Jenuh') {
$adjustment -= rand(0, 5);
$factors[] = 'butuh istirahat';
}
// Random factor kecil
$adjustment += rand(-2, 2);
}
// ===========================================
// HITUNG DURASI FINAL
// ===========================================
// Batasi adjustment agar tidak terlalu ekstrim
$maxAdjustment = $this->getMaxAdjustment($category);
$adjustment = max(min($adjustment, $maxAdjustment), -$maxAdjustment);
$finalDuration = $baseDuration + $adjustment;
// Bulatkan ke 5 menit terdekat
$finalDuration = round($finalDuration / 5) * 5;
// Pastikan dalam range yang wajar per kategori
$finalDuration = $this->ensureInRange($finalDuration, $category);
// Generate notes yang personal
$notes = $this->generatePersonalNotes($category, $factors, $adjustment, $finalDuration, $streak);
Log::info('Recommendation calculated:', [
'user_id' => $userId,
'category' => $category,
'base' => $baseDuration,
'adjustment' => $adjustment,
'final' => $finalDuration,
'factors' => $factors
]);
return [
'minutes' => $finalDuration,
'notes' => $notes,
'factors' => $factors,
'adjustment' => $adjustment,
'base_duration' => $baseDuration
];
}
/**
* Variasi awal untuk user baru
*/
private function getInitialVariation($category)
{
$variations = [
'Ringan' => rand(-2, 5),
'Sedang' => rand(-3, 8),
'Intensif' => rand(-5, 15)
];
return $variations[$category];
}
/**
* Hitung skor konsistensi (0-1)
*/
private function calculateConsistency($activities)
{
if ($activities->count() < 3) return 0.5;
$durations = $activities->pluck('duration_minutes')->toArray();
$avg = array_sum($durations) / count($durations);
// Hitung standar deviasi
$variance = 0;
foreach ($durations as $dur) {
$variance += pow($dur - $avg, 2);
}
$stdDev = sqrt($variance / count($durations));
// Konsistensi = kebalikan dari koefisien variasi
$cv = $stdDev / $avg;
$consistency = max(0, min(1, 1 - $cv));
return $consistency;
}
/**
* Hitung trend (meningkat/menurun/stabil)
*/
private function calculateTrend($activities)
{
if ($activities->count() < 4) return 'stabil';
$recent = $activities->take(3)->avg('duration_minutes');
$older = $activities->slice(3, 3)->avg('duration_minutes');
$diff = $recent - $older;
if ($diff > 10) return 'meningkat';
if ($diff < -10) return 'menurun';
return 'stabil';
}
/**
* Hitung streak hari beruntun
*/
private function calculateStreak($activities)
{
if ($activities->isEmpty()) return 0;
$streak = 1;
$prevDate = null;
foreach ($activities as $activity) {
if ($prevDate) {
$diff = $prevDate->diffInDays($activity->activity_date);
if ($diff == 1) {
$streak++;
} else {
break;
}
}
$prevDate = $activity->activity_date;
}
return $streak;
}
/**
* Bobot mood (sesuai skema proposal)
*/
private function getMoodWeight($mood)
{
return match($mood) {
'Bagus' => 2.0,
'Lumayan' => 1.5,
'Biasa Saja' => 1.0,
'Cukup Jenuh' => 0.5,
'Jenuh' => 0.0,
default => 1.0
};
}
/**
* Maksimum adjustment per kategori
*/
private function getMaxAdjustment($category)
{
return match($category) {
'Ringan' => 10, // 20-40
'Sedang' => 20, // 25-65 (lebih lebar)
'Intensif' => 35, // 45-115 (sangat lebar)
default => 15
};
}
/**
* Pastikan durasi dalam range wajar
*/
private function ensureInRange($duration, $category)
{
$ranges = [
'Ringan' => ['min' => 20, 'max' => 40],
'Sedang' => ['min' => 30, 'max' => 70], // Diperlebar
'Intensif' => ['min' => 60, 'max' => 120] // 60-120
];
$range = $ranges[$category];
return max($range['min'], min($duration, $range['max']));
}
/**
* Generate notes sederhana untuk user baru
*/
private function generateSimpleNotes($category, $duration)
{
return match($category) {
'Ringan' => "📚 Belajar ringan {$duration} menit. Selamat datang di LearnMood!",
'Sedang' => "🎯 Belajar {$duration} menit dengan fokus. Selamat datang!",
'Intensif' => "🚀 Sesi intensif {$duration} menit. Selamat datang!",
default => "Selamat datang di LearnMood!"
};
}
/**
* Generate notes personal berdasarkan faktor-faktor
*/
private function generatePersonalNotes($category, $factors, $adjustment, $duration, $streak)
{
$notes = [];
// Base note
$notes[] = match($category) {
'Ringan' => "📚 Belajar ringan {$duration} menit",
'Sedang' => "🎯 Belajar {$duration} menit dengan fokus",
'Intensif' => "🚀 Sesi intensif {$duration} menit"
};
// Notes berdasarkan streak
if ($streak >= 7) {
$notes[] = "Luar biasa! Streak 7 hari! 🌟";
} elseif ($streak >= 5) {
$notes[] = "Hebat! Streak 5 hari! 🔥";
} elseif ($streak >= 3) {
$notes[] = "Mantap! Streak 3 hari! ⭐";
}
// Notes berdasarkan faktor
if (in_array('konsisten', $factors)) {
$notes[] = "Konsistensimu luar biasa!";
}
if (in_array('trend meningkat', $factors)) {
$notes[] = "Durasi belajarmu terus meningkat!";
} elseif (in_array('trend menurun', $factors)) {
$notes[] = "Semangat! Coba tingkatkan lagi.";
}
if (in_array('mood positif', $factors)) {
$notes[] = "Mood positif, belajar jadi lebih mudah!";
} elseif (in_array('mood negatif', $factors)) {
$notes[] = "Mood sedang kurang, jangan paksakan diri.";
}
if (in_array('tidur ideal', $factors)) {
$notes[] = "Tidur cukup mendukung konsentrasi.";
} elseif (in_array('kurang tidur', $factors)) {
$notes[] = "Coba tidur lebih awal nanti malam.";
}
if (in_array('perlu recovery', $factors)) {
$notes[] = "Beberapa hari ini intensif, jaga keseimbangan.";
}
if (in_array('kapasitas tinggi', $factors)) {
$notes[] = "Kamu mampu lebih dari target!";
} elseif (in_array('target terlalu tinggi', $factors)) {
$notes[] = "Target mungkin terlalu tinggi, fokus ke kualitas.";
}
// Khusus Intensif
if ($category == 'Intensif') {
if ($duration >= 110) {
$notes[] = "Wow, super intensif! Pastikan istirahat cukup.";
} elseif ($duration >= 90) {
$notes[] = "Sesi intensif optimal! Pertahankan.";
} elseif ($duration <= 70) {
$notes[] = "Intensif ringan, bagus untuk adaptasi.";
}
}
// Khusus Sedang
if ($category == 'Sedang' && $duration >= 60) {
$notes[] = "Hampir masuk intensif! Tingkatkan sedikit lagi.";
}
// Info adjustment
if ($adjustment > 5) {
$notes[] = "(+{$adjustment} menit dari baseline)";
} elseif ($adjustment < -5) {
$notes[] = "({$adjustment} menit dari baseline - fokus kualitas)";
}
return implode(' ', $notes);
}
}