264 lines
10 KiB
Python
264 lines
10 KiB
Python
from flask import Flask, request, jsonify
|
|
import pandas as pd
|
|
import numpy as np
|
|
import os
|
|
import joblib
|
|
import warnings
|
|
from sklearn.exceptions import InconsistentVersionWarning
|
|
from sklearn.preprocessing import LabelEncoder
|
|
from sklearn.model_selection import train_test_split
|
|
from sklearn.ensemble import RandomForestClassifier
|
|
from sklearn.metrics import accuracy_score
|
|
|
|
warnings.filterwarnings("ignore", category=InconsistentVersionWarning)
|
|
|
|
MODEL_PATH = 'rekomendasi_rf_model.joblib'
|
|
ENCODER_PATH = 'rekomendasi_labelencoders.joblib'
|
|
TARGET_ENCODER_PATH = 'rekomendasi_targetencoder.joblib'
|
|
DATASET_PATH = 'dataset/data1.csv'
|
|
|
|
app = Flask(__name__)
|
|
|
|
model = None
|
|
le_dict = None
|
|
target_encoder = None
|
|
df = None
|
|
|
|
fitur_cols = [
|
|
"Intensitas Cahaya (Kategori)",
|
|
"Intensitas Air (Kategori)",
|
|
"Cara Penyiraman (Frekuensi)",
|
|
"Konsep Utama Taman",
|
|
"Ukuran Taman yang Cocok (Kategori)",
|
|
"Lokasi Penanaman (Indoor/Outdoor)"
|
|
]
|
|
|
|
target_col = "Tipe Utama Tanaman"
|
|
nama_tanaman_col = "Nama Tanaman"
|
|
|
|
def load_model_data():
|
|
global model, le_dict, target_encoder, df
|
|
model = joblib.load(MODEL_PATH)
|
|
le_dict = joblib.load(ENCODER_PATH)
|
|
target_encoder = joblib.load(TARGET_ENCODER_PATH)
|
|
df = pd.read_csv(DATASET_PATH, sep=';')
|
|
for col in df.columns:
|
|
df[col] = df[col].astype(str).str.strip()
|
|
print("Model dan dataset berhasil dimuat ke memory.")
|
|
|
|
try:
|
|
load_model_data()
|
|
except Exception as e:
|
|
print(f"Gagal memuat model awal: {e}")
|
|
|
|
@app.route('/retrain', methods=['POST'])
|
|
def retrain_model():
|
|
global model, le_dict, target_encoder, df
|
|
try:
|
|
if 'file' not in request.files:
|
|
return jsonify({"success": False, "error": "File CSV tidak ditemukan."}), 400
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({"success": False, "error": "File CSV kosong."}), 400
|
|
|
|
os.makedirs('dataset', exist_ok=True)
|
|
|
|
try:
|
|
new_data = pd.read_csv(file, sep=';')
|
|
except Exception as e:
|
|
return jsonify({"success": False, "error": f"Gagal membaca file CSV: {str(e)}"}), 400
|
|
|
|
for col in new_data.columns:
|
|
new_data[col] = new_data[col].astype(str).str.strip()
|
|
|
|
print("Dataset baru diterima.")
|
|
print(f"Jumlah data baru: {len(new_data)}")
|
|
|
|
# Sertakan nama_tanaman_col ke dalam kolom yang wajib ada
|
|
required_columns = fitur_cols + [target_col, nama_tanaman_col]
|
|
missing_columns = [col for col in required_columns if col not in new_data.columns]
|
|
|
|
if missing_columns:
|
|
return jsonify({"success": False, "error": "Kolom dataset tidak lengkap.", "kolom_hilang": missing_columns}), 400
|
|
|
|
new_data = new_data[required_columns].copy()
|
|
new_data = new_data.dropna(subset=required_columns).copy()
|
|
|
|
if len(new_data) < 1:
|
|
return jsonify({"success": False, "error": "Dataset baru tidak memiliki data yang valid."}), 400
|
|
|
|
if os.path.exists(DATASET_PATH):
|
|
try:
|
|
old_data = pd.read_csv(DATASET_PATH, sep=';')
|
|
for col in old_data.columns:
|
|
old_data[col] = old_data[col].astype(str).str.strip()
|
|
|
|
missing_old_columns = [col for col in required_columns if col not in old_data.columns]
|
|
if missing_old_columns:
|
|
return jsonify({"success": False, "error": "Dataset lama tidak memiliki kolom yang diperlukan.", "kolom_hilang": missing_old_columns}), 500
|
|
|
|
old_data = old_data[required_columns].copy()
|
|
print(f"Jumlah data lama: {len(old_data)}")
|
|
except Exception as e:
|
|
return jsonify({"success": False, "error": f"Gagal membaca dataset lama: {str(e)}"}), 500
|
|
else:
|
|
old_data = pd.DataFrame(columns=required_columns)
|
|
print("Dataset lama tidak ditemukan.")
|
|
|
|
combined_df = pd.concat([old_data, new_data], ignore_index=True)
|
|
before_duplicate = len(combined_df)
|
|
combined_df = combined_df.drop_duplicates(subset=required_columns, keep='first').reset_index(drop=True)
|
|
after_duplicate = len(combined_df)
|
|
duplicate_count = before_duplicate - after_duplicate
|
|
|
|
if len(combined_df) < 2:
|
|
return jsonify({"success": False, "error": "Dataset minimal harus memiliki 2 data."}), 400
|
|
|
|
training_df = combined_df.copy()
|
|
X = training_df[fitur_cols].copy()
|
|
y = training_df[target_col].copy()
|
|
|
|
new_le_dict = {}
|
|
for col in fitur_cols:
|
|
le = LabelEncoder()
|
|
X[col] = le.fit_transform(X[col].astype(str))
|
|
new_le_dict[col] = le
|
|
|
|
new_target_encoder = LabelEncoder()
|
|
y_encoded = new_target_encoder.fit_transform(y.astype(str))
|
|
jumlah_tipe = len(new_target_encoder.classes_)
|
|
|
|
class_counts = pd.Series(y_encoded).value_counts()
|
|
can_stratify = len(y_encoded) >= 10 and class_counts.min() >= 2 and int(np.ceil(len(y_encoded) * 0.2)) >= jumlah_tipe
|
|
|
|
if can_stratify:
|
|
X_train, X_test, y_train, y_test = train_test_split(X, y_encoded, test_size=0.2, random_state=42, stratify=y_encoded)
|
|
mode_training = "training_testing"
|
|
else:
|
|
X_train = X
|
|
y_train = y_encoded
|
|
X_test = pd.DataFrame()
|
|
y_test = np.array([])
|
|
mode_training = "seluruh_data"
|
|
|
|
new_model = RandomForestClassifier(n_estimators=100, max_depth=10, min_samples_split=2, random_state=42)
|
|
new_model.fit(X_train, y_train)
|
|
|
|
accuracy_percent = None
|
|
if mode_training == "training_testing":
|
|
y_pred = new_model.predict(X_test)
|
|
accuracy = accuracy_score(y_test, y_pred)
|
|
accuracy_percent = round(accuracy * 100, 2)
|
|
|
|
model_path_temp = MODEL_PATH + '.tmp'
|
|
encoder_path_temp = ENCODER_PATH + '.tmp'
|
|
target_encoder_path_temp = TARGET_ENCODER_PATH + '.tmp'
|
|
dataset_path_temp = DATASET_PATH + '.tmp'
|
|
|
|
joblib.dump(new_model, model_path_temp)
|
|
joblib.dump(new_le_dict, encoder_path_temp)
|
|
joblib.dump(new_target_encoder, target_encoder_path_temp)
|
|
training_df.to_csv(dataset_path_temp, sep=';', index=False)
|
|
|
|
os.replace(model_path_temp, MODEL_PATH)
|
|
os.replace(encoder_path_temp, ENCODER_PATH)
|
|
os.replace(target_encoder_path_temp, TARGET_ENCODER_PATH)
|
|
|
|
try:
|
|
os.replace(dataset_path_temp, DATASET_PATH)
|
|
except PermissionError:
|
|
if os.path.exists(dataset_path_temp):
|
|
try:
|
|
os.remove(dataset_path_temp)
|
|
except Exception:
|
|
pass
|
|
return jsonify({"success": False, "error": f"Dataset berhasil diproses, tetapi {DATASET_PATH} tidak dapat diganti karena sedang digunakan."}), 500
|
|
|
|
model = new_model
|
|
le_dict = new_le_dict
|
|
target_encoder = new_target_encoder
|
|
df = training_df
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": "Dataset lama + baru berhasil digabung dan model berhasil dilatih ulang.",
|
|
"data_lama": int(len(old_data)),
|
|
"data_baru": int(len(new_data)),
|
|
"data_duplikat": int(duplicate_count),
|
|
"total_data": int(len(training_df)),
|
|
"data_training": int(len(X_train)),
|
|
"data_testing": int(len(X_test)),
|
|
"jumlah_tipe": int(jumlah_tipe),
|
|
"akurasi": accuracy_percent,
|
|
"mode_training": mode_training
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({"success": False, "error": str(e)}), 500
|
|
|
|
@app.route('/predict', methods=['POST'])
|
|
def predict():
|
|
global model, le_dict, target_encoder, df
|
|
if model is None:
|
|
return jsonify({"error": "Model belum tersedia."}), 500
|
|
|
|
data = request.get_json(silent=True)
|
|
if not data:
|
|
return jsonify({"error": "Format request harus berupa JSON."}), 400
|
|
|
|
input_user_text = {}
|
|
for col in fitur_cols:
|
|
value = data.get(col, "")
|
|
input_user_text[col] = str(value).strip()
|
|
|
|
for col in fitur_cols:
|
|
if input_user_text[col] == "":
|
|
return jsonify({"error": f"Input untuk '{col}' tidak boleh kosong."}), 400
|
|
if input_user_text[col] not in le_dict[col].classes_:
|
|
return jsonify({"error": f"Input '{input_user_text[col]}' untuk '{col}' tidak valid.", "pilihan": list(le_dict[col].classes_)}), 400
|
|
|
|
input_encoded = {}
|
|
try:
|
|
for col in fitur_cols:
|
|
input_encoded[col] = le_dict[col].transform([input_user_text[col]])[0]
|
|
except Exception as e:
|
|
return jsonify({"error": f"Gagal encoding: {str(e)}"}), 400
|
|
|
|
input_df = pd.DataFrame([input_encoded])
|
|
try:
|
|
proba = model.predict_proba(input_df)[0]
|
|
except Exception as e:
|
|
return jsonify({"error": f"Gagal prediksi model: {str(e)}"}), 500
|
|
|
|
jumlah_rekomendasi = min(3, len(proba))
|
|
top3_index = proba.argsort()[-jumlah_rekomendasi:][::-1]
|
|
|
|
top_types = []
|
|
for idx in top3_index:
|
|
tipe = target_encoder.inverse_transform([idx])[0]
|
|
persen = round(float(proba[idx]) * 100, 2)
|
|
top_types.append({"nama": tipe, "persen": persen})
|
|
|
|
rekomendasi = []
|
|
for tipe_info in top_types:
|
|
tipe = tipe_info["nama"]
|
|
tanaman_df = df[df[target_col] == tipe].copy()
|
|
skor = []
|
|
for _, row in tanaman_df.iterrows():
|
|
cocok = sum(1 for col in fitur_cols if row[col] == input_user_text[col])
|
|
skor.append(cocok)
|
|
tanaman_df["Skor"] = skor
|
|
tanaman_df = tanaman_df.sort_values(by="Skor", ascending=False)
|
|
top5 = tanaman_df.head(5)
|
|
|
|
daftar_tanaman = []
|
|
for _, row in top5.iterrows():
|
|
daftar_tanaman.append({"nama": row[nama_tanaman_col], "skor": int(row["Skor"])})
|
|
|
|
rekomendasi.append({"tipe": tipe, "persen": tipe_info["persen"], "tanaman": daftar_tanaman})
|
|
|
|
return jsonify({"rekomendasi": rekomendasi})
|
|
|
|
if __name__ == '__main__':
|
|
port = int(os.environ.get("PORT", 5000))
|
|
app.run(host='0.0.0.0', port=port) |