import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go import os import glob # Konfigurasi Halaman Dasar st.set_page_config( page_title="Framework Analytics Command Center", page_icon="⚛️", layout="wide", initial_sidebar_state="expanded" ) # ========================================== # 1. INJEKSI CSS KUSTOM (THEMA TECHIE/CYBER) # ========================================== # ========================================== # 1. GANTI KESELURUHAN FUNGSI inject_custom_css() # ========================================== def inject_custom_css(): custom_css = """ """ st.markdown(custom_css, unsafe_allow_html=True) # ========================================== # 2. DATA LOADER (DENGAN CACHING ROBUST) # ========================================== @st.cache_data(show_spinner="Menginisialisasi Data Engine...") def load_datasets(): """ Mesin pemuat data (Data Loader) dengan ketahanan tinggi terhadap kegagalan I/O. Menggunakan pencarian jalur dinamis (glob) untuk menyesuaikan struktur direktori Google Colab, serta menginisialisasi DataFrame kosong (fallback) untuk mencegah crash (NoneType error). """ datasets = { 'raw_stats': pd.DataFrame(), 'raw_issues': pd.DataFrame(), 'clean_stats': pd.DataFrame(), 'clean_issues': pd.DataFrame() } file_mapping = { "raw_stats": ["raw_repo_stats.csv", "data_raw/raw_repo_stats.csv", "*/raw_repo_stats.csv"], "raw_issues": ["raw_issues_data.csv", "data_raw/raw_issues_data.csv", "*/raw_issues_data.csv"], "clean_stats": ["processed_stats_engineered.csv", "data_processed/processed_stats_engineered.csv", "*/processed_stats_engineered.csv", "data_processed/github_repo_stats_processed.csv"], "clean_issues": ["processed_issues_cleaned.csv", "data_processed/processed_issues_cleaned.csv", "*/processed_issues_cleaned.csv", "data_processed/github_issues_processed.csv"] } critical_missing = [] for key, patterns in file_mapping.items(): file_found = False for pattern in patterns: available_files = glob.glob(pattern) if available_files: latest_file = max(available_files, key=os.path.getctime) try: datasets[key] = pd.read_csv(latest_file) file_found = True break except Exception as e: st.error(f"Gagal membaca {latest_file}: {str(e)}") if not file_found: critical_missing.append(key) if critical_missing: st.warning(f"Peringatan Sistem: Dataset berikut belum tersedia di direktori: {', '.join(critical_missing)}.") return datasets # ========================================== # 3. HALAMAN: PENGENALAN PROYEK # ========================================== def render_pengenalan(): st.title("Pengenalan Proyek") # 1. Latar Belakang (Menggunakan native container) with st.container(): st.subheader("Latar Belakang & Tujuan") st.write("Proyek ini bertujuan untuk membandingkan tren, kepuasan pengguna, serta kelebihan dan kekurangan dari berbagai framework **frontend** dan **backend** populer. Saya mengambil data dari ekosistem GitHub untuk melihat seberapa banyak audiens yang menggunakan framework tersebut, seberapa cepat masalah (*bug*) diselesaikan, dan bagaimana emosi komunitasnya. Hasil analisis ini dirancang untuk membantu perusahaan dalam mengambil keputusan saat memilih teknologi aplikasi.") st.divider() # 2. Batasan & Tabel Framework (Pemisahan Kategori Frontend & Backend) with st.container(): st.subheader("Batasan Metodologi & Pilihan Framework") st.write("Untuk menjaga integritas analisis, ruang lingkup dibatasi pada **8 framework utama** yang mewakili pemimpin pasar dan penantang modern. Data yang diekstraksi mencakup **500 tiket keluhan (issues) terbaru** guna memastikan relevansi terhadap kondisi ekosistem saat ini.") # Membuat dua kolom untuk memisahkan tabel Frontend dan Backend col_fe, col_be = st.columns(2) with col_fe: st.markdown("
Ekosistem Frontend
", unsafe_allow_html=True) df_frontend = pd.DataFrame({ "Framework": ["**React**", "**Vue (Core)**", "**Angular**", "**Svelte**"], "Alasan Pemilihan": [ "Standar industri global dengan ekosistem paling masif.", "Alternatif populer dengan kurva pembelajaran yang fleksibel.", "Solusi utama untuk aplikasi skala besar (Enterprise).", "Inovator baru dengan performa kompilasi yang sangat efisien." ] }) st.table(df_frontend) with col_be: st.markdown("Ekosistem Backend
", unsafe_allow_html=True) df_backend = pd.DataFrame({ "Framework": ["**Express.js**", "**Flask**", "**FastAPI**", "**Spring Boot**"], "Alasan Pemilihan": [ "Dominasi absolut pada runtime Node.js (JavaScript).", "Framework Python mikro yang sangat ringan dan fleksibel.", "Standar baru Python untuk performa tinggi dan asinkron.", "Arsitektur paling tangguh untuk sistem backend berbasis Java." ] }) st.table(df_backend) # with col_fe: # st.markdown("Ekosistem Frontend
", unsafe_allow_html=True) # df_frontend = pd.DataFrame({ # "Framework": ["React", "Vue (Core)", "Angular", "Svelte"], # "Alasan Pemilihan": [ # "Standar industri global dengan ekosistem paling masif.", # "Alternatif populer dengan kurva pembelajaran yang fleksibel.", # "Solusi utama untuk aplikasi skala besar (Enterprise).", # "Inovator baru dengan performa kompilasi yang sangat efisien." # ] # }) # st.dataframe(df_frontend, hide_index=True, use_container_width=True) # with col_be: # st.markdown("Ekosistem Backend
", unsafe_allow_html=True) # df_backend = pd.DataFrame({ # "Framework": ["Express.js", "Flask", "FastAPI", "Spring Boot"], # "Alasan Pemilihan": [ # "Dominasi absolut pada runtime Node.js (JavaScript).", # "Framework Python mikro yang sangat ringan dan fleksibel.", # "Standar baru Python untuk performa tinggi dan asinkron.", # "Arsitektur paling tangguh untuk sistem backend berbasis Java." # ] # }) # st.dataframe(df_backend, hide_index=True, use_container_width=True) st.write("**Mengapa hanya 8 Framework?**\nKombinasi ini dipilih dengan sengaja karena sudah mewakili kondisi pasar perangkat lunak secara keseluruhan. Jika saya memasukkan terlalu banyak framework, datanya justru akan menjadi bising (*noisy*).") # Menggunakan st.error untuk memberikan kotak penekanan (highlight) otomatis st.error("**Mengapa Django Tidak Dimasukkan?**\nTim pengembang Django **tidak menggunakan fitur GitHub Issues** untuk melacak perbaikan bug. Mereka menggunakan platform pelacakan tiket eksternal (*Trac*). Memasukkan Django akan menghasilkan data yang kosong dan merusak metrik perbandingan. Sebagai gantinya, saya menggunakan **Flask**.") st.write("**Mengapa dibatasi 500 Issues terbaru?**\nTujuannya untuk melihat performa framework pada kondisi **paling aktual**. Masalah dari 2-3 tahun lalu biasanya terkait versi lama yang sudah tidak relevan saat ini.") st.divider() # 3. Alur Kerja & Arsitektur Data (Sangat Rinci & Profesional) with st.container(): st.subheader("Arsitektur & Pipeline Rekayasa Data (End-to-End)") st.write("Analisis ini tidak sekadar menarik data mentah, melainkan menerapkan metodologi rekayasa data tingkat lanjut (*Advanced Data Engineering*) dan algoritma *Machine Learning* untuk Pemrosesan Bahasa Alami (*NLP*). Berikut adalah rincian teknis dari sistem *pipeline* yang berjalan di belakang layar:") # Diagram Pipeline menggunakan st.code agar terlihat "Techie" dan aman 100% dari error rendering st.code(""" [SUMBER DATA] [PRA-PEMROSESAN] [PEMODELAN AI] [DEPLOYMENT] ┌───────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ ┌────────────┐ │ GitHub API v3 │ ───➔ │ Audit & Cleaning │ ───➔ │ Rekayasa Fitur & NLP │ ───➔ │ Dashboard │ │ (500 Issues) │ │ (Missing Values) │ │ (VADER, TF-IDF, Ratios) │ │ Analytics │ └───────────────┘ └──────────────────┘ └─────────────────────────┘ └────────────┘ """, language="markdown") # Detail langkah dengan st.expander agar rapi, interaktif, detail, namun tidak memakan tempat with st.expander("1. Ekstraksi & Ingestion Data (*Data Gathering*)", expanded=True): st.write("Menggunakan GitHub REST API v3 dengan manajemen otentikasi (Token) untuk mengekstraksi dua entitas utama secara paralel:") st.markdown(""" * **Membuat Repository Metadata** untuk menarik metrik absolut seperti jumlah *Stars, Forks, Watchers*, dan total *Issues* yang masih terbuka untuk mengukur popularitas awal. * Menarik 500 tiket masalah (*closed issues*) terbaru per framework, lengkap dengan metadata waktu pembuatan, waktu penutupan, status label, dan isi teks komplain pengguna. """) with st.expander("2. Audit Kualitas & Validasi Data (*Data Quality Control*)"): st.write("Sebelum diproses, seluruh data mentah melewati gerbang inspeksi ketat untuk menjaga integritas hasil akhir:") st.markdown(""" * **Mengecek Duplikasi** untuk memastikan tidak ada ID *issue* yang terekam ganda akibat masalah *pagination* pada API. * **Menangani Missing Values** dengan menghapus atau mengimputasi baris data yang kehilangan informasi krusial (misalnya deskripsi keluhan kosong). * **Menvalidasi Integritas Relasional** agar emastikan setiap tiket komplain terikat dengan nama framework (*repo_name*) yang valid. """) with st.expander("3. Rekayasa Fitur (*Feature Engineering*) - CRITICAL"): st.write("Ini adalah tahapan krusial untuk menciptakan metrik baru yang memiliki nilai bisnis strategis dari gabungan data mentah:") st.markdown(""" * **Ekstraksi Temporal:** Menghitung variabel `resolution_time_days` (Selisih waktu antara *created_at* dan *closed_at*). Ini adalah metrik operasional mutlak untuk menilai seberapa cepat tim pemelihara (*maintainers*) menyelesaikan *bug*. * **Metrik Keterlibatan (Engagement Ratios):** Menciptakan metrik derivatif seperti rasio bintang terhadap *fork* (`star_to_fork_ratio`) dan persentase keterlibatan pengikut. Ini mengukur seberapa banyak audiens yang sekadar menyukai dibandingkan audiens yang benar-benar mengadopsi dan memodifikasi kode. """) with st.expander("4. Pemrosesan Bahasa Alami (NLP) & Analisis Sentimen"): st.write("Mengubah teks komplain manusia yang tidak terstruktur menjadi skor matematis objektif:") st.markdown(""" * **Text Preprocessing:** Membersihkan teks dari karakter *noise*, tautan URL, format kode (Markdown), *stop-words*, dan melakukan reduksi kata (*stemming/lemmatization*). * **Sentiment Scoring (Custom VADER):** Menggunakan kamus algoritma sentimen (Leksikon) yang *dimodifikasi khusus untuk istilah teknis perangkat lunak* (seperti *'bug'*, *'error'*, *'crash'*). Algoritma ini memproduksi skor sentimen (-1.0 hingga 1.0) untuk menentukan apakah emosi pengguna saat menemui *bug* tergolong Negatif, Netral, atau Positif. * **Ekstraksi Term (TF-IDF):** Mengidentifikasi kata kunci teknis spesifik yang paling sering dikeluhkan pada setiap framework secara otomatis. """) with st.expander("5. Sintesis Laporan & Visualisasi (*Deployment*)"): st.write("Tahap akhir mengubah angka dan skor menjadi wawasan (*insights*) yang mudah dipahami") # ========================================== # 4. HALAMAN: AUDIT DATA MENTAH # ========================================== def render_audit_data(datasets): st.title("Audit Data Mentah (Raw Dataset Inspection)") st.markdown("Inspeksi tingkat tinggi terhadap struktur dan kualitas dataset mentah sebelum masuk ke tahap rekayasa fitur.
", unsafe_allow_html=True) df_stats = datasets.get('raw_stats') df_issues = datasets.get('raw_issues') # Pengaman sistem absolut untuk mencegah error NoneType / attribute .shape if df_stats is None or df_stats.empty or df_issues is None or df_issues.empty: st.warning("Data mentah tidak ditemukan atau kosong. Pastikan file raw_repo_stats.csv dan raw_issues_data.csv berhasil dimuat oleh Data Loader.") return tab1, tab2 = st.tabs(["Profil Repositori (df_repo_stats)", "Profil Isu (df_raw_issues)"]) with tab1: col1, col2, col3 = st.columns(3) col1.metric("Total Baris Data", f"{df_stats.shape[0]}") col2.metric("Total Kolom", f"{df_stats.shape[1]}") col3.metric("Data Duplikat", f"{df_stats.duplicated().sum()}") st.markdown("#### Tinjauan Data Awal (Head)") st.dataframe(df_stats.head(8), use_container_width=True) col_info, col_desc = st.columns(2) with col_info: st.markdown("#### Informasi Struktur Kolom") buffer_info = pd.DataFrame({ 'Tipe Data': df_stats.dtypes.astype(str), 'Nilai Kosong (Null)': df_stats.isnull().sum() }) st.dataframe(buffer_info, use_container_width=True) with col_desc: st.markdown("#### Distribusi Statistik") st.dataframe(df_stats.describe(), use_container_width=True) with tab2: col1, col2, col3 = st.columns(3) col1.metric("Total Baris Data", f"{df_issues.shape[0]}") col2.metric("Total Kolom", f"{df_issues.shape[1]}") col3.metric("Data Duplikat", f"{df_issues.duplicated().sum()}") st.markdown("#### Tinjauan Data Awal (Sampel 5 Baris)") st.dataframe(df_issues.head(5), use_container_width=True) col_info, col_desc = st.columns(2) with col_info: st.markdown("#### Informasi Struktur Kolom") buffer_info_issues = pd.DataFrame({ 'Tipe Data': df_issues.dtypes.astype(str), 'Nilai Kosong (Null)': df_issues.isnull().sum() }) st.dataframe(buffer_info_issues, use_container_width=True) with col_desc: st.markdown("#### Distribusi Statistik (Kolom Numerik)") st.dataframe(df_issues.describe(), use_container_width=True) def render_audit_data_terproses(datasets): st.title("Audit Data Terproses (Cleaned & Engineered)") st.markdown("Inspeksi terhadap dataset setelah melalui tahap pembersihan, rekayasa fitur, dan analisis sentimen (NLP).
", unsafe_allow_html=True) df_stats_clean = datasets.get('clean_stats') df_issues_clean = datasets.get('clean_issues') # Mengambil data mentah untuk perbandingan (Before vs After) df_stats_raw = datasets.get('raw_stats') df_issues_raw = datasets.get('raw_issues') # Pengaman sistem absolut if df_stats_clean is None or df_stats_clean.empty or df_issues_clean is None or df_issues_clean.empty: st.warning("Data terproses tidak ditemukan atau kosong. Pastikan file processed_stats_engineered.csv dan processed_issues_cleaned.csv berhasil dimuat oleh Data Loader.") return # Menyelaraskan penamaan tab dengan halaman Audit Data Mentah tab1, tab2 = st.tabs([ "Profil Repositori (df_repo_stats)", "Profil Isu (df_raw_issues)" ]) with tab1: st.markdown("KONDISI AWAL (RAW DATA)
", unsafe_allow_html=True) col_r1, col_r2, col_r3 = st.columns(3) raw_rows = df_stats_raw.shape[0] if df_stats_raw is not None else 0 raw_cols = df_stats_raw.shape[1] if df_stats_raw is not None else 0 raw_dupes = df_stats_raw.duplicated().sum() if df_stats_raw is not None else 0 col_r1.metric("Total Baris", f"{raw_rows}") col_r2.metric("Total Kolom", f"{raw_cols}") col_r3.metric("Data Duplikat", f"{raw_dupes}") # Baris 2: Metrik Data Terproses (After) st.markdown("KONDISI AKHIR (PROCESSED DATA)
", unsafe_allow_html=True) col_c1, col_c2, col_c3 = st.columns(3) clean_rows = df_stats_clean.shape[0] clean_cols = df_stats_clean.shape[1] clean_dupes = df_stats_clean.duplicated().sum() # Menggunakan delta untuk menunjukkan perubahan col_c1.metric("Total Baris", f"{clean_rows}", delta=f"{clean_rows - raw_rows} baris", delta_color="off") col_c2.metric("Total Kolom Fitur", f"{clean_cols}", delta=f"{clean_cols - raw_cols} fitur baru", delta_color="normal") col_c3.metric("Data Duplikat", f"{clean_dupes}", delta=f"{clean_dupes - raw_dupes}", delta_color="inverse") st.divider() # PERBAIKAN: Informasi Fitur Baru sesuai dengan dataset aktual st.markdown("#### Penambahan Fitur Rekayasa (Feature Engineering)") st.info("**Fitur Baru yang Dihasilkan (+2 Kolom):**\n\nKolom-kolom ini dirancang untuk mengukur efisiensi dan tingkat keterlibatan komunitas melebihi angka absolut:\n- `star_to_fork_ratio`: Rasio antara jumlah Stars dan Forks.\n- `subscriber_engagement_rate`: Rasio antara Stars terhadap jumlah Subscribers (pengikut aktif).") st.markdown("#### Tinjauan Data Rekayasa (Head)") st.dataframe(df_stats_clean.head(8), use_container_width=True) col_info, col_desc = st.columns(2) with col_info: st.markdown("#### Informasi Struktur Kolom") buffer_info = pd.DataFrame({ 'Tipe Data': df_stats_clean.dtypes.astype(str), 'Nilai Kosong (Null)': df_stats_clean.isnull().sum() }) st.dataframe(buffer_info, use_container_width=True) with col_desc: st.markdown("#### Distribusi Statistik Final") st.dataframe(df_stats_clean.describe(), use_container_width=True) with tab2: st.markdown("KONDISI AWAL (RAW DATA)
", unsafe_allow_html=True) col_r1, col_r2, col_r3 = st.columns(3) raw_iss_rows = df_issues_raw.shape[0] if df_issues_raw is not None else 0 raw_iss_cols = df_issues_raw.shape[1] if df_issues_raw is not None else 0 # Pengecekan duplikasi khusus untuk data issues mentah (mengabaikan kolom labels) if df_issues_raw is not None: cols_to_check_raw = [col for col in df_issues_raw.columns if col != 'labels'] raw_iss_dupes = df_issues_raw.duplicated(subset=cols_to_check_raw).sum() else: raw_iss_dupes = 0 col_r1.metric("Total Baris", f"{raw_iss_rows}") col_r2.metric("Total Kolom", f"{raw_iss_cols}") col_r3.metric("Data Duplikat", f"{raw_iss_dupes}") # Baris 2: Metrik Data Terproses (After) st.markdown("KONDISI AKHIR (PROCESSED DATA)
", unsafe_allow_html=True) col_c1, col_c2, col_c3 = st.columns(3) clean_iss_rows = df_issues_clean.shape[0] clean_iss_cols = df_issues_clean.shape[1] # Pengecekan duplikasi khusus untuk data issues terproses cols_to_check_clean = [col for col in df_issues_clean.columns if col != 'labels'] clean_iss_dupes = df_issues_clean.duplicated(subset=cols_to_check_clean).sum() col_c1.metric("Total Baris", f"{clean_iss_rows}", delta=f"{clean_iss_rows - raw_iss_rows} baris (Noise dihapus)", delta_color="normal") col_c2.metric("Total Kolom Fitur", f"{clean_iss_cols}", delta=f"{clean_iss_cols - raw_iss_cols} fitur baru", delta_color="normal") col_c3.metric("Data Duplikat", f"{clean_iss_dupes}", delta=f"{clean_iss_dupes - raw_iss_dupes}", delta_color="inverse") st.divider() # PERBAIKAN: Informasi Fitur Baru sesuai dengan dataset aktual st.markdown("#### Penambahan Fitur NLP & Temporal") st.info("**Fitur Baru yang Dihasilkan (+5 Kolom):**\n\nKolom-kolom ini dihasilkan melalui pemrosesan temporal dan NLP (Natural Language Processing):\n- `labels_str`: Dibuat untuk mengubah tipe data list label menjadi string agar bisa divalidasi.\n- `resolution_time_days`: Hasil kalkulasi selisih waktu penanganan issue.\n- `clean_text`: Hasil pembersihan teks dari noise, kode, dan URL.\n- `standardized_label`: Hasil pengelompokan label ke kategori 'bug', 'feature', dll.\n- `sentiment_score`: Skor numerik hasil analisis sentimen VADER.") st.markdown("#### Tinjauan Data NLP & Sentimen (Sampel 5 Baris)") st.dataframe(df_issues_clean.head(5), use_container_width=True) col_info, col_desc = st.columns(2) with col_info: st.markdown("#### Informasi Struktur Kolom") buffer_info_issues = pd.DataFrame({ 'Tipe Data': df_issues_clean.dtypes.astype(str), 'Nilai Kosong (Null)': df_issues_clean.isnull().sum() }) st.dataframe(buffer_info_issues, use_container_width=True) with col_desc: st.markdown("#### Distribusi Statistik (Metrik Waktu & Sentimen)") st.dataframe(df_issues_clean.describe(), use_container_width=True) # ========================================== # 5. HALAMAN: EKSPLORASI DATA (EDA) # ========================================== def render_eda(datasets): st.title("Eksplorasi Data Interaktif") st.markdown("Visualisasi analitik menggunakan representasi grafis fundamental yang didesain agar mudah dipahami oleh eksekutif maupun orang awam dalam hitungan detik.
", unsafe_allow_html=True) df_stats = datasets.get('clean_stats', datasets.get('raw_stats', pd.DataFrame())) df_issues = datasets.get('clean_issues', datasets.get('raw_issues', pd.DataFrame())) if df_stats.empty: st.warning("Data tidak tersedia. Pastikan dataset telah dimuat.") return # PERBAIKAN: Menghapus emoji agar selaras dengan CSS kustom dan terlihat profesional tab_repo, tab_issues = st.tabs([ "Analisis Repositori (Popularitas)", "Analisis Sentimen & Kecepatan Resolusi" ]) frontend_kws = ['react', 'vue', 'angular', 'svelte'] # Standardisasi Nama Kolom x_col = 'full_name' if 'full_name' in df_stats.columns else df_stats.columns[0] col_stars = 'stars' if 'stars' in df_stats.columns else ('stargazers_count' if 'stargazers_count' in df_stats.columns else None) col_forks = 'forks' if 'forks' in df_stats.columns else ('forks_count' if 'forks_count' in df_stats.columns else None) # Membersihkan nama repository df_stats['repo_short'] = df_stats[x_col].apply(lambda x: str(x).split('/')[-1].capitalize()) df_stats['kategori'] = df_stats[x_col].apply(lambda x: 'Frontend' if any(k in str(x).lower() for k in frontend_kws) else 'Backend') # ========================================== # TAB 1: ANALISIS REPOSITORI (POPULARITAS) # ========================================== with tab_repo: st.markdown("Keterangan: Biru menunjukkan popularitas murni (Stars). Hijau menunjukkan audiens yang secara aktif menduplikasi kode untuk berkontribusi (Forks).
", unsafe_allow_html=True) if col_stars and col_forks: df_sorted = df_filter.sort_values(by=col_stars, ascending=False) fig_bar = go.Figure() # Trace untuk Stars fig_bar.add_trace(go.Bar( x=df_sorted['repo_short'], y=df_sorted[col_stars], name='Stars (Popularitas)', marker_color='#58a6ff', text=df_sorted[col_stars], texttemplate='%{text:.2s}', textposition='inside', textfont=dict(color='white', size=15, family='JetBrains Mono', weight='bold') )) # Trace untuk Forks fig_bar.add_trace(go.Bar( x=df_sorted['repo_short'], y=df_sorted[col_forks], name='Forks (Pengembangan)', marker_color='#3fb950', text=df_sorted[col_forks], texttemplate='%{text:.2s}', textposition='inside', textfont=dict(color='white', size=15, family='JetBrains Mono', weight='bold') )) fig_bar.update_layout( barmode='group', plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, font=dict(size=14) # Memperbesar ukuran font legend ), margin=dict(t=10, b=0) ) st.plotly_chart(fig_bar, use_container_width=True) st.divider() col1, col2 = st.columns(2) # --------------------------------------------------------- # VIZ 2: DONUT CHART (PANGSA PASAR) # --------------------------------------------------------- with col1: st.subheader("Pangsa Dominasi Pasar") st.markdown("Keterangan: Ukuran potongan donat menunjukkan persentase dominasi framework di pasar berdasarkan total Stars.
", unsafe_allow_html=True) if col_stars: # Menggunakan px.pie agar legend otomatis ter-generate dengan rapi fig_donut = px.pie( df_filter, values=col_stars, names='repo_short', hole=0.6, color='repo_short', color_discrete_map=color_map ) fig_donut.update_traces( textposition='inside', textinfo='percent', insidetextfont=dict(color='white', size=16, family='JetBrains Mono', weight='bold') ) fig_donut.update_layout( plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), margin=dict(t=10, b=20, l=0, r=0), showlegend=True, legend=dict( orientation="h", yanchor="top", y=-0.1, xanchor="center", x=0.5, font=dict(size=15), # Memperbesar ukuran font legend secara signifikan title="" # Menghilangkan judul legend agar lebih bersih ) ) fig_donut.add_annotation( text="MARKETKeterangan: Semakin panjang grafik, semakin banyak developer yang menghidupkan notifikasi pembaruan untuk framework ini.
", unsafe_allow_html=True) col_watchers = 'subscribers_count' if 'subscribers_count' in df_stats.columns else ('watchers_count' if 'watchers_count' in df_stats.columns else col_stars) df_watchers = df_filter.sort_values(by=col_watchers, ascending=True) # Menggunakan px.bar agar legend otomatis ter-generate dan sesuai dengan warna fig_hbar = px.bar( df_watchers, x=col_watchers, y='repo_short', orientation='h', color='repo_short', color_discrete_map=color_map, text=col_watchers ) fig_hbar.update_traces( textposition='inside', insidetextanchor='middle', textfont=dict(color='white', size=15, family='JetBrains Mono', weight='bold'), width=0.7 # Memaksa ketebalan batang agar proporsional ) fig_hbar.update_layout( plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), margin=dict(t=10, b=0, l=0, r=0), xaxis_title="Jumlah Watchers", yaxis_title="", showlegend=True, legend=dict( orientation="h", yanchor="top", y=-0.15, # Menurunkan posisi legend agar tidak menabrak sumbu X xanchor="center", x=0.5, font=dict(size=15), # Memperbesar ukuran font legend title="" # Menghilangkan judul legend ) ) st.plotly_chart(fig_hbar, use_container_width=True) st.markdown("Keterangan: Grafik ini menghitung jumlah baris data (tiket keluhan/issues) per framework yang berhasil ditarik dan diproses oleh algoritma. Menggambarkan ketebalan data sumber.
", unsafe_allow_html=True) # df_volume = df_isu_filter['repo_short'].value_counts().reset_index() # df_volume.columns = ['repo_short', 'jumlah_baris'] # df_volume = df_volume.sort_values(by='jumlah_baris', ascending=True) # fig_vol = px.bar(df_volume, x='jumlah_baris', y='repo_short', orientation='h', text='jumlah_baris', color_discrete_sequence=['#30363d']) # fig_vol.update_traces(textposition='inside', textfont=dict(color='white', size=14, family='JetBrains Mono')) # fig_vol.update_layout( # plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), # margin=dict(t=10, b=10, l=0, r=0), xaxis_title="Jumlah Baris Data (Tiket Keluhan)", yaxis_title="", height=250 # ) # st.plotly_chart(fig_vol, use_container_width=True) # st.divider() # VIZ 5: Bar Chart Horizontal (Rata-rata Sentimen) - Teks Diperjelas Putih st.subheader(f"Indeks Kepuasan Komunitas (Rata-rata Sentimen)") st.markdown("Keterangan: Batang Hijau (Positif) berarti pengembang mayoritas merasa puas atau terbantu. Batang Merah (Negatif) berarti pengembang banyak menggunakan kata frustrasi/marah saat melapor bug.
", unsafe_allow_html=True) df_avg_sent = df_isu_filter.groupby('repo_short')['sentiment_score'].mean().reset_index().sort_values(by='sentiment_score', ascending=True) df_avg_sent['color'] = df_avg_sent['sentiment_score'].apply(lambda x: '#3fb950' if x > 0 else '#f85149') fig_avg = go.Figure(go.Bar( x=df_avg_sent['sentiment_score'], y=df_avg_sent['repo_short'], orientation='h', marker_color=df_avg_sent['color'], text=df_avg_sent['sentiment_score'].round(3), textposition='inside', # Teks ditempatkan di dalam batang textfont=dict(color='white', size=16, family='JetBrains Mono', weight='bold') # Warna putih tegas )) fig_avg.update_layout( plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), margin=dict(t=10, b=20, l=0, r=0), xaxis_title="Skor Sentimen (-1.0 s/d 1.0)", yaxis_title="", shapes=[dict(type='line', yref='paper', y0=0, y1=1, xref='x', x0=0, x1=0, line=dict(color='#58a6ff', width=3, dash='dot'))] ) st.plotly_chart(fig_avg, use_container_width=True) st.divider() # PENGGANTI SCATTER PLOT (DIPECAH MENJADI 2 VISUALISASI YANG SUPER INTUITIF) col3, col4 = st.columns(2) with col3: # VIZ 6: Proporsi Emosi (100% Stacked Bar Chart) st.subheader("Distribusi Emosi Pengguna") st.markdown("Keterangan: Visualisasi ini memecah total keluhan menjadi persentase emosi. Semakin panjang blok hijau, semakin sehat komunitasnya.
", unsafe_allow_html=True) # Fungsi kategorisasi sentimen def categorize_sentiment(score): if score >= 0.05: return 'Positif' elif score <= -0.05: return 'Negatif' else: return 'Netral' df_isu_filter['Kategori_Emosi'] = df_isu_filter['sentiment_score'].apply(categorize_sentiment) df_emotion = df_isu_filter.groupby(['repo_short', 'Kategori_Emosi']).size().reset_index(name='Jumlah') # Hitung Persentase df_total = df_emotion.groupby('repo_short')['Jumlah'].transform('sum') df_emotion['Persentase'] = (df_emotion['Jumlah'] / df_total) * 100 # Urutan warna spesifik color_map = {'Positif': '#3fb950', 'Netral': '#8b949e', 'Negatif': '#f85149'} fig_emotion = px.bar( df_emotion, y='repo_short', x='Persentase', color='Kategori_Emosi', orientation='h', color_discrete_map=color_map, text=df_emotion['Persentase'].apply(lambda x: f"{x:.0f}%") ) fig_emotion.update_traces(textposition='inside', textfont=dict(color='white', weight='bold')) fig_emotion.update_layout( barmode='stack', plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), margin=dict(t=10, b=0, l=0, r=0), xaxis_title="Persentase (%)", yaxis_title="", legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5, title="") ) st.plotly_chart(fig_emotion, use_container_width=True) with col4: # VIZ 7: Rata-rata Kecepatan Resolusi st.subheader("Kecepatan Tim Menyelesaikan Bug") st.markdown("Keterangan: Menghitung rata-rata waktu (dalam hari) untuk memperbaiki sebuah masalah. Semakin pendek batangnya, semakin responsif framework tersebut.
", unsafe_allow_html=True) # Agregasi data df_speed = df_isu_filter.groupby('repo_short')['resolution_time_days'].mean().reset_index() # PERBAIKAN: Pembulatan ke satuan terdekat (Round to Nearest Integer) # Contoh: 8.2 menjadi 8, 8.7 menjadi 9 df_speed['days_rounded'] = df_speed['resolution_time_days'].round(0).astype(int) # Mengurutkan data agar batang terpanjang di bawah (paling lambat) df_speed = df_speed.sort_values(by='days_rounded', ascending=False) fig_speed = go.Figure(go.Bar( x=df_speed['days_rounded'], y=df_speed['repo_short'], orientation='h', marker_color='#00F2FF', # Warna Cyan Techie text=df_speed['days_rounded'].astype(str) + " Hari", # Menampilkan angka bulat textposition='inside', textfont=dict(color='black', size=14, family='JetBrains Mono', weight='bold'), width=0.7 # Mengatur ketebalan batang agar proporsional )) fig_speed.update_layout( plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#c9d1d9'), margin=dict(t=45, b=0, l=0, r=0), xaxis_title="Rata-rata Waktu Resolusi (Hari)", yaxis_title="", xaxis=dict(showgrid=True, gridcolor='#30363d') ) st.plotly_chart(fig_speed, use_container_width=True) else: st.warning("Data hasil algoritma NLP Sentimen tidak ditemukan atau format tidak sesuai.") st.markdown("Rangkuman akhir berdasarkan tingkat penggunaan, kecepatan perbaikan masalah, dan kepuasan komunitas untuk membantu Anda memilih teknologi yang tepat.
", unsafe_allow_html=True) st.divider() # ========================================== # 1. MATRIKS KESIMPULAN (KATEGORISASI STRATEGIS) # ========================================== st.subheader("Kategorisasi Performa Framework") st.write("Berdasarkan hubungan antara jumlah pengguna dan kestabilan sistem yang ditemukan pada tahap Eksplorasi Data, seluruh framework dikelompokkan ke dalam tiga profil utama:") col_fe, col_be = st.columns(2) with col_fe: st.markdown("Ekosistem Frontend
", unsafe_allow_html=True) df_fe_conc = pd.DataFrame({ "Kategori": ["**Standar Utama Industri**", "**Pilihan Seimbang**", "**Inovator Berkinerja Cepat**"], "Framework": ["React & Angular", "Vue (Core)", "Svelte"], "Karakteristik Utama": [ "Sangat banyak digunakan, namun memiliki tumpukan masalah (bug) yang cukup tinggi.", "Keseimbangan yang pas antara popularitas dan kecepatan perbaikan masalah.", "Kepuasan pengguna naik-turun, namun memiliki tingkat partisipasi pengembang tertinggi." ] }) # PERBAIKAN: Menggunakan st.table karena mendukung rendering Markdown (bold **) secara native st.table(df_fe_conc) with col_be: st.markdown("Ekosistem Backend
", unsafe_allow_html=True) df_be_conc = pd.DataFrame({ "Kategori": ["**Standar Utama Industri**", "**Pilihan Seimbang**", "**Inovator Berkinerja Cepat**"], "Framework": ["Spring Boot & Express.js", "Flask", "FastAPI"], "Karakteristik Utama": [ "Sangat stabil dengan waktu perbaikan masalah yang sangat konsisten.", "Ringan dan stabil, cocok untuk arsitektur layanan berskala kecil.", "Paling disukai oleh pengembang dengan kecepatan perbaikan masalah paling tinggi." ] }) # PERBAIKAN: Menggunakan st.table karena mendukung rendering Markdown (bold **) secara native st.table(df_be_conc) st.divider() # ========================================== # 2. VISUALISASI RATING BINTANG (METRIK GABUNGAN) # ========================================== st.subheader("Skor Kelayakan Bisnis (Kesiapan Skala Perusahaan)") st.write("Nilai gabungan ini dihitung secara dinamis menggunakan algoritma agregasi tertimbang dari tiga faktor utama: Pangsa Pasar (40%), Kecepatan Perbaikan (30%), dan Kepuasan Komunitas (30%). Skala 1 hingga 5 bintang menunjukkan seberapa aman teknologi ini untuk investasi jangka panjang perusahaan Anda.") # Rumus Kalkulasi Skor Enterprise def calculate_enterprise_score(share_pct, speed_days, sentiment_score, max_share): """ Menghitung skor akhir (1-5) berdasarkan data aktual dari visualisasi EDA. - Market Share: Dinormalisasi terhadap pemimpin pasar di ekosistemnya. - Speed: Menggunakan kurva penalti (semakin lama hari resolusi, skor semakin turun). - Sentiment: Dinormalisasi dari skala -1.0 s/d 1.0 menjadi skala 1.0 s/d 5.0. """ market_score = (share_pct / max_share) * 5.0 speed_score = max(1.0, 5.0 - (speed_days / 25.0)) sent_score = ((sentiment_score + 1.0) / 2.0) * 4.0 + 1.0 final_score = (market_score * 0.40) + (speed_score * 0.30) + (sent_score * 0.30) return round(final_score, 1) # Fungsi internal untuk merender bintang HTML dengan dukungan Bintang Setengah (Half-Star) def render_stars(score): full_stars = int(score) decimal_part = score - full_stars html = "" # Render bintang penuh html += "★" * full_stars # Render bintang setengah menggunakan trik CSS Linear Gradient if 0.3 <= decimal_part <= 0.7: html += "★" empty_stars = 4 - full_stars elif decimal_part > 0.7: html += "★" empty_stars = 4 - full_stars else: empty_stars = 5 - full_stars # Render bintang kosong html += "★" * empty_stars return f"Kesiapan Frontend
", unsafe_allow_html=True) for fw, score in fe_scores.items(): st.markdown(f"""Kesiapan Backend
", unsafe_allow_html=True) for fw, score in be_scores.items(): st.markdown(f"""React (FE) + Spring Boot (BE)
Sangat disarankan untuk aplikasi perbankan, toko online raksasa, atau sistem terpusat yang rumit. Pilihan ini mengutamakan kestabilan dan kemudahan mencari tenaga kerja di pasaran. Waktu pembuatannya mungkin sedikit lebih lama, tetapi sistem ini terbukti paling tahan banting terhadap gangguan atau kerusakan.
Vue/Svelte (FE) + FastAPI (BE)
Sangat cocok untuk perusahaan rintisan (startup), layanan kecerdasan buatan, atau aplikasi yang harus segera diluncurkan ke pasar. Pilihan ini menjamin tim pembuat aplikasi merasa nyaman bekerja, sekaligus menjaga beban kerja server tetap ringan dan hemat biaya operasional.