Coverage for src/duelboard/utils.py: 100%
47 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 19:18 +0900
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 19:18 +0900
1"""Utility functions for Elo calculations."""
3from __future__ import annotations
5from typing import TYPE_CHECKING
7import pandas as pd
9from .models import Battle, BattleOutcome
11if TYPE_CHECKING:
12 from .types import RatingsDict
15def load_battles_from_csv(
16 file_path: str,
17 player_a_col: str = "player_a",
18 player_b_col: str = "player_b",
19 winner_col: str = "winner",
20 **pandas_kwargs: object,
21) -> list[Battle]:
22 """Load battles from CSV file.
24 Args:
25 file_path: Path to CSV file
26 player_a_col: Column name for player A
27 player_b_col: Column name for player B
28 winner_col: Column name for winner
29 **pandas_kwargs: Additional arguments for pandas.read_csv
31 Returns:
32 List of Battle objects
33 """
34 df = pd.read_csv(file_path, **pandas_kwargs)
35 return dataframe_to_battles(df, player_a_col, player_b_col, winner_col)
38def load_battles_from_json(
39 file_path: str,
40 player_a_col: str = "player_a",
41 player_b_col: str = "player_b",
42 winner_col: str = "winner",
43 **pandas_kwargs: object,
44) -> list[Battle]:
45 """Load battles from JSON file.
47 Args:
48 file_path: Path to JSON file
49 player_a_col: Column name for player A
50 player_b_col: Column name for player B
51 winner_col: Column name for winner
52 **pandas_kwargs: Additional arguments for pandas.read_json
54 Returns:
55 List of Battle objects
56 """
57 df = pd.read_json(file_path, **pandas_kwargs)
58 return dataframe_to_battles(df, player_a_col, player_b_col, winner_col)
61def dataframe_to_battles(
62 df: pd.DataFrame,
63 player_a_col: str = "player_a",
64 player_b_col: str = "player_b",
65 winner_col: str = "winner",
66) -> list[Battle]:
67 """Convert DataFrame to list of Battle objects.
69 Args:
70 df: DataFrame with battle data
71 player_a_col: Column name for player A
72 player_b_col: Column name for player B
73 winner_col: Column name for winner
75 Returns:
76 List of Battle objects
77 """
78 battles = []
79 for _, row in df.iterrows():
80 # Create metadata from remaining columns
81 metadata = row.drop([player_a_col, player_b_col, winner_col]).to_dict()
83 battle = Battle(
84 player_a=row[player_a_col],
85 player_b=row[player_b_col],
86 outcome=BattleOutcome(row[winner_col]),
87 metadata=metadata,
88 )
89 battles.append(battle)
91 return battles
94def battles_to_dataframe(battles: list[Battle]) -> pd.DataFrame:
95 """Convert list of Battle objects to DataFrame.
97 Args:
98 battles: List of Battle objects
100 Returns:
101 DataFrame with battle data
102 """
103 data = []
104 for battle in battles:
105 row = {
106 "player_a": battle.player_a,
107 "player_b": battle.player_b,
108 "winner": battle.outcome.value,
109 }
111 # Add metadata if available
112 if battle.metadata:
113 row.update(battle.metadata)
115 data.append(row)
117 return pd.DataFrame(data)
120def filter_anonymous_battles(df: pd.DataFrame, anony_col: str = "anony") -> pd.DataFrame:
121 """Filter to only anonymous battles (following the notebook approach).
123 Args:
124 df: DataFrame with battle data
125 anony_col: Column name for anonymity flag
127 Returns:
128 Filtered DataFrame with only anonymous battles
129 """
130 if anony_col in df.columns:
131 return df[df[anony_col]].reset_index(drop=True)
132 return df
135def filter_non_tie_battles(df: pd.DataFrame, winner_col: str = "winner") -> pd.DataFrame:
136 """Filter out tie battles.
138 Args:
139 df: DataFrame with battle data
140 winner_col: Column name for winner
142 Returns:
143 Filtered DataFrame without tie battles
144 """
145 return df[~df[winner_col].str.contains("tie", case=False, na=False)].reset_index(drop=True)
148def get_rating_summary(ratings: RatingsDict) -> pd.DataFrame:
149 """Get summary DataFrame of ratings.
151 Args:
152 ratings: Dictionary of player ratings
154 Returns:
155 DataFrame with rating summary sorted by rating
156 """
157 data = []
158 for player, rating in ratings.items():
159 row = {
160 "player": player,
161 "rating": rating.rating,
162 "battles": rating.battles,
163 }
165 if rating.confidence_interval:
166 row["ci_lower"] = rating.confidence_interval[0]
167 row["ci_upper"] = rating.confidence_interval[1]
168 row["ci_width"] = rating.confidence_interval[1] - rating.confidence_interval[0]
170 data.append(row)
172 df = pd.DataFrame(data)
173 return df.sort_values("rating", ascending=False).reset_index(drop=True)
176def rank_players_by_rating(ratings: RatingsDict) -> list[str]:
177 """Get player names ranked by rating (highest first).
179 Args:
180 ratings: Dictionary of player ratings
182 Returns:
183 List of player names sorted by rating
184 """
185 return [rating.player for rating in sorted(ratings.values(), key=lambda x: x.rating, reverse=True)]
188def export_ratings_to_csv(ratings: RatingsDict, file_path: str) -> None:
189 """Export ratings to CSV file.
191 Args:
192 ratings: Dictionary of player ratings
193 file_path: Path to output CSV file
194 """
195 df = get_rating_summary(ratings)
196 df.to_csv(file_path, index=False)