irhamni commited on
Commit
47ecaa5
Β·
verified Β·
1 Parent(s): 2605500

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +429 -551
app.py CHANGED
@@ -355,11 +355,6 @@ def _parse_pop_khusus(path_xlsx: str) -> pd.DataFrame:
355
  if c_mix is None:
356
  raise ValueError("POP_KHUSUS: kolom gabungan Provinsi/Kab/Kota tidak ditemukan.")
357
 
358
- # =========================
359
- # UPDATE SESUAI REQUEST:
360
- # POP khusus ada di kolom POP_KHUSUS
361
- # target 68% khusus ada di kolom SAMPEL_KHUSUS_68%
362
- # =========================
363
  c_target = pick_col(df, [
364
  "SAMPEL_KHUSUS_68%", "Sampel_Khusus_68%", "sampel_khusus_68%",
365
  "SAMPEL_KHUSUS_68", "Sampel_Khusus_68", "sampel_khusus_68",
@@ -367,7 +362,6 @@ def _parse_pop_khusus(path_xlsx: str) -> pd.DataFrame:
367
  "sampel_total","Sampel_total","TOTAL_SAMPEL","total_sampel",
368
  "target","Target","Sampel"
369
  ])
370
-
371
  c_pop = pick_col(df, [
372
  "POP_KHUSUS", "Pop_Khusus", "pop_khusus",
373
  "total_populasi","Total Populasi","POPULASI","populasi",
@@ -383,8 +377,8 @@ def _parse_pop_khusus(path_xlsx: str) -> pd.DataFrame:
383
  c_target = numeric_cols[0]
384
 
385
  mix = df[c_mix].astype(str).fillna("").str.strip()
386
- target_series = df[c_target].apply(coerce_num) if c_target else pd.Series([np.nan]*len(df))
387
- pop_series = df[c_pop].apply(coerce_num) if c_pop else pd.Series([np.nan]*len(df))
388
 
389
  rows = []
390
  current_prov = None
@@ -420,29 +414,7 @@ def _parse_pop_khusus(path_xlsx: str) -> pd.DataFrame:
420
  m_need_target = pop["Target68_Total_Jenis"].isna() & pop["Pop_Total_Jenis"].notna() & (pop["Pop_Total_Jenis"] > 0)
421
  pop.loc[m_need_target, "Target68_Total_Jenis"] = pop.loc[m_need_target, "Pop_Total_Jenis"] * float(FALLBACK_TARGET_RATIO)
422
 
423
- # agregat per kab (tetap seperti sebelumnya)
424
- pop = pop.groupby("kab_key", as_index=False).agg({
425
- "Kab_Kota_Label": "first",
426
- "Provinsi_Label": "first",
427
- "Target68_Total_Jenis": "max",
428
- "Pop_Total_Jenis": "max",
429
- "prov_key": "first",
430
- })
431
- return pop
432
-
433
-
434
- pop["kab_key"] = pop["Kab_Kota_Label"].apply(norm_kab_label)
435
- pop["prov_key"] = pop["Provinsi_Label"].apply(norm_prov_label)
436
-
437
- pop["Target68_Total_Jenis"] = pd.to_numeric(pop["Target68_Total_Jenis"], errors="coerce")
438
- pop["Pop_Total_Jenis"] = pd.to_numeric(pop["Pop_Total_Jenis"], errors="coerce")
439
-
440
- m_need_pop = pop["Pop_Total_Jenis"].isna() & pop["Target68_Total_Jenis"].notna() & (pop["Target68_Total_Jenis"] > 0)
441
- pop.loc[m_need_pop, "Pop_Total_Jenis"] = pop.loc[m_need_pop, "Target68_Total_Jenis"] / float(FALLBACK_TARGET_RATIO)
442
-
443
- m_need_target = pop["Target68_Total_Jenis"].isna() & pop["Pop_Total_Jenis"].notna() & (pop["Pop_Total_Jenis"] > 0)
444
- pop.loc[m_need_target, "Target68_Total_Jenis"] = pop.loc[m_need_target, "Pop_Total_Jenis"] * float(FALLBACK_TARGET_RATIO)
445
-
446
  pop = pop.groupby("kab_key", as_index=False).agg({
447
  "Kab_Kota_Label": "first",
448
  "Provinsi_Label": "first",
@@ -452,40 +424,60 @@ def _parse_pop_khusus(path_xlsx: str) -> pd.DataFrame:
452
  })
453
  return pop
454
 
455
- def load_default_files(force=False):
456
  key = (
457
  DATA_FILE, POP_KAB, POP_PROV, POP_KHUSUS,
458
  _mtime(DATA_FILE), _mtime(POP_KAB), _mtime(POP_PROV), _mtime(POP_KHUSUS)
459
  )
460
 
461
  if (not force) and _CACHE["key"] == key and _CACHE["df_all"] is not None:
462
- return _CACHE["df_all"], _CACHE["df_raw"], _CACHE["pop_kab"], _CACHE["pop_prov"], _CACHE["pop_khusus"], _CACHE["meta"], _CACHE["info"]
 
 
 
 
463
 
464
  for p, label in [(DATA_FILE, "DM"), (POP_KAB, "POP_KAB"), (POP_PROV, "POP_PROV"), (POP_KHUSUS, "POP_KHUSUS")]:
465
  if not Path(p).exists():
466
  info = f"❌ File {label} tidak ditemukan: `{p}`"
467
- _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
 
 
 
 
468
  return None, None, None, None, None, {}, info
469
 
 
 
 
470
  fp = Path(DATA_FILE)
471
  xls = pd.ExcelFile(fp)
472
  frames = [pd.read_excel(fp, sheet_name=s) for s in xls.sheet_names]
473
  df_raw = pd.concat(frames, ignore_index=True, sort=False)
474
 
475
- prov_col = pick_col(df_raw, ["provinsi", "Provinsi", "PROVINSI"])
476
- kab_col = pick_col(df_raw, ["kab_kota", "Kab/Kota", "Kab_Kota", "KAB/KOTA", "kabupaten_kota", "Kabupaten/Kota", "kabupaten kota", "kota"])
477
- kew_col = pick_col(df_raw, ["kewenangan", "jenis_kewenangan", "Kewenangan", "KEWENANGAN"])
478
  jenis_col = pick_col(df_raw, ["jenis_perpustakaan", "Jenis Perpustakaan", "JENIS_PERPUSTAKAAN"])
479
- nama_col = pick_col(df_raw, ["nm_perpustakaan","nama_perpustakaan","Nama Perpustakaan","nm_instansi_lembaga","nm_perpus"])
480
 
481
  missing = []
482
- if prov_col is None: missing.append("Provinsi")
483
- if kab_col is None: missing.append("Kab/Kota")
484
- if kew_col is None: missing.append("Kewenangan")
485
- if jenis_col is None: missing.append("Jenis Perpustakaan")
 
 
 
 
 
486
  if missing:
487
  info = f"❌ Kolom wajib tidak ditemukan di DM: {', '.join(missing)}"
488
- _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
 
 
 
 
489
  return None, None, None, None, None, {}, info
490
 
491
  val_map_jenis = {
@@ -494,12 +486,12 @@ def load_default_files(force=False):
494
  "PERPUSTAKAAN KHUSUS": "khusus", "KHUSUS": "khusus",
495
  }
496
 
497
- df_raw["KEW_NORM"] = df_raw[kew_col].apply(norm_kew)
498
- df_raw["_dataset"] = df_raw[jenis_col].astype(str).str.strip().str.upper().map(val_map_jenis)
499
  df_raw["PROV_DISP"] = df_raw[prov_col].apply(norm_prov_disp)
500
- df_raw["KAB_DISP"] = df_raw[kab_col].apply(_disp_text)
501
- df_raw["prov_key"] = df_raw["PROV_DISP"].apply(norm_prov_label)
502
- df_raw["kab_key"] = df_raw["KAB_DISP"].apply(norm_kab_label)
503
 
504
  if nama_col and nama_col in df_raw.columns:
505
  kcols = [prov_col, kab_col, kew_col, jenis_col, nama_col]
@@ -513,11 +505,11 @@ def load_default_files(force=False):
513
  after = len(df_raw)
514
 
515
  # =========================
516
- # POP KAB
517
  # =========================
518
  pk = pd.read_excel(POP_KAB)
519
 
520
- c_kab = pick_col(pk, ["KABUPATEN_KOTA","Kab/Kota","Kabupaten/Kota","KAB/KOTA","Kabupaten_Kota","kab_kota","kabupaten_kota"])
521
  c_prov = pick_col(pk, ["PROVINSI","Provinsi","provinsi"])
522
 
523
  c_target_total = pick_col(pk, [
@@ -525,80 +517,113 @@ def load_default_files(force=False):
525
  "target_total_68","Target_Total_68","target_68","TARGET_68"
526
  ])
527
 
528
- c_pop_total = pick_col(pk, [
529
- "total_populasi","Total Populasi","POPULASI","populasi",
530
- "jumlah_penduduk","Jumlah Penduduk","PENDUDUK","penduduk",
531
- "total_penduduk","Total Penduduk","TOTAL_PENDUDUK","total_pend",
532
- "jumlah_populasi","Jumlah Populasi","pop_total","Pop_Total",
533
- "n_populasi","N_POPULASI","n_penduduk","N_PENDUDUK"
534
- ])
535
 
536
  if c_kab is None or c_target_total is None:
537
  info = "❌ POP_KAB: wajib ada kolom Kab/Kota dan sampel_total (target 68%)."
538
- _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
 
 
 
 
539
  return None, None, None, None, None, {}, info
540
 
541
  pop_kab = pd.DataFrame({
542
  "Provinsi_Label": pk[c_prov].astype(str).str.strip() if c_prov else "",
543
  "Kab_Kota_Label": pk[c_kab].astype(str).str.strip(),
544
  "Target68_Total": pk[c_target_total].apply(coerce_num),
545
- "Pop_Total": pk[c_pop_total].apply(coerce_num) if c_pop_total else np.nan,
 
 
 
546
  })
547
 
548
- pop_kab["Pop_Total"] = pd.to_numeric(pop_kab["Pop_Total"], errors="coerce")
549
- pop_kab["Target68_Total"] = pd.to_numeric(pop_kab["Target68_Total"], errors="coerce")
 
 
 
 
550
 
551
- mask_need_pop = pop_kab["Pop_Total"].isna() & pop_kab["Target68_Total"].notna() & (pop_kab["Target68_Total"] > 0)
552
- pop_kab.loc[mask_need_pop, "Pop_Total"] = pop_kab.loc[mask_need_pop, "Target68_Total"] / float(FALLBACK_TARGET_RATIO)
 
 
 
 
 
553
 
554
  pop_kab["kab_key"] = pop_kab["Kab_Kota_Label"].apply(norm_kab_label)
 
555
  pop_kab = pop_kab.groupby("kab_key", as_index=False).agg({
556
- "Kab_Kota_Label":"first",
557
- "Provinsi_Label":"first",
558
- "Target68_Total":"max",
559
- "Pop_Total":"max",
 
 
 
 
560
  })
561
 
562
  # =========================
563
- # POP PROV
564
  # =========================
565
  pp = pd.read_excel(POP_PROV)
566
 
567
  c_pr = pick_col(pp, ["Provinsi","PROVINSI","provinsi","Propinsi","PROPINSI","propinsi"])
568
- c_target_total = pick_col(pp, [
569
- "total _sampel","total_sampel","TOTAL_SAMPEL","Total Sampel",
570
- "target_total_68","Target_Total_68","target_68","TARGET_68"
571
- ])
572
- c_pop_total = pick_col(pp, [
573
- "total_populasi","Total Populasi","POPULASI","populasi",
574
- "jumlah_penduduk","Jumlah Penduduk","PENDUDUK","penduduk",
575
- "total_penduduk","Total Penduduk","TOTAL_PENDUDUK","total_pend",
576
- "total_pend_dukcapil","TOTAL_PEND_DUKCAPIL",
577
- "pop_total","Pop_Total"
578
- ])
579
 
580
  if c_pr is None or c_target_total is None:
581
  info = "❌ POP_PROV: wajib ada kolom Provinsi dan total _sampel (target 68%)."
582
- _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
 
 
 
 
583
  return None, None, None, None, None, {}, info
584
 
585
  pop_prov = pd.DataFrame({
586
  "Provinsi_Label": pp[c_pr].astype(str).str.strip(),
587
  "Target68_Total_Prov": pp[c_target_total].apply(coerce_num),
588
- "Pop_Total_Prov": pp[c_pop_total].apply(coerce_num) if c_pop_total else np.nan,
 
 
 
 
 
589
  })
590
 
591
- pop_prov["Pop_Total_Prov"] = pd.to_numeric(pop_prov["Pop_Total_Prov"], errors="coerce")
592
- pop_prov["Target68_Total_Prov"] = pd.to_numeric(pop_prov["Target68_Total_Prov"], errors="coerce")
 
 
 
 
 
593
 
594
- mask_need_pop = pop_prov["Pop_Total_Prov"].isna() & pop_prov["Target68_Total_Prov"].notna() & (pop_prov["Target68_Total_Prov"] > 0)
595
- pop_prov.loc[mask_need_pop, "Pop_Total_Prov"] = pop_prov.loc[mask_need_pop, "Target68_Total_Prov"] / float(FALLBACK_TARGET_RATIO)
596
 
597
  pop_prov["prov_key"] = pop_prov["Provinsi_Label"].apply(norm_prov_label)
 
598
  pop_prov = pop_prov.groupby("prov_key", as_index=False).agg({
599
- "Provinsi_Label":"first",
600
- "Target68_Total_Prov":"max",
601
- "Pop_Total_Prov":"max",
 
 
 
 
602
  })
603
 
604
  # =========================
@@ -608,19 +633,22 @@ def load_default_files(force=False):
608
  pop_khusus = _parse_pop_khusus(POP_KHUSUS)
609
  except Exception as e:
610
  info = f"❌ POP_KHUSUS gagal dibaca: {repr(e)}"
611
- _CACHE.update({"key": key, "df_all": None, "df_raw": None, "pop_kab": None, "pop_prov": None, "pop_khusus": None, "meta": {}, "info": info})
 
 
 
 
612
  return None, None, None, None, None, {}, info
613
 
614
  df_all = prepare_global(df_raw)
615
-
616
  meta = dict(prov_col=prov_col, kab_col=kab_col, kew_col=kew_col, jenis_col=jenis_col, nama_col=nama_col)
617
 
618
  info = (
619
  f"βœ… Mode NO UPLOAD (cache aktif)<br>"
620
  f"βœ… DM: <b>{fp.name}</b> | Baris: {before} β†’ dedup: {after}<br>"
621
- f"βœ… POP_KAB: <b>{Path(POP_KAB).name}</b> (n={len(pop_kab)}) β€” target 68% via <code>sampel_total</code> (Pop_Total auto fallback)<br>"
622
- f"βœ… POP_PROV: <b>{Path(POP_PROV).name}</b> (n={len(pop_prov)}) β€” target 68% via <code>total _sampel</code> (Pop_Total auto fallback)<br>"
623
- f"βœ… POP_KHUSUS: <b>{Path(POP_KHUSUS).name}</b> (n={len(pop_khusus)}) β€” format gabungan Provinsi/Kab/Kota (Target/Pop auto fallback)<br>"
624
  f"πŸ•’ mtime: DM={time.ctime(_mtime(DATA_FILE))} | Kab={time.ctime(_mtime(POP_KAB))} | Prov={time.ctime(_mtime(POP_PROV))} | Khusus={time.ctime(_mtime(POP_KHUSUS))}"
625
  )
626
 
@@ -634,74 +662,35 @@ def load_default_files(force=False):
634
  "meta": meta,
635
  "info": info
636
  })
 
637
  return df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info
638
 
639
 
640
  # ============================================================
641
- # 6) FAKTOR WILAYAH β€” PER JENIS (PATCH UTAMA)
 
 
642
  # ============================================================
643
 
644
  def _read_target_pop_per_jenis_from_pop(pop_df: pd.DataFrame, mode: str):
645
- """
646
- Mengambil mapping target/pop PER JENIS untuk sekolah & umum dari POP_KAB/POP_PROV
647
- sesuai nama kolom REAL di file Excel user.
648
-
649
- Return:
650
- dict: {"sekolah": (target_col, pop_col), "umum": (target_col, pop_col)}
651
- """
652
  if pop_df is None or pop_df.empty:
653
  return {"sekolah": (None, None), "umum": (None, None)}
654
 
655
- # =========================
656
- # POP KAB (Data_populasi_Kab_kota_fixed.xlsx)
657
- # - umum: jumlah_populasi_umum, Sampel_umum_68%
658
- # - sekolah: jumlah_populasi_sekolah, Sampel_sekolah_68%
659
- # =========================
660
- sekolah_target = pick_col(pop_df, [
661
- "Sampel_sekolah_68%", "Sampel_sekolah_68", "SAMPEL_SEKOLAH_68%", "SAMPEL_SEKOLAH_68",
662
- "TARGET_SEKOLAH_68", "Target_Sekolah_68", "target_sekolah_68",
663
- "SAMPEL_SEKOLAH_68", "Sampel_Sekolah_68"
664
- ])
665
- sekolah_pop = pick_col(pop_df, [
666
- "jumlah_populasi_sekolah", "Jumlah_populasi_sekolah", "JUMLAH_POPULASI_SEKOLAH",
667
- "POP_SEKOLAH", "Pop_Sekolah", "pop_sekolah",
668
- "POPULASI_SEKOLAH", "Populasi_Sekolah"
669
- ])
670
 
671
- umum_target = pick_col(pop_df, [
672
- "Sampel_umum_68%", "Sampel_umum_68", "SAMPEL_UMUM_68%", "SAMPEL_UMUM_68",
673
- "TARGET_UMUM_68", "Target_Umum_68", "target_umum_68",
674
- "SAMPEL_UMUM_68", "Sampel_Umum_68"
675
- ])
676
- umum_pop = pick_col(pop_df, [
677
- "jumlah_populasi_umum", "Jumlah_populasi_umum", "JUMLAH_POPULASI_UMUM",
678
- "POP_UMUM", "Pop_Umum", "pop_umum",
679
- "POPULASI_UMUM", "Populasi_Umum",
680
- # POP PROV umum:
681
- "perpus_umum_prop", "Perpus_umum_prop", "PERPUS_UMUM_PROP"
682
- ])
683
-
684
- # =========================
685
- # POP PROV (Data_populasi_propinsi.xlsx)
686
- # - sekolah: total_pend, total _sampel
687
- # - umum: perpus_umum_prop, target dihitung jika tidak ada
688
- # =========================
689
- if str(mode).upper() == "PROV":
690
- # override sekolah kalau ada kolom prov yang lebih spesifik
691
- sekolah_pop2 = pick_col(pop_df, ["total_pend", "TOTAL_PEND", "total_penduduk", "Total Pend"])
692
- sekolah_target2 = pick_col(pop_df, ["total _sampel", "total_sampel", "TOTAL_SAMPEL", "Total Sampel"])
693
-
694
- if sekolah_pop2 is not None:
695
- sekolah_pop = sekolah_pop2
696
- if sekolah_target2 is not None:
697
- sekolah_target = sekolah_target2
698
-
699
- # umum target prov kadang tidak ada -> akan dihitung dari pop (0.68 * pop) di bawah
700
- # (jadi umum_target boleh None)
701
 
702
  return {"sekolah": (sekolah_target, sekolah_pop), "umum": (umum_target, umum_pop)}
703
 
704
-
705
  def build_faktor_wilayah_jenis(
706
  df_filtered: pd.DataFrame,
707
  pop_kab: pd.DataFrame,
@@ -709,13 +698,6 @@ def build_faktor_wilayah_jenis(
709
  pop_khusus: pd.DataFrame,
710
  kew_value: str
711
  ):
712
- """
713
- Output: faktor per (wilayah x jenis)
714
- Kolom:
715
- group_key, [Kab/Kota|Provinsi], Jenis,
716
- n_jenis, target_total_68_jenis, pop_total_jenis,
717
- coverage_jenis_%, faktor_penyesuaian_jenis, gap_target68_jenis
718
- """
719
  if df_filtered is None or df_filtered.empty:
720
  return pd.DataFrame()
721
 
@@ -725,7 +707,6 @@ def build_faktor_wilayah_jenis(
725
  if df.empty:
726
  return pd.DataFrame()
727
 
728
- # tentukan level
729
  if "PROV" in kew_norm:
730
  key_col, label_col, label_name, mode = "prov_key", "PROV_DISP", "Provinsi", "PROV"
731
  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([]))
@@ -733,7 +714,6 @@ def build_faktor_wilayah_jenis(
733
  key_col, label_col, label_name, mode = "kab_key", "KAB_DISP", "Kab/Kota", "KAB"
734
  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([]))
735
 
736
- # hitung n per jenis
737
  base_n = (
738
  df.groupby([key_col, label_col, "_dataset"], dropna=False)
739
  .size()
@@ -742,75 +722,71 @@ def build_faktor_wilayah_jenis(
742
  )
743
  base_n["Jenis"] = base_n["Jenis"].astype(str).str.lower().str.strip()
744
 
745
- # mapping kolom target/pop sesuai Excel user
746
- tp_map = _read_target_pop_per_jenis_from_pop(pop_base.reset_index(), mode=mode)
747
-
748
- # default 0 (biar tidak NaN)
749
  base_n["target_total_68_jenis"] = 0.0
750
  base_n["pop_total_jenis"] = 0.0
751
 
752
- # =========================
753
- # sekolah & umum dari POP_KAB / POP_PROV
754
- # =========================
755
  for j in ["sekolah", "umum"]:
756
- tcol, pcol = tp_map.get(j, (None, None))
757
  if pop_base.empty:
758
  continue
759
 
760
- # pop
761
  if pcol is not None and pcol in pop_base.columns:
762
  pser = pd.to_numeric(pop_base[pcol], errors="coerce").fillna(0.0)
763
  else:
764
  pser = pd.Series(0.0, index=pop_base.index)
765
 
766
- # target (kalau tidak ada kolom target khususβ€”khususnya PROV untuk umumβ€”hitung dari pop)
767
  if tcol is not None and tcol in pop_base.columns:
768
  tser = pd.to_numeric(pop_base[tcol], errors="coerce").fillna(0.0)
769
  else:
770
- # fallback: target = 0.68 * pop (khusus PROV untuk umum biasanya)
771
  tser = (pser.astype(float) * float(FALLBACK_TARGET_RATIO)).fillna(0.0)
772
 
773
  mask = base_n["Jenis"].eq(j)
774
  base_n.loc[mask, "pop_total_jenis"] = base_n.loc[mask, "group_key"].map(pser).fillna(0.0).values
775
  base_n.loc[mask, "target_total_68_jenis"] = base_n.loc[mask, "group_key"].map(tser).fillna(0.0).values
776
 
777
- # =========================
778
  # KHUSUS dari POP_KHUSUS (sum per wilayah)
779
- # =========================
780
  if pop_khusus is not None and not pop_khusus.empty:
781
  pk = pop_khusus.copy()
782
- pk["Target68_Total_Jenis"] = pd.to_numeric(pk.get("Target68_Total_Jenis", np.nan), errors="coerce").fillna(0.0)
783
- pk["Pop_Total_Jenis"] = pd.to_numeric(pk.get("Pop_Total_Jenis", np.nan), errors="coerce").fillna(0.0)
784
 
785
  if mode == "PROV" and "prov_key" in pk.columns:
786
- pk_map = pk.groupby("prov_key", as_index=False).agg(
787
- target_total_68_jenis=("Target68_Total_Jenis", "sum"),
788
- pop_total_jenis=("Pop_Total_Jenis", "sum"),
789
- ).rename(columns={"prov_key": "group_key"})
 
 
 
 
790
  elif mode == "KAB" and "kab_key" in pk.columns:
791
- pk_map = pk.groupby("kab_key", as_index=False).agg(
792
- target_total_68_jenis=("Target68_Total_Jenis", "sum"),
793
- pop_total_jenis=("Pop_Total_Jenis", "sum"),
794
- ).rename(columns={"kab_key": "group_key"})
 
 
 
 
795
  else:
796
- pk_map = pd.DataFrame(columns=["group_key","target_total_68_jenis","pop_total_jenis"])
797
 
798
  if not pk_map.empty:
799
- mask_khusus = base_n["Jenis"].eq("khusus")
800
- tmp = base_n.loc[mask_khusus, ["group_key"]].merge(pk_map, on="group_key", how="left")
801
- base_n.loc[mask_khusus, "target_total_68_jenis"] = pd.to_numeric(tmp["target_total_68_jenis"], errors="coerce").fillna(0.0).values
802
- base_n.loc[mask_khusus, "pop_total_jenis"] = pd.to_numeric(tmp["pop_total_jenis"], errors="coerce").fillna(0.0).values
803
 
804
- # =========================
805
- # fallback pop dari target (kalau pop masih 0 tapi target ada)
806
- # =========================
807
  base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0.0)
808
  base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0.0)
809
 
810
  m_need_pop = (base_n["pop_total_jenis"] <= 0) & (base_n["target_total_68_jenis"] > 0)
811
  base_n.loc[m_need_pop, "pop_total_jenis"] = base_n.loc[m_need_pop, "target_total_68_jenis"] / float(FALLBACK_TARGET_RATIO)
812
 
813
- # faktor
814
  base_n["faktor_penyesuaian_jenis"] = [
815
  faktor_penyesuaian_total(n, t)
816
  for n, t in zip(
@@ -820,7 +796,7 @@ def build_faktor_wilayah_jenis(
820
  ]
821
 
822
  base_n["coverage_jenis_%"] = [
823
- (safe_div(n, p) * 100) if (p is not None and not pd.isna(p) and float(p) > 0) else 0.0
824
  for n, p in zip(
825
  pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
826
  pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).astype(float).tolist()
@@ -828,19 +804,20 @@ def build_faktor_wilayah_jenis(
828
  ]
829
 
830
  base_n["gap_target68_jenis"] = [
831
- max(t - n, 0)
832
  for n, t in zip(
833
  pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
834
  pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).astype(float).tolist()
835
  )
836
  ]
837
 
838
- # DISPLAY sesuai request (target/pop int, coverage 2 desimal)
839
  base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
840
  base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).round(0).astype(int)
 
 
841
  base_n["coverage_jenis_%"] = pd.to_numeric(base_n["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
842
  base_n["faktor_penyesuaian_jenis"] = pd.to_numeric(base_n["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
843
- base_n["gap_target68_jenis"] = pd.to_numeric(base_n["gap_target68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
844
 
845
  return base_n
846
 
@@ -923,7 +900,8 @@ def build_agg_wilayah_jenis(df_filtered: pd.DataFrame, faktor_wilayah_jenis: pd.
923
 
924
  return agg
925
  # ============================================================
926
- # 8) AGREGAT WILAYAH (KESELURUHAN) β€” FIX: avg3 + tampilkan POP/TARGET/N per jenis
 
927
  # ============================================================
928
 
929
  def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
@@ -976,69 +954,59 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_j
976
  Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
977
  )
978
 
979
- # --- tempel POP/TARGET/N/COVERAGE/FAKTOR per jenis ke agg_total ---
980
  if faktor_wilayah_jenis is not None and not faktor_wilayah_jenis.empty:
981
  fw = faktor_wilayah_jenis.copy()
982
  fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
983
 
984
- # pivot per jenis
985
  piv = fw.pivot_table(
986
  index=["group_key", label_name],
987
  columns="Jenis",
988
- values=[
989
- "pop_total_jenis",
990
- "target_total_68_jenis",
991
- "n_jenis",
992
- "coverage_jenis_%",
993
- "faktor_penyesuaian_jenis",
994
- "gap_target68_jenis"
995
- ],
996
  aggfunc="first"
997
  )
 
998
  piv.columns = [f"{v}_{k}" for v, k in piv.columns]
999
  piv = piv.reset_index()
1000
 
1001
  out = out.merge(piv, on=["group_key", label_name], how="left")
1002
 
1003
- # rapihin NaN -> 0 + tipe
1004
  for j in ["sekolah", "umum", "khusus"]:
1005
  for basecol in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
1006
  c = f"{basecol}_{j}"
1007
  if c in out.columns:
1008
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1009
 
1010
- c_cov = f"coverage_jenis_%_{j}"
1011
- if c_cov in out.columns:
1012
- out[c_cov] = pd.to_numeric(out[c_cov], errors="coerce").fillna(0.0).round(2)
1013
-
1014
- c_fac = f"faktor_penyesuaian_jenis_{j}"
1015
- if c_fac in out.columns:
1016
- out[c_fac] = pd.to_numeric(out[c_fac], errors="coerce").fillna(0.0).round(3)
1017
-
1018
- # === TOTAL UPDATE (sesuai data POP yang ada) ===
1019
- # Total Pop dan Target = sum 3 jenis (sekolah+umum+khusus)
1020
- def _sum3(a, b, c):
1021
- return (
1022
- pd.to_numeric(a, errors="coerce").fillna(0)
1023
- + pd.to_numeric(b, errors="coerce").fillna(0)
1024
- + pd.to_numeric(c, errors="coerce").fillna(0)
1025
- )
1026
-
1027
- if ("pop_total_jenis_sekolah" in out.columns) and ("pop_total_jenis_umum" in out.columns) and ("pop_total_jenis_khusus" in out.columns):
1028
- out["Pop_Total_Update"] = _sum3(out["pop_total_jenis_sekolah"], out["pop_total_jenis_umum"], out["pop_total_jenis_khusus"]).round(0).astype(int)
1029
-
1030
- if ("target_total_68_jenis_sekolah" in out.columns) and ("target_total_68_jenis_umum" in out.columns) and ("target_total_68_jenis_khusus" in out.columns):
1031
- out["Target68_Total_Update"] = _sum3(out["target_total_68_jenis_sekolah"], out["target_total_68_jenis_umum"], out["target_total_68_jenis_khusus"]).round(0).astype(int)
1032
-
1033
- if ("n_jenis_sekolah" in out.columns) and ("n_jenis_umum" in out.columns) and ("n_jenis_khusus" in out.columns):
1034
- out["Terkumpul_Total_Update"] = _sum3(out["n_jenis_sekolah"], out["n_jenis_umum"], out["n_jenis_khusus"]).round(0).astype(int)
1035
-
1036
- # coverage_total_update = min(terkumpul/target,1)*100
1037
- if ("Terkumpul_Total_Update" in out.columns) and ("Target68_Total_Update" in out.columns):
1038
- den = pd.to_numeric(out["Target68_Total_Update"], errors="coerce").fillna(0).astype(float)
1039
- num = pd.to_numeric(out["Terkumpul_Total_Update"], errors="coerce").fillna(0).astype(float)
1040
- cov = np.where(den > 0, np.minimum(num / den, 1.0) * 100.0, 0.0)
1041
- out["Coverage_Target68_Total_Update_%"] = pd.Series(cov).round(2)
1042
 
1043
  # rounding index
1044
  for c in [
@@ -1052,17 +1020,19 @@ def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_j
1052
  if c in out.columns:
1053
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1054
 
1055
- # n_total integer
1056
- if "n_total" in out.columns:
1057
- out["n_total"] = pd.to_numeric(out["n_total"], errors="coerce").fillna(0).round(0).astype(int)
1058
 
1059
  return out
1060
 
 
1061
  # ============================================================
1062
- # 9) SUMMARY (PER JENIS) + KESELURUHAN (FIX Γ·3 + tambah cakupan & penyesuaian)
 
 
 
1063
  # ============================================================
1064
 
1065
- def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None):
1066
  jenis_list = ["sekolah", "umum", "khusus"]
1067
 
1068
  def _row_default(jenis):
@@ -1070,92 +1040,70 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame, fa
1070
  "Jenis": jenis,
1071
  "Jumlah_Wilayah": 0,
1072
  "Total_Perpus": 0,
1073
-
1074
- # POP/TARGET/N per jenis (sum nasional pada scope filter)
1075
  "Pop_Total_Jenis": 0,
1076
  "Target68_Total_Jenis": 0,
1077
  "Terkumpul_Jenis": 0,
1078
- "Coverage_Target68_Jenis_%": 0.00,
1079
-
1080
- # skor
1081
  "Rata2_sub_koleksi": 0.0,
1082
  "Rata2_sub_sdm": 0.0,
1083
  "Rata2_sub_pelayanan": 0.0,
1084
  "Rata2_sub_pengelolaan": 0.0,
1085
  "Rata2_dim_kepatuhan": 0.0,
1086
  "Rata2_dim_kinerja": 0.0,
1087
-
1088
- # dasar & final + penyesuaian poin
1089
  "Indeks_Dasar_0_100": 0.0,
1090
  "Indeks_Final_Disesuaikan_0_100": 0.0,
1091
- "Penyesuaian_Poin": 0.0,
1092
  }
1093
 
1094
  rows_by_jenis = {j: _row_default(j) for j in jenis_list}
1095
 
1096
- # ===== ambil POP/TARGET/N per jenis dari faktor_wilayah_jenis =====
1097
- fw_sum = {}
1098
- if faktor_wilayah_jenis is not None and (not faktor_wilayah_jenis.empty):
1099
- fw = faktor_wilayah_jenis.copy()
 
 
1100
  fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
1101
 
1102
- for j in jenis_list:
1103
- sub = fw[fw["Jenis"] == j].copy()
1104
- if sub.empty:
1105
- fw_sum[j] = {"pop": 0, "target": 0, "n": 0, "cov": 0.0}
1106
- continue
1107
-
1108
- pop = int(pd.to_numeric(sub.get("pop_total_jenis", 0), errors="coerce").fillna(0).sum())
1109
- target = int(pd.to_numeric(sub.get("target_total_68_jenis", 0), errors="coerce").fillna(0).sum())
1110
- n = int(pd.to_numeric(sub.get("n_jenis", 0), errors="coerce").fillna(0).sum())
1111
-
1112
- cov = 0.0
1113
- if target > 0:
1114
- cov = float(min(n / float(target), 1.0) * 100.0)
1115
-
1116
- fw_sum[j] = {"pop": pop, "target": target, "n": n, "cov": cov}
1117
-
1118
- # ===== isi ringkasan dari agg_jenis =====
1119
- if agg_jenis is not None and not agg_jenis.empty:
1120
- for jenis in jenis_list:
1121
- sub = agg_jenis[agg_jenis["Jenis"].astype(str).str.lower() == jenis].copy()
1122
- if sub.empty:
1123
- # tetap isi pop/target/n kalau ada
1124
- if jenis in fw_sum:
1125
- rows_by_jenis[jenis]["Pop_Total_Jenis"] = fw_sum[jenis]["pop"]
1126
- rows_by_jenis[jenis]["Target68_Total_Jenis"] = fw_sum[jenis]["target"]
1127
- rows_by_jenis[jenis]["Terkumpul_Jenis"] = fw_sum[jenis]["n"]
1128
- rows_by_jenis[jenis]["Coverage_Target68_Jenis_%"] = fw_sum[jenis]["cov"]
1129
- continue
1130
 
 
1131
  dasar = float(pd.to_numeric(sub["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0).mean())
1132
  final = float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean())
1133
 
1134
- rows_by_jenis[jenis] = {
1135
- "Jenis": jenis,
1136
  "Jumlah_Wilayah": int(sub.shape[0]),
1137
  "Total_Perpus": int(pd.to_numeric(sub["Jumlah"], errors="coerce").fillna(0).sum()),
1138
-
1139
- "Pop_Total_Jenis": int(fw_sum.get(jenis, {}).get("pop", 0)),
1140
- "Target68_Total_Jenis": int(fw_sum.get(jenis, {}).get("target", 0)),
1141
- "Terkumpul_Jenis": int(fw_sum.get(jenis, {}).get("n", 0)),
1142
- "Coverage_Target68_Jenis_%": float(fw_sum.get(jenis, {}).get("cov", 0.0)),
1143
-
1144
  "Rata2_sub_koleksi": float(pd.to_numeric(sub["Rata2_sub_koleksi"], errors="coerce").fillna(0).mean()),
1145
  "Rata2_sub_sdm": float(pd.to_numeric(sub["Rata2_sub_sdm"], errors="coerce").fillna(0).mean()),
1146
  "Rata2_sub_pelayanan": float(pd.to_numeric(sub["Rata2_sub_pelayanan"], errors="coerce").fillna(0).mean()),
1147
  "Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
1148
  "Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
1149
  "Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
1150
-
1151
  "Indeks_Dasar_0_100": dasar,
1152
  "Indeks_Final_Disesuaikan_0_100": final,
1153
- "Penyesuaian_Poin": float(final - dasar),
1154
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1155
 
1156
  rows = [rows_by_jenis[j] for j in jenis_list]
1157
 
1158
- # ===== keseluruhan = avg3 tetap Γ·3 (missing=0) =====
1159
  def _avg3(field):
1160
  return (
1161
  float(rows_by_jenis["sekolah"][field])
@@ -1163,43 +1111,58 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame, fa
1163
  + float(rows_by_jenis["khusus"][field])
1164
  ) / 3.0
1165
 
1166
- final_all = _avg3("Indeks_Final_Disesuaikan_0_100")
1167
- dasar_all = _avg3("Indeks_Dasar_0_100")
1168
-
1169
- total_perpus_all = int(rows_by_jenis["sekolah"]["Total_Perpus"] + rows_by_jenis["umum"]["Total_Perpus"] + rows_by_jenis["khusus"]["Total_Perpus"])
1170
- jumlah_wilayah_all = int(agg_total.shape[0]) if (agg_total is not None and not agg_total.empty) else int(max(rows_by_jenis["sekolah"]["Jumlah_Wilayah"], rows_by_jenis["umum"]["Jumlah_Wilayah"], rows_by_jenis["khusus"]["Jumlah_Wilayah"]))
1171
-
1172
- # Total POP/TARGET/N keseluruhan = sum3 (bukan avg3), karena ini memang total cakupan
1173
- pop_all = int(rows_by_jenis["sekolah"]["Pop_Total_Jenis"] + rows_by_jenis["umum"]["Pop_Total_Jenis"] + rows_by_jenis["khusus"]["Pop_Total_Jenis"])
1174
- target_all = int(rows_by_jenis["sekolah"]["Target68_Total_Jenis"] + rows_by_jenis["umum"]["Target68_Total_Jenis"] + rows_by_jenis["khusus"]["Target68_Total_Jenis"])
1175
- n_all = int(rows_by_jenis["sekolah"]["Terkumpul_Jenis"] + rows_by_jenis["umum"]["Terkumpul_Jenis"] + rows_by_jenis["khusus"]["Terkumpul_Jenis"])
1176
- cov_all = float(min(n_all / float(target_all), 1.0) * 100.0) if target_all > 0 else 0.0
1177
-
1178
- rows.append({
1179
- "Jenis": "keseluruhan",
1180
- "Jumlah_Wilayah": jumlah_wilayah_all,
1181
- "Total_Perpus": total_perpus_all,
1182
-
1183
- "Pop_Total_Jenis": pop_all,
1184
- "Target68_Total_Jenis": target_all,
1185
- "Terkumpul_Jenis": n_all,
1186
- "Coverage_Target68_Jenis_%": cov_all,
1187
-
1188
- "Rata2_sub_koleksi": _avg3("Rata2_sub_koleksi"),
1189
- "Rata2_sub_sdm": _avg3("Rata2_sub_sdm"),
1190
- "Rata2_sub_pelayanan": _avg3("Rata2_sub_pelayanan"),
1191
- "Rata2_sub_pengelolaan": _avg3("Rata2_sub_pengelolaan"),
1192
- "Rata2_dim_kepatuhan": _avg3("Rata2_dim_kepatuhan"),
1193
- "Rata2_dim_kinerja": _avg3("Rata2_dim_kinerja"),
1194
-
1195
- "Indeks_Dasar_0_100": dasar_all,
1196
- "Indeks_Final_Disesuaikan_0_100": final_all,
1197
- "Penyesuaian_Poin": float(final_all - dasar_all),
1198
- })
 
 
 
 
 
 
 
 
 
 
1199
 
1200
  out = pd.DataFrame(rows)
1201
 
1202
- # rounding
 
 
 
 
 
1203
  for c in [
1204
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1205
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
@@ -1209,12 +1172,6 @@ def build_summary_per_jenis(agg_jenis: pd.DataFrame, agg_total: pd.DataFrame, fa
1209
  for c in ["Indeks_Dasar_0_100","Indeks_Final_Disesuaikan_0_100","Penyesuaian_Poin"]:
1210
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1211
 
1212
- # integer columns
1213
- for c in ["Jumlah_Wilayah","Total_Perpus","Pop_Total_Jenis","Target68_Total_Jenis","Terkumpul_Jenis"]:
1214
- out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1215
-
1216
- out["Coverage_Target68_Jenis_%"] = pd.to_numeric(out["Coverage_Target68_Jenis_%"], errors="coerce").fillna(0.0).round(2)
1217
-
1218
  return out
1219
 
1220
  # ============================================================
@@ -1420,51 +1377,56 @@ def _make_bell_curve(dfp: pd.DataFrame, xcol: str, title: str, label_col: str |
1420
 
1421
 
1422
  # ============================================================
1423
- # 13) KPI DASHBOARD (PATCH: hanya FINAL & DASAR)
 
 
1424
  # ============================================================
1425
 
1426
- def compute_dashboard_kpis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame):
1427
- def _get_val(j, col):
1428
- sub = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == j]
1429
- if sub.empty:
1430
- return 0.0
1431
- return float(pd.to_numeric(sub[col], errors="coerce").fillna(0).iloc[0])
1432
 
1433
- final_all = _get_val("keseluruhan", "Indeks_Final_Disesuaikan_0_100")
1434
- dasar_all = _get_val("keseluruhan", "Indeks_Dasar_0_100")
1435
 
1436
- return {"final_all": final_all, "dasar_all": dasar_all}
 
 
 
 
 
 
1437
 
 
1438
 
1439
- def build_kpi_markdown(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame | None = None) -> str:
1440
- # faktor_wilayah_jenis tetap diterima supaya pemanggil run_calc tidak perlu diubah banyak,
1441
- # tapi tidak dipakai lagi di KPI dashboard (dipindah ke tabel ringkasan).
1442
  if summary_jenis is None or summary_jenis.empty:
1443
  return ""
1444
 
1445
- k = compute_dashboard_kpis(summary_jenis, agg_total, agg_jenis)
1446
 
1447
  def fmt(x, nd=2):
1448
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
1449
 
1450
  return f"""
1451
  <div style="display:flex; gap:12px; flex-wrap:wrap;">
1452
- <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:220px;">
1453
  <div style="opacity:0.8;">Indeks IPLM FINAL (Disesuaikan)</div>
1454
  <div style="font-size:26px; font-weight:700;">{fmt(k["final_all"],2)}</div>
1455
- <div style="opacity:0.7;">Rata-rata 3 jenis (tetap Γ·3) β€” sumber: Ringkasan</div>
1456
  </div>
1457
 
1458
- <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:220px;">
1459
  <div style="opacity:0.8;">Indeks Dasar (Tanpa Penyesuaian)</div>
1460
  <div style="font-size:26px; font-weight:700;">{fmt(k["dasar_all"],2)}</div>
1461
- <div style="opacity:0.7;">Rata-rata 3 jenis (tetap Γ·3)</div>
1462
  </div>
1463
  </div>
1464
  """.strip()
1465
 
 
1466
  # ============================================================
1467
- # 14) LLM + WORD
1468
  # ============================================================
1469
 
1470
  _HF_CLIENT = None
@@ -1480,81 +1442,69 @@ def get_llm_client():
1480
  _HF_CLIENT = None
1481
  return None
1482
 
1483
-
1484
- def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, verif_total: pd.DataFrame, wilayah: str, kew: str) -> str:
1485
  lines = []
1486
  lines.append(f"Wilayah filter: {wilayah}")
1487
  lines.append(f"Kewenangan: {kew}")
1488
- lines.append("Metode: Indeks dasar dihitung per entitas (Yeo-Johnson + MinMax nasional per indikator), lalu diagregasi per wilayahΓ—jenis.")
1489
- lines.append("Penyesuaian: faktor = min(total_terkumpul / target_total_68, 1.0).")
1490
- lines.append("FIX keseluruhan: nilai keseluruhan = rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3).")
1491
 
1492
  if summary_jenis is not None and not summary_jenis.empty:
1493
  lines.append("\nRingkasan (jenis + keseluruhan):")
1494
  for _, r in summary_jenis.iterrows():
1495
- try:
1496
- jenis = str(r.get("Jenis", "")).strip()
1497
- jw = int(pd.to_numeric(r.get("Jumlah_Wilayah", 0), errors="coerce") or 0)
1498
- tp = int(pd.to_numeric(r.get("Total_Perpus", 0), errors="coerce") or 0)
1499
- fin = float(pd.to_numeric(r.get("Indeks_Final_Disesuaikan_0_100", 0), errors="coerce") or 0)
1500
- das = float(pd.to_numeric(r.get("Indeks_Dasar_0_100", 0), errors="coerce") or 0)
1501
- cov = float(pd.to_numeric(r.get("Coverage_Target68_Jenis_%", 0), errors="coerce") or 0)
1502
- lines.append(f"- {jenis}: wilayah={jw}, total_perpus={tp}, dasar={das:.2f}, final={fin:.2f}, coverage_target68={cov:.2f}%")
1503
- except Exception:
1504
- continue
1505
 
1506
  if agg_total is not None and not agg_total.empty:
1507
  label_col = "Kab/Kota" if "Kab/Kota" in agg_total.columns else ("Provinsi" if "Provinsi" in agg_total.columns else None)
1508
- if label_col:
1509
- lines.append("\nTop 5 wilayah (Final tertinggi):")
1510
- top = agg_total.sort_values("Indeks_Final_Wilayah_0_100", ascending=False).head(5)
1511
- for _, r in top.iterrows():
1512
- wl = str(r.get(label_col, "(wilayah)"))
1513
- fin = float(pd.to_numeric(r.get("Indeks_Final_Wilayah_0_100", 0), errors="coerce") or 0)
1514
- lines.append(f"- {wl}: Final={fin:.2f}")
1515
 
1516
  return "\n".join(lines)
1517
 
 
 
1518
 
1519
- def generate_llm_analysis(summary_jenis, agg_total, verif_total, wilayah, kew):
1520
- ctx = build_context(summary_jenis, agg_total, verif_total, wilayah, kew)
1521
 
1522
- # kalau LLM dimatikan / token gak ada -> return teks aman
1523
  client = get_llm_client()
1524
- if (client is None) or (not USE_LLM):
1525
- return (
1526
- "Analisis otomatis (LLM) tidak tersedia.\n\n"
1527
- "Catatan: Set USE_LLM=True dan pastikan HF_TOKEN tersedia bila ingin mengaktifkan analisis LLM."
1528
- )
1529
 
1530
  system_prompt = (
1531
  "Anda adalah analis kebijakan perpustakaan dan literasi di Indonesia. "
1532
  "Tugas Anda menyusun analisis berbasis data IPLM secara formal, tajam, dan operasional."
1533
  )
1534
-
1535
  user_prompt = f"""
1536
  DATA RINGKAS IPLM:
1537
 
1538
  {ctx}
1539
 
1540
  TULISKAN ANALISIS BAHASA INDONESIA FORMAL, STRUKTUR:
1541
- 1) Gambaran umum hasil wilayah (1 paragraf).
1542
- 2) Analisis jenis sekolah, umum, khusus serta indeks keseluruhan (2 paragraf).
1543
- 3) Penjelasan makna penyesuaian berbasis target 68% (1 paragraf, netral).
1544
- 4) Rekomendasi program 3–5 tahun (2 paragraf, konkret dan dapat dieksekusi).
1545
 
1546
  ATURAN:
1547
  - Jangan memakai label eksplisit "rendah/sedang/tinggi".
1548
  - Gunakan frasa netral: "memerlukan penguatan", "memerlukan konsolidasi", dsb.
 
1549
  """
1550
 
1551
  try:
1552
  resp = client.chat_completion(
1553
  model=LLM_MODEL_NAME,
1554
- messages=[
1555
- {"role": "system", "content": system_prompt},
1556
- {"role": "user", "content": user_prompt},
1557
- ],
1558
  max_tokens=1100,
1559
  temperature=0.25,
1560
  top_p=0.9,
@@ -1564,36 +1514,21 @@ ATURAN:
1564
  except Exception as e:
1565
  return f"⚠️ Error saat memanggil LLM: {repr(e)}"
1566
 
1567
-
1568
- def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_text):
1569
  doc = Document()
1570
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1571
 
1572
  doc.add_heading("Ringkasan Dashboard", level=2)
 
 
 
1573
 
1574
- # KPI hanya FINAL & DASAR (cakupan + penyesuaian dipindah ke tabel ringkasan)
1575
- k_final = 0.0
1576
- k_dasar = 0.0
1577
- try:
1578
- if summary_jenis is not None and (not summary_jenis.empty):
1579
- row_all = summary_jenis[summary_jenis["Jenis"].astype(str).str.lower() == "keseluruhan"]
1580
- if not row_all.empty:
1581
- k_final = float(pd.to_numeric(row_all["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
1582
- k_dasar = float(pd.to_numeric(row_all["Indeks_Dasar_0_100"], errors="coerce").fillna(0).iloc[0])
1583
- except Exception:
1584
- k_final, k_dasar = 0.0, 0.0
1585
-
1586
- doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k_final:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1587
- doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k_dasar:.2f} (rata-rata 3 jenis, tetap Γ·3; missing=0)")
1588
-
1589
- doc.add_paragraph("Ringkasan (Jenis + Keseluruhan) β€” termasuk Pop/Target68/Terkumpul/Coverage + Penyesuaian Poin:")
1590
  show = summary_jenis.copy()
1591
-
1592
  preferred = [
1593
- "Jenis", "Jumlah_Wilayah", "Total_Perpus",
1594
- "Pop_Total_Jenis", "Target68_Total_Jenis", "Terkumpul_Jenis", "Coverage_Target68_Jenis_%",
1595
- "Rata2_dim_kepatuhan", "Rata2_dim_kinerja",
1596
- "Indeks_Dasar_0_100", "Indeks_Final_Disesuaikan_0_100", "Penyesuaian_Poin"
1597
  ]
1598
  show = show[[c for c in preferred if c in show.columns]]
1599
 
@@ -1608,32 +1543,36 @@ def generate_word_report(wilayah, summary_jenis, agg_total, agg_jenis, analysis_
1608
  v = row[c]
1609
  if pd.isna(v):
1610
  cells[i].text = ""
1611
- elif isinstance(v, (int, np.integer)):
1612
- cells[i].text = str(int(v))
1613
  elif isinstance(v, (float, np.floating)):
1614
- if "Coverage" in c:
1615
- cells[i].text = f"{float(v):.2f}"
1616
- elif "Rata2_" in c:
1617
- cells[i].text = f"{float(v):.3f}"
1618
- elif "Indeks" in c or "Penyesuaian" in c:
1619
  cells[i].text = f"{float(v):.2f}"
1620
  else:
1621
  cells[i].text = f"{float(v):.2f}"
 
 
1622
  else:
1623
  cells[i].text = str(v)
1624
 
1625
- doc.add_heading("Metodologi", level=2)
1626
- doc.add_paragraph(
1627
- "Indeks dasar dihitung per entitas menggunakan transformasi Yeo-Johnson dan normalisasi MinMax nasional per indikator. "
1628
- "Nilai kemudian diagregasi per wilayahΓ—jenis."
1629
- )
1630
- doc.add_paragraph(
1631
- "Penyesuaian dilakukan berbasis kecukupan sampel minimum 68% pada level wilayah, "
1632
- "dengan rumus faktor = min(total_terkumpul / target_total_68, 1.0)."
1633
- )
1634
- doc.add_paragraph(
1635
- "Nilai keseluruhan (FIX) dihitung sebagai rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3, dengan missing dianggap 0."
1636
- )
 
 
 
 
 
 
 
 
1637
 
1638
  doc.add_heading("Analisis Naratif (LLM)", level=2)
1639
  for p in (analysis_text or "").split("\n"):
@@ -1654,9 +1593,9 @@ def _empty_outputs(msg="⚠️ Data belum siap."):
1654
  empty_fig = go.Figure()
1655
  return (
1656
  "", # kpi_md
1657
- empty, empty, empty, empty, empty,
1658
- None, None, None, None, None,
1659
- empty_fig, empty_fig, empty_fig,
1660
  msg, "Analisis belum tersedia."
1661
  )
1662
 
@@ -1665,9 +1604,7 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1665
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1666
  return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1667
 
1668
- # =========================
1669
  # FILTER ANALISIS (df_all)
1670
- # =========================
1671
  df = df_all.copy()
1672
  if prov_value and prov_value != "(Semua)":
1673
  df = df[df["PROV_DISP"] == prov_value]
@@ -1679,71 +1616,44 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1679
  if df.empty:
1680
  return _empty_outputs("Tidak ada data untuk filter ini.")
1681
 
1682
- # ==================================================
1683
- # PIPELINE BARU (FAKTOR 68% PER JENIS)
1684
- # ==================================================
1685
  faktor_wilayah_jenis = build_faktor_wilayah_jenis(
1686
- df,
1687
- pop_kab,
1688
- pop_prov,
1689
- pop_khusus,
1690
- kew_value or "(Semua)"
1691
  )
1692
 
1693
- agg_jenis_full = build_agg_wilayah_jenis(
1694
- df,
1695
- faktor_wilayah_jenis,
1696
- kew_value or "(Semua)"
1697
- )
1698
-
1699
- agg_total = build_agg_wilayah_total_from_jenis(
1700
- agg_jenis_full,
1701
- faktor_wilayah_jenis,
1702
- kew_value or "(Semua)"
1703
- )
1704
 
1705
- summary_jenis = build_summary_per_jenis(
1706
- agg_jenis_full,
1707
- agg_total,
1708
- faktor_wilayah_jenis=faktor_wilayah_jenis
1709
- )
1710
-
1711
- verif_total = build_verif_jenis(
1712
- faktor_wilayah_jenis,
1713
- kew_value or "(Semua)"
1714
- )
1715
 
1716
- detail_view = attach_final_to_detail(
1717
- df,
1718
- agg_total,
1719
- meta,
1720
- kew_value or "(Semua)"
1721
- )
1722
-
1723
- # ==================================================
1724
- # UPDATE SESUAI PERMINTAAN (UI ONLY)
1725
- # Tabel Agregat Wilayah Γ— Jenis cukup sampai Indeks_Dasar_Agregat_0_100
1726
- # ==================================================
1727
  if agg_jenis_full is None or agg_jenis_full.empty:
1728
  agg_jenis_view = agg_jenis_full
1729
  else:
1730
  kew_norm = str(kew_value or "").upper()
1731
- label_name = "Kab/Kota" if ("KAB" in kew_norm or "KOTA" in kew_norm) else ("Provinsi" if "PROV" in kew_norm else "Kab/Kota")
 
 
 
1732
  cols_upto = [
1733
  "group_key",
1734
  label_name,
1735
  "Jenis",
1736
  "Jumlah",
1737
- "Rata2_sub_koleksi", "Rata2_sub_sdm", "Rata2_sub_pelayanan", "Rata2_sub_pengelolaan",
1738
- "Rata2_dim_kepatuhan", "Rata2_dim_kinerja",
1739
  "Indeks_Dasar_Agregat_0_100",
1740
  ]
1741
  cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1742
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1743
 
1744
- # =========================
1745
  # FILTER RAW DOWNLOAD (df_raw)
1746
- # =========================
1747
  raw = df_raw.copy()
1748
  if prov_value and prov_value != "(Semua)":
1749
  raw = raw[raw["PROV_DISP"] == prov_value]
@@ -1752,13 +1662,11 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1752
  if kew_value and kew_value != "(Semua)":
1753
  raw = raw[raw["KEW_NORM"] == kew_value]
1754
 
1755
- # =========================
1756
- # Bell curve per JENIS (per entitas)
1757
- # =========================
1758
  if detail_view is None or detail_view.empty:
1759
  fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1760
- fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
1761
- fig_khusus = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Khusus", min_points=2)
1762
  else:
1763
  xcol_ent = "Indeks_Dasar_0_100" if "Indeks_Dasar_0_100" in detail_view.columns else "Indeks_Final_0_100"
1764
  label_col_e = "nm_perpustakaan" if "nm_perpustakaan" in detail_view.columns else None
@@ -1766,76 +1674,46 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1766
 
1767
  def _fig_jenis_ent(jenis_key: str, judul: str):
1768
  d = detail_view[detail_view["Jenis"].astype(str).str.lower() == jenis_key].copy()
1769
- return _make_bell_curve(
1770
- d,
1771
- xcol=xcol_ent,
1772
- title=judul,
1773
- label_col=label_col_e,
1774
- hover_cols=hover_cols_e,
1775
- min_points=2
1776
- )
1777
 
1778
  fig_sekolah = _fig_jenis_ent("sekolah", "Bell Curve β€” Jenis: Sekolah (Indeks per Entitas)")
1779
- fig_umum = _fig_jenis_ent("umum", "Bell Curve β€” Jenis: Umum (Indeks per Entitas)")
1780
- fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β€” Jenis: Khusus (Indeks per Entitas)")
1781
-
1782
- # =========================
1783
- # KPI markdown (FINAL sumber Ringkasan) β€” hanya 2 kartu
1784
- # =========================
1785
- kpi_md = build_kpi_markdown(
1786
- summary_jenis,
1787
- agg_total,
1788
- agg_jenis_full,
1789
- faktor_wilayah_jenis=faktor_wilayah_jenis
1790
- )
1791
 
1792
- # =========================
1793
- # SAVE OUTPUTS (Excel + Word)
1794
- # =========================
1795
  tmpdir = tempfile.mkdtemp()
1796
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
1797
- kab_slug = (_canon(kab_value or "SEMUA").upper() or "SEMUA")
1798
- kew_slug = (_canon(kew_value or "SEMUA").upper() or "SEMUA")
1799
 
1800
  p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1801
- p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1802
- p_jenis = str(Path(tmpdir) / f"IPLM_RAW_DATA_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1803
- p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1804
- p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1805
 
1806
  summary_jenis.to_excel(p_summary, index=False)
1807
  agg_total.to_excel(p_total, index=False)
1808
- raw.to_excel(p_jenis, index=False)
1809
  detail_view.to_excel(p_detail, index=False)
1810
  verif_total.to_excel(p_verif, index=False)
1811
 
1812
  wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
1813
-
1814
- analysis_text = generate_llm_analysis(
1815
- summary_jenis,
1816
- agg_total,
1817
- verif_total,
1818
- wilayah_txt,
1819
- kew_value or "(Semua)"
1820
- )
1821
-
1822
- word_path = generate_word_report(
1823
- wilayah_txt,
1824
- summary_jenis,
1825
- agg_total,
1826
- agg_jenis_full,
1827
- analysis_text
1828
- )
1829
 
1830
  msg = (
1831
- f"βœ… Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan)={len(agg_total)} | "
1832
- f"jenis(agregat)={len(agg_jenis_full)} | FIX: Agregat Wilayah (Keseluruhan) = avg3 dari 3 jenis (Γ·3)"
1833
  )
1834
 
1835
  return (
1836
  kpi_md,
1837
  summary_jenis, agg_total, agg_jenis_view, detail_view, verif_total,
1838
- p_summary, p_total, p_jenis, p_detail, word_path,
1839
  fig_umum, fig_sekolah, fig_khusus,
1840
  msg, analysis_text
1841
  )
@@ -1843,13 +1721,13 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
1843
  except Exception as e:
1844
  return _empty_outputs(f"⚠️ Runtime error: {repr(e)}")
1845
 
1846
-
1847
  # ============================================================
1848
  # 16) UI (NO UPLOAD)
1849
  # ============================================================
1850
 
1851
  def ui_load(force=False):
1852
  df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=force)
 
1853
  if df_all is None or (isinstance(df_all, pd.DataFrame) and df_all.empty):
1854
  return (
1855
  None, None, None, None, None, {}, info,
@@ -1877,17 +1755,18 @@ def on_prov_change(prov_value):
1877
  df_all, _, _, _, _, _, _ = load_default_files(force=False)
1878
  if df_all is None or df_all.empty:
1879
  return gr.update(choices=["(Semua)"], value="(Semua)")
 
1880
  if prov_value is None or prov_value == "(Semua)":
1881
  vals = df_all["KAB_DISP"].dropna().unique().tolist()
1882
  else:
1883
  vals = df_all.loc[df_all["PROV_DISP"] == prov_value, "KAB_DISP"].dropna().unique().tolist()
 
1884
  vals = sorted([v for v in vals if v])
1885
  return gr.update(choices=["(Semua)"] + vals, value="(Semua)")
1886
 
1887
-
1888
  with gr.Blocks() as demo:
1889
  gr.Markdown(f"""
1890
- # IPLM 2025 β€” Final (Penyesuaian Berbasis Kecukupan Sampel 68%)
1891
  **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1892
  - `DATA_FILE` = **{DATA_FILE}**
1893
  - `POP_KAB` = **{POP_KAB}**
@@ -1895,15 +1774,14 @@ with gr.Blocks() as demo:
1895
  - `POP_KHUSUS` = **{POP_KHUSUS}**
1896
 
1897
  **FIX UTAMA (konsistensi nilai):**
1898
- - **Agregat Wilayah (Keseluruhan) = rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3)**
1899
  - Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
1900
- - KPI dashboard hanya menampilkan **FINAL** dan **DASAR**
1901
- - **Cakupan (Target 68%) dan Penyesuaian Poin dipindahkan ke tabel Ringkasan**
1902
- - Download Data Mentah = RAW hasil filter
1903
 
1904
  **UPDATE (tampilan):**
1905
- - Tabel Ringkasan memuat: Pop/Target68/Terkumpul/Coverage per jenis + Penyesuaian Poin
1906
- - Tabel Agregat Wilayah memuat: Pop/Target68/Terkumpul per jenis + Total Update (sum 3 jenis)
1907
  - Tabel "Agregat Wilayah Γ— Jenis" ditampilkan hanya sampai Indeks_Dasar_Agregat_0_100
1908
  """)
1909
 
@@ -1926,13 +1804,12 @@ with gr.Blocks() as demo:
1926
  run_btn = gr.Button("Jalankan Perhitungan")
1927
  msg_out = gr.Markdown()
1928
 
1929
- # KPI dashboard (hanya 2 kartu)
1930
  kpi_out = gr.Markdown()
1931
 
1932
  gr.Markdown("## Ringkasan (Jenis + Keseluruhan) β€” Pop/Target68/Terkumpul/Coverage + Penyesuaian")
1933
  out_summary = gr.DataFrame(interactive=False)
1934
 
1935
- gr.Markdown("## Agregat Wilayah (Keseluruhan) β€” FIX: avg3 dari 3 jenis + Pop/Target68/Terkumpul (per jenis & total)")
1936
  out_agg_total = gr.DataFrame(interactive=False)
1937
 
1938
  gr.Markdown("## Agregat Wilayah Γ— Jenis (Sekolah, Umum, Khusus) β€” (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
@@ -1941,7 +1818,7 @@ with gr.Blocks() as demo:
1941
  gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
1942
  out_detail = gr.DataFrame(interactive=False)
1943
 
1944
- gr.Markdown("## Kecukupan Sampel 68% (tanpa angka koma)")
1945
  out_verif = gr.DataFrame(interactive=False)
1946
 
1947
  gr.Markdown("## Bell Curve β€” per Jenis Perpustakaan (Indeks per Entitas)")
@@ -1960,7 +1837,7 @@ with gr.Blocks() as demo:
1960
  with gr.Row():
1961
  dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1962
  dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1963
- dl_jenis = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1964
  dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1965
  dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
1966
 
@@ -1970,7 +1847,7 @@ with gr.Blocks() as demo:
1970
  outputs=[
1971
  kpi_out,
1972
  out_summary, out_agg_total, out_agg_jenis, out_detail, out_verif,
1973
- dl_summary, dl_total, dl_jenis, dl_detail, dl_word,
1974
  bell_umum, bell_sekolah, bell_khusus,
1975
  msg_out, analysis_out
1976
  ]
@@ -1983,3 +1860,4 @@ with gr.Blocks() as demo:
1983
  )
1984
 
1985
  demo.launch()
 
 
355
  if c_mix is None:
356
  raise ValueError("POP_KHUSUS: kolom gabungan Provinsi/Kab/Kota tidak ditemukan.")
357
 
 
 
 
 
 
358
  c_target = pick_col(df, [
359
  "SAMPEL_KHUSUS_68%", "Sampel_Khusus_68%", "sampel_khusus_68%",
360
  "SAMPEL_KHUSUS_68", "Sampel_Khusus_68", "sampel_khusus_68",
 
362
  "sampel_total","Sampel_total","TOTAL_SAMPEL","total_sampel",
363
  "target","Target","Sampel"
364
  ])
 
365
  c_pop = pick_col(df, [
366
  "POP_KHUSUS", "Pop_Khusus", "pop_khusus",
367
  "total_populasi","Total Populasi","POPULASI","populasi",
 
377
  c_target = numeric_cols[0]
378
 
379
  mix = df[c_mix].astype(str).fillna("").str.strip()
380
+ target_series = df[c_target].apply(coerce_num) if c_target else pd.Series([np.nan] * len(df))
381
+ pop_series = df[c_pop].apply(coerce_num) if c_pop else pd.Series([np.nan] * len(df))
382
 
383
  rows = []
384
  current_prov = None
 
414
  m_need_target = pop["Target68_Total_Jenis"].isna() & pop["Pop_Total_Jenis"].notna() & (pop["Pop_Total_Jenis"] > 0)
415
  pop.loc[m_need_target, "Target68_Total_Jenis"] = pop.loc[m_need_target, "Pop_Total_Jenis"] * float(FALLBACK_TARGET_RATIO)
416
 
417
+ # keep per kab_key
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  pop = pop.groupby("kab_key", as_index=False).agg({
419
  "Kab_Kota_Label": "first",
420
  "Provinsi_Label": "first",
 
424
  })
425
  return pop
426
 
427
+ def load_default_files(force: bool = False):
428
  key = (
429
  DATA_FILE, POP_KAB, POP_PROV, POP_KHUSUS,
430
  _mtime(DATA_FILE), _mtime(POP_KAB), _mtime(POP_PROV), _mtime(POP_KHUSUS)
431
  )
432
 
433
  if (not force) and _CACHE["key"] == key and _CACHE["df_all"] is not None:
434
+ return (
435
+ _CACHE["df_all"], _CACHE["df_raw"],
436
+ _CACHE["pop_kab"], _CACHE["pop_prov"], _CACHE["pop_khusus"],
437
+ _CACHE["meta"], _CACHE["info"]
438
+ )
439
 
440
  for p, label in [(DATA_FILE, "DM"), (POP_KAB, "POP_KAB"), (POP_PROV, "POP_PROV"), (POP_KHUSUS, "POP_KHUSUS")]:
441
  if not Path(p).exists():
442
  info = f"❌ File {label} tidak ditemukan: `{p}`"
443
+ _CACHE.update({
444
+ "key": key, "df_all": None, "df_raw": None,
445
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
446
+ "meta": {}, "info": info
447
+ })
448
  return None, None, None, None, None, {}, info
449
 
450
+ # =========================
451
+ # DM gabungan semua sheet
452
+ # =========================
453
  fp = Path(DATA_FILE)
454
  xls = pd.ExcelFile(fp)
455
  frames = [pd.read_excel(fp, sheet_name=s) for s in xls.sheet_names]
456
  df_raw = pd.concat(frames, ignore_index=True, sort=False)
457
 
458
+ prov_col = pick_col(df_raw, ["provinsi", "Provinsi", "PROVINSI"])
459
+ kab_col = pick_col(df_raw, ["kab_kota", "Kab/Kota", "Kab_Kota", "KAB/KOTA", "kabupaten_kota", "Kabupaten/Kota", "kabupaten kota", "kota"])
460
+ kew_col = pick_col(df_raw, ["kewenangan", "jenis_kewenangan", "Kewenangan", "KEWENANGAN"])
461
  jenis_col = pick_col(df_raw, ["jenis_perpustakaan", "Jenis Perpustakaan", "JENIS_PERPUSTAKAAN"])
462
+ nama_col = pick_col(df_raw, ["nm_perpustakaan","nama_perpustakaan","Nama Perpustakaan","nm_instansi_lembaga","nm_perpus"])
463
 
464
  missing = []
465
+ if prov_col is None:
466
+ missing.append("Provinsi")
467
+ if kab_col is None:
468
+ missing.append("Kab/Kota")
469
+ if kew_col is None:
470
+ missing.append("Kewenangan")
471
+ if jenis_col is None:
472
+ missing.append("Jenis Perpustakaan")
473
+
474
  if missing:
475
  info = f"❌ Kolom wajib tidak ditemukan di DM: {', '.join(missing)}"
476
+ _CACHE.update({
477
+ "key": key, "df_all": None, "df_raw": None,
478
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
479
+ "meta": {}, "info": info
480
+ })
481
  return None, None, None, None, None, {}, info
482
 
483
  val_map_jenis = {
 
486
  "PERPUSTAKAAN KHUSUS": "khusus", "KHUSUS": "khusus",
487
  }
488
 
489
+ df_raw["KEW_NORM"] = df_raw[kew_col].apply(norm_kew)
490
+ df_raw["_dataset"] = df_raw[jenis_col].astype(str).str.strip().str.upper().map(val_map_jenis)
491
  df_raw["PROV_DISP"] = df_raw[prov_col].apply(norm_prov_disp)
492
+ df_raw["KAB_DISP"] = df_raw[kab_col].apply(_disp_text)
493
+ df_raw["prov_key"] = df_raw["PROV_DISP"].apply(norm_prov_label)
494
+ df_raw["kab_key"] = df_raw["KAB_DISP"].apply(norm_kab_label)
495
 
496
  if nama_col and nama_col in df_raw.columns:
497
  kcols = [prov_col, kab_col, kew_col, jenis_col, nama_col]
 
505
  after = len(df_raw)
506
 
507
  # =========================
508
+ # POP KAB (KEEP PER-JENIS)
509
  # =========================
510
  pk = pd.read_excel(POP_KAB)
511
 
512
+ c_kab = pick_col(pk, ["KABUPATEN_KOTA","Kab/Kota","Kabupaten/Kota","KAB/KOTA","Kabupaten_Kota","kab_kota","kabupaten_kota"])
513
  c_prov = pick_col(pk, ["PROVINSI","Provinsi","provinsi"])
514
 
515
  c_target_total = pick_col(pk, [
 
517
  "target_total_68","Target_Total_68","target_68","TARGET_68"
518
  ])
519
 
520
+ # REAL kolom file user:
521
+ c_pop_umum = pick_col(pk, ["jumlah_populasi_umum","Jumlah_populasi_umum","JUMLAH_POPULASI_UMUM","POP_UMUM","pop_umum"])
522
+ c_target_umum = pick_col(pk, ["Sampel_umum_68%","Sampel_umum_68","SAMPEL_UMUM_68%","SAMPEL_UMUM_68","TARGET_UMUM_68"])
523
+
524
+ c_pop_sekolah = pick_col(pk, ["jumlah_populasi_sekolah","Jumlah_populasi_sekolah","JUMLAH_POPULASI_SEKOLAH","POP_SEKOLAH","pop_sekolah"])
525
+ c_target_sekolah = pick_col(pk, ["Sampel_sekolah_68%","Sampel_sekolah_68","SAMPEL_SEKOLAH_68%","SAMPEL_SEKOLAH_68","TARGET_SEKOLAH_68"])
 
526
 
527
  if c_kab is None or c_target_total is None:
528
  info = "❌ POP_KAB: wajib ada kolom Kab/Kota dan sampel_total (target 68%)."
529
+ _CACHE.update({
530
+ "key": key, "df_all": None, "df_raw": None,
531
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
532
+ "meta": {}, "info": info
533
+ })
534
  return None, None, None, None, None, {}, info
535
 
536
  pop_kab = pd.DataFrame({
537
  "Provinsi_Label": pk[c_prov].astype(str).str.strip() if c_prov else "",
538
  "Kab_Kota_Label": pk[c_kab].astype(str).str.strip(),
539
  "Target68_Total": pk[c_target_total].apply(coerce_num),
540
+ "Pop_Umum": pk[c_pop_umum].apply(coerce_num) if c_pop_umum else np.nan,
541
+ "Target68_Umum": pk[c_target_umum].apply(coerce_num) if c_target_umum else np.nan,
542
+ "Pop_Sekolah": pk[c_pop_sekolah].apply(coerce_num) if c_pop_sekolah else np.nan,
543
+ "Target68_Sekolah": pk[c_target_sekolah].apply(coerce_num) if c_target_sekolah else np.nan,
544
  })
545
 
546
+ # fallback target per jenis dari pop
547
+ m = pop_kab["Target68_Umum"].isna() & pop_kab["Pop_Umum"].notna() & (pop_kab["Pop_Umum"] > 0)
548
+ pop_kab.loc[m, "Target68_Umum"] = pop_kab.loc[m, "Pop_Umum"] * float(FALLBACK_TARGET_RATIO)
549
+
550
+ m = pop_kab["Target68_Sekolah"].isna() & pop_kab["Pop_Sekolah"].notna() & (pop_kab["Pop_Sekolah"] > 0)
551
+ pop_kab.loc[m, "Target68_Sekolah"] = pop_kab.loc[m, "Pop_Sekolah"] * float(FALLBACK_TARGET_RATIO)
552
 
553
+ pop_kab["Pop_Total"] = (
554
+ pd.to_numeric(pop_kab["Pop_Umum"], errors="coerce").fillna(0.0)
555
+ + pd.to_numeric(pop_kab["Pop_Sekolah"], errors="coerce").fillna(0.0)
556
+ )
557
+
558
+ m_need_pop = (pop_kab["Pop_Total"] <= 0) & pop_kab["Target68_Total"].notna() & (pop_kab["Target68_Total"] > 0)
559
+ pop_kab.loc[m_need_pop, "Pop_Total"] = pop_kab.loc[m_need_pop, "Target68_Total"] / float(FALLBACK_TARGET_RATIO)
560
 
561
  pop_kab["kab_key"] = pop_kab["Kab_Kota_Label"].apply(norm_kab_label)
562
+
563
  pop_kab = pop_kab.groupby("kab_key", as_index=False).agg({
564
+ "Kab_Kota_Label": "first",
565
+ "Provinsi_Label": "first",
566
+ "Target68_Total": "max",
567
+ "Pop_Total": "max",
568
+ "Pop_Umum": "max",
569
+ "Target68_Umum": "max",
570
+ "Pop_Sekolah": "max",
571
+ "Target68_Sekolah": "max",
572
  })
573
 
574
  # =========================
575
+ # POP PROV (KEEP PER-JENIS)
576
  # =========================
577
  pp = pd.read_excel(POP_PROV)
578
 
579
  c_pr = pick_col(pp, ["Provinsi","PROVINSI","provinsi","Propinsi","PROPINSI","propinsi"])
580
+ c_target_total = pick_col(pp, ["total _sampel","total_sampel","TOTAL_SAMPEL","Total Sampel","target_total_68","Target_Total_68"])
581
+
582
+ # REAL kolom file user:
583
+ c_pop_sekolah = pick_col(pp, ["total_pend","TOTAL_PEND","total_penduduk","Total Penduduk"])
584
+ c_pop_umum = pick_col(pp, ["perpus_umum_prop","PERPUS_UMUM_PROP","Perpus_umum_prop"])
 
 
 
 
 
 
585
 
586
  if c_pr is None or c_target_total is None:
587
  info = "❌ POP_PROV: wajib ada kolom Provinsi dan total _sampel (target 68%)."
588
+ _CACHE.update({
589
+ "key": key, "df_all": None, "df_raw": None,
590
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
591
+ "meta": {}, "info": info
592
+ })
593
  return None, None, None, None, None, {}, info
594
 
595
  pop_prov = pd.DataFrame({
596
  "Provinsi_Label": pp[c_pr].astype(str).str.strip(),
597
  "Target68_Total_Prov": pp[c_target_total].apply(coerce_num),
598
+
599
+ "Pop_Sekolah_Prov": pp[c_pop_sekolah].apply(coerce_num) if c_pop_sekolah else np.nan,
600
+ "Target68_Sekolah_Prov": pp[c_target_total].apply(coerce_num), # sesuai file user
601
+
602
+ "Pop_Umum_Prov": pp[c_pop_umum].apply(coerce_num) if c_pop_umum else np.nan,
603
+ "Target68_Umum_Prov": np.nan,
604
  })
605
 
606
+ m = pop_prov["Target68_Umum_Prov"].isna() & pop_prov["Pop_Umum_Prov"].notna() & (pop_prov["Pop_Umum_Prov"] > 0)
607
+ pop_prov.loc[m, "Target68_Umum_Prov"] = pop_prov.loc[m, "Pop_Umum_Prov"] * float(FALLBACK_TARGET_RATIO)
608
+
609
+ pop_prov["Pop_Total_Prov"] = (
610
+ pd.to_numeric(pop_prov["Pop_Sekolah_Prov"], errors="coerce").fillna(0.0)
611
+ + pd.to_numeric(pop_prov["Pop_Umum_Prov"], errors="coerce").fillna(0.0)
612
+ )
613
 
614
+ m_need_pop = (pop_prov["Pop_Total_Prov"] <= 0) & pop_prov["Target68_Total_Prov"].notna() & (pop_prov["Target68_Total_Prov"] > 0)
615
+ pop_prov.loc[m_need_pop, "Pop_Total_Prov"] = pop_prov.loc[m_need_pop, "Target68_Total_Prov"] / float(FALLBACK_TARGET_RATIO)
616
 
617
  pop_prov["prov_key"] = pop_prov["Provinsi_Label"].apply(norm_prov_label)
618
+
619
  pop_prov = pop_prov.groupby("prov_key", as_index=False).agg({
620
+ "Provinsi_Label": "first",
621
+ "Target68_Total_Prov": "max",
622
+ "Pop_Total_Prov": "max",
623
+ "Pop_Sekolah_Prov": "max",
624
+ "Target68_Sekolah_Prov": "max",
625
+ "Pop_Umum_Prov": "max",
626
+ "Target68_Umum_Prov": "max",
627
  })
628
 
629
  # =========================
 
633
  pop_khusus = _parse_pop_khusus(POP_KHUSUS)
634
  except Exception as e:
635
  info = f"❌ POP_KHUSUS gagal dibaca: {repr(e)}"
636
+ _CACHE.update({
637
+ "key": key, "df_all": None, "df_raw": None,
638
+ "pop_kab": None, "pop_prov": None, "pop_khusus": None,
639
+ "meta": {}, "info": info
640
+ })
641
  return None, None, None, None, None, {}, info
642
 
643
  df_all = prepare_global(df_raw)
 
644
  meta = dict(prov_col=prov_col, kab_col=kab_col, kew_col=kew_col, jenis_col=jenis_col, nama_col=nama_col)
645
 
646
  info = (
647
  f"βœ… Mode NO UPLOAD (cache aktif)<br>"
648
  f"βœ… DM: <b>{fp.name}</b> | Baris: {before} β†’ dedup: {after}<br>"
649
+ f"βœ… POP_KAB: <b>{Path(POP_KAB).name}</b> (n={len(pop_kab)}) β€” keep per-jenis (umum+sekolah)<br>"
650
+ f"βœ… POP_PROV: <b>{Path(POP_PROV).name}</b> (n={len(pop_prov)}) β€” keep per-jenis (umum+sekolah)<br>"
651
+ f"βœ… POP_KHUSUS: <b>{Path(POP_KHUSUS).name}</b> (n={len(pop_khusus)}) β€” khusus per kab + prov_key<br>"
652
  f"πŸ•’ mtime: DM={time.ctime(_mtime(DATA_FILE))} | Kab={time.ctime(_mtime(POP_KAB))} | Prov={time.ctime(_mtime(POP_PROV))} | Khusus={time.ctime(_mtime(POP_KHUSUS))}"
653
  )
654
 
 
662
  "meta": meta,
663
  "info": info
664
  })
665
+
666
  return df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info
667
 
668
 
669
  # ============================================================
670
+ # 6) FAKTOR WILAYAH β€” PER JENIS
671
+ # - sekolah/umum: dari pop_kab/pop_prov (kolom per-jenis hasil loader)
672
+ # - khusus: dari pop_khusus (sum per wilayah)
673
  # ============================================================
674
 
675
  def _read_target_pop_per_jenis_from_pop(pop_df: pd.DataFrame, mode: str):
 
 
 
 
 
 
 
676
  if pop_df is None or pop_df.empty:
677
  return {"sekolah": (None, None), "umum": (None, None)}
678
 
679
+ mode = str(mode).upper()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
 
681
+ if mode == "KAB":
682
+ sekolah_target = pick_col(pop_df, ["Target68_Sekolah"])
683
+ sekolah_pop = pick_col(pop_df, ["Pop_Sekolah"])
684
+ umum_target = pick_col(pop_df, ["Target68_Umum"])
685
+ umum_pop = pick_col(pop_df, ["Pop_Umum"])
686
+ else:
687
+ sekolah_target = pick_col(pop_df, ["Target68_Sekolah_Prov"])
688
+ sekolah_pop = pick_col(pop_df, ["Pop_Sekolah_Prov"])
689
+ umum_target = pick_col(pop_df, ["Target68_Umum_Prov"])
690
+ umum_pop = pick_col(pop_df, ["Pop_Umum_Prov"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
 
692
  return {"sekolah": (sekolah_target, sekolah_pop), "umum": (umum_target, umum_pop)}
693
 
 
694
  def build_faktor_wilayah_jenis(
695
  df_filtered: pd.DataFrame,
696
  pop_kab: pd.DataFrame,
 
698
  pop_khusus: pd.DataFrame,
699
  kew_value: str
700
  ):
 
 
 
 
 
 
 
701
  if df_filtered is None or df_filtered.empty:
702
  return pd.DataFrame()
703
 
 
707
  if df.empty:
708
  return pd.DataFrame()
709
 
 
710
  if "PROV" in kew_norm:
711
  key_col, label_col, label_name, mode = "prov_key", "PROV_DISP", "Provinsi", "PROV"
712
  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([]))
 
714
  key_col, label_col, label_name, mode = "kab_key", "KAB_DISP", "Kab/Kota", "KAB"
715
  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([]))
716
 
 
717
  base_n = (
718
  df.groupby([key_col, label_col, "_dataset"], dropna=False)
719
  .size()
 
722
  )
723
  base_n["Jenis"] = base_n["Jenis"].astype(str).str.lower().str.strip()
724
 
 
 
 
 
725
  base_n["target_total_68_jenis"] = 0.0
726
  base_n["pop_total_jenis"] = 0.0
727
 
728
+ # sekolah & umum dari POP_KAB/POP_PROV
729
+ tp = _read_target_pop_per_jenis_from_pop(pop_base.reset_index(), mode=mode)
 
730
  for j in ["sekolah", "umum"]:
731
+ tcol, pcol = tp.get(j, (None, None))
732
  if pop_base.empty:
733
  continue
734
 
 
735
  if pcol is not None and pcol in pop_base.columns:
736
  pser = pd.to_numeric(pop_base[pcol], errors="coerce").fillna(0.0)
737
  else:
738
  pser = pd.Series(0.0, index=pop_base.index)
739
 
 
740
  if tcol is not None and tcol in pop_base.columns:
741
  tser = pd.to_numeric(pop_base[tcol], errors="coerce").fillna(0.0)
742
  else:
 
743
  tser = (pser.astype(float) * float(FALLBACK_TARGET_RATIO)).fillna(0.0)
744
 
745
  mask = base_n["Jenis"].eq(j)
746
  base_n.loc[mask, "pop_total_jenis"] = base_n.loc[mask, "group_key"].map(pser).fillna(0.0).values
747
  base_n.loc[mask, "target_total_68_jenis"] = base_n.loc[mask, "group_key"].map(tser).fillna(0.0).values
748
 
 
749
  # KHUSUS dari POP_KHUSUS (sum per wilayah)
 
750
  if pop_khusus is not None and not pop_khusus.empty:
751
  pk = pop_khusus.copy()
752
+ pk["Target68_Total_Jenis"] = pd.to_numeric(pk.get("Target68_Total_Jenis", 0), errors="coerce").fillna(0.0)
753
+ pk["Pop_Total_Jenis"] = pd.to_numeric(pk.get("Pop_Total_Jenis", 0), errors="coerce").fillna(0.0)
754
 
755
  if mode == "PROV" and "prov_key" in pk.columns:
756
+ pk_map = (
757
+ pk.groupby("prov_key", as_index=False)
758
+ .agg(
759
+ target_total_68_jenis=("Target68_Total_Jenis", "sum"),
760
+ pop_total_jenis=("Pop_Total_Jenis", "sum"),
761
+ )
762
+ .rename(columns={"prov_key": "group_key"})
763
+ )
764
  elif mode == "KAB" and "kab_key" in pk.columns:
765
+ pk_map = (
766
+ pk.groupby("kab_key", as_index=False)
767
+ .agg(
768
+ target_total_68_jenis=("Target68_Total_Jenis", "sum"),
769
+ pop_total_jenis=("Pop_Total_Jenis", "sum"),
770
+ )
771
+ .rename(columns={"kab_key": "group_key"})
772
+ )
773
  else:
774
+ pk_map = pd.DataFrame(columns=["group_key", "target_total_68_jenis", "pop_total_jenis"])
775
 
776
  if not pk_map.empty:
777
+ idx = pk_map.set_index("group_key")
778
+ mask = base_n["Jenis"].eq("khusus")
779
+ base_n.loc[mask, "target_total_68_jenis"] = base_n.loc[mask, "group_key"].map(idx["target_total_68_jenis"]).fillna(0.0).values
780
+ base_n.loc[mask, "pop_total_jenis"] = base_n.loc[mask, "group_key"].map(idx["pop_total_jenis"]).fillna(0.0).values
781
 
782
+ # fallback pop dari target
 
 
783
  base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0.0)
784
  base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0.0)
785
 
786
  m_need_pop = (base_n["pop_total_jenis"] <= 0) & (base_n["target_total_68_jenis"] > 0)
787
  base_n.loc[m_need_pop, "pop_total_jenis"] = base_n.loc[m_need_pop, "target_total_68_jenis"] / float(FALLBACK_TARGET_RATIO)
788
 
789
+ # faktor, coverage, gap
790
  base_n["faktor_penyesuaian_jenis"] = [
791
  faktor_penyesuaian_total(n, t)
792
  for n, t in zip(
 
796
  ]
797
 
798
  base_n["coverage_jenis_%"] = [
799
+ (safe_div(n, p) * 100.0) if (p is not None and not pd.isna(p) and float(p) > 0) else 0.0
800
  for n, p in zip(
801
  pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
802
  pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).astype(float).tolist()
 
804
  ]
805
 
806
  base_n["gap_target68_jenis"] = [
807
+ max(t - n, 0.0)
808
  for n, t in zip(
809
  pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).astype(float).tolist(),
810
  pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).astype(float).tolist()
811
  )
812
  ]
813
 
814
+ # display format
815
  base_n["target_total_68_jenis"] = pd.to_numeric(base_n["target_total_68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
816
  base_n["pop_total_jenis"] = pd.to_numeric(base_n["pop_total_jenis"], errors="coerce").fillna(0).round(0).astype(int)
817
+ base_n["n_jenis"] = pd.to_numeric(base_n["n_jenis"], errors="coerce").fillna(0).round(0).astype(int)
818
+ base_n["gap_target68_jenis"] = pd.to_numeric(base_n["gap_target68_jenis"], errors="coerce").fillna(0).round(0).astype(int)
819
  base_n["coverage_jenis_%"] = pd.to_numeric(base_n["coverage_jenis_%"], errors="coerce").fillna(0.0).round(2)
820
  base_n["faktor_penyesuaian_jenis"] = pd.to_numeric(base_n["faktor_penyesuaian_jenis"], errors="coerce").fillna(1.0).round(3)
 
821
 
822
  return base_n
823
 
 
900
 
901
  return agg
902
  # ============================================================
903
+ # 8) AGREGAT WILAYAH (KESELURUHAN) β€” FIX: avg3 dari 3 jenis
904
+ # + tampilkan Pop/Target/Terkumpul per jenis & total
905
  # ============================================================
906
 
907
  def build_agg_wilayah_total_from_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame, kew_value: str):
 
954
  Indeks_Final_Wilayah_0_100=("Indeks_Final_Agregat_0_100", "mean"),
955
  )
956
 
957
+ # tempel Pop/Target/Terkumpul per jenis & total
958
  if faktor_wilayah_jenis is not None and not faktor_wilayah_jenis.empty:
959
  fw = faktor_wilayah_jenis.copy()
960
  fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
961
 
 
962
  piv = fw.pivot_table(
963
  index=["group_key", label_name],
964
  columns="Jenis",
965
+ values=["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis", "faktor_penyesuaian_jenis"],
 
 
 
 
 
 
 
966
  aggfunc="first"
967
  )
968
+
969
  piv.columns = [f"{v}_{k}" for v, k in piv.columns]
970
  piv = piv.reset_index()
971
 
972
  out = out.merge(piv, on=["group_key", label_name], how="left")
973
 
974
+ # NaN -> 0 / 1
975
  for j in ["sekolah", "umum", "khusus"]:
976
  for basecol in ["pop_total_jenis", "target_total_68_jenis", "n_jenis", "gap_target68_jenis"]:
977
  c = f"{basecol}_{j}"
978
  if c in out.columns:
979
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
980
 
981
+ cfac = f"faktor_penyesuaian_jenis_{j}"
982
+ if cfac in out.columns:
983
+ out[cfac] = pd.to_numeric(out[cfac], errors="coerce").fillna(1.0).round(3)
984
+
985
+ # TOTAL (sum 3 jenis)
986
+ out["pop_total_all"] = (
987
+ out.get("pop_total_jenis_sekolah", 0)
988
+ + out.get("pop_total_jenis_umum", 0)
989
+ + out.get("pop_total_jenis_khusus", 0)
990
+ ).astype(int)
991
+
992
+ out["target_total_68_all"] = (
993
+ out.get("target_total_68_jenis_sekolah", 0)
994
+ + out.get("target_total_68_jenis_umum", 0)
995
+ + out.get("target_total_68_jenis_khusus", 0)
996
+ ).astype(int)
997
+
998
+ out["terkumpul_all"] = (
999
+ out.get("n_jenis_sekolah", 0)
1000
+ + out.get("n_jenis_umum", 0)
1001
+ + out.get("n_jenis_khusus", 0)
1002
+ ).astype(int)
1003
+
1004
+ out["coverage_target68_all_%"] = np.where(
1005
+ pd.to_numeric(out["target_total_68_all"], errors="coerce").fillna(0).values > 0,
1006
+ (pd.to_numeric(out["terkumpul_all"], errors="coerce").fillna(0).values / pd.to_numeric(out["target_total_68_all"], errors="coerce").fillna(0).values) * 100.0,
1007
+ 0.0
1008
+ )
1009
+ out["coverage_target68_all_%"] = pd.to_numeric(out["coverage_target68_all_%"], errors="coerce").fillna(0.0).round(2)
 
 
 
1010
 
1011
  # rounding index
1012
  for c in [
 
1020
  if c in out.columns:
1021
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1022
 
1023
+ out["n_total"] = pd.to_numeric(out["n_total"], errors="coerce").fillna(0).round(0).astype(int)
 
 
1024
 
1025
  return out
1026
 
1027
+
1028
  # ============================================================
1029
+ # 9) SUMMARY (PER JENIS) + KESELURUHAN
1030
+ # - selalu 4 baris: sekolah, umum, khusus, keseluruhan
1031
+ # - tampilkan Pop/Target/Terkumpul/Coverage per jenis
1032
+ # - Penyesuaian_Poin = Final - Dasar
1033
  # ============================================================
1034
 
1035
+ def build_summary_per_jenis(agg_jenis: pd.DataFrame, faktor_wilayah_jenis: pd.DataFrame):
1036
  jenis_list = ["sekolah", "umum", "khusus"]
1037
 
1038
  def _row_default(jenis):
 
1040
  "Jenis": jenis,
1041
  "Jumlah_Wilayah": 0,
1042
  "Total_Perpus": 0,
 
 
1043
  "Pop_Total_Jenis": 0,
1044
  "Target68_Total_Jenis": 0,
1045
  "Terkumpul_Jenis": 0,
1046
+ "Coverage_Target68_Jenis_%": 0.0,
 
 
1047
  "Rata2_sub_koleksi": 0.0,
1048
  "Rata2_sub_sdm": 0.0,
1049
  "Rata2_sub_pelayanan": 0.0,
1050
  "Rata2_sub_pengelolaan": 0.0,
1051
  "Rata2_dim_kepatuhan": 0.0,
1052
  "Rata2_dim_kinerja": 0.0,
 
 
1053
  "Indeks_Dasar_0_100": 0.0,
1054
  "Indeks_Final_Disesuaikan_0_100": 0.0,
1055
+ "Penyesuaian_Poin": 0.0
1056
  }
1057
 
1058
  rows_by_jenis = {j: _row_default(j) for j in jenis_list}
1059
 
1060
+ a = agg_jenis.copy() if (agg_jenis is not None) else pd.DataFrame()
1061
+ if not a.empty:
1062
+ a["Jenis"] = a["Jenis"].astype(str).str.lower().str.strip()
1063
+
1064
+ fw = faktor_wilayah_jenis.copy() if (faktor_wilayah_jenis is not None) else pd.DataFrame()
1065
+ if not fw.empty:
1066
  fw["Jenis"] = fw["Jenis"].astype(str).str.lower().str.strip()
1067
 
1068
+ for jenis in jenis_list:
1069
+ sub = a[a["Jenis"] == jenis].copy() if not a.empty else pd.DataFrame()
1070
+ subfw = fw[fw["Jenis"] == jenis].copy() if not fw.empty else pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1071
 
1072
+ if not sub.empty:
1073
  dasar = float(pd.to_numeric(sub["Indeks_Dasar_Agregat_0_100"], errors="coerce").fillna(0).mean())
1074
  final = float(pd.to_numeric(sub["Indeks_Final_Agregat_0_100"], errors="coerce").fillna(0).mean())
1075
 
1076
+ rows_by_jenis[jenis].update({
 
1077
  "Jumlah_Wilayah": int(sub.shape[0]),
1078
  "Total_Perpus": int(pd.to_numeric(sub["Jumlah"], errors="coerce").fillna(0).sum()),
 
 
 
 
 
 
1079
  "Rata2_sub_koleksi": float(pd.to_numeric(sub["Rata2_sub_koleksi"], errors="coerce").fillna(0).mean()),
1080
  "Rata2_sub_sdm": float(pd.to_numeric(sub["Rata2_sub_sdm"], errors="coerce").fillna(0).mean()),
1081
  "Rata2_sub_pelayanan": float(pd.to_numeric(sub["Rata2_sub_pelayanan"], errors="coerce").fillna(0).mean()),
1082
  "Rata2_sub_pengelolaan": float(pd.to_numeric(sub["Rata2_sub_pengelolaan"], errors="coerce").fillna(0).mean()),
1083
  "Rata2_dim_kepatuhan": float(pd.to_numeric(sub["Rata2_dim_kepatuhan"], errors="coerce").fillna(0).mean()),
1084
  "Rata2_dim_kinerja": float(pd.to_numeric(sub["Rata2_dim_kinerja"], errors="coerce").fillna(0).mean()),
 
1085
  "Indeks_Dasar_0_100": dasar,
1086
  "Indeks_Final_Disesuaikan_0_100": final,
1087
+ "Penyesuaian_Poin": (final - dasar),
1088
+ })
1089
+
1090
+ if not subfw.empty:
1091
+ pop_j = int(pd.to_numeric(subfw["pop_total_jenis"], errors="coerce").fillna(0).sum())
1092
+ tgt_j = int(pd.to_numeric(subfw["target_total_68_jenis"], errors="coerce").fillna(0).sum())
1093
+ n_j = int(pd.to_numeric(subfw["n_jenis"], errors="coerce").fillna(0).sum())
1094
+
1095
+ cov = (n_j / tgt_j * 100.0) if tgt_j > 0 else 0.0
1096
+
1097
+ rows_by_jenis[jenis].update({
1098
+ "Pop_Total_Jenis": pop_j,
1099
+ "Target68_Total_Jenis": tgt_j,
1100
+ "Terkumpul_Jenis": n_j,
1101
+ "Coverage_Target68_Jenis_%": cov
1102
+ })
1103
 
1104
  rows = [rows_by_jenis[j] for j in jenis_list]
1105
 
1106
+ # keseluruhan: avg3 (tetap Γ·3)
1107
  def _avg3(field):
1108
  return (
1109
  float(rows_by_jenis["sekolah"][field])
 
1111
  + float(rows_by_jenis["khusus"][field])
1112
  ) / 3.0
1113
 
1114
+ keseluruhan = _row_default("keseluruhan")
1115
+ keseluruhan["Jumlah_Wilayah"] = int(max(
1116
+ rows_by_jenis["sekolah"]["Jumlah_Wilayah"],
1117
+ rows_by_jenis["umum"]["Jumlah_Wilayah"],
1118
+ rows_by_jenis["khusus"]["Jumlah_Wilayah"],
1119
+ ))
1120
+ keseluruhan["Total_Perpus"] = int(
1121
+ rows_by_jenis["sekolah"]["Total_Perpus"]
1122
+ + rows_by_jenis["umum"]["Total_Perpus"]
1123
+ + rows_by_jenis["khusus"]["Total_Perpus"]
1124
+ )
1125
+
1126
+ keseluruhan["Pop_Total_Jenis"] = int(
1127
+ rows_by_jenis["sekolah"]["Pop_Total_Jenis"]
1128
+ + rows_by_jenis["umum"]["Pop_Total_Jenis"]
1129
+ + rows_by_jenis["khusus"]["Pop_Total_Jenis"]
1130
+ )
1131
+ keseluruhan["Target68_Total_Jenis"] = int(
1132
+ rows_by_jenis["sekolah"]["Target68_Total_Jenis"]
1133
+ + rows_by_jenis["umum"]["Target68_Total_Jenis"]
1134
+ + rows_by_jenis["khusus"]["Target68_Total_Jenis"]
1135
+ )
1136
+ keseluruhan["Terkumpul_Jenis"] = int(
1137
+ rows_by_jenis["sekolah"]["Terkumpul_Jenis"]
1138
+ + rows_by_jenis["umum"]["Terkumpul_Jenis"]
1139
+ + rows_by_jenis["khusus"]["Terkumpul_Jenis"]
1140
+ )
1141
+ keseluruhan["Coverage_Target68_Jenis_%"] = (
1142
+ (keseluruhan["Terkumpul_Jenis"] / keseluruhan["Target68_Total_Jenis"] * 100.0)
1143
+ if keseluruhan["Target68_Total_Jenis"] > 0 else 0.0
1144
+ )
1145
+
1146
+ keseluruhan["Rata2_sub_koleksi"] = _avg3("Rata2_sub_koleksi")
1147
+ keseluruhan["Rata2_sub_sdm"] = _avg3("Rata2_sub_sdm")
1148
+ keseluruhan["Rata2_sub_pelayanan"] = _avg3("Rata2_sub_pelayanan")
1149
+ keseluruhan["Rata2_sub_pengelolaan"] = _avg3("Rata2_sub_pengelolaan")
1150
+ keseluruhan["Rata2_dim_kepatuhan"] = _avg3("Rata2_dim_kepatuhan")
1151
+ keseluruhan["Rata2_dim_kinerja"] = _avg3("Rata2_dim_kinerja")
1152
+ keseluruhan["Indeks_Dasar_0_100"] = _avg3("Indeks_Dasar_0_100")
1153
+ keseluruhan["Indeks_Final_Disesuaikan_0_100"] = _avg3("Indeks_Final_Disesuaikan_0_100")
1154
+ keseluruhan["Penyesuaian_Poin"] = keseluruhan["Indeks_Final_Disesuaikan_0_100"] - keseluruhan["Indeks_Dasar_0_100"]
1155
+
1156
+ rows.append(keseluruhan)
1157
 
1158
  out = pd.DataFrame(rows)
1159
 
1160
+ # format display
1161
+ for c in ["Jumlah_Wilayah","Total_Perpus","Pop_Total_Jenis","Target68_Total_Jenis","Terkumpul_Jenis"]:
1162
+ out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0).round(0).astype(int)
1163
+
1164
+ out["Coverage_Target68_Jenis_%"] = pd.to_numeric(out["Coverage_Target68_Jenis_%"], errors="coerce").fillna(0.0).round(2)
1165
+
1166
  for c in [
1167
  "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1168
  "Rata2_dim_kepatuhan","Rata2_dim_kinerja"
 
1172
  for c in ["Indeks_Dasar_0_100","Indeks_Final_Disesuaikan_0_100","Penyesuaian_Poin"]:
1173
  out[c] = pd.to_numeric(out[c], errors="coerce").fillna(0.0).round(2)
1174
 
 
 
 
 
 
 
1175
  return out
1176
 
1177
  # ============================================================
 
1377
 
1378
 
1379
  # ============================================================
1380
+ # 13) KPI DASHBOARD
1381
+ # - HAPUS: Cakupan Sampel & Penyesuaian Nilai dari dashboard
1382
+ # - Dashboard hanya tampil: FINAL & DASAR
1383
  # ============================================================
1384
 
1385
+ def compute_dashboard_kpis(summary_jenis: pd.DataFrame):
1386
+ if summary_jenis is None or summary_jenis.empty:
1387
+ return {"final_all": 0.0, "dasar_all": 0.0}
 
 
 
1388
 
1389
+ s = summary_jenis.copy()
1390
+ s["Jenis"] = s["Jenis"].astype(str).str.lower().str.strip()
1391
 
1392
+ sub = s[s["Jenis"] == "keseluruhan"]
1393
+ if sub.empty:
1394
+ final_all = float(pd.to_numeric(s["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).mean())
1395
+ dasar_all = float(pd.to_numeric(s["Indeks_Dasar_0_100"], errors="coerce").fillna(0).mean())
1396
+ else:
1397
+ final_all = float(pd.to_numeric(sub["Indeks_Final_Disesuaikan_0_100"], errors="coerce").fillna(0).iloc[0])
1398
+ dasar_all = float(pd.to_numeric(sub["Indeks_Dasar_0_100"], errors="coerce").fillna(0).iloc[0])
1399
 
1400
+ return {"final_all": final_all, "dasar_all": dasar_all}
1401
 
1402
+ def build_kpi_markdown(summary_jenis: pd.DataFrame) -> str:
 
 
1403
  if summary_jenis is None or summary_jenis.empty:
1404
  return ""
1405
 
1406
+ k = compute_dashboard_kpis(summary_jenis)
1407
 
1408
  def fmt(x, nd=2):
1409
  return "NA" if pd.isna(x) else f"{x:.{nd}f}"
1410
 
1411
  return f"""
1412
  <div style="display:flex; gap:12px; flex-wrap:wrap;">
1413
+ <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:240px;">
1414
  <div style="opacity:0.8;">Indeks IPLM FINAL (Disesuaikan)</div>
1415
  <div style="font-size:26px; font-weight:700;">{fmt(k["final_all"],2)}</div>
1416
+ <div style="opacity:0.7;">Sumber: baris "keseluruhan" (avg3 tetap Γ·3)</div>
1417
  </div>
1418
 
1419
+ <div style="border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:240px;">
1420
  <div style="opacity:0.8;">Indeks Dasar (Tanpa Penyesuaian)</div>
1421
  <div style="font-size:26px; font-weight:700;">{fmt(k["dasar_all"],2)}</div>
1422
+ <div style="opacity:0.7;">Sumber: baris "keseluruhan" (avg3 tetap Γ·3)</div>
1423
  </div>
1424
  </div>
1425
  """.strip()
1426
 
1427
+
1428
  # ============================================================
1429
+ # 14) LLM + WORD (FIX: generate_llm_analysis pasti ada)
1430
  # ============================================================
1431
 
1432
  _HF_CLIENT = None
 
1442
  _HF_CLIENT = None
1443
  return None
1444
 
1445
+ def build_context(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, wilayah: str, kew: str) -> str:
 
1446
  lines = []
1447
  lines.append(f"Wilayah filter: {wilayah}")
1448
  lines.append(f"Kewenangan: {kew}")
1449
+ lines.append("Metode: Indeks dasar dihitung per entitas (Yeo-Johnson + MinMax nasional), lalu agregat per wilayahΓ—jenis.")
1450
+ lines.append("Penyesuaian berbasis kecukupan sampel (target 68%) dihitung PER JENIS (sekolah/umum/khusus).")
1451
+ lines.append("Keseluruhan wilayah (FIX): rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3).")
1452
 
1453
  if summary_jenis is not None and not summary_jenis.empty:
1454
  lines.append("\nRingkasan (jenis + keseluruhan):")
1455
  for _, r in summary_jenis.iterrows():
1456
+ lines.append(
1457
+ f"- {r['Jenis']}: wilayah={int(r['Jumlah_Wilayah'])}, total_perpus={int(r['Total_Perpus'])}, "
1458
+ f"pop={int(r['Pop_Total_Jenis'])}, target68={int(r['Target68_Total_Jenis'])}, terkumpul={int(r['Terkumpul_Jenis'])}, "
1459
+ f"coverage={float(r['Coverage_Target68_Jenis_%']):.2f}%, "
1460
+ f"dasar={float(r['Indeks_Dasar_0_100']):.2f}, final={float(r['Indeks_Final_Disesuaikan_0_100']):.2f}"
1461
+ )
 
 
 
 
1462
 
1463
  if agg_total is not None and not agg_total.empty:
1464
  label_col = "Kab/Kota" if "Kab/Kota" in agg_total.columns else ("Provinsi" if "Provinsi" in agg_total.columns else None)
1465
+ lines.append("\nTop 5 wilayah (Final tertinggi):")
1466
+ top = agg_total.sort_values("Indeks_Final_Wilayah_0_100", ascending=False).head(5)
1467
+ for _, r in top.iterrows():
1468
+ wl = r.get(label_col, "(wilayah)") if label_col else "(wilayah)"
1469
+ lines.append(f"- {wl}: Final={float(r['Indeks_Final_Wilayah_0_100']):.2f} | Dasar={float(r['Indeks_Dasar_Agregat_0_100']):.2f} | n_total={int(r.get('n_total', 0))}")
 
 
1470
 
1471
  return "\n".join(lines)
1472
 
1473
+ def generate_llm_analysis(summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, wilayah: str, kew: str) -> str:
1474
+ ctx = build_context(summary_jenis, agg_total, wilayah, kew)
1475
 
1476
+ if (not USE_LLM) or (HF_TOKEN is None):
1477
+ return "Analisis otomatis (LLM) nonaktif / token tidak tersedia."
1478
 
 
1479
  client = get_llm_client()
1480
+ if client is None:
1481
+ return "Analisis otomatis (LLM) tidak tersedia (client gagal dibuat)."
 
 
 
1482
 
1483
  system_prompt = (
1484
  "Anda adalah analis kebijakan perpustakaan dan literasi di Indonesia. "
1485
  "Tugas Anda menyusun analisis berbasis data IPLM secara formal, tajam, dan operasional."
1486
  )
 
1487
  user_prompt = f"""
1488
  DATA RINGKAS IPLM:
1489
 
1490
  {ctx}
1491
 
1492
  TULISKAN ANALISIS BAHASA INDONESIA FORMAL, STRUKTUR:
1493
+ 1) Gambaran umum hasil (1 paragraf).
1494
+ 2) Analisis per jenis (sekolah, umum, khusus) + keseluruhan (2 paragraf).
1495
+ 3) Penjelasan pembacaan Pop/Target68/Terkumpul/Coverage (1 paragraf).
1496
+ 4) Rekomendasi program 3–5 tahun (2 paragraf, konkret).
1497
 
1498
  ATURAN:
1499
  - Jangan memakai label eksplisit "rendah/sedang/tinggi".
1500
  - Gunakan frasa netral: "memerlukan penguatan", "memerlukan konsolidasi", dsb.
1501
+ - Fokus pada nilai agregat wilayah.
1502
  """
1503
 
1504
  try:
1505
  resp = client.chat_completion(
1506
  model=LLM_MODEL_NAME,
1507
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
 
 
 
1508
  max_tokens=1100,
1509
  temperature=0.25,
1510
  top_p=0.9,
 
1514
  except Exception as e:
1515
  return f"⚠️ Error saat memanggil LLM: {repr(e)}"
1516
 
1517
+ def generate_word_report(wilayah: str, summary_jenis: pd.DataFrame, agg_total: pd.DataFrame, analysis_text: str):
 
1518
  doc = Document()
1519
  doc.add_heading(f"Laporan IPLM β€” {wilayah}", level=1)
1520
 
1521
  doc.add_heading("Ringkasan Dashboard", level=2)
1522
+ k = compute_dashboard_kpis(summary_jenis)
1523
+ doc.add_paragraph(f"Indeks IPLM FINAL (Disesuaikan): {k['final_all']:.2f}")
1524
+ doc.add_paragraph(f"Indeks Dasar (Tanpa Penyesuaian): {k['dasar_all']:.2f}")
1525
 
1526
+ doc.add_heading("Ringkasan (Jenis + Keseluruhan)", level=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1527
  show = summary_jenis.copy()
 
1528
  preferred = [
1529
+ "Jenis","Jumlah_Wilayah","Total_Perpus",
1530
+ "Pop_Total_Jenis","Target68_Total_Jenis","Terkumpul_Jenis","Coverage_Target68_Jenis_%",
1531
+ "Indeks_Dasar_0_100","Indeks_Final_Disesuaikan_0_100","Penyesuaian_Poin"
 
1532
  ]
1533
  show = show[[c for c in preferred if c in show.columns]]
1534
 
 
1543
  v = row[c]
1544
  if pd.isna(v):
1545
  cells[i].text = ""
 
 
1546
  elif isinstance(v, (float, np.floating)):
1547
+ if c.endswith("_%"):
 
 
 
 
1548
  cells[i].text = f"{float(v):.2f}"
1549
  else:
1550
  cells[i].text = f"{float(v):.2f}"
1551
+ elif isinstance(v, (int, np.integer)):
1552
+ cells[i].text = str(int(v))
1553
  else:
1554
  cells[i].text = str(v)
1555
 
1556
+ doc.add_heading("Agregat Wilayah (Keseluruhan)", level=2)
1557
+ if agg_total is not None and not agg_total.empty:
1558
+ cols = [c for c in agg_total.columns if c not in ["group_key"]]
1559
+ t2 = doc.add_table(rows=1, cols=len(cols))
1560
+ h2 = t2.rows[0].cells
1561
+ for i, c in enumerate(cols):
1562
+ h2[i].text = str(c)
1563
+
1564
+ for _, r in agg_total.head(50).iterrows():
1565
+ rr = t2.add_row().cells
1566
+ for i, c in enumerate(cols):
1567
+ vv = r[c]
1568
+ if pd.isna(vv):
1569
+ rr[i].text = ""
1570
+ elif isinstance(vv, (float, np.floating)):
1571
+ rr[i].text = f"{float(vv):.2f}"
1572
+ elif isinstance(vv, (int, np.integer)):
1573
+ rr[i].text = str(int(vv))
1574
+ else:
1575
+ rr[i].text = str(vv)
1576
 
1577
  doc.add_heading("Analisis Naratif (LLM)", level=2)
1578
  for p in (analysis_text or "").split("\n"):
 
1593
  empty_fig = go.Figure()
1594
  return (
1595
  "", # kpi_md
1596
+ empty, empty, empty, empty, empty, # summary, agg_total, agg_jenis_view, detail, verif
1597
+ None, None, None, None, None, # downloads
1598
+ empty_fig, empty_fig, empty_fig, # figs
1599
  msg, "Analisis belum tersedia."
1600
  )
1601
 
 
1604
  if df_all is None or df_all.empty or df_raw is None or df_raw.empty:
1605
  return _empty_outputs("⚠️ Data belum ter-load. Pastikan file tersedia di repo/server.")
1606
 
 
1607
  # FILTER ANALISIS (df_all)
 
1608
  df = df_all.copy()
1609
  if prov_value and prov_value != "(Semua)":
1610
  df = df[df["PROV_DISP"] == prov_value]
 
1616
  if df.empty:
1617
  return _empty_outputs("Tidak ada data untuk filter ini.")
1618
 
1619
+ # PIPELINE: faktor -> agg_jenis -> agg_total -> summary
 
 
1620
  faktor_wilayah_jenis = build_faktor_wilayah_jenis(
1621
+ df_filtered=df,
1622
+ pop_kab=pop_kab,
1623
+ pop_prov=pop_prov,
1624
+ pop_khusus=pop_khusus,
1625
+ kew_value=(kew_value or "(Semua)")
1626
  )
1627
 
1628
+ agg_jenis_full = build_agg_wilayah_jenis(df, faktor_wilayah_jenis, kew_value or "(Semua)")
1629
+ agg_total = build_agg_wilayah_total_from_jenis(agg_jenis_full, faktor_wilayah_jenis, kew_value or "(Semua)")
1630
+ summary_jenis = build_summary_per_jenis(agg_jenis_full, faktor_wilayah_jenis)
 
 
 
 
 
 
 
 
1631
 
1632
+ verif_total = build_verif_jenis(faktor_wilayah_jenis, kew_value or "(Semua)")
1633
+ detail_view = attach_final_to_detail(df, agg_total, meta, kew_value or "(Semua)")
 
 
 
 
 
 
 
 
1634
 
1635
+ # VIEW: Agregat Wilayah Γ— Jenis (tampil sampai Indeks_Dasar_Agregat_0_100)
 
 
 
 
 
 
 
 
 
 
1636
  if agg_jenis_full is None or agg_jenis_full.empty:
1637
  agg_jenis_view = agg_jenis_full
1638
  else:
1639
  kew_norm = str(kew_value or "").upper()
1640
+ label_name = "Kab/Kota"
1641
+ if "PROV" in kew_norm:
1642
+ label_name = "Provinsi"
1643
+
1644
  cols_upto = [
1645
  "group_key",
1646
  label_name,
1647
  "Jenis",
1648
  "Jumlah",
1649
+ "Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_sub_pelayanan","Rata2_sub_pengelolaan",
1650
+ "Rata2_dim_kepatuhan","Rata2_dim_kinerja",
1651
  "Indeks_Dasar_Agregat_0_100",
1652
  ]
1653
  cols_upto = [c for c in cols_upto if c in agg_jenis_full.columns]
1654
  agg_jenis_view = agg_jenis_full[cols_upto].copy()
1655
 
 
1656
  # FILTER RAW DOWNLOAD (df_raw)
 
1657
  raw = df_raw.copy()
1658
  if prov_value and prov_value != "(Semua)":
1659
  raw = raw[raw["PROV_DISP"] == prov_value]
 
1662
  if kew_value and kew_value != "(Semua)":
1663
  raw = raw[raw["KEW_NORM"] == kew_value]
1664
 
1665
+ # Bell curve per jenis (per entitas)
 
 
1666
  if detail_view is None or detail_view.empty:
1667
  fig_sekolah = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Sekolah", min_points=2)
1668
+ fig_umum = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Umum", min_points=2)
1669
+ fig_khusus = _make_bell_curve(pd.DataFrame(), "Indeks_Dasar_0_100", "Bell Curve β€” Jenis: Khusus", min_points=2)
1670
  else:
1671
  xcol_ent = "Indeks_Dasar_0_100" if "Indeks_Dasar_0_100" in detail_view.columns else "Indeks_Final_0_100"
1672
  label_col_e = "nm_perpustakaan" if "nm_perpustakaan" in detail_view.columns else None
 
1674
 
1675
  def _fig_jenis_ent(jenis_key: str, judul: str):
1676
  d = detail_view[detail_view["Jenis"].astype(str).str.lower() == jenis_key].copy()
1677
+ return _make_bell_curve(d, xcol=xcol_ent, title=judul, label_col=label_col_e, hover_cols=hover_cols_e, min_points=2)
 
 
 
 
 
 
 
1678
 
1679
  fig_sekolah = _fig_jenis_ent("sekolah", "Bell Curve β€” Jenis: Sekolah (Indeks per Entitas)")
1680
+ fig_umum = _fig_jenis_ent("umum", "Bell Curve β€” Jenis: Umum (Indeks per Entitas)")
1681
+ fig_khusus = _fig_jenis_ent("khusus", "Bell Curve β€” Jenis: Khusus (Indeks per Entitas)")
1682
+
1683
+ # KPI markdown: hanya FINAL & DASAR
1684
+ kpi_md = build_kpi_markdown(summary_jenis)
 
 
 
 
 
 
 
1685
 
1686
+ # Save downloads
 
 
1687
  tmpdir = tempfile.mkdtemp()
1688
  prov_slug = (_canon(prov_value or "SEMUA").upper() or "SEMUA")
1689
+ kab_slug = (_canon(kab_value or "SEMUA").upper() or "SEMUA")
1690
+ kew_slug = (_canon(kew_value or "SEMUA").upper() or "SEMUA")
1691
 
1692
  p_summary = str(Path(tmpdir) / f"IPLM_RingkasanJenisKeseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1693
+ p_total = str(Path(tmpdir) / f"IPLM_AgregatWilayah_Keseluruhan_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1694
+ p_raw = str(Path(tmpdir) / f"IPLM_RAW_DATA_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1695
+ p_detail = str(Path(tmpdir) / f"IPLM_DetailEntitas_FinalMenempelWilayah_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1696
+ p_verif = str(Path(tmpdir) / f"IPLM_KecukupanSampel68_PER_JENIS_{prov_slug}_{kab_slug}_{kew_slug}.xlsx")
1697
 
1698
  summary_jenis.to_excel(p_summary, index=False)
1699
  agg_total.to_excel(p_total, index=False)
1700
+ raw.to_excel(p_raw, index=False)
1701
  detail_view.to_excel(p_detail, index=False)
1702
  verif_total.to_excel(p_verif, index=False)
1703
 
1704
  wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
1705
+ analysis_text = generate_llm_analysis(summary_jenis, agg_total, wilayah_txt, kew_value or "(Semua)")
1706
+ word_path = generate_word_report(wilayah_txt, summary_jenis, agg_total, analysis_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1707
 
1708
  msg = (
1709
+ f"βœ… Selesai: raw={len(raw)} | entitas={len(detail_view)} | wilayah(keseluruhan) & jenis dihitung | "
1710
+ f"FIX: keseluruhan = avg3(3 jenis) Γ·3 | Pop/Target/Terkumpul/Coverage ada di tabel Ringkasan"
1711
  )
1712
 
1713
  return (
1714
  kpi_md,
1715
  summary_jenis, agg_total, agg_jenis_view, detail_view, verif_total,
1716
+ p_summary, p_total, p_raw, p_detail, word_path,
1717
  fig_umum, fig_sekolah, fig_khusus,
1718
  msg, analysis_text
1719
  )
 
1721
  except Exception as e:
1722
  return _empty_outputs(f"⚠️ Runtime error: {repr(e)}")
1723
 
 
1724
  # ============================================================
1725
  # 16) UI (NO UPLOAD)
1726
  # ============================================================
1727
 
1728
  def ui_load(force=False):
1729
  df_all, df_raw, pop_kab, pop_prov, pop_khusus, meta, info = load_default_files(force=force)
1730
+
1731
  if df_all is None or (isinstance(df_all, pd.DataFrame) and df_all.empty):
1732
  return (
1733
  None, None, None, None, None, {}, info,
 
1755
  df_all, _, _, _, _, _, _ = load_default_files(force=False)
1756
  if df_all is None or df_all.empty:
1757
  return gr.update(choices=["(Semua)"], value="(Semua)")
1758
+
1759
  if prov_value is None or prov_value == "(Semua)":
1760
  vals = df_all["KAB_DISP"].dropna().unique().tolist()
1761
  else:
1762
  vals = df_all.loc[df_all["PROV_DISP"] == prov_value, "KAB_DISP"].dropna().unique().tolist()
1763
+
1764
  vals = sorted([v for v in vals if v])
1765
  return gr.update(choices=["(Semua)"] + vals, value="(Semua)")
1766
 
 
1767
  with gr.Blocks() as demo:
1768
  gr.Markdown(f"""
1769
+ # IPLM 2025 β€” Final (Penyesuaian Berbasis Kecukupan Sampel 68% PER JENIS)
1770
  **Mode NO UPLOAD (cache aktif).** File dibaca dari repo/server:
1771
  - `DATA_FILE` = **{DATA_FILE}**
1772
  - `POP_KAB` = **{POP_KAB}**
 
1774
  - `POP_KHUSUS` = **{POP_KHUSUS}**
1775
 
1776
  **FIX UTAMA (konsistensi nilai):**
1777
+ - **Keseluruhan wilayah = rata-rata 3 jenis (sekolah+umum+khusus) Γ· 3 (missing=0, tetap Γ·3)**
1778
  - Ringkasan selalu tampil **sekolah, umum, khusus, keseluruhan** (walau 0)
1779
+ - Dashboard KPI hanya: **FINAL** & **DASAR**
1780
+ - Pop/Target/Terkumpul/Coverage + Penyesuaian Poin dipindah ke tabel Ringkasan
 
1781
 
1782
  **UPDATE (tampilan):**
1783
+ - target_total_68 & pop_total ditampilkan integer
1784
+ - coverage ditampilkan 2 desimal
1785
  - Tabel "Agregat Wilayah Γ— Jenis" ditampilkan hanya sampai Indeks_Dasar_Agregat_0_100
1786
  """)
1787
 
 
1804
  run_btn = gr.Button("Jalankan Perhitungan")
1805
  msg_out = gr.Markdown()
1806
 
 
1807
  kpi_out = gr.Markdown()
1808
 
1809
  gr.Markdown("## Ringkasan (Jenis + Keseluruhan) β€” Pop/Target68/Terkumpul/Coverage + Penyesuaian")
1810
  out_summary = gr.DataFrame(interactive=False)
1811
 
1812
+ gr.Markdown("## Agregat Wilayah (Keseluruhan) β€” FIX: avg3 dari 3 jenis + Pop/Target/Terkumpul (per jenis & total)")
1813
  out_agg_total = gr.DataFrame(interactive=False)
1814
 
1815
  gr.Markdown("## Agregat Wilayah Γ— Jenis (Sekolah, Umum, Khusus) β€” (ditampilkan sampai Indeks_Dasar_Agregat_0_100)")
 
1818
  gr.Markdown("## Detail Entitas (Final menempel dari wilayah)")
1819
  out_detail = gr.DataFrame(interactive=False)
1820
 
1821
+ gr.Markdown("## Kecukupan Sampel 68% PER JENIS (tanpa angka koma)")
1822
  out_verif = gr.DataFrame(interactive=False)
1823
 
1824
  gr.Markdown("## Bell Curve β€” per Jenis Perpustakaan (Indeks per Entitas)")
 
1837
  with gr.Row():
1838
  dl_summary = gr.DownloadButton(label="Download Ringkasan (.xlsx)")
1839
  dl_total = gr.DownloadButton(label="Download Agregat Wilayah (.xlsx)")
1840
+ dl_raw = gr.DownloadButton(label="Download Data Mentah (.xlsx)")
1841
  dl_detail = gr.DownloadButton(label="Download Detail Entitas (.xlsx)")
1842
  dl_word = gr.DownloadButton(label="Download Laporan Word (.docx)")
1843
 
 
1847
  outputs=[
1848
  kpi_out,
1849
  out_summary, out_agg_total, out_agg_jenis, out_detail, out_verif,
1850
+ dl_summary, dl_total, dl_raw, dl_detail, dl_word,
1851
  bell_umum, bell_sekolah, bell_khusus,
1852
  msg_out, analysis_out
1853
  ]
 
1860
  )
1861
 
1862
  demo.launch()
1863
+