Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
177ac1e
feat: add global NBA player prediction pipeline
Feb 15, 2026
332b367
feat(frontend): add UI demo
nidhid11 Feb 18, 2026
8caa7d1
feat(query): base function
omkar055 Feb 19, 2026
bfb8a8d
feat(frontend): added sending and receiving requests
rhtruong Feb 19, 2026
9030aec
chore(frontend): remove unnecessary files
rhtruong Feb 19, 2026
76c391a
Merge branch 'arnav-model-servicing' into training-player-data
ArnavSatish Feb 19, 2026
0fa1c30
Updated training pipeline with opponent one-hot, days rest, lag featu…
Feb 19, 2026
508828c
Merge branch 'training-player-data' of github.com:JonathanPLev/Transf…
ArnavSatish Feb 19, 2026
443cf05
ignore model artifacts
ArnavSatish Feb 19, 2026
8697ff3
Add FastAPI backend server with health, ready, and predict endpoints;…
ArnavSatish Feb 19, 2026
42dbba6
Fix lint: replace bare except with ValueError
ArnavSatish Feb 19, 2026
0acf1fa
Fix lint: remove unused imports
ArnavSatish Feb 19, 2026
80d0d84
Added lag features for 5 games, added minutes played lag features, ch…
Feb 21, 2026
26988b1
Merge branch 'training-player-data' of github.com:JonathanPLev/Transf…
Feb 21, 2026
d77b8c2
feat(query): time function
omkar055 Feb 22, 2026
853a877
chore(frontend): updating sending and requesting functions
rhtruong Feb 24, 2026
f88f340
Merge branch 'training-player-data' of https://github.com/JonathanPLe…
rhtruong Feb 24, 2026
bb5e77d
(chore): Resolve uv.lock merge conflict
omkar055 Feb 26, 2026
92def0b
(feat): update deps + fetch_player_data
omkar055 Feb 26, 2026
014c8df
feat(injury): pulled injury report from nbainjuries
omkar055 Feb 26, 2026
fd2384c
feat(schedule_status): final flow of function
omkar055 Mar 2, 2026
6a05f49
fix(build_predictors): fixed injury status bug
omkar055 Mar 2, 2026
fed4eb2
fix(flow check): changed days_rest and opponent_id
omkar055 Mar 2, 2026
4f16ff7
fix: correct days_rest anchor + opponent_id logic
omkar055 Mar 2, 2026
a39f266
fix(nba_dataset): parse MM:SS numMinutes and insert stub games for FK
ArnavSatish Mar 2, 2026
944594a
Add LeagueSchedule25_26.csv to download script
ArnavSatish Mar 3, 2026
8d18bee
fix(features): debugged team after db referesh
omkar055 Mar 2, 2026
8f99499
(feat): csv dataset cuz db broke
omkar055 Mar 3, 2026
29d8f52
fix(league_scuedule): workaround for calculating training data)
omkar055 Mar 3, 2026
2f3c080
fix: schedule csv + server alignment
omkar055 Mar 3, 2026
623bc4d
feat(demo): frontend + backend integration
rhtruong Mar 4, 2026
ceef786
refactor(demo): switch demo to using db
rhtruong Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,7 @@ __marimo__/
src/nba_dataset/Data/*.csv

Data/
model_state.pt
preproc.joblib
*.pt
*.joblib
57 changes: 57 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NBA Player Performance Predictor</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="bubble-container">
<div class="bubble">
<h1>🏀 NBA Predictor</h1>
<p class="subtitle">Predict player performance for their next game</p>

<div class="input-section">
<input
type="text"
id="playerNameInput"
placeholder="Enter player name..."
maxlength="64"
autocomplete="off"
/>
<button id="submitButton" onclick="handleSubmit()">
Submit
</button>
</div>

<div id="loadingSection" class="loading-section hidden">
<div class="basketball-loader">
<div class="basketball">🏀</div>
</div>
<p id="loadingMessage" class="loading-message">Dribbling the data...</p>
</div>

<div id="resultSection" class="result-section hidden">
<div class="result-header">
<h2>🎯 Prediction</h2>
</div>
<div id="resultContent" class="result-content"></div>
<button class="retry-button" onclick="resetForm()">Predict Another Player</button>
</div>

<div id="errorSection" class="error-section hidden">
<div class="error-icon">⚠️</div>
<p id="errorMessage" class="error-message"></p>
<button class="retry-button" onclick="resetForm()">Try Another Player</button>
</div>
</div>
</div>

<script>
window.__PREDICT_API_BASE__ = "http://localhost:8000";
</script>
<script src="player-api.js"></script>
<script src="script.js"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions frontend/launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash

echo "Launching NBA Predictor Frontend..."

if command -v python3 &> /dev/null; then
echo "Starting local server on http://localhost:8000"
echo "Press Ctrl+C to stop the server"

python3 -m http.server 8000 &

SERVER_PID=$!

sleep 2

if [[ "$OSTYPE" == "darwin"* ]]; then
#macOS
open http://localhost:8000
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
#linux
xdg-open http://localhost:8000
fi

echo "Server running with PID: $SERVER_PID"
echo "To stop: kill $SERVER_PID"

wait $SERVER_PID
else
echo "Python 3 not found. Opening file directly..."
if [[ "$OSTYPE" == "darwin"* ]]; then
open index.html
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
xdg-open index.html
fi
fi
153 changes: 153 additions & 0 deletions frontend/player-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
(function () {
"use strict";

const API_BASE = (window.__PREDICT_API_BASE__ || "").replace(/\/+$/, "");
const PREDICT_ENDPOINT = API_BASE + "/predict";
const REQUEST_TIMEOUT_MS = 30_000;

const MAX_NAME_LENGTH = 64;
const ALLOWED_CHARS = /^[a-zA-Z\s\-']+$/;
const CONTROL_CHARS = /[\x00-\x1F\x7F-\x9F]/g;
const FANCY_APOSTROPHES = /[\u2018\u2019\u201A\u201B\u0060\u00B4]/g;

function sanitizePlayerName(raw) {
if (typeof raw !== "string") return null;

let s = raw.trim();
if (s.length === 0) return null;

s = s.replace(CONTROL_CHARS, "");
s = s.replace(/\s+/g, " ").trim();
s = s.replace(FANCY_APOSTROPHES, "'");

if (s.length > MAX_NAME_LENGTH) {
s = s.substring(0, MAX_NAME_LENGTH).trim();
}

if (!ALLOWED_CHARS.test(s)) return null;
if (!/[a-zA-Z]/.test(s)) return null;

return s;
}

async function sendPredictRequest(cleanedName) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);

try {
const res = await fetch(PREDICT_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({ query: cleanedName }),
signal: controller.signal,
});
return res;
} finally {
clearTimeout(timeout);
}
}

function decodeResponse(httpStatus, body) {
const result = {
statusCode: httpStatus,
playerResult: null,
officialPlayerName: null,
teamAgainst: null,
timeAndDateEST: null,
errorMessage: null,
};

if (!body || typeof body !== "object") {
result.errorMessage = "Received an invalid response from the server.";
return result;
}

if (typeof body.prediction === "number") {
result.playerResult = body.prediction;
}

const meta = body.metadata || {};
result.officialPlayerName =
meta.player_name || meta.cleaned_query || null;
result.teamAgainst =
meta.opponent || meta.team_against || null;
result.timeAndDateEST =
meta.game_time || meta.game_datetime_est || null;

const errorType = meta.error_type || null;

if (httpStatus === 404) {
result.errorMessage =
"Player not found. Please check the spelling and try again.";
} else if (httpStatus === 422) {
if (errorType === "inactive") {
result.errorMessage =
"This player is not currently active in the NBA. Please try another player.";
} else {
result.errorMessage =
"This player is currently injured and cannot be predicted. Please try another player.";
}
} else if (httpStatus >= 500) {
result.errorMessage =
"The server encountered an error. Please try again later.";
}

return result;
}

async function submitPlayerNameAndGetPrediction(rawInput) {
const cleaned = sanitizePlayerName(rawInput);
if (!cleaned) {
return {
statusCode: 422,
playerResult: null,
officialPlayerName: null,
teamAgainst: null,
timeAndDateEST: null,
errorMessage:
"Invalid player name.",
};
}


let httpResponse;
try {
httpResponse = await sendPredictRequest(cleaned);
} catch (err) {
const isTimeout = err.name === "AbortError";
return {
statusCode: isTimeout ? 408 : 0,
playerResult: null,
officialPlayerName: null,
teamAgainst: null,
timeAndDateEST: null,
errorMessage: isTimeout
? "Request timed out. The server may be busy — please try again."
: "Unable to connect to the prediction server. Please check your connection and try again.",
};
}

let body;
try {
body = await httpResponse.json();
} catch (_) {
return {
statusCode: httpResponse.status,
playerResult: null,
officialPlayerName: null,
teamAgainst: null,
timeAndDateEST: null,
errorMessage: "Received a malformed response from the server.",
};
}


return decodeResponse(httpResponse.status, body);
}

window.submitPlayerNameAndGetPrediction = submitPlayerNameAndGetPrediction;
window.sanitizePlayerName = sanitizePlayerName;
})();
Loading
Loading