# Copyright (C) 2021 Silvan Fischbacher
import os
import numpy as np
import pandas as pd
from fussballgott import team
[docs]def get_teams_from_df(df, columns=["Home", "Away"]):
"""
Get all teams from a DataFrame with a schedule.
:param df: DataFrame with schedule
:param columns: Columns of the DataFrame that contain the teams
:return: Array with all teams
"""
teams = []
for c in columns:
teams.append(df[c])
teams = np.concatenate(teams)
return np.unique(teams)
[docs]def league(files, delimiter=";"):
"""
Load a league from a file or a list of files. Setups the teams classes,
the schedule, the table and the missing games.
Input can be:
- One file with the schedule and results of the games played so far
- Two files, one with the schedule and one with the table
:param files: File or list of files
:param delimiter: Delimiter of the files
:return: teams, schedule, table, missing_games
"""
# only schedule is given
if len(files) == 1 or isinstance(files, str):
schedule_and_results = pd.read_csv(
files, delimiter=delimiter, encoding="unicode_escape"
)
schedule = schedule_and_results[["Home", "Away"]].copy()
teams = get_teams_from_df(schedule_and_results)
table, missing_games = create_table(schedule_and_results, teams)
# schedule and table are given
elif len(files) == 2:
schedule = pd.read_csv(files[0], delimiter=delimiter, encoding="unicode_escape")
table = pd.read_csv(
files[1],
delimiter=delimiter,
encoding="unicode_escape",
index_col=0,
)
teams = get_teams_from_df(table, ["Team"])
missing_games = get_missing_games(table, schedule)
else:
raise ValueError("files must be a list with one or two files or a str")
# create the team classes
teams_dict = {}
for t in teams:
teams_dict[t] = team.Team(
name=t,
GoalsF=table[table["Team"] == t]["GF"].values[0],
GoalsA=table[table["Team"] == t]["GA"].values[0],
played=table[table["Team"] == t]["Played"].values[0],
penalty_scoring=0.75,
)
return teams_dict, schedule, table, missing_games
[docs]def tournament(filename, path="", delimiter=";"):
"""
Load the tournament plan from file and setup team classes. Assuming your filename is
`tournament_`, you should have the following files in your path:
* `tournament_mode.CSV` to describe the mode of the tournament
* `tournament_teams.CSV` to describe the teams playing
* `tournament_{n}.CSV` with n ranging from 2 to 2**x, where x is the number of rounds
played in the tournament. The files contain the schedule of the games played in
the knockout round.
* `tournament_special_rule.CSV` if there is a special rule for the tournament (e.g.
some of the best third placed teams qualify for the next round)
:param filename: Name of the file
:param path: Path to the file
:param delimiter: Delimiter of the file
:return: teams, schedule, table, missing_games
"""
# read out the mode files
mode = (
pd.read_csv(
os.path.join(path, filename + "mode.CSV"),
header=None,
index_col=0,
delimiter=delimiter,
)
.squeeze()
.to_dict()
)
if mode["Group Stage"] == "True":
group_stage = True
elif mode["Group Stage"] == "False":
group_stage = False
else:
raise ValueError(
"Group Stage must be defined as True or False in your mode file"
)
# read out all the knockout round files
ko = int(mode["First knockout round"])
ko_round = []
while ko > 1:
ko_round.append(
pd.read_csv(
os.path.join(path, filename + "{}.CSV".format(ko)), delimiter=delimiter
)
)
ko = int(ko / 2)
# check if there is a special rule file for the knockout round
try:
if int(mode["number of additional teams qualified"]) > 0:
ko_round.append(
pd.read_csv(
os.path.join(path, filename + "special_rule.CSV"),
delimiter=delimiter,
)
)
except Exception:
pass
# build the team table
team_table = pd.read_csv(
os.path.join(path, filename + "teams.CSV"), delimiter=delimiter
)
teams = {}
if group_stage:
groups = dict.fromkeys(team_table["Group"])
def update_group(dic, k, value):
a = dic[k]
if a is None:
return [value]
else:
a.append(value)
return a
# building team classes
for name in team_table["Name"].values:
# check if there is penalty information, otherwise set to default 0.75
try:
penalty = team_table[team_table["Name"] == name]["Penalty Scoring"].values[
0
]
except Exception:
penalty = 0.75
teams[name] = team.Team(
name=name,
GoalsF=team_table[team_table["Name"] == name]["GoalsF"].values[0],
GoalsA=team_table[team_table["Name"] == name]["GoalsA"].values[0],
played=team_table[team_table["Name"] == name]["Played"].values[0],
penalty_scoring=penalty,
)
if group_stage:
g = team_table[team_table["Name"] == name]["Group"].values[0]
groups[g] = update_group(groups, g, name)
if group_stage:
return mode, ko_round, teams, groups
else:
return mode, ko_round, teams
[docs]def create_table(sched_n_r, teams):
"""
Create the table from the schedule and results
:param sched_n_r: Schedule and results
:param teams: List of teams
:return: Table, missing games
"""
games = np.shape(sched_n_r)[0]
missing_games = np.ones(games, dtype=bool)
table = pd.DataFrame(index=teams)
table["Team"] = table.index
table["Played"] = 0
table["GF"] = 0
table["GA"] = 0
table["GD"] = 0
table["Points"] = 0
for i in range(games):
game = sched_n_r[i : i + 1]
if game["Goals Home"].isnull().values[0]:
pass
else:
missing_games[i] = False
table.loc[game["Home"].values[0], "Played"] += 1
table.loc[game["Away"].values[0], "Played"] += 1
table.loc[game["Home"].values[0], "GF"] += game["Goals Home"].values[0]
table.loc[game["Away"].values[0], "GF"] += game["Goals Away"].values[0]
table.loc[game["Home"].values[0], "GA"] += game["Goals Away"].values[0]
table.loc[game["Away"].values[0], "GA"] += game["Goals Home"].values[0]
table.loc[game["Home"].values[0], "GD"] += (
game["Goals Home"].values[0] - game["Goals Away"].values[0]
)
table.loc[game["Away"].values[0], "GD"] += (
game["Goals Away"].values[0] - game["Goals Home"].values[0]
)
if game["Goals Home"].values[0] > game["Goals Away"].values[0]:
table.loc[game["Home"].values[0], "Points"] += 3
if game["Goals Home"].values[0] < game["Goals Away"].values[0]:
table.loc[game["Away"].values[0], "Points"] += 3
if game["Goals Home"].values[0] == game["Goals Away"].values[0]:
table.loc[game["Home"].values[0], "Points"] += 1
table.loc[game["Away"].values[0], "Points"] += 1
"""
table["Played"][game["Home"].values[0]] += 1
table["Played"][game["Away"].values[0]] += 1
table["GF"][game["Home"].values[0]] += game["Goals Home"].values[0]
table["GF"][game["Away"].values[0]] += game["Goals Away"].values[0]
table["GA"][game["Home"].values[0]] += game["Goals Away"].values[0]
table["GA"][game["Away"].values[0]] += game["Goals Home"].values[0]
if game["Goals Home"].values[0] > game["Goals Away"].values[0]:
table["Points"][game["Home"].values[0]] += 3
if game["Goals Home"].values[0] < game["Goals Away"].values[0]:
table["Points"][game["Away"].values[0]] += 3
if game["Goals Home"].values[0] == game["Goals Away"].values[0]:
table["Points"][game["Home"].values[0]] += 1
table["Points"][game["Away"].values[0]] += 1
"""
table["GD"] = table["GF"] - table["GA"]
sorted_table = tiebreaker(table, rule="Goal Difference")
sorted_table.index = np.arange(1, len(teams) + 1)
return sorted_table, missing_games
[docs]def tiebreaker(table, rule="Goal Difference"):
"""
Sort the table according to the tiebreaker rule
:param table: Table to be sorted
:param rule: Tiebreaker rule
:return: Sorted table
"""
if rule == "Goal Difference":
return table.sort_values(["Points", "GD", "GF"], ascending=False)
else:
raise NotImplementedError(f"Tiebreaker rule {rule} not implemented")
[docs]def get_missing_games(table, schedule):
"""
Get the missing games from the schedule
:param table: Table
:param schedule: Schedule
:return: Boolean array with missing games
"""
index = np.zeros_like(schedule["Home"].values, dtype=bool)
teams = get_teams_from_df(schedule, ["Home", "Away"])
for t in teams:
played = table[table["Team"] == t]["Played"].values[0]
t_schedule = schedule[
np.logical_or(schedule["Home"] == t, schedule["Away"] == t)
].index
index[t_schedule[played:]] = True
return index