320 lines
12 KiB
Python
320 lines
12 KiB
Python
import streamlit as st
|
|
import pandas as pd
|
|
import matplotlib.pyplot as plt
|
|
import plotly.express as px
|
|
from wordcloud import WordCloud
|
|
from collections import Counter
|
|
import os
|
|
|
|
# Import loader dari folder utils
|
|
from utils.loader import load_model
|
|
|
|
# 1. KONFIGURASI HALAMAN
|
|
st.set_page_config(
|
|
page_title="Dashboard Analisis Sentimen",
|
|
layout="wide"
|
|
)
|
|
|
|
# 2. CUSTOM CSS
|
|
def load_css():
|
|
css_path = os.path.join("assets", "style.css")
|
|
if os.path.exists(css_path):
|
|
with open(css_path) as f:
|
|
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
|
|
|
|
load_css()
|
|
|
|
# 3. LOAD DATA & MODEL
|
|
@st.cache_data
|
|
def load_data():
|
|
try:
|
|
return pd.read_csv("hasil_preprocessing_data.csv")
|
|
except:
|
|
return pd.DataFrame()
|
|
|
|
df = load_data()
|
|
model, tfidf = load_model()
|
|
|
|
# 4. DATA PENGUJIAN HASIL SENTIMEN
|
|
# Confusion Matrix Data: [[TN, FP], [FN, TP]]
|
|
cm_data = [[3, 17], [0, 98]]
|
|
tn, fp, fn, tp = 3, 17, 0, 98
|
|
|
|
# Data Classification Report Manual
|
|
report_dict = {
|
|
"Negatif": {"precision": 1.0000, "recall": 0.1500, "f1-score": 0.2609, "support": 20},
|
|
"Positif": {"precision": 0.8522, "recall": 1.0000, "f1-score": 0.9202, "support": 98},
|
|
"accuracy": 0.8559,
|
|
"macro avg": {"precision": 0.9242, "recall": 0.5513, "f1-score": 0.5520, "support": 118},
|
|
"weighted avg": {"precision": 0.8736, "recall": 0.8511, "f1-score": 0.7966, "support": 118}
|
|
}
|
|
|
|
akurasi_asli = report_dict["accuracy"]
|
|
|
|
if "sentimen" not in df.columns:
|
|
df["sentimen"] = df["validasi_label"].map({0: "Negatif", 1: "Positif"})
|
|
|
|
jumlah_sentimen = df["sentimen"].value_counts()
|
|
|
|
# 5. HEADER DASHBOARD
|
|
st.markdown('<div class="blue-header">📊 DASHBOARD ANALISIS SENTIMEN</div>', unsafe_allow_html=True)
|
|
|
|
st.markdown(
|
|
"<p style='text-align:center; color:#7f8c8d; margin-bottom:25px;'>"
|
|
"Analisis Sentimen Ulasan Produk Pembersih Wajah pada Platform Twitter/X"
|
|
"</p>",
|
|
unsafe_allow_html=True
|
|
)
|
|
|
|
st.info(f"Dashboard ini menampilkan hasil analisis menggunakan **Logistic Regression**. Akurasi Pengujian: **{akurasi_asli*100:.2f}%**")
|
|
|
|
st.markdown("---")
|
|
|
|
# 6. METRIC CARD
|
|
st.subheader("📌 Ringkasan Data Sentimen")
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
|
|
def metric_card(judul, nilai):
|
|
st.markdown(
|
|
f"""
|
|
<div class="metric-card">
|
|
<div class="metric-title">{judul}</div>
|
|
<div class="metric-value">{nilai}</div>
|
|
</div>
|
|
""",
|
|
unsafe_allow_html=True
|
|
)
|
|
|
|
with col1:
|
|
metric_card("📄 Total Tweet", len(df))
|
|
with col2:
|
|
metric_card("😊 Positif", jumlah_sentimen.get("Positif", 0))
|
|
with col3:
|
|
metric_card("😠 Negatif", jumlah_sentimen.get("Negatif", 0))
|
|
with col4:
|
|
metric_card("🎯 Akurasi Test", f"{akurasi_asli*100:.2f}%")
|
|
|
|
st.markdown("---")
|
|
|
|
# 7. DISTRIBUSI SENTIMEN (VERSI RINGKAS)
|
|
st.subheader("📊 Distribusi Sentimen Tweet")
|
|
|
|
# Menggunakan kolom agar chart tidak melebar ke seluruh layar
|
|
col_chart, col_text = st.columns([1, 1.5])
|
|
|
|
with col_chart:
|
|
# Ukuran figure diperkecil menjadi (4, 3) agar lebih proporsional
|
|
fig, ax = plt.subplots(figsize=(4, 3))
|
|
|
|
# Menggunakan warna default Matplotlib seperti sebelumnya
|
|
ax.pie(
|
|
jumlah_sentimen.values,
|
|
labels=jumlah_sentimen.index,
|
|
autopct="%1.1f%%",
|
|
startangle=90
|
|
)
|
|
|
|
# Menghilangkan margin berlebih di sekitar pie chart
|
|
plt.tight_layout()
|
|
st.pyplot(fig)
|
|
|
|
with col_text:
|
|
# Memberikan ruang kosong atau teks ringkasan agar tata letak seimbang
|
|
st.write(" ")
|
|
st.write(" ")
|
|
st.markdown(f"""
|
|
**Keterangan:**
|
|
|
|
Visualisasi ini menunjukkan perbandingan antara ulasan **Positif** dan **Negatif**.
|
|
Dominasi ulasan saat ini berada pada sentimen **{jumlah_sentimen.idxmax()}** dengan total **{len(df)}** data yang dianalisis.
|
|
""")
|
|
|
|
st.markdown("---")
|
|
|
|
# 8. WORDCLOUD & FREKUENSI KATA
|
|
st.subheader("☁️ WordCloud dan Kata yang Sering Muncul")
|
|
|
|
# 1. Definisi Stopwords Tambahan (Agar sinkron dengan WordCloud & Barchart)
|
|
from wordcloud import STOPWORDS
|
|
custom_stopwords = set(STOPWORDS)
|
|
custom_stopwords.update([
|
|
'https', 'co', '...', 'amp', 'harga', 'water', 'facial',
|
|
'wash', 'pakai', 'produk', 'banget', 'saya'
|
|
])
|
|
|
|
sentimen_pilihan = st.selectbox("Pilih Sentimen:", ["Positif", "Negatif"])
|
|
|
|
# 2. Filter data berdasarkan pilihan
|
|
df_filtered = df[df["sentimen"] == sentimen_pilihan]
|
|
|
|
# 3. Proses Pembersihan Teks (Hapus stopwords dan kata pendek < 3 huruf)
|
|
def clean_text_for_viz(text_series):
|
|
all_words = " ".join(text_series.astype(str)).lower().split()
|
|
# Hanya ambil kata yang bukan stopword dan panjangnya > 2 karakter
|
|
cleaned_words = [w for w in all_words if w not in custom_stopwords and len(w) > 2]
|
|
return cleaned_words
|
|
|
|
list_kata = clean_text_for_viz(df_filtered["stemming_data"])
|
|
teks_bersih = " ".join(list_kata)
|
|
|
|
if teks_bersih.strip() != "":
|
|
# Tentukan parameter visual
|
|
if sentimen_pilihan == "Positif":
|
|
map_warna = "Greens"
|
|
judul_wc = "WordCloud Sentimen Positif"
|
|
judul_bar = "Frekuensi Kata Sentimen Positif"
|
|
else:
|
|
map_warna = "Reds"
|
|
judul_wc = "WordCloud Sentimen Negatif"
|
|
judul_bar = "Frekuensi Kata Sentimen Negatif"
|
|
|
|
col_wc, col_bar = st.columns([1, 1.2])
|
|
|
|
with col_wc:
|
|
# Generate WordCloud dari teks yang sudah bersih
|
|
wc = WordCloud(
|
|
width=800,
|
|
height=500,
|
|
background_color="white",
|
|
colormap=map_warna,
|
|
# Stopwords dikosongkan karena teks sudah kita bersihkan manual di atas
|
|
stopwords=None
|
|
).generate(teks_bersih)
|
|
|
|
fig_wc, ax_wc = plt.subplots()
|
|
ax_wc.imshow(wc, interpolation='bilinear')
|
|
ax_wc.axis("off")
|
|
ax_wc.set_title(judul_wc, fontsize=15, pad=10)
|
|
st.pyplot(fig_wc)
|
|
|
|
with col_bar:
|
|
# Hitung frekuensi dari list_kata yang sama dengan sumber WordCloud
|
|
counts_data = Counter(list_kata).most_common(20)
|
|
words, counts = zip(*counts_data)
|
|
|
|
fig_bar, ax_bar = plt.subplots(figsize=(10, 6))
|
|
|
|
# Gunakan warna warni tab10 agar mirip foto
|
|
colors = plt.cm.tab10(range(len(words)))
|
|
bars = ax_bar.bar(words, counts, color=colors)
|
|
|
|
# Tambahkan label angka di atas bar (Persis seperti di foto)
|
|
for bar in bars:
|
|
yval = bar.get_height()
|
|
ax_bar.text(
|
|
bar.get_x() + bar.get_width()/2,
|
|
yval + (max(counts) * 0.01),
|
|
int(yval),
|
|
ha='center', va='bottom', fontsize=10, fontweight='bold'
|
|
)
|
|
|
|
ax_bar.set_title(judul_bar, fontsize=14, fontweight='bold')
|
|
ax_bar.set_ylabel("Jumlah Kata", fontweight='bold')
|
|
ax_bar.set_xlabel("Kata-Kata Sering Muncul", fontweight='bold')
|
|
|
|
plt.xticks(rotation=45, ha="right")
|
|
plt.tight_layout()
|
|
st.pyplot(fig_bar)
|
|
else:
|
|
st.warning(f"Tidak ada data teks yang cukup untuk menampilkan visualisasi {sentimen_pilihan}.")
|
|
|
|
st.markdown("---")
|
|
|
|
# 9. CONTOH TWEET (FIXED WRAP TEXT)
|
|
st.subheader("📋 Contoh Tweet Ulasan Produk Pembersih Wajah")
|
|
|
|
sample_pos = df[df["sentimen"] == "Positif"].sample(
|
|
n=min(5, len(df[df["sentimen"] == "Positif"])),
|
|
random_state=42
|
|
)
|
|
|
|
sample_neg = df[df["sentimen"] == "Negatif"].sample(
|
|
n=min(5, len(df[df["sentimen"] == "Negatif"])),
|
|
random_state=42
|
|
)
|
|
|
|
sample_data = pd.concat([sample_pos, sample_neg]).sample(frac=1)
|
|
|
|
st.dataframe(
|
|
sample_data[["full_text", "sentimen"]],
|
|
use_container_width=True
|
|
)
|
|
|
|
# 10. EVALUASI MODEL (CONFUSION MATRIX & REPORT)
|
|
st.subheader("🎯 Evaluasi Performa Model")
|
|
|
|
col_cm, col_desc = st.columns([1.2, 1])
|
|
|
|
with col_cm:
|
|
z = cm_data
|
|
x = ['Prediksi Negatif', 'Prediksi Positif']
|
|
y = ['Aktual Negatif', 'Aktual Positif']
|
|
|
|
fig_cm = px.imshow(
|
|
z, x=x, y=y,
|
|
text_auto=True,
|
|
color_continuous_scale='Blues',
|
|
labels=dict(x="Hasil Prediksi", y="Data Aktual", color="Jumlah")
|
|
)
|
|
fig_cm.update_layout(title_text='Confusion Matrix')
|
|
st.plotly_chart(fig_cm, use_container_width=True)
|
|
|
|
with col_desc:
|
|
st.markdown(f"""
|
|
**Keterangan:**
|
|
|
|
* ✅ **TP (True Positive) : {tp}** Sampel positif yang berhasil diprediksi dengan benar sebagai positif.
|
|
|
|
* ✅ **TN (True Negative) : {tn}** Sampel negatif yang berhasil diprediksi dengan benar sebagai negatif.
|
|
|
|
* ❌ **FP (False Positive) : {fp}** Sampel negatif yang salah diprediksi sebagai positif.
|
|
|
|
* ❌ **FN (False Negative) : {fn}** Sampel positif yang salah diprediksi sebagai negatif.
|
|
""")
|
|
|
|
# Classification Report Table
|
|
st.write("**Classification Report**")
|
|
report_df = pd.DataFrame({
|
|
"precision": [1.0000, 0.8522, None, 0.9261, 0.8772],
|
|
"recall": [0.1500, 1.0000, None, 0.5750, 0.8559],
|
|
"f1-score": [0.2609, 0.9202, 0.8559, 0.5905, 0.8084],
|
|
"support": [20, 98, 118, 118, 118]
|
|
}, index=["Negatif", "Positif", "accuracy", "macro avg", "weighted avg"])
|
|
|
|
st.table(report_df.fillna("-").style.format(precision=2))
|
|
|
|
# Penjelasan Berdasarkan Hasil
|
|
st.markdown(f"""
|
|
### 📊 Penjelasan Metrik Evaluasi
|
|
|
|
1. **Accuracy (Akurasi)**: Merupakan ukuran tingkat kedekatan antara hasil prediksi dengan nilai sebenarnya. Berdasarkan hasil, sistem memiliki akurasi sebesar **{report_dict['accuracy']:.4f}**, yang berarti sekitar **{report_dict['accuracy']*100:.2f}%** data uji berhasil diklasifikasikan dengan benar.
|
|
|
|
2. **Precision (Presisi)**: Merupakan ukuran yang menunjukkan ketepatan model dalam memprediksi kelas. Berdasarkan hasil, kelas **Negatif memiliki nilai 1.00 (100%)**, artinya setiap kali sistem memprediksi negatif, prediksi tersebut selalu benar. Sedangkan kelas **Positif memiliki nilai {report_dict['Positif']['precision']:.2f} ({report_dict['Positif']['precision']*100:.0f}%)**.
|
|
|
|
3. **Recall**: Menilai tingkat efektivitas sistem dalam menemukan kembali semua data dari kelas tertentu. Kelas **Positif memiliki recall sempurna 1.00 (100%)**, yang berarti seluruh ulasan positif berhasil diidentifikasi. Namun, kelas **Negatif memiliki recall {report_dict['Negatif']['recall']:.2f} ({report_dict['Negatif']['recall']*100:.1f}%)**, menunjukkan sistem masih kesulitan menangkap sebagian besar ulasan negatif yang ada.
|
|
|
|
4. **F1-Score**: Merupakan rata-rata harmonik antara precision dan recall. Nilai F1-Score sebesar **{report_dict['Positif']['f1-score']:.2f}** pada kelas positif menunjukkan performa yang sangat stabil, sementara nilai **{report_dict['Negatif']['f1-score']:.2f}** pada kelas negatif menunjukkan perlunya peningkatan performa pada identifikasi sentimen negatif.
|
|
""")
|
|
|
|
# 11. KESIMPULAN
|
|
st.markdown("---")
|
|
st.subheader("📝 Kesimpulan")
|
|
|
|
if jumlah_sentimen.get("Positif", 0) > jumlah_sentimen.get("Negatif", 0):
|
|
st.success(
|
|
"Analisis dilakukan berdasarkan beberapa kata kunci, yaitu 'pakai facial wash', "
|
|
"'facial wash cocok', 'facial wash jerawat', 'cleanser', dan 'pakai face wash', "
|
|
"serta nama merek seperti Wardah, Garnier, Cetaphil, Glad2Glow, Hada Labo, "
|
|
"Skintific, COSRX, dan Glow & Lovely. Hasil analisis menunjukkan bahwa mayoritas "
|
|
"ulasan memiliki sentimen **positif**, yang menandakan produk pembersih wajah "
|
|
"diterima dengan baik oleh pengguna."
|
|
)
|
|
else:
|
|
st.warning(
|
|
"Analisis dilakukan berdasarkan beberapa kata kunci, yaitu 'pakai facial wash', "
|
|
"'facial wash cocok', 'facial wash jerawat', 'cleanser', dan 'pakai face wash', "
|
|
"serta nama merek seperti Wardah, Garnier, Cetaphil, Glad2Glow, Hada Labo, "
|
|
"Skintific, COSRX, dan Glow & Lovely. Hasil analisis menunjukkan bahwa mayoritas "
|
|
"ulasan memiliki sentimen **negatif**, sehingga perlu dilakukan evaluasi terhadap "
|
|
"kualitas produk pembersih wajah."
|
|
) |