diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index fc9a24d..9d86be1 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -30,4 +30,6 @@ jobs: run: coverage run -m pytest . - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/archeryutils/classifications/__init__.py b/archeryutils/classifications/__init__.py index 552aae0..9d49ecd 100644 --- a/archeryutils/classifications/__init__.py +++ b/archeryutils/classifications/__init__.py @@ -5,6 +5,7 @@ calculate_agb_field_classification, ) from .agb_indoor_classifications import ( + agb_indoor_classification_fraction, agb_indoor_classification_scores, calculate_agb_indoor_classification, ) @@ -13,6 +14,7 @@ calculate_agb_old_indoor_classification, ) from .agb_outdoor_classifications import ( + agb_outdoor_classification_fraction, agb_outdoor_classification_scores, calculate_agb_outdoor_classification, ) @@ -20,8 +22,10 @@ __all__ = [ "calculate_agb_outdoor_classification", "agb_outdoor_classification_scores", + "agb_outdoor_classification_fraction", "calculate_agb_indoor_classification", "agb_indoor_classification_scores", + "agb_indoor_classification_fraction", "calculate_agb_old_indoor_classification", "agb_old_indoor_classification_scores", "calculate_agb_field_classification", diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index dda1eae..316763b 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -310,3 +310,94 @@ def agb_indoor_classification_scores( int_class_scores[i] += 1 return int_class_scores + + +def agb_indoor_classification_fraction( + score: float, + roundname: str, + bowstyle: str, + gender: str, + age_group: str, +) -> float: + """ + Calculate the fraction towards the next classification an archer is. + + Calculates fraction through current classification a score is based on handicap. + If above maximum possible classification returns 1.0, if below minimum returns 0.0. + + Parameters + ---------- + score : float + numerical score on the round + roundname : str + name of round shot as given by 'codename' in json + bowstyle : str + archer's bowstyle under AGB outdoor target rules + gender : str + archer's gender under AGB outdoor target rules + age_group : str + archer's age group under AGB outdoor target rules + + Returns + ------- + float + fraction (as a decimal) towards the next classification in terms of handicap. + If above maximum return 1.0, if below minimum return 0.0. + + Examples + -------- + A score of 525 on a WA18 round for an adult male recurve is I-B2, but around + 60% of the way towards I-B1 in terms of handicap: + + >>> from archeryutils import classifications as class_func + >>> class_func.agb_indoor_classification_fraction( + ... 525, "wa18", "recurve", "male", "adult" + ... ) + 0.6005602030896947 + + """ + # enforce full size face and compound scoring where required + if bowstyle.lower() in ("compound"): + roundname = cls_funcs.get_compound_codename(roundname) + roundname = cls_funcs.strip_spots(roundname) + + # Check for early return if on score boundary: + # If above max classification score return 1.0 early. + # Else if a boundary score return 0.0 (avoids integer rounding errors later). + all_class_scores = agb_indoor_classification_scores( + roundname, + bowstyle, + gender, + age_group, + ) + if ( + score >= np.abs(all_class_scores[0]) + or score == ALL_INDOOR_ROUNDS[roundname].max_score() + ): + return 1.0 + elif score in all_class_scores: + return 0.0 + + # If no early return from score boundaries proceed using handicaps. + hc_sys = hc.handicap_scheme("AGB") + handicap = hc_sys.handicap_from_score(score, ALL_INDOOR_ROUNDS[roundname]) + + groupname = cls_funcs.get_groupname(bowstyle, gender, age_group) + group_data = agb_indoor_classifications[groupname] + + group_hcs = group_data["class_HC"] + + loc = 0 + while loc < len(group_hcs): + if handicap < group_hcs[loc]: + break + loc += 1 + + if loc == 0: + # Handicap below max classification possible + return 1.0 # pragma: no cover - Match outdoor: sanity check but not triggered + if loc == len(group_hcs): + # Handicap above lowest classification + return 0.0 + + return (group_hcs[loc] - handicap) / (group_hcs[loc] - group_hcs[loc - 1]) diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index cdcc966..a0c5c62 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -567,3 +567,128 @@ def agb_outdoor_classification_scores( int_class_scores = [int(x) for x in class_scores] return int_class_scores + + +def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments + score: float, + roundname: str, + bowstyle: str, + gender: str, + age_group: str, + restrict: bool | None = True, +) -> float: + """ + Calculate the fraction towards the next classification an archer is. + + Calculates fraction through current classification a score is based on handicap. + If above maximum possible classification returns 1.0, if below minimum returns 0.0. + + Parameters + ---------- + score : float + numerical score on the round + roundname : str + name of round shot as given by 'codename' in json + bowstyle : str + archer's bowstyle under AGB outdoor target rules + gender : str + archer's gender under AGB outdoor target rules + age_group : str + archer's age group under AGB outdoor target rules + restrict : bool, default=True + whether to restrict to the classifications officially available on this + round for this category + + Returns + ------- + float + fraction (as a decimal) towards the next classification in terms of handicap. + If above maximum return 1.0, if below minimum return 0.0. + + Examples + -------- + A score of 450 on a WA720 70m round for an adult male recurve is B3, but around + 33% of the way towards B2 in terms of handicap: + + >>> from archeryutils import classifications as class_func + >>> class_func.agb_outdoor_classification_fraction( + ... 450, "wa720_70", "recurve", "male", "adult" + ... ) + 0.3348216315578329 + + A score of 632 on a national would be A1 class, the highest possible for the round: + + >>> class_func.agb_outdoor_classification_fraction( + ... 620, "western", "recurve", "male", "adult" + ... ) + 1.0 + + If we use restrict=False we ignore the distance and prestige restrictions to use + purely the classification handicap values: + + >>> class_func.agb_outdoor_classification_fraction( + ... 620, + ... "wa720_50_b", + ... "recurve", + ... "male", + ... "adult" + ... restrict=False + ... ) + + """ + # Check for early return if on score boundary: + # If above max classification score return 1.0 early. + # Else if a boundary score return 0.0 (avoids integer rounding errors later). + # Note this section is operating under `restrict=True` + all_class_scores = agb_outdoor_classification_scores( + roundname, + bowstyle, + gender, + age_group, + ) + if restrict: + # Reduce list to classifications that have scores (remove -9999) + all_class_scores = [x for x in all_class_scores if x >= 0.0] + all_class_scores.append(-9999) + if ( + score >= np.abs(all_class_scores[0]) + or score == ALL_OUTDOOR_ROUNDS[roundname].max_score() + ): + return 1.0 + elif score in all_class_scores: + return 0.0 + + # If no early return from score boundaries proceed using handicaps. + hc_sys = hc.handicap_scheme("AGB") + handicap = hc_sys.handicap_from_score(score, ALL_OUTDOOR_ROUNDS[roundname]) + + groupname = cls_funcs.get_groupname(bowstyle, gender, age_group) + group_data = agb_outdoor_classifications[groupname] + + if restrict: + class_data = {} + for i, class_i in enumerate(group_data["classes"]): + class_data[class_i] = { + "min_dist": group_data["min_dists"][i], + "class_HC": group_data["class_HC"][i], + } + class_data = _check_prestige_distance(roundname, groupname, class_data) + + group_hcs = np.array([class_data[cls]["class_HC"] for cls in class_data]) + else: + group_hcs = group_data["class_HC"] + + # Fetch the handicaps either side and get fraction + loc = 0 + while loc < len(group_hcs): + if handicap < group_hcs[loc]: + break + loc += 1 + + if loc == 0: + # Handicap below max classification possible + return 1.0 + if loc == len(group_hcs): + # Handicap above lowest classification + return 0.0 + return (group_hcs[loc] - handicap) / (group_hcs[loc] - group_hcs[loc - 1]) diff --git a/archeryutils/classifications/tests/test_agb_indoor.py b/archeryutils/classifications/tests/test_agb_indoor.py index 1f65411..6a92165 100644 --- a/archeryutils/classifications/tests/test_agb_indoor.py +++ b/archeryutils/classifications/tests/test_agb_indoor.py @@ -2,6 +2,7 @@ import pytest +import archeryutils as au import archeryutils.classifications as class_funcs from archeryutils import load_rounds @@ -86,7 +87,7 @@ def test_agb_indoor_classification_scores_ages( age_group=age_group, ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "age_group,scores_expected", @@ -127,7 +128,7 @@ def test_agb_indoor_classification_scores_genders( age_group=age_group, ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "bowstyle,scores_expected", @@ -159,7 +160,7 @@ def test_agb_indoor_classification_scores_bowstyles( age_group="adult", ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "bowstyle,scores_expected", @@ -191,7 +192,7 @@ def test_agb_indoor_classification_scores_nonbowstyles( age_group="adult", ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,scores_expected", @@ -227,7 +228,7 @@ def test_agb_indoor_classification_scores_triple_faces( age_group="adult", ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,bowstyle,gender,age_group", @@ -420,3 +421,208 @@ def test_calculate_agb_indoor_classification_invalid_scores( gender="male", age_group="adult", ) + + +class TestCalculateAgbIndoorClassificationFraction: + """Class to test the indoor classification fraction function.""" + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa18", + 450, + "adult", + "compound", + 0.847856946666746, + ), + ( + "wa18", + 425, + "adult", + "barebow", + 0.5975078167952219, + ), + ( + "portsmouth", + 538, + "Under 18", + "recurve", + 0.4473199669958774, + ), + ], + ) + def test_agb_indoor_classification_fraction( # noqa: PLR0913 Too many arguments + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction is as expected.""" + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa18", + 1, + "adult", + "compound", + 0.0, + ), + ( + "wa18", + 20, + "adult", + "barebow", + 0.0, + ), + ( + "wa18", + 30, + "adult", + "compound", + 0.0, + ), + ( + "portsmouth", + 1, + "Under 18", + "recurve", + 0.0, + ), + ], + ) + def test_agb_indoor_classification_fraction_low( # noqa: PLR0913 many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction below lowest classification is 0.0.""" + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa18", + 599, + "adult", + "compound", + 1.0, + ), + ( + "worcester", + 300, + "adult", + "compound", + 1.0, + ), + ( + "wa18", + 550, + "adult", + "barebow", + 1.0, + ), + ( + "portsmouth", + 588, + "under 18", + "recurve", + 1.0, + ), + ], + ) + def test_agb_indoor_classification_fraction_high( # noqa: PLR0913 Too many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction above highest classification is 1.0.""" + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa18", + 546, + "adult", + "compound", + 0.0, + ), + ( + "worcester", + 294, + "adult", + "compound", + 0.0, + ), + ( + "wa18", + 535, + "adult", + "barebow", + 1.0, + ), + ( + "portsmouth", + 571, + "under 18", + "recurve", + 1.0, + ), + ], + ) + def test_agb_indoor_classification_fraction_boundary( # noqa: PLR0913 Too many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction on score boundaries is 0.0 or 1.0.""" + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) diff --git a/archeryutils/classifications/tests/test_agb_outdoor.py b/archeryutils/classifications/tests/test_agb_outdoor.py index 777bed0..df7d088 100644 --- a/archeryutils/classifications/tests/test_agb_outdoor.py +++ b/archeryutils/classifications/tests/test_agb_outdoor.py @@ -2,6 +2,7 @@ import pytest +import archeryutils as au import archeryutils.classifications as class_funcs from archeryutils import load_rounds @@ -96,7 +97,7 @@ def test_agb_outdoor_classification_scores_ages( age_group=age_group, ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,age_group,scores_expected", @@ -142,7 +143,7 @@ def test_agb_outdoor_classification_scores_genders( age_group=age_group, ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,bowstyle,gender,scores_expected", @@ -200,7 +201,7 @@ def test_agb_outdoor_classification_scores_bowstyles( age_group="adult", ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,bowstyle,gender,scores_expected", @@ -240,7 +241,7 @@ def test_agb_outdoor_classification_scores_nonbowstyles( age_group="adult", ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,scores_expected", @@ -268,7 +269,7 @@ def test_agb_outdoor_classification_scores_triple_faces( age_group="adult", ) - assert scores == scores_expected[::-1] + assert scores == pytest.approx(scores_expected[::-1]) @pytest.mark.parametrize( "roundname,bowstyle,gender,age_group", @@ -565,3 +566,254 @@ def test_calculate_agb_outdoor_classification_invalid_scores( gender="male", age_group="adult", ) + + +class TestCalculateAgbOutdoorClassificationFraction: + """Class to test the outdoor classification fraction function.""" + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa720_70", + 450, + "adult", + "compound", + 0.3906252368174717, + ), + ( + "wa720_50_b", + 425, + "adult", + "barebow", + 0.11099804827974227, + ), + ( + "wa720_50_c", + 450, + "adult", + "compound", + 0.42456775138238356, + ), + ( + "wa720_60", + 620, + "Under 18", + "recurve", + 0.7257808930669505, + ), + ], + ) + def test_agb_outdoor_classification_fraction( # noqa: PLR0913 many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction is as expected.""" + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa720_70", + 1, + "adult", + "compound", + 0.0, + ), + ( + "wa720_50_b", + 20, + "adult", + "barebow", + 0.0, + ), + ( + "wa720_50_c", + 30, + "adult", + "compound", + 0.0, + ), + ( + "wa720_60", + 1, + "Under 18", + "recurve", + 0.0, + ), + ], + ) + def test_agb_outdoor_classification_fraction_low( # noqa: PLR0913 many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction below lowest classification is 0.0.""" + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa720_70", + 720, + "adult", + "compound", + 1.0, + ), + ( + "wa720_50_b", + 650, + "adult", + "barebow", + 1.0, + ), + ( + "wa720_50_c", + 718, + "adult", + "compound", + 1.0, + ), + ( + "wa720_60", + 650, + "under 18", + "recurve", + 1.0, + ), + ], + ) + def test_agb_outdoor_classification_fraction_high( # noqa: PLR0913 many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction above highest classification is 1.0.""" + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + @pytest.mark.parametrize( + "roundname,score,age_group,bowstyle,frac_expected", + [ + ( + "wa720_70", + 613, + "adult", + "compound", + 1.0, + ), + ( + "wa720_50_b", + 626, + "adult", + "barebow", + 1.0, + ), + ( + "wa720_50_c", + 640, + "adult", + "compound", + 0.0, + ), + ( + "wa720_60", + 338, + "under 18", + "recurve", + 0.0, + ), + ], + ) + def test_agb_outdoor_classification_fraction_boundary( # noqa: PLR0913 many args + self, + score: float, + roundname: str, + age_group: str, + bowstyle: str, + frac_expected: float, + ) -> None: + """Check that classification fraction above highest classification is 1.0.""" + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + + assert frac_returned == pytest.approx(frac_expected) + + def test_agb_outdoor_classification_fraction_restrict( + self, + ) -> None: + """Check that classification fraction functions with restrict.""" + frac_restricted = class_funcs.agb_outdoor_classification_fraction( + score=620, + roundname="national", + bowstyle="recurve", + gender="male", + age_group="adult", + ) + assert frac_restricted == 1.0 + + frac_unrestricted = class_funcs.agb_outdoor_classification_fraction( + score=620, + roundname="national", + bowstyle="recurve", + gender="male", + age_group="adult", + restrict=False, + ) + + assert frac_unrestricted == pytest.approx(0.4541258975704667) + + def test_agb_outdoor_classification_fraction_unrestrict_large( + self, + ) -> None: + """Check classification fraction unrestricted for large scores.""" + frac_unrestricted = class_funcs.agb_outdoor_classification_fraction( + score=646, + roundname="national", + bowstyle="recurve", + gender="male", + age_group="adult", + restrict=False, + ) + + assert frac_unrestricted == pytest.approx(1.0) diff --git a/archeryutils/handicaps/tests/test_handicaps.py b/archeryutils/handicaps/tests/test_handicaps.py index 618f733..e4a90e0 100644 --- a/archeryutils/handicaps/tests/test_handicaps.py +++ b/archeryutils/handicaps/tests/test_handicaps.py @@ -147,7 +147,7 @@ def test_handicap_scheme_subclass_generation( ) -> None: """Check that appropriate subclasses are generated by handicap_scheme().""" # Comparison to type as mypy doesn't like mixing isinstance() & user-defined cls - assert type(hc.handicap_scheme(scheme)) == hcclass + assert type(hc.handicap_scheme(scheme)) is hcclass class TestSigmaT: diff --git a/archeryutils/utils.py b/archeryutils/utils.py index d23f522..b216d01 100644 --- a/archeryutils/utils.py +++ b/archeryutils/utils.py @@ -20,8 +20,8 @@ def _get_sys_info() -> list: # pragma: no cover # get full commit hash commit = None if os.path.isdir(".git") and os.path.isdir("archeryutils"): - with subprocess.Popen( - 'git log --format="%H" -n 1'.split(" "), # noqa: S603 subprocess call this is safe + with subprocess.Popen( # noqa: S603 subprocess call this is safe + 'git log --format="%H" -n 1'.split(" "), stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) as pipe: diff --git a/examples.ipynb b/examples.ipynb index c978f82..7911737 100644 --- a/examples.ipynb +++ b/examples.ipynb @@ -803,6 +803,78 @@ ")\n", "print(class_scores)" ] + }, + { + "cell_type": "markdown", + "id": "ad53dfcd-4040-47ed-8607-a38c7dd0ae24", + "metadata": {}, + "source": [ + "### Classification Fractions\n", + "\n", + "There is also the classification fraction functionality to tell archers how far through a classification band their score is, and how far they are from the next one. This is done from the `X_classification_fraction()` functions.\n", + "\n", + "These take a round alias and categories as strings above, and return a list of scores required for each classification in descending order.\n", + "\n", + "Where a classification is not available from a particular round a fill value of -9999 is returned." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecabb3da-549a-4aaa-a325-75bfcbe67503", + "metadata": {}, + "outputs": [], + "source": [ + "# AGB Outdoor\n", + "class_from_score = class_func.calculate_agb_outdoor_classification(\n", + " 1004,\n", + " \"york\",\n", + " \"recurve\",\n", + " \"male\",\n", + " \"adult\",\n", + ")\n", + "\n", + "class_frac = class_func.agb_outdoor_classification_fraction(\n", + " 1004,\n", + " \"york\",\n", + " \"recurve\",\n", + " \"male\",\n", + " \"adult\",\n", + ")\n", + "\n", + "print(\n", + " f\"A score of 1004 on a York is class {class_from_score} for a male recurve, {100.0*class_frac:.0f}% of the way towards the next classification.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d5a2b8a-7d72-4963-9b6c-d283e943b688", + "metadata": {}, + "outputs": [], + "source": [ + "# AGB Indoor\n", + "class_from_score = class_func.calculate_agb_indoor_classification(\n", + " 562,\n", + " \"wa18_compound_triple\",\n", + " \"compound\",\n", + " \"female\",\n", + " \"adult\",\n", + ")\n", + "\n", + "class_frac = class_func.agb_indoor_classification_fraction(\n", + " 562,\n", + " \"wa18_compound_triple\",\n", + " \"compound\",\n", + " \"female\",\n", + " \"adult\",\n", + ")\n", + "\n", + "print(\n", + " f\"A score of 562 on a WA18 is class {class_from_score} for a female compound, {100.0*class_frac:.0f}% of the way towards the next classification.\"\n", + ")" + ] } ], "metadata": { @@ -821,7 +893,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.3" } }, "nbformat": 4,