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""" + +
+ +
+ + {table_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)