Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |