Spaces:
Sleeping
Sleeping
File size: 8,688 Bytes
451ab90 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | import os
import sys
import numpy as np
import pandas as pd
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import analysis as A # noqa: E402
@pytest.fixture(scope="module")
def df():
return A.load_players()
def test_dataset_shape_and_columns(df):
assert len(df) == 148
for col in A.REQUIRED_COLUMNS:
assert col in df.columns
assert df[A.STAT_COLUMNS + [A.SALARY_COLUMN]].isna().sum().sum() == 0
assert df[A.NAME_COLUMN].is_unique
def test_first_player_is_pete_crow_armstrong(df):
assert df.at[0, A.NAME_COLUMN] == "Pete Crow-Armstrong"
assert df.at[0, A.SALARY_COLUMN] == 19166667
def test_percentile_rank_definition():
others = np.array([1, 2, 3, 4])
assert A.percentile_rank(others, 5) == 100.0
assert A.percentile_rank(others, 0) == 0.0
assert A.percentile_rank(others, 2.5) == 50.0
assert A.percentile_rank(others, 2) == 37.5 # one below, one tie counts half
assert A.percentile_rank(others, 0, higher_is_better=False) == 100.0
def test_performance_table_directions(df):
idx = A.player_index(df, "Pete Crow-Armstrong")
table = A.performance_table(df, idx)
assert list(table["Statistic"]) == A.STAT_COLUMNS
war = table.set_index("Statistic").loc["WAR"]
assert war["Rank"] == "1 of 148"
assert war["Percentile"] == 100.0
k = table.set_index("Statistic").loc["K%"]
# Higher strikeout rate than most players, so a low percentile and a negative z-score.
assert k["Percentile"] < 50
assert k["z-score"] < 0
assert 0 <= table["Percentile"].min() and table["Percentile"].max() <= 100
def test_format_helpers():
assert A.format_stat("PA", 436.0) == "436"
assert A.format_stat("K%", 0.261467) == "26.1%"
assert A.format_stat("AVG", 0.28074) == "0.281"
assert A.format_stat("WAR", 5.8616) == "5.9"
assert A.format_stat("wRC+", 144.98) == "145"
assert A.format_money(19166667) == "$19,166,667"
assert A.format_money_short(19166667) == "$19.17M"
assert A.format_money_short(780000) == "$780K"
def test_salary_summary(df):
idx = A.player_index(df, "Pete Crow-Armstrong")
s = A.salary_summary(df, idx)
assert s["n_players"] == 148
assert s["n_market_contracts"] + s["n_minimum_contracts"] == 148
assert s["is_market_contract"] is True
assert 0 < s["salary_percentile"] < 100
assert s["salary_rank"] == int((df[A.SALARY_COLUMN] > 19166667).sum()) + 1
def test_comparables_exclude_self_and_have_k_rows(df):
idx = A.player_index(df, "Pete Crow-Armstrong")
comps, summary = A.comparable_players(df, idx)
assert len(comps) == A.N_COMPARABLES
assert "Pete Crow-Armstrong" not in comps["Player"].tolist()
assert comps["Distance"].astype(float).is_monotonic_increasing
salaries = df.set_index(A.NAME_COLUMN).loc[comps["Player"], A.SALARY_COLUMN]
assert (salaries > A.MARKET_CONTRACT_THRESHOLD).all()
assert summary["comparable_names"] == comps["Player"].tolist()
assert summary["comparable_median_salary"] == float(salaries.median())
def test_cost_per_war_handles_non_positive_war(df):
idx = int(df.index[df["WAR"] <= 0][0])
out = A.cost_per_war(df, idx)
assert out["cost_per_war"] is None
idx = A.player_index(df, "Pete Crow-Armstrong")
out = A.cost_per_war(df, idx)
assert out["cost_per_war"] == pytest.approx(19166667 / df.at[idx, "WAR"])
def test_percentile_within_distribution_interpolates():
q = np.linspace(0, 1, len(A.QUANTILE_GRID))
assert A.percentile_within_distribution(-1, q) == 1.0
assert A.percentile_within_distribution(2, q) == 99.0
assert A.percentile_within_distribution(0.5, q) == pytest.approx(50.0, abs=0.6)
def test_load_players_rejects_bad_data(tmp_path, df):
bad = df.copy()
bad.loc[0, "WAR"] = np.nan
path = tmp_path / "bad.csv"
bad.to_csv(path, index=False)
with pytest.raises(ValueError):
A.load_players(str(path))
dup = df.copy()
dup.loc[1, A.NAME_COLUMN] = dup.loc[0, A.NAME_COLUMN]
dup.to_csv(path, index=False)
with pytest.raises(ValueError):
A.load_players(str(path))
def test_cached_validation_requires_matching_fingerprint(tmp_path, df):
path = tmp_path / "loo.json"
A.save_validation({"fingerprint": "wrong", "n_estimators": A._model_n_estimators()}, str(path))
assert A.load_cached_validation(df, str(path)) is None
A.save_validation({"fingerprint": A.dataset_fingerprint(df), "n_estimators": A._model_n_estimators(), "r2_log_salary": 0.1}, str(path))
assert A.load_cached_validation(df, str(path))["r2_log_salary"] == 0.1
def test_assessment_rules():
def perf_with(pct):
return pd.DataFrame({"Statistic": A.STAT_COLUMNS, "Percentile": [pct] * len(A.STAT_COLUMNS)})
comps = {"comparable_median_salary": 2_000_000}
# Underpaid relative to the model and to rank.
out = A.assessment(perf_with(90.0), {"salary": 1_000_000, "salary_percentile": 30.0},
{"actual_percentile_in_predicted": 10.0}, comps)
assert out["model_band"] == "below"
assert out["headline"].startswith("Performance supports the salary")
assert out["comparables_reading"].startswith("paid less")
# Inside the model range, ranks close.
out = A.assessment(perf_with(60.0), {"salary": 2_100_000, "salary_percentile": 55.0},
{"actual_percentile_in_predicted": 50.0}, comps)
assert out["model_band"] == "inside"
assert out["headline"].startswith("Performance supports the salary")
assert out["comparables_reading"].startswith("paid about the same")
# Overpaid relative to the model and to rank.
out = A.assessment(perf_with(20.0), {"salary": 30_000_000, "salary_percentile": 95.0},
{"actual_percentile_in_predicted": 90.0}, comps)
assert out["model_band"] == "above"
assert out["headline"].startswith("Performance does not support")
assert out["comparables_reading"].startswith("paid more")
# Conflicts produce a mixed reading.
out = A.assessment(perf_with(95.0), {"salary": 30_000_000, "salary_percentile": 60.0},
{"actual_percentile_in_predicted": 90.0}, comps)
assert out["headline"].startswith("The evidence is mixed")
out = A.assessment(perf_with(10.0), {"salary": 5_000_000, "salary_percentile": 60.0},
{"actual_percentile_in_predicted": 40.0}, comps)
assert out["headline"].startswith("The evidence is mixed")
@pytest.mark.slow
def test_model_prediction_for_pete_crow_armstrong(df):
idx = A.player_index(df, "Pete Crow-Armstrong")
out = A.model_salary_for_player(df, idx, n_estimators=4)
assert out["n_train"] == int((df[A.SALARY_COLUMN] > A.MARKET_CONTRACT_THRESHOLD).sum()) - 1
assert out["selected_player_is_market_contract"] is True
q = out["predicted_quantiles"]
assert q["q10"] <= q["q25"] <= q["q50"] <= q["q75"] <= q["q90"]
assert 1.0 <= out["actual_percentile_in_predicted"] <= 99.0
assert out["predicted_median_salary"] > 0
@pytest.mark.slow
def test_analyze_player_bundle_is_json_serializable(df):
result = A.analyze_player(df, "Pete Crow-Armstrong", n_estimators=4)
payload = A.analysis_to_json(result)
assert payload["player"]["name"] == "Pete Crow-Armstrong"
assert len(payload["performance_table"]) == len(A.STAT_COLUMNS)
assert isinstance(payload["assessment"]["headline"], str)
def test_training_mask_excludes_minimum_contracts_and_selected_player(df):
idx = A.player_index(df, "Pete Crow-Armstrong")
mask = A.model_training_mask(df, idx)
assert not mask[idx]
assert (df.loc[mask, A.SALARY_COLUMN] > A.MARKET_CONTRACT_THRESHOLD).all()
minimum_idx = int(df.index[df[A.SALARY_COLUMN] <= A.MARKET_CONTRACT_THRESHOLD][0])
assert not A.model_training_mask(df, minimum_idx)[minimum_idx]
assert A.model_training_mask(df, minimum_idx).sum() == A.model_training_mask(df).sum()
def test_salary_percentile_among_market_is_none_for_minimum_players(df):
minimum_idx = int(df.index[df[A.SALARY_COLUMN] <= A.MARKET_CONTRACT_THRESHOLD][0])
assert A.salary_summary(df, minimum_idx)["salary_percentile_among_market"] is None
idx = A.player_index(df, "Pete Crow-Armstrong")
assert 0 < A.salary_summary(df, idx)["salary_percentile_among_market"] < 100
def test_analysis_to_json_replaces_non_finite_values():
payload = A.analysis_to_json({"a": float("nan"), "b": np.float64("inf"), "c": np.int64(3), "d": [np.float32(1.5)],
"e": pd.DataFrame({"x": [np.nan, 2.0]})})
assert payload == {"a": None, "b": None, "c": 3, "d": [1.5], "e": [{"x": None}, {"x": 2.0}]}
|