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