diff --git a/src/common/constants.basketball.ts b/src/common/constants.basketball.ts index 7efda131d6..e9ff7481c4 100644 --- a/src/common/constants.basketball.ts +++ b/src/common/constants.basketball.ts @@ -275,6 +275,7 @@ const PLAYER_STATS_TABLES = { "dbpm", "bpm", "vorp", + "sovr", ], }, gameHighs: { diff --git a/src/common/getCols.ts b/src/common/getCols.ts index 22066939cc..b420f9800a 100644 --- a/src/common/getCols.ts +++ b/src/common/getCols.ts @@ -1186,6 +1186,12 @@ const sportSpecificCols = bySport<{ sortType: "number", title: "VORP", }, + "stat:sovr": { + desc: "Statistical Overall", + sortSequence: ["desc", "asc"], + sortType: "number", + title: "sOVR", + }, "stat:fgAtRim": { desc: "At Rim Made", sortSequence: ["desc", "asc"], diff --git a/src/common/processPlayerStats.basketball.ts b/src/common/processPlayerStats.basketball.ts index a8a4b40f20..e7e402c750 100644 --- a/src/common/processPlayerStats.basketball.ts +++ b/src/common/processPlayerStats.basketball.ts @@ -9,6 +9,7 @@ const straightThrough = [ "obpm", "dbpm", "vorp", + "sovr", "yearsWithTeam", "astp", "blkp", diff --git a/src/ui/util/helpers.ts b/src/ui/util/helpers.ts index 734315534b..e0efc8e6ae 100644 --- a/src/ui/util/helpers.ts +++ b/src/ui/util/helpers.ts @@ -243,6 +243,7 @@ const roundOverrides: Record< obpm: "oneDecimalPlace", dbpm: "oneDecimalPlace", vorp: "oneDecimalPlace", + sovr: "noDecimalPlace", fgMax: "noDecimalPlace", fgaMax: "noDecimalPlace", tpMax: "noDecimalPlace", diff --git a/src/worker/core/player/stats.basketball.ts b/src/worker/core/player/stats.basketball.ts index 040d591c06..0dc4f27c36 100644 --- a/src/worker/core/player/stats.basketball.ts +++ b/src/worker/core/player/stats.basketball.ts @@ -19,6 +19,7 @@ const stats = { "obpm", "dbpm", "vorp", + "sovr", ] as const, raw: [ "gp", diff --git a/src/worker/core/player/value.ts b/src/worker/core/player/value.ts index 05f924e413..6822daeb76 100644 --- a/src/worker/core/player/value.ts +++ b/src/worker/core/player/value.ts @@ -82,10 +82,6 @@ const value = ( pr.pot = pr.pot - options.ovrMean + defaultOvrMean; } - // From linear regression OVR ~ PER - const slope = 1.531; - const intercept = 31.693; - // 1. Account for stats (and current ratings if not enough stats) const ps = p.stats.filter(playerStats => !playerStats.playoffs); let current = pr.ovr; @@ -94,36 +90,39 @@ const value = ( if (isSport("basketball") && ps.length > 0) { const ps1 = ps.at(-1); // Most recent stats + // weights and values for the last few years + // this is a prior + let m0 = 2000; + let v0 = pr.ovr; + + let m1 = 0; + let v1 = 0; + const w1 = 1; + + let m2 = 0; + let v2 = 0; + const w2 = 0.1; + // PER may be undefined for exhibition game players from old historical seasons. See ps2 check below too. - if (Object.hasOwn(ps1, "per")) { - if (ps.length === 1 || ps[0].min >= 2000) { + if (Object.hasOwn(ps1, "sovr")) { + if (ps.length >= 1 && ps1.sovr > 0) { // Only one year of stats - current = intercept + slope * ps1.per; - - if (ps1.min < 2000) { - current = (current * ps1.min) / 2000 + pr.ovr * (1 - ps1.min / 2000); - } - } else { + v1 = ps1.sovr; + m1 = w1 * ps1.min; + //console.log("ONE YEAR"); + } + if (ps.length >= 2) { // Two most recent seasons const ps2 = ps[ps.length - 2]; - if (Object.hasOwn(ps2, "per")) { - if (ps1.min + ps2.min > 0) { - current = - intercept + - (slope * (ps1.per * ps1.min + ps2.per * ps2.min)) / - (ps1.min + ps2.min); - - if (ps1.min + ps2.min < 2000) { - current = - (current * (ps1.min + ps2.min)) / 2000 + - pr.ovr * (1 - (ps1.min + ps2.min) / 2000); - } - } + if (Object.hasOwn(ps2, "sovr") && ps2.sovr > 0) { + v2 = ps2.sovr; + m2 = w2 * ps2.min; + //console.log("TWO YEAR"); } } - - current = 0.8 * pr.ovr + 0.2 * current; // Include some part of the ratings + const total_w = m0 + m1 + m2; + current = (m0 / total_w) * v0 + (m1 / total_w) * v1 + (m2 / total_w) * v2; } } diff --git a/src/worker/db/getCopies/playersPlus.ts b/src/worker/db/getCopies/playersPlus.ts index b42cb4fb9b..7c67df2f89 100644 --- a/src/worker/db/getCopies/playersPlus.ts +++ b/src/worker/db/getCopies/playersPlus.ts @@ -385,6 +385,7 @@ export const weightByMinutes = bySport({ "obpm", "dbpm", "bpm", + "sovr", ], football: [], hockey: [], diff --git a/src/worker/util/advStats.basketball.ts b/src/worker/util/advStats.basketball.ts index 709d8a13e8..e527b45941 100644 --- a/src/worker/util/advStats.basketball.ts +++ b/src/worker/util/advStats.basketball.ts @@ -149,6 +149,51 @@ const calculatePER = ( ewa: EWA, }; }; +// just for fun +const calculateSOVR = ( + players: any[], + teamsByTid: Record, + league: any, +) => { + const paceAdj: Record = {}; + for (const t of Object.values(teamsByTid)) { + paceAdj[t.tid] = t.stats.pace === 0 ? 1 : league.pace / t.stats.pace; + } + + const sOvr: number[] = []; + + for (let i = 0; i < players.length; i++) { + const t_pace = paceAdj[players[i].tid]; + + const p_mp = t_pace * (players[i].stats.min + 5); + const p_orb = (36 * players[i].stats.orb) / p_mp; + const p_drb = (36 * players[i].stats.drb) / p_mp; + const p_trb = p_orb + p_drb; + const p_ast = (36 * players[i].stats.ast) / p_mp; + const p_tov = (36 * players[i].stats.tov) / p_mp; + const p_stl = (36 * players[i].stats.stl) / p_mp; + const p_blk = (36 * players[i].stats.blk) / p_mp; + const p_pf = (36 * players[i].stats.pf) / p_mp; + const p_pts = (36 * players[i].stats.pts) / p_mp; + const p_pm = (36 * players[i].stats.pm) / p_mp; + const p_const = 1; + + // normalized + const pa_sovr = + 37.97 * p_const + + 0.54 * p_trb + + 0.64 * p_ast + + -1.38 * p_tov + + 5.23 * p_stl + + 2.3 * p_blk + + -2.6 * p_pf + + 0.96 * p_pts; + 0.27 * p_pm; // normal + sOvr[i] = pa_sovr; + } + + return { sovr: sOvr }; +}; // https://www.basketball-reference.com/about/bpm2.html /** @@ -749,6 +794,7 @@ const advStats = async () => { "pf", "drb", "pts", + "pm", ], ratings: ["pos"], season: g.get("season"), @@ -863,6 +909,7 @@ const advStats = async () => { ...calculatePercentages(players, teamsByTid), ...calculateRatings(players, teamsByTid, league), ...calculateBPM(players, teamsByTid, league), + ...calculateSOVR(players, teamsByTid, league), }; await advStatsSave(players, playersRaw, updatedStats); }; diff --git a/src/worker/views/roster.ts b/src/worker/views/roster.ts index c9b263dabc..120cd23ed8 100644 --- a/src/worker/views/roster.ts +++ b/src/worker/views/roster.ts @@ -41,7 +41,7 @@ const updateRoster = async ( ) { const stats = bySport({ baseball: ["gp", "keyStats", "war"], - basketball: ["gp", "min", "pts", "trb", "ast", "per"], + basketball: ["gp", "min", "pts", "trb", "ast", "per", "sovr"], football: ["gp", "keyStats", "av"], hockey: ["gp", "amin", "keyStats", "ops", "dps", "ps"], });