ilafi commited on
Commit
bd32345
·
verified ·
1 Parent(s): 34c06c2

Upload 14 files

Browse files
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ DATA_IPLM_FINAL_CLEAN_pusat_12012026.xlsx filter=lfs diff=lfs merge=lfs -text
2
+ DATA_IPLM_FINAL_CLEAN_pusat_15012026.xlsx filter=lfs diff=lfs merge=lfs -text
3
+ DATA%2520IPLM%2520FINAL_CLEAN_pusat.xlsx filter=lfs diff=lfs merge=lfs -text
4
+ IPLM_clean_manual_131225.xlsx filter=lfs diff=lfs merge=lfs -text
DATA%2520IPLM%2520FINAL_CLEAN_pusat.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:822fcc274a633a7feb180eeadf2a5e9ab02963de7b8ea0156bc6cb4562e4a571
3
+ size 30828517
DATA_IPLM_FINAL_CLEAN_pusat_12012026.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c41b12e2be40e452904c807da50a3149cfee756b226a8cf50277345c6d3853dd
3
+ size 30830638
DATA_IPLM_FINAL_CLEAN_pusat_15012026.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3c4f9815c8156c3327c66f39a77dfb0bd5526152e6dbec756080e18e160f33d7
3
+ size 30830327
Data_populasi_Kab_kota.xlsx ADDED
Binary file (74.8 kB). View file
 
Data_populasi_Kab_kota_fixed.xlsx ADDED
Binary file (74.8 kB). View file
 
Data_populasi_Kab_kota_fixed_lama.xlsx ADDED
Binary file (74.8 kB). View file
 
Data_populasi_perp_khusus.xlsx ADDED
Binary file (27 kB). View file
 
Data_populasi_propinsi (1).xlsx ADDED
Binary file (15.6 kB). View file
 
IPLM_clean_manual_131225.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5560e8f700d983ca8b0b13d188ef06c2dab1cae364a24870226afd9c72b5c1db
3
+ size 21587518
README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Iplm
3
+ emoji: 🌍
4
+ colorFrom: green
5
+ colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 6.0.2
8
+ app_file: app.py
9
+ pinned: false
10
+ license: bsd
11
+ ---
12
+
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,1897 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ IPLM 2025 — FINAL (NO UPLOAD) — FULL REWRITE (NO RINGKAS)
4
+
5
+ ✅ Jenis tampil: sekolah, umum, khusus (khusus ditampilkan sebagai jenis)
6
+ ✅ Indeks dasar per entitas: Yeo-Johnson + MinMax nasional per indikator
7
+ ✅ Penyesuaian 68% berbasis TOTAL pengumpulan wilayah:
8
+ faktor_penyesuaian = min(n_total_terkumpul / target_total_68, 1.0)
9
+
10
+ ✅ AGREGAT WILAYAH (KESELURUHAN) — FIX UTAMA (RUMUS BARU):
11
+ Semua kolom “keseluruhan” wilayah WAJIB diambil dari rata-rata 3 jenis
12
+ (sekolah + umum + khusus) ÷ 3 (missing=0, tetap ÷3)
13
+ -> termasuk Indeks_Dasar_Agregat_0_100 dan Indeks_Final_Wilayah_0_100
14
+
15
+ ✅ Agregat Wilayah × Jenis:
16
+ Indeks_Final_Agregat_0_100 = Indeks_Dasar_Agregat_0_100 × faktor_penyesuaian_wilayah
17
+ (faktor wilayah sama untuk semua jenis)
18
+
19
+ ✅ Ringkasan (Jenis + Keseluruhan) selalu 4 baris: sekolah, umum, khusus, keseluruhan
20
+ ✅ Keseluruhan ringkasan = (final_sekolah+final_umum+final_khusus)/3 (missing=0, tetap ÷3)
21
+
22
+ ✅ Detail entitas: Indeks_Final_0_100 menempel dari Agregat Wilayah (Keseluruhan) (bukan per-row)
23
+ ✅ Bell curve per JENIS berbasis indeks per entitas (row-level)
24
+ ✅ LLM analysis + Word
25
+ ✅ Download (tanpa upload box)
26
+ ✅ Download Data Mentah (.xlsx) = RAW hasil filter (bukan agregat)
27
+
28
+ FIX DISPLAY:
29
+ ✅ “null/NaN” untuk target/pop/coverage jenis -> dibuat 0 agar tidak tampil null
30
+ ✅ Verifikasi 68% (tanpa koma) -> semua angka dibulatkan jadi integer
31
+
32
+ PERMINTAAN UPDATE (HANYA INI):
33
+ 1) TABEL faktor_wilayah:
34
+ - target_total_68 -> bilangan bulat
35
+ - pop_total -> bilangan bulat
36
+ - coverage_total_% -> decimal 2 digit
37
+ 2) TABEL "Agregat Wilayah × Jenis" (UI) hanya sampai kolom Indeks_Dasar_Agregat_0_100
38
+ (kolom setelah itu tidak ditampilkan)
39
+ """
40
+
41
+ import os
42
+ import re
43
+ import time
44
+ import tempfile
45
+ from pathlib import Path
46
+
47
+ import gradio as gr
48
+ import numpy as np
49
+ import pandas as pd
50
+ import plotly.graph_objects as go
51
+ from sklearn.preprocessing import PowerTransformer
52
+
53
+ from docx import Document
54
+ from huggingface_hub import InferenceClient
55
+
56
+
57
+ # ============================================================
58
+ # 1) KONFIGURASI
59
+ # ============================================================
60
+
61
+ DATA_FILE = os.getenv("DATA_FILE", "DATA_IPLM_FINAL_CLEAN_pusat_15012026.xlsx")
62
+ POP_KAB = os.getenv("POP_KAB", "Data_populasi_Kab_kota_fixed.xlsx")
63
+ POP_PROV = os.getenv("POP_PROV", "Data_populasi_propinsi.xlsx")
64
+ POP_KHUSUS = os.getenv("POP_KHUSUS", "Data_populasi_perp_khusus.xlsx")
65
+
66
+ W_KEPATUHAN = float(os.getenv("W_KEPATUHAN", "0.30"))
67
+ W_KINERJA = float(os.getenv("W_KINERJA", "0.70"))
68
+
69
+ FALLBACK_TARGET_RATIO = 0.68
70
+
71
+ USE_LLM = True
72
+ LLM_MODEL_NAME = os.getenv("LLM_MODEL_NAME", "meta-llama/Meta-Llama-3-8B-Instruct")
73
+ HF_TOKEN = (
74
+ os.getenv("HF_SECRET")
75
+ or os.getenv("HF_TOKEN")
76
+ or os.getenv("HUGGINGFACEHUB_API_TOKEN")
77
+ or os.getenv("HF_API_TOKEN")
78
+ )
79
+
80
+
81
+ # ============================================================
82
+ # 2) UTIL
83
+ # ============================================================
84
+
85
+ def _mtime(path_str: str):
86
+ p = Path(path_str)
87
+ return p.stat().st_mtime if p.exists() else None
88
+
89
+ def _canon(s: str) -> str:
90
+ return re.sub(r"[^a-z0-9]+", "", str(s).lower())
91
+
92
+ def _disp_text(x):
93
+ if pd.isna(x):
94
+ return None
95
+ t = str(x).strip().upper()
96
+ return " ".join(t.split())
97
+
98
+ def pick_col(df, candidates):
99
+ if df is None or df.empty:
100
+ return None
101
+ for c in candidates:
102
+ if c in df.columns:
103
+ return c
104
+ can_map = {_canon(c): c for c in df.columns}
105
+ for c in candidates:
106
+ k = _canon(c)
107
+ if k in can_map:
108
+ return can_map[k]
109
+ return None
110
+
111
+ def coerce_num(val):
112
+ if pd.isna(val):
113
+ return np.nan
114
+ t = str(val).strip()
115
+ if t == "" or t in {"-", "–", "—", "NA", "N/A", "null", "NULL"}:
116
+ return np.nan
117
+ t = t.replace("\u00a0", " ").replace("Rp", "").replace("%", "")
118
+ t = re.sub(r"[^0-9,.\-]", "", t)
119
+
120
+ if t.count(".") > 1 and t.count(",") == 1:
121
+ t = t.replace(".", "").replace(",", ".")
122
+ elif t.count(",") > 1 and t.count(".") == 1:
123
+ t = t.replace(",", "")
124
+ elif t.count(",") == 1 and t.count(".") == 0:
125
+ t = t.replace(",", ".")
126
+ else:
127
+ t = t.replace(",", "")
128
+
129
+ try:
130
+ return float(t)
131
+ except Exception:
132
+ return np.nan
133
+
134
+ def minmax_norm(s: pd.Series) -> pd.Series:
135
+ x = pd.to_numeric(s, errors="coerce").astype(float)
136
+ mn, mx = x.min(skipna=True), x.max(skipna=True)
137
+ if pd.isna(mn) or pd.isna(mx) or mx == mn:
138
+ return pd.Series(0.0, index=s.index)
139
+ return (x - mn) / (mx - mn)
140
+
141
+ def norm_kew(v):
142
+ if pd.isna(v):
143
+ return None
144
+ t = str(v).strip().upper()
145
+ if "KAB" in t or "KOTA" in t:
146
+ return "KAB/KOTA"
147
+ if "PROV" in t:
148
+ return "PROVINSI"
149
+ if "PUSAT" in t or "NASIONAL" in t:
150
+ return "PUSAT"
151
+ return t
152
+
153
+ def norm_prov_disp(s):
154
+ if pd.isna(s):
155
+ return None
156
+ t = str(s).strip().upper()
157
+ t = t.replace("\u00a0", " ")
158
+ t = " ".join(t.split())
159
+ t = t.replace("PROPINSI", "PROVINSI")
160
+ while t.startswith("PROVINSI PROVINSI "):
161
+ t = t.replace("PROVINSI PROVINSI ", "PROVINSI ", 1)
162
+ if t.startswith("PROVINSI "):
163
+ name = t[len("PROVINSI "):].strip()
164
+ else:
165
+ name = t
166
+ name = " ".join(name.split())
167
+ if not name:
168
+ return None
169
+ return f"PROVINSI {name}"
170
+
171
+ def norm_prov_label(s):
172
+ if pd.isna(s):
173
+ return None
174
+ t = str(s).strip().upper().replace("\u00a0", " ")
175
+ t = " ".join(t.split())
176
+ t = t.replace("PROPINSI", "PROVINSI")
177
+ t = t.replace("PROVINSI", "").strip()
178
+ return re.sub(r"[^A-Z0-9]+", "", t)
179
+
180
+ def norm_kab_label(s):
181
+ if pd.isna(s):
182
+ return None
183
+ t = str(s).upper()
184
+ t = t.replace("KABUPATEN", "KAB")
185
+ t = t.replace("KAB.", "KAB")
186
+ t = t.replace("KOTA ADMINISTRASI", "KOTA")
187
+ t = t.replace("KOTA ADM.", "KOTA")
188
+ t = t.replace("KOTA.", "KOTA")
189
+ t = " ".join(t.split())
190
+ return re.sub(r"[^A-Z0-9]+", "", t)
191
+
192
+ def safe_div(num, den):
193
+ if den is None or pd.isna(den) or float(den) <= 0:
194
+ return np.nan
195
+ return float(num) / float(den)
196
+
197
+ def faktor_penyesuaian_total(n_total: float, target_total: float) -> float:
198
+ if target_total is None or pd.isna(target_total) or float(target_total) <= 0:
199
+ return 1.0
200
+ if n_total is None or pd.isna(n_total) or float(n_total) < 0:
201
+ n_total = 0.0
202
+ return float(min(float(n_total) / float(target_total), 1.0))
203
+
204
+
205
+ # ============================================================
206
+ # 3) INDIKATOR IPLM
207
+ # ============================================================
208
+
209
+ koleksi_cols = [
210
+ "JudulTercetak","EksemplarTercetak","JudulElektronik","EksemplarElektronik",
211
+ "TambahJudulTercetak","TambahEksemplarTercetak",
212
+ "TambahJudulElektronik","TambahEksemplarElektronik",
213
+ "KomitmenAnggaranKoleksi"
214
+ ]
215
+ sdm_cols = [
216
+ "TenagaKualifikasiIlmuPerpustakaan",
217
+ "TenagaFungsionalProfesional",
218
+ "TenagaPKB",
219
+ "AnggaranTenaga"
220
+ ]
221
+ pelayanan_cols = [
222
+ "PesertaBudayaBaca","PemustakaLuringDaring","PemustakaFasilitasTIK",
223
+ "PemanfaatanJudulTercetak","PemanfaatanEksemplarTercetak",
224
+ "PemanfaatanJudulElektronik","PemanfaatanEksemplarElektronik"
225
+ ]
226
+ pengelolaan_cols = [
227
+ "KegiatanBudayaBaca","KegiatanKerjasama","VariasiLayanan","Kebijakan","AnggaranLayanan"
228
+ ]
229
+ all_indicators = koleksi_cols + sdm_cols + pelayanan_cols + pengelolaan_cols
230
+
231
+ alias_map_raw = {
232
+ "j_judul_koleksi_tercetak": "JudulTercetak",
233
+ "j_eksemplar_koleksi_tercetak": "EksemplarTercetak",
234
+ "j_judul_koleksi_digital": "JudulElektronik",
235
+ "j_eksemplar_koleksi_digital": "EksemplarElektronik",
236
+ "tambah_judul_koleksi_tercetak": "TambahJudulTercetak",
237
+ "tambah_eksemplar_koleksi_tercetak": "TambahEksemplarTercetak",
238
+ "tambah_judul_koleksi_digital": "TambahJudulElektronik",
239
+ "tambah_eksemplar_koleksi_digital": "TambahEksemplarElektronik",
240
+ "j_anggaran_koleksi": "KomitmenAnggaranKoleksi",
241
+ "j_tenaga_ilmu_perpus": "TenagaKualifikasiIlmuPerpustakaan",
242
+ "j_tenaga_nonilmu_perpus": "TenagaFungsionalProfesional",
243
+ "j_tenaga_pkb": "TenagaPKB",
244
+ "j_anggaran_diklat_perpus": "AnggaranTenaga",
245
+ "j_peserta_budaya_baca": "PesertaBudayaBaca",
246
+ "j_pemustaka_luring_daring": "PemustakaLuringDaring",
247
+ "j_pemustaka_fasilitas_tik": "PemustakaFasilitasTIK",
248
+ "j_judul_koleksi_tercetak_termanfaat": "PemanfaatanJudulTercetak",
249
+ "j_eksemplar_koleksi_tercetak_termanfaat": "PemanfaatanEksemplarTercetak",
250
+ "j_judul_koleksi_digital_termanfaat": "PemanfaatanJudulElektronik",
251
+ "j_eksemplar_koleksi_digital_termanfaat": "PemanfaatanEksemplarElektronik",
252
+ "j_kegiatan_budaya_baca_peningkatan_literasi": "KegiatanBudayaBaca",
253
+ "j_kerjasama_pengembangan_perpus": "KegiatanKerjasama",
254
+ "j_variasi_layanan": "VariasiLayanan",
255
+ "j_kebijakan_prosedur_pelayanan": "Kebijakan",
256
+ "j_anggaran_peningkatan_pelayanan": "AnggaranLayanan",
257
+ }
258
+ alias_map = {_canon(k): v for k, v in alias_map_raw.items()}
259
+
260
+
261
+ # ============================================================
262
+ # 4) PIPELINE NASIONAL (ENTITAS)
263
+ # ============================================================
264
+
265
+ def _mean_norm_cols(row, cols):
266
+ vals = []
267
+ for c in cols:
268
+ k = f"norm_{c}"
269
+ if k in row.index:
270
+ v = row[k]
271
+ if pd.isna(v):
272
+ v = 0.0
273
+ vals.append(float(v))
274
+ return float(np.mean(vals)) if vals else 0.0
275
+
276
+ def prepare_global(df_src: pd.DataFrame) -> pd.DataFrame:
277
+ if df_src is None or df_src.empty:
278
+ return df_src
279
+ df = df_src.copy()
280
+
281
+ rename_map = {}
282
+ for col in df.columns:
283
+ c = _canon(col)
284
+ if c in alias_map:
285
+ rename_map[col] = alias_map[c]
286
+ else:
287
+ for tgt in all_indicators:
288
+ if c == _canon(tgt):
289
+ rename_map[col] = tgt
290
+ break
291
+ if rename_map:
292
+ df = df.rename(columns=rename_map)
293
+
294
+ available = [c for c in all_indicators if c in df.columns]
295
+ for c in available:
296
+ df[c] = df[c].apply(coerce_num)
297
+
298
+ for c in available:
299
+ x = pd.to_numeric(df[c], errors="coerce").astype(float).values
300
+ mask = ~np.isnan(x)
301
+ transformed = np.full_like(x, np.nan, dtype=float)
302
+ if mask.sum() > 1:
303
+ pt = PowerTransformer(method="yeo-johnson", standardize=False)
304
+ transformed[mask] = pt.fit_transform(x[mask].reshape(-1, 1)).ravel()
305
+ else:
306
+ transformed[mask] = x[mask]
307
+ df[f"norm_{c}"] = minmax_norm(pd.Series(transformed, index=df.index))
308
+
309
+ df["sub_koleksi"] = df.apply(lambda r: _mean_norm_cols(r, [c for c in koleksi_cols if c in available]), axis=1)
310
+ df["sub_sdm"] = df.apply(lambda r: _mean_norm_cols(r, [c for c in sdm_cols if c in available]), axis=1)
311
+ df["sub_pelayanan"] = df.apply(lambda r: _mean_norm_cols(r, [c for c in pelayanan_cols if c in available]), axis=1)
312
+ df["sub_pengelolaan"] = df.apply(lambda r: _mean_norm_cols(r, [c for c in pengelolaan_cols if c in available]), axis=1)
313
+
314
+ df["dim_kepatuhan"] = df[["sub_koleksi","sub_sdm"]].mean(axis=1)
315
+ df["dim_kinerja"] = df[["sub_pelayanan","sub_pengelolaan"]].mean(axis=1)
316
+
317
+ df["Indeks_Dasar_0_100"] = 100 * (W_KEPATUHAN * df["dim_kepatuhan"] + W_KINERJA * df["dim_kinerja"])
318
+
319
+ for c in ["sub_koleksi","sub_sdm","sub_pelayanan","sub_pengelolaan","dim_kepatuhan","dim_kinerja","Indeks_Dasar_0_100"]:
320
+ df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0.0)
321
+
322
+ return df
323
+
324
+
325
+ # ============================================================
326
+ # 5) CACHE LOADER (NO UPLOAD)
327
+ # ============================================================
328
+
329
+ _CACHE = {
330
+ "key": None,
331
+ "df_all": None,
332
+ "df_raw": None,
333
+ "pop_kab": None,
334
+ "pop_prov": None,
335
+ "pop_khusus": None,
336
+ "meta": None,
337
+ "info": None
338
+ }
339
+
340
+ def _parse_pop_khusus(path_xlsx: str) -> pd.DataFrame:
341
+ df = pd.read_excel(path_xlsx)
342
+ if df is None or df.empty:
343
+ return pd.DataFrame()
344
+
345
+ # file kamu: Propinsi/Kab/kota | POP_KHUSUS | SAMPEL_KHUSUS_68%
346
+ c_mix = pick_col(df, [
347
+ "Propinsi/Kab/kota", "Propinsi/Kab/Kota", "Provinsi/Kab/Kota",
348
+ "Provinsi/Kab/kota", "Provinsi/Kabupaten/Kota",
349
+ "Wilayah", "Nama Wilayah"
350
+ ])
351
+ if c_mix is None:
352
+ raise ValueError("POP_KHUSUS: kolom gabungan Provinsi/Kab/Kota tidak ditemukan.")
353
+
354
+ c_pop = pick_col(df, ["POP_KHUSUS", "Pop_Khusus", "pop_khusus"])
355
+ c_target = pick_col(df, ["SAMPEL_KHUSUS_68%", "Sampel_Khusus_68%", "sampel_khusus_68%"])
356
+
357
+ if c_pop is None or c_target is None:
358
+ raise ValueError("POP_KHUSUS: kolom 'POP_KHUSUS' dan/atau 'SAMPEL_KHUSUS_68%' tidak ditemukan.")
359
+
360
+ mix = df[c_mix].astype(str).fillna("").str.strip()
361
+ pop_series = df[c_pop].apply(coerce_num)
362
+ tgt_series = df[c_target].apply(coerce_num)
363
+
364
+ rows = []
365
+ current_prov = None
366
+
367
+ for m, pval, tval in zip(mix.tolist(), pop_series.tolist(), tgt_series.tolist()):
368
+ mm = _disp_text(m) or ""
369
+ if mm == "":
370
+ continue
371
+
372
+ # === PROV row: dianggap TOTAL PROVINSI (punya nilai!) ===
373
+ if mm.startswith("PROVINSI "):
374
+ prov_name = mm.replace("PROVINSI", "").strip()
375
+ current_prov = prov_name
376
+
377
+ rows.append({
378
+ "LEVEL": "PROV",
379
+ "Provinsi_Label": f"PROVINSI {prov_name}",
380
+ "Kab_Kota_Label": None,
381
+ "Pop_Total_Jenis": pval,
382
+ "Target68_Total_Jenis": tval,
383
+ })
384
+ continue
385
+
386
+ # === KAB/KOTA row ===
387
+ rows.append({
388
+ "LEVEL": "KAB",
389
+ "Provinsi_Label": f"PROVINSI {current_prov}" if current_prov else None,
390
+ "Kab_Kota_Label": mm,
391
+ "Pop_Total_Jenis": pval,
392
+ "Target68_Total_Jenis": tval,
393
+ })
394
+
395
+ pop = pd.DataFrame(rows)
396
+ if pop.empty:
397
+ return pop
398
+
399
+ pop["Pop_Total_Jenis"] = pd.to_numeric(pop["Pop_Total_Jenis"], errors="coerce")
400
+ pop["Target68_Total_Jenis"] = pd.to_numeric(pop["Target68_Total_Jenis"], errors="coerce")
401
+
402
+ # fallback aman (jaga-jaga)
403
+ m_need_pop = pop["Pop_Total_Jenis"].isna() & pop["Target68_Total_Jenis"].notna() & (pop["Target68_Total_Jenis"] > 0)
404
+ pop.loc[m_need_pop, "Pop_Total_Jenis"] = pop.loc[m_need_pop, "Target68_Total_Jenis"] / float(FALLBACK_TARGET_RATIO)
405
+
406
+ m_need_target = pop["Target68_Total_Jenis"].isna() & pop["Pop_Total_Jenis"].notna() & (pop["Pop_Total_Jenis"] > 0)
407
+ pop.loc[m_need_target, "Target68_Total_Jenis"] = pop.loc[m_need_target, "Pop_Total_Jenis"] * float(FALLBACK_TARGET_RATIO)
408
+
409
+ # keys
410
+ pop["prov_key"] = pop["Provinsi_Label"].apply(norm_prov_label)
411
+ pop["kab_key"] = pop["Kab_Kota_Label"].apply(norm_kab_label) if "Kab_Kota_Label" in pop.columns else None
412
+
413
+ return pop
414
+
415
+
416
+ def load_default_files(force=False):
417
+ key = (
418
+ DATA_FILE, POP_KAB, POP_PROV, POP_KHUSUS,
419
+ _mtime(DATA_FILE), _mtime(POP_KAB), _mtime(POP_PROV), _mtime(POP_KHUSUS)
420
+ )
421
+
422
+ if (not force) and _CACHE["key"] == key and _CACHE["df_all"] is not None:
423
+ return _CACHE["df_all"], _CACHE["df_raw"], _CACHE["pop_kab"], _CACHE["pop_prov"], _CACHE["pop_khusus"], _CACHE["meta"], _CACHE["info"]
424
+
425
+ for p, label in [(DATA_FILE, "DM"), (POP_KAB, "POP_KAB"), (POP_PROV, "POP_PROV"), (POP_KHUSUS, "POP_KHUSUS")]:
426
+ if not Path(p).exists():
427
+ info = f"❌ File {label} tidak ditemukan: `{p}`"
428
+ _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
429
+ return None, None, None, None, None, {}, info
430
+
431
+ fp = Path(DATA_FILE)
432
+ xls = pd.ExcelFile(fp)
433
+ frames = [pd.read_excel(fp, sheet_name=s) for s in xls.sheet_names]
434
+ df_raw = pd.concat(frames, ignore_index=True, sort=False)
435
+
436
+ prov_col = pick_col(df_raw, ["provinsi", "Provinsi", "PROVINSI"])
437
+ kab_col = pick_col(df_raw, ["kab_kota", "Kab/Kota", "Kab_Kota", "KAB/KOTA", "kabupaten_kota", "Kabupaten/Kota", "kabupaten kota", "kota"])
438
+ kew_col = pick_col(df_raw, ["kewenangan", "jenis_kewenangan", "Kewenangan", "KEWENANGAN"])
439
+ jenis_col = pick_col(df_raw, ["jenis_perpustakaan", "Jenis Perpustakaan", "JENIS_PERPUSTAKAAN"])
440
+ nama_col = pick_col(df_raw, ["nm_perpustakaan","nama_perpustakaan","Nama Perpustakaan","nm_instansi_lembaga","nm_perpus"])
441
+
442
+ missing = []
443
+ if prov_col is None: missing.append("Provinsi")
444
+ if kab_col is None: missing.append("Kab/Kota")
445
+ if kew_col is None: missing.append("Kewenangan")
446
+ if jenis_col is None: missing.append("Jenis Perpustakaan")
447
+ if missing:
448
+ info = f"❌ Kolom wajib tidak ditemukan di DM: {', '.join(missing)}"
449
+ _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
450
+ return None, None, None, None, None, {}, info
451
+
452
+ val_map_jenis = {
453
+ "PERPUSTAKAAN SEKOLAH": "sekolah", "SEKOLAH": "sekolah",
454
+ "PERPUSTAKAAN UMUM": "umum", "UMUM": "umum", "PERPUSTAKAAN DAERAH": "umum",
455
+ "PERPUSTAKAAN KHUSUS": "khusus", "KHUSUS": "khusus",
456
+ }
457
+
458
+ df_raw["KEW_NORM"] = df_raw[kew_col].apply(norm_kew)
459
+ df_raw["_dataset"] = df_raw[jenis_col].astype(str).str.strip().str.upper().map(val_map_jenis)
460
+ df_raw["PROV_DISP"] = df_raw[prov_col].apply(norm_prov_disp)
461
+ df_raw["KAB_DISP"] = df_raw[kab_col].apply(_disp_text)
462
+ df_raw["prov_key"] = df_raw["PROV_DISP"].apply(norm_prov_label)
463
+ df_raw["kab_key"] = df_raw["KAB_DISP"].apply(norm_kab_label)
464
+
465
+ if nama_col and nama_col in df_raw.columns:
466
+ kcols = [prov_col, kab_col, kew_col, jenis_col, nama_col]
467
+ else:
468
+ kcols = [prov_col, kab_col, kew_col, jenis_col]
469
+
470
+ tmp = df_raw[kcols].astype(str).fillna("").apply(lambda s: s.str.strip(), axis=0)
471
+ df_raw["_row_key"] = tmp.apply(lambda r: "||".join(r.values.tolist()), axis=1).apply(_canon)
472
+ before = len(df_raw)
473
+ df_raw = df_raw.drop_duplicates(subset=["_row_key"], keep="first").copy()
474
+ after = len(df_raw)
475
+
476
+ # =========================
477
+ # POP KAB
478
+ # =========================
479
+ pk = pd.read_excel(POP_KAB)
480
+
481
+ c_kab = pick_col(pk, ["KABUPATEN_KOTA","Kab/Kota","Kabupaten/Kota","KAB/KOTA","Kabupaten_Kota","kab_kota","kabupaten_kota"])
482
+ c_prov = pick_col(pk, ["PROVINSI","Provinsi","provinsi"])
483
+
484
+ # NOTE: kita tetap load semua kolom, karena BLOK 6 akan pakai nama kolom asli
485
+ if c_kab is None:
486
+ info = "❌ POP_KAB: wajib ada kolom Kab/Kota."
487
+ _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
488
+ return None, None, None, None, None, {}, info
489
+
490
+ pop_kab = pk.copy()
491
+ pop_kab["Kab_Kota_Label"] = pk[c_kab].astype(str).str.strip()
492
+ pop_kab["Provinsi_Label"] = pk[c_prov].astype(str).str.strip() if c_prov else ""
493
+ pop_kab["kab_key"] = pop_kab["Kab_Kota_Label"].apply(norm_kab_label)
494
+
495
+ pop_kab = pop_kab.groupby("kab_key", as_index=False).first()
496
+
497
+ # =========================
498
+ # POP PROV
499
+ # =========================
500
+ pp = pd.read_excel(POP_PROV)
501
+
502
+ c_pr = pick_col(pp, ["Provinsi","PROVINSI","provinsi","Propinsi","PROPINSI","propinsi"])
503
+ if c_pr is None:
504
+ info = "❌ POP_PROV: wajib ada kolom Provinsi."
505
+ _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
506
+ return None, None, None, None, None, {}, info
507
+
508
+ pop_prov = pp.copy()
509
+ pop_prov["Provinsi_Label"] = pp[c_pr].astype(str).str.strip()
510
+ pop_prov["prov_key"] = pop_prov["Provinsi_Label"].apply(norm_prov_label)
511
+ pop_prov = pop_prov.groupby("prov_key", as_index=False).first()
512
+
513
+ # =========================
514
+ # POP KHUSUS
515
+ # =========================
516
+ try:
517
+ pop_khusus = _parse_pop_khusus(POP_KHUSUS)
518
+ except Exception as e:
519
+ info = f"❌ POP_KHUSUS gagal dibaca: {repr(e)}"
520
+ _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
521
+ return None, None, None, None, None, {}, info
522
+
523
+ df_all = prepare_global(df_raw)
524
+
525
+ meta = dict(prov_col=prov_col, kab_col=kab_col, kew_col=kew_col, jenis_col=jenis_col, nama_col=nama_col)
526
+
527
+ info = (
528
+ f"✅ Mode NO UPLOAD (cache aktif)<br>"
529
+ f"✅ DM: <b>{fp.name}</b> | Baris: {before} → dedup: {after}<br>"
530
+ f"✅ POP_KAB: <b>{Path(POP_KAB).name}</b> (n={len(pop_kab)})<br>"
531
+ f"✅ POP_PROV: <b>{Path(POP_PROV).name}</b> (n={len(pop_prov)})<br>"
532
+ f"✅ POP_KHUSUS: <b>{Path(POP_KHUSUS).name}</b> (n={len(pop_khusus)}) — (PROV row ikut dihitung)<br>"
533
+ f"🕒 mtime: DM={time.ctime(_mtime(DATA_FILE))} | Kab={time.ctime(_mtime(POP_KAB))} | Prov={time.ctime(_mtime(POP_PROV))} | Khusus={time.ctime(_mtime(POP_KHUSUS))}"
534
+ )
535
+
536
+ _CACHE.update({
537
+ "key": key,
538
+ "df_all": df_all,
539
+ "df_raw": df_raw,
540
+ "pop_kab": pop_kab,
541
+ "pop_prov": pop_prov,
542
+ "pop_khusus": pop_khusus,
543
+ "meta": meta,
544
+ "info": info
545
+ })
546
+ return df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info
547
+
548
+ # =========================
549
+ # DM gabungan semua sheet
550
+ # =========================
551
+ fp = Path(DATA_FILE)
552
+ xls = pd.ExcelFile(fp)
553
+ frames = [pd.read_excel(fp, sheet_name=s) for s in xls.sheet_names]
554
+ df_raw = pd.concat(frames, ignore_index=True, sort=False)
555
+
556
+ prov_col = pick_col(df_raw, ["provinsi", "Provinsi", "PROVINSI"])
557
+ kab_col = pick_col(df_raw, ["kab_kota", "Kab/Kota", "Kab_Kota", "KAB/KOTA", "kabupaten_kota", "Kabupaten/Kota", "kabupaten kota", "kota"])
558
+ kew_col = pick_col(df_raw, ["kewenangan", "jenis_kewenangan", "Kewenangan", "KEWENANGAN"])
559
+ jenis_col = pick_col(df_raw, ["jenis_perpustakaan", "Jenis Perpustakaan", "JENIS_PERPUSTAKAAN"])
560
+ nama_col = pick_col(df_raw, ["nm_perpustakaan","nama_perpustakaan","Nama Perpustakaan","nm_instansi_lembaga","nm_perpus"])
561
+
562
+ missing = []
563
+ if prov_col is None:
564
+ missing.append("Provinsi")
565
+ if kab_col is None:
566
+ missing.append("Kab/Kota")
567
+ if kew_col is None:
568
+ missing.append("Kewenangan")
569
+ if jenis_col is None:
570
+ missing.append("Jenis Perpustakaan")
571
+
572
+ if missing:
573
+ info = f"❌ Kolom wajib tidak ditemukan di DM: {', '.join(missing)}"
574
+ _CACHE.update({
575
+ "key": key, "df_all": None, "df_raw": None,
576
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
577
+ "meta": {}, "info": info
578
+ })
579
+ return None, None, None, None, None, {}, info
580
+
581
+ val_map_jenis = {
582
+ "PERPUSTAKAAN SEKOLAH": "sekolah", "SEKOLAH": "sekolah",
583
+ "PERPUSTAKAAN UMUM": "umum", "UMUM": "umum", "PERPUSTAKAAN DAERAH": "umum",
584
+ "PERPUSTAKAAN KHUSUS": "khusus", "KHUSUS": "khusus",
585
+ }
586
+
587
+ df_raw["KEW_NORM"] = df_raw[kew_col].apply(norm_kew)
588
+ df_raw["_dataset"] = df_raw[jenis_col].astype(str).str.strip().str.upper().map(val_map_jenis)
589
+ df_raw["PROV_DISP"] = df_raw[prov_col].apply(norm_prov_disp)
590
+ df_raw["KAB_DISP"] = df_raw[kab_col].apply(_disp_text)
591
+ df_raw["prov_key"] = df_raw["PROV_DISP"].apply(norm_prov_label)
592
+ df_raw["kab_key"] = df_raw["KAB_DISP"].apply(norm_kab_label)
593
+
594
+ if nama_col and nama_col in df_raw.columns:
595
+ kcols = [prov_col, kab_col, kew_col, jenis_col, nama_col]
596
+ else:
597
+ kcols = [prov_col, kab_col, kew_col, jenis_col]
598
+
599
+ tmp = df_raw[kcols].astype(str).fillna("").apply(lambda s: s.str.strip(), axis=0)
600
+ df_raw["_row_key"] = tmp.apply(lambda r: "||".join(r.values.tolist()), axis=1).apply(_canon)
601
+ before = len(df_raw)
602
+ df_raw = df_raw.drop_duplicates(subset=["_row_key"], keep="first").copy()
603
+ after = len(df_raw)
604
+
605
+ # =========================
606
+ # POP KAB (KEEP PER-JENIS)
607
+ # =========================
608
+ pk = pd.read_excel(POP_KAB)
609
+
610
+ c_kab = pick_col(pk, ["KABUPATEN_KOTA","Kab/Kota","Kabupaten/Kota","KAB/KOTA","Kabupaten_Kota","kab_kota","kabupaten_kota"])
611
+ c_prov = pick_col(pk, ["PROVINSI","Provinsi","provinsi"])
612
+
613
+ c_target_total = pick_col(pk, [
614
+ "sampel_total","Sampel_total","Sampel Total","TOTAL_SAMPEL","total_sampel",
615
+ "target_total_68","Target_Total_68","target_68","TARGET_68"
616
+ ])
617
+
618
+ # REAL kolom file user:
619
+ c_pop_umum = pick_col(pk, ["jumlah_populasi_umum","Jumlah_populasi_umum","JUMLAH_POPULASI_UMUM","POP_UMUM","pop_umum"])
620
+ c_target_umum = pick_col(pk, ["Sampel_umum_68%","Sampel_umum_68","SAMPEL_UMUM_68%","SAMPEL_UMUM_68","TARGET_UMUM_68"])
621
+
622
+ c_pop_sekolah = pick_col(pk, ["jumlah_populasi_sekolah","Jumlah_populasi_sekolah","JUMLAH_POPULASI_SEKOLAH","POP_SEKOLAH","pop_sekolah"])
623
+ c_target_sekolah = pick_col(pk, ["Sampel_sekolah_68%","Sampel_sekolah_68","SAMPEL_SEKOLAH_68%","SAMPEL_SEKOLAH_68","TARGET_SEKOLAH_68"])
624
+
625
+ if c_kab is None or c_target_total is None:
626
+ info = "❌ POP_KAB: wajib ada kolom Kab/Kota dan sampel_total (target 68%)."
627
+ _CACHE.update({
628
+ "key": key, "df_all": None, "df_raw": None,
629
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
630
+ "meta": {}, "info": info
631
+ })
632
+ return None, None, None, None, None, {}, info
633
+
634
+ pop_kab = pd.DataFrame({
635
+ "Provinsi_Label": pk[c_prov].astype(str).str.strip() if c_prov else "",
636
+ "Kab_Kota_Label": pk[c_kab].astype(str).str.strip(),
637
+ "Target68_Total": pk[c_target_total].apply(coerce_num),
638
+ "Pop_Umum": pk[c_pop_umum].apply(coerce_num) if c_pop_umum else np.nan,
639
+ "Target68_Umum": pk[c_target_umum].apply(coerce_num) if c_target_umum else np.nan,
640
+ "Pop_Sekolah": pk[c_pop_sekolah].apply(coerce_num) if c_pop_sekolah else np.nan,
641
+ "Target68_Sekolah": pk[c_target_sekolah].apply(coerce_num) if c_target_sekolah else np.nan,
642
+ })
643
+
644
+ # fallback target per jenis dari pop
645
+ m = pop_kab["Target68_Umum"].isna() & pop_kab["Pop_Umum"].notna() & (pop_kab["Pop_Umum"] > 0)
646
+ pop_kab.loc[m, "Target68_Umum"] = pop_kab.loc[m, "Pop_Umum"] * float(FALLBACK_TARGET_RATIO)
647
+
648
+ m = pop_kab["Target68_Sekolah"].isna() & pop_kab["Pop_Sekolah"].notna() & (pop_kab["Pop_Sekolah"] > 0)
649
+ pop_kab.loc[m, "Target68_Sekolah"] = pop_kab.loc[m, "Pop_Sekolah"] * float(FALLBACK_TARGET_RATIO)
650
+
651
+ pop_kab["Pop_Total"] = (
652
+ pd.to_numeric(pop_kab["Pop_Umum"], errors="coerce").fillna(0.0)
653
+ + pd.to_numeric(pop_kab["Pop_Sekolah"], errors="coerce").fillna(0.0)
654
+ )
655
+
656
+ m_need_pop = (pop_kab["Pop_Total"] <= 0) & pop_kab["Target68_Total"].notna() & (pop_kab["Target68_Total"] > 0)
657
+ pop_kab.loc[m_need_pop, "Pop_Total"] = pop_kab.loc[m_need_pop, "Target68_Total"] / float(FALLBACK_TARGET_RATIO)
658
+
659
+ pop_kab["kab_key"] = pop_kab["Kab_Kota_Label"].apply(norm_kab_label)
660
+
661
+ pop_kab = pop_kab.groupby("kab_key", as_index=False).agg({
662
+ "Kab_Kota_Label": "first",
663
+ "Provinsi_Label": "first",
664
+ "Target68_Total": "max",
665
+ "Pop_Total": "max",
666
+ "Pop_Umum": "max",
667
+ "Target68_Umum": "max",
668
+ "Pop_Sekolah": "max",
669
+ "Target68_Sekolah": "max",
670
+ })
671
+
672
+ # =========================
673
+ # POP PROV (KEEP PER-JENIS)
674
+ # =========================
675
+ pp = pd.read_excel(POP_PROV)
676
+
677
+ c_pr = pick_col(pp, ["Provinsi","PROVINSI","provinsi","Propinsi","PROPINSI","propinsi"])
678
+ c_target_total = pick_col(pp, ["total _sampel","total_sampel","TOTAL_SAMPEL","Total Sampel","target_total_68","Target_Total_68"])
679
+
680
+ # REAL kolom file user:
681
+ c_pop_sekolah = pick_col(pp, ["total_pend","TOTAL_PEND","total_penduduk","Total Penduduk"])
682
+ c_pop_umum = pick_col(pp, ["perpus_umum_prop","PERPUS_UMUM_PROP","Perpus_umum_prop"])
683
+
684
+ if c_pr is None or c_target_total is None:
685
+ info = "❌ POP_PROV: wajib ada kolom Provinsi dan total _sampel (target 68%)."
686
+ _CACHE.update({
687
+ "key": key, "df_all": None, "df_raw": None,
688
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
689
+ "meta": {}, "info": info
690
+ })
691
+ return None, None, None, None, None, {}, info
692
+
693
+ pop_prov = pd.DataFrame({
694
+ "Provinsi_Label": pp[c_pr].astype(str).str.strip(),
695
+ "Target68_Total_Prov": pp[c_target_total].apply(coerce_num),
696
+
697
+ "Pop_Sekolah_Prov": pp[c_pop_sekolah].apply(coerce_num) if c_pop_sekolah else np.nan,
698
+ "Target68_Sekolah_Prov": pp[c_target_total].apply(coerce_num), # sesuai file user
699
+
700
+ "Pop_Umum_Prov": pp[c_pop_umum].apply(coerce_num) if c_pop_umum else np.nan,
701
+ "Target68_Umum_Prov": np.nan,
702
+ })
703
+
704
+ m = pop_prov["Target68_Umum_Prov"].isna() & pop_prov["Pop_Umum_Prov"].notna() & (pop_prov["Pop_Umum_Prov"] > 0)
705
+ pop_prov.loc[m, "Target68_Umum_Prov"] = pop_prov.loc[m, "Pop_Umum_Prov"] * float(FALLBACK_TARGET_RATIO)
706
+
707
+ pop_prov["Pop_Total_Prov"] = (
708
+ pd.to_numeric(pop_prov["Pop_Sekolah_Prov"], errors="coerce").fillna(0.0)
709
+ + pd.to_numeric(pop_prov["Pop_Umum_Prov"], errors="coerce").fillna(0.0)
710
+ )
711
+
712
+ m_need_pop = (pop_prov["Pop_Total_Prov"] <= 0) & pop_prov["Target68_Total_Prov"].notna() & (pop_prov["Target68_Total_Prov"] > 0)
713
+ pop_prov.loc[m_need_pop, "Pop_Total_Prov"] = pop_prov.loc[m_need_pop, "Target68_Total_Prov"] / float(FALLBACK_TARGET_RATIO)
714
+
715
+ pop_prov["prov_key"] = pop_prov["Provinsi_Label"].apply(norm_prov_label)
716
+
717
+ pop_prov = pop_prov.groupby("prov_key", as_index=False).agg({
718
+ "Provinsi_Label": "first",
719
+ "Target68_Total_Prov": "max",
720
+ "Pop_Total_Prov": "max",
721
+ "Pop_Sekolah_Prov": "max",
722
+ "Target68_Sekolah_Prov": "max",
723
+ "Pop_Umum_Prov": "max",
724
+ "Target68_Umum_Prov": "max",
725
+ })
726
+
727
+ # =========================
728
+ # POP KHUSUS
729
+ # =========================
730
+ try:
731
+ pop_khusus = _parse_pop_khusus(POP_KHUSUS)
732
+ except Exception as e:
733
+ info = f"❌ POP_KHUSUS gagal dibaca: {repr(e)}"
734
+ _CACHE.update({
735
+ "key": key, "df_all": None, "df_raw": None,
736
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
737
+ "meta": {}, "info": info
738
+ })
739
+ return None, None, None, None, None, {}, info
740
+
741
+ df_all = prepare_global(df_raw)
742
+ meta = dict(prov_col=prov_col, kab_col=kab_col, kew_col=kew_col, jenis_col=jenis_col, nama_col=nama_col)
743
+
744
+ info = (
745
+ f"✅ Mode NO UPLOAD (cache aktif)<br>"
746
+ f"✅ DM: <b>{fp.name}</b> | Baris: {before} → dedup: {after}<br>"
747
+ f"✅ POP_KAB: <b>{Path(POP_KAB).name}</b> (n={len(pop_kab)}) — keep per-jenis (umum+sekolah)<br>"
748
+ f"✅ POP_PROV: <b>{Path(POP_PROV).name}</b> (n={len(pop_prov)}) — keep per-jenis (umum+sekolah)<br>"
749
+ f"✅ POP_KHUSUS: <b>{Path(POP_KHUSUS).name}</b> (n={len(pop_khusus)}) — khusus per kab + prov_key<br>"
750
+ f"🕒 mtime: DM={time.ctime(_mtime(DATA_FILE))} | Kab={time.ctime(_mtime(POP_KAB))} | Prov={time.ctime(_mtime(POP_PROV))} | Khusus={time.ctime(_mtime(POP_KHUSUS))}"
751
+ )
752
+
753
+ _CACHE.update({
754
+ "key": key,
755
+ "df_all": df_all,
756
+ "df_raw": df_raw,
757
+ "pop_kab": pop_kab,
758
+ "pop_prov": pop_prov,
759
+ "pop_khusus": pop_khusus,
760
+ "meta": meta,
761
+ "info": info
762
+ })
763
+
764
+ return df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info
765
+
766
+
767
+ # ============================================================
768
+ # 6) FAKTOR WILAYAH — PER JENIS
769
+ # ============================================================
770
+
771
+ def build_faktor_wilayah_jenis(
772
+ df_filtered: pd.DataFrame,
773
+ pop_kab: pd.DataFrame,
774
+ pop_prov: pd.DataFrame,
775
+ pop_khusus: pd.DataFrame,
776
+ kew_value: str
777
+ ):
778
+ if df_filtered is None or df_filtered.empty:
779
+ return pd.DataFrame()
780
+
781
+ kew_norm = str(kew_value or "").upper()
782
+ df = df_filtered.copy()
783
+ df = df[df["_dataset"].isin(["sekolah", "umum", "khusus"])].copy()
784
+ if df.empty:
785
+ return pd.DataFrame()
786
+
787
+ jenis_list = ["sekolah", "umum", "khusus"]
788
+
789
+ # tentukan level
790
+ if "PROV" in kew_norm:
791
+ key_col, label_col, label_name, mode = "prov_key", "PROV_DISP", "Provinsi", "PROV"
792
+ base_pop = pop_prov.copy() if (pop_prov is not None and not pop_prov.empty) else pd.DataFrame()
793
+ if not base_pop.empty and "prov_key" not in base_pop.columns:
794
+ base_pop["prov_key"] = base_pop["Provinsi_Label"].apply(norm_prov_label) if "Provinsi_Label" in base_pop.columns else base_pop.iloc[:, 0].apply(norm_prov_label)
795
+ base_pop = base_pop.set_index("prov_key") if (not base_pop.empty and "prov_key" in base_pop.columns) else pd.DataFrame().set_index(pd.Index([]))
796
+ else:
797
+ key_col, label_col, label_name, mode = "kab_key", "KAB_DISP", "Kab/Kota", "KAB"
798
+ base_pop = pop_kab.copy() if (pop_kab is not None and not pop_kab.empty) else pd.DataFrame()
799
+ if not base_pop.empty and "kab_key" not in base_pop.columns:
800
+ base_pop["kab_key"] = base_pop["Kab_Kota_Label"].apply(norm_kab_label) if "Kab_Kota_Label" in base_pop.columns else base_pop.iloc[:, 0].apply(norm_kab_label)
801
+ base_pop = base_pop.set_index("kab_key") if (not base_pop.empty and "kab_key" in base_pop.columns) else pd.DataFrame().set_index(pd.Index([]))
802
+
803
+ # =========================================================
804
+ # ✅ GRID WAJIB: semua wilayah × 3 jenis (meski n=0)
805
+ # =========================================================
806
+ base_keys = df[[key_col, label_col]].drop_duplicates().rename(columns={key_col: "group_key", label_col: label_name})
807
+ full = base_keys.assign(_tmp=1).merge(
808
+ pd.DataFrame({"Jenis": jenis_list, "_tmp": 1}),
809
+ on="_tmp"
810
+ ).drop(columns="_tmp")
811
+
812
+ # hitung n per jenis dari DM (boleh 0)
813
+ cnt = (
814
+ df.groupby([key_col, label_col, "_dataset"], dropna=False)
815
+ .size()
816
+ .reset_index(name="n_jenis")
817
+ .rename(columns={key_col: "group_key", label_col: label_name, "_dataset": "Jenis"})
818
+ )
819
+ cnt["Jenis"] = cnt["Jenis"].astype(str).str.lower().str.strip()
820
+
821
+ base_n = full.merge(cnt, on=["group_key", label_name, "Jenis"], how="left")
822
+ base_n["n_jenis"] = pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(int)
823
+
824
+ base_n["target_total_68_jenis"] = 0.0
825
+ base_n["pop_total_jenis"] = 0.0
826
+
827
+ # =========================
828
+ # SEKOLAH + UMUM dari POP_KAB / POP_PROV
829
+ # =========================
830
+ if not base_pop.empty:
831
+ if mode == "KAB":
832
+ pop_sekolah = pd.to_numeric(base_pop.get("jumlah_populasi_sekolah", 0), errors="coerce").fillna(0.0)
833
+ tgt_sekolah = pd.to_numeric(base_pop.get("Sampel_sekolah_68%", 0), errors="coerce").fillna(0.0)
834
+
835
+ pop_umum = pd.to_numeric(base_pop.get("jumlah_populasi_umum", 0), errors="coerce").fillna(0.0)
836
+ tgt_umum = pd.to_numeric(base_pop.get("Sampel_umum_68%", 0), errors="coerce").fillna(0.0)
837
+ else:
838
+ sma = pd.to_numeric(base_pop.get("sma ", base_pop.get("sma", 0)), errors="coerce").fillna(0.0)
839
+ smk = pd.to_numeric(base_pop.get("smk", 0), errors="coerce").fillna(0.0)
840
+ slb = pd.to_numeric(base_pop.get("slb", 0), errors="coerce").fillna(0.0)
841
+
842
+ ssma = pd.to_numeric(base_pop.get("sampel sma", 0), errors="coerce").fillna(0.0)
843
+ ssmk = pd.to_numeric(base_pop.get("sampel smk", 0), errors="coerce").fillna(0.0)
844
+ sslb = pd.to_numeric(base_pop.get("sampel slb", 0), errors="coerce").fillna(0.0)
845
+
846
+ pop_sekolah = sma + smk + slb
847
+ tgt_sekolah = ssma + ssmk + sslb
848
+
849
+ pop_umum = pd.to_numeric(base_pop.get("perpus_umum_prop", 0), errors="coerce").fillna(0.0)
850
+ tgt_umum = pop_umum * float(FALLBACK_TARGET_RATIO)
851
+
852
+ m = base_n["Jenis"].eq("sekolah")
853
+ base_n.loc[m, "pop_total_jenis"] = base_n.loc[m, "group_key"].map(pop_sekolah).fillna(0.0).values
854
+ base_n.loc[m, "target_total_68_jenis"] = base_n.loc[m, "group_key"].map(tgt_sekolah).fillna(0.0).values
855
+
856
+ m = base_n["Jenis"].eq("umum")
857
+ base_n.loc[m, "pop_total_jenis"] = base_n.loc[m, "group_key"].map(pop_umum).fillna(0.0).values
858
+ base_n.loc[m, "target_total_68_jenis"] = base_n.loc[m, "group_key"].map(tgt_umum).fillna(0.0).values
859
+
860
+ # =========================
861
+ # KHUSUS dari POP_KHUSUS
862
+ # =========================
863
+ if pop_khusus is not None and not pop_khusus.empty:
864
+ pk = pop_khusus.copy()
865
+
866
+ pk["Pop_Total_Jenis"] = pd.to_numeric(pk.get("Pop_Total_Jenis", 0), errors="coerce").fillna(0.0)
867
+ pk["Target68_Total_Jenis"] = pd.to_numeric(pk.get("Target68_Total_Jenis", 0), errors="coerce").fillna(0.0)
868
+
869
+ if mode == "PROV":
870
+ pk_prov = pk[pk["LEVEL"].astype(str).str.upper() == "PROV"].copy()
871
+ pk_map = pk_prov.groupby("prov_key", as_index=True).agg(
872
+ pop=("Pop_Total_Jenis", "sum"),
873
+ target=("Target68_Total_Jenis", "sum"),
874
+ )
875
+ pop_series = pk_map["pop"]
876
+ tgt_series = pk_map["target"]
877
+ else:
878
+ pk_kab = pk[pk["LEVEL"].astype(str).str.upper() == "KAB"].copy()
879
+ pk_map = pk_kab.groupby("kab_key", as_index=True).agg(
880
+ pop=("Pop_Total_Jenis", "sum"),
881
+ target=("Target68_Total_Jenis", "sum"),
882
+ )
883
+ pop_series = pk_map["pop"]
884
+ tgt_series = pk_map["target"]
885
+
886
+ m = base_n["Jenis"].eq("khusus")
887
+ base_n.loc[m, "pop_total_jenis"] = base_n.loc[m, "group_key"].map(pop_series).fillna(0.0).values
888
+ base_n.loc[m, "target_total_68_jenis"] = base_n.loc[m, "group_key"].map(tgt_series).fillna(0.0).values
889
+
890
+ # fallback pop dari target (jaga-jaga)
891
+ base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0.0)
892
+ base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0.0)
893
+
894
+ m_need_pop = (base_n["pop_total_jenis"] <= 0) & (base_n["target_total_68_jenis"] > 0)
895
+ base_n.loc[m_need_pop, "pop_total_jenis"] = base_n.loc[m_need_pop, "target_total_68_jenis"] / float(FALLBACK_TARGET_RATIO)
896
+
897
+ # faktor / coverage / gap
898
+ base_n["faktor_penyesuaian_jenis"] = [
899
+ faktor_penyesuaian_total(n, t)
900
+ for n, t in zip(
901
+ pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float),
902
+ pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).astype(float),
903
+ )
904
+ ]
905
+
906
+ base_n["coverage_jenis_%"] = [
907
+ (safe_div(n, p) * 100.0) if (p is not None and not pd.isna(p) and float(p) > 0) else 0.0
908
+ for n, p in zip(
909
+ pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float),
910
+ pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).astype(float),
911
+ )
912
+ ]
913
+
914
+ base_n["gap_target68_jenis"] = [
915
+ max(float(t) - float(n), 0.0)
916
+ for n, t in zip(
917
+ pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float),
918
+ pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).astype(float),
919
+ )
920
+ ]
921
+
922
+ # display
923
+ base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
924
+ base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).round(0).astype(int)
925
+ base_n["coverage_jenis_%"] = pd.to_numeric(base_n["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
926
+ base_n["faktor_penyesuaian_jenis"] = pd.to_numeric(base_n["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
927
+ base_n["gap_target68_jenis"] = pd.to_numeric(base_n["gap_target68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
928
+
929
+ return base_n
930
+
931
+
932
+
933
+ # ============================================================
934
+ # 7) AGREGAT WILAYAH × JENIS (PATCH: faktor 68% PER JENIS)
935
+ # ============================================================
936
+
937
+ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
938
+ if df_filtered is None or df_filtered.empty:
939
+ return pd.DataFrame()
940
+
941
+ kew_norm = str(kew_value or "").upper()
942
+ df = df_filtered.copy()
943
+
944
+ if "PROV" in kew_norm:
945
+ key_col, label_col, label_name = "prov_key", "PROV_DISP", "Provinsi"
946
+ else:
947
+ key_col, label_col, label_name = "kab_key", "KAB_DISP", "Kab/Kota"
948
+
949
+ df = df[df["_dataset"].isin(["sekolah", "umum", "khusus"])].copy()
950
+ if df.empty:
951
+ return pd.DataFrame()
952
+
953
+ jenis_list = ["sekolah", "umum", "khusus"]
954
+
955
+ # =========================================================
956
+ # ✅ GRID WAJIB: semua wilayah × 3 jenis (meski agregat kosong)
957
+ # =========================================================
958
+ base_keys = df[[key_col, label_col]].drop_duplicates().rename(columns={key_col: "group_key", label_col: label_name})
959
+ full = base_keys.assign(_tmp=1).merge(
960
+ pd.DataFrame({"Jenis": jenis_list, "_tmp": 1}),
961
+ on="_tmp"
962
+ ).drop(columns="_tmp")
963
+
964
+ # agregat dari data yang ada
965
+ agg_real = df.groupby([key_col, label_col, "_dataset"], dropna=False).agg(
966
+ Jumlah=("Indeks_Dasar_0_100", "size"),
967
+ Rata2_sub_koleksi=("sub_koleksi", "mean"),
968
+ Rata2_sub_sdm=("sub_sdm", "mean"),
969
+ Rata2_sub_pelayanan=("sub_pelayanan", "mean"),
970
+ Rata2_sub_pengelolaan=("sub_pengelolaan", "mean"),
971
+ Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
972
+ Rata2_dim_kinerja=("dim_kinerja", "mean"),
973
+ Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_0_100", "mean"),
974
+ ).reset_index().rename(columns={key_col: "group_key", label_col: label_name, "_dataset": "Jenis"})
975
+
976
+ agg_real["Jenis"] = agg_real["Jenis"].astype(str).str.lower().str.strip()
977
+
978
+ # tempel ke grid + fill 0
979
+ agg = full.merge(agg_real, on=["group_key", label_name, "Jenis"], how="left")
980
+ for c in ["Jumlah","Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
981
+ "Rata2_dim_kepatuhan","Rata2_dim_kinerja","Indeks_Dasar_Agregat_0_100"]:
982
+ if c in agg.columns:
983
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0)
984
+
985
+ agg["Jumlah"] = agg["Jumlah"].round(0).astype(int)
986
+
987
+ # merge faktor PER JENIS (sekarang pasti match, karena grid ada)
988
+ if faktor_wilayah_jenis is None or faktor_wilayah_jenis.empty:
989
+ agg["faktor_penyesuaian_jenis"] = 1.0
990
+ agg["target_total_68_jenis"] = 0
991
+ agg["pop_total_jenis"] = 0
992
+ agg["coverage_jenis_%"] = 0.0
993
+ agg["gap_target68_jenis"] = 0
994
+ else:
995
+ fw = faktor_wilayah_jenis.copy()
996
+ fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
997
+
998
+ keep = ["group_key", label_name, "Jenis",
999
+ "faktor_penyesuaian_jenis", "target_total_68_jenis", "pop_total_jenis",
1000
+ "coverage_jenis_%", "gap_target68_jenis"]
1001
+ fw = fw[[c for c in keep if c in fw.columns]].copy()
1002
+
1003
+ agg = agg.merge(fw, on=["group_key", label_name, "Jenis"], how="left")
1004
+
1005
+ agg["faktor_penyesuaian_jenis"] = pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0)
1006
+
1007
+ for c in ["target_total_68_jenis","pop_total_jenis","gap_target68_jenis"]:
1008
+ if c in agg.columns:
1009
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0).round(0).astype(int)
1010
+
1011
+ if "coverage_jenis_%" in agg.columns:
1012
+ agg["coverage_jenis_%"] = pd.to_numeric(agg["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
1013
+
1014
+ # Indeks FINAL PER JENIS
1015
+ agg["Indeks_Final_Agregat_0_100"] = (
1016
+ pd.to_numeric(agg["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0.0)
1017
+ * pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0)
1018
+ )
1019
+
1020
+ # rounding tampilan
1021
+ for c in [
1022
+ "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1023
+ "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
1024
+ ]:
1025
+ if c in agg.columns:
1026
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0).round(3)
1027
+
1028
+ for c in ["Indeks_Dasar_Agregat_0_100","Indeks_Final_Agregat_0_100"]:
1029
+ if c in agg.columns:
1030
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0).round(2)
1031
+
1032
+ agg["faktor_penyesuaian_jenis"] = pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
1033
+
1034
+ return agg
1035
+
1036
+ # ============================================================
1037
+ # 8) AGREGAT WILAYAH (KESELURUHAN) — FIX: avg3 dari 3 jenis
1038
+ # + tampilkan Pop/Target/Terkumpul per jenis & total
1039
+ # ============================================================
1040
+
1041
+ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
1042
+ if agg_jenis is None or agg_jenis.empty:
1043
+ return pd.DataFrame()
1044
+
1045
+ kew_norm = str(kew_value or "").upper()
1046
+ label_name = "Provinsi" if "PROV" in kew_norm else "Kab/Kota"
1047
+
1048
+ jenis_list = ["sekolah", "umum", "khusus"]
1049
+
1050
+ a = agg_jenis.copy()
1051
+ a["Jenis"] = a["Jenis"].astype(str).str.lower().str.strip()
1052
+
1053
+ base_keys = a[["group_key", label_name]].drop_duplicates()
1054
+
1055
+ full = base_keys.assign(_tmp=1).merge(
1056
+ pd.DataFrame({"Jenis": jenis_list, "_tmp": 1}),
1057
+ on="_tmp"
1058
+ ).drop(columns="_tmp")
1059
+
1060
+ cols_need = [
1061
+ "Jumlah",
1062
+ "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1063
+ "Rata2_dim_kepatuhan","Rata2_dim_kinerja",
1064
+ "Indeks_Dasar_Agregat_0_100",
1065
+ "Indeks_Final_Agregat_0_100",
1066
+ ]
1067
+ cols_present = [c for c in cols_need if c in a.columns]
1068
+
1069
+ full = full.merge(
1070
+ a[["group_key", label_name, "Jenis"] + cols_present],
1071
+ on=["group_key", label_name, "Jenis"],
1072
+ how="left"
1073
+ )
1074
+
1075
+ # missing=0 (avg3 tetap ÷3)
1076
+ for c in cols_present:
1077
+ full[c] = pd.to_numeric(full[c], errors="coerce").fillna(0.0)
1078
+
1079
+ out = full.groupby(["group_key", label_name], as_index=False).agg(
1080
+ n_total=("Jumlah", "sum"),
1081
+ Rata2_sub_koleksi=("Rata2_sub_koleksi", "mean"),
1082
+ Rata2_sub_sdm=("Rata2_sub_sdm", "mean"),
1083
+ Rata2_sub_pelayanan=("Rata2_sub_pelayanan", "mean"),
1084
+ Rata2_sub_pengelolaan=("Rata2_sub_pengelolaan", "mean"),
1085
+ Rata2_dim_kepatuhan=("Rata2_dim_kepatuhan", "mean"),
1086
+ Rata2_dim_kinerja=("Rata2_dim_kinerja", "mean"),
1087
+ Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_Agregat_0_100", "mean"),
1088
+ Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
1089
+ )
1090
+
1091
+ # tempel Pop/Target/Terkumpul per jenis & total
1092
+ if faktor_wilayah_jenis is not None and not faktor_wilayah_jenis.empty:
1093
+ fw = faktor_wilayah_jenis.copy()
1094
+ fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
1095
+
1096
+ piv = fw.pivot_table(
1097
+ index=["group_key", label_name],
1098
+ columns="Jenis",
1099
+ values=["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis", "faktor_penyesuaian_jenis"],
1100
+ aggfunc="first"
1101
+ )
1102
+
1103
+ piv.columns = [f"{v}_{k}" for v, k in piv.columns]
1104
+ piv = piv.reset_index()
1105
+
1106
+ out = out.merge(piv, on=["group_key", label_name], how="left")
1107
+
1108
+ # NaN -> 0 / 1
1109
+ for j in ["sekolah", "umum", "khusus"]:
1110
+ for basecol in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
1111
+ c = f"{basecol}_{j}"
1112
+ if c in out.columns:
1113
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1114
+
1115
+ cfac = f"faktor_penyesuaian_jenis_{j}"
1116
+ if cfac in out.columns:
1117
+ out[cfac] = pd.to_numeric(out[cfac], errors="coerce").fillna(1.0).round(3)
1118
+
1119
+ # TOTAL (sum 3 jenis)
1120
+ out["pop_total_all"] = (
1121
+ out.get("pop_total_jenis_sekolah", 0)
1122
+ + out.get("pop_total_jenis_umum", 0)
1123
+ + out.get("pop_total_jenis_khusus", 0)
1124
+ ).astype(int)
1125
+
1126
+ out["target_total_68_all"] = (
1127
+ out.get("target_total_68_jenis_sekolah", 0)
1128
+ + out.get("target_total_68_jenis_umum", 0)
1129
+ + out.get("target_total_68_jenis_khusus", 0)
1130
+ ).astype(int)
1131
+
1132
+ out["terkumpul_all"] = (
1133
+ out.get("n_jenis_sekolah", 0)
1134
+ + out.get("n_jenis_umum", 0)
1135
+ + out.get("n_jenis_khusus", 0)
1136
+ ).astype(int)
1137
+
1138
+ out["coverage_target68_all_%"] = np.where(
1139
+ pd.to_numeric(out["target_total_68_all"], errors="coerce").fillna(0).values > 0,
1140
+ (pd.to_numeric(out["terkumpul_all"], errors="coerce").fillna(0).values / pd.to_numeric(out["target_total_68_all"], errors="coerce").fillna(0).values) * 100.0,
1141
+ 0.0
1142
+ )
1143
+ out["coverage_target68_all_%"] = pd.to_numeric(out["coverage_target68_all_%"], errors="coerce").fillna(0.0).round(2)
1144
+
1145
+ # rounding index
1146
+ for c in [
1147
+ "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1148
+ "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
1149
+ ]:
1150
+ if c in out.columns:
1151
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
1152
+
1153
+ for c in ["Indeks_Dasar_Agregat_0_100","Indeks_Final_Wilayah_0_100"]:
1154
+ if c in out.columns:
1155
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1156
+
1157
+ out["n_total"] = pd.to_numeric(out["n_total"], errors="coerce").fillna(0).round(0).astype(int)
1158
+
1159
+ return out
1160
+
1161
+
1162
+ # ============================================================
1163
+ # 9) SUMMARY (PER JENIS) + KESELURUHAN
1164
+ # ============================================================
1165
+
1166
+ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
1167
+ jenis_list = ["sekolah", "umum", "khusus"]
1168
+
1169
+ def _row_default(jenis):
1170
+ return {
1171
+ "Jenis": jenis,
1172
+ "Jumlah_Wilayah": 0,
1173
+ "Total_Perpus": 0,
1174
+ "Pop_Total_Jenis": 0,
1175
+ "Target68_Total_Jenis": 0,
1176
+ "Terkumpul_Jenis": 0,
1177
+ "Coverage_Target68_Jenis_%": 0.0,
1178
+ "Indeks_Dasar_0_100": 0.0,
1179
+ "Indeks_Final_Disesuaikan_0_100": 0.0,
1180
+ "Penyesuaian_Poin": 0.0,
1181
+ }
1182
+
1183
+ rows_by_jenis = {j: _row_default(j) for j in jenis_list}
1184
+
1185
+ if agg_jenis is not None and not agg_jenis.empty:
1186
+ a = agg_jenis.copy()
1187
+ a["Jenis"] = a["Jenis"].astype(str).str.lower().str.strip()
1188
+
1189
+ for c in ["Jumlah","Indeks_Dasar_Agregat_0_100","Indeks_Final_Agregat_0_100","pop_total_jenis","target_total_68_jenis"]:
1190
+ if c in a.columns:
1191
+ a[c] = pd.to_numeric(a[c], errors="coerce").fillna(0)
1192
+
1193
+ for jenis in jenis_list:
1194
+ sub = a[a["Jenis"] == jenis].copy()
1195
+ if sub.empty:
1196
+ continue
1197
+
1198
+ jumlah_wilayah = int(sub.shape[0])
1199
+ terkumpul = int(pd.to_numeric(sub.get("Jumlah", 0), errors="coerce").fillna(0).sum())
1200
+ pop_total = int(pd.to_numeric(sub.get("pop_total_jenis", 0), errors="coerce").fillna(0).sum())
1201
+ target68 = int(pd.to_numeric(sub.get("target_total_68_jenis", 0), errors="coerce").fillna(0).sum())
1202
+
1203
+ coverage = (terkumpul / target68 * 100.0) if target68 > 0 else 0.0
1204
+ dasar = float(pd.to_numeric(sub.get("Indeks_Dasar_Agregat_0_100", 0), errors="coerce").fillna(0).mean())
1205
+ final = float(pd.to_numeric(sub.get("Indeks_Final_Agregat_0_100", 0), errors="coerce").fillna(0).mean())
1206
+
1207
+ rows_by_jenis[jenis] = {
1208
+ "Jenis": jenis,
1209
+ "Jumlah_Wilayah": jumlah_wilayah,
1210
+ "Total_Perpus": terkumpul,
1211
+ "Pop_Total_Jenis": pop_total,
1212
+ "Target68_Total_Jenis": target68,
1213
+ "Terkumpul_Jenis": terkumpul,
1214
+ "Coverage_Target68_Jenis_%": float(coverage),
1215
+ "Indeks_Dasar_0_100": float(dasar),
1216
+ "Indeks_Final_Disesuaikan_0_100": float(final),
1217
+ "Penyesuaian_Poin": float(final - dasar),
1218
+ }
1219
+
1220
+ rows = [rows_by_jenis[j] for j in jenis_list]
1221
+
1222
+ dasar_all = (rows_by_jenis["sekolah"]["Indeks_Dasar_0_100"]
1223
+ + rows_by_jenis["umum"]["Indeks_Dasar_0_100"]
1224
+ + rows_by_jenis["khusus"]["Indeks_Dasar_0_100"]) / 3.0
1225
+
1226
+ final_all = (rows_by_jenis["sekolah"]["Indeks_Final_Disesuaikan_0_100"]
1227
+ + rows_by_jenis["umum"]["Indeks_Final_Disesuaikan_0_100"]
1228
+ + rows_by_jenis["khusus"]["Indeks_Final_Disesuaikan_0_100"]) / 3.0
1229
+
1230
+ pop_all = int(rows_by_jenis["sekolah"]["Pop_Total_Jenis"]
1231
+ + rows_by_jenis["umum"]["Pop_Total_Jenis"]
1232
+ + rows_by_jenis["khusus"]["Pop_Total_Jenis"])
1233
+
1234
+ target_all = int(rows_by_jenis["sekolah"]["Target68_Total_Jenis"]
1235
+ + rows_by_jenis["umum"]["Target68_Total_Jenis"]
1236
+ + rows_by_jenis["khusus"]["Target68_Total_Jenis"])
1237
+
1238
+ terkumpul_all = int(rows_by_jenis["sekolah"]["Terkumpul_Jenis"]
1239
+ + rows_by_jenis["umum"]["Terkumpul_Jenis"]
1240
+ + rows_by_jenis["khusus"]["Terkumpul_Jenis"])
1241
+
1242
+ coverage_all = (terkumpul_all / target_all * 100.0) if target_all > 0 else 0.0
1243
+
1244
+ jumlah_wilayah_all = int(agg_total.shape[0]) if (agg_total is not None and not agg_total.empty) else int(
1245
+ max(rows_by_jenis["sekolah"]["Jumlah_Wilayah"],
1246
+ rows_by_jenis["umum"]["Jumlah_Wilayah"],
1247
+ rows_by_jenis["khusus"]["Jumlah_Wilayah"])
1248
+ )
1249
+
1250
+ rows.append({
1251
+ "Jenis": "keseluruhan",
1252
+ "Jumlah_Wilayah": jumlah_wilayah_all,
1253
+ "Total_Perpus": terkumpul_all,
1254
+ "Pop_Total_Jenis": pop_all,
1255
+ "Target68_Total_Jenis": target_all,
1256
+ "Terkumpul_Jenis": terkumpul_all,
1257
+ "Coverage_Target68_Jenis_%": float(coverage_all),
1258
+ "Indeks_Dasar_0_100": float(dasar_all),
1259
+ "Indeks_Final_Disesuaikan_0_100": float(final_all),
1260
+ "Penyesuaian_Poin": float(final_all - dasar_all),
1261
+ })
1262
+
1263
+ out = pd.DataFrame(rows)
1264
+
1265
+ for c in ["Jumlah_Wilayah","Total_Perpus","Pop_Total_Jenis","Target68_Total_Jenis","Terkumpul_Jenis"]:
1266
+ if c in out.columns:
1267
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1268
+
1269
+ for c in ["Coverage_Target68_Jenis_%","Indeks_Dasar_0_100","Indeks_Final_Disesuaikan_0_100","Penyesuaian_Poin"]:
1270
+ if c in out.columns:
1271
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1272
+
1273
+ return out
1274
+
1275
+
1276
+
1277
+
1278
+ # ============================================================
1279
+ # 10) DETAIL ENTITAS: Final menempel dari agg_total (wilayah)
1280
+ # ============================================================
1281
+
1282
+ def attach_final_to_detail(df_filtered: pd.DataFrame, agg_total: pd.DataFrame, meta: dict, kew_value: str):
1283
+ if df_filtered is None or df_filtered.empty:
1284
+ return pd.DataFrame()
1285
+
1286
+ kew_norm = str(kew_value or "").upper()
1287
+ df = df_filtered.copy()
1288
+
1289
+ if "KAB" in kew_norm or "KOTA" in kew_norm:
1290
+ key_col = "kab_key"
1291
+ label_cols = ("PROV_DISP", "KAB_DISP")
1292
+ elif "PROV" in kew_norm:
1293
+ key_col = "prov_key"
1294
+ label_cols = ("PROV_DISP", "KAB_DISP")
1295
+ else:
1296
+ key_col = "kab_key"
1297
+ label_cols = ("PROV_DISP", "KAB_DISP")
1298
+
1299
+ if agg_total is None or agg_total.empty:
1300
+ df["Indeks_Final_0_100"] = df["Indeks_Dasar_0_100"]
1301
+ else:
1302
+ m = agg_total[["group_key", "Indeks_Final_Wilayah_0_100"]].copy()
1303
+ df = df.merge(m, left_on=key_col, right_on="group_key", how="left")
1304
+ df["Indeks_Final_0_100"] = df["Indeks_Final_Wilayah_0_100"].fillna(df["Indeks_Dasar_0_100"])
1305
+ df = df.drop(columns=[c for c in ["group_key","Indeks_Final_Wilayah_0_100"] if c in df.columns])
1306
+
1307
+ base_cols = [label_cols[0], label_cols[1], "KEW_NORM", "_dataset"]
1308
+ if meta.get("nama_col") and meta["nama_col"] in df.columns:
1309
+ df["nm_perpustakaan"] = df[meta["nama_col"]].astype(str)
1310
+ base_cols.insert(2, "nm_perpustakaan")
1311
+
1312
+ keep = base_cols + [
1313
+ "sub_koleksi","sub_sdm","sub_pelayanan","sub_pengelolaan",
1314
+ "dim_kepatuhan","dim_kinerja",
1315
+ "Indeks_Dasar_0_100",
1316
+ "Indeks_Final_0_100",
1317
+ ]
1318
+ keep = [c for c in keep if c in df.columns]
1319
+
1320
+ out = df[keep].copy()
1321
+ out = out.rename(columns={label_cols[0]:"Provinsi", label_cols[1]:"Kab/Kota", "_dataset":"Jenis"})
1322
+
1323
+ for c in ["sub_koleksi","sub_sdm","sub_pelayanan","sub_pengelolaan","dim_kepatuhan","dim_kinerja"]:
1324
+ if c in out.columns:
1325
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
1326
+ for c in ["Indeks_Dasar_0_100","Indeks_Final_0_100"]:
1327
+ if c in out.columns:
1328
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1329
+
1330
+ return out
1331
+
1332
+
1333
+ # ============================================================
1334
+ # 11) VERIFIKASI PER JENIS (OPSIONAL, TANPA KOMA)
1335
+ # ============================================================
1336
+
1337
+ def build_verif_jenis(faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
1338
+ if faktor_wilayah_jenis is None or faktor_wilayah_jenis.empty:
1339
+ return pd.DataFrame()
1340
+
1341
+ kew_norm = str(kew_value or "").upper()
1342
+ label_col = "Provinsi" if "PROV" in kew_norm else "Kab/Kota"
1343
+
1344
+ out = faktor_wilayah_jenis.copy()
1345
+ keep = [c for c in [
1346
+ label_col, "Jenis",
1347
+ "pop_total_jenis", "target_total_68_jenis", "n_jenis",
1348
+ "coverage_jenis_%", "faktor_penyesuaian_jenis", "gap_target68_jenis"
1349
+ ] if c in out.columns]
1350
+
1351
+ out = out[keep].copy()
1352
+
1353
+ # tanpa koma untuk integer columns
1354
+ for c in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
1355
+ if c in out.columns:
1356
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1357
+
1358
+ # coverage 2 desimal tetap boleh (request awal kamu coverage decimal 2)
1359
+ if "coverage_jenis_%" in out.columns:
1360
+ out["coverage_jenis_%"] = pd.to_numeric(out["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
1361
+
1362
+ # faktor 3 desimal
1363
+ if "faktor_penyesuaian_jenis" in out.columns:
1364
+ out["faktor_penyesuaian_jenis"] = pd.to_numeric(out["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
1365
+
1366
+ return out
1367
+
1368
+
1369
+ # ============================================================
1370
+ # 12) BELL CURVE
1371
+ # ============================================================
1372
+
1373
+ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str | None = None, hover_cols: list[str] | None = None, min_points: int = 2):
1374
+ fig = go.Figure()
1375
+ fig.update_layout(
1376
+ title=title,
1377
+ xaxis_title="Indeks (0–100)",
1378
+ yaxis_title="Kepadatan",
1379
+ hovermode="x unified",
1380
+ margin=dict(l=40, r=20, t=60, b=40),
1381
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
1382
+ )
1383
+
1384
+ if dfp is None or dfp.empty or xcol not in dfp.columns:
1385
+ fig.add_annotation(text="Tidak ada data untuk ditampilkan.", x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False)
1386
+ fig.update_xaxes(range=[0, 100])
1387
+ fig.update_yaxes(rangemode="tozero")
1388
+ return fig
1389
+
1390
+ d = dfp.dropna(subset=[xcol]).copy()
1391
+ if len(d) < 1:
1392
+ fig.add_annotation(text="Tidak ada data untuk ditampilkan.", x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False)
1393
+ fig.update_xaxes(range=[0, 100])
1394
+ fig.update_yaxes(rangemode="tozero")
1395
+ return fig
1396
+
1397
+ if len(d) < min_points:
1398
+ x_single = float(pd.to_numeric(d[xcol], errors="coerce").iloc[0])
1399
+ hovertext = None
1400
+ if label_col and label_col in d.columns:
1401
+ hovertext = [f"{d[label_col].iloc[0]}<br>{xcol}: {x_single:.2f}"]
1402
+ fig.add_trace(go.Scatter(
1403
+ x=[x_single], y=[0], mode="markers", name="Data", marker=dict(size=10),
1404
+ hovertext=hovertext,
1405
+ hovertemplate="%{hovertext}<extra></extra>" if hovertext is not None else "Indeks: %{x:.2f}<extra></extra>",
1406
+ showlegend=False,
1407
+ ))
1408
+ fig.add_vline(x=x_single, line_width=1, line_dash="dash", annotation_text=f"Nilai: {x_single:.1f}", annotation_position="top")
1409
+ fig.add_annotation(text="Data hanya 1 titik (kurva normal tidak dibuat).", x=0.5, y=0.08, xref="paper", yref="paper", showarrow=False)
1410
+ fig.update_xaxes(range=[0, 100])
1411
+ fig.update_yaxes(rangemode="tozero")
1412
+ return fig
1413
+
1414
+ x = pd.to_numeric(d[xcol], errors="coerce").astype(float).values
1415
+ x = x[np.isfinite(x)]
1416
+ if len(x) < 2:
1417
+ fig.add_annotation(text="Data tidak cukup untuk kurva.", x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False)
1418
+ fig.update_xaxes(range=[0, 100])
1419
+ fig.update_yaxes(rangemode="tozero")
1420
+ return fig
1421
+
1422
+ mu = float(np.mean(x))
1423
+ sigma = float(np.std(x, ddof=1)) if len(x) > 1 else 0.0
1424
+ if not np.isfinite(sigma) or sigma <= 1e-6:
1425
+ sigma = max(float(np.std(x, ddof=0)), 1e-3)
1426
+
1427
+ xmin = max(0.0, float(np.min(x)) - 5.0)
1428
+ xmax = min(100.0, float(np.max(x)) + 5.0)
1429
+ if xmax - xmin < 1e-6:
1430
+ xmin = max(0.0, mu - 1.0)
1431
+ xmax = min(100.0, mu + 1.0)
1432
+
1433
+ xs = np.linspace(xmin, xmax, 250)
1434
+ pdf = (1.0 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((xs - mu) / sigma) ** 2)
1435
+
1436
+ fig.add_trace(go.Scatter(
1437
+ x=xs, y=pdf, mode="lines", name="Kurva Normal",
1438
+ hovertemplate="x=%{x:.2f}<br>pdf=%{y:.4f}<extra></extra>"
1439
+ ))
1440
+
1441
+ hovertext = None
1442
+ if label_col and label_col in d.columns:
1443
+ hcols = hover_cols or []
1444
+ parts = []
1445
+ for _, r in d.iterrows():
1446
+ try:
1447
+ xv = float(pd.to_numeric(r.get(xcol, np.nan), errors="coerce"))
1448
+ except Exception:
1449
+ xv = np.nan
1450
+ s = f"{r[label_col]}"
1451
+ s += f"<br>{xcol}: {xv:.2f}" if np.isfinite(xv) else f"<br>{xcol}: NA"
1452
+ for c in hcols:
1453
+ if c in d.columns and pd.notna(r.get(c, np.nan)):
1454
+ v = r[c]
1455
+ if isinstance(v, (int, np.integer)):
1456
+ s += f"<br>{c}: {int(v)}"
1457
+ elif isinstance(v, (float, np.floating)):
1458
+ s += f"<br>{c}: {float(v):.3f}"
1459
+ else:
1460
+ s += f"<br>{c}: {v}"
1461
+ parts.append(s)
1462
+ hovertext = parts
1463
+
1464
+ fig.add_trace(go.Scatter(
1465
+ x=x, y=np.zeros_like(x), mode="markers", name="Data", marker=dict(size=8),
1466
+ hovertext=hovertext,
1467
+ hovertemplate="%{hovertext}<extra></extra>" if hovertext is not None else "Indeks: %{x:.2f}<extra></extra>",
1468
+ showlegend=False
1469
+ ))
1470
+
1471
+ q1, q2, q3 = np.percentile(x, [25, 50, 75])
1472
+ for xv, lab in [(q1, "Q1"), (q2, "Q2 (Median)"), (q3, "Q3"), (mu, "Mean")]:
1473
+ fig.add_vline(x=float(xv), line_width=1, line_dash="dash", annotation_text=f"{lab}: {xv:.1f}", annotation_position="top")
1474
+
1475
+ fig.update_xaxes(range=[0, 100])
1476
+ fig.update_yaxes(rangemode="tozero")
1477
+ return fig
1478
+
1479
+
1480
+ # ============================================================
1481
+ # 13) KPI DASHBOARD (FINAL: hanya Final & Dasar)
1482
+ # ============================================================
1483
+
1484
+ def compute_dashboard_kpis(summary_jenis: pd.DataFrame):
1485
+ def _get(j, col):
1486
+ sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1487
+ if sub.empty:
1488
+ return 0.0
1489
+ return float(pd.to_numeric(sub[col], errors="coerce").fillna(0).iloc[0])
1490
+
1491
+ final_all = _get("keseluruhan", "Indeks_Final_Disesuaikan_0_100")
1492
+ dasar_all = _get("keseluruhan", "Indeks_Dasar_0_100")
1493
+
1494
+ return {"final_all": final_all, "dasar_all": dasar_all}
1495
+
1496
+
1497
+ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame = None, agg_jenis: pd.DataFrame = None, faktor_wilayah_jenis=None) -> str:
1498
+ if summary_jenis is None or summary_jenis.empty:
1499
+ return ""
1500
+
1501
+ k = compute_dashboard_kpis(summary_jenis)
1502
+
1503
+ def fmt(x, nd=2):
1504
+ return "NA" if pd.isna(x) else f"{x:.{nd}f}"
1505
+
1506
+ return f"""
1507
+ <div style="display:flex; gap:12px; flex-wrap:wrap;">
1508
+ <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:260px;">
1509
+ <div style="opacity:0.8;">Indeks IPLM FINAL (Disesuaikan)</div>
1510
+ <div style="font-size:26px; font-weight:700;">{fmt(k["final_all"],2)}</div>
1511
+ <div style="opacity:0.7;">Sumber: Ringkasan baris “keseluruhan”</div>
1512
+ </div>
1513
+
1514
+ <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:260px;">
1515
+ <div style="opacity:0.8;">Indeks Dasar (Tanpa Penyesuaian)</div>
1516
+ <div style="font-size:26px; font-weight:700;">{fmt(k["dasar_all"],2)}</div>
1517
+ <div style="opacity:0.7;">Sumber: Ringkasan baris “keseluruhan”</div>
1518
+ </div>
1519
+ </div>
1520
+ """.strip()
1521
+
1522
+
1523
+
1524
+ # ============================================================
1525
+ # 14) LLM + WORD
1526
+ # ============================================================
1527
+
1528
+ _HF_CLIENT = None
1529
+
1530
+ def get_llm_client():
1531
+ global _HF_CLIENT
1532
+ if _HF_CLIENT is not None:
1533
+ return _HF_CLIENT
1534
+ try:
1535
+ _HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN) if HF_TOKEN else InferenceClient(model=LLM_MODEL_NAME)
1536
+ return _HF_CLIENT
1537
+ except Exception:
1538
+ _HF_CLIENT = None
1539
+ return None
1540
+
1541
+ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_total: pd.DataFrame, wilayah: str, kew: str) -> str:
1542
+ lines = []
1543
+ lines.append(f"Wilayah filter: {wilayah}")
1544
+ lines.append(f"Kewenangan: {kew}")
1545
+
1546
+ if summary_jenis is not None and not summary_jenis.empty:
1547
+ lines.append("\nRingkasan (jenis + keseluruhan):")
1548
+ for _, r in summary_jenis.iterrows():
1549
+ lines.append(
1550
+ f"- {r['Jenis']}: pop={int(r.get('Pop_Total_Jenis',0))}, target68={int(r.get('Target68_Total_Jenis',0))}, "
1551
+ f"terkumpul={int(r.get('Terkumpul_Jenis',0))}, coverage={float(r.get('Coverage_Target68_Jenis_%',0)):.2f}%, "
1552
+ f"dasar={float(r.get('Indeks_Dasar_0_100',0)):.2f}, final={float(r.get('Indeks_Final_Disesuaikan_0_100',0)):.2f}"
1553
+ )
1554
+
1555
+ if agg_total is not None and not agg_total.empty and "Indeks_Final_Wilayah_0_100" in agg_total.columns:
1556
+ label_col = "Kab/Kota" if "Kab/Kota" in agg_total.columns else ("Provinsi" if "Provinsi" in agg_total.columns else None)
1557
+ lines.append("\nTop 5 wilayah (Final tertinggi):")
1558
+ top = agg_total.sort_values("Indeks_Final_Wilayah_0_100", ascending=False).head(5)
1559
+ for _, r in top.iterrows():
1560
+ wl = r.get(label_col, "(wilayah)") if label_col else "(wilayah)"
1561
+ lines.append(f"- {wl}: Final={float(r['Indeks_Final_Wilayah_0_100']):.2f}")
1562
+
1563
+ return "\n".join(lines)
1564
+
1565
+ def generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah, kew):
1566
+ ctx = build_context(summary_jenis, agg_total, verif_total, wilayah, kew)
1567
+ client = get_llm_client()
1568
+ if client is None or not USE_LLM:
1569
+ return "Analisis otomatis (LLM) tidak digunakan / tidak tersedia."
1570
+
1571
+ system_prompt = "Anda adalah analis kebijakan perpustakaan di Indonesia. Tulis analisis ringkas berbasis data."
1572
+ user_prompt = f"""
1573
+ DATA IPLM (RINGKAS):
1574
+
1575
+ {ctx}
1576
+
1577
+ Buat analisis 3 paragraf:
1578
+ 1) Gambaran umum.
1579
+ 2) Per jenis (sekolah/umum/khusus) + keseluruhan.
1580
+ 3) Rekomendasi singkat.
1581
+ Catatan khusus : IPLM adalah Indeks Pengembangan Literasi Masyarakat
1582
+ """
1583
+ try:
1584
+ resp = client.chat_completion(
1585
+ model=LLM_MODEL_NAME,
1586
+ messages=[{"role":"system","content":system_prompt},{"role":"user","content":user_prompt}],
1587
+ max_tokens=700,
1588
+ temperature=0.25,
1589
+ top_p=0.9,
1590
+ )
1591
+ text = resp.choices[0].message.content.strip()
1592
+ return text if text else "LLM mengembalikan respon kosong."
1593
+ except Exception as e:
1594
+ return f"⚠️ Error LLM: {repr(e)}"
1595
+
1596
+ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_text):
1597
+ doc = Document()
1598
+ doc.add_heading(f"Laporan IPLM — {wilayah}", level=1)
1599
+
1600
+ doc.add_heading("Ringkasan (Jenis + Keseluruhan)", level=2)
1601
+
1602
+ show = summary_jenis.copy() if summary_jenis is not None else pd.DataFrame()
1603
+ if not show.empty:
1604
+ preferred = [
1605
+ "Jenis","Jumlah_Wilayah","Total_Perpus",
1606
+ "Pop_Total_Jenis","Target68_Total_Jenis","Terkumpul_Jenis","Coverage_Target68_Jenis_%",
1607
+ "Indeks_Dasar_0_100","Indeks_Final_Disesuaikan_0_100","Penyesuaian_Poin"
1608
+ ]
1609
+ show = show[[c for c in preferred if c in show.columns]]
1610
+
1611
+ table = doc.add_table(rows=1, cols=len(show.columns))
1612
+ hdr = table.rows[0].cells
1613
+ for i, c in enumerate(show.columns):
1614
+ hdr[i].text = str(c)
1615
+
1616
+ for _, row in show.iterrows():
1617
+ cells = table.add_row().cells
1618
+ for i, c in enumerate(show.columns):
1619
+ v = row[c]
1620
+ if pd.isna(v):
1621
+ cells[i].text = ""
1622
+ elif isinstance(v, (float, np.floating)):
1623
+ cells[i].text = f"{float(v):.2f}"
1624
+ elif isinstance(v, (int, np.integer)):
1625
+ cells[i].text = str(int(v))
1626
+ else:
1627
+ cells[i].text = str(v)
1628
+
1629
+ doc.add_heading("Analisis (opsional)", level=2)
1630
+ for p in (analysis_text or "").split("\n"):
1631
+ if p.strip():
1632
+ doc.add_paragraph(p.strip())
1633
+
1634
+ outpath = tempfile.mktemp(suffix=".docx")
1635
+ doc.save(outpath)
1636
+ return outpath
1637
+
1638
+ # ============================================================
1639
+ # 15) CORE RUN
1640
+ # ============================================================
1641
+
1642
+ def _empty_outputs(msg="⚠️ Data belum siap."):
1643
+ empty = pd.DataFrame()
1644
+ empty_fig = go.Figure()
1645
+ return (
1646
+ "", # kpi_md
1647
+ empty, empty, empty, empty, empty,
1648
+ None, None, None, None, None,
1649
+ empty_fig, empty_fig, empty_fig,
1650
+ msg, "Analisis belum tersedia."
1651
+ )
1652
+
1653
+ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta):
1654
+ try:
1655
+ if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1656
+ return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1657
+
1658
+ # FILTER (df_all)
1659
+ df = df_all.copy()
1660
+ if prov_value and prov_value != "(Semua)":
1661
+ df = df[df["PROV_DISP"] == prov_value]
1662
+ if kab_value and kab_value != "(Semua)":
1663
+ df = df[df["KAB_DISP"] == kab_value]
1664
+ if kew_value and kew_value != "(Semua)":
1665
+ df = df[df["KEW_NORM"] == kew_value]
1666
+
1667
+ if df.empty:
1668
+ return _empty_outputs("Tidak ada data untuk filter ini.")
1669
+
1670
+ # pipeline
1671
+ faktor_wilayah_jenis = build_faktor_wilayah_jenis(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1672
+ agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah_jenis, kew_value or "(Semua)")
1673
+ agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah_jenis, kew_value or "(Semua)")
1674
+
1675
+ # SUMMARY (ini yang tampil Pop/Target/Terkumpul/Coverage)
1676
+ summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1677
+
1678
+ verif_total = build_verif_jenis(faktor_wilayah_jenis, kew_value or "(Semua)")
1679
+ detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
1680
+
1681
+ # view agg_jenis (UI cuma sampai indeks dasar)
1682
+ if agg_jenis_full is None or agg_jenis_full.empty:
1683
+ agg_jenis_view = agg_jenis_full
1684
+ else:
1685
+ kew_norm = str(kew_value or "").upper()
1686
+ label_name = "Kab/Kota" if ("KAB" in kew_norm or "KOTA" in kew_norm) else ("Provinsi" if "PROV" in kew_norm else "Kab/Kota")
1687
+ cols_upto = [
1688
+ "group_key",
1689
+ label_name,
1690
+ "Jenis",
1691
+ "Jumlah",
1692
+ "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1693
+ "Rata2_dim_kepatuhan","Rata2_dim_kinerja",
1694
+ "Indeks_Dasar_Agregat_0_100",
1695
+ ]
1696
+ cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1697
+ agg_jenis_view = agg_jenis_full[cols_upto].copy()
1698
+
1699
+ # FILTER RAW DOWNLOAD (df_raw)
1700
+ raw = df_raw.copy()
1701
+ if prov_value and prov_value != "(Semua)":
1702
+ raw = raw[raw["PROV_DISP"] == prov_value]
1703
+ if kab_value and kab_value != "(Semua)":
1704
+ raw = raw[raw["KAB_DISP"] == kab_value]
1705
+ if kew_value and kew_value != "(Semua)":
1706
+ raw = raw[raw["KEW_NORM"] == kew_value]
1707
+
1708
+ # bell curve per jenis (entitas)
1709
+ if detail_view is None or detail_view.empty:
1710
+ fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve — Jenis: Sekolah", min_points=2)
1711
+ fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve — Jenis: Umum", min_points=2)
1712
+ fig_khusus = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve — Jenis: Khusus", min_points=2)
1713
+ else:
1714
+ xcol_ent = "Indeks_Dasar_0_100" if "Indeks_Dasar_0_100" in detail_view.columns else "Indeks_Final_0_100"
1715
+ label_col_e = "nm_perpustakaan" if "nm_perpustakaan" in detail_view.columns else None
1716
+ hover_cols_e = [c for c in ["Provinsi", "Kab/Kota", "KEW_NORM", "Jenis", "Indeks_Dasar_0_100", "Indeks_Final_0_100"] if c in detail_view.columns]
1717
+
1718
+ def _fig_jenis_ent(jenis_key: str, judul: str):
1719
+ d = detail_view[detail_view["Jenis"].astype(str).str.lower() == jenis_key].copy()
1720
+ return _make_bell_curve(d, xcol=xcol_ent, title=judul, label_col=label_col_e, hover_cols=hover_cols_e, min_points=2)
1721
+
1722
+ fig_sekolah = _fig_jenis_ent("sekolah", "Bell Curve — Jenis: Sekolah (Indeks per Entitas)")
1723
+ fig_umum = _fig_jenis_ent("umum", "Bell Curve — Jenis: Umum (Indeks per Entitas)")
1724
+ fig_khusus = _fig_jenis_ent("khusus", "Bell Curve — Jenis: Khusus (Indeks per Entitas)")
1725
+
1726
+ # KPI (dashboard cuma final & dasar)
1727
+ kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis_full, faktor_wilayah_jenis=faktor_wilayah_jenis)
1728
+
1729
+ tmpdir = tempfile.mkdtemp()
1730
+ prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
1731
+ kab_slug = (_canon(kab_value or "SEMUA").upper() or "SEMUA")
1732
+ kew_slug = (_canon(kew_value or "SEMUA").upper() or "SEMUA")
1733
+
1734
+ p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1735
+ p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1736
+ p_jenis = str(Path(tmpdir) / f"IPLM_RAW_DATA_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1737
+ p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1738
+ p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1739
+
1740
+ summary_jenis.to_excel(p_summary, index=False)
1741
+ agg_total.to_excel(p_total, index=False)
1742
+ raw.to_excel(p_jenis, index=False)
1743
+ detail_view.to_excel(p_detail, index=False)
1744
+ verif_total.to_excel(p_verif, index=False)
1745
+
1746
+ wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
1747
+ analysis_text = generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah_txt, kew_value or "(Semua)")
1748
+ word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, agg_jenis_full, analysis_text)
1749
+
1750
+ msg = (
1751
+ f"✅ Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
1752
+ f"jenis(agregat)={len(agg_jenis_full)} | Pop/Target sekolah+umum+khusus sudah diambil dari Excel populasi"
1753
+ )
1754
+
1755
+ return (
1756
+ kpi_md,
1757
+ summary_jenis, agg_total, agg_jenis_view, detail_view, verif_total,
1758
+ p_summary, p_total, p_jenis, p_detail, word_path,
1759
+ fig_umum, fig_sekolah, fig_khusus,
1760
+ msg, analysis_text
1761
+ )
1762
+
1763
+ except Exception as e:
1764
+ return _empty_outputs(f"⚠️ Runtime error: {repr(e)}")
1765
+
1766
+
1767
+ # ============================================================
1768
+ # 16) UI (NO UPLOAD)
1769
+ # ============================================================
1770
+
1771
+ def ui_load(force=False):
1772
+ df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=force)
1773
+ if df_all is None or (isinstance(df_all, pd.DataFrame) and df_all.empty):
1774
+ return (
1775
+ None, None, None, None, None, {}, info,
1776
+ gr.update(choices=["(Semua)"], value="(Semua)"),
1777
+ gr.update(choices=["(Semua)"], value="(Semua)"),
1778
+ gr.update(choices=["(Semua)"], value="(Semua)"),
1779
+ )
1780
+
1781
+ prov_vals = df_all["PROV_DISP"].dropna().astype(str).tolist()
1782
+ prov_vals = [v for v in prov_vals if v and v.strip()]
1783
+ prov_choices = ["(Semua)"] + sorted(set(prov_vals))
1784
+
1785
+ kab_choices = ["(Semua)"] + sorted([x for x in df_all["KAB_DISP"].dropna().unique().tolist() if x])
1786
+ kew_choices = ["(Semua)"] + sorted([x for x in df_all["KEW_NORM"].dropna().unique().tolist() if x])
1787
+ default_kew = "PROVINSI" if "PROVINSI" in kew_choices else ("KAB/KOTA" if "KAB/KOTA" in kew_choices else "(Semua)")
1788
+
1789
+ return (
1790
+ df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
1791
+ gr.update(choices=prov_choices, value="(Semua)"),
1792
+ gr.update(choices=kab_choices, value="(Semua)"),
1793
+ gr.update(choices=kew_choices, value=default_kew),
1794
+ )
1795
+
1796
+ def on_prov_change(prov_value):
1797
+ df_all, _, _, _, _, _, _ = load_default_files(force=False)
1798
+ if df_all is None or df_all.empty:
1799
+ return gr.update(choices=["(Semua)"], value="(Semua)")
1800
+ if prov_value is None or prov_value == "(Semua)":
1801
+ vals = df_all["KAB_DISP"].dropna().unique().tolist()
1802
+ else:
1803
+ vals = df_all.loc[df_all["PROV_DISP"] == prov_value, "KAB_DISP"].dropna().unique().tolist()
1804
+ vals = sorted([v for v in vals if v])
1805
+ return gr.update(choices=["(Semua)"] + vals, value="(Semua)")
1806
+
1807
+
1808
+ with gr.Blocks() as demo:
1809
+ gr.Markdown(f"""
1810
+ # IPLM 2025 — Final (Pop/Target68 per Jenis dari Excel Populasi)
1811
+ **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1812
+ - `DATA_FILE` = **{DATA_FILE}**
1813
+ - `POP_KAB` = **{POP_KAB}**
1814
+ - `POP_PROV` = **{POP_PROV}**
1815
+ - `POP_KHUSUS` = **{POP_KHUSUS}**
1816
+
1817
+ **FIX UTAMA:**
1818
+ - Ringkasan tampil Pop/Target68/Terkumpul/Coverage untuk **sekolah, umum, khusus, keseluruhan**
1819
+ - Pop/Target sekolah+umum dari Excel POP_KAB/POP_PROV (kolom asli)
1820
+ - Pop/Target khusus dari POP_KHUSUS (Propinsi/Kab/kota | POP_KHUSUS | SAMPEL_KHUSUS_68%)
1821
+ """)
1822
+
1823
+ state_df = gr.State(None)
1824
+ state_raw = gr.State(None)
1825
+ state_pop_kab = gr.State(None)
1826
+ state_pop_prov = gr.State(None)
1827
+ state_pop_khusus = gr.State(None)
1828
+ state_meta = gr.State({})
1829
+
1830
+ info_box = gr.Markdown()
1831
+
1832
+ with gr.Row():
1833
+ dd_prov = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)")
1834
+ dd_kab = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)")
1835
+ dd_kew = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)")
1836
+
1837
+ dd_prov.change(fn=on_prov_change, inputs=[dd_prov], outputs=dd_kab)
1838
+
1839
+ run_btn = gr.Button("Jalankan Perhitungan")
1840
+ msg_out = gr.Markdown()
1841
+
1842
+ kpi_out = gr.Markdown()
1843
+
1844
+ gr.Markdown("## Ringkasan (Jenis + Keseluruhan) — Pop/Target68/Terkumpul/Coverage + Penyesuaian")
1845
+ out_summary = gr.DataFrame(interactive=False)
1846
+
1847
+ gr.Markdown("## Agregat Wilayah (Keseluruhan) — FIX: avg3 dari 3 jenis")
1848
+ out_agg_total = gr.DataFrame(interactive=False)
1849
+
1850
+ gr.Markdown("## Agregat Wilayah × Jenis (Sekolah, Umum, Khusus) — (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
1851
+ out_agg_jenis = gr.DataFrame(interactive=False)
1852
+
1853
+ gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
1854
+ out_detail = gr.DataFrame(interactive=False)
1855
+
1856
+ gr.Markdown("## Kecukupan Sampel 68% (tanpa angka koma)")
1857
+ out_verif = gr.DataFrame(interactive=False)
1858
+
1859
+ gr.Markdown("## Bell Curve — per Jenis Perpustakaan (Indeks per Entitas)")
1860
+ gr.Markdown("### Perpustakaan Umum")
1861
+ bell_umum = gr.Plot(scale=1)
1862
+
1863
+ gr.Markdown("### Perpustakaan Sekolah")
1864
+ bell_sekolah = gr.Plot(scale=1)
1865
+
1866
+ gr.Markdown("### Perpustakaan Khusus")
1867
+ bell_khusus = gr.Plot(scale=1)
1868
+
1869
+ gr.Markdown("## Analisis Otomatis (opsional)")
1870
+ analysis_out = gr.Markdown()
1871
+
1872
+ with gr.Row():
1873
+ dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1874
+ dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1875
+ dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1876
+ dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1877
+ dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
1878
+
1879
+ run_btn.click(
1880
+ fn=run_calc,
1881
+ inputs=[dd_prov, dd_kab, dd_kew, state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta],
1882
+ outputs=[
1883
+ kpi_out,
1884
+ out_summary, out_agg_total, out_agg_jenis, out_detail, out_verif,
1885
+ dl_summary, dl_total, dl_jenis, dl_detail, dl_word,
1886
+ bell_umum, bell_sekolah, bell_khusus,
1887
+ msg_out, analysis_out
1888
+ ]
1889
+ )
1890
+
1891
+ demo.load(
1892
+ fn=lambda: ui_load(force=False),
1893
+ inputs=[],
1894
+ outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
1895
+ )
1896
+
1897
+ demo.launch()
gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ DATA_IPLM_FINAL_CLEAN_pusat_12012026.xlsx filter=lfs diff=lfs merge=lfs -text
2
+ DATA_IPLM_FINAL_CLEAN_pusat_15012026.xlsx filter=lfs diff=lfs merge=lfs -text
3
+ DATA%20IPLM%20FINAL_CLEAN_pusat.xlsx filter=lfs diff=lfs merge=lfs -text
4
+ DATA%20IPLM%20FINAL_CLEAN.xlsx filter=lfs diff=lfs merge=lfs -text
5
+ IPLM_clean_manual_131225.xlsx filter=lfs diff=lfs merge=lfs -text
gitattributes (1) ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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[[:space:]](1).xlsx filter=lfs diff=lfs merge=lfs -text
37
+ DM.xlsx filter=lfs diff=lfs merge=lfs -text
38
+ DM[[:space:]](3).xlsx filter=lfs diff=lfs merge=lfs -text
39
+ data_iplm_clean_complete.xlsx filter=lfs diff=lfs merge=lfs -text
40
+ DMx.xlsx filter=lfs diff=lfs merge=lfs -text
41
+ DATA_IPLM_clean_SNP_jenis[[:space:]](2).xlsx filter=lfs diff=lfs merge=lfs -text
42
+ DATA_IPLM_clean_SNP_jenis.xlsx filter=lfs diff=lfs merge=lfs -text
43
+ IPLM_clean_manual_131225.xlsx filter=lfs diff=lfs merge=lfs -text
44
+ DATA[[:space:]]IPLM[[:space:]]FINAL_CLEAN.xlsx filter=lfs diff=lfs merge=lfs -text
45
+ DATA[[:space:]]IPLM[[:space:]]FINAL_CLEAN_pusat.xlsx filter=lfs diff=lfs merge=lfs -text
46
+ DATA_IPLM_FINAL_CLEAN_pusat_12012026.xlsx filter=lfs diff=lfs merge=lfs -text
47
+ DATA_IPLM_FINAL_CLEAN_pusat_15012026.xlsx filter=lfs diff=lfs merge=lfs -text
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ gradio
4
+ python-docx
5
+ openpyxl
6
+ huggingface-hub
7
+ scikit-learn
8
+ plotly
9
+ matplotlib