Spaces:
Running
Running
File size: 16,196 Bytes
c095e08 |
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 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 |
"""
NBA ML Prediction System - Flask Backend
=========================================
REST API for the React frontend.
"""
from flask import Flask, jsonify, request
from flask_cors import CORS
import sys
import logging
from pathlib import Path
# Configure logging to reduce verbosity
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("src.injury_collector").setLevel(logging.WARNING)
logging.getLogger("src.prediction_tracker").setLevel(logging.WARNING)
logging.getLogger("chromadb").setLevel(logging.WARNING)
# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.prediction_pipeline import PredictionPipeline
# Initialize Flask app
app = Flask(__name__)
CORS(app, origins=["http://localhost:5173", "http://localhost:3000", "http://127.0.0.1:5173"])
# Initialize prediction pipeline (ELO ratings loaded on startup)
print("Initializing prediction pipeline...")
pipeline = PredictionPipeline()
print("Pipeline ready!")
@app.route("/api/health")
def health_check():
"""Health check endpoint."""
return jsonify({"status": "healthy", "pipeline_ready": pipeline is not None})
@app.route("/api/games/live")
def get_live_games():
"""Get today's games with live scores and predictions."""
games = pipeline.get_games_with_predictions()
# Process each game - save predictions for upcoming, update results for completed
for game in games:
status = game.get("status")
game_id = game.get("game_id")
pred = game.get("prediction", {})
if game_id and pred:
if status == "NOT_STARTED":
# Only save if prediction doesn't already exist
existing = pipeline.prediction_tracker.get_prediction(game_id)
if not existing:
pipeline.prediction_tracker.save_prediction(game_id, {
"game_date": game.get("game_date"),
"home_team": game.get("home_team"),
"away_team": game.get("away_team"),
"predicted_winner": pred.get("predicted_winner"),
"home_win_probability": pred.get("home_win_probability"),
"away_win_probability": pred.get("away_win_probability"),
"confidence": pred.get("confidence"),
"home_elo": pred.get("home_elo"),
"away_elo": pred.get("away_elo"),
})
elif status == "FINAL":
# Update result for completed game
home_score = game.get("home_score", 0)
away_score = game.get("away_score", 0)
actual_winner = game.get("home_team") if home_score > away_score else game.get("away_team")
# Check if prediction exists, if not save it first
existing = pipeline.prediction_tracker.get_prediction(game_id)
if not existing:
# Save prediction first (for games completed before tracking started)
pipeline.prediction_tracker.save_prediction(game_id, {
"game_date": game.get("game_date"),
"home_team": game.get("home_team"),
"away_team": game.get("away_team"),
"predicted_winner": pred.get("predicted_winner"),
"home_win_probability": pred.get("home_win_probability"),
"away_win_probability": pred.get("away_win_probability"),
"confidence": pred.get("confidence"),
"home_elo": pred.get("home_elo"),
"away_elo": pred.get("away_elo"),
})
# Now update with result
pipeline.prediction_tracker.update_result(
game_id,
actual_winner,
home_score,
away_score
)
# Mark whether our prediction was correct
game["prediction_correct"] = pred.get("predicted_winner") == actual_winner
# Separate by status
return jsonify({
"live": [g for g in games if g.get("status") == "IN_PROGRESS"],
"final": [g for g in games if g.get("status") == "FINAL"],
"upcoming": [g for g in games if g.get("status") == "NOT_STARTED"],
"total": len(games)
})
@app.route("/api/games/upcoming")
def get_upcoming_games():
"""Get upcoming games for the next N days."""
days = request.args.get("days", 7, type=int)
days = max(1, min(days, 14)) # Clamp between 1-14
games = pipeline.get_upcoming_games(days_ahead=days)
# Add predictions to each game
enriched_games = []
for game in games:
pred = pipeline.predict_game(game["home_team"], game["away_team"])
enriched_games.append({
**game,
"prediction": pred
})
return jsonify({"games": enriched_games, "count": len(enriched_games)})
@app.route("/api/predict")
def predict_game():
"""Predict outcome for a single game."""
home = request.args.get("home", "").upper()
away = request.args.get("away", "").upper()
if not home or not away:
return jsonify({"error": "Missing home or away team parameter"}), 400
prediction = pipeline.predict_game(home, away)
return jsonify(prediction)
@app.route("/api/accuracy")
def get_accuracy():
"""Get comprehensive model accuracy statistics."""
stats = pipeline.get_accuracy_stats()
recent = pipeline.get_recent_predictions(50) # Get more for analysis
# Calculate additional metrics
completed = [p for p in recent if p.get("is_correct", -1) >= 0]
correct = [p for p in completed if p.get("is_correct") == 1]
# Home vs Away accuracy
home_picks = [p for p in completed if p.get("predicted_winner") == p.get("home_team")]
home_correct = [p for p in home_picks if p.get("is_correct") == 1]
away_picks = [p for p in completed if p.get("predicted_winner") == p.get("away_team")]
away_correct = [p for p in away_picks if p.get("is_correct") == 1]
# Current streak
streak = 0
streak_type = None
for p in sorted(completed, key=lambda x: x.get("updated_at", ""), reverse=True):
if streak_type is None:
streak_type = "W" if p.get("is_correct") == 1 else "L"
if (p.get("is_correct") == 1 and streak_type == "W") or (p.get("is_correct") == 0 and streak_type == "L"):
streak += 1
else:
break
# Last 10 games
last_10 = completed[:10] if len(completed) >= 10 else completed
last_10_correct = sum(1 for p in last_10 if p.get("is_correct") == 1)
# Average win probability for correct vs incorrect predictions
correct_avg_prob = sum(max(p.get("home_win_prob", 0.5), p.get("away_win_prob", 0.5)) for p in correct) / len(correct) if correct else 0
incorrect = [p for p in completed if p.get("is_correct") == 0]
incorrect_avg_prob = sum(max(p.get("home_win_prob", 0.5), p.get("away_win_prob", 0.5)) for p in incorrect) / len(incorrect) if incorrect else 0
# Build enhanced response
enhanced_stats = {
**stats,
"home_pick_accuracy": len(home_correct) / len(home_picks) if home_picks else 0,
"away_pick_accuracy": len(away_correct) / len(away_picks) if away_picks else 0,
"home_picks_total": len(home_picks),
"away_picks_total": len(away_picks),
"current_streak": streak,
"streak_type": streak_type or "N/A",
"last_10_record": f"{last_10_correct}-{len(last_10) - last_10_correct}",
"last_10_accuracy": last_10_correct / len(last_10) if last_10 else 0,
"avg_probability_correct": correct_avg_prob,
"avg_probability_incorrect": incorrect_avg_prob,
"pending_predictions": len([p for p in recent if p.get("is_correct", -1) == -1]),
}
return jsonify({
"stats": enhanced_stats,
"recent_predictions": recent[:20] # Return 20 most recent for display
})
@app.route("/api/mvp")
def get_mvp_race():
"""Get current MVP race standings."""
mvp_df = pipeline.get_mvp_race()
# Convert DataFrame to list of dicts
candidates = []
for idx, row in mvp_df.iterrows():
candidates.append({
"rank": len(candidates) + 1,
"name": row["PLAYER_NAME"],
"ppg": round(float(row["PTS"]), 1),
"rpg": round(float(row["REB"]), 1),
"apg": round(float(row["AST"]), 1),
"mvp_score": round(float(row["mvp_score"]), 1),
"similarity": round(float(row["mvp_similarity"]) * 100, 1)
})
return jsonify({"candidates": candidates})
@app.route("/api/championship")
def get_championship_odds():
"""Get current championship odds."""
champ_df = pipeline.get_championship_odds()
# Convert DataFrame to list of dicts
teams = []
for idx, row in champ_df.iterrows():
# ChampionshipPredictor returns: TEAM_ABBREVIATION, W_PCT, playoff_experience, strength_rating, champ_probability
teams.append({
"rank": len(teams) + 1,
"team": row.get("TEAM_ABBREVIATION", row.get("Team", "N/A")),
"odds": round(float(row.get("champ_probability", row.get("Championship_Odds", 0))) * 100, 1),
"win_pct": round(float(row.get("W_PCT", 0.5)) * 100, 1)
})
return jsonify({"teams": teams})
@app.route("/api/teams")
def get_teams():
"""Get list of all NBA teams."""
from src.config import NBA_TEAMS
teams = [{"id": tid, "abbrev": abbrev} for tid, abbrev in NBA_TEAMS.items()]
teams.sort(key=lambda x: x["abbrev"])
return jsonify({"teams": teams})
@app.route("/api/roster/<team_abbrev>")
def get_team_roster(team_abbrev):
"""Get projected starting 5 for a team using LIVE 2025-26 season stats."""
try:
from nba_api.stats.endpoints import leaguedashplayerstats
import time
# Fetch current season player stats from NBA API
time.sleep(0.5) # Rate limiting
stats = leaguedashplayerstats.LeagueDashPlayerStats(
season='2025-26',
per_mode_detailed='PerGame'
)
df = stats.get_data_frames()[0]
# Filter by team
team_abbrev = team_abbrev.upper()
team_players = df[df['TEAM_ABBREVIATION'] == team_abbrev].copy()
if team_players.empty:
return jsonify({"team": team_abbrev, "starters": []})
# Sort by minutes played (starters play the most minutes)
team_players = team_players.sort_values('MIN', ascending=False)
# Get top 5 players (projected starters)
starters = []
for _, player in team_players.head(5).iterrows():
starters.append({
'name': player['PLAYER_NAME'],
'position': player.get('POSITION', ''),
'pts': round(float(player['PTS']), 1),
'reb': round(float(player.get('REB', 0)), 1),
'ast': round(float(player.get('AST', 0)), 1),
'min': round(float(player.get('MIN', 0)), 1)
})
return jsonify({"team": team_abbrev, "starters": starters})
except Exception as e:
print(f"Error fetching roster for {team_abbrev}: {e}")
# Fallback to pipeline method
roster = pipeline.get_team_roster(team_abbrev.upper())
return jsonify({"team": team_abbrev.upper(), "starters": roster})
@app.route("/api/standings")
def get_standings():
"""Get current NBA standings by conference."""
# Team name to abbreviation mapping for fallback - includes team names, cities, and variants
TEAM_NAME_TO_ABBREV = {
# Team nicknames
"hawks": "ATL", "celtics": "BOS", "nets": "BKN", "hornets": "CHA",
"bulls": "CHI", "cavaliers": "CLE", "cavs": "CLE", "mavericks": "DAL", "mavs": "DAL",
"nuggets": "DEN", "pistons": "DET", "warriors": "GSW", "dubs": "GSW",
"rockets": "HOU", "pacers": "IND", "clippers": "LAC", "lakers": "LAL",
"grizzlies": "MEM", "heat": "MIA", "bucks": "MIL", "timberwolves": "MIN", "wolves": "MIN",
"pelicans": "NOP", "pels": "NOP", "knicks": "NYK", "thunder": "OKC",
"magic": "ORL", "76ers": "PHI", "sixers": "PHI", "suns": "PHX",
"trail blazers": "POR", "blazers": "POR", "trailblazers": "POR",
"kings": "SAC", "spurs": "SAS", "raptors": "TOR", "jazz": "UTA", "wizards": "WAS",
# City names
"atlanta": "ATL", "boston": "BOS", "brooklyn": "BKN", "charlotte": "CHA",
"chicago": "CHI", "cleveland": "CLE", "dallas": "DAL", "denver": "DEN",
"detroit": "DET", "golden state": "GSW", "houston": "HOU", "indiana": "IND",
"los angeles c": "LAC", "la c": "LAC", "los angeles l": "LAL", "la l": "LAL",
"memphis": "MEM", "miami": "MIA", "milwaukee": "MIL", "minnesota": "MIN",
"new orleans": "NOP", "new york": "NYK", "oklahoma city": "OKC", "oklahoma": "OKC",
"orlando": "ORL", "philadelphia": "PHI", "phoenix": "PHX", "portland": "POR",
"sacramento": "SAC", "san antonio": "SAS", "toronto": "TOR", "utah": "UTA", "washington": "WAS"
}
def get_abbrev_from_name(team_name):
"""Extract abbreviation from team name."""
team_name_lower = team_name.lower().strip()
for name_part, abbrev in TEAM_NAME_TO_ABBREV.items():
if name_part in team_name_lower:
return abbrev
return ""
try:
from nba_api.stats.endpoints import leaguestandings
import time
time.sleep(0.5)
standings = leaguestandings.LeagueStandings(season='2025-26')
df = standings.get_data_frames()[0]
# Debug: print column names on first run
print(f"Standings columns: {list(df.columns)}")
east = []
west = []
for _, row in df.iterrows():
# Try multiple possible column names for team abbreviation
abbrev = ""
for col_name in ["TeamSlug", "TeamAbbreviation", "TEAM_ABBREVIATION", "team_abbreviation"]:
if col_name in row.index and row.get(col_name):
abbrev = str(row.get(col_name)).upper().strip()
break
# Build team name from city + name
team_city = str(row.get("TeamCity", ""))
team_name = str(row.get("TeamName", ""))
full_team_name = f"{team_city} {team_name}".strip()
# If abbreviation still not found, extract from team name
if not abbrev:
abbrev = get_abbrev_from_name(full_team_name)
team_data = {
"team": abbrev, # Use abbreviation for frontend TeamLogo component
"team_abbrev": abbrev, # Duplicate for clarity
"team_name": full_team_name,
"wins": int(row.get("WINS", 0)),
"losses": int(row.get("LOSSES", 0)),
"win_pct": float(row.get("WinPCT", 0)),
"gb": str(row.get("ConferenceGamesBack", "-")),
"streak": str(row.get("strCurrentStreak", "-")),
"conference": row.get("Conference", ""),
}
if row.get("Conference") == "East":
east.append(team_data)
else:
west.append(team_data)
# Sort by wins descending
east.sort(key=lambda x: (-x["wins"], x["losses"]))
west.sort(key=lambda x: (-x["wins"], x["losses"]))
return jsonify({"east": east, "west": west})
except Exception as e:
print(f"Error fetching standings: {e}")
import traceback
traceback.print_exc()
return jsonify({"east": [], "west": [], "error": str(e)})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
|