irhamni commited on
Commit
92959c9
Β·
verified Β·
1 Parent(s): d7e1bbd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -211
app.py CHANGED
@@ -4,25 +4,20 @@ IPLM 2025 β€” FINAL (NO UPLOAD) β€” FULL REWRITE (NO RINGKAS)
4
 
5
  βœ… Jenis tampil: sekolah, umum, khusus (khusus ditampilkan sebagai jenis)
6
  βœ… Indeks dasar per entitas: Yeo-Johnson + MinMax nasional per indikator
7
- βœ… Penyesuaian 68% berbasis TOTAL pengumpulan wilayah:
8
  faktor_penyesuaian = min(n_total_terkumpul / target_total_68, 1.0)
9
- βœ… 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,7 +587,6 @@ def load_default_files(force=False):
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,8 +621,7 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
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,39 +629,11 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
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,10 +670,8 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
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,12 +740,11 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
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,7 +765,6 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
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,8 +790,10 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
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,6 +815,7 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
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,16 +826,20 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
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,11 +860,12 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
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,17 +1086,19 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
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,6 +1112,7 @@ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame,
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,7 +1173,7 @@ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg
1201
 
1202
 
1203
  # ============================================================
1204
- # 13) LLM + WORD (Word pakai Ringkasan Dashboard)
1205
  # ============================================================
1206
 
1207
  _HF_CLIENT = None
@@ -1225,7 +1197,6 @@ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_to
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,14 +1268,16 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
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,21 +1310,22 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
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,7 +1344,7 @@ def _empty_outputs(msg="⚠️ Data belum siap."):
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,6 +1356,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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,7 +1374,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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,7 +1383,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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,6 +1401,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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,30 +1411,31 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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,7 +1445,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1468
 
1469
 
1470
  # ============================================================
1471
- # 15) UI (NO UPLOAD) β€” LANJUTAN (tidak terputus)
1472
  # ============================================================
1473
 
1474
  def ui_load(force=False):
@@ -1485,172 +1462,118 @@ 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)
 
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
 
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
  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
  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
  )
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
  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
  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
  # ============================================================
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
  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
  "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
  "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
  # ============================================================
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
  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
 
1174
 
1175
  # ============================================================
1176
+ # 13) LLM + WORD (Word pakai Ringkasan Dashboard, tanpa "Kewenangan: ...")
1177
  # ============================================================
1178
 
1179
  _HF_CLIENT = None
 
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
  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
  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
  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
  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
  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
  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
  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
 
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
 
1446
 
1447
  # ============================================================
1448
+ # 15) UI (NO UPLOAD)
1449
  # ============================================================
1450
 
1451
  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()