IRT Calibration

Detail modul irt_calibration.py - model Rasch 1PL, MLE, Fisher information, kalibrasi b.

Modul: IRT Calibration

Source: backend/app/services/irt_calibration.py Lihat juga: Variabel & Rumus, Alur IRT, Alur Kalibrasi IRT

Tanggung Jawab

Implementasi Item Response Theory model Rasch 1PL (1 Parameter Logistik):

  • Estimasi theta siswa via Maximum Likelihood Estimation (MLE)
  • Kalibrasi parameter b soal dari matriks respon
  • Hitung Fisher information & Standard Error (SE)
  • Konversi silang theta ↔ NN

Konstanta Internal

Dari irt_calibration.py:

python
THETA_MIN = -3.0          # Batas bawah kemampuan siswa
THETA_MAX = 3.0           # Batas atas kemampuan siswa
B_MIN = -3.0              # Batas bawah kesulitan soal
B_MAX = 3.0               # Batas atas kesulitan soal
SE_PRECISION_THRESHOLD = 0.5    # PRD: SE < 0.5 setelah ≥ 15 soal
MLE_BOUNDS = (-6.0, 6.0)        # Batas optimasi (lebar), sebelum di-clip ke [-3, +3]
EDGE_CASE_THETA_HIGH = 4.0      # Semua jawaban benar
EDGE_CASE_THETA_LOW = -4.0      # Semua jawaban salah
NUMERICAL_CLIP = 30             # Clipping eksponen untuk stabilitas numerik

Rumus Inti

1. Probabilitas Jawaban Benar (Rasch 1PL)

python
def calculate_probability(theta: float, b: float) -> float:
    """
    P(θ) = 1 / (1 + e^-(θ - b))
    """
    exponent = theta - b
    # Numerical stability: clip exponent ke [-30, +30]
    exponent = max(-30, min(30, exponent))
    return 1.0 / (1.0 + math.exp(-exponent))

Interpretasi:

  • θ > b → P > 0.5 (siswa mampu → kemungkinan benar tinggi)
  • θ = b → P = 0.5 (siswa tepat di batas kemampuan soal)
  • θ < b → P < 0.5 (soal terlalu sulit untuk siswa)

2. Fisher Information

python
def calculate_fisher_information(theta: float, b: float) -> float:
    """
    I(θ) = P(θ) × (1 - P(θ))
    """
    p = calculate_probability(theta, b)
    return p * (1 - p)

Sifat penting: Information maksimal saat θ = b (P = 0.5 → I = 0.25). Inilah dasar CAT: soal paling informatif adalah yang kesulitan-nya setara dengan kemampuan siswa.

3. Standard Error

python
def calculate_theta_se(theta: float, b_params: list[float]) -> float:
    """
    SE(θ) = 1 / sqrt(Σ I(θ))
    """
    total_info = sum(calculate_probability(theta, b) * (1 - calculate_probability(theta, b))
                     for b in b_params)
    if total_info <= 0:
        return 3.0  # High uncertainty
    return min(1.0 / math.sqrt(total_info), 3.0)  # Cap at 3.0

Interpretasi: SE rendah = estimasi theta presisi. SE tinggi = butuh lebih banyak soal informatif.

Estimasi Theta (MLE)

python
def estimate_theta_mle(responses, b_params, initial_theta=0.0):
    """
    responses: [0, 1, 1, 0, ...]  (binary)
    b_params:  [b1, b2, b3, ...]  (kesulitan tiap soal)

    Returns: (theta, standard_error)
    """
    # Edge cases: semua benar / semua salah
    if sum(responses) == len(responses): return 3.0, 1.5   # All correct
    if sum(responses) == 0:                return -3.0, 1.5  # All incorrect

    # MLE: minimize negative log-likelihood
    def neg_log_likelihood(theta):
        p = 1 / (1 + exp(-(theta - b_params)))
        return -sum(responses * log(p) + (1-responses) * log(1-p))

    result = minimize_scalar(neg_log_likelihood, bounds=(-3, 3), method='bounded')
    theta = result.x if result.success else initial_theta

    # Clamp ke range valid
    theta = max(-3.0, min(3.0, theta))

    return theta, calculate_theta_se(theta, b_params)

Penting: Optimizer pakai scipy.optimize.minimize_scalar dengan method bounded di range [-3, +3].

Kalibrasi Parameter b

Fungsi estimate_b(responses_matrix, max_iterations=20, convergence_threshold=0.001) mengestimasi b untuk semua soal sekaligus lewat algoritma EM-style iterative:

  1. Inisialisasi: theta = 0 untuk semua siswa, b = 0 untuk semua soal
  2. Iterasi (max 20):
    • E-step: Estimasi theta tiap siswa dengan b saat ini
    • M-step: Update b tiap soal dengan theta saat ini
  3. Berhenti saat konvergen (delta < 0.001) atau max iterasi
  4. Handle edge case: siswa dengan semua benar/salah → theta = ±4.0

Minimum data: min_calibration_sample (default 100) respon per soal agar kalibrasi reliable.

Fungsi Async (Database)

FungsiTujuan
get_session_responses(db, session_id)Ambil respon + b_params untuk sesi
update_session_theta(db, session_id, force_recalculate)Re-estimate theta untuk sesi
update_theta_after_response(db, session_id, item_id, is_correct)Update theta real-time setelah 1 jawaban
calibrate_item(db, item_id, ...)Kalibrasi satu item
calibrate_all(db, tryout_id, ...)Kalibrasi batch semua item di tryout
get_calibration_status(db, tryout_id)Status kalibrasi (sudah/belum, sample size)
validate_irt_parameters(...)Validasi input sebelum kalibrasi
fallback_to_ctt(reason, context)Return response CTT saat IRT gagal

Konversi θ ↔ NN

Untuk perbandingan skor IRT dengan CTT:

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

def nn_to_theta(nn: int) -> float:
    """NN ∈ [0, 1000] → θ ∈ [-3, +3]"""
    return ((nn - 500) / 500) * 3

Edge Cases

KasusPenanganan
Empty responses / b_paramsReturn (0.0, 3.0) — theta netral, SE maksimal
Semua jawaban benarReturn (3.0, 1.5) — clamped ke THETA_MAX
Semua jawaban salahReturn (-3.0, 1.5) — clamped ke THETA_MIN
responses dan b_params beda panjangRaise IRTCalibrationError
MLE tidak konvergenPakai initial_theta (default 0.0)
Total information = 0SE = 3.0 (uncertainty maksimal)

Numerical Stability

Beberapa praktik penting yang dipakai:

  1. Exponent clipping: exp(-x) di-clip ke [-30, +30] untuk hindari overflow/underflow
  2. Probability clipping: p di-clip ke [1e-10, 1 - 1e-10] sebelum log(p) — hindari log(0)
  3. SE cap: SE di-cap ke 3.0 maksimal (tidak inf)
  4. Variance clamp: Variance di-max(0, ...) untuk hindari negatif akibat floating point

Dependency

  • numpy (operasi vektor untuk batch kalibrasi)
  • scipy.optimize.minimize_scalar (MLE)
  • sqlalchemy async session
  • Model: Item, Session, UserAnswer

Bacaan Lanjutan

Last updated Jul 25, 2026