Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class fraction #91

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions archeryutils/classifications/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -13,15 +14,18 @@
calculate_agb_old_indoor_classification,
)
from .agb_outdoor_classifications import (
agb_outdoor_classification_fraction,
agb_outdoor_classification_scores,
calculate_agb_outdoor_classification,
)

__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",
Expand Down
76 changes: 76 additions & 0 deletions archeryutils/classifications/agb_indoor_classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,79 @@ 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

"""
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)
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

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 agb_outdoor_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:

>>> 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
... )

"""
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])
154 changes: 154 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,156 @@ 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 == 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 == 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 == frac_expected
Loading
Loading