Skip to content

Commit

Permalink
Merge pull request #76 from srvanrell/initial-rating-assistant
Browse files Browse the repository at this point in the history
Initial rating assistant
  • Loading branch information
srvanrell authored Jun 24, 2024
2 parents 7b1b585 + c20c5f4 commit 42ee363
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 42 deletions.
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "ranking_table_tennis"
version = "2024.6.18"
version = "2024.6.24"
description = "A ranking table tennis system"
readme = "README.md"
authors = [ "Sebastian Vanrell srvanrell_gmail_com" ]
Expand Down
12 changes: 10 additions & 2 deletions ranking_table_tennis/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ def main():
)
parser.add_argument(
"--unattended",
help="Preprocessing is resolved unattended. --config-initial-date is the only valid param.",
help=(
"Preprocessing is resolved unattended. "
"--config-initial-date and --dont-download are the only valid param."
),
action="store_true",
)
parser.add_argument(
"--dont-download",
help="Preprocessing unattended and dont't downloading tournaments.",
action="store_true",
)
parser.add_argument(
Expand Down Expand Up @@ -82,7 +90,7 @@ def main():
if args.cmd == "preprocess" and args.unattended:
from ranking_table_tennis import preprocess_unattended

preprocess_unattended.main(args.config_initial_date)
preprocess_unattended.main(args.config_initial_date, not args.dont_download)
elif args.cmd == "preprocess":
from ranking_table_tennis import preprocess

Expand Down
2 changes: 1 addition & 1 deletion ranking_table_tennis/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)
from ranking_table_tennis.helpers.github import no_updates_stop_workflow
from ranking_table_tennis.helpers.gspread import publish_to_web
from ranking_table_tennis.helpers.initial_rating import print_rating_context
from ranking_table_tennis.helpers.initial_rating import suggest_initial_rating
from ranking_table_tennis.helpers.pickle import load_from_pickle, save_to_pickle
from ranking_table_tennis.helpers.plotter import plot_championships, plot_ratings
from ranking_table_tennis.helpers.publisher import (
Expand Down
119 changes: 90 additions & 29 deletions ranking_table_tennis/helpers/initial_rating.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

import numpy as np
import pandas as pd

from ranking_table_tennis import helpers, models
Expand All @@ -8,47 +9,107 @@
logger = logging.getLogger(__name__)


def print_rating_context(
def suggest_initial_rating(
tournaments: models.Tournaments,
players: models.Players,
initial_ranking: models.Rankings,
name: str,
name_unknown_player: str,
tid: str,
) -> None:
# Helper to assign an initial rating to name
print("# Information that should help you assign rating")
print("\n# Matches")
logger.info("# Following information should help you assign rating")

cfg = ConfigManager().current_config

# Print matches results for the unknown player
matches = tournaments.get_matches(tid, False, [])
matches_selected = matches[(matches.player_a == name) | (matches.player_b == name)].copy()
print(matches_selected[["winner", "winner_pid", "loser", "loser_pid", "round", "category"]])

# Print rating of known players, if available
try:
known_rankings = helpers.load_from_pickle(cfg.io.pickle.rankings)
except (FileNotFoundError):
logger.warn(
"WARNING: Previous rankings are not available. Initial rankings will be loaded."
limits_suggested_rating = [np.nan, np.nan]

matches_as_winner = matches.loc[(matches.winner == name_unknown_player), :].copy()
if matches_as_winner.empty:
logger.warn("no matches as winner")
else:
fill_rating_column(matches_as_winner, players, tournaments, initial_ranking, tid, "loser")
matches_str = str(
matches_as_winner[
["winner", "winner_pid", "loser", "loser_pid", "round", "category", "rival_rating"]
]
)
known_rankings = initial_ranking
logger.info(f"\n# Matches as winner\n{matches_str}\n")

try:
tids = [cfg.initial_metadata.initial_tid] + [t for t in tournaments] # to use prev_tid
pids_with_rating = known_rankings.get_entries(tids[-2]).pid.to_list()
pids_selected = (
pd.concat([matches_selected.winner_pid, matches_selected.loser_pid], ignore_index=True)
.dropna()
.pipe(lambda s: s[s.isin(pids_with_rating)]) # filter pids with known rating
.unique()
limits_suggested_rating[0] = matches_as_winner["rival_rating"].max()

matches_as_loser = matches.loc[(matches.loser == name_unknown_player), :].copy()
if matches_as_loser.empty:
logger.warn("no matches as loser")
else:
fill_rating_column(matches_as_loser, players, tournaments, initial_ranking, tid, "winner")
matches_str = str(
matches_as_loser[
["winner", "winner_pid", "loser", "loser_pid", "round", "category", "rival_rating"]
]
)
print(f"\n# Known ratings (categories thresholds: {cfg.compute.categories_thresholds})")
for pid in pids_selected:
print(
f"# {tids[-2]}, {players[pid]['name']}, "
f"rating: {known_rankings.get_entries(tids[-2], pid).get('rating')}"
)
except AttributeError:
logger.warn("Sorry, no previous ranking is available to help you")
logger.info(f"\n# Matches as loser\n{matches_str}\n")

limits_suggested_rating[1] = matches_as_loser["rival_rating"].min()

logger.info(f"# Known ratings (categories thresholds: {cfg.compute.categories_thresholds})")

# Suggesting rating
VIRTUAL_RANGE_NO_LIMIT = 60
if np.isnan(limits_suggested_rating).all():
# If no limits are available, cannot suggest a rating
logger.warn("no rating to suggest")
elif np.isnan(limits_suggested_rating[0]):
limits_suggested_rating[0] = limits_suggested_rating[1] - VIRTUAL_RANGE_NO_LIMIT
elif np.isnan(limits_suggested_rating[1]):
limits_suggested_rating[1] = limits_suggested_rating[0] + VIRTUAL_RANGE_NO_LIMIT

suggested_rating = np.mean(limits_suggested_rating)
logger.info(f"Suggested rating: {suggested_rating}")

return suggested_rating


def fill_rating_column(
matches_df, players, tournaments, initial_rankings, tid, winner_or_loser: str
):
matches_df["rival_rating"] = pd.NA

cfg = ConfigManager().current_config
tids = [cfg.initial_metadata.initial_tid] + [t for t in tournaments] # to use prev_tid
prev_tid = tids[tids.index(tid) - 1]

# First try to use known previous ranking
try:
known_rankings = helpers.load_from_pickle(cfg.io.pickle.rankings)
except FileNotFoundError:
logger.warn("Sorry, no previous rating is available to help you")
return

for ix, row in matches_df.iterrows():
rival_pid = row[f"{winner_or_loser}_pid"]
rival_name = row[winner_or_loser]
rival_rating = pd.NA

# First try to use known previous ranking
try:
rival_rating = known_rankings.get_entries(prev_tid, rival_pid).get("rating")
if rival_rating < 0:
rival_rating = pd.NA
except (AttributeError, UnboundLocalError):
logger.warn("Sorry, no previous rating is available to help you with %s", rival_name)

# If it is a new player, try to use the initial rating (that could be already defined)
assigned_pid = players.get_pid(rival_name)
# if pd.isna(rival_rating) and pd.isna(rival_pid) and assigned_pid is not None:
if pd.isna(rival_rating) and assigned_pid is not None:
try:
rival_rating = initial_rankings.get_entries(tids[0], assigned_pid).get("rating")
if rival_rating < 0:
rival_rating = pd.NA
except (AttributeError, UnboundLocalError):
logger.warn("Sorry, no initial rating is available to help you with %s", rival_pid)

matches_df.loc[ix, "rival_rating"] = rival_rating
11 changes: 10 additions & 1 deletion ranking_table_tennis/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def main(offline=True, assume_yes=False, config_initial_date="220101"):
unknown_player = True

# Will print available ratings of known players
helpers.print_rating_context(tournaments, players, rankings, name, tid)
helpers.suggest_initial_rating(tournaments, players, rankings, name, tid)

text_to_show = f"\nEnter the initial rating points for {name}"
text_to_show += " (category will be auto-assigned):\n"
Expand All @@ -103,6 +103,15 @@ def main(offline=True, assume_yes=False, config_initial_date="220101"):
f"\n{rankings[initial_tid, pid][['tid', 'pid', 'rating', 'category']]}\n"
f"{players[pid]['name']}"
)
elif rankings[initial_tid, pid, "rating"] < 0:
unknown_player_should_update = True
# Will print available ratings of known players
helpers.suggest_initial_rating(tournaments, players, rankings, name, tid)

text_to_show = f"\nEnter the initial rating points for {name}"
text_to_show += " (category will be auto-assigned):\n"
initial_rating = int(input(text_to_show))
rankings[initial_tid, pid, "rating"] = initial_rating

if unknown_player:
retrieve = input(
Expand Down
30 changes: 26 additions & 4 deletions ranking_table_tennis/preprocess_unattended.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
logger = logging.getLogger(__name__)


def main(config_initial_date="220101"):
def main(config_initial_date="220101", download=True):
"""Preprocess matches on xlsx tournaments database. Resolves with no human interaction
Function to run before compute_rankings.main().
Expand All @@ -28,9 +28,10 @@ def main(config_initial_date="220101"):
# Stop preprocess if there were no recent updates on the spreadsheet
helpers.no_updates_stop_workflow(cfg.io.tournaments_spreadsheet_id)

xlsx_file = cfg.io.data_folder + cfg.io.xlsx.tournaments_filename
logger.info("Downloading and saving '%s'" % xlsx_file)
request.urlretrieve(cfg.io.tournaments_gdrive, xlsx_file)
if download:
xlsx_file = cfg.io.data_folder + cfg.io.xlsx.tournaments_filename
logger.info("Downloading and saving '%s'" % xlsx_file)
request.urlretrieve(cfg.io.tournaments_gdrive, xlsx_file)

# Loading all tournament data
tournaments = helpers.load_tournaments_sheets()
Expand Down Expand Up @@ -65,9 +66,30 @@ def main(config_initial_date="220101"):
unknown_player_should_update = True
rankings.add_new_entry(initial_tid, pid)
logger.info(
f"Initial rating to be revised"
f"\n{rankings[initial_tid, pid][['tid', 'pid', 'rating', 'category']]}\n"
f"{players[pid]['name']}"
)
elif rankings[initial_tid, pid, "rating"] < 0:
# Will print available ratings of known players
suggested_rating = helpers.suggest_initial_rating(
tournaments, players, rankings, name, tid
)

if suggested_rating > 0:
unknown_player_should_update = True
rankings[initial_tid, pid, "rating"] = suggested_rating
logger.info(
f"Filled with suggested ranking "
f"\n{rankings[initial_tid, pid][['tid', 'pid', 'rating', 'category']]}\n"
f"{players[pid]['name']}"
)
else:
logger.info(
f"Initial rating to be revised"
f"\n{rankings[initial_tid, pid][['tid', 'pid', 'rating', 'category']]}\n"
f"{players[pid]['name']}"
)

# FIXME Update config but do not affect initial tid
# tournament_date = tournaments[tid].iloc[0].date.strftime("%y%m%d")
Expand Down

0 comments on commit 42ee363

Please sign in to comment.