irhamni commited on
Commit
cf17ffb
·
verified ·
1 Parent(s): d5a7f09

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1597
app.py DELETED
@@ -1,1597 +0,0 @@
1
- import os
2
- import re
3
- import tempfile
4
- from pathlib import Path
5
-
6
- import gradio as gr
7
- import numpy as np
8
- import pandas as pd
9
- import plotly.graph_objects as go
10
- from huggingface_hub import InferenceClient
11
- from sklearn.preprocessing import PowerTransformer
12
-
13
- # ============================================================
14
- # 1. KONFIGURASI FILE & PARAMETER
15
- # ============================================================
16
-
17
- DATA_FILE = "DM.xlsx" # data utama perpustakaan
18
- META_KAB_FILE = "jumlahdesa_fixed.xlsx" # kecamatan & desa/kel per kab/kota
19
- META_SDSMP_FILE = "jumlah_SD_SMP.xlsx" # jumlah SD & SMP per kab/kota
20
- META_SMA_FILE = "Data_SMA_propinsi_update.xlsx" # jumlah SMA per provinsi
21
-
22
- # Kelompok indikator IPLM
23
- koleksi_cols = [
24
- "JudulTercetak","EksemplarTercetak","JudulElektronik","EksemplarElektronik",
25
- "TambahJudulTercetak","TambahEksemplarTercetak",
26
- "TambahJudulElektronik","TambahEksemplarElektronik",
27
- "KomitmenAnggaranKoleksi"
28
- ]
29
- sdm_cols = [
30
- "TenagaKualifikasiIlmuPerpustakaan",
31
- "TenagaFungsionalProfesional",
32
- "TenagaPKB",
33
- "AnggaranTenaga"
34
- ]
35
- pelayanan_cols = [
36
- "PesertaBudayaBaca","PemustakaLuringDaring","PemustakaFasilitasTIK",
37
- "PemanfaatanJudulTercetak","PemanfaatanEksemplarTercetak",
38
- "PemanfaatanJudulElektronik","PemanfaatanEksemplarElektronik"
39
- ]
40
- pengelolaan_cols = [
41
- "KegiatanBudayaBaca","KegiatanKerjasama","VariasiLayanan","Kebijakan","AnggaranLayanan"
42
- ]
43
- all_indicators = koleksi_cols + sdm_cols + pelayanan_cols + pengelolaan_cols
44
-
45
- # Bobot indeks IPLM
46
- w_kepatuhan = 0.30
47
- w_kinerja = 0.70
48
-
49
- # Bobot untuk Confidence
50
- W_DATA = 0.7
51
- W_SAMPLE = 0.3
52
- SAMPLE_THRESHOLD = 10 # ambang jumlah perpus per kab/kota
53
-
54
- # Target normatif per jenis perpustakaan
55
- TARGETS = {
56
- "sekolah": {
57
- "JudulTercetak": 1000,
58
- "EksemplarTercetak": 5000,
59
- "KegiatanBudayaBaca": 12,
60
- "PemustakaLuringDaring": 1000,
61
- },
62
- "umum": {
63
- "JudulTercetak": 500,
64
- "EksemplarTercetak": 1000,
65
- "KegiatanBudayaBaca": 24,
66
- "PemustakaLuringDaring": 1000,
67
- "VariasiLayanan": 7,
68
- "TenagaKualifikasiIlmuPerpustakaan": 1,
69
- },
70
- "khusus": {
71
- "JudulTercetak": 5000,
72
- "EksemplarTercetak": 10000,
73
- "KegiatanBudayaBaca": 6,
74
- "PemustakaLuringDaring": 1000,
75
- }
76
- }
77
-
78
- # ============================================================
79
- # 1b. KONFIGURASI LLM (Hugging Face Inference)
80
- # ============================================================
81
-
82
- # Pilih model yang stabil untuk text_generation:
83
- # 1b. KONFIGURASI LLM (Hugging Face Inference)
84
- USE_LLM = True
85
-
86
- # Pilih salah satu model yang kompatibel
87
- LLM_MODEL_NAME = "meta-llama/Meta-Llama-3-8B-Instruct"
88
- # LLM_MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.2"
89
- # Alternatif lain (juga kompatibel):
90
- # LLM_MODEL_NAME = "mistralai/Mistral-7B-Instruct-v0.2"
91
-
92
- HF_TOKEN = (
93
- os.getenv("HF_TOKEN")
94
- or os.getenv("HUGGINGFACEHUB_API_TOKEN")
95
- or os.getenv("HF_API_TOKEN")
96
- )
97
-
98
- _HF_CLIENT = None
99
-
100
-
101
- def get_llm_client():
102
- """
103
- Inisialisasi InferenceClient sekali, lalu dipakai ulang.
104
- Kalau gagal (misal token salah / model tidak support), kembalikan None.
105
- """
106
- global _HF_CLIENT
107
- if _HF_CLIENT is not None:
108
- return _HF_CLIENT
109
-
110
- try:
111
- if HF_TOKEN:
112
- _HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN)
113
- else:
114
- # Bisa saja tetap jalan tanpa token jika model public, tapi rate-limit keras.
115
- _HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME)
116
- return _HF_CLIENT
117
- except Exception:
118
- _HF_CLIENT = None
119
- return None
120
-
121
-
122
- # ============================================================
123
- # 2. FUNGSI UTIL
124
- # ============================================================
125
-
126
- def _canon(s: str) -> str:
127
- return re.sub(r"[^a-z0-9]+", "", str(s).lower())
128
-
129
-
130
- def coerce_num(val):
131
- if pd.isna(val):
132
- return np.nan
133
- t = str(val).strip()
134
- if t == "" or t in {"-", "–", "—"}:
135
- return np.nan
136
- t = t.replace("\u00a0", " ").replace("Rp", "").replace("%", "")
137
- t = re.sub(r"[^0-9,.\-]", "", t)
138
- if t.count(".") > 1 and t.count(",") == 1:
139
- t = t.replace(".", "").replace(",", ".")
140
- elif t.count(",") > 1 and t.count(".") == 1:
141
- t = t.replace(",", "")
142
- elif t.count(",") == 1 and t.count(".") == 0:
143
- t = t.replace(",", ".")
144
- else:
145
- t = t.replace(",", "")
146
- try:
147
- return float(t)
148
- except Exception:
149
- return np.nan
150
-
151
-
152
- def minmax_norm(s: pd.Series) -> pd.Series:
153
- x = s.astype(float)
154
- mn, mx = x.min(skipna=True), x.max(skipna=True)
155
- if pd.isna(mn) or pd.isna(mx) or mx == mn:
156
- return pd.Series(0.0, index=s.index)
157
- return (x - mn) / (mx - mn)
158
-
159
-
160
- def pick_col(df, candidates):
161
- for c in candidates:
162
- if c in df.columns:
163
- return c
164
- can_map = {_canon(c): c for c in df.columns}
165
- for c in candidates:
166
- k = _canon(c)
167
- if k in can_map:
168
- return can_map[k]
169
- return None
170
-
171
-
172
- def norm_kew(v):
173
- if pd.isna(v):
174
- return None
175
- t = str(v).strip().upper()
176
- if "KAB" in t or "KOTA" in t:
177
- return "KAB/KOTA"
178
- if "PROV" in t:
179
- return "PROVINSI"
180
- if "PUSAT" in t or "NASIONAL" in t:
181
- return "PUSAT"
182
- return t
183
-
184
-
185
- def _norm_text(x):
186
- if pd.isna(x):
187
- return None
188
- t = str(x).strip().upper()
189
- return " ".join(t.split())
190
-
191
-
192
- def penalized_mean(row, cols):
193
- vals = []
194
- for c in cols:
195
- colname = f"norm_{c}"
196
- if colname in row.index:
197
- v = row[colname]
198
- if pd.isna(v):
199
- v = 0.0
200
- vals.append(v)
201
- if not vals:
202
- return np.nan
203
- return float(np.sum(vals) / len(vals))
204
-
205
-
206
- def skor_normatif(value, target):
207
- if pd.isna(value):
208
- return 0.0
209
- return min(float(value) / target, 1.0)
210
-
211
-
212
- def slugify(s: str) -> str:
213
- if s is None:
214
- return "NA"
215
- t = str(s).strip()
216
- if t == "":
217
- return "NA"
218
- return _canon(t).upper()
219
-
220
-
221
- def norm_prov_label(s):
222
- if pd.isna(s):
223
- return None
224
- t = str(s).upper()
225
- for bad in ["PROVINSI", "PROPINSI"]:
226
- t = t.replace(bad, "")
227
- t = " ".join(t.split())
228
- return re.sub(r"[^A-Z0-9]+", "", t)
229
-
230
-
231
- def norm_kab_label(s):
232
- """
233
- Normalisasi nama Kab/Kota tapi tetap membedakan:
234
- - 'Kabupaten Bandung' -> 'KABBANDUNG'
235
- - 'Kota Bandung' -> 'KOTABANDUNG'
236
- """
237
- if pd.isna(s):
238
- return None
239
-
240
- t = str(s).upper()
241
- t = t.replace("KABUPATEN", "KAB")
242
- t = t.replace("KAB.", "KAB")
243
- t = t.replace("KAB ", "KAB ")
244
-
245
- t = t.replace("KOTA ADMINISTRASI", "KOTA")
246
- t = t.replace("KOTA ADM.", "KOTA")
247
- t = t.replace("KOTA.", "KOTA")
248
-
249
- t = " ".join(t.split())
250
- return re.sub(r"[^A-Z0-9]+", "", t)
251
-
252
-
253
- # ============================================================
254
- # 3. LOAD DATA DM.xlsx + META
255
- # ============================================================
256
-
257
- DATA_INFO = ""
258
- df_all_raw = None
259
- meta_kab_df = None
260
- meta_sma_df = None
261
-
262
- prov_col_glob = kab_col_glob = kew_col_glob = jenis_col_glob = nama_col_glob = None
263
-
264
- try:
265
- fp = Path(DATA_FILE)
266
- if not fp.exists():
267
- raise FileNotFoundError(f"File tidak ditemukan: {DATA_FILE}")
268
-
269
- xls = pd.ExcelFile(fp)
270
- frames = [pd.read_excel(fp, sheet_name=s) for s in xls.sheet_names]
271
- df_all_raw = pd.concat(frames, ignore_index=True, sort=False)
272
-
273
- prov_col_glob = pick_col(df_all_raw, ["provinsi", "Provinsi", "PROVINSI"])
274
- kab_col_glob = pick_col(df_all_raw, ["kab_kota", "Kab_Kota", "Kab/Kota", "KAB/KOTA", "kabupaten_kota", "kota"])
275
- kew_col_glob = pick_col(df_all_raw, ["kewenangan", "jenis_kewenangan", "Kewenangan", "KEWENANGAN"])
276
- jenis_col_glob = pick_col(df_all_raw, ["jenis_perpustakaan", "JENIS_PERPUSTAKAAN", "Jenis Perpustakaan", "jenis perpustakaan"])
277
- nama_col_glob = pick_col(df_all_raw, ["nama_perpustakaan", "nm_perpustakaan", "nm_instansi_lembaga", "Nama Perpustakaan"])
278
-
279
- if kew_col_glob:
280
- df_all_raw["KEW_NORM"] = df_all_raw[kew_col_glob].apply(norm_kew)
281
- else:
282
- df_all_raw["KEW_NORM"] = None
283
-
284
- val_map_jenis = {
285
- "PERPUSTAKAAN SEKOLAH": "sekolah",
286
- "SEKOLAH": "sekolah",
287
- "PERPUSTAKAAN UMUM": "umum",
288
- "UMUM": "umum",
289
- "PERPUSTAKAAN DAERAH": "umum",
290
- "PERPUSTAKAAN KHUSUS": "khusus",
291
- "KHUSUS": "khusus",
292
- }
293
- if jenis_col_glob:
294
- df_all_raw["_dataset"] = df_all_raw[jenis_col_glob].apply(_norm_text).map(val_map_jenis)
295
- else:
296
- df_all_raw["_dataset"] = None
297
-
298
- def all_prov_choices():
299
- if prov_col_glob is None:
300
- return ["(Semua)"]
301
- s = df_all_raw[prov_col_glob].dropna().astype(str).str.strip()
302
- vals = sorted([o for o in s.unique() if o != ""])
303
- return ["(Semua)"] + vals
304
-
305
- def get_kab_choices_for_prov(prov_value):
306
- if kab_col_glob is None:
307
- return ["(Semua)"]
308
- if prov_value is None or prov_value == "(Semua)" or prov_col_glob is None:
309
- s = df_all_raw[kab_col_glob].dropna().astype(str).str.strip()
310
- else:
311
- m = df_all_raw[prov_col_glob].astype(str).str.strip() == prov_value
312
- s = df_all_raw.loc[m, kab_col_glob].dropna().astype(str).str.strip()
313
- vals = sorted([x for x in s.unique() if x != ""])
314
- return ["(Semua)"] + vals
315
-
316
- def all_kew_choices():
317
- s = df_all_raw["KEW_NORM"].dropna().astype(str).str.strip()
318
- vals = sorted([o for o in s.unique() if o != ""])
319
- if not vals:
320
- return ["(Semua)"]
321
- return ["(Semua)"] + vals
322
-
323
- prov_choices = all_prov_choices()
324
- kab_choices = get_kab_choices_for_prov(prov_choices[0] if prov_choices else "(Semua)")
325
- kew_choices = all_kew_choices()
326
- default_kew = "KAB/KOTA" if "KAB/KOTA" in kew_choices else kew_choices[0]
327
-
328
- DATA_INFO = f"Data terbaca dari: **{DATA_FILE}** | Jumlah baris: **{len(df_all_raw)}**"
329
- except Exception as e:
330
- df_all_raw = None
331
- prov_choices = kab_choices = kew_choices = ["(Semua)"]
332
- default_kew = "(Semua)"
333
- DATA_INFO = f"⚠️ Gagal memuat data dari file: `{DATA_FILE}`\n\nError: `{e}`"
334
-
335
- # 3b. META KECAMATAN/DESA + SD/SMP + SMA
336
- extra_info = []
337
-
338
- # --- jumlah kecamatan & desa/kel per kab/kota ---
339
- try:
340
- meta_kab_raw = pd.read_excel(META_KAB_FILE)
341
- col_kab = pick_col(meta_kab_raw, ["Kab/Kota", "Kab_Kota", "kab/kota", "kabupaten_kota"])
342
- col_kec = pick_col(meta_kab_raw, ["Kecamatan", "jml_kecamatan", "jumlah_kecamatan"])
343
- col_des = pick_col(meta_kab_raw, ["Desa/Kel", "Desa Kelurahan", "Desa", "Desa_kel"])
344
-
345
- if col_kab and col_kec and col_des:
346
- meta_kab_df = pd.DataFrame({
347
- "Kab_Kota_Label": meta_kab_raw[col_kab].astype(str).str.strip(),
348
- "Jml_Kecamatan": meta_kab_raw[col_kec].apply(coerce_num),
349
- "Jml_DesaKel": meta_kab_raw[col_des].apply(coerce_num),
350
- })
351
- meta_kab_df["kab_key"] = meta_kab_df["Kab_Kota_Label"].apply(norm_kab_label)
352
- extra_info.append(f"Verifikasi Kab/Kota (Kec/Desa) dari **{META_KAB_FILE}** (n={len(meta_kab_df)})")
353
- else:
354
- meta_kab_df = None
355
- extra_info.append(f"Verifikasi Kab/Kota: kolom kunci tidak lengkap di `{META_KAB_FILE}`")
356
- except Exception as e:
357
- meta_kab_df = None
358
- extra_info.append(f"⚠️ Gagal memuat `{META_KAB_FILE}` ({e})")
359
-
360
- # --- jumlah SD & SMP per kab/kota ---
361
- try:
362
- sd_smp_raw = pd.read_excel(META_SDSMP_FILE)
363
- col_kab2 = pick_col(sd_smp_raw, [
364
- "Kabupaten/Kota_Kabupaten/Kota", "Kabupaten/Kota",
365
- "Kab/Kota", "Kab_Kota", "kab/kota", "kabupaten_kota"
366
- ])
367
- col_sd = pick_col(sd_smp_raw, ["SD", "Jumlah SD", "Total SD", "SD_Total", "jml_sd", "Jml_SD"])
368
- col_smp = pick_col(sd_smp_raw, ["SMP", "Jumlah SMP", "Total SMP", "SMP_Total", "jml_smp", "Jml_SMP"])
369
-
370
- if col_kab2 and (col_sd or col_smp):
371
- df_sd_smp = pd.DataFrame({
372
- "Kab_Kota_Label_SD": sd_smp_raw[col_kab2].astype(str).str.strip(),
373
- })
374
- df_sd_smp["Jml_SD"] = sd_smp_raw[col_sd].apply(coerce_num) if col_sd else 0.0
375
- df_sd_smp["Jml_SMP"] = sd_smp_raw[col_smp].apply(coerce_num) if col_smp else 0.0
376
-
377
- df_sd_smp["kab_key"] = df_sd_smp["Kab_Kota_Label_SD"].apply(norm_kab_label)
378
-
379
- df_sd_smp_grp = df_sd_smp.groupby("kab_key", as_index=False).agg({
380
- "Jml_SD": "sum",
381
- "Jml_SMP": "sum",
382
- })
383
-
384
- if meta_kab_df is not None:
385
- meta_kab_df = meta_kab_df.merge(
386
- df_sd_smp_grp,
387
- on="kab_key",
388
- how="left"
389
- )
390
- else:
391
- meta_kab_df = df_sd_smp_grp.copy()
392
- meta_kab_df["Kab_Kota_Label"] = df_sd_smp.groupby("kab_key")["Kab_Kota_Label_SD"].first().values
393
-
394
- extra_info.append(
395
- f"Data SD/SMP per Kab/Kota dari **{META_SDSMP_FILE}** ditambahkan (n={len(df_sd_smp_grp)})"
396
- )
397
- else:
398
- extra_info.append(f"Data SD/SMP: kolom kunci tidak lengkap di `{META_SDSMP_FILE}`")
399
- except Exception as e:
400
- extra_info.append(f"⚠️ Gagal memuat `{META_SDSMP_FILE}` ({e})")
401
-
402
- # --- jumlah SMA per provinsi ---
403
- try:
404
- meta_sma_raw = pd.read_excel(META_SMA_FILE)
405
-
406
- col_prov_sma = pick_col(meta_sma_raw, [
407
- "Provinsi", "provinsi", "PROVINSI", "NAMA_PROVINSI", "Nama Provinsi",
408
- "nm_prov", "nm_provinsi", "prov"
409
- ])
410
- col_sma = pick_col(meta_sma_raw, [
411
- "Jml_SMA", "Jumlah SMA", "SMA", "Total SMA", "SMA_Total",
412
- "jumlah_sma", "total_sma", "jml_sma", "total"
413
- ])
414
-
415
- if col_prov_sma is None:
416
- raise ValueError("Kolom provinsi tidak ditemukan dalam file SMA.")
417
- if col_sma is None:
418
- raise ValueError("Kolom jumlah SMA tidak ditemukan.")
419
-
420
- meta_sma_df = pd.DataFrame({
421
- "Provinsi_Label": meta_sma_raw[col_prov_sma].astype(str).str.strip(),
422
- "Jml_SMA": meta_sma_raw[col_sma].apply(coerce_num),
423
- })
424
- meta_sma_df["prov_key"] = meta_sma_df["Provinsi_Label"].apply(norm_prov_label)
425
- meta_sma_df = meta_sma_df.groupby(["prov_key", "Provinsi_Label"], as_index=False).agg(
426
- {"Jml_SMA": "sum"}
427
- )
428
-
429
- extra_info.append(f"Verifikasi SMA per Provinsi berhasil dimuat ({len(meta_sma_df)} provinsi).")
430
- except Exception as e:
431
- meta_sma_df = None
432
- extra_info.append(f"⚠️ Gagal memuat file SMA: {e}")
433
-
434
- if extra_info:
435
- DATA_INFO = DATA_INFO + "<br>" + "<br>".join(extra_info)
436
-
437
-
438
- # ============================================================
439
- # 4. BELL CURVE
440
- # ============================================================
441
-
442
- def make_bell_figure(df_all: pd.DataFrame,
443
- title: str,
444
- index_col: str = "Indeks_Real_0_100",
445
- name_col: str = None,
446
- min_points: int = 5) -> go.Figure:
447
-
448
- fig = go.Figure()
449
-
450
- if index_col not in df_all.columns:
451
- fig.update_layout(
452
- title=title,
453
- xaxis_title="Indeks (0–100)",
454
- yaxis_title="Kepadatan (relatif)",
455
- )
456
- return fig
457
-
458
- df_plot = df_all.copy()
459
- df_plot = df_plot[pd.notna(df_plot[index_col])]
460
-
461
- if df_plot.empty or len(df_plot) < min_points:
462
- fig.update_layout(
463
- title=title,
464
- xaxis_title="Indeks (0–100)",
465
- yaxis_title="Kepadatan (relatif)",
466
- annotations=[
467
- dict(
468
- text="Grafik tidak ditampilkan (data terlalu sedikit).",
469
- x=0.5, y=0.5, xref="paper", yref="paper",
470
- showarrow=False, font=dict(size=14)
471
- )
472
- ]
473
- )
474
- return fig
475
-
476
- x_vals = df_plot[index_col].values.astype(float)
477
- mu = x_vals.mean()
478
- sigma = x_vals.std(ddof=1) if len(x_vals) > 1 else 1.0
479
-
480
- xs = np.linspace(max(0, x_vals.min() - 5), min(100, x_vals.max() + 5), 200)
481
- pdf = (1.0 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((xs - mu) / sigma) ** 2)
482
- pdf = pdf / pdf.max()
483
- y_max = 1.0
484
-
485
- if name_col and name_col in df_plot.columns:
486
- hover_text = [
487
- f"{str(n)}<br>Indeks: {v:.2f}"
488
- for n, v in zip(df_plot[name_col], x_vals)
489
- ]
490
- else:
491
- hover_text = [f"Indeks: {v:.2f}" for v in x_vals]
492
-
493
- fig.add_trace(go.Scatter(
494
- x=xs,
495
- y=pdf,
496
- mode="lines",
497
- name="Bell curve",
498
- hoverinfo="skip"
499
- ))
500
-
501
- fig.add_trace(go.Scatter(
502
- x=x_vals,
503
- y=np.zeros_like(x_vals),
504
- mode="markers",
505
- name="Perpustakaan",
506
- hovertext=hover_text,
507
- hovertemplate="%{hovertext}<extra></extra>"
508
- ))
509
-
510
- q1, q2, q3 = np.quantile(x_vals, [0.25, 0.5, 0.75])
511
- for q, label in [(q1, "Q1"), (q2, "Q2 (Median)"), (q3, "Q3")]:
512
- fig.add_trace(go.Scatter(
513
- x=[q, q],
514
- y=[0, y_max * 1.05],
515
- mode="lines",
516
- name=label,
517
- hovertemplate=f"{label}: {q:.2f}<extra></extra>"
518
- ))
519
-
520
- fig.update_layout(
521
- title=title,
522
- xaxis_title="Indeks IPLM (0–100)",
523
- yaxis_title="Kepadatan (relatif)",
524
- yaxis=dict(showticklabels=False, zeroline=True, range=[0, y_max * 1.2]),
525
- margin=dict(l=40, r=20, t=60, b=40),
526
- hovermode="x"
527
- )
528
-
529
- return fig
530
-
531
-
532
- # ============================================================
533
- # 5. PIPELINE REALSCORE + NORMATIF
534
- # ============================================================
535
-
536
- def run_pipeline_core(df_subset: pd.DataFrame, kab_name=None, kew_name=None):
537
- df = df_subset.copy()
538
- df_raw = df_subset.copy()
539
-
540
- canonical_targets = set(all_indicators)
541
- alias_map_raw = {
542
- "j_judul_koleksi_tercetak": "JudulTercetak",
543
- "j_eksemplar_koleksi_tercetak": "EksemplarTercetak",
544
- "j_judul_koleksi_digital": "JudulElektronik",
545
- "j_eksemplar_koleksi_digital": "EksemplarElektronik",
546
- "tambah_judul_koleksi_tercetak": "TambahJudulTercetak",
547
- "tambah_eksemplar_koleksi_tercetak": "TambahEksemplarTercetak",
548
- "tambah_judul_koleksi_digital": "TambahJudulElektronik",
549
- "tambah_eksemplar_koleksi_digital": "TambahEksemplarElektronik",
550
- "j_anggaran_koleksi": "KomitmenAnggaranKoleksi",
551
- "j_tenaga_ilmu_perpus": "TenagaKualifikasiIlmuPerpustakaan",
552
- "j_tenaga_nonilmu_perpus": "TenagaFungsionalProfesional",
553
- "j_tenaga_pkb": "TenagaPKB",
554
- "j_anggaran_diklat_perpus": "AnggaranTenaga",
555
- "j_peserta_budaya_baca": "PesertaBudayaBaca",
556
- "j_pemustaka_luring_daring": "PemustakaLuringDaring",
557
- "j_pemustaka_fasilitas_tik": "PemustakaFasilitasTIK",
558
- "j_judul_koleksi_tercetak_termanfaat": "PemanfaatanJudulTercetak",
559
- "j_eksemplar_koleksi_tercetak_termanfaat": "PemanfaatanEksemplarTercetak",
560
- "j_judul_koleksi_digital_termanfaat": "PemanfaatanJudulElektronik",
561
- "j_eksemplar_koleksi_digital_termanfaat": "PemanfaatanEksemplarElektronik",
562
- "j_kegiatan_budaya_baca_peningkatan_literasi": "KegiatanBudayaBaca",
563
- "j_kerjasama_pengembangan_perpus": "KegiatanKerjasama",
564
- "j_variasi_layanan": "VariasiLayanan",
565
- "j_kebijakan_prosedur_pelayanan": "Kebijakan",
566
- "j_anggaran_peningkatan_pelayanan": "AnggaranLayanan"
567
- }
568
- alias_map = {_canon(k): v for k, v in alias_map_raw.items()}
569
-
570
- rename_map = {}
571
- for col in list(df.columns):
572
- ccol = _canon(col)
573
- if ccol in alias_map:
574
- rename_map[col] = alias_map[ccol]
575
- else:
576
- for tgt in canonical_targets:
577
- if ccol == _canon(tgt):
578
- rename_map[col] = tgt
579
- break
580
- if rename_map:
581
- df = df.rename(columns=rename_map)
582
-
583
- available_indicators = [c for c in all_indicators if c in df.columns]
584
- for c in available_indicators:
585
- df[c] = df[c].apply(coerce_num)
586
-
587
- # Yeo–Johnson + MinMax
588
- yj_cols = []
589
- for c in available_indicators:
590
- yj_col = f"yj_{c}"
591
- x = df[c].astype(float).values
592
- mask = ~np.isnan(x)
593
- transformed = np.full_like(x, np.nan, dtype=float)
594
- if mask.sum() > 1:
595
- pt = PowerTransformer(method="yeo-johnson", standardize=False)
596
- transformed[mask] = pt.fit_transform(x[mask].reshape(-1, 1)).ravel()
597
- else:
598
- transformed[mask] = x[mask]
599
- df[yj_col] = transformed
600
- yj_cols.append(yj_col)
601
-
602
- for yj_col in yj_cols:
603
- base = yj_col[3:]
604
- df[f"norm_{base}"] = minmax_norm(df[yj_col])
605
-
606
- # Sub-indeks real
607
- df["sub_koleksi"] = df.apply(lambda r: penalized_mean(r, [c for c in koleksi_cols if c in available_indicators]), axis=1)
608
- df["sub_sdm"] = df.apply(lambda r: penalized_mean(r, [c for c in sdm_cols if c in available_indicators]), axis=1)
609
- df["sub_pelayanan"] = df.apply(lambda r: penalized_mean(r, [c for c in pelayanan_cols if c in available_indicators]), axis=1)
610
- df["sub_pengelolaan"] = df.apply(lambda r: penalized_mean(r, [c for c in pengelolaan_cols if c in available_indicators]), axis=1)
611
-
612
- df["dim_kepatuhan"] = df[["sub_koleksi", "sub_sdm"]].mean(axis=1)
613
- df["dim_kinerja"] = df[["sub_pelayanan", "sub_pengelolaan"]].mean(axis=1)
614
-
615
- df["Indeks_Real_0_100"] = 100 * (w_kepatuhan * df["dim_kepatuhan"] + w_kinerja * df["dim_kinerja"])
616
-
617
- # Confidence
618
- df["n_ind_filled"] = df[available_indicators].notna().sum(axis=1)
619
- df["n_ind_total"] = len(available_indicators)
620
-
621
- df["Confidence_Data"] = np.where(
622
- df["n_ind_total"] > 0,
623
- df["n_ind_filled"] / df["n_ind_total"],
624
- np.nan
625
- )
626
-
627
- if kab_col_glob and kab_col_glob in df.columns:
628
- df["_Kab_norm"] = df[kab_col_glob].astype(str).str.upper().str.strip()
629
- freq_kab = df["_Kab_norm"].value_counts()
630
- df["Jml_Perpus_Kab"] = df["_Kab_norm"].map(freq_kab)
631
- df["Confidence_Sample"] = (df["Jml_Perpus_Kab"] / SAMPLE_THRESHOLD).clip(0, 1)
632
- else:
633
- df["Jml_Perpus_Kab"] = np.nan
634
- df["Confidence_Sample"] = 1.0
635
-
636
- df["Confidence_IPLM"] = (
637
- W_DATA * df["Confidence_Data"].fillna(0) +
638
- W_SAMPLE * df["Confidence_Sample"].fillna(0)
639
- )
640
-
641
- df["Indeks_Real_AdjData"] = df["Indeks_Real_0_100"] * df["Confidence_Data"].fillna(0)
642
- df["Indeks_Real_AdjConf"] = df["Indeks_Real_0_100"] * df["Confidence_IPLM"].fillna(0)
643
-
644
- # Indeks normatif
645
- df["Indeks_Normatif_0_100"] = np.nan
646
- df["sub_koleksi_n"] = np.nan
647
- df["sub_sdm_n"] = np.nan
648
- df["sub_pelayanan_n"] = np.nan
649
- df["sub_pengelolaan_n"] = np.nan
650
- df["dim_kepatuhan_n"] = np.nan
651
- df["dim_kinerja_n"] = np.nan
652
-
653
- for i, row in df.iterrows():
654
- jenis = row.get("_dataset", None)
655
- if jenis not in TARGETS:
656
- continue
657
- t = TARGETS[jenis]
658
-
659
- skor_ind = {}
660
- for ind, target in t.items():
661
- if ind in df.columns:
662
- skor_ind[ind] = skor_normatif(row[ind], target)
663
-
664
- sub_koleksi_n = np.mean([
665
- skor_ind.get("JudulTercetak", 0),
666
- skor_ind.get("EksemplarTercetak", 0)
667
- ])
668
- sub_sdm_n = skor_ind.get("TenagaKualifikasiIlmuPerpustakaan", 0)
669
- sub_pelayanan_n = np.mean([
670
- skor_ind.get("PemustakaLuringDaring", 0),
671
- skor_ind.get("KegiatanBudayaBaca", 0)
672
- ])
673
- sub_pengelolaan_n = skor_ind.get("VariasiLayanan", 0)
674
-
675
- dim_kepatuhan_n = np.mean([sub_koleksi_n, sub_sdm_n])
676
- dim_kinerja_n = np.mean([sub_pelayanan_n, sub_pengelolaan_n])
677
-
678
- indeks_normatif = 100 * (w_kepatuhan * dim_kepatuhan_n + w_kinerja * dim_kinerja_n)
679
-
680
- df.at[i, "sub_koleksi_n"] = sub_koleksi_n
681
- df.at[i, "sub_sdm_n"] = sub_sdm_n
682
- df.at[i, "sub_pelayanan_n"] = sub_pelayanan_n
683
- df.at[i, "sub_pengelolaan_n"] = sub_pengelolaan_n
684
- df.at[i, "dim_kepatuhan_n"] = dim_kepatuhan_n
685
- df.at[i, "dim_kinerja_n"] = dim_kinerja_n
686
- df.at[i, "Indeks_Normatif_0_100"] = indeks_normatif
687
-
688
- df["Indeks_Normatif_AdjConf"] = df["Indeks_Normatif_0_100"] * df["Confidence_IPLM"].fillna(0)
689
-
690
- # DETAIL untuk tampilan
691
- detail_cols = []
692
- if prov_col_glob and prov_col_glob in df.columns:
693
- detail_cols.append(prov_col_glob)
694
- if kab_col_glob and kab_col_glob in df.columns:
695
- detail_cols.append(kab_col_glob)
696
- if nama_col_glob and nama_col_glob in df.columns:
697
- detail_cols.append(nama_col_glob)
698
-
699
- detail_cols += [
700
- "_dataset",
701
- "sub_koleksi", "sub_sdm", "sub_pelayanan", "sub_pengelolaan",
702
- "dim_kepatuhan", "dim_kinerja",
703
- "Indeks_Real_0_100",
704
- "Indeks_Normatif_0_100",
705
- "Confidence_IPLM",
706
- ]
707
- detail_cols = [c for c in detail_cols if c in df.columns]
708
-
709
- detail_df = df[detail_cols].copy().round(3)
710
-
711
- # AGREGAT per jenis
712
- expected_ds = ["sekolah", "umum", "khusus"]
713
- label_map = {
714
- "sekolah": "Perpustakaan Sekolah",
715
- "umum": "Perpustakaan Umum",
716
- "khusus": "Perpustakaan Khusus"
717
- }
718
-
719
- rows = []
720
- for ds in expected_ds:
721
- dsub = df[df["_dataset"] == ds].copy()
722
- if dsub.empty:
723
- rows.append({
724
- "Jenis Perpustakaan": label_map.get(ds, ds),
725
- "Jumlah Perpustakaan": 0,
726
- "Rata2_DimKepatuhan": 0.0,
727
- "Rata2_DimKinerja": 0.0,
728
- "Rata2_Indeks_IPLM_0_100": 0.0,
729
- })
730
- else:
731
- rows.append({
732
- "Jenis Perpustakaan": label_map.get(ds, ds),
733
- "Jumlah Perpustakaan": len(dsub),
734
- "Rata2_DimKepatuhan": dsub["dim_kepatuhan"].mean(skipna=True),
735
- "Rata2_DimKinerja": dsub["dim_kinerja"].mean(skipna=True),
736
- "Rata2_Indeks_IPLM_0_100": dsub["Indeks_Real_0_100"].mean(skipna=True),
737
- })
738
-
739
- if rows:
740
- base_rows = rows[:len(expected_ds)]
741
- total_jumlah = int(sum(r["Jumlah Perpustakaan"] for r in base_rows))
742
- mean_dim_kep = float(np.mean([r["Rata2_DimKepatuhan"] for r in base_rows]))
743
- mean_dim_kinerja = float(np.mean([r["Rata2_DimKinerja"] for r in base_rows]))
744
- mean_indeks = float(np.mean([r["Rata2_Indeks_IPLM_0_100"] for r in base_rows]))
745
-
746
- rows.append({
747
- "Jenis Perpustakaan": "Rata-rata keseluruhan",
748
- "Jumlah Perpustakaan": total_jumlah,
749
- "Rata2_DimKepatuhan": mean_dim_kep,
750
- "Rata2_DimKinerja": mean_dim_kinerja,
751
- "Rata2_Indeks_IPLM_0_100": mean_indeks,
752
- })
753
-
754
- agg_view = pd.DataFrame(rows).round(3)
755
-
756
- # Simpan Excel (AGG, DETAIL, RAW)
757
- kab_slug = slugify(kab_name) if kab_name else "SEMUA_KAB"
758
- kew_slug = slugify(kew_name) if kew_name else "SEMUA_KEW"
759
- tmpdir = tempfile.mkdtemp()
760
-
761
- agg_path = os.path.join(tmpdir, f"IPLM_RealscoreNormatif_Agregat_{kab_slug}_{kew_slug}.xlsx")
762
- detail_path = os.path.join(tmpdir, f"IPLM_RealscoreNormatif_Detail_{kab_slug}_{kew_slug}.xlsx")
763
- raw_path = os.path.join(tmpdir, f"IPLM_RealscoreNormatif_Raw_{kab_slug}_{kew_slug}.xlsx")
764
-
765
- agg_view.to_excel(agg_path, index=False)
766
- df.to_excel(detail_path, index=False)
767
- df_raw.to_excel(raw_path, index=False)
768
-
769
- # Bell curve
770
- name_col = nama_col_glob if (nama_col_glob and nama_col_glob in detail_df.columns) else None
771
-
772
- fig_all = make_bell_figure(detail_df, "Sebaran Indeks RealScore – Semua Perpustakaan",
773
- index_col="Indeks_Real_0_100", name_col=name_col)
774
-
775
- fig_sekolah = make_bell_figure(
776
- detail_df[detail_df["_dataset"] == "sekolah"],
777
- "Sebaran Indeks RealScore – Perpustakaan Sekolah",
778
- index_col="Indeks_Real_0_100", name_col=name_col, min_points=3
779
- )
780
-
781
- fig_umum = make_bell_figure(
782
- detail_df[detail_df["_dataset"] == "umum"],
783
- "Sebaran Indeks RealScore – Perpustakaan Umum",
784
- index_col="Indeks_Real_0_100", name_col=name_col, min_points=3
785
- )
786
-
787
- fig_khusus = make_bell_figure(
788
- detail_df[detail_df["_dataset"] == "khusus"],
789
- "Sebaran Indeks RealScore – Perpustakaan Khusus",
790
- index_col="Indeks_Real_0_100", name_col=name_col, min_points=3
791
- )
792
-
793
- return (
794
- agg_view,
795
- detail_df,
796
- agg_path,
797
- detail_path,
798
- raw_path,
799
- fig_all,
800
- fig_sekolah,
801
- fig_umum,
802
- fig_khusus,
803
- )
804
-
805
-
806
- # ============================================================
807
- # 6. VERIFIKASI SAMPEL
808
- # ============================================================
809
-
810
- def compute_verification(df_filtered: pd.DataFrame, kew_value):
811
- if df_filtered is None or len(df_filtered) == 0:
812
- return pd.DataFrame()
813
-
814
- kew_norm = str(kew_value or "").upper()
815
-
816
- # ---------- Kewenangan KAB/KOTA ----------
817
- if ("KAB" in kew_norm or "KOTA" in kew_norm) and (kab_col_glob is not None) and (meta_kab_df is not None):
818
- tmp = df_filtered.copy()
819
- tmp = tmp[pd.notna(tmp[kab_col_glob])]
820
- if tmp.empty:
821
- return pd.DataFrame()
822
-
823
- tmp["kab_key"] = tmp[kab_col_glob].apply(norm_kab_label)
824
-
825
- # total perpus
826
- g_total = tmp.groupby("kab_key").size().rename("jml_perpus_sampel_total").reset_index()
827
-
828
- # klasifikasi jenjang sekolah (kalau ada)
829
- if "sub_jenis_perpus" in tmp.columns:
830
- def jenjang(x):
831
- if pd.isna(x):
832
- return "OTHER"
833
- t = str(x).upper()
834
- if " SD " in f" {t} " or " SD/" in t or " MI " in f" {t} ":
835
- return "SD"
836
- if " SMP " in f" {t} " or " SMP/" in t or " MTS " in f" {t} ":
837
- return "SMP"
838
- return "OTHER"
839
- tmp["jenjang_sekolah"] = tmp["sub_jenis_perpus"].apply(jenjang)
840
- else:
841
- tmp["jenjang_sekolah"] = "OTHER"
842
-
843
- if "_dataset" in tmp.columns:
844
- mask_sek = tmp["_dataset"] == "sekolah"
845
- else:
846
- mask_sek = True
847
-
848
- tmp_sek = tmp[mask_sek].copy()
849
- tmp_sd = tmp_sek[tmp_sek["jenjang_sekolah"] == "SD"].copy()
850
- tmp_smp = tmp_sek[tmp_sek["jenjang_sekolah"] == "SMP"].copy()
851
-
852
- g_sd = tmp_sd.groupby("kab_key").size().rename("jml_perpus_sd_sampel").reset_index()
853
- g_smp = tmp_smp.groupby("kab_key").size().rename("jml_perpus_smp_sampel").reset_index()
854
- g_sekolah = tmp_sek.groupby("kab_key").size().rename("jml_perpus_sekolah_total").reset_index()
855
-
856
- if "_dataset" in tmp.columns:
857
- tmp_umum = tmp[tmp["_dataset"] == "umum"].copy()
858
- else:
859
- tmp_umum = tmp.copy()
860
- g_umum = tmp_umum.groupby("kab_key").size().rename("jml_perpus_umum_sampel").reset_index()
861
-
862
- use_cols = ["kab_key", "Kab_Kota_Label", "Jml_Kecamatan", "Jml_DesaKel", "Jml_SD", "Jml_SMP"]
863
- use_cols = [c for c in use_cols if (meta_kab_df is not None and c in meta_kab_df.columns)]
864
-
865
- merged = (
866
- g_total
867
- .merge(g_sd, on="kab_key", how="left")
868
- .merge(g_smp, on="kab_key", how="left")
869
- .merge(g_sekolah, on="kab_key", how="left")
870
- .merge(g_umum, on="kab_key", how="left")
871
- .merge(meta_kab_df[use_cols], on="kab_key", how="left")
872
- )
873
-
874
- for c in ["jml_perpus_sampel_total", "jml_perpus_sd_sampel",
875
- "jml_perpus_smp_sampel", "jml_perpus_sekolah_total",
876
- "jml_perpus_umum_sampel"]:
877
- if c in merged.columns:
878
- merged[c] = merged[c].fillna(0).astype(int)
879
-
880
- def safe_pct(num, den):
881
- if pd.isna(den) or den <= 0:
882
- return np.nan
883
- return 100.0 * float(num) / float(den)
884
-
885
- # sekolah SD+SMP
886
- if "Jml_SD" in merged.columns or "Jml_SMP" in merged.columns:
887
- merged["total_sd_smp"] = merged[["Jml_SD", "Jml_SMP"]].sum(axis=1, skipna=True)
888
- else:
889
- merged["total_sd_smp"] = np.nan
890
-
891
- merged["cov_sekolah_total_%"] = merged.apply(
892
- lambda r: safe_pct(r["jml_perpus_sekolah_total"], r.get("total_sd_smp", np.nan)),
893
- axis=1
894
- )
895
-
896
- # umum vs kombinasi (Kecamatan + Desa/Kel)
897
- merged["total_kec_desakel"] = merged.get("Jml_Kecamatan", np.nan) + merged.get("Jml_DesaKel", np.nan)
898
- merged["cov_umum_vs_kec_desakel_%"] = merged.apply(
899
- lambda r: safe_pct(r["jml_perpus_umum_sampel"], r.get("total_kec_desakel", np.nan)),
900
- axis=1
901
- )
902
-
903
- out = pd.DataFrame({
904
- "Kab/Kota": merged["Kab_Kota_Label"],
905
- "Perpus Sampel (Total)": merged["jml_perpus_sampel_total"],
906
- "Perpus Sampel – SD": merged["jml_perpus_sd_sampel"],
907
- "Perpus Sampel – SMP": merged["jml_perpus_smp_sampel"],
908
- "Perpus Sampel – Sekolah (Total SD+SMP)": merged["jml_perpus_sekolah_total"],
909
- "Sekolah (SD+SMP)": merged.get("total_sd_smp", np.nan),
910
- "Coverage Perpus Sekolah vs Sekolah (%)": merged["cov_sekolah_total_%"],
911
- "Perpus Sampel – Umum": merged["jml_perpus_umum_sampel"],
912
- "Jumlah Kecamatan": merged.get("Jml_Kecamatan", np.nan),
913
- "Jumlah Desa/Kel": merged.get("Jml_DesaKel", np.nan),
914
- "Coverage Perpus Umum vs Kec+Desa/Kel (%)": merged["cov_umum_vs_kec_desakel_%"],
915
- })
916
-
917
- return out.sort_values("Kab/Kota").reset_index(drop=True).round(3)
918
-
919
- # ---------- Kewenangan PROVINSI ----------
920
- if ("PROV" in kew_norm) and (meta_sma_df is not None):
921
- tmp = df_filtered.copy()
922
-
923
- if prov_col_glob is None:
924
- possible = [c for c in tmp.columns if "prov" in c.lower()]
925
- if possible:
926
- prov_use = possible[0]
927
- else:
928
- return pd.DataFrame({"Info": ["Kolom provinsi tidak ditemukan di DM.xlsx"]})
929
- else:
930
- prov_use = prov_col_glob
931
-
932
- tmp = tmp[pd.notna(tmp[prov_use])]
933
- if tmp.empty:
934
- return pd.DataFrame({"Info": ["Tidak ada data perpustakaan pada kewenangan provinsi."]})
935
-
936
- tmp["prov_key"] = tmp[prov_use].apply(norm_prov_label)
937
-
938
- g_total = tmp.groupby("prov_key").size().rename("Jumlah_Perpus_Sampel").reset_index()
939
-
940
- if "_dataset" in tmp.columns:
941
- tmp_sek = tmp[tmp["_dataset"] == "sekolah"].copy()
942
- else:
943
- tmp_sek = tmp.copy()
944
- g_sek = tmp_sek.groupby("prov_key").size().rename("Jml_Perpus_SMA_Sampel").reset_index()
945
-
946
- merged = g_total.merge(g_sek, on="prov_key", how="left") \
947
- .merge(meta_sma_df[["prov_key", "Provinsi_Label", "Jml_SMA"]],
948
- on="prov_key", how="left")
949
-
950
- merged["Jml_Perpus_SMA_Sampel"] = merged["Jml_Perpus_SMA_Sampel"].fillna(0).astype(int)
951
-
952
- def cov_sma(row):
953
- tot = row.get("Jml_SMA", np.nan)
954
- if pd.isna(tot) or tot <= 0:
955
- return np.nan
956
- return 100.0 * row["Jml_Perpus_SMA_Sampel"] / tot
957
-
958
- merged["Coverage_Perpus_SMA_vs_SMA_%"] = merged.apply(cov_sma, axis=1)
959
-
960
- cols_out = [
961
- "Provinsi_Label",
962
- "Jumlah_Perpus_Sampel",
963
- "Jml_Perpus_SMA_Sampel",
964
- "Jml_SMA",
965
- "Coverage_Perpus_SMA_vs_SMA_%",
966
- ]
967
- exists = [c for c in cols_out if c in merged.columns]
968
- if not exists:
969
- return pd.DataFrame()
970
-
971
- return merged[exists].sort_values("Provinsi_Label").reset_index(drop=True).round(3)
972
-
973
- return pd.DataFrame()
974
-
975
-
976
- # ============================================================
977
- # 7. KONTEKS RINGKAS UNTUK LLM (RAG MINI)
978
- # ============================================================
979
-
980
- def build_context_for_llm(detail_df: pd.DataFrame,
981
- agg_df: pd.DataFrame,
982
- verif_df: pd.DataFrame,
983
- kab_name: str,
984
- kew_value: str) -> str:
985
- wilayah = kab_name
986
- if kew_value and kew_value != "(Semua)":
987
- wilayah = f"{kab_name} (kewenangan {kew_value})"
988
-
989
- lines = []
990
- lines.append(f"Wilayah: {wilayah}")
991
- lines.append(f"Jumlah perpustakaan sampel: {len(detail_df)}")
992
-
993
- if "Indeks_Real_0_100" in detail_df.columns:
994
- mean_ind = detail_df["Indeks_Real_0_100"].mean(skipna=True)
995
- lines.append(f"Rata-rata Indeks IPLM 0-100: {mean_ind:.2f}")
996
-
997
- if "dim_kepatuhan" in detail_df.columns:
998
- mean_kep = detail_df["dim_kepatuhan"].mean(skipna=True)
999
- lines.append(f"Rata-rata dimensi kepatuhan (0-1): {mean_kep:.3f}")
1000
- else:
1001
- mean_kep = np.nan
1002
-
1003
- if "dim_kinerja" in detail_df.columns:
1004
- mean_kin = detail_df["dim_kinerja"].mean(skipna=True)
1005
- lines.append(f"Rata-rata dimensi kinerja (0-1): {mean_kin:.3f}")
1006
- else:
1007
- mean_kin = np.nan
1008
-
1009
- if "Confidence_IPLM" in detail_df.columns:
1010
- mean_conf = detail_df["Confidence_IPLM"].mean(skipna=True)
1011
- lines.append(f"Rata-rata Confidence_IPLM (0-1): {mean_conf:.2f}")
1012
-
1013
- # ringkasan per jenis perpustakaan
1014
- if agg_df is not None and not agg_df.empty and "Jenis Perpustakaan" in agg_df.columns:
1015
- lines.append("\nRingkasan per jenis perpustakaan:")
1016
- for _, r in agg_df.iterrows():
1017
- jp = str(r.get("Jenis Perpustakaan", "") or "")
1018
- if jp.lower().startswith("rata-rata"):
1019
- continue
1020
- n = r.get("Jumlah Perpustakaan", np.nan)
1021
- idx = r.get("Rata2_Indeks_IPLM_0_100", np.nan)
1022
- lines.append(f"- {jp}: n={int(n)}, rata-rata indeks={idx:.2f}")
1023
-
1024
- # contoh ekstrem tinggi & rendah (top-3 dan bottom-3)
1025
- if "Indeks_Real_0_100" in detail_df.columns:
1026
- df_valid = detail_df.dropna(subset=["Indeks_Real_0_100"]).copy()
1027
- if "Confidence_IPLM" in df_valid.columns:
1028
- df_valid = df_valid.sort_values("Confidence_IPLM", ascending=False)
1029
- col_nama = nama_col_glob if (nama_col_glob and nama_col_glob in df_valid.columns) else None
1030
-
1031
- if not df_valid.empty and col_nama:
1032
- top3 = df_valid.sort_values("Indeks_Real_0_100", ascending=False).head(3)
1033
- bottom3 = df_valid.sort_values("Indeks_Real_0_100", ascending=True).head(3)
1034
-
1035
- lines.append("\nPerpustakaan dengan indeks (contoh singkat):")
1036
- for _, r in top3.iterrows():
1037
- lines.append(
1038
- f"- {str(r[col_nama])}: indeks={r['Indeks_Real_0_100']:.2f}, "
1039
- f"kepatuhan={r['dim_kepatuhan']:.3f}, kinerja={r['dim_kinerja']:.3f}"
1040
- )
1041
-
1042
- lines.append("\nPerpustakaan dengan indeks (contoh singkat):")
1043
- for _, r in bottom3.iterrows():
1044
- lines.append(
1045
- f"- {str(r[col_nama])}: indeks={r['Indeks_Real_0_100']:.2f}, "
1046
- f"kepatuhan={r['dim_kepatuhan']:.3f}, kinerja={r['dim_kinerja']:.3f}"
1047
- )
1048
-
1049
- # ringkasan coverage (kalau ada verif_df)
1050
- if verif_df is not None and not verif_df.empty:
1051
- try:
1052
- if "Coverage Perpus Sekolah vs Sekolah (%)" in verif_df.columns:
1053
- cov_sek = verif_df["Coverage Perpus Sekolah vs Sekolah (%)"]
1054
- if len(cov_sek.dropna()) > 0:
1055
- avg_cov_sek = cov_sek.mean()
1056
- lines.append(
1057
- f"Rata-rata coverage perpustakaan sekolah terhadap SD+SMP: {avg_cov_sek:.2f}%"
1058
- )
1059
- if "Coverage Perpus Umum vs Kec+Desa/Kel (%)" in verif_df.columns:
1060
- cov_umum = verif_df["Coverage Perpus Umum vs Kec+Desa/Kel (%)"]
1061
- if len(cov_umum.dropna()) > 0:
1062
- avg_cov_umum = cov_umum.mean()
1063
- lines.append(
1064
- f"Rata-rata coverage perpustakaan umum terhadap kecamatan+desa/kelurahan: {avg_cov_umum:.2f}%"
1065
- )
1066
- except Exception:
1067
- pass
1068
-
1069
- return "\n".join(lines)
1070
-
1071
-
1072
- # ============================================================
1073
- # 7a. RULE-BASED ANALYSIS (FALLBACK)
1074
- # ============================================================
1075
-
1076
- def classify_level(x):
1077
- if pd.isna(x):
1078
- return "tidak tersedia"
1079
- if x < 40:
1080
- return "-"
1081
- if x < 60:
1082
- return "-"
1083
- return "-"
1084
-
1085
-
1086
- def generate_rule_based_analysis(detail_df: pd.DataFrame,
1087
- agg_df: pd.DataFrame,
1088
- kab_name: str,
1089
- kew_value: str) -> str:
1090
- if detail_df is None or detail_df.empty:
1091
- return "Tidak ada data yang dapat dianalisis."
1092
-
1093
- wilayah = kab_name
1094
- if kew_value and kew_value != "(Semua)":
1095
- wilayah = f"{kab_name} (kewenangan {kew_value})"
1096
-
1097
- # angka agregat
1098
- mean_ind = detail_df.get("Indeks_Real_0_100", pd.Series(dtype=float)).mean(skipna=True)
1099
- mean_kep = detail_df.get("dim_kepatuhan", pd.Series(dtype=float)).mean(skipna=True)
1100
- mean_kin = detail_df.get("dim_kinerja", pd.Series(dtype=float)).mean(skipna=True)
1101
- mean_conf = detail_df.get("Confidence_IPLM", pd.Series(dtype=float)).mean(skipna=True)
1102
-
1103
- lines = []
1104
- lines.append("## Analisis Otomatis & Rekomendasi Kebijakan (Rule-based)\n")
1105
- lines.append("### Gambaran Umum Wilayah")
1106
- lines.append(f"- Wilayah: {wilayah}")
1107
- lines.append(f"- Jumlah perpustakaan dalam sampel: {len(detail_df)}")
1108
- lines.append(f"- Rata-rata Indeks IPLM 2025: {mean_ind:.2f}")
1109
- lines.append(f"- Rata-rata dimensi kepatuhan: {mean_kep:.3f}")
1110
- lines.append(f"- Rata-rata dimensi kinerja: {mean_kin:.3f}")
1111
- if not pd.isna(mean_conf):
1112
- lines.append(f"- Rata-rata Confidence_IPLM: {mean_conf:.2f}")
1113
-
1114
- lines.append("\n### Capaian per Jenis Perpustakaan")
1115
- if agg_df is not None and not agg_df.empty:
1116
- for _, r in agg_df.iterrows():
1117
- jp = str(r.get("Jenis Perpustakaan", ""))
1118
- if not jp or jp.lower().startswith("rata-rata"):
1119
- continue
1120
- idx = r.get("Rata2_Indeks_IPLM_0_100", np.nan)
1121
- n = int(r.get("Jumlah Perpustakaan", 0))
1122
- lines.append(f"- {jp}: rata-rata indeks {idx:.2f} dengan {n} perpustakaan.")
1123
- else:
1124
- lines.append("- Data agregat per jenis perpustakaan tidak tersedia.")
1125
-
1126
- lines.append("\n### Arah Kebijakan dan Rekomendasi Program")
1127
- lines.append(
1128
- "Prioritas utama adalah penguatan layanan dasar perpustakaan serta peningkatan "
1129
- "ketersediaan SDM dan koleksi. Dimensi kepatuhan yang relatif rendah mengindikasikan "
1130
- "perlunya pembenahan pada aspek koleksi, kebijakan layanan, dan kualifikasi pustakawan. "
1131
- "Dimensi kinerja yang masih terbatas menunjukkan bahwa intensitas pemanfaatan dan "
1132
- "kegiatan literasi perlu diperkuat agar perpustakaan benar-benar berfungsi sebagai "
1133
- "pusat belajar masyarakat."
1134
- )
1135
- lines.append(
1136
- "Program-program yang dapat diprioritaskan antara lain: peningkatan alokasi anggaran "
1137
- "untuk pengembangan koleksi mutakhir, penguatan kapasitas pustakawan melalui pelatihan "
1138
- "berkelanjutan, perluasan kegiatan budaya baca yang menyasar komunitas rentan, serta "
1139
- "kolaborasi lintas sektor dengan satuan pendidikan, organisasi masyarakat, dan pelaku "
1140
- "usaha lokal. Seluruh intervensi perlu disertai mekanisme monitoring dan evaluasi "
1141
- "berbasis data IPLM agar perbaikan yang dilakukan dapat terukur dari waktu ke waktu."
1142
- )
1143
-
1144
- lines.append(
1145
- "\n> Peringatan: analisis ini bersifat otomatis berbasis data IPLM. Untuk penetapan kebijakan, "
1146
- "perlu verifikasi dan kajian kualitatif lebih lanjut."
1147
- )
1148
-
1149
- return "\n".join(lines)
1150
-
1151
- # ============================================================
1152
- # 7b. ANALISIS BERBASIS LLM (DENGAN FALLBACK RULE-BASED)
1153
- # ============================================================
1154
-
1155
- def generate_llm_analysis(detail_df: pd.DataFrame,
1156
- agg_df: pd.DataFrame,
1157
- verif_df: pd.DataFrame,
1158
- kab_name: str,
1159
- kew_value: str) -> str:
1160
- """
1161
- Analisis otomatis:
1162
- - Jika pemanggilan LLM gagal -> fallback ke rule-based dengan pesan error ringkas.
1163
- """
1164
-
1165
- # MODE LLM AKTIF: selalu coba, token bisa dari HF_TOKEN / HUGGINGFACEHUB_API_TOKEN
1166
- context = build_context_for_llm(detail_df, agg_df, verif_df, kab_name, kew_value)
1167
-
1168
- client = get_llm_client()
1169
- if client is None:
1170
- rb = generate_rule_based_analysis(detail_df, agg_df, kab_name, kew_value)
1171
- return (
1172
- "⚠️ Terjadi kendala saat menginisialisasi model LLM, sehingga analisis otomatis "
1173
- "saat ini menggunakan pendekatan **rule-based**.\n\n"
1174
- + rb
1175
- )
1176
-
1177
- system_prompt = (
1178
- "Anda adalah analis kebijakan perpustakaan dan literasi yang berpengalaman di Indonesia. "
1179
- "Tugas Anda adalah membaca ringkasan data Indeks Pembangunan Literasi Masyarakat (IPLM) "
1180
- "dan menyusun analisis kebijakan yang tajam, tetapi tetap komunikatif dan mudah dipahami "
1181
- "oleh pemangku kepentingan pemerintah daerah."
1182
- )
1183
-
1184
- user_prompt = f"""
1185
- DATA RINGKAS IPLM UNTUK WILAYAH BERIKUT:
1186
-
1187
- {context}
1188
-
1189
- TULISKAN ANALISIS DALAM BAHASA INDONESIA FORMAL, DENGAN STRUKTUR:
1190
-
1191
- 1. Gambaran umum kondisi perpustakaan di wilayah tersebut (1 paragraf).
1192
- 2. Analisis capaian indeks: soroti kekuatan dan kelemahan utama, terutama perbedaan antar jenis perpustakaan (2 paragraf).
1193
- 3. Analisis risiko dan kesenjangan layanan, termasuk jika coverage perpustakaan terhadap satuan pendidikan atau wilayah administratif masih rendah (1-2 paragraf).
1194
- 4. Rekomendasi program dan kebijakan prioritas yang konkret untuk 3-5 tahun ke depan. Susun dalam bentuk paragraf naratif, bukan bullet list (2 paragraf).
1195
-
1196
- PANDUAN GAYA:
1197
- - Jangan hanya mengulang angka apa adanya, tetapi jelaskan maknanya.
1198
- - Gunakan istilah kebijakan publik dan manajemen program perpustakaan ketika relevan.
1199
- - Hindari kalimat terlalu panjang; gunakan kalimat efektif dan jelas.
1200
- """
1201
-
1202
- try:
1203
- messages = [
1204
- {"role": "system", "content": system_prompt},
1205
- {"role": "user", "content": user_prompt},
1206
- ]
1207
-
1208
- resp = client.chat_completion(
1209
- model=LLM_MODEL_NAME,
1210
- messages=messages,
1211
- max_tokens=900,
1212
- temperature=0.35,
1213
- top_p=0.9,
1214
- )
1215
-
1216
- text = resp.choices[0].message.content.strip()
1217
- if not text:
1218
- raise ValueError("Respon LLM kosong.")
1219
-
1220
- return text
1221
-
1222
- except Exception as e:
1223
- rb = generate_rule_based_analysis(detail_df, agg_df, kab_name, kew_value)
1224
- return (
1225
- "⚠️ Terjadi error saat memanggil model LLM, sehingga analisis berikut "
1226
- "dibuat menggunakan pendekatan **rule-based**.\n\n"
1227
- f"(Detail teknis: {repr(e)})\n\n"
1228
- f"{rb}"
1229
- )
1230
-
1231
-
1232
- # ============================================================
1233
- # 8. WORD REPORT (Plotly Pie + Indeks + Agregat + LLM Narrative)
1234
- # ============================================================
1235
-
1236
- from docx import Document
1237
- from docx.shared import Inches
1238
- import plotly.express as px
1239
-
1240
- # Cek apakah kaleido tersedia
1241
- try:
1242
- import kaleido # noqa: F401
1243
- HAS_KALEIDO = True
1244
- except Exception:
1245
- HAS_KALEIDO = False
1246
-
1247
-
1248
- def make_pie_plotly(num, den, title):
1249
- """
1250
- Generate pie chart PNG menggunakan Plotly.
1251
- Jika kaleido tidak tersedia / gagal, return None (tanpa error).
1252
- """
1253
- # kalau tidak ada kaleido, jangan pakai write_image
1254
- if not HAS_KALEIDO:
1255
- return None
1256
-
1257
- if den is None or den <= 0:
1258
- values = [0, 1]
1259
- labels = ["Terjangkau", "Belum Terjangkau"]
1260
- else:
1261
- values = [num, max(den - num, 0)]
1262
- labels = ["Terjangkau", "Belum Terjangkau"]
1263
-
1264
- fig = px.pie(
1265
- values=values,
1266
- names=labels,
1267
- title=title,
1268
- hole=0.3
1269
- )
1270
-
1271
- tmp = tempfile.mktemp(suffix=".png")
1272
- try:
1273
- fig.write_image(tmp, scale=2) # butuh kaleido
1274
- return tmp
1275
- except Exception:
1276
- # kalau masih gagal (misal ada error lain), jangan jatuhkan app
1277
- return None
1278
-
1279
-
1280
- def generate_word_report_all(detail_df, agg_df, verif_df, prov, kab, kew, analysis_text):
1281
- """
1282
- Membuat laporan lengkap untuk wilayah yang dipilih:
1283
- - Ringkasan indeks
1284
- - Tabel agregat
1285
- - (opsional) Pie chart coverage
1286
- - Narasi otomatis (LLM/rule-based)
1287
- """
1288
- # Tidak berlaku untuk PUSAT
1289
- if kew == "PUSAT":
1290
- return None
1291
-
1292
- wilayah = kab if kab != "(Semua)" else prov
1293
-
1294
- doc = Document()
1295
- doc.add_heading(f"Laporan IPLM – {wilayah}", level=1)
1296
-
1297
- # =====================
1298
- # 1. Ringkasan Indeks
1299
- # =====================
1300
- doc.add_heading("Ringkasan Indeks", level=2)
1301
-
1302
- mean_ind = detail_df["Indeks_Real_0_100"].mean(skipna=True)
1303
- mean_kep = detail_df["dim_kepatuhan"].mean(skipna=True)
1304
- mean_kin = detail_df["dim_kinerja"].mean(skipna=True)
1305
- mean_conf = detail_df["Confidence_IPLM"].mean(skipna=True)
1306
-
1307
- doc.add_paragraph(f"- Jumlah perpustakaan: {len(detail_df)}")
1308
- doc.add_paragraph(f"- Rata-rata Indeks IPLM: {mean_ind:.2f}")
1309
- doc.add_paragraph(f"- Rata-rata Dimensi Kepatuhan: {mean_kep:.3f}")
1310
- doc.add_paragraph(f"- Rata-rata Dimensi Kinerja: {mean_kin:.3f}")
1311
- doc.add_paragraph(f"- Rata-rata Confidence IPLM: {mean_conf:.2f}")
1312
-
1313
- # =====================
1314
- # 2. Tabel Agregat
1315
- # =====================
1316
- doc.add_heading("Ringkasan Agregat per Jenis Perpustakaan", level=2)
1317
-
1318
- table = doc.add_table(rows=1, cols=len(agg_df.columns))
1319
- hdr = table.rows[0].cells
1320
- for i, c in enumerate(agg_df.columns):
1321
- hdr[i].text = str(c)
1322
-
1323
- for _, row in agg_df.iterrows():
1324
- r = table.add_row().cells
1325
- for i, c in enumerate(agg_df.columns):
1326
- r[i].text = str(row[c])
1327
-
1328
- # =====================
1329
- # 3. PIE CHART COVERAGE (opsional, hanya kalau kaleido & data ada)
1330
- # =====================
1331
- doc.add_heading("Coverage / Cakupan Pembinaan", level=2)
1332
-
1333
- if not HAS_KALEIDO:
1334
- doc.add_paragraph(
1335
- "Grafik pie coverage tidak dibuat karena modul 'kaleido' "
1336
- "tidak tersedia di server. Hanya ringkasan teks yang ditampilkan."
1337
- )
1338
- elif verif_df is not None and not verif_df.empty:
1339
-
1340
- if kew == "KAB/KOTA":
1341
- for _, r in verif_df.iterrows():
1342
- nama = r["Kab/Kota"]
1343
-
1344
- # Sekolah SD+SMP
1345
- if "Sekolah (SD+SMP)" in verif_df.columns:
1346
- img_path = make_pie_plotly(
1347
- r["Perpus Sampel – Sekolah (Total SD+SMP)"],
1348
- r["Sekolah (SD+SMP)"],
1349
- f"Coverage Perpustakaan Sekolah – {nama}"
1350
- )
1351
- if img_path:
1352
- doc.add_paragraph(f"Coverage Perpustakaan Sekolah – {nama}")
1353
- doc.add_picture(img_path, width=Inches(4))
1354
-
1355
- # Umum
1356
- if "Jumlah Kecamatan" in verif_df.columns and "Jumlah Desa/Kel" in verif_df.columns:
1357
- denom = r["Jumlah Kecamatan"] + r["Jumlah Desa/Kel"]
1358
- img_path = make_pie_plotly(
1359
- r["Perpus Sampel – Umum"],
1360
- denom,
1361
- f"Coverage Perpustakaan Umum – {nama}"
1362
- )
1363
- if img_path:
1364
- doc.add_paragraph(f"Coverage Perpustakaan Umum – {nama}")
1365
- doc.add_picture(img_path, width=Inches(4))
1366
-
1367
- elif kew == "PROVINSI":
1368
- for _, r in verif_df.iterrows():
1369
- nama = r["Provinsi_Label"]
1370
- img_path = make_pie_plotly(
1371
- r["Jml_Perpus_SMA_Sampel"],
1372
- r["Jml_SMA"],
1373
- f"Coverage Perpustakaan SMA – {nama}"
1374
- )
1375
- if img_path:
1376
- doc.add_paragraph(f"Coverage Perpustakaan SMA – {nama}")
1377
- doc.add_picture(img_path, width=Inches(4))
1378
-
1379
- # =====================
1380
- # 4. Narasi LLM / Rule-based
1381
- # =====================
1382
- doc.add_heading("Analisis Naratif Otomatis", level=2)
1383
- for paragraph in analysis_text.split("\n"):
1384
- if paragraph.strip():
1385
- doc.add_paragraph(paragraph)
1386
-
1387
- # =====================
1388
- # Simpan
1389
- # =====================
1390
- outpath = tempfile.mktemp(suffix=".docx")
1391
- doc.save(outpath)
1392
- return outpath
1393
-
1394
- # ============================================================
1395
- # 8. FUNGSI GRADIO
1396
- # ============================================================
1397
-
1398
- def run_app(prov_value, kab_value, kew_value):
1399
- if df_all_raw is None:
1400
- empty = pd.DataFrame()
1401
- return (
1402
- empty, empty, empty, # agg_df, detail_df, verif_df
1403
- None, None, None, # agg_path, detail_path, raw_path
1404
- None, # word_path
1405
- None, None, None, None, # fig_all, fig_sekolah, fig_umum, fig_khusus
1406
- "Data belum berhasil dimuat. Periksa kembali nama file di DATA_FILE.",
1407
- "Belum ada analisis otomatis yang dapat ditampilkan."
1408
- )
1409
-
1410
- df = df_all_raw.copy()
1411
-
1412
- # Filter provinsi
1413
- if prov_col_glob and prov_value and prov_value != "(Semua)":
1414
- df = df[df[prov_col_glob].astype(str).str.strip() == prov_value]
1415
-
1416
- # Filter kab/kota
1417
- if kab_col_glob and kab_value and kab_value != "(Semua)":
1418
- df = df[df[kab_col_glob].astype(str).str.strip() == kab_value]
1419
-
1420
- # Filter kewenangan
1421
- if kew_value and kew_value != "(Semua)":
1422
- df = df[df["KEW_NORM"] == kew_value]
1423
-
1424
- if len(df) == 0:
1425
- empty = pd.DataFrame()
1426
- return (
1427
- empty, empty, empty, # agg_df, detail_df, verif_df
1428
- None, None, None, # agg_path, detail_path, raw_path
1429
- None, # word_path
1430
- None, None, None, None, # fig_all, fig_sekolah, fig_umum, fig_khusus
1431
- "Tidak ada data untuk kombinasi filter yang dipilih.",
1432
- "Belum ada analisis otomatis yang dapat ditampilkan."
1433
- )
1434
-
1435
- kab_name = kab_value if kab_value and kab_value != "(Semua)" else "SEMUA KAB/KOTA"
1436
- kew_name = kew_value if kew_value and kew_value != "(Semua)" else "SEMUA KEWENANGAN"
1437
-
1438
- (
1439
- agg_df,
1440
- detail_df,
1441
- agg_path,
1442
- detail_path,
1443
- raw_path,
1444
- fig_all,
1445
- fig_sekolah,
1446
- fig_umum,
1447
- fig_khusus,
1448
- ) = run_pipeline_core(df, kab_name=kab_name, kew_name=kew_name)
1449
-
1450
- # Verifikasi sampel
1451
- verif_df = compute_verification(df, kew_value)
1452
-
1453
- # Pesan ringkas di UI
1454
- mean_conf = None
1455
- if "Confidence_IPLM" in detail_df.columns:
1456
- mean_conf = detail_df["Confidence_IPLM"].mean(skipna=True)
1457
-
1458
- msg = f"Berhasil dihitung untuk {len(detail_df)} baris perpustakaan."
1459
- if mean_conf is not None and not np.isnan(mean_conf):
1460
- msg += f" | Rata-rata Confidence_IPLM: {mean_conf:.2f}"
1461
- if not verif_df.empty:
1462
- msg += " | Verifikasi sampel tersedia."
1463
-
1464
- # Analisis otomatis (LLM / rule-based)
1465
- analysis_text = generate_llm_analysis(
1466
- detail_df=detail_df,
1467
- agg_df=agg_df,
1468
- verif_df=verif_df,
1469
- kab_name=kab_name,
1470
- kew_value=kew_value,
1471
- )
1472
-
1473
- # Laporan Word
1474
- word_path = generate_word_report_all(
1475
- detail_df, agg_df, verif_df,
1476
- prov_value, kab_value, kew_value,
1477
- analysis_text
1478
- )
1479
-
1480
- return (
1481
- agg_df,
1482
- detail_df,
1483
- verif_df,
1484
- agg_path,
1485
- detail_path,
1486
- raw_path,
1487
- word_path,
1488
- fig_all,
1489
- fig_sekolah,
1490
- fig_umum,
1491
- fig_khusus,
1492
- msg,
1493
- analysis_text,
1494
- )
1495
-
1496
-
1497
- def on_prov_change(prov_value):
1498
- if df_all_raw is None or kab_col_glob is None:
1499
- return gr.update(choices=["(Semua)"], value="(Semua)")
1500
- if prov_value is None or prov_value == "(Semua)" or prov_col_glob is None:
1501
- s = df_all_raw[kab_col_glob].dropna().astype(str).str.strip()
1502
- else:
1503
- m = df_all_raw[prov_col_glob].astype(str).str.strip() == prov_value
1504
- s = df_all_raw.loc[m, kab_col_glob].dropna().astype(str).str.strip()
1505
- vals = sorted([x for x in s.unique() if x != ""])
1506
- new_choices = ["(Semua)"] + vals
1507
- return gr.update(choices=new_choices, value="(Semua)")
1508
-
1509
-
1510
- # ============================================================
1511
- # 9. BUILD UI GRADIO
1512
- # ============================================================
1513
-
1514
- with gr.Blocks() as demo:
1515
- gr.Markdown(
1516
- f"""
1517
- # IPLM 2025 – RealScore + Normatif + Verifikasi Sampel + Analisis Otomatis (LLM + Rule-based)
1518
-
1519
- Dataset diambil langsung dari file di repository (tanpa upload):
1520
-
1521
- - **`{DATA_FILE}`** – Data perpustakaan (semua jenis, multi-sheet).
1522
- - **`{META_KAB_FILE}`** – Jumlah kecamatan & desa/kel per kab/kota.
1523
- - **`{META_SDSMP_FILE}`** – Jumlah SD & SMP per kab/kota.
1524
- - **`{META_SMA_FILE}`** – Jumlah SMA per provinsi.
1525
-
1526
- {DATA_INFO}
1527
- """
1528
- )
1529
-
1530
- with gr.Row():
1531
- dd_prov = gr.Dropdown(label="Provinsi", choices=prov_choices, value=prov_choices[0])
1532
- dd_kab = gr.Dropdown(label="Kab/Kota", choices=kab_choices, value=kab_choices[0])
1533
- dd_kew = gr.Dropdown(label="Kewenangan", choices=kew_choices, value=default_kew)
1534
-
1535
- dd_prov.change(
1536
- fn=on_prov_change,
1537
- inputs=dd_prov,
1538
- outputs=dd_kab,
1539
- )
1540
-
1541
- run_btn = gr.Button("Jalankan Perhitungan")
1542
- msg_out = gr.Markdown()
1543
-
1544
- gr.Markdown("### Hasil Agregat (RealScore) per Jenis Perpustakaan")
1545
- agg_df_out = gr.DataFrame(interactive=False)
1546
-
1547
- gr.Markdown("### Detail Indeks (Real + Normatif) per Perpustakaan")
1548
- detail_df_out = gr.DataFrame(interactive=False)
1549
-
1550
- gr.Markdown("### Verifikasi Kondisi Sampel di Lapangan")
1551
- verif_df_out = gr.DataFrame(
1552
- label="Perbandingan jumlah sampel dengan populasi unit (SD/SMP/SMA, Kecamatan, Desa/Kel)",
1553
- interactive=False
1554
- )
1555
-
1556
- gr.Markdown("### Sebaran Indeks – Semua Perpustakaan (RealScore)")
1557
- bell_all_out = gr.Plot()
1558
-
1559
- gr.Markdown("### Sebaran Indeks – Perpustakaan Sekolah")
1560
- bell_sekolah_out = gr.Plot()
1561
-
1562
- gr.Markdown("### Sebaran Indeks – Perpustakaan Umum")
1563
- bell_umum_out = gr.Plot()
1564
-
1565
- gr.Markdown("### Sebaran Indeks – Perpustakaan Khusus")
1566
- bell_khusus_out = gr.Plot()
1567
-
1568
- gr.Markdown("### Analisis Otomatis & Rekomendasi Kebijakan")
1569
- analysis_out = gr.Markdown()
1570
-
1571
- with gr.Row():
1572
- agg_file_out = gr.File(label="Download File Agregat (.xlsx)")
1573
- detail_file_out = gr.File(label="Download File Detail (.xlsx)")
1574
- raw_file_out = gr.File(label="Download Data Mentah (.xlsx)")
1575
- word_file_out = gr.File(label="Download Laporan Word (.docx)")
1576
-
1577
- run_btn.click(
1578
- fn=run_app,
1579
- inputs=[dd_prov, dd_kab, dd_kew],
1580
- outputs=[
1581
- agg_df_out,
1582
- detail_df_out,
1583
- verif_df_out,
1584
- agg_file_out,
1585
- detail_file_out,
1586
- raw_file_out,
1587
- word_file_out,
1588
- bell_all_out,
1589
- bell_sekolah_out,
1590
- bell_umum_out,
1591
- bell_khusus_out,
1592
- msg_out,
1593
- analysis_out,
1594
- ],
1595
- )
1596
-
1597
- demo.launch()