import streamlit as st
from collections import Counter
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import ast
import re
# ==========================================
# 🎨 COLOR PALETTE (SPOTIFY THEME)
# ==========================================
COLOR_POS = "#1DB954" # Spotify Green
COLOR_NEG = "#E22134" # Red
COLOR_NEU = "#B3B3B3" # Grey
COLOR_BG = "rgba(0,0,0,0)" # Transparent
# ==========================================
# 1. KPI METRICS (KARTU STATISTIK)
# ==========================================
def display_kpi_metrics(df):
"""Menampilkan total ulasan dan persentase sentimen"""
if df.empty:
return
total_reviews = len(df)
pos_count = len(df[df["Global Sentiment"] == "Positive"])
neg_count = len(df[df["Global Sentiment"] == "Negative"])
pos_pct = (pos_count / total_reviews) * 100
neg_pct = (neg_count / total_reviews) * 100
# Layout 3 Kolom
c1, c2, c3 = st.columns(3)
with c1:
st.metric("Total Data", f"{total_reviews:,}", "Ulasan")
with c2:
st.metric(
"Sentimen Positif",
f"{pos_count:,}",
f"{pos_pct:.1f}%",
delta_color="normal",
)
with c3:
st.metric(
"Sentimen Negatif",
f"{neg_count:,}",
f"-{neg_pct:.1f}%",
delta_color="inverse",
)
st.markdown("---")
# ==========================================
# 2. SENTIMENT DONUT CHART
# ==========================================
def plot_sentiment_donut(df):
"""Pie Chart bolong tengah (Donut) untuk Global Sentiment"""
counts = df["Global Sentiment"].value_counts().reset_index()
counts.columns = ["Sentiment", "Count"]
fig = px.pie(
counts,
values="Count",
names="Sentiment",
hole=0.6,
color="Sentiment",
color_discrete_map={"Positive": COLOR_POS, "Negative": COLOR_NEG},
title="Proporsi Sentimen Global",
)
# Styling agar menyatu dengan Dark Mode
fig.update_layout(
plot_bgcolor=COLOR_BG,
paper_bgcolor=COLOR_BG,
font=dict(color="white", size=14),
showlegend=True,
legend=dict(orientation="h", y=-0.1),
)
# Menambahkan Text di tengah Donut
fig.add_annotation(
text="Sentiment", showarrow=False, font_size=20, font_color="white"
)
st.plotly_chart(fig, use_container_width=True)
# ==========================================
# 3. ASPECT STACKED BAR CHART (STYLED)
# ==========================================
def plot_aspect_bar_chart(df):
"""
Visualisasi Aspek dengan styling HTML pada label text.
Count = Bold Putih, Persen = Kuning Emas.
"""
aspect_data = []
aspect_cols = [c for c in df.columns if "_Sentiment" in c]
if not aspect_cols:
st.warning("Belum ada data aspek yang diproses.")
return
for col in aspect_cols:
aspect_name = col.replace("_Sentiment", "")
counts = df[col].value_counts()
if "Positive" in counts:
aspect_data.append(
{
"Aspect": aspect_name,
"Sentiment": "Positive",
"Count": counts["Positive"],
}
)
if "Negative" in counts:
aspect_data.append(
{
"Aspect": aspect_name,
"Sentiment": "Negative",
"Count": counts["Negative"],
}
)
if not aspect_data:
st.info("Tidak ada aspek spesifik terdeteksi.")
return
df_aspects = pd.DataFrame(aspect_data)
# Hitung Persentase
total_per_aspect = df_aspects.groupby("Aspect")["Count"].transform("sum")
df_aspects["Pct"] = (df_aspects["Count"] / total_per_aspect * 100).round(1)
# --- STYLING LABEL DENGAN HTML ---
# {Count} : Angka tebal
# ... : Ubah warna & ukuran persen
df_aspects["Label"] = df_aspects.apply(
lambda x: f"{x['Count']} ({x['Pct']}%)",
axis=1,
)
fig = px.bar(
df_aspects,
x="Count",
y="Aspect",
color="Sentiment",
orientation="h",
title="Analisis Sentimen per Aspek",
color_discrete_map={"Positive": COLOR_POS, "Negative": COLOR_NEG},
text="Label", # Masukkan kolom label HTML
template="plotly_dark",
)
fig.update_layout(
plot_bgcolor=COLOR_BG,
paper_bgcolor=COLOR_BG,
font=dict(color="white", size=14),
xaxis_title="Jumlah Ulasan",
yaxis_title="",
yaxis={"categoryorder": "total ascending"},
barmode="stack",
)
# Update Traces agar HTML terbaca
fig.update_traces(
textposition="inside",
insidetextanchor="middle",
texttemplate="%{text}", # PENTING: Memaksa Plotly render HTML
hovertemplate="%{y}
Sentimen: %{data.name}
Jumlah: %{x}
Persentase: %{customdata[0]}%",
customdata=df_aspects[["Pct"]], # Kirim data persen ke tooltip
)
st.plotly_chart(fig, use_container_width=True)
# ==========================================
# 4. WORDCLOUD GENERATOR
# ==========================================
def generate_wordcloud(df, sentiment_filter):
"""Membuat WordCloud dari ulasan berdasarkan filter sentimen"""
# Filter Data
subset = df[df["Global Sentiment"] == sentiment_filter]
if subset.empty:
st.caption("Tidak ada data untuk kategori ini.")
return
print("subset")
print(subset.columns)
text_combined = " ".join(subset["Original Text"].astype(str).tolist())
# Setup Warna (Hijau untuk Positif, Merah Api untuk Negatif)
colormap = "Greens" if sentiment_filter == "Positive" else "Reds"
wc = WordCloud(
width=800,
height=400,
background_color="#121212", # Dark Background
colormap=colormap,
max_words=100,
contour_color="white",
contour_width=1,
).generate(text_combined)
# Tampilkan menggunakan Matplotlib di Streamlit
fig, ax = plt.subplots(figsize=(10, 5), facecolor="#121212")
ax.imshow(wc, interpolation="bilinear")
ax.axis("off")
st.pyplot(fig)
# ==========================================
# 5. TRIGGER SENTIMENT CHART
# ==========================================
def plot_trigger_sentiment_chart(df):
"""
Visualisasi Trigger Words dengan styling HTML yang lebih cantik.
"""
if "Aspects JSON" not in df.columns:
st.warning("Data aspek detail tidak ditemukan.")
return
trigger_data = []
def clean_json_str(s):
return re.sub(r"np\.float32\(([^)]+)\)", r"\1", str(s))
for _, row in df.iterrows():
try:
json_str = clean_json_str(row["Aspects JSON"])
if pd.isna(json_str) or json_str == "{}":
continue
aspect_data = ast.literal_eval(json_str)
for details in aspect_data.values():
trigger_str = details.get("trigger", "")
label = details.get("label", "Negative")
if trigger_str:
words = [w.strip() for w in trigger_str.split(",")]
for w in words:
if w:
trigger_data.append({"Keyword": w, "Sentiment": label})
except Exception:
continue
if not trigger_data:
st.info("Belum ada kata kunci spesifik.")
return
df_trig = pd.DataFrame(trigger_data)
df_counts = (
df_trig.groupby(["Keyword", "Sentiment"]).size().reset_index(name="Count")
)
# Sorting & Filtering Top 25
df_total = df_counts.groupby("Keyword")["Count"].sum().reset_index(name="Total")
top_keywords = df_total.nlargest(25, "Total")["Keyword"].tolist()
df_final = df_counts[df_counts["Keyword"].isin(top_keywords)].copy()
# Hitung Persentase
total_per_keyword = df_final.groupby("Keyword")["Count"].transform("sum")
df_final["Pct"] = (df_final["Count"] / total_per_keyword * 100).round(1)
# --- STYLING LABEL ---
# Count: Putih Tebal
# Persen: Kuning Emas (#FFD700), Font agak kecil
df_final["Label"] = df_final.apply(
lambda x: f"{x['Count']} ({x['Pct']}%)",
axis=1,
)
dynamic_height = 400 + (len(top_keywords) * 35)
fig = px.bar(
df_final,
x="Count",
y="Keyword",
color="Sentiment",
orientation="h",
title="Frekuensi Kata Pemicu per Sentimen",
color_discrete_map={"Positive": COLOR_POS, "Negative": COLOR_NEG},
text="Label",
template="plotly_dark",
height=dynamic_height,
)
fig.update_layout(
plot_bgcolor=COLOR_BG,
paper_bgcolor=COLOR_BG,
font=dict(color="white", size=14),
yaxis={"categoryorder": "total ascending"},
xaxis_title="Jumlah Kemunculan",
yaxis_title="",
barmode="stack",
margin=dict(l=150, r=50, t=80, b=50),
legend=dict(orientation="h", y=1.02, x=0, title_text=""),
)
# Update Traces untuk render HTML dan Tooltip Bagus
fig.update_traces(
textposition="inside",
insidetextanchor="middle",
texttemplate="%{text}", # Render HTML
hovertemplate="%{y}
Sentimen: %{data.name}
Jumlah: %{x}
Persentase: %{customdata[0]}%",
customdata=df_final[["Pct"]],
)
st.plotly_chart(fig, use_container_width=True)