| 715 | legend.set_frame_on(False) |
| 716 | ax.grid(True, alpha=0.3) |
| 717 | |
| 718 | # Hide unused subplots |
| 719 | for idx in range(n_players, len(axes)): |
| 720 | axes[idx].set_visible(False) |
| 721 | |
| 722 | plt.tight_layout() |
| 723 | safe_game_name = game_name.replace("/", "_").replace(" ", "_") |
| 724 | self._save_plot(output_dir, f"{safe_game_name}_validation") |
| 725 | plt.close() |
| 726 | |
| 727 | |
| 728 | class BootStrapRankStability: |
| 729 | def __init__( |
| 730 | self, |
| 731 | builder: ScoreMatrixBuilder, |
| 732 | *, |
| 733 | n_bootstrap: int = 1000, |
| 734 | game: str = "ALL", |
| 735 | regularization: float = 0.01, |
| 736 | topks: list[int] | None = None, |
| 737 | bootstrap_type: Literal["nonparametric", "parametric"] = "nonparametric", |
| 738 | output_dir: Path | None = None, |
| 739 | ): |
| 740 | self.builder = builder |
| 741 | self.n_bootstrap = n_bootstrap |
| 742 | self.game = game |
| 743 | self.regularization = regularization |
| 744 | self.topks = topks |
| 745 | self.bootstrap_type = bootstrap_type |
| 746 | self.output_dir = output_dir |
| 747 | |
| 748 | @staticmethod |
| 749 | def _save_plot(output_dir: Path, filename_base: str) -> None: |
| 750 | """Save plot in both PDF and PNG formats.""" |
| 751 | for fmt in ["pdf", "png"]: |
| 752 | output_path = output_dir / f"{filename_base}.{fmt}" |
| 753 | plt.savefig(output_path, format=fmt, bbox_inches="tight", dpi=300 if fmt == "png" else None) |
| 754 | logger.info(f"Saved plot: {output_path}") |
| 755 | |
| 756 | @staticmethod |
| 757 | def _elos_from_result(result: dict) -> dict[str, float]: |
| 758 | return {p: BradleyTerryFitter.bt_to_elo(s) for p, s in zip(result["players"], result["strengths"])} |
| 759 | |
| 760 | @staticmethod |
| 761 | def _ranking_from_elos(elos: dict[str, float]) -> list[str]: |
| 762 | return [p for p, _ in sorted(elos.items(), key=lambda kv: kv[1], reverse=True)] |
| 763 | |
| 764 | @staticmethod |
| 765 | def _positions(ranking: list[str]) -> dict[str, int]: |
| 766 | return {p: i for i, p in enumerate(ranking)} |
| 767 | |
| 768 | @staticmethod |
| 769 | def _max_footrule(n: int) -> float: |
| 770 | return (n * n) / 2 if n % 2 == 0 else (n * n - 1) / 2 |
| 771 | |
| 772 | def _fit_on_matrix(self, matchups: dict[tuple[str, str], list[float]]) -> dict: |
| 773 | fitter = BradleyTerryFitter(matchups, regularization=self.regularization, compute_uncertainties=False) |
| 774 | return fitter.fit() |