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)