ilafi commited on
Commit
2ec5545
·
verified ·
1 Parent(s): 8429bdb

Upload 13 files

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ DM_001.xlsx filter=lfs diff=lfs merge=lfs -text
2
+ IPLM_clean_manual_131225.xlsx filter=lfs diff=lfs merge=lfs -text
3
+ IPLM_clean_Manual.xlsx filter=lfs diff=lfs merge=lfs -text
DM_001.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2fa184564d92e1ef3fdf054b49b175e7c873b13ea9400f28e2acebf7d5db6975
3
+ size 19492069
Data_populasi_Kab_kota.xlsx ADDED
Binary file (74.8 kB). View file
 
Data_populasi_propinsi.xlsx ADDED
Binary file (15.6 kB). View file
 
IPLM_clean_Manual.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:08a933980244eb97e0dbc132c1054e48a97a73f65d15073ae5cf162f974234f8
3
+ size 19944587
IPLM_clean_manual_131225.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f3627b56829ec5e6d34cf880cf9ff260dd9ac0ba274e70b96215a4327df1f93d
3
+ size 21234517
README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: IPLM DM
3
+ emoji: 🌖
4
+ colorFrom: gray
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 6.1.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
SD-SMP-kab.xlsx ADDED
Binary file (32 kB). View file
 
SMA.xlsx ADDED
Binary file (27.7 kB). View file
 
app.py ADDED
@@ -0,0 +1,859 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ app.py — Dashboard Kekurangan Sampel IPLM (TANPA HITUNG INDEKS)
4
+ FIX FULL:
5
+ - Target 68% diambil dari META:
6
+ * Kab/Kota: kolom sampel_total
7
+ * Provinsi: kolom total _sampel (atau variasinya)
8
+ - Normalisasi label diperkuat:
9
+ * kab/kota: hapus kata "DAN", seragamkan KAB/KOTA, buang simbol
10
+ * provinsi: buang prefix "PROVINSI/PROPINSI", buang simbol
11
+ - Jika META tidak match:
12
+ * ditandai META_MATCH="TIDAK" + Target NaN (bukan 0), supaya tidak menyesatkan
13
+ """
14
+
15
+ import os
16
+ import re
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ import gradio as gr
21
+ import numpy as np
22
+ import pandas as pd
23
+ import plotly.graph_objects as go
24
+ from huggingface_hub import InferenceClient
25
+
26
+ from docx import Document
27
+
28
+ import plotly.express as px
29
+ try:
30
+ import kaleido # noqa: F401
31
+ HAS_KALEIDO = True
32
+ except Exception:
33
+ HAS_KALEIDO = False
34
+
35
+
36
+ # ============================================================
37
+ # 1) KONFIGURASI FILE
38
+ # ============================================================
39
+ DATA_FILE = "IPLM_clean_manual_131225.xlsx"
40
+ META_KAB_FILE = "Data_populasi_Kab_kota.xlsx"
41
+ META_PROV_FILE = "Data_populasi_propinsi.xlsx"
42
+
43
+ TARGET_COVERAGE = 0.68
44
+
45
+ # ============================================================
46
+ # 1b) LLM
47
+ # ============================================================
48
+ USE_LLM = True
49
+ LLM_MODEL_NAME = "meta-llama/Meta-Llama-3-8B-Instruct"
50
+ HF_TOKEN = (
51
+ os.getenv("HF_SECRET")
52
+ or os.getenv("HUGGINGFACEHUB_API_TOKEN")
53
+ or os.getenv("HF_API_TOKEN")
54
+ )
55
+
56
+ _HF_CLIENT = None
57
+ def get_llm_client():
58
+ global _HF_CLIENT
59
+ if _HF_CLIENT is not None:
60
+ return _HF_CLIENT
61
+ try:
62
+ if HF_TOKEN:
63
+ _HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN)
64
+ else:
65
+ _HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME)
66
+ return _HF_CLIENT
67
+ except Exception:
68
+ _HF_CLIENT = None
69
+ return None
70
+
71
+
72
+ # ============================================================
73
+ # 2) UTIL
74
+ # ============================================================
75
+ def _canon(s: str) -> str:
76
+ return re.sub(r"[^a-z0-9]+", "", str(s).lower())
77
+
78
+ def pick_col(df, candidates):
79
+ for c in candidates:
80
+ if c in df.columns:
81
+ return c
82
+ can_map = {_canon(c): c for c in df.columns}
83
+ for c in candidates:
84
+ k = _canon(c)
85
+ if k in can_map:
86
+ return can_map[k]
87
+ return None
88
+
89
+ def coerce_num(val):
90
+ if pd.isna(val):
91
+ return np.nan
92
+ t = str(val).strip()
93
+ if t == "" or t in {"-", "–", "—"}:
94
+ return np.nan
95
+ t = t.replace("\u00a0", " ").replace("Rp", "").replace("%", "")
96
+ t = re.sub(r"[^0-9,.\-]", "", t)
97
+ if t.count(".") > 1 and t.count(",") == 1:
98
+ t = t.replace(".", "").replace(",", ".")
99
+ elif t.count(",") > 1 and t.count(".") == 1:
100
+ t = t.replace(",", "")
101
+ elif t.count(",") == 1 and t.count(".") == 0:
102
+ t = t.replace(",", ".")
103
+ else:
104
+ t = t.replace(",", "")
105
+ try:
106
+ return float(t)
107
+ except Exception:
108
+ return np.nan
109
+
110
+ def norm_kew(v):
111
+ if pd.isna(v):
112
+ return None
113
+ t = str(v).strip().upper()
114
+ if "KAB" in t or "KOTA" in t:
115
+ return "KAB/KOTA"
116
+ if "PROV" in t:
117
+ return "PROVINSI"
118
+ if "PUSAT" in t or "NASIONAL" in t:
119
+ return "PUSAT"
120
+ return t
121
+
122
+ def _norm_text(x):
123
+ if pd.isna(x):
124
+ return None
125
+ t = str(x).strip().upper()
126
+ return " ".join(t.split())
127
+
128
+ # ---- Normalisasi PROV (untuk join) ----
129
+ def norm_prov_label(s):
130
+ if pd.isna(s):
131
+ return None
132
+ t = str(s).upper().strip()
133
+ t = " ".join(t.split())
134
+ # buang prefix
135
+ t = re.sub(r"^\s*(PROVINSI|PROPINSI)\s+", "", t)
136
+ # buang tanda baca
137
+ t = re.sub(r"[^A-Z0-9 ]+", " ", t)
138
+ t = " ".join(t.split())
139
+ # key
140
+ return re.sub(r"[^A-Z0-9]+", "", t)
141
+
142
+ # ---- Normalisasi KAB/KOTA (untuk join) ----
143
+ def norm_kab_label(s):
144
+ """
145
+ FIX UTAMA:
146
+ - Samakan variasi "KABUPATEN/KAB./KAB" dan "KOTA ADM./KOTA ADMINISTRASI"
147
+ - Hapus kata 'DAN' agar match kasus: "PANGKAJENE DAN KEPULAUAN" vs "PANGKAJENE KEPULAUAN"
148
+ - Buang simbol, spasi ganda
149
+ """
150
+ if pd.isna(s):
151
+ return None
152
+ t = str(s).upper().strip()
153
+ t = " ".join(t.split())
154
+
155
+ # seragamkan kab/kota
156
+ t = t.replace("KABUPATEN", "KAB")
157
+ t = t.replace("KAB.", "KAB")
158
+ t = t.replace("KOTA ADMINISTRASI", "KOTA")
159
+ t = t.replace("KOTA ADM.", "KOTA")
160
+ t = t.replace("KOTA.", "KOTA")
161
+
162
+ # FIX: buang "DAN" sebagai stopword join
163
+ t = re.sub(r"\bDAN\b", " ", t)
164
+
165
+ # bersihin simbol
166
+ t = re.sub(r"[^A-Z0-9 ]+", " ", t)
167
+ t = " ".join(t.split())
168
+
169
+ return re.sub(r"[^A-Z0-9]+", "", t)
170
+
171
+ # ---- Display bersih (untuk dropdown/UI) ----
172
+ def clean_prov_display(s):
173
+ if pd.isna(s):
174
+ return None
175
+ t = str(s).upper().strip()
176
+ t = " ".join(t.split())
177
+ t = t.replace("PROPINSI", "PROVINSI")
178
+ while t.startswith("PROVINSI PROVINSI "):
179
+ t = t.replace("PROVINSI PROVINSI ", "PROVINSI ", 1)
180
+ t = t.replace("PROVINSI PROVINSI ", "PROVINSI ")
181
+ if not t.startswith("PROVINSI "):
182
+ t = "PROVINSI " + t
183
+ return t
184
+
185
+ def clean_kab_display(s):
186
+ if pd.isna(s):
187
+ return None
188
+ t = str(s).upper().strip()
189
+ t = " ".join(t.split())
190
+ t = t.replace("KABUPATEN", "KAB.")
191
+ t = t.replace("KAB ", "KAB. ")
192
+ t = t.replace("KOTA ADMINISTRASI", "KOTA")
193
+ # rapikan variasi "DAN" supaya konsisten tampilan juga
194
+ t = re.sub(r"\bDAN\b", " ", t)
195
+ t = " ".join(t.split())
196
+ return t
197
+
198
+ def make_pie_plotly(num, den, title):
199
+ if not HAS_KALEIDO:
200
+ return None
201
+ if den is None or pd.isna(den) or den <= 0:
202
+ values = [0, 1]
203
+ labels = ["Terjangkau", "Belum Terjangkau"]
204
+ else:
205
+ num = 0 if pd.isna(num) else float(num)
206
+ den = float(den)
207
+ values = [max(num, 0), max(den - num, 0)]
208
+ labels = ["Terjangkau", "Belum Terjangkau"]
209
+ fig = px.pie(values=values, names=labels, title=title, hole=0.35)
210
+ tmp = tempfile.mktemp(suffix=".png")
211
+ try:
212
+ fig.write_image(tmp, scale=2)
213
+ return tmp
214
+ except Exception:
215
+ return None
216
+
217
+
218
+ # ============================================================
219
+ # 3) LOAD DATA (DM + META)
220
+ # ============================================================
221
+ DATA_INFO = ""
222
+ df_all_raw = None
223
+
224
+ meta_kab_df = None # kab_key -> target total + opsional sekolah/umum
225
+ meta_prov_df = None # prov_key -> target total
226
+
227
+ prov_col_glob = None
228
+ kab_col_glob = None
229
+ kew_col_glob = None
230
+ jenis_col_glob = None
231
+ subjenis_col_glob = None
232
+ nama_col_glob = None
233
+
234
+ extra_info = []
235
+
236
+ # ---- Load DM ----
237
+ try:
238
+ fp = Path(DATA_FILE)
239
+ if not fp.exists():
240
+ raise FileNotFoundError(f"File tidak ditemukan: {DATA_FILE}")
241
+
242
+ xls = pd.ExcelFile(fp)
243
+ frames = [pd.read_excel(fp, sheet_name=s) for s in xls.sheet_names]
244
+ df_all_raw = pd.concat(frames, ignore_index=True, sort=False)
245
+
246
+ prov_col_glob = pick_col(df_all_raw, ["provinsi", "Provinsi", "PROVINSI"])
247
+ kab_col_glob = pick_col(df_all_raw, ["kab_kota", "kab/kota", "Kab/Kota", "KAB/KOTA", "kabupaten_kota", "kota"])
248
+ kew_col_glob = pick_col(df_all_raw, ["kewenangan", "jenis_kewenangan", "Kewenangan", "KEWENANGAN"])
249
+ jenis_col_glob = pick_col(df_all_raw, ["jenis_perpustakaan", "JENIS_PERPUSTAKAAN", "Jenis Perpustakaan"])
250
+ subjenis_col_glob = pick_col(df_all_raw, ["sub_jenis_perpus", "Sub Jenis", "SubJenis", "subjenis", "jenjang"])
251
+ nama_col_glob = pick_col(df_all_raw, ["nm_perpustakaan", "nama_perpustakaan", "nm_instansi_lembaga", "Nama Perpustakaan"])
252
+
253
+ if kew_col_glob:
254
+ df_all_raw["KEW_NORM"] = df_all_raw[kew_col_glob].apply(norm_kew)
255
+ else:
256
+ df_all_raw["KEW_NORM"] = None
257
+
258
+ val_map_jenis = {
259
+ "PERPUSTAKAAN SEKOLAH": "sekolah",
260
+ "SEKOLAH": "sekolah",
261
+ "PERPUSTAKAAN UMUM": "umum",
262
+ "UMUM": "umum",
263
+ "PERPUSTAKAAN DAERAH": "umum",
264
+ "PERPUSTAKAAN KHUSUS": "khusus",
265
+ "KHUSUS": "khusus",
266
+ "PERPUSTAKAAN PERGURUAN TINGGI": "khusus",
267
+ "PERGURUAN TINGGI": "khusus",
268
+ }
269
+ if jenis_col_glob:
270
+ df_all_raw["_dataset"] = df_all_raw[jenis_col_glob].apply(_norm_text).map(val_map_jenis)
271
+ else:
272
+ df_all_raw["_dataset"] = None
273
+
274
+ if prov_col_glob and prov_col_glob in df_all_raw.columns:
275
+ df_all_raw["prov_clean"] = df_all_raw[prov_col_glob].apply(clean_prov_display)
276
+ else:
277
+ df_all_raw["prov_clean"] = None
278
+
279
+ if kab_col_glob and kab_col_glob in df_all_raw.columns:
280
+ df_all_raw["kab_clean"] = df_all_raw[kab_col_glob].apply(clean_kab_display)
281
+ else:
282
+ df_all_raw["kab_clean"] = None
283
+
284
+ DATA_INFO = f"Data terbaca dari: **{DATA_FILE}** | Jumlah baris: **{len(df_all_raw)}**"
285
+ except Exception as e:
286
+ df_all_raw = None
287
+ DATA_INFO = f"⚠️ Gagal memuat `{DATA_FILE}` | Error: `{e}`"
288
+
289
+ # ---- Meta Kab/Kota ----
290
+ try:
291
+ meta_kab_raw = pd.read_excel(META_KAB_FILE)
292
+
293
+ col_kab = pick_col(meta_kab_raw, ["KABUPATEN_KOTA", "KAB/KOTA", "Kab/Kota", "Kab_Kota", "kab/kota", "kabupaten_kota"])
294
+ col_target_total = pick_col(meta_kab_raw, ["sampel_total", "Sampel_total", "SAMPEL_TOTAL"])
295
+
296
+ col_target_umum = pick_col(meta_kab_raw, ["Sampel_umum_68%", "sampel_umum_68%", "SAMPEL_UMUM_68%"])
297
+ col_target_sek = pick_col(meta_kab_raw, ["Sampel_sekolah_68%", "sampel_sekolah_68%", "SAMPEL_SEKOLAH_68%"])
298
+
299
+ if col_kab and col_target_total:
300
+ meta_kab_df = pd.DataFrame({
301
+ "Kab_Kota_Label": meta_kab_raw[col_kab].astype(str).str.strip(),
302
+ "Target_Total_68": meta_kab_raw[col_target_total].apply(coerce_num),
303
+ })
304
+ meta_kab_df["Target_Umum_68"] = meta_kab_raw[col_target_umum].apply(coerce_num) if col_target_umum else np.nan
305
+ meta_kab_df["Target_Sekolah_68"] = meta_kab_raw[col_target_sek].apply(coerce_num) if col_target_sek else np.nan
306
+
307
+ meta_kab_df["kab_key"] = meta_kab_df["Kab_Kota_Label"].apply(norm_kab_label)
308
+
309
+ meta_kab_df = meta_kab_df.groupby("kab_key", as_index=False).agg({
310
+ "Kab_Kota_Label": "first",
311
+ "Target_Total_68": "first",
312
+ "Target_Umum_68": "first",
313
+ "Target_Sekolah_68": "first",
314
+ })
315
+
316
+ extra_info.append(f"Meta Kab/Kota terbaca: **{META_KAB_FILE}** (n={len(meta_kab_df)}) | Target=`sampel_total`")
317
+ else:
318
+ meta_kab_df = None
319
+ extra_info.append(f"⚠️ Kolom `KABUPATEN_KOTA` atau `sampel_total` tidak ditemukan di `{META_KAB_FILE}`")
320
+ except Exception as e:
321
+ meta_kab_df = None
322
+ extra_info.append(f"⚠️ Gagal memuat `{META_KAB_FILE}` ({e})")
323
+
324
+ # ---- Meta Provinsi ----
325
+ try:
326
+ meta_prov_raw = pd.read_excel(META_PROV_FILE)
327
+
328
+ col_prov = pick_col(meta_prov_raw, ["Provinsi", "provinsi", "PROVINSI", "NAMA_PROVINSI", "Nama Provinsi", "nm_prov", "nm_provinsi", "prov"])
329
+
330
+ # banyak variasi spasi/underscore
331
+ col_target_total = pick_col(meta_prov_raw, ["total _sampel", "total_sampel", "TOTAL _SAMPEL", "TOTAL_SAMPEL", "total sampel", "TOTAL SAMPEL"])
332
+
333
+ if col_prov and col_target_total:
334
+ meta_prov_df = pd.DataFrame({
335
+ "Provinsi_Label": meta_prov_raw[col_prov].astype(str).str.strip(),
336
+ "Target_Total_68": meta_prov_raw[col_target_total].apply(coerce_num),
337
+ })
338
+ meta_prov_df["prov_key"] = meta_prov_df["Provinsi_Label"].apply(norm_prov_label)
339
+ meta_prov_df = meta_prov_df.groupby("prov_key", as_index=False).agg({
340
+ "Provinsi_Label": "first",
341
+ "Target_Total_68": "first",
342
+ })
343
+ extra_info.append(f"Meta Provinsi terbaca: **{META_PROV_FILE}** ({len(meta_prov_df)} provinsi) | Target=`{col_target_total}`")
344
+ else:
345
+ meta_prov_df = None
346
+ extra_info.append(f"⚠️ Kolom `Provinsi` atau `total _sampel` tidak ditemukan di `{META_PROV_FILE}`")
347
+ except Exception as e:
348
+ meta_prov_df = None
349
+ extra_info.append(f"⚠️ Gagal memuat file populasi provinsi: {e}")
350
+
351
+ if extra_info:
352
+ DATA_INFO = DATA_INFO + "<br>" + "<br>".join(extra_info)
353
+
354
+
355
+ # ============================================================
356
+ # 4) DROPDOWN
357
+ # ============================================================
358
+ def all_prov_choices():
359
+ if df_all_raw is None or "prov_clean" not in df_all_raw.columns:
360
+ return ["(Semua)"]
361
+ s = df_all_raw["prov_clean"].dropna().astype(str).str.strip()
362
+ vals = sorted([o for o in s.unique() if o and o != ""])
363
+ return ["(Semua)"] + vals
364
+
365
+ def get_kab_choices_for_prov(prov_value):
366
+ if df_all_raw is None or "kab_clean" not in df_all_raw.columns:
367
+ return ["(Semua)"]
368
+ if prov_value is None or prov_value == "(Semua)":
369
+ s = df_all_raw["kab_clean"].dropna().astype(str).str.strip()
370
+ else:
371
+ m = df_all_raw["prov_clean"].astype(str).str.strip() == str(prov_value).strip()
372
+ s = df_all_raw.loc[m, "kab_clean"].dropna().astype(str).str.strip()
373
+ vals = sorted([x for x in s.unique() if x and x != ""])
374
+ return ["(Semua)"] + vals
375
+
376
+ def all_kew_choices():
377
+ if df_all_raw is None:
378
+ return ["(Semua)"]
379
+ s = df_all_raw.get("KEW_NORM", pd.Series(dtype=object)).dropna().astype(str).str.strip()
380
+ vals = sorted([o for o in s.unique() if o != ""])
381
+ return ["(Semua)"] + vals if vals else ["(Semua)"]
382
+
383
+ prov_choices = all_prov_choices()
384
+ kab_choices = get_kab_choices_for_prov(prov_choices[0] if prov_choices else "(Semua)")
385
+ kew_choices = all_kew_choices()
386
+ default_kew = "KAB/KOTA" if "KAB/KOTA" in kew_choices else (kew_choices[0] if kew_choices else "(Semua)")
387
+
388
+
389
+ # ============================================================
390
+ # 5) VERIFIKASI GAP — TARGET DARI META (bukan hitung ulang)
391
+ # ============================================================
392
+ def compute_gap_verification(df_filtered: pd.DataFrame, kew_value: str) -> pd.DataFrame:
393
+ if df_filtered is None or len(df_filtered) == 0:
394
+ return pd.DataFrame()
395
+
396
+ kew_norm = str(kew_value or "").upper()
397
+
398
+ # =================== KAB/KOTA ===================
399
+ if ("KAB" in kew_norm or "KOTA" in kew_norm):
400
+ if "kab_clean" not in df_filtered.columns or meta_kab_df is None:
401
+ return pd.DataFrame({"Info": ["Kolom kab_clean atau meta kab tidak tersedia."]})
402
+
403
+ tmp = df_filtered.copy()
404
+ tmp = tmp[pd.notna(tmp["kab_clean"])]
405
+ if tmp.empty:
406
+ return pd.DataFrame()
407
+
408
+ tmp["kab_key"] = tmp["kab_clean"].apply(norm_kab_label)
409
+
410
+ g_total = tmp.groupby("kab_key").size().rename("Sampel Total (DM)").reset_index()
411
+
412
+ tmp_sek = tmp[tmp["_dataset"] == "sekolah"].copy() if "_dataset" in tmp.columns else tmp.copy()
413
+ g_sek_total = tmp_sek.groupby("kab_key").size().rename("Sampel Sekolah (DM)").reset_index()
414
+
415
+ tmp_umum = tmp[tmp["_dataset"] == "umum"].copy() if "_dataset" in tmp.columns else tmp.copy()
416
+ g_umum = tmp_umum.groupby("kab_key").size().rename("Sampel Umum (DM)").reset_index()
417
+
418
+ merged = (
419
+ g_total
420
+ .merge(g_sek_total, on="kab_key", how="left")
421
+ .merge(g_umum, on="kab_key", how="left")
422
+ .merge(
423
+ meta_kab_df[["kab_key", "Kab_Kota_Label", "Target_Total_68", "Target_Umum_68", "Target_Sekolah_68"]],
424
+ on="kab_key", how="left"
425
+ )
426
+ )
427
+
428
+ for c in ["Sampel Total (DM)", "Sampel Sekolah (DM)", "Sampel Umum (DM)"]:
429
+ merged[c] = merged[c].fillna(0).astype(int)
430
+
431
+ # marker match meta
432
+ merged["META_MATCH"] = np.where(pd.notna(merged["Target_Total_68"]), "YA", "TIDAK")
433
+
434
+ # target dari meta (ceil biar integer ke atas)
435
+ merged["Target Total (68%)"] = np.ceil(pd.to_numeric(merged["Target_Total_68"], errors="coerce"))
436
+ merged["Target Sekolah (68%)"] = np.ceil(pd.to_numeric(merged["Target_Sekolah_68"], errors="coerce"))
437
+ merged["Target Umum (68%)"] = np.ceil(pd.to_numeric(merged["Target_Umum_68"], errors="coerce"))
438
+
439
+ # kekurangan: kalau target NaN -> NaN (bukan 0)
440
+ def _gap(target_series, sampel_series):
441
+ t = pd.to_numeric(target_series, errors="coerce")
442
+ s = pd.to_numeric(sampel_series, errors="coerce").fillna(0)
443
+ out = t - s
444
+ out = out.where(t.notna(), np.nan)
445
+ return out.clip(lower=0)
446
+
447
+ merged["Kekurangan Sampel Total"] = _gap(merged["Target Total (68%)"], merged["Sampel Total (DM)"])
448
+ merged["Kekurangan Sampel Sekolah"] = _gap(merged["Target Sekolah (68%)"], merged["Sampel Sekolah (DM)"])
449
+ merged["Kekurangan Sampel Umum"] = _gap(merged["Target Umum (68%)"], merged["Sampel Umum (DM)"])
450
+
451
+ out = pd.DataFrame({
452
+ "Kab/Kota": merged["Kab_Kota_Label"].fillna(merged["kab_key"]),
453
+ "META_MATCH": merged["META_MATCH"],
454
+
455
+ "Sampel Total (DM)": merged["Sampel Total (DM)"],
456
+ "Target Total (68%) [META:sampel_total]": merged["Target Total (68%)"],
457
+ "Kekurangan Sampel Total": merged["Kekurangan Sampel Total"],
458
+
459
+ "Sampel Sekolah (DM)": merged["Sampel Sekolah (DM)"],
460
+ "Target Sekolah (68%) [META]": merged["Target Sekolah (68%)"],
461
+ "Kekurangan Sampel Sekolah": merged["Kekurangan Sampel Sekolah"],
462
+
463
+ "Sampel Umum (DM)": merged["Sampel Umum (DM)"],
464
+ "Target Umum (68%) [META]": merged["Target Umum (68%)"],
465
+ "Kekurangan Sampel Umum": merged["Kekurangan Sampel Umum"],
466
+ })
467
+
468
+ # cast tampilan angka: biarkan NaN tetap NaN supaya ketahuan mismatch meta
469
+ num_cols = [c for c in out.columns if c not in {"Kab/Kota", "META_MATCH"}]
470
+ for c in num_cols:
471
+ out[c] = pd.to_numeric(out[c], errors="coerce")
472
+
473
+ return out.sort_values(["META_MATCH", "Kab/Kota"], ascending=[True, True]).reset_index(drop=True)
474
+
475
+ # =================== PROVINSI ===================
476
+ if ("PROV" in kew_norm):
477
+ if meta_prov_df is None or "prov_clean" not in df_filtered.columns:
478
+ return pd.DataFrame({"Info": ["Meta provinsi atau kolom prov_clean tidak tersedia."]})
479
+
480
+ tmp = df_filtered.copy()
481
+ tmp = tmp[pd.notna(tmp["prov_clean"])]
482
+ if tmp.empty:
483
+ return pd.DataFrame({"Info": ["Tidak ada data sampel kewenangan provinsi."]})
484
+
485
+ tmp["prov_key"] = tmp["prov_clean"].apply(norm_prov_label)
486
+ g_total = tmp.groupby("prov_key").size().rename("Sampel Total (DM)").reset_index()
487
+
488
+ merged = g_total.merge(meta_prov_df[["prov_key", "Provinsi_Label", "Target_Total_68"]], on="prov_key", how="left")
489
+ merged["Sampel Total (DM)"] = merged["Sampel Total (DM)"].fillna(0).astype(int)
490
+ merged["META_MATCH"] = np.where(pd.notna(merged["Target_Total_68"]), "YA", "TIDAK")
491
+
492
+ merged["Target Total (68%)"] = np.ceil(pd.to_numeric(merged["Target_Total_68"], errors="coerce"))
493
+ t = pd.to_numeric(merged["Target Total (68%)"], errors="coerce")
494
+ s = pd.to_numeric(merged["Sampel Total (DM)"], errors="coerce").fillna(0)
495
+ gap = (t - s).where(t.notna(), np.nan).clip(lower=0)
496
+ merged["Kekurangan Sampel Total"] = gap
497
+
498
+ out = pd.DataFrame({
499
+ "Provinsi": merged["Provinsi_Label"].fillna(merged["prov_key"]),
500
+ "META_MATCH": merged["META_MATCH"],
501
+ "Sampel Total (DM)": merged["Sampel Total (DM)"],
502
+ "Target Total (68%) [META:total _sampel]": merged["Target Total (68%)"],
503
+ "Kekurangan Sampel Total": merged["Kekurangan Sampel Total"],
504
+ })
505
+
506
+ for c in ["Sampel Total (DM)", "Target Total (68%) [META:total _sampel]", "Kekurangan Sampel Total"]:
507
+ out[c] = pd.to_numeric(out[c], errors="coerce")
508
+
509
+ return out.sort_values(["META_MATCH", "Provinsi"], ascending=[True, True]).reset_index(drop=True)
510
+
511
+ return pd.DataFrame({"Info": ["Kewenangan tidak dikenali / tidak didukung."]})
512
+
513
+
514
+ # ============================================================
515
+ # 6) GRAFIK GAP — pakai Kekurangan Total (abaikan NaN)
516
+ # ============================================================
517
+ def make_gap_figure(verif_df: pd.DataFrame, kew_value: str) -> go.Figure:
518
+ fig = go.Figure()
519
+ if verif_df is None or verif_df.empty:
520
+ fig.update_layout(title="Kekurangan Sampel (tidak ada data)", xaxis_title="Unit", yaxis_title="Kekurangan (unit)")
521
+ return fig
522
+
523
+ kew_norm = str(kew_value or "").upper()
524
+
525
+ def _num(s):
526
+ return pd.to_numeric(s, errors="coerce").fillna(0).astype(int)
527
+
528
+ if ("KAB" in kew_norm or "KOTA" in kew_norm) and ("Kab/Kota" in verif_df.columns):
529
+ dfp = verif_df.copy()
530
+ dfp["gap_total"] = _num(dfp.get("Kekurangan Sampel Total", 0))
531
+ dfp = dfp.sort_values("gap_total", ascending=False)
532
+
533
+ x = dfp["Kab/Kota"].astype(str).tolist()
534
+ gap_total = _num(dfp["gap_total"])
535
+
536
+ fig.add_trace(go.Bar(
537
+ x=x, y=gap_total, name="Kekurangan Total",
538
+ text=gap_total, textposition="outside",
539
+ hovertemplate="%{x}<br>Kekurangan total: %{y} unit<extra></extra>"
540
+ ))
541
+ fig.update_layout(
542
+ title=f"Kekurangan Sampel TOTAL (KAB/KOTA) — Target {int(TARGET_COVERAGE*100)}% (META)",
543
+ xaxis_title="Kab/Kota", yaxis_title="Kekurangan (unit)",
544
+ margin=dict(l=40, r=20, t=60, b=140),
545
+ )
546
+ fig.update_xaxes(tickangle=-35)
547
+ return fig
548
+
549
+ if ("PROV" in kew_norm) and ("Provinsi" in verif_df.columns):
550
+ dfp = verif_df.copy()
551
+ dfp["gap_total"] = _num(dfp.get("Kekurangan Sampel Total", 0))
552
+ dfp = dfp.sort_values("gap_total", ascending=False)
553
+
554
+ x = dfp["Provinsi"].astype(str).tolist()
555
+ gap_total = _num(dfp["gap_total"])
556
+
557
+ fig.add_trace(go.Bar(
558
+ x=x, y=gap_total, name="Kekurangan Total",
559
+ text=gap_total, textposition="outside",
560
+ hovertemplate="%{x}<br>Kekurangan total: %{y} unit<extra></extra>"
561
+ ))
562
+ fig.update_layout(
563
+ title=f"Kekurangan Sampel TOTAL (PROVINSI) — Target {int(TARGET_COVERAGE*100)}% (META)",
564
+ xaxis_title="Provinsi", yaxis_title="Kekurangan (unit)",
565
+ margin=dict(l=40, r=20, t=60, b=140),
566
+ )
567
+ fig.update_xaxes(tickangle=-35)
568
+ return fig
569
+
570
+ fig.update_layout(title="Kekurangan Sampel — format data tidak dikenali", xaxis_title="Unit", yaxis_title="Kekurangan (unit)")
571
+ return fig
572
+
573
+
574
+ # ============================================================
575
+ # 7) LLM NARASI
576
+ # ============================================================
577
+ def build_context_gap(verif_df: pd.DataFrame, prov: str, kab: str, kew: str) -> str:
578
+ wilayah = kab if kab and kab != "(Semua)" else (prov if prov and prov != "(Semua)" else "NASIONAL")
579
+ lines = []
580
+ lines.append(f"Wilayah filter: {wilayah}")
581
+ lines.append(f"Kewenangan: {kew}")
582
+ lines.append(f"Target pengumpulan: {int(TARGET_COVERAGE*100)}% (TARGET diambil dari META).")
583
+ lines.append(f"Jumlah unit analisis: {len(verif_df)}")
584
+
585
+ if "Kekurangan Sampel Total" in verif_df.columns:
586
+ total_gap = int(pd.to_numeric(verif_df["Kekurangan Sampel Total"], errors="coerce").fillna(0).sum())
587
+ lines.append(f"Total Kekurangan Sampel Total: {total_gap}")
588
+
589
+ if "META_MATCH" in verif_df.columns:
590
+ n_no = int((verif_df["META_MATCH"] == "TIDAK").sum())
591
+ if n_no > 0:
592
+ lines.append(f"PERINGATAN: ada {n_no} unit yang tidak match ke META (target tidak tersedia).")
593
+
594
+ keycol = "Kab/Kota" if "Kab/Kota" in verif_df.columns else ("Provinsi" if "Provinsi" in verif_df.columns else verif_df.columns[0])
595
+ if "Kekurangan Sampel Total" in verif_df.columns:
596
+ t = verif_df.copy()
597
+ t["Kekurangan Sampel Total"] = pd.to_numeric(t["Kekurangan Sampel Total"], errors="coerce").fillna(0)
598
+ top = t.sort_values("Kekurangan Sampel Total", ascending=False).head(10)
599
+ lines.append("\nTop prioritas (gap terbesar):")
600
+ for _, r in top.iterrows():
601
+ lines.append(f"- {r[keycol]}: gap_total={int(r['Kekurangan Sampel Total'])}")
602
+
603
+ return "\n".join(lines)
604
+
605
+ def rule_based_gap_report(verif_df: pd.DataFrame, prov: str, kab: str, kew: str) -> str:
606
+ if verif_df is None or verif_df.empty:
607
+ return "Tidak ada data verifikasi yang dapat dilaporkan."
608
+
609
+ wilayah = kab if kab and kab != "(Semua)" else (prov if prov and prov != "(Semua)" else "NASIONAL")
610
+ lines = []
611
+ lines.append("## Ringkasan Kekurangan Sampel IPLM (Rule-based)\n")
612
+ lines.append(f"Wilayah: {wilayah}")
613
+ lines.append(f"Kewenangan: {kew}")
614
+ lines.append(f"Target pengumpulan: {int(TARGET_COVERAGE*100)}% (TARGET diambil dari META: kab/kota=`sampel_total`, provinsi=`total _sampel`).")
615
+ lines.append(f"Jumlah unit analisis: {len(verif_df)}\n")
616
+
617
+ if "Kekurangan Sampel Total" in verif_df.columns:
618
+ total_gap = int(pd.to_numeric(verif_df["Kekurangan Sampel Total"], errors="coerce").fillna(0).sum())
619
+ lines.append(f"- Total Kekurangan Sampel Total: **{total_gap}** unit yang perlu dilengkapi menuju target.")
620
+ else:
621
+ lines.append("Kolom kekurangan sampel total tidak ditemukan.")
622
+
623
+ if "META_MATCH" in verif_df.columns:
624
+ n_no = int((verif_df["META_MATCH"] == "TIDAK").sum())
625
+ if n_no > 0:
626
+ lines.append(f"- Catatan: **{n_no}** unit belum match ke META, sehingga target tidak tersedia (perlu pembenahan label/meta).")
627
+
628
+ lines.append("\nArah tindak lanjut: prioritaskan wilayah dengan gap terbesar, dan pastikan mapping unit ke META valid untuk monitoring yang akurat.")
629
+ return "\n".join(lines)
630
+
631
+ def generate_llm_gap_report(verif_df: pd.DataFrame, prov: str, kab: str, kew: str) -> str:
632
+ ctx = build_context_gap(verif_df, prov, kab, kew)
633
+ client = get_llm_client()
634
+ if client is None or not USE_LLM:
635
+ return "⚠️ LLM tidak tersedia, memakai laporan rule-based.\n\n" + rule_based_gap_report(verif_df, prov, kab, kew)
636
+
637
+ system_prompt = (
638
+ "Anda adalah analis kebijakan dan manajer program IPLM. "
639
+ "Fokus Anda hanya pada gap sampel (kekurangan unit) dan strategi menutup kekurangan tersebut."
640
+ )
641
+ user_prompt = f"""
642
+ DATA RINGKAS GAP SAMPEL IPLM:
643
+
644
+ {ctx}
645
+
646
+ TULIS LAPORAN (BAHASA INDONESIA FORMAL) DENGAN STRUKTUR:
647
+ 1) Ringkasan kondisi pengumpulan data (1 paragraf).
648
+ 2) Total kekurangan sampel yang masih perlu dikumpulkan menuju target {int(TARGET_COVERAGE*100)}% (1 paragraf).
649
+ 3) Prioritas wilayah (gap terbesar) dan alasan operasional (1 paragraf).
650
+ 4) Rencana aksi 30–60 hari (naratif, bukan bullet).
651
+
652
+ BATASAN:
653
+ - Jangan membahas indeks/skor IPLM.
654
+ - Tegaskan bahwa target berasal dari META: kab/kota=`sampel_total`, provinsi=`total _sampel`.
655
+ - Jika ada unit META_MATCH=TIDAK, sebutkan sebagai isu kualitas data/master reference.
656
+ """
657
+ try:
658
+ resp = client.chat_completion(
659
+ model=LLM_MODEL_NAME,
660
+ messages=[{"role": "system", "content": system_prompt},
661
+ {"role": "user", "content": user_prompt}],
662
+ max_tokens=900,
663
+ temperature=0.2,
664
+ top_p=0.9,
665
+ )
666
+ text = resp.choices[0].message.content.strip()
667
+ if not text:
668
+ raise ValueError("Respon LLM kosong.")
669
+ return text
670
+ except Exception as e:
671
+ return (
672
+ "⚠️ Error saat memanggil LLM, memakai laporan rule-based.\n\n"
673
+ f"(Detail teknis: {repr(e)})\n\n"
674
+ + rule_based_gap_report(verif_df, prov, kab, kew)
675
+ )
676
+
677
+
678
+ # ============================================================
679
+ # 8) WORD REPORT
680
+ # ============================================================
681
+ def generate_word_report_gap(verif_df: pd.DataFrame, prov: str, kab: str, kew: str, analysis_text: str):
682
+ wilayah = kab if kab and kab != "(Semua)" else (prov if prov and prov != "(Semua)" else "NASIONAL")
683
+
684
+ doc = Document()
685
+ doc.add_heading(f"Laporan Kekurangan Sampel IPLM – {wilayah}", level=1)
686
+ doc.add_paragraph(f"Kewenangan: {kew}")
687
+ doc.add_paragraph(f"Target pengumpulan: {int(TARGET_COVERAGE*100)}% (TARGET diambil dari META).")
688
+ doc.add_paragraph(f"Jumlah unit analisis: {len(verif_df)}")
689
+
690
+ doc.add_heading("Tabel Verifikasi (Target & Kekurangan Sampel)", level=2)
691
+
692
+ view = verif_df.copy()
693
+ if len(view) > 200:
694
+ doc.add_paragraph("Catatan: tabel dipotong (200 baris pertama) untuk menjaga ukuran dokumen.")
695
+ view = view.head(200)
696
+
697
+ table = doc.add_table(rows=1, cols=len(view.columns))
698
+ hdr = table.rows[0].cells
699
+ for i, c in enumerate(view.columns):
700
+ hdr[i].text = str(c)
701
+
702
+ for _, row in view.iterrows():
703
+ r = table.add_row().cells
704
+ for i, c in enumerate(view.columns):
705
+ r[i].text = "" if pd.isna(row[c]) else str(row[c])
706
+
707
+ doc.add_heading("Ringkasan Visual (Opsional)", level=2)
708
+ if not HAS_KALEIDO:
709
+ doc.add_paragraph("Grafik pie tidak dibuat karena 'kaleido' tidak tersedia di server.")
710
+ else:
711
+ pie_made = False
712
+ if "Sampel Total (DM)" in verif_df.columns:
713
+ samp = pd.to_numeric(verif_df["Sampel Total (DM)"], errors="coerce").fillna(0).sum()
714
+ tgt_col = None
715
+ for c in verif_df.columns:
716
+ if "Target Total (68%)" in c:
717
+ tgt_col = c
718
+ break
719
+ if tgt_col:
720
+ tgt = pd.to_numeric(verif_df[tgt_col], errors="coerce").fillna(0).sum()
721
+ img = make_pie_plotly(samp, tgt, "Capaian TOTAL (DM) terhadap Target TOTAL (META)")
722
+ if img:
723
+ doc.add_paragraph("Capaian TOTAL terhadap Target TOTAL (META)")
724
+ doc.add_picture(img)
725
+ pie_made = True
726
+
727
+ if not pie_made:
728
+ doc.add_paragraph("Tidak ada pasangan kolom sampel-target yang valid untuk dibuat pie chart.")
729
+
730
+ doc.add_heading("Analisis Naratif (LLM)", level=2)
731
+ for p in analysis_text.split("\n"):
732
+ if p.strip():
733
+ doc.add_paragraph(p)
734
+
735
+ outpath = tempfile.mktemp(suffix=".docx")
736
+ doc.save(outpath)
737
+ return outpath
738
+
739
+
740
+ # ============================================================
741
+ # 9) CORE RUN
742
+ # ============================================================
743
+ def run_core(prov_value, kab_value, kew_value):
744
+ if df_all_raw is None or df_all_raw.empty:
745
+ empty = pd.DataFrame()
746
+ return empty, empty, None, None, None, None, "Data DM tidak terbaca.", "Tidak ada analisis."
747
+
748
+ df = df_all_raw.copy()
749
+
750
+ if prov_value and prov_value != "(Semua)" and "prov_clean" in df.columns:
751
+ df = df[df["prov_clean"].astype(str).str.strip() == str(prov_value).strip()]
752
+
753
+ if kab_value and kab_value != "(Semua)" and "kab_clean" in df.columns:
754
+ df = df[df["kab_clean"].astype(str).str.strip() == str(kab_value).strip()]
755
+
756
+ if kew_value and kew_value != "(Semua)":
757
+ df = df[df["KEW_NORM"] == kew_value]
758
+
759
+ if len(df) == 0:
760
+ empty = pd.DataFrame()
761
+ return empty, empty, None, None, None, None, "Tidak ada data untuk kombinasi filter yang dipilih.", "Tidak ada analisis."
762
+
763
+ verif_df = compute_gap_verification(df, kew_value)
764
+
765
+ cols = []
766
+ for c in ["prov_clean", "kab_clean", nama_col_glob, kew_col_glob, jenis_col_glob, subjenis_col_glob, "_dataset", "KEW_NORM"]:
767
+ if c and c in df.columns and c not in cols:
768
+ cols.append(c)
769
+ detail_df = df[cols].copy() if cols else df.copy()
770
+
771
+ fig_gap = make_gap_figure(verif_df, kew_value)
772
+
773
+ tmpdir = tempfile.mkdtemp()
774
+ rekap_excel_path = os.path.join(tmpdir, "Rekap_Kekurangan_Sampel_IPLM_Target_META.xlsx")
775
+ raw_dm_path = os.path.join(tmpdir, "DM_Subset_Raw.xlsx")
776
+
777
+ with pd.ExcelWriter(rekap_excel_path, engine="openpyxl") as w:
778
+ verif_df.to_excel(w, sheet_name="Verifikasi_Gap_Target_META", index=False)
779
+ detail_df.to_excel(w, sheet_name="Detail_Subset_DM", index=False)
780
+
781
+ df.to_excel(raw_dm_path, index=False)
782
+
783
+ analysis_text = generate_llm_gap_report(verif_df, prov_value, kab_value, kew_value)
784
+ word_path = generate_word_report_gap(verif_df, prov_value, kab_value, kew_value, analysis_text)
785
+
786
+ # message ringkas + warning mismatch meta
787
+ warn = ""
788
+ if "META_MATCH" in verif_df.columns:
789
+ n_no = int((verif_df["META_MATCH"] == "TIDAK").sum())
790
+ if n_no > 0:
791
+ warn = f" ⚠️ {n_no} unit tidak match ke META (target NaN)."
792
+
793
+ msg = f"OK. Subset DM: {len(df)} baris | Verifikasi: {len(verif_df)} baris | Target: {int(TARGET_COVERAGE*100)}% (META).{warn}"
794
+
795
+ return verif_df, detail_df, fig_gap, rekap_excel_path, raw_dm_path, word_path, msg, analysis_text
796
+
797
+ def on_prov_change(prov_value):
798
+ return gr.update(choices=get_kab_choices_for_prov(prov_value), value="(Semua)")
799
+
800
+
801
+ # ============================================================
802
+ # 10) UI
803
+ # ============================================================
804
+ with gr.Blocks() as demo:
805
+ gr.Markdown(
806
+ f"""
807
+ # Dashboard Kekurangan Sampel IPLM — Target {int(TARGET_COVERAGE*100)}% (Tanpa Hitung Indeks)
808
+
809
+ **Target dari META (bukan hitung ulang):**
810
+ - Kab/Kota: `{META_KAB_FILE}` kolom **`sampel_total`**
811
+ - Provinsi: `{META_PROV_FILE}` kolom **`total _sampel`** (variasi spasi/underscore didukung)
812
+
813
+ {DATA_INFO}
814
+ """
815
+ )
816
+
817
+ with gr.Row():
818
+ dd_prov = gr.Dropdown(label="Provinsi", choices=prov_choices, value=prov_choices[0])
819
+ dd_kab = gr.Dropdown(label="Kab/Kota", choices=kab_choices, value=kab_choices[0])
820
+ dd_kew = gr.Dropdown(label="Kewenangan", choices=kew_choices, value=default_kew)
821
+
822
+ dd_prov.change(fn=on_prov_change, inputs=dd_prov, outputs=dd_kab)
823
+
824
+ run_btn = gr.Button("Hitung Kekurangan Sampel")
825
+ msg_out = gr.Markdown()
826
+
827
+ gr.Markdown("### Verifikasi (Target & Kekurangan Sampel) — Target dari META")
828
+ verif_out = gr.DataFrame(interactive=False)
829
+
830
+ gr.Markdown("### Grafik Kekurangan Sampel TOTAL (unit)")
831
+ gap_plot_out = gr.Plot()
832
+
833
+ gr.Markdown("### Detail Subset DM (yang terfilter)")
834
+ detail_out = gr.DataFrame(interactive=False)
835
+
836
+ gr.Markdown("### Analisis Naratif (LLM)")
837
+ analysis_out = gr.Markdown()
838
+
839
+ with gr.Row():
840
+ rekap_excel_out = gr.File(label="Download Rekap (Verifikasi + Detail) (.xlsx)")
841
+ raw_dm_out = gr.File(label="Download Data Mentah Subset DM (.xlsx)")
842
+ word_out = gr.File(label="Download Laporan Word (.docx)")
843
+
844
+ run_btn.click(
845
+ fn=run_core,
846
+ inputs=[dd_prov, dd_kab, dd_kew],
847
+ outputs=[
848
+ verif_out,
849
+ detail_out,
850
+ gap_plot_out,
851
+ rekap_excel_out,
852
+ raw_dm_out,
853
+ word_out,
854
+ msg_out,
855
+ analysis_out
856
+ ],
857
+ )
858
+
859
+ demo.launch()
gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ DM_001.xlsx filter=lfs diff=lfs merge=lfs -text
2
+ IPLM_clean_manual_131225.xlsx filter=lfs diff=lfs merge=lfs -text
3
+ IPLM_clean_Manual.xlsx filter=lfs diff=lfs merge=lfs -text
gitattributes (1) ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ DM_001.xlsx filter=lfs diff=lfs merge=lfs -text
37
+ IPLM_clean_Manual.xlsx filter=lfs diff=lfs merge=lfs -text
38
+ IPLM_clean_manual_131225.xlsx filter=lfs diff=lfs merge=lfs -text
jumlahdesa_fixed%2520%25281%2529.xlsx ADDED
Binary file (24.2 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core data
2
+ pandas>=2.0.0
3
+ numpy>=1.24.0
4
+ openpyxl>=3.1.2
5
+
6
+ # UI
7
+ gradio>=4.0.0
8
+
9
+ # LLM (Hugging Face)
10
+ huggingface-hub>=0.20.0
11
+
12
+ # Word report
13
+ python-docx>=1.1.0
14
+
15
+ # Plot (opsional – untuk pie chart di Word)
16
+ plotly>=5.18.0
17
+ kaleido>=0.2.1