Files
encoach_backend_v4/custom_addons/encoach_placement/services/cefr_mapper.py
Yamen Ahmad 907a5c0e92 feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
2026-04-10 17:26:42 +04:00

84 lines
2.2 KiB
Python

class CefrMapper:
"""Maps IRT theta values to CEFR levels and IELTS band scores."""
THETA_TO_CEFR = [
(-4.0, -2.5, 'pre_a1'),
(-2.5, -1.5, 'a1'),
(-1.5, -0.5, 'a2'),
(-0.5, 0.5, 'b1'),
(0.5, 1.5, 'b2'),
(1.5, 2.5, 'c1'),
(2.5, 4.0, 'c2'),
]
CEFR_TO_BAND = {
'pre_a1': 2.0,
'a1': 3.0,
'a2': 4.0,
'b1': 5.0,
'b2': 6.5,
'c1': 7.5,
'c2': 9.0,
}
CEFR_LABELS = {
'pre_a1': 'Pre-A1 (Beginner)',
'a1': 'A1 (Elementary)',
'a2': 'A2 (Pre-Intermediate)',
'b1': 'B1 (Intermediate)',
'b2': 'B2 (Upper-Intermediate)',
'c1': 'C1 (Advanced)',
'c2': 'C2 (Proficient)',
}
@staticmethod
def theta_to_cefr(theta):
for low, high, level in CefrMapper.THETA_TO_CEFR:
if low <= theta < high:
return level
return 'c2' if theta >= 2.5 else 'pre_a1'
@staticmethod
def theta_to_band(theta):
cefr = CefrMapper.theta_to_cefr(theta)
base_band = CefrMapper.CEFR_TO_BAND.get(cefr, 5.0)
for low, high, level in CefrMapper.THETA_TO_CEFR:
if level == cefr:
range_width = high - low
if range_width > 0:
position = (theta - low) / range_width
else:
position = 0.5
cefr_list = list(CefrMapper.CEFR_TO_BAND.keys())
idx = cefr_list.index(cefr)
next_band = CefrMapper.CEFR_TO_BAND.get(
cefr_list[min(idx + 1, len(cefr_list) - 1)], base_band + 1.0
)
band = base_band + position * (next_band - base_band)
return round(band * 2) / 2
return base_band
@staticmethod
def band_to_cefr(band):
if band < 2.5:
return 'pre_a1'
if band < 3.5:
return 'a1'
if band < 4.5:
return 'a2'
if band < 5.5:
return 'b1'
if band < 7.0:
return 'b2'
if band < 8.0:
return 'c1'
return 'c2'
@staticmethod
def get_cefr_label(cefr_code):
return CefrMapper.CEFR_LABELS.get(cefr_code, cefr_code)