| 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 |
|
|
|
|
| |
| st.set_page_config(page_title="THC Konsolidasi", layout="wide") |
|
|
| |
| 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' |
|
|
| |
| 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' |
| ] |
|
|
| |
|
|
| @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""" |
| |
| for col in new_columns: |
| if col not in df.columns: |
| df[col] = 0 |
| |
| |
| df = df.rename(columns=rename_dict) |
| |
| |
| if 'ID ANGGOTA' not in df.columns and 'ID' in df.columns: |
| df = df.rename(columns={'ID': 'ID ANGGOTA'}) |
| |
| |
| if 'KEL' not in df.columns and 'KELOMPOK' in df.columns: |
| df = df.rename(columns={'KELOMPOK': 'KEL'}) |
| |
| |
| 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 |
|
|
| |
|
|
| 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 |
|
|
| |
|
|
| 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) |
| |
| |
| 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}") |
| |
| |
| df['TRANS. DATE'] = pd.to_datetime(df['TRANS. DATE'], format='%d/%m/%Y', errors='coerce') |
| |
| |
| 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: |
| |
| 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() |
| |
| |
| 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_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' |
| ] |
| |
| |
| features_scaled = scaler.transform(agg[feature_cols_actual]) |
| |
| |
| 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() |
|
|
| |
|
|
| tab1, tab2 = st.tabs(["π Proses Gabungan & Final", "π Analisa Simpanan"]) |
|
|
| |
| 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 = [] |
| |
| |
| 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: |
| |
| 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() |
|
|
| |
| 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() |
|
|
| |
| 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)") |
|
|
| |
| df_hasil_raw = tambah_kolom_estimasi(combined_df.copy()) |
|
|
| |
| 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() |
|
|
| |
| 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) |
|
|
| |
| 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()) |
|
|
|
|
| |
| with tab2: |
| st.subheader("π Analisa Simpanan - Anomali Detection") |
| st.write("Upload data dari Proses Gabungan & Final") |
| st.divider() |
| |
| |
| 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' |
| ]) |
| |
| |
| 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") |
| |
| |
| 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() |
| |
| |
| 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 |
| ) |
| |
| |
| 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 = 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() |
| |
| |
| 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") |
|
|