Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/attributes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

- uses: actions/setup-python@v4
with:
python-version: "3.12"
python-version: "3.13"

- name: Install package
run: python -m pip install .
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12"] # test oldest and latest supported versions
python-version: ["3.10", "3.13"] # test oldest and latest supported versions
runs-on: [ubuntu-latest, macos-latest] # can be extended to other OSes, e.g. [ubuntu-latest, macos-latest, windows-latest]

steps:
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.13
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12-slim-bookworm
FROM python:3.13-slim-bookworm

WORKDIR /airsenal
COPY . /airsenal
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ Our own AIrsenal team's ID for the 2025/26 season is **[742663](https://fantasy.

## Installation

⚠️ Due to a dependency that currently forces AIrsenal to use an older version of `jaxlib`, AIrsenal doesn't work on Python 3.13 or later.

We recommend using [uv](https://docs.astral.sh/uv/) for managing Python versions and dependencies. For instructions on how to install uv, go to: https://docs.astral.sh/uv/getting-started/installation/.

### Installation from source [Recommended]
Expand Down
4 changes: 4 additions & 0 deletions airsenal/framework/FPL_scoring_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

saves_for_point = 3

def_cons_required = {"GK": 999, "DEF": 10, "MID": 12, "FWD": 12}

points_for_def_cons = 2


def get_appearance_points(minutes):
"""
Expand Down
90 changes: 90 additions & 0 deletions airsenal/framework/prediction_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
import numpy as np
import pandas as pd
from scipy.stats import multinomial
from sqlalchemy import and_
from sqlalchemy.orm.session import Session

from airsenal.framework.FPL_scoring_rules import (
def_cons_required,
get_appearance_points,
points_for_assist,
points_for_cs,
points_for_def_cons,
points_for_goal,
points_for_red_card,
points_for_yellow_card,
Expand Down Expand Up @@ -296,6 +299,33 @@ def get_bonus_points(
return df_bonus[1].loc[player_id]


def get_def_con_points(
player_id: int, minutes: int | float, df_def_con: tuple[pd.Series, pd.Series]
) -> float:
"""
Returns expected defensive contribution points scored by player_id when playing
minutes minutes.

df_def_con : Tuple containing df of average def_con_pts scored when playing at least
60 minutes in 1st index, and when playing between 30 and 60 minutes in 2nd index
(as calculated by fit_def_con_points()).

NOTE: Minutes values are currently hardcoded - this function and fit_def_con_points
must be changed together.
"""
if minutes >= 60 and player_id in df_def_con[0].index:
print("Def cons points for", player_id, df_def_con[0].loc[player_id]) # DEBUG
return df_def_con[0].loc[player_id]
if (
minutes >= 60
or (minutes >= 30 and player_id not in df_def_con[1].index)
or minutes < 30
):
return 0
print("Def cons points for", player_id, df_def_con[1].loc[player_id]) # DEBUG
return df_def_con[1].loc[player_id]


def get_save_points(
position: str, player_id: int, minutes: int | float, df_saves: pd.Series
) -> float:
Expand Down Expand Up @@ -331,6 +361,7 @@ def calc_predicted_points_for_player(
df_bonus: tuple[pd.Series, pd.Series] | None,
df_saves: pd.Series | None,
df_cards: pd.Series | None,
df_def_con: tuple[pd.Series, pd.Series] | None,
season: str,
gw_range: list[int] | None = None,
fixtures_behind: int | None = None,
Expand Down Expand Up @@ -457,6 +488,8 @@ def calc_predicted_points_for_player(
points += get_save_points(
position, player.player_id, mins, df_saves
)
if df_def_con is not None:
points += get_def_con_points(player.player_id, mins, df_def_con)

points /= len(recent_minutes)

Expand All @@ -479,6 +512,7 @@ def calc_predicted_points_for_pos(
df_bonus: tuple[pd.Series, pd.Series] | None,
df_saves: pd.Series | None,
df_cards: pd.Series | None,
df_def_con: tuple[pd.Series, pd.Series] | None,
season: str,
gw_range: list[int],
tag: str,
Expand All @@ -498,6 +532,7 @@ def calc_predicted_points_for_pos(
df_bonus=df_bonus,
df_saves=df_saves,
df_cards=df_cards,
df_def_con=df_def_con,
season=season,
gw_range=gw_range,
tag=tag,
Expand Down Expand Up @@ -679,6 +714,7 @@ def get_player_scores(
gameweek: int,
min_minutes: int = 0,
max_minutes: int = 90,
position: str | None = None,
dbsession: Session = session,
) -> pd.DataFrame:
"""
Expand All @@ -691,6 +727,16 @@ def get_player_scores(
.filter(PlayerScore.minutes <= max_minutes)
.join(Fixture)
)
if position:
query = query.join(
PlayerAttributes,
and_(
PlayerAttributes.player_id == PlayerScore.player_id,
PlayerAttributes.season == Fixture.season,
PlayerAttributes.gameweek == Fixture.gameweek,
),
).filter(PlayerAttributes.position == position)

df = pd.read_sql(query.statement, dbsession.connection())

is_fut = partial(is_future_gameweek, current_season=season, next_gameweek=gameweek)
Expand Down Expand Up @@ -805,3 +851,47 @@ def fit_card_points(
)

return mean_group_min_count(df, "player_id", "card_pts", min_count=min_matches)


def fit_def_con(
gameweek: int = NEXT_GAMEWEEK,
season: str = CURRENT_SEASON,
min_matches: int = 10,
dbsession: Session = session,
) -> tuple[pd.Series, pd.Series]:
"""
Calculate the average defensive contribution points scored by each player for
matches they play between 60 and 90 minutes, and matches they play between 30 and
59 minutes.
Mean is calculated as sum of all bonus points divided by either the number of
maches the player has played in or min_matches, whichever is greater.

Returns tuple of dataframes - first index bonus points for 60 to 90 mins, second
index bonus points for 30 to 59 mins.
"""

def get_def_con_df(min_minutes, max_minutes):
dfs = []
for position in ["DEF", "MID", "FWD"]:
df = get_player_scores(
season,
gameweek,
min_minutes=min_minutes,
max_minutes=max_minutes,
position=position,
dbsession=dbsession,
).dropna(subset="defensive_contribution")
df["def_con_pts"] = (
df["defensive_contribution"] / def_cons_required[position]
).astype(int) * points_for_def_cons
dfs.append(df)

df = pd.concat(dfs)
return mean_group_min_count(
df, "player_id", "def_con_pts", min_count=min_matches
)

df_90 = get_def_con_df(60, 90)
df_60 = get_def_con_df(30, 59)

return (df_90, df_60)
12 changes: 12 additions & 0 deletions airsenal/scripts/fill_predictedscore_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
calc_predicted_points_for_player,
fit_bonus_points,
fit_card_points,
fit_def_con,
fit_save_points,
get_all_fitted_player_data,
)
Expand All @@ -49,6 +50,7 @@ def allocate_predictions(
df_bonus: tuple,
df_saves: Series,
df_cards: Series,
df_def_con: tuple[Series, Series],
season: str,
tag: str,
dbsession: Session,
Expand All @@ -69,6 +71,7 @@ def allocate_predictions(
df_bonus,
df_saves,
df_cards,
df_def_con,
season,
gw_range=gw_range,
tag=tag,
Expand All @@ -86,6 +89,7 @@ def calc_all_predicted_points(
include_bonus: bool = True,
include_cards: bool = True,
include_saves: bool = True,
include_def_con: bool = True,
num_thread: int = 4,
tag: str = "",
player_model: NumpyroPlayerModel | ConjugatePlayerModel | None = None,
Expand Down Expand Up @@ -129,6 +133,10 @@ def calc_all_predicted_points(
df_cards = fit_card_points(gameweek=gw_range[0], season=season)
else:
df_cards = None
if include_def_con:
df_def_con = fit_def_con(gameweek=gw_range[0], season=season)
else:
df_def_con = None

players = list_players(season=season, gameweek=gw_range[0], dbsession=dbsession)

Expand All @@ -146,6 +154,7 @@ def calc_all_predicted_points(
df_bonus,
df_saves,
df_cards,
df_def_con,
season,
tag,
dbsession,
Expand All @@ -172,6 +181,7 @@ def calc_all_predicted_points(
df_bonus,
df_saves,
df_cards,
df_def_con,
season,
gw_range=gw_range,
tag=tag,
Expand All @@ -190,6 +200,7 @@ def make_predictedscore_table(
include_bonus: bool = True,
include_cards: bool = True,
include_saves: bool = True,
include_def_con: bool = True,
tag_prefix: str | None = None,
player_model: NumpyroPlayerModel | ConjugatePlayerModel | None = None,
team_model: ExtendedDixonColesMatchPredictor
Expand All @@ -212,6 +223,7 @@ def make_predictedscore_table(
include_bonus=include_bonus,
include_cards=include_cards,
include_saves=include_saves,
include_def_con=include_def_con,
num_thread=num_thread,
tag=tag,
player_model=player_model,
Expand Down
34 changes: 17 additions & 17 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@ authors = [
]
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.10,<3.13"
requires-python = ">=3.10,<4"
dependencies = [
"beautifulsoup4>=4.13.5",
"bpl @ git+https://github.com/anguswilliams91/bpl-next",
"click>=8.1.7",
"pandas>=2.2.1",
"requests>=2.31.0",
"sqlalchemy>=2.0.27",
"tqdm>=4.66.2",
"dateparser>=1.2.0",
"prettytable>=3.10.0",
"beautifulsoup4>=4.12.3",
"platformdirs>=4.2.0",
"deap>=1.4.1",
"python-dateutil>=2.8.2",
"lxml>=5.1.0",
"html5lib>=1.1",
"jax==0.4.24",
"jaxlib==0.4.24",
"numpy>1,<2",
"click>=8.3.0",
"curl-cffi>=0.13.0",
"dateparser>=1.2.2",
"deap>=1.4.3",
"html5lib>=1.1",
"jax>=0.6.2",
"jaxlib>=0.6.2",
"lxml>=6.0.1",
"numpy>=2.2.6",
"pandas>=2.3.2",
"platformdirs>=4.4.0",
"prettytable>=3.16.0",
"python-dateutil>=2.9.0.post0",
"requests>=2.32.5",
"sqlalchemy>=2.0.43",
"tqdm>=4.67.1",
]

[tool.hatch.metadata]
Expand Down
Loading