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 all 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: 3 additions & 1 deletion .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
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
91 changes: 91 additions & 0 deletions archeryutils/classifications/agb_indoor_classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
125 changes: 125 additions & 0 deletions archeryutils/classifications/agb_outdoor_classifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Loading
Loading