klasifikasi-keringanan-ukt/streamlit_app.py

427 lines
13 KiB
Python

from datetime import datetime
from pathlib import Path
import pandas as pd
import streamlit as st
from sklearn.compose import ColumnTransformer
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.tree import DecisionTreeClassifier
APP_TITLE = "Klasifikasi Kelayakan Keringanan UKT"
DATA_PATH = Path(__file__).with_name("klasifikasi_mhs.csv")
HISTORY_PATH = Path(__file__).with_name("riwayat_prediksi_ukt.csv")
TARGET = "Kelayakan Keringanan UKT"
FEATURES = [
"Tempat Tinggal",
"Pekerjaan Orang Tua",
"Penghasilan Orang Tua",
"Jumlah Tanggungan Orang Tua",
"Kendaraan",
]
LABEL_TEXT = {
0: "Layak menerima keringanan UKT",
1: "Tidak layak menerima keringanan UKT",
}
TEMPAT_TINGGAL_OPTIONS = {
"Rumah sendiri": 0,
"Bukan rumah sendiri": 1,
}
MODEL_OPTIONS = {
"Decision Tree": "decision_tree",
"Naive Bayes": "naive_bayes",
}
st.set_page_config(
page_title=APP_TITLE,
layout="wide",
)
def normalize_target(series: pd.Series) -> pd.Series:
return (
series.astype(str)
.str.strip()
.replace({"1 1": "1"})
.astype(int)
)
def load_data() -> pd.DataFrame:
if not DATA_PATH.exists():
st.error(f"File data tidak ditemukan: {DATA_PATH}")
st.stop()
data = pd.read_csv(DATA_PATH)
data[TARGET] = normalize_target(data[TARGET])
return data
def save_training_row(row: dict) -> None:
data = load_data()
updated = pd.concat([data, pd.DataFrame([row])], ignore_index=True)
updated.to_csv(DATA_PATH, index=False)
def save_prediction_history(row: dict) -> None:
row["Waktu Prediksi"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
history = pd.DataFrame([row])
if HISTORY_PATH.exists():
old_history = pd.read_csv(HISTORY_PATH)
history = pd.concat([old_history, history], ignore_index=True)
history.to_csv(HISTORY_PATH, index=False)
def make_one_hot_encoder() -> OneHotEncoder:
try:
return OneHotEncoder(handle_unknown="ignore", sparse_output=False)
except TypeError:
return OneHotEncoder(handle_unknown="ignore", sparse=False)
def make_preprocessor() -> ColumnTransformer:
return ColumnTransformer(
transformers=[
("pekerjaan", make_one_hot_encoder(), ["Pekerjaan Orang Tua"]),
(
"angka",
"passthrough",
[
"Tempat Tinggal",
"Penghasilan Orang Tua",
"Jumlah Tanggungan Orang Tua",
"Kendaraan",
],
),
],
remainder="drop",
)
def build_models() -> dict:
return {
"decision_tree": Pipeline(
steps=[
("preprocess", make_preprocessor()),
(
"model",
DecisionTreeClassifier(
max_depth=4,
random_state=42,
class_weight="balanced",
),
),
]
),
"naive_bayes": Pipeline(
steps=[
("preprocess", make_preprocessor()),
("model", GaussianNB()),
]
),
}
def train_models(data: pd.DataFrame) -> tuple[dict, pd.DataFrame]:
x = data[FEATURES]
y = data[TARGET]
models = build_models()
metrics = []
can_split = y.nunique() == 2 and y.value_counts().min() >= 2
if can_split:
x_train, x_test, y_train, y_test = train_test_split(
x,
y,
test_size=0.2,
random_state=42,
stratify=y,
)
else:
x_train, x_test, y_train, y_test = x, x, y, y
for display_name, model_key in MODEL_OPTIONS.items():
model = models[model_key]
model.fit(x_train, y_train)
prediction = model.predict(x_test)
metrics.append(
{
"Model": display_name,
"Accuracy": accuracy_score(y_test, prediction),
"F1-Score": f1_score(y_test, prediction, average="weighted", zero_division=0),
}
)
return models, pd.DataFrame(metrics).round(4)
def make_input_dataframe(
tempat_tinggal: int,
pekerjaan: str,
penghasilan: int,
tanggungan: int,
kendaraan: int,
) -> pd.DataFrame:
return pd.DataFrame(
[
{
"Tempat Tinggal": tempat_tinggal,
"Pekerjaan Orang Tua": pekerjaan,
"Penghasilan Orang Tua": penghasilan,
"Jumlah Tanggungan Orang Tua": tanggungan,
"Kendaraan": kendaraan,
}
]
)
def predict_with_models(models: dict, input_df: pd.DataFrame) -> pd.DataFrame:
results = []
for display_name, model_key in MODEL_OPTIONS.items():
model = models[model_key]
prediction = int(model.predict(input_df)[0])
probability_text = "-"
if hasattr(model, "predict_proba"):
probabilities = model.predict_proba(input_df)[0]
classes = list(model.classes_)
probability = probabilities[classes.index(prediction)]
probability_text = f"{probability:.2%}"
results.append(
{
"Model": display_name,
"Prediksi": LABEL_TEXT[prediction],
"Kode Prediksi": prediction,
"Keyakinan Model": probability_text,
}
)
return pd.DataFrame(results)
def render_prediction_result(results: pd.DataFrame, selected_model_name: str) -> int:
selected = results[results["Model"] == selected_model_name].iloc[0]
prediction_code = int(selected["Kode Prediksi"])
prediction_text = selected["Prediksi"]
if prediction_code == 0:
st.success(f"Hasil prediksi {selected_model_name}: {prediction_text}")
else:
st.warning(f"Hasil prediksi {selected_model_name}: {prediction_text}")
st.dataframe(
results[["Model", "Prediksi", "Keyakinan Model"]],
use_container_width=True,
hide_index=True,
)
return prediction_code
def render_form(prefix: str, data: pd.DataFrame) -> pd.DataFrame:
pekerjaan_list = sorted(data["Pekerjaan Orang Tua"].dropna().astype(str).unique())
pekerjaan_options = pekerjaan_list + ["Lainnya"]
col_left, col_right = st.columns(2)
with col_left:
tempat_label = st.selectbox(
"Tempat tinggal",
options=list(TEMPAT_TINGGAL_OPTIONS.keys()),
key=f"{prefix}_tempat",
)
pekerjaan_choice = st.selectbox(
"Pekerjaan orang tua",
options=pekerjaan_options,
key=f"{prefix}_pekerjaan_choice",
)
if pekerjaan_choice == "Lainnya":
pekerjaan = st.text_input(
"Tulis pekerjaan orang tua",
value="",
key=f"{prefix}_pekerjaan_baru",
).strip()
else:
pekerjaan = pekerjaan_choice
with col_right:
penghasilan = st.number_input(
"Penghasilan orang tua per bulan",
min_value=0,
value=3_000_000,
step=100_000,
key=f"{prefix}_penghasilan",
)
tanggungan = st.number_input(
"Jumlah tanggungan orang tua",
min_value=0,
value=3,
step=1,
key=f"{prefix}_tanggungan",
)
kendaraan = st.number_input(
"Jumlah kendaraan",
min_value=0,
value=1,
step=1,
key=f"{prefix}_kendaraan",
)
if not pekerjaan:
st.warning("Isi pekerjaan orang tua terlebih dahulu.")
return pd.DataFrame()
return make_input_dataframe(
tempat_tinggal=TEMPAT_TINGGAL_OPTIONS[tempat_label],
pekerjaan=pekerjaan,
penghasilan=int(penghasilan),
tanggungan=int(tanggungan),
kendaraan=int(kendaraan),
)
data = load_data()
models, metrics_df = train_models(data)
st.title(APP_TITLE)
st.caption("Deployment interaktif menggunakan Decision Tree dan Naive Bayes")
model_name = st.sidebar.radio("Model utama", list(MODEL_OPTIONS.keys()))
st.sidebar.divider()
st.sidebar.write("Arti label")
st.sidebar.write("0 = Layak")
st.sidebar.write("1 = Tidak Layak")
tab_ringkasan, tab_pilih, tab_input, tab_data = st.tabs(
[
"Ringkasan",
"Pilih Data Lama",
"Input Data Baru",
"Data & Riwayat",
]
)
with tab_ringkasan:
total_data = len(data)
layak_count = int((data[TARGET] == 0).sum())
tidak_layak_count = int((data[TARGET] == 1).sum())
col_a, col_b, col_c = st.columns(3)
col_a.metric("Total data latih", total_data)
col_b.metric("Layak", layak_count)
col_c.metric("Tidak layak", tidak_layak_count)
st.subheader("Performa model")
st.dataframe(metrics_df, use_container_width=True, hide_index=True)
st.subheader("Distribusi label")
label_counts = (
data[TARGET]
.map(LABEL_TEXT)
.value_counts()
.rename_axis("Label")
.reset_index(name="Jumlah")
)
st.bar_chart(label_counts, x="Label", y="Jumlah")
with tab_pilih:
st.subheader("Cek prediksi dari data yang sudah ada")
pilihan = [
(
idx,
f"Data {idx + 1} | {row['Pekerjaan Orang Tua']} | "
f"Rp{int(row['Penghasilan Orang Tua']):,} | "
f"Tanggungan {int(row['Jumlah Tanggungan Orang Tua'])}",
)
for idx, row in data.iterrows()
]
selected_index = st.selectbox(
"Pilih data mahasiswa",
options=[idx for idx, _ in pilihan],
format_func=lambda value: dict(pilihan)[value],
)
selected_row = data.loc[[selected_index], FEATURES]
st.dataframe(selected_row, use_container_width=True, hide_index=True)
actual_label = int(data.loc[selected_index, TARGET])
st.info(f"Label asli pada dataset: {LABEL_TEXT[actual_label]}")
selected_results = predict_with_models(models, selected_row)
render_prediction_result(selected_results, model_name)
with tab_input:
st.subheader("Prediksi data mahasiswa baru")
input_df = render_form("predict", data)
if not input_df.empty:
st.dataframe(input_df, use_container_width=True, hide_index=True)
input_results = predict_with_models(models, input_df)
predicted_label = render_prediction_result(input_results, model_name)
with st.form("save_prediction_form"):
st.write("Simpan data ini")
save_mode = st.radio(
"Jenis penyimpanan",
[
"Simpan sebagai riwayat prediksi saja",
"Tambahkan ke dataset latih dengan label aktual",
],
)
actual_label_text = st.selectbox(
"Label aktual",
["Layak menerima keringanan UKT", "Tidak layak menerima keringanan UKT"],
disabled=save_mode == "Simpan sebagai riwayat prediksi saja",
)
submitted = st.form_submit_button("Simpan")
if submitted:
row = input_df.iloc[0].to_dict()
row["Prediksi Model Utama"] = predicted_label
row["Prediksi Teks"] = LABEL_TEXT[predicted_label]
if save_mode == "Tambahkan ke dataset latih dengan label aktual":
actual_label = 0 if actual_label_text.startswith("Layak") else 1
training_row = input_df.iloc[0].to_dict()
training_row[TARGET] = actual_label
save_training_row(training_row)
st.success("Data baru sudah ditambahkan ke dataset latih. Aplikasi akan memuat ulang model.")
st.rerun()
else:
save_prediction_history(row)
st.success("Riwayat prediksi sudah disimpan.")
with tab_data:
st.subheader("Dataset saat ini")
st.dataframe(data, use_container_width=True, hide_index=True)
st.download_button(
"Download dataset CSV",
data=data.to_csv(index=False).encode("utf-8"),
file_name="klasifikasi_mhs_terbaru.csv",
mime="text/csv",
)
st.subheader("Riwayat prediksi")
if HISTORY_PATH.exists():
history_df = pd.read_csv(HISTORY_PATH)
st.dataframe(history_df, use_container_width=True, hide_index=True)
st.download_button(
"Download riwayat prediksi",
data=history_df.to_csv(index=False).encode("utf-8"),
file_name="riwayat_prediksi_ukt.csv",
mime="text/csv",
)
else:
st.info("Belum ada riwayat prediksi yang disimpan.")