Detail modul ctt_scoring.py - rumus, fungsi, validasi terhadap Excel klien, dan edge cases.
Modul: CTT Scoring
Source: backend/app/services/ctt_scoring.py
Lihat juga: Variabel & Rumus, Alur CTT
Tanggung Jawab
Implementasi Classical Test Theory scoring, kompatibel 100% dengan workflow Excel klien. Mencakup:
- Perhitungan
p, Bobot, NM, NN
- Kategorisasi tingkat kesulitan
- Update statistik tryout (inkremental)
- Konversi silang CTT ↔ IRT
Rumus Inti
Setiap fungsi di module ini mengikuti rumus dari Excel klien secara eksak:
p = Σ Benar / Total Peserta
Bobot = 1 - p
NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000
NN = 500 + 100 × ((NM - Rataan) / SB)
Fungsi Publik
Fungsi Pure (matematika)
| Fungsi | Signature | Output |
|---|
calculate_ctt_p | (total_correct: int, total_participants: int) → float | p-value [0, 1] |
calculate_ctt_bobot | (p_value: float) → float | bobot [0, 1] |
calculate_ctt_nm | (total_bobot_siswa: float, total_bobot_max: float) → int | NM [0, 1000] |
calculate_ctt_nn | (nm: int, rataan: float, sb: float) → int | NN [0, 1000] (di-clip) |
categorize_difficulty | (p_value: float) → str | "mudah" / "sedang" / "sulit" |
convert_ctt_p_to_irt_b | (p_value: float) → float | b [-3, +3] |
map_theta_to_nn | (theta: float) → int | NN [0, 1000] |
Fungsi Async (database)
| Fungsi | Tujuan |
|---|
calculate_ctt_p_for_item(db, item_id) | Hitung ulang p item dari semua UserAnswer |
update_tryout_stats(db, website_id, tryout_id, nm) | Update inkremental TryoutStats (peserta baru) |
get_total_bobot_max(db, website_id, tryout_id, level) | Σ bobot semua soal (untuk normalisasi NM) |
Kategori Kesulitan
Standar CTT (dari PRD section 13.2):
| CTT p | Kategori | Level Field | Setara b IRT |
|---|
p < 0.30 | Sukar | sulit | b > 0.85 |
0.30 ≤ p ≤ 0.70 | Sedang | sedang | -0.85 ≤ b ≤ 0.85 |
p > 0.70 | Mudah | mudah | b < -0.85 |
def categorize_difficulty(p_value: float) -> str:
if p_value < 0.30:
return "sulit"
elif p_value > 0.70:
return "mudah"
else:
return "sedang"
Validasi terhadap Excel Klien
Acceptance criteria wajib (PRD FR-1.1):
- Hasil p-value harus match Excel klien sampai 4 desimal
- Bobot =
1 - p (persis)
- NM setara
SUMPRODUCT Excel
- NN di-clip ke
[0, 1000]
Validasi dilakukan dengan test yang membandingkan output fungsi Python vs hasil formula Excel pada dataset contoh.
Update Statistik Inkremental
Fungsi update_tryout_stats memperbarui tabel TryoutStats tanpa recompute penuh:
stats.participant_count += 1
stats.total_nm_sum += nm
stats.total_nm_sq_sum += nm * nm
stats.min_nm = min(stats.min_nm, nm)
stats.max_nm = max(stats.max_nm, nm)
n = stats.participant_count
mean = stats.total_nm_sum / n
variance = (stats.total_nm_sq_sum / n) - (mean ** 2)
sb = sqrt(max(0, variance))
Kenapa inkremental? Saat peserta mencapai ribuan, recompute SELECT * FROM user_answers jadi lambat. Dengan running sums, update adalah O(1).
Konversi CTT ↔ IRT
Ketika IRT belum terkalibrasi, kita bisa estimasi awal b dari p:
def convert_ctt_p_to_irt_b(p_value: float) -> float:
b = -math.log((1 - p_value) / p_value)
return max(-3.0, min(3.0, b))
Sebaliknya, theta IRT bisa dipetakan ke skala NN:
def map_theta_to_nn(theta: float) -> int:
return round(500 + (theta / 3) * 500)
Edge Cases
| Kasus | Penanganan |
|---|
total_participants = 0 | p tidak terdefinisi — function raise error |
total_bobot_max = 0 (semua soal bobot 0) | NM = 0 (return default, hindari div by zero) |
sb = 0 (semua peserta skor sama) | NN = 500 (default, tidak ada spread) |
p = 0 (semua salah) | convert_ctt_p_to_irt_b clip ke b = 3.0 |
p = 1 (semua benar) | convert_ctt_p_to_irt_b clip ke b = -3.0 |
Dependency
math (log, sqrt)
sqlalchemy async session
- Model:
TryoutStats, Item, UserAnswer
Bacaan Lanjutan