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}]}