From f75fcfc6a668310c6beec2ce55ccd3b26698958f Mon Sep 17 00:00:00 2001 From: 202310715084 PUTRI ADELIA AZIZAH <202310715084@mhs.ubharajaya.ac.id> Date: Mon, 13 Jul 2026 03:59:08 +0700 Subject: [PATCH] Upload files to "/" --- klasifikasi_mhs.csv | 101 +++++++++++ requirements.txt | 3 + run_streamlit.bat | 3 + streamlit_app.py | 426 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 533 insertions(+) create mode 100644 klasifikasi_mhs.csv create mode 100644 requirements.txt create mode 100644 run_streamlit.bat create mode 100644 streamlit_app.py diff --git a/klasifikasi_mhs.csv b/klasifikasi_mhs.csv new file mode 100644 index 0000000..baab266 --- /dev/null +++ b/klasifikasi_mhs.csv @@ -0,0 +1,101 @@ +Tempat Tinggal,Pekerjaan Orang Tua,Penghasilan Orang Tua,Jumlah Tanggungan Orang Tua,Kendaraan,Kelayakan Keringanan UKT +0,PNS,10000000,3,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,4,0,0 +1,Nelayan,3000000,5,1,0 +0,Buruh,2000000,2,1,1 +1,Ibu Rumah Tangga,5000000,3,0,0 +0,PNS,9000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +1,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,3,0,0 +1,Buruh,2000000,2,1,0 +0,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,1 +1,PNS,10000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Petani,4000000,4,0,0 +0,Buruh,2000000,2,1,1 +1,TNI/POLRI,8000000,2,2,1 +1,Nelayan,3000000,5,1,0 +0,Guru,7000000,2,1,1 +1,Ibu Rumah Tangga,5000000,3,0,1 +0,PNS,9000000,2,2,1 +1,Wiraswasta,6000000,1,1,0 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,3,0,0 +1,Buruh,2000000,2,1,1 +0,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +1,PNS,10000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Petani,4000000,4,0,0 +0,Buruh,2000000,2,1,1 +1,TNI/POLRI,8000000,2,2,1 +1,Nelayan,3000000,5,1,0 +0,Guru,7000000,2,1,1 +1,Ibu Rumah Tangga,5000000,3,0,1 +0,PNS,9000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,3,0,0 +1,Buruh,2000000,2,1,1 +0,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +1,PNS,10000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Petani,4000000,4,0,0 +0,Buruh,2000000,2,1,1 +1,TNI/POLRI,8000000,2,2,1 +1,Nelayan,3000000,5,1,0 +0,Guru,7000000,2,1,1 +1,Ibu Rumah Tangga,5000000,3,0,1 +0,PNS,9000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,3,0,0 +1,Buruh,2000000,2,1,1 +0,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +1,PNS,10000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Petani,4000000,4,0,0 +0,Buruh,2000000,2,1,1 +1,TNI/POLRI,8000000,2,2,1 +1,Nelayan,3000000,5,1,0 +0,Guru,7000000,2,1,1 +1,Ibu Rumah Tangga,5000000,3,0,1 +0,PNS,9000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,3,0,0 +1,Buruh,2000000,2,1,1 +0,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +1,PNS,10000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Petani,4000000,4,0,0 +0,Buruh,2000000,2,1,1 +1,TNI/POLRI,8000000,2,2,1 +1,Nelayan,3000000,5,1,0 +0,Guru,7000000,2,1,1 +1,Ibu Rumah Tangga,5000000,3,0,1 +0,PNS,9000000,2,2,1 +1,Wiraswasta,6000000,1,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,TNI/POLRI,8000000,2,2,1 +1,Petani,4000000,3,0,0 +1,Buruh,2000000,2,1,1 +1,Guru,7000000,2,1,1 +0,Ibu Rumah Tangga,3000000,4,1,0 +0,PNS,9000000,2,2,1 1 +1,Wiraswasta,6000000,1,1,1 +1,TNI/POLRI,8000000,2,2,1 +0,Nelayan,3000000,5,1,0 +0,Petani,700000,3,1,0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0831f84 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +streamlit +pandas +scikit-learn diff --git a/run_streamlit.bat b/run_streamlit.bat new file mode 100644 index 0000000..63c0964 --- /dev/null +++ b/run_streamlit.bat @@ -0,0 +1,3 @@ +@echo off +cd /d "%~dp0" +python -m streamlit run streamlit_app.py diff --git a/streamlit_app.py b/streamlit_app.py new file mode 100644 index 0000000..f6446e8 --- /dev/null +++ b/streamlit_app.py @@ -0,0 +1,426 @@ +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.")