Download app.py from dvsaudit/thc_cons: direct link, hf CLI and curl.
- Browser
- Download file 27.8 kB
-
https://huggingface.co/spaces/dvsaudit/thc_cons/resolve/main/app.py
- Command line
-
hf download hf://spaces/dvsaudit/thc_cons/app.py
-
curl -L -o app.py https://huggingface.co/spaces/dvsaudit/thc_cons/resolve/main/app.py
27.8 kB
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 | |
| 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") | |