Spaces:
Running
Running
| 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() | |