Arena / arena.py
FredOru's picture
v0.2
9565067
import trueskill as ts
import pandas as pd
from typing import Dict, List, Tuple, Union
import db
import pandas as pd
from typing import TypedDict
MU_init = ts.Rating().mu
SIGMA_init = ts.Rating().sigma
class Prompt(TypedDict):
id: int
name: str
text: str
class Arena:
"""
Une arène pour comparer et classer des prompts en utilisant l'algorithme TrueSkill.
"""
def init_estimates(self, prompt_id) -> None:
"""
Initialise les estimations d'un prompt avec des ratings TrueSkill par défaut.
"""
estimates = db.load("estimates")
if prompt_id in estimates["prompt_id"].values:
# supprimer la ligne existante
db.delete(
"estimates",
int(estimates[estimates["prompt_id"] == prompt_id].iloc[0].id),
)
db.insert(
"estimates",
{
"prompt_id": prompt_id,
"mu": MU_init,
"sigma": SIGMA_init,
},
)
def select_match(self, user_state) -> Tuple[Prompt, Prompt] | None:
"""
Sélectionne deux prompts pour un match en privilégiant ceux avec une grande incertitude.
Returns:
Un tuple contenant les IDs des deux prompts à comparer (prompt_a, prompt_b)
"""
# le prompt le plus incertain (sigma le plus élevé)
estimates = db.load("estimates")
# retirer le prompt de l'utilisateur pour qu'il ne puisse pas voter pour son propre prompt
estimates = estimates[
estimates["prompt_id"] != db.get_prompt_id(user_state["team"])
]
def order_match(id_a, id_b):
"""Return a tuple of ids ordered by the id."""
return (id_a, id_b) if id_a < id_b else (id_b, id_a)
# les matchs possibles entre les prompts par ordre d'incertitude décroissant
matches = (
estimates.merge(estimates, how="cross", suffixes=["_a", "_b"])
.query("id_a != id_b")
.assign(delta_mu=lambda df_: abs(df_["mu_a"] - df_["mu_b"]))
.sort_values(by=["sigma_a", "delta_mu"], ascending=[False, True])
.assign(
match=lambda df_: df_.apply(
lambda row: order_match(int(row["id_a"]), int(row["id_b"])), axis=1
)
)
)
user_votes = db.load("votes").loc[
lambda df_: df_["user_id"] == user_state["id"]
]
if user_votes.empty:
user_votes = user_votes.assign(match=[])
else: # les votes de l'utilisateur
user_votes = user_votes.assign(
match=lambda df_: df_.apply(
lambda row: order_match(
int(row["winner_id"]), int(row["loser_id"])
),
axis=1,
)
)
# on ne garde que les matchs qui n'ont pas encore été votés par l'utilisateur
user_matches = matches.loc[~matches["match"].isin(user_votes["match"])]
if user_matches.empty:
# Si l'utilisateur a déjà voté sur tous les matchs, on ne peut pas en sélectionner de nouveaux
return None
selected_match = user_matches.iloc[0]
prompts = db.load("prompts")
prompt_a = (
prompts.query(f"id == {selected_match['prompt_id_a']}").iloc[0].to_dict()
)
prompt_b = (
prompts.query(f"id == {selected_match['prompt_id_b']}").iloc[0].to_dict()
)
return prompt_a, prompt_b
def record_result(self, winner_id: str, loser_id: str, user_id: str) -> None:
# Obtenir les ratings actuels
estimates = db.load("estimates")
winner_estimate = (
estimates[estimates["prompt_id"] == winner_id].iloc[0].to_dict()
)
loser_estimate = estimates[estimates["prompt_id"] == loser_id].iloc[0].to_dict()
winner_rating = ts.Rating(winner_estimate["mu"], winner_estimate["sigma"])
loser_rating = ts.Rating(loser_estimate["mu"], loser_estimate["sigma"])
winner_new_rating, loser_new_rating = ts.rate_1vs1(winner_rating, loser_rating)
db.update(
"estimates",
winner_estimate["id"],
{"mu": winner_new_rating.mu, "sigma": winner_new_rating.sigma},
)
db.update(
"estimates",
loser_estimate["id"],
{"mu": loser_new_rating.mu, "sigma": loser_new_rating.sigma},
)
db.insert(
"votes",
{
"winner_id": winner_id,
"loser_id": loser_id,
"user_id": user_id,
# "timestamp": datetime.datetime.now().isoformat(),
},
)
return None
def get_rankings(self) -> pd.DataFrame:
"""
Obtient le classement actuel des prompts.
Returns:
Liste de dictionnaires contenant le classement de chaque prompt avec
ses informations (rang, id, texte, mu, sigma, score)
"""
prompts = db.load("prompts")
estimates = db.load("estimates").drop(columns=["id"])
rankings = prompts.merge(estimates, left_on="id", right_on="prompt_id").drop(
columns=["id", "prompt_id"]
)
rankings = rankings.sort_values(by="mu", ascending=False)
# ajouter un colonne position
rankings["position"] = range(1, len(rankings) + 1)
# eventuellement afficher plutôt mu - 3 sigma pour être conservateur
# rankings["score"] = rankings["mu"] - 3 * rankings["sigma"]
return rankings[["position", "team"]]
def get_competition_matrix(self) -> pd.DataFrame:
"""
Obtient la matrice de combats des prompts.
Returns:
DataFrame contenant en ligne et en colonne les noms d'équipes,
et dans la cellule le pourcentage de victoires de l'équipe de la ligne contre l'équipe de la colonne.
"""
prompts = db.load("prompts")
votes = db.load("votes")
competition_matrix = pd.DataFrame(
index=prompts["team"], columns=prompts["team"], data=0
)
competition_matrix.index.name = None
competition_matrix.columns.name = None
wins = competition_matrix.copy()
matches = competition_matrix.copy()
for _, row in votes.iterrows():
winner_name = prompts.loc[prompts["id"] == row["winner_id"], "team"].values[
0
]
loser_name = prompts.loc[prompts["id"] == row["loser_id"], "team"].values[0]
wins.at[winner_name, loser_name] += 1
matches.at[winner_name, loser_name] += 1
matches.at[loser_name, winner_name] += 1
competition_matrix = wins.div(matches)
competition_matrix = competition_matrix.map(
lambda x: "" if pd.isna(x) or x == 0 else f"{x:.0%}"
)
for i in range(len(competition_matrix)):
competition_matrix.iloc[i, i] = "X"
competition_matrix = competition_matrix.replace("", "?").reset_index(names="")
return competition_matrix
def get_progress(self) -> str:
"""
Renvoie des statistiques sur la progression du tournoi.
Returns:
Dictionnaire contenant des informations sur la progression:
- total_prompts: nombre total de prompts
- total_matches: nombre total de matchs joués
- avg_sigma: incertitude moyenne des ratings
- progress: pourcentage estimé de progression du tournoi
- estimated_remaining_matches: estimation du nombre de matchs restants
"""
prompts = db.load("prompts")
estimates = db.load("estimates")
votes = db.load("votes")
avg_sigma = estimates["sigma"].mean()
# Estimer quel pourcentage du tournoi est complété
# En se basant sur la réduction moyenne de sigma par rapport à la valeur initiale
initial_sigma = ts.Rating().sigma
progress = min(100, max(0, (1 - avg_sigma / initial_sigma) * 100))
msg = f"""{len(prompts)} propositions à départager
{len(votes)} matchs joués
{avg_sigma:.2f} d'incertitude moyenne"""
return msg