File size: 8,340 Bytes
1bc7a0c
 
330f0b8
1bc7a0c
0fab24c
1bc7a0c
 
 
 
 
 
 
 
 
 
330f0b8
 
1bc7a0c
330f0b8
 
 
 
9565067
330f0b8
9565067
330f0b8
1bc7a0c
9565067
 
 
 
 
 
330f0b8
9565067
 
 
 
 
 
 
 
330f0b8
9565067
1bc7a0c
 
330f0b8
 
 
1bc7a0c
 
9565067
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1bc7a0c
330f0b8
9565067
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330f0b8
9565067
 
330f0b8
9565067
 
 
 
 
 
 
 
 
 
 
 
330f0b8
 
9565067
330f0b8
1bc7a0c
 
 
 
 
330f0b8
1bc7a0c
 
 
 
 
 
 
 
 
 
 
 
 
 
330f0b8
 
1bc7a0c
 
330f0b8
1bc7a0c
 
9565067
1bc7a0c
 
330f0b8
 
1bc7a0c
330f0b8
1bc7a0c
330f0b8
 
 
 
 
 
 
1bc7a0c
 
 
 
 
330f0b8
9565067
 
 
 
1bc7a0c
9565067
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330f0b8
1bc7a0c
330f0b8
 
 
 
 
 
 
 
 
 
 
1bc7a0c
 
 
330f0b8
1bc7a0c
330f0b8
 
 
1bc7a0c
330f0b8
 
1bc7a0c
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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