Update app.py
Browse files
app.py
CHANGED
|
@@ -4,25 +4,20 @@ IPLM 2025 β FINAL (NO UPLOAD) β FULL REWRITE (NO RINGKAS)
|
|
| 4 |
|
| 5 |
β
Jenis tampil: sekolah, umum, khusus (khusus ditampilkan sebagai jenis)
|
| 6 |
β
Indeks dasar per entitas: Yeo-Johnson + MinMax nasional per indikator
|
| 7 |
-
β
|
| 8 |
faktor_penyesuaian = min(n_total_terkumpul / target_total_68, 1.0)
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
Indeks_Final_Wilayah_0_100 = Indeks_Dasar_Agregat_0_100 Γ faktor_penyesuaian
|
| 12 |
-
-> Maka nilai Aceh Jaya di agregat provinsi akan sama dengan nilai Aceh Jaya saat dipilih individu (definisi keseluruhan sama).
|
| 13 |
-
β
Ringkasan (Jenis + Keseluruhan) selalu 4 baris: sekolah, umum, khusus, keseluruhan
|
| 14 |
-
β
Indeks_Final_Disesuaikan_0_100 (keseluruhan) = (final_sekolah+final_umum+final_khusus)/3 (missing=0, tetap Γ·3)
|
| 15 |
-
β
Dashboard KPI FINAL mengambil dari Ringkasan (baris keseluruhan)
|
| 16 |
-
β
Detail entitas: Indeks_Final_0_100 menempel dari Indeks_Final_Wilayah_0_100 (bukan per-row)
|
| 17 |
β
Bell curve per JENIS berbasis indeks per entitas (row-level)
|
| 18 |
β
LLM analysis + Word
|
| 19 |
β
Download (tanpa upload box)
|
| 20 |
-
β
Download Data Mentah (.xlsx) = RAW hasil filter (bukan agregat jenis)
|
| 21 |
-
β
Word report: hilangkan "Kewenangan: ..." ganti "Ringkasan Dashboard"
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
| 26 |
"""
|
| 27 |
|
| 28 |
import os
|
|
@@ -592,7 +587,6 @@ def load_default_files(force=False):
|
|
| 592 |
|
| 593 |
# ============================================================
|
| 594 |
# 6) AGREGAT WILAYAH (KESELURUHAN) + PENYESUAIAN TOTAL
|
| 595 |
-
# FIX INKONSISTENSI: "Indeks_Dasar_Agregat_0_100" pakai rata-rata 3 jenis Γ·3
|
| 596 |
# ============================================================
|
| 597 |
|
| 598 |
def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_prov: pd.DataFrame, kew_value: str):
|
|
@@ -627,8 +621,7 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
|
|
| 627 |
pop_field = "Pop_Total"
|
| 628 |
name_field = "Kab_Kota_Label"
|
| 629 |
|
| 630 |
-
|
| 631 |
-
agg_base = df.groupby([key_col, label_col], dropna=False).agg(
|
| 632 |
n_total=("Indeks_Dasar_0_100", "size"),
|
| 633 |
Rata2_sub_koleksi=("sub_koleksi", "mean"),
|
| 634 |
Rata2_sub_sdm=("sub_sdm", "mean"),
|
|
@@ -636,39 +629,11 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
|
|
| 636 |
Rata2_sub_pengelolaan=("sub_pengelolaan", "mean"),
|
| 637 |
Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
|
| 638 |
Rata2_dim_kinerja=("dim_kinerja", "mean"),
|
|
|
|
| 639 |
).reset_index()
|
| 640 |
|
| 641 |
-
# ===== KUNCI KONSISTENSI =====
|
| 642 |
-
# indeks dasar per jenis per wilayah, lalu overall = (sekolah+umum+khusus)/3 (missing=0)
|
| 643 |
-
jenis_mean = (
|
| 644 |
-
df[df["_dataset"].isin(["sekolah", "umum", "khusus"])]
|
| 645 |
-
.groupby([key_col, label_col, "_dataset"], dropna=False)["Indeks_Dasar_0_100"]
|
| 646 |
-
.mean()
|
| 647 |
-
.reset_index()
|
| 648 |
-
)
|
| 649 |
-
|
| 650 |
-
piv = jenis_mean.pivot_table(
|
| 651 |
-
index=[key_col, label_col],
|
| 652 |
-
columns="_dataset",
|
| 653 |
-
values="Indeks_Dasar_0_100",
|
| 654 |
-
aggfunc="mean"
|
| 655 |
-
).reset_index()
|
| 656 |
-
|
| 657 |
-
for j in ["sekolah", "umum", "khusus"]:
|
| 658 |
-
if j not in piv.columns:
|
| 659 |
-
piv[j] = 0.0
|
| 660 |
-
piv[["sekolah", "umum", "khusus"]] = piv[["sekolah", "umum", "khusus"]].fillna(0.0)
|
| 661 |
-
piv["Indeks_Dasar_Agregat_0_100"] = (piv["sekolah"] + piv["umum"] + piv["khusus"]) / 3.0
|
| 662 |
-
|
| 663 |
-
agg = agg_base.merge(
|
| 664 |
-
piv[[key_col, label_col, "Indeks_Dasar_Agregat_0_100"]],
|
| 665 |
-
on=[key_col, label_col],
|
| 666 |
-
how="left"
|
| 667 |
-
)
|
| 668 |
-
|
| 669 |
agg = agg.rename(columns={key_col: "group_key", label_col: label_name})
|
| 670 |
|
| 671 |
-
# attach target/pop
|
| 672 |
target_vals, pop_vals, label_fix = [], [], []
|
| 673 |
for _, r in agg.iterrows():
|
| 674 |
gk = r["group_key"]
|
|
@@ -705,10 +670,8 @@ def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, po
|
|
| 705 |
)
|
| 706 |
]
|
| 707 |
|
| 708 |
-
# FINAL wilayah konsisten
|
| 709 |
agg["Indeks_Final_Wilayah_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
|
| 710 |
|
| 711 |
-
# rounding
|
| 712 |
for c in [
|
| 713 |
"Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
|
| 714 |
"Rata2_dim_kepatuhan","Rata2_dim_kinerja"
|
|
@@ -777,12 +740,11 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
|
|
| 777 |
agg["faktor_penyesuaian"] = agg["faktor_penyesuaian_wilayah"]
|
| 778 |
agg["Indeks_Final_Agregat_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
|
| 779 |
|
| 780 |
-
# khusus: optional attach target/pop jenis (hanya untuk info)
|
| 781 |
agg["target_total_68_jenis"] = np.nan
|
| 782 |
agg["pop_total_jenis"] = np.nan
|
| 783 |
agg["coverage_jenis"] = np.nan
|
| 784 |
|
| 785 |
-
if (pop_khusus is not None) and (not pop_khusus.empty) and ("KAB" in kew_norm or "KOTA" in kew_norm or kew_norm
|
| 786 |
pk = pop_khusus.set_index("kab_key")
|
| 787 |
for i, r in agg.iterrows():
|
| 788 |
if str(r.get("Jenis", "")).lower() != "khusus":
|
|
@@ -803,7 +765,6 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
|
|
| 803 |
m2 = agg["pop_total_jenis"].notna() & (agg["pop_total_jenis"] > 0)
|
| 804 |
agg.loc[m2, "coverage_jenis"] = (agg.loc[m2, "Jumlah"].astype(float) / agg.loc[m2, "pop_total_jenis"].astype(float)) * 100.0
|
| 805 |
|
| 806 |
-
# rounding
|
| 807 |
for c in [
|
| 808 |
"Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
|
| 809 |
"Rata2_dim_kepatuhan","Rata2_dim_kinerja"
|
|
@@ -829,8 +790,10 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, agg_total: pd.DataFrame,
|
|
| 829 |
# ============================================================
|
| 830 |
|
| 831 |
def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
|
|
|
|
| 832 |
jenis_list = ["sekolah", "umum", "khusus"]
|
| 833 |
|
|
|
|
| 834 |
def _row_default(jenis):
|
| 835 |
return {
|
| 836 |
"Jenis": jenis,
|
|
@@ -852,6 +815,7 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
|
|
| 852 |
sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
|
| 853 |
if sub.empty:
|
| 854 |
continue
|
|
|
|
| 855 |
rows_by_jenis[jenis] = {
|
| 856 |
"Jenis": jenis,
|
| 857 |
"Jumlah_Wilayah": int(sub.shape[0]),
|
|
@@ -862,16 +826,20 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
|
|
| 862 |
"Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
|
| 863 |
"Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
|
| 864 |
"Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
|
|
|
|
| 865 |
"Indeks_Final_Disesuaikan_0_100": float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean()),
|
| 866 |
}
|
| 867 |
|
|
|
|
| 868 |
rows = [rows_by_jenis[j] for j in jenis_list]
|
| 869 |
|
|
|
|
| 870 |
final_sekolah = float(rows_by_jenis["sekolah"]["Indeks_Final_Disesuaikan_0_100"])
|
| 871 |
final_umum = float(rows_by_jenis["umum"]["Indeks_Final_Disesuaikan_0_100"])
|
| 872 |
final_khusus = float(rows_by_jenis["khusus"]["Indeks_Final_Disesuaikan_0_100"])
|
| 873 |
final_all = (final_sekolah + final_umum + final_khusus) / 3.0
|
| 874 |
|
|
|
|
| 875 |
def _avg3(field):
|
| 876 |
return (float(rows_by_jenis["sekolah"][field]) + float(rows_by_jenis["umum"][field]) + float(rows_by_jenis["khusus"][field])) / 3.0
|
| 877 |
|
|
@@ -892,11 +860,12 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
|
|
| 892 |
"Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
|
| 893 |
"Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
|
| 894 |
"Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
|
| 895 |
-
"Indeks_Final_Disesuaikan_0_100": final_all,
|
| 896 |
})
|
| 897 |
|
| 898 |
out = pd.DataFrame(rows)
|
| 899 |
|
|
|
|
| 900 |
for c in [
|
| 901 |
"Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
|
| 902 |
"Rata2_dim_kepatuhan","Rata2_dim_kinerja"
|
|
@@ -1117,17 +1086,19 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
|
|
| 1117 |
# ============================================================
|
| 1118 |
|
| 1119 |
def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
|
|
|
|
| 1120 |
def _get_final(j):
|
| 1121 |
sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
|
| 1122 |
if sub.empty:
|
| 1123 |
return 0.0
|
| 1124 |
return float(pd.to_numeric(sub["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
|
| 1125 |
|
| 1126 |
-
final_sekolah =
|
| 1127 |
final_umum = _get_final("umum")
|
| 1128 |
final_khusus = _get_final("khusus")
|
| 1129 |
-
final_all = (final_sekolah + final_umum + final_khusus) / 3.0
|
| 1130 |
|
|
|
|
| 1131 |
def _get_dasar(j):
|
| 1132 |
if agg_jenis is None or agg_jenis.empty:
|
| 1133 |
return 0.0
|
|
@@ -1141,6 +1112,7 @@ def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame,
|
|
| 1141 |
dasar_khusus = _get_dasar("khusus")
|
| 1142 |
dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
|
| 1143 |
|
|
|
|
| 1144 |
if agg_total is not None and not agg_total.empty:
|
| 1145 |
n_sum = float(pd.to_numeric(agg_total.get("n_total", 0), errors="coerce").fillna(0).sum())
|
| 1146 |
t_ser = pd.to_numeric(agg_total.get("target_total_68", np.nan), errors="coerce")
|
|
@@ -1201,7 +1173,7 @@ def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg
|
|
| 1201 |
|
| 1202 |
|
| 1203 |
# ============================================================
|
| 1204 |
-
# 13) LLM + WORD (Word pakai Ringkasan Dashboard)
|
| 1205 |
# ============================================================
|
| 1206 |
|
| 1207 |
_HF_CLIENT = None
|
|
@@ -1225,7 +1197,6 @@ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_to
|
|
| 1225 |
lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0); Indeks_Final = Indeks_Dasar_Agregat Γ faktor.")
|
| 1226 |
lines.append("Jenis yang ditampilkan: sekolah, umum, khusus (SEMUA jenis menggunakan faktor wilayah).")
|
| 1227 |
lines.append("Catatan ringkasan: nilai 'keseluruhan' dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, missing=0.")
|
| 1228 |
-
lines.append("Catatan konsistensi: Indeks_Dasar_Agregat wilayah juga dihitung sebagai rata-rata 3 jenis Γ·3 (missing=0).")
|
| 1229 |
|
| 1230 |
if summary_jenis is not None and not summary_jenis.empty:
|
| 1231 |
lines.append("\nRingkasan (jenis + keseluruhan):")
|
|
@@ -1297,14 +1268,16 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
|
|
| 1297 |
doc = Document()
|
| 1298 |
doc.add_heading(f"Laporan IPLM β {wilayah}", level=1)
|
| 1299 |
|
|
|
|
| 1300 |
doc.add_heading("Ringkasan Dashboard", level=2)
|
| 1301 |
-
k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
|
| 1302 |
|
|
|
|
| 1303 |
doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k['final_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
|
| 1304 |
doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
|
| 1305 |
doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
|
| 1306 |
doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
|
| 1307 |
|
|
|
|
| 1308 |
doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
|
| 1309 |
show = summary_jenis.copy()
|
| 1310 |
preferred = [
|
|
@@ -1337,21 +1310,22 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
|
|
| 1337 |
else:
|
| 1338 |
cells[i].text = str(v)
|
| 1339 |
|
|
|
|
| 1340 |
doc.add_heading("Metodologi", level=2)
|
| 1341 |
doc.add_paragraph(
|
| 1342 |
"Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
|
| 1343 |
-
"Nilai kemudian diagregasi per wilayah untuk memperoleh Indeks Dasar wilayah."
|
| 1344 |
-
)
|
| 1345 |
-
doc.add_paragraph(
|
| 1346 |
-
"Indeks Dasar wilayah (keseluruhan) dihitung sebagai rata-rata 3 jenis perpustakaan (sekolah+umum+khusus) Γ· 3, "
|
| 1347 |
-
"dengan missing dianggap 0, agar konsisten antar level analisis."
|
| 1348 |
)
|
| 1349 |
doc.add_paragraph(
|
| 1350 |
"Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
|
| 1351 |
"dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0). "
|
| 1352 |
"Indeks_Final_Wilayah = Indeks_Dasar_Agregat Γ faktor."
|
| 1353 |
)
|
|
|
|
|
|
|
|
|
|
| 1354 |
|
|
|
|
| 1355 |
doc.add_heading("Analisis Naratif (LLM)", level=2)
|
| 1356 |
for p in (analysis_text or "").split("\n"):
|
| 1357 |
if p.strip():
|
|
@@ -1370,7 +1344,7 @@ def _empty_outputs(msg="β οΈ Data belum siap."):
|
|
| 1370 |
empty = pd.DataFrame()
|
| 1371 |
empty_fig = go.Figure()
|
| 1372 |
return (
|
| 1373 |
-
"",
|
| 1374 |
empty, empty, empty, empty, empty,
|
| 1375 |
None, None, None, None, None,
|
| 1376 |
empty_fig, empty_fig, empty_fig,
|
|
@@ -1382,6 +1356,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1382 |
if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
|
| 1383 |
return _empty_outputs("β οΈ Data belum ter-load. Pastikan file tersedia di repo/server.")
|
| 1384 |
|
|
|
|
| 1385 |
df = df_all.copy()
|
| 1386 |
if prov_value and prov_value != "(Semua)":
|
| 1387 |
df = df[df["PROV_DISP"] == prov_value]
|
|
@@ -1399,7 +1374,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1399 |
verif_total = build_verif_total(agg_total)
|
| 1400 |
detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
|
| 1401 |
|
| 1402 |
-
# RAW
|
| 1403 |
raw = df_raw.copy()
|
| 1404 |
if prov_value and prov_value != "(Semua)":
|
| 1405 |
raw = raw[raw["PROV_DISP"] == prov_value]
|
|
@@ -1408,7 +1383,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1408 |
if kew_value and kew_value != "(Semua)":
|
| 1409 |
raw = raw[raw["KEW_NORM"] == kew_value]
|
| 1410 |
|
| 1411 |
-
# Bell curve per
|
| 1412 |
if detail_view is None or detail_view.empty:
|
| 1413 |
fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β Jenis: Sekolah", min_points=2)
|
| 1414 |
fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β Jenis: Umum", min_points=2)
|
|
@@ -1426,6 +1401,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1426 |
fig_umum = _fig_jenis_ent("umum", "Bell Curve β Jenis: Umum (Indeks per Entitas)")
|
| 1427 |
fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β Jenis: Khusus (Indeks per Entitas)")
|
| 1428 |
|
|
|
|
| 1429 |
kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis)
|
| 1430 |
|
| 1431 |
tmpdir = tempfile.mkdtemp()
|
|
@@ -1435,30 +1411,31 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1435 |
|
| 1436 |
p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1437 |
p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1438 |
-
|
| 1439 |
p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1440 |
p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1441 |
|
| 1442 |
summary_jenis.to_excel(p_summary, index=False)
|
| 1443 |
agg_total.to_excel(p_total, index=False)
|
| 1444 |
-
raw.to_excel(
|
| 1445 |
detail_view.to_excel(p_detail, index=False)
|
| 1446 |
verif_total.to_excel(p_verif, index=False)
|
| 1447 |
|
| 1448 |
wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
|
| 1449 |
analysis_text = generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah_txt, kew_value or "(Semua)")
|
|
|
|
|
|
|
| 1450 |
word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, agg_jenis, analysis_text)
|
| 1451 |
|
| 1452 |
msg = (
|
| 1453 |
f"β
Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
|
| 1454 |
-
f"jenis(agregat)={len(agg_jenis)} | ringkasan: 3 jenis selalu tampil & keseluruhan Γ·3
|
| 1455 |
-
f"CONSISTENCY FIX: indeks wilayah keseluruhan juga Γ·3"
|
| 1456 |
)
|
| 1457 |
|
| 1458 |
return (
|
| 1459 |
kpi_md,
|
| 1460 |
summary_jenis, agg_total, agg_jenis, detail_view, verif_total,
|
| 1461 |
-
p_summary, p_total,
|
| 1462 |
fig_umum, fig_sekolah, fig_khusus,
|
| 1463 |
msg, analysis_text
|
| 1464 |
)
|
|
@@ -1468,7 +1445,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1468 |
|
| 1469 |
|
| 1470 |
# ============================================================
|
| 1471 |
-
# 15) UI (NO UPLOAD)
|
| 1472 |
# ============================================================
|
| 1473 |
|
| 1474 |
def ui_load(force=False):
|
|
@@ -1485,172 +1462,118 @@ def ui_load(force=False):
|
|
| 1485 |
prov_vals = [v for v in prov_vals if v and v.strip()]
|
| 1486 |
prov_choices = ["(Semua)"] + sorted(set(prov_vals))
|
| 1487 |
|
| 1488 |
-
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
|
| 1492 |
-
kab_vals = df_all["KAB_DISP"].dropna().astype(str).tolist()
|
| 1493 |
-
kab_vals = [v for v in kab_vals if v and v.strip()]
|
| 1494 |
-
kab_choices = ["(Semua)"] + sorted(set(kab_vals))
|
| 1495 |
|
| 1496 |
return (
|
| 1497 |
df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
|
| 1498 |
gr.update(choices=prov_choices, value="(Semua)"),
|
| 1499 |
gr.update(choices=kab_choices, value="(Semua)"),
|
| 1500 |
-
gr.update(choices=kew_choices, value=
|
| 1501 |
)
|
| 1502 |
|
| 1503 |
-
def
|
|
|
|
| 1504 |
if df_all is None or df_all.empty:
|
| 1505 |
return gr.update(choices=["(Semua)"], value="(Semua)")
|
| 1506 |
-
if prov_value
|
| 1507 |
-
|
| 1508 |
else:
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1518 |
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
|
|
|
| 1522 |
|
| 1523 |
-
|
| 1524 |
-
|
| 1525 |
-
# ---------- STATE ----------
|
| 1526 |
-
st_df_all = gr.State(None)
|
| 1527 |
-
st_df_raw = gr.State(None)
|
| 1528 |
-
st_pop_kab = gr.State(None)
|
| 1529 |
-
st_pop_prov = gr.State(None)
|
| 1530 |
-
st_pop_khusus = gr.State(None)
|
| 1531 |
-
st_meta = gr.State({})
|
| 1532 |
-
st_info = gr.State("")
|
| 1533 |
-
|
| 1534 |
-
# ---------- HEADER ----------
|
| 1535 |
-
gr.Markdown(
|
| 1536 |
-
"""
|
| 1537 |
-
# IPLM 2025 β Dashboard (NO UPLOAD)
|
| 1538 |
-
**Konsistensi hitung keseluruhan wilayah (Kab/Kota/Provinsi) vs individu dijamin dengan:**
|
| 1539 |
-
- Indeks dasar keseluruhan wilayah = (dasar_sekolah + dasar_umum + dasar_khusus) Γ· 3 (missing=0, tetap Γ·3)
|
| 1540 |
-
- Faktor penyesuaian = min(total_terkumpul / target_total_68, 1.0)
|
| 1541 |
-
- Indeks final wilayah = indeks dasar keseluruhan Γ faktor penyesuaian
|
| 1542 |
-
""".strip()
|
| 1543 |
-
)
|
| 1544 |
|
| 1545 |
-
|
|
|
|
| 1546 |
|
| 1547 |
-
|
| 1548 |
-
prov_dd = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)", interactive=True)
|
| 1549 |
-
kab_dd = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)", interactive=True)
|
| 1550 |
-
kew_dd = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)", interactive=True)
|
| 1551 |
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
btn_run = gr.Button("Run", variant="primary")
|
| 1555 |
-
|
| 1556 |
-
# ---------- KPI ----------
|
| 1557 |
-
kpi_html = gr.HTML(value="")
|
| 1558 |
-
|
| 1559 |
-
# ---------- TABLES ----------
|
| 1560 |
-
with gr.Tabs():
|
| 1561 |
-
with gr.Tab("Ringkasan (Jenis + Keseluruhan)"):
|
| 1562 |
-
tbl_summary = gr.Dataframe(
|
| 1563 |
-
headers=["Jenis","Jumlah_Wilayah","Total_Perpus","Rata2_dim_kepatuhan","Rata2_dim_kinerja","Indeks_Final_Disesuaikan_0_100"],
|
| 1564 |
-
interactive=False
|
| 1565 |
-
)
|
| 1566 |
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
|
| 1570 |
-
|
| 1571 |
-
|
| 1572 |
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
|
| 1576 |
-
|
| 1577 |
-
|
| 1578 |
|
| 1579 |
-
#
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
dl_total = gr.File(label="Download Agregat Keseluruhan (xlsx)")
|
| 1583 |
-
dl_raw = gr.File(label="Download RAW Data (xlsx)")
|
| 1584 |
-
dl_detail = gr.File(label="Download Detail Entitas (xlsx)")
|
| 1585 |
-
dl_word = gr.File(label="Download Word Report (docx)")
|
| 1586 |
-
|
| 1587 |
-
# ---------- CHARTS ----------
|
| 1588 |
-
with gr.Tabs():
|
| 1589 |
-
with gr.Tab("Bell Curve β Umum"):
|
| 1590 |
-
fig_umum = gr.Plot()
|
| 1591 |
-
with gr.Tab("Bell Curve β Sekolah"):
|
| 1592 |
-
fig_sekolah = gr.Plot()
|
| 1593 |
-
with gr.Tab("Bell Curve β Khusus"):
|
| 1594 |
-
fig_khusus = gr.Plot()
|
| 1595 |
-
|
| 1596 |
-
# ---------- LLM OUTPUT ----------
|
| 1597 |
-
with gr.Accordion("Analisis Otomatis (LLM)", open=False):
|
| 1598 |
-
status_text = gr.Markdown("")
|
| 1599 |
-
llm_text = gr.Markdown("")
|
| 1600 |
-
|
| 1601 |
-
# ============================================================
|
| 1602 |
-
# EVENTS
|
| 1603 |
-
# ============================================================
|
| 1604 |
-
|
| 1605 |
-
def _on_load():
|
| 1606 |
-
df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info, prov_upd, kab_upd, kew_upd = ui_load(force=False)
|
| 1607 |
-
return (
|
| 1608 |
-
df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
|
| 1609 |
-
info, prov_upd, kab_upd, kew_upd
|
| 1610 |
-
)
|
| 1611 |
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
inputs=[],
|
| 1615 |
-
outputs=[st_df_all, st_df_raw, st_pop_kab, st_pop_prov, st_pop_khusus, st_meta, st_info,
|
| 1616 |
-
info_box, prov_dd, kab_dd, kew_dd]
|
| 1617 |
-
)
|
| 1618 |
|
| 1619 |
-
|
| 1620 |
-
|
| 1621 |
-
*ui_load(force=True)[:7],
|
| 1622 |
-
ui_load(force=True)[6], # info
|
| 1623 |
-
ui_load(force=True)[7], # prov upd
|
| 1624 |
-
ui_load(force=True)[8], # kab upd
|
| 1625 |
-
ui_load(force=True)[9], # kew upd
|
| 1626 |
-
),
|
| 1627 |
-
inputs=[],
|
| 1628 |
-
outputs=[st_df_all, st_df_raw, st_pop_kab, st_pop_prov, st_pop_khusus, st_meta, st_info,
|
| 1629 |
-
info_box, prov_dd, kab_dd, kew_dd]
|
| 1630 |
-
)
|
| 1631 |
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
inputs=[prov_dd, st_df_all],
|
| 1635 |
-
outputs=[kab_dd]
|
| 1636 |
-
)
|
| 1637 |
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1641 |
outputs=[
|
| 1642 |
-
|
| 1643 |
-
|
| 1644 |
-
dl_summary, dl_total,
|
| 1645 |
-
|
| 1646 |
-
|
| 1647 |
]
|
| 1648 |
)
|
| 1649 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1650 |
|
| 1651 |
-
|
| 1652 |
-
# 17) LAUNCH
|
| 1653 |
-
# ============================================================
|
| 1654 |
-
|
| 1655 |
-
if __name__ == "__main__":
|
| 1656 |
-
demo.launch(share=True)
|
|
|
|
| 4 |
|
| 5 |
β
Jenis tampil: sekolah, umum, khusus (khusus ditampilkan sebagai jenis)
|
| 6 |
β
Indeks dasar per entitas: Yeo-Johnson + MinMax nasional per indikator
|
| 7 |
+
β
Agregasi wilayah (mean) β penyesuaian 68% berbasis TOTAL pengumpulan wilayah:
|
| 8 |
faktor_penyesuaian = min(n_total_terkumpul / target_total_68, 1.0)
|
| 9 |
+
Indeks_Final_Wilayah = Indeks_Dasar_Agregat * faktor_penyesuaian
|
| 10 |
+
β
Detail entitas: Indeks_Final_0_100 menempel dari Indeks_Final_Wilayah (bukan per-row)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
β
Bell curve per JENIS berbasis indeks per entitas (row-level)
|
| 12 |
β
LLM analysis + Word
|
| 13 |
β
Download (tanpa upload box)
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
PERBAIKAN UTAMA:
|
| 16 |
+
β
Ringkasan (Jenis + Keseluruhan) selalu tampil 4 baris: sekolah, umum, khusus, keseluruhan
|
| 17 |
+
β
Indeks_Final_Disesuaikan_0_100 (keseluruhan) = (final_sekolah+final_umum+final_khusus)/3 (missing=0, tetap Γ·3)
|
| 18 |
+
β
Dashboard KPI FINAL mengambil dari Ringkasan (baris keseluruhan) β bukan mean baris yang tersedia
|
| 19 |
+
β
Word report: hilangkan "Kewenangan: ..." ganti "Ringkasan Dashboard" (mengacu KPI & Ringkasan)
|
| 20 |
+
β
Tombol "Download Data Mentah (.xlsx)" berisi RAW hasil filter (bukan agregat jenis)
|
| 21 |
"""
|
| 22 |
|
| 23 |
import os
|
|
|
|
| 587 |
|
| 588 |
# ============================================================
|
| 589 |
# 6) AGREGAT WILAYAH (KESELURUHAN) + PENYESUAIAN TOTAL
|
|
|
|
| 590 |
# ============================================================
|
| 591 |
|
| 592 |
def build_agg_wilayah_total(df_filtered: pd.DataFrame, pop_kab: pd.DataFrame, pop_prov: pd.DataFrame, kew_value: str):
|
|
|
|
| 621 |
pop_field = "Pop_Total"
|
| 622 |
name_field = "Kab_Kota_Label"
|
| 623 |
|
| 624 |
+
agg = df.groupby([key_col, label_col], dropna=False).agg(
|
|
|
|
| 625 |
n_total=("Indeks_Dasar_0_100", "size"),
|
| 626 |
Rata2_sub_koleksi=("sub_koleksi", "mean"),
|
| 627 |
Rata2_sub_sdm=("sub_sdm", "mean"),
|
|
|
|
| 629 |
Rata2_sub_pengelolaan=("sub_pengelolaan", "mean"),
|
| 630 |
Rata2_dim_kepatuhan=("dim_kepatuhan", "mean"),
|
| 631 |
Rata2_dim_kinerja=("dim_kinerja", "mean"),
|
| 632 |
+
Indeks_Dasar_Agregat_0_100=("Indeks_Dasar_0_100", "mean"),
|
| 633 |
).reset_index()
|
| 634 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
agg = agg.rename(columns={key_col: "group_key", label_col: label_name})
|
| 636 |
|
|
|
|
| 637 |
target_vals, pop_vals, label_fix = [], [], []
|
| 638 |
for _, r in agg.iterrows():
|
| 639 |
gk = r["group_key"]
|
|
|
|
| 670 |
)
|
| 671 |
]
|
| 672 |
|
|
|
|
| 673 |
agg["Indeks_Final_Wilayah_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
|
| 674 |
|
|
|
|
| 675 |
for c in [
|
| 676 |
"Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
|
| 677 |
"Rata2_dim_kepatuhan","Rata2_dim_kinerja"
|
|
|
|
| 740 |
agg["faktor_penyesuaian"] = agg["faktor_penyesuaian_wilayah"]
|
| 741 |
agg["Indeks_Final_Agregat_0_100"] = agg["Indeks_Dasar_Agregat_0_100"] * agg["faktor_penyesuaian"]
|
| 742 |
|
|
|
|
| 743 |
agg["target_total_68_jenis"] = np.nan
|
| 744 |
agg["pop_total_jenis"] = np.nan
|
| 745 |
agg["coverage_jenis"] = np.nan
|
| 746 |
|
| 747 |
+
if (pop_khusus is not None) and (not pop_khusus.empty) and ("KAB" in kew_norm or "KOTA" in kew_norm or kew_norm == "(SEMUA)" or kew_norm == "(SEMUA)".upper()):
|
| 748 |
pk = pop_khusus.set_index("kab_key")
|
| 749 |
for i, r in agg.iterrows():
|
| 750 |
if str(r.get("Jenis", "")).lower() != "khusus":
|
|
|
|
| 765 |
m2 = agg["pop_total_jenis"].notna() & (agg["pop_total_jenis"] > 0)
|
| 766 |
agg.loc[m2, "coverage_jenis"] = (agg.loc[m2, "Jumlah"].astype(float) / agg.loc[m2, "pop_total_jenis"].astype(float)) * 100.0
|
| 767 |
|
|
|
|
| 768 |
for c in [
|
| 769 |
"Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
|
| 770 |
"Rata2_dim_kepatuhan","Rata2_dim_kinerja"
|
|
|
|
| 790 |
# ============================================================
|
| 791 |
|
| 792 |
def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame):
|
| 793 |
+
# Wajib selalu ada 3 jenis, walau 0
|
| 794 |
jenis_list = ["sekolah", "umum", "khusus"]
|
| 795 |
|
| 796 |
+
# helper bikin row default
|
| 797 |
def _row_default(jenis):
|
| 798 |
return {
|
| 799 |
"Jenis": jenis,
|
|
|
|
| 815 |
sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
|
| 816 |
if sub.empty:
|
| 817 |
continue
|
| 818 |
+
|
| 819 |
rows_by_jenis[jenis] = {
|
| 820 |
"Jenis": jenis,
|
| 821 |
"Jumlah_Wilayah": int(sub.shape[0]),
|
|
|
|
| 826 |
"Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
|
| 827 |
"Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
|
| 828 |
"Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
|
| 829 |
+
# FINAL PER JENIS = mean Indeks_Final_Agregat_0_100 (wilayahΓjenis)
|
| 830 |
"Indeks_Final_Disesuaikan_0_100": float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean()),
|
| 831 |
}
|
| 832 |
|
| 833 |
+
# susun output 3 jenis
|
| 834 |
rows = [rows_by_jenis[j] for j in jenis_list]
|
| 835 |
|
| 836 |
+
# KESELURUHAN harus Γ·3 (missing=0)
|
| 837 |
final_sekolah = float(rows_by_jenis["sekolah"]["Indeks_Final_Disesuaikan_0_100"])
|
| 838 |
final_umum = float(rows_by_jenis["umum"]["Indeks_Final_Disesuaikan_0_100"])
|
| 839 |
final_khusus = float(rows_by_jenis["khusus"]["Indeks_Final_Disesuaikan_0_100"])
|
| 840 |
final_all = (final_sekolah + final_umum + final_khusus) / 3.0
|
| 841 |
|
| 842 |
+
# untuk indikator lain pada keseluruhan, juga pakai rata-rata 3 jenis (missing=0)
|
| 843 |
def _avg3(field):
|
| 844 |
return (float(rows_by_jenis["sekolah"][field]) + float(rows_by_jenis["umum"][field]) + float(rows_by_jenis["khusus"][field])) / 3.0
|
| 845 |
|
|
|
|
| 860 |
"Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
|
| 861 |
"Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
|
| 862 |
"Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
|
| 863 |
+
"Indeks_Final_Disesuaikan_0_100": final_all, # FIX: selalu Γ·3
|
| 864 |
})
|
| 865 |
|
| 866 |
out = pd.DataFrame(rows)
|
| 867 |
|
| 868 |
+
# rounding
|
| 869 |
for c in [
|
| 870 |
"Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
|
| 871 |
"Rata2_dim_kepatuhan","Rata2_dim_kinerja"
|
|
|
|
| 1086 |
# ============================================================
|
| 1087 |
|
| 1088 |
def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
|
| 1089 |
+
# pastikan ada baris 3 jenis + keseluruhan
|
| 1090 |
def _get_final(j):
|
| 1091 |
sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
|
| 1092 |
if sub.empty:
|
| 1093 |
return 0.0
|
| 1094 |
return float(pd.to_numeric(sub["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
|
| 1095 |
|
| 1096 |
+
final_sekolah = _get_final("sekolah")
|
| 1097 |
final_umum = _get_final("umum")
|
| 1098 |
final_khusus = _get_final("khusus")
|
| 1099 |
+
final_all = (final_sekolah + final_umum + final_khusus) / 3.0 # sumber KPI final
|
| 1100 |
|
| 1101 |
+
# dasar = rata-rata 3 jenis (missing=0, tetap Γ·3)
|
| 1102 |
def _get_dasar(j):
|
| 1103 |
if agg_jenis is None or agg_jenis.empty:
|
| 1104 |
return 0.0
|
|
|
|
| 1112 |
dasar_khusus = _get_dasar("khusus")
|
| 1113 |
dasar_all = (dasar_sekolah + dasar_umum + dasar_khusus) / 3.0
|
| 1114 |
|
| 1115 |
+
# cakupan
|
| 1116 |
if agg_total is not None and not agg_total.empty:
|
| 1117 |
n_sum = float(pd.to_numeric(agg_total.get("n_total", 0), errors="coerce").fillna(0).sum())
|
| 1118 |
t_ser = pd.to_numeric(agg_total.get("target_total_68", np.nan), errors="coerce")
|
|
|
|
| 1173 |
|
| 1174 |
|
| 1175 |
# ============================================================
|
| 1176 |
+
# 13) LLM + WORD (Word pakai Ringkasan Dashboard, tanpa "Kewenangan: ...")
|
| 1177 |
# ============================================================
|
| 1178 |
|
| 1179 |
_HF_CLIENT = None
|
|
|
|
| 1197 |
lines.append("Rumus penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0); Indeks_Final = Indeks_Dasar_Agregat Γ faktor.")
|
| 1198 |
lines.append("Jenis yang ditampilkan: sekolah, umum, khusus (SEMUA jenis menggunakan faktor wilayah).")
|
| 1199 |
lines.append("Catatan ringkasan: nilai 'keseluruhan' dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, missing=0.")
|
|
|
|
| 1200 |
|
| 1201 |
if summary_jenis is not None and not summary_jenis.empty:
|
| 1202 |
lines.append("\nRingkasan (jenis + keseluruhan):")
|
|
|
|
| 1268 |
doc = Document()
|
| 1269 |
doc.add_heading(f"Laporan IPLM β {wilayah}", level=1)
|
| 1270 |
|
| 1271 |
+
# Ringkasan Dashboard (mengganti "Kewenangan: ...")
|
| 1272 |
doc.add_heading("Ringkasan Dashboard", level=2)
|
|
|
|
| 1273 |
|
| 1274 |
+
k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
|
| 1275 |
doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k['final_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
|
| 1276 |
doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
|
| 1277 |
doc.add_paragraph(f"Cakupan Sampel (berdasarkan target 68%): {k['cakupan_pct']:.0f}% (min(total_terkumpul/target_68, 1.0))")
|
| 1278 |
doc.add_paragraph(f"Penyesuaian Nilai (rata-rata): {k['dampak']:.2f} poin (faktor penyesuaian mean: {k['faktor_mean']:.3f})")
|
| 1279 |
|
| 1280 |
+
# Tabel Ringkasan (Jenis + Keseluruhan)
|
| 1281 |
doc.add_paragraph("Ringkasan (Jenis + Keseluruhan):")
|
| 1282 |
show = summary_jenis.copy()
|
| 1283 |
preferred = [
|
|
|
|
| 1310 |
else:
|
| 1311 |
cells[i].text = str(v)
|
| 1312 |
|
| 1313 |
+
# Metodologi
|
| 1314 |
doc.add_heading("Metodologi", level=2)
|
| 1315 |
doc.add_paragraph(
|
| 1316 |
"Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
|
| 1317 |
+
"Nilai kemudian diagregasi per wilayah (rata-rata) untuk memperoleh Indeks Dasar wilayah."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1318 |
)
|
| 1319 |
doc.add_paragraph(
|
| 1320 |
"Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
|
| 1321 |
"dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0). "
|
| 1322 |
"Indeks_Final_Wilayah = Indeks_Dasar_Agregat Γ faktor."
|
| 1323 |
)
|
| 1324 |
+
doc.add_paragraph(
|
| 1325 |
+
"Ringkasan 'keseluruhan' dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, dengan missing dianggap 0."
|
| 1326 |
+
)
|
| 1327 |
|
| 1328 |
+
# Analisis LLM
|
| 1329 |
doc.add_heading("Analisis Naratif (LLM)", level=2)
|
| 1330 |
for p in (analysis_text or "").split("\n"):
|
| 1331 |
if p.strip():
|
|
|
|
| 1344 |
empty = pd.DataFrame()
|
| 1345 |
empty_fig = go.Figure()
|
| 1346 |
return (
|
| 1347 |
+
"", # kpi_md
|
| 1348 |
empty, empty, empty, empty, empty,
|
| 1349 |
None, None, None, None, None,
|
| 1350 |
empty_fig, empty_fig, empty_fig,
|
|
|
|
| 1356 |
if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
|
| 1357 |
return _empty_outputs("β οΈ Data belum ter-load. Pastikan file tersedia di repo/server.")
|
| 1358 |
|
| 1359 |
+
# FILTER ANALISIS (df_all)
|
| 1360 |
df = df_all.copy()
|
| 1361 |
if prov_value and prov_value != "(Semua)":
|
| 1362 |
df = df[df["PROV_DISP"] == prov_value]
|
|
|
|
| 1374 |
verif_total = build_verif_total(agg_total)
|
| 1375 |
detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
|
| 1376 |
|
| 1377 |
+
# FILTER RAW DOWNLOAD (df_raw)
|
| 1378 |
raw = df_raw.copy()
|
| 1379 |
if prov_value and prov_value != "(Semua)":
|
| 1380 |
raw = raw[raw["PROV_DISP"] == prov_value]
|
|
|
|
| 1383 |
if kew_value and kew_value != "(Semua)":
|
| 1384 |
raw = raw[raw["KEW_NORM"] == kew_value]
|
| 1385 |
|
| 1386 |
+
# Bell curve per JENIS (per entitas)
|
| 1387 |
if detail_view is None or detail_view.empty:
|
| 1388 |
fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β Jenis: Sekolah", min_points=2)
|
| 1389 |
fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β Jenis: Umum", min_points=2)
|
|
|
|
| 1401 |
fig_umum = _fig_jenis_ent("umum", "Bell Curve β Jenis: Umum (Indeks per Entitas)")
|
| 1402 |
fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β Jenis: Khusus (Indeks per Entitas)")
|
| 1403 |
|
| 1404 |
+
# KPI markdown (FINAL sumber Ringkasan)
|
| 1405 |
kpi_md = build_kpi_markdown(summary_jenis, agg_total, agg_jenis)
|
| 1406 |
|
| 1407 |
tmpdir = tempfile.mkdtemp()
|
|
|
|
| 1411 |
|
| 1412 |
p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1413 |
p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1414 |
+
p_jenis = str(Path(tmpdir) / f"IPLM_RAW_DATA_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1415 |
p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1416 |
p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
|
| 1417 |
|
| 1418 |
summary_jenis.to_excel(p_summary, index=False)
|
| 1419 |
agg_total.to_excel(p_total, index=False)
|
| 1420 |
+
raw.to_excel(p_jenis, index=False)
|
| 1421 |
detail_view.to_excel(p_detail, index=False)
|
| 1422 |
verif_total.to_excel(p_verif, index=False)
|
| 1423 |
|
| 1424 |
wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
|
| 1425 |
analysis_text = generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah_txt, kew_value or "(Semua)")
|
| 1426 |
+
|
| 1427 |
+
# Word report: tanpa "Kewenangan: ..."
|
| 1428 |
word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, agg_jenis, analysis_text)
|
| 1429 |
|
| 1430 |
msg = (
|
| 1431 |
f"β
Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
|
| 1432 |
+
f"jenis(agregat)={len(agg_jenis)} | ringkasan: 3 jenis selalu tampil & keseluruhan Γ·3"
|
|
|
|
| 1433 |
)
|
| 1434 |
|
| 1435 |
return (
|
| 1436 |
kpi_md,
|
| 1437 |
summary_jenis, agg_total, agg_jenis, detail_view, verif_total,
|
| 1438 |
+
p_summary, p_total, p_jenis, p_detail, word_path,
|
| 1439 |
fig_umum, fig_sekolah, fig_khusus,
|
| 1440 |
msg, analysis_text
|
| 1441 |
)
|
|
|
|
| 1445 |
|
| 1446 |
|
| 1447 |
# ============================================================
|
| 1448 |
+
# 15) UI (NO UPLOAD)
|
| 1449 |
# ============================================================
|
| 1450 |
|
| 1451 |
def ui_load(force=False):
|
|
|
|
| 1462 |
prov_vals = [v for v in prov_vals if v and v.strip()]
|
| 1463 |
prov_choices = ["(Semua)"] + sorted(set(prov_vals))
|
| 1464 |
|
| 1465 |
+
kab_choices = ["(Semua)"] + sorted([x for x in df_all["KAB_DISP"].dropna().unique().tolist() if x])
|
| 1466 |
+
kew_choices = ["(Semua)"] + sorted([x for x in df_all["KEW_NORM"].dropna().unique().tolist() if x])
|
| 1467 |
+
default_kew = "PROVINSI" if "PROVINSI" in kew_choices else ("KAB/KOTA" if "KAB/KOTA" in kew_choices else "(Semua)")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1468 |
|
| 1469 |
return (
|
| 1470 |
df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info,
|
| 1471 |
gr.update(choices=prov_choices, value="(Semua)"),
|
| 1472 |
gr.update(choices=kab_choices, value="(Semua)"),
|
| 1473 |
+
gr.update(choices=kew_choices, value=default_kew),
|
| 1474 |
)
|
| 1475 |
|
| 1476 |
+
def on_prov_change(prov_value):
|
| 1477 |
+
df_all, _, _, _, _, _, _ = load_default_files(force=False)
|
| 1478 |
if df_all is None or df_all.empty:
|
| 1479 |
return gr.update(choices=["(Semua)"], value="(Semua)")
|
| 1480 |
+
if prov_value is None or prov_value == "(Semua)":
|
| 1481 |
+
vals = df_all["KAB_DISP"].dropna().unique().tolist()
|
| 1482 |
else:
|
| 1483 |
+
vals = df_all.loc[df_all["PROV_DISP"] == prov_value, "KAB_DISP"].dropna().unique().tolist()
|
| 1484 |
+
vals = sorted([v for v in vals if v])
|
| 1485 |
+
return gr.update(choices=["(Semua)"] + vals, value="(Semua)")
|
| 1486 |
+
|
| 1487 |
+
|
| 1488 |
+
with gr.Blocks() as demo:
|
| 1489 |
+
gr.Markdown(f"""
|
| 1490 |
+
# IPLM 2025 β Final (Penyesuaian Berbasis Kecukupan Sampel 68%)
|
| 1491 |
+
**Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
|
| 1492 |
+
- `DATA_FILE` = **{DATA_FILE}**
|
| 1493 |
+
- `POP_KAB` = **{POP_KAB}**
|
| 1494 |
+
- `POP_PROV` = **{POP_PROV}**
|
| 1495 |
+
- `POP_KHUSUS` = **{POP_KHUSUS}**
|
| 1496 |
+
|
| 1497 |
+
**Update FIX (konsistensi dashboard):**
|
| 1498 |
+
- Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
|
| 1499 |
+
- **Keseluruhan = rata-rata 3 jenis Γ·3 (missing=0)**
|
| 1500 |
+
- KPI FINAL dashboard sumber dari Ringkasan (bukan mean baris yang ada)
|
| 1501 |
+
- Download Data Mentah = RAW hasil filter
|
| 1502 |
+
- Laporan Word: tanpa baris "Kewenangan: ...", diganti "Ringkasan Dashboard"
|
| 1503 |
+
""")
|
| 1504 |
+
|
| 1505 |
+
state_df = gr.State(None)
|
| 1506 |
+
state_raw = gr.State(None)
|
| 1507 |
+
state_pop_kab = gr.State(None)
|
| 1508 |
+
state_pop_prov = gr.State(None)
|
| 1509 |
+
state_pop_khusus = gr.State(None)
|
| 1510 |
+
state_meta = gr.State({})
|
| 1511 |
+
|
| 1512 |
+
info_box = gr.Markdown()
|
| 1513 |
|
| 1514 |
+
with gr.Row():
|
| 1515 |
+
dd_prov = gr.Dropdown(label="Provinsi", choices=["(Semua)"], value="(Semua)")
|
| 1516 |
+
dd_kab = gr.Dropdown(label="Kab/Kota", choices=["(Semua)"], value="(Semua)")
|
| 1517 |
+
dd_kew = gr.Dropdown(label="Kewenangan", choices=["(Semua)"], value="(Semua)")
|
| 1518 |
|
| 1519 |
+
dd_prov.change(fn=on_prov_change, inputs=[dd_prov], outputs=dd_kab)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1520 |
|
| 1521 |
+
run_btn = gr.Button("Jalankan Perhitungan")
|
| 1522 |
+
msg_out = gr.Markdown()
|
| 1523 |
|
| 1524 |
+
kpi_out = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
| 1525 |
|
| 1526 |
+
gr.Markdown("## Ringkasan (Jenis + Keseluruhan)")
|
| 1527 |
+
out_summary = gr.DataFrame(interactive=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1528 |
|
| 1529 |
+
gr.Markdown("## Agregat Wilayah (Keseluruhan)")
|
| 1530 |
+
out_agg_total = gr.DataFrame(interactive=False)
|
| 1531 |
|
| 1532 |
+
gr.Markdown("## Agregat Wilayah Γ Jenis (Sekolah, Umum, Khusus)")
|
| 1533 |
+
out_agg_jenis = gr.DataFrame(interactive=False)
|
| 1534 |
|
| 1535 |
+
gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
|
| 1536 |
+
out_detail = gr.DataFrame(interactive=False)
|
| 1537 |
|
| 1538 |
+
gr.Markdown("## Kecukupan Sampel 68% (tanpa angka koma)")
|
| 1539 |
+
out_verif = gr.DataFrame(interactive=False)
|
| 1540 |
|
| 1541 |
+
gr.Markdown("## Bell Curve β per Jenis Perpustakaan (Indeks per Entitas)")
|
| 1542 |
+
gr.Markdown("### Perpustakaan Umum")
|
| 1543 |
+
bell_umum = gr.Plot(scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1544 |
|
| 1545 |
+
gr.Markdown("### Perpustakaan Sekolah")
|
| 1546 |
+
bell_sekolah = gr.Plot(scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1547 |
|
| 1548 |
+
gr.Markdown("### Perpustakaan Khusus")
|
| 1549 |
+
bell_khusus = gr.Plot(scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1550 |
|
| 1551 |
+
gr.Markdown("## Analisis Otomatis (LLM)")
|
| 1552 |
+
analysis_out = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
| 1553 |
|
| 1554 |
+
with gr.Row():
|
| 1555 |
+
dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
|
| 1556 |
+
dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
|
| 1557 |
+
dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
|
| 1558 |
+
dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
|
| 1559 |
+
dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
|
| 1560 |
+
|
| 1561 |
+
run_btn.click(
|
| 1562 |
+
fn=run_calc,
|
| 1563 |
+
inputs=[dd_prov, dd_kab, dd_kew, state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta],
|
| 1564 |
outputs=[
|
| 1565 |
+
kpi_out,
|
| 1566 |
+
out_summary, out_agg_total, out_agg_jenis, out_detail, out_verif,
|
| 1567 |
+
dl_summary, dl_total, dl_jenis, dl_detail, dl_word,
|
| 1568 |
+
bell_umum, bell_sekolah, bell_khusus,
|
| 1569 |
+
msg_out, analysis_out
|
| 1570 |
]
|
| 1571 |
)
|
| 1572 |
|
| 1573 |
+
demo.load(
|
| 1574 |
+
fn=lambda: ui_load(force=False),
|
| 1575 |
+
inputs=[],
|
| 1576 |
+
outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
|
| 1577 |
+
)
|
| 1578 |
|
| 1579 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|