386 lines
14 KiB
Python
386 lines
14 KiB
Python
# flask-api/app.py
|
|
from flask import Flask, request, jsonify
|
|
from flask_cors import CORS
|
|
import joblib
|
|
import numpy as np
|
|
import os
|
|
from datetime import datetime
|
|
|
|
app = Flask(__name__)
|
|
CORS(app) # Mengizinkan akses dari Laravel
|
|
|
|
# ============================================
|
|
# KONFIGURASI PATH MODEL
|
|
# ============================================
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
MODELS_DIR = os.path.join(BASE_DIR, 'models')
|
|
|
|
# ============================================
|
|
# LOAD MODEL DAN ENCODERS
|
|
# ============================================
|
|
print("=" * 50)
|
|
print("🚀 Memulai Flask API Server...")
|
|
print("=" * 50)
|
|
|
|
try:
|
|
# Load model Decision Tree
|
|
model = joblib.load(os.path.join(MODELS_DIR, 'model_harian.pkl'))
|
|
print("✅ Model Decision Tree berhasil diload")
|
|
|
|
# Load encoder untuk mood (input)
|
|
encoder_mood = joblib.load(os.path.join(MODELS_DIR, 'encoder_mood.pkl'))
|
|
print(f"✅ Encoder mood berhasil diload (classes: {list(encoder_mood.classes_)})")
|
|
|
|
# Load encoder untuk label (output)
|
|
encoder_label = joblib.load(os.path.join(MODELS_DIR, 'encoder_label.pkl'))
|
|
print(f"✅ Encoder label berhasil diload (classes: {list(encoder_label.classes_)})")
|
|
|
|
model_loaded = True
|
|
|
|
except FileNotFoundError as e:
|
|
print(f"❌ ERROR: File model tidak ditemukan - {e}")
|
|
print("Pastikan folder 'models' berisi:")
|
|
print(" - model_harian.pkl")
|
|
print(" - encoder_mood.pkl")
|
|
print(" - encoder_label.pkl")
|
|
model_loaded = False
|
|
model = None
|
|
encoder_mood = None
|
|
encoder_label = None
|
|
|
|
print("=" * 50)
|
|
|
|
# ============================================
|
|
# ENDPOINT HEALTH CHECK
|
|
# ============================================
|
|
@app.route('/health', methods=['GET'])
|
|
def health():
|
|
"""Cek status server dan model"""
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'model_loaded': model_loaded,
|
|
'timestamp': datetime.now().isoformat(),
|
|
'mood_classes': list(encoder_mood.classes_) if encoder_mood else [],
|
|
'label_classes': list(encoder_label.classes_) if encoder_label else []
|
|
})
|
|
|
|
# ============================================
|
|
# ENDPOINT INFO MODEL
|
|
# ============================================
|
|
@app.route('/info', methods=['GET'])
|
|
def info():
|
|
"""Informasi detail tentang model"""
|
|
if not model_loaded:
|
|
return jsonify({'error': 'Model tidak tersedia'}), 503
|
|
|
|
return jsonify({
|
|
'model_type': type(model).__name__,
|
|
'model_parameters': model.get_params() if hasattr(model, 'get_params') else {},
|
|
'features': ['mood (encoded)', 'durasi_belajar (menit)', 'durasi_tidur (jam)'],
|
|
'mood_classes': list(encoder_mood.classes_),
|
|
'label_classes': list(encoder_label.classes_),
|
|
'description': 'Decision Tree Classifier untuk rekomendasi durasi belajar'
|
|
})
|
|
|
|
# ============================================
|
|
# ENDPOINT PREDIKSI HARIAN (HARI 1-7)
|
|
# ============================================
|
|
@app.route('/predict', methods=['POST'])
|
|
def predict_daily():
|
|
"""
|
|
Prediksi berdasarkan input HARIAN
|
|
Digunakan untuk 7 hari pertama
|
|
"""
|
|
if not model_loaded:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Model tidak tersedia'
|
|
}), 503
|
|
|
|
try:
|
|
data = request.json
|
|
|
|
# ========================================
|
|
# VALIDASI INPUT
|
|
# ========================================
|
|
required_fields = ['mood', 'durasi_belajar', 'durasi_tidur']
|
|
for field in required_fields:
|
|
if field not in data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Field "{field}" diperlukan'
|
|
}), 400
|
|
|
|
mood = data['mood']
|
|
durasi_belajar = int(data['durasi_belajar'])
|
|
durasi_tidur = int(data['durasi_tidur'])
|
|
|
|
# ========================================
|
|
# ENCODE MOOD
|
|
# ========================================
|
|
try:
|
|
mood_encoded = encoder_mood.transform([mood])[0]
|
|
except ValueError as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Mood "{mood}" tidak valid. Mood yang tersedia: {list(encoder_mood.classes_)}'
|
|
}), 400
|
|
|
|
# ========================================
|
|
# PREDIKSI DENGAN DECISION TREE
|
|
# ========================================
|
|
features = np.array([[
|
|
mood_encoded,
|
|
durasi_belajar,
|
|
durasi_tidur
|
|
]])
|
|
|
|
prediction = model.predict(features)
|
|
label = encoder_label.inverse_transform(prediction)[0]
|
|
|
|
# Hitung confidence (probabilitas)
|
|
proba = model.predict_proba(features)[0]
|
|
confidence = float(max(proba))
|
|
|
|
# Probabilitas per kelas
|
|
proba_dict = {}
|
|
for i, prob in enumerate(proba):
|
|
class_name = encoder_label.inverse_transform([i])[0]
|
|
proba_dict[class_name] = float(prob)
|
|
|
|
# ========================================
|
|
# RESPONSE
|
|
# ========================================
|
|
return jsonify({
|
|
'success': True,
|
|
'prediction': label,
|
|
'confidence': confidence,
|
|
'probabilities': proba_dict,
|
|
'input_received': {
|
|
'mood': mood,
|
|
'durasi_belajar': durasi_belajar,
|
|
'durasi_tidur': durasi_tidur
|
|
},
|
|
'model_used': 'Decision Tree (daily)'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
# ============================================
|
|
# ENDPOINT PREDIKSI POLA (HARI KE-8 dst)
|
|
# ============================================
|
|
@app.route('/predict/pattern', methods=['POST'])
|
|
def predict_pattern():
|
|
"""
|
|
Prediksi berdasarkan POLA 7 HARI TERAKHIR
|
|
Tetap menggunakan Decision Tree!
|
|
Digunakan untuk hari ke-8 dan seterusnya
|
|
"""
|
|
if not model_loaded:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Model tidak tersedia'
|
|
}), 503
|
|
|
|
try:
|
|
data = request.json
|
|
|
|
# ========================================
|
|
# VALIDASI INPUT
|
|
# ========================================
|
|
required_fields = ['avg_duration', 'most_frequent_mood', 'avg_sleep']
|
|
for field in required_fields:
|
|
if field not in data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'Field "{field}" diperlukan'
|
|
}), 400
|
|
|
|
avg_duration = float(data['avg_duration'])
|
|
most_frequent_mood = data['most_frequent_mood']
|
|
avg_sleep = float(data['avg_sleep'])
|
|
trend = data.get('trend', 'stabil')
|
|
consistency = data.get('consistency', 0)
|
|
|
|
# ========================================
|
|
# ENCODE MOOD
|
|
# ========================================
|
|
try:
|
|
mood_encoded = encoder_mood.transform([most_frequent_mood])[0]
|
|
except ValueError as e:
|
|
# Fallback: coba cari mood terdekat
|
|
available_moods = list(encoder_mood.classes_)
|
|
print(f"⚠️ Mood '{most_frequent_mood}' tidak dikenal. Menggunakan 'Biasa Saja' sebagai fallback.")
|
|
mood_encoded = encoder_mood.transform(['Biasa Saja'])[0]
|
|
|
|
# ========================================
|
|
# PREDIKSI DENGAN DECISION TREE
|
|
# Fitur yang digunakan: [mood_encoded, avg_duration, avg_sleep]
|
|
# ========================================
|
|
features = np.array([[
|
|
mood_encoded,
|
|
int(avg_duration),
|
|
int(avg_sleep)
|
|
]])
|
|
|
|
prediction = model.predict(features)
|
|
label = encoder_label.inverse_transform(prediction)[0]
|
|
|
|
# Hitung confidence (probabilitas)
|
|
proba = model.predict_proba(features)[0]
|
|
confidence = float(max(proba))
|
|
|
|
# ========================================
|
|
# ANALISIS TAMBAHAN (untuk frontend)
|
|
# ========================================
|
|
|
|
# Rekomendasi berdasarkan trend
|
|
trend_recommendation = ""
|
|
if trend == 'meningkat':
|
|
trend_recommendation = "Bagus! Durasi belajarmu terus meningkat. Pertahankan momentum ini!"
|
|
elif trend == 'menurun':
|
|
trend_recommendation = "Durasi belajarmu menurun. Coba buat jadwal belajar yang lebih konsisten."
|
|
else:
|
|
trend_recommendation = "Konsistensimu stabil. Tingkatkan sedikit demi sedikit untuk hasil lebih baik."
|
|
|
|
# Rekomendasi berdasarkan konsistensi
|
|
consistency_recommendation = ""
|
|
if consistency >= 6:
|
|
consistency_recommendation = "Luar biasa! Kamu sangat konsisten. Terus pertahankan!"
|
|
elif consistency >= 4:
|
|
consistency_recommendation = "Cukup baik, tapi masih ada hari yang terlewat. Ayo lebih konsisten lagi!"
|
|
else:
|
|
consistency_recommendation = "Masih banyak hari yang terlewat. Yuk, mulai rutin mencatat aktivitas!"
|
|
|
|
# Rekomendasi berdasarkan mood
|
|
mood_recommendation = ""
|
|
if most_frequent_mood in ['Bagus', 'Lumayan']:
|
|
mood_recommendation = "Mood positifmu mendukung belajar efektif!"
|
|
else:
|
|
mood_recommendation = "Coba cari aktivitas yang menyenangkan sebelum belajar agar mood lebih baik."
|
|
|
|
# Rekomendasi berdasarkan tidur
|
|
sleep_recommendation = ""
|
|
if avg_sleep >= 7:
|
|
sleep_recommendation = "Tidur cukup! Ini bagus untuk konsentrasi."
|
|
elif avg_sleep >= 5:
|
|
sleep_recommendation = "Tidur kurang ideal. Coba tidur lebih awal."
|
|
else:
|
|
sleep_recommendation = "Kurang tidur akan mempengaruhi konsentrasi belajar. Prioritaskan istirahat!"
|
|
|
|
# ========================================
|
|
# RESPONSE
|
|
# ========================================
|
|
return jsonify({
|
|
'success': True,
|
|
'prediction': label,
|
|
'confidence': confidence,
|
|
'pattern_analysis': {
|
|
'avg_duration': round(avg_duration, 1),
|
|
'most_frequent_mood': most_frequent_mood,
|
|
'avg_sleep': round(avg_sleep, 1),
|
|
'trend': trend,
|
|
'consistency': f"{consistency}/7 hari",
|
|
'trend_recommendation': trend_recommendation,
|
|
'consistency_recommendation': consistency_recommendation,
|
|
'mood_recommendation': mood_recommendation,
|
|
'sleep_recommendation': sleep_recommendation
|
|
},
|
|
'input_received': {
|
|
'avg_duration': avg_duration,
|
|
'most_frequent_mood': most_frequent_mood,
|
|
'avg_sleep': avg_sleep,
|
|
'trend': trend,
|
|
'consistency': consistency
|
|
},
|
|
'model_used': 'Decision Tree (pattern-based)',
|
|
'note': 'Prediksi berdasarkan pola 7 hari terakhir menggunakan Decision Tree'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
# ============================================
|
|
# ENDPOINT BATCH PREDICTION (Opsional)
|
|
# ============================================
|
|
@app.route('/predict/batch', methods=['POST'])
|
|
def predict_batch():
|
|
"""
|
|
Prediksi untuk multiple data (batch)
|
|
"""
|
|
if not model_loaded:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Model tidak tersedia'
|
|
}), 503
|
|
|
|
try:
|
|
data = request.json
|
|
samples = data.get('samples', [])
|
|
|
|
if not samples:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Field "samples" diperlukan'
|
|
}), 400
|
|
|
|
results = []
|
|
for sample in samples:
|
|
try:
|
|
mood_encoded = encoder_mood.transform([sample['mood']])[0]
|
|
features = np.array([[
|
|
mood_encoded,
|
|
int(sample['durasi_belajar']),
|
|
int(sample['durasi_tidur'])
|
|
]])
|
|
|
|
prediction = model.predict(features)
|
|
label = encoder_label.inverse_transform(prediction)[0]
|
|
|
|
results.append({
|
|
'input': sample,
|
|
'prediction': label
|
|
})
|
|
except Exception as e:
|
|
results.append({
|
|
'input': sample,
|
|
'error': str(e)
|
|
})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'results': results
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
# ============================================
|
|
# RUN SERVER
|
|
# ============================================
|
|
if __name__ == '__main__':
|
|
print("\n" + "=" * 50)
|
|
print("🚀 Menjalankan Flask API Server...")
|
|
print("=" * 50)
|
|
print(f"📍 Endpoint yang tersedia:")
|
|
print(f" GET /health - Cek status server")
|
|
print(f" GET /info - Informasi model")
|
|
print(f" POST /predict - Prediksi harian (hari 1-7)")
|
|
print(f" POST /predict/pattern - Prediksi pola (hari 8+)")
|
|
print(f" POST /predict/batch - Prediksi batch")
|
|
print("=" * 50)
|
|
print("\n🔥 Server berjalan di http://127.0.0.1:5000")
|
|
print(" Tekan Ctrl+C untuk menghentikan server")
|
|
print("=" * 50)
|
|
|
|
app.run(debug=True, host='0.0.0.0', port=5000) |