"""Normalisation of model answers into one comparable record (app/scoring.py).""" import math import unittest from app.scoring import _lookup, normalise, option_keys NOUL = {"type": "noul", "instructions": "x"} CHOICE = {"type": "choice", "instructions": "x", "criteria": {"bug": "b", "how_to": "h", "sales": "s"}} SCORE = {"type": "score", "instructions": "x", "criteria": ["Low", "Mid", "High", "Critical"]} class OptionKeysTest(unittest.TestCase): def test_noul_keys_are_true_false(self): self.assertEqual(option_keys(NOUL), ["true", "false"]) def test_score_keys_are_level_indices(self): self.assertEqual(option_keys(SCORE), ["0", "1", "2", "3"]) def test_choice_keys_follow_criteria_order(self): q = {"type": "choice", "criteria": {"sales": "s", "bug": "b", "how_to": "h"}} # deliberately not alphabetical self.assertEqual(option_keys(q), ["sales", "bug", "how_to"]) def test_choice_accepts_list_criteria(self): self.assertEqual(option_keys({"type": "choice", "criteria": ["a", "b"]}), ["a", "b"]) class LookupTest(unittest.TestCase): def test_finds_true_under_bool_and_word_spellings(self): for spelling in (True, "True", "yes", 1): with self.subTest(spelling=spelling): self.assertEqual(_lookup({spelling: 0.7}, "true"), 0.7) def test_finds_score_level_under_int_key(self): self.assertEqual(_lookup({2: 0.4}, "2"), 0.4) def test_missing_key_returns_none(self): self.assertIsNone(_lookup({"x": 1}, "true")) def test_non_dict_returns_none(self): self.assertIsNone(_lookup(None, "true")) class NormaliseTest(unittest.TestCase): def test_noul_builds_probs_from_p_true(self): rec = normalise(NOUL, None, p_true=0.8) self.assertAlmostEqual(rec["probs"]["true"], 0.8) self.assertAlmostEqual(rec["probs"]["false"], 0.2) self.assertEqual(rec["choice"], "true") self.assertAlmostEqual(rec["p_true"], 0.8) def test_noul_laya_conf_is_top_probability(self): rec = normalise(NOUL, None, p_true=0.3) self.assertEqual(rec["choice"], "false") self.assertAlmostEqual(rec["laya_conf"], 0.7) def test_probabilities_are_renormalised_to_one(self): rec = normalise(CHOICE, {"bug": 2.0, "how_to": 1.0, "sales": 1.0}) self.assertAlmostEqual(sum(rec["probs"].values()), 1.0) self.assertAlmostEqual(rec["probs"]["bug"], 0.5) def test_missing_options_count_as_zero(self): rec = normalise(CHOICE, {"bug": 1.0}) self.assertEqual(rec["probs"], {"bug": 1.0, "how_to": 0.0, "sales": 0.0}) def test_answer_only_becomes_point_mass_on_choice(self): rec = normalise(CHOICE, None, choice="SALES") self.assertEqual(rec["probs"], {"bug": 0.0, "how_to": 0.0, "sales": 1.0}) self.assertEqual(rec["choice"], "sales") def test_score_answer_only_becomes_point_mass_on_rounded_level(self): rec = normalise(SCORE, None, level=2.6) # 2.6 rounds to 3 but truncates to 2 self.assertEqual(rec["choice"], "3") self.assertEqual(rec["probs"]["3"], 1.0) def test_score_reports_expected_level_and_level_count(self): rec = normalise(SCORE, {"0": 0.5, "3": 0.5}) self.assertAlmostEqual(rec["expected_level"], 1.5) self.assertEqual(rec["levels"], 4) def test_uniform_distribution_has_zero_entropy_confidence(self): rec = normalise(CHOICE, {"bug": 1, "how_to": 1, "sales": 1}) self.assertAlmostEqual(rec["entropy_conf"], 0.0) self.assertAlmostEqual(rec["laya_conf"], 0.0) def test_certain_distribution_has_full_entropy_confidence(self): rec = normalise(CHOICE, {"bug": 1, "how_to": 0, "sales": 0}) self.assertAlmostEqual(rec["entropy_conf"], 1.0) self.assertAlmostEqual(rec["top_prob"], 1.0) def test_entropy_confidence_matches_formula(self): probs = {"bug": 0.6, "how_to": 0.3, "sales": 0.1} expected = 1 - (-sum(p * math.log(p) for p in probs.values())) / math.log(3) self.assertAlmostEqual(normalise(CHOICE, probs)["entropy_conf"], expected) def test_choice_laya_conf_is_entropy_confidence(self): rec = normalise(CHOICE, {"bug": 0.6, "how_to": 0.3, "sales": 0.1}) self.assertAlmostEqual(rec["laya_conf"], rec["entropy_conf"]) def test_no_information_gives_uniform_record(self): rec = normalise(CHOICE, None) self.assertEqual(sorted(rec["probs"].values()), [0.0, 0.0, 0.0]) self.assertIn(rec["choice"], CHOICE["criteria"]) if __name__ == "__main__": unittest.main()