import streamlit as st import pandas as pd import numpy as np import io import json from pathlib import Path import joblib import altair as alt # Di bagian paling atas setelah import st.set_page_config(page_title="THC Konsolidasi", layout="wide") # ── Setup ──────────────────────────────────────────────────────────────────── st.set_page_config(page_title="THC Konsolidasi", layout="wide") st.title('📊 THC Konsolidasi - Proses Gabungan & Analisa Simpanan') st.divider() MODELS_DIR = Path(__file__).parent / 'tools' TOOLS_DIR = Path(__file__).parent / 'tools' # ── Define Column Structure ────────────────────────────────────────────────── DESIRED_ORDER = [ 'ID ANGGOTA', 'NAMA', 'CENTER', 'KEL', 'HARI', 'JAM', 'SL', 'TRANS. DATE', 'Db Qurban', 'Cr Qurban', 'Db Khusus', 'Cr Khusus', 'Db HariRaya', 'Cr HariRaya', 'Db Pensiun', 'Cr Pensiun', 'Db Pokok', 'Cr Pokok', 'Db SIPADAN', 'Cr SIPADAN', 'Db Sukarela', 'Cr Sukarela', 'Db Wajib', 'Cr Wajib', 'Db Total', 'Cr Total', 'Db PTN', 'Cr PTN', 'Db PRT', 'Cr PRT', 'Db DTP', 'Cr DTP', 'Db PMB', 'Cr PMB', 'Db PRR', 'Cr PRR', 'Db PSA', 'Cr PSA', 'Db PU', 'Cr PU', 'Db Total2', 'Cr Total2' ] ESTIMASI_COLS = [ 'Estimasi Nominal Kecil Menabung', 'Estimasi Nominal Kecil Penarikan', 'Estimasi Uang', 'Estimasi Nabung 1', 'Estimasi Nabung 2', 'Estimasi Nabung 3', 'Estimasi Penarikan 1', 'Estimasi Penarikan 2', 'T/F 1', 'T/F2', 'Final Filter' ] # ── Helper Functions ───────────────────────────────────────────────────────── @st.cache_data def load_models_and_metadata(): """Load scaler dan Isolation Forest model""" try: scaler_path = MODELS_DIR / 'scaler.pkl' iso_path = MODELS_DIR / 'isolation_forest.pkl' meta_path = TOOLS_DIR / 'metadata.json' if not scaler_path.exists() or not iso_path.exists(): return None, None, None scaler = joblib.load(scaler_path) iso_forest = joblib.load(iso_path) metadata = json.loads(meta_path.read_text()) if meta_path.exists() else {} return scaler, iso_forest, metadata except Exception as e: return None, None, None @st.cache_data def load_excel(file): return pd.read_excel(file, engine='openpyxl') def process_dataframe(df, new_columns, rename_dict): """Standardisasi nama dan kolom DataFrame""" # Add missing columns for col in new_columns: if col not in df.columns: df[col] = 0 # Rename columns df = df.rename(columns=rename_dict) # Standardize ID column if 'ID ANGGOTA' not in df.columns and 'ID' in df.columns: df = df.rename(columns={'ID': 'ID ANGGOTA'}) # Standardize KEL column if 'KEL' not in df.columns and 'KELOMPOK' in df.columns: df = df.rename(columns={'KELOMPOK': 'KEL'}) # Hilangkan duplikasi kolom df = df.loc[:, ~df.columns.duplicated()] return df def detect_delimiter(buffer: io.BytesIO) -> str: """Detect CSV delimiter""" pos = buffer.tell() first_line = buffer.readline().decode('utf-8', errors='ignore') buffer.seek(pos) counts = {'\t': first_line.count('\t'), ';': first_line.count(';'), ',': first_line.count(',')} return max(counts, key=counts.get) def load_data(uploaded_file) -> pd.DataFrame: """Load CSV/Excel file""" try: if uploaded_file.name.endswith(('.xlsx', '.xls')): df = pd.read_excel(uploaded_file, engine='openpyxl') else: sep = detect_delimiter(uploaded_file) df = pd.read_csv(uploaded_file, sep=sep) return df except Exception as e: st.error(f"❌ Error loading file: {str(e)}") return None # ── TAB 1 FUNCTIONS: PROSES GABUNGAN & FINAL ───────────────────────────────── def ambil_3_digit_akhir(val): try: if pd.isna(val): return 0 return int(str(int(val))[-3:]) except Exception: return 0 def estimasi_uang(val): try: if pd.isna(val): return 0 return int(np.ceil(val / 1000.0) * 1000) except Exception: return 0 def estimasi_nabung_2(x): return x - 500 if x > 500 else 0 def estimasi_nabung_3(x): return x + 500 if x < 500 else 0 def tf_1(row): if row["Estimasi Nabung 1"] < 500: return ( (row["Estimasi Nabung 1"] == row["Estimasi Nominal Kecil Menabung"]) or (row["Estimasi Nabung 3"] == row["Estimasi Nominal Kecil Menabung"]) ) else: return ( (row["Estimasi Nominal Kecil Menabung"] == row["Estimasi Nabung 1"]) or (row["Estimasi Nominal Kecil Menabung"] == row["Estimasi Nabung 2"]) ) def estimasi_penarikan_2(x): return x - 500 if x > 500 else 0 def tf2(row): if row["Estimasi Penarikan 1"] < 500: return row["Estimasi Penarikan 1"] == row["Estimasi Nominal Kecil Penarikan"] else: return ( (row["Estimasi Nominal Kecil Penarikan"] == row["Estimasi Penarikan 1"]) or (row["Estimasi Nominal Kecil Penarikan"] == row["Estimasi Penarikan 2"]) ) def final_filter(row): return bool(row["T/F 1"] or row["T/F2"]) def tambah_kolom_estimasi(df): """Tambah kolom estimasi untuk anomali detection""" df["Estimasi Nominal Kecil Menabung"] = df["Db Total"].apply(ambil_3_digit_akhir) df["Estimasi Nominal Kecil Penarikan"] = df["Cr Total"].apply(ambil_3_digit_akhir) df["Estimasi Uang"] = df["Db Total2"].apply(estimasi_uang) df["Estimasi Nabung 1"] = df["Estimasi Uang"] - df["Db Total2"] df["Estimasi Nabung 2"] = df["Estimasi Nabung 1"].apply(estimasi_nabung_2) df["Estimasi Nabung 3"] = df["Estimasi Nabung 1"].apply(estimasi_nabung_3) df["Estimasi Penarikan 1"] = df["Db Total2"].apply(ambil_3_digit_akhir) df["Estimasi Penarikan 2"] = df["Estimasi Penarikan 1"].apply(estimasi_penarikan_2) df["T/F 1"] = df.apply(tf_1, axis=1) df["T/F2"] = df.apply(tf2, axis=1) df["Final Filter"] = df.apply(final_filter, axis=1) return df # ── TAB 2 FUNCTIONS: ANALISA SIMPANAN ──────────────────────────────────────── def prepare_data_analisa(df_raw: pd.DataFrame) -> pd.DataFrame: """Prepare data untuk analisa simpanan""" try: df = df_raw.copy() col_mapping = { 'ID': 'ID ANGGOTA', 'KELOMPOK': 'KEL', 'Db Hariraya': 'Db HariRaya', 'Cr Hariraya': 'Cr HariRaya' } df = df.rename(columns=col_mapping) # Validasi kolom minimal required_cols = ['ID ANGGOTA', 'NAMA', 'CENTER', 'Db Sukarela', 'Cr Sukarela', 'TRANS. DATE'] missing = [c for c in required_cols if c not in df.columns] if missing: raise ValueError(f"Kolom tidak ditemukan: {missing}") # Clean & convert df['TRANS. DATE'] = pd.to_datetime(df['TRANS. DATE'], format='%d/%m/%Y', errors='coerce') # Extract date features df['MINGGU'] = df['TRANS. DATE'].dt.isocalendar().week.astype(int) df['TAHUN'] = df['TRANS. DATE'].dt.year df['YEAR_WEEK'] = df['TAHUN'].astype(str) + '-W' + df['MINGGU'].astype(str).str.zfill(2) return df except Exception as e: st.error(f"❌ Error preparing data: {str(e)}") return None def detect_hariraya_anomaly(df: pd.DataFrame, window: int = 3, threshold: float = 1.0) -> pd.DataFrame: """Detect HariRaya savings anomalies using Rolling Z-Score""" try: weekly = ( df.groupby(['ID ANGGOTA', 'NAMA', 'CENTER', 'YEAR_WEEK'], as_index=False) .agg({'Db HariRaya': 'sum', 'TRANS. DATE': 'first'}) .sort_values(['ID ANGGOTA', 'YEAR_WEEK']) .reset_index(drop=True) ) records = [] for id_val, group in weekly.groupby('ID ANGGOTA', sort=False): g = group.sort_values('YEAR_WEEK').copy() g['Rolling_Mean'] = g['Db HariRaya'].rolling(window=window, min_periods=1).mean() g['Rolling_Std'] = g['Db HariRaya'].rolling(window=window, min_periods=1).std().fillna(0) g['Z_Score'] = np.where( g['Rolling_Std'] > 0, (g['Db HariRaya'] - g['Rolling_Mean']) / g['Rolling_Std'], 0 ) g['Anomaly_HariRaya'] = (np.abs(g['Z_Score']) > threshold).astype(int) records.append(g) return pd.concat(records, ignore_index=True) if records else pd.DataFrame() except Exception as e: st.warning(f"⚠️ Error detecting HariRaya anomalies: {str(e)}") return pd.DataFrame() def detect_sukarela_anomaly(df: pd.DataFrame, scaler, iso_forest, feature_cols: list) -> pd.DataFrame: """Detect Sukarela savings anomalies using Isolation Forest""" try: # Check if columns exist if 'Db Sukarela' not in df.columns or 'Cr Sukarela' not in df.columns: st.error("❌ Kolom 'Db Sukarela' atau 'Cr Sukarela' tidak ditemukan") return pd.DataFrame() # Aggregate dengan named aggregation (syntax pandas yang benar) agg = ( df.groupby(['ID ANGGOTA', 'NAMA', 'CENTER'], as_index=False) .agg( Db_Sukarela_Total=('Db Sukarela', 'sum'), Db_Sukarela_Avg=('Db Sukarela', 'mean'), Db_Sukarela_Std=('Db Sukarela', 'std'), Db_Sukarela_Max=('Db Sukarela', 'max'), Cr_Sukarela_Total=('Cr Sukarela', 'sum'), Cr_Sukarela_Avg=('Cr Sukarela', 'mean'), Cr_Sukarela_Std=('Cr Sukarela', 'std'), Cr_Sukarela_Max=('Cr Sukarela', 'max'), ) .fillna(0) ) # Feature columns untuk ML model feature_cols_actual = [ 'Db_Sukarela_Total', 'Db_Sukarela_Avg', 'Db_Sukarela_Std', 'Db_Sukarela_Max', 'Cr_Sukarela_Total', 'Cr_Sukarela_Avg', 'Cr_Sukarela_Std', 'Cr_Sukarela_Max' ] # Scale features features_scaled = scaler.transform(agg[feature_cols_actual]) # Predict anomalies agg['Anomaly_Sukarela'] = iso_forest.predict(features_scaled) agg['Anomaly_Sukarela'] = (agg['Anomaly_Sukarela'] == -1).astype(int) return agg except Exception as e: st.warning(f"⚠️ Error detecting Sukarela anomalies: {str(e)}") import traceback st.error(f"Debug info: {traceback.format_exc()}") return pd.DataFrame() # ── MAIN APP ───────────────────────────────────────────────────────────────── tab1, tab2 = st.tabs(["📊 Proses Gabungan & Final", "📋 Analisa Simpanan"]) # ── TAB 1: PROSES GABUNGAN & FINAL ─────────────────────────────────────────── with tab1: st.subheader("🔄 Tahap 1: Merge File") st.write("1️⃣ Upload 4 file Excel (THC FINAL, TAK, TLP, KDP) ini ambil dari hasil penarikan database") st.write("2️⃣ Proses Gabungan (Merge semua file dari penarikan Database)") st.write("3️⃣ Proses Final (Estimasi & Anomali Detection)") st.divider() uploaded_files = st.file_uploader( "📤 Unggah 4 file Excel THC FINAL.xlsx, TAK.xlsx, TLP.xlsx, KDP.xlsx", accept_multiple_files=True, type=["xlsx"], key="tab1_upload" ) if uploaded_files: try: dfs = {file.name: load_excel(file) for file in uploaded_files} combined_df_list = [] # Process THC FINAL if 'THC FINAL.xlsx' in dfs: df_thc = dfs['THC FINAL.xlsx'] df_thc = process_dataframe(df_thc, [], {}) combined_df_list.append(df_thc) st.success("✓ THC FINAL.xlsx diproses") if 'TAK.xlsx' in dfs: df_tak = dfs['TAK.xlsx'] rename_dict_tak = { 'KELOMPOK': 'KEL', 'DEBIT_PINJAMAN ARTA': 'Db PRT', 'DEBIT_PINJAMAN DT. PENDIDIKAN': 'Db DTP', 'DEBIT_PINJAMAN MIKROBISNIS': 'Db PMB', 'DEBIT_PINJAMAN SANITASI': 'Db PSA', 'DEBIT_PINJAMAN UMUM': 'Db PU', 'DEBIT_PINJAMAN RENOVASI RUMAH': 'Db PRR', 'DEBIT_PINJAMAN PERTANIAN': 'Db PTN', 'DEBIT_TOTAL': 'Db Total2', 'CREDIT_PINJAMAN ARTA': 'Cr PRT', 'CREDIT_PINJAMAN DT. PENDIDIKAN': 'Cr DTP', 'CREDIT_PINJAMAN MIKROBISNIS': 'Cr PMB', 'CREDIT_PINJAMAN SANITASI': 'Cr PSA', 'CREDIT_PINJAMAN UMUM': 'Cr PU', 'CREDIT_PINJAMAN RENOVASI RUMAH': 'Cr PRR', 'CREDIT_PINJAMAN PERTANIAN': 'Cr PTN', 'CREDIT_TOTAL': 'Cr Total2' } df_tak = process_dataframe(df_tak, list(rename_dict_tak.keys()), rename_dict_tak) combined_df_list.append(df_tak) st.success("✓ TAK.xlsx diproses") if 'TLP.xlsx' in dfs: df_tlp = dfs['TLP.xlsx'] rename_dict_tlp = { 'KELOMPOK': 'KEL', 'DEBIT_Simpanan Hari Raya': 'Db HariRaya', 'DEBIT_Simpanan Pensiun': 'Db Pensiun', 'DEBIT_Simpanan Pokok': 'Db Pokok', 'DEBIT_Simpanan Sukarela': 'Db Sukarela', 'DEBIT_Simpanan Wajib': 'Db Wajib', 'DEBIT_Simpanan Qurban': 'Db Qurban', 'DEBIT_Simpanan Sipadan': 'Db SIPADAN', 'DEBIT_Simpanan Khusus': 'Db Khusus', 'DEBIT_TOTAL': 'Db Total', 'CREDIT_Simpanan Hari Raya': 'Cr HariRaya', 'CREDIT_Simpanan Pensiun': 'Cr Pensiun', 'CREDIT_Simpanan Pokok': 'Cr Pokok', 'CREDIT_Simpanan Sukarela': 'Cr Sukarela', 'CREDIT_Simpanan Wajib': 'Cr Wajib', 'CREDIT_Simpanan Qurban': 'Cr Qurban', 'CREDIT_Simpanan Sipadan': 'Cr SIPADAN', 'CREDIT_Simpanan Khusus': 'Cr Khusus', 'CREDIT_TOTAL': 'Cr Total' } df_tlp = process_dataframe(df_tlp, list(rename_dict_tlp.keys()), rename_dict_tlp) combined_df_list.append(df_tlp) st.success("✓ TLP.xlsx diproses") if 'KDP.xlsx' in dfs: df_kdp = dfs['KDP.xlsx'] rename_dict_kdp = { 'KELOMPOK': 'KEL', 'DEBIT_Simpanan Hari Raya': 'Db HariRaya', 'DEBIT_Simpanan Pensiun': 'Db Pensiun', 'DEBIT_Simpanan Pokok': 'Db Pokok', 'DEBIT_Simpanan Sukarela': 'Db Sukarela', 'DEBIT_Simpanan Wajib': 'Db Wajib', 'DEBIT_Simpanan Qurban': 'Db Qurban', 'DEBIT_Simpanan Sipadan': 'Db SIPADAN', 'DEBIT_Simpanan Khusus': 'Db Khusus', 'DEBIT_TOTAL': 'Db Total', 'CREDIT_Simpanan Hari Raya': 'Cr HariRaya', 'CREDIT_Simpanan Pensiun': 'Cr Pensiun', 'CREDIT_Simpanan Pokok': 'Cr Pokok', 'CREDIT_Simpanan Sukarela': 'Cr Sukarela', 'CREDIT_Simpanan Wajib': 'Cr Wajib', 'CREDIT_Simpanan Qurban': 'Cr Qurban', 'CREDIT_Simpanan Sipadan': 'Cr SIPADAN', 'CREDIT_Simpanan Khusus': 'Cr Khusus', 'CREDIT_TOTAL': 'Cr Total', 'DEBIT_PU': 'Db PU', 'CREDIT_PU': 'Cr PU', 'DEBIT_TOTAL2': 'Db Total2', 'CREDIT_TOTAL2': 'Cr Total2' } df_kdp = process_dataframe(df_kdp, list(rename_dict_kdp.keys()), rename_dict_kdp) combined_df_list.append(df_kdp) st.success("✓ KDP.xlsx diproses") if combined_df_list: # 1. Concat semua df cleaned_list = [] for df in combined_df_list: df = df.loc[:, ~df.columns.duplicated()].copy() df = df.reset_index(drop=True) cleaned_list.append(df) combined_df = pd.concat(cleaned_list, ignore_index=True, sort=False) combined_df = combined_df.reset_index(drop=True) combined_df = combined_df.loc[:, ~combined_df.columns.duplicated()].copy() # 2. Rename & Standarisasi col_mapping = { 'ID': 'ID ANGGOTA', 'KELOMPOK': 'KEL', 'Db Sihara': 'Db HariRaya', 'Cr Sihara': 'Cr HariRaya', 'Db Hariraya': 'Db HariRaya', 'Cr Hariraya': 'Cr HariRaya', 'Db Total 2': 'Db Total2', 'Cr Total 2': 'Cr Total2', 'Db Total Simpanan': 'Db Total', 'Cr Total Simpanan': 'Cr Total', 'Db Total Pinjaman': 'Db Total2', 'Cr Total Pinjaman': 'Cr Total2' } combined_df = combined_df.rename(columns=col_mapping) combined_df = combined_df.loc[:, ~combined_df.columns.duplicated()].copy() # 3. Susun Kolom (Anti Reindex Error) other_cols = [c for c in combined_df.columns if c not in DESIRED_ORDER] all_target_cols = list(dict.fromkeys(DESIRED_ORDER + other_cols)) final_df = pd.DataFrame(index=combined_df.index) for col in all_target_cols: if col in combined_df.columns: col_data = combined_df[col] if isinstance(col_data, pd.DataFrame): final_df[col] = col_data.iloc[:, 0] else: final_df[col] = col_data else: final_df[col] = 0 combined_df = final_df.copy() st.divider() st.subheader("🔍 Tahap 2: Proses Final (Estimasi & Anomali)") # 4. Tambah Estimasi df_hasil_raw = tambah_kolom_estimasi(combined_df.copy()) # 5. Susun Kolom Akhir final_col_order = list(dict.fromkeys(DESIRED_ORDER + ESTIMASI_COLS)) df_final = pd.DataFrame(index=df_hasil_raw.index) for col in final_col_order: if col in df_hasil_raw.columns: col_data = df_hasil_raw[col] if isinstance(col_data, pd.DataFrame): df_final[col] = col_data.iloc[:, 0] else: df_final[col] = col_data else: df_final[col] = 0 df_hasil = df_final.copy() # Metrics col1, col2 = st.columns(2) with col1: st.metric("📊 Total Baris Data", len(df_hasil)) with col2: anomali_count = df_hasil["Final Filter"].sum() st.metric("⚠️ Anomali Terdeteksi", int(anomali_count)) st.divider() st.write("📋 Preview hasil akhir (20 baris pertama):") st.dataframe(df_hasil.head(20), use_container_width=True) # Download output = io.BytesIO() with pd.ExcelWriter(output, engine='xlsxwriter') as writer: df_hasil.to_excel(writer, index=False, sheet_name='THC Hasil') output.seek(0) st.download_button( label="📥 Download Data Lengkap (Estimasi + Final Filter)", data=output.getvalue(), file_name="THC_Gabungan_dan_Final_Hasil.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) except Exception as e: import traceback st.error(f"❌ Error: {str(e)}") st.code(traceback.format_exc()) # ── TAB 2: ANALISA SIMPANAN ────────────────────────────────────────────────── with tab2: st.subheader("🔍 Analisa Simpanan - Anomali Detection") st.write("Upload data dari Proses Gabungan & Final") st.divider() # Load models scaler, iso_forest, metadata = load_models_and_metadata() if scaler is None or iso_forest is None: st.error("❌ Model tidak tersedia di tools/. Pastikan scaler.pkl dan isolation_forest.pkl ada.") else: feature_cols = metadata.get('feature_cols', [ 'Db_Sukarela_Total', 'Db_Sukarela_Avg', 'Db_Sukarela_Std', 'Db_Sukarela_Max', 'Cr_Sukarela_Total', 'Cr_Sukarela_Avg', 'Cr_Sukarela_Std', 'Cr_Sukarela_Max' ]) # File upload uploaded_file = st.file_uploader( "📤 Upload File (CSV/Excel)", type=['csv', 'xlsx', 'xls'], key="tab2_upload" ) if uploaded_file is not None: df_raw = load_data(uploaded_file) if df_raw is None: st.stop() df_prep = prepare_data_analisa(df_raw) if df_prep is None: st.stop() st.success(f"✓ Data berhasil dimuat: {len(df_prep)} transaksi dari {df_prep['ID ANGGOTA'].nunique()} anggota") # Anomaly detection st.subheader("🔍 Menjalankan Anomali Deteksi...") col1, col2 = st.columns(2) with col1: st.write("🔹 HariRaya Anomaly (Rolling Z-Score)...") hariraya_results = detect_hariraya_anomaly(df_prep, window=3, threshold=1.0) hariraya_anomalies = hariraya_results[hariraya_results['Anomaly_HariRaya'] == 1] if len(hariraya_results) > 0 else pd.DataFrame() st.metric("Anomali HariRaya", len(hariraya_anomalies)) with col2: st.write("🔹 Sukarela Anomaly (Isolation Forest)...") sukarela_results = detect_sukarela_anomaly(df_prep, scaler, iso_forest, []) sukarela_anomalies = sukarela_results[sukarela_results['Anomaly_Sukarela'] == 1] if len(sukarela_results) > 0 else pd.DataFrame() st.metric("Anomali Sukarela", len(sukarela_anomalies)) st.divider() # Results tabs st.subheader("📊 Detail Hasil Anomali") tab2_1, tab2_2, tab2_3 = st.tabs(["🔴 HariRaya Anomalies", "🟡 Sukarela Anomalies", "📋 Summary"]) with tab2_1: if len(hariraya_anomalies) > 0: display_cols = [col for col in ['ID ANGGOTA', 'NAMA', 'CENTER', 'Db HariRaya', 'Z_Score', 'YEAR_WEEK'] if col in hariraya_anomalies.columns] st.dataframe( hariraya_anomalies[display_cols].sort_values('Z_Score', ascending=False), use_container_width=True ) # Chart if len(hariraya_anomalies) > 0: chart = alt.Chart(hariraya_anomalies).mark_bar().encode( x='ID ANGGOTA:N', y='Db HariRaya:Q', color=alt.value('red') ).properties(height=400, title="HariRaya Anomalies") st.altair_chart(chart, use_container_width=True) else: st.info("✓ Tidak ada anomali HariRaya terdeteksi") with tab2_2: if len(sukarela_anomalies) > 0: st.dataframe( sukarela_anomalies[[ 'ID ANGGOTA', 'NAMA', 'CENTER', 'Db_Sukarela_Total', 'Cr_Sukarela_Total', 'Db_Sukarela_Avg', 'Cr_Sukarela_Avg' ]].sort_values('Db_Sukarela_Total', ascending=False), use_container_width=True ) # Chart chart = alt.Chart(sukarela_anomalies).mark_circle(size=100).encode( x='Db_Sukarela_Avg:Q', y='Cr_Sukarela_Avg:Q', color=alt.value('red'), tooltip=['ID ANGGOTA', 'NAMA', 'Db_Sukarela_Total'] ).properties(height=400, title="Sukarela Anomalies (Debit vs Kredit)") st.altair_chart(chart, use_container_width=True) else: st.info("✓ Tidak ada anomali Sukarela terdeteksi") with tab2_3: st.write("**Ringkasan Anomali Terdeteksi:**") summary_data = { "Tipe Anomali": ["HariRaya", "Sukarela"], "Jumlah Anomali": [len(hariraya_anomalies), len(sukarela_anomalies)], "% dari Total": [ f"{(len(hariraya_anomalies)/len(hariraya_results)*100):.2f}%" if len(hariraya_results) > 0 else "0%", f"{(len(sukarela_anomalies)/len(sukarela_results)*100):.2f}%" if len(sukarela_results) > 0 else "0%" ] } st.dataframe(pd.DataFrame(summary_data), use_container_width=True) st.divider() # Export st.subheader("💾 Export Hasil") col1, col2 = st.columns(2) with col1: if len(hariraya_anomalies) > 0: excel_buffer = io.BytesIO() with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer: hariraya_anomalies.to_excel(writer, sheet_name='HariRaya', index=False) excel_buffer.seek(0) st.download_button( label="📥 HariRaya Anomalies", data=excel_buffer.getvalue(), file_name="Analisa_HariRaya_Anomalies.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) else: st.info("Tidak ada data HariRaya") with col2: if len(sukarela_anomalies) > 0: excel_buffer = io.BytesIO() with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer: sukarela_anomalies.to_excel(writer, sheet_name='Sukarela', index=False) excel_buffer.seek(0) st.download_button( label="📥 Sukarela Anomalies", data=excel_buffer.getvalue(), file_name="Analisa_Sukarela_Anomalies.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) else: st.info("Tidak ada data Sukarela") else: st.info("👆 Upload file untuk analisa simpanan")