Skip to content
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ player data from SofaScore, FotMob, Transfermarkt, KBStats, and Analyst.
KBStats last-five-match-slot analysis.
- `notebooks/05_predicted_lineups`: live predicted and confirmed lineup
collectors.
- `notebooks/07_squad_optimisation`: optimizer notebooks for Bundesliga Arena
and KickbaseKIS Arena, plus `manually_create_lineup.ipynb` for manual entry.
- `notebooks/07_squad_optimisation`: optimizer notebooks for Bundesliga Arena,
KickbaseKIS Arena, All Limits Arena, and Kickbase.insider Arena, plus
`manually_create_lineup.ipynb` for manual entry.
- `notebooks/tests`: diagnostic notebooks that are not production pipeline steps.
- `data/reference/kickbase`: curated Kickbase scoring and event-frequency rules.
- `outputs/sofascore`: SofaScore match IDs, odds, team reference, form, team
Expand Down
1 change: 1 addition & 0 deletions data/reference/kickbase_player_name_cross_references.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
schalke,Timo Becker,T. BECKER,2026-08-19T13:29:03+02:00
bremen,Paul Erevbenagie,Erebenagie,2026-08-19T13:29:07+02:00
leipzig,Maarten Vandevoordt,VANDEVOORT,2026-08-19T13:29:30+02:00
schalke,Vitalie Becker,V. BECKER,2026-08-25T12:10:49+02:00
9 changes: 9 additions & 0 deletions data/reference/player_name_cross_references.csv
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ sofascore,,Lukeba Castello Jr.,976421,Castello Lukeba,2026-08-18T19:50:00+02:00
sofascore,,Frederik Rönnow,137692,Frederik Rønnow,2026-08-18T23:06:02+02:00
sofascore,,Lukasz Poreba,942336,Łukasz Poręba,2026-08-23T00:53:03+02:00
sofascore,,Florian Le Joncour,787816,Florian Yves Le Joncour,2026-08-23T00:53:06+02:00
sofascore,,Lukas Emanuel Petkov,960017,Lukas Petkov,2026-08-23T18:00:56+02:00
sofascore,,Albert Frank Millgramm,2122582,Albert Millgramm,2026-08-23T20:56:09+02:00
sofascore,,Éric Dina Ebimbe,904994,Eric Ebimbe,2026-08-25T12:10:25+02:00
sofascore,,Tom Rothe,1210666,Tom Alexander Rothe,2026-08-25T12:11:32+02:00
ligainsider,bayern,Kim Minjae,27022,Minjae Kim,2026-08-18T19:37:20+02:00
ligainsider,leverkusen,Ezequiél Fernández,38674,Equi Fernandez,2026-08-18T19:45:31+02:00
ligainsider,frankfurt,Oscar Højlund,37210,Oscar Hoejlund,2026-08-18T19:47:54+02:00
Expand Down Expand Up @@ -73,3 +77,8 @@ ligainsider,schalke,Éric Dina Ebimbe,26881,Junior Dina Ebimbe,2026-08-18T23:08:
ligainsider,stuttgart,Jeff Chabot,12583,Julian Chabot,2026-08-19T09:19:24+02:00
ligainsider,leipzig,Rômulo,38565,Romulo Cardoso,2026-08-19T09:19:26+02:00
ligainsider,bremen,Niclas Füllkrug,1867,Niclas Fuellkrug,2026-08-23T00:52:59+02:00
ligainsider,frankfurt,Ayoube Amaimouni,38710,Ayoube Amaimouni Echghouyab,2026-08-23T18:00:58+02:00
rotowire,mainz,Jae-Sung Lee,32009,Lee Jae-Sung,2026-08-23T18:01:22+02:00
rotowire,paderborn,Albert Frank Millgramm,48815,Albert Millgramm,2026-08-23T20:56:05+02:00
ligainsider,frankfurt,Mario Götze,501,Mario Goetze,2026-08-25T12:10:24+02:00
rotowire,frankfurt,Ayoube Amaimouni,47580,Ayoube Amaimouni-Echghouyab,2026-08-25T12:10:34+02:00
62 changes: 50 additions & 12 deletions manual_lineup_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ def prepare_optimization_data(
# Manual-only arena, formation, name-resolution, validation, and display helpers.
from dataclasses import dataclass
from difflib import SequenceMatcher
from typing import Iterable
from typing import Iterable, Mapping

from sofascore_average_rating_score import FUZZY_MATCH_THRESHOLD, MAX_PROMPT_CANDIDATES, normalize_name

Expand All @@ -908,16 +908,49 @@ class ArenaRules:
budget_eur: int
max_players_per_club: int
max_players_per_match: int = 4
squad_size: int = 11
formations: Mapping[str, Mapping[str, int]] | None = None
selected_lineup_filename: str | None = None


INSIDER_ARENA_FORMATIONS = {
"2-2-1": {"DEF": 2, "MID": 2, "FOR": 1},
"2-1-2": {"DEF": 2, "MID": 1, "FOR": 2},
"1-2-2": {"DEF": 1, "MID": 2, "FOR": 2},
}


ARENA_RULES = {
"Bundesliga Arena": ArenaRules("Bundesliga Arena", 250_000_000, 3),
"KickbaseKIS Arena": ArenaRules("KickbaseKIS Arena", 150_000_000, 2),
"Bundesliga Arena": ArenaRules(
"Bundesliga Arena", 250_000_000, 3, formations=ALLOWED_FORMATIONS
),
"KickbaseKIS Arena": ArenaRules(
"KickbaseKIS Arena", 150_000_000, 2, formations=ALLOWED_FORMATIONS
),
"All Limits Arena": ArenaRules(
"All Limits Arena",
150_000_000,
1,
max_players_per_match=2,
formations=ALLOWED_FORMATIONS,
selected_lineup_filename="all-limits-arena.json",
),
"Kickbase.insider Arena": ArenaRules(
"Kickbase.insider Arena",
180_000_000,
1,
max_players_per_match=2,
squad_size=6,
formations=INSIDER_ARENA_FORMATIONS,
selected_lineup_filename="kickbase.insider-arena.json",
),
}
ARENA_ALIASES = {
"bundesligaarena": "Bundesliga Arena",
"kickbasekisarena": "KickbaseKIS Arena",
"kickbasekisarena": "KickbaseKIS Arena",
"alllimitsarena": "All Limits Arena",
"kickbaseinsiderarena": "Kickbase.insider Arena",
}


Expand Down Expand Up @@ -956,16 +989,17 @@ def normalize_formation(value: object) -> str:
return re.sub(r"\s+", "", text)


def request_formation() -> tuple[str, dict[str, int]]:
"""Prompt until a project-supported eleven-player formation is entered."""
def request_formation(arena: ArenaRules) -> tuple[str, dict[str, int]]:
"""Prompt until a formation valid for the chosen arena is entered."""
formations = arena.formations or ALLOWED_FORMATIONS
while True:
answer = normalize_formation(input(f"Formation ({', '.join(ALLOWED_FORMATIONS)}): "))
counts = ALLOWED_FORMATIONS.get(answer)
if counts is not None and sum(counts.values()) + 1 == 11:
answer = normalize_formation(input(f"Formation ({', '.join(formations)}): "))
counts = formations.get(answer)
if counts is not None and sum(counts.values()) + 1 == arena.squad_size:
return answer, dict(counts)
print(
f"{answer!r} is not a supported formation. "
f"Choose one of: {', '.join(ALLOWED_FORMATIONS)}."
f"Choose one of: {', '.join(formations)}."
)


Expand Down Expand Up @@ -1096,15 +1130,19 @@ def validate_manual_lineup(
) -> dict[str, object]:
"""Validate every optimizer rule without running the optimizer."""
checks: list[dict[str, object]] = []
expected = {"GK": 1, **ALLOWED_FORMATIONS[formation]}
formations = arena.formations or ALLOWED_FORMATIONS
expected = {"GK": 1, **formations[formation]}
actual_positions = prepared.positions.loc[selected_indices].value_counts().to_dict()
formation_ok = len(selected_indices) == 11 and all(
formation_ok = len(selected_indices) == arena.squad_size and all(
actual_positions.get(position, 0) == count for position, count in expected.items()
)
checks.append({
"rule": "Formation and squad size",
"passed": formation_ok,
"details": f"actual={actual_positions}; expected={expected}; players={len(selected_indices)}/11",
"details": (
f"actual={actual_positions}; expected={expected}; "
f"players={len(selected_indices)}/{arena.squad_size}"
),
"budget": False,
})
ids = prepared.df.loc[selected_indices, prepared.columns["player_id"]].astype(str).str.strip()
Expand Down
14 changes: 7 additions & 7 deletions notebooks/02_odds/02_fotmob_match_odds.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -869,9 +869,9 @@
" 'away_team': 'VfB Stuttgart',\n",
" 'bookmakers': [{'bookmaker': 'Tipico',\n",
" 'bookmaker_source_id': 171,\n",
" 'home_win': 1.27,\n",
" 'draw': 6.7,\n",
" 'away_win': 8.5}],\n",
" 'home_win': 1.25,\n",
" 'draw': 7.0,\n",
" 'away_win': 9.0}],\n",
" 'issues': []},\n",
" {'match_id': 5881146,\n",
" 'home_team': 'Elversberg',\n",
Expand All @@ -887,9 +887,9 @@
" 'away_team': 'Hoffenheim',\n",
" 'bookmakers': [{'bookmaker': 'Tipico',\n",
" 'bookmaker_source_id': 171,\n",
" 'home_win': 3.1,\n",
" 'home_win': 3.0,\n",
" 'draw': 3.6,\n",
" 'away_win': 2.2}],\n",
" 'away_win': 2.25}],\n",
" 'issues': []},\n",
" {'match_id': 5881149,\n",
" 'home_team': 'Mainz 05',\n",
Expand Down Expand Up @@ -942,8 +942,8 @@
" 'bookmakers': [{'bookmaker': 'Tipico',\n",
" 'bookmaker_source_id': 171,\n",
" 'home_win': 2.15,\n",
" 'draw': 3.7,\n",
" 'away_win': 3.1}],\n",
" 'draw': 3.6,\n",
" 'away_win': 3.2}],\n",
" 'issues': []}]\n",
"\n",
"Summary\n",
Expand Down
Loading
Loading