irhamni commited on
Commit
91ca655
Β·
verified Β·
1 Parent(s): d9e61d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +90 -289
app.py CHANGED
@@ -1,9 +1,9 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- IPLM 2025 β€” Final (Target Sampel 33.88% per Jenis) + Kinerja Relatif (Percentile)
4
 
5
  ───────────────────────────────────────────────────────────────────────────────
6
- DOKUMENTASI / KONSEP (DIPERTAHANKAN + DIPERJELAS)
7
 
8
  A. Skor ABSOLUT (untuk akuntabilitas)
9
  ------------------------------------
@@ -31,53 +31,9 @@ A. Skor ABSOLUT (untuk akuntabilitas)
31
  Indeks_Final_Wilayah_0_100(keseluruhan) = (final_sekolah + final_umum + final_khusus) / 3
32
  - Missing jenis dianggap 0 tetapi tetap dibagi 3 (sesuai requirement).
33
 
34
- B. Skor KINERJA RELATIF (untuk benchmarking, bukan pengganti skor absolut)
35
- ---------------------------------------------------------------------------
36
- Kolom utama: Score_Kinerja_WilayahTotal_Percentile_0_100
37
- Definisi: posisi relatif suatu wilayah dibanding wilayah lain secara NASIONAL.
38
-
39
- Karakteristik utama percentile:
40
- β€’ Skala 0–100
41
- β€’ Tidak bergantung pada asumsi distribusi normal
42
- β€’ Stabil terhadap nilai ekstrem (karena berbasis peringkat)
43
- β€’ Mudah diinterpretasikan sebagai posisi peringkat
44
-
45
- RUMUS / IMPLEMENTASI (yang benar dan sesuai FIX bug):
46
- 1) Tentukan "universe" perhitungan GLOBAL sesuai mode kewenangan:
47
- - Jika kewenangan = "KAB/KOTA": universe = semua kab/kota (nasional) yang KEW_NORM == "KAB/KOTA"
48
- - Jika kewenangan = "PROVINSI": universe = semua provinsi (nasional) yang KEW_NORM == "PROVINSI"
49
- - Jika "(Semua)": default mengikuti pilihan (atau semua yang relevan) β†’ pada UI kita pakai nilai dropdown.
50
-
51
- 2) Hitung dulu agg_total_global untuk universe tersebut (tanpa filter prov/kab):
52
- - Dari df_all (nasional) β†’ faktor_wilayah_jenis β†’ agg_jenis_global β†’ agg_total_global
53
-
54
- 3) Hitung percentile GLOBAL dari Indeks_Final_Wilayah_0_100 pada agg_total_global:
55
- - Secara konsep:
56
- Percentile(w) = 100 * (rank_w / N)
57
- - Implementasi pandas yang audit-friendly:
58
- rank(pct=True, method="average") * 100
59
-
60
- 4) Tempelkan nilai percentile global itu ke hasil filter (agg_total yang biasanya hanya 1 baris):
61
- - WAJIB pakai mapping by group_key (bukan merge yang bikin kolom _x/_y)
62
- - Kenapa? agar tidak terjadi:
63
- β€’ percentile jadi 100 karena dihitung dari 1 baris filter
64
- β€’ atau KPI membaca kolom yang salah akibat suffix merge
65
-
66
- C. Bug yang kamu laporkan (0.00 / 100 semua)
67
- --------------------------------------------
68
- Kasus 1: "100 semua" untuk 1 wilayah yang difilter β†’ terjadi jika percentile dihitung dari data filter.
69
- Solusi: percentile selalu dihitung di agg_total_global lalu ditempel.
70
-
71
- Kasus 2: KPI jadi 0.00 (padahal harus 99-an) β†’ terjadi jika merge menghasilkan kolom
72
- Score_Kinerja_WilayahTotal_Percentile_0_100_x/_y sehingga kolom yang dibaca kosong/NaN.
73
- Solusi: mapping dengan dict (tidak ada suffix), dan pastikan KPI membaca kolom final.
74
-
75
- ───────────────────────────────────────────────────────────────────────────────
76
- KODE DI BAWAH INI SUDAH FIX:
77
- βœ… Score_Kinerja_WilayahTotal_Percentile_0_100 dihitung GLOBAL (nasional) sesuai kewenangan
78
- βœ… Ditempel pakai MAP (no _x/_y)
79
- βœ… KPI selalu baca kolom final yang benar
80
- βœ… Tetap mempertahankan semua fitur: ringkasan, agregat, verif, detail, bell curve, export
81
  """
82
 
83
  import os
@@ -100,7 +56,7 @@ except Exception:
100
  DOCX_AVAILABLE = False
101
  Document = None
102
 
103
- # huggingface client opsional
104
  HF_AVAILABLE = True
105
  try:
106
  from huggingface_hub import InferenceClient
@@ -124,11 +80,7 @@ W_KINERJA = float(os.getenv("W_KINERJA", "0.70"))
124
  # βœ… target sampel 33.88% per jenis
125
  TARGET_RATIO = float(os.getenv("TARGET_RATIO", "0.3388"))
126
 
127
- # kinerja relatif
128
- USE_PERCENTILE = True
129
- USE_ROBUST_Z = True
130
-
131
- # LLM opsional
132
  USE_LLM = True
133
  LLM_MODEL_NAME = os.getenv("LLM_MODEL_NAME", "meta-llama/Meta-Llama-3-8B-Instruct")
134
  HF_TOKEN = (
@@ -267,71 +219,6 @@ def faktor_penyesuaian_total(n_total: float, target_total: float) -> float:
267
  n_total = 0.0
268
  return float(min(float(n_total) / float(target_total), 1.0))
269
 
270
- def add_kinerja_scores(
271
- df: pd.DataFrame,
272
- score_col: str,
273
- group_cols: list | None,
274
- prefix: str = "Score_Kinerja"
275
- ) -> pd.DataFrame:
276
- """
277
- Tambah kolom:
278
- - {prefix}_Percentile_0_100 = rank(pct=True)*100
279
- - {prefix}_RobustZ_0_100 = 50 + 10*z_robust (MAD-based), clip 0..100
280
- """
281
- if df is None or df.empty or score_col not in df.columns:
282
- return df
283
-
284
- out = df.copy()
285
-
286
- # Percentile 0–100
287
- if USE_PERCENTILE:
288
- if group_cols:
289
- out[f"{prefix}_Percentile_0_100"] = (
290
- out.groupby(group_cols, dropna=False)[score_col]
291
- .rank(pct=True, method="average") * 100.0
292
- )
293
- else:
294
- out[f"{prefix}_Percentile_0_100"] = out[score_col].rank(pct=True, method="average") * 100.0
295
-
296
- out[f"{prefix}_Percentile_0_100"] = (
297
- pd.to_numeric(out[f"{prefix}_Percentile_0_100"], errors="coerce")
298
- .fillna(0.0).clip(0, 100).round(2)
299
- )
300
-
301
- # Robust Z to 0–100
302
- if USE_ROBUST_Z:
303
- def _robustz_to_0_100(s: pd.Series) -> pd.Series:
304
- v = pd.to_numeric(s, errors="coerce").astype(float)
305
- v = v.replace([np.inf, -np.inf], np.nan)
306
- if v.dropna().shape[0] < 2:
307
- return pd.Series(50.0, index=v.index)
308
-
309
- med = float(np.nanmedian(v.values))
310
- mad = float(np.nanmedian(np.abs(v.values - med)))
311
-
312
- if (not np.isfinite(mad)) or mad <= 1e-12:
313
- sd = float(np.nanstd(v.values, ddof=1))
314
- if (not np.isfinite(sd)) or sd <= 1e-12:
315
- return pd.Series(50.0, index=v.index)
316
- z = (v - med) / sd
317
- else:
318
- z = (v - med) / (1.4826 * mad)
319
-
320
- score = 50.0 + 10.0 * z
321
- return score.clip(0, 100).fillna(50.0)
322
-
323
- if group_cols:
324
- out[f"{prefix}_RobustZ_0_100"] = out.groupby(group_cols, dropna=False)[score_col].transform(_robustz_to_0_100)
325
- else:
326
- out[f"{prefix}_RobustZ_0_100"] = _robustz_to_0_100(out[score_col])
327
-
328
- out[f"{prefix}_RobustZ_0_100"] = (
329
- pd.to_numeric(out[f"{prefix}_RobustZ_0_100"], errors="coerce")
330
- .fillna(50.0).clip(0, 100).round(2)
331
- )
332
-
333
- return out
334
-
335
 
336
  # ============================================================
337
  # 3) INDIKATOR IPLM
@@ -680,6 +567,24 @@ def load_default_files(force=False):
680
  # 6) FAKTOR WILAYAH β€” PER JENIS (TARGET 33.88%)
681
  # ============================================================
682
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  def build_faktor_wilayah_jenis(
684
  df_filtered: pd.DataFrame,
685
  pop_kab: pd.DataFrame,
@@ -709,16 +614,22 @@ def build_faktor_wilayah_jenis(
709
  key_col, label_col, label_name, mode = "prov_key", "PROV_DISP", "Provinsi", "PROV"
710
  base_pop = pop_prov.copy() if (pop_prov is not None and not pop_prov.empty) else pd.DataFrame()
711
  if not base_pop.empty and "prov_key" not in base_pop.columns:
712
- 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)
 
 
 
713
  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([]))
714
  else:
715
  key_col, label_col, label_name, mode = "kab_key", "KAB_DISP", "Kab/Kota", "KAB"
716
  base_pop = pop_kab.copy() if (pop_kab is not None and not pop_kab.empty) else pd.DataFrame()
717
  if not base_pop.empty and "kab_key" not in base_pop.columns:
718
- 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)
 
 
 
719
  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([]))
720
 
721
- # GRID: semua wilayah Γ— 3 jenis
722
  base_keys = df[[key_col, label_col]].drop_duplicates().rename(columns={key_col: "group_key", label_col: label_name})
723
  full = base_keys.assign(_tmp=1).merge(
724
  pd.DataFrame({"Jenis": jenis_list, "_tmp": 1}),
@@ -741,24 +652,34 @@ def build_faktor_wilayah_jenis(
741
  base_n["pop_total_jenis"] = 0.0
742
 
743
  # SEKOLAH + UMUM dari POP_KAB/POP_PROV
 
 
 
 
 
744
  if not base_pop.empty:
745
  if mode == "KAB":
746
- pop_sekolah = pd.to_numeric(base_pop.get("jumlah_populasi_sekolah", 0), errors="coerce").fillna(0.0)
747
- pop_umum = pd.to_numeric(base_pop.get("jumlah_populasi_umum", 0), errors="coerce").fillna(0.0)
748
-
 
 
 
 
 
 
 
749
  tgt_sekolah = pop_sekolah * float(TARGET_RATIO)
750
  tgt_umum = pop_umum * float(TARGET_RATIO)
751
  else:
752
- sma = pd.to_numeric(base_pop.get("sma ", base_pop.get("sma", 0)), errors="coerce").fillna(0.0)
753
- smk = pd.to_numeric(base_pop.get("smk", 0)),
754
- slb = pd.to_numeric(base_pop.get("slb", 0)),
755
- smk = pd.to_numeric(base_pop.get("smk", 0), errors="coerce").fillna(0.0)
756
- slb = pd.to_numeric(base_pop.get("slb", 0), errors="coerce").fillna(0.0)
757
-
758
- pop_sekolah = sma + smk + slb
759
  tgt_sekolah = pop_sekolah * float(TARGET_RATIO)
760
 
761
- pop_umum = pd.to_numeric(base_pop.get("perpus_umum_prop", 0), errors="coerce").fillna(0.0)
762
  tgt_umum = pop_umum * float(TARGET_RATIO)
763
 
764
  m = base_n["Jenis"].eq("sekolah")
@@ -837,14 +758,11 @@ def build_faktor_wilayah_jenis(
837
 
838
  def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
839
  """
840
- Agregasi:
841
- wilayah Γ— jenis:
842
  - Jumlah (n entitas)
843
  - rata-rata sub/dim
844
  - Indeks_Dasar_Agregat_0_100 = mean(Indeks_Dasar_0_100)
845
  - Indeks_Final_Agregat_0_100 = Indeks_Dasar_Agregat_0_100 * faktor_penyesuaian_jenis
846
- + score kinerja relatif per jenis:
847
- Score_Kinerja_WilayahJenis_Percentile_0_100
848
  """
849
  if df_filtered is None or df_filtered.empty:
850
  return pd.DataFrame()
@@ -924,14 +842,6 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.
924
  * pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0)
925
  )
926
 
927
- # Kinerja relatif per jenis
928
- agg = add_kinerja_scores(
929
- agg,
930
- score_col="Indeks_Final_Agregat_0_100",
931
- group_cols=["Jenis"],
932
- prefix="Score_Kinerja_WilayahJenis"
933
- )
934
-
935
  # rounding
936
  for c in [
937
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
@@ -1053,8 +963,6 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_j
1053
  )
1054
  out["coverage_target33_88_all_%"] = pd.to_numeric(out["coverage_target33_88_all_%"], errors="coerce").fillna(0.0).round(2)
1055
 
1056
- # NOTE: percentile global untuk wilayah keseluruhan tidak dihitung di sini.
1057
- # Ia dihitung oleh fungsi global (compute_global_wilayah_scores) lalu ditempel.
1058
  for c in [
1059
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1060
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -1070,79 +978,6 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_j
1070
  return out
1071
 
1072
 
1073
- # ============================================================
1074
- # 8B) GLOBAL SCORE TABLE (FIX: percentile harus dihitung nasional)
1075
- # ============================================================
1076
-
1077
- _GLOBAL_SCORE_CACHE = {}
1078
-
1079
- def compute_global_wilayah_scores(df_all, pop_kab, pop_prov, pop_khusus, kew_value: str):
1080
- """
1081
- FIX UTAMA:
1082
- - Hitung agg_total GLOBAL (nasional) sesuai mode kewenangan (KAB/KOTA vs PROVINSI)
1083
- - Lalu hitung Score_Kinerja_WilayahTotal_Percentile_0_100 pada agg_total_global
1084
- - Return mapping dict: group_key -> percentile (dan robustZ jika dipakai)
1085
-
1086
- Kenapa mapping dict?
1087
- - Menghindari merge suffix _x/_y
1088
- - Mencegah KPI membaca kolom yang salah (0.00)
1089
- """
1090
- kew_norm = str(kew_value or "").upper()
1091
- cache_key = (
1092
- kew_norm,
1093
- _mtime(DATA_FILE), _mtime(POP_KAB), _mtime(POP_PROV), _mtime(POP_KHUSUS),
1094
- float(TARGET_RATIO), float(W_KEPATUHAN), float(W_KINERJA),
1095
- bool(USE_PERCENTILE), bool(USE_ROBUST_Z)
1096
- )
1097
- if cache_key in _GLOBAL_SCORE_CACHE:
1098
- return _GLOBAL_SCORE_CACHE[cache_key]
1099
-
1100
- if df_all is None or df_all.empty:
1101
- _GLOBAL_SCORE_CACHE[cache_key] = ({}, {})
1102
- return {}, {}
1103
-
1104
- # Universe global sesuai kewenangan
1105
- if kew_norm in {"KAB/KOTA", "PROVINSI"}:
1106
- df_univ = df_all[df_all["KEW_NORM"] == kew_norm].copy()
1107
- else:
1108
- # fallback: pakai semua (tapi tetap nanti label mengikuti agg_total yang dipakai)
1109
- df_univ = df_all.copy()
1110
-
1111
- faktor = build_faktor_wilayah_jenis(df_univ, pop_kab, pop_prov, pop_khusus, kew_norm)
1112
- agg_jenis = build_agg_wilayah_jenis(df_univ, faktor, kew_norm)
1113
- agg_total = build_agg_wilayah_total_from_jenis(agg_jenis, faktor, kew_norm)
1114
-
1115
- # Hitung score relatif global pada agg_total_global
1116
- agg_total = add_kinerja_scores(
1117
- agg_total,
1118
- score_col="Indeks_Final_Wilayah_0_100",
1119
- group_cols=None,
1120
- prefix="Score_Kinerja_WilayahTotal"
1121
- )
1122
-
1123
- pctl_map = {}
1124
- rz_map = {}
1125
-
1126
- if "group_key" in agg_total.columns and "Score_Kinerja_WilayahTotal_Percentile_0_100" in agg_total.columns:
1127
- pctl_map = (
1128
- agg_total[["group_key", "Score_Kinerja_WilayahTotal_Percentile_0_100"]]
1129
- .dropna(subset=["group_key"])
1130
- .set_index("group_key")["Score_Kinerja_WilayahTotal_Percentile_0_100"]
1131
- .to_dict()
1132
- )
1133
-
1134
- if "group_key" in agg_total.columns and "Score_Kinerja_WilayahTotal_RobustZ_0_100" in agg_total.columns:
1135
- rz_map = (
1136
- agg_total[["group_key", "Score_Kinerja_WilayahTotal_RobustZ_0_100"]]
1137
- .dropna(subset=["group_key"])
1138
- .set_index("group_key")["Score_Kinerja_WilayahTotal_RobustZ_0_100"]
1139
- .to_dict()
1140
- )
1141
-
1142
- _GLOBAL_SCORE_CACHE[cache_key] = (pctl_map, rz_map)
1143
- return pctl_map, rz_map
1144
-
1145
-
1146
  # ============================================================
1147
  # 9) SUMMARY (PER JENIS) + KESELURUHAN
1148
  # ============================================================
@@ -1259,7 +1094,6 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
1259
 
1260
  # ============================================================
1261
  # 10) DETAIL ENTITAS: Final menempel dari agg_total (wilayah)
1262
- # + skor kinerja relatif per jenis (entitas-level)
1263
  # ============================================================
1264
 
1265
  def attach_final_to_detail(df_filtered: pd.DataFrame, agg_total: pd.DataFrame, meta: dict, kew_value: str):
@@ -1300,14 +1134,6 @@ def attach_final_to_detail(df_filtered: pd.DataFrame, agg_total: pd.DataFrame, m
1300
  out = df[keep].copy()
1301
  out = out.rename(columns={label_cols[0]:"Provinsi", label_cols[1]:"Kab/Kota", "_dataset":"Jenis"})
1302
 
1303
- # skor kinerja relatif per entitas (dibandingkan sesama jenis)
1304
- out = add_kinerja_scores(
1305
- out,
1306
- score_col="Indeks_Dasar_0_100",
1307
- group_cols=["Jenis"],
1308
- prefix="Score_Kinerja_Entitas"
1309
- )
1310
-
1311
  for c in ["sub_koleksi","sub_sdm","sub_pelayanan","sub_pengelolaan","dim_kepatuhan","dim_kinerja"]:
1312
  if c in out.columns:
1313
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
@@ -1352,10 +1178,10 @@ def build_verif_jenis(faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
1352
 
1353
 
1354
  # ============================================================
1355
- # 12) BELL CURVE (sama seperti versi kamu, disederhanakan aman)
1356
  # ============================================================
1357
 
1358
- def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str | None = None, hover_cols: list | None = None, min_points: int = 2):
1359
  fig = go.Figure()
1360
  fig.update_layout(
1361
  title=title,
@@ -1411,7 +1237,7 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
1411
 
1412
 
1413
  # ============================================================
1414
- # 13) KPI DASHBOARD (skor absolut + percentile GLOBAL)
1415
  # ============================================================
1416
 
1417
  def _safe_first(df, col, default=0.0, where=None):
@@ -1424,22 +1250,17 @@ def _safe_first(df, col, default=0.0, where=None):
1424
  return default
1425
  return float(pd.to_numeric(sub[col], errors="coerce").fillna(default).iloc[0])
1426
 
1427
- def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame):
1428
  final_all = _safe_first(summary_jenis, "Indeks_Final_Disesuaikan_0_100", 0.0, where=summary_jenis["Jenis"].astype(str).str.lower().eq("keseluruhan"))
1429
  dasar_all = _safe_first(summary_jenis, "Indeks_Dasar_0_100", 0.0, where=summary_jenis["Jenis"].astype(str).str.lower().eq("keseluruhan"))
 
 
1430
 
1431
- # KPI percentile wilayah terpilih: di agg_total (sudah ditempel global)
1432
- pctl_sel = 0.0
1433
- if agg_total is not None and not agg_total.empty and "Score_Kinerja_WilayahTotal_Percentile_0_100" in agg_total.columns:
1434
- pctl_sel = float(pd.to_numeric(agg_total["Score_Kinerja_WilayahTotal_Percentile_0_100"], errors="coerce").fillna(0.0).iloc[0])
1435
-
1436
- return {"final_all": final_all, "dasar_all": dasar_all, "pctl_sel": pctl_sel}
1437
-
1438
- def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame) -> str:
1439
  if summary_jenis is None or summary_jenis.empty:
1440
  return ""
1441
 
1442
- k = compute_dashboard_kpis(summary_jenis, agg_total)
1443
 
1444
  def fmt(x, nd=2):
1445
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
@@ -1459,9 +1280,9 @@ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame) ->
1459
  </div>
1460
 
1461
  <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:260px;">
1462
- <div style="opacity:0.8;">Nilai Kinerja (Percentile Wilayah)</div>
1463
- <div style="font-size:26px; font-weight:700;">{fmt(k["pctl_sel"],2)}</div>
1464
- <div style="opacity:0.7;">Percentile GLOBAL (nasional), bukan hasil filter</div>
1465
  </div>
1466
  </div>
1467
  """.strip()
@@ -1497,9 +1318,9 @@ def generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah, kew):
1497
  model=LLM_MODEL_NAME,
1498
  messages=[
1499
  {"role":"system","content":"Anda adalah analis kebijakan perpustakaan di Indonesia. Tulis analisis ringkas berbasis data."},
1500
- {"role":"user","content":f"{ctx}\nBuat analisis 3 paragraf: skor absolut, kinerja relatif percentile, rekomendasi singkat."}
1501
  ],
1502
- max_tokens=500,
1503
  temperature=0.25,
1504
  top_p=0.9,
1505
  )
@@ -1514,7 +1335,6 @@ def generate_word_report(wilayah, summary_jenis, analysis_text):
1514
  doc = Document()
1515
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1516
  doc.add_paragraph(f"Target sampel per jenis: {TARGET_RATIO*100:.2f}%")
1517
- doc.add_paragraph("Catatan: Percentile kinerja wilayah yang ditampilkan adalah percentile GLOBAL (nasional), bukan dari hasil filter.")
1518
  doc.add_heading("Ringkasan (Jenis + Keseluruhan)", level=2)
1519
  if summary_jenis is not None and not summary_jenis.empty:
1520
  show = summary_jenis.copy()
@@ -1585,29 +1405,14 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1585
  agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah_jenis, kew_norm)
1586
 
1587
  # =========================================================
1588
- # 3) FIX PERCENTILE: hitung GLOBAL dulu, lalu TEMPEL via MAP
1589
- # (NO MERGE β†’ no _x/_y, KPI tidak akan 0.00)
1590
- # =========================================================
1591
- pctl_map, rz_map = compute_global_wilayah_scores(df_all, pop_kab, pop_prov, pop_khusus, kew_norm)
1592
-
1593
- if agg_total is not None and not agg_total.empty and "group_key" in agg_total.columns:
1594
- agg_total["Score_Kinerja_WilayahTotal_Percentile_0_100"] = (
1595
- agg_total["group_key"].map(pctl_map).fillna(0.0).astype(float).round(2)
1596
- )
1597
- if USE_ROBUST_Z:
1598
- agg_total["Score_Kinerja_WilayahTotal_RobustZ_0_100"] = (
1599
- agg_total["group_key"].map(rz_map).fillna(50.0).astype(float).round(2)
1600
- )
1601
-
1602
- # =========================================================
1603
- # 4) OUTPUT TABLES
1604
  # =========================================================
1605
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1606
  verif_total = build_verif_jenis(faktor_wilayah_jenis, kew_norm)
1607
  detail_view = attach_final_to_detail(df, agg_total, meta, kew_norm)
1608
 
1609
  # =========================================================
1610
- # 5) agg_jenis view (UI hanya sampai indeks dasar)
1611
  # =========================================================
1612
  if agg_jenis_full is None or agg_jenis_full.empty:
1613
  agg_jenis_view = agg_jenis_full
@@ -1627,7 +1432,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1627
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1628
 
1629
  # =========================================================
1630
- # 6) FILTER RAW DOWNLOAD (harus raw hasil filter)
1631
  # =========================================================
1632
  raw = df_raw.copy()
1633
  if prov_value and prov_value != "(Semua)":
@@ -1638,14 +1443,15 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1638
  raw = raw[raw["KEW_NORM"] == kew_value]
1639
 
1640
  # =========================================================
1641
- # 7) Bell curve per jenis (entitas)
 
1642
  # =========================================================
1643
  if detail_view is None or detail_view.empty:
1644
- fig_umum = _make_bell_curve(pd.DataFrame(), "Score_Kinerja_Entitas_Percentile_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
1645
- fig_sekolah = _make_bell_curve(pd.DataFrame(), "Score_Kinerja_Entitas_Percentile_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1646
- fig_khusus = _make_bell_curve(pd.DataFrame(), "Score_Kinerja_Entitas_Percentile_0_100", "Bell Curve β€” Jenis: Khusus", min_points=2)
1647
  else:
1648
- xcol_ent = "Score_Kinerja_Entitas_Percentile_0_100" if "Score_Kinerja_Entitas_Percentile_0_100" in detail_view.columns else "Indeks_Dasar_0_100"
1649
  def _fig(j):
1650
  d = detail_view[detail_view["Jenis"].astype(str).str.lower() == j].copy()
1651
  return _make_bell_curve(d, xcol_ent, f"Bell Curve β€” Jenis: {j.title()} (Skor: {xcol_ent})", min_points=2)
@@ -1654,12 +1460,12 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1654
  fig_khusus = _fig("khusus")
1655
 
1656
  # =========================================================
1657
- # 8) KPI (percentile sudah GLOBAL)
1658
  # =========================================================
1659
- kpi_md = build_kpi_markdown(summary_jenis, agg_total)
1660
 
1661
  # =========================================================
1662
- # 9) Export (xlsx + opsional docx)
1663
  # =========================================================
1664
  tmpdir = tempfile.mkdtemp()
1665
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
@@ -1743,7 +1549,7 @@ def on_prov_change(prov_value):
1743
 
1744
  with gr.Blocks() as demo:
1745
  gr.Markdown(f"""
1746
- # IPLM 2025 β€” Final (Target Sampel **33.88%** per Jenis) + Kinerja Relatif (Percentile GLOBAL)
1747
  **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1748
  - `DATA_FILE` = **{DATA_FILE}**
1749
  - `POP_KAB` = **{POP_KAB}**
@@ -1752,15 +1558,10 @@ with gr.Blocks() as demo:
1752
 
1753
  **TARGET RATIO (per jenis): {TARGET_RATIO*100:.2f}%**
1754
 
1755
- βœ… Dashboard KPI menampilkan juga:
1756
- - `Score_Kinerja_WilayahTotal_Percentile_0_100` (**GLOBAL nasional**; bukan hasil filter)
1757
-
1758
- **Skor Absolut (untuk akuntabilitas):**
1759
- - `Indeks_Final_*` (sudah disesuaikan target 33.88%)
1760
-
1761
- **Skor Kinerja Relatif (untuk benchmarking):**
1762
- - `Score_Kinerja_*_Percentile_0_100` (utama, stabil tanpa asumsi normal)
1763
- - `Score_Kinerja_*_RobustZ_0_100` (opsional, tahan outlier)
1764
  """)
1765
 
1766
  state_df = gr.State(None)
@@ -1787,19 +1588,19 @@ with gr.Blocks() as demo:
1787
  gr.Markdown("## Ringkasan (Jenis + Keseluruhan) β€” Pop/Target33.88/Terkumpul/Coverage + Penyesuaian")
1788
  out_summary = gr.DataFrame(interactive=False)
1789
 
1790
- gr.Markdown("## Agregat Wilayah (Keseluruhan) β€” FIX avg3 + Score Kinerja Relatif (GLOBAL)")
1791
  out_agg_total = gr.DataFrame(interactive=False)
1792
 
1793
  gr.Markdown("## Agregat Wilayah Γ— Jenis β€” (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
1794
  out_agg_jenis = gr.DataFrame(interactive=False)
1795
 
1796
- gr.Markdown("## Detail Entitas (Final menempel dari wilayah + Skor Kinerja Relatif per Jenis)")
1797
  out_detail = gr.DataFrame(interactive=False)
1798
 
1799
  gr.Markdown("## Kecukupan Sampel 33.88% (tanpa angka koma untuk integer)")
1800
  out_verif = gr.DataFrame(interactive=False)
1801
 
1802
- gr.Markdown("## Bell Curve β€” per Jenis")
1803
  gr.Markdown("### Perpustakaan Umum")
1804
  bell_umum = gr.Plot(scale=1)
1805
 
@@ -1837,4 +1638,4 @@ with gr.Blocks() as demo:
1837
  outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
1838
  )
1839
 
1840
- demo.launch()
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ IPLM 2025 β€” Final (Target Sampel 33.88% per Jenis)
4
 
5
  ───────────────────────────────────────────────────────────────────────────────
6
+ KONSEP (DIPERTAHANKAN + DIPERJELAS)
7
 
8
  A. Skor ABSOLUT (untuk akuntabilitas)
9
  ------------------------------------
 
31
  Indeks_Final_Wilayah_0_100(keseluruhan) = (final_sekolah + final_umum + final_khusus) / 3
32
  - Missing jenis dianggap 0 tetapi tetap dibagi 3 (sesuai requirement).
33
 
34
+ CATATAN:
35
+ - Versi ini SUDAH MENGHILANGKAN seluruh fitur "Kinerja Relatif (Percentile/RobustZ)".
36
+ - Dashboard hanya menampilkan skor absolut dan penyesuaian target 33.88% per jenis.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  """
38
 
39
  import os
 
56
  DOCX_AVAILABLE = False
57
  Document = None
58
 
59
+ # huggingface client opsional (LLM)
60
  HF_AVAILABLE = True
61
  try:
62
  from huggingface_hub import InferenceClient
 
80
  # βœ… target sampel 33.88% per jenis
81
  TARGET_RATIO = float(os.getenv("TARGET_RATIO", "0.3388"))
82
 
83
+ # LLM opsional (tidak wajib; aman dimatikan)
 
 
 
 
84
  USE_LLM = True
85
  LLM_MODEL_NAME = os.getenv("LLM_MODEL_NAME", "meta-llama/Meta-Llama-3-8B-Instruct")
86
  HF_TOKEN = (
 
219
  n_total = 0.0
220
  return float(min(float(n_total) / float(target_total), 1.0))
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  # ============================================================
224
  # 3) INDIKATOR IPLM
 
567
  # 6) FAKTOR WILAYAH β€” PER JENIS (TARGET 33.88%)
568
  # ============================================================
569
 
570
+ def _get_series_from_cols(base_pop: pd.DataFrame, col_candidates: list, index_name: str):
571
+ """
572
+ Ambil series dari base_pop berdasarkan kandidat nama kolom.
573
+ Return series float dengan index base_pop.index.
574
+ """
575
+ for c in col_candidates:
576
+ if c in base_pop.columns:
577
+ return pd.to_numeric(base_pop[c], errors="coerce").fillna(0.0)
578
+ # fallback: coba versi canon
579
+ can_map = {_canon(c): c for c in base_pop.columns}
580
+ for c in col_candidates:
581
+ k = _canon(c)
582
+ if k in can_map:
583
+ cc = can_map[k]
584
+ return pd.to_numeric(base_pop[cc], errors="coerce").fillna(0.0)
585
+ # jika tidak ada, return zeros
586
+ return pd.Series(0.0, index=base_pop.index, name=f"{index_name}_zeros")
587
+
588
  def build_faktor_wilayah_jenis(
589
  df_filtered: pd.DataFrame,
590
  pop_kab: pd.DataFrame,
 
614
  key_col, label_col, label_name, mode = "prov_key", "PROV_DISP", "Provinsi", "PROV"
615
  base_pop = pop_prov.copy() if (pop_prov is not None and not pop_prov.empty) else pd.DataFrame()
616
  if not base_pop.empty and "prov_key" not in base_pop.columns:
617
+ if "Provinsi_Label" in base_pop.columns:
618
+ base_pop["prov_key"] = base_pop["Provinsi_Label"].apply(norm_prov_label)
619
+ else:
620
+ base_pop["prov_key"] = base_pop.iloc[:, 0].apply(norm_prov_label)
621
  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([]))
622
  else:
623
  key_col, label_col, label_name, mode = "kab_key", "KAB_DISP", "Kab/Kota", "KAB"
624
  base_pop = pop_kab.copy() if (pop_kab is not None and not pop_kab.empty) else pd.DataFrame()
625
  if not base_pop.empty and "kab_key" not in base_pop.columns:
626
+ if "Kab_Kota_Label" in base_pop.columns:
627
+ base_pop["kab_key"] = base_pop["Kab_Kota_Label"].apply(norm_kab_label)
628
+ else:
629
+ base_pop["kab_key"] = base_pop.iloc[:, 0].apply(norm_kab_label)
630
  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([]))
631
 
632
+ # GRID: semua wilayah Γ— 3 jenis (yang muncul di data hasil filter)
633
  base_keys = df[[key_col, label_col]].drop_duplicates().rename(columns={key_col: "group_key", label_col: label_name})
634
  full = base_keys.assign(_tmp=1).merge(
635
  pd.DataFrame({"Jenis": jenis_list, "_tmp": 1}),
 
652
  base_n["pop_total_jenis"] = 0.0
653
 
654
  # SEKOLAH + UMUM dari POP_KAB/POP_PROV
655
+ pop_sekolah = None
656
+ pop_umum = None
657
+ tgt_sekolah = None
658
+ tgt_umum = None
659
+
660
  if not base_pop.empty:
661
  if mode == "KAB":
662
+ pop_sekolah = _get_series_from_cols(
663
+ base_pop,
664
+ ["jumlah_populasi_sekolah", "pop_sekolah", "sekolah"],
665
+ "pop_sekolah"
666
+ )
667
+ pop_umum = _get_series_from_cols(
668
+ base_pop,
669
+ ["jumlah_populasi_umum", "pop_umum", "umum"],
670
+ "pop_umum"
671
+ )
672
  tgt_sekolah = pop_sekolah * float(TARGET_RATIO)
673
  tgt_umum = pop_umum * float(TARGET_RATIO)
674
  else:
675
+ # prov: sekolah = sma + smk + slb (nama kolom bisa bervariasi)
676
+ sma = _get_series_from_cols(base_pop, ["sma", "SMA", "sma "], "sma")
677
+ smk = _get_series_from_cols(base_pop, ["smk", "SMK"], "smk")
678
+ slb = _get_series_from_cols(base_pop, ["slb", "SLB"], "slb")
679
+ pop_sekolah = (sma + smk + slb)
 
 
680
  tgt_sekolah = pop_sekolah * float(TARGET_RATIO)
681
 
682
+ pop_umum = _get_series_from_cols(base_pop, ["perpus_umum_prop", "perpus_umum", "umum"], "pop_umum")
683
  tgt_umum = pop_umum * float(TARGET_RATIO)
684
 
685
  m = base_n["Jenis"].eq("sekolah")
 
758
 
759
  def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
760
  """
761
+ Agregasi wilayah Γ— jenis:
 
762
  - Jumlah (n entitas)
763
  - rata-rata sub/dim
764
  - Indeks_Dasar_Agregat_0_100 = mean(Indeks_Dasar_0_100)
765
  - Indeks_Final_Agregat_0_100 = Indeks_Dasar_Agregat_0_100 * faktor_penyesuaian_jenis
 
 
766
  """
767
  if df_filtered is None or df_filtered.empty:
768
  return pd.DataFrame()
 
842
  * pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0)
843
  )
844
 
 
 
 
 
 
 
 
 
845
  # rounding
846
  for c in [
847
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
 
963
  )
964
  out["coverage_target33_88_all_%"] = pd.to_numeric(out["coverage_target33_88_all_%"], errors="coerce").fillna(0.0).round(2)
965
 
 
 
966
  for c in [
967
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
968
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
978
  return out
979
 
980
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981
  # ============================================================
982
  # 9) SUMMARY (PER JENIS) + KESELURUHAN
983
  # ============================================================
 
1094
 
1095
  # ============================================================
1096
  # 10) DETAIL ENTITAS: Final menempel dari agg_total (wilayah)
 
1097
  # ============================================================
1098
 
1099
  def attach_final_to_detail(df_filtered: pd.DataFrame, agg_total: pd.DataFrame, meta: dict, kew_value: str):
 
1134
  out = df[keep].copy()
1135
  out = out.rename(columns={label_cols[0]:"Provinsi", label_cols[1]:"Kab/Kota", "_dataset":"Jenis"})
1136
 
 
 
 
 
 
 
 
 
1137
  for c in ["sub_koleksi","sub_sdm","sub_pelayanan","sub_pengelolaan","dim_kepatuhan","dim_kinerja"]:
1138
  if c in out.columns:
1139
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
 
1178
 
1179
 
1180
  # ============================================================
1181
+ # 12) BELL CURVE (menampilkan distribusi skor ABSOLUT)
1182
  # ============================================================
1183
 
1184
+ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, min_points: int = 2):
1185
  fig = go.Figure()
1186
  fig.update_layout(
1187
  title=title,
 
1237
 
1238
 
1239
  # ============================================================
1240
+ # 13) KPI DASHBOARD (skor absolut saja)
1241
  # ============================================================
1242
 
1243
  def _safe_first(df, col, default=0.0, where=None):
 
1250
  return default
1251
  return float(pd.to_numeric(sub[col], errors="coerce").fillna(default).iloc[0])
1252
 
1253
+ def compute_dashboard_kpis(summary_jenis: pd.DataFrame):
1254
  final_all = _safe_first(summary_jenis, "Indeks_Final_Disesuaikan_0_100", 0.0, where=summary_jenis["Jenis"].astype(str).str.lower().eq("keseluruhan"))
1255
  dasar_all = _safe_first(summary_jenis, "Indeks_Dasar_0_100", 0.0, where=summary_jenis["Jenis"].astype(str).str.lower().eq("keseluruhan"))
1256
+ cov_all = _safe_first(summary_jenis, "Coverage_Target33_88_Jenis_%", 0.0, where=summary_jenis["Jenis"].astype(str).str.lower().eq("keseluruhan"))
1257
+ return {"final_all": final_all, "dasar_all": dasar_all, "cov_all": cov_all}
1258
 
1259
+ def build_kpi_markdown(summary_jenis: pd.DataFrame) -> str:
 
 
 
 
 
 
 
1260
  if summary_jenis is None or summary_jenis.empty:
1261
  return ""
1262
 
1263
+ k = compute_dashboard_kpis(summary_jenis)
1264
 
1265
  def fmt(x, nd=2):
1266
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
 
1280
  </div>
1281
 
1282
  <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:260px;">
1283
+ <div style="opacity:0.8;">Coverage terhadap Target 33.88% (Keseluruhan)</div>
1284
+ <div style="font-size:26px; font-weight:700;">{fmt(k["cov_all"],2)}%</div>
1285
+ <div style="opacity:0.7;">(Terkumpul Γ· Target33.88) Γ— 100</div>
1286
  </div>
1287
  </div>
1288
  """.strip()
 
1318
  model=LLM_MODEL_NAME,
1319
  messages=[
1320
  {"role":"system","content":"Anda adalah analis kebijakan perpustakaan di Indonesia. Tulis analisis ringkas berbasis data."},
1321
+ {"role":"user","content":f"{ctx}\nBuat analisis 3 paragraf: (1) skor dasar vs final, (2) kecukupan sampel 33.88% per jenis, (3) rekomendasi singkat."}
1322
  ],
1323
+ max_tokens=520,
1324
  temperature=0.25,
1325
  top_p=0.9,
1326
  )
 
1335
  doc = Document()
1336
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1337
  doc.add_paragraph(f"Target sampel per jenis: {TARGET_RATIO*100:.2f}%")
 
1338
  doc.add_heading("Ringkasan (Jenis + Keseluruhan)", level=2)
1339
  if summary_jenis is not None and not summary_jenis.empty:
1340
  show = summary_jenis.copy()
 
1405
  agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah_jenis, kew_norm)
1406
 
1407
  # =========================================================
1408
+ # 3) OUTPUT TABLES
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1409
  # =========================================================
1410
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1411
  verif_total = build_verif_jenis(faktor_wilayah_jenis, kew_norm)
1412
  detail_view = attach_final_to_detail(df, agg_total, meta, kew_norm)
1413
 
1414
  # =========================================================
1415
+ # 4) agg_jenis view (UI hanya sampai indeks dasar)
1416
  # =========================================================
1417
  if agg_jenis_full is None or agg_jenis_full.empty:
1418
  agg_jenis_view = agg_jenis_full
 
1432
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1433
 
1434
  # =========================================================
1435
+ # 5) FILTER RAW DOWNLOAD (harus raw hasil filter)
1436
  # =========================================================
1437
  raw = df_raw.copy()
1438
  if prov_value and prov_value != "(Semua)":
 
1443
  raw = raw[raw["KEW_NORM"] == kew_value]
1444
 
1445
  # =========================================================
1446
+ # 6) Bell curve per jenis (ENTITAS) β€” skor ABSOLUT
1447
+ # default pakai Indeks_Final_0_100 (lebih β€œnyambung” dg penyesuaian)
1448
  # =========================================================
1449
  if detail_view is None or detail_view.empty:
1450
+ fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Final_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
1451
+ fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Final_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1452
+ fig_khusus = _make_bell_curve(pd.DataFrame(), "Indeks_Final_0_100", "Bell Curve β€” Jenis: Khusus", min_points=2)
1453
  else:
1454
+ xcol_ent = "Indeks_Final_0_100" if "Indeks_Final_0_100" in detail_view.columns else "Indeks_Dasar_0_100"
1455
  def _fig(j):
1456
  d = detail_view[detail_view["Jenis"].astype(str).str.lower() == j].copy()
1457
  return _make_bell_curve(d, xcol_ent, f"Bell Curve β€” Jenis: {j.title()} (Skor: {xcol_ent})", min_points=2)
 
1460
  fig_khusus = _fig("khusus")
1461
 
1462
  # =========================================================
1463
+ # 7) KPI (skor absolut)
1464
  # =========================================================
1465
+ kpi_md = build_kpi_markdown(summary_jenis)
1466
 
1467
  # =========================================================
1468
+ # 8) Export (xlsx + opsional docx)
1469
  # =========================================================
1470
  tmpdir = tempfile.mkdtemp()
1471
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
 
1549
 
1550
  with gr.Blocks() as demo:
1551
  gr.Markdown(f"""
1552
+ # IPLM 2025 β€” Final (Target Sampel **33.88%** per Jenis)
1553
  **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1554
  - `DATA_FILE` = **{DATA_FILE}**
1555
  - `POP_KAB` = **{POP_KAB}**
 
1558
 
1559
  **TARGET RATIO (per jenis): {TARGET_RATIO*100:.2f}%**
1560
 
1561
+ βœ… Output fokus pada **Skor Absolut**:
1562
+ - `Indeks_Dasar_0_100` (entitas)
1563
+ - `Indeks_Final_*` (agregat) = skor dasar Γ— faktor kecukupan sampel 33.88% (per jenis)
1564
+ - `Keseluruhan` wajib **avg3** (missing=0 tapi tetap dibagi 3)
 
 
 
 
 
1565
  """)
1566
 
1567
  state_df = gr.State(None)
 
1588
  gr.Markdown("## Ringkasan (Jenis + Keseluruhan) β€” Pop/Target33.88/Terkumpul/Coverage + Penyesuaian")
1589
  out_summary = gr.DataFrame(interactive=False)
1590
 
1591
+ gr.Markdown("## Agregat Wilayah (Keseluruhan) β€” FIX avg3 (Skor Absolut)")
1592
  out_agg_total = gr.DataFrame(interactive=False)
1593
 
1594
  gr.Markdown("## Agregat Wilayah Γ— Jenis β€” (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
1595
  out_agg_jenis = gr.DataFrame(interactive=False)
1596
 
1597
+ gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
1598
  out_detail = gr.DataFrame(interactive=False)
1599
 
1600
  gr.Markdown("## Kecukupan Sampel 33.88% (tanpa angka koma untuk integer)")
1601
  out_verif = gr.DataFrame(interactive=False)
1602
 
1603
+ gr.Markdown("## Bell Curve β€” per Jenis (Skor Absolut Entitas)")
1604
  gr.Markdown("### Perpustakaan Umum")
1605
  bell_umum = gr.Plot(scale=1)
1606
 
 
1638
  outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
1639
  )
1640
 
1641
+ demo.launch()