irhamni commited on
Commit
e678383
Β·
verified Β·
1 Parent(s): b7236ce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -93
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 UTAMA (sesuai instruksi Anda):
5
- - LLM TIDAK lagi menulis narasi 3 paragraf.
6
- - LLM sekarang mengisi kolom "Interpretasi" dan "Rekomendasi" untuk tabel:
7
- (Kepatuhan, Koleksi, Tenaga, Kinerja, Pelayanan, Penyelenggaraan/Pengelolaan, Nilai IPLM)
8
- - Output tabel tersebut dibuat dalam format MS Word (.docx) dan bisa diunduh dari aplikasi.
9
- - Nilai (kolom "Nilai") diambil APA ADANYA dari hasil perhitungan aplikasi (bukan dari LLM).
10
-
11
- Catatan:
12
- - Script ini tetap mempertahankan seluruh pipeline perhitungan Anda (Yeo-Johnson + MinMax + agregasi + penyesuaian 33.88%).
13
- - Saya hanya "mengganti fungsi LLM + Word report" agar menghasilkan tabel interpretasi & rekomendasi seperti contoh.
 
 
 
 
 
 
 
 
 
 
 
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, Inches
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 _to_2dec(x):
1104
  try:
1105
- if x is None or (isinstance(x, float) and math.isnan(x)):
1106
- return 0.0
 
 
1107
  return float(x)
1108
  except Exception:
1109
- return 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1110
 
1111
  def build_interpretasi_table_values(agg_total, wilayah_label, target_ratio):
1112
  """
1113
- Mengambil NILAI apa adanya dari hasil aplikasi (agg_total):
1114
- - Kepatuhan = 100 * Rata2_dim_kepatuhan
1115
- - Koleksi = 100 * Rata2_sub_koleksi
1116
- - Tenaga = 100 * Rata2_sub_sdm
1117
- - Kinerja = 100 * Rata2_dim_kinerja
1118
- - Pelayanan = 100 * Rata2_sub_pelayanan
1119
- - Penyelenggaraan/Pengelolaan = 100 * Rata2_sub_pengelolaan
1120
  - Nilai IPLM = Indeks_Final_Wilayah_0_100
1121
 
1122
- Jika agg_total punya lebih dari 1 baris (mis. Nasional),
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
- for c in ["Rata2_dim_kepatuhan","Rata2_sub_koleksi","Rata2_sub_sdm","Rata2_dim_kinerja","Rata2_sub_pelayanan","Rata2_sub_pengelolaan","Indeks_Final_Wilayah_0_100"]:
 
 
 
 
 
 
 
 
 
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": 100.0 * float(a["Rata2_dim_kepatuhan"].mean()),
1141
- "koleksi": 100.0 * float(a["Rata2_sub_koleksi"].mean()),
1142
- "tenaga": 100.0 * float(a["Rata2_sub_sdm"].mean()),
1143
- "kinerja": 100.0 * float(a["Rata2_dim_kinerja"].mean()),
1144
- "pelayanan": 100.0 * float(a["Rata2_sub_pelayanan"].mean()),
1145
- "pengelolaan": 100.0 * float(a["Rata2_sub_pengelolaan"].mean()),
1146
  "iplm": float(a["Indeks_Final_Wilayah_0_100"].mean()),
1147
  }
1148
 
1149
- # pembulatan display (tetap "nilai aplikasi", hanya format tampilan)
1150
- for k in base:
1151
- base[k] = round(_to_2dec(base[k]), 2)
 
 
 
 
 
 
 
 
1152
 
1153
  rows = [
1154
- {"No":"1", "Dimensi":"Kepatuhan", "Nilai":base["kepatuhan"]},
1155
- {"No":"1.1", "Dimensi":"Variabel Koleksi", "Nilai":base["koleksi"]},
1156
- {"No":"1.2", "Dimensi":"Variabel Tenaga Perpustakaan", "Nilai":base["tenaga"]},
1157
- {"No":"2", "Dimensi":"Kinerja", "Nilai":base["kinerja"]},
1158
- {"No":"2.1", "Dimensi":"Variabel Pelayanan", "Nilai":base["pelayanan"]},
1159
- {"No":"2.2", "Dimensi":"Variabel Penyelenggaraan/Pengelolaan", "Nilai":base["pengelolaan"]},
1160
- {"No":"4", "Dimensi":"Nilai IPLM", "Nilai":base["iplm"]},
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
- dengan gaya netral-deskriptif (tanpa label tinggi/rendah/baik/buruk).
1173
- Output wajib JSON agar mudah diparse.
 
 
 
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({**r, "Interpretasi":"", "Rekomendasi":""})
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 tiap baris tabel.\n"
1193
- "Gaya wajib: netral dan deskriptif; dilarang menggunakan label normatif seperti baik/buruk, tinggi/sedang/rendah, maju/tertinggal.\n"
1194
- "Gunakan kalimat yang menjelaskan makna angka sebagai ringkasan kondisi berdasarkan data yang dilaporkan, tanpa menghakimi.\n"
1195
- "Rekomendasi: operasional, spesifik, dan dapat ditindaklanjuti (2-3 butir ringkas) tanpa menyebut kategori penilaian.\n"
1196
- "Dilarang mengubah NILAI. NILAI hanya dipakai sebagai konteks.\n"
1197
- "Output harus JSON valid, tanpa teks tambahan."
 
 
 
 
 
 
 
 
1198
  )
1199
 
1200
  user = (
1201
- "Berikut input data tabel (JSON). Kembalikan JSON dengan struktur:\n"
1202
  "{\n"
1203
  ' "rows": [\n'
1204
- ' {"No":"...","Dimensi":"...","Nilai":12.34,"Interpretasi":"...","Rekomendasi":"..."}\n'
1205
  " ]\n"
1206
  "}\n"
1207
- "Pastikan jumlah baris sama dan urutan sama.\n\n"
 
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=900,
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
- return rows_out, "LLM mengisi Interpretasi & Rekomendasi."
 
 
 
 
 
 
 
 
 
1231
  except Exception as e:
1232
  out = []
1233
  for r in rows:
1234
- out.append({**r, "Interpretasi":"", "Rekomendasi":""})
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('w:tblPr')
1268
  tbl.append(tblPr)
1269
- tblBorders = OxmlElement('w:tblBorders')
1270
  for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
1271
- elem = OxmlElement(f'w:{edge}')
1272
- elem.set(qn('w:val'), 'single')
1273
- elem.set(qn('w:sz'), '8')
1274
- elem.set(qn('w:space'), '0')
1275
- elem.set(qn('w:color'), 'FFFFFF')
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 r in p.runs:
1306
- r.bold = True
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
- # nilai (apa adanya dari aplikasi, hanya format 2 desimal)
 
 
 
1313
  try:
1314
- row_cells[2].text = f"{float(r.get('Nilai',0.0)):.2f}"
 
 
 
 
 
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
- # ====== NEW: Word tabel interpretasi & rekomendasi ======
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
- rows_filled, llm_status = llm_fill_interpretasi_rekomendasi(header, rows, wilayah_txt, kew_value or "(Semua)")
1448
- word_path = generate_word_table_interpretasi(header, rows_filled, wilayah_txt)
 
 
 
 
 
 
 
 
 
 
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
- - LLM mengisi tabel "Interpretasi & Rekomendasi IPLM" dalam Word (.docx) yang bisa diunduh.
1528
- - Nilai tetap dari aplikasi.
 
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()