Update app.py
Browse files
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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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").
|
| 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 |
-
|
| 713 |
)
|
| 714 |
]
|
| 715 |
|
| 716 |
-
# =====
|
| 717 |
base["target_total_68"] = pd.to_numeric(base["target_total_68"], errors="coerce").fillna(0).round(0).astype(int)
|
| 718 |
-
base["pop_total"]
|
| 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
|
| 730 |
-
|
| 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 |
-
|
| 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 |
-
|
| 754 |
-
"
|
| 755 |
-
"
|
| 756 |
-
"
|
| 757 |
-
|
| 758 |
-
"
|
| 759 |
-
|
| 760 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
how="left"
|
| 766 |
-
)
|
| 767 |
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
|
| 772 |
-
|
| 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 |
-
#
|
| 786 |
-
if faktor_wilayah is
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
fw = faktor_wilayah.copy()
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 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 |
-
|
| 849 |
-
|
| 850 |
|
| 851 |
-
# rounding (
|
| 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
|
| 857 |
-
|
| 858 |
|
| 859 |
-
for c in ["Indeks_Dasar_Agregat_0_100","
|
| 860 |
-
if c in
|
| 861 |
-
|
| 862 |
|
| 863 |
-
|
| 864 |
-
|
|
|
|
| 865 |
|
| 866 |
-
for c in ["target_total_68","pop_total","coverage_total_%"]:
|
| 867 |
-
if c in
|
| 868 |
-
|
|
|
|
|
|
|
| 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,
|
| 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)":
|