irhamni commited on
Commit
38d5982
Β·
verified Β·
1 Parent(s): 06d9484

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +273 -226
app.py CHANGED
@@ -903,7 +903,7 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.
903
 
904
  return agg
905
  # ============================================================
906
- # 8) AGREGAT WILAYAH (KESELURUHAN) β€” FIX: avg3 + tampilkan POP/TARGET/GAP per jenis
907
  # ============================================================
908
 
909
  def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
@@ -956,32 +956,69 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_j
956
  Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
957
  )
958
 
959
- # --- tempel POP/TARGET/SAMPEL/GAP per jenis ke agg_total ---
960
  if faktor_wilayah_jenis is not None and not faktor_wilayah_jenis.empty:
961
  fw = faktor_wilayah_jenis.copy()
962
  fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
963
 
964
- # pivot target/pop/n/gap per jenis
965
  piv = fw.pivot_table(
966
  index=["group_key", label_name],
967
  columns="Jenis",
968
- values=["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"],
 
 
 
 
 
 
 
969
  aggfunc="first"
970
  )
971
-
972
- # flatten columns
973
  piv.columns = [f"{v}_{k}" for v, k in piv.columns]
974
  piv = piv.reset_index()
975
 
976
  out = out.merge(piv, on=["group_key", label_name], how="left")
977
 
978
- # rapihin NaN -> 0
979
- for j in ["sekolah","umum","khusus"]:
980
  for basecol in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
981
  c = f"{basecol}_{j}"
982
  if c in out.columns:
983
- if basecol == "pop_total_jenis" or basecol == "target_total_68_jenis" or basecol == "n_jenis" or basecol == "gap_target68_jenis":
984
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
 
986
  # rounding index
987
  for c in [
@@ -995,14 +1032,17 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_j
995
  if c in out.columns:
996
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
997
 
998
- return out
 
 
999
 
 
1000
 
1001
  # ============================================================
1002
- # 9) SUMMARY (PER JENIS) + KESELURUHAN (FIX Γ·3 + selalu tampil 3 jenis)
1003
  # ============================================================
1004
 
1005
- def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
1006
  jenis_list = ["sekolah", "umum", "khusus"]
1007
 
1008
  def _row_default(jenis):
@@ -1010,44 +1050,92 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
1010
  "Jenis": jenis,
1011
  "Jumlah_Wilayah": 0,
1012
  "Total_Perpus": 0,
 
 
 
 
 
 
 
 
1013
  "Rata2_sub_koleksi": 0.0,
1014
  "Rata2_sub_sdm": 0.0,
1015
  "Rata2_sub_pelayanan": 0.0,
1016
  "Rata2_sub_pengelolaan": 0.0,
1017
  "Rata2_dim_kepatuhan": 0.0,
1018
  "Rata2_dim_kinerja": 0.0,
 
 
 
1019
  "Indeks_Final_Disesuaikan_0_100": 0.0,
 
1020
  }
1021
 
1022
  rows_by_jenis = {j: _row_default(j) for j in jenis_list}
1023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
  if agg_jenis is not None and not agg_jenis.empty:
1025
  for jenis in jenis_list:
1026
  sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
1027
  if sub.empty:
 
 
 
 
 
 
1028
  continue
1029
 
 
 
 
1030
  rows_by_jenis[jenis] = {
1031
  "Jenis": jenis,
1032
  "Jumlah_Wilayah": int(sub.shape[0]),
1033
  "Total_Perpus": int(pd.to_numeric(sub["Jumlah"], errors="coerce").fillna(0).sum()),
 
 
 
 
 
 
1034
  "Rata2_sub_koleksi": float(pd.to_numeric(sub["Rata2_sub_koleksi"], errors="coerce").fillna(0).mean()),
1035
  "Rata2_sub_sdm": float(pd.to_numeric(sub["Rata2_sub_sdm"], errors="coerce").fillna(0).mean()),
1036
  "Rata2_sub_pelayanan": float(pd.to_numeric(sub["Rata2_sub_pelayanan"], errors="coerce").fillna(0).mean()),
1037
  "Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
1038
  "Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
1039
  "Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
1040
- "Indeks_Final_Disesuaikan_0_100": float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean()),
 
 
 
1041
  }
1042
 
1043
  rows = [rows_by_jenis[j] for j in jenis_list]
1044
 
1045
- final_all = (
1046
- float(rows_by_jenis["sekolah"]["Indeks_Final_Disesuaikan_0_100"])
1047
- + float(rows_by_jenis["umum"]["Indeks_Final_Disesuaikan_0_100"])
1048
- + float(rows_by_jenis["khusus"]["Indeks_Final_Disesuaikan_0_100"])
1049
- ) / 3.0
1050
-
1051
  def _avg3(field):
1052
  return (
1053
  float(rows_by_jenis["sekolah"][field])
@@ -1055,34 +1143,59 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
1055
  + float(rows_by_jenis["khusus"][field])
1056
  ) / 3.0
1057
 
 
 
 
1058
  total_perpus_all = int(rows_by_jenis["sekolah"]["Total_Perpus"] + rows_by_jenis["umum"]["Total_Perpus"] + rows_by_jenis["khusus"]["Total_Perpus"])
1059
  jumlah_wilayah_all = int(agg_total.shape[0]) if (agg_total is not None and not agg_total.empty) else int(max(rows_by_jenis["sekolah"]["Jumlah_Wilayah"], rows_by_jenis["umum"]["Jumlah_Wilayah"], rows_by_jenis["khusus"]["Jumlah_Wilayah"]))
1060
 
 
 
 
 
 
 
1061
  rows.append({
1062
  "Jenis": "keseluruhan",
1063
  "Jumlah_Wilayah": jumlah_wilayah_all,
1064
  "Total_Perpus": total_perpus_all,
 
 
 
 
 
 
1065
  "Rata2_sub_koleksi": _avg3("Rata2_sub_koleksi"),
1066
  "Rata2_sub_sdm": _avg3("Rata2_sub_sdm"),
1067
  "Rata2_sub_pelayanan": _avg3("Rata2_sub_pelayanan"),
1068
  "Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
1069
  "Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
1070
  "Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
 
 
1071
  "Indeks_Final_Disesuaikan_0_100": final_all,
 
1072
  })
1073
 
1074
  out = pd.DataFrame(rows)
1075
 
 
1076
  for c in [
1077
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1078
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
1079
  ]:
1080
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
1081
 
1082
- out["Indeks_Final_Disesuaikan_0_100"] = pd.to_numeric(out["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0.0).round(2)
 
1083
 
1084
- return out
 
 
1085
 
 
 
 
1086
 
1087
  # ============================================================
1088
  # 10) DETAIL ENTITAS: Final menempel dari agg_total (wilayah)
@@ -1287,75 +1400,29 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
1287
 
1288
 
1289
  # ============================================================
1290
- # 13) KPI DASHBOARD (PATCH: cakupan 68% PER JENIS -> avg3)
1291
  # ============================================================
1292
 
1293
- def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None):
1294
- def _get_final(j):
1295
  sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1296
  if sub.empty:
1297
  return 0.0
1298
- return float(pd.to_numeric(sub["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
1299
-
1300
- final_sekolah = _get_final("sekolah")
1301
- final_umum = _get_final("umum")
1302
- final_khusus = _get_final("khusus")
1303
- final_all = (final_sekolah + final_umum + final_khusus) / 3.0
1304
-
1305
- def _get_dasar(j):
1306
- if agg_jenis is None or agg_jenis.empty:
1307
- return 0.0
1308
- sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == j]
1309
- if sub.empty:
1310
- return 0.0
1311
- return float(pd.to_numeric(sub["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0).mean())
1312
 
1313
- dasar_sekolah = _get_dasar("sekolah")
1314
- dasar_umum = _get_dasar("umum")
1315
- dasar_khusus = _get_dasar("khusus")
1316
- dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
1317
 
1318
- # === PATCH: cakupan dihitung PER JENIS (total n_jenis / total target_jenis), lalu avg3 ===
1319
- cakupan_pct = 0.0
1320
- faktor_mean = 1.0
1321
- if faktor_wilayah_jenis is not None and (not faktor_wilayah_jenis.empty):
1322
- fw = faktor_wilayah_jenis.copy()
1323
- fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
1324
-
1325
- factors = []
1326
- for j in ["sekolah","umum","khusus"]:
1327
- sub = fw[fw["Jenis"] == j]
1328
- if sub.empty:
1329
- factors.append(0.0)
1330
- continue
1331
-
1332
- n_sum = float(pd.to_numeric(sub.get("n_jenis", 0), errors="coerce").fillna(0).sum())
1333
- t_sum = float(pd.to_numeric(sub.get("target_total_68_jenis", 0), errors="coerce").fillna(0).sum())
1334
- f = min(n_sum / t_sum, 1.0) if (t_sum and t_sum > 0) else 0.0
1335
- factors.append(f)
1336
-
1337
- cakupan_pct = (sum(factors) / 3.0) * 100.0
1338
-
1339
- # faktor_mean juga dibuat avg3 dari faktor total per jenis (opsional)
1340
- # kalau mau: mean faktor per-wilayahΓ—jenis
1341
- faktor_mean = float(pd.to_numeric(fw.get("faktor_penyesuaian_jenis", 1.0), errors="coerce").fillna(1.0).mean())
1342
-
1343
- dampak = final_all - dasar_all
1344
-
1345
- return {
1346
- "final_all": final_all,
1347
- "dasar_all": dasar_all,
1348
- "cakupan_pct": cakupan_pct,
1349
- "faktor_mean": faktor_mean,
1350
- "dampak": dampak
1351
- }
1352
 
1353
 
1354
  def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None) -> str:
 
 
1355
  if summary_jenis is None or summary_jenis.empty:
1356
  return ""
1357
 
1358
- k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis, faktor_wilayah_jenis=faktor_wilayah_jenis)
1359
 
1360
  def fmt(x, nd=2):
1361
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
@@ -1373,131 +1440,42 @@ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg
1373
  <div style="font-size:26px; font-weight:700;">{fmt(k["dasar_all"],2)}</div>
1374
  <div style="opacity:0.7;">Rata-rata 3 jenis (tetap Γ·3)</div>
1375
  </div>
1376
-
1377
- <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:220px;">
1378
- <div style="opacity:0.8;">Cakupan Sampel (berdasarkan target 68%)</div>
1379
- <div style="font-size:26px; font-weight:700;">{fmt(k["cakupan_pct"],0)}%</div>
1380
- <div style="opacity:0.7;">Rumus: min(total_terkumpul/target_68, 1.0)</div>
1381
- </div>
1382
-
1383
- <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:220px;">
1384
- <div style="opacity:0.8;">Penyesuaian Nilai (rata-rata)</div>
1385
- <div style="font-size:26px; font-weight:700;">{fmt(k["dampak"],2)} poin</div>
1386
- <div style="opacity:0.7;">Faktor penyesuaian (mean): {fmt(k["faktor_mean"],3)}</div>
1387
- </div>
1388
  </div>
1389
  """.strip()
1390
 
1391
-
1392
  # ============================================================
1393
  # 14) LLM + WORD
1394
  # ============================================================
1395
 
1396
- _HF_CLIENT = None
1397
-
1398
- def get_llm_client():
1399
- global _HF_CLIENT
1400
- if _HF_CLIENT is not None:
1401
- return _HF_CLIENT
1402
- try:
1403
- _HF_CLIENT = InferenceClient(model=LLM_MODEL_NAME, token=HF_TOKEN) if HF_TOKEN else InferenceClient(model=LLM_MODEL_NAME)
1404
- return _HF_CLIENT
1405
- except Exception:
1406
- _HF_CLIENT = None
1407
- return None
1408
-
1409
- def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_total: pd.DataFrame, wilayah: str, kew: str) -> str:
1410
- lines = []
1411
- lines.append(f"Wilayah filter: {wilayah}")
1412
- lines.append(f"Kewenangan: {kew}")
1413
- lines.append("Metode: Indeks dasar dihitung per entitas (Yeo-Johnson + MinMax nasional per indikator), lalu diagregasi per wilayahΓ—jenis. Setelah itu dilakukan penyesuaian berbasis kecukupan sampel minimum 68% pada level wilayah.")
1414
- lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0). Faktor wilayah dipakai untuk semua jenis.")
1415
- lines.append("Rumus keseluruhan wilayah (FIX): nilai keseluruhan wilayah diambil dari rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3).")
1416
-
1417
- if summary_jenis is not None and not summary_jenis.empty:
1418
- lines.append("\nRingkasan (jenis + keseluruhan):")
1419
- for _, r in summary_jenis.iterrows():
1420
- lines.append(
1421
- f"- {r['Jenis']}: wilayah={int(r['Jumlah_Wilayah'])}, total_perpus={int(r['Total_Perpus'])}, "
1422
- f"dim_kepatuhan={float(r['Rata2_dim_kepatuhan']):.3f}, dim_kinerja={float(r['Rata2_dim_kinerja']):.3f}, "
1423
- f"final_disesuaikan={float(r['Indeks_Final_Disesuaikan_0_100']):.2f}"
1424
- )
1425
-
1426
- if agg_total is not None and not agg_total.empty:
1427
- label_col = "Kab/Kota" if "Kab/Kota" in agg_total.columns else ("Provinsi" if "Provinsi" in agg_total.columns else None)
1428
- lines.append("\nTop 5 wilayah (Final disesuaikan tertinggi):")
1429
- top = agg_total.sort_values("Indeks_Final_Wilayah_0_100", ascending=False).head(5)
1430
- for _, r in top.iterrows():
1431
- wl = r.get(label_col, "(wilayah)") if label_col else "(wilayah)"
1432
- lines.append(f"- {wl}: Final={float(r['Indeks_Final_Wilayah_0_100']):.2f} | Faktor={float(r.get('faktor_penyesuaian', 1.0)):.3f} | total={int(r.get('n_total', 0))}")
1433
-
1434
- if verif_total is not None and not verif_total.empty and "GAP_Ke_Target68_Total" in verif_total.columns:
1435
- lines.append("\nTop 5 wilayah (GAP ke target 68% terbesar):")
1436
- tmp = verif_total.sort_values("GAP_Ke_Target68_Total", ascending=False).head(5)
1437
- name_col = "Kab/Kota" if "Kab/Kota" in tmp.columns else ("Provinsi" if "Provinsi" in tmp.columns else None)
1438
- for _, r in tmp.iterrows():
1439
- nm = r.get(name_col, "") if name_col else ""
1440
- lines.append(f"- {nm}: GAP={int(r['GAP_Ke_Target68_Total'])}")
1441
-
1442
- return "\n".join(lines)
1443
-
1444
- def generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah, kew):
1445
- ctx = build_context(summary_jenis, agg_total, verif_total, wilayah, kew)
1446
- client = get_llm_client()
1447
- if client is None or not USE_LLM:
1448
- return "Analisis otomatis (LLM) tidak tersedia. Pastikan token HuggingFace tersedia dan model bisa diakses."
1449
-
1450
- system_prompt = (
1451
- "Anda adalah analis kebijakan perpustakaan dan literasi di Indonesia. "
1452
- "Tugas Anda menyusun analisis berbasis data IPLM secara formal, tajam, dan operasional."
1453
- )
1454
- user_prompt = f"""
1455
- DATA RINGKAS IPLM (PENYESUAIAN BERBASIS KECUKUPAN SAMPEL 68% DI LEVEL WILAYAH):
1456
-
1457
- {ctx}
1458
-
1459
- TULISKAN ANALISIS BAHASA INDONESIA FORMAL, STRUKTUR:
1460
- 1) Gambaran umum hasil wilayah (1 paragraf).
1461
- 2) Analisis jenis sekolah, umum, khusus serta indeks keseluruhan (2 paragraf).
1462
- 3) Penjelasan makna penyesuaian berbasis kecukupan sampel (1 paragraf, bahasa netral non-konfrontatif).
1463
- 4) Rekomendasi program 3–5 tahun (2 paragraf, konkret dan dapat dieksekusi).
1464
-
1465
- ATURAN:
1466
- - Jangan memakai label eksplisit "rendah/sedang/tinggi".
1467
- - Gunakan frasa netral: "memerlukan penguatan", "memerlukan konsolidasi", dsb.
1468
- - Fokus pada Indeks FINAL WILAYAH (disesuaikan), bukan individu.
1469
- """
1470
- try:
1471
- resp = client.chat_completion(
1472
- model=LLM_MODEL_NAME,
1473
- messages=[{"role":"system","content":system_prompt},{"role":"user","content":user_prompt}],
1474
- max_tokens=1100,
1475
- temperature=0.25,
1476
- top_p=0.9,
1477
- )
1478
- text = resp.choices[0].message.content.strip()
1479
- return text if text else "LLM mengembalikan respon kosong."
1480
- except Exception as e:
1481
- return f"⚠️ Error saat memanggil LLM: {repr(e)}"
1482
-
1483
- def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_text):
1484
  doc = Document()
1485
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1486
 
1487
  doc.add_heading("Ringkasan Dashboard", level=2)
1488
 
1489
- k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
1490
- doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k['final_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1491
- doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1492
- doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
1493
- doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
 
 
 
 
 
 
1494
 
1495
- doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
 
 
 
1496
  show = summary_jenis.copy()
 
1497
  preferred = [
1498
- "Jenis","Jumlah_Wilayah","Total_Perpus",
1499
- "Rata2_dim_kepatuhan","Rata2_dim_kinerja",
1500
- "Indeks_Final_Disesuaikan_0_100"
 
1501
  ]
1502
  show = show[[c for c in preferred if c in show.columns]]
1503
 
@@ -1512,15 +1490,17 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
1512
  v = row[c]
1513
  if pd.isna(v):
1514
  cells[i].text = ""
 
 
1515
  elif isinstance(v, (float, np.floating)):
1516
- if "Indeks" in c:
1517
  cells[i].text = f"{float(v):.2f}"
1518
  elif "Rata2_" in c:
1519
  cells[i].text = f"{float(v):.3f}"
 
 
1520
  else:
1521
- cells[i].text = f"{float(v):.0f}"
1522
- elif isinstance(v, (int, np.integer)):
1523
- cells[i].text = str(int(v))
1524
  else:
1525
  cells[i].text = str(v)
1526
 
@@ -1568,7 +1548,9 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1568
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1569
  return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1570
 
 
1571
  # FILTER ANALISIS (df_all)
 
1572
  df = df_all.copy()
1573
  if prov_value and prov_value != "(Semua)":
1574
  df = df[df["PROV_DISP"] == prov_value]
@@ -1580,21 +1562,51 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1580
  if df.empty:
1581
  return _empty_outputs("Tidak ada data untuk filter ini.")
1582
 
1583
- # ==== PIPELINE BARU (PATCH: faktor 68% PER JENIS) ====
1584
- faktor_wilayah_jenis = build_faktor_wilayah_jenis(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1585
- agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah_jenis, kew_value or "(Semua)")
1586
- agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah_jenis, kew_value or "(Semua)")
1587
- summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1588
-
1589
- # verif_total: kalau kamu masih mau tabel verifikasi, sekarang logikanya sebaiknya per jenis
1590
- # tapi karena request kamu fokus ringkasan + agg_total, verif_total bisa tetap pakai "ringkas" dari faktor_wilayah_jenis
1591
- # (opsional) kita bikin verif_total sederhana dari faktor_wilayah_jenis:
1592
- verif_total = build_verif_jenis(faktor_wilayah_jenis, kew_value or "(Semua)")
1593
- detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
1594
 
 
 
 
 
 
1595
 
1596
- # ====== UPDATE SESUAI PERMINTAAN (UI ONLY) ======
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1597
  # Tabel Agregat Wilayah Γ— Jenis cukup sampai Indeks_Dasar_Agregat_0_100
 
1598
  if agg_jenis_full is None or agg_jenis_full.empty:
1599
  agg_jenis_view = agg_jenis_full
1600
  else:
@@ -1605,14 +1617,16 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1605
  label_name,
1606
  "Jenis",
1607
  "Jumlah",
1608
- "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1609
- "Rata2_dim_kepatuhan","Rata2_dim_kinerja",
1610
  "Indeks_Dasar_Agregat_0_100",
1611
  ]
1612
  cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1613
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1614
 
 
1615
  # FILTER RAW DOWNLOAD (df_raw)
 
1616
  raw = df_raw.copy()
1617
  if prov_value and prov_value != "(Semua)":
1618
  raw = raw[raw["PROV_DISP"] == prov_value]
@@ -1621,7 +1635,9 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1621
  if kew_value and kew_value != "(Semua)":
1622
  raw = raw[raw["KEW_NORM"] == kew_value]
1623
 
 
1624
  # Bell curve per JENIS (per entitas)
 
1625
  if detail_view is None or detail_view.empty:
1626
  fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1627
  fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
@@ -1633,15 +1649,32 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1633
 
1634
  def _fig_jenis_ent(jenis_key: str, judul: str):
1635
  d = detail_view[detail_view["Jenis"].astype(str).str.lower() == jenis_key].copy()
1636
- return _make_bell_curve(d, xcol=xcol_ent, title=judul, label_col=label_col_e, hover_cols=hover_cols_e, min_points=2)
 
 
 
 
 
 
 
1637
 
1638
  fig_sekolah = _fig_jenis_ent("sekolah", "Bell Curve β€” Jenis: Sekolah (Indeks per Entitas)")
1639
  fig_umum = _fig_jenis_ent("umum", "Bell Curve β€” Jenis: Umum (Indeks per Entitas)")
1640
  fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β€” Jenis: Khusus (Indeks per Entitas)")
1641
 
1642
- # KPI markdown (FINAL sumber Ringkasan)
1643
- kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis_full, faktor_wilayah_jenis=faktor_wilayah_jenis)
 
 
 
 
 
 
 
1644
 
 
 
 
1645
  tmpdir = tempfile.mkdtemp()
1646
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
1647
  kab_slug = (_canon(kab_value or "SEMUA").upper() or "SEMUA")
@@ -1660,9 +1693,22 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1660
  verif_total.to_excel(p_verif, index=False)
1661
 
1662
  wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
1663
- analysis_text = generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah_txt, kew_value or "(Semua)")
1664
 
1665
- word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, agg_jenis_full, analysis_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1666
 
1667
  msg = (
1668
  f"βœ… Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
@@ -1699,9 +1745,9 @@ def ui_load(force=False):
1699
  prov_vals = [v for v in prov_vals if v and v.strip()]
1700
  prov_choices = ["(Semua)"] + sorted(set(prov_vals))
1701
 
1702
- kab_choices = ["(Semua)"] + sorted([x for x in df_all["KAB_DISP"].dropna().unique().tolist() if x])
1703
- kew_choices = ["(Semua)"] + sorted([x for x in df_all["KEW_NORM"].dropna().unique().tolist() if x])
1704
- default_kew = "PROVINSI" if "PROVINSI" in kew_choices else ("KAB/KOTA" if "KAB/KOTA" in kew_choices else "(Semua)")
1705
 
1706
  return (
1707
  df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
@@ -1734,13 +1780,13 @@ with gr.Blocks() as demo:
1734
  **FIX UTAMA (konsistensi nilai):**
1735
  - **Agregat Wilayah (Keseluruhan) = rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3)**
1736
  - Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
1737
- - KPI FINAL dashboard sumber dari Ringkasan
 
1738
  - Download Data Mentah = RAW hasil filter
1739
- - Kecukupan Sampel 68%: tanpa angka koma
1740
 
1741
  **UPDATE (tampilan):**
1742
- - target_total_68 & pop_total di faktor_wilayah = bilangan bulat
1743
- - coverage_total_% = 2 desimal
1744
  - Tabel "Agregat Wilayah Γ— Jenis" ditampilkan hanya sampai Indeks_Dasar_Agregat_0_100
1745
  """)
1746
 
@@ -1755,20 +1801,21 @@ with gr.Blocks() as demo:
1755
 
1756
  with gr.Row():
1757
  dd_prov = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)")
1758
- dd_kab = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)")
1759
- dd_kew = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)")
1760
 
1761
  dd_prov.change(fn=on_prov_change, inputs=[dd_prov], outputs=dd_kab)
1762
 
1763
  run_btn = gr.Button("Jalankan Perhitungan")
1764
  msg_out = gr.Markdown()
1765
 
 
1766
  kpi_out = gr.Markdown()
1767
 
1768
- gr.Markdown("## Ringkasan (Jenis + Keseluruhan)")
1769
  out_summary = gr.DataFrame(interactive=False)
1770
 
1771
- gr.Markdown("## Agregat Wilayah (Keseluruhan) β€” FIX: avg3 dari 3 jenis")
1772
  out_agg_total = gr.DataFrame(interactive=False)
1773
 
1774
  gr.Markdown("## Agregat Wilayah Γ— Jenis (Sekolah, Umum, Khusus) β€” (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
@@ -1795,10 +1842,10 @@ with gr.Blocks() as demo:
1795
 
1796
  with gr.Row():
1797
  dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1798
- dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1799
- dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1800
- dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1801
- dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
1802
 
1803
  run_btn.click(
1804
  fn=run_calc,
 
903
 
904
  return agg
905
  # ============================================================
906
+ # 8) AGREGAT WILAYAH (KESELURUHAN) β€” FIX: avg3 + tampilkan POP/TARGET/N per jenis
907
  # ============================================================
908
 
909
  def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
 
956
  Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
957
  )
958
 
959
+ # --- tempel POP/TARGET/N/COVERAGE/FAKTOR per jenis ke agg_total ---
960
  if faktor_wilayah_jenis is not None and not faktor_wilayah_jenis.empty:
961
  fw = faktor_wilayah_jenis.copy()
962
  fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
963
 
964
+ # pivot per jenis
965
  piv = fw.pivot_table(
966
  index=["group_key", label_name],
967
  columns="Jenis",
968
+ values=[
969
+ "pop_total_jenis",
970
+ "target_total_68_jenis",
971
+ "n_jenis",
972
+ "coverage_jenis_%",
973
+ "faktor_penyesuaian_jenis",
974
+ "gap_target68_jenis"
975
+ ],
976
  aggfunc="first"
977
  )
 
 
978
  piv.columns = [f"{v}_{k}" for v, k in piv.columns]
979
  piv = piv.reset_index()
980
 
981
  out = out.merge(piv, on=["group_key", label_name], how="left")
982
 
983
+ # rapihin NaN -> 0 + tipe
984
+ for j in ["sekolah", "umum", "khusus"]:
985
  for basecol in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
986
  c = f"{basecol}_{j}"
987
  if c in out.columns:
988
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
989
+
990
+ c_cov = f"coverage_jenis_%_{j}"
991
+ if c_cov in out.columns:
992
+ out[c_cov] = pd.to_numeric(out[c_cov], errors="coerce").fillna(0.0).round(2)
993
+
994
+ c_fac = f"faktor_penyesuaian_jenis_{j}"
995
+ if c_fac in out.columns:
996
+ out[c_fac] = pd.to_numeric(out[c_fac], errors="coerce").fillna(0.0).round(3)
997
+
998
+ # === TOTAL UPDATE (sesuai data POP yang ada) ===
999
+ # Total Pop dan Target = sum 3 jenis (sekolah+umum+khusus)
1000
+ def _sum3(a, b, c):
1001
+ return (
1002
+ pd.to_numeric(a, errors="coerce").fillna(0)
1003
+ + pd.to_numeric(b, errors="coerce").fillna(0)
1004
+ + pd.to_numeric(c, errors="coerce").fillna(0)
1005
+ )
1006
+
1007
+ if ("pop_total_jenis_sekolah" in out.columns) and ("pop_total_jenis_umum" in out.columns) and ("pop_total_jenis_khusus" in out.columns):
1008
+ out["Pop_Total_Update"] = _sum3(out["pop_total_jenis_sekolah"], out["pop_total_jenis_umum"], out["pop_total_jenis_khusus"]).round(0).astype(int)
1009
+
1010
+ if ("target_total_68_jenis_sekolah" in out.columns) and ("target_total_68_jenis_umum" in out.columns) and ("target_total_68_jenis_khusus" in out.columns):
1011
+ out["Target68_Total_Update"] = _sum3(out["target_total_68_jenis_sekolah"], out["target_total_68_jenis_umum"], out["target_total_68_jenis_khusus"]).round(0).astype(int)
1012
+
1013
+ if ("n_jenis_sekolah" in out.columns) and ("n_jenis_umum" in out.columns) and ("n_jenis_khusus" in out.columns):
1014
+ out["Terkumpul_Total_Update"] = _sum3(out["n_jenis_sekolah"], out["n_jenis_umum"], out["n_jenis_khusus"]).round(0).astype(int)
1015
+
1016
+ # coverage_total_update = min(terkumpul/target,1)*100
1017
+ if ("Terkumpul_Total_Update" in out.columns) and ("Target68_Total_Update" in out.columns):
1018
+ den = pd.to_numeric(out["Target68_Total_Update"], errors="coerce").fillna(0).astype(float)
1019
+ num = pd.to_numeric(out["Terkumpul_Total_Update"], errors="coerce").fillna(0).astype(float)
1020
+ cov = np.where(den > 0, np.minimum(num / den, 1.0) * 100.0, 0.0)
1021
+ out["Coverage_Target68_Total_Update_%"] = pd.Series(cov).round(2)
1022
 
1023
  # rounding index
1024
  for c in [
 
1032
  if c in out.columns:
1033
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1034
 
1035
+ # n_total integer
1036
+ if "n_total" in out.columns:
1037
+ out["n_total"] = pd.to_numeric(out["n_total"], errors="coerce").fillna(0).round(0).astype(int)
1038
 
1039
+ return out
1040
 
1041
  # ============================================================
1042
+ # 9) SUMMARY (PER JENIS) + KESELURUHAN (FIX Γ·3 + tambah cakupan & penyesuaian)
1043
  # ============================================================
1044
 
1045
+ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None):
1046
  jenis_list = ["sekolah", "umum", "khusus"]
1047
 
1048
  def _row_default(jenis):
 
1050
  "Jenis": jenis,
1051
  "Jumlah_Wilayah": 0,
1052
  "Total_Perpus": 0,
1053
+
1054
+ # POP/TARGET/N per jenis (sum nasional pada scope filter)
1055
+ "Pop_Total_Jenis": 0,
1056
+ "Target68_Total_Jenis": 0,
1057
+ "Terkumpul_Jenis": 0,
1058
+ "Coverage_Target68_Jenis_%": 0.00,
1059
+
1060
+ # skor
1061
  "Rata2_sub_koleksi": 0.0,
1062
  "Rata2_sub_sdm": 0.0,
1063
  "Rata2_sub_pelayanan": 0.0,
1064
  "Rata2_sub_pengelolaan": 0.0,
1065
  "Rata2_dim_kepatuhan": 0.0,
1066
  "Rata2_dim_kinerja": 0.0,
1067
+
1068
+ # dasar & final + penyesuaian poin
1069
+ "Indeks_Dasar_0_100": 0.0,
1070
  "Indeks_Final_Disesuaikan_0_100": 0.0,
1071
+ "Penyesuaian_Poin": 0.0,
1072
  }
1073
 
1074
  rows_by_jenis = {j: _row_default(j) for j in jenis_list}
1075
 
1076
+ # ===== ambil POP/TARGET/N per jenis dari faktor_wilayah_jenis =====
1077
+ fw_sum = {}
1078
+ if faktor_wilayah_jenis is not None and (not faktor_wilayah_jenis.empty):
1079
+ fw = faktor_wilayah_jenis.copy()
1080
+ fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
1081
+
1082
+ for j in jenis_list:
1083
+ sub = fw[fw["Jenis"] == j].copy()
1084
+ if sub.empty:
1085
+ fw_sum[j] = {"pop": 0, "target": 0, "n": 0, "cov": 0.0}
1086
+ continue
1087
+
1088
+ pop = int(pd.to_numeric(sub.get("pop_total_jenis", 0), errors="coerce").fillna(0).sum())
1089
+ target = int(pd.to_numeric(sub.get("target_total_68_jenis", 0), errors="coerce").fillna(0).sum())
1090
+ n = int(pd.to_numeric(sub.get("n_jenis", 0), errors="coerce").fillna(0).sum())
1091
+
1092
+ cov = 0.0
1093
+ if target > 0:
1094
+ cov = float(min(n / float(target), 1.0) * 100.0)
1095
+
1096
+ fw_sum[j] = {"pop": pop, "target": target, "n": n, "cov": cov}
1097
+
1098
+ # ===== isi ringkasan dari agg_jenis =====
1099
  if agg_jenis is not None and not agg_jenis.empty:
1100
  for jenis in jenis_list:
1101
  sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
1102
  if sub.empty:
1103
+ # tetap isi pop/target/n kalau ada
1104
+ if jenis in fw_sum:
1105
+ rows_by_jenis[jenis]["Pop_Total_Jenis"] = fw_sum[jenis]["pop"]
1106
+ rows_by_jenis[jenis]["Target68_Total_Jenis"] = fw_sum[jenis]["target"]
1107
+ rows_by_jenis[jenis]["Terkumpul_Jenis"] = fw_sum[jenis]["n"]
1108
+ rows_by_jenis[jenis]["Coverage_Target68_Jenis_%"] = fw_sum[jenis]["cov"]
1109
  continue
1110
 
1111
+ dasar = float(pd.to_numeric(sub["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0).mean())
1112
+ final = float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean())
1113
+
1114
  rows_by_jenis[jenis] = {
1115
  "Jenis": jenis,
1116
  "Jumlah_Wilayah": int(sub.shape[0]),
1117
  "Total_Perpus": int(pd.to_numeric(sub["Jumlah"], errors="coerce").fillna(0).sum()),
1118
+
1119
+ "Pop_Total_Jenis": int(fw_sum.get(jenis, {}).get("pop", 0)),
1120
+ "Target68_Total_Jenis": int(fw_sum.get(jenis, {}).get("target", 0)),
1121
+ "Terkumpul_Jenis": int(fw_sum.get(jenis, {}).get("n", 0)),
1122
+ "Coverage_Target68_Jenis_%": float(fw_sum.get(jenis, {}).get("cov", 0.0)),
1123
+
1124
  "Rata2_sub_koleksi": float(pd.to_numeric(sub["Rata2_sub_koleksi"], errors="coerce").fillna(0).mean()),
1125
  "Rata2_sub_sdm": float(pd.to_numeric(sub["Rata2_sub_sdm"], errors="coerce").fillna(0).mean()),
1126
  "Rata2_sub_pelayanan": float(pd.to_numeric(sub["Rata2_sub_pelayanan"], errors="coerce").fillna(0).mean()),
1127
  "Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
1128
  "Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
1129
  "Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
1130
+
1131
+ "Indeks_Dasar_0_100": dasar,
1132
+ "Indeks_Final_Disesuaikan_0_100": final,
1133
+ "Penyesuaian_Poin": float(final - dasar),
1134
  }
1135
 
1136
  rows = [rows_by_jenis[j] for j in jenis_list]
1137
 
1138
+ # ===== keseluruhan = avg3 tetap Γ·3 (missing=0) =====
 
 
 
 
 
1139
  def _avg3(field):
1140
  return (
1141
  float(rows_by_jenis["sekolah"][field])
 
1143
  + float(rows_by_jenis["khusus"][field])
1144
  ) / 3.0
1145
 
1146
+ final_all = _avg3("Indeks_Final_Disesuaikan_0_100")
1147
+ dasar_all = _avg3("Indeks_Dasar_0_100")
1148
+
1149
  total_perpus_all = int(rows_by_jenis["sekolah"]["Total_Perpus"] + rows_by_jenis["umum"]["Total_Perpus"] + rows_by_jenis["khusus"]["Total_Perpus"])
1150
  jumlah_wilayah_all = int(agg_total.shape[0]) if (agg_total is not None and not agg_total.empty) else int(max(rows_by_jenis["sekolah"]["Jumlah_Wilayah"], rows_by_jenis["umum"]["Jumlah_Wilayah"], rows_by_jenis["khusus"]["Jumlah_Wilayah"]))
1151
 
1152
+ # Total POP/TARGET/N keseluruhan = sum3 (bukan avg3), karena ini memang total cakupan
1153
+ pop_all = int(rows_by_jenis["sekolah"]["Pop_Total_Jenis"] + rows_by_jenis["umum"]["Pop_Total_Jenis"] + rows_by_jenis["khusus"]["Pop_Total_Jenis"])
1154
+ target_all = int(rows_by_jenis["sekolah"]["Target68_Total_Jenis"] + rows_by_jenis["umum"]["Target68_Total_Jenis"] + rows_by_jenis["khusus"]["Target68_Total_Jenis"])
1155
+ n_all = int(rows_by_jenis["sekolah"]["Terkumpul_Jenis"] + rows_by_jenis["umum"]["Terkumpul_Jenis"] + rows_by_jenis["khusus"]["Terkumpul_Jenis"])
1156
+ cov_all = float(min(n_all / float(target_all), 1.0) * 100.0) if target_all > 0 else 0.0
1157
+
1158
  rows.append({
1159
  "Jenis": "keseluruhan",
1160
  "Jumlah_Wilayah": jumlah_wilayah_all,
1161
  "Total_Perpus": total_perpus_all,
1162
+
1163
+ "Pop_Total_Jenis": pop_all,
1164
+ "Target68_Total_Jenis": target_all,
1165
+ "Terkumpul_Jenis": n_all,
1166
+ "Coverage_Target68_Jenis_%": cov_all,
1167
+
1168
  "Rata2_sub_koleksi": _avg3("Rata2_sub_koleksi"),
1169
  "Rata2_sub_sdm": _avg3("Rata2_sub_sdm"),
1170
  "Rata2_sub_pelayanan": _avg3("Rata2_sub_pelayanan"),
1171
  "Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
1172
  "Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
1173
  "Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
1174
+
1175
+ "Indeks_Dasar_0_100": dasar_all,
1176
  "Indeks_Final_Disesuaikan_0_100": final_all,
1177
+ "Penyesuaian_Poin": float(final_all - dasar_all),
1178
  })
1179
 
1180
  out = pd.DataFrame(rows)
1181
 
1182
+ # rounding
1183
  for c in [
1184
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1185
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
1186
  ]:
1187
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
1188
 
1189
+ for c in ["Indeks_Dasar_0_100","Indeks_Final_Disesuaikan_0_100","Penyesuaian_Poin"]:
1190
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1191
 
1192
+ # integer columns
1193
+ for c in ["Jumlah_Wilayah","Total_Perpus","Pop_Total_Jenis","Target68_Total_Jenis","Terkumpul_Jenis"]:
1194
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1195
 
1196
+ out["Coverage_Target68_Jenis_%"] = pd.to_numeric(out["Coverage_Target68_Jenis_%"], errors="coerce").fillna(0.0).round(2)
1197
+
1198
+ return out
1199
 
1200
  # ============================================================
1201
  # 10) DETAIL ENTITAS: Final menempel dari agg_total (wilayah)
 
1400
 
1401
 
1402
  # ============================================================
1403
+ # 13) KPI DASHBOARD (PATCH: hanya FINAL & DASAR)
1404
  # ============================================================
1405
 
1406
+ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
1407
+ def _get_val(j, col):
1408
  sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1409
  if sub.empty:
1410
  return 0.0
1411
+ return float(pd.to_numeric(sub[col], errors="coerce").fillna(0).iloc[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
1412
 
1413
+ final_all = _get_val("keseluruhan", "Indeks_Final_Disesuaikan_0_100")
1414
+ dasar_all = _get_val("keseluruhan", "Indeks_Dasar_0_100")
 
 
1415
 
1416
+ return {"final_all": final_all, "dasar_all": dasar_all}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
 
1418
 
1419
  def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None) -> str:
1420
+ # faktor_wilayah_jenis tetap diterima supaya pemanggil run_calc tidak perlu diubah banyak,
1421
+ # tapi tidak dipakai lagi di KPI dashboard (dipindah ke tabel ringkasan).
1422
  if summary_jenis is None or summary_jenis.empty:
1423
  return ""
1424
 
1425
+ k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
1426
 
1427
  def fmt(x, nd=2):
1428
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
 
1440
  <div style="font-size:26px; font-weight:700;">{fmt(k["dasar_all"],2)}</div>
1441
  <div style="opacity:0.7;">Rata-rata 3 jenis (tetap Γ·3)</div>
1442
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
1443
  </div>
1444
  """.strip()
1445
 
 
1446
  # ============================================================
1447
  # 14) LLM + WORD
1448
  # ============================================================
1449
 
1450
+ _def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1451
  doc = Document()
1452
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1453
 
1454
  doc.add_heading("Ringkasan Dashboard", level=2)
1455
 
1456
+ # KPI hanya FINAL & DASAR (cakupan + penyesuaian dipindah ke tabel ringkasan)
1457
+ k_final = 0.0
1458
+ k_dasar = 0.0
1459
+ try:
1460
+ if summary_jenis is not None and (not summary_jenis.empty):
1461
+ row_all = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == "keseluruhan"]
1462
+ if not row_all.empty:
1463
+ k_final = float(pd.to_numeric(row_all["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
1464
+ k_dasar = float(pd.to_numeric(row_all["Indeks_Dasar_0_100"], errors="coerce").fillna(0).iloc[0])
1465
+ except Exception:
1466
+ k_final, k_dasar = 0.0, 0.0
1467
 
1468
+ doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k_final:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1469
+ doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k_dasar:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1470
+
1471
+ doc.add_paragraph("Ringkasan (Jenis + Keseluruhan) β€” termasuk Pop/Target68/Terkumpul/Coverage + Penyesuaian Poin:")
1472
  show = summary_jenis.copy()
1473
+
1474
  preferred = [
1475
+ "Jenis", "Jumlah_Wilayah", "Total_Perpus",
1476
+ "Pop_Total_Jenis", "Target68_Total_Jenis", "Terkumpul_Jenis", "Coverage_Target68_Jenis_%",
1477
+ "Rata2_dim_kepatuhan", "Rata2_dim_kinerja",
1478
+ "Indeks_Dasar_0_100", "Indeks_Final_Disesuaikan_0_100", "Penyesuaian_Poin"
1479
  ]
1480
  show = show[[c for c in preferred if c in show.columns]]
1481
 
 
1490
  v = row[c]
1491
  if pd.isna(v):
1492
  cells[i].text = ""
1493
+ elif isinstance(v, (int, np.integer)):
1494
+ cells[i].text = str(int(v))
1495
  elif isinstance(v, (float, np.floating)):
1496
+ if "Coverage" in c:
1497
  cells[i].text = f"{float(v):.2f}"
1498
  elif "Rata2_" in c:
1499
  cells[i].text = f"{float(v):.3f}"
1500
+ elif "Indeks" in c or "Penyesuaian" in c:
1501
+ cells[i].text = f"{float(v):.2f}"
1502
  else:
1503
+ cells[i].text = f"{float(v):.2f}"
 
 
1504
  else:
1505
  cells[i].text = str(v)
1506
 
 
1548
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1549
  return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1550
 
1551
+ # =========================
1552
  # FILTER ANALISIS (df_all)
1553
+ # =========================
1554
  df = df_all.copy()
1555
  if prov_value and prov_value != "(Semua)":
1556
  df = df[df["PROV_DISP"] == prov_value]
 
1562
  if df.empty:
1563
  return _empty_outputs("Tidak ada data untuk filter ini.")
1564
 
1565
+ # ==================================================
1566
+ # PIPELINE BARU (FAKTOR 68% PER JENIS)
1567
+ # ==================================================
1568
+ faktor_wilayah_jenis = build_faktor_wilayah_jenis(
1569
+ df,
1570
+ pop_kab,
1571
+ pop_prov,
1572
+ pop_khusus,
1573
+ kew_value or "(Semua)"
1574
+ )
 
1575
 
1576
+ agg_jenis_full = build_agg_wilayah_jenis(
1577
+ df,
1578
+ faktor_wilayah_jenis,
1579
+ kew_value or "(Semua)"
1580
+ )
1581
 
1582
+ agg_total = build_agg_wilayah_total_from_jenis(
1583
+ agg_jenis_full,
1584
+ faktor_wilayah_jenis,
1585
+ kew_value or "(Semua)"
1586
+ )
1587
+
1588
+ summary_jenis = build_summary_per_jenis(
1589
+ agg_jenis_full,
1590
+ agg_total,
1591
+ faktor_wilayah_jenis=faktor_wilayah_jenis
1592
+ )
1593
+
1594
+ verif_total = build_verif_jenis(
1595
+ faktor_wilayah_jenis,
1596
+ kew_value or "(Semua)"
1597
+ )
1598
+
1599
+ detail_view = attach_final_to_detail(
1600
+ df,
1601
+ agg_total,
1602
+ meta,
1603
+ kew_value or "(Semua)"
1604
+ )
1605
+
1606
+ # ==================================================
1607
+ # UPDATE SESUAI PERMINTAAN (UI ONLY)
1608
  # Tabel Agregat Wilayah Γ— Jenis cukup sampai Indeks_Dasar_Agregat_0_100
1609
+ # ==================================================
1610
  if agg_jenis_full is None or agg_jenis_full.empty:
1611
  agg_jenis_view = agg_jenis_full
1612
  else:
 
1617
  label_name,
1618
  "Jenis",
1619
  "Jumlah",
1620
+ "Rata2_sub_koleksi", "Rata2_sub_sdm", "Rata2_sub_pelayanan", "Rata2_sub_pengelolaan",
1621
+ "Rata2_dim_kepatuhan", "Rata2_dim_kinerja",
1622
  "Indeks_Dasar_Agregat_0_100",
1623
  ]
1624
  cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1625
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1626
 
1627
+ # =========================
1628
  # FILTER RAW DOWNLOAD (df_raw)
1629
+ # =========================
1630
  raw = df_raw.copy()
1631
  if prov_value and prov_value != "(Semua)":
1632
  raw = raw[raw["PROV_DISP"] == prov_value]
 
1635
  if kew_value and kew_value != "(Semua)":
1636
  raw = raw[raw["KEW_NORM"] == kew_value]
1637
 
1638
+ # =========================
1639
  # Bell curve per JENIS (per entitas)
1640
+ # =========================
1641
  if detail_view is None or detail_view.empty:
1642
  fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1643
  fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
 
1649
 
1650
  def _fig_jenis_ent(jenis_key: str, judul: str):
1651
  d = detail_view[detail_view["Jenis"].astype(str).str.lower() == jenis_key].copy()
1652
+ return _make_bell_curve(
1653
+ d,
1654
+ xcol=xcol_ent,
1655
+ title=judul,
1656
+ label_col=label_col_e,
1657
+ hover_cols=hover_cols_e,
1658
+ min_points=2
1659
+ )
1660
 
1661
  fig_sekolah = _fig_jenis_ent("sekolah", "Bell Curve β€” Jenis: Sekolah (Indeks per Entitas)")
1662
  fig_umum = _fig_jenis_ent("umum", "Bell Curve β€” Jenis: Umum (Indeks per Entitas)")
1663
  fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β€” Jenis: Khusus (Indeks per Entitas)")
1664
 
1665
+ # =========================
1666
+ # KPI markdown (FINAL sumber Ringkasan) β€” hanya 2 kartu
1667
+ # =========================
1668
+ kpi_md = build_kpi_markdown(
1669
+ summary_jenis,
1670
+ agg_total,
1671
+ agg_jenis_full,
1672
+ faktor_wilayah_jenis=faktor_wilayah_jenis
1673
+ )
1674
 
1675
+ # =========================
1676
+ # SAVE OUTPUTS (Excel + Word)
1677
+ # =========================
1678
  tmpdir = tempfile.mkdtemp()
1679
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
1680
  kab_slug = (_canon(kab_value or "SEMUA").upper() or "SEMUA")
 
1693
  verif_total.to_excel(p_verif, index=False)
1694
 
1695
  wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
 
1696
 
1697
+ analysis_text = generate_llm_analysis(
1698
+ summary_jenis,
1699
+ agg_total,
1700
+ verif_total,
1701
+ wilayah_txt,
1702
+ kew_value or "(Semua)"
1703
+ )
1704
+
1705
+ word_path = generate_word_report(
1706
+ wilayah_txt,
1707
+ summary_jenis,
1708
+ agg_total,
1709
+ agg_jenis_full,
1710
+ analysis_text
1711
+ )
1712
 
1713
  msg = (
1714
  f"βœ… Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
 
1745
  prov_vals = [v for v in prov_vals if v and v.strip()]
1746
  prov_choices = ["(Semua)"] + sorted(set(prov_vals))
1747
 
1748
+ kab_choices = ["(Semua)"] + sorted([x for x in df_all["KAB_DISP"].dropna().unique().tolist() if x])
1749
+ kew_choices = ["(Semua)"] + sorted([x for x in df_all["KEW_NORM"].dropna().unique().tolist() if x])
1750
+ default_kew = "PROVINSI" if "PROVINSI" in kew_choices else ("KAB/KOTA" if "KAB/KOTA" in kew_choices else "(Semua)")
1751
 
1752
  return (
1753
  df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
 
1780
  **FIX UTAMA (konsistensi nilai):**
1781
  - **Agregat Wilayah (Keseluruhan) = rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3)**
1782
  - Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
1783
+ - KPI dashboard hanya menampilkan **FINAL** dan **DASAR**
1784
+ - **Cakupan (Target 68%) dan Penyesuaian Poin dipindahkan ke tabel Ringkasan**
1785
  - Download Data Mentah = RAW hasil filter
 
1786
 
1787
  **UPDATE (tampilan):**
1788
+ - Tabel Ringkasan memuat: Pop/Target68/Terkumpul/Coverage per jenis + Penyesuaian Poin
1789
+ - Tabel Agregat Wilayah memuat: Pop/Target68/Terkumpul per jenis + Total Update (sum 3 jenis)
1790
  - Tabel "Agregat Wilayah Γ— Jenis" ditampilkan hanya sampai Indeks_Dasar_Agregat_0_100
1791
  """)
1792
 
 
1801
 
1802
  with gr.Row():
1803
  dd_prov = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)")
1804
+ dd_kab = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)")
1805
+ dd_kew = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)")
1806
 
1807
  dd_prov.change(fn=on_prov_change, inputs=[dd_prov], outputs=dd_kab)
1808
 
1809
  run_btn = gr.Button("Jalankan Perhitungan")
1810
  msg_out = gr.Markdown()
1811
 
1812
+ # KPI dashboard (hanya 2 kartu)
1813
  kpi_out = gr.Markdown()
1814
 
1815
+ gr.Markdown("## Ringkasan (Jenis + Keseluruhan) β€” Pop/Target68/Terkumpul/Coverage + Penyesuaian")
1816
  out_summary = gr.DataFrame(interactive=False)
1817
 
1818
+ gr.Markdown("## Agregat Wilayah (Keseluruhan) β€” FIX: avg3 dari 3 jenis + Pop/Target68/Terkumpul (per jenis & total)")
1819
  out_agg_total = gr.DataFrame(interactive=False)
1820
 
1821
  gr.Markdown("## Agregat Wilayah Γ— Jenis (Sekolah, Umum, Khusus) β€” (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
 
1842
 
1843
  with gr.Row():
1844
  dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1845
+ dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1846
+ dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1847
+ dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1848
+ dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
1849
 
1850
  run_btn.click(
1851
  fn=run_calc,