analisis-sentimen-program-m.../dashboard.py

1502 lines
60 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 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("""
<style>
.main-header {
font-size: 3rem;
color: #1f77b4;
text-align: center;
margin-bottom: 2rem;
}
.metric-card {
background-color: #f0f2f6;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
}
.stAlert > div {
padding: 1rem;
}
</style>
""", 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='<b>%{label}</b><br>%{value}<br>(%{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='<b>Tahun %{x}</b><br>Jumlah: %{y}<extra></extra>'
)])
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='<b>%{x}</b><br>Jumlah: %{y}<extra></extra>'
)])
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"""
<div style="overflow-x: auto; max-width: 100%;">
{html_table}
</div>
""",
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('<h1 class="main-header">📊 Analisis Sentimen Program MBG Bahasa Indonesia</h1>', 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} &emsp; **FP** = {agg_fp}
- **FN** = {agg_fn} &emsp; **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"""
<div style="padding: 2rem; border: 3px solid {sentiment_color}; border-radius: 1rem; text-align: center; background-color: rgba({'128,128,128' if sentiment_color == 'gray' else '0,255,0' if sentiment_color == 'green' else '255,0,0'}, 0.1);">
<h2 style="color: {sentiment_color}; margin: 0;">{sentiment_icon} {result['sentiment']}</h2>
<h3 style="margin: 0.5rem 0;">Confidence: {result['confidence']:.1%}</h3>
</div>
""", 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='<b>%{label}</b><br>%{value}<br>(%{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"""
<div style="background-color: #f0f2f6; padding: 1.5rem; border-radius: 0.5rem; border-left: 5px solid {dominant_color};">
<h4 style="margin-top: 0; color: {dominant_color};">{dominant_icon} Sentimen Dominan: {dominant_sentiment}</h4>
</div>
""", 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()