diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..25c0680
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -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]
diff --git a/README.md b/README.md
index df46c86..bc4f4f7 100644
--- a/README.md
+++ b/README.md
@@ -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
-```
\ No newline at end of file
diff --git a/attribute_ratings.xlsx b/attribute_ratings.xlsx
new file mode 100644
index 0000000..fefe71f
Binary files /dev/null and b/attribute_ratings.xlsx differ
diff --git a/position_score_calculator.py b/position_score_calculator.py
index 34c7a55..7c1213d 100644
--- a/position_score_calculator.py
+++ b/position_score_calculator.py
@@ -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)
diff --git a/requirements.txt b/requirements.txt
index 1f2c5eb..83f6f96 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,5 @@
pandas
-lxml
\ No newline at end of file
+numpy
+lxml
+openpyxl
+pre-commit
diff --git a/role_score_calculator.py b/role_score_calculator.py
new file mode 100644
index 0000000..89ede0f
--- /dev/null
+++ b/role_score_calculator.py
@@ -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"""
+
+