import streamlit as st import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from wordcloud import WordCloud import plotly.express as px import plotly.graph_objects as go from collections import Counter from sentiment_model import SentimentAnalyzer import warnings from dateutil import parser as date_parser from datetime import datetime import re import os warnings.filterwarnings('ignore') # Base directory: folder tempat dashboard.py berada # Ini memastikan path file benar di Streamlit Cloud maupun lokal BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # Set font untuk mendukung bahasa Indonesia plt.rcParams['font.family'] = 'DejaVu Sans' # Konfigurasi halaman st.set_page_config( page_title="Analisis Sentimen Bahasa Indonesia", page_icon="๐Ÿ“Š", layout="wide" ) # Load CSS untuk styling st.markdown(""" """, unsafe_allow_html=True) @st.cache_data def load_data(): """Load dataset yang sudah diproses""" try: filepath = os.path.join(BASE_DIR, 'mbg_processed.csv') df = pd.read_csv(filepath) return df except FileNotFoundError: st.error("File mbg_processed.csv tidak ditemukan. Jalankan sentiment_model.py terlebih dahulu.") return None except Exception as e: st.error(f"Error memuat data: {str(e)}") return None @st.cache_resource def load_model(): """Load model yang sudah dilatih, atau latih ulang jika pkl tidak valid""" analyzer = SentimentAnalyzer() model_path = os.path.join(BASE_DIR, 'sentiment_model.pkl') data_path = os.path.join(BASE_DIR, 'data_mbg_labelled.csv') # --- Coba load dari pkl --- pkl_loaded_ok = False if os.path.exists(model_path): try: analyzer.load_model(model_path) if hasattr(analyzer.vectorizer, 'idf_'): pkl_loaded_ok = True except Exception: pass # pkl rusak / versi berbeda โ†’ akan retrain # --- Fallback: latih ulang dari data CSV --- if not pkl_loaded_ok: if not os.path.exists(data_path): st.error( f"โŒ File model ({model_path}) tidak valid DAN " f"dataset ({data_path}) tidak ditemukan. " "Pastikan kedua file sudah di-commit ke GitHub." ) return None try: df = analyzer.load_and_preprocess_data(data_path) analyzer.train_and_evaluate_model(df) # Simpan pkl baru agar berikutnya langsung bisa di-load try: analyzer.save_model(model_path) except Exception: pass # Jika tidak bisa menyimpan (read-only fs), abaikan except Exception as e: st.error(f"โŒ Gagal melatih model: {str(e)}") return None return analyzer def create_pie_chart(df): """Membuat pie chart distribusi sentimen""" sentiment_counts = df['sentiment'].value_counts() labels = ['Negatif', 'Positif'] values = [sentiment_counts[0], sentiment_counts[1]] colors = ['#ff7f7f', '#7fbf7f'] fig = go.Figure(data=[go.Pie( labels=labels, values=values, hole=0.3, marker=dict(colors=colors), texttemplate='%{label}
%{value}
(%{percent})' )]) fig.update_traces( textposition='inside', textfont_size=12 ) fig.update_layout( title={ 'text': "Distribusi Sentimen Dataset", 'x': 0.5, 'xanchor': 'center', 'font': {'size': 16} }, font=dict(size=12), showlegend=True, height=400 ) return fig def create_wordcloud(text, title, colormap='viridis'): """Membuat word cloud dengan font yang mendukung bahasa Indonesia""" if len(text) == 0: return None # Gabungkan semua teks combined_text = ' '.join(text) if not combined_text.strip(): return None wordcloud = WordCloud( width=800, height=400, background_color='white', colormap=colormap, max_words=100, relative_scaling=0.5, min_font_size=10, prefer_horizontal=0.9, collocations=False ).generate(combined_text) fig, ax = plt.subplots(figsize=(10, 5)) ax.imshow(wordcloud, interpolation='bilinear') ax.axis('off') ax.set_title(title, fontsize=16, fontweight='bold', pad=20) return fig def get_top_words(texts, n=20): """Mendapatkan kata-kata yang paling sering muncul""" all_words = [] for text in texts: if pd.notna(text) and text.strip(): all_words.extend(text.split()) # Filter kata yang terlalu pendek all_words = [word for word in all_words if len(word) > 2] word_freq = Counter(all_words) return word_freq.most_common(n) def detect_date_column(df): """Mendeteksi kolom tanggal dari berbagai nama kolom yang umum""" # Daftar nama kolom yang umum untuk tanggal date_column_names = [ 'created_at', 'date', 'timestamp', 'time', 'datetime', 'created', 'posted_at', 'published_at', 'tanggal', 'waktu', 'created_date', 'post_date', 'tweet_created_at', 'creation_date', 'date_created' ] # Cek kolom yang ada di dataframe (case-insensitive) for col in df.columns: col_lower = col.lower().strip() if col_lower in date_column_names: return col # Jika tidak ditemukan dari nama, coba deteksi dari isi kolom for col in df.columns: if df[col].dtype == 'object': # Hanya cek kolom string # Ambil sample non-null values sample_values = df[col].dropna().head(10) if len(sample_values) > 0: # Coba parse beberapa nilai parsed_count = 0 for val in sample_values: if parse_date_robust(str(val)) is not None: parsed_count += 1 # Jika lebih dari 70% bisa diparsing sebagai tanggal, anggap sebagai kolom tanggal if parsed_count / len(sample_values) > 0.7: return col return None def parse_date_robust(date_string): """Parse tanggal dengan berbagai format secara robust""" if pd.isna(date_string) or not date_string or str(date_string).strip() == '': return None date_string = str(date_string).strip() try: # Coba parse dengan dateutil parser (sangat fleksibel) parsed_date = date_parser.parse(date_string, fuzzy=True) return parsed_date except: pass # Coba format-format khusus yang mungkin tidak ditangani dateutil date_formats = [ '%Y-%m-%d %H:%M:%S', '%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%d-%m-%Y', '%Y/%m/%d', '%d %b %Y', '%d %B %Y', '%b %d, %Y', '%B %d, %Y', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%d %H:%M:%S.%f', ] for fmt in date_formats: try: return datetime.strptime(date_string, fmt) except: continue return None def get_date_range(df, date_column): """Mendapatkan rentang tanggal dari kolom tanggal""" if date_column is None or date_column not in df.columns: return None, None # Parse semua tanggal dates = [] for val in df[date_column].dropna(): parsed = parse_date_robust(val) if parsed: dates.append(parsed) if not dates: return None, None min_date = min(dates) max_date = max(dates) return min_date, max_date def get_temporal_statistics(df, date_column): """Mendapatkan statistik temporal (per tahun dan per bulan)""" if date_column is None or date_column not in df.columns: return None # Parse semua tanggal dates = [] for val in df[date_column].dropna(): parsed = parse_date_robust(val) if parsed: dates.append(parsed) if not dates: return None # Buat DataFrame dari dates untuk analisis df_dates = pd.DataFrame({'date': dates}) # Ekstrak tahun dan bulan df_dates['year'] = df_dates['date'].dt.year df_dates['month'] = df_dates['date'].dt.month df_dates['year_month'] = df_dates['date'].dt.to_period('M').astype(str) # Hitung per tahun yearly_counts = df_dates['year'].value_counts().sort_index() # Hitung per bulan monthly_counts = df_dates['year_month'].value_counts().sort_index() # Konversi bulan ke format yang lebih readable month_names_id = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'Mei', 6: 'Jun', 7: 'Jul', 8: 'Agt', 9: 'Sep', 10: 'Okt', 11: 'Nov', 12: 'Des' } monthly_labels = [] for ym in monthly_counts.index: year, month = ym.split('-') month_name = month_names_id[int(month)] monthly_labels.append(f"{month_name} {year}") return { 'yearly_counts': yearly_counts, 'monthly_counts': monthly_counts, 'monthly_labels': monthly_labels, 'total_dates': len(dates) } def create_temporal_charts(temporal_stats): """Membuat visualisasi distribusi temporal""" if temporal_stats is None: return None, None # Chart distribusi per tahun - Filter hanya tahun yang valid yearly_counts = temporal_stats['yearly_counts'] # Filter: hanya ambil tahun yang berupa integer dan dalam range 1900-2100 valid_years = [] valid_counts = [] for year, count in yearly_counts.items(): try: year_int = int(year) if 1900 <= year_int <= 2100: valid_years.append(str(year_int)) valid_counts.append(count) except (ValueError, TypeError): # Skip tahun yang tidak valid (bukan integer atau di luar range) continue fig_yearly = go.Figure([go.Bar( x=valid_years, y=valid_counts, marker_color='lightblue', text=valid_counts, textposition='auto', hovertemplate='Tahun %{x}
Jumlah: %{y}' )]) fig_yearly.update_layout( title={ 'text': "Distribusi Data per Tahun", 'x': 0.5, 'xanchor': 'center', 'font': {'size': 16} }, xaxis_title="Tahun", yaxis_title="Jumlah Data", xaxis={'type': 'category'}, # Set as category to prevent decimal years height=400, showlegend=False, hovermode='x' ) # Chart distribusi per bulan monthly_counts = temporal_stats['monthly_counts'] monthly_labels = temporal_stats['monthly_labels'] fig_monthly = go.Figure([go.Bar( x=monthly_labels, y=monthly_counts.values, marker_color='lightcoral', text=monthly_counts.values, textposition='auto', hovertemplate='%{x}
Jumlah: %{y}' )]) fig_monthly.update_layout( title={ 'text': "Distribusi Data per Bulan", 'x': 0.5, 'xanchor': 'center', 'font': {'size': 16} }, xaxis_title="Bulan", yaxis_title="Jumlah Data", height=400, showlegend=False, hovermode='x', xaxis={'tickangle': -45} ) return fig_yearly, fig_monthly def show_preprocessing_steps(analyzer, text): """Menampilkan tabel langkah-langkah preprocessing""" if text.strip(): processed_text, steps = analyzer.preprocess_text(text, show_steps=True) st.subheader("๐Ÿ“‹ Tahapan Preprocessing") # Buat DataFrame untuk menampilkan steps steps_data = [ ["๐Ÿ”ค Teks Asli", steps['original']], ["๐Ÿงน Pembersihan", steps['cleaned']], ["๐Ÿ“ Case Folding", steps['casefolded']], ["โœ‚๏ธ Tokenisasi", str(steps['tokenized'])], ["๐Ÿ”„ Normalisasi", str(steps['normalized'])], ["๐Ÿšซ Hapus Stopwords", str(steps['no_stopwords'])], ["๐ŸŒฑ Stemming", str(steps['stemmed'])], ["โœ… Hasil Akhir", steps['final']] ] steps_df = pd.DataFrame(steps_data, columns=["Tahap", "Hasil"]) # Styling untuk DataFrame def highlight_rows(row): if row.name == 0: # Original text return ['background-color: #e8f4fd; color: black'] * len(row) elif row.name == len(steps_df) - 1: # Final result return ['background-color: #e8f5e8; color: black'] * len(row) else: return ['background-color: #f9f9f9; color: black'] * len(row) styled_df = steps_df.style.apply(highlight_rows, axis=1) # Render tabel dengan HTML agar bisa discroll horizontal html_table = styled_df.to_html(escape=False) st.markdown( f"""
{html_table}
""", unsafe_allow_html=True ) return processed_text return None def show_examples(): """Menampilkan contoh-contoh teks untuk analisis""" st.subheader("๐Ÿ’ก Contoh Teks untuk Dicoba") examples = [ "MBG program yang bermanfaat untuk generasi emas!", "Makan Bergizi Gratis merupakan bentuk komiitmen pemerintah dalam perbaikan gizi", "makan bergizi gratis ini bagus banget", "MBG program buang buang anggaran", "Makan Bergizi Gratis menjadi Makan Beracun Gratis", "MBG menjadi ladang korupsi", "Stop mbg sebelum jatuh lebih banyak korban keracunan", ] for i, example in enumerate(examples, 1): if st.button(f"Contoh {i}: {example[:50]}...", key=f"example_{i}"): return example return None def main(): """Fungsi utama dashboard""" # Header st.markdown('

๐Ÿ“Š Analisis Sentimen Program MBG Bahasa Indonesia

', unsafe_allow_html=True) # Load data dan model df = load_data() analyzer = load_model() if df is None or analyzer is None: st.stop() # Sidebar st.sidebar.header("๐Ÿงญ Navigasi") page = st.sidebar.selectbox( "Pilih Halaman:", ["๐Ÿ“ˆ Dashboard Utama", "๐Ÿ”ฎ Prediksi Sentimen", "๐Ÿ“‹ Demo Preprocessing", "๐Ÿ“ Analisis CSV"] ) if page == "๐Ÿ“ˆ Dashboard Utama": show_main_dashboard(df) elif page == "๐Ÿ”ฎ Prediksi Sentimen": show_prediction_page(analyzer) elif page == "๐Ÿ“‹ Demo Preprocessing": show_preprocessing_demo(analyzer) elif page == "๐Ÿ“ Analisis CSV": show_csv_analysis_page(analyzer) def show_main_dashboard(df): """Menampilkan dashboard utama""" st.header("๐Ÿ“Š Dashboard Analisis Sentimen") # Metrics col1, col2, col3, col4 = st.columns(4) total_data = len(df) positive_count = len(df[df['sentiment'] == 1]) negative_count = len(df[df['sentiment'] == 0]) positive_ratio = (positive_count / total_data) * 100 with col1: st.metric("๐Ÿ“„ Total Data", f"{total_data:,}") with col2: st.metric("๐Ÿ˜Š Sentimen Positif", f"{positive_count:,}") with col3: st.metric("๐Ÿ˜ž Sentimen Negatif", f"{negative_count:,}") with col4: st.metric("๐Ÿ“Š Rasio Positif", f"{positive_ratio:.1f}%") # Charts col1, col2 = st.columns(2) with col1: # Pie Chart fig_pie = create_pie_chart(df) st.plotly_chart(fig_pie, use_container_width=True) with col2: # Bar Chart untuk top words secara keseluruhan all_texts = df['processed_text'].dropna().tolist() top_words = get_top_words(all_texts, 15) if top_words: words, counts = zip(*top_words) fig_bar = go.Figure([go.Bar( x=list(counts), y=list(words), orientation='h', marker_color='lightblue' )]) fig_bar.update_layout( title="15 Kata Teratas (Keseluruhan)", xaxis_title="Frekuensi", yaxis_title="Kata", height=400 ) st.plotly_chart(fig_bar, use_container_width=True) # Word Clouds st.header("โ˜๏ธ Word Clouds") col1, col2 = st.columns(2) with col1: st.subheader("๐Ÿ˜Š Sentimen Positif") positive_texts = df[df['sentiment'] == 1]['processed_text'].dropna().tolist() if positive_texts: fig_wc_pos = create_wordcloud(positive_texts, "Word Cloud Sentimen Positif", 'Greens') if fig_wc_pos: st.pyplot(fig_wc_pos, clear_figure=True) else: st.info("Tidak ada data sentimen positif") with col2: st.subheader("๐Ÿ˜ž Sentimen Negatif") negative_texts = df[df['sentiment'] == 0]['processed_text'].dropna().tolist() if negative_texts: fig_wc_neg = create_wordcloud(negative_texts, "Word Cloud Sentimen Negatif", 'Reds') if fig_wc_neg: st.pyplot(fig_wc_neg, clear_figure=True) else: st.info("Tidak ada data sentimen negatif") # Top words by sentiment st.header("๐Ÿ“ˆ Kata-kata Teratas per Sentimen") col1, col2 = st.columns(2) with col1: st.subheader("๐Ÿ˜Š Top 15 Kata Sentimen Positif") if positive_texts: top_words_pos = get_top_words(positive_texts, 15) if top_words_pos: words_pos, counts_pos = zip(*top_words_pos) fig_pos = go.Figure([go.Bar( x=list(counts_pos), y=list(words_pos), orientation='h', marker_color='lightgreen' )]) fig_pos.update_layout( xaxis_title="Frekuensi", yaxis_title="Kata", height=400 ) st.plotly_chart(fig_pos, use_container_width=True) with col2: st.subheader("๐Ÿ˜ž Top 15 Kata Sentimen Negatif") if negative_texts: top_words_neg = get_top_words(negative_texts, 15) if top_words_neg: words_neg, counts_neg = zip(*top_words_neg) fig_neg = go.Figure([go.Bar( x=list(counts_neg), y=list(words_neg), orientation='h', marker_color='lightcoral' )]) fig_neg.update_layout( xaxis_title="Frekuensi", yaxis_title="Kata", height=400 ) st.plotly_chart(fig_neg, use_container_width=True) # Dataset Display Section st.markdown("---") st.header("๐Ÿ“‹ Dataset yang Digunakan untuk Training Model") try: # Load dataset dataset_path = os.path.join(BASE_DIR, 'data_mbg_labelled.csv') df_dataset = pd.read_csv(dataset_path) # Pilih kolom yang dibutuhkan if all(col in df_dataset.columns for col in ['created_at', 'text', 'sentiment']): display_df = df_dataset[['created_at', 'text', 'sentiment']].copy() # Map sentiment ke label yang lebih readable display_df['sentiment'] = display_df['sentiment'].map({0: 'Negatif', 1: 'Positif'}) # Rename columns untuk display yang lebih baik display_df = display_df.rename(columns={ 'created_at': 'Tanggal Dibuat', 'text': 'Teks', 'sentiment': 'Sentimen' }) # Tampilkan tabel tanpa width constraint agar text bisa wrap st.dataframe( display_df, use_container_width=True, height=400 ) else: st.warning("โš ๏ธ Kolom yang diperlukan (created_at, text, sentiment) tidak ditemukan dalam dataset.") except FileNotFoundError: st.error(f"โŒ File dataset '{dataset_path}' tidak ditemukan.") except Exception as e: st.error(f"โŒ Error saat memuat dataset: {str(e)}") # Model Performance Section st.markdown("---") st.header("๐Ÿ“Š Performa Model (5-Fold Cross Validation)") try: import json metrics_path = os.path.join(BASE_DIR, 'model_metrics.json') with open(metrics_path, 'r') as f: metrics = json.load(f) avg = metrics['average'] k_fold = metrics.get('k_fold', 5) best_C = metrics.get('best_C', '-') best_gamma = metrics.get('best_gamma', '-') # โ”€โ”€ Info model โ”€โ”€ st.info( f"โ„น๏ธ Model SVM (RBF kernel) dievaluasi dengan **{k_fold}-Fold Stratified Cross Validation**. " f"Parameter terbaik dari Grid Search: **C={best_C}**, **gamma={best_gamma}**." ) # โ”€โ”€ Kartu metrik rata-rata โ”€โ”€ st.subheader("๐Ÿ“ˆ Rata-Rata Metrik Semua Fold") col1, col2, col3, col4, col5 = st.columns(5) with col1: st.metric("๐ŸŽฏ Accuracy (Avg)", f"{avg['accuracy']:.2%}") with col2: st.metric("๐Ÿ“ˆ Precision (Avg)", f"{avg['precision']:.2%}") with col3: st.metric("๐Ÿ“Š Recall (Avg)", f"{avg['recall']:.2%}") with col4: st.metric("โš–๏ธ F1-Score (Avg)", f"{avg['f1_score']:.2%}") with col5: st.metric("๐Ÿ“‰ Std Dev Accuracy", f"{avg['std_accuracy']:.2%}") st.markdown("---") # โ”€โ”€ Tabel performa per fold โ”€โ”€ st.subheader(f"๐Ÿ“‹ Hasil Per Fold ({k_fold}-Fold)") fold_rows = [] for r in metrics['kfold_results']: fold_rows.append({ 'Fold': f"Fold {r['fold']}", 'Accuracy': f"{r['accuracy']:.4f} ({r['accuracy']:.2%})", 'Precision': f"{r['precision']:.4f} ({r['precision']:.2%})", 'Recall': f"{r['recall']:.4f} ({r['recall']:.2%})", 'F1-Score': f"{r['f1_score']:.4f} ({r['f1_score']:.2%})", 'TP': r['TP'], 'TN': r['TN'], 'FP': r['FP'], 'FN': r['FN'], }) fold_df = pd.DataFrame(fold_rows) st.dataframe(fold_df, use_container_width=True, hide_index=True) # โ”€โ”€ Fold terbaik โ”€โ”€ best_fold_num = metrics.get('best_fold', '-') st.success(f"โœ… **Fold terbaik berdasarkan Accuracy: Fold {best_fold_num}**") st.markdown("---") # โ”€โ”€ Confusion Matrix Gabungan & Classification Report โ”€โ”€ col_left, col_right = st.columns(2) # Hitung CM gabungan dari semua fold agg_tp = sum(r['TP'] for r in metrics['kfold_results']) agg_tn = sum(r['TN'] for r in metrics['kfold_results']) agg_fp = sum(r['FP'] for r in metrics['kfold_results']) agg_fn = sum(r['FN'] for r in metrics['kfold_results']) agg_total = agg_tp + agg_tn + agg_fp + agg_fn agg_sup_neg = agg_tn + agg_fp agg_sup_pos = agg_tp + agg_fn agg_prec_neg = agg_tn / (agg_tn + agg_fn) if (agg_tn + agg_fn) > 0 else 0 agg_prec_pos = agg_tp / (agg_tp + agg_fp) if (agg_tp + agg_fp) > 0 else 0 agg_rec_neg = agg_tn / (agg_tn + agg_fp) if (agg_tn + agg_fp) > 0 else 0 agg_rec_pos = agg_tp / (agg_tp + agg_fn) if (agg_tp + agg_fn) > 0 else 0 agg_f1_neg = (2 * agg_prec_neg * agg_rec_neg / (agg_prec_neg + agg_rec_neg) if (agg_prec_neg + agg_rec_neg) > 0 else 0) agg_f1_pos = (2 * agg_prec_pos * agg_rec_pos / (agg_prec_pos + agg_rec_pos) if (agg_prec_pos + agg_rec_pos) > 0 else 0) agg_prec_w = (agg_prec_neg * agg_sup_neg + agg_prec_pos * agg_sup_pos) / agg_total agg_rec_w = (agg_rec_neg * agg_sup_neg + agg_rec_pos * agg_sup_pos) / agg_total agg_f1_w = (agg_f1_neg * agg_sup_neg + agg_f1_pos * agg_sup_pos) / agg_total agg_acc = (agg_tp + agg_tn) / agg_total with col_left: st.subheader(f"๐Ÿ”ข Confusion Matrix Gabungan ({k_fold} Fold)") st.caption("Merupakan jumlah TP, TN, FP, FN dari seluruh fold.") import plotly.figure_factory as ff z = [[agg_tn, agg_fp], [agg_fn, agg_tp]] x_labs = ['Prediksi Negatif', 'Prediksi Positif'] y_labs = ['Aktual Negatif', 'Aktual Positif'] z_text = [ [f"TN: {agg_tn}", f"FP: {agg_fp}"], [f"FN: {agg_fn}", f"TP: {agg_tp}"], ] fig_cm = ff.create_annotated_heatmap( z, x=x_labs, y=y_labs, annotation_text=z_text, colorscale='Blues', showscale=True ) fig_cm.update_layout( title=f"Confusion Matrix Gabungan ({k_fold}-Fold)", xaxis_title="Prediksi", yaxis_title="Aktual", height=400 ) st.plotly_chart(fig_cm, use_container_width=True) st.markdown(f""" **Keterangan nilai:** - **TN** = {agg_tn}   **FP** = {agg_fp} - **FN** = {agg_fn}   **TP** = {agg_tp} - **Total data** = {agg_total} - **Accuracy (CM Gabungan)** = (TP+TN)/Total = ({agg_tp}+{agg_tn})/{agg_total} = **{agg_acc:.2%}** **Penjelasan:** - **TN (True Negative)**: Prediksi Negatif, Aktual Negatif โœ… - **TP (True Positive)**: Prediksi Positif, Aktual Positif โœ… - **FP (False Positive)**: Prediksi Positif, Aktual Negatif โŒ (Error Tipe I) - **FN (False Negative)**: Prediksi Negatif, Aktual Positif โŒ (Error Tipe II) """) with col_right: st.subheader("๐Ÿ“‹ Classification Report (CM Gabungan)") report_data = [ { 'Kelas' : 'Negatif', 'Precision': f"{agg_prec_neg:.4f} ({agg_prec_neg:.2%})", 'Recall' : f"{agg_rec_neg:.4f} ({agg_rec_neg:.2%})", 'F1-Score' : f"{agg_f1_neg:.4f} ({agg_f1_neg:.2%})", 'Support' : agg_sup_neg, }, { 'Kelas' : 'Positif', 'Precision': f"{agg_prec_pos:.4f} ({agg_prec_pos:.2%})", 'Recall' : f"{agg_rec_pos:.4f} ({agg_rec_pos:.2%})", 'F1-Score' : f"{agg_f1_pos:.4f} ({agg_f1_pos:.2%})", 'Support' : agg_sup_pos, }, { 'Kelas' : 'Weighted Avg', 'Precision': f"{agg_prec_w:.4f} ({agg_prec_w:.2%})", 'Recall' : f"{agg_rec_w:.4f} ({agg_rec_w:.2%})", 'F1-Score' : f"{agg_f1_w:.4f} ({agg_f1_w:.2%})", 'Support' : agg_total, }, ] report_df = pd.DataFrame(report_data) st.dataframe(report_df, use_container_width=True, hide_index=True) st.markdown(""" **Penjelasan Metrik:** - **Precision**: Dari semua prediksi kelas X, berapa persen yang benar? - **Recall**: Dari semua data aktual kelas X, berapa persen yang berhasil diprediksi? - **F1-Score**: Harmonic mean dari Precision dan Recall - **Weighted Avg**: Rata-rata tertimbang berdasarkan support (jumlah data aktual per kelas) """) # Bar chart perbandingan metrik per kelas fig_bar = go.Figure() for metric_label, neg_val, pos_val in [ ('Precision', agg_prec_neg, agg_prec_pos), ('Recall', agg_rec_neg, agg_rec_pos), ('F1-Score', agg_f1_neg, agg_f1_pos), ]: fig_bar.add_trace(go.Bar( name=metric_label, x=['Negatif', 'Positif'], y=[neg_val, pos_val], text=[f"{neg_val:.2%}", f"{pos_val:.2%}"], textposition='auto' )) # Zoom y-axis ke sekitar nilai aktual agar perbedaan bar terlihat all_metric_vals = [agg_prec_neg, agg_prec_pos, agg_rec_neg, agg_rec_pos, agg_f1_neg, agg_f1_pos] bar_ymin = max(0, min(all_metric_vals) - 0.05) # 5pp di bawah nilai terkecil bar_ymax = min(1, max(all_metric_vals) + 0.03) # 3pp di atas nilai terbesar fig_bar.update_layout( title="Perbandingan Metrik per Kelas (CM Gabungan)", xaxis_title="Kelas", yaxis_title="Score", barmode='group', height=350, yaxis=dict(range=[bar_ymin, bar_ymax]) ) st.plotly_chart(fig_bar, use_container_width=True) st.markdown("---") # โ”€โ”€ Line chart akurasi per fold โ”€โ”€ st.subheader("๐Ÿ“‰ Akurasi per Fold") fold_nums = [r['fold'] for r in metrics['kfold_results']] fold_accs = [r['accuracy'] for r in metrics['kfold_results']] fig_line = go.Figure() # Zoom y-axis ke sekitar nilai aktual; beri ruang atas agar label tidak terpotong line_ymin = max(0, min(fold_accs) - 0.05) line_ymax = min(1, max(fold_accs) + 0.06) # ruang ekstra untuk label teks fig_line.add_trace(go.Scatter( x=[f"Fold {n}" for n in fold_nums], y=fold_accs, mode='lines+markers+text', text=[f"{v:.2%}" for v in fold_accs], textposition='bottom center', # di bawah titik agar tidak terpotong batas atas marker=dict(size=10, color='steelblue'), line=dict(width=2), name='Accuracy' )) fig_line.add_hline( y=avg['accuracy'], line_dash='dash', line_color='red', annotation_text=f"Rata-rata: {avg['accuracy']:.2%}", annotation_position='bottom right' ) fig_line.update_layout( title="Akurasi Setiap Fold", xaxis_title="Fold", yaxis_title="Accuracy", height=400, yaxis=dict(range=[line_ymin, line_ymax]), margin=dict(t=50, b=60) # beri ruang atas & bawah ) st.plotly_chart(fig_line, use_container_width=True) except FileNotFoundError: st.warning("โš ๏ธ File model_metrics.json tidak ditemukan. Jalankan sentiment_model.py terlebih dahulu.") except Exception as e: st.error(f"โŒ Error saat memuat metrik model: {str(e)}") def show_prediction_page(analyzer): """Halaman prediksi sentimen""" st.header("๐Ÿ”ฎ Prediksi Sentimen") st.write("Masukkan teks bahasa Indonesia untuk memprediksi sentimennya:") # # Contoh teks # selected_example = show_examples() # Input teks default_text = "MBG bermanfaat bagi anak-anak dan orangtua" user_input = st.text_area( "Teks untuk dianalisis:", height=100, value=default_text, placeholder="Contoh: Pelayanannya sangat memuaskan dan staffnya ramah sekali!" ) if st.button("๐Ÿš€ Analisis Sentimen", type="primary"): # Validasi input kosong if not user_input.strip(): st.warning("โš ๏ธ Silakan masukkan teks terlebih dahulu.") else: # Cek apakah input hanya simbol/tanda baca cleaned_for_check = re.sub(r'[^\w\s]', '', user_input).strip() if not cleaned_for_check: st.error("โš ๏ธ Input hanya berisi simbol atau tanda baca. Tidak dapat melakukan analisis sentimen karena tidak ada teks bermakna.") else: with st.spinner("Menganalisis sentimen..."): # Prediksi result = analyzer.predict_sentiment(user_input) # Cek jika hasil tidak dapat ditentukan if result['sentiment'] == 'Tidak dapat menentukan': st.warning("โš ๏ธ Tidak dapat menentukan sentimen. Teks mungkin terlalu pendek atau tidak mengandung kata bermakna setelah preprocessing.") # Tampilkan hasil col1, col2 = st.columns(2) with col1: # Hasil prediksi if result['sentiment'] == 'Tidak dapat menentukan': sentiment_color = "gray" sentiment_icon = "๐Ÿ˜" else: sentiment_color = "green" if result['sentiment'] == "Positif" else "red" sentiment_icon = "๐Ÿ˜Š" if result['sentiment'] == "Positif" else "๐Ÿ˜ž" st.markdown(f"""

{sentiment_icon} {result['sentiment']}

Confidence: {result['confidence']:.1%}

""", unsafe_allow_html=True) with col2: # Probability chart fig = go.Figure([go.Bar( x=['๐Ÿ˜ž Negatif', '๐Ÿ˜Š Positif'], y=[result['probability_negative'], result['probability_positive']], marker_color=['lightcoral', 'lightgreen'], text=[f"{result['probability_negative']:.1%}", f"{result['probability_positive']:.1%}"], textposition='auto' )]) fig.update_layout( title="Probabilitas Sentimen", yaxis_title="Probabilitas", height=300, showlegend=False ) st.plotly_chart(fig, use_container_width=True) # Preprocessing steps st.markdown("---") show_preprocessing_steps(analyzer, user_input) def show_preprocessing_demo(analyzer): """Halaman demo preprocessing""" st.header("๐Ÿ“‹ Demo Preprocessing") st.write("Lihat bagaimana teks bahasa Indonesia diproses melalui setiap tahap preprocessing:") # Input teks demo_text = st.text_area( "Masukkan teks untuk melihat proses preprocessing:", height=100, placeholder="Contoh: MBG sangat bermanfaat bagi anak-anak dan orangtua!๐Ÿ˜Š", value="MBG sangat bermanfaat bagi anak-anak dan orangtua!๐Ÿ˜Š" ) if st.button("๐Ÿ” Jalankan Preprocessing", type="primary"): # Validasi input kosong if not demo_text.strip(): st.warning("โš ๏ธ Silakan masukkan teks terlebih dahulu.") else: # Cek apakah input hanya simbol/tanda baca cleaned_for_check = re.sub(r'[^\w\s]', '', demo_text).strip() if not cleaned_for_check: st.warning("โš ๏ธ Input hanya berisi simbol atau tanda baca. Sistem akan tetap memproses tetapi hasil mungkin kosong setelah pembersihan.") show_preprocessing_steps(analyzer, demo_text) # Penjelasan setiap tahap st.subheader("๐Ÿ“š Penjelasan Tahapan:") explanations = { "๐Ÿ”ค Teks Asli": "Teks input yang belum diproses", "๐Ÿงน Pembersihan": "Menghapus URL, mention, hashtag, angka, emoji, dan karakter khusus", "๐Ÿ“ Case Folding": "Mengubah semua huruf menjadi huruf kecil untuk konsistensi", "โœ‚๏ธ Tokenisasi": "Memecah teks menjadi token/kata individual", "๐Ÿ”„ Normalisasi": "Mengubah singkatan dan slang menjadi bentuk baku (contoh: 'bgt' โ†’ 'sangat')", "๐Ÿšซ Hapus Stopwords": "Menghapus kata-kata umum bahasa Indonesia yang tidak bermakna", "๐ŸŒฑ Stemming": "Mengubah kata ke bentuk dasarnya menggunakan algoritma Sastrawi", "โœ… Hasil Akhir": "Teks yang sudah siap untuk dianalisis oleh model machine learning" } for stage, explanation in explanations.items(): st.write(f"**{stage}**: {explanation}") # Tips untuk preprocessing st.subheader("๐Ÿ’ก Tips Preprocessing Bahasa Indonesia:") st.info(""" - **Normalisasi** sangat penting untuk bahasa Indonesia karena banyaknya singkatan dan slang - **Stemming** menggunakan algoritma Sastrawi yang dirancang khusus untuk bahasa Indonesia - **Stopwords** disesuaikan dengan kata-kata umum bahasa Indonesia - **Cleaning** menghapus noise seperti emoji dan karakter khusus yang sering muncul di media sosial """) else: st.info("โ„น๏ธ Masukkan teks untuk melihat tahapan preprocessing.") def process_csv_predictions(analyzer, df_csv, text_column, confidence_threshold=0.5): """Memproses prediksi sentimen untuk CSV dengan filtering confidence""" results = [] for idx, row in df_csv.iterrows(): text = str(row[text_column]) if pd.isna(text) or not text.strip(): continue # Prediksi sentimen prediction = analyzer.predict_sentiment(text) # Simpan hasil results.append({ 'text': text, 'sentiment': prediction['sentiment'], 'confidence': prediction['confidence'], 'probability_negative': prediction['probability_negative'], 'probability_positive': prediction['probability_positive'] }) # Buat DataFrame hasil df_results = pd.DataFrame(results) # Filter berdasarkan confidence threshold df_confident = df_results[df_results['confidence'] >= confidence_threshold].copy() df_neutral = df_results[df_results['confidence'] < confidence_threshold].copy() return df_confident, df_neutral def create_csv_pie_chart(df_results): """Membuat pie chart distribusi sentimen untuk hasil CSV""" if df_results.empty: return None sentiment_counts = df_results['sentiment'].value_counts() labels = [] values = [] colors = [] if 'Negatif' in sentiment_counts.index: labels.append('Negatif') values.append(sentiment_counts['Negatif']) colors.append('#ff7f7f') if 'Positif' in sentiment_counts.index: labels.append('Positif') values.append(sentiment_counts['Positif']) colors.append('#7fbf7f') if not labels: return None fig = go.Figure(data=[go.Pie( labels=labels, values=values, hole=0.3, marker=dict(colors=colors), texttemplate='%{label}
%{value}
(%{percent})' )]) fig.update_traces( textposition='inside', textfont_size=12 ) fig.update_layout( title={ 'text': "Distribusi Sentimen CSV", 'x': 0.5, 'xanchor': 'center', 'font': {'size': 16} }, font=dict(size=12), showlegend=True, height=400 ) return fig def show_csv_analysis_page(analyzer): """Halaman analisis CSV""" st.header("๐Ÿ“ Analisis Sentimen dari CSV") st.write(""" Upload file CSV untuk melakukan prediksi sentimen secara batch. File CSV harus memiliki kolom **'text'** atau **'full_text'** yang berisi teks untuk dianalisis. """) # Upload CSV uploaded_file = st.file_uploader( "Upload file CSV", type=['csv'], help="File CSV harus memiliki kolom 'text' atau 'full_text'" ) if uploaded_file is not None: try: # Baca CSV df_csv = pd.read_csv(uploaded_file) # Validasi CSV tidak kosong if len(df_csv) == 0: st.warning("โš ๏ธ File CSV tidak memiliki data. Silakan upload file dengan data yang valid.") return st.success(f"โœ… File berhasil diupload! Total baris: {len(df_csv):,}") # Deteksi kolom tanggal dan analisis temporal date_column = detect_date_column(df_csv) if date_column: min_date, max_date = get_date_range(df_csv, date_column) temporal_stats = get_temporal_statistics(df_csv, date_column) if min_date and max_date and temporal_stats: # Tampilkan informasi rentang waktu st.markdown("---") st.subheader("๐Ÿ“… Informasi Temporal Data") col1, col2 = st.columns([2, 1]) with col1: st.info(f"**Rentang Waktu:** {min_date.strftime('%d %B %Y, %H:%M:%S')} sampai {max_date.strftime('%d %B %Y, %H:%M:%S')}") with col2: st.metric("๐Ÿ“Š Total Data Bertanggal", f"{temporal_stats['total_dates']:,}") st.caption(f"Kolom tanggal yang terdeteksi: **{date_column}**") # Tampilkan chart temporal fig_yearly, fig_monthly = create_temporal_charts(temporal_stats) if fig_yearly and fig_monthly: col1, col2 = st.columns(2) with col1: st.plotly_chart(fig_yearly, use_container_width=True) with col2: st.plotly_chart(fig_monthly, use_container_width=True) st.markdown("---") else: st.warning(f"โš ๏ธ Kolom tanggal terdeteksi ({date_column}) tetapi tidak dapat mem-parsing tanggal") else: st.info("โ„น๏ธ Tidak ada kolom tanggal yang terdeteksi dalam CSV") # Validasi kolom text_column = None if 'text' in df_csv.columns: text_column = 'text' elif 'full_text' in df_csv.columns: text_column = 'full_text' else: st.error("โŒ File CSV harus memiliki kolom 'text' atau 'full_text'") st.info(f"Kolom yang tersedia: {', '.join(df_csv.columns)}") return st.info(f"๐Ÿ“ Menggunakan kolom teks: **{text_column}**") # Tampilkan preview with st.expander("๐Ÿ‘€ Preview Data CSV"): st.dataframe(df_csv.head(10), use_container_width=True) # Confidence threshold confidence_threshold = st.slider( "Confidence Threshold ", min_value=0.0, max_value=1.0, value=0.5, step=0.05, help="Prediksi dengan confidence di bawah threshold ini tidak dimasukkan dalam analisis" ) # Tombol proses if st.button("๐Ÿš€ Proses Prediksi Sentimen", type="primary"): with st.spinner("Memproses prediksi sentimen..."): # Proses prediksi df_confident, df_neutral = process_csv_predictions( analyzer, df_csv, text_column, confidence_threshold ) # Simpan ke session state st.session_state['csv_results'] = df_confident st.session_state['csv_neutral'] = df_neutral st.session_state['csv_processed'] = True st.success("โœ… Prediksi selesai!") st.rerun() except Exception as e: st.error(f"โŒ Error membaca file CSV: {str(e)}") return # Tampilkan hasil jika sudah diproses if st.session_state.get('csv_processed', False): df_confident = st.session_state.get('csv_results') df_neutral = st.session_state.get('csv_neutral') if df_confident is not None: st.markdown("---") st.header("๐Ÿ“Š Hasil Analisis") # Metrics col1, col2, col3, col4 = st.columns(4) total_data = len(df_confident) + len(df_neutral) positive_count = len(df_confident[df_confident['sentiment'] == 'Positif']) negative_count = len(df_confident[df_confident['sentiment'] == 'Negatif']) neutral_count = len(df_neutral) with col1: st.metric("๐Ÿ“„ Total Data", f"{total_data:,}") with col2: st.metric("๐Ÿ˜Š Sentimen Positif", f"{positive_count:,}") with col3: st.metric("๐Ÿ˜ž Sentimen Negatif", f"{negative_count:,}") with col4: st.metric("๐Ÿ˜ Confidence Level < Threshold", f"{neutral_count:,}") # Visualisasi if not df_confident.empty: st.markdown("---") st.subheader("๐Ÿ“ˆ Visualisasi Sentimen") col1, col2 = st.columns(2) with col1: # Pie Chart fig_pie = create_csv_pie_chart(df_confident) if fig_pie: st.plotly_chart(fig_pie, use_container_width=True) with col2: # Bar Chart distribusi confidence fig_conf = go.Figure([go.Histogram( x=df_confident['confidence'], nbinsx=20, marker_color='lightblue' )]) fig_conf.update_layout( title="Distribusi Confidence Score", xaxis_title="Confidence", yaxis_title="Frekuensi", height=400 ) st.plotly_chart(fig_conf, use_container_width=True) # Word Clouds st.markdown("---") st.subheader("โ˜๏ธ Word Clouds") col1, col2 = st.columns(2) with col1: st.markdown("**๐Ÿ˜Š Sentimen Positif**") positive_texts = df_confident[df_confident['sentiment'] == 'Positif']['text'].tolist() if positive_texts: # Preprocess texts untuk wordcloud processed_positive = [analyzer.preprocess_text(text) for text in positive_texts] fig_wc_pos = create_wordcloud(processed_positive, "Word Cloud Sentimen Positif", 'Greens') if fig_wc_pos: st.pyplot(fig_wc_pos, clear_figure=True) else: st.info("Tidak ada data sentimen positif") with col2: st.markdown("**๐Ÿ˜ž Sentimen Negatif**") negative_texts = df_confident[df_confident['sentiment'] == 'Negatif']['text'].tolist() if negative_texts: # Preprocess texts untuk wordcloud processed_negative = [analyzer.preprocess_text(text) for text in negative_texts] fig_wc_neg = create_wordcloud(processed_negative, "Word Cloud Sentimen Negatif", 'Reds') if fig_wc_neg: st.pyplot(fig_wc_neg, clear_figure=True) else: st.info("Tidak ada data sentimen negatif") # Top words by sentiment st.markdown("---") st.subheader("๐Ÿ“ˆ Kata-kata Teratas per Sentimen") col1, col2 = st.columns(2) with col1: st.markdown("**๐Ÿ˜Š Top 15 Kata Sentimen Positif**") if positive_texts: processed_positive = [analyzer.preprocess_text(text) for text in positive_texts] top_words_pos = get_top_words(processed_positive, 15) if top_words_pos: words_pos, counts_pos = zip(*top_words_pos) fig_pos = go.Figure([go.Bar( x=list(counts_pos), y=list(words_pos), orientation='h', marker_color='lightgreen' )]) fig_pos.update_layout( xaxis_title="Frekuensi", yaxis_title="Kata", height=400 ) st.plotly_chart(fig_pos, use_container_width=True) with col2: st.markdown("**๐Ÿ˜ž Top 15 Kata Sentimen Negatif**") if negative_texts: processed_negative = [analyzer.preprocess_text(text) for text in negative_texts] top_words_neg = get_top_words(processed_negative, 15) if top_words_neg: words_neg, counts_neg = zip(*top_words_neg) fig_neg = go.Figure([go.Bar( x=list(counts_neg), y=list(words_neg), orientation='h', marker_color='lightcoral' )]) fig_neg.update_layout( xaxis_title="Frekuensi", yaxis_title="Kata", height=400 ) st.plotly_chart(fig_neg, use_container_width=True) # Tabel hasil st.markdown("---") st.subheader("๐Ÿ“‹ Tabel Hasil Prediksi") if not df_confident.empty: # Format tabel untuk ditampilkan st.subheader("๐Ÿ“Š Hasil Prediksi (Confident)") st.write(f"Total data dengan confidence โ‰ฅ {confidence_threshold}: **{len(df_confident):,}**") # Prepare display dataframe df_display = df_confident[['text', 'sentiment', 'confidence']].copy() df_display.columns = ['Teks', 'Sentimen', 'Confidence'] # Tampilkan tabel tanpa width constraint agar text bisa wrap st.dataframe( df_display, use_container_width=True, height=400 ) # Download hasil csv_download = df_confident.to_csv(index=False).encode('utf-8') st.download_button( label="๐Ÿ“ฅ Download Hasil Prediksi (CSV)", data=csv_download, file_name="hasil_prediksi_sentimen.csv", mime="text/csv" ) # ===== BAGIAN KESIMPULAN/SUMMARY ===== st.markdown("---") st.subheader("๐Ÿ“ Kesimpulan Analisis Sentimen") # Hitung statistik untuk kesimpulan total_analyzed = len(df_confident) + len(df_neutral) pct_positive = (positive_count / total_analyzed * 100) if total_analyzed > 0 else 0 pct_negative = (negative_count / total_analyzed * 100) if total_analyzed > 0 else 0 pct_neutral = (neutral_count / total_analyzed * 100) if total_analyzed > 0 else 0 avg_confidence = df_confident['confidence'].mean() if not df_confident.empty else 0 # Tentukan sentimen dominan if positive_count > negative_count: dominant_sentiment = "Positif" dominant_icon = "๐Ÿ˜Š" dominant_color = "green" elif negative_count > positive_count: dominant_sentiment = "Negatif" dominant_icon = "๐Ÿ˜ž" dominant_color = "red" else: dominant_sentiment = "Seimbang" dominant_icon = "๐Ÿ˜" dominant_color = "gray" # Build interpretasi list interpretasi_list = [] # Interpretasi berdasarkan distribusi sentimen if pct_positive > 70: interpretasi_list.append("Data menunjukkan **sentimen sangat positif** dengan lebih dari 70% respons positif.") interpretasi_list.append("Ini mengindikasikan tingkat kepuasan atau penerimaan yang sangat baik.") elif pct_positive > 50: interpretasi_list.append("Data menunjukkan **sentimen cenderung positif** dengan mayoritas respons positif.") interpretasi_list.append("Secara umum, terdapat penerimaan yang baik meskipun masih ada ruang untuk perbaikan.") elif pct_negative > 70: interpretasi_list.append("Data menunjukkan **sentimen sangat negatif** dengan lebih dari 70% respons negatif.") interpretasi_list.append("Ini mengindikasikan adanya masalah serius yang perlu segera ditangani.") elif pct_negative > 50: interpretasi_list.append("Data menunjukkan **sentimen cenderung negatif** dengan mayoritas respons negatif.") interpretasi_list.append("Diperlukan perhatian khusus untuk meningkatkan kualitas atau layanan.") else: interpretasi_list.append("Data menunjukkan **sentimen yang seimbang** antara positif dan negatif.") interpretasi_list.append("Terdapat opini yang beragam, menunjukkan pengalaman yang bervariasi.") # Interpretasi berdasarkan confidence if avg_confidence > 0.8: interpretasi_list.append(f"Confidence score yang tinggi ({avg_confidence:.1%}) menunjukkan model sangat yakin dengan prediksinya.") interpretasi_list.append("Hasil analisis dapat diandalkan untuk pengambilan keputusan.") elif avg_confidence > 0.6: interpretasi_list.append(f"Confidence score yang cukup baik ({avg_confidence:.1%}) menunjukkan prediksi yang dapat diandalkan.") interpretasi_list.append("Sebagian besar hasil analisis dapat dipercaya.") else: interpretasi_list.append(f"Confidence score yang moderat ({avg_confidence:.1%}) menunjukkan beberapa prediksi mungkin ambigu.") interpretasi_list.append("Disarankan untuk melakukan validasi manual pada data dengan confidence rendah.") # Interpretasi data netral if pct_neutral > 30: interpretasi_list.append(f"Terdapat **{pct_neutral:.1f}%** data dengan confidence level < threshold.") interpretasi_list.append("Data dengan confidence level < threshold yang tinggi mungkin mengindikasikan teks yang ambigu atau memerlukan konteks tambahan.") # Build rekomendasi list rekomendasi_list = [] if pct_negative > 40: rekomendasi_list.append("Identifikasi tema atau topik utama dari sentimen negatif untuk perbaikan.") rekomendasi_list.append("Lakukan analisis lebih lanjut pada kata-kata yang sering muncul di sentimen negatif.") if pct_neutral > 20: rekomendasi_list.append("Review data dengan confidence level < threshold secara manual untuk memahami konteks yang lebih baik.") rekomendasi_list.append("Pertimbangkan untuk meningkatkan threshold confidence jika diperlukan hasil yang lebih pasti.") rekomendasi_list.append("Gunakan word cloud dan top words untuk memahami topik yang paling sering dibahas.") rekomendasi_list.append("Pantau tren sentimen secara berkala untuk melihat perubahan dari waktu ke waktu.") # Tampilkan kesimpulan dalam card dengan st.container with st.container(): st.markdown(f"""

{dominant_icon} Sentimen Dominan: {dominant_sentiment}

""", unsafe_allow_html=True) st.markdown("**Ringkasan Hasil Analisis:**") st.markdown(f"- Dari **{total_analyzed:,}** data yang dianalisis, terdapat:") st.markdown(f" - **{positive_count:,}** sentimen positif ({pct_positive:.1f}%)") st.markdown(f" - **{negative_count:,}** sentimen negatif ({pct_negative:.1f}%)") st.markdown(f" - **{neutral_count:,}** data dengan confidence level < threshold ({pct_neutral:.1f}%)") st.markdown(f"- Rata-rata confidence score: **{avg_confidence:.1%}**") st.markdown("**Interpretasi:**") for item in interpretasi_list: st.markdown(f"- {item}") st.markdown("**Rekomendasi:**") for item in rekomendasi_list: st.markdown(f"- {item}") else: st.warning("โš ๏ธ Tidak ada data dengan confidence di atas threshold") # Tabel data netral if not df_neutral.empty: st.markdown("---") st.subheader(f"๐Ÿ˜ Data (Confidence < threshold) - Total: {len(df_neutral):,}") with st.expander(f"๐Ÿ“‹ Data dengan Confidence Level < Threshold ({confidence_threshold})"): st.write(f"Total data dengan confidence level < threshold: **{len(df_neutral):,}**") st.caption("Data ini memiliki confidence di bawah threshold dan dianggap tidak pasti") # Prepare display dataframe df_neutral_display = df_neutral[['text', 'sentiment', 'confidence']].copy() df_neutral_display.columns = ['Teks', 'Sentimen', 'Confidence'] # Tampilkan tabel tanpa width constraint agar text bisa wrap st.dataframe( df_neutral_display, use_container_width=True, height=300 ) # Download data netral csv_neutral_download = df_neutral.to_csv(index=False).encode('utf-8') st.download_button( label="๐Ÿ“ฅ Download Data dengan Confidence Score dibawah Threshold (CSV)", data=csv_neutral_download, file_name="data_confidence_level_dibawah_threshold.csv", mime="text/csv" ) if __name__ == "__main__": main()