2026_MLB_Model / tests /test_props_view_model.py
Syntrex's picture
Hide tracked-only games from HR game explorer
7ac6efd
raw
history blame
13.6 kB
from __future__ import annotations
import os
import sys
import unittest
import pandas as pd
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from analytics.props_view_model import (
build_best_on_slate_df,
build_best_on_slate_summary,
build_featured_hr_props_df,
build_game_player_props_map,
build_games_summary_df,
build_hr_props_view_model,
build_player_prop_detail_map,
select_best_lines_per_prop,
)
class TestPropsViewModel(unittest.TestCase):
def _sample_mapped_df(self) -> pd.DataFrame:
return pd.DataFrame(
[
{
"event_id": "evt-1",
"away_team": "Yankees",
"home_team": "Giants",
"commence_time": "2026-03-25T00:05:00Z",
"player_name": "aaron judge",
"player_name_raw": "Aaron Judge",
"sportsbook": "Caesars",
"market": "hr",
"market_family": "hr",
"threshold": 1,
"display_label": "1+ HR",
"is_modeled": True,
"has_model_probability": True,
"odds_american": 245,
"model_hr_prob": 0.19,
"bet_ev": 0.041,
"confidence_score": 84.0,
"edge": 0.035,
"final_recommendation_score": 0.91,
"player_event_market_key": "evt-1|aaron judge|hr|1",
},
{
"event_id": "evt-1",
"away_team": "Yankees",
"home_team": "Giants",
"commence_time": "2026-03-25T00:05:00Z",
"player_name": "aaron judge",
"player_name_raw": "Aaron Judge",
"sportsbook": "FanDuel",
"market": "hr",
"market_family": "hr",
"threshold": 1,
"display_label": "1+ HR",
"is_modeled": True,
"has_model_probability": True,
"odds_american": 230,
"model_hr_prob": 0.19,
"bet_ev": 0.036,
"confidence_score": 80.0,
"edge": 0.029,
"final_recommendation_score": 0.83,
"player_event_market_key": "evt-1|aaron judge|hr|1",
},
{
"event_id": "evt-1",
"away_team": "Yankees",
"home_team": "Giants",
"commence_time": "2026-03-25T00:05:00Z",
"player_name": "aaron judge",
"player_name_raw": "Aaron Judge",
"sportsbook": "Caesars",
"market": "hr",
"market_family": "hr",
"threshold": 2,
"display_label": "2+ HR",
"is_modeled": False,
"has_model_probability": False,
"odds_american": 1750,
"model_hr_prob": None,
"edge": None,
"player_event_market_key": "evt-1|aaron judge|hr|2",
},
{
"event_id": "evt-1",
"away_team": "Yankees",
"home_team": "Giants",
"commence_time": "2026-03-25T00:05:00Z",
"player_name": "giancarlo stanton",
"player_name_raw": "Giancarlo Stanton",
"sportsbook": "Caesars",
"market": "hr",
"market_family": "hr",
"threshold": 1,
"display_label": "1+ HR",
"is_modeled": True,
"has_model_probability": True,
"odds_american": 390,
"model_hr_prob": 0.16,
"bet_ev": 0.068,
"confidence_score": 77.0,
"edge": 0.041,
"final_recommendation_score": 0.95,
"player_event_market_key": "evt-1|giancarlo stanton|hr|1",
},
{
"event_id": "evt-2",
"away_team": "Mets",
"home_team": "Cubs",
"commence_time": "2026-03-25T01:10:00Z",
"player_name": "pete alonso",
"player_name_raw": "Pete Alonso",
"sportsbook": "Caesars",
"market": "hr",
"market_family": "hr",
"threshold": 1,
"display_label": "1+ HR",
"is_modeled": True,
"has_model_probability": True,
"odds_american": 310,
"model_hr_prob": 0.15,
"bet_ev": 0.022,
"confidence_score": 73.0,
"edge": 0.024,
"final_recommendation_score": 0.78,
"player_event_market_key": "evt-2|pete alonso|hr|1",
},
]
)
def _sample_mapped_df_with_missing_primary_prob(self) -> pd.DataFrame:
df = self._sample_mapped_df()
mask = df["player_name"] == "pete alonso"
df.loc[mask, "has_model_probability"] = False
df.loc[mask, "model_hr_prob"] = None
df.loc[mask, "edge"] = None
df.loc[mask, "bet_ev"] = None
df.loc[mask, "model_probability_status"] = "missing_baseline"
return df
def _sample_cross_market_df(self) -> pd.DataFrame:
base = self._sample_mapped_df()
strikeout_rows = pd.DataFrame(
[
{
"event_id": "evt-3",
"away_team": "Pirates",
"home_team": "Mets",
"commence_time": "2026-03-25T17:15:00Z",
"player_name": "paul skenes",
"player_name_raw": "Paul Skenes",
"sportsbook": "DraftKings",
"market": "k",
"market_family": "k",
"display_label": "Over 7.5 Ks",
"selection_side": "over",
"line": 7.5,
"is_modeled": True,
"odds_american": 110,
"fair_prob": 0.58,
"bet_ev": 0.081,
"confidence_score": 76.0,
"edge": 0.034,
"final_recommendation_score": 0.88,
"player_event_market_key": "evt-3|paul skenes|k|7.5|over",
},
{
"event_id": "evt-3",
"away_team": "Pirates",
"home_team": "Mets",
"commence_time": "2026-03-25T17:15:00Z",
"player_name": "paul skenes",
"player_name_raw": "Paul Skenes",
"sportsbook": "FanDuel",
"market": "k",
"market_family": "k",
"display_label": "Over 7.5 Ks",
"selection_side": "over",
"line": 7.5,
"is_modeled": True,
"odds_american": 102,
"fair_prob": 0.58,
"bet_ev": 0.063,
"confidence_score": 74.0,
"edge": 0.027,
"final_recommendation_score": 0.80,
"player_event_market_key": "evt-3|paul skenes|k|7.5|over",
},
]
)
return pd.concat([base, strikeout_rows], ignore_index=True, sort=False)
def test_featured_props_only_use_modeled_primary_rows(self) -> None:
featured = build_featured_hr_props_df(self._sample_mapped_df(), limit=5)
self.assertEqual(len(featured), 3)
self.assertEqual(featured.iloc[0]["player_name"], "giancarlo stanton")
self.assertTrue((featured["threshold"] == 1).all())
self.assertTrue(featured["is_modeled"].all())
self.assertNotIn("2+ HR", featured["display_label"].tolist())
judge_row = featured[featured["player_name"] == "aaron judge"].iloc[0]
self.assertEqual(judge_row["sportsbook"], "Caesars")
self.assertEqual(int(judge_row["odds_american"]), 245)
def test_featured_props_excludes_primary_rows_missing_probability(self) -> None:
featured = build_featured_hr_props_df(self._sample_mapped_df_with_missing_primary_prob(), limit=5)
self.assertNotIn("pete alonso", featured["player_name"].tolist())
def test_games_summary_tracks_modeled_props_and_top_edge(self) -> None:
summary = build_games_summary_df(self._sample_mapped_df())
self.assertEqual(len(summary), 2)
first_game = summary.iloc[0]
self.assertEqual(first_game["event_id"], "evt-1")
self.assertEqual(int(first_game["modeled_props_count"]), 2)
self.assertEqual(first_game["top_player_name"], "giancarlo stanton")
self.assertAlmostEqual(float(first_game["best_edge"]), 0.041, places=6)
def test_games_summary_excludes_primary_rows_without_probability(self) -> None:
summary = build_games_summary_df(self._sample_mapped_df_with_missing_primary_prob())
self.assertNotIn("evt-2", summary["event_id"].tolist())
def test_player_detail_map_separates_primary_and_alt_rows(self) -> None:
detail_map = build_player_prop_detail_map(self._sample_mapped_df())
judge = detail_map["evt-1|aaron judge"]
self.assertTrue(judge["has_modeled_row"])
self.assertTrue(judge["has_alt_ladders"])
self.assertEqual(len(judge["primary_rows"]), 2)
self.assertEqual(len(judge["alt_rows"]), 1)
self.assertEqual(judge["best_primary_row"]["display_label"], "1+ HR")
self.assertEqual(judge["best_book"], "Caesars")
self.assertAlmostEqual(float(judge["best_bet_ev"]), 0.041, places=6)
def test_game_player_map_groups_players_under_each_game(self) -> None:
game_map = build_game_player_props_map(self._sample_mapped_df())
self.assertEqual(set(game_map.keys()), {"evt-1", "evt-2"})
yankees_giants = game_map["evt-1"]
self.assertEqual(yankees_giants["top_player_name"], "giancarlo stanton")
self.assertEqual(len(yankees_giants["players"]), 2)
first_player = yankees_giants["players"][0]
self.assertEqual(first_player["player_name"], "giancarlo stanton")
self.assertFalse(first_player["has_alt_ladders"])
def test_full_view_model_returns_all_sections(self) -> None:
vm = build_hr_props_view_model(self._sample_mapped_df(), featured_limit=2)
self.assertEqual(set(vm.keys()), {
"featured_props_df",
"best_on_slate_df",
"best_on_slate_summary",
"games_summary_df",
"game_player_props_map",
"player_prop_detail_map",
})
self.assertEqual(len(vm["featured_props_df"]), 2)
self.assertEqual(len(vm["games_summary_df"]), 2)
self.assertIn("evt-1", vm["game_player_props_map"])
def test_select_best_lines_per_prop_is_threshold_aware(self) -> None:
best = select_best_lines_per_prop(self._sample_mapped_df())
self.assertEqual(len(best), 4)
judge_primary = best[(best["player_name"] == "aaron judge") & (best["threshold"] == 1)].iloc[0]
judge_alt = best[(best["player_name"] == "aaron judge") & (best["threshold"] == 2)].iloc[0]
self.assertEqual(judge_primary["sportsbook"], "Caesars")
self.assertEqual(int(judge_primary["odds_american"]), 245)
self.assertEqual(judge_alt["sportsbook"], "Caesars")
self.assertEqual(int(judge_alt["odds_american"]), 1750)
def test_best_on_slate_includes_hr_and_strikeouts_after_best_line_reduction(self) -> None:
best = build_best_on_slate_df(self._sample_cross_market_df(), limit=10)
self.assertEqual(set(best["market_family"].tolist()), {"hr", "k"})
skenes = best[best["player_name"] == "paul skenes"].iloc[0]
self.assertEqual(skenes["sportsbook"], "DraftKings")
self.assertEqual(float(best.iloc[0]["bet_ev"]), 0.081)
def test_best_on_slate_summary_counts_modes_and_books(self) -> None:
summary = build_best_on_slate_summary(self._sample_cross_market_df())
self.assertEqual(int(summary["modeled_props_count"]), 4)
self.assertEqual(int(summary["sportsbooks_count"]), 2)
self.assertEqual(int(summary["markets_count"]), 2)
self.assertAlmostEqual(float(summary["best_ev"]), 0.081, places=6)
def test_full_view_model_preserves_full_slate_summary_when_featured_is_limited(self) -> None:
vm = build_hr_props_view_model(self._sample_cross_market_df(), featured_limit=2)
self.assertEqual(len(vm["best_on_slate_df"]), 2)
self.assertEqual(int(vm["best_on_slate_summary"]["modeled_props_count"]), 4)
self.assertEqual(int(vm["best_on_slate_summary"]["markets_count"]), 2)
def test_game_player_map_excludes_games_with_zero_modeled_primary_props(self) -> None:
game_map = build_game_player_props_map(self._sample_mapped_df_with_missing_primary_prob())
self.assertNotIn("evt-2", game_map)
if __name__ == "__main__":
unittest.main()