CTT Scoring

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:

python
# Step 1: Tingkat Kesukaran
p = Σ Benar / Total Peserta            # calculate_ctt_p()

# Step 2: Bobot (weight)
Bobot = 1 - p                          # calculate_ctt_bobot()

# Step 3: Nilai Mentah
NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000   # calculate_ctt_nm()

# Step 4: Nilai Nasional (normalized)
NN = 500 + 100 × ((NM - Rataan) / SB)  # calculate_ctt_nn()

Fungsi Publik

Fungsi Pure (matematika)

FungsiSignatureOutput
calculate_ctt_p(total_correct: int, total_participants: int) → floatp-value [0, 1]
calculate_ctt_bobot(p_value: float) → floatbobot [0, 1]
calculate_ctt_nm(total_bobot_siswa: float, total_bobot_max: float) → intNM [0, 1000]
calculate_ctt_nn(nm: int, rataan: float, sb: float) → intNN [0, 1000] (di-clip)
categorize_difficulty(p_value: float) → str"mudah" / "sedang" / "sulit"
convert_ctt_p_to_irt_b(p_value: float) → floatb [-3, +3]
map_theta_to_nn(theta: float) → intNN [0, 1000]

Fungsi Async (database)

FungsiTujuan
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 pKategoriLevel FieldSetara b IRT
p < 0.30Sukarsulitb > 0.85
0.30 ≤ p ≤ 0.70Sedangsedang-0.85 ≤ b ≤ 0.85
p > 0.70Mudahmudahb < -0.85
python
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:

python
# Saat peserta baru selesai sesi (NM terhitung):
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)

# Rataan & SB dihitung dari running sums:
n = stats.participant_count
mean = stats.total_nm_sum / n
variance = (stats.total_nm_sq_sum / n) - (mean ** 2)
sb = sqrt(max(0, variance))   # clamp anti floating-point negatif

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:

python
def convert_ctt_p_to_irt_b(p_value: float) -> float:
    # Approximation: b ≈ -ln((1-p)/p)
    # Memetakan p ∈ (0, 1) → b ∈ (-∞, +∞), di-clip ke [-3, +3]
    b = -math.log((1 - p_value) / p_value)
    return max(-3.0, min(3.0, b))

Sebaliknya, theta IRT bisa dipetakan ke skala NN:

python
def map_theta_to_nn(theta: float) -> int:
    # θ ∈ [-3, +3] → NN ∈ [0, 1000]
    return round(500 + (theta / 3) * 500)

Edge Cases

KasusPenanganan
total_participants = 0p 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

Last updated Jul 25, 2026