irhamni commited on
Commit
ba3da9f
·
verified ·
1 Parent(s): 14cdebf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -174
app.py CHANGED
@@ -605,9 +605,17 @@ 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, pop_khusus: pd.DataFrame, kew_value: str):
 
 
 
 
 
 
611
  if df_filtered is None or df_filtered.empty:
612
  return pd.DataFrame()
613
 
@@ -622,7 +630,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
- khusus_group_key = "kab_key"
 
 
 
 
 
 
 
 
 
 
626
  elif "PROV" in kew_norm:
627
  key_col = "prov_key"
628
  label_col = "PROV_DISP"
@@ -631,7 +649,17 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
631
  target_field = "Target68_Total_Prov"
632
  pop_field = "Pop_Total_Prov"
633
  name_field = "Provinsi_Label"
634
- khusus_group_key = "prov_key"
 
 
 
 
 
 
 
 
 
 
635
  else:
636
  key_col = "kab_key"
637
  label_col = "KAB_DISP"
@@ -640,22 +668,59 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
640
  target_field = "Target68_Total"
641
  pop_field = "Pop_Total"
642
  name_field = "Kab_Kota_Label"
643
- khusus_group_key = "kab_key"
 
 
 
 
 
 
 
 
644
 
645
  base = df.groupby([key_col, label_col], dropna=False).agg(
646
  n_total=("Indeks_Dasar_0_100", "size"),
647
  ).reset_index().rename(columns={key_col: "group_key", label_col: label_name})
648
 
649
- # --- ambil target & pop dari POP_KAB / POP_PROV (existing) ---
650
  target_vals, pop_vals, label_fix = [], [], []
651
  for _, r in base.iterrows():
652
  gk = r["group_key"]
 
 
653
  if gk in pop.index:
654
  target_total = pop.loc[gk, target_field] if target_field in pop.columns else np.nan
655
  pop_total = pop.loc[gk, pop_field] if pop_field in pop.columns else np.nan
656
  nm = pop.loc[gk, name_field] if name_field in pop.columns else r[label_name]
657
  else:
658
  target_total, pop_total, nm = np.nan, np.nan, r[label_name]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  target_vals.append(target_total)
660
  pop_vals.append(pop_total)
661
  label_fix.append(nm)
@@ -664,59 +729,31 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
664
  base["target_total_68"] = pd.to_numeric(pd.Series(target_vals), errors="coerce")
665
  base["pop_total"] = pd.to_numeric(pd.Series(pop_vals), errors="coerce")
666
 
667
- # fallback pop dari target kalau pop kosong
668
  m = base["pop_total"].isna() & base["target_total_68"].notna() & (base["target_total_68"] > 0)
669
  base.loc[m, "pop_total"] = base.loc[m, "target_total_68"] / float(FALLBACK_TARGET_RATIO)
670
 
671
- # =========================================================
672
- # ✅ UPDATE LO: Tambahkan komponen KHUSUS dari POP_KHUSUS
673
- # target_total_68 += target_khusus
674
- # pop_total += pop_khusus
675
- # =========================================================
676
- if (pop_khusus is not None) and (not pop_khusus.empty):
677
- pk = pop_khusus.copy()
678
-
679
- # pastikan numeric
680
- pk["Target68_Total_Jenis"] = pd.to_numeric(pk.get("Target68_Total_Jenis", 0), errors="coerce").fillna(0.0)
681
- pk["Pop_Total_Jenis"] = pd.to_numeric(pk.get("Pop_Total_Jenis", 0), errors="coerce").fillna(0.0)
682
-
683
- if khusus_group_key in pk.columns:
684
- add_df = pk.groupby(khusus_group_key, as_index=False).agg(
685
- add_target=("Target68_Total_Jenis", "sum"),
686
- add_pop=("Pop_Total_Jenis", "sum"),
687
- ).rename(columns={khusus_group_key: "group_key"})
688
-
689
- base = base.merge(add_df, on="group_key", how="left")
690
- base["add_target"] = pd.to_numeric(base.get("add_target", 0), errors="coerce").fillna(0.0)
691
- base["add_pop"] = pd.to_numeric(base.get("add_pop", 0), errors="coerce").fillna(0.0)
692
-
693
- base["target_total_68"] = pd.to_numeric(base.get("target_total_68", 0), errors="coerce").fillna(0.0) + base["add_target"]
694
- base["pop_total"] = pd.to_numeric(base.get("pop_total", 0), errors="coerce").fillna(0.0) + base["add_pop"]
695
-
696
- base = base.drop(columns=[c for c in ["add_target", "add_pop"] if c in base.columns])
697
-
698
- # faktor penyesuaian pakai TOTAL baru
699
  base["faktor_penyesuaian"] = [
700
  faktor_penyesuaian_total(n, t)
701
  for n, t in zip(
702
  pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
703
- pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).astype(float).tolist()
704
  )
705
  ]
706
 
707
- # coverage pakai TOTAL baru
708
  base["coverage_total_%"] = [
709
  (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else np.nan
710
  for n, p in zip(
711
  pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
712
- pd.to_numeric(base["pop_total"], errors="coerce").fillna(0).astype(float).tolist()
713
  )
714
  ]
715
 
716
- # ===== display fix (sesuai permintaan lo sebelumnya) =====
717
  base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).round(0).astype(int)
718
- base["pop_total"] = pd.to_numeric(base["pop_total"], errors="coerce").fillna(0).round(0).astype(int)
719
- base["coverage_total_%"]= pd.to_numeric(base["coverage_total_%"], errors="coerce").fillna(0.0).round(2)
720
  base["faktor_penyesuaian"] = pd.to_numeric(base["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
721
 
722
  return base
@@ -726,148 +763,110 @@ def build_faktor_wilayah(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_p
726
  # 7) AGREGAT WILAYAH × JENIS (Final pakai faktor wilayah)
727
  # ============================================================
728
 
729
- def build_agg_wilayah_total_from_jenis(
730
- agg_jenis: pd.DataFrame,
731
- faktor_wilayah: pd.DataFrame,
732
- pop_khusus: pd.DataFrame, # <-- PATCH: tambah
733
- kew_value: str
734
- ):
735
- if agg_jenis is None or agg_jenis.empty:
736
  return pd.DataFrame()
737
 
738
  kew_norm = str(kew_value or "").upper()
739
- label_name = "Kab/Kota" if ("KAB" in kew_norm or "KOTA" in kew_norm) else ("Provinsi" if "PROV" in kew_norm else "Kab/Kota")
740
-
741
- jenis_list = ["sekolah", "umum", "khusus"]
742
-
743
- a = agg_jenis.copy()
744
- a["Jenis"] = a["Jenis"].astype(str).str.lower().str.strip()
745
-
746
- base_keys = a[["group_key", label_name]].drop_duplicates()
747
-
748
- full = base_keys.assign(_tmp=1).merge(
749
- pd.DataFrame({"Jenis": jenis_list, "_tmp": 1}),
750
- on="_tmp"
751
- ).drop(columns="_tmp")
752
 
753
- cols_need = [
754
- "Jumlah",
755
- "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
756
- "Rata2_dim_kepatuhan","Rata2_dim_kinerja",
757
- "Indeks_Dasar_Agregat_0_100",
758
- "Indeks_Final_Agregat_0_100",
759
- ]
760
- cols_present = [c for c in cols_need if c in a.columns]
 
 
 
 
761
 
762
- full = full.merge(
763
- a[["group_key", label_name, "Jenis"] + cols_present],
764
- on=["group_key", label_name, "Jenis"],
765
- how="left"
766
- )
767
 
768
- # missing=0 (kunci avg3 tetap ÷3)
769
- for c in cols_present:
770
- full[c] = pd.to_numeric(full[c], errors="coerce").fillna(0.0)
 
 
 
 
 
 
 
771
 
772
- # keseluruhan wilayah = avg3 dari 3 jenis
773
- out = full.groupby(["group_key", label_name], as_index=False).agg(
774
- n_total=("Jumlah", "sum"),
775
- Rata2_sub_koleksi=("Rata2_sub_koleksi", "mean"),
776
- Rata2_sub_sdm=("Rata2_sub_sdm", "mean"),
777
- Rata2_sub_pelayanan=("Rata2_sub_pelayanan", "mean"),
778
- Rata2_sub_pengelolaan=("Rata2_sub_pengelolaan", "mean"),
779
- Rata2_dim_kepatuhan=("Rata2_dim_kepatuhan", "mean"),
780
- Rata2_dim_kinerja=("Rata2_dim_kinerja", "mean"),
781
- Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_Agregat_0_100", "mean"),
782
- Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
783
- )
784
 
785
- # tempel faktor/target/pop/coverage (informasi verifikasi)
786
- if faktor_wilayah is not None and not faktor_wilayah.empty:
 
 
 
 
 
787
  fw = faktor_wilayah.copy()
788
- keep_fw = ["group_key", label_name, "target_total_68", "pop_total", "faktor_penyesuaian", "coverage_total_%"]
789
- keep_fw = [c for c in keep_fw if c in fw.columns]
790
- out = out.merge(fw[keep_fw], on=["group_key", label_name], how="left")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
791
 
792
- # =========================================================
793
- # PATCH UTAMA:
794
- # Tambahkan target_total_68 & pop_total dari POP_KHUSUS
795
- # (tanpa kolom khusus tambahan)
796
- # =========================================================
797
- if (pop_khusus is not None) and (not pop_khusus.empty):
798
- pk = pop_khusus.copy()
799
-
800
- # pastikan numerik
801
- for c in ["Target68_Total_Jenis", "Pop_Total_Jenis"]:
802
- if c in pk.columns:
803
- pk[c] = pd.to_numeric(pk[c], errors="coerce").fillna(0.0)
804
-
805
- add_df = None
806
-
807
- # KAB/KOTA: join langsung by kab_key == group_key
808
- if ("KAB" in kew_norm or "KOTA" in kew_norm):
809
- if "kab_key" in pk.columns:
810
- add_df = pk.groupby("kab_key", as_index=False).agg(
811
- add_target=("Target68_Total_Jenis", "sum"),
812
- add_pop=("Pop_Total_Jenis", "sum"),
813
- ).rename(columns={"kab_key": "group_key"})
814
-
815
- # PROVINSI: agregasi by prov_key == group_key
816
- elif ("PROV" in kew_norm):
817
- if "prov_key" in pk.columns:
818
- add_df = pk.groupby("prov_key", as_index=False).agg(
819
- add_target=("Target68_Total_Jenis", "sum"),
820
- add_pop=("Pop_Total_Jenis", "sum"),
821
- ).rename(columns={"prov_key": "group_key"})
822
-
823
- if add_df is not None and not add_df.empty:
824
- out = out.merge(add_df, on="group_key", how="left")
825
- out["add_target"] = pd.to_numeric(out.get("add_target", 0), errors="coerce").fillna(0.0)
826
- out["add_pop"] = pd.to_numeric(out.get("add_pop", 0), errors="coerce").fillna(0.0)
827
-
828
- # tambah ke total existing (kalau belum ada kolomnya -> dianggap 0)
829
- out["target_total_68"] = pd.to_numeric(out.get("target_total_68", 0), errors="coerce").fillna(0.0) + out["add_target"]
830
- out["pop_total"] = pd.to_numeric(out.get("pop_total", 0), errors="coerce").fillna(0.0) + out["add_pop"]
831
-
832
- # opsional tapi konsisten: update coverage & faktor berdasarkan total baru (DI TABEL INI SAJA)
833
- out["coverage_total_%"] = [
834
- (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else 0.0
835
- for n, p in zip(
836
- pd.to_numeric(out.get("n_total", 0), errors="coerce").fillna(0).astype(float).tolist(),
837
- pd.to_numeric(out.get("pop_total", 0), errors="coerce").fillna(0).astype(float).tolist()
838
- )
839
- ]
840
- out["faktor_penyesuaian"] = [
841
- faktor_penyesuaian_total(n, t)
842
- for n, t in zip(
843
- pd.to_numeric(out.get("n_total", 0), errors="coerce").fillna(0).astype(float).tolist(),
844
- pd.to_numeric(out.get("target_total_68", 0), errors="coerce").fillna(0).astype(float).tolist()
845
- )
846
- ]
847
 
848
- # bersihkan kolom helper
849
- out = out.drop(columns=[c for c in ["add_target", "add_pop"] if c in out.columns])
850
 
851
- # rounding (yang sudah ada sebelumnya)
852
  for c in [
853
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
854
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
855
  ]:
856
- if c in out.columns:
857
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(3)
858
 
859
- for c in ["Indeks_Dasar_Agregat_0_100","Indeks_Final_Wilayah_0_100"]:
860
- if c in out.columns:
861
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
862
 
863
- if "faktor_penyesuaian" in out.columns:
864
- out["faktor_penyesuaian"] = pd.to_numeric(out["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
 
865
 
866
- for c in ["target_total_68","pop_total","coverage_total_%"]:
867
- if c in out.columns:
868
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0)
 
 
869
 
870
- return out
871
 
872
  # ============================================================
873
  # 8) AGREGAT WILAYAH (KESELURUHAN) — RUMUS BARU: AVG3 dari 3 jenis
@@ -1496,17 +1495,6 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
1496
  # 15) CORE RUN
1497
  # ============================================================
1498
 
1499
- def _empty_outputs(msg="⚠️ Data belum siap."):
1500
- empty = pd.DataFrame()
1501
- empty_fig = go.Figure()
1502
- return (
1503
- "", # kpi_md
1504
- empty, empty, empty, empty, empty,
1505
- None, None, None, None, None,
1506
- empty_fig, empty_fig, empty_fig,
1507
- msg, "Analisis belum tersedia."
1508
- )
1509
-
1510
  def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta):
1511
  try:
1512
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
@@ -1525,9 +1513,10 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1525
  return _empty_outputs("Tidak ada data untuk filter ini.")
1526
 
1527
  # ==== PIPELINE BARU (KUNCI KONSISTENSI) ====
 
1528
  faktor_wilayah = build_faktor_wilayah(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1529
  agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah, pop_khusus, kew_value or "(Semua)")
1530
- agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah, pop_khusus, kew_value or "(Semua)")
1531
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1532
  verif_total = build_verif_total(faktor_wilayah) # tanpa koma
1533
  detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
@@ -1551,6 +1540,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1551
  cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1552
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1553
 
 
1554
  # FILTER RAW DOWNLOAD (df_raw)
1555
  raw = df_raw.copy()
1556
  if prov_value and prov_value != "(Semua)":
 
605
 
606
  # ============================================================
607
  # 6) FAKTOR WILAYAH (TOTAL) — hanya untuk faktor/target/pop/coverage
608
+ # UPDATE: total wilayah (pop_total & target_total_68) ditambah kontribusi POP_KHUSUS
609
+ # TANPA bikin kolom khusus baru (langsung dijumlahkan)
610
  # ============================================================
611
 
612
+ def build_faktor_wilayah(
613
+ df_filtered: pd.DataFrame,
614
+ pop_kab: pd.DataFrame,
615
+ pop_prov: pd.DataFrame,
616
+ pop_khusus: pd.DataFrame,
617
+ kew_value: str
618
+ ):
619
  if df_filtered is None or df_filtered.empty:
620
  return pd.DataFrame()
621
 
 
630
  target_field = "Target68_Total"
631
  pop_field = "Pop_Total"
632
  name_field = "Kab_Kota_Label"
633
+
634
+ # khs (kab) -> pakai kab_key
635
+ khs = pop_khusus.copy() if (pop_khusus is not None and not pop_khusus.empty) else pd.DataFrame()
636
+ if not khs.empty and "kab_key" in khs.columns:
637
+ khs_kab = khs.groupby("kab_key", as_index=False).agg({
638
+ "Target68_Total_Jenis": "sum",
639
+ "Pop_Total_Jenis": "sum"
640
+ }).set_index("kab_key")
641
+ else:
642
+ khs_kab = pd.DataFrame().set_index(pd.Index([]))
643
+
644
  elif "PROV" in kew_norm:
645
  key_col = "prov_key"
646
  label_col = "PROV_DISP"
 
649
  target_field = "Target68_Total_Prov"
650
  pop_field = "Pop_Total_Prov"
651
  name_field = "Provinsi_Label"
652
+
653
+ # khs (prov) -> agregasi dari pop_khusus ke prov_key
654
+ khs = pop_khusus.copy() if (pop_khusus is not None and not pop_khusus.empty) else pd.DataFrame()
655
+ if not khs.empty and "prov_key" in khs.columns:
656
+ khs_prov = khs.groupby("prov_key", as_index=False).agg({
657
+ "Target68_Total_Jenis": "sum",
658
+ "Pop_Total_Jenis": "sum"
659
+ }).set_index("prov_key")
660
+ else:
661
+ khs_prov = pd.DataFrame().set_index(pd.Index([]))
662
+
663
  else:
664
  key_col = "kab_key"
665
  label_col = "KAB_DISP"
 
668
  target_field = "Target68_Total"
669
  pop_field = "Pop_Total"
670
  name_field = "Kab_Kota_Label"
671
+
672
+ khs = pop_khusus.copy() if (pop_khusus is not None and not pop_khusus.empty) else pd.DataFrame()
673
+ if not khs.empty and "kab_key" in khs.columns:
674
+ khs_kab = khs.groupby("kab_key", as_index=False).agg({
675
+ "Target68_Total_Jenis": "sum",
676
+ "Pop_Total_Jenis": "sum"
677
+ }).set_index("kab_key")
678
+ else:
679
+ khs_kab = pd.DataFrame().set_index(pd.Index([]))
680
 
681
  base = df.groupby([key_col, label_col], dropna=False).agg(
682
  n_total=("Indeks_Dasar_0_100", "size"),
683
  ).reset_index().rename(columns={key_col: "group_key", label_col: label_name})
684
 
 
685
  target_vals, pop_vals, label_fix = [], [], []
686
  for _, r in base.iterrows():
687
  gk = r["group_key"]
688
+
689
+ # ===== ambil target/pop dari POP utama (kab/prov) =====
690
  if gk in pop.index:
691
  target_total = pop.loc[gk, target_field] if target_field in pop.columns else np.nan
692
  pop_total = pop.loc[gk, pop_field] if pop_field in pop.columns else np.nan
693
  nm = pop.loc[gk, name_field] if name_field in pop.columns else r[label_name]
694
  else:
695
  target_total, pop_total, nm = np.nan, np.nan, r[label_name]
696
+
697
+ # ===== UPDATE: tambah target/pop dari POP_KHUSUS (langsung dijumlah) =====
698
+ add_target, add_pop = 0.0, 0.0
699
+ if "PROV" in kew_norm:
700
+ if "khs_prov" in locals() and (gk in khs_prov.index):
701
+ add_target = khs_prov.loc[gk, "Target68_Total_Jenis"] if "Target68_Total_Jenis" in khs_prov.columns else 0.0
702
+ add_pop = khs_prov.loc[gk, "Pop_Total_Jenis"] if "Pop_Total_Jenis" in khs_prov.columns else 0.0
703
+ else:
704
+ if "khs_kab" in locals() and (gk in khs_kab.index):
705
+ add_target = khs_kab.loc[gk, "Target68_Total_Jenis"] if "Target68_Total_Jenis" in khs_kab.columns else 0.0
706
+ add_pop = khs_kab.loc[gk, "Pop_Total_Jenis"] if "Pop_Total_Jenis" in khs_kab.columns else 0.0
707
+
708
+ try:
709
+ add_target = float(pd.to_numeric(add_target, errors="coerce") or 0.0)
710
+ except Exception:
711
+ add_target = 0.0
712
+ try:
713
+ add_pop = float(pd.to_numeric(add_pop, errors="coerce") or 0.0)
714
+ except Exception:
715
+ add_pop = 0.0
716
+
717
+ # jumlahkan (kalau NaN -> treat 0 dulu)
718
+ t0 = float(pd.to_numeric(target_total, errors="coerce")) if pd.notna(target_total) else 0.0
719
+ p0 = float(pd.to_numeric(pop_total, errors="coerce")) if pd.notna(pop_total) else 0.0
720
+
721
+ target_total = t0 + add_target
722
+ pop_total = p0 + add_pop
723
+
724
  target_vals.append(target_total)
725
  pop_vals.append(pop_total)
726
  label_fix.append(nm)
 
729
  base["target_total_68"] = pd.to_numeric(pd.Series(target_vals), errors="coerce")
730
  base["pop_total"] = pd.to_numeric(pd.Series(pop_vals), errors="coerce")
731
 
732
+ # fallback pop_total bila kosong tapi target ada
733
  m = base["pop_total"].isna() & base["target_total_68"].notna() & (base["target_total_68"] > 0)
734
  base.loc[m, "pop_total"] = base.loc[m, "target_total_68"] / float(FALLBACK_TARGET_RATIO)
735
 
736
+ # faktor pakai target_total_68 yang SUDAH ditambah khusus
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  base["faktor_penyesuaian"] = [
738
  faktor_penyesuaian_total(n, t)
739
  for n, t in zip(
740
  pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
741
+ pd.to_numeric(base["target_total_68"], errors="coerce").tolist()
742
  )
743
  ]
744
 
 
745
  base["coverage_total_%"] = [
746
  (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else np.nan
747
  for n, p in zip(
748
  pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
749
+ base["pop_total"].tolist()
750
  )
751
  ]
752
 
753
+ # ====== UPDATE SESUAI PERMINTAAN (DISPLAY) ======
754
  base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).round(0).astype(int)
755
+ base["pop_total"] = pd.to_numeric(base["pop_total"], errors="coerce").fillna(0).round(0).astype(int)
756
+ base["coverage_total_%"] = pd.to_numeric(base["coverage_total_%"], errors="coerce").fillna(0.0).round(2)
757
  base["faktor_penyesuaian"] = pd.to_numeric(base["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
758
 
759
  return base
 
763
  # 7) AGREGAT WILAYAH × JENIS (Final pakai faktor wilayah)
764
  # ============================================================
765
 
766
+ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah: pd.DataFrame, pop_khusus: pd.DataFrame, kew_value: str):
767
+ if df_filtered is None or df_filtered.empty:
 
 
 
 
 
768
  return pd.DataFrame()
769
 
770
  kew_norm = str(kew_value or "").upper()
771
+ df = df_filtered.copy()
 
 
 
 
 
 
 
 
 
 
 
 
772
 
773
+ if "KAB" in kew_norm or "KOTA" in kew_norm:
774
+ key_col = "kab_key"
775
+ label_col = "KAB_DISP"
776
+ label_name = "Kab/Kota"
777
+ elif "PROV" in kew_norm:
778
+ key_col = "prov_key"
779
+ label_col = "PROV_DISP"
780
+ label_name = "Provinsi"
781
+ else:
782
+ key_col = "kab_key"
783
+ label_col = "KAB_DISP"
784
+ label_name = "Kab/Kota"
785
 
786
+ df = df[df["_dataset"].isin(["sekolah", "umum", "khusus"])].copy()
787
+ if df.empty:
788
+ return pd.DataFrame()
 
 
789
 
790
+ agg = df.groupby([key_col, label_col, "_dataset"], dropna=False).agg(
791
+ Jumlah=("Indeks_Dasar_0_100", "size"),
792
+ Rata2_sub_koleksi=("sub_koleksi", "mean"),
793
+ Rata2_sub_sdm=("sub_sdm", "mean"),
794
+ Rata2_sub_pelayanan=("sub_pelayanan", "mean"),
795
+ Rata2_sub_pengelolaan=("sub_pengelolaan", "mean"),
796
+ Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
797
+ Rata2_dim_kinerja=("dim_kinerja", "mean"),
798
+ Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_0_100", "mean"),
799
+ ).reset_index()
800
 
801
+ agg = agg.rename(columns={key_col: "group_key", label_col: label_name, "_dataset": "Jenis"})
 
 
 
 
 
 
 
 
 
 
 
802
 
803
+ # faktor wilayah (sama untuk semua jenis)
804
+ if faktor_wilayah is None or faktor_wilayah.empty:
805
+ agg["faktor_penyesuaian_wilayah"] = 1.0
806
+ agg["target_total_68"] = np.nan
807
+ agg["pop_total"] = np.nan
808
+ agg["coverage_total_%"] = np.nan
809
+ else:
810
  fw = faktor_wilayah.copy()
811
+ keep = ["group_key", label_name, "faktor_penyesuaian", "target_total_68", "pop_total", "coverage_total_%"]
812
+ keep = [c for c in keep if c in fw.columns]
813
+ fw = fw[keep].rename(columns={"faktor_penyesuaian": "faktor_penyesuaian_wilayah"})
814
+ agg = agg.merge(fw, on=["group_key", label_name], how="left")
815
+ agg["faktor_penyesuaian_wilayah"] = pd.to_numeric(agg["faktor_penyesuaian_wilayah"], errors="coerce").fillna(1.0)
816
+
817
+ agg["faktor_penyesuaian"] = agg["faktor_penyesuaian_wilayah"]
818
+ agg["Indeks_Final_Agregat_0_100"] = pd.to_numeric(agg["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0.0) * agg["faktor_penyesuaian"]
819
+
820
+ # target/pop/coverage per jenis (hanya khusus, dari POP_KHUSUS) — supaya tidak tampil null -> isi 0
821
+ agg["target_total_68_jenis"] = 0.0
822
+ agg["pop_total_jenis"] = 0.0
823
+ agg["coverage_jenis"] = 0.0
824
+
825
+ 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()}):
826
+ pk = pop_khusus.set_index("kab_key")
827
+ for i, r in agg.iterrows():
828
+ if str(r.get("Jenis", "")).lower() != "khusus":
829
+ continue
830
+ gk = r.get("group_key", None)
831
+ if gk in pk.index:
832
+ t = pk.loc[gk, "Target68_Total_Jenis"] if "Target68_Total_Jenis" in pk.columns else np.nan
833
+ p = pk.loc[gk, "Pop_Total_Jenis"] if "Pop_Total_Jenis" in pk.columns else np.nan
834
+ if pd.notna(t):
835
+ agg.at[i, "target_total_68_jenis"] = float(t)
836
+ if pd.notna(p):
837
+ agg.at[i, "pop_total_jenis"] = float(p)
838
 
839
+ agg["target_total_68_jenis"] = pd.to_numeric(agg["target_total_68_jenis"], errors="coerce").fillna(0.0)
840
+ agg["pop_total_jenis"] = pd.to_numeric(agg["pop_total_jenis"], errors="coerce").fillna(0.0)
841
+
842
+ m_need_pop = (agg["pop_total_jenis"] <= 0) & (agg["target_total_68_jenis"] > 0)
843
+ agg.loc[m_need_pop, "pop_total_jenis"] = agg.loc[m_need_pop, "target_total_68_jenis"] / float(FALLBACK_TARGET_RATIO)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
 
845
+ m2 = agg["pop_total_jenis"] > 0
846
+ agg.loc[m2, "coverage_jenis"] = (pd.to_numeric(agg.loc[m2, "Jumlah"], errors="coerce").fillna(0.0) / agg.loc[m2, "pop_total_jenis"]) * 100.0
847
 
848
+ # rounding (tampilan konsisten)
849
  for c in [
850
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
851
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
852
  ]:
853
+ if c in agg.columns:
854
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0).round(3)
855
 
856
+ for c in ["Indeks_Dasar_Agregat_0_100","Indeks_Final_Agregat_0_100"]:
857
+ if c in agg.columns:
858
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0).round(2)
859
 
860
+ for c in ["faktor_penyesuaian_wilayah","faktor_penyesuaian"]:
861
+ if c in agg.columns:
862
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(1.0).round(3)
863
 
864
+ for c in ["target_total_68","pop_total","coverage_total_%","target_total_68_jenis","pop_total_jenis","coverage_jenis"]:
865
+ if c in agg.columns:
866
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0)
867
+
868
+ return agg
869
 
 
870
 
871
  # ============================================================
872
  # 8) AGREGAT WILAYAH (KESELURUHAN) — RUMUS BARU: AVG3 dari 3 jenis
 
1495
  # 15) CORE RUN
1496
  # ============================================================
1497
 
 
 
 
 
 
 
 
 
 
 
 
1498
  def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta):
1499
  try:
1500
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
 
1513
  return _empty_outputs("Tidak ada data untuk filter ini.")
1514
 
1515
  # ==== PIPELINE BARU (KUNCI KONSISTENSI) ====
1516
+ # UPDATE: faktor_wilayah sekarang menambahkan POP_KHUSUS ke target_total_68 & pop_total (total wilayah)
1517
  faktor_wilayah = build_faktor_wilayah(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1518
  agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah, pop_khusus, kew_value or "(Semua)")
1519
+ agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah, kew_value or "(Semua)")
1520
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1521
  verif_total = build_verif_total(faktor_wilayah) # tanpa koma
1522
  detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
 
1540
  cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1541
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1542
 
1543
+
1544
  # FILTER RAW DOWNLOAD (df_raw)
1545
  raw = df_raw.copy()
1546
  if prov_value and prov_value != "(Semua)":