irhamni commited on
Commit
d7e1bbd
Β·
verified Β·
1 Parent(s): 39ac352

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -134
app.py CHANGED
@@ -4,20 +4,25 @@ IPLM 2025 β€” FINAL (NO UPLOAD) β€” FULL REWRITE (NO RINGKAS)
4
 
5
  βœ… Jenis tampil: sekolah, umum, khusus (khusus ditampilkan sebagai jenis)
6
  βœ… Indeks dasar per entitas: Yeo-Johnson + MinMax nasional per indikator
7
- βœ… Agregasi wilayah (mean) β†’ penyesuaian 68% berbasis TOTAL pengumpulan wilayah:
8
  faktor_penyesuaian = min(n_total_terkumpul / target_total_68, 1.0)
9
- Indeks_Final_Wilayah = Indeks_Dasar_Agregat * faktor_penyesuaian
10
- βœ… Detail entitas: Indeks_Final_0_100 menempel dari Indeks_Final_Wilayah (bukan per-row)
 
 
 
 
 
 
11
  βœ… Bell curve per JENIS berbasis indeks per entitas (row-level)
12
  βœ… LLM analysis + Word
13
  βœ… Download (tanpa upload box)
 
 
14
 
15
- PERBAIKAN UTAMA:
16
- βœ… Ringkasan (Jenis + Keseluruhan) selalu tampil 4 baris: sekolah, umum, khusus, keseluruhan
17
- βœ… Indeks_Final_Disesuaikan_0_100 (keseluruhan) = (final_sekolah+final_umum+final_khusus)/3 (missing=0, tetap Γ·3)
18
- βœ… Dashboard KPI FINAL mengambil dari Ringkasan (baris keseluruhan) β€” bukan mean baris yang tersedia
19
- βœ… Word report: hilangkan "Kewenangan: ..." ganti "Ringkasan Dashboard" (mengacu KPI & Ringkasan)
20
- βœ… Tombol "Download Data Mentah (.xlsx)" berisi RAW hasil filter (bukan agregat jenis)
21
  """
22
 
23
  import os
@@ -587,6 +592,7 @@ def load_default_files(force=False):
587
 
588
  # ============================================================
589
  # 6) AGREGAT WILAYAH (KESELURUHAN) + PENYESUAIAN TOTAL
 
590
  # ============================================================
591
 
592
  def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_prov: pd.DataFrame, kew_value: str):
@@ -621,7 +627,8 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
621
  pop_field = "Pop_Total"
622
  name_field = "Kab_Kota_Label"
623
 
624
- agg = df.groupby([key_col, label_col], dropna=False).agg(
 
625
  n_total=("Indeks_Dasar_0_100", "size"),
626
  Rata2_sub_koleksi=("sub_koleksi", "mean"),
627
  Rata2_sub_sdm=("sub_sdm", "mean"),
@@ -629,11 +636,39 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
629
  Rata2_sub_pengelolaan=("sub_pengelolaan", "mean"),
630
  Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
631
  Rata2_dim_kinerja=("dim_kinerja", "mean"),
632
- Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_0_100", "mean"),
633
  ).reset_index()
634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  agg = agg.rename(columns={key_col: "group_key", label_col: label_name})
636
 
 
637
  target_vals, pop_vals, label_fix = [], [], []
638
  for _, r in agg.iterrows():
639
  gk = r["group_key"]
@@ -670,8 +705,10 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
670
  )
671
  ]
672
 
 
673
  agg["Indeks_Final_Wilayah_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
674
 
 
675
  for c in [
676
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
677
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -740,11 +777,12 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
740
  agg["faktor_penyesuaian"] = agg["faktor_penyesuaian_wilayah"]
741
  agg["Indeks_Final_Agregat_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
742
 
 
743
  agg["target_total_68_jenis"] = np.nan
744
  agg["pop_total_jenis"] = np.nan
745
  agg["coverage_jenis"] = np.nan
746
 
747
- if (pop_khusus is not None) and (not pop_khusus.empty) and ("KAB" in kew_norm or "KOTA" in kew_norm or kew_norm == "(SEMUA)" or kew_norm == "(SEMUA)".upper()):
748
  pk = pop_khusus.set_index("kab_key")
749
  for i, r in agg.iterrows():
750
  if str(r.get("Jenis", "")).lower() != "khusus":
@@ -765,6 +803,7 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
765
  m2 = agg["pop_total_jenis"].notna() & (agg["pop_total_jenis"] > 0)
766
  agg.loc[m2, "coverage_jenis"] = (agg.loc[m2, "Jumlah"].astype(float) / agg.loc[m2, "pop_total_jenis"].astype(float)) * 100.0
767
 
 
768
  for c in [
769
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
770
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -790,10 +829,8 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
790
  # ============================================================
791
 
792
  def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
793
- # Wajib selalu ada 3 jenis, walau 0
794
  jenis_list = ["sekolah", "umum", "khusus"]
795
 
796
- # helper bikin row default
797
  def _row_default(jenis):
798
  return {
799
  "Jenis": jenis,
@@ -815,7 +852,6 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
815
  sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
816
  if sub.empty:
817
  continue
818
-
819
  rows_by_jenis[jenis] = {
820
  "Jenis": jenis,
821
  "Jumlah_Wilayah": int(sub.shape[0]),
@@ -826,20 +862,16 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
826
  "Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
827
  "Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
828
  "Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
829
- # FINAL PER JENIS = mean Indeks_Final_Agregat_0_100 (wilayahΓ—jenis)
830
  "Indeks_Final_Disesuaikan_0_100": float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean()),
831
  }
832
 
833
- # susun output 3 jenis
834
  rows = [rows_by_jenis[j] for j in jenis_list]
835
 
836
- # KESELURUHAN harus Γ·3 (missing=0)
837
  final_sekolah = float(rows_by_jenis["sekolah"]["Indeks_Final_Disesuaikan_0_100"])
838
  final_umum = float(rows_by_jenis["umum"]["Indeks_Final_Disesuaikan_0_100"])
839
  final_khusus = float(rows_by_jenis["khusus"]["Indeks_Final_Disesuaikan_0_100"])
840
  final_all = (final_sekolah + final_umum + final_khusus) / 3.0
841
 
842
- # untuk indikator lain pada keseluruhan, juga pakai rata-rata 3 jenis (missing=0)
843
  def _avg3(field):
844
  return (float(rows_by_jenis["sekolah"][field]) + float(rows_by_jenis["umum"][field]) + float(rows_by_jenis["khusus"][field])) / 3.0
845
 
@@ -860,12 +892,11 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
860
  "Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
861
  "Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
862
  "Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
863
- "Indeks_Final_Disesuaikan_0_100": final_all, # FIX: selalu Γ·3
864
  })
865
 
866
  out = pd.DataFrame(rows)
867
 
868
- # rounding
869
  for c in [
870
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
871
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -1086,19 +1117,17 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
1086
  # ============================================================
1087
 
1088
  def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
1089
- # pastikan ada baris 3 jenis + keseluruhan
1090
  def _get_final(j):
1091
  sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1092
  if sub.empty:
1093
  return 0.0
1094
  return float(pd.to_numeric(sub["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
1095
 
1096
- final_sekolah = _get_final("sekolah")
1097
  final_umum = _get_final("umum")
1098
  final_khusus = _get_final("khusus")
1099
- final_all = (final_sekolah + final_umum + final_khusus) / 3.0 # sumber KPI final
1100
 
1101
- # dasar = rata-rata 3 jenis (missing=0, tetap Γ·3)
1102
  def _get_dasar(j):
1103
  if agg_jenis is None or agg_jenis.empty:
1104
  return 0.0
@@ -1112,7 +1141,6 @@ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame,
1112
  dasar_khusus = _get_dasar("khusus")
1113
  dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
1114
 
1115
- # cakupan
1116
  if agg_total is not None and not agg_total.empty:
1117
  n_sum = float(pd.to_numeric(agg_total.get("n_total", 0), errors="coerce").fillna(0).sum())
1118
  t_ser = pd.to_numeric(agg_total.get("target_total_68", np.nan), errors="coerce")
@@ -1173,7 +1201,7 @@ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg
1173
 
1174
 
1175
  # ============================================================
1176
- # 13) LLM + WORD (Word pakai Ringkasan Dashboard, tanpa "Kewenangan: ...")
1177
  # ============================================================
1178
 
1179
  _HF_CLIENT = None
@@ -1197,6 +1225,7 @@ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_to
1197
  lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0); Indeks_Final = Indeks_Dasar_Agregat Γ— faktor.")
1198
  lines.append("Jenis yang ditampilkan: sekolah, umum, khusus (SEMUA jenis menggunakan faktor wilayah).")
1199
  lines.append("Catatan ringkasan: nilai 'keseluruhan' dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, missing=0.")
 
1200
 
1201
  if summary_jenis is not None and not summary_jenis.empty:
1202
  lines.append("\nRingkasan (jenis + keseluruhan):")
@@ -1268,16 +1297,14 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
1268
  doc = Document()
1269
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1270
 
1271
- # Ringkasan Dashboard (mengganti "Kewenangan: ...")
1272
  doc.add_heading("Ringkasan Dashboard", level=2)
1273
-
1274
  k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
 
1275
  doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k['final_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1276
  doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1277
  doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
1278
  doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
1279
 
1280
- # Tabel Ringkasan (Jenis + Keseluruhan)
1281
  doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
1282
  show = summary_jenis.copy()
1283
  preferred = [
@@ -1310,22 +1337,21 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
1310
  else:
1311
  cells[i].text = str(v)
1312
 
1313
- # Metodologi
1314
  doc.add_heading("Metodologi", level=2)
1315
  doc.add_paragraph(
1316
  "Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
1317
- "Nilai kemudian diagregasi per wilayah (rata-rata) untuk memperoleh Indeks Dasar wilayah."
 
 
 
 
1318
  )
1319
  doc.add_paragraph(
1320
  "Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
1321
  "dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0). "
1322
  "Indeks_Final_Wilayah = Indeks_Dasar_Agregat Γ— faktor."
1323
  )
1324
- doc.add_paragraph(
1325
- "Ringkasan 'keseluruhan' dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, dengan missing dianggap 0."
1326
- )
1327
 
1328
- # Analisis LLM
1329
  doc.add_heading("Analisis Naratif (LLM)", level=2)
1330
  for p in (analysis_text or "").split("\n"):
1331
  if p.strip():
@@ -1344,7 +1370,7 @@ def _empty_outputs(msg="⚠️ Data belum siap."):
1344
  empty = pd.DataFrame()
1345
  empty_fig = go.Figure()
1346
  return (
1347
- "", # kpi_md
1348
  empty, empty, empty, empty, empty,
1349
  None, None, None, None, None,
1350
  empty_fig, empty_fig, empty_fig,
@@ -1356,7 +1382,6 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1356
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1357
  return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1358
 
1359
- # FILTER ANALISIS (df_all)
1360
  df = df_all.copy()
1361
  if prov_value and prov_value != "(Semua)":
1362
  df = df[df["PROV_DISP"] == prov_value]
@@ -1374,7 +1399,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1374
  verif_total = build_verif_total(agg_total)
1375
  detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
1376
 
1377
- # FILTER RAW DOWNLOAD (df_raw)
1378
  raw = df_raw.copy()
1379
  if prov_value and prov_value != "(Semua)":
1380
  raw = raw[raw["PROV_DISP"] == prov_value]
@@ -1383,7 +1408,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1383
  if kew_value and kew_value != "(Semua)":
1384
  raw = raw[raw["KEW_NORM"] == kew_value]
1385
 
1386
- # Bell curve per JENIS (per entitas)
1387
  if detail_view is None or detail_view.empty:
1388
  fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1389
  fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
@@ -1401,7 +1426,6 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1401
  fig_umum = _fig_jenis_ent("umum", "Bell Curve β€” Jenis: Umum (Indeks per Entitas)")
1402
  fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β€” Jenis: Khusus (Indeks per Entitas)")
1403
 
1404
- # KPI markdown (FINAL sumber Ringkasan)
1405
  kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis)
1406
 
1407
  tmpdir = tempfile.mkdtemp()
@@ -1411,31 +1435,30 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1411
 
1412
  p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1413
  p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1414
- p_jenis = str(Path(tmpdir) / f"IPLM_RAW_DATA_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1415
  p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1416
  p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1417
 
1418
  summary_jenis.to_excel(p_summary, index=False)
1419
  agg_total.to_excel(p_total, index=False)
1420
- raw.to_excel(p_jenis, index=False)
1421
  detail_view.to_excel(p_detail, index=False)
1422
  verif_total.to_excel(p_verif, index=False)
1423
 
1424
  wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
1425
  analysis_text = generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah_txt, kew_value or "(Semua)")
1426
-
1427
- # Word report: tanpa "Kewenangan: ..."
1428
  word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, agg_jenis, analysis_text)
1429
 
1430
  msg = (
1431
  f"βœ… Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
1432
- f"jenis(agregat)={len(agg_jenis)} | ringkasan: 3 jenis selalu tampil & keseluruhan Γ·3"
 
1433
  )
1434
 
1435
  return (
1436
  kpi_md,
1437
  summary_jenis, agg_total, agg_jenis, detail_view, verif_total,
1438
- p_summary, p_total, p_jenis, p_detail, word_path,
1439
  fig_umum, fig_sekolah, fig_khusus,
1440
  msg, analysis_text
1441
  )
@@ -1445,7 +1468,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1445
 
1446
 
1447
  # ============================================================
1448
- # 15) UI (NO UPLOAD)
1449
  # ============================================================
1450
 
1451
  def ui_load(force=False):
@@ -1462,118 +1485,172 @@ def ui_load(force=False):
1462
  prov_vals = [v for v in prov_vals if v and v.strip()]
1463
  prov_choices = ["(Semua)"] + sorted(set(prov_vals))
1464
 
1465
- kab_choices = ["(Semua)"] + sorted([x for x in df_all["KAB_DISP"].dropna().unique().tolist() if x])
1466
- kew_choices = ["(Semua)"] + sorted([x for x in df_all["KEW_NORM"].dropna().unique().tolist() if x])
1467
- default_kew = "PROVINSI" if "PROVINSI" in kew_choices else ("KAB/KOTA" if "KAB/KOTA" in kew_choices else "(Semua)")
 
 
 
 
1468
 
1469
  return (
1470
  df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
1471
  gr.update(choices=prov_choices, value="(Semua)"),
1472
  gr.update(choices=kab_choices, value="(Semua)"),
1473
- gr.update(choices=kew_choices, value=default_kew),
1474
  )
1475
 
1476
- def on_prov_change(prov_value):
1477
- df_all, _, _, _, _, _, _ = load_default_files(force=False)
1478
  if df_all is None or df_all.empty:
1479
  return gr.update(choices=["(Semua)"], value="(Semua)")
1480
- if prov_value is None or prov_value == "(Semua)":
1481
- vals = df_all["KAB_DISP"].dropna().unique().tolist()
1482
  else:
1483
- vals = df_all.loc[df_all["PROV_DISP"] == prov_value, "KAB_DISP"].dropna().unique().tolist()
1484
- vals = sorted([v for v in vals if v])
1485
- return gr.update(choices=["(Semua)"] + vals, value="(Semua)")
1486
-
1487
-
1488
- with gr.Blocks() as demo:
1489
- gr.Markdown(f"""
1490
- # IPLM 2025 β€” Final (Penyesuaian Berbasis Kecukupan Sampel 68%)
1491
- **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1492
- - `DATA_FILE` = **{DATA_FILE}**
1493
- - `POP_KAB` = **{POP_KAB}**
1494
- - `POP_PROV` = **{POP_PROV}**
1495
- - `POP_KHUSUS` = **{POP_KHUSUS}**
1496
-
1497
- **Update FIX (konsistensi dashboard):**
1498
- - Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
1499
- - **Keseluruhan = rata-rata 3 jenis Γ·3 (missing=0)**
1500
- - KPI FINAL dashboard sumber dari Ringkasan (bukan mean baris yang ada)
1501
- - Download Data Mentah = RAW hasil filter
1502
- - Laporan Word: tanpa baris "Kewenangan: ...", diganti "Ringkasan Dashboard"
1503
- """)
1504
-
1505
- state_df = gr.State(None)
1506
- state_raw = gr.State(None)
1507
- state_pop_kab = gr.State(None)
1508
- state_pop_prov = gr.State(None)
1509
- state_pop_khusus = gr.State(None)
1510
- state_meta = gr.State({})
1511
-
1512
- info_box = gr.Markdown()
1513
 
1514
- with gr.Row():
1515
- dd_prov = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)")
1516
- dd_kab = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)")
1517
- dd_kew = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)")
1518
 
1519
- dd_prov.change(fn=on_prov_change, inputs=[dd_prov], outputs=dd_kab)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1520
 
1521
- run_btn = gr.Button("Jalankan Perhitungan")
1522
- msg_out = gr.Markdown()
1523
 
1524
- kpi_out = gr.Markdown()
 
 
 
1525
 
1526
- gr.Markdown("## Ringkasan (Jenis + Keseluruhan)")
1527
- out_summary = gr.DataFrame(interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
1528
 
1529
- gr.Markdown("## Agregat Wilayah (Keseluruhan)")
1530
- out_agg_total = gr.DataFrame(interactive=False)
1531
 
1532
- gr.Markdown("## Agregat Wilayah Γ— Jenis (Sekolah, Umum, Khusus)")
1533
- out_agg_jenis = gr.DataFrame(interactive=False)
1534
 
1535
- gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
1536
- out_detail = gr.DataFrame(interactive=False)
1537
 
1538
- gr.Markdown("## Kecukupan Sampel 68% (tanpa angka koma)")
1539
- out_verif = gr.DataFrame(interactive=False)
1540
 
1541
- gr.Markdown("## Bell Curve β€” per Jenis Perpustakaan (Indeks per Entitas)")
1542
- gr.Markdown("### Perpustakaan Umum")
1543
- bell_umum = gr.Plot(scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1544
 
1545
- gr.Markdown("### Perpustakaan Sekolah")
1546
- bell_sekolah = gr.Plot(scale=1)
 
 
 
 
1547
 
1548
- gr.Markdown("### Perpustakaan Khusus")
1549
- bell_khusus = gr.Plot(scale=1)
 
 
 
 
 
 
 
 
 
 
1550
 
1551
- gr.Markdown("## Analisis Otomatis (LLM)")
1552
- analysis_out = gr.Markdown()
 
 
 
1553
 
1554
- with gr.Row():
1555
- dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1556
- dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1557
- dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1558
- dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1559
- dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
1560
-
1561
- run_btn.click(
1562
- fn=run_calc,
1563
- inputs=[dd_prov, dd_kab, dd_kew, state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta],
1564
  outputs=[
1565
- kpi_out,
1566
- out_summary, out_agg_total, out_agg_jenis, out_detail, out_verif,
1567
- dl_summary, dl_total, dl_jenis, dl_detail, dl_word,
1568
- bell_umum, bell_sekolah, bell_khusus,
1569
- msg_out, analysis_out
1570
  ]
1571
  )
1572
 
1573
- demo.load(
1574
- fn=lambda: ui_load(force=False),
1575
- inputs=[],
1576
- outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
1577
- )
1578
 
1579
- demo.launch()
 
 
 
 
 
 
4
 
5
  βœ… Jenis tampil: sekolah, umum, khusus (khusus ditampilkan sebagai jenis)
6
  βœ… Indeks dasar per entitas: Yeo-Johnson + MinMax nasional per indikator
7
+ βœ… Penyesuaian 68% berbasis TOTAL pengumpulan wilayah:
8
  faktor_penyesuaian = min(n_total_terkumpul / target_total_68, 1.0)
9
+ βœ… KONSISTENSI UTAMA (FIX INKONSISTENSI):
10
+ Indeks_Dasar_Agregat_0_100 (KESELURUHAN wilayah) = (dasar_sekolah + dasar_umum + dasar_khusus) / 3 (missing=0, tetap Γ·3)
11
+ Indeks_Final_Wilayah_0_100 = Indeks_Dasar_Agregat_0_100 Γ— faktor_penyesuaian
12
+ -> Maka nilai Aceh Jaya di agregat provinsi akan sama dengan nilai Aceh Jaya saat dipilih individu (definisi keseluruhan sama).
13
+ βœ… Ringkasan (Jenis + Keseluruhan) selalu 4 baris: sekolah, umum, khusus, keseluruhan
14
+ βœ… Indeks_Final_Disesuaikan_0_100 (keseluruhan) = (final_sekolah+final_umum+final_khusus)/3 (missing=0, tetap Γ·3)
15
+ βœ… Dashboard KPI FINAL mengambil dari Ringkasan (baris keseluruhan)
16
+ βœ… Detail entitas: Indeks_Final_0_100 menempel dari Indeks_Final_Wilayah_0_100 (bukan per-row)
17
  βœ… Bell curve per JENIS berbasis indeks per entitas (row-level)
18
  βœ… LLM analysis + Word
19
  βœ… Download (tanpa upload box)
20
+ βœ… Download Data Mentah (.xlsx) = RAW hasil filter (bukan agregat jenis)
21
+ βœ… Word report: hilangkan "Kewenangan: ..." ganti "Ringkasan Dashboard"
22
 
23
+ CATATAN:
24
+ - Kolom sub/dim pada agregat wilayah tetap dihitung mean seluruh entitas (itu tidak memengaruhi konsistensi indeks keseluruhan).
25
+ - Konsistensi indeks keseluruhan dijamin karena semua level pakai definisi Γ·3.
 
 
 
26
  """
27
 
28
  import os
 
592
 
593
  # ============================================================
594
  # 6) AGREGAT WILAYAH (KESELURUHAN) + PENYESUAIAN TOTAL
595
+ # FIX INKONSISTENSI: "Indeks_Dasar_Agregat_0_100" pakai rata-rata 3 jenis Γ·3
596
  # ============================================================
597
 
598
  def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_prov: pd.DataFrame, kew_value: str):
 
627
  pop_field = "Pop_Total"
628
  name_field = "Kab_Kota_Label"
629
 
630
+ # agregat mean sub/dim (boleh weighted by count, tidak untuk indeks keseluruhan)
631
+ agg_base = df.groupby([key_col, label_col], dropna=False).agg(
632
  n_total=("Indeks_Dasar_0_100", "size"),
633
  Rata2_sub_koleksi=("sub_koleksi", "mean"),
634
  Rata2_sub_sdm=("sub_sdm", "mean"),
 
636
  Rata2_sub_pengelolaan=("sub_pengelolaan", "mean"),
637
  Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
638
  Rata2_dim_kinerja=("dim_kinerja", "mean"),
 
639
  ).reset_index()
640
 
641
+ # ===== KUNCI KONSISTENSI =====
642
+ # indeks dasar per jenis per wilayah, lalu overall = (sekolah+umum+khusus)/3 (missing=0)
643
+ jenis_mean = (
644
+ df[df["_dataset"].isin(["sekolah", "umum", "khusus"])]
645
+ .groupby([key_col, label_col, "_dataset"], dropna=False)["Indeks_Dasar_0_100"]
646
+ .mean()
647
+ .reset_index()
648
+ )
649
+
650
+ piv = jenis_mean.pivot_table(
651
+ index=[key_col, label_col],
652
+ columns="_dataset",
653
+ values="Indeks_Dasar_0_100",
654
+ aggfunc="mean"
655
+ ).reset_index()
656
+
657
+ for j in ["sekolah", "umum", "khusus"]:
658
+ if j not in piv.columns:
659
+ piv[j] = 0.0
660
+ piv[["sekolah", "umum", "khusus"]] = piv[["sekolah", "umum", "khusus"]].fillna(0.0)
661
+ piv["Indeks_Dasar_Agregat_0_100"] = (piv["sekolah"] + piv["umum"] + piv["khusus"]) / 3.0
662
+
663
+ agg = agg_base.merge(
664
+ piv[[key_col, label_col, "Indeks_Dasar_Agregat_0_100"]],
665
+ on=[key_col, label_col],
666
+ how="left"
667
+ )
668
+
669
  agg = agg.rename(columns={key_col: "group_key", label_col: label_name})
670
 
671
+ # attach target/pop
672
  target_vals, pop_vals, label_fix = [], [], []
673
  for _, r in agg.iterrows():
674
  gk = r["group_key"]
 
705
  )
706
  ]
707
 
708
+ # FINAL wilayah konsisten
709
  agg["Indeks_Final_Wilayah_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
710
 
711
+ # rounding
712
  for c in [
713
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
714
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
777
  agg["faktor_penyesuaian"] = agg["faktor_penyesuaian_wilayah"]
778
  agg["Indeks_Final_Agregat_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
779
 
780
+ # khusus: optional attach target/pop jenis (hanya untuk info)
781
  agg["target_total_68_jenis"] = np.nan
782
  agg["pop_total_jenis"] = np.nan
783
  agg["coverage_jenis"] = np.nan
784
 
785
+ if (pop_khusus is not None) and (not pop_khusus.empty) and ("KAB" in kew_norm or "KOTA" in kew_norm or kew_norm in {"(SEMUA)", "(SEMUA)".upper()}):
786
  pk = pop_khusus.set_index("kab_key")
787
  for i, r in agg.iterrows():
788
  if str(r.get("Jenis", "")).lower() != "khusus":
 
803
  m2 = agg["pop_total_jenis"].notna() & (agg["pop_total_jenis"] > 0)
804
  agg.loc[m2, "coverage_jenis"] = (agg.loc[m2, "Jumlah"].astype(float) / agg.loc[m2, "pop_total_jenis"].astype(float)) * 100.0
805
 
806
+ # rounding
807
  for c in [
808
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
809
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
829
  # ============================================================
830
 
831
  def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
 
832
  jenis_list = ["sekolah", "umum", "khusus"]
833
 
 
834
  def _row_default(jenis):
835
  return {
836
  "Jenis": jenis,
 
852
  sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
853
  if sub.empty:
854
  continue
 
855
  rows_by_jenis[jenis] = {
856
  "Jenis": jenis,
857
  "Jumlah_Wilayah": int(sub.shape[0]),
 
862
  "Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
863
  "Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
864
  "Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
 
865
  "Indeks_Final_Disesuaikan_0_100": float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean()),
866
  }
867
 
 
868
  rows = [rows_by_jenis[j] for j in jenis_list]
869
 
 
870
  final_sekolah = float(rows_by_jenis["sekolah"]["Indeks_Final_Disesuaikan_0_100"])
871
  final_umum = float(rows_by_jenis["umum"]["Indeks_Final_Disesuaikan_0_100"])
872
  final_khusus = float(rows_by_jenis["khusus"]["Indeks_Final_Disesuaikan_0_100"])
873
  final_all = (final_sekolah + final_umum + final_khusus) / 3.0
874
 
 
875
  def _avg3(field):
876
  return (float(rows_by_jenis["sekolah"][field]) + float(rows_by_jenis["umum"][field]) + float(rows_by_jenis["khusus"][field])) / 3.0
877
 
 
892
  "Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
893
  "Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
894
  "Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
895
+ "Indeks_Final_Disesuaikan_0_100": final_all,
896
  })
897
 
898
  out = pd.DataFrame(rows)
899
 
 
900
  for c in [
901
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
902
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
1117
  # ============================================================
1118
 
1119
  def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
 
1120
  def _get_final(j):
1121
  sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1122
  if sub.empty:
1123
  return 0.0
1124
  return float(pd.to_numeric(sub["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
1125
 
1126
+ final_sekolah = _mland(_get_final("sekolah")) if False else _get_final("sekolah")
1127
  final_umum = _get_final("umum")
1128
  final_khusus = _get_final("khusus")
1129
+ final_all = (final_sekolah + final_umum + final_khusus) / 3.0
1130
 
 
1131
  def _get_dasar(j):
1132
  if agg_jenis is None or agg_jenis.empty:
1133
  return 0.0
 
1141
  dasar_khusus = _get_dasar("khusus")
1142
  dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
1143
 
 
1144
  if agg_total is not None and not agg_total.empty:
1145
  n_sum = float(pd.to_numeric(agg_total.get("n_total", 0), errors="coerce").fillna(0).sum())
1146
  t_ser = pd.to_numeric(agg_total.get("target_total_68", np.nan), errors="coerce")
 
1201
 
1202
 
1203
  # ============================================================
1204
+ # 13) LLM + WORD (Word pakai Ringkasan Dashboard)
1205
  # ============================================================
1206
 
1207
  _HF_CLIENT = None
 
1225
  lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0); Indeks_Final = Indeks_Dasar_Agregat Γ— faktor.")
1226
  lines.append("Jenis yang ditampilkan: sekolah, umum, khusus (SEMUA jenis menggunakan faktor wilayah).")
1227
  lines.append("Catatan ringkasan: nilai 'keseluruhan' dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, missing=0.")
1228
+ lines.append("Catatan konsistensi: Indeks_Dasar_Agregat wilayah juga dihitung sebagai rata-rata 3 jenis Γ·3 (missing=0).")
1229
 
1230
  if summary_jenis is not None and not summary_jenis.empty:
1231
  lines.append("\nRingkasan (jenis + keseluruhan):")
 
1297
  doc = Document()
1298
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1299
 
 
1300
  doc.add_heading("Ringkasan Dashboard", level=2)
 
1301
  k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
1302
+
1303
  doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k['final_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1304
  doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1305
  doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
1306
  doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
1307
 
 
1308
  doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
1309
  show = summary_jenis.copy()
1310
  preferred = [
 
1337
  else:
1338
  cells[i].text = str(v)
1339
 
 
1340
  doc.add_heading("Metodologi", level=2)
1341
  doc.add_paragraph(
1342
  "Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
1343
+ "Nilai kemudian diagregasi per wilayah untuk memperoleh Indeks Dasar wilayah."
1344
+ )
1345
+ doc.add_paragraph(
1346
+ "Indeks Dasar wilayah (keseluruhan) dihitung sebagai rata-rata 3 jenis perpustakaan (sekolah+umum+khusus) Γ· 3, "
1347
+ "dengan missing dianggap 0, agar konsisten antar level analisis."
1348
  )
1349
  doc.add_paragraph(
1350
  "Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
1351
  "dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0). "
1352
  "Indeks_Final_Wilayah = Indeks_Dasar_Agregat Γ— faktor."
1353
  )
 
 
 
1354
 
 
1355
  doc.add_heading("Analisis Naratif (LLM)", level=2)
1356
  for p in (analysis_text or "").split("\n"):
1357
  if p.strip():
 
1370
  empty = pd.DataFrame()
1371
  empty_fig = go.Figure()
1372
  return (
1373
+ "",
1374
  empty, empty, empty, empty, empty,
1375
  None, None, None, None, None,
1376
  empty_fig, empty_fig, empty_fig,
 
1382
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1383
  return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1384
 
 
1385
  df = df_all.copy()
1386
  if prov_value and prov_value != "(Semua)":
1387
  df = df[df["PROV_DISP"] == prov_value]
 
1399
  verif_total = build_verif_total(agg_total)
1400
  detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
1401
 
1402
+ # RAW download (df_raw)
1403
  raw = df_raw.copy()
1404
  if prov_value and prov_value != "(Semua)":
1405
  raw = raw[raw["PROV_DISP"] == prov_value]
 
1408
  if kew_value and kew_value != "(Semua)":
1409
  raw = raw[raw["KEW_NORM"] == kew_value]
1410
 
1411
+ # Bell curve per jenis (entitas)
1412
  if detail_view is None or detail_view.empty:
1413
  fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1414
  fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
 
1426
  fig_umum = _fig_jenis_ent("umum", "Bell Curve β€” Jenis: Umum (Indeks per Entitas)")
1427
  fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β€” Jenis: Khusus (Indeks per Entitas)")
1428
 
 
1429
  kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis)
1430
 
1431
  tmpdir = tempfile.mkdtemp()
 
1435
 
1436
  p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1437
  p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1438
+ p_raw = str(Path(tmpdir) / f"IPLM_RAW_DATA_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1439
  p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1440
  p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1441
 
1442
  summary_jenis.to_excel(p_summary, index=False)
1443
  agg_total.to_excel(p_total, index=False)
1444
+ raw.to_excel(p_raw, index=False)
1445
  detail_view.to_excel(p_detail, index=False)
1446
  verif_total.to_excel(p_verif, index=False)
1447
 
1448
  wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
1449
  analysis_text = generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah_txt, kew_value or "(Semua)")
 
 
1450
  word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, agg_jenis, analysis_text)
1451
 
1452
  msg = (
1453
  f"βœ… Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
1454
+ f"jenis(agregat)={len(agg_jenis)} | ringkasan: 3 jenis selalu tampil & keseluruhan Γ·3 | "
1455
+ f"CONSISTENCY FIX: indeks wilayah keseluruhan juga Γ·3"
1456
  )
1457
 
1458
  return (
1459
  kpi_md,
1460
  summary_jenis, agg_total, agg_jenis, detail_view, verif_total,
1461
+ p_summary, p_total, p_raw, p_detail, word_path,
1462
  fig_umum, fig_sekolah, fig_khusus,
1463
  msg, analysis_text
1464
  )
 
1468
 
1469
 
1470
  # ============================================================
1471
+ # 15) UI (NO UPLOAD) β€” LANJUTAN (tidak terputus)
1472
  # ============================================================
1473
 
1474
  def ui_load(force=False):
 
1485
  prov_vals = [v for v in prov_vals if v and v.strip()]
1486
  prov_choices = ["(Semua)"] + sorted(set(prov_vals))
1487
 
1488
+ kew_vals = df_all["KEW_NORM"].dropna().astype(str).tolist()
1489
+ kew_vals = [v for v in kew_vals if v and v.strip()]
1490
+ kew_choices = ["(Semua)"] + sorted(set(kew_vals))
1491
+
1492
+ kab_vals = df_all["KAB_DISP"].dropna().astype(str).tolist()
1493
+ kab_vals = [v for v in kab_vals if v and v.strip()]
1494
+ kab_choices = ["(Semua)"] + sorted(set(kab_vals))
1495
 
1496
  return (
1497
  df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
1498
  gr.update(choices=prov_choices, value="(Semua)"),
1499
  gr.update(choices=kab_choices, value="(Semua)"),
1500
+ gr.update(choices=kew_choices, value="(Semua)"),
1501
  )
1502
 
1503
+ def ui_update_kab_choices(prov_value, df_all):
 
1504
  if df_all is None or df_all.empty:
1505
  return gr.update(choices=["(Semua)"], value="(Semua)")
1506
+ if prov_value and prov_value != "(Semua)":
1507
+ sub = df_all[df_all["PROV_DISP"] == prov_value]
1508
  else:
1509
+ sub = df_all
1510
+ kabs = sub["KAB_DISP"].dropna().astype(str).tolist()
1511
+ kabs = [k for k in kabs if k and k.strip()]
1512
+ kab_choices = ["(Semua)"] + sorted(set(kabs))
1513
+ return gr.update(choices=kab_choices, value="(Semua)")
1514
+
1515
+ def ui_run(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta):
1516
+ return run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
 
 
 
 
 
1518
 
1519
+ # ============================================================
1520
+ # 16) BUILD APP
1521
+ # ============================================================
1522
+
1523
+ with gr.Blocks(title="IPLM 2025 β€” Dashboard (NO UPLOAD)") as demo:
1524
+
1525
+ # ---------- STATE ----------
1526
+ st_df_all = gr.State(None)
1527
+ st_df_raw = gr.State(None)
1528
+ st_pop_kab = gr.State(None)
1529
+ st_pop_prov = gr.State(None)
1530
+ st_pop_khusus = gr.State(None)
1531
+ st_meta = gr.State({})
1532
+ st_info = gr.State("")
1533
+
1534
+ # ---------- HEADER ----------
1535
+ gr.Markdown(
1536
+ """
1537
+ # IPLM 2025 β€” Dashboard (NO UPLOAD)
1538
+ **Konsistensi hitung keseluruhan wilayah (Kab/Kota/Provinsi) vs individu dijamin dengan:**
1539
+ - Indeks dasar keseluruhan wilayah = (dasar_sekolah + dasar_umum + dasar_khusus) Γ· 3 (missing=0, tetap Γ·3)
1540
+ - Faktor penyesuaian = min(total_terkumpul / target_total_68, 1.0)
1541
+ - Indeks final wilayah = indeks dasar keseluruhan Γ— faktor penyesuaian
1542
+ """.strip()
1543
+ )
1544
 
1545
+ info_box = gr.HTML(value="")
 
1546
 
1547
+ with gr.Row():
1548
+ prov_dd = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)", interactive=True)
1549
+ kab_dd = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)", interactive=True)
1550
+ kew_dd = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)", interactive=True)
1551
 
1552
+ with gr.Row():
1553
+ btn_reload = gr.Button("Reload File (Cache)", variant="secondary")
1554
+ btn_run = gr.Button("Run", variant="primary")
1555
+
1556
+ # ---------- KPI ----------
1557
+ kpi_html = gr.HTML(value="")
1558
+
1559
+ # ---------- TABLES ----------
1560
+ with gr.Tabs():
1561
+ with gr.Tab("Ringkasan (Jenis + Keseluruhan)"):
1562
+ tbl_summary = gr.Dataframe(
1563
+ headers=["Jenis","Jumlah_Wilayah","Total_Perpus","Rata2_dim_kepatuhan","Rata2_dim_kinerja","Indeks_Final_Disesuaikan_0_100"],
1564
+ interactive=False
1565
+ )
1566
 
1567
+ with gr.Tab("Agregat Wilayah (Keseluruhan)"):
1568
+ tbl_agg_total = gr.Dataframe(interactive=False)
1569
 
1570
+ with gr.Tab("Agregat Wilayah Γ— Jenis"):
1571
+ tbl_agg_jenis = gr.Dataframe(interactive=False)
1572
 
1573
+ with gr.Tab("Detail Entitas (Final menempel Wilayah)"):
1574
+ tbl_detail = gr.Dataframe(interactive=False)
1575
 
1576
+ with gr.Tab("Verifikasi 68% (Total Wilayah)"):
1577
+ tbl_verif = gr.Dataframe(interactive=False)
1578
 
1579
+ # ---------- DOWNLOADS ----------
1580
+ with gr.Row():
1581
+ dl_summary = gr.File(label="Download Ringkasan (xlsx)")
1582
+ dl_total = gr.File(label="Download Agregat Keseluruhan (xlsx)")
1583
+ dl_raw = gr.File(label="Download RAW Data (xlsx)")
1584
+ dl_detail = gr.File(label="Download Detail Entitas (xlsx)")
1585
+ dl_word = gr.File(label="Download Word Report (docx)")
1586
+
1587
+ # ---------- CHARTS ----------
1588
+ with gr.Tabs():
1589
+ with gr.Tab("Bell Curve β€” Umum"):
1590
+ fig_umum = gr.Plot()
1591
+ with gr.Tab("Bell Curve β€” Sekolah"):
1592
+ fig_sekolah = gr.Plot()
1593
+ with gr.Tab("Bell Curve β€” Khusus"):
1594
+ fig_khusus = gr.Plot()
1595
+
1596
+ # ---------- LLM OUTPUT ----------
1597
+ with gr.Accordion("Analisis Otomatis (LLM)", open=False):
1598
+ status_text = gr.Markdown("")
1599
+ llm_text = gr.Markdown("")
1600
+
1601
+ # ============================================================
1602
+ # EVENTS
1603
+ # ============================================================
1604
+
1605
+ def _on_load():
1606
+ df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info, prov_upd, kab_upd, kew_upd = ui_load(force=False)
1607
+ return (
1608
+ df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
1609
+ info, prov_upd, kab_upd, kew_upd
1610
+ )
1611
 
1612
+ demo.load(
1613
+ _on_load,
1614
+ inputs=[],
1615
+ outputs=[st_df_all, st_df_raw, st_pop_kab, st_pop_prov, st_pop_khusus, st_meta, st_info,
1616
+ info_box, prov_dd, kab_dd, kew_dd]
1617
+ )
1618
 
1619
+ btn_reload.click(
1620
+ lambda: (
1621
+ *ui_load(force=True)[:7],
1622
+ ui_load(force=True)[6], # info
1623
+ ui_load(force=True)[7], # prov upd
1624
+ ui_load(force=True)[8], # kab upd
1625
+ ui_load(force=True)[9], # kew upd
1626
+ ),
1627
+ inputs=[],
1628
+ outputs=[st_df_all, st_df_raw, st_pop_kab, st_pop_prov, st_pop_khusus, st_meta, st_info,
1629
+ info_box, prov_dd, kab_dd, kew_dd]
1630
+ )
1631
 
1632
+ prov_dd.change(
1633
+ ui_update_kab_choices,
1634
+ inputs=[prov_dd, st_df_all],
1635
+ outputs=[kab_dd]
1636
+ )
1637
 
1638
+ btn_run.click(
1639
+ ui_run,
1640
+ inputs=[prov_dd, kab_dd, kew_dd, st_df_all, st_df_raw, st_pop_kab, st_pop_prov, st_pop_khusus, st_meta],
 
 
 
 
 
 
 
1641
  outputs=[
1642
+ kpi_html,
1643
+ tbl_summary, tbl_agg_total, tbl_agg_jenis, tbl_detail, tbl_verif,
1644
+ dl_summary, dl_total, dl_raw, dl_detail, dl_word,
1645
+ fig_umum, fig_sekolah, fig_khusus,
1646
+ status_text, llm_text
1647
  ]
1648
  )
1649
 
 
 
 
 
 
1650
 
1651
+ # ============================================================
1652
+ # 17) LAUNCH
1653
+ # ============================================================
1654
+
1655
+ if __name__ == "__main__":
1656
+ demo.launch(share=True)