irhamni commited on
Commit
aa8a3d6
·
verified ·
1 Parent(s): ec7294b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -189
app.py CHANGED
@@ -36,11 +36,6 @@ 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
- ✅ 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,10 +605,9 @@ def load_default_files(force=False):
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,17 +622,6 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
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,17 +630,6 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
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,16 +638,6 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
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,30 +660,9 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
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,7 +675,7 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
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,6 +686,7 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
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,7 +1289,6 @@ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_to
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,7 +1367,6 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
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,7 +1457,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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,168 +1537,156 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
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(
1716
- prov, kab, kew,
1717
- df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta
1718
- )
1719
-
1720
- def _reload():
1721
- global df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info
1722
 
1723
- df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=True)
1724
- provs, kabs, kews = init_choices(df_raw)
1725
 
1726
- return (
1727
- gr.update(value=info or ""),
1728
- gr.update(choices=provs, value="(Semua)"),
1729
- gr.update(choices=kabs, value="(Semua)"),
1730
- gr.update(choices=kews, value="(Semua)"),
1731
- "✅ Reload selesai."
1732
- )
1733
 
1734
- prov_dd.change(
1735
- fn=lambda p: _filter_kab_choices(df_raw, p),
1736
- inputs=[prov_dd],
1737
- outputs=[kab_dd]
1738
- )
 
1739
 
1740
  run_btn.click(
1741
- fn=_run,
1742
- inputs=[prov_dd, kab_dd, kew_dd],
1743
  outputs=[
1744
- kpi_md,
1745
- tbl_summary, tbl_total, tbl_jenis, tbl_faktor, dl_verif,
1746
- dl_summary, dl_total, dl_detail, dl_verif, dl_raw,
1747
- fig_sekolah, fig_umum, fig_khusus,
1748
- status, llm_text, dl_word
1749
  ]
1750
  )
1751
 
1752
- reload_btn.click(
1753
- fn=_reload,
1754
  inputs=[],
1755
- outputs=[info_box, prov_dd, kab_dd, kew_dd, status]
1756
  )
1757
 
1758
-
1759
- # ============================================================
1760
- # 17) LAUNCH
1761
- # ============================================================
1762
-
1763
- if __name__ == "__main__":
1764
- demo.queue().launch(share=True)
 
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
 
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
  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
  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
  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
  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
  (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
  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
  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
  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
  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
 
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()