savegame fix + chart updates

This commit is contained in:
sam-paech 2025-07-05 00:27:02 +10:00
parent 7edc7c465f
commit e351aa3841
4 changed files with 211 additions and 41 deletions

View file

@ -55,28 +55,22 @@ def _numeric_columns(df: pd.DataFrame) -> List[str]:
def _load_games(exp: Path) -> pd.DataFrame:
"""
Return a DataFrame with one row per (game_id, power_name) containing
all numeric columns from *_game_analysis.csv plus these derived
columns:
all numeric columns from *_game_analysis.csv plus these derived columns:
max_supply_centers_owned
max_territories_controlled
max_military_units (all per-power maxima across phases)
max_game_score (max across powers within the game)
The phase files live under .../analysis/** and are searched
recursively so the script works with both individual and
combined layouts.
max_supply_centers_owned per-power max across phases
max_territories_controlled per-power max across phases
max_military_units per-power max across phases
max_game_score game-level max across powers
"""
root = exp / "analysis"
# ---------- game-level CSVs ---------------------------------
# ----------- game-level CSVs -----------------------------------------
game_csvs = list(root.rglob("*_game_analysis.csv"))
if not game_csvs:
raise FileNotFoundError(f"no *_game_analysis.csv found under {root}")
df_game = pd.concat((pd.read_csv(p) for p in game_csvs), ignore_index=True)
# ---------- derive max_game_score ---------------------------
# ----------- derive max_game_score -----------------------------------
if "game_score" in df_game.columns:
df_game["max_game_score"] = (
df_game.groupby("game_id")["game_score"].transform("max")
@ -84,11 +78,10 @@ def _load_games(exp: Path) -> pd.DataFrame:
else:
df_game["max_game_score"] = np.nan
# ---------- phase-level maxima for the other three ----------
# ----------- per-power maxima from phase files -----------------------
phase_csvs = list(root.rglob("*_phase_analysis.csv"))
if phase_csvs:
df_phase = pd.concat((pd.read_csv(p) for p in phase_csvs), ignore_index=True)
mapping = {
"supply_centers_owned_count": "max_supply_centers_owned",
"territories_controlled_count": "max_territories_controlled",
@ -104,14 +97,18 @@ def _load_games(exp: Path) -> pd.DataFrame:
)
df_game = df_game.merge(max_df, on=["game_id", "power_name"], how="left")
# ensure all four columns exist
# ----------- guarantee all max-columns exist -------------------------
for col in _MAX_METRICS:
if col not in df_game.columns:
df_game[col] = np.nan
# ----------- critical de-duplication (fixes doubled n) ---------------
df_game = df_game.drop_duplicates(subset=["game_id", "power_name"], keep="first")
return df_game
# ───────────────────── Welch statistics ──────────────────────
def _welch(a: np.ndarray, b: np.ndarray, alpha: float) -> Dict:
_t, p_val = stats.ttest_ind(a, b, equal_var=False)
@ -162,6 +159,92 @@ def _significant(df: pd.DataFrame, alpha: float) -> pd.DataFrame:
return df[keep].sort_values("p_value").reset_index(drop=True)
# ---------- phase-level helpers -----------------------------------------
def _load_phase(exp: Path) -> pd.DataFrame:
root = exp / "analysis"
phase_csvs = list(root.rglob("*_phase_analysis.csv"))
if not phase_csvs:
raise FileNotFoundError(f"no *_phase_analysis.csv found under {root}")
return pd.concat((pd.read_csv(p) for p in phase_csvs), ignore_index=True)
def _phase_index(ph_series: pd.Series) -> pd.Series:
_SEASON_ORDER = {"S": 0, "F": 1, "W": 2, "A": 3}
def _key(ph: str) -> tuple[int, int]:
year = int(ph[1:5]) if len(ph) >= 5 and ph[1:5].isdigit() else 0
season = _SEASON_ORDER.get(ph[0], 9)
return year, season
uniq = sorted(ph_series.unique(), key=_key)
return ph_series.map({ph: i for i, ph in enumerate(uniq)})
def _plot_phase_overlay(exp_a: Path, exp_b: Path, out_dir: Path) -> None:
import seaborn as sns
import matplotlib.pyplot as plt
df_a = _load_phase(exp_a)
df_b = _load_phase(exp_b)
tag_a, tag_b = exp_a.name or str(exp_a), exp_b.name or str(exp_b)
df_a["experiment"] = tag_a
df_b["experiment"] = tag_b
df = pd.concat([df_a, df_b], ignore_index=True)
if "phase_index" not in df.columns:
df["phase_index"] = _phase_index(df["game_phase"])
num_cols = [c for c in df.select_dtypes("number").columns
if c not in _EXCLUDE and c != "phase_index"]
# aggregate across games: mean per phase × power × experiment
agg = (
df.groupby(["experiment", "phase_index", "game_phase", "power_name"],
as_index=False)[num_cols]
.mean()
)
palette = sns.color_palette("tab10", n_colors=len(agg["power_name"].unique()))
power_colors = dict(zip(sorted(agg["power_name"].unique()), palette))
out_dir.mkdir(parents=True, exist_ok=True)
n_phases = agg["phase_index"].nunique()
fig_w = max(8, n_phases * 0.1 + 4)
for col in num_cols:
plt.figure(figsize=(fig_w, 6))
for power in sorted(agg["power_name"].unique()):
for exp_tag, style in [(tag_a, "--"), (tag_b, "-")]:
sub = agg[(agg["power_name"] == power) &
(agg["experiment"] == exp_tag)]
if sub.empty:
continue
plt.plot(
sub["phase_index"],
sub[col],
linestyle=style,
color=power_colors[power],
marker="o",
label=f"{power} {exp_tag}",
)
phases_sorted = (
agg.drop_duplicates("phase_index")
.sort_values("phase_index")[["phase_index", "game_phase"]]
)
plt.xticks(
phases_sorted["phase_index"],
phases_sorted["game_phase"],
rotation=90,
fontsize=8,
)
plt.title(col.replace("_", " ").title())
plt.xlabel("Game Phase")
plt.legend(ncol=2, fontsize=8)
plt.tight_layout()
plt.savefig(out_dir / f"{col}.png", dpi=140)
plt.close()
# ───────────────────────── public API ─────────────────────────
def run(exp_a: Path, exp_b: Path, alpha: float = 0.05) -> None:
df_a = _load_games(exp_a)
@ -177,7 +260,7 @@ def run(exp_a: Path, exp_b: Path, alpha: float = 0.05) -> None:
tag_a = exp_a.name or str(exp_a)
tag_b = exp_b.name or str(exp_b)
out_dir = exp_a / "analysis" / "comparison"
out_dir = exp_b / "analysis" / "comparison"
out_dir.mkdir(parents=True, exist_ok=True)
# ── section 1: aggregated across powers ───────────────────
@ -252,3 +335,15 @@ def run(exp_a: Path, exp_b: Path, alpha: float = 0.05) -> None:
print("\nCSV outputs:")
print(f"{agg_csv}")
print(f"{pow_csv}")
print('\n\nGenerating plots...')
# overlay phase-level plots
try:
_plot_phase_overlay(exp_a, exp_b, out_dir / "phase_overlay")
print(f"\nPhase overlay plots → {out_dir / 'phase_overlay'}")
except Exception as exc:
print(f"\n[warning] phase overlay plot generation failed: {exc}")
print('Complete')