CAT Selection

Detail modul cat_selection.py - pemilihan soal adaptif (fixed/adaptive/hybrid), terminasi, reuse awareness.

Modul: CAT Selection

Source: backend/app/services/cat_selection.py Lihat juga: Alur CAT, Alur Hybrid, Konsep → Mode Tryout

Tanggung Jawab

Computer Adaptive Testing (CAT) — modul yang memilih soal mana yang harus ditampilkan berikutnya kepada siswa:

  • 3 strategi: fixed, adaptive, hybrid
  • Update theta real-time setelah setiap jawaban
  • Cek terminasi sesi (SE cukup presisi atau max items)
  • Filter soal: hindari reuse per user (lintas sesi)

Entry Point

python
async def get_next_item(
    db: AsyncSession,
    session_id: str,
    selection_mode: Literal["fixed", "adaptive", "hybrid"] = "fixed",
    hybrid_transition_slot: int = 10,
    ai_generation_enabled: bool = False,
    level_filter: Optional[str] = None
) -> NextItemResult:

Dispatcher utama yang mendelegasikan ke salah satu dari 3 handler.

Strategi 1: Fixed (get_next_item_fixed)

Soal dikirim sesuai urutan slot (1, 2, 3, ...). Tidak ada logika adaptif.

python
query = (
    select(Item)
    .where(
        Item.tryout_id == tryout_id,
        Item.website_id == website_id,
        _servable_item_filter(),
    )
    .order_by(Item.slot, Item.level)
)
# Exclude already answered
# Return first available (lowest slot)

Use case: Default aman, kompatibel dengan Excel klien, peserta sedikit, IRT belum terkalibrasi.

Strategi 2: Adaptive (get_next_item_adaptive)

Pilih soal yang paling informatif untuk estimasi theta siswa saat ini.

Algoritma Pemilihan

python
current_theta = session.theta or 0.0   # Mulai dari 0 kalau belum ada

# Filter kandidat:
# 1. calibrated = True (wajib punya b parameter valid)
# 2. Belum dijawab di sesi ini
# 3. (slot, level) belum dijawab user lintas sesi
# 4. AI-generated hanya jika ai_generation_enabled=True

best_item = None
best_score = float('inf')

for item in items:
    if item.irt_b is None:
        continue   # Skip soal tanpa b

    b_distance = abs(item.irt_b - current_theta)
    information = calculate_item_information(current_theta, item.irt_b)

    # Skor: minimize distance, maximize information
    # Bobot information 0.1 (lebih kecil dari distance)
    score = b_distance - (0.1 * information)

    if score < best_score:
        best_score = score
        best_item = item

Rumus kunci:

text
score = |b - θ| - 0.1 × I(θ)

Pilih skor terkecil. Berarti: soal dengan b paling dekat ke θ, dan information tinggi.

Kenapa Pakai Bobot 0.1 untuk Information?

  • b_distance range: [0, 6] (θ, b ∈ [-3, +3])
  • information range: [0, 0.25]
  • Faktor 0.1 menyeimbangkan: information berkontribusi [0, 0.025] — signifikan tapi tidak mendominasi

Hasilnya: prioritaskan soal yang dekat dengan kemampuan siswa, dengan tie-breaker ke soal yang lebih informatif.

Use case: Sudah punya ≥ min_calibration_sample respon per soal, ingin tes presisi tinggi dengan panjang minimal.

Strategi 3: Hybrid (get_next_item_hybrid)

Transisi mulus dari fixed ke adaptive berdasarkan slot.

python
if current_slot < hybrid_transition_slot:
    # Mode Fixed (CTT-style)
    return await get_next_item_fixed(...)
else:
    # Mode Adaptive (IRT-style)
    return await get_next_item_adaptive(...)

Konfigurasi:

  • hybrid_transition_slot (default: null — harus di-set admin)
  • Contoh: hybrid_transition_slot = 10 → 10 soal pertama fixed, sisanya adaptive

Use case: Transisi dari CTT ke IRT — siswa "warm-up" dengan soal fixed, lalu adaptif setelah data cukup.

Update Theta Real-Time

python
async def update_theta(db, session_id):
    """Re-estimate theta berdasarkan semua respon di sesi."""
    responses, b_params = await get_session_responses(db, session_id)
    theta, se = estimate_theta_mle(responses, b_params, initial_theta=session.theta)
    session.theta = theta
    session.theta_se = se
    await db.commit()
    return theta, se

Dipanggil setelah setiap jawaban untuk update theta sebelum pilih soal berikutnya.

Terminasi Sesi

python
async def should_terminate(db, session_id, max_items=None, se_threshold=0.5):
    """
    Kondisi terminasi (salah satu):
    1. items_answered >= max_items
    2. SE < 0.5 AND items_answered >= 15  (PRD requirement)
    3. Tidak ada soal tersisa (filter empty)
    """
    if max_items and items_answered >= max_items:
        return TerminationCheck(should_terminate=True, reason="max items reached")

    if (current_se < se_threshold) and (items_answered >= 15):
        return TerminationCheck(should_terminate=True, reason="SE threshold met")

    return TerminationCheck(should_terminate=False, reason="continuing")

Konstanta:

  • DEFAULT_SE_THRESHOLD = 0.5 (PRD: presisi cukup)
  • MIN_ITEMS_FOR_SE = 15 (PRD: minimum soal sebelum terminasi berbasis SE)

Insight: Dengan CAT, tes bisa selesai dalam 15-30 soal (vs 50-100 di fixed) untuk siswa dengan kemampuan jelas — karena SE cepat konvergen.

Reuse Awareness

python
async def check_user_level_reuse(db, wp_user_id, website_id, tryout_id, slot, level):
    """
    Cek apakah user sudah pernah menjawab soal di (slot, level) tertentu,
    lintas sesi di tryout yang sama.
    """
    existing = await db.execute(
        select(UserAnswer).join(Session).where(
            Session.wp_user_id == wp_user_id,
            Session.website_id == website_id,
            Session.tryout_id == tryout_id,
            UserAnswer.slot == slot,
            UserAnswer.level == level,
        )
    )
    return existing.first() is not None

Tujuan: Cegah siswa melihat soal yang sama di attempt berbeda. Kalau sudah pernah jawab (slot, level) tertentu, soal di filter itu dianggap "sudah terpakai" untuk user ini.

Helper Functions

FungsiTujuan
_servable_item_filter()Filter SQL: variant_status = 'active', exclude draft/rejected
_get_user_answered_slot_levels(db, wp_user_id, ...)Set of (slot, level) yang sudah dijawab user
get_available_levels_for_slot(db, tryout_id, slot)Level yang tersedia untuk slot (mis. ['mudah', 'sedang', 'sulit'])
simulate_cat_selection(db, ...)Simulasi alur CAT tanpa real session (untuk testing/debugging)

Output: NextItemResult

python
class NextItemResult:
    item: Optional[Item]       # Soal terpilih (None jika habis)
    selection_method: str      # "fixed" | "adaptive" | "hybrid"
    slot: Optional[int]
    level: Optional[str]
    reason: str                # Penjelasan kenapa soal ini dipilih

Contoh reason:

  • Fixed: "Fixed order selection - slot 5"
  • Adaptive: "Adaptive selection - b=0.234 ≈ θ=0.180"

Edge Cases

KasusPenanganan
Tidak ada soal terkalibrasiAdaptive return item=None dengan reason "No calibrated items available"
Semua soal sudah dijawab userReturn item=None dengan reason "No more items available"
Session tidak ditemukanRaise CATSelectionError
selection_mode invalidRaise CATSelectionError
Soal dengan irt_b = None walau calibrated=TrueSkip (data inconsistent — seharusnya tidak terjadi)

Dependency

  • irt_calibration.estimate_theta_mle (update theta)
  • irt_calibration.calculate_item_information (skoring kandidat)
  • Model: Session, Item, UserAnswer

Bacaan Lanjutan

Last updated Jul 25, 2026