Tambah Analisis Proses Asesmen, Kelompok Capaian, riwayat nilai terstruktur, dan perbaikan bug CPL
- Portofolio: section "Analisis Proses Asesmen" (dropdown tertutup default) — distribusi huruf mutu, kelompok capaian (mean ± SD), Sub-CPMK terlemah/terkuat, kesenjangan rencana vs realisasi, mahasiswa berisiko, tren lintas tahun ajaran - Portofolio: Nomor Dokumen & Tanggal Terbit terisi otomatis; placeholder contoh di Realisasi Pembelajaran dan Kendala & Tindak Lanjut; tombol "Isi Otomatis" merangkai draf Simpulan dari analisis + realisasi + kendala - Nilai: tabel Kelompok Capaian (Atas/Tengah/Bawah, shading warna) dipindah ke dropdown "Detail Tambahan" bersama Skala Poin/Rincian Bobot/Hasil Ketercapaian; tabel utama tak lagi ada scroll vertikal internal - Nilai: riwayat pengisian dicatat terstruktur (waktu, dosen, aksi, komponen), tanpa batas 8 entri, dengan ringkasan per komponen + dropdown Log Lengkap — mendukung akuntabilitas team teaching - Fix bug data: seed kurikulum menyimpan 'cpl' CPMK sebagai string tunggal, bukan list, sehingga pecah per huruf saat di-join() di tabel Hasil Ketercapaian/Rekap (mis. "CPL-03" -> "C, P, L, -, 0, 3"); dinormalisasi di db.py (runtime) dan tools/extract_seed.py (agar re-seed berikutnya benar) - Hover tooltip deskripsi lengkap di kode Sub-CPMK/CPMK/CPL (halaman Nilai dan Rekap CPL/CPMK) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+140
@@ -205,6 +205,146 @@ def rollup_mk(peta: dict, nilai_data: dict, ambang: float) -> dict:
|
||||
"mode": nilai_data.get("mode", "agregat")}
|
||||
|
||||
|
||||
def sub_terurut(peta: dict, rollup_sub: dict) -> list[dict]:
|
||||
"""Sub-CPMK ternilai, terurut naik dari yang terlemah — bahan analisis
|
||||
proses asesmen di Portofolio (mana yang paling perlu perhatian)."""
|
||||
out = []
|
||||
for s in peta["subs"]:
|
||||
info = rollup_sub.get(s["kode"])
|
||||
if info and info["rata"] is not None:
|
||||
out.append({"kode": s["kode"], "uraian": s["uraian"],
|
||||
"rata": info["rata"], "lulus_pct": info["lulus_pct"]})
|
||||
return sorted(out, key=lambda s: s["rata"])
|
||||
|
||||
|
||||
def kesenjangan_rencana_realisasi(bobot_sub: dict[str, float],
|
||||
rollup_sub: dict) -> list[str]:
|
||||
"""Sub-CPMK yang diberi bobot penilaian di RPS tapi belum ada nilai
|
||||
masuk sama sekali — kesenjangan antara rencana dan realisasi asesmen."""
|
||||
return sorted(
|
||||
s for s in bobot_sub
|
||||
if bobot_sub[s] and (s not in rollup_sub or rollup_sub[s]["rata"] is None)
|
||||
)
|
||||
|
||||
|
||||
def mahasiswa_berisiko(nilai_data: dict, akhir: list[dict], ambang: float,
|
||||
min_sub_dibawah: int = 2) -> list[dict]:
|
||||
"""Mahasiswa dengan >= min_sub_dibawah Sub-CPMK di bawah ambang, atau
|
||||
nilai akhir tertimbangnya sendiri di bawah ambang (early-warning list)."""
|
||||
if nilai_data.get("mode") != "per_mahasiswa":
|
||||
return []
|
||||
akhir_by_nim = {a["nim"]: a["akhir"] for a in akhir}
|
||||
out = []
|
||||
for m in nilai_data.get("mahasiswa", []):
|
||||
skor = {s: v for s, v in (m.get("skor") or {}).items() if v is not None}
|
||||
if not skor:
|
||||
continue
|
||||
n_dibawah = sum(1 for v in skor.values() if v < ambang)
|
||||
nim = m.get("nim", "")
|
||||
nilai_akhir = akhir_by_nim.get(nim)
|
||||
if n_dibawah >= min_sub_dibawah or (nilai_akhir is not None and nilai_akhir < ambang):
|
||||
out.append({"nim": nim, "nama": m.get("nama", ""),
|
||||
"n_dibawah": n_dibawah, "total_sub": len(skor),
|
||||
"akhir": nilai_akhir})
|
||||
return sorted(out, key=lambda m: (-m["n_dibawah"], m["akhir"] if m["akhir"] is not None else 999))
|
||||
|
||||
|
||||
def tren_mk(kode: str, daftar_tahun: list[str], ambang: float) -> list[dict]:
|
||||
"""Rata-rata nilai akhir & % kelulusan MK ini per tahun ajaran yang
|
||||
berdata — untuk melihat apakah capaian membaik atau tidak antar tahun."""
|
||||
out = []
|
||||
for t in daftar_tahun:
|
||||
nilai_t = db.ambil_dok("nilai", kode, t)
|
||||
if not nilai_t:
|
||||
continue
|
||||
nilai_data_t = nilai_t["data"]
|
||||
rps_t = db.ambil_rps(kode, t)
|
||||
bobot_t = bobot_sub_dari_rps(rps_t["data"] if rps_t else None)
|
||||
if nilai_data_t.get("mode") == "per_mahasiswa":
|
||||
akhir_t = nilai_akhir_mahasiswa(nilai_data_t, bobot_t)
|
||||
if akhir_t:
|
||||
xs = [a["akhir"] for a in akhir_t]
|
||||
out.append({"tahun": t, "mean": _rata(xs),
|
||||
"lulus_pct": 100.0 * sum(1 for x in xs if x >= ambang) / len(xs),
|
||||
"n": len(xs)})
|
||||
continue
|
||||
sub_t = skor_sub(nilai_data_t, ambang)
|
||||
nilai_ada = [v["rata"] for v in sub_t.values() if v["rata"] is not None]
|
||||
if nilai_ada:
|
||||
out.append({"tahun": t, "mean": _rata(nilai_ada), "lulus_pct": None, "n": None})
|
||||
return out
|
||||
|
||||
|
||||
def kelompok_capaian(akhir: list[dict], statistik: dict | None) -> dict | None:
|
||||
"""Kelompokkan mahasiswa jadi Atas/Tengah/Bawah berdasarkan mean ± 1 SD
|
||||
(acuan norma, lazim dipakai dalam evaluasi pendidikan) — pelengkap
|
||||
'mahasiswa berisiko' yang acuannya ambang kelulusan tetap, bukan sebaran
|
||||
kelas ini."""
|
||||
if not statistik or not akhir:
|
||||
return None
|
||||
batas_bawah = statistik["mean"] - statistik["stdev"]
|
||||
batas_atas = statistik["mean"] + statistik["stdev"]
|
||||
atas, tengah, bawah = [], [], []
|
||||
for a in akhir:
|
||||
if a["akhir"] > batas_atas:
|
||||
atas.append(a)
|
||||
elif a["akhir"] < batas_bawah:
|
||||
bawah.append(a)
|
||||
else:
|
||||
tengah.append(a)
|
||||
return {
|
||||
"batas_bawah": batas_bawah, "batas_atas": batas_atas,
|
||||
"atas": sorted(atas, key=lambda a: -a["akhir"]),
|
||||
"tengah": sorted(tengah, key=lambda a: -a["akhir"]),
|
||||
"bawah": sorted(bawah, key=lambda a: -a["akhir"]),
|
||||
}
|
||||
|
||||
|
||||
def saran_simpulan(akhir: list[dict], statistik: dict | None, sub_urut: list[dict],
|
||||
kesenjangan: list[str], berisiko: list[dict],
|
||||
tren: list[dict], ambang: float) -> str:
|
||||
"""Rangkai draf narasi Simpulan Hasil Evaluasi dari hasil Analisis Proses
|
||||
Asesmen — dipakai sebagai placeholder awal, dosen tetap yang menulis versi
|
||||
finalnya (termasuk simpulan ketercapaian CPL/CPMK dan tindak lanjut)."""
|
||||
bagian = []
|
||||
if statistik:
|
||||
lulus_pct = 100.0 * sum(1 for a in akhir if a["akhir"] >= ambang) / len(akhir)
|
||||
bagian.append(
|
||||
f"Rata-rata nilai akhir {statistik['mean']:.1f} (median {statistik['median']:.1f}) "
|
||||
f"dari {statistik['n']} mahasiswa; {lulus_pct:.0f}% mencapai ambang {ambang:.0f}."
|
||||
)
|
||||
if sub_urut:
|
||||
terlemah, terkuat = sub_urut[0], sub_urut[-1]
|
||||
if terlemah["kode"] != terkuat["kode"]:
|
||||
bagian.append(
|
||||
f"Capaian terendah pada {terlemah['kode']} (skor {terlemah['rata']:.1f}), "
|
||||
f"tertinggi pada {terkuat['kode']} (skor {terkuat['rata']:.1f})."
|
||||
)
|
||||
else:
|
||||
bagian.append(f"Satu-satunya Sub-CPMK ternilai: {terlemah['kode']} (skor {terlemah['rata']:.1f}).")
|
||||
if kesenjangan:
|
||||
bagian.append(
|
||||
f"{len(kesenjangan)} Sub-CPMK direncanakan di RPS namun belum ternilai: "
|
||||
f"{', '.join(kesenjangan)}."
|
||||
)
|
||||
if berisiko:
|
||||
bagian.append(f"{len(berisiko)} mahasiswa teridentifikasi berisiko (nilai rendah di beberapa Sub-CPMK).")
|
||||
if len(tren) > 1:
|
||||
sekarang, sebelum = tren[-1], tren[-2]
|
||||
if sekarang["mean"] is not None and sebelum["mean"] is not None:
|
||||
selisih = sekarang["mean"] - sebelum["mean"]
|
||||
arah = "naik" if selisih > 0 else ("turun" if selisih < 0 else "stabil")
|
||||
bagian.append(
|
||||
f"Dibanding {sebelum['tahun']} ({sebelum['mean']:.1f}), rata-rata {arah} "
|
||||
f"{abs(selisih):.1f} poin di {sekarang['tahun']}."
|
||||
)
|
||||
bagian.append(
|
||||
"Lengkapi dengan simpulan ketercapaian CPL/CPMK terkait dan rencana tindak lanjut "
|
||||
"untuk siklus berikutnya."
|
||||
)
|
||||
return " ".join(bagian)
|
||||
|
||||
|
||||
def rollup_prodi(tahun: str | None = None) -> dict:
|
||||
"""Ketercapaian tingkat prodi + kelengkapan bukti per CPL/CPMK."""
|
||||
tahun = tahun or db.tahun_aktif()
|
||||
|
||||
Reference in New Issue
Block a user