Update app.py
Browse files
app.py
CHANGED
|
@@ -638,180 +638,207 @@ def load_default_files(force=False):
|
|
| 638 |
|
| 639 |
|
| 640 |
# ============================================================
|
| 641 |
-
# 6) FAKTOR WILAYAH
|
| 642 |
-
#
|
| 643 |
-
#
|
|
|
|
| 644 |
# ============================================================
|
| 645 |
|
| 646 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 660 |
-
|
| 661 |
-
label_col = "
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
pk_kab = pd.DataFrame().set_index(pd.Index([]))
|
| 676 |
|
| 677 |
-
|
|
|
|
| 678 |
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
|
| 699 |
-
|
| 700 |
-
#
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
else:
|
| 715 |
-
|
| 716 |
|
| 717 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
gk = r["group_key"]
|
| 726 |
|
| 727 |
-
|
| 728 |
-
|
| 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(
|
| 769 |
-
pd.to_numeric(
|
| 770 |
)
|
| 771 |
]
|
| 772 |
|
| 773 |
-
|
| 774 |
-
(safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else
|
| 775 |
for n, p in zip(
|
| 776 |
-
pd.to_numeric(
|
| 777 |
-
|
| 778 |
)
|
| 779 |
]
|
| 780 |
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
|
| 787 |
-
return
|
| 788 |
|
| 789 |
# ============================================================
|
| 790 |
-
# 7) AGREGAT WILAYAH × JENIS (
|
| 791 |
# ============================================================
|
| 792 |
|
| 793 |
-
def build_agg_wilayah_jenis(df_filtered: pd.DataFrame,
|
| 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 "
|
| 801 |
-
key_col = "
|
| 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
|
| 832 |
|
| 833 |
-
# faktor
|
| 834 |
-
if
|
| 835 |
-
agg["
|
| 836 |
-
agg["
|
| 837 |
-
agg["
|
| 838 |
-
agg["
|
|
|
|
| 839 |
else:
|
| 840 |
-
fw =
|
| 841 |
-
keep = ["group_key", label_name, "
|
|
|
|
|
|
|
| 842 |
keep = [c for c in keep if c in fw.columns]
|
| 843 |
-
fw = fw[keep]
|
| 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 |
-
|
| 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 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
agg["coverage_jenis"] = 0.0
|
| 858 |
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 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
|
| 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 |
-
|
| 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) —
|
| 928 |
# ============================================================
|
| 929 |
|
| 930 |
-
def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame,
|
| 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 = "
|
| 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 (
|
| 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
|
| 982 |
-
if
|
| 983 |
-
fw =
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
|
| 988 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1153 |
# ============================================================
|
| 1154 |
|
| 1155 |
-
def
|
| 1156 |
-
if
|
| 1157 |
return pd.DataFrame()
|
| 1158 |
|
| 1159 |
-
|
| 1160 |
-
label_col = "
|
| 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 |
-
|
| 1183 |
-
for c in
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 (
|
| 1582 |
-
|
| 1583 |
-
agg_jenis_full = build_agg_wilayah_jenis(df,
|
| 1584 |
-
agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full,
|
| 1585 |
summary_jenis = build_summary_per_jenis(agg_jenis_full, agg_total)
|
| 1586 |
-
|
| 1587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|