Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions archeryutils/classifications/agb_indoor_classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,73 @@ 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 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])
101 changes: 100 additions & 1 deletion archeryutils/classifications/agb_outdoor_classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
agb_outdoor_classification_scores
"""

from typing import Any, Literal, TypedDict, cast
from typing import Any, Literal, Optional, TypedDict, cast
Comment thread
jatkinson1000 marked this conversation as resolved.
Outdated

import numpy as np
import numpy.typing as npt
Expand Down Expand Up @@ -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])
167 changes: 167 additions & 0 deletions archeryutils/classifications/tests/test_agb_indoor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

import archeryutils as au
import archeryutils.classifications as class_funcs
from archeryutils import load_rounds

Expand Down Expand Up @@ -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
Loading