TIFNJK_E41221742_Renaldi-En.../views/proses_data.py

445 lines
24 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import streamlit as st
import pandas as pd
import numpy as np
import os
import hashlib
from sklearn.model_selection import train_test_split
import graphviz
import plotly.express as px
import plotly.graph_objects as go
from sklearn.metrics import confusion_matrix
@st.cache_data
def load_data(file_path):
try:
return pd.read_csv(file_path)
except:
return pd.DataFrame()
def render_proses_data():
st.title("⚙️ Tahapan Proses Data & Modeling")
st.markdown("Berikut adalah dokumentasi teknis alur pengolahan data dari mentah hingga evaluasi model, disertai penjelasan metodologi.")
# LOAD DATA
df_mentah = load_data('data/Data_Lengkap_Tokenisasi.csv')
# ==============================================================================
# NAVIGASI
# ==============================================================================
opsi_tahapan = [
"1. Crawling Data",
"2. Preprocessing",
"3. Persiapan Data Latih",
"4. Arsitektur Model",
"5. Evaluasi Model",
"6. Topic Modeling (LDA)"
]
pilihan = st.radio("Pilih Tahapan Proses:", options=opsi_tahapan, horizontal=True, label_visibility="collapsed")
st.markdown("---")
# ==============================================================================
# KONTEN TAHAPAN
# ==============================================================================
# --- 1. CRAWLING DATA ---
if pilihan == "1. Crawling Data":
st.header("1. Pengumpulan Data (Crawling)")
st.info("Tools: **Tweet-Harvest (Node.js)** API Scraper")
st.success(f"✅ Total Data Terkumpul: **{len(df_mentah):,} Data** (Setelah Deduplikasi)")
st.warning("⚠️ **Catatan Imbalance:** Distribusi sentimen awal tidak seimbang, ditangani dengan ROS (Random Over Sampling) pada tahap Training.")
st.markdown("### 📋 Kriteria Pengambilan Data")
st.markdown("""
- **Platform**: X (Twitter)
- **Periode**: 01 Februari 2025 - 31 Maret 2025
- **Filter Sistem**: Hanya Bahasa Indonesia (`lang:id`) & Mengabaikan Retweet (`-is:retweet`).
**Kata Kunci (Search Queries):**
**1. Core Keywords (Isu Utama):**
* `"efisiensi anggaran pendidikan" lang:id -is:retweet`
* `"pemotongan anggaran pendidikan" lang:id -is:retweet`
* `"anggaran pendidikan dikurangi" lang:id -is:retweet`
**2. Program Spesifik:**
* `("dana BOS" OR "bantuan operasional sekolah") ("dipotong" OR "dikurangi" OR "efisiensi" OR "kurang") lang:id -is:retweet`
* `("PIP" OR "program indonesia pintar") ("dipotong" OR "dikurangi" OR "efisiensi" OR "cair") lang:id -is:retweet`
* `("KIP Kuliah" OR "kartu indonesia pintar") ("dipotong" OR "dikurangi" OR "efisiensi" OR "sulit") lang:id -is:retweet`
* `("tunjangan guru" OR "sertifikasi guru") ("dipotong" OR "dikurangi" OR "efisiensi" OR "telat") lang:id -is:retweet`
**3. Kombinasi Isu Umum:**
* `(anggaran OR dana) (pendidikan OR sekolah OR kampus OR guru) (efisiensi OR potong OR dikurangi OR berkurang) lang:id -is:retweet`
- **Proses Lanjutan**: Deduplikasi (Hapus ID & Teks yang berulang).
""")
st.markdown("### 🔍 Preview Data Mentah")
if not df_mentah.empty:
search_mentah = st.text_input("Cari kata dalam Tweet (Mentah):", placeholder="Contoh: dana bos", key="cari_mentah")
if search_mentah:
df_tampil = df_mentah[df_mentah['Teks Tweet'].str.contains(search_mentah, case=False, na=False)].copy()
else:
df_tampil = df_mentah.copy()
df_tampil = df_tampil[['created_at', 'username', 'Teks Tweet']].rename(columns={'created_at': 'Created At', 'username': 'Username'})
df_tampil.index = range(1, len(df_tampil) + 1)
st.dataframe(df_tampil, use_container_width=True, height=250)
# --- 2. PREPROCESSING ---
elif pilihan == "2. Preprocessing":
st.header("2. Preprocessing Teks")
st.markdown("""
**Tujuan:** Mengubah data teks tidak terstruktur menjadi format bersih yang siap diproses mesin.
Pada penelitian ini, kami memutuskan untuk **TIDAK MELAKUKAN Stemming & Stopword Removal**.
* **Alasan:** Model Deep Learning (seperti LSTM) membutuhkan konteks kalimat utuh untuk memahami nuansa sentimen (contoh: kata *"tidak"* sangat penting untuk membalikkan makna *"suka"* menjadi *"tidak suka"*). Menghapus *stopword* justru dapat merusak tata bahasa yang akan dibaca oleh model secara sekuensial.
""")
with st.expander(" Rincian 5 Langkah Preprocessing", expanded=True):
st.markdown("""
1. **Case Folding:** Menyeragamkan huruf menjadi kecil (*lowercase*).
2. **Cleaning:** Menghapus elemen non-teks (URL, Mention `@`, Hashtag `#`, Angka, Tanda Baca).
3. **Tokenizing:** Memecah kalimat menjadi potongan kata per kata.
4. **Normalisasi Slang:** Mengubah kata tidak baku (*bgt, gk, sy*) menjadi baku (*banget, tidak, saya*) menggunakan kamus *lexicon*.
5. **Detokenizing:** Menggabungkan kata kembali menjadi kalimat utuh.
""")
st.subheader("🔍 Komparasi Sebelum vs Sesudah")
if not df_mentah.empty:
search_pre = st.text_input("Cari kata (Hasil Akhir):", placeholder="Contoh: guru", key="cari_pre")
cols = ['Teks Tweet', 'Tweet_CaseFolded', 'Tweet_Cleaned', 'Tweet_Tokenized', 'Tweet_Normalized', 'Tweet_Final']
cols_exist = [c for c in cols if c in df_mentah.columns]
df_tampil_pre = df_mentah[cols_exist].copy()
if search_pre:
df_tampil_pre = df_tampil_pre[df_tampil_pre['Tweet_Final'].str.contains(search_pre, case=False, na=False)]
df_tampil_pre.index = range(1, len(df_tampil_pre) + 1)
st.dataframe(df_tampil_pre, use_container_width=True, height=400)
else:
st.warning("Data preprocessing belum tersedia.")
# --- 3. PERSIAPAN DATA LATIH ---
elif pilihan == "3. Persiapan Data Latih":
st.header("3. Transformasi & Splitting Data")
st.markdown("""
Agar teks dapat diproses oleh Neural Network, data harus diubah menjadi bentuk numerik (vektor).
Selain itu, dilakukan penyeimbangan data agar model tidak bias.
""")
st.subheader("A. Tokenization & Padding")
st.write("Setiap kata unik dalam dataset diberi ID angka. Karena panjang tweet berbeda-beda, kita lakukan **Padding (Post)** agar semua input memiliki panjang seragam (**100 kata**). Angka 0 di akhir akan diabaikan oleh fitur *Masking* pada model.")
if not df_mentah.empty and 'Label' in df_mentah.columns:
df_token = df_mentah.dropna(subset=['Label']).copy()
# Helper simulasi token
def get_word_id(word): return int(hashlib.md5(word.encode()).hexdigest(), 16) % 3000 + 1
df_token['Detail Token'] = df_token['Tweet_Final'].apply(lambda t: ", ".join([f"{w}:{get_word_id(w)}" for w in str(t).split()[:10]]))
df_token['Padding Sequence (100)'] = df_token['Tweet_Final'].apply(lambda t: str(([get_word_id(w) for w in str(t).split()] + [0]*100)[:20]) + " ...")
st.dataframe(df_token[['Tweet_Final', 'Detail Token', 'Padding Sequence (100)']], use_container_width=True)
st.markdown("---")
st.subheader("B. Splitting 80:20 & Skenario 5 Percobaan")
st.markdown("""
**Skenario Pelatihan:**
Model dilatih menggunakan **5 Skenario Percobaan** (P1 hingga P5) dengan porsi data latih masing-masing 20%, 40%, 60%, 80%, dan 100% (dari total 80% split data latih).
**Penanganan Imbalance (ROS):**
Kami menduplikasi data minoritas (Positif/Netral) secara acak (*Random Over Sampling*) di **setiap porsi data latih** hingga jumlahnya setara dengan kelas mayoritas (Negatif). Data Testing (20%) dibiarkan murni agar evaluasi tetap objektif.
""")
df_train, df_test = train_test_split(df_token, test_size=0.2, random_state=42, stratify=df_token['Label'])
kelas_mayoritas = df_train['Label'].value_counts().max()
col_metric1, col_metric2, col_metric3 = st.columns(3)
col_metric1.metric("Maksimal Data Latih (80%)", f"{len(df_train):,} Sample", "Skenario P5")
col_metric2.metric("Data Uji Tetap (20%)", f"{len(df_test):,} Sample", "Validasi Objektif")
col_metric3.metric("Target ROS P5", f"{kelas_mayoritas}", "Per Kelas Sentimen")
st.success(f"✅ **Status Data:** Dataset latih telah diseimbangkan (Balanced) menggunakan teknik ROS pada tahapan pemodelan.")
# --- 4. ARSITEKTUR MODEL ---
elif pilihan == "4. Arsitektur Model":
st.header("🧠 4. Arsitektur Model: LSTM Standar")
st.markdown("""
Kami menggunakan arsitektur **Long Short-Term Memory (LSTM)** yang dipadukan dengan *Keras Embedding Layer* dan fitur *Masking*.
""")
c_text, c_spacer, c_img = st.columns([1.5, 0.2, 1])
with c_text:
st.subheader("Rincian Layer & Fungsinya:")
st.markdown("""
1. **Embedding (Keras):** Mengubah indeks kata menjadi vektor padat (128 dimensi). Fitur `mask_zero=True` diaktifkan agar model murni fokus pada teks tanpa terdistraksi oleh angka padding (0) di akhir kalimat.
2. **SpatialDropout1D (0.2):** Mematikan sebagian 1D feature maps secara acak untuk mencegah model "menghafal" data secara berlebihan (*overfitting*).
3. **LSTM (64 Units):** Memproses urutan kata secara sekuensial (dari awal hingga akhir kalimat) agar model bisa memahami relasi dan pola frasa sentimen dengan sangat baik.
4. **Dense Layer (32 Units):** Ekstraksi fitur tingkat tinggi menggunakan fungsi aktivasi ReLU dengan peluruhan (Dropout 0.2).
5. **Dense Output (3 Units):** Layer akhir dengan aktivasi *Softmax* yang menghasilkan nilai probabilitas klasifikasi untuk **Negatif, Netral, dan Positif**.
""")
param_data = {
"Nama Layer": ["Embedding", "SpatialDropout", "LSTM", "Dense", "Dense Output"],
"Output Shape": ["(None, 100, 128)", "(None, 100, 128)", "(None, 64)", "(None, 32)", "(None, 3)"],
"Jml Parameter": ["1,280,000", "0", "49,408", "2,080", "99"]
}
st.dataframe(pd.DataFrame(param_data), use_container_width=True)
with c_spacer:
st.empty()
with c_img:
st.caption("Visualisasi Alur Data:")
try:
graph = graphviz.Digraph(node_attr={'shape': 'box', 'style': 'filled', 'fillcolor': '#E8F0FE'})
graph.attr(rankdir='TB')
graph.node('I', 'Input Teks\n(Integer Encoded)', fillcolor='#FFEBEE')
graph.node('E', 'Embedding Layer\n(Dimensi 128, Masking)', fillcolor='#FFF3E0')
graph.node('L', 'LSTM Layer\n(Proses Sekuensial)', fillcolor='#E3F2FD')
graph.node('D', 'Dense & Softmax\n(Klasifikasi 3 Kelas)', fillcolor='#E8F5E9')
graph.edge('I', 'E')
graph.edge('E', 'L')
graph.edge('L', 'D')
st.graphviz_chart(graph, use_container_width=True)
except:
st.info("Install graphviz untuk melihat diagram alir.")
# ==============================================================================
# 5. EVALUASI MODEL
# ==============================================================================
elif pilihan == "5. Evaluasi Model":
st.header("5. Evaluasi Performa Model (Skenario P1-P5)")
st.markdown("Evaluasi ini mencakup perbandingan 5 skenario pelatihan berdasarkan ukuran rasio data latih (20% hingga 100%), yang diuji menggunakan **Data Testing murni (20%)**.")
tab_a, tab_b, tab_c = st.tabs(["📊 Metrik (Model P5)", "📈 Perbandingan 5 Skenario", "📉 Detail Learning Curve"])
# --- TAB A: TABEL ANGKA ---
with tab_a:
st.subheader("1. Classification Report (Model P5)")
st.markdown("""
- **Precision:** Ketepatan prediksi model (Meminimalisir salah tebak positif palsu).
- **Recall:** Kelengkapan prediksi (Meminimalisir salah tebak negatif palsu).
- **F1-Score:** Rata-rata harmonis antara Precision dan Recall.
""")
path_perf = 'model/Tabel_Performa_LSTM.csv'
if not os.path.exists(path_perf): path_perf = 'Tabel_Performa_LSTM.csv'
if os.path.exists(path_perf):
df_perf = pd.read_csv(path_perf, index_col=0)
st.table(
df_perf.style.highlight_max(axis=0, props='background-color: #FFEB3B; color: black; font-weight: bold')
)
if 'accuracy' in df_perf.index:
acc = df_perf.loc['accuracy', 'f1-score']
st.metric("Akurasi Total (Data Testing P5)", f"{acc*100:.2f}%")
else:
st.warning("⚠️ File 'Tabel_Performa_LSTM.csv' belum tersedia.")
st.markdown("---")
st.subheader("2. Confusion Matrix (Model P5)")
path_cm = 'model/Data_Confusion_Matrix.csv'
if os.path.exists(path_cm):
df_cm_data = pd.read_csv(path_cm)
if 'y_true' in df_cm_data.columns and 'y_pred' in df_cm_data.columns:
labels = ['Negatif', 'Netral', 'Positif']
cm = confusion_matrix(df_cm_data['y_true'], df_cm_data['y_pred'])
fig_cm = px.imshow(cm, text_auto=True, labels=dict(x="Prediksi Model", y="Label Aktual (Asli)", color="Jumlah Data"), x=labels, y=labels, color_continuous_scale='Blues')
fig_cm.update_layout(title="Matrix Kebenaran Prediksi P5")
st.plotly_chart(fig_cm, use_container_width=True)
else:
st.warning("⚠️ File 'Data_Confusion_Matrix.csv' tidak ditemukan.")
# --- TAB B: BAR CHART PERBANDINGAN SKENARIO (DINAMIS DARI CSV) ---
with tab_b:
st.subheader("Perbandingan Akurasi Skenario P1 hingga P5")
st.markdown("Grafik interaktif ini menunjukkan bahwa semakin besar porsi data latih yang diberikan, maka kemampuan model dalam mengklasifikasi sentimen cenderung semakin baik.")
path_akurasi = 'model/Akurasi_Skenario.csv'
if os.path.exists(path_akurasi):
df_acc_skenario = pd.read_csv(path_akurasi)
rata_rata = df_acc_skenario['Akurasi'].mean()
# Buat label gabungan P1 (20%), dst
df_acc_skenario['Label_X'] = df_acc_skenario['Skenario'] + " (" + df_acc_skenario['Porsi_Data'] + ")"
fig_bar = px.bar(
df_acc_skenario, x='Label_X', y='Akurasi',
text='Akurasi',
color='Skenario',
color_discrete_sequence=px.colors.qualitative.Set1,
title="Persentase Akurasi per Skenario Data Latih",
labels={'Label_X': 'Skenario (Porsi Data Latih)', 'Akurasi': 'Akurasi (%)'}
)
fig_bar.update_traces(texttemplate='%{text:.2f}%', textposition='outside')
fig_bar.add_hline(y=rata_rata, line_dash="dot", line_color="red", annotation_text=f"Rata-rata: {rata_rata:.2f}%")
fig_bar.update_layout(yaxis_range=[0, 100], showlegend=False)
st.plotly_chart(fig_bar, use_container_width=True)
else:
st.warning("⚠️ File 'Akurasi_Skenario.csv' belum tersedia. Harap export dari Colab.")
# --- TAB C: KURVA PEMBELAJARAN SEMUA SKENARIO (DINAMIS DARI CSV) ---
with tab_c:
st.subheader("Grafik Pergerakan Learning Curve")
st.info("Pilih skenario di bawah ini untuk melihat detail pergerakan Akurasi dan Loss-nya secara interaktif.")
path_hist_semua = 'model/Riwayat_Training_Semua.csv'
if os.path.exists(path_hist_semua):
df_all_hist = pd.read_csv(path_hist_semua)
# Opsi interaktif untuk memilih Skenario
skenario_pilihan = st.selectbox("Pilih Skenario:", ['P1', 'P2', 'P3', 'P4', 'P5'], index=4)
# Filter data berdasarkan skenario yang dipilih
df_hist_filter = df_all_hist[df_all_hist['Skenario'] == skenario_pilihan]
col_chart1, col_chart2 = st.columns(2)
with col_chart1:
fig_acc_line = go.Figure()
fig_acc_line.add_trace(go.Scatter(x=df_hist_filter['Epoch'], y=df_hist_filter['accuracy'], mode='lines+markers', name='Train Acc'))
fig_acc_line.add_trace(go.Scatter(x=df_hist_filter['Epoch'], y=df_hist_filter['val_accuracy'], mode='lines+markers', name='Val Acc'))
fig_acc_line.update_layout(title=f"Akurasi ({skenario_pilihan})", xaxis_title="Epochs", yaxis_title="Akurasi", hovermode="x unified")
st.plotly_chart(fig_acc_line, use_container_width=True)
with col_chart2:
fig_loss_line = go.Figure()
fig_loss_line.add_trace(go.Scatter(x=df_hist_filter['Epoch'], y=df_hist_filter['loss'], mode='lines+markers', name='Train Loss', line=dict(color='orange')))
fig_loss_line.add_trace(go.Scatter(x=df_hist_filter['Epoch'], y=df_hist_filter['val_loss'], mode='lines+markers', name='Val Loss', line=dict(color='red')))
fig_loss_line.update_layout(title=f"Loss ({skenario_pilihan})", xaxis_title="Epochs", yaxis_title="Loss", hovermode="x unified")
st.plotly_chart(fig_loss_line, use_container_width=True)
else:
st.warning("⚠️ File 'Riwayat_Training_Semua.csv' belum tersedia. Harap export dari Colab.")
# ==============================================================================
# 6. TOPIC MODELING (LDA)
# ==============================================================================
elif pilihan == "6. Topic Modeling (LDA)":
st.header("6. Topic Modeling (LDA)")
st.markdown("""
**Tujuan:** Menggali "Apa yang sebenarnya dibicarakan publik?" di balik masing-masing sentimen menggunakan metode **Latent Dirichlet Allocation (LDA)**.
""")
# --- BAGIAN A: METRIK EVALUASI (COHERENCE SCORE) ---
st.subheader("A. Optimasi Jumlah Topik (Coherence Score)")
st.info("💡 Grafik ini menunjukkan bagaimana model menentukan jumlah topik (K) terbaik secara ilmiah berdasarkan skor *Coherence c_v* tertinggi.")
col_lda1, col_lda2 = st.columns([2, 1])
with col_lda1:
path_coherence = 'model/Nilai_Coherence.csv'
if not os.path.exists(path_coherence): path_coherence = 'Nilai_Coherence.csv'
if os.path.exists(path_coherence):
df_coh = pd.read_csv(path_coherence)
# Plot Line Chart
fig_coh = px.line(df_coh, x='Num_Topics', y='Coherence_Score', markers=True,
title="Pergerakan Nilai Coherence Score",
labels={'Num_Topics': 'Jumlah Topik', 'Coherence_Score': 'Skor Koherensi (c_v)'})
max_score = df_coh['Coherence_Score'].max()
best_topic_num = df_coh.loc[df_coh['Coherence_Score'].idxmax(), 'Num_Topics']
fig_coh.add_annotation(x=best_topic_num, y=max_score,
text=f"Optimal: {int(best_topic_num)} Topik",
showarrow=True, arrowhead=1)
st.plotly_chart(fig_coh, use_container_width=True)
else:
st.warning("⚠️ File 'Nilai_Coherence.csv' tidak ditemukan.")
with col_lda2:
st.markdown("### 📝 Interpretasi:")
st.write("""
Algoritma mesin bekerja dengan mencari pola kata yang sering muncul bersamaan di dalam satu dokumen teks.
**Coherence Score** bertugas untuk mengukur seberapa masuk akal ("nyambung") kumpulan kata-kata dalam satu topik. Semakin tinggi skornya, maka topik tersebut akan semakin mudah diinterpretasikan oleh pembaca/manusia.
""")
st.markdown("---")
# --- BAGIAN B: VISUALISASI TOPIK (BAR CHART DARI CSV) ---
st.subheader("B. Visualisasi Kata Kunci per Topik")
st.write("Berikut adalah distribusi kata-kata kunci dominan yang mewakili setiap topik berdasarkan prediksi sentimen data *testing*.")
path_lda = 'model/Hasil_Analisis_Topik_LDA.csv'
if not os.path.exists(path_lda): path_lda = 'Hasil_Analisis_Topik_LDA.csv'
if os.path.exists(path_lda):
try:
df_lda = pd.read_csv(path_lda)
# Fungsi Parsing Teks dari format CSV
def parse_lda_string(text_data):
data_items = []
# Memisahkan format yang sudah kita bersihkan di Colab
for word in str(text_data).split(','):
word = word.strip()
if word:
# Bobot diset dinamis untuk memunculkan visual Bar Horizontal (berdasarkan urutan)
data_items.append({'Kata': word})
df_res = pd.DataFrame(data_items)
if not df_res.empty:
# Memberikan bobot buatan berdasarkan urutan (agar chart terbentuk rapi dari atas ke bawah)
df_res['Bobot'] = range(len(df_res), 0, -1)
df_res = df_res.sort_values(by='Bobot', ascending=True)
return df_res
# Tabs untuk Topik
t_neg, t_net, t_pos = st.tabs(["🔴 Topik Negatif", "⚪ Topik Netral", "🟢 Topik Positif"])
mapping = {'negatif': t_neg, 'netral': t_net, 'positif': t_pos}
for sentimen, tab in mapping.items():
with tab:
# Filter CSV berdasarkan sentimen
df_subset = df_lda[df_lda['Sentimen'].str.lower() == sentimen]
if df_subset.empty:
st.warning(f"Belum ada data ekstraksi topik untuk sentimen {sentimen.upper()}.")
else:
col_t1, col_t2 = st.columns(2)
# Tampilkan Topik dengan 2 kolom berjajar
for idx, row in df_subset.iterrows():
topik_ke = row['Topik Ke']
df_chart = parse_lda_string(row['Kata Kunci'])
if not df_chart.empty:
fig = px.bar(
df_chart, x='Bobot', y='Kata', orientation='h',
title=f"<b>Topik {topik_ke}</b>",
color='Bobot',
color_continuous_scale='Reds' if sentimen == 'negatif' else 'Greys' if sentimen == 'netral' else 'Greens'
)
# Sembunyikan X-axis karena ini hanya bobot representasi urutan
fig.update_layout(height=280, showlegend=False, xaxis_title=None, xaxis_visible=False)
if idx % 2 == 0:
with col_t1: st.plotly_chart(fig, use_container_width=True)
else:
with col_t2: st.plotly_chart(fig, use_container_width=True)
except Exception as e:
st.error(f"Gagal memproses visualisasi data LDA: {e}")
else:
st.warning("⚠️ File 'Hasil_Analisis_Topik_LDA.csv' belum tersedia di dalam folder model.")