irhamni commited on
Commit
06d9484
·
verified ·
1 Parent(s): 3c9cc11

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +283 -276
app.py CHANGED
@@ -638,180 +638,207 @@ def load_default_files(force=False):
638
 
639
 
640
  # ============================================================
641
- # 6) FAKTOR WILAYAH (TOTAL) hanya untuk faktor/target/pop/coverage
642
- # UPDATE: pop_total & target_total_68 wilayah ditambah data KHUSUS (POP_KHUSUS)
643
- # TANPA ubah logika faktor (tetap min(n_total/target,1))
 
644
  # ============================================================
645
 
646
- def build_faktor_wilayah(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  df_filtered: pd.DataFrame,
648
  pop_kab: pd.DataFrame,
649
  pop_prov: pd.DataFrame,
650
  pop_khusus: pd.DataFrame,
651
  kew_value: str
652
  ):
 
 
 
 
 
 
 
653
  if df_filtered is None or df_filtered.empty:
654
  return pd.DataFrame()
655
 
656
  kew_norm = str(kew_value or "").upper()
657
  df = df_filtered.copy()
 
 
 
658
 
659
- if "KAB" in kew_norm or "KOTA" in kew_norm:
660
- key_col = "kab_key"
661
- label_col = "KAB_DISP"
662
- label_name = "Kab/Kota"
663
- pop = pop_kab.set_index("kab_key") if (pop_kab is not None and not pop_kab.empty) else pd.DataFrame().set_index(pd.Index([]))
664
- target_field = "Target68_Total"
665
- pop_field = "Pop_Total"
666
- name_field = "Kab_Kota_Label"
667
-
668
- # agregat KHUSUS per kab_key (sum)
669
- if pop_khusus is not None and not pop_khusus.empty and "kab_key" in pop_khusus.columns:
670
- pk_kab = pop_khusus.groupby("kab_key", as_index=False).agg({
671
- "Target68_Total_Jenis": "sum",
672
- "Pop_Total_Jenis": "sum"
673
- }).set_index("kab_key")
674
- else:
675
- pk_kab = pd.DataFrame().set_index(pd.Index([]))
676
 
677
- pk_prov = None # tidak dipakai
 
678
 
679
- elif "PROV" in kew_norm:
680
- key_col = "prov_key"
681
- label_col = "PROV_DISP"
682
- label_name = "Provinsi"
683
- pop = pop_prov.set_index("prov_key") if (pop_prov is not None and not pop_prov.empty) else pd.DataFrame().set_index(pd.Index([]))
684
- target_field = "Target68_Total_Prov"
685
- pop_field = "Pop_Total_Prov"
686
- name_field = "Provinsi_Label"
687
-
688
- # agregat KHUSUS per prov_key (sum)
689
- if pop_khusus is not None and not pop_khusus.empty and "prov_key" in pop_khusus.columns:
690
- pk_prov = pop_khusus.groupby("prov_key", as_index=False).agg({
691
- "Target68_Total_Jenis": "sum",
692
- "Pop_Total_Jenis": "sum"
693
- }).set_index("prov_key")
694
- else:
695
- pk_prov = pd.DataFrame().set_index(pd.Index([]))
696
 
697
- pk_kab = None # tidak dipakai
 
 
 
 
698
 
699
- else:
700
- # fallback: perlakukan seperti kab/kota
701
- key_col = "kab_key"
702
- label_col = "KAB_DISP"
703
- label_name = "Kab/Kota"
704
- pop = pop_kab.set_index("kab_key") if (pop_kab is not None and not pop_kab.empty) else pd.DataFrame().set_index(pd.Index([]))
705
- target_field = "Target68_Total"
706
- pop_field = "Pop_Total"
707
- name_field = "Kab_Kota_Label"
708
-
709
- if pop_khusus is not None and not pop_khusus.empty and "kab_key" in pop_khusus.columns:
710
- pk_kab = pop_khusus.groupby("kab_key", as_index=False).agg({
711
- "Target68_Total_Jenis": "sum",
712
- "Pop_Total_Jenis": "sum"
713
- }).set_index("kab_key")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  else:
715
- pk_kab = pd.DataFrame().set_index(pd.Index([]))
716
 
717
- pk_prov = None
 
 
 
 
718
 
719
- base = df.groupby([key_col, label_col], dropna=False).agg(
720
- n_total=("Indeks_Dasar_0_100", "size"),
721
- ).reset_index().rename(columns={key_col: "group_key", label_col: label_name})
722
 
723
- target_vals, pop_vals, label_fix = [], [], []
724
- for _, r in base.iterrows():
725
- gk = r["group_key"]
726
 
727
- # --- ambil dari POP_KAB/POP_PROV (existing) ---
728
- if gk in pop.index:
729
- target_total = pop.loc[gk, target_field] if target_field in pop.columns else np.nan
730
- pop_total = pop.loc[gk, pop_field] if pop_field in pop.columns else np.nan
731
- nm = pop.loc[gk, name_field] if name_field in pop.columns else r[label_name]
732
- else:
733
- target_total, pop_total, nm = np.nan, np.nan, r[label_name]
734
-
735
- # --- TAMBAH KHUSUS (ini inti request kamu) ---
736
- add_target, add_pop = 0.0, 0.0
737
- if "PROV" in kew_norm:
738
- if pk_prov is not None and (gk in pk_prov.index):
739
- add_target = pk_prov.loc[gk, "Target68_Total_Jenis"] if "Target68_Total_Jenis" in pk_prov.columns else 0.0
740
- add_pop = pk_prov.loc[gk, "Pop_Total_Jenis"] if "Pop_Total_Jenis" in pk_prov.columns else 0.0
741
- else:
742
- if pk_kab is not None and (gk in pk_kab.index):
743
- add_target = pk_kab.loc[gk, "Target68_Total_Jenis"] if "Target68_Total_Jenis" in pk_kab.columns else 0.0
744
- add_pop = pk_kab.loc[gk, "Pop_Total_Jenis"] if "Pop_Total_Jenis" in pk_kab.columns else 0.0
745
-
746
- # amanin numeric
747
- t0 = float(pd.to_numeric(target_total, errors="coerce")) if pd.notna(target_total) else 0.0
748
- p0 = float(pd.to_numeric(pop_total, errors="coerce")) if pd.notna(pop_total) else 0.0
749
- at = float(pd.to_numeric(add_target, errors="coerce")) if pd.notna(add_target) else 0.0
750
- ap = float(pd.to_numeric(add_pop, errors="coerce")) if pd.notna(add_pop) else 0.0
751
-
752
- target_vals.append(t0 + at)
753
- pop_vals.append(p0 + ap)
754
- label_fix.append(nm)
755
-
756
- base[label_name] = label_fix
757
- base["target_total_68"] = pd.to_numeric(pd.Series(target_vals), errors="coerce")
758
- base["pop_total"] = pd.to_numeric(pd.Series(pop_vals), errors="coerce")
759
-
760
- # fallback pop_total bila kosong tapi target ada
761
- m = base["pop_total"].isna() & base["target_total_68"].notna() & (base["target_total_68"] > 0)
762
- base.loc[m, "pop_total"] = base.loc[m, "target_total_68"] / float(FALLBACK_TARGET_RATIO)
763
-
764
- # logika faktor TIDAK DIUBAH
765
- base["faktor_penyesuaian"] = [
766
  faktor_penyesuaian_total(n, t)
767
  for n, t in zip(
768
- pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
769
- pd.to_numeric(base["target_total_68"], errors="coerce").tolist()
770
  )
771
  ]
772
 
773
- base["coverage_total_%"] = [
774
- (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else np.nan
775
  for n, p in zip(
776
- pd.to_numeric(base["n_total"], errors="coerce").fillna(0).astype(float).tolist(),
777
- base["pop_total"].tolist()
778
  )
779
  ]
780
 
781
- # ====== DISPLAY (sesuai permintaan awal kamu) ======
782
- base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).round(0).astype(int)
783
- base["pop_total"] = pd.to_numeric(base["pop_total"], errors="coerce").fillna(0).round(0).astype(int)
784
- base["coverage_total_%"] = pd.to_numeric(base["coverage_total_%"], errors="coerce").fillna(0.0).round(2)
785
- base["faktor_penyesuaian"] = pd.to_numeric(base["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
 
 
 
 
 
 
 
 
 
786
 
787
- return base
788
 
789
  # ============================================================
790
- # 7) AGREGAT WILAYAH × JENIS (Final pakai faktor wilayah)
791
  # ============================================================
792
 
793
- def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah: pd.DataFrame, pop_khusus: pd.DataFrame, kew_value: str):
794
  if df_filtered is None or df_filtered.empty:
795
  return pd.DataFrame()
796
 
797
  kew_norm = str(kew_value or "").upper()
798
  df = df_filtered.copy()
799
 
800
- if "KAB" in kew_norm or "KOTA" in kew_norm:
801
- key_col = "kab_key"
802
- label_col = "KAB_DISP"
803
- label_name = "Kab/Kota"
804
- mode = "KAB"
805
- elif "PROV" in kew_norm:
806
- key_col = "prov_key"
807
- label_col = "PROV_DISP"
808
- label_name = "Provinsi"
809
- mode = "PROV"
810
  else:
811
- key_col = "kab_key"
812
- label_col = "KAB_DISP"
813
- label_name = "Kab/Kota"
814
- mode = "KAB"
815
 
816
  df = df[df["_dataset"].isin(["sekolah", "umum", "khusus"])].copy()
817
  if df.empty:
@@ -826,81 +853,41 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah: pd.DataFr
826
  Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
827
  Rata2_dim_kinerja=("dim_kinerja", "mean"),
828
  Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_0_100", "mean"),
829
- ).reset_index()
830
 
831
- agg = agg.rename(columns={key_col: "group_key", label_col: label_name, "_dataset": "Jenis"})
832
 
833
- # faktor wilayah (sama untuk semua jenis)
834
- if faktor_wilayah is None or faktor_wilayah.empty:
835
- agg["faktor_penyesuaian_wilayah"] = 1.0
836
- agg["target_total_68"] = np.nan
837
- agg["pop_total"] = np.nan
838
- agg["coverage_total_%"] = np.nan
 
839
  else:
840
- fw = faktor_wilayah.copy()
841
- keep = ["group_key", label_name, "faktor_penyesuaian", "target_total_68", "pop_total", "coverage_total_%"]
 
 
842
  keep = [c for c in keep if c in fw.columns]
843
- fw = fw[keep].rename(columns={"faktor_penyesuaian": "faktor_penyesuaian_wilayah"})
844
- agg = agg.merge(fw, on=["group_key", label_name], how="left")
845
- agg["faktor_penyesuaian_wilayah"] = pd.to_numeric(agg["faktor_penyesuaian_wilayah"], errors="coerce").fillna(1.0)
846
 
847
- agg["faktor_penyesuaian"] = agg["faktor_penyesuaian_wilayah"]
848
- agg["Indeks_Final_Agregat_0_100"] = (
849
- pd.to_numeric(agg["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0.0) * agg["faktor_penyesuaian"]
850
- )
851
 
852
- # =========================
853
- # PATCH INTI: target/pop/coverage per jenis (khusus) via MERGE (anti miss)
854
- # =========================
855
- agg["target_total_68_jenis"] = 0.0
856
- agg["pop_total_jenis"] = 0.0
857
- agg["coverage_jenis"] = 0.0
858
 
859
- if pop_khusus is not None and (not pop_khusus.empty):
860
- pk = pop_khusus.copy()
861
-
862
- # pastikan numeric
863
- pk["Target68_Total_Jenis"] = pd.to_numeric(pk.get("Target68_Total_Jenis", np.nan), errors="coerce")
864
- pk["Pop_Total_Jenis"] = pd.to_numeric(pk.get("Pop_Total_Jenis", np.nan), errors="coerce")
865
-
866
- # ambil mapping sesuai mode
867
- if mode == "PROV":
868
- # agregat prov (sum) supaya provinsi khusus masuk
869
- if "prov_key" in pk.columns:
870
- pk_map = pk.groupby("prov_key", as_index=False).agg(
871
- target_total_68_jenis=("Target68_Total_Jenis", "sum"),
872
- pop_total_jenis=("Pop_Total_Jenis", "sum"),
873
- ).rename(columns={"prov_key": "group_key"})
874
- else:
875
- pk_map = pd.DataFrame(columns=["group_key", "target_total_68_jenis", "pop_total_jenis"])
876
- else:
877
- # kab/kota (sum) supaya kab/kota khusus masuk
878
- if "kab_key" in pk.columns:
879
- pk_map = pk.groupby("kab_key", as_index=False).agg(
880
- target_total_68_jenis=("Target68_Total_Jenis", "sum"),
881
- pop_total_jenis=("Pop_Total_Jenis", "sum"),
882
- ).rename(columns={"kab_key": "group_key"})
883
- else:
884
- pk_map = pd.DataFrame(columns=["group_key", "target_total_68_jenis", "pop_total_jenis"])
885
-
886
- # merge hanya untuk baris Jenis=khusus (biar sekolah/umum tetap 0)
887
- is_khusus = agg["Jenis"].astype(str).str.lower().str.strip().eq("khusus")
888
- if is_khusus.any() and (pk_map is not None) and (not pk_map.empty):
889
- tmp = agg.loc[is_khusus, ["group_key"]].merge(pk_map, on="group_key", how="left")
890
- agg.loc[is_khusus, "target_total_68_jenis"] = pd.to_numeric(tmp["target_total_68_jenis"], errors="coerce").fillna(0.0).values
891
- agg.loc[is_khusus, "pop_total_jenis"] = pd.to_numeric(tmp["pop_total_jenis"], errors="coerce").fillna(0.0).values
892
-
893
- # fallback pop dari target
894
- agg["target_total_68_jenis"] = pd.to_numeric(agg["target_total_68_jenis"], errors="coerce").fillna(0.0)
895
- agg["pop_total_jenis"] = pd.to_numeric(agg["pop_total_jenis"], errors="coerce").fillna(0.0)
896
-
897
- m_need_pop = (agg["pop_total_jenis"] <= 0) & (agg["target_total_68_jenis"] > 0)
898
- agg.loc[m_need_pop, "pop_total_jenis"] = agg.loc[m_need_pop, "target_total_68_jenis"] / float(FALLBACK_TARGET_RATIO)
899
-
900
- m2 = agg["pop_total_jenis"] > 0
901
- 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
902
 
903
- # rounding (tampilan konsisten)
904
  for c in [
905
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
906
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -912,27 +899,19 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah: pd.DataFr
912
  if c in agg.columns:
913
  agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0).round(2)
914
 
915
- for c in ["faktor_penyesuaian_wilayah","faktor_penyesuaian"]:
916
- if c in agg.columns:
917
- agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(1.0).round(3)
918
-
919
- for c in ["target_total_68","pop_total","coverage_total_%","target_total_68_jenis","pop_total_jenis","coverage_jenis"]:
920
- if c in agg.columns:
921
- agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0)
922
 
923
  return agg
924
-
925
-
926
  # ============================================================
927
- # 8) AGREGAT WILAYAH (KESELURUHAN) — RUMUS BARU: AVG3 dari 3 jenis
928
  # ============================================================
929
 
930
- def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah: pd.DataFrame, kew_value: str):
931
  if agg_jenis is None or agg_jenis.empty:
932
  return pd.DataFrame()
933
 
934
  kew_norm = str(kew_value or "").upper()
935
- label_name = "Kab/Kota" if ("KAB" in kew_norm or "KOTA" in kew_norm) else ("Provinsi" if "PROV" in kew_norm else "Kab/Kota")
936
 
937
  jenis_list = ["sekolah", "umum", "khusus"]
938
 
@@ -961,11 +940,10 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah:
961
  how="left"
962
  )
963
 
964
- # missing=0 (kunci avg3 tetap ÷3)
965
  for c in cols_present:
966
  full[c] = pd.to_numeric(full[c], errors="coerce").fillna(0.0)
967
 
968
- # keseluruhan wilayah = avg3 dari 3 jenis
969
  out = full.groupby(["group_key", label_name], as_index=False).agg(
970
  n_total=("Jumlah", "sum"),
971
  Rata2_sub_koleksi=("Rata2_sub_koleksi", "mean"),
@@ -978,14 +956,34 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah:
978
  Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
979
  )
980
 
981
- # tempel faktor/target/pop/coverage (informasi verifikasi)
982
- if faktor_wilayah is not None and not faktor_wilayah.empty:
983
- fw = faktor_wilayah.copy()
984
- keep_fw = ["group_key", label_name, "target_total_68", "pop_total", "faktor_penyesuaian", "coverage_total_%"]
985
- keep_fw = [c for c in keep_fw if c in fw.columns]
986
- out = out.merge(fw[keep_fw], on=["group_key", label_name], how="left")
 
 
 
 
 
 
987
 
988
- # rounding
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989
  for c in [
990
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
991
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -997,13 +995,6 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah:
997
  if c in out.columns:
998
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
999
 
1000
- if "faktor_penyesuaian" in out.columns:
1001
- out["faktor_penyesuaian"] = pd.to_numeric(out["faktor_penyesuaian"], errors="coerce").fillna(1.0).round(3)
1002
-
1003
- for c in ["target_total_68","pop_total","coverage_total_%"]:
1004
- if c in out.columns:
1005
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0)
1006
-
1007
  return out
1008
 
1009
 
@@ -1149,41 +1140,37 @@ def attach_final_to_detail(df_filtered: pd.DataFrame, agg_total: pd.DataFrame, m
1149
 
1150
 
1151
  # ============================================================
1152
- # 11) VERIFIKASI TOTAL (tanpa koma) — pakai faktor_wilayah
1153
  # ============================================================
1154
 
1155
- def build_verif_total(faktor_wilayah: pd.DataFrame):
1156
- if faktor_wilayah is None or faktor_wilayah.empty:
1157
  return pd.DataFrame()
1158
 
1159
- df = faktor_wilayah.copy()
1160
- label_col = "Kab/Kota" if "Kab/Kota" in df.columns else ("Provinsi" if "Provinsi" in df.columns else "Wilayah")
1161
-
1162
- out = pd.DataFrame({
1163
- label_col: df[label_col].astype(str),
1164
- "Pop_Total": df.get("pop_total", 0),
1165
- "Target_68_Total": df.get("target_total_68", 0),
1166
- "Sampel_Total_Terkumpul": df.get("n_total", 0),
1167
- "Coverage_Total_%": df.get("coverage_total_%", 0),
1168
- "Faktor_Penyesuaian_(Sampel/Target68)_persen": pd.to_numeric(df.get("faktor_penyesuaian", 1.0), errors="coerce").fillna(1.0) * 100,
1169
- "GAP_Ke_Target68_Total": [
1170
- max(t - n, 0) if (t is not None and not pd.isna(t)) else 0
1171
- for n, t in zip(
1172
- pd.to_numeric(df.get("n_total", 0), errors="coerce").fillna(0).astype(float).tolist(),
1173
- pd.to_numeric(df.get("target_total_68", 0), errors="coerce").fillna(0).astype(float).tolist()
1174
- )
1175
- ],
1176
- "Catatan": [
1177
- ("Target68_Total_tidak_valid" if (t is None or pd.isna(t) or float(t) <= 0) else "")
1178
- for t in pd.to_numeric(df.get("target_total_68", 0), errors="coerce").tolist()
1179
- ]
1180
- })
1181
 
1182
- # TANPA KOMA: semua numerik jadi integer
1183
- for c in out.columns:
1184
- if c in [label_col, "Catatan"]:
1185
- continue
1186
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1187
 
1188
  return out
1189
 
@@ -1300,10 +1287,10 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
1300
 
1301
 
1302
  # ============================================================
1303
- # 13) KPI DASHBOARD (Final harus sumber Ringkasan)
1304
  # ============================================================
1305
 
1306
- def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
1307
  def _get_final(j):
1308
  sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1309
  if sub.empty:
@@ -1328,16 +1315,30 @@ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame,
1328
  dasar_khusus = _get_dasar("khusus")
1329
  dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
1330
 
1331
- if agg_total is not None and not agg_total.empty:
1332
- n_sum = float(pd.to_numeric(agg_total.get("n_total", 0), errors="coerce").fillna(0).sum())
1333
- t_ser = pd.to_numeric(agg_total.get("target_total_68", np.nan), errors="coerce")
1334
- t_sum = float(t_ser[t_ser > 0].fillna(0).sum()) if hasattr(t_ser, "__len__") else float(t_ser)
1335
- coverage_factor = min(n_sum / t_sum, 1.0) if (t_sum and t_sum > 0) else 1.0
1336
- cakupan_pct = coverage_factor * 100.0
1337
- faktor_mean = float(pd.to_numeric(agg_total.get("faktor_penyesuaian", 1.0), errors="coerce").fillna(1.0).mean())
1338
- else:
1339
- cakupan_pct = 0.0
1340
- faktor_mean = 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1341
 
1342
  dampak = final_all - dasar_all
1343
 
@@ -1349,11 +1350,12 @@ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame,
1349
  "dampak": dampak
1350
  }
1351
 
1352
- def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame) -> str:
 
1353
  if summary_jenis is None or summary_jenis.empty:
1354
  return ""
1355
 
1356
- k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
1357
 
1358
  def fmt(x, nd=2):
1359
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
@@ -1578,13 +1580,18 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1578
  if df.empty:
1579
  return _empty_outputs("Tidak ada data untuk filter ini.")
1580
 
1581
- # ==== PIPELINE BARU (KUNCI KONSISTENSI) ====
1582
- faktor_wilayah = build_faktor_wilayah(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1583
- agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah, pop_khusus, kew_value or "(Semua)")
1584
- agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah, kew_value or "(Semua)")
1585
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1586
- verif_total = build_verif_total(faktor_wilayah) # tanpa koma
1587
- detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
 
 
 
 
 
1588
 
1589
  # ====== UPDATE SESUAI PERMINTAAN (UI ONLY) ======
1590
  # Tabel Agregat Wilayah × Jenis cukup sampai Indeks_Dasar_Agregat_0_100
@@ -1633,7 +1640,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1633
  fig_khusus = _fig_jenis_ent("khusus", "Bell Curve — Jenis: Khusus (Indeks per Entitas)")
1634
 
1635
  # KPI markdown (FINAL sumber Ringkasan)
1636
- kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis_full)
1637
 
1638
  tmpdir = tempfile.mkdtemp()
1639
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
 
638
 
639
 
640
  # ============================================================
641
+ # 6) FAKTOR WILAYAH — PER JENIS (PATCH UTAMA)
642
+ # faktor_jenis = min(n_jenis / target68_jenis, 1.0)
643
+ # target/pop sekolah & umum diambil dari POP_KAB/POP_PROV jika tersedia
644
+ # target/pop khusus diambil dari POP_KHUSUS (gabungan)
645
  # ============================================================
646
 
647
+ def _read_target_pop_per_jenis_from_pop(pop_df: pd.DataFrame, mode: str):
648
+ """
649
+ Mengambil mapping target/pop PER JENIS untuk sekolah & umum dari POP_KAB/POP_PROV
650
+ jika file populasi kamu memang punya kolom per jenis.
651
+
652
+ Return:
653
+ dict: {
654
+ "sekolah": (target_col, pop_col),
655
+ "umum": (target_col, pop_col),
656
+ }
657
+ Kalau tidak ketemu, return None untuk kolom tersebut -> nanti dianggap 0.
658
+ """
659
+ if pop_df is None or pop_df.empty:
660
+ return {"sekolah": (None, None), "umum": (None, None)}
661
+
662
+ # Kandidat kolom target/pop per jenis (buat fleksibel)
663
+ # Silakan tambahkan alias kolom kamu di sini bila beda penamaan
664
+ sekolah_target = pick_col(pop_df, [
665
+ "TARGET_SEKOLAH_68", "Target_Sekolah_68", "target_sekolah_68",
666
+ "SAMPEL_SEKOLAH_68", "Sampel_Sekolah_68", "sampel_sekolah_68",
667
+ "target68_sekolah", "Target68_Sekolah"
668
+ ])
669
+ sekolah_pop = pick_col(pop_df, [
670
+ "POP_SEKOLAH", "Pop_Sekolah", "pop_sekolah",
671
+ "POPULASI_SEKOLAH", "Populasi_Sekolah"
672
+ ])
673
+
674
+ umum_target = pick_col(pop_df, [
675
+ "TARGET_UMUM_68", "Target_Umum_68", "target_umum_68",
676
+ "SAMPEL_UMUM_68", "Sampel_Umum_68", "sampel_umum_68",
677
+ "target68_umum", "Target68_Umum"
678
+ ])
679
+ umum_pop = pick_col(pop_df, [
680
+ "POP_UMUM", "Pop_Umum", "pop_umum",
681
+ "POPULASI_UMUM", "Populasi_Umum"
682
+ ])
683
+
684
+ return {
685
+ "sekolah": (sekolah_target, sekolah_pop),
686
+ "umum": (umum_target, umum_pop),
687
+ }
688
+
689
+
690
+ def build_faktor_wilayah_jenis(
691
  df_filtered: pd.DataFrame,
692
  pop_kab: pd.DataFrame,
693
  pop_prov: pd.DataFrame,
694
  pop_khusus: pd.DataFrame,
695
  kew_value: str
696
  ):
697
+ """
698
+ Output: faktor per (wilayah x jenis)
699
+ Kolom minimal:
700
+ group_key, [Kab/Kota|Provinsi], Jenis,
701
+ n_jenis, target_total_68_jenis, pop_total_jenis,
702
+ coverage_jenis_%, faktor_penyesuaian_jenis, gap_target68_jenis
703
+ """
704
  if df_filtered is None or df_filtered.empty:
705
  return pd.DataFrame()
706
 
707
  kew_norm = str(kew_value or "").upper()
708
  df = df_filtered.copy()
709
+ df = df[df["_dataset"].isin(["sekolah", "umum", "khusus"])].copy()
710
+ if df.empty:
711
+ return pd.DataFrame()
712
 
713
+ # tentukan level
714
+ if "PROV" in kew_norm:
715
+ key_col, label_col, label_name, mode = "prov_key", "PROV_DISP", "Provinsi", "PROV"
716
+ pop_base = pop_prov.set_index("prov_key") if (pop_prov is not None and not pop_prov.empty and "prov_key" in pop_prov.columns) else pd.DataFrame().set_index(pd.Index([]))
717
+ else:
718
+ key_col, label_col, label_name, mode = "kab_key", "KAB_DISP", "Kab/Kota", "KAB"
719
+ pop_base = pop_kab.set_index("kab_key") if (pop_kab is not None and not pop_kab.empty and "kab_key" in pop_kab.columns) else pd.DataFrame().set_index(pd.Index([]))
720
+
721
+ # hitung n per jenis
722
+ base_n = (
723
+ df.groupby([key_col, label_col, "_dataset"], dropna=False)
724
+ .size()
725
+ .reset_index(name="n_jenis")
726
+ .rename(columns={key_col: "group_key", label_col: label_name, "_dataset": "Jenis"})
727
+ )
728
+ base_n["Jenis"] = base_n["Jenis"].astype(str).str.lower().str.strip()
 
729
 
730
+ # mapping target/pop sekolah & umum dari POP_KAB/POP_PROV (jika ada)
731
+ tp_map = _read_target_pop_per_jenis_from_pop(pop_base.reset_index(), mode=mode)
732
 
733
+ # siapkan kolom target/pop default
734
+ base_n["target_total_68_jenis"] = 0.0
735
+ base_n["pop_total_jenis"] = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
 
737
+ # isi sekolah & umum dari POP_KAB/POP_PROV bila kolomnya tersedia
738
+ for j in ["sekolah", "umum"]:
739
+ tcol, pcol = tp_map.get(j, (None, None))
740
+ if tcol is None and pcol is None:
741
+ continue
742
 
743
+ # ambil series target/pop per wilayah
744
+ # (pop_base masih set_index group_key)
745
+ if not pop_base.empty:
746
+ if tcol is not None and tcol in pop_base.columns:
747
+ tser = pd.to_numeric(pop_base[tcol], errors="coerce").fillna(0.0)
748
+ else:
749
+ tser = pd.Series(0.0, index=pop_base.index)
750
+
751
+ if pcol is not None and pcol in pop_base.columns:
752
+ pser = pd.to_numeric(pop_base[pcol], errors="coerce").fillna(0.0)
753
+ else:
754
+ pser = pd.Series(0.0, index=pop_base.index)
755
+
756
+ mask = base_n["Jenis"].eq(j)
757
+ # map by index
758
+ base_n.loc[mask, "target_total_68_jenis"] = base_n.loc[mask, "group_key"].map(tser).fillna(0.0).values
759
+ base_n.loc[mask, "pop_total_jenis"] = base_n.loc[mask, "group_key"].map(pser).fillna(0.0).values
760
+
761
+ # isi KHUSUS dari POP_KHUSUS (sum per wilayah)
762
+ if pop_khusus is not None and not pop_khusus.empty:
763
+ pk = pop_khusus.copy()
764
+ pk["Target68_Total_Jenis"] = pd.to_numeric(pk.get("Target68_Total_Jenis", np.nan), errors="coerce").fillna(0.0)
765
+ pk["Pop_Total_Jenis"] = pd.to_numeric(pk.get("Pop_Total_Jenis", np.nan), errors="coerce").fillna(0.0)
766
+
767
+ if mode == "PROV" and "prov_key" in pk.columns:
768
+ pk_map = pk.groupby("prov_key", as_index=False).agg(
769
+ target_total_68_jenis=("Target68_Total_Jenis", "sum"),
770
+ pop_total_jenis=("Pop_Total_Jenis", "sum"),
771
+ ).rename(columns={"prov_key": "group_key"})
772
+ elif mode == "KAB" and "kab_key" in pk.columns:
773
+ pk_map = pk.groupby("kab_key", as_index=False).agg(
774
+ target_total_68_jenis=("Target68_Total_Jenis", "sum"),
775
+ pop_total_jenis=("Pop_Total_Jenis", "sum"),
776
+ ).rename(columns={"kab_key": "group_key"})
777
  else:
778
+ pk_map = pd.DataFrame(columns=["group_key","target_total_68_jenis","pop_total_jenis"])
779
 
780
+ if not pk_map.empty:
781
+ mask_khusus = base_n["Jenis"].eq("khusus")
782
+ tmp = base_n.loc[mask_khusus, ["group_key"]].merge(pk_map, on="group_key", how="left")
783
+ base_n.loc[mask_khusus, "target_total_68_jenis"] = pd.to_numeric(tmp["target_total_68_jenis"], errors="coerce").fillna(0.0).values
784
+ base_n.loc[mask_khusus, "pop_total_jenis"] = pd.to_numeric(tmp["pop_total_jenis"], errors="coerce").fillna(0.0).values
785
 
786
+ # fallback pop dari target
787
+ base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0.0)
788
+ base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0.0)
789
 
790
+ m_need_pop = (base_n["pop_total_jenis"] <= 0) & (base_n["target_total_68_jenis"] > 0)
791
+ base_n.loc[m_need_pop, "pop_total_jenis"] = base_n.loc[m_need_pop, "target_total_68_jenis"] / float(FALLBACK_TARGET_RATIO)
 
792
 
793
+ # faktor per jenis
794
+ base_n["faktor_penyesuaian_jenis"] = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795
  faktor_penyesuaian_total(n, t)
796
  for n, t in zip(
797
+ pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
798
+ pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).astype(float).tolist()
799
  )
800
  ]
801
 
802
+ base_n["coverage_jenis_%"] = [
803
+ (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else 0.0
804
  for n, p in zip(
805
+ pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
806
+ pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).astype(float).tolist()
807
  )
808
  ]
809
 
810
+ base_n["gap_target68_jenis"] = [
811
+ max(t - n, 0)
812
+ for n, t in zip(
813
+ pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
814
+ pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).astype(float).tolist()
815
+ )
816
+ ]
817
+
818
+ # DISPLAY sesuai request (target/pop int, coverage 2 desimal)
819
+ base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
820
+ base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).round(0).astype(int)
821
+ base_n["coverage_jenis_%"] = pd.to_numeric(base_n["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
822
+ base_n["faktor_penyesuaian_jenis"] = pd.to_numeric(base_n["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
823
+ base_n["gap_target68_jenis"] = pd.to_numeric(base_n["gap_target68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
824
 
825
+ return base_n
826
 
827
  # ============================================================
828
+ # 7) AGREGAT WILAYAH × JENIS (PATCH: faktor 68% PER JENIS)
829
  # ============================================================
830
 
831
+ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
832
  if df_filtered is None or df_filtered.empty:
833
  return pd.DataFrame()
834
 
835
  kew_norm = str(kew_value or "").upper()
836
  df = df_filtered.copy()
837
 
838
+ if "PROV" in kew_norm:
839
+ key_col, label_col, label_name = "prov_key", "PROV_DISP", "Provinsi"
 
 
 
 
 
 
 
 
840
  else:
841
+ key_col, label_col, label_name = "kab_key", "KAB_DISP", "Kab/Kota"
 
 
 
842
 
843
  df = df[df["_dataset"].isin(["sekolah", "umum", "khusus"])].copy()
844
  if df.empty:
 
853
  Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
854
  Rata2_dim_kinerja=("dim_kinerja", "mean"),
855
  Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_0_100", "mean"),
856
+ ).reset_index().rename(columns={key_col: "group_key", label_col: label_name, "_dataset": "Jenis"})
857
 
858
+ agg["Jenis"] = agg["Jenis"].astype(str).str.lower().str.strip()
859
 
860
+ # merge faktor PER JENIS
861
+ if faktor_wilayah_jenis is None or faktor_wilayah_jenis.empty:
862
+ agg["faktor_penyesuaian_jenis"] = 1.0
863
+ agg["target_total_68_jenis"] = 0
864
+ agg["pop_total_jenis"] = 0
865
+ agg["coverage_jenis_%"] = 0.0
866
+ agg["gap_target68_jenis"] = 0
867
  else:
868
+ fw = faktor_wilayah_jenis.copy()
869
+ keep = ["group_key", label_name, "Jenis",
870
+ "faktor_penyesuaian_jenis", "target_total_68_jenis", "pop_total_jenis",
871
+ "coverage_jenis_%", "gap_target68_jenis"]
872
  keep = [c for c in keep if c in fw.columns]
873
+ fw = fw[keep]
874
+ agg = agg.merge(fw, on=["group_key", label_name, "Jenis"], how="left")
 
875
 
876
+ agg["faktor_penyesuaian_jenis"] = pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0)
 
 
 
877
 
878
+ for c in ["target_total_68_jenis","pop_total_jenis","gap_target68_jenis"]:
879
+ if c in agg.columns:
880
+ agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0).round(0).astype(int)
881
+ if "coverage_jenis_%" in agg.columns:
882
+ agg["coverage_jenis_%"] = pd.to_numeric(agg["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
 
883
 
884
+ # Indeks FINAL PER JENIS
885
+ agg["Indeks_Final_Agregat_0_100"] = (
886
+ pd.to_numeric(agg["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0.0)
887
+ * pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0)
888
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
 
890
+ # rounding tampilan
891
  for c in [
892
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
893
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
899
  if c in agg.columns:
900
  agg[c] = pd.to_numeric(agg[c], errors="coerce").fillna(0.0).round(2)
901
 
902
+ agg["faktor_penyesuaian_jenis"] = pd.to_numeric(agg["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
 
 
 
 
 
 
903
 
904
  return agg
 
 
905
  # ============================================================
906
+ # 8) AGREGAT WILAYAH (KESELURUHAN) — FIX: avg3 + tampilkan POP/TARGET/GAP per jenis
907
  # ============================================================
908
 
909
+ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
910
  if agg_jenis is None or agg_jenis.empty:
911
  return pd.DataFrame()
912
 
913
  kew_norm = str(kew_value or "").upper()
914
+ label_name = "Provinsi" if "PROV" in kew_norm else "Kab/Kota"
915
 
916
  jenis_list = ["sekolah", "umum", "khusus"]
917
 
 
940
  how="left"
941
  )
942
 
943
+ # missing=0 (avg3 tetap ÷3)
944
  for c in cols_present:
945
  full[c] = pd.to_numeric(full[c], errors="coerce").fillna(0.0)
946
 
 
947
  out = full.groupby(["group_key", label_name], as_index=False).agg(
948
  n_total=("Jumlah", "sum"),
949
  Rata2_sub_koleksi=("Rata2_sub_koleksi", "mean"),
 
956
  Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
957
  )
958
 
959
+ # --- tempel POP/TARGET/SAMPEL/GAP per jenis ke agg_total ---
960
+ if faktor_wilayah_jenis is not None and not faktor_wilayah_jenis.empty:
961
+ fw = faktor_wilayah_jenis.copy()
962
+ fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
963
+
964
+ # pivot target/pop/n/gap per jenis
965
+ piv = fw.pivot_table(
966
+ index=["group_key", label_name],
967
+ columns="Jenis",
968
+ values=["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"],
969
+ aggfunc="first"
970
+ )
971
 
972
+ # flatten columns
973
+ piv.columns = [f"{v}_{k}" for v, k in piv.columns]
974
+ piv = piv.reset_index()
975
+
976
+ out = out.merge(piv, on=["group_key", label_name], how="left")
977
+
978
+ # rapihin NaN -> 0
979
+ for j in ["sekolah","umum","khusus"]:
980
+ for basecol in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
981
+ c = f"{basecol}_{j}"
982
+ if c in out.columns:
983
+ if basecol == "pop_total_jenis" or basecol == "target_total_68_jenis" or basecol == "n_jenis" or basecol == "gap_target68_jenis":
984
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
985
+
986
+ # rounding index
987
  for c in [
988
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
989
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
995
  if c in out.columns:
996
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
997
 
 
 
 
 
 
 
 
998
  return out
999
 
1000
 
 
1140
 
1141
 
1142
  # ============================================================
1143
+ # 11) VERIFIKASI PER JENIS (OPSIONAL, TANPA KOMA)
1144
  # ============================================================
1145
 
1146
+ def build_verif_jenis(faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
1147
+ if faktor_wilayah_jenis is None or faktor_wilayah_jenis.empty:
1148
  return pd.DataFrame()
1149
 
1150
+ kew_norm = str(kew_value or "").upper()
1151
+ label_col = "Provinsi" if "PROV" in kew_norm else "Kab/Kota"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1152
 
1153
+ out = faktor_wilayah_jenis.copy()
1154
+ keep = [c for c in [
1155
+ label_col, "Jenis",
1156
+ "pop_total_jenis", "target_total_68_jenis", "n_jenis",
1157
+ "coverage_jenis_%", "faktor_penyesuaian_jenis", "gap_target68_jenis"
1158
+ ] if c in out.columns]
1159
+
1160
+ out = out[keep].copy()
1161
+
1162
+ # tanpa koma untuk integer columns
1163
+ for c in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
1164
+ if c in out.columns:
1165
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1166
+
1167
+ # coverage 2 desimal tetap boleh (request awal kamu coverage decimal 2)
1168
+ if "coverage_jenis_%" in out.columns:
1169
+ out["coverage_jenis_%"] = pd.to_numeric(out["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
1170
+
1171
+ # faktor 3 desimal
1172
+ if "faktor_penyesuaian_jenis" in out.columns:
1173
+ out["faktor_penyesuaian_jenis"] = pd.to_numeric(out["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
1174
 
1175
  return out
1176
 
 
1287
 
1288
 
1289
  # ============================================================
1290
+ # 13) KPI DASHBOARD (PATCH: cakupan 68% PER JENIS -> avg3)
1291
  # ============================================================
1292
 
1293
+ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None):
1294
  def _get_final(j):
1295
  sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1296
  if sub.empty:
 
1315
  dasar_khusus = _get_dasar("khusus")
1316
  dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
1317
 
1318
+ # === PATCH: cakupan dihitung PER JENIS (total n_jenis / total target_jenis), lalu avg3 ===
1319
+ cakupan_pct = 0.0
1320
+ faktor_mean = 1.0
1321
+ if faktor_wilayah_jenis is not None and (not faktor_wilayah_jenis.empty):
1322
+ fw = faktor_wilayah_jenis.copy()
1323
+ fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
1324
+
1325
+ factors = []
1326
+ for j in ["sekolah","umum","khusus"]:
1327
+ sub = fw[fw["Jenis"] == j]
1328
+ if sub.empty:
1329
+ factors.append(0.0)
1330
+ continue
1331
+
1332
+ n_sum = float(pd.to_numeric(sub.get("n_jenis", 0), errors="coerce").fillna(0).sum())
1333
+ t_sum = float(pd.to_numeric(sub.get("target_total_68_jenis", 0), errors="coerce").fillna(0).sum())
1334
+ f = min(n_sum / t_sum, 1.0) if (t_sum and t_sum > 0) else 0.0
1335
+ factors.append(f)
1336
+
1337
+ cakupan_pct = (sum(factors) / 3.0) * 100.0
1338
+
1339
+ # faktor_mean juga dibuat avg3 dari faktor total per jenis (opsional)
1340
+ # kalau mau: mean faktor per-wilayah×jenis
1341
+ faktor_mean = float(pd.to_numeric(fw.get("faktor_penyesuaian_jenis", 1.0), errors="coerce").fillna(1.0).mean())
1342
 
1343
  dampak = final_all - dasar_all
1344
 
 
1350
  "dampak": dampak
1351
  }
1352
 
1353
+
1354
+ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None) -> str:
1355
  if summary_jenis is None or summary_jenis.empty:
1356
  return ""
1357
 
1358
+ k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis, faktor_wilayah_jenis=faktor_wilayah_jenis)
1359
 
1360
  def fmt(x, nd=2):
1361
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
 
1580
  if df.empty:
1581
  return _empty_outputs("Tidak ada data untuk filter ini.")
1582
 
1583
+ # ==== PIPELINE BARU (PATCH: faktor 68% PER JENIS) ====
1584
+ faktor_wilayah_jenis = build_faktor_wilayah_jenis(df, pop_kab, pop_prov, pop_khusus, kew_value or "(Semua)")
1585
+ agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah_jenis, kew_value or "(Semua)")
1586
+ agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah_jenis, kew_value or "(Semua)")
1587
  summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
1588
+
1589
+ # verif_total: kalau kamu masih mau tabel verifikasi, sekarang logikanya sebaiknya per jenis
1590
+ # tapi karena request kamu fokus ringkasan + agg_total, verif_total bisa tetap pakai "ringkas" dari faktor_wilayah_jenis
1591
+ # (opsional) kita bikin verif_total sederhana dari faktor_wilayah_jenis:
1592
+ verif_total = build_verif_jenis(faktor_wilayah_jenis, kew_value or "(Semua)")
1593
+ detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
1594
+
1595
 
1596
  # ====== UPDATE SESUAI PERMINTAAN (UI ONLY) ======
1597
  # Tabel Agregat Wilayah × Jenis cukup sampai Indeks_Dasar_Agregat_0_100
 
1640
  fig_khusus = _fig_jenis_ent("khusus", "Bell Curve — Jenis: Khusus (Indeks per Entitas)")
1641
 
1642
  # KPI markdown (FINAL sumber Ringkasan)
1643
+ kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis_full, faktor_wilayah_jenis=faktor_wilayah_jenis)
1644
 
1645
  tmpdir = tempfile.mkdtemp()
1646
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")