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

fix: Remaining SOS was still incorrect #115

Merged
merged 1 commit into from
Oct 10, 2024
Merged
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
19 changes: 11 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Distribution / packaging
.env

*__pycache__*
*.ipynb_checkpoints*
.DS_Store
*.DS_Store
.vscode*
dist/*
*historical_stats.csv

# Notebooks
*.ipynb_checkpoints*
*new espn.ipynb
src/new espn.ipynb
draft_history.csv
src/.DS_Store
league_info_backup.csv
src/tmp.csv
website.ipynb
dev.ipynb

src/tmp.csv
all_league_creds.csv
draft_history.csv
*historical_stats.csv
historical_stats_all_teams.csv
league_info_backup.csv
12 changes: 10 additions & 2 deletions fantasy_stats/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ def league(request, league_id: int, league_year: int, week: int = None):
weekly_awards = django_weekly_stats(league_obj, week)
power_rankings = django_power_rankings(league_obj, week)
luck_index = django_luck_index(league_obj, week)
strength_of_schedule = django_strength_of_schedule(league_obj, week + 1)
strength_of_schedule, schedule_period = django_strength_of_schedule(
league_obj, week
)
standings = django_standings(league_obj, week)

context = {
Expand All @@ -313,6 +315,7 @@ def league(request, league_id: int, league_year: int, week: int = None):
"power_rankings": power_rankings,
"luck_index": luck_index,
"strength_of_schedule": strength_of_schedule,
"sos_weeks": schedule_period,
"standings": standings,
"scores_are_finalized": league_obj.current_week <= week,
}
Expand Down Expand Up @@ -388,14 +391,19 @@ def simulation(
league_obj, n_simulations
)

strength_of_schedule, schedule_period = django_strength_of_schedule(
league_obj, week - 1
)

context = {
"league_info": league_info,
"league": league_obj,
"page_week": week,
"playoff_odds": playoff_odds,
"rank_dist": rank_dist,
"seeding_outcomes": seeding_outcomes,
"strength_of_schedule": django_strength_of_schedule(league_obj, week + 1),
"strength_of_schedule": strength_of_schedule,
"sos_weeks": schedule_period,
"n_positions": len(league_obj.teams),
"positions": [ordinal(i) for i in range(1, len(league_obj.teams) + 1)],
"n_playoff_spots": league_obj.settings.playoff_team_count,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "doritostats"
version = "3.4.16"
version = "3.4.17"
description = "This project aims to make ESPN Fantasy Football statistics easily available. With the introduction of version 3 of the ESPN's API, this structure creates leagues, teams, and player classes that allow for advanced data analytics and the potential for many new features to be added."
authors = ["Desi Pilla <[email protected]>"]
license = "https://github.com/DesiPilla/espn-api-v3/blob/master/LICENSE"
Expand Down
141 changes: 113 additions & 28 deletions src/doritostats/analytic_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def get_remaining_schedule_difficulty(
regular_season_length: int,
strength: str = "points_for",
league: Optional[League] = None,
) -> float:
) -> Tuple[float, Tuple[int, int], Tuple[int, int]]:
"""
This function returns the average score of a team's remaining opponents.

Expand All @@ -245,49 +245,120 @@ def get_remaining_schedule_difficulty(
- "win_pct" means that the difficult is defined by the average winning percentage of each of their remaining opponents.
- "power_rank" means that the difficult is defined by the average power rank of each of their remaining opponents.

Returns:
float: strength of schedule, as defined by the `strength` parameter
Tuple[int, int]: Tuple containing the range of weeks included in calculating a team's opponent's strength
* For example, if the SOS for Week 6 and beyond is desired, this tuple would be (1, 5)
Tuple[int, int]: Tuple containing the range of weeks defining a team's remaining schedule
* For example, if the SOS for Week 6 and beyond is desired, this tuple would be (6, regular_season_length)

"""
if week >= regular_season_length:
return 0
return 0, (0, 0), (0, 0)

# Get the remaining schedule for the team
remaining_schedule = team.schedule[week:regular_season_length]
n_completed_weeks = len(
[o for o in team.outcomes[:regular_season_length] if o != "U"]
)

# How many completed weeks should be included?
strength_weeks_to_consider = min(week, n_completed_weeks)

if strength_weeks_to_consider == 0:
return 0, (0, 0), (0, 0)

# Define the week ranges for calculating the strength of schedule
strength_period = (1, strength_weeks_to_consider)
schedule_period = (
regular_season_length - len(remaining_schedule) + 1,
regular_season_length,
)

if strength == "points_for":
# Get all scores from remaining opponenets through specified week
# Get all scores from remaining opponents through specified week
remaining_strength = np.array(
[opp.scores[:week][:n_completed_weeks] for opp in remaining_schedule]
[opp.scores[:strength_weeks_to_consider] for opp in remaining_schedule]
).flatten()

# Return average score
return remaining_strength.mean()
# # Slower, but easier for dubugging
# remaining_strength = pd.DataFrame(
# [opp.scores[:strength_weeks_to_consider] for opp in remaining_schedule],
# columns=[
# "Week {} score".format(i)
# for i in range(1, strength_weeks_to_consider + 1)
# ],
# index=[
# "Week {} opponent - {}".format(
# regular_season_length - len(remaining_schedule) + i + 1, opp.owner
# )
# for i, opp in enumerate(remaining_schedule)
# ],
# )
# return (remaining_strength, strength_period, schedule_period)

# Return average score and calculation periods
return (remaining_strength.mean(), strength_period, schedule_period)

elif strength == "win_pct":
# Get win pct of remaining opponenets through specified week
# Get win pct of remaining opponents through specified week
remaining_strength = np.array(
[opp.outcomes[:week] for opp in remaining_schedule]
[opp.outcomes[:strength_weeks_to_consider] for opp in remaining_schedule]
).flatten()

# Divide # of wins by (# of wins + # of losses) -- this excludes matches that tied or have not occurred yet
return calculate_win_pct(remaining_strength)
# # Slower, but easier for dubugging
# remaining_strength = pd.DataFrame(
# [
# calculate_win_pct(np.array(opp.outcomes[:strength_weeks_to_consider]))
# for opp in remaining_schedule
# ],
# columns=["Win pct"],
# index=[
# "Week {} opponent - {}".format(
# regular_season_length - len(remaining_schedule) + i + 1, opp.owner
# )
# for i, opp in enumerate(remaining_schedule)
# ],
# )
# return (remaining_strength, strength_period, schedule_period)

# Return average win pct and calculation periods
return (calculate_win_pct(remaining_strength), strength_period, schedule_period)

elif strength == "power_rank":
power_rankings = {t: float(r) for r, t in league.power_rankings(week=week)}
# Get the power ranking from remaining opponents through specified week
power_rankings = {
t: float(r)
for r, t in league.power_rankings(week=strength_weeks_to_consider)
}

# Get all scores from remaining opponenets through specified week
remaining_strength = np.array(
[power_rankings[opp] for opp in remaining_schedule]
).flatten()

# Return average power rank
return remaining_strength.mean()
# # Slower, but easier for dubugging
# remaining_strength = pd.DataFrame(
# [power_rankings[opp] for opp in remaining_schedule],
# columns=["Power rank"],
# index=[
# "Week {} opponent - {}".format(
# regular_season_length - len(remaining_schedule) + i + 1, opp.owner
# )
# for i, opp in enumerate(remaining_schedule)
# ],
# )
# return (remaining_strength, strength_period, schedule_period)

# Return average power rank and calculation periods
return (remaining_strength.mean(), strength_period, schedule_period)

else:
raise Exception("Unrecognized parameter passed for `strength`")


def get_remaining_schedule_difficulty_df(league: League, week: int) -> pd.DataFrame:
def get_remaining_schedule_difficulty_df(
league: League, week: int
) -> Tuple[pd.DataFrame, Tuple[int, int], Tuple[int, int]]:
"""
This function creates a dataframe containing each team's remaining strength of schedule. Strength of schedule is determined by two factors:
- "opp_points_for" is the average points for scored by each of a team's remaining opponents.
Expand All @@ -301,7 +372,11 @@ def get_remaining_schedule_difficulty_df(league: League, week: int) -> pd.DataFr
week (int): First week to include as "remaining". I.e., week = 10 will calculate the remaining SOS for Weeks 10 -> end of season.

Returns:
pd.DataFrame
pd.DataFrame: Dataframe containing the each team's remaining strength of schedule
Tuple[int, int]: Tuple containing the range of weeks included in calculating a team's opponent's strength
* For example, if the SOS for Week 6 and beyond is desired, this tuple would be (1, 5)
Tuple[int, int]: Tuple containing the range of weeks defining a team's remaining schedule
* For example, if the SOS for Week 6 and beyond is desired, this tuple would be (6, regular_season_length)
"""
if (week < 1) or league.current_week < 2:
return pd.DataFrame(
Expand All @@ -323,29 +398,35 @@ def get_remaining_schedule_difficulty_df(league: League, week: int) -> pd.DataFr
remaining_difficulty_dict[team] = {}

# SOS by points for
remaining_difficulty_dict[team][
"opp_points_for"
] = get_remaining_schedule_difficulty(
(
remaining_difficulty_dict[team]["opp_points_for"],
strength_period,
schedule_period,
) = get_remaining_schedule_difficulty(
team,
week,
regular_season_length=league.settings.reg_season_count,
strength="points_for",
) # type: ignore

# SOS by win pct
remaining_difficulty_dict[team][
"opp_win_pct"
] = get_remaining_schedule_difficulty(
(
remaining_difficulty_dict[team]["opp_win_pct"],
_,
_,
) = get_remaining_schedule_difficulty(
team,
week,
regular_season_length=league.settings.reg_season_count,
strength="win_pct",
) # type: ignore

# SOS by win pct
remaining_difficulty_dict[team][
"opp_power_rank"
] = get_remaining_schedule_difficulty(
(
remaining_difficulty_dict[team]["opp_power_rank"],
_,
_,
) = get_remaining_schedule_difficulty(
team,
week,
regular_season_length=league.settings.reg_season_count,
Expand Down Expand Up @@ -385,9 +466,13 @@ def get_remaining_schedule_difficulty_df(league: League, week: int) -> pd.DataFr
["opp_points_for_index", "opp_win_pct_index", "opp_power_rank_index"]
].mean(axis=1)

return remaining_difficulty[
["opp_points_for", "opp_win_pct", "opp_power_rank", "overall_difficulty"]
].sort_values(by="overall_difficulty", ascending=False)
return (
remaining_difficulty[
["opp_points_for", "opp_win_pct", "opp_power_rank", "overall_difficulty"]
].sort_values(by="overall_difficulty", ascending=False),
strength_period,
schedule_period,
)


def sort_lineups_by_func(
Expand Down
14 changes: 9 additions & 5 deletions src/doritostats/django_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime
from typing import List, Optional
from typing import Dict, List, Optional, Tuple, Union
from espn_api.football import League

from fantasy_stats.models import LeagueInfo
Expand Down Expand Up @@ -322,7 +322,9 @@ def django_standings(league: League, week: int):
return standings


def django_strength_of_schedule(league: League, week: int):
def django_strength_of_schedule(
league: League, week: int
) -> Tuple[List[Dict[str, Union[str, float]]], Tuple[int, int]]:
"""This is a helper function to get the remaining strength of schedule for each team in the league.
The results are returned as a list of dictionaries, which is the most conveneint format
for django to render.
Expand All @@ -332,10 +334,12 @@ def django_strength_of_schedule(league: League, week: int):
week (int): First week to include as "remaining". I.e., week = 10 will calculate the remaining SOS for Weeks 10 -> end of season.

Returns:
_type_: _description_
List[Dict[str, Union[str, float]]]: List of dictionaries containing the strength of schedule for each team
Tuple[int, int]: Tuple containing the range of weeks defining a team's remaining schedule
* For example, if the SOS for Week 6 and beyond is desired, this tuple would be (6, regular_season_length)
"""
# Get strength of schedule for the current week
sos_df = get_remaining_schedule_difficulty_df(league, week)
sos_df, _, schedule_period = get_remaining_schedule_difficulty_df(league, week)

# Add the strength of schedule for each team
django_sos = []
Expand All @@ -353,7 +357,7 @@ def django_strength_of_schedule(league: League, week: int):
}
)

return django_sos
return django_sos, schedule_period


def django_simulation(league: League, n_simulations: int):
Expand Down
2 changes: 1 addition & 1 deletion src/doritostats/simulation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,8 @@ def get_seeding_outcomes_df(final_standings: pd.DataFrame) -> pd.DataFrame:

return final_standings_agg.sort_values(
by=[
"first_in_league",
"make_playoffs",
"first_in_league",
"first_in_division",
"last_in_division",
"last_in_league",
Expand Down
5 changes: 4 additions & 1 deletion templates/fantasy_stats/league.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ <h2>Standings</h2>

{# Remaining strength of schedule #}
<div class="wrapper-wide">
<h2>Remaining strength of schedule</h2>
<h2>
Remaining strength of schedule (for Weeks {{ sos_weeks.0 }}-{{ sos_weeks.1 }})
</h2>
{% if scores_are_finalized %}
<em>
<strong>
Expand All @@ -225,6 +227,7 @@ <h2>Remaining strength of schedule</h2>
</strong>
</em>
{% endif %}

<table class="table">
{# Table header #}
<div class="row header">
Expand Down
4 changes: 3 additions & 1 deletion templates/fantasy_stats/simulation.html
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ <h2>Seeding outcomes</h2>



<h2>Remaining strength of schedule</h2>
<h2>
Remaining strength of schedule (for Weeks {{ sos_weeks.0 }}-{{ sos_weeks.1 }})
</h2>
<table class="table">

{# Table header #}
Expand Down
2 changes: 1 addition & 1 deletion tests/test_analytic_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def test_get_remaining_schedule_difficulty(
pytest.approx(
utils.get_remaining_schedule_difficulty(
team, week, regular_season_length, strength, league
),
)[0],
0.0001,
)
== result
Expand Down
Loading