dvsaudit commited on
Commit
bd15e44
Β·
verified Β·
1 Parent(s): 369b08c

Upload 7 files

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ tools/isolation_forest.pkl filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ RUN useradd -m -u 1000 user
7
+
8
+ WORKDIR /home/user/app
9
+
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ COPY --chown=user:user . .
14
+
15
+ EXPOSE 7860
16
+
17
+ USER user
18
+
19
+ CMD ["python", "-m", "streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--browser.gatherUsageStats=false"]
app.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import io
5
+ import json
6
+ from pathlib import Path
7
+ import joblib
8
+ import altair as alt
9
+
10
+ # ── Setup ────────────────────────────────────────────────────────────────────
11
+ st.set_page_config(page_title="THC Konsolidasi", layout="wide")
12
+ st.title('πŸ“Š THC Konsolidasi - Proses Gabungan & Analisa Simpanan')
13
+ st.divider()
14
+
15
+ MODELS_DIR = Path(__file__).parent / 'tools'
16
+ TOOLS_DIR = Path(__file__).parent / 'tools'
17
+
18
+ # ── Define Column Structure ──────────────────────────────────────────────────
19
+ DESIRED_ORDER = [
20
+ 'ID ANGGOTA', 'NAMA', 'CENTER', 'KEL', 'HARI', 'JAM', 'SL', 'TRANS. DATE',
21
+ 'Db Qurban', 'Cr Qurban', 'Db Khusus', 'Cr Khusus', 'Db HariRaya', 'Cr HariRaya',
22
+ 'Db Pensiun', 'Cr Pensiun', 'Db Pokok', 'Cr Pokok', 'Db SIPADAN', 'Cr SIPADAN',
23
+ 'Db Sukarela', 'Cr Sukarela', 'Db Wajib', 'Cr Wajib', 'Db Total', 'Cr Total',
24
+ 'Db PTN', 'Cr PTN', 'Db PRT', 'Cr PRT', 'Db DTP', 'Cr DTP', 'Db PMB', 'Cr PMB',
25
+ 'Db PRR', 'Cr PRR', 'Db PSA', 'Cr PSA', 'Db PU', 'Cr PU', 'Db Total2', 'Cr Total2'
26
+ ]
27
+
28
+ ESTIMASI_COLS = [
29
+ 'Estimasi Nominal Kecil Menabung', 'Estimasi Nominal Kecil Penarikan',
30
+ 'Estimasi Uang', 'Estimasi Nabung 1', 'Estimasi Nabung 2', 'Estimasi Nabung 3',
31
+ 'Estimasi Penarikan 1', 'Estimasi Penarikan 2', 'T/F 1', 'T/F2', 'Final Filter'
32
+ ]
33
+
34
+ # ── Helper Functions ─────────────────────────────────────────────────────────
35
+
36
+ @st.cache_data
37
+ def load_models_and_metadata():
38
+ """Load scaler dan Isolation Forest model"""
39
+ try:
40
+ scaler_path = MODELS_DIR / 'scaler.pkl'
41
+ iso_path = MODELS_DIR / 'isolation_forest.pkl'
42
+ meta_path = TOOLS_DIR / 'metadata.json'
43
+
44
+ if not scaler_path.exists() or not iso_path.exists():
45
+ return None, None, None
46
+
47
+ scaler = joblib.load(scaler_path)
48
+ iso_forest = joblib.load(iso_path)
49
+ metadata = json.loads(meta_path.read_text()) if meta_path.exists() else {}
50
+
51
+ return scaler, iso_forest, metadata
52
+ except Exception as e:
53
+ return None, None, None
54
+
55
+ @st.cache_data
56
+ def load_excel(file):
57
+ return pd.read_excel(file, engine='openpyxl')
58
+
59
+ def process_dataframe(df, new_columns, rename_dict):
60
+ """Standardisasi nama dan kolom DataFrame"""
61
+ # Add missing columns
62
+ for col in new_columns:
63
+ if col not in df.columns:
64
+ df[col] = 0
65
+
66
+ # Rename columns
67
+ df = df.rename(columns=rename_dict)
68
+
69
+ # Standardize ID column
70
+ if 'ID ANGGOTA' not in df.columns and 'ID' in df.columns:
71
+ df = df.rename(columns={'ID': 'ID ANGGOTA'})
72
+
73
+ # Standardize KEL column
74
+ if 'KEL' not in df.columns and 'KELOMPOK' in df.columns:
75
+ df = df.rename(columns={'KELOMPOK': 'KEL'})
76
+
77
+ # Hilangkan duplikasi kolom
78
+ df = df.loc[:, ~df.columns.duplicated()]
79
+
80
+ return df
81
+
82
+ def detect_delimiter(buffer: io.BytesIO) -> str:
83
+ """Detect CSV delimiter"""
84
+ pos = buffer.tell()
85
+ first_line = buffer.readline().decode('utf-8', errors='ignore')
86
+ buffer.seek(pos)
87
+ counts = {'\t': first_line.count('\t'), ';': first_line.count(';'), ',': first_line.count(',')}
88
+ return max(counts, key=counts.get)
89
+
90
+ def load_data(uploaded_file) -> pd.DataFrame:
91
+ """Load CSV/Excel file"""
92
+ try:
93
+ if uploaded_file.name.endswith(('.xlsx', '.xls')):
94
+ df = pd.read_excel(uploaded_file, engine='openpyxl')
95
+ else:
96
+ sep = detect_delimiter(uploaded_file)
97
+ df = pd.read_csv(uploaded_file, sep=sep)
98
+ return df
99
+ except Exception as e:
100
+ st.error(f"❌ Error loading file: {str(e)}")
101
+ return None
102
+
103
+ # ── TAB 1 FUNCTIONS: PROSES GABUNGAN & FINAL ─────────────────────────────────
104
+
105
+ def ambil_3_digit_akhir(val):
106
+ try:
107
+ if pd.isna(val):
108
+ return 0
109
+ return int(str(int(val))[-3:])
110
+ except Exception:
111
+ return 0
112
+
113
+ def estimasi_uang(val):
114
+ try:
115
+ if pd.isna(val):
116
+ return 0
117
+ return int(np.ceil(val / 1000.0) * 1000)
118
+ except Exception:
119
+ return 0
120
+
121
+ def estimasi_nabung_2(x):
122
+ return x - 500 if x > 500 else 0
123
+
124
+ def estimasi_nabung_3(x):
125
+ return x + 500 if x < 500 else 0
126
+
127
+ def tf_1(row):
128
+ if row["Estimasi Nabung 1"] < 500:
129
+ return (
130
+ (row["Estimasi Nabung 1"] == row["Estimasi Nominal Kecil Menabung"])
131
+ or (row["Estimasi Nabung 3"] == row["Estimasi Nominal Kecil Menabung"])
132
+ )
133
+ else:
134
+ return (
135
+ (row["Estimasi Nominal Kecil Menabung"] == row["Estimasi Nabung 1"])
136
+ or (row["Estimasi Nominal Kecil Menabung"] == row["Estimasi Nabung 2"])
137
+ )
138
+
139
+ def estimasi_penarikan_2(x):
140
+ return x - 500 if x > 500 else 0
141
+
142
+ def tf2(row):
143
+ if row["Estimasi Penarikan 1"] < 500:
144
+ return row["Estimasi Penarikan 1"] == row["Estimasi Nominal Kecil Penarikan"]
145
+ else:
146
+ return (
147
+ (row["Estimasi Nominal Kecil Penarikan"] == row["Estimasi Penarikan 1"])
148
+ or (row["Estimasi Nominal Kecil Penarikan"] == row["Estimasi Penarikan 2"])
149
+ )
150
+
151
+ def final_filter(row):
152
+ return bool(row["T/F 1"] or row["T/F2"])
153
+
154
+ def tambah_kolom_estimasi(df):
155
+ """Tambah kolom estimasi untuk anomali detection"""
156
+ df["Estimasi Nominal Kecil Menabung"] = df["Db Total"].apply(ambil_3_digit_akhir)
157
+ df["Estimasi Nominal Kecil Penarikan"] = df["Cr Total"].apply(ambil_3_digit_akhir)
158
+ df["Estimasi Uang"] = df["Db Total2"].apply(estimasi_uang)
159
+ df["Estimasi Nabung 1"] = df["Estimasi Uang"] - df["Db Total2"]
160
+ df["Estimasi Nabung 2"] = df["Estimasi Nabung 1"].apply(estimasi_nabung_2)
161
+ df["Estimasi Nabung 3"] = df["Estimasi Nabung 1"].apply(estimasi_nabung_3)
162
+ df["Estimasi Penarikan 1"] = df["Db Total2"].apply(ambil_3_digit_akhir)
163
+ df["Estimasi Penarikan 2"] = df["Estimasi Penarikan 1"].apply(estimasi_penarikan_2)
164
+ df["T/F 1"] = df.apply(tf_1, axis=1)
165
+ df["T/F2"] = df.apply(tf2, axis=1)
166
+ df["Final Filter"] = df.apply(final_filter, axis=1)
167
+ return df
168
+
169
+ # ── TAB 2 FUNCTIONS: ANALISA SIMPANAN ────────────────────────────────────────
170
+
171
+ def prepare_data_analisa(df_raw: pd.DataFrame) -> pd.DataFrame:
172
+ """Prepare data untuk analisa simpanan"""
173
+ try:
174
+ df = df_raw.copy()
175
+ col_mapping = {
176
+ 'ID': 'ID ANGGOTA',
177
+ 'KELOMPOK': 'KEL',
178
+ 'Db Hariraya': 'Db HariRaya',
179
+ 'Cr Hariraya': 'Cr HariRaya'
180
+ }
181
+ df = df.rename(columns=col_mapping)
182
+
183
+ # Validasi kolom minimal
184
+ required_cols = ['ID ANGGOTA', 'NAMA', 'CENTER', 'Db Sukarela', 'Cr Sukarela', 'TRANS. DATE']
185
+ missing = [c for c in required_cols if c not in df.columns]
186
+ if missing:
187
+ raise ValueError(f"Kolom tidak ditemukan: {missing}")
188
+
189
+ # Clean & convert
190
+ df['TRANS. DATE'] = pd.to_datetime(df['TRANS. DATE'], format='%d/%m/%Y', errors='coerce')
191
+
192
+ # Extract date features
193
+ df['MINGGU'] = df['TRANS. DATE'].dt.isocalendar().week.astype(int)
194
+ df['TAHUN'] = df['TRANS. DATE'].dt.year
195
+ df['YEAR_WEEK'] = df['TAHUN'].astype(str) + '-W' + df['MINGGU'].astype(str).str.zfill(2)
196
+
197
+ return df
198
+ except Exception as e:
199
+ st.error(f"❌ Error preparing data: {str(e)}")
200
+ return None
201
+
202
+ def detect_hariraya_anomaly(df: pd.DataFrame, window: int = 3, threshold: float = 1.0) -> pd.DataFrame:
203
+ """Detect HariRaya savings anomalies using Rolling Z-Score"""
204
+ try:
205
+ weekly = (
206
+ df.groupby(['ID ANGGOTA', 'NAMA', 'CENTER', 'YEAR_WEEK'], as_index=False)
207
+ .agg({'Db HariRaya': 'sum', 'TRANS. DATE': 'first'})
208
+ .sort_values(['ID ANGGOTA', 'YEAR_WEEK'])
209
+ .reset_index(drop=True)
210
+ )
211
+
212
+ records = []
213
+ for id_val, group in weekly.groupby('ID ANGGOTA', sort=False):
214
+ g = group.sort_values('YEAR_WEEK').copy()
215
+ g['Rolling_Mean'] = g['Db HariRaya'].rolling(window=window, min_periods=1).mean()
216
+ g['Rolling_Std'] = g['Db HariRaya'].rolling(window=window, min_periods=1).std().fillna(0)
217
+ g['Z_Score'] = np.where(
218
+ g['Rolling_Std'] > 0,
219
+ (g['Db HariRaya'] - g['Rolling_Mean']) / g['Rolling_Std'],
220
+ 0
221
+ )
222
+ g['Anomaly_HariRaya'] = (np.abs(g['Z_Score']) > threshold).astype(int)
223
+ records.append(g)
224
+
225
+ return pd.concat(records, ignore_index=True) if records else pd.DataFrame()
226
+ except Exception as e:
227
+ st.warning(f"⚠️ Error detecting HariRaya anomalies: {str(e)}")
228
+ return pd.DataFrame()
229
+
230
+ def detect_sukarela_anomaly(df: pd.DataFrame, scaler, iso_forest, feature_cols: list) -> pd.DataFrame:
231
+ """Detect Sukarela savings anomalies using Isolation Forest"""
232
+ try:
233
+ # Check if columns exist
234
+ if 'Db Sukarela' not in df.columns or 'Cr Sukarela' not in df.columns:
235
+ st.error("❌ Kolom 'Db Sukarela' atau 'Cr Sukarela' tidak ditemukan")
236
+ return pd.DataFrame()
237
+
238
+ # Aggregate dengan named aggregation (syntax pandas yang benar)
239
+ agg = (
240
+ df.groupby(['ID ANGGOTA', 'NAMA', 'CENTER'], as_index=False)
241
+ .agg(
242
+ Db_Sukarela_Total=('Db Sukarela', 'sum'),
243
+ Db_Sukarela_Avg=('Db Sukarela', 'mean'),
244
+ Db_Sukarela_Std=('Db Sukarela', 'std'),
245
+ Db_Sukarela_Max=('Db Sukarela', 'max'),
246
+ Cr_Sukarela_Total=('Cr Sukarela', 'sum'),
247
+ Cr_Sukarela_Avg=('Cr Sukarela', 'mean'),
248
+ Cr_Sukarela_Std=('Cr Sukarela', 'std'),
249
+ Cr_Sukarela_Max=('Cr Sukarela', 'max'),
250
+ )
251
+ .fillna(0)
252
+ )
253
+
254
+ # Feature columns untuk ML model
255
+ feature_cols_actual = [
256
+ 'Db_Sukarela_Total', 'Db_Sukarela_Avg', 'Db_Sukarela_Std', 'Db_Sukarela_Max',
257
+ 'Cr_Sukarela_Total', 'Cr_Sukarela_Avg', 'Cr_Sukarela_Std', 'Cr_Sukarela_Max'
258
+ ]
259
+
260
+ # Scale features
261
+ features_scaled = scaler.transform(agg[feature_cols_actual])
262
+
263
+ # Predict anomalies
264
+ agg['Anomaly_Sukarela'] = iso_forest.predict(features_scaled)
265
+ agg['Anomaly_Sukarela'] = (agg['Anomaly_Sukarela'] == -1).astype(int)
266
+
267
+ return agg
268
+ except Exception as e:
269
+ st.warning(f"⚠️ Error detecting Sukarela anomalies: {str(e)}")
270
+ import traceback
271
+ st.error(f"Debug info: {traceback.format_exc()}")
272
+ return pd.DataFrame()
273
+
274
+ # ── MAIN APP ─────────────────────────────────────────────────────────────────
275
+
276
+ tab1, tab2 = st.tabs(["πŸ“Š Proses Gabungan & Final", "πŸ“‹ Analisa Simpanan"])
277
+
278
+ # ── TAB 1: PROSES GABUNGAN & FINAL ───────────────────────────────────────────
279
+ with tab1:
280
+ st.subheader("πŸ”„ Tahap 1: Merge File")
281
+ st.write("1️⃣ Upload 4 file Excel (THC FINAL, TAK, TLP, KDP) ini ambil dari hasil penarikan database")
282
+ st.write("2️⃣ Proses Gabungan (Merge semua file dari penarikan Database)")
283
+ st.write("3️⃣ Proses Final (Estimasi & Anomali Detection)")
284
+ st.divider()
285
+
286
+ uploaded_files = st.file_uploader(
287
+ "πŸ“€ Unggah 4 file Excel THC FINAL.xlsx, TAK.xlsx, TLP.xlsx, KDP.xlsx",
288
+ accept_multiple_files=True,
289
+ type=["xlsx"],
290
+ key="tab1_upload"
291
+ )
292
+
293
+ if uploaded_files:
294
+ try:
295
+ dfs = {file.name: load_excel(file) for file in uploaded_files}
296
+ combined_df_list = []
297
+
298
+ # Process THC FINAL
299
+ if 'THC FINAL.xlsx' in dfs:
300
+ df_thc = dfs['THC FINAL.xlsx']
301
+ df_thc = process_dataframe(df_thc, [], {})
302
+ combined_df_list.append(df_thc)
303
+ st.success("βœ“ THC FINAL.xlsx diproses")
304
+
305
+ if 'TAK.xlsx' in dfs:
306
+ df_tak = dfs['TAK.xlsx']
307
+ rename_dict_tak = {
308
+ 'KELOMPOK': 'KEL', 'DEBIT_PINJAMAN ARTA': 'Db PRT', 'DEBIT_PINJAMAN DT. PENDIDIKAN': 'Db DTP',
309
+ 'DEBIT_PINJAMAN MIKROBISNIS': 'Db PMB', 'DEBIT_PINJAMAN SANITASI': 'Db PSA',
310
+ 'DEBIT_PINJAMAN UMUM': 'Db PU', 'DEBIT_PINJAMAN RENOVASI RUMAH': 'Db PRR',
311
+ 'DEBIT_PINJAMAN PERTANIAN': 'Db PTN', 'DEBIT_TOTAL': 'Db Total2',
312
+ 'CREDIT_PINJAMAN ARTA': 'Cr PRT', 'CREDIT_PINJAMAN DT. PENDIDIKAN': 'Cr DTP',
313
+ 'CREDIT_PINJAMAN MIKROBISNIS': 'Cr PMB', 'CREDIT_PINJAMAN SANITASI': 'Cr PSA',
314
+ 'CREDIT_PINJAMAN UMUM': 'Cr PU', 'CREDIT_PINJAMAN RENOVASI RUMAH': 'Cr PRR',
315
+ 'CREDIT_PINJAMAN PERTANIAN': 'Cr PTN', 'CREDIT_TOTAL': 'Cr Total2'
316
+ }
317
+ df_tak = process_dataframe(df_tak, list(rename_dict_tak.keys()), rename_dict_tak)
318
+ combined_df_list.append(df_tak)
319
+ st.success("βœ“ TAK.xlsx diproses")
320
+
321
+ if 'TLP.xlsx' in dfs:
322
+ df_tlp = dfs['TLP.xlsx']
323
+ rename_dict_tlp = {
324
+ 'KELOMPOK': 'KEL', 'DEBIT_Simpanan Hari Raya': 'Db HariRaya',
325
+ 'DEBIT_Simpanan Pensiun': 'Db Pensiun', 'DEBIT_Simpanan Pokok': 'Db Pokok',
326
+ 'DEBIT_Simpanan Sukarela': 'Db Sukarela', 'DEBIT_Simpanan Wajib': 'Db Wajib',
327
+ 'DEBIT_Simpanan Qurban': 'Db Qurban', 'DEBIT_Simpanan Sipadan': 'Db SIPADAN',
328
+ 'DEBIT_Simpanan Khusus': 'Db Khusus', 'DEBIT_TOTAL': 'Db Total',
329
+ 'CREDIT_Simpanan Hari Raya': 'Cr HariRaya', 'CREDIT_Simpanan Pensiun': 'Cr Pensiun',
330
+ 'CREDIT_Simpanan Pokok': 'Cr Pokok', 'CREDIT_Simpanan Sukarela': 'Cr Sukarela',
331
+ 'CREDIT_Simpanan Wajib': 'Cr Wajib', 'CREDIT_Simpanan Qurban': 'Cr Qurban',
332
+ 'CREDIT_Simpanan Sipadan': 'Cr SIPADAN', 'CREDIT_Simpanan Khusus': 'Cr Khusus',
333
+ 'CREDIT_TOTAL': 'Cr Total'
334
+ }
335
+ df_tlp = process_dataframe(df_tlp, list(rename_dict_tlp.keys()), rename_dict_tlp)
336
+ combined_df_list.append(df_tlp)
337
+ st.success("βœ“ TLP.xlsx diproses")
338
+
339
+ if 'KDP.xlsx' in dfs:
340
+ df_kdp = dfs['KDP.xlsx']
341
+ rename_dict_kdp = {
342
+ 'KELOMPOK': 'KEL', 'DEBIT_Simpanan Hari Raya': 'Db HariRaya',
343
+ 'DEBIT_Simpanan Pensiun': 'Db Pensiun', 'DEBIT_Simpanan Pokok': 'Db Pokok',
344
+ 'DEBIT_Simpanan Sukarela': 'Db Sukarela', 'DEBIT_Simpanan Wajib': 'Db Wajib',
345
+ 'DEBIT_Simpanan Qurban': 'Db Qurban', 'DEBIT_Simpanan Sipadan': 'Db SIPADAN',
346
+ 'DEBIT_Simpanan Khusus': 'Db Khusus', 'DEBIT_TOTAL': 'Db Total',
347
+ 'CREDIT_Simpanan Hari Raya': 'Cr HariRaya', 'CREDIT_Simpanan Pensiun': 'Cr Pensiun',
348
+ 'CREDIT_Simpanan Pokok': 'Cr Pokok', 'CREDIT_Simpanan Sukarela': 'Cr Sukarela',
349
+ 'CREDIT_Simpanan Wajib': 'Cr Wajib', 'CREDIT_Simpanan Qurban': 'Cr Qurban',
350
+ 'CREDIT_Simpanan Sipadan': 'Cr SIPADAN', 'CREDIT_Simpanan Khusus': 'Cr Khusus',
351
+ 'CREDIT_TOTAL': 'Cr Total', 'DEBIT_PU': 'Db PU', 'CREDIT_PU': 'Cr PU',
352
+ 'DEBIT_TOTAL2': 'Db Total2', 'CREDIT_TOTAL2': 'Cr Total2'
353
+ }
354
+ df_kdp = process_dataframe(df_kdp, list(rename_dict_kdp.keys()), rename_dict_kdp)
355
+ combined_df_list.append(df_kdp)
356
+ st.success("βœ“ KDP.xlsx diproses")
357
+
358
+ # --- PROSES GABUNGAN ---
359
+ # 1. Bersihkan setiap dataframe di awal
360
+ cleaned_list = []
361
+ for name, df in dfs.items():
362
+ df = df.loc[:, ~df.columns.duplicated()].copy()
363
+ df = df.reset_index(drop=True)
364
+ cleaned_list.append(df)
365
+
366
+ # 2. Gabungkan (Concat)
367
+ combined_df = pd.concat(cleaned_list, ignore_index=True, sort=False)
368
+ combined_df = combined_df.reset_index(drop=True)
369
+ combined_df = combined_df.loc[:, ~combined_df.columns.duplicated()].copy()
370
+
371
+ # 3. Rename & Standarisasi
372
+ col_mapping = {
373
+ 'ID': 'ID ANGGOTA', 'KELOMPOK': 'KEL',
374
+ 'Db Sihara': 'Db HariRaya', 'Cr Sihara': 'Cr HariRaya',
375
+ 'Db Hariraya': 'Db HariRaya', 'Cr Hariraya': 'Cr HariRaya',
376
+ 'Db Total 2': 'Db Total2', 'Cr Total 2': 'Cr Total2',
377
+ 'Db Total Simpanan': 'Db Total', 'Cr Total Simpanan': 'Cr Total',
378
+ 'Db Total Pinjaman': 'Db Total2', 'Cr Total Pinjaman': 'Cr Total2'
379
+ }
380
+ combined_df = combined_df.rename(columns=col_mapping)
381
+ # Penting: Buang duplikat SETELAH rename
382
+ combined_df = combined_df.loc[:, ~combined_df.columns.duplicated()].copy()
383
+
384
+ # 4. Susun Kolom (Metode Rekonstruksi - Anti Reindex Error)
385
+ final_df = pd.DataFrame(index=combined_df.index)
386
+
387
+ # Tentukan daftar kolom yang diinginkan (Desired + Others)
388
+ existing_cols = [c for c in DESIRED_ORDER if c in combined_df.columns]
389
+ other_cols = [c for c in combined_df.columns if c not in DESIRED_ORDER]
390
+ all_target_cols = list(dict.fromkeys(DESIRED_ORDER + other_cols))
391
+
392
+ for col in all_target_cols:
393
+ if col in combined_df.columns:
394
+ col_data = combined_df[col]
395
+ # Jika masih ada duplikat (kemungkinan kecil), ambil kolom pertama
396
+ if isinstance(col_data, pd.DataFrame):
397
+ final_df[col] = col_data.iloc[:, 0]
398
+ else:
399
+ final_df[col] = col_data
400
+ else:
401
+ # Jika kolom tidak ada di file asli, isi 0
402
+ final_df[col] = 0
403
+
404
+ combined_df = final_df.copy()
405
+
406
+ st.divider()
407
+ st.subheader("πŸ” Tahap 2: Proses Final (Estimasi & Anomali)")
408
+
409
+ # 5. Tambah Estimasi
410
+ df_hasil_raw = tambah_kolom_estimasi(combined_df.copy())
411
+
412
+ # 6. Susun Kolom Akhir (Metode Rekonstruksi)
413
+ final_col_order = list(dict.fromkeys(DESIRED_ORDER + ESTIMASI_COLS))
414
+ df_final = pd.DataFrame(index=df_hasil_raw.index)
415
+
416
+ for col in final_col_order:
417
+ if col in df_hasil_raw.columns:
418
+ col_data = df_hasil_raw[col]
419
+ if isinstance(col_data, pd.DataFrame):
420
+ df_final[col] = col_data.iloc[:, 0]
421
+ else:
422
+ df_final[col] = col_data
423
+ else:
424
+ df_final[col] = 0
425
+
426
+ df_hasil = df_final.copy()
427
+
428
+ # Metrics
429
+ col1, col2 = st.columns(2)
430
+ with col1:
431
+ st.metric("πŸ“Š Total Baris Data", len(df_hasil))
432
+ with col2:
433
+ anomali_count = df_hasil["Final Filter"].sum()
434
+ st.metric("⚠️ Anomali Terdeteksi", int(anomali_count))
435
+
436
+ st.divider()
437
+ st.write("πŸ“‹ Preview hasil akhir (20 baris pertama):")
438
+ st.dataframe(df_hasil.head(20), use_container_width=True)
439
+
440
+ # Download
441
+ output = io.BytesIO()
442
+ with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
443
+ df_hasil.to_excel(writer, index=False, sheet_name='THC Hasil')
444
+ output.seek(0)
445
+
446
+ st.download_button(
447
+ label="πŸ“₯ Download Data Lengkap (Estimasi + Final Filter)",
448
+ data=output.getvalue(),
449
+ file_name="THC_Gabungan_dan_Final_Hasil.xlsx",
450
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
451
+ )
452
+
453
+ except Exception as e:
454
+ import traceback
455
+ st.error(f"❌ Error: {str(e)}")
456
+ st.code(traceback.format_exc())
457
+ # Debug: Cek kolom duplikat
458
+ if 'duplicate labels' in str(e).lower():
459
+ st.warning("⚠️ Terdeteksi kolom ganda. Cek daftar kolom ini:")
460
+ st.write(combined_df.columns[combined_df.columns.duplicated()].tolist())
461
+
462
+ # ── TAB 2: ANALISA SIMPANAN ──────────────────────────────────────────────────
463
+ with tab2:
464
+ st.subheader("πŸ” Analisa Simpanan - Anomali Detection")
465
+ st.write("Upload data dari Proses Gabungan & Final")
466
+ st.divider()
467
+
468
+ # Load models
469
+ scaler, iso_forest, metadata = load_models_and_metadata()
470
+
471
+ if scaler is None or iso_forest is None:
472
+ st.error("❌ Model tidak tersedia di tools/. Pastikan scaler.pkl dan isolation_forest.pkl ada.")
473
+ else:
474
+ feature_cols = metadata.get('feature_cols', [
475
+ 'Db_Sukarela_Total', 'Db_Sukarela_Avg', 'Db_Sukarela_Std', 'Db_Sukarela_Max',
476
+ 'Cr_Sukarela_Total', 'Cr_Sukarela_Avg', 'Cr_Sukarela_Std', 'Cr_Sukarela_Max'
477
+ ])
478
+
479
+ # File upload
480
+ uploaded_file = st.file_uploader(
481
+ "πŸ“€ Upload File (CSV/Excel)",
482
+ type=['csv', 'xlsx', 'xls'],
483
+ key="tab2_upload"
484
+ )
485
+
486
+ if uploaded_file is not None:
487
+ df_raw = load_data(uploaded_file)
488
+ if df_raw is None:
489
+ st.stop()
490
+
491
+ df_prep = prepare_data_analisa(df_raw)
492
+ if df_prep is None:
493
+ st.stop()
494
+
495
+ st.success(f"βœ“ Data berhasil dimuat: {len(df_prep)} transaksi dari {df_prep['ID ANGGOTA'].nunique()} anggota")
496
+
497
+ # Anomaly detection
498
+ st.subheader("πŸ” Menjalankan Anomali Deteksi...")
499
+
500
+ col1, col2 = st.columns(2)
501
+
502
+ with col1:
503
+ st.write("πŸ”Ή HariRaya Anomaly (Rolling Z-Score)...")
504
+ hariraya_results = detect_hariraya_anomaly(df_prep, window=3, threshold=1.0)
505
+ hariraya_anomalies = hariraya_results[hariraya_results['Anomaly_HariRaya'] == 1] if len(hariraya_results) > 0 else pd.DataFrame()
506
+ st.metric("Anomali HariRaya", len(hariraya_anomalies))
507
+
508
+ with col2:
509
+ st.write("πŸ”Ή Sukarela Anomaly (Isolation Forest)...")
510
+ sukarela_results = detect_sukarela_anomaly(df_prep, scaler, iso_forest, [])
511
+ sukarela_anomalies = sukarela_results[sukarela_results['Anomaly_Sukarela'] == 1] if len(sukarela_results) > 0 else pd.DataFrame()
512
+ st.metric("Anomali Sukarela", len(sukarela_anomalies))
513
+
514
+ st.divider()
515
+
516
+ # Results tabs
517
+ st.subheader("πŸ“Š Detail Hasil Anomali")
518
+
519
+ tab2_1, tab2_2, tab2_3 = st.tabs(["πŸ”΄ HariRaya Anomalies", "🟑 Sukarela Anomalies", "πŸ“‹ Summary"])
520
+
521
+ with tab2_1:
522
+ if len(hariraya_anomalies) > 0:
523
+ display_cols = [col for col in ['ID ANGGOTA', 'NAMA', 'CENTER', 'Db HariRaya', 'Z_Score', 'YEAR_WEEK']
524
+ if col in hariraya_anomalies.columns]
525
+ st.dataframe(
526
+ hariraya_anomalies[display_cols].sort_values('Z_Score', ascending=False),
527
+ use_container_width=True
528
+ )
529
+
530
+ # Chart
531
+ if len(hariraya_anomalies) > 0:
532
+ chart = alt.Chart(hariraya_anomalies).mark_bar().encode(
533
+ x='ID ANGGOTA:N',
534
+ y='Db HariRaya:Q',
535
+ color=alt.value('red')
536
+ ).properties(height=400, title="HariRaya Anomalies")
537
+ st.altair_chart(chart, use_container_width=True)
538
+ else:
539
+ st.info("βœ“ Tidak ada anomali HariRaya terdeteksi")
540
+
541
+ with tab2_2:
542
+ if len(sukarela_anomalies) > 0:
543
+ st.dataframe(
544
+ sukarela_anomalies[[
545
+ 'ID ANGGOTA', 'NAMA', 'CENTER',
546
+ 'Db_Sukarela_Total', 'Cr_Sukarela_Total',
547
+ 'Db_Sukarela_Avg', 'Cr_Sukarela_Avg'
548
+ ]].sort_values('Db_Sukarela_Total', ascending=False),
549
+ use_container_width=True
550
+ )
551
+
552
+ # Chart
553
+ chart = alt.Chart(sukarela_anomalies).mark_circle(size=100).encode(
554
+ x='Db_Sukarela_Avg:Q',
555
+ y='Cr_Sukarela_Avg:Q',
556
+ color=alt.value('red'),
557
+ tooltip=['ID ANGGOTA', 'NAMA', 'Db_Sukarela_Total']
558
+ ).properties(height=400, title="Sukarela Anomalies (Debit vs Kredit)")
559
+ st.altair_chart(chart, use_container_width=True)
560
+ else:
561
+ st.info("βœ“ Tidak ada anomali Sukarela terdeteksi")
562
+
563
+ with tab2_3:
564
+ st.write("**Ringkasan Anomali Terdeteksi:**")
565
+ summary_data = {
566
+ "Tipe Anomali": ["HariRaya", "Sukarela"],
567
+ "Jumlah Anomali": [len(hariraya_anomalies), len(sukarela_anomalies)],
568
+ "% dari Total": [
569
+ f"{(len(hariraya_anomalies)/len(hariraya_results)*100):.2f}%" if len(hariraya_results) > 0 else "0%",
570
+ f"{(len(sukarela_anomalies)/len(sukarela_results)*100):.2f}%" if len(sukarela_results) > 0 else "0%"
571
+ ]
572
+ }
573
+ st.dataframe(pd.DataFrame(summary_data), use_container_width=True)
574
+
575
+ st.divider()
576
+
577
+ # Export
578
+ st.subheader("πŸ’Ύ Export Hasil")
579
+
580
+ col1, col2 = st.columns(2)
581
+
582
+ with col1:
583
+ if len(hariraya_anomalies) > 0:
584
+ excel_buffer = io.BytesIO()
585
+ with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
586
+ hariraya_anomalies.to_excel(writer, sheet_name='HariRaya', index=False)
587
+ excel_buffer.seek(0)
588
+ st.download_button(
589
+ label="πŸ“₯ HariRaya Anomalies",
590
+ data=excel_buffer.getvalue(),
591
+ file_name="Analisa_HariRaya_Anomalies.xlsx",
592
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
593
+ )
594
+ else:
595
+ st.info("Tidak ada data HariRaya")
596
+
597
+ with col2:
598
+ if len(sukarela_anomalies) > 0:
599
+ excel_buffer = io.BytesIO()
600
+ with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
601
+ sukarela_anomalies.to_excel(writer, sheet_name='Sukarela', index=False)
602
+ excel_buffer.seek(0)
603
+ st.download_button(
604
+ label="πŸ“₯ Sukarela Anomalies",
605
+ data=excel_buffer.getvalue(),
606
+ file_name="Analisa_Sukarela_Anomalies.xlsx",
607
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
608
+ )
609
+ else:
610
+ st.info("Tidak ada data Sukarela")
611
+ else:
612
+ st.info("πŸ‘† Upload file untuk analisa simpanan")
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.28.0
2
+ pandas>=2.1.0
3
+ numpy
4
+ openpyxl
5
+ xlsxwriter
6
+ pyarrow>=12.0.0
7
+ scikit-learn
8
+ altair
9
+ joblib
tools/README.md ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ”§ Tools Directory
2
+
3
+ Folder ini berisi model machine learning dan konfigurasi untuk anomali detection.
4
+
5
+ ---
6
+
7
+ ## πŸ“¦ Files Required
8
+
9
+ ```
10
+ tools/
11
+ β”œβ”€β”€ scaler.pkl ← StandardScaler model (binary)
12
+ β”œβ”€β”€ isolation_forest.pkl ← Isolation Forest model (binary)
13
+ β”œβ”€β”€ metadata.json ← Configuration
14
+ β”œβ”€β”€ model_train.py ← Training script (optional)
15
+ └── simpanan_analisis.py ← Helper functions (optional)
16
+ ```
17
+
18
+ ---
19
+
20
+ ## πŸ“‹ File Descriptions
21
+
22
+ ### scaler.pkl
23
+
24
+ - **Type:** Binary (Pickle format)
25
+ - **Purpose:** Standardize features untuk ML model
26
+ - **Used by:** Tab 2 (Analisa Simpanan)
27
+ - **Size:** ~1-2 KB
28
+
29
+ ### isolation_forest.pkl
30
+
31
+ - **Type:** Binary (Pickle format)
32
+ - **Purpose:** Pre-trained Isolation Forest model untuk anomali detection
33
+ - **Used by:** Tab 2 (Analisa Simpanan)
34
+ - **Features:** 8 fitur (Db_Sukarela dan Cr_Sukarela aggregates)
35
+ - **Size:** ~10-50 KB
36
+
37
+ ### metadata.json
38
+
39
+ - **Type:** JSON (Text)
40
+ - **Purpose:** Konfigurasi model dan parameter
41
+ - **Content:**
42
+ ```json
43
+ {
44
+ "feature_cols": [...], // Nama kolom untuk model
45
+ "rolling_zscore_threshold": 1.0, // Threshold untuk Z-Score
46
+ "rolling_window": 3 // Window size untuk rolling aggregation
47
+ }
48
+ ```
49
+
50
+ ### model_train.py (Optional)
51
+
52
+ Script untuk melatih ulang model jika diperlukan.
53
+
54
+ ### simpanan_analisis.py (Optional)
55
+
56
+ Helper functions untuk analisa simpanan.
57
+
58
+ ---
59
+
60
+ ## ⚠️ Important Notes
61
+
62
+ ### Binary Model Files (.pkl)
63
+
64
+ - File `scaler.pkl` dan `isolation_forest.pkl` HARUS ada untuk Tab 2 berfungsi
65
+ - Files ini di-generate saat training, bukan di-create manual
66
+ - Jangan di-edit manual - format binary
67
+
68
+ ### File Locations
69
+
70
+ - App mencari file di path relatif: `../tools/` (dari app folder)
71
+ - Pastikan struktur folder sesuai template
72
+
73
+ ### Model Updates
74
+
75
+ Jika ingin update model (retrain):
76
+
77
+ 1. Jalankan `python tools/model_train.py`
78
+ 2. File `.pkl` baru akan di-generate
79
+ 3. Restart aplikasi
80
+
81
+ ---
82
+
83
+ ## πŸ” Troubleshooting
84
+
85
+ ### Error: "Model tidak ditemukan"
86
+
87
+ ```
88
+ Solusi:
89
+ 1. Pastikan scaler.pkl ada di tools/
90
+ 2. Pastikan isolation_forest.pkl ada di tools/
91
+ 3. Check file paths (case-sensitive di Linux/Mac)
92
+ ```
93
+
94
+ ### Error: "Model load failed"
95
+
96
+ ```
97
+ Solusi:
98
+ 1. Pastikan Python version sama saat training dan running
99
+ 2. Pastikan scikit-learn version sama
100
+ 3. Re-train model jika masih error
101
+ ```
102
+
103
+ ---
104
+
105
+ ## πŸ“Š Model Info
106
+
107
+ ### Isolation Forest
108
+
109
+ - **Algorithm:** Anomaly detection via Isolation Forest
110
+ - **Training data:** Historical simpanan transactions
111
+ - **Features:** 8 aggregated metrics
112
+ - Db_Sukarela_Total, Db_Sukarela_Avg, Db_Sukarela_Std, Db_Sukarela_Max
113
+ - Cr_Sukarela_Total, Cr_Sukarela_Avg, Cr_Sukarela_Std, Cr_Sukarela_Max
114
+ - **Output:** Binary (Anomaly: -1, Normal: 1)
115
+
116
+ ### StandardScaler
117
+
118
+ - **Purpose:** Feature normalization
119
+ - **Fitted on:** Historical data statistics
120
+ - **Usage:** Pre-process features before model prediction
121
+
122
+ ---
123
+
124
+ ## πŸš€ Quick Check
125
+
126
+ Verify tools setup:
127
+
128
+ ```bash
129
+ cd THC_APP
130
+ python -c "import joblib; print('scaler:', joblib.load('tools/scaler.pkl')); print('model:', joblib.load('tools/isolation_forest.pkl'))"
131
+ ```
132
+
133
+ Success jika output menunjukkan objects tanpa error.
134
+
135
+ ---
136
+
137
+ **Last Updated:** May 2026
tools/isolation_forest.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:72be34b5739efdb19152074b352fc76dca7f970aa0647f35abcca51732c64c0d
3
+ size 706553
tools/metadata.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "feature_cols": [
3
+ "Db_Sukarela_Total",
4
+ "Db_Sukarela_Avg",
5
+ "Db_Sukarela_Std",
6
+ "Db_Sukarela_Max",
7
+ "Cr_Sukarela_Total",
8
+ "Cr_Sukarela_Avg",
9
+ "Cr_Sukarela_Std",
10
+ "Cr_Sukarela_Max"
11
+ ]
12
+ }
tools/scaler.pkl ADDED
Binary file (807 Bytes). View file