Update app.py
Browse files
app.py
CHANGED
|
@@ -1,16 +1,27 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
IPLM 2025 β Final (Target Sampel 33.88% per Jenis) β TANPA Kinerja Relatif / Percentile
|
| 4 |
-
UPDATE
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
import os
|
|
@@ -31,7 +42,7 @@ from sklearn.preprocessing import PowerTransformer
|
|
| 31 |
DOCX_AVAILABLE = True
|
| 32 |
try:
|
| 33 |
from docx import Document
|
| 34 |
-
from docx.shared import Pt
|
| 35 |
from docx.oxml import OxmlElement
|
| 36 |
from docx.oxml.ns import qn
|
| 37 |
except Exception:
|
|
@@ -547,8 +558,8 @@ def build_faktor_wilayah_jenis(df_filtered, pop_kab, pop_prov, pop_khusus, kew_v
|
|
| 547 |
|
| 548 |
if not base_pop.empty:
|
| 549 |
if mode == "KAB":
|
| 550 |
-
pop_sekolah = pd.to_numeric(base_pop.get("jumlah_populasi_sekolah", 0), errors="coerce").fillna(0.0)
|
| 551 |
-
pop_umum = pd.to_numeric(base_pop.get("jumlah_populasi_umum", 0), errors="coerce").fillna(0.0)
|
| 552 |
tgt_sekolah = pop_sekolah * float(TARGET_RATIO)
|
| 553 |
tgt_umum = pop_umum * float(TARGET_RATIO)
|
| 554 |
else:
|
|
@@ -1100,27 +1111,53 @@ def get_llm_client():
|
|
| 1100 |
_HF_CLIENT = None
|
| 1101 |
return None
|
| 1102 |
|
| 1103 |
-
def
|
| 1104 |
try:
|
| 1105 |
-
if x is None
|
| 1106 |
-
return
|
|
|
|
|
|
|
| 1107 |
return float(x)
|
| 1108 |
except Exception:
|
| 1109 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1110 |
|
| 1111 |
def build_interpretasi_table_values(agg_total, wilayah_label, target_ratio):
|
| 1112 |
"""
|
| 1113 |
-
|
| 1114 |
-
- Kepatuhan =
|
| 1115 |
-
- Koleksi
|
| 1116 |
-
- Tenaga
|
| 1117 |
-
- Kinerja
|
| 1118 |
-
- Pelayanan =
|
| 1119 |
-
-
|
| 1120 |
- Nilai IPLM = Indeks_Final_Wilayah_0_100
|
| 1121 |
|
| 1122 |
-
Jika agg_total
|
| 1123 |
-
diambil rata-rata kolom-kolom tersebut.
|
| 1124 |
"""
|
| 1125 |
if agg_total is None or agg_total.empty:
|
| 1126 |
base = {
|
|
@@ -1130,34 +1167,51 @@ def build_interpretasi_table_values(agg_total, wilayah_label, target_ratio):
|
|
| 1130 |
}
|
| 1131 |
else:
|
| 1132 |
a = agg_total.copy()
|
| 1133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1134 |
if c in a.columns:
|
| 1135 |
a[c] = pd.to_numeric(a[c], errors="coerce").fillna(0.0)
|
| 1136 |
else:
|
| 1137 |
a[c] = 0.0
|
| 1138 |
|
| 1139 |
base = {
|
| 1140 |
-
"kepatuhan":
|
| 1141 |
-
"koleksi":
|
| 1142 |
-
"tenaga":
|
| 1143 |
-
"kinerja":
|
| 1144 |
-
"pelayanan":
|
| 1145 |
-
"pengelolaan":
|
| 1146 |
"iplm": float(a["Indeks_Final_Wilayah_0_100"].mean()),
|
| 1147 |
}
|
| 1148 |
|
| 1149 |
-
# pembulatan display (tetap "
|
| 1150 |
-
|
| 1151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1152 |
|
| 1153 |
rows = [
|
| 1154 |
-
{"No":"1", "Dimensi":"Kepatuhan", "Nilai":
|
| 1155 |
-
{"No":"1.1", "Dimensi":"Variabel Koleksi", "Nilai":
|
| 1156 |
-
{"No":"1.2", "Dimensi":"Variabel Tenaga Perpustakaan", "Nilai":
|
| 1157 |
-
{"No":"2", "Dimensi":"Kinerja", "Nilai":
|
| 1158 |
-
{"No":"2.1", "Dimensi":"Variabel Pelayanan", "Nilai":
|
| 1159 |
-
{"No":"2.2", "Dimensi":"Variabel Penyelenggaraan/Pengelolaan", "Nilai":
|
| 1160 |
-
{"No":"4", "Dimensi":"Nilai IPLM", "Nilai":
|
| 1161 |
]
|
| 1162 |
|
| 1163 |
header = {
|
|
@@ -1166,45 +1220,62 @@ def build_interpretasi_table_values(agg_total, wilayah_label, target_ratio):
|
|
| 1166 |
}
|
| 1167 |
return header, rows
|
| 1168 |
|
| 1169 |
-
def llm_fill_interpretasi_rekomendasi(header, rows, wilayah_label, kew_label):
|
| 1170 |
"""
|
| 1171 |
-
LLM diminta mengisi kolom Interpretasi dan Rekomendasi
|
| 1172 |
-
|
| 1173 |
-
|
|
|
|
|
|
|
|
|
|
| 1174 |
"""
|
| 1175 |
client = get_llm_client()
|
| 1176 |
if client is None or (not USE_LLM):
|
| 1177 |
-
# fallback kosong
|
| 1178 |
out = []
|
| 1179 |
for r in rows:
|
| 1180 |
-
out.append({
|
| 1181 |
return out, "LLM tidak digunakan / tidak tersedia."
|
| 1182 |
|
| 1183 |
payload = {
|
| 1184 |
"wilayah": wilayah_label,
|
| 1185 |
"kewenangan": kew_label,
|
| 1186 |
"target_sampel_per_jenis": header["target_sampel"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
"baris": rows
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
system = (
|
| 1191 |
"Anda adalah analis kebijakan perpustakaan di Indonesia.\n"
|
| 1192 |
-
"Tugas: isi kolom Interpretasi dan Rekomendasi untuk
|
| 1193 |
-
"
|
| 1194 |
-
"
|
| 1195 |
-
"
|
| 1196 |
-
"
|
| 1197 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
)
|
| 1199 |
|
| 1200 |
user = (
|
| 1201 |
-
"
|
| 1202 |
"{\n"
|
| 1203 |
' "rows": [\n'
|
| 1204 |
-
' {"No":"...","Dimensi":"...","Nilai":
|
| 1205 |
" ]\n"
|
| 1206 |
"}\n"
|
| 1207 |
-
"
|
|
|
|
| 1208 |
f"INPUT:\n{json.dumps(payload, ensure_ascii=False)}"
|
| 1209 |
)
|
| 1210 |
|
|
@@ -1215,30 +1286,33 @@ def llm_fill_interpretasi_rekomendasi(header, rows, wilayah_label, kew_label):
|
|
| 1215 |
{"role": "system", "content": system},
|
| 1216 |
{"role": "user", "content": user},
|
| 1217 |
],
|
| 1218 |
-
max_tokens=
|
| 1219 |
temperature=0.2,
|
| 1220 |
top_p=0.9,
|
| 1221 |
)
|
| 1222 |
text = resp.choices[0].message.content.strip()
|
| 1223 |
-
|
| 1224 |
-
# parse JSON
|
| 1225 |
data = json.loads(text)
|
| 1226 |
rows_out = data.get("rows", [])
|
| 1227 |
-
# fallback jika tidak sesuai
|
| 1228 |
if not isinstance(rows_out, list) or len(rows_out) != len(rows):
|
| 1229 |
raise ValueError("Format JSON rows tidak sesuai.")
|
| 1230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
except Exception as e:
|
| 1232 |
out = []
|
| 1233 |
for r in rows:
|
| 1234 |
-
out.append({
|
| 1235 |
return out, f"LLM error: {repr(e)}"
|
| 1236 |
|
| 1237 |
|
| 1238 |
def _set_cell_shading(cell, fill_hex="1F1F1F"):
|
| 1239 |
-
"""
|
| 1240 |
-
Set shading background untuk cell (python-docx).
|
| 1241 |
-
"""
|
| 1242 |
tcPr = cell._tc.get_or_add_tcPr()
|
| 1243 |
shd = OxmlElement("w:shd")
|
| 1244 |
shd.set(qn("w:val"), "clear")
|
|
@@ -1247,9 +1321,6 @@ def _set_cell_shading(cell, fill_hex="1F1F1F"):
|
|
| 1247 |
tcPr.append(shd)
|
| 1248 |
|
| 1249 |
def _set_cell_text_color(cell, rgb_hex="FFFFFF"):
|
| 1250 |
-
"""
|
| 1251 |
-
Set font color untuk semua run dalam cell.
|
| 1252 |
-
"""
|
| 1253 |
for p in cell.paragraphs:
|
| 1254 |
for run in p.runs:
|
| 1255 |
rPr = run._r.get_or_add_rPr()
|
|
@@ -1258,25 +1329,22 @@ def _set_cell_text_color(cell, rgb_hex="FFFFFF"):
|
|
| 1258 |
rPr.append(color)
|
| 1259 |
|
| 1260 |
def _set_table_borders(table):
|
| 1261 |
-
"""
|
| 1262 |
-
Tambah border sederhana.
|
| 1263 |
-
"""
|
| 1264 |
tbl = table._tbl
|
| 1265 |
tblPr = tbl.tblPr
|
| 1266 |
if tblPr is None:
|
| 1267 |
-
tblPr = OxmlElement(
|
| 1268 |
tbl.append(tblPr)
|
| 1269 |
-
tblBorders = OxmlElement(
|
| 1270 |
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
| 1271 |
-
elem = OxmlElement(f
|
| 1272 |
-
elem.set(qn(
|
| 1273 |
-
elem.set(qn(
|
| 1274 |
-
elem.set(qn(
|
| 1275 |
-
elem.set(qn(
|
| 1276 |
tblBorders.append(elem)
|
| 1277 |
tblPr.append(tblBorders)
|
| 1278 |
|
| 1279 |
-
def generate_word_table_interpretasi(header, rows_filled, wilayah_label):
|
| 1280 |
if (not DOCX_AVAILABLE) or (Document is None):
|
| 1281 |
return None
|
| 1282 |
|
|
@@ -1302,27 +1370,45 @@ def generate_word_table_interpretasi(header, rows_filled, wilayah_label):
|
|
| 1302 |
_set_cell_shading(hdr_cells[i], "1A1A1A")
|
| 1303 |
_set_cell_text_color(hdr_cells[i], "FFFFFF")
|
| 1304 |
for p in hdr_cells[i].paragraphs:
|
| 1305 |
-
for
|
| 1306 |
-
|
| 1307 |
|
| 1308 |
for r in rows_filled:
|
| 1309 |
row_cells = table.add_row().cells
|
| 1310 |
row_cells[0].text = str(r.get("No",""))
|
| 1311 |
row_cells[1].text = str(r.get("Dimensi",""))
|
| 1312 |
-
|
|
|
|
|
|
|
|
|
|
| 1313 |
try:
|
| 1314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1315 |
except Exception:
|
| 1316 |
row_cells[2].text = str(r.get("Nilai",""))
|
|
|
|
| 1317 |
row_cells[3].text = str(r.get("Interpretasi","") or "")
|
| 1318 |
row_cells[4].text = str(r.get("Rekomendasi","") or "")
|
| 1319 |
|
| 1320 |
-
# shading body (gelap) + teks putih agar mirip contoh
|
| 1321 |
for c in row_cells:
|
| 1322 |
_set_cell_shading(c, "262626")
|
| 1323 |
_set_cell_text_color(c, "FFFFFF")
|
| 1324 |
|
|
|
|
| 1325 |
doc.add_paragraph("") # spacer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1326 |
|
| 1327 |
outpath = tempfile.mktemp(suffix=".docx")
|
| 1328 |
doc.save(outpath)
|
|
@@ -1441,11 +1527,21 @@ def run_calc(prov_value, kab_value, kew_value, df_all, df_raw, pop_kab, pop_prov
|
|
| 1441 |
detail_view.to_excel(p_detail, index=False)
|
| 1442 |
verif_total.to_excel(p_verif, index=False)
|
| 1443 |
|
| 1444 |
-
# ======
|
| 1445 |
wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
|
| 1446 |
header, rows = build_interpretasi_table_values(agg_total, wilayah_txt, TARGET_RATIO)
|
| 1447 |
-
|
| 1448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1449 |
|
| 1450 |
msg = (
|
| 1451 |
f"Selesai (TARGET {TARGET_RATIO*100:.2f}%): raw={len(raw)} | entitas={len(detail_view)} | "
|
|
@@ -1523,9 +1619,10 @@ Dashboard KPI:
|
|
| 1523 |
- Indeks IPLM FINAL (disesuaikan 33.88%)
|
| 1524 |
- Indeks Dasar (tanpa penyesuaian)
|
| 1525 |
|
| 1526 |
-
UPDATE LLM:
|
| 1527 |
-
-
|
| 1528 |
-
- Nilai
|
|
|
|
| 1529 |
""")
|
| 1530 |
|
| 1531 |
state_df = gr.State(None)
|
|
@@ -1605,4 +1702,4 @@ UPDATE LLM:
|
|
| 1605 |
outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
|
| 1606 |
)
|
| 1607 |
|
| 1608 |
-
demo.launch()
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
IPLM 2025 β Final (Target Sampel 33.88% per Jenis) β TANPA Kinerja Relatif / Percentile
|
| 4 |
+
UPDATE (sesuai instruksi terbaru Anda) β TANPA mengubah pipeline lain:
|
| 5 |
+
|
| 6 |
+
FOKUS PEMBENAHAN (LLM + WORD):
|
| 7 |
+
1) Nilai Kepatuhan, Koleksi, Tenaga, Kinerja, Pelayanan, Pengelolaan:
|
| 8 |
+
- TIDAK dikalikan 100.
|
| 9 |
+
- Ditulis APA ADANYA dari kolom agregat aplikasi:
|
| 10 |
+
Rata2_dim_kepatuhan, Rata2_sub_koleksi, Rata2_sub_sdm, Rata2_dim_kinerja,
|
| 11 |
+
Rata2_sub_pelayanan, Rata2_sub_pengelolaan.
|
| 12 |
+
2) Nilai IPLM ditulis apa adanya: Indeks_Final_Wilayah_0_100.
|
| 13 |
+
3) LLM mengisi Interpretasi & Rekomendasi:
|
| 14 |
+
- Interpretasi: deskriptif, kondisi riil berbasis relasi angka (lebih besar/kecil, gap, dominan, konsistensi),
|
| 15 |
+
plus pemaknaan substantif dimensi (koleksi/sdm/pelayanan/pengelolaan) TANPA label normatif.
|
| 16 |
+
- Rekomendasi: operasional, 2β3 butir ringkas, menaut ke pola angka (gap/ketimpangan/kontribusi).
|
| 17 |
+
4) Di bawah tabel Word: tambah deskripsi jumlah perpustakaan sumber data (dari tabel agregat wilayah Γ jenis / βgambar 2β):
|
| 18 |
+
sekolah=..., umum=..., khusus=..., total=...
|
| 19 |
+
|
| 20 |
+
Catatan penting:
|
| 21 |
+
- Semua perhitungan dan dashboard tetap.
|
| 22 |
+
- Yang diubah hanya: (a) cara mengambil nilai untuk tabel Word (tanpa *100),
|
| 23 |
+
(b) prompt LLM untuk isi interpretasi/rekomendasi agar nyambung dengan angka,
|
| 24 |
+
(c) tambahan paragraf jumlah perpustakaan di bawah tabel Word.
|
| 25 |
"""
|
| 26 |
|
| 27 |
import os
|
|
|
|
| 42 |
DOCX_AVAILABLE = True
|
| 43 |
try:
|
| 44 |
from docx import Document
|
| 45 |
+
from docx.shared import Pt
|
| 46 |
from docx.oxml import OxmlElement
|
| 47 |
from docx.oxml.ns import qn
|
| 48 |
except Exception:
|
|
|
|
| 558 |
|
| 559 |
if not base_pop.empty:
|
| 560 |
if mode == "KAB":
|
| 561 |
+
pop_sekolah = pd.to_numeric(base_pop.get("jumlah_populasi_sekolah_base", base_pop.get("jumlah_populasi_sekolah", 0)), errors="coerce").fillna(0.0)
|
| 562 |
+
pop_umum = pd.to_numeric(base_pop.get("jumlah_populasi_umum_base", base_pop.get("jumlah_populasi_umum", 0)), errors="coerce").fillna(0.0)
|
| 563 |
tgt_sekolah = pop_sekolah * float(TARGET_RATIO)
|
| 564 |
tgt_umum = pop_umum * float(TARGET_RATIO)
|
| 565 |
else:
|
|
|
|
| 1111 |
_HF_CLIENT = None
|
| 1112 |
return None
|
| 1113 |
|
| 1114 |
+
def _to_float(x, default=0.0):
|
| 1115 |
try:
|
| 1116 |
+
if x is None:
|
| 1117 |
+
return float(default)
|
| 1118 |
+
if isinstance(x, float) and math.isnan(x):
|
| 1119 |
+
return float(default)
|
| 1120 |
return float(x)
|
| 1121 |
except Exception:
|
| 1122 |
+
return float(default)
|
| 1123 |
+
|
| 1124 |
+
def summarize_jumlah_perpus_dari_agg_jenis(agg_jenis_full, wilayah_label, kew_value):
|
| 1125 |
+
"""
|
| 1126 |
+
Ambil jumlah perpustakaan sumber data dari tabel agregat wilayah Γ jenis (gambar 2).
|
| 1127 |
+
Untuk filter 1 wilayah (kab/prov), agg_jenis_full biasanya 3 baris (sekolah/umum/khusus).
|
| 1128 |
+
Untuk nasional/semua wilayah, ini akan menjumlahkan seluruh wilayah per jenis.
|
| 1129 |
+
"""
|
| 1130 |
+
if agg_jenis_full is None or agg_jenis_full.empty:
|
| 1131 |
+
return {"sekolah": 0, "umum": 0, "khusus": 0, "total": 0}
|
| 1132 |
+
|
| 1133 |
+
a = agg_jenis_full.copy()
|
| 1134 |
+
if "Jenis" not in a.columns:
|
| 1135 |
+
return {"sekolah": 0, "umum": 0, "khusus": 0, "total": 0}
|
| 1136 |
+
|
| 1137 |
+
a["Jenis"] = a["Jenis"].astype(str).str.lower().str.strip()
|
| 1138 |
+
if "Jumlah" in a.columns:
|
| 1139 |
+
a["Jumlah"] = pd.to_numeric(a["Jumlah"], errors="coerce").fillna(0).astype(int)
|
| 1140 |
+
else:
|
| 1141 |
+
a["Jumlah"] = 0
|
| 1142 |
+
|
| 1143 |
+
out = {}
|
| 1144 |
+
for j in ["sekolah", "umum", "khusus"]:
|
| 1145 |
+
out[j] = int(a.loc[a["Jenis"].eq(j), "Jumlah"].sum())
|
| 1146 |
+
out["total"] = int(out["sekolah"] + out["umum"] + out["khusus"])
|
| 1147 |
+
return out
|
| 1148 |
|
| 1149 |
def build_interpretasi_table_values(agg_total, wilayah_label, target_ratio):
|
| 1150 |
"""
|
| 1151 |
+
MENGAMBIL NILAI APA ADANYA (tanpa *100) dari hasil aplikasi (agg_total):
|
| 1152 |
+
- Kepatuhan = Rata2_dim_kepatuhan
|
| 1153 |
+
- Koleksi = Rata2_sub_koleksi
|
| 1154 |
+
- Tenaga = Rata2_sub_sdm
|
| 1155 |
+
- Kinerja = Rata2_dim_kinerja
|
| 1156 |
+
- Pelayanan = Rata2_sub_pelayanan
|
| 1157 |
+
- Pengelolaan = Rata2_sub_pengelolaan
|
| 1158 |
- Nilai IPLM = Indeks_Final_Wilayah_0_100
|
| 1159 |
|
| 1160 |
+
Jika agg_total > 1 baris (mis. nasional), diambil mean kolom-kolom tersebut.
|
|
|
|
| 1161 |
"""
|
| 1162 |
if agg_total is None or agg_total.empty:
|
| 1163 |
base = {
|
|
|
|
| 1167 |
}
|
| 1168 |
else:
|
| 1169 |
a = agg_total.copy()
|
| 1170 |
+
cols_needed = [
|
| 1171 |
+
"Rata2_dim_kepatuhan",
|
| 1172 |
+
"Rata2_sub_koleksi",
|
| 1173 |
+
"Rata2_sub_sdm",
|
| 1174 |
+
"Rata2_dim_kinerja",
|
| 1175 |
+
"Rata2_sub_pelayanan",
|
| 1176 |
+
"Rata2_sub_pengelolaan",
|
| 1177 |
+
"Indeks_Final_Wilayah_0_100",
|
| 1178 |
+
]
|
| 1179 |
+
for c in cols_needed:
|
| 1180 |
if c in a.columns:
|
| 1181 |
a[c] = pd.to_numeric(a[c], errors="coerce").fillna(0.0)
|
| 1182 |
else:
|
| 1183 |
a[c] = 0.0
|
| 1184 |
|
| 1185 |
base = {
|
| 1186 |
+
"kepatuhan": float(a["Rata2_dim_kepatuhan"].mean()),
|
| 1187 |
+
"koleksi": float(a["Rata2_sub_koleksi"].mean()),
|
| 1188 |
+
"tenaga": float(a["Rata2_sub_sdm"].mean()),
|
| 1189 |
+
"kinerja": float(a["Rata2_dim_kinerja"].mean()),
|
| 1190 |
+
"pelayanan": float(a["Rata2_sub_pelayanan"].mean()),
|
| 1191 |
+
"pengelolaan": float(a["Rata2_sub_pengelolaan"].mean()),
|
| 1192 |
"iplm": float(a["Indeks_Final_Wilayah_0_100"].mean()),
|
| 1193 |
}
|
| 1194 |
|
| 1195 |
+
# pembulatan display (nilai tetap "apa adanya", hanya format)
|
| 1196 |
+
# untuk sub/dim (0β1) biasanya 3 desimal; untuk IPLM (0β100) 2 desimal.
|
| 1197 |
+
base_disp = {
|
| 1198 |
+
"kepatuhan": round(_to_float(base["kepatuhan"]), 3),
|
| 1199 |
+
"koleksi": round(_to_float(base["koleksi"]), 3),
|
| 1200 |
+
"tenaga": round(_to_float(base["tenaga"]), 3),
|
| 1201 |
+
"kinerja": round(_to_float(base["kinerja"]), 3),
|
| 1202 |
+
"pelayanan": round(_to_float(base["pelayanan"]), 3),
|
| 1203 |
+
"pengelolaan": round(_to_float(base["pengelolaan"]), 3),
|
| 1204 |
+
"iplm": round(_to_float(base["iplm"]), 2),
|
| 1205 |
+
}
|
| 1206 |
|
| 1207 |
rows = [
|
| 1208 |
+
{"No":"1", "Dimensi":"Kepatuhan", "Nilai":base_disp["kepatuhan"], "SumberKolom":"Rata2_dim_kepatuhan"},
|
| 1209 |
+
{"No":"1.1", "Dimensi":"Variabel Koleksi", "Nilai":base_disp["koleksi"], "SumberKolom":"Rata2_sub_koleksi"},
|
| 1210 |
+
{"No":"1.2", "Dimensi":"Variabel Tenaga Perpustakaan", "Nilai":base_disp["tenaga"], "SumberKolom":"Rata2_sub_sdm"},
|
| 1211 |
+
{"No":"2", "Dimensi":"Kinerja", "Nilai":base_disp["kinerja"], "SumberKolom":"Rata2_dim_kinerja"},
|
| 1212 |
+
{"No":"2.1", "Dimensi":"Variabel Pelayanan", "Nilai":base_disp["pelayanan"], "SumberKolom":"Rata2_sub_pelayanan"},
|
| 1213 |
+
{"No":"2.2", "Dimensi":"Variabel Penyelenggaraan/Pengelolaan", "Nilai":base_disp["pengelolaan"], "SumberKolom":"Rata2_sub_pengelolaan"},
|
| 1214 |
+
{"No":"4", "Dimensi":"Nilai IPLM", "Nilai":base_disp["iplm"], "SumberKolom":"Indeks_Final_Wilayah_0_100"},
|
| 1215 |
]
|
| 1216 |
|
| 1217 |
header = {
|
|
|
|
| 1220 |
}
|
| 1221 |
return header, rows
|
| 1222 |
|
| 1223 |
+
def llm_fill_interpretasi_rekomendasi(header, rows, wilayah_label, kew_label, jumlah_perpus_by_jenis):
|
| 1224 |
"""
|
| 1225 |
+
LLM diminta mengisi kolom Interpretasi dan Rekomendasi dengan narasi yang NYAMBUNG ke angka:
|
| 1226 |
+
- Interpretasi: jelaskan apa arti angka untuk kondisi operasional perpustakaan (koleksi/sdm/pelayanan/pengelolaan),
|
| 1227 |
+
memakai relasi angka antardimensi (lebih besar/kecil, selisih, dominan, gap, konsistensi) TANPA label normatif.
|
| 1228 |
+
- Rekomendasi: 2β3 aksi teknis per baris yang langsung meng-address pola angka (misal dimensi lebih kecil β prioritas aktivitas),
|
| 1229 |
+
serta mengaitkan dengan volume data (jumlah perpustakaan per jenis) bila relevan.
|
| 1230 |
+
Output wajib JSON.
|
| 1231 |
"""
|
| 1232 |
client = get_llm_client()
|
| 1233 |
if client is None or (not USE_LLM):
|
|
|
|
| 1234 |
out = []
|
| 1235 |
for r in rows:
|
| 1236 |
+
out.append({k: r.get(k) for k in ["No","Dimensi","Nilai"]} | {"Interpretasi":"", "Rekomendasi":""})
|
| 1237 |
return out, "LLM tidak digunakan / tidak tersedia."
|
| 1238 |
|
| 1239 |
payload = {
|
| 1240 |
"wilayah": wilayah_label,
|
| 1241 |
"kewenangan": kew_label,
|
| 1242 |
"target_sampel_per_jenis": header["target_sampel"],
|
| 1243 |
+
"jumlah_perpustakaan_sumber_data": jumlah_perpus_by_jenis,
|
| 1244 |
+
"catatan_skala": (
|
| 1245 |
+
"Baris Kepatuhan/Koleksi/Tenaga/Kinerja/Pelayanan/Pengelolaan memakai nilai agregat 'apa adanya' "
|
| 1246 |
+
"(umumnya rentang 0β1 karena berasal dari sub/dim hasil normalisasi). "
|
| 1247 |
+
"Baris 'Nilai IPLM' memakai Indeks_Final_Wilayah_0_100 (rentang 0β100)."
|
| 1248 |
+
),
|
| 1249 |
"baris": rows
|
| 1250 |
}
|
| 1251 |
|
| 1252 |
system = (
|
| 1253 |
"Anda adalah analis kebijakan perpustakaan di Indonesia.\n"
|
| 1254 |
+
"Tugas: isi kolom Interpretasi dan Rekomendasi untuk setiap baris tabel.\n"
|
| 1255 |
+
"ATURAN WAJIB:\n"
|
| 1256 |
+
"1) Jangan mengubah nilai angka. Jangan menghitung ulang skor.\n"
|
| 1257 |
+
"2) Netral-deskriptif: dilarang memakai label normatif seperti baik/buruk, tinggi/sedang/rendah, memuaskan/kurang, optimal/tidak optimal.\n"
|
| 1258 |
+
"3) Interpretasi harus nyambung langsung ke angka dan relasinya antardimensi: gunakan istilah lebih besar/kecil, selisih, gap, dominan, konsisten/tidak konsisten, kontribusi, proporsi.\n"
|
| 1259 |
+
"4) Interpretasi juga harus menjelaskan kondisi riil berbasis dimensi:\n"
|
| 1260 |
+
" - Koleksi: pengembangan, ketersediaan, pemanfaatan koleksi (sebagai fungsi layanan),\n"
|
| 1261 |
+
" - Tenaga: kecukupan/kapasitas SDM dan pengembangan kompetensi,\n"
|
| 1262 |
+
" - Pelayanan: aktivitas layanan dan pemanfaatan layanan,\n"
|
| 1263 |
+
" - Pengelolaan: tata kelola, kebijakan, kolaborasi, dukungan anggaran layanan,\n"
|
| 1264 |
+
" - Kepatuhan = gabungan koleksi+tenaga; Kinerja = gabungan pelayanan+pengelolaan.\n"
|
| 1265 |
+
" Jelaskan tanpa menghakimi; fokus pada apa yang angka itu representasikan.\n"
|
| 1266 |
+
"5) Rekomendasi harus operasional dan spesifik (2β3 butir singkat) untuk tiap baris. Gunakan pola angka untuk menurunkan aksi.\n"
|
| 1267 |
+
"6) Output HARUS JSON valid saja (tanpa teks tambahan), dengan struktur persis.\n"
|
| 1268 |
)
|
| 1269 |
|
| 1270 |
user = (
|
| 1271 |
+
"Kembalikan JSON:\n"
|
| 1272 |
"{\n"
|
| 1273 |
' "rows": [\n'
|
| 1274 |
+
' {"No":"...","Dimensi":"...","Nilai":..., "Interpretasi":"...","Rekomendasi":"..."}\n'
|
| 1275 |
" ]\n"
|
| 1276 |
"}\n"
|
| 1277 |
+
"- Urutan dan jumlah baris harus sama.\n"
|
| 1278 |
+
"- 'Rekomendasi' boleh berupa bullet dengan tanda '-' dalam satu string.\n\n"
|
| 1279 |
f"INPUT:\n{json.dumps(payload, ensure_ascii=False)}"
|
| 1280 |
)
|
| 1281 |
|
|
|
|
| 1286 |
{"role": "system", "content": system},
|
| 1287 |
{"role": "user", "content": user},
|
| 1288 |
],
|
| 1289 |
+
max_tokens=1100,
|
| 1290 |
temperature=0.2,
|
| 1291 |
top_p=0.9,
|
| 1292 |
)
|
| 1293 |
text = resp.choices[0].message.content.strip()
|
|
|
|
|
|
|
| 1294 |
data = json.loads(text)
|
| 1295 |
rows_out = data.get("rows", [])
|
|
|
|
| 1296 |
if not isinstance(rows_out, list) or len(rows_out) != len(rows):
|
| 1297 |
raise ValueError("Format JSON rows tidak sesuai.")
|
| 1298 |
+
cleaned = []
|
| 1299 |
+
for i, r in enumerate(rows_out):
|
| 1300 |
+
cleaned.append({
|
| 1301 |
+
"No": str(r.get("No", rows[i]["No"])),
|
| 1302 |
+
"Dimensi": str(r.get("Dimensi", rows[i]["Dimensi"])),
|
| 1303 |
+
"Nilai": rows[i]["Nilai"], # paksa nilai dari aplikasi
|
| 1304 |
+
"Interpretasi": str(r.get("Interpretasi","") or ""),
|
| 1305 |
+
"Rekomendasi": str(r.get("Rekomendasi","") or ""),
|
| 1306 |
+
})
|
| 1307 |
+
return cleaned, "LLM mengisi Interpretasi & Rekomendasi."
|
| 1308 |
except Exception as e:
|
| 1309 |
out = []
|
| 1310 |
for r in rows:
|
| 1311 |
+
out.append({k: r.get(k) for k in ["No","Dimensi","Nilai"]} | {"Interpretasi":"", "Rekomendasi":""})
|
| 1312 |
return out, f"LLM error: {repr(e)}"
|
| 1313 |
|
| 1314 |
|
| 1315 |
def _set_cell_shading(cell, fill_hex="1F1F1F"):
|
|
|
|
|
|
|
|
|
|
| 1316 |
tcPr = cell._tc.get_or_add_tcPr()
|
| 1317 |
shd = OxmlElement("w:shd")
|
| 1318 |
shd.set(qn("w:val"), "clear")
|
|
|
|
| 1321 |
tcPr.append(shd)
|
| 1322 |
|
| 1323 |
def _set_cell_text_color(cell, rgb_hex="FFFFFF"):
|
|
|
|
|
|
|
|
|
|
| 1324 |
for p in cell.paragraphs:
|
| 1325 |
for run in p.runs:
|
| 1326 |
rPr = run._r.get_or_add_rPr()
|
|
|
|
| 1329 |
rPr.append(color)
|
| 1330 |
|
| 1331 |
def _set_table_borders(table):
|
|
|
|
|
|
|
|
|
|
| 1332 |
tbl = table._tbl
|
| 1333 |
tblPr = tbl.tblPr
|
| 1334 |
if tblPr is None:
|
| 1335 |
+
tblPr = OxmlElement("w:tblPr")
|
| 1336 |
tbl.append(tblPr)
|
| 1337 |
+
tblBorders = OxmlElement("w:tblBorders")
|
| 1338 |
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
| 1339 |
+
elem = OxmlElement(f"w:{edge}")
|
| 1340 |
+
elem.set(qn("w:val"), "single")
|
| 1341 |
+
elem.set(qn("w:sz"), "8")
|
| 1342 |
+
elem.set(qn("w:space"), "0")
|
| 1343 |
+
elem.set(qn("w:color"), "FFFFFF")
|
| 1344 |
tblBorders.append(elem)
|
| 1345 |
tblPr.append(tblBorders)
|
| 1346 |
|
| 1347 |
+
def generate_word_table_interpretasi(header, rows_filled, wilayah_label, jumlah_perpus_by_jenis):
|
| 1348 |
if (not DOCX_AVAILABLE) or (Document is None):
|
| 1349 |
return None
|
| 1350 |
|
|
|
|
| 1370 |
_set_cell_shading(hdr_cells[i], "1A1A1A")
|
| 1371 |
_set_cell_text_color(hdr_cells[i], "FFFFFF")
|
| 1372 |
for p in hdr_cells[i].paragraphs:
|
| 1373 |
+
for rr in p.runs:
|
| 1374 |
+
rr.bold = True
|
| 1375 |
|
| 1376 |
for r in rows_filled:
|
| 1377 |
row_cells = table.add_row().cells
|
| 1378 |
row_cells[0].text = str(r.get("No",""))
|
| 1379 |
row_cells[1].text = str(r.get("Dimensi",""))
|
| 1380 |
+
|
| 1381 |
+
# format nilai:
|
| 1382 |
+
# - sub/dim biasanya 0β1 β tampilkan 3 desimal
|
| 1383 |
+
# - IPLM 0β100 β tampilkan 2 desimal
|
| 1384 |
try:
|
| 1385 |
+
dim = str(r.get("Dimensi","")).strip().lower()
|
| 1386 |
+
val = _to_float(r.get("Nilai", 0.0), 0.0)
|
| 1387 |
+
if dim == "nilai iplm":
|
| 1388 |
+
row_cells[2].text = f"{val:.2f}"
|
| 1389 |
+
else:
|
| 1390 |
+
row_cells[2].text = f"{val:.3f}"
|
| 1391 |
except Exception:
|
| 1392 |
row_cells[2].text = str(r.get("Nilai",""))
|
| 1393 |
+
|
| 1394 |
row_cells[3].text = str(r.get("Interpretasi","") or "")
|
| 1395 |
row_cells[4].text = str(r.get("Rekomendasi","") or "")
|
| 1396 |
|
|
|
|
| 1397 |
for c in row_cells:
|
| 1398 |
_set_cell_shading(c, "262626")
|
| 1399 |
_set_cell_text_color(c, "FFFFFF")
|
| 1400 |
|
| 1401 |
+
# ===== tambahan: deskripsi jumlah perpustakaan sumber data (gambar 2) =====
|
| 1402 |
doc.add_paragraph("") # spacer
|
| 1403 |
+
j = jumlah_perpus_by_jenis or {"sekolah":0,"umum":0,"khusus":0,"total":0}
|
| 1404 |
+
p = doc.add_paragraph()
|
| 1405 |
+
p.add_run("Sumber data (jumlah perpustakaan pada tabel agregat wilayah Γ jenis): ").bold = True
|
| 1406 |
+
doc.add_paragraph(
|
| 1407 |
+
f"Perpustakaan sekolah = {int(j.get('sekolah',0))}, "
|
| 1408 |
+
f"perpustakaan umum = {int(j.get('umum',0))}, "
|
| 1409 |
+
f"perpustakaan khusus = {int(j.get('khusus',0))}, "
|
| 1410 |
+
f"total = {int(j.get('total',0))}."
|
| 1411 |
+
)
|
| 1412 |
|
| 1413 |
outpath = tempfile.mktemp(suffix=".docx")
|
| 1414 |
doc.save(outpath)
|
|
|
|
| 1527 |
detail_view.to_excel(p_detail, index=False)
|
| 1528 |
verif_total.to_excel(p_verif, index=False)
|
| 1529 |
|
| 1530 |
+
# ====== Word tabel interpretasi & rekomendasi ======
|
| 1531 |
wilayah_txt = kab_value if (kab_value and kab_value != "(Semua)") else (prov_value if (prov_value and prov_value != "(Semua)") else "Nasional/All")
|
| 1532 |
header, rows = build_interpretasi_table_values(agg_total, wilayah_txt, TARGET_RATIO)
|
| 1533 |
+
|
| 1534 |
+
# jumlah perpustakaan sumber data (gambar 2)
|
| 1535 |
+
jumlah_perpus = summarize_jumlah_perpus_dari_agg_jenis(agg_jenis_full, wilayah_txt, kew_norm)
|
| 1536 |
+
|
| 1537 |
+
rows_filled, llm_status = llm_fill_interpretasi_rekomendasi(
|
| 1538 |
+
header=header,
|
| 1539 |
+
rows=rows,
|
| 1540 |
+
wilayah_label=wilayah_txt,
|
| 1541 |
+
kew_label=(kew_value or "(Semua)"),
|
| 1542 |
+
jumlah_perpus_by_jenis=jumlah_perpus
|
| 1543 |
+
)
|
| 1544 |
+
word_path = generate_word_table_interpretasi(header, rows_filled, wilayah_txt, jumlah_perpus)
|
| 1545 |
|
| 1546 |
msg = (
|
| 1547 |
f"Selesai (TARGET {TARGET_RATIO*100:.2f}%): raw={len(raw)} | entitas={len(detail_view)} | "
|
|
|
|
| 1619 |
- Indeks IPLM FINAL (disesuaikan 33.88%)
|
| 1620 |
- Indeks Dasar (tanpa penyesuaian)
|
| 1621 |
|
| 1622 |
+
UPDATE LLM + WORD:
|
| 1623 |
+
- Tabel Word "Interpretasi & Rekomendasi" memakai NILAI APA ADANYA (tanpa dikali 100) untuk sub/dim.
|
| 1624 |
+
- Baris "Nilai IPLM" memakai Indeks_Final_Wilayah_0_100 apa adanya.
|
| 1625 |
+
- Di bawah tabel Word ditambahkan ringkasan jumlah perpustakaan sumber data (sekolah/umum/khusus/total) dari tabel agregat wilayah Γ jenis.
|
| 1626 |
""")
|
| 1627 |
|
| 1628 |
state_df = gr.State(None)
|
|
|
|
| 1702 |
outputs=[state_df, state_raw, state_pop_kab, state_pop_prov, state_pop_khusus, state_meta, info_box, dd_prov, dd_kab, dd_kew]
|
| 1703 |
)
|
| 1704 |
|
| 1705 |
+
demo.launch()
|