|
| 1 | +from scipy.stats import binomtest |
| 2 | + |
| 3 | +from codeclash.constants import RESULT_TIE |
| 4 | + |
| 5 | + |
| 6 | +def calculate_p_value(scores: dict[str, int]) -> float: |
| 7 | + """Calculate the p-value for the statistical significance of the winner. |
| 8 | +
|
| 9 | + Input: scores as a dictionary of player names and number of games won. |
| 10 | + Special case: 'Tie' (constants.RESULT_TIE) is a tie between all players. |
| 11 | +
|
| 12 | + Ties (RESULT_TIE) are excluded by conditioning on decisive games. |
| 13 | +
|
| 14 | + Uses an exact one-sided binomial test for the top player's share vs 1/K, |
| 15 | + Bonferroni-corrected for choosing the winner post hoc among K players. |
| 16 | + Returns 1.0 if there is no unique winner or no decisive games. |
| 17 | + """ |
| 18 | + player_wins = {p: c for p, c in scores.items() if p != RESULT_TIE} |
| 19 | + decisive_games = sum(player_wins.values()) |
| 20 | + n_players = len(player_wins) |
| 21 | + assert n_players > 1, "At least two players are required to calculate significance" |
| 22 | + if not player_wins or not decisive_games: |
| 23 | + # No winner |
| 24 | + return 1.0 |
| 25 | + top_player_wins = max(player_wins.values()) |
| 26 | + if sum(c == top_player_wins for c in player_wins.values()) != 1: |
| 27 | + # Multiple players have the same number of wins |
| 28 | + # Definitely not significant |
| 29 | + return 1.0 |
| 30 | + # Null-hypothesis: The top player wins 1/n_players of the games |
| 31 | + p0 = 1.0 / n_players |
| 32 | + p_one = binomtest(top_player_wins, decisive_games, p=p0, alternative="greater").pvalue |
| 33 | + # Bonferonni correction: Imagine a game with 100 players. And let's assume that there's one |
| 34 | + # player that wins 10 games, and everyone else 0 or 1. The p-value would probably look pretty convincing |
| 35 | + # However, since we have so many players, it's actually not unlikely that _some_ player will win 10 games |
| 36 | + # by chance. So essentially by selecting the winner post-hoc in a multiple-comparison setting, we've inflated |
| 37 | + # the significance. Bonferonni correction is a simple way to account for this. |
| 38 | + p_bonferonni = p_one * n_players |
| 39 | + return min(1.0, p_bonferonni) |
0 commit comments