|
|
| import streamlit as st |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| import os |
| import glob |
|
|
| |
| st.set_page_config( |
| page_title="Framework Analytics Command Center", |
| page_icon="⚛️", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| def inject_custom_css(): |
| custom_css = """ |
| <style> |
| /* Impor Font */ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap'); |
| |
| /* Warna Latar Belakang Utama & Font */ |
| .stApp { |
| background-color: #0d1117; |
| color: #c9d1d9; |
| font-family: 'Inter', sans-serif; |
| } |
| |
| /* Sidebar Styling */ |
| [data-testid="stSidebar"] { |
| background-color: #161b22; |
| border-right: 1px solid #30363d; |
| } |
| |
| /* Memunculkan kembali toggle (panah) sidebar */ |
| header[data-testid="stHeader"] { |
| background-color: transparent !important; |
| } |
| #MainMenu {visibility: hidden;} |
| footer {visibility: hidden;} |
| |
| /* Heading Styling */ |
| h1, h2, h3, h4 { |
| font-family: 'Inter', sans-serif; |
| font-weight: 700; |
| color: #58a6ff; |
| letter-spacing: -0.5px; |
| } |
| h1 { |
| font-size: 2.5rem; |
| background: -webkit-linear-gradient(45deg, #58a6ff, #3fb950); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| /* Glassmorphism Container / Cards */ |
| .glass-card { |
| background: rgba(22, 27, 34, 0.6); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| border-radius: 12px; |
| border: 1px solid rgba(88, 166, 255, 0.2); |
| padding: 24px; |
| margin-bottom: 24px; |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); |
| } |
| |
| /* Metrics Styling */ |
| [data-testid="stMetricValue"] { |
| font-family: 'JetBrains Mono', monospace; |
| color: #3fb950; |
| font-size: 2rem !important; |
| font-weight: 700; |
| } |
| [data-testid="stMetricLabel"] { |
| color: #8b949e; |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| font-size: 0.85rem; |
| } |
| |
| /* Custom Scrollbar */ |
| ::-webkit-scrollbar { |
| width: 8px; |
| height: 8px; |
| } |
| ::-webkit-scrollbar-track { |
| background: #0d1117; |
| } |
| ::-webkit-scrollbar-thumb { |
| background: #30363d; |
| border-radius: 4px; |
| } |
| ::-webkit-scrollbar-thumb:hover { |
| background: #58a6ff; |
| } |
| |
| /* Tabs Styling */ |
| .stTabs [data-baseweb="tab-list"] { |
| gap: 2px; |
| background-color: transparent; |
| } |
| .stTabs [data-baseweb="tab"] { |
| background-color: #21262d; |
| border-radius: 8px 8px 0px 0px; |
| color: #8b949e; |
| padding: 10px 20px; |
| font-weight: 600; |
| border: 1px solid #30363d; |
| border-bottom: none; |
| } |
| .stTabs [aria-selected="true"] { |
| background-color: #1f2937 !important; |
| color: #58a6ff !important; |
| border-top: 2px solid #58a6ff !important; |
| } |
| </style> |
| """ |
| st.markdown(custom_css, unsafe_allow_html=True) |
|
|
|
|
| |
| |
| |
| @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 |
|
|
|
|
| |
| |
| |
| def render_pengenalan(): |
| st.title("Pengenalan Proyek") |
|
|
| |
| 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() |
|
|
| |
| 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.") |
|
|
| |
| col_fe, col_be = st.columns(2) |
|
|
| with col_fe: |
| st.markdown("<p style='color: #58a6ff; font-weight: bold; font-size: 1.1rem;'>Ekosistem Frontend</p>", 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("<p style='color: #3fb950; font-weight: bold; font-size: 1.1rem;'>Ekosistem Backend</p>", 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) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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*).") |
|
|
| |
| 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() |
|
|
| |
| 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:") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| |
| |
| def render_audit_data(datasets): |
| st.title("Audit Data Mentah (Raw Dataset Inspection)") |
| st.markdown("<p style='color: #8b949e;'>Inspeksi tingkat tinggi terhadap struktur dan kualitas dataset mentah sebelum masuk ke tahap rekayasa fitur.</p>", unsafe_allow_html=True) |
|
|
| df_stats = datasets.get('raw_stats') |
| df_issues = datasets.get('raw_issues') |
|
|
| |
| 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("<p style='color: #8b949e;'>Inspeksi terhadap dataset setelah melalui tahap pembersihan, rekayasa fitur, dan analisis sentimen (NLP).</p>", unsafe_allow_html=True) |
|
|
| df_stats_clean = datasets.get('clean_stats') |
| df_issues_clean = datasets.get('clean_issues') |
|
|
| |
| df_stats_raw = datasets.get('raw_stats') |
| df_issues_raw = datasets.get('raw_issues') |
|
|
| |
| 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 |
|
|
| |
| tab1, tab2 = st.tabs([ |
| "Profil Repositori (df_repo_stats)", |
| "Profil Isu (df_raw_issues)" |
| ]) |
|
|
| with tab1: |
| st.markdown("<h4 style='color: #58a6ff; margin-top: 10px;'>Perbandingan Dimensi Data (Before vs After)</h4>", unsafe_allow_html=True) |
|
|
| |
| st.markdown("<p style='color: #8b949e; font-size: 0.85rem; margin-bottom: 0;'>KONDISI AWAL (RAW DATA)</p>", 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}") |
|
|
| |
| st.markdown("<p style='color: #3fb950; font-size: 0.85rem; margin-bottom: 0; margin-top: 15px;'>KONDISI AKHIR (PROCESSED DATA)</p>", 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() |
|
|
| |
| 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() |
|
|
| |
| 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("<h4 style='color: #58a6ff; margin-top: 10px;'>Perbandingan Dimensi Data (Before vs After)</h4>", unsafe_allow_html=True) |
|
|
| |
| st.markdown("<p style='color: #8b949e; font-size: 0.85rem; margin-bottom: 0;'>KONDISI AWAL (RAW DATA)</p>", 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 |
|
|
| |
| 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}") |
|
|
| |
| st.markdown("<p style='color: #3fb950; font-size: 0.85rem; margin-bottom: 0; margin-top: 15px;'>KONDISI AKHIR (PROCESSED DATA)</p>", 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] |
|
|
| |
| 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() |
|
|
| |
| 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) |
|
|
| |
| |
| |
| def render_eda(datasets): |
| st.title("Eksplorasi Data Interaktif") |
| st.markdown("<p style='color: #8b949e; font-size: 1.1rem;'>Visualisasi analitik menggunakan representasi grafis fundamental yang didesain agar mudah dipahami oleh eksekutif maupun orang awam dalam hitungan detik.</p>", 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 |
|
|
| |
| tab_repo, tab_issues = st.tabs([ |
| "Analisis Repositori (Popularitas)", |
| "Analisis Sentimen & Kecepatan Resolusi" |
| ]) |
|
|
| frontend_kws = ['react', 'vue', 'angular', 'svelte'] |
|
|
| |
| 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) |
|
|
| |
| 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') |
|
|
| |
| |
| |
| with tab_repo: |
| st.markdown("<div class='glass-card'>", unsafe_allow_html=True) |
| st.subheader("Filter Analisis Ekosistem") |
| pilihan_ekosistem = st.radio("Pilih Ekosistem:", ["Frontend", "Backend"], horizontal=True, key="radio_repo", label_visibility="collapsed") |
|
|
| |
| df_filter = df_stats[df_stats['kategori'] == pilihan_ekosistem].copy() |
|
|
| |
| basic_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'] |
|
|
| |
| color_map = {} |
| unique_repos = df_filter['repo_short'].unique() |
| for i, repo in enumerate(unique_repos): |
| color_map[repo] = basic_colors[i % len(basic_colors)] |
|
|
| st.divider() |
|
|
| |
| |
| |
| st.subheader("Tingkat Adopsi & Kontribusi Kode") |
| st.markdown("<p style='color:#8b949e;'><b>Keterangan:</b> Biru menunjukkan popularitas murni (Stars). Hijau menunjukkan audiens yang secara aktif menduplikasi kode untuk berkontribusi (Forks).</p>", 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() |
|
|
| |
| 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') |
| )) |
|
|
| |
| 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) |
| ), |
| margin=dict(t=10, b=0) |
| ) |
| st.plotly_chart(fig_bar, use_container_width=True) |
|
|
| st.divider() |
|
|
| col1, col2 = st.columns(2) |
|
|
| |
| |
| |
| with col1: |
| st.subheader("Pangsa Dominasi Pasar") |
| st.markdown("<p style='color:#8b949e;'><b>Keterangan:</b> Ukuran potongan donat menunjukkan persentase dominasi framework di pasar berdasarkan total Stars.</p>", unsafe_allow_html=True) |
|
|
| if col_stars: |
| |
| 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), |
| title="" |
| ) |
| ) |
|
|
| fig_donut.add_annotation( |
| text="MARKET<br>SHARE", |
| x=0.5, y=0.5, |
| font=dict(size=18, family="Inter", weight="bold"), |
| showarrow=False, |
| font_color="#FFFFFF" |
| ) |
|
|
| st.plotly_chart(fig_donut, use_container_width=True) |
|
|
| |
| |
| |
| with col2: |
| st.subheader("Audiens Pemantau Aktif") |
| st.markdown("<p style='color:#8b949e;'><b>Keterangan:</b> Semakin panjang grafik, semakin banyak developer yang menghidupkan notifikasi pembaruan untuk framework ini.</p>", 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) |
|
|
| |
| 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 |
| ) |
|
|
| 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, |
| xanchor="center", |
| x=0.5, |
| font=dict(size=15), |
| title="" |
| ) |
| ) |
|
|
| st.plotly_chart(fig_hbar, use_container_width=True) |
|
|
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| |
| |
| |
| with tab_issues: |
| st.markdown("<div class='glass-card'>", unsafe_allow_html=True) |
|
|
| if not df_issues.empty and 'resolution_time_days' in df_issues.columns and 'sentiment_score' in df_issues.columns: |
| issue_repo_col = 'repo_name' if 'repo_name' in df_issues.columns else df_issues.columns[0] |
| df_issues['repo_short'] = df_issues[issue_repo_col].apply(lambda x: str(x).split('/')[-1].capitalize()) |
| df_issues['kategori'] = df_issues[issue_repo_col].apply(lambda x: 'Frontend' if any(k in str(x).lower() for k in frontend_kws) else 'Backend') |
|
|
| st.subheader("Filter Analisis Sentimen & Kecepatan") |
| pilihan_isu = st.radio("Pilih Ekosistem:", ["Frontend", "Backend"], horizontal=True, key="radio_isu", label_visibility="collapsed") |
|
|
| |
| df_isu_filter = df_issues[df_issues['kategori'] == pilihan_isu].copy() |
|
|
| st.divider() |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| |
| st.subheader(f"Indeks Kepuasan Komunitas (Rata-rata Sentimen)") |
| st.markdown("<p style='color:#8b949e;'><b>Keterangan:</b> Batang <span style='color:#3fb950; font-weight:bold;'>Hijau (Positif)</span> berarti pengembang mayoritas merasa puas atau terbantu. Batang <span style='color:#f85149; font-weight:bold;'>Merah (Negatif)</span> berarti pengembang banyak menggunakan kata frustrasi/marah saat melapor bug.</p>", 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', |
| textfont=dict(color='white', size=16, family='JetBrains Mono', weight='bold') |
| )) |
|
|
| 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() |
|
|
| |
|
|
| col3, col4 = st.columns(2) |
|
|
| with col3: |
| |
| st.subheader("Distribusi Emosi Pengguna") |
| st.markdown("<p style='color:#8b949e; font-size:0.85rem;'><b>Keterangan:</b> Visualisasi ini memecah total keluhan menjadi persentase emosi. Semakin panjang blok hijau, semakin sehat komunitasnya.</p>", unsafe_allow_html=True) |
|
|
| |
| 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') |
|
|
| |
| df_total = df_emotion.groupby('repo_short')['Jumlah'].transform('sum') |
| df_emotion['Persentase'] = (df_emotion['Jumlah'] / df_total) * 100 |
|
|
| |
| 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: |
| |
| st.subheader("Kecepatan Tim Menyelesaikan Bug") |
| st.markdown("<p style='color:#8b949e; font-size:0.85rem;'><b>Keterangan:</b> Menghitung rata-rata waktu (dalam hari) untuk memperbaiki sebuah masalah. <b>Semakin pendek batangnya, semakin responsif framework tersebut.</b></p>", unsafe_allow_html=True) |
|
|
| |
| df_speed = df_isu_filter.groupby('repo_short')['resolution_time_days'].mean().reset_index() |
|
|
| |
| |
| df_speed['days_rounded'] = df_speed['resolution_time_days'].round(0).astype(int) |
|
|
| |
| 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', |
| text=df_speed['days_rounded'].astype(str) + " Hari", |
| textposition='inside', |
| textfont=dict(color='black', size=14, family='JetBrains Mono', weight='bold'), |
| width=0.7 |
| )) |
|
|
| 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("</div>", unsafe_allow_html=True) |
|
|
| |
| |
| |
| def render_kesimpulan(): |
| st.title("Kesimpulan & Rekomendasi") |
| st.markdown("<p style='color: #8b949e; font-size: 1.1rem;'>Rangkuman akhir berdasarkan tingkat penggunaan, kecepatan perbaikan masalah, dan kepuasan komunitas untuk membantu Anda memilih teknologi yang tepat.</p>", unsafe_allow_html=True) |
| st.divider() |
|
|
| |
| |
| |
| 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("<p style='color: #58a6ff; font-weight: bold; font-size: 1.1rem;'>Ekosistem Frontend</p>", 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." |
| ] |
| }) |
| |
| st.table(df_fe_conc) |
|
|
| with col_be: |
| st.markdown("<p style='color: #3fb950; font-weight: bold; font-size: 1.1rem;'>Ekosistem Backend</p>", 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." |
| ] |
| }) |
| |
| st.table(df_be_conc) |
|
|
| st.divider() |
|
|
| |
| |
| |
| 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.") |
|
|
| |
| 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) |
|
|
| |
| def render_stars(score): |
| full_stars = int(score) |
| decimal_part = score - full_stars |
|
|
| html = "" |
| |
| html += "<span style='color: #f1c40f; font-size: 1.3rem; text-shadow: 0 0 5px rgba(241, 196, 15, 0.4);'>★</span>" * full_stars |
|
|
| |
| if 0.3 <= decimal_part <= 0.7: |
| html += "<span style='font-size: 1.3rem; background: -webkit-linear-gradient(0deg, #f1c40f 50%, #30363d 50%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;'>★</span>" |
| empty_stars = 4 - full_stars |
| elif decimal_part > 0.7: |
| html += "<span style='color: #f1c40f; font-size: 1.3rem; text-shadow: 0 0 5px rgba(241, 196, 15, 0.4);'>★</span>" |
| empty_stars = 4 - full_stars |
| else: |
| empty_stars = 5 - full_stars |
|
|
| |
| html += "<span style='color: #30363d; font-size: 1.3rem;'>★</span>" * empty_stars |
|
|
| return f"<div style='display: flex; align-items: center;'>{html} <span style='color:#8b949e; font-size:1rem; margin-left:12px; font-family: JetBrains Mono; font-weight: bold;'>({score:.1f}/5.0)</span></div>" |
|
|
| |
| |
| fe_raw_data = { |
| "React": {"share": 50.5, "speed": 38.9, "sent": -0.184}, |
| "Angular": {"share": 20.6, "speed": 6.2, "sent": -0.182}, |
| "Svelte": {"share": 17.9, "speed": 20.1, "sent": -0.364}, |
| "Vue (Core)": {"share": 11.0, "speed": 22.0, "sent": -0.149} |
| } |
|
|
| |
| be_raw_data = { |
| "Spring Boot": {"share": 25.2, "speed": 1.5, "sent": 0.040}, |
| "Flask": {"share": 22.3, "speed": 8.0, "sent": -0.036}, |
| "FastAPI": {"share": 30.9, "speed": 110.8, "sent": 0.581}, |
| "Express.js": {"share": 21.5, "speed": 55.6, "sent": 0.075} |
| } |
|
|
| |
| fe_scores = {fw: calculate_enterprise_score(d['share'], d['speed'], d['sent'], 50.5) for fw, d in fe_raw_data.items()} |
| be_scores = {fw: calculate_enterprise_score(d['share'], d['speed'], d['sent'], 30.9) for fw, d in be_raw_data.items()} |
|
|
| |
| fe_scores = dict(sorted(fe_scores.items(), key=lambda item: item[1], reverse=True)) |
| be_scores = dict(sorted(be_scores.items(), key=lambda item: item[1], reverse=True)) |
|
|
| col_star_fe, col_star_be = st.columns(2) |
|
|
| with col_star_fe: |
| st.markdown("<div class='glass-card'>", unsafe_allow_html=True) |
| st.markdown("<p style='color: #58a6ff; font-weight: bold; margin-bottom: 15px; font-size: 1.1rem;'>Kesiapan Frontend</p>", unsafe_allow_html=True) |
|
|
| for fw, score in fe_scores.items(): |
| st.markdown(f""" |
| <div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; border-bottom: 1px solid #30363d; padding-bottom: 8px;'> |
| <span style='color: #c9d1d9; font-weight: 600; font-size: 1.05rem;'>{fw}</span> |
| {render_stars(score)} |
| </div> |
| """, unsafe_allow_html=True) |
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| with col_star_be: |
| st.markdown("<div class='glass-card'>", unsafe_allow_html=True) |
| st.markdown("<p style='color: #3fb950; font-weight: bold; margin-bottom: 15px; font-size: 1.1rem;'>Kesiapan Backend</p>", unsafe_allow_html=True) |
|
|
| for fw, score in be_scores.items(): |
| st.markdown(f""" |
| <div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; border-bottom: 1px solid #30363d; padding-bottom: 8px;'> |
| <span style='color: #c9d1d9; font-weight: 600; font-size: 1.05rem;'>{fw}</span> |
| {render_stars(score)} |
| </div> |
| """, unsafe_allow_html=True) |
| st.markdown("</div>", unsafe_allow_html=True) |
|
|
| st.divider() |
|
|
| |
| |
| |
| st.subheader("Rekomendasi Arsitektur Teknologi") |
| st.write("Panduan penerapan teknologi berdasarkan ukuran dan kebutuhan utama bisnis Anda.") |
|
|
| col_rec1, col_rec2 = st.columns(2) |
|
|
| with col_rec1: |
| st.markdown(""" |
| <div style='background-color: rgba(88, 166, 255, 0.1); border-left: 4px solid #58a6ff; padding: 20px; border-radius: 0 8px 8px 0; height: 100%;'> |
| <h4 style='color: #58a6ff; margin-top: 0;'>Sistem Skala Korporasi <span style='font-weight: normal;'>(Skala Besar dan Tangguh)</span></h4> |
| <p style='color: #c9d1d9; font-weight: bold; font-size: 1.1rem;'>React (FE) + Spring Boot (BE)</p> |
| <p style='color: #8b949e; font-size: 0.95rem; margin-bottom: 0; line-height: 1.6;'> |
| 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. |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| with col_rec2: |
| st.markdown(""" |
| <div style='background-color: rgba(63, 185, 80, 0.1); border-left: 4px solid #3fb950; padding: 20px; border-radius: 0 8px 8px 0; height: 100%;'> |
| <h4 style='color: #3fb950; margin-top: 0;'>Sistem Modern Berkinerja Tinggi <span style='font-weight: normal;'>(Skala Kecil dan Cepat/Efisien)</span></h4> |
| <p style='color: #c9d1d9; font-weight: bold; font-size: 1.1rem;'>Vue/Svelte (FE) + FastAPI (BE)</p> |
| <p style='color: #8b949e; font-size: 0.95rem; margin-bottom: 0; line-height: 1.6;'> |
| 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. |
| </p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| |
| |
| def main(): |
| inject_custom_css() |
|
|
| st.sidebar.markdown("<h2 style='text-align: center; font-family: JetBrains Mono; color: #58a6ff;'>MENU NAVIGASI</h2>", unsafe_allow_html=True) |
| st.sidebar.markdown("<hr style='border-color: #30363d;'>", unsafe_allow_html=True) |
|
|
| |
| menu_options = [ |
| "Pengenalan Proyek", |
| "Audit Data Mentah", |
| "Audit Data Terproses", |
| "Eksplorasi Data", |
| "Kesimpulan" |
| ] |
|
|
| selected_page = st.sidebar.radio("Pilih Menu:", menu_options) |
|
|
| datasets = load_datasets() |
|
|
| if selected_page == "Pengenalan Proyek": |
| render_pengenalan() |
| elif selected_page == "Audit Data Mentah": |
| render_audit_data(datasets) |
| elif selected_page == "Audit Data Terproses": |
| render_audit_data_terproses(datasets) |
| elif selected_page == "Eksplorasi Data": |
| render_eda(datasets) |
| elif selected_page == "Kesimpulan": |
| render_kesimpulan() |
|
|
| if __name__ == "__main__": |
| main() |
|
|