From ce93728aeec657836b6819f22ea0967f05acf840 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 6 May 2024 11:53:30 +0100 Subject: [PATCH 01/13] First implementation of classification fraction function. --- .../agb_indoor_classifications.py | 65 ++++++ .../agb_outdoor_classifications.py | 101 ++++++++- .../classifications/tests/test_agb_indoor.py | 167 +++++++++++++++ .../classifications/tests/test_agb_outdoor.py | 195 ++++++++++++++++++ 4 files changed, 527 insertions(+), 1 deletion(-) diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index dda1eae0..466c86b7 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -310,3 +310,68 @@ def agb_indoor_classification_scores( int_class_scores[i] += 1 return int_class_scores + + +def 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: + + >>> import archeryutils as au + >>> au.classifications.agb_indoor_classifications.classification_fraction( + ... 525, "wa18", "recurve", "male", "adult" + ... ) + 0.6005602030896947 + + """ + 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: + return 1.0 + if loc == len(group_hcs): + 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 cdcc9660..b561dea3 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -7,7 +7,7 @@ agb_outdoor_classification_scores """ -from typing import Any, Literal, TypedDict, cast +from typing import Any, Literal, Optional, TypedDict, cast import numpy as np import numpy.typing as npt @@ -567,3 +567,102 @@ def agb_outdoor_classification_scores( int_class_scores = [int(x) for x in class_scores] return int_class_scores + + +def classification_fraction( # noqa: PLR0913 Too many arguments + score: float, + roundname: str, + bowstyle: str, + gender: str, + age_group: str, + restrict: Optional[bool] = 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: + + >>> import archeryutils as au + >>> au.classifications.agb_outdoor_classifications.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: + + >>> au.classifications.agb_outdoor_classifications.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: + + >>> au.classifications.agb_outdoor_classifications.classification_fraction( + ... 620, + ... "wa720_50_b", + ... "recurve", + ... "male", + ... "adult" + ... restrict=False + ... ) + + """ + 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"] + + loc = 0 + while loc < len(group_hcs): + if handicap < group_hcs[loc]: + break + loc += 1 + + if loc == 0: + return 1.0 + if loc == len(group_hcs): + 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 1f654117..2aa5079e 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 @@ -420,3 +421,169 @@ 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.7661562067030987, + ), + ( + "wa18", + 425, + "adult", + "barebow", + 0.5975078167952219, + ), + ( + "wa18", + 450, + "adult", + "compound", + 0.7661562067030987, + ), + ( + "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 = ( + au.classifications.agb_indoor_classifications.classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + ) + + assert frac_returned == 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 = ( + au.classifications.agb_indoor_classifications.classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + ) + + assert frac_returned == 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 = ( + au.classifications.agb_indoor_classifications.classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + ) + + assert frac_returned == frac_expected diff --git a/archeryutils/classifications/tests/test_agb_outdoor.py b/archeryutils/classifications/tests/test_agb_outdoor.py index 777bed03..3b304c07 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 @@ -565,3 +566,197 @@ 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 = ( + au.classifications.agb_outdoor_classifications.classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + ) + + assert frac_returned == 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 = ( + au.classifications.agb_outdoor_classifications.classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + ) + + assert frac_returned == 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 = ( + au.classifications.agb_outdoor_classifications.classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, + ) + ) + + assert frac_returned == frac_expected + + def test_agb_outdoor_classification_fraction_restrict( + self, + ) -> None: + """Check that classification fraction functions with restrict.""" + frac_restricted = ( + au.classifications.agb_outdoor_classifications.classification_fraction( + score=620, + roundname="national", + bowstyle="recurve", + gender="male", + age_group="adult", + ) + ) + assert frac_restricted == 1.0 + + frac_unrestricted = ( + au.classifications.agb_outdoor_classifications.classification_fraction( + score=620, + roundname="national", + bowstyle="recurve", + gender="male", + age_group="adult", + restrict=False, + ) + ) + + assert frac_unrestricted == 0.4541258975704667 From e03cb27a3133564bc043aceb0093e2d529172a50 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Thu, 9 May 2024 07:23:52 +0100 Subject: [PATCH 02/13] Bug fix in classification fraction where max score might not exceed top classification. --- archeryutils/classifications/agb_indoor_classifications.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index 466c86b7..7a3d651b 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -370,8 +370,13 @@ def classification_fraction( break loc += 1 - if loc == 0: + if loc == 0 or (score == ALL_INDOOR_ROUNDS[roundname].max_score()): + # Handicap below max classification possible + # OR + # max score (but does not achieve highest classification) 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]) From ddbdda14309a970ff28972b172ec8bda5da3b8c4 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 17:14:17 +0100 Subject: [PATCH 03/13] Rename functions to match classification type and add to init. --- archeryutils/classifications/__init__.py | 4 ++++ .../classifications/agb_indoor_classifications.py | 6 +++--- .../classifications/agb_outdoor_classifications.py | 10 +++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/archeryutils/classifications/__init__.py b/archeryutils/classifications/__init__.py index 552aae06..e7e2a842 100644 --- a/archeryutils/classifications/__init__.py +++ b/archeryutils/classifications/__init__.py @@ -7,6 +7,7 @@ from .agb_indoor_classifications import ( agb_indoor_classification_scores, calculate_agb_indoor_classification, + agb_indoor_classification_fraction, ) from .agb_old_indoor_classifications import ( agb_old_indoor_classification_scores, @@ -15,13 +16,16 @@ from .agb_outdoor_classifications import ( agb_outdoor_classification_scores, calculate_agb_outdoor_classification, + agb_outdoor_classification_fraction, ) __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 7a3d651b..3d080edb 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -312,7 +312,7 @@ def agb_indoor_classification_scores( return int_class_scores -def classification_fraction( +def agb_indoor_classification_fraction( score: float, roundname: str, bowstyle: str, @@ -349,8 +349,8 @@ def classification_fraction( 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: - >>> import archeryutils as au - >>> au.classifications.agb_indoor_classifications.classification_fraction( + >>> from archeryutils import classifications as class_func + >>> class_func.agb_indoor_classification_fraction( ... 525, "wa18", "recurve", "male", "adult" ... ) 0.6005602030896947 diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index b561dea3..8c790698 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -569,7 +569,7 @@ def agb_outdoor_classification_scores( return int_class_scores -def classification_fraction( # noqa: PLR0913 Too many arguments +def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments score: float, roundname: str, bowstyle: str, @@ -610,15 +610,15 @@ def classification_fraction( # noqa: PLR0913 Too many arguments 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: - >>> import archeryutils as au - >>> au.classifications.agb_outdoor_classifications.classification_fraction( + >>> 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: - >>> au.classifications.agb_outdoor_classifications.classification_fraction( + >>> class_func.agb_outdoor_classification_fraction( ... 620, "western", "recurve", "male", "adult" ... ) 1.0 @@ -626,7 +626,7 @@ def classification_fraction( # noqa: PLR0913 Too many arguments If we use restrict=False we ignore the distance and prestige restrictions to use purely the classification handicap values: - >>> au.classifications.agb_outdoor_classifications.classification_fraction( + >>> class_func.agb_outdoor_classification_fraction( ... 620, ... "wa720_50_b", ... "recurve", From 3d8c6e6086c27a1eb07bfd2501a9e47ee25bf04a Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 17:15:20 +0100 Subject: [PATCH 04/13] Bugfix: Ensure full-face compound round used for indoor classification fractions. --- archeryutils/classifications/agb_indoor_classifications.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index 3d080edb..8c6b5ab4 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -357,6 +357,12 @@ def agb_indoor_classification_fraction( """ hc_sys = hc.handicap_scheme("AGB") + + # 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) + handicap = hc_sys.handicap_from_score(score, ALL_INDOOR_ROUNDS[roundname]) groupname = cls_funcs.get_groupname(bowstyle, gender, age_group) From ac978c5db9c6a55d6debb287a6b6b623aed29402 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 17:26:15 +0100 Subject: [PATCH 05/13] Apply formatting and fix test values after bugfix. --- archeryutils/classifications/__init__.py | 4 +- .../classifications/tests/test_agb_indoor.py | 51 +++++-------- .../classifications/tests/test_agb_outdoor.py | 72 ++++++++----------- 3 files changed, 52 insertions(+), 75 deletions(-) diff --git a/archeryutils/classifications/__init__.py b/archeryutils/classifications/__init__.py index e7e2a842..9d49ecda 100644 --- a/archeryutils/classifications/__init__.py +++ b/archeryutils/classifications/__init__.py @@ -5,18 +5,18 @@ calculate_agb_field_classification, ) from .agb_indoor_classifications import ( + agb_indoor_classification_fraction, agb_indoor_classification_scores, calculate_agb_indoor_classification, - agb_indoor_classification_fraction, ) from .agb_old_indoor_classifications import ( agb_old_indoor_classification_scores, calculate_agb_old_indoor_classification, ) from .agb_outdoor_classifications import ( + agb_outdoor_classification_fraction, agb_outdoor_classification_scores, calculate_agb_outdoor_classification, - agb_outdoor_classification_fraction, ) __all__ = [ diff --git a/archeryutils/classifications/tests/test_agb_indoor.py b/archeryutils/classifications/tests/test_agb_indoor.py index 2aa5079e..199be9a0 100644 --- a/archeryutils/classifications/tests/test_agb_indoor.py +++ b/archeryutils/classifications/tests/test_agb_indoor.py @@ -434,7 +434,7 @@ class TestCalculateAgbIndoorClassificationFraction: 450, "adult", "compound", - 0.7661562067030987, + 0.847856946666746, ), ( "wa18", @@ -443,13 +443,6 @@ class TestCalculateAgbIndoorClassificationFraction: "barebow", 0.5975078167952219, ), - ( - "wa18", - 450, - "adult", - "compound", - 0.7661562067030987, - ), ( "portsmouth", 538, @@ -468,14 +461,12 @@ def test_agb_indoor_classification_fraction( # noqa: PLR0913 Too many arguments frac_expected: float, ) -> None: """Check that classification fraction is as expected.""" - frac_returned = ( - au.classifications.agb_indoor_classifications.classification_fraction( - roundname=roundname, - score=score, - bowstyle=bowstyle, - gender="male", - age_group=age_group, - ) + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, ) assert frac_returned == frac_expected @@ -522,14 +513,12 @@ def test_agb_indoor_classification_fraction_low( # noqa: PLR0913 many args frac_expected: float, ) -> None: """Check that classification fraction below lowest classification is 0,0.""" - frac_returned = ( - au.classifications.agb_indoor_classifications.classification_fraction( - roundname=roundname, - score=score, - bowstyle=bowstyle, - gender="male", - age_group=age_group, - ) + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, ) assert frac_returned == frac_expected @@ -576,14 +565,12 @@ def test_agb_indoor_classification_fraction_high( # noqa: PLR0913 Too many args frac_expected: float, ) -> None: """Check that classification fraction above highest classification is 1,0.""" - frac_returned = ( - au.classifications.agb_indoor_classifications.classification_fraction( - roundname=roundname, - score=score, - bowstyle=bowstyle, - gender="male", - age_group=age_group, - ) + frac_returned = class_funcs.agb_indoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, ) assert frac_returned == frac_expected diff --git a/archeryutils/classifications/tests/test_agb_outdoor.py b/archeryutils/classifications/tests/test_agb_outdoor.py index 3b304c07..a99f6892 100644 --- a/archeryutils/classifications/tests/test_agb_outdoor.py +++ b/archeryutils/classifications/tests/test_agb_outdoor.py @@ -613,14 +613,12 @@ def test_agb_outdoor_classification_fraction( # noqa: PLR0913 many args frac_expected: float, ) -> None: """Check that classification fraction is as expected.""" - frac_returned = ( - au.classifications.agb_outdoor_classifications.classification_fraction( - roundname=roundname, - score=score, - bowstyle=bowstyle, - gender="male", - age_group=age_group, - ) + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, ) assert frac_returned == frac_expected @@ -667,14 +665,12 @@ def test_agb_outdoor_classification_fraction_low( # noqa: PLR0913 many args frac_expected: float, ) -> None: """Check that classification fraction below lowest classification is 0,0.""" - frac_returned = ( - au.classifications.agb_outdoor_classifications.classification_fraction( - roundname=roundname, - score=score, - bowstyle=bowstyle, - gender="male", - age_group=age_group, - ) + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, ) assert frac_returned == frac_expected @@ -721,14 +717,12 @@ def test_agb_outdoor_classification_fraction_high( # noqa: PLR0913 many args frac_expected: float, ) -> None: """Check that classification fraction above highest classification is 1,0.""" - frac_returned = ( - au.classifications.agb_outdoor_classifications.classification_fraction( - roundname=roundname, - score=score, - bowstyle=bowstyle, - gender="male", - age_group=age_group, - ) + frac_returned = class_funcs.agb_outdoor_classification_fraction( + roundname=roundname, + score=score, + bowstyle=bowstyle, + gender="male", + age_group=age_group, ) assert frac_returned == frac_expected @@ -737,26 +731,22 @@ def test_agb_outdoor_classification_fraction_restrict( self, ) -> None: """Check that classification fraction functions with restrict.""" - frac_restricted = ( - au.classifications.agb_outdoor_classifications.classification_fraction( - score=620, - roundname="national", - bowstyle="recurve", - gender="male", - age_group="adult", - ) + 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 = ( - au.classifications.agb_outdoor_classifications.classification_fraction( - score=620, - roundname="national", - bowstyle="recurve", - gender="male", - age_group="adult", - restrict=False, - ) + frac_unrestricted = class_funcs.agb_outdoor_classification_fraction( + score=620, + roundname="national", + bowstyle="recurve", + gender="male", + age_group="adult", + restrict=False, ) assert frac_unrestricted == 0.4541258975704667 From 58c8ab816e47ad5831d61d711f496bcaa5a4cd65 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 17:30:47 +0100 Subject: [PATCH 06/13] Apply latest ruff linting rules. --- archeryutils/handicaps/tests/test_handicaps.py | 2 +- archeryutils/utils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/archeryutils/handicaps/tests/test_handicaps.py b/archeryutils/handicaps/tests/test_handicaps.py index 618f7336..e4a90e05 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 d23f5221..b216d01d 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: From 1bfbd160dcae328d7a5d1172a9769b322974b11a Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 17:38:15 +0100 Subject: [PATCH 07/13] Update coverage workflow with new token. --- .github/workflows/coverage.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index fc9a24dd..9d86be13 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 }} From 29fce0101540bff673e56828cc22adabdf24dd71 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 17:53:57 +0100 Subject: [PATCH 08/13] Update typing and close assertions in tests as suggested by @LiamPattinson. --- .../agb_outdoor_classifications.py | 4 ++-- .../classifications/tests/test_agb_indoor.py | 16 ++++++++-------- .../classifications/tests/test_agb_outdoor.py | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index 8c790698..e288e9bb 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -7,7 +7,7 @@ agb_outdoor_classification_scores """ -from typing import Any, Literal, Optional, TypedDict, cast +from typing import Any, Literal, TypedDict, cast import numpy as np import numpy.typing as npt @@ -575,7 +575,7 @@ def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments bowstyle: str, gender: str, age_group: str, - restrict: Optional[bool] = True, + restrict: bool | None = True, ) -> float: """ Calculate the fraction towards the next classification an archer is. diff --git a/archeryutils/classifications/tests/test_agb_indoor.py b/archeryutils/classifications/tests/test_agb_indoor.py index 199be9a0..e2eb924d 100644 --- a/archeryutils/classifications/tests/test_agb_indoor.py +++ b/archeryutils/classifications/tests/test_agb_indoor.py @@ -87,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", @@ -128,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", @@ -160,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", @@ -192,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", @@ -228,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", @@ -469,7 +469,7 @@ def test_agb_indoor_classification_fraction( # noqa: PLR0913 Too many arguments age_group=age_group, ) - assert frac_returned == frac_expected + assert frac_returned == pytest.approx(frac_expected) @pytest.mark.parametrize( "roundname,score,age_group,bowstyle,frac_expected", @@ -521,7 +521,7 @@ def test_agb_indoor_classification_fraction_low( # noqa: PLR0913 many args age_group=age_group, ) - assert frac_returned == frac_expected + assert frac_returned == pytest.approx(frac_expected) @pytest.mark.parametrize( "roundname,score,age_group,bowstyle,frac_expected", @@ -573,4 +573,4 @@ def test_agb_indoor_classification_fraction_high( # noqa: PLR0913 Too many args age_group=age_group, ) - assert frac_returned == frac_expected + 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 a99f6892..9ec628f8 100644 --- a/archeryutils/classifications/tests/test_agb_outdoor.py +++ b/archeryutils/classifications/tests/test_agb_outdoor.py @@ -97,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", @@ -143,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", @@ -201,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", @@ -241,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", @@ -269,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", @@ -621,7 +621,7 @@ def test_agb_outdoor_classification_fraction( # noqa: PLR0913 many args age_group=age_group, ) - assert frac_returned == frac_expected + assert frac_returned == pytest.approx(frac_expected) @pytest.mark.parametrize( "roundname,score,age_group,bowstyle,frac_expected", @@ -673,7 +673,7 @@ def test_agb_outdoor_classification_fraction_low( # noqa: PLR0913 many args age_group=age_group, ) - assert frac_returned == frac_expected + assert frac_returned == pytest.approx(frac_expected) @pytest.mark.parametrize( "roundname,score,age_group,bowstyle,frac_expected", @@ -725,7 +725,7 @@ def test_agb_outdoor_classification_fraction_high( # noqa: PLR0913 many args age_group=age_group, ) - assert frac_returned == frac_expected + assert frac_returned == pytest.approx(frac_expected) def test_agb_outdoor_classification_fraction_restrict( self, @@ -749,4 +749,4 @@ def test_agb_outdoor_classification_fraction_restrict( restrict=False, ) - assert frac_unrestricted == 0.4541258975704667 + assert frac_unrestricted == pytest.approx(0.4541258975704667) From a6d0202b3a73879753430206cf79d4d308fed912 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 20:48:19 +0100 Subject: [PATCH 09/13] Add code to catch when a score is on the boundary for classification fraction. --- .../agb_indoor_classifications.py | 23 +++++++++++++++---- .../agb_outdoor_classifications.py | 17 ++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index 8c6b5ab4..9726b5f3 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -356,13 +356,28 @@ def agb_indoor_classification_fraction( 0.6005602030896947 """ - hc_sys = hc.handicap_scheme("AGB") - # 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). + # Note this section is operating under `restrict=True` + 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) @@ -376,10 +391,8 @@ def agb_indoor_classification_fraction( break loc += 1 - if loc == 0 or (score == ALL_INDOOR_ROUNDS[roundname].max_score()): + if loc == 0: # Handicap below max classification possible - # OR - # max score (but does not achieve highest classification) return 1.0 if loc == len(group_hcs): # Handicap above lowest classification diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index e288e9bb..c33e459b 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -636,6 +636,22 @@ def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments ... ) """ + # 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 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]) @@ -655,6 +671,7 @@ def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments 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]: From 28e07511ccbf62e1e8790c87c6e48cbd424b1b7b Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 21:15:54 +0100 Subject: [PATCH 10/13] Add tests for scores on boundary and account for restrict=False in outdoor with boundary scores. --- .../agb_indoor_classifications.py | 10 ++-- .../agb_outdoor_classifications.py | 14 +++-- .../classifications/tests/test_agb_indoor.py | 56 ++++++++++++++++++- .../classifications/tests/test_agb_outdoor.py | 56 ++++++++++++++++++- 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index 9726b5f3..a026143d 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -362,16 +362,18 @@ def agb_indoor_classification_fraction( 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). - # Note this section is operating under `restrict=True` + # 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(): + 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 diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index c33e459b..1a874ac4 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -637,16 +637,22 @@ def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments """ # 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` + # 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 score >= np.abs(all_class_scores[0]) or score == ALL_OUTDOOR_ROUNDS[roundname].max_score(): + if restrict: + 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 diff --git a/archeryutils/classifications/tests/test_agb_indoor.py b/archeryutils/classifications/tests/test_agb_indoor.py index e2eb924d..6a921651 100644 --- a/archeryutils/classifications/tests/test_agb_indoor.py +++ b/archeryutils/classifications/tests/test_agb_indoor.py @@ -512,7 +512,7 @@ def test_agb_indoor_classification_fraction_low( # noqa: PLR0913 many args bowstyle: str, frac_expected: float, ) -> None: - """Check that classification fraction below lowest classification is 0,0.""" + """Check that classification fraction below lowest classification is 0.0.""" frac_returned = class_funcs.agb_indoor_classification_fraction( roundname=roundname, score=score, @@ -564,7 +564,59 @@ def test_agb_indoor_classification_fraction_high( # noqa: PLR0913 Too many args bowstyle: str, frac_expected: float, ) -> None: - """Check that classification fraction above highest classification is 1,0.""" + """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, diff --git a/archeryutils/classifications/tests/test_agb_outdoor.py b/archeryutils/classifications/tests/test_agb_outdoor.py index 9ec628f8..b0fb0923 100644 --- a/archeryutils/classifications/tests/test_agb_outdoor.py +++ b/archeryutils/classifications/tests/test_agb_outdoor.py @@ -664,7 +664,7 @@ def test_agb_outdoor_classification_fraction_low( # noqa: PLR0913 many args bowstyle: str, frac_expected: float, ) -> None: - """Check that classification fraction below lowest classification is 0,0.""" + """Check that classification fraction below lowest classification is 0.0.""" frac_returned = class_funcs.agb_outdoor_classification_fraction( roundname=roundname, score=score, @@ -716,7 +716,59 @@ def test_agb_outdoor_classification_fraction_high( # noqa: PLR0913 many args bowstyle: str, frac_expected: float, ) -> None: - """Check that classification fraction above highest classification is 1,0.""" + """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, From b839eeea9ea51a874555f11626a1e99a27e204dd Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 21:17:17 +0100 Subject: [PATCH 11/13] Add examples for classification fraction to notebook. --- examples.ipynb | 74 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/examples.ipynb b/examples.ipynb index c978f82b..79117373 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, From 042dd6773fe8f90afc477cddcfda9a09cc75dcd9 Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 21:22:44 +0100 Subject: [PATCH 12/13] Add comment to explain unclear fragment. --- archeryutils/classifications/agb_outdoor_classifications.py | 1 + 1 file changed, 1 insertion(+) diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index 1a874ac4..9ab42b9e 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -647,6 +647,7 @@ def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments 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 ( From bcdda676f63390d03d30766ef4adca28bea99a8b Mon Sep 17 00:00:00 2001 From: Jack Atkinson Date: Mon, 15 Jul 2024 21:49:39 +0100 Subject: [PATCH 13/13] Add test for unrestricted fraction check but above max classification score in outdoor and ignore related line in indoor that will not be triggered. --- .../classifications/agb_indoor_classifications.py | 2 +- .../agb_outdoor_classifications.py | 2 ++ .../classifications/tests/test_agb_outdoor.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/archeryutils/classifications/agb_indoor_classifications.py b/archeryutils/classifications/agb_indoor_classifications.py index a026143d..316763b1 100644 --- a/archeryutils/classifications/agb_indoor_classifications.py +++ b/archeryutils/classifications/agb_indoor_classifications.py @@ -395,7 +395,7 @@ def agb_indoor_classification_fraction( if loc == 0: # Handicap below max classification possible - return 1.0 + 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 diff --git a/archeryutils/classifications/agb_outdoor_classifications.py b/archeryutils/classifications/agb_outdoor_classifications.py index 9ab42b9e..a0c5c629 100644 --- a/archeryutils/classifications/agb_outdoor_classifications.py +++ b/archeryutils/classifications/agb_outdoor_classifications.py @@ -686,7 +686,9 @@ def agb_outdoor_classification_fraction( # noqa: PLR0913 Too many arguments 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_outdoor.py b/archeryutils/classifications/tests/test_agb_outdoor.py index b0fb0923..df7d0881 100644 --- a/archeryutils/classifications/tests/test_agb_outdoor.py +++ b/archeryutils/classifications/tests/test_agb_outdoor.py @@ -802,3 +802,18 @@ def test_agb_outdoor_classification_fraction_restrict( ) 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)