added position calculator with logic, added pre-commit linting, updated requirements and readme, added attribute rating for position calculator

This commit is contained in:
jake-fawcett 2023-11-23 22:24:28 +00:00
parent 4eefd5f29f
commit 574cb795d8
6 changed files with 292 additions and 128 deletions

46
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,46 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-added-large-files
args: [--maxkb=2500]
- id: check-ast
- id: check-case-conflict
- id: check-docstring-first
- id: check-json
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: name-tests-test
args: [--pytest-test-first]
# - id: no-commit-to-branch
# args: [--branch, main]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
args: ["--profile", "black"]
- repo: https://github.com/pre-commit/mirrors-yapf
rev: v0.32.0
hooks:
- id: yapf
additional_dependencies: [toml]
args: [--style "google" ]
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
args: [--line-length, '180']
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
args: [--docstring-convention, google, --max-line-length, '180', --ignore, 'D100,D101,D102,D103,D104']
additional_dependencies: [flake8-bugbear, flake8-docstrings, pydocstyle==6.1.1]
- repo: https://github.com/pycqa/bandit
rev: 1.7.4
hooks:
- id: bandit
args: [--skip, B608]

View File

@ -1,9 +1,16 @@
# Football Manager player role evaluation
Python code to evaluate Player attributes in Football Manager against roles based on role weightings. Work in progress.
Python code to evaluate Player attributes in Football Manager against roles based on role weightings. Work in progress. Inspired by squirrel_plays_FOF's video [FM24 player recruitment using python](https://www.youtube.com/watch?v=hnAuOakqR90)
Inspired by squirrel_plays_FOF's video [FM24 player recruitment using python](https://www.youtube.com/watch?v=hnAuOakqR90)
This is split into two scripts; position_score_calculator.py which calculates score based on positions ([Inspired by Mark on fm-arena](https://fm-arena.com/thread/1949-fm22-positional-filters-what-are-the-best-attributes-for-each-position/)) and role_score_calculator.py which caculates scores based on roles (this is missing plenty of roles at the moment!).
## Usage
position_score_calculator.py:
```
python3 role_score_calculator.py --input-filepath "squad.html" --output-filepath "squad_output.html" --roles gk fb dm w iw
```
role_score_calculator.py:
```
python3 position_score_calculator.py --input-filepath "squad.html" --output-filepath "squad_output.html"
```
python3 position_score_calculator.py --input-filepath "squad.html" --output-filepath "squad_output.html" --roles gk fb dm w iw
```

BIN
attribute_ratings.xlsx Normal file

Binary file not shown.

View File

@ -1,91 +1,35 @@
import pandas as pd
import argparse
# Define Player attributes
# TODO: Add roles.
gk = {
"role_name": "gk",
"primary_multiplier": 5,
"primary_attributes": ["Agi", "Ref"],
"secondary_multiplier": 3,
"secondary_attributes": ["1v1", "Ant", "Cmd", "Cnt", "Kic", "Pos"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Acc", "Aer", "Cmp", "Dec", "Fir", "Han", "Pas", "Thr", "Vis"]
}
import pandas as pd
fb = {
"role_name": "fb",
"primary_multiplier": 5,
"primary_attributes": ["Wor", "Acc", "Pac", "Sta"],
"secondary_multiplier": 3,
"secondary_attributes": ["Cro", "Dri", "Mar", "OtB", "Tck", "Tea"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Agi", "Ant", "Cnt", "Dec", "Fir", "Pas", "Pos", "Tec"]
}
cd = {
"role_name": "cd",
"primary_multiplier": 3,
"primary_attributes": ["Cmp", "Hea", "Jum", "Mar", "Pas", "Pos", "Str", "Tck", "Pac"],
"secondary_multiplier": 1,
"secondary_attributes": ["Agg", "Ant", "Bra", "Cnt", "Dec", "Fir", "Tec", "Vis"]
}
dm = {
"role_name": "dm",
"primary_multiplier": 5,
"primary_attributes": ["Wor", "Pac", "Sta", "Pas"],
"secondary_multiplier": 3,
"secondary_attributes": ["Tck", "Ant", "Cnt", "Pos", "Bal", "Agi"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Tea", "Fir", "Mar", "Agg", "Cmp", "Dec", "Str"]
}
b2b = {
"role_name": "b2b",
"primary_multiplier": 5,
"primary_attributes": ["Pas", "Wor", "Sta"],
"secondary_multiplier": 3,
"secondary_attributes": ["Tck", "OtB", "Tea", "Vis", "Str", "Dec", "Pos", "Pac"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Agg", "Ant", "Fin", "Lon", "Cmp", "Acc", "Bal", "Fir", "Dri", "Tec"]
}
w = {
"role_name": "w",
"primary_multiplier": 3,
"primary_attributes": ["Acc", "Cro", "Dri", "OtB", "Pac", "Tec"],
"secondary_multiplier": 1,
"secondary_attributes": ["Agi", "Fir", "Pas", "Sta", "Wor"],
}
iw = {
"role_name": "iw",
"primary_multiplier": 5,
"primary_attributes": ["Acc", "Pac", "Wor"],
"secondary_multiplier": 3,
"secondary_attributes": ["Dri", "Pas", "Tec", "OtB"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Cro", "Fir", "Cmp", "Dec", "Vis", "Agi", "Sta"]
}
def load_xlsx_data_to_dataframe(filepath: str) -> pd.DataFrame:
"""Read XLSX file into a Dataframe.
Keyword arguments:
filepath -- path to xlsx file
"""
df = pd.read_excel(filepath, engine="openpyxl", nrows=12)
return df
def load_html_data_to_dataframe(filepath: str) -> pd.DataFrame:
"""Read HTML file exported by FM into a Dataframe
"""Read HTML file exported by FM into a Dataframe.
Keyword arguments:
filepath -- path to fm player html file
"""
player_df = pd.read_html(filepath, header=0, encoding="utf-8", keep_default_na=False)[0]
df = pd.read_html(filepath, header=0, encoding="utf-8", keep_default_na=False)[0]
# Clean Dataframe to get rid of unknown values and ability ranges (takes the lowest value)
# This casts to a string to be able to split, so we have to cast back to an int later.
player_df = player_df.replace("-", 0)
player_df = player_df.map(lambda x: str(x).split("-")[0])
return player_df
df = df.replace("-", 0)
df = df.map(lambda x: str(x).split("-")[0])
return df
def export_html_from_dataframe(player_df: pd.DataFrame, filepath: str) -> str:
"""Export Dataframe as html with jQuery Data Tables
"""Export Dataframe as html with jQuery Data Tables.
Taken from: https://www.thepythoncode.com/article/convert-pandas-dataframe-to-html-table-python.
Keyword arguments:
@ -115,71 +59,43 @@ def export_html_from_dataframe(player_df: pd.DataFrame, filepath: str) -> str:
"""
open(filepath, "w", encoding="utf-8").write(html)
# TODO: Do I even want this?
def calc_composite_scores(player_df: pd.DataFrame) -> pd.DataFrame:
"""Calculate Speed, Workrate and Set Piece scores
def calc_role_scores(player_df: pd.DataFrame, attribute_df: pd.DataFrame) -> pd.DataFrame:
"""Calculate Player position scores based on selected attribute weightings.
Keyword arguments:
player_df: Dataframe of Players and Attributes
player_df: Dataframe of Players and their Attributes
attribute_df: Dataframe of Attributes and their Weightings
"""
player_df['Spd'] = ( player_df['Pac'] + player_df['Acc'] ) / 2
player_df['Work'] = ( player_df['Wor'] + player_df['Sta'] ) / 2
player_df['SetP'] = ( player_df['Jum'] + player_df['Bra'] ) / 2
for _, weightings in attribute_df.iterrows():
role = weightings["Ratings Weights"]
player_df[role] = 0
for attribute in weightings.index[1:]:
weighting = weightings[attribute]
try:
player_df[role] += round(pd.to_numeric(player_df[attribute]) * weighting / 20, 2)
except Exception as e: # Used to Nat being used twice (Nationality and Natural Fitness)
print(e)
continue
return player_df
def sum_attributes(player_df: pd.DataFrame, role: str, attribute_type: str, attributes: [str]) -> pd.DataFrame:
"""Create a new Column containing the sum of provided attribute columns
Keyword arguments:
player_df: Dataframe of Players and Attributes
role: Name of role to be used as additional column in dataframe
attribute_type: Type of Attribute [Primary, Secondary, Tertiary]
attributes: List of Attributes to Sum
"""
player_df[f'{role}_{attribute_type}'] = 0
for attribute in attributes:
player_df[f'{role}_{attribute_type}'] += pd.to_numeric(player_df[attribute])
player_df[f'{role}_{attribute_type}'] = round(player_df[f'{role}_{attribute_type}'] / len(attributes), 2)
return player_df
def calc_role_scores(player_df: pd.DataFrame, role: dict) -> pd.DataFrame:
"""Calculate Player Role scores based on selected attributes.
Keyword arguments:
player_df: Dataframe of Players and Attributes
role: Dictionary containing role name, role attributes and role attribute weightings
"""
player_df = sum_attributes(player_df, role["role_name"], "primary", role["primary_attributes"])
player_df = sum_attributes(player_df, role["role_name"], "secondary", role["secondary_attributes"])
if "tertiary_attributes" in role:
print("here")
player_df = sum_attributes(player_df, role["role_name"], "tertiary", role["tertiary_attributes"])
divisor = role["primary_multiplier"] + role["secondary_multiplier"] + role["tertiary_multiplier"]
player_df[f'{role["role_name"]}'] = round((((player_df[f'{role["role_name"]}_primary'] * 5) + (player_df[f'{role["role_name"]}_secondary'] * 3) + (player_df[f'{role["role_name"]}_tertiary'] * 1)) / divisor ), 2)
return player_df
def calc_role_scores_for_tactic_roles(player_df: pd.DataFrame, tactic_roles: [dict]):
for role in tactic_roles:
player_df = calc_role_scores(player_df, role)
return player_df
if __name__ == "__main__":
# Parse Input args
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input-filepath", type=str, help="Path to Input Html file")
parser.add_argument("-o", "--output-filepath", type=str, help="Path to Export resultant Html file")
parser.add_argument("-r", "--roles", nargs='+', type=str, help="Space seperated list of roles for Evaluation")
parser.add_argument("-a", "--attribute-filepath", type=str, help="Path to Attribute XLSX file", default="./attribute_ratings.xlsx")
args = parser.parse_args()
input_filepath = args.input_filepath
output_filepath = args.output_filepath
roles = args.roles
attribute_filepath = args.attribute_filepath
# Take Role arg and convert to list of role dictionaries
tactic_roles = []
for role in roles:
tactic_roles.append(globals()[role])
# Inport data, calculate scores for role, export results as html
# Inport data, calculate scores for role,
attribute_df = load_xlsx_data_to_dataframe(attribute_filepath)
player_df = load_html_data_to_dataframe(input_filepath)
player_df = calc_role_scores_for_tactic_roles(player_df, tactic_roles)
player_df = calc_role_scores(player_df, attribute_df)
# trim attributes from final output
player_df = player_df.drop(player_df.columns[15:-11], axis=1)
# export results as html
export_html_from_dataframe(player_df, output_filepath)

View File

@ -1,2 +1,5 @@
pandas
lxml
numpy
lxml
openpyxl
pre-commit

192
role_score_calculator.py Normal file
View File

@ -0,0 +1,192 @@
import argparse
import pandas as pd
# Define Player attributes
# TODO: Add roles.
gk = {
"role_name": "gk",
"primary_multiplier": 5,
"primary_attributes": ["Agi", "Ref"],
"secondary_multiplier": 3,
"secondary_attributes": ["1v1", "Ant", "Cmd", "Cnt", "Kic", "Pos"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Acc", "Aer", "Cmp", "Dec", "Fir", "Han", "Pas", "Thr", "Vis"],
}
fb = {
"role_name": "fb",
"primary_multiplier": 5,
"primary_attributes": ["Wor", "Acc", "Pac", "Sta"],
"secondary_multiplier": 3,
"secondary_attributes": ["Cro", "Dri", "Mar", "OtB", "Tck", "Tea"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Agi", "Ant", "Cnt", "Dec", "Fir", "Pas", "Pos", "Tec"],
}
cd = {
"role_name": "cd",
"primary_multiplier": 3,
"primary_attributes": ["Cmp", "Hea", "Jum", "Mar", "Pas", "Pos", "Str", "Tck", "Pac"],
"secondary_multiplier": 1,
"secondary_attributes": ["Agg", "Ant", "Bra", "Cnt", "Dec", "Fir", "Tec", "Vis"],
}
dm = {
"role_name": "dm",
"primary_multiplier": 5,
"primary_attributes": ["Wor", "Pac", "Sta", "Pas"],
"secondary_multiplier": 3,
"secondary_attributes": ["Tck", "Ant", "Cnt", "Pos", "Bal", "Agi"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Tea", "Fir", "Mar", "Agg", "Cmp", "Dec", "Str"],
}
b2b = {
"role_name": "b2b",
"primary_multiplier": 5,
"primary_attributes": ["Pas", "Wor", "Sta"],
"secondary_multiplier": 3,
"secondary_attributes": ["Tck", "OtB", "Tea", "Vis", "Str", "Dec", "Pos", "Pac"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Agg", "Ant", "Fin", "Lon", "Cmp", "Acc", "Bal", "Fir", "Dri", "Tec"],
}
w = {
"role_name": "w",
"primary_multiplier": 3,
"primary_attributes": ["Acc", "Cro", "Dri", "OtB", "Pac", "Tec"],
"secondary_multiplier": 1,
"secondary_attributes": ["Agi", "Fir", "Pas", "Sta", "Wor"],
}
iw = {
"role_name": "iw",
"primary_multiplier": 5,
"primary_attributes": ["Acc", "Pac", "Wor"],
"secondary_multiplier": 3,
"secondary_attributes": ["Dri", "Pas", "Tec", "OtB"],
"tertiary_multiplier": 1,
"tertiary_attributes": ["Cro", "Fir", "Cmp", "Dec", "Vis", "Agi", "Sta"],
}
def load_html_data_to_dataframe(filepath: str) -> pd.DataFrame:
"""Read HTML file exported by FM into a Dataframe.
Keyword arguments:
filepath -- path to fm player html file
"""
player_df = pd.read_html(filepath, header=0, encoding="utf-8", keep_default_na=False)[0]
# Clean Dataframe to get rid of unknown values and ability ranges (takes the lowest value)
# This casts to a string to be able to split, so we have to cast back to an int later.
player_df = player_df.replace("-", 0)
player_df = player_df.map(lambda x: str(x).split("-")[0])
return player_df
def export_html_from_dataframe(player_df: pd.DataFrame, filepath: str) -> str:
"""Export Dataframe as html with jQuery Data Tables. Taken from: https://www.thepythoncode.com/article/convert-pandas-dataframe-to-html-table-python.
Keyword arguments:
filepath -- path to fm player html file
"""
table_html = player_df.to_html(table_id="table", index=False)
html = f"""
<html>
<header>
<link href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css" rel="stylesheet">
</header>
<body>
{table_html}
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready( function () {{
$('#table').DataTable({{
paging: false,
order: [[12, 'desc']],
// scrollY: 400,
}});
}});
</script>
</body>
</html>
"""
open(filepath, "w", encoding="utf-8").write(html)
# TODO: Do I even want this?
def calc_composite_scores(player_df: pd.DataFrame) -> pd.DataFrame:
"""Calculate Speed, Workrate and Set Piece scores.
Keyword arguments:
player_df: Dataframe of Players and Attributes
"""
player_df["Spd"] = (player_df["Pac"] + player_df["Acc"]) / 2
player_df["Work"] = (player_df["Wor"] + player_df["Sta"]) / 2
player_df["SetP"] = (player_df["Jum"] + player_df["Bra"]) / 2
return player_df
def sum_attributes(player_df: pd.DataFrame, role: str, attribute_type: str, attributes: [str]) -> pd.DataFrame:
"""Create a new Column containing the sum of provided attribute columns.
Keyword arguments:
player_df: Dataframe of Players and Attributes
role: Name of role to be used as additional column in dataframe
attribute_type: Type of Attribute [Primary, Secondary, Tertiary]
attributes: List of Attributes to Sum
"""
player_df[f"{role}_{attribute_type}"] = 0
for attribute in attributes:
player_df[f"{role}_{attribute_type}"] += pd.to_numeric(player_df[attribute])
player_df[f"{role}_{attribute_type}"] = round(player_df[f"{role}_{attribute_type}"] / len(attributes), 2)
return player_df
def calc_role_scores(player_df: pd.DataFrame, role: dict) -> pd.DataFrame:
"""Calculate Player Role scores based on selected attributes.
Keyword arguments:
player_df: Dataframe of Players and Attributes
role: Dictionary containing role name, role attributes and role attribute weightings
"""
player_df = sum_attributes(player_df, role["role_name"], "primary", role["primary_attributes"])
player_df = sum_attributes(player_df, role["role_name"], "secondary", role["secondary_attributes"])
if "tertiary_attributes" in role:
print("here")
player_df = sum_attributes(player_df, role["role_name"], "tertiary", role["tertiary_attributes"])
divisor = role["primary_multiplier"] + role["secondary_multiplier"] + role["tertiary_multiplier"]
player_df[f'{role["role_name"]}'] = round(
(((player_df[f'{role["role_name"]}_primary'] * 5) + (player_df[f'{role["role_name"]}_secondary'] * 3) + (player_df[f'{role["role_name"]}_tertiary'] * 1)) / divisor), 2
)
return player_df
def calc_role_scores_for_tactic_roles(player_df: pd.DataFrame, tactic_roles: [dict]):
for role in tactic_roles:
player_df = calc_role_scores(player_df, role)
return player_df
if __name__ == "__main__":
# Parse Input args
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input-filepath", type=str, help="Path to Input Html file")
parser.add_argument("-o", "--output-filepath", type=str, help="Path to Export resultant Html file")
parser.add_argument("-r", "--roles", nargs="+", type=str, help="Space seperated list of roles for Evaluation")
args = parser.parse_args()
input_filepath = args.input_filepath
output_filepath = args.output_filepath
roles = args.roles
# Take Role arg and convert to list of role dictionaries
tactic_roles = []
for role in roles:
tactic_roles.append(globals()[role])
# Inport data, calculate scores for role, export results as html
player_df = load_html_data_to_dataframe(input_filepath)
player_df = calc_role_scores_for_tactic_roles(player_df, tactic_roles)
export_html_from_dataframe(player_df, output_filepath)