""" visualization.py ---------------------- Berisi fungsi-fungsi untuk menampilkan berbagai visualisasi data kritik dan saran dalam bentuk bar chart, pie chart, serta distribusi berdasarkan tahun, semester, program studi, dan mata kuliah menggunakan Streamlit & Plotly. UPDATED: Visualisasi dinamis yang menyesuaikan dengan kolom yang tersedia """ import streamlit as st import pandas as pd import plotly.express as px from config import ASPEK_COLUMNS # Definisi warna untuk setiap kategori sentimen sentimen_palette = { "netral": "#FFE24C", "positif": "#4CFF72", "negatif": "#FF4C4C" } # Urutan kategori untuk konsistensi tampilan di semua chart category_order = ["netral", "positif", "negatif"] # Konfigurasi Plotly config_options = { "scrollZoom": False, "displayModeBar": False } def show_sentiment_bar_chart(df_predicted, aspek_columns): """Menampilkan bar chart distribusi sentimen per aspek.""" # Validasi data dan kolom yang diperlukan if df_predicted.empty or not set(aspek_columns).issubset(df_predicted.columns): st.warning("Data atau kolom aspek tidak tersedia untuk ditampilkan.") return # Transformasi dari wide ke long format untuk visualisasi df_long = df_predicted.melt( value_vars=aspek_columns, var_name="aspek", value_name="sentimen" ) # Konversi ke categorical untuk memastikan urutan yang konsisten df_long["sentimen"] = pd.Categorical( df_long["sentimen"], categories=category_order, ordered=True ) # Agregasi data untuk menghitung jumlah per aspek dan sentimen count_data = df_long.groupby( ["aspek", "sentimen"], observed=False ).size().reset_index(name="jumlah") fig = px.bar( count_data, x="aspek", y="jumlah", color="sentimen", barmode="group", color_discrete_map=sentimen_palette, category_orders={"sentimen": category_order} ) fig.update_layout(title="Distribusi Sentimen per Aspek") st.plotly_chart(fig, use_container_width=True, config=config_options) def show_sentiment_pie_chart(df_predicted, aspek_columns): """Menampilkan pie chart distribusi total sentimen.""" # Flatten semua nilai sentimen dari semua aspek menjadi 1D array sentimen_total = df_predicted[aspek_columns].values.ravel() sentimen_counts = pd.Series(sentimen_total).value_counts().reset_index() sentimen_counts.columns = ["sentimen", "jumlah"] sentimen_counts = sentimen_counts.sort_values("jumlah", ascending=False) # Donut chart dengan hole parameter fig = px.pie(sentimen_counts, names="sentimen", values="jumlah", color="sentimen", color_discrete_map=sentimen_palette, hole=0.3) fig.update_layout(title="Total Komposisi Sentimen") fig.update_traces(textposition='inside', textinfo='percent+label') st.plotly_chart(fig, use_container_width=True, config=config_options) def show_year_distribution(df): """Menampilkan distribusi jumlah kritik/saran per tahun.""" # Ekstraksi tahun dari kolom tanggal jika kolom tahun tidak tersedia if 'tanggal' in df.columns and 'tahun' not in df.columns: df['tahun'] = pd.to_datetime(df['tanggal'], errors='coerce').dt.year # Return None jika tidak ada data tahun (untuk handling di pemanggil) if 'tahun' not in df.columns: return None df_tahun = df.dropna(subset=['tahun']).copy() if df_tahun.empty: return None df_tahun['tahun'] = df_tahun['tahun'].astype(int) year_counts = df_tahun['tahun'].value_counts().reset_index() year_counts.columns = ['tahun', 'jumlah'] year_counts = year_counts.sort_values('jumlah', ascending=False) fig = px.bar(year_counts, x='tahun', y='jumlah', color='tahun', title="Distribusi Kritik/Saran per Tahun") fig.update_layout(xaxis=dict(type='category')) st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_semester_distribution(df): """Menampilkan distribusi jumlah kritik/saran per semester.""" if 'semester' not in df.columns: return None semester_counts = df['semester'].value_counts().reset_index() semester_counts.columns = ['semester', 'jumlah'] semester_counts = semester_counts.sort_values('jumlah', ascending=False) fig = px.bar(semester_counts, x='semester', y='jumlah', color='semester', title="Distribusi Kritik/Saran per Semester") fig.update_layout(xaxis=dict(categoryorder='total descending')) st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_prodi_distribution(df): """Menampilkan jumlah kritik/saran per program studi.""" if 'nama_prodi' not in df.columns: return None prodi_counts = df['nama_prodi'].value_counts().reset_index() prodi_counts.columns = ['nama_prodi', 'jumlah'] # Sort ascending untuk horizontal bar (nilai kecil di bawah) prodi_counts = prodi_counts.sort_values(by='jumlah', ascending=True) fig = px.bar( prodi_counts, x='jumlah', y='nama_prodi', orientation='h', color='jumlah', title="Jumlah Kritik/Saran per Program Studi" ) st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_top10_matkul_distribution(df): """Menampilkan 10 mata kuliah dengan jumlah kritik/saran terbanyak.""" required_cols = ['nama_matakuliah', 'kode_matakuliah'] missing_cols = [col for col in required_cols if col not in df.columns] if missing_cols: return None # Groupby untuk menghitung frekuensi per mata kuliah matkul_counts = ( df.groupby(['kode_matakuliah', 'nama_matakuliah'], observed=False) .size() .reset_index(name='jumlah') .sort_values(by='jumlah', ascending=False) .head(10) ) # Gabungkan kode dan nama untuk label yang informatif matkul_counts['label'] = ( matkul_counts['kode_matakuliah'] + " - " + matkul_counts['nama_matakuliah'] ) matkul_counts = matkul_counts.sort_values(by='jumlah', ascending=True) fig = px.bar( matkul_counts, x='jumlah', y='label', orientation='h', title="Top 10 Mata Kuliah Berdasarkan Kritik/Saran", color='jumlah' ) st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_sentiment_by_year(df, aspek_columns): """Menampilkan distribusi sentimen per tahun.""" # Ekstraksi tahun dari kolom tanggal jika diperlukan if 'tanggal' in df.columns and 'tahun' not in df.columns: df['tahun'] = pd.to_datetime(df['tanggal'], errors='coerce').dt.year if 'tahun' not in df.columns: return None # Transformasi ke long format dengan id_vars tahun df_long = df.melt(id_vars=['tahun'], value_vars=aspek_columns, var_name='aspek', value_name='sentimen') year_sentiment = df_long.groupby( ['tahun', 'sentimen'], observed=False ).size().reset_index(name='jumlah') year_sentiment = year_sentiment.sort_values('jumlah', ascending=False) fig = px.bar(year_sentiment, x='tahun', y='jumlah', color='sentimen', barmode='group', color_discrete_map=sentimen_palette) fig.update_layout(title="Distribusi Sentimen Kritik/Saran per Tahun") st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_sentiment_by_semester(df, aspek_columns): """Menampilkan distribusi sentimen per semester.""" if 'semester' not in df.columns: return None df_long = df.melt(id_vars=['semester'], value_vars=aspek_columns, var_name='aspek', value_name='sentimen') semester_sentiment = df_long.groupby( ['semester', 'sentimen'], observed=False ).size().reset_index(name='jumlah') semester_sentiment = semester_sentiment.sort_values( 'jumlah', ascending=False) fig = px.bar(semester_sentiment, x='semester', y='jumlah', color='sentimen', barmode='group', color_discrete_map=sentimen_palette) fig.update_layout(title="Distribusi Sentimen Kritik/Saran per Semester") st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_sentiment_by_prodi(df, aspek_columns): """Menampilkan distribusi sentimen per program studi.""" if 'nama_prodi' not in df.columns: return None df_long = df.melt( id_vars=['nama_prodi'], value_vars=aspek_columns, var_name='aspek', value_name='sentimen' ) prodi_sentiment = ( df_long.groupby(['nama_prodi', 'sentimen'], observed=False) .size() .reset_index(name='jumlah') ) # Hitung total per prodi untuk mengurutkan dari terbanyak ke sedikit total_per_prodi = ( prodi_sentiment.groupby('nama_prodi')['jumlah'] .sum() .sort_values(ascending=False) ) # Reverse order untuk horizontal bar (nilai besar di atas) ordered_categories = total_per_prodi.index.tolist()[::-1] # Konversi ke categorical untuk kontrol urutan tampilan prodi_sentiment['nama_prodi'] = pd.Categorical( prodi_sentiment['nama_prodi'], categories=ordered_categories, ordered=True ) fig = px.bar( prodi_sentiment, y='nama_prodi', x='jumlah', color='sentimen', barmode='group', orientation='h', color_discrete_map=sentimen_palette ) fig.update_layout( title="Distribusi Sentimen per Program Studi", yaxis={'categoryorder': 'array', 'categoryarray': ordered_categories} ) st.plotly_chart(fig, use_container_width=True, config=config_options) return True def show_sentiment_by_top10_matkul(df, aspek_columns): """Menampilkan distribusi sentimen pada 10 mata kuliah teratas.""" required_cols = ['kode_matakuliah', 'nama_matakuliah'] missing_cols = [col for col in required_cols if col not in df.columns] if missing_cols: return None # Filter top 10 mata kuliah berdasarkan frekuensi df_top10 = ( df.groupby(['kode_matakuliah', 'nama_matakuliah'], observed=False) .size() .sort_values(ascending=False) .head(10) .index ) df_filtered = df[df.set_index( ['kode_matakuliah', 'nama_matakuliah']).index.isin(df_top10)] df_long = df_filtered.melt( id_vars=['kode_matakuliah', 'nama_matakuliah'], value_vars=aspek_columns, var_name='aspek', value_name='sentimen' ) # Gabungkan kode dan nama untuk label df_long['label'] = ( df_long['kode_matakuliah'] + " - " + df_long['nama_matakuliah'] ) matkul_sentiment = ( df_long.groupby(['label', 'sentimen'], observed=False) .size() .reset_index(name='jumlah') ) # Urutkan berdasarkan total sentimen per mata kuliah total_per_label = ( matkul_sentiment.groupby('label')['jumlah'] .sum() .sort_values(ascending=False) ) ordered_labels = total_per_label.index.tolist()[::-1] matkul_sentiment['label'] = pd.Categorical( matkul_sentiment['label'], categories=ordered_labels, ordered=True ) fig = px.bar( matkul_sentiment, y='label', x='jumlah', color='sentimen', barmode='group', orientation='h', color_discrete_map=sentimen_palette ) fig.update_layout( title="Distribusi Sentimen pada Top 10 Mata Kuliah", yaxis={'categoryorder': 'array', 'categoryarray': ordered_labels} ) st.plotly_chart(fig, use_container_width=True, config=config_options) return True