irhamni commited on
Commit
355a217
·
verified ·
1 Parent(s): 499b8e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +183 -117
app.py CHANGED
@@ -36,6 +36,11 @@ PERMINTAAN UPDATE (HANYA INI):
36
  - coverage_total_% -> decimal 2 digit
37
  2) TABEL "Agregat Wilayah × Jenis" (UI) hanya sampai kolom Indeks_Dasar_Agregat_0_100
38
  (kolom setelah itu tidak ditampilkan)
 
 
 
 
 
39
  """
40
 
41
  import os
@@ -605,9 +610,10 @@ def load_default_files(force=False):
605
 
606
  # ============================================================
607
  # 6) FAKTOR WILAYAH (TOTAL) — hanya untuk faktor/target/pop/coverage
 
608
  # ============================================================
609
 
610
- def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_prov: pd.DataFrame, kew_value: str):
611
  if df_filtered is None or df_filtered.empty:
612
  return pd.DataFrame()
613
 
@@ -622,6 +628,17 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
622
  target_field = "Target68_Total"
623
  pop_field = "Pop_Total"
624
  name_field = "Kab_Kota_Label"
 
 
 
 
 
 
 
 
 
 
 
625
  elif "PROV" in kew_norm:
626
  key_col = "prov_key"
627
  label_col = "PROV_DISP"
@@ -630,6 +647,17 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
630
  target_field = "Target68_Total_Prov"
631
  pop_field = "Pop_Total_Prov"
632
  name_field = "Provinsi_Label"
 
 
 
 
 
 
 
 
 
 
 
633
  else:
634
  key_col = "kab_key"
635
  label_col = "KAB_DISP"
@@ -638,6 +666,16 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
638
  target_field = "Target68_Total"
639
  pop_field = "Pop_Total"
640
  name_field = "Kab_Kota_Label"
 
 
 
 
 
 
 
 
 
 
641
 
642
  base = df.groupby([key_col, label_col], dropna=False).agg(
643
  n_total=("Indeks_Dasar_0_100", "size"),
@@ -660,9 +698,30 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
660
  base["target_total_68"] = pd.to_numeric(pd.Series(target_vals), errors="coerce")
661
  base["pop_total"] = pd.to_numeric(pd.Series(pop_vals), errors="coerce")
662
 
 
663
  m = base["pop_total"].isna() & base["target_total_68"].notna() & (base["target_total_68"] > 0)
664
  base.loc[m, "pop_total"] = base.loc[m, "target_total_68"] / float(FALLBACK_TARGET_RATIO)
665
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  base["faktor_penyesuaian"] = [
667
  faktor_penyesuaian_total(n, t)
668
  for n, t in zip(
@@ -675,7 +734,7 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
675
  (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else np.nan
676
  for n, p in zip(
677
  pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
678
- base["pop_total"].tolist()
679
  )
680
  ]
681
 
@@ -686,7 +745,6 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
686
  base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).round(0).astype(int)
687
  base["pop_total"] = pd.to_numeric(base["pop_total"], errors="coerce").fillna(0).round(0).astype(int)
688
  base["coverage_total_%"] = pd.to_numeric(base["coverage_total_%"], errors="coerce").fillna(0.0).round(2)
689
- # tetap seperti sebelumnya:
690
  base["faktor_penyesuaian"] = pd.to_numeric(base["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
691
 
692
  return base
@@ -1289,6 +1347,7 @@ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_to
1289
  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.")
1290
  lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0). Faktor wilayah dipakai untuk semua jenis.")
1291
  lines.append("Rumus keseluruhan wilayah (FIX): nilai keseluruhan wilayah diambil dari rata-rata 3 jenis (sekolah+umum+khusus) ÷ 3 (missing=0, tetap ÷3).")
 
1292
 
1293
  if summary_jenis is not None and not summary_jenis.empty:
1294
  lines.append("\nRingkasan (jenis + keseluruhan):")
@@ -1367,6 +1426,7 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
1367
  doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap ÷3; missing=0)")
1368
  doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
1369
  doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
 
1370
 
1371
  doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
1372
  show = summary_jenis.copy()
@@ -1457,7 +1517,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1457
  return _empty_outputs("Tidak ada data untuk filter ini.")
1458
 
1459
  # ==== PIPELINE BARU (KUNCI KONSISTENSI) ====
1460
- faktor_wilayah = build_faktor_wilayah(df, pop_kab, pop_prov, kew_value or "(Semua)")
1461
  agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah, pop_khusus, kew_value or "(Semua)")
1462
  agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah, kew_value or "(Semua)")
1463
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
@@ -1537,156 +1597,162 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1537
 
1538
  msg = (
1539
  f"✅ Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
1540
- f"jenis(agregat)={len(agg_jenis_full)} | FIX: Agregat Wilayah (Keseluruhan) = avg3 dari 3 jenis (÷3)"
 
 
 
1541
  )
1542
 
1543
  return (
1544
  kpi_md,
1545
- summary_jenis, agg_total, agg_jenis_view, detail_view, verif_total,
1546
- p_summary, p_total, p_jenis, p_detail, word_path,
1547
- fig_umum, fig_sekolah, fig_khusus,
1548
- msg, analysis_text
1549
  )
1550
-
1551
  except Exception as e:
1552
- return _empty_outputs(f"⚠️ Runtime error: {repr(e)}")
1553
 
1554
 
1555
  # ============================================================
1556
- # 16) UI (NO UPLOAD)
1557
  # ============================================================
1558
 
1559
- def ui_load(force=False):
1560
- df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=force)
1561
- if df_all is None or (isinstance(df_all, pd.DataFrame) and df_all.empty):
1562
- return (
1563
- None, None, None, None, None, {}, info,
1564
- gr.update(choices=["(Semua)"], value="(Semua)"),
1565
- gr.update(choices=["(Semua)"], value="(Semua)"),
1566
- gr.update(choices=["(Semua)"], value="(Semua)"),
1567
- )
1568
 
1569
- prov_vals = df_all["PROV_DISP"].dropna().astype(str).tolist()
1570
- prov_vals = [v for v in prov_vals if v and v.strip()]
1571
- prov_choices = ["(Semua)"] + sorted(set(prov_vals))
1572
 
1573
- kab_choices = ["(Semua)"] + sorted([x for x in df_all["KAB_DISP"].dropna().unique().tolist() if x])
1574
- kew_choices = ["(Semua)"] + sorted([x for x in df_all["KEW_NORM"].dropna().unique().tolist() if x])
1575
- default_kew = "PROVINSI" if "PROVINSI" in kew_choices else ("KAB/KOTA" if "KAB/KOTA" in kew_choices else "(Semua)")
 
1576
 
1577
- return (
1578
- df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
1579
- gr.update(choices=prov_choices, value="(Semua)"),
1580
- gr.update(choices=kab_choices, value="(Semua)"),
1581
- gr.update(choices=kew_choices, value=default_kew),
1582
- )
1583
 
1584
- def on_prov_change(prov_value):
1585
- df_all, _, _, _, _, _, _ = load_default_files(force=False)
1586
- if df_all is None or df_all.empty:
1587
  return gr.update(choices=["(Semua)"], value="(Semua)")
1588
- if prov_value is None or prov_value == "(Semua)":
1589
- vals = df_all["KAB_DISP"].dropna().unique().tolist()
1590
- else:
1591
- vals = df_all.loc[df_all["PROV_DISP"] == prov_value, "KAB_DISP"].dropna().unique().tolist()
1592
- vals = sorted([v for v in vals if v])
1593
- return gr.update(choices=["(Semua)"] + vals, value="(Semua)")
1594
-
1595
-
1596
- with gr.Blocks() as demo:
1597
- gr.Markdown(f"""
1598
- # IPLM 2025 — Final (Penyesuaian Berbasis Kecukupan Sampel 68%)
1599
- **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1600
- - `DATA_FILE` = **{DATA_FILE}**
1601
- - `POP_KAB` = **{POP_KAB}**
1602
- - `POP_PROV` = **{POP_PROV}**
1603
- - `POP_KHUSUS` = **{POP_KHUSUS}**
1604
-
1605
- **FIX UTAMA (konsistensi nilai):**
1606
- - **Agregat Wilayah (Keseluruhan) = rata-rata 3 jenis (sekolah+umum+khusus) ÷ 3 (missing=0, tetap ÷3)**
1607
- - Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
1608
- - KPI FINAL dashboard sumber dari Ringkasan
1609
- - Download Data Mentah = RAW hasil filter
1610
- - Kecukupan Sampel 68%: tanpa angka koma
1611
-
1612
- **UPDATE (tampilan):**
1613
- - target_total_68 & pop_total di faktor_wilayah = bilangan bulat
1614
- - coverage_total_% = 2 desimal
1615
- - Tabel "Agregat Wilayah × Jenis" ditampilkan hanya sampai Indeks_Dasar_Agregat_0_100
1616
- """)
1617
-
1618
- state_df = gr.State(None)
1619
- state_raw = gr.State(None)
1620
- state_pop_kab = gr.State(None)
1621
- state_pop_prov = gr.State(None)
1622
- state_pop_khusus = gr.State(None)
1623
- state_meta = gr.State({})
1624
-
1625
- info_box = gr.Markdown()
1626
 
1627
  with gr.Row():
1628
- dd_prov = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)")
1629
- dd_kab = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)")
1630
- dd_kew = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)")
1631
 
1632
- dd_prov.change(fn=on_prov_change, inputs=[dd_prov], outputs=dd_kab)
 
1633
 
1634
- run_btn = gr.Button("Jalankan Perhitungan")
1635
- msg_out = gr.Markdown()
 
1636
 
1637
- kpi_out = gr.Markdown()
1638
 
1639
- gr.Markdown("## Ringkasan (Jenis + Keseluruhan)")
1640
- out_summary = gr.DataFrame(interactive=False)
 
1641
 
1642
- gr.Markdown("## Agregat Wilayah (Keseluruhan) — FIX: avg3 dari 3 jenis")
1643
- out_agg_total = gr.DataFrame(interactive=False)
1644
 
1645
- gr.Markdown("## Agregat Wilayah × Jenis (Sekolah, Umum, Khusus) — (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
1646
- out_agg_jenis = gr.DataFrame(interactive=False)
 
1647
 
1648
- gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
1649
- out_detail = gr.DataFrame(interactive=False)
 
1650
 
1651
- gr.Markdown("## Kecukupan Sampel 68% (tanpa angka koma)")
1652
- out_verif = gr.DataFrame(interactive=False)
 
1653
 
1654
- gr.Markdown("## Bell Curve — per Jenis Perpustakaan (Indeks per Entitas)")
1655
- gr.Markdown("### Perpustakaan Umum")
1656
- bell_umum = gr.Plot(scale=1)
1657
 
1658
- gr.Markdown("### Perpustakaan Sekolah")
1659
- bell_sekolah = gr.Plot(scale=1)
 
 
1660
 
1661
- gr.Markdown("### Perpustakaan Khusus")
1662
- bell_khusus = gr.Plot(scale=1)
 
 
1663
 
1664
- gr.Markdown("## Analisis Otomatis (LLM)")
1665
- analysis_out = gr.Markdown()
 
1666
 
1667
- with gr.Row():
1668
- dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1669
- dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1670
- dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1671
- dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1672
- dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1673
 
1674
  run_btn.click(
1675
- fn=run_calc,
1676
- inputs=[dd_prov, dd_kab, dd_kew, state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta],
1677
  outputs=[
1678
- kpi_out,
1679
- out_summary, out_agg_total, out_agg_jenis, out_detail, out_verif,
1680
- dl_summary, dl_total, dl_jenis, dl_detail, dl_word,
1681
- bell_umum, bell_sekolah, bell_khusus,
1682
- msg_out, analysis_out
1683
  ]
1684
  )
1685
 
1686
- demo.load(
1687
- fn=lambda: ui_load(force=False),
1688
  inputs=[],
1689
- outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
1690
  )
1691
 
1692
- demo.launch()
 
 
 
 
 
 
36
  - coverage_total_% -> decimal 2 digit
37
  2) TABEL "Agregat Wilayah × Jenis" (UI) hanya sampai kolom Indeks_Dasar_Agregat_0_100
38
  (kolom setelah itu tidak ditampilkan)
39
+
40
+ ✅ UPDATE TAMBAHAN (SESUSAI PERMINTAAN TERAKHIR):
41
+ - KHUSUS (Data_populasi_perp_khusus.xlsx) DITAMBAHKAN ke pop_total & target_total_68 untuk WILAYAH (Keseluruhan)
42
+ -> caranya: pada faktor_wilayah, pop_total += Pop_Total_Jenis dan target_total_68 += Target68_Total_Jenis
43
+ -> tanpa menambah kolom baru khusus (hanya nilai pop_total & target_total_68 yang berubah)
44
  """
45
 
46
  import os
 
610
 
611
  # ============================================================
612
  # 6) FAKTOR WILAYAH (TOTAL) — hanya untuk faktor/target/pop/coverage
613
+ # ✅ UPDATE: tambah KHUSUS ke pop_total & target_total_68 (tanpa kolom baru)
614
  # ============================================================
615
 
616
+ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_prov: pd.DataFrame, pop_khusus: pd.DataFrame, kew_value: str):
617
  if df_filtered is None or df_filtered.empty:
618
  return pd.DataFrame()
619
 
 
628
  target_field = "Target68_Total"
629
  pop_field = "Pop_Total"
630
  name_field = "Kab_Kota_Label"
631
+ # khusus: by kab_key
632
+ pk_add = None
633
+ if pop_khusus is not None and not pop_khusus.empty:
634
+ pk_add = pop_khusus.copy()
635
+ pk_add["kab_key"] = pk_add["kab_key"].astype(str)
636
+ pk_add["Target68_Total_Jenis"] = pd.to_numeric(pk_add.get("Target68_Total_Jenis", 0), errors="coerce").fillna(0.0)
637
+ pk_add["Pop_Total_Jenis"] = pd.to_numeric(pk_add.get("Pop_Total_Jenis", 0), errors="coerce").fillna(0.0)
638
+ pk_add = pk_add.groupby("kab_key", as_index=True).agg(
639
+ add_target=("Target68_Total_Jenis", "sum"),
640
+ add_pop=("Pop_Total_Jenis", "sum")
641
+ )
642
  elif "PROV" in kew_norm:
643
  key_col = "prov_key"
644
  label_col = "PROV_DISP"
 
647
  target_field = "Target68_Total_Prov"
648
  pop_field = "Pop_Total_Prov"
649
  name_field = "Provinsi_Label"
650
+ # khusus: by prov_key (sum kab_key khusus per prov)
651
+ pk_add = None
652
+ if pop_khusus is not None and not pop_khusus.empty:
653
+ pk_add = pop_khusus.copy()
654
+ pk_add["prov_key"] = pk_add["prov_key"].astype(str)
655
+ pk_add["Target68_Total_Jenis"] = pd.to_numeric(pk_add.get("Target68_Total_Jenis", 0), errors="coerce").fillna(0.0)
656
+ pk_add["Pop_Total_Jenis"] = pd.to_numeric(pk_add.get("Pop_Total_Jenis", 0), errors="coerce").fillna(0.0)
657
+ pk_add = pk_add.groupby("prov_key", as_index=True).agg(
658
+ add_target=("Target68_Total_Jenis", "sum"),
659
+ add_pop=("Pop_Total_Jenis", "sum")
660
+ )
661
  else:
662
  key_col = "kab_key"
663
  label_col = "KAB_DISP"
 
666
  target_field = "Target68_Total"
667
  pop_field = "Pop_Total"
668
  name_field = "Kab_Kota_Label"
669
+ pk_add = None
670
+ if pop_khusus is not None and not pop_khusus.empty:
671
+ pk_add = pop_khusus.copy()
672
+ pk_add["kab_key"] = pk_add["kab_key"].astype(str)
673
+ pk_add["Target68_Total_Jenis"] = pd.to_numeric(pk_add.get("Target68_Total_Jenis", 0), errors="coerce").fillna(0.0)
674
+ pk_add["Pop_Total_Jenis"] = pd.to_numeric(pk_add.get("Pop_Total_Jenis", 0), errors="coerce").fillna(0.0)
675
+ pk_add = pk_add.groupby("kab_key", as_index=True).agg(
676
+ add_target=("Target68_Total_Jenis", "sum"),
677
+ add_pop=("Pop_Total_Jenis", "sum")
678
+ )
679
 
680
  base = df.groupby([key_col, label_col], dropna=False).agg(
681
  n_total=("Indeks_Dasar_0_100", "size"),
 
698
  base["target_total_68"] = pd.to_numeric(pd.Series(target_vals), errors="coerce")
699
  base["pop_total"] = pd.to_numeric(pd.Series(pop_vals), errors="coerce")
700
 
701
+ # fallback pop dari target (jika pop kosong)
702
  m = base["pop_total"].isna() & base["target_total_68"].notna() & (base["target_total_68"] > 0)
703
  base.loc[m, "pop_total"] = base.loc[m, "target_total_68"] / float(FALLBACK_TARGET_RATIO)
704
 
705
+ # ============================================================
706
+ # ✅ UPDATE TAMBAHAN: TAMBAHKAN POP/TARGET KHUSUS KE TOTAL WILAYAH
707
+ # tanpa menambah kolom baru khusus
708
+ # ============================================================
709
+ if "pk_add" in locals() and pk_add is not None and not pk_add.empty:
710
+ add_targets = []
711
+ add_pops = []
712
+ for _, r in base.iterrows():
713
+ gk = str(r["group_key"])
714
+ if gk in pk_add.index:
715
+ add_targets.append(float(pk_add.loc[gk, "add_target"]))
716
+ add_pops.append(float(pk_add.loc[gk, "add_pop"]))
717
+ else:
718
+ add_targets.append(0.0)
719
+ add_pops.append(0.0)
720
+
721
+ base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0.0) + pd.Series(add_targets, index=base.index)
722
+ base["pop_total"] = pd.to_numeric(base["pop_total"], errors="coerce").fillna(0.0) + pd.Series(add_pops, index=base.index)
723
+
724
+ # faktor & coverage (pakai total yang sudah terupdate)
725
  base["faktor_penyesuaian"] = [
726
  faktor_penyesuaian_total(n, t)
727
  for n, t in zip(
 
734
  (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else np.nan
735
  for n, p in zip(
736
  pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
737
+ pd.to_numeric(base["pop_total"], errors="coerce").tolist()
738
  )
739
  ]
740
 
 
745
  base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).round(0).astype(int)
746
  base["pop_total"] = pd.to_numeric(base["pop_total"], errors="coerce").fillna(0).round(0).astype(int)
747
  base["coverage_total_%"] = pd.to_numeric(base["coverage_total_%"], errors="coerce").fillna(0.0).round(2)
 
748
  base["faktor_penyesuaian"] = pd.to_numeric(base["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
749
 
750
  return base
 
1347
  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.")
1348
  lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0). Faktor wilayah dipakai untuk semua jenis.")
1349
  lines.append("Rumus keseluruhan wilayah (FIX): nilai keseluruhan wilayah diambil dari rata-rata 3 jenis (sekolah+umum+khusus) ÷ 3 (missing=0, tetap ÷3).")
1350
+ lines.append("Catatan update: pop_total & target_total_68 wilayah telah ditambah komponen KHUSUS dari Data_populasi_perp_khusus.xlsx (tanpa kolom tambahan).")
1351
 
1352
  if summary_jenis is not None and not summary_jenis.empty:
1353
  lines.append("\nRingkasan (jenis + keseluruhan):")
 
1426
  doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap ÷3; missing=0)")
1427
  doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
1428
  doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
1429
+ doc.add_paragraph("Catatan: pop_total & target_total_68 wilayah telah ditambah komponen KHUSUS dari Data_populasi_perp_khusus.xlsx.")
1430
 
1431
  doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
1432
  show = summary_jenis.copy()
 
1517
  return _empty_outputs("Tidak ada data untuk filter ini.")
1518
 
1519
  # ==== PIPELINE BARU (KUNCI KONSISTENSI) ====
1520
+ faktor_wilayah = build_faktor_wilayah(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1521
  agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah, pop_khusus, kew_value or "(Semua)")
1522
  agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah, kew_value or "(Semua)")
1523
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
 
1597
 
1598
  msg = (
1599
  f"✅ Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
1600
+ f"jenis(agregat)={len(agg_jenis_full)} | FIX: Agregat Wilayah (Keseluruhan) = avg3 dari 3 jenis (÷3) | "
1601
+ f"UPDATE: pop_total & target_total_68 wilayah sudah ditambah KHUSUS (tanpa kolom baru) | "
1602
+ f"DISPLAY: faktor_wilayah target/pop integer, coverage 2 desimal | "
1603
+ f"UI: tabel Agregat Wilayah×Jenis hanya sampai Indeks_Dasar_Agregat_0_100"
1604
  )
1605
 
1606
  return (
1607
  kpi_md,
1608
+ summary_jenis, agg_total, agg_jenis_view, faktor_wilayah, verif_total,
1609
+ p_summary, p_total, p_detail, p_verif, p_jenis,
1610
+ fig_sekolah, fig_umum, fig_khusus,
1611
+ msg, analysis_text, word_path
1612
  )
 
1613
  except Exception as e:
1614
+ return _empty_outputs(f"⚠️ Error: {repr(e)}")
1615
 
1616
 
1617
  # ============================================================
1618
+ # 16) GRADIO UI (TIDAK DIUBAH, HANYA MENGGUNAKAN OUTPUT DI ATAS)
1619
  # ============================================================
1620
 
1621
+ def init_choices(df_raw: pd.DataFrame):
1622
+ if df_raw is None or df_raw.empty:
1623
+ return ["(Semua)"], ["(Semua)"], ["(Semua)"]
 
 
 
 
 
 
1624
 
1625
+ provs = sorted([p for p in df_raw["PROV_DISP"].dropna().unique().tolist() if str(p).strip() != ""])
1626
+ kabs = sorted([k for k in df_raw["KAB_DISP"].dropna().unique().tolist() if str(k).strip() != ""])
1627
+ kews = sorted([k for k in df_raw["KEW_NORM"].dropna().unique().tolist() if str(k).strip() != ""])
1628
 
1629
+ provs = ["(Semua)"] + provs
1630
+ kabs = ["(Semua)"] + kabs
1631
+ kews = ["(Semua)"] + kews
1632
+ return provs, kabs, kews
1633
 
 
 
 
 
 
 
1634
 
1635
+ def _filter_kab_choices(df_raw: pd.DataFrame, prov_value: str):
1636
+ if df_raw is None or df_raw.empty:
 
1637
  return gr.update(choices=["(Semua)"], value="(Semua)")
1638
+ if not prov_value or prov_value == "(Semua)":
1639
+ kabs = sorted([k for k in df_raw["KAB_DISP"].dropna().unique().tolist() if str(k).strip() != ""])
1640
+ return gr.update(choices=["(Semua)"] + kabs, value="(Semua)")
1641
+ sub = df_raw[df_raw["PROV_DISP"] == prov_value]
1642
+ kabs = sorted([k for k in sub["KAB_DISP"].dropna().unique().tolist() if str(k).strip() != ""])
1643
+ return gr.update(choices=["(Semua)"] + kabs, value="(Semua)")
1644
+
1645
+
1646
+ with gr.Blocks(title="IPLM 2025 — FINAL (NO UPLOAD)") as demo:
1647
+
1648
+ # Load default (cache) once at startup
1649
+ df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=False)
1650
+ prov_choices, kab_choices, kew_choices = init_choices(df_raw)
1651
+
1652
+ gr.Markdown(
1653
+ """
1654
+ # IPLM 2025 — Dashboard FINAL (NO UPLOAD)
1655
+ - Agregat wilayah **keseluruhan** = **rata-rata 3 jenis (sekolah+umum+khusus) ÷ 3** (missing=0, tetap ÷3)
1656
+ - Penyesuaian 68% = **min(total_terkumpul / target_total_68, 1.0)** pada level wilayah (faktor sama untuk semua jenis)
1657
+ - **UPDATE**: `pop_total` & `target_total_68` wilayah sudah **ditambah komponen KHUSUS** dari `Data_populasi_perp_khusus.xlsx` (tanpa kolom baru).
1658
+ - **DISPLAY**: tabel faktor_wilayah target/pop integer, coverage 2 desimal.
1659
+ """
1660
+ )
1661
+
1662
+ info_box = gr.HTML(value=info or "")
 
 
 
 
 
 
 
 
 
 
 
 
 
1663
 
1664
  with gr.Row():
1665
+ prov_dd = gr.Dropdown(choices=prov_choices, value="(Semua)", label="Filter Provinsi")
1666
+ kab_dd = gr.Dropdown(choices=kab_choices, value="(Semua)", label="Filter Kab/Kota")
1667
+ kew_dd = gr.Dropdown(choices=kew_choices, value="(Semua)", label="Filter Kewenangan")
1668
 
1669
+ # Hero KPI
1670
+ kpi_md = gr.HTML(value="")
1671
 
1672
+ with gr.Row():
1673
+ run_btn = gr.Button("Hitung / Refresh", variant="primary")
1674
+ reload_btn = gr.Button("Reload File (paksa baca ulang)", variant="secondary")
1675
 
1676
+ status = gr.Markdown(value="")
1677
 
1678
+ with gr.Tabs():
1679
+ with gr.Tab("Ringkasan (Jenis + Keseluruhan)"):
1680
+ tbl_summary = gr.Dataframe(label="Ringkasan (selalu 4 baris: sekolah, umum, khusus, keseluruhan)")
1681
 
1682
+ dl_summary = gr.File(label="Download Ringkasan (Excel)")
 
1683
 
1684
+ with gr.Tab("Agregat Wilayah (Keseluruhan)"):
1685
+ tbl_total = gr.Dataframe(label="Agregat Wilayah (Keseluruhan) — FIX avg3 ÷3")
1686
+ dl_total = gr.File(label="Download Agregat Wilayah Keseluruhan (Excel)")
1687
 
1688
+ with gr.Tab("Agregat Wilayah × Jenis"):
1689
+ # UI tabel hanya sampai Indeks_Dasar_Agregat_0_100 (sudah dibuat di run_calc)
1690
+ tbl_jenis = gr.Dataframe(label="Agregat Wilayah × Jenis (UI dipotong sampai Indeks_Dasar_Agregat_0_100)")
1691
 
1692
+ with gr.Tab("Faktor Wilayah (Target 68%)"):
1693
+ tbl_faktor = gr.Dataframe(label="Faktor Wilayah: target_total_68 (int), pop_total (int), coverage_total_% (2 desimal)")
1694
+ dl_verif = gr.File(label="Download Verifikasi 68% (Excel)")
1695
 
1696
+ with gr.Tab("Detail Entitas (Final menempel wilayah)"):
1697
+ tbl_detail = gr.Dataframe(label="Detail Entitas (Indeks_Final_0_100 menempel dari Agregat Wilayah Keseluruhan)")
1698
+ dl_detail = gr.File(label="Download Detail Entitas (Excel)")
1699
 
1700
+ with gr.Tab("Bell Curve (per Jenis, per Entitas)"):
1701
+ fig_sekolah = gr.Plot(label="Sekolah")
1702
+ fig_umum = gr.Plot(label="Umum")
1703
+ fig_khusus = gr.Plot(label="Khusus")
1704
 
1705
+ with gr.Tab("LLM Analysis + Word"):
1706
+ llm_text = gr.Textbox(label="Analisis Naratif", lines=20)
1707
+ dl_raw = gr.File(label="Download RAW Data (Excel)")
1708
+ dl_word = gr.File(label="Download Word Report (.docx)")
1709
 
1710
+ # ============================================================
1711
+ # Events
1712
+ # ============================================================
1713
 
1714
+ def _run(prov, kab, kew):
1715
+ return run_calc(prov, kab, kew, df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta)
1716
+
1717
+ def _reload():
1718
+ nonlocal df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info
1719
+ df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=True)
1720
+ provs, kabs, kews = init_choices(df_raw)
1721
+ return (
1722
+ gr.update(value=info or ""),
1723
+ gr.update(choices=provs, value="(Semua)"),
1724
+ gr.update(choices=kabs, value="(Semua)"),
1725
+ gr.update(choices=kews, value="(Semua)"),
1726
+ "✅ Reload selesai."
1727
+ )
1728
+
1729
+ prov_dd.change(
1730
+ fn=lambda p: _filter_kab_choices(df_raw, p),
1731
+ inputs=[prov_dd],
1732
+ outputs=[kab_dd]
1733
+ )
1734
 
1735
  run_btn.click(
1736
+ fn=_run,
1737
+ inputs=[prov_dd, kab_dd, kew_dd],
1738
  outputs=[
1739
+ kpi_md,
1740
+ tbl_summary, tbl_total, tbl_jenis, tbl_faktor, dl_verif, # note: dl_verif diisi dari path verif
1741
+ dl_summary, dl_total, dl_detail, dl_verif, dl_raw,
1742
+ fig_sekolah, fig_umum, fig_khusus,
1743
+ status, llm_text, dl_word
1744
  ]
1745
  )
1746
 
1747
+ reload_btn.click(
1748
+ fn=_reload,
1749
  inputs=[],
1750
+ outputs=[info_box, prov_dd, kab_dd, kew_dd, status]
1751
  )
1752
 
1753
+ # ============================================================
1754
+ # 17) LAUNCH
1755
+ # ============================================================
1756
+
1757
+ if __name__ == "__main__":
1758
+ demo.queue().launch(share=True)