08-06-26
This commit is contained in:
parent
95c066e72d
commit
e07198eae2
|
|
@ -1,3 +1,181 @@
|
|||
# import pandas as pd
|
||||
# import numpy as np
|
||||
# import re
|
||||
# import joblib
|
||||
# import json
|
||||
# import warnings
|
||||
# from pathlib import Path
|
||||
# from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
# from sklearn.linear_model import LogisticRegression
|
||||
# from sklearn.model_selection import StratifiedKFold
|
||||
# from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay
|
||||
# from sklearn.pipeline import Pipeline
|
||||
# import matplotlib
|
||||
# matplotlib.use('Agg')
|
||||
# import matplotlib.pyplot as plt
|
||||
# warnings.filterwarnings('ignore')
|
||||
|
||||
# BASE_DIR = Path(__file__).parent
|
||||
# TRAIN_FILE = BASE_DIR / "processed" / "training_corpus.csv"
|
||||
# TEST_FILE = BASE_DIR / "processed" / "test_set.csv"
|
||||
# ML_DIR = BASE_DIR.parent / "fastapi" / "ml_assets"
|
||||
# ML_DIR.mkdir(parents=True, exist_ok=True)
|
||||
# TARGET_CLASSES = ["Programmer", "Data Analyst", "Wirausaha Informatika", "Non-IT"]
|
||||
# CONFIDENCE_THRESHOLD = 0.50
|
||||
|
||||
# # [v4] Tidak import Sastrawi — corpus sudah di-stem oleh prepare_corpus.py
|
||||
# # Stemming dua kali pada data yang sama dapat mendegradasi teks.
|
||||
# STOPWORDS = {
|
||||
# "yang", "di", "ke", "dari", "dan", "atau", "dengan", "untuk", "pada", "dalam",
|
||||
# "adalah", "ini", "itu", "tidak", "juga", "sudah", "akan", "bisa", "ada", "oleh",
|
||||
# "karena", "secara", "serta", "sebagai", "bagi", "telah", "maka", "namun", "sehingga",
|
||||
# "jika", "agar", "ketika", "saat", "sebelum", "sesudah", "hingga", "sampai", "antara",
|
||||
# "sekitar", "hanya", "saja", "belum", "masih", "lagi", "pun", "justru", "walaupun",
|
||||
# "meskipun", "bahkan", "cukup", "sangat", "paling", "lebih", "kurang", "lain",
|
||||
# "macam", "cara", "hal", "tentang", "mengenai", "terhadap", "kepada", "menuju",
|
||||
# "kecuali", "selain", "tanpa", "demi", "guna", "khususnya", "umumnya", "kebanyakan",
|
||||
# "sebagian", "beberapa", "semua", "setiap", "tiap", "satu", "dua", "tiga", "empat",
|
||||
# "lima", "enam", "tujuh", "delapan", "sembilan", "sepuluh", "ratus", "ribu", "juta"
|
||||
# }
|
||||
|
||||
# def preprocess_text(text):
|
||||
# """Pembersihan dasar — tanpa stemming ulang karena korpus sudah di-stem."""
|
||||
# if pd.isna(text) or not isinstance(text, str): return ""
|
||||
# text = text.lower().strip()
|
||||
# if text in {"nan", "none", "null", "-", "0", "", "tidak diisi"}: return ""
|
||||
# text = text.replace('-', ' ').replace('/', ' ').replace('_', ' ').replace(',', ' ')
|
||||
# text = re.sub(r'[^\w\s]', '', text)
|
||||
# text = re.sub(r'\d+', '', text)
|
||||
# words = [w for w in text.split() if w not in STOPWORDS and len(w) > 2]
|
||||
# return " ".join(words)
|
||||
|
||||
# def main():
|
||||
# if not TRAIN_FILE.exists():
|
||||
# print("File training_corpus.csv belum ada. Jalankan prepare_corpus.py terlebih dahulu."); return
|
||||
|
||||
# df_train = pd.read_csv(TRAIN_FILE, sep=";", dtype=str).dropna(subset=["job_text_raw", "label"])
|
||||
# df_test = pd.read_csv(TEST_FILE, sep=";", dtype=str).dropna(subset=["job_text_raw", "label"]) if TEST_FILE.exists() else None
|
||||
|
||||
# print("Sedang memproses teks (cleaning dasar tanpa stemming redundan)...")
|
||||
# X_train = df_train["job_text_raw"].apply(preprocess_text)
|
||||
# y_train = df_train["label"]
|
||||
# mask = X_train.str.len() > 0
|
||||
# X_train, y_train = X_train[mask], y_train[mask]
|
||||
|
||||
# X_test, y_test = pd.Series(dtype=str), pd.Series(dtype=str)
|
||||
# if df_test is not None:
|
||||
# X_test = df_test["job_text_raw"].apply(preprocess_text)
|
||||
# y_test = df_test["label"]
|
||||
# mask_t = X_test.str.len() > 0
|
||||
# X_test, y_test = X_test[mask_t], y_test[mask_t]
|
||||
|
||||
# print(f"Jumlah data latih: {len(X_train)} baris | Data uji: {len(X_test)} baris\n")
|
||||
|
||||
# # [KEEP v3] K-Fold cross validation — estimasi variance performa model
|
||||
# print("Melakukan pengujian K-Fold (5 putaran) pada data latih...")
|
||||
# skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
|
||||
# fold_metrics = []
|
||||
# for i, (tr_idx, te_idx) in enumerate(skf.split(X_train, y_train)):
|
||||
# model = Pipeline([
|
||||
# ('tfidf', TfidfVectorizer(max_features=3000, ngram_range=(1,2), sublinear_tf=True, min_df=1)),
|
||||
# ('clf', LogisticRegression(max_iter=1000, class_weight='balanced', solver='lbfgs'))
|
||||
# ])
|
||||
# model.fit(X_train.iloc[tr_idx], y_train.iloc[tr_idx])
|
||||
# y_pred = model.predict(X_train.iloc[te_idx])
|
||||
# rep = classification_report(y_train.iloc[te_idx], y_pred, output_dict=True, zero_division=0)
|
||||
# fold_metrics.append(rep)
|
||||
# print(f" Akurasi putaran ke-{i+1}: {rep['accuracy']:.4f}")
|
||||
|
||||
# avg_acc = np.mean([f['accuracy'] for f in fold_metrics])
|
||||
# print(f"\n RATA-RATA PENGUJIAN K-FOLD:")
|
||||
# print(f" Akurasi Keseluruhan : {avg_acc:.4f} ± {np.std([f['accuracy'] for f in fold_metrics]):.4f}")
|
||||
# for cls in TARGET_CLASSES:
|
||||
# p = np.mean([f.get(cls, {}).get('precision', 0) for f in fold_metrics])
|
||||
# r = np.mean([f.get(cls, {}).get('recall', 0) for f in fold_metrics])
|
||||
# f1 = np.mean([f.get(cls, {}).get('f1-score', 0) for f in fold_metrics])
|
||||
# print(f" {cls:25} | P: {p:.3f} | R: {r:.3f} | F1: {f1:.3f}")
|
||||
|
||||
# print("\n Membuat model final dari seluruh data latih yang tersedia...")
|
||||
# final_model = Pipeline([
|
||||
# ('tfidf', TfidfVectorizer(max_features=3000, ngram_range=(1,2), sublinear_tf=True, min_df=1)),
|
||||
# ('clf', LogisticRegression(max_iter=1000, class_weight='balanced', solver='lbfgs'))
|
||||
# ])
|
||||
# final_model.fit(X_train, y_train)
|
||||
|
||||
# metrics_test = None
|
||||
# if len(X_test) > 0:
|
||||
# y_pred = final_model.predict(X_test)
|
||||
# rep_test = classification_report(y_test, y_pred, output_dict=True, zero_division=0)
|
||||
# metrics_test = rep_test
|
||||
|
||||
# print("\n HASIL PENGUJIAN PADA DATA TEST:")
|
||||
# print(classification_report(y_test, y_pred, zero_division=0))
|
||||
|
||||
# for cls in TARGET_CLASSES:
|
||||
# sup = rep_test[cls]['support'] if cls in rep_test else 0
|
||||
# if sup < 5: print(f"Catatan: Kelas '{cls}' cuma punya {sup} data uji — skor F1-nya mungkin kurang akurat.")
|
||||
|
||||
# # [KEEP v3] Confusion matrix
|
||||
# cm = confusion_matrix(y_test, y_pred, labels=TARGET_CLASSES)
|
||||
# disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=TARGET_CLASSES)
|
||||
# disp.plot(cmap='Blues', values_format='d', xticks_rotation=45, colorbar=False)
|
||||
# plt.title('Confusion Matrix - Hold-Out Test'); plt.tight_layout()
|
||||
# plt.savefig(ML_DIR / 'confusion_matrix_test.png', dpi=300); plt.close()
|
||||
# print("Grafik confusion matrix berhasil disimpan ke confusion_matrix_test.png")
|
||||
|
||||
# # [BUGFIX] Feature importance per kelas
|
||||
# # clf.coef_ diurutkan mengikuti clf.classes_ (alfabetikal sklearn),
|
||||
# # BUKAN urutan TARGET_CLASSES yang hardcoded. Pakai clf.classes_ sebagai
|
||||
# # iterator agar index i selalu selaras dengan baris koefisien yang benar.
|
||||
# vec, clf = final_model.named_steps['tfidf'], final_model.named_steps['clf']
|
||||
# feats, coeffs = vec.get_feature_names_out(), clf.coef_
|
||||
# print("\n5 KATA PALING BERPENGARUH UNTUK TIAP KELAS:")
|
||||
# for i, cls in enumerate(clf.classes_): # <-- clf.classes_, bukan TARGET_CLASSES
|
||||
# top = [(feats[j], coeffs[i][j]) for j in coeffs[i].argsort()[-5:][::-1] if coeffs[i][j] > 0]
|
||||
# print(f" [{cls}] " + ", ".join([f"{w}({c:.2f})" for w, c in top]))
|
||||
|
||||
# # [KEEP v3] Confidence distribution
|
||||
# proba = final_model.predict_proba(X_test)
|
||||
# max_p = np.max(proba, axis=1)
|
||||
# plt.hist(max_p, bins=15, edgecolor='black', alpha=0.7, color='teal')
|
||||
# plt.axvline(x=CONFIDENCE_THRESHOLD, color='red', linestyle='--', linewidth=2, label=f'Threshold {CONFIDENCE_THRESHOLD}')
|
||||
# plt.xlabel('Confidence Score'); plt.ylabel('Count'); plt.title('Distribusi Confidence Score')
|
||||
# plt.legend(); plt.grid(axis='y', alpha=0.3); plt.tight_layout()
|
||||
# plt.savefig(ML_DIR / 'confidence_distribution.png', dpi=300); plt.close()
|
||||
# below = np.sum(max_p < CONFIDENCE_THRESHOLD)
|
||||
# print(f"\nPengecekan Keyakinan Model: {below} dari {len(max_p)} prediksi ({below/len(max_p)*100:.1f}%) di bawah threshold {CONFIDENCE_THRESHOLD}.")
|
||||
|
||||
# # [KEEP v3] Daftar prediksi yang meleset
|
||||
# mis = np.where(y_test.values != y_pred)[0]
|
||||
# if len(mis) > 0:
|
||||
# print("\n DAFTAR PREDIKSI YANG MELESET:")
|
||||
# for idx in mis[:min(6, len(mis))]:
|
||||
# txt = df_test.iloc[idx]['job_text_raw'][:80]
|
||||
# print(f" • Seharusnya: {y_test.iloc[idx]:20} | Ditebak: {y_pred[idx]:20} | Teks: '{txt}...'")
|
||||
# else:
|
||||
# print("\n Semua prediksi pada test set benar.")
|
||||
|
||||
# model_path = ML_DIR / "ml_pipeline_internal.pkl"
|
||||
# joblib.dump(final_model, model_path)
|
||||
|
||||
# metrics = {
|
||||
# "methodology": "Internal MIF only",
|
||||
# "k_fold": {
|
||||
# "accuracy_mean": float(avg_acc),
|
||||
# "accuracy_std": float(np.std([f['accuracy'] for f in fold_metrics])),
|
||||
# "folds": fold_metrics
|
||||
# },
|
||||
# "threshold_config": CONFIDENCE_THRESHOLD
|
||||
# }
|
||||
# if metrics_test: metrics["hold_out_test"] = metrics_test
|
||||
# with open(ML_DIR / "metrics_internal_only.json", 'w') as f: json.dump(metrics, f, indent=2)
|
||||
|
||||
# print(f"\n Model berhasil disimpan di: {model_path}")
|
||||
# print(f"Laporan metrik disimpan di: {ML_DIR / 'metrics_internal_only.json'}")
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import re
|
||||
|
|
@ -13,7 +191,9 @@ from sklearn.pipeline import Pipeline
|
|||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# PERBAIKAN 1: Hapus warnings.filterwarnings('ignore') secara global.
|
||||
# Biarkan warning muncul. Jika ada ConvergenceWarning, Anda akan tahu bahwa max_iter perlu dinaikkan lagi.
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
TRAIN_FILE = BASE_DIR / "processed" / "training_corpus.csv"
|
||||
|
|
@ -23,8 +203,6 @@ ML_DIR.mkdir(parents=True, exist_ok=True)
|
|||
TARGET_CLASSES = ["Programmer", "Data Analyst", "Wirausaha Informatika", "Non-IT"]
|
||||
CONFIDENCE_THRESHOLD = 0.50
|
||||
|
||||
# [v4] Tidak import Sastrawi — corpus sudah di-stem oleh prepare_corpus.py
|
||||
# Stemming dua kali pada data yang sama dapat mendegradasi teks.
|
||||
STOPWORDS = {
|
||||
"yang", "di", "ke", "dari", "dan", "atau", "dengan", "untuk", "pada", "dalam",
|
||||
"adalah", "ini", "itu", "tidak", "juga", "sudah", "akan", "bisa", "ada", "oleh",
|
||||
|
|
@ -45,13 +223,37 @@ def preprocess_text(text):
|
|||
if text in {"nan", "none", "null", "-", "0", "", "tidak diisi"}: return ""
|
||||
text = text.replace('-', ' ').replace('/', ' ').replace('_', ' ').replace(',', ' ')
|
||||
text = re.sub(r'[^\w\s]', '', text)
|
||||
text = re.sub(r'\d+', '', text)
|
||||
|
||||
# PERBAIKAN 2: Penambahan komentar eksplisit.
|
||||
# Anda harus sadar bahwa menghapus angka menghilangkan sinyal seperti "Python 3" atau "5 tahun".
|
||||
# Jika ini keputusan sadar, biarkan. Jika tidak, hapus baris di bawah ini.
|
||||
text = re.sub(r'\d+', '', text)
|
||||
|
||||
words = [w for w in text.split() if w not in STOPWORDS and len(w) > 2]
|
||||
return " ".join(words)
|
||||
|
||||
def get_pipeline():
|
||||
"""Fungsi helper untuk memastikan konsistensi pipeline antara K-Fold dan Final Model."""
|
||||
return Pipeline([
|
||||
('tfidf', TfidfVectorizer(
|
||||
max_features=3000,
|
||||
ngram_range=(1, 2),
|
||||
sublinear_tf=True,
|
||||
min_df=2, # PERBAIKAN 3: Diubah dari 1 ke 2 untuk mencegah overfitting pada noise/typo.
|
||||
norm='l2' # PERBAIKAN 4: Eksplisit menyatakan normalisasi L2 (default, tapi ditulis agar transparan).
|
||||
)),
|
||||
('clf', LogisticRegression(
|
||||
max_iter=2000, # PERBAIKAN 5: Dinaikkan dari 1000 untuk mencegah ConvergenceWarning pada data sparse.
|
||||
class_weight='balanced',
|
||||
solver='lbfgs',
|
||||
random_state=42 # PERBAIKAN 6: Ditambahkan untuk reproducibility (hasil yang konsisten).
|
||||
))
|
||||
])
|
||||
|
||||
def main():
|
||||
if not TRAIN_FILE.exists():
|
||||
print("File training_corpus.csv belum ada. Jalankan prepare_corpus.py terlebih dahulu."); return
|
||||
print("File training_corpus.csv belum ada. Jalankan prepare_corpus.py terlebih dahulu.")
|
||||
return
|
||||
|
||||
df_train = pd.read_csv(TRAIN_FILE, sep=";", dtype=str).dropna(subset=["job_text_raw", "label"])
|
||||
df_test = pd.read_csv(TEST_FILE, sep=";", dtype=str).dropna(subset=["job_text_raw", "label"]) if TEST_FILE.exists() else None
|
||||
|
|
@ -71,15 +273,14 @@ def main():
|
|||
|
||||
print(f"Jumlah data latih: {len(X_train)} baris | Data uji: {len(X_test)} baris\n")
|
||||
|
||||
# [KEEP v3] K-Fold cross validation — estimasi variance performa model
|
||||
# K-Fold cross validation
|
||||
print("Melakukan pengujian K-Fold (5 putaran) pada data latih...")
|
||||
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
|
||||
fold_metrics = []
|
||||
|
||||
for i, (tr_idx, te_idx) in enumerate(skf.split(X_train, y_train)):
|
||||
model = Pipeline([
|
||||
('tfidf', TfidfVectorizer(max_features=3000, ngram_range=(1,2), sublinear_tf=True, min_df=1)),
|
||||
('clf', LogisticRegression(max_iter=1000, class_weight='balanced', solver='lbfgs'))
|
||||
])
|
||||
# PERBAIKAN 7: Menggunakan fungsi get_pipeline() untuk menghindari duplikasi kode dan inkonsistensi
|
||||
model = get_pipeline()
|
||||
model.fit(X_train.iloc[tr_idx], y_train.iloc[tr_idx])
|
||||
y_pred = model.predict(X_train.iloc[te_idx])
|
||||
rep = classification_report(y_train.iloc[te_idx], y_pred, output_dict=True, zero_division=0)
|
||||
|
|
@ -96,10 +297,8 @@ def main():
|
|||
print(f" {cls:25} | P: {p:.3f} | R: {r:.3f} | F1: {f1:.3f}")
|
||||
|
||||
print("\n Membuat model final dari seluruh data latih yang tersedia...")
|
||||
final_model = Pipeline([
|
||||
('tfidf', TfidfVectorizer(max_features=3000, ngram_range=(1,2), sublinear_tf=True, min_df=1)),
|
||||
('clf', LogisticRegression(max_iter=1000, class_weight='balanced', solver='lbfgs'))
|
||||
])
|
||||
# PERBAIKAN 8: Menggunakan fungsi get_pipeline()
|
||||
final_model = get_pipeline()
|
||||
final_model.fit(X_train, y_train)
|
||||
|
||||
metrics_test = None
|
||||
|
|
@ -113,39 +312,58 @@ def main():
|
|||
|
||||
for cls in TARGET_CLASSES:
|
||||
sup = rep_test[cls]['support'] if cls in rep_test else 0
|
||||
if sup < 5: print(f"Catatan: Kelas '{cls}' cuma punya {sup} data uji — skor F1-nya mungkin kurang akurat.")
|
||||
if sup < 5:
|
||||
print(f"Catatan: Kelas '{cls}' cuma punya {sup} data uji — skor F1-nya mungkin kurang akurat.")
|
||||
|
||||
# [KEEP v3] Confusion matrix
|
||||
# Confusion matrix
|
||||
cm = confusion_matrix(y_test, y_pred, labels=TARGET_CLASSES)
|
||||
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=TARGET_CLASSES)
|
||||
disp.plot(cmap='Blues', values_format='d', xticks_rotation=45, colorbar=False)
|
||||
plt.title('Confusion Matrix - Hold-Out Test'); plt.tight_layout()
|
||||
plt.savefig(ML_DIR / 'confusion_matrix_test.png', dpi=300); plt.close()
|
||||
plt.title('Confusion Matrix - Hold-Out Test')
|
||||
plt.tight_layout()
|
||||
plt.savefig(ML_DIR / 'confusion_matrix_test.png', dpi=300)
|
||||
plt.close()
|
||||
print("Grafik confusion matrix berhasil disimpan ke confusion_matrix_test.png")
|
||||
|
||||
# [BUGFIX] Feature importance per kelas
|
||||
# clf.coef_ diurutkan mengikuti clf.classes_ (alfabetikal sklearn),
|
||||
# BUKAN urutan TARGET_CLASSES yang hardcoded. Pakai clf.classes_ sebagai
|
||||
# iterator agar index i selalu selaras dengan baris koefisien yang benar.
|
||||
vec, clf = final_model.named_steps['tfidf'], final_model.named_steps['clf']
|
||||
feats, coeffs = vec.get_feature_names_out(), clf.coef_
|
||||
print("\n5 KATA PALING BERPENGARUH UNTUK TIAP KELAS:")
|
||||
for i, cls in enumerate(clf.classes_): # <-- clf.classes_, bukan TARGET_CLASSES
|
||||
top = [(feats[j], coeffs[i][j]) for j in coeffs[i].argsort()[-5:][::-1] if coeffs[i][j] > 0]
|
||||
print(f" [{cls}] " + ", ".join([f"{w}({c:.2f})" for w, c in top]))
|
||||
# PERBAIKAN 9: Feature importance yang menampilkan koefisien POSITIF dan NEGATIF
|
||||
vec = final_model.named_steps['tfidf']
|
||||
clf = final_model.named_steps['clf']
|
||||
feats = vec.get_feature_names_out()
|
||||
coeffs = clf.coef_
|
||||
|
||||
print("\n3 KATA PALING BERPENGARUH (POSITIF & NEGATIF) UNTUK TIAP KELAS:")
|
||||
for i, cls in enumerate(clf.classes_):
|
||||
# Ambil 3 indeks dengan nilai tertinggi (positif) dan 3 terendah (negatif)
|
||||
pos_idx = coeffs[i].argsort()[-3:][::-1]
|
||||
neg_idx = coeffs[i].argsort()[:3]
|
||||
|
||||
top_pos = [(feats[j], coeffs[i][j]) for j in pos_idx if coeffs[i][j] > 0]
|
||||
top_neg = [(feats[j], coeffs[i][j]) for j in neg_idx if coeffs[i][j] < 0]
|
||||
|
||||
pos_str = ", ".join([f"{w}({c:.2f})" for w, c in top_pos]) if top_pos else "Tidak ada"
|
||||
neg_str = ", ".join([f"{w}({c:.2f})" for w, c in top_neg]) if top_neg else "Tidak ada"
|
||||
|
||||
print(f" [{cls}]")
|
||||
print(f" Mendorong (+) : {pos_str}")
|
||||
print(f" Menghambat (-): {neg_str}")
|
||||
|
||||
# [KEEP v3] Confidence distribution
|
||||
# Confidence distribution
|
||||
proba = final_model.predict_proba(X_test)
|
||||
max_p = np.max(proba, axis=1)
|
||||
plt.hist(max_p, bins=15, edgecolor='black', alpha=0.7, color='teal')
|
||||
plt.axvline(x=CONFIDENCE_THRESHOLD, color='red', linestyle='--', linewidth=2, label=f'Threshold {CONFIDENCE_THRESHOLD}')
|
||||
plt.xlabel('Confidence Score'); plt.ylabel('Count'); plt.title('Distribusi Confidence Score')
|
||||
plt.legend(); plt.grid(axis='y', alpha=0.3); plt.tight_layout()
|
||||
plt.savefig(ML_DIR / 'confidence_distribution.png', dpi=300); plt.close()
|
||||
plt.xlabel('Confidence Score')
|
||||
plt.ylabel('Count')
|
||||
plt.title('Distribusi Confidence Score')
|
||||
plt.legend()
|
||||
plt.grid(axis='y', alpha=0.3)
|
||||
plt.tight_layout()
|
||||
plt.savefig(ML_DIR / 'confidence_distribution.png', dpi=300)
|
||||
plt.close()
|
||||
below = np.sum(max_p < CONFIDENCE_THRESHOLD)
|
||||
print(f"\nPengecekan Keyakinan Model: {below} dari {len(max_p)} prediksi ({below/len(max_p)*100:.1f}%) di bawah threshold {CONFIDENCE_THRESHOLD}.")
|
||||
|
||||
# [KEEP v3] Daftar prediksi yang meleset
|
||||
# Daftar prediksi yang meleset
|
||||
mis = np.where(y_test.values != y_pred)[0]
|
||||
if len(mis) > 0:
|
||||
print("\n DAFTAR PREDIKSI YANG MELESET:")
|
||||
|
|
@ -155,6 +373,7 @@ def main():
|
|||
else:
|
||||
print("\n Semua prediksi pada test set benar.")
|
||||
|
||||
# Simpan model dan metrik
|
||||
model_path = ML_DIR / "ml_pipeline_internal.pkl"
|
||||
joblib.dump(final_model, model_path)
|
||||
|
||||
|
|
@ -167,11 +386,14 @@ def main():
|
|||
},
|
||||
"threshold_config": CONFIDENCE_THRESHOLD
|
||||
}
|
||||
if metrics_test: metrics["hold_out_test"] = metrics_test
|
||||
with open(ML_DIR / "metrics_internal_only.json", 'w') as f: json.dump(metrics, f, indent=2)
|
||||
if metrics_test:
|
||||
metrics["hold_out_test"] = metrics_test
|
||||
|
||||
with open(ML_DIR / "metrics_internal_only.json", 'w', encoding='utf-8') as f:
|
||||
json.dump(metrics, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n Model berhasil disimpan di: {model_path}")
|
||||
print(f"Laporan metrik disimpan di: {ML_DIR / 'metrics_internal_only.json'}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
|
@ -181,7 +181,7 @@ async def trigger_retrain(file: UploadFile = File(None)):
|
|||
with lock:
|
||||
if STATUS_PATH.exists():
|
||||
try:
|
||||
current = json.loads(STATUS_PATH.read_text())
|
||||
current = json.loads(STATUS_PATH.read_text(encoding="utf-8"))
|
||||
if current.get("stage") in ["started", "backup", "loading_data", "preprocessing", "training", "evaluating", "promoting"]:
|
||||
raise HTTPException(status_code=409, detail="Re-training sedang berjalan. Tunggu hingga selesai.")
|
||||
except json.JSONDecodeError:
|
||||
|
|
@ -199,7 +199,7 @@ async def trigger_retrain(file: UploadFile = File(None)):
|
|||
"stage": "started",
|
||||
"message": "Memulai proses re-training...",
|
||||
"timestamp": __import__('datetime').datetime.now().isoformat(),
|
||||
}, ensure_ascii=False))
|
||||
}, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
cmd = [sys.executable, str(RETRAIN_WORKER)]
|
||||
if extra_csv_path: cmd.append(extra_csv_path)
|
||||
|
|
@ -224,13 +224,13 @@ def retrain_status():
|
|||
# [v4] Lock saat baca untuk menghindari partial write dari worker
|
||||
lock = FileLock(LOCK_PATH, timeout=2)
|
||||
with lock:
|
||||
data = json.loads(STATUS_PATH.read_text())
|
||||
data = json.loads(STATUS_PATH.read_text(encoding="utf-8"))
|
||||
terminal_stages = {"promoted", "rolled_back", "failed"}
|
||||
if data.get("stage") in terminal_stages and not data.get("_reloaded"):
|
||||
try:
|
||||
app.state.pipeline = joblib.load(PIPELINE_PATH)
|
||||
data["_reloaded"] = True
|
||||
STATUS_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
STATUS_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
logger.info(f"Pipeline di-reload setelah retrain (stage={data['stage']})")
|
||||
except Exception as e:
|
||||
logger.error(f"Gagal reload pipeline: {e}")
|
||||
|
|
@ -262,7 +262,7 @@ def process_classification_bg(job_id: str, file_path: Path, filename: str, pipel
|
|||
}
|
||||
if results is not None:
|
||||
data["results"] = results
|
||||
status_file.write_text(json.dumps(data, ensure_ascii=False))
|
||||
status_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
try:
|
||||
update_status("processing", "Parsing file excel/csv...")
|
||||
|
|
@ -397,7 +397,7 @@ async def classify_tracer(background_tasks: BackgroundTasks, file: UploadFile =
|
|||
temp_file.write_bytes(contents)
|
||||
|
||||
status_file = JOBS_DIR / f"{job_id}.json"
|
||||
status_file.write_text(json.dumps({"status": "pending", "message": "Job masuk antrean"}))
|
||||
status_file.write_text(json.dumps({"status": "pending", "message": "Job masuk antrean"}), encoding="utf-8")
|
||||
|
||||
background_tasks.add_task(process_classification_bg, job_id, temp_file, file.filename, app.state.pipeline)
|
||||
|
||||
|
|
@ -410,7 +410,7 @@ def classify_status(job_id: str):
|
|||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
try:
|
||||
data = json.loads(status_file.read_text())
|
||||
data = json.loads(status_file.read_text(encoding="utf-8"))
|
||||
return JSONResponse(content=data)
|
||||
except json.JSONDecodeError:
|
||||
return JSONResponse(content={"status": "processing", "message": "Updating status..."})
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ def write_status(stage: str, message: str, extra: dict = None):
|
|||
with lock:
|
||||
payload = {"stage": stage, "message": message, "timestamp": datetime.now().isoformat()}
|
||||
if extra: payload.update(extra)
|
||||
STATUS_PATH.write_text(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||
STATUS_PATH.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
logger.info(f"[{stage}] {message}")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -428,7 +428,7 @@ def main(extra_csv_path: str = None):
|
|||
"delta": round(delta, 4),
|
||||
}
|
||||
}
|
||||
METRICS_PATH.write_text(json.dumps(new_metrics_full, indent=2, ensure_ascii=False))
|
||||
METRICS_PATH.write_text(json.dumps(new_metrics_full, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
write_status(
|
||||
stage="promoted",
|
||||
|
|
@ -455,9 +455,9 @@ def main(extra_csv_path: str = None):
|
|||
# Override stage ke 'failed' agar UI tahu ini bukan rollback biasa
|
||||
lock = FileLock(LOCK_PATH, timeout=10)
|
||||
with lock:
|
||||
status = json.loads(STATUS_PATH.read_text())
|
||||
status = json.loads(STATUS_PATH.read_text(encoding="utf-8"))
|
||||
status["stage"] = "failed"
|
||||
STATUS_PATH.write_text(json.dumps(status, indent=2, ensure_ascii=False))
|
||||
STATUS_PATH.write_text(json.dumps(status, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import joblib
|
||||
|
||||
# Load pipeline model
|
||||
pipeline = joblib.load('ml_assets/ml_pipeline_internal.pkl')
|
||||
vec = pipeline.named_steps['tfidf']
|
||||
clf = pipeline.named_steps['clf']
|
||||
|
||||
# Ambil semua nama fitur (vocabulary)
|
||||
feats = vec.get_feature_names_out()
|
||||
|
||||
# Looping untuk semua kelas
|
||||
for class_name in clf.classes_:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"KELAS: {class_name}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
class_idx = list(clf.classes_).index(class_name)
|
||||
class_coefs = clf.coef_[class_idx]
|
||||
|
||||
# 1. Top 5 Fitur POSITIF (Paling tinggi)
|
||||
print("\n[+] Top 5 Fitur POSITIF (Paling Mendorong ke Kelas Ini):")
|
||||
top_positive = sorted(zip(feats, class_coefs), key=lambda x: -x[1])[:5]
|
||||
for word, coef in top_positive:
|
||||
print(f" {word}: {coef:.4f}")
|
||||
|
||||
# 2. Top 5 Fitur NEGATIF (Paling rendah)
|
||||
print("\n[-] Top 5 Fitur NEGATIF (Paling Menghambat Kelas Ini):")
|
||||
top_negative = sorted(zip(feats, class_coefs), key=lambda x: x[1])[:5]
|
||||
for word, coef in top_negative:
|
||||
print(f" {word}: {coef:.4f}")
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
|
@ -0,0 +1 @@
|
|||
{"status": "error", "message": "Processing failed: 'charmap' codec can't encode character '\\U0001f602' in position 885944: character maps to <undefined>", "processed_rows": 0, "total_rows": 0, "source_type": "unknown"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"methodology": "Internal MIF only",
|
||||
"k_fold": {
|
||||
"accuracy_mean": 0.728,
|
||||
"accuracy_std": 0.08634813257969161,
|
||||
"accuracy_mean": 0.712,
|
||||
"accuracy_std": 0.08447484832777147,
|
||||
"folds": [
|
||||
{
|
||||
"Data Analyst": {
|
||||
|
|
@ -12,15 +12,15 @@
|
|||
"support": 1.0
|
||||
},
|
||||
"Non-IT": {
|
||||
"precision": 0.8,
|
||||
"recall": 0.8888888888888888,
|
||||
"f1-score": 0.8421052631578947,
|
||||
"precision": 0.8095238095238095,
|
||||
"recall": 0.9444444444444444,
|
||||
"f1-score": 0.8717948717948718,
|
||||
"support": 18.0
|
||||
},
|
||||
"Programmer": {
|
||||
"precision": 0.8,
|
||||
"precision": 0.8888888888888888,
|
||||
"recall": 0.6153846153846154,
|
||||
"f1-score": 0.6956521739130435,
|
||||
"f1-score": 0.7272727272727273,
|
||||
"support": 13.0
|
||||
},
|
||||
"Wirausaha Informatika": {
|
||||
|
|
@ -29,17 +29,17 @@
|
|||
"f1-score": 0.8421052631578947,
|
||||
"support": 18.0
|
||||
},
|
||||
"accuracy": 0.8,
|
||||
"accuracy": 0.82,
|
||||
"macro avg": {
|
||||
"precision": 0.6000000000000001,
|
||||
"recall": 0.5982905982905983,
|
||||
"f1-score": 0.5949656750572082,
|
||||
"precision": 0.6246031746031746,
|
||||
"recall": 0.6121794871794872,
|
||||
"f1-score": 0.6102932155563734,
|
||||
"support": 50.0
|
||||
},
|
||||
"weighted avg": {
|
||||
"precision": 0.784,
|
||||
"recall": 0.8,
|
||||
"f1-score": 0.7871853546910755,
|
||||
"precision": 0.8105396825396824,
|
||||
"recall": 0.82,
|
||||
"f1-score": 0.8060949576739049,
|
||||
"support": 50.0
|
||||
}
|
||||
},
|
||||
|
|
@ -51,9 +51,9 @@
|
|||
"support": 1.0
|
||||
},
|
||||
"Non-IT": {
|
||||
"precision": 0.7727272727272727,
|
||||
"recall": 1.0,
|
||||
"f1-score": 0.8717948717948718,
|
||||
"precision": 0.7619047619047619,
|
||||
"recall": 0.9411764705882353,
|
||||
"f1-score": 0.8421052631578947,
|
||||
"support": 17.0
|
||||
},
|
||||
"Programmer": {
|
||||
|
|
@ -68,37 +68,37 @@
|
|||
"f1-score": 0.75,
|
||||
"support": 18.0
|
||||
},
|
||||
"accuracy": 0.76,
|
||||
"accuracy": 0.74,
|
||||
"macro avg": {
|
||||
"precision": 0.6136363636363636,
|
||||
"recall": 0.5654761904761905,
|
||||
"f1-score": 0.555448717948718,
|
||||
"precision": 0.6109307359307359,
|
||||
"recall": 0.5507703081232493,
|
||||
"f1-score": 0.5480263157894737,
|
||||
"support": 50.0
|
||||
},
|
||||
"weighted avg": {
|
||||
"precision": 0.7881818181818181,
|
||||
"recall": 0.76,
|
||||
"f1-score": 0.7344102564102565,
|
||||
"precision": 0.7845021645021645,
|
||||
"recall": 0.74,
|
||||
"f1-score": 0.7243157894736842,
|
||||
"support": 50.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Data Analyst": {
|
||||
"precision": 1.0,
|
||||
"precision": 0.25,
|
||||
"recall": 1.0,
|
||||
"f1-score": 1.0,
|
||||
"f1-score": 0.4,
|
||||
"support": 1.0
|
||||
},
|
||||
"Non-IT": {
|
||||
"precision": 0.6296296296296297,
|
||||
"recall": 1.0,
|
||||
"f1-score": 0.7727272727272727,
|
||||
"precision": 0.6,
|
||||
"recall": 0.8823529411764706,
|
||||
"f1-score": 0.7142857142857143,
|
||||
"support": 17.0
|
||||
},
|
||||
"Programmer": {
|
||||
"precision": 1.0,
|
||||
"recall": 0.35714285714285715,
|
||||
"f1-score": 0.5263157894736842,
|
||||
"recall": 0.2857142857142857,
|
||||
"f1-score": 0.4444444444444444,
|
||||
"support": 14.0
|
||||
},
|
||||
"Wirausaha Informatika": {
|
||||
|
|
@ -107,31 +107,31 @@
|
|||
"f1-score": 0.9142857142857143,
|
||||
"support": 18.0
|
||||
},
|
||||
"accuracy": 0.78,
|
||||
"accuracy": 0.72,
|
||||
"macro avg": {
|
||||
"precision": 0.8927015250544663,
|
||||
"recall": 0.8115079365079365,
|
||||
"f1-score": 0.8033321941216678,
|
||||
"precision": 0.6977941176470588,
|
||||
"recall": 0.7642390289449112,
|
||||
"f1-score": 0.6182539682539683,
|
||||
"support": 50.0
|
||||
},
|
||||
"weighted avg": {
|
||||
"precision": 0.8528976034858389,
|
||||
"recall": 0.78,
|
||||
"f1-score": 0.7592385509227614,
|
||||
"precision": 0.8278235294117647,
|
||||
"recall": 0.72,
|
||||
"f1-score": 0.7044444444444444,
|
||||
"support": 50.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Data Analyst": {
|
||||
"precision": 1.0,
|
||||
"precision": 0.3333333333333333,
|
||||
"recall": 1.0,
|
||||
"f1-score": 1.0,
|
||||
"f1-score": 0.5,
|
||||
"support": 1.0
|
||||
},
|
||||
"Non-IT": {
|
||||
"precision": 0.6538461538461539,
|
||||
"recall": 1.0,
|
||||
"f1-score": 0.7906976744186046,
|
||||
"precision": 0.6666666666666666,
|
||||
"recall": 0.9411764705882353,
|
||||
"f1-score": 0.7804878048780488,
|
||||
"support": 17.0
|
||||
},
|
||||
"Programmer": {
|
||||
|
|
@ -146,17 +146,17 @@
|
|||
"f1-score": 0.7428571428571429,
|
||||
"support": 18.0
|
||||
},
|
||||
"accuracy": 0.74,
|
||||
"accuracy": 0.72,
|
||||
"macro avg": {
|
||||
"precision": 0.8546380090497737,
|
||||
"recall": 0.7876984126984127,
|
||||
"f1-score": 0.7833887043189369,
|
||||
"precision": 0.6911764705882353,
|
||||
"recall": 0.7729925303454716,
|
||||
"f1-score": 0.6558362369337979,
|
||||
"support": 50.0
|
||||
},
|
||||
"weighted avg": {
|
||||
"precision": 0.797601809954751,
|
||||
"recall": 0.74,
|
||||
"f1-score": 0.724265780730897,
|
||||
"precision": 0.7886274509803921,
|
||||
"recall": 0.72,
|
||||
"f1-score": 0.7107944250871081,
|
||||
"support": 50.0
|
||||
}
|
||||
},
|
||||
|
|
@ -210,9 +210,9 @@
|
|||
"support": 2.0
|
||||
},
|
||||
"Non-IT": {
|
||||
"precision": 0.7777777777777778,
|
||||
"recall": 0.9722222222222222,
|
||||
"f1-score": 0.8641975308641975,
|
||||
"precision": 0.7727272727272727,
|
||||
"recall": 0.9444444444444444,
|
||||
"f1-score": 0.85,
|
||||
"support": 36.0
|
||||
},
|
||||
"Programmer": {
|
||||
|
|
@ -227,17 +227,17 @@
|
|||
"f1-score": 0.8888888888888888,
|
||||
"support": 39.0
|
||||
},
|
||||
"accuracy": 0.8411214953271028,
|
||||
"accuracy": 0.8317757009345794,
|
||||
"macro avg": {
|
||||
"precision": 0.6462301587301588,
|
||||
"recall": 0.6321581196581196,
|
||||
"f1-score": 0.6282716049382716,
|
||||
"precision": 0.6449675324675325,
|
||||
"recall": 0.6252136752136752,
|
||||
"f1-score": 0.6247222222222222,
|
||||
"support": 107.0
|
||||
},
|
||||
"weighted avg": {
|
||||
"precision": 0.8404539385847796,
|
||||
"recall": 0.8411214953271028,
|
||||
"f1-score": 0.8278296988577363,
|
||||
"precision": 0.8387547032406844,
|
||||
"recall": 0.8317757009345794,
|
||||
"f1-score": 0.8230529595015575,
|
||||
"support": 107.0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"stage": "rolled_back",
|
||||
"message": "Model lama dipertahankan. Model baru lebih buruk secara signifikan (-22.52% weighted F1)",
|
||||
"timestamp": "2026-05-28T16:37:10.275000",
|
||||
"message": "Model lama dipertahankan. Model baru lebih buruk secara signifikan (-20.44% weighted F1)",
|
||||
"timestamp": "2026-06-08T11:35:03.809787",
|
||||
"result": "rolled_back",
|
||||
"reason": "Model baru lebih buruk secara signifikan (-22.52% weighted F1)",
|
||||
"old_f1": 0.8788,
|
||||
"new_f1": 0.6536,
|
||||
"reason": "Model baru lebih buruk secara signifikan (-20.44% weighted F1)",
|
||||
"old_f1": 0.9064,
|
||||
"new_f1": 0.702,
|
||||
"_reloaded": true
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -18,7 +18,7 @@
|
|||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
||||
<div>
|
||||
<a href="/">
|
||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
||||
<img src="{{ asset('images/logo_sista_mif.png') }}" class="w-32 h-auto object-contain" alt="Logo SISTA MIF" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
||||
<img src="{{ asset('images/logo_sista_mif_dashboard.png') }}" class="block h-12 w-auto object-contain" alt="Logo Dashboard" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue