150 lines
4.0 KiB
JavaScript
150 lines
4.0 KiB
JavaScript
const DISTRIBUTION_SHAPES = {
|
|
points: 'normal',
|
|
rebounds: 'normal',
|
|
assists: 'normal',
|
|
home_runs: 'negative_binomial',
|
|
stolen_bases: 'negative_binomial',
|
|
pitcher_strikeouts: 'bimodal_mixture',
|
|
walks: 'poisson',
|
|
hits: 'normal',
|
|
total_bases: 'normal',
|
|
rbis: 'normal',
|
|
runs_scored: 'poisson',
|
|
strikeouts_batter: 'poisson',
|
|
earned_runs: 'poisson',
|
|
outs_recorded: 'normal',
|
|
walks_allowed: 'poisson',
|
|
hits_allowed: 'normal',
|
|
pitches_thrown: 'normal',
|
|
};
|
|
|
|
/**
|
|
* Get the distribution shape for a stat type.
|
|
* @param {string} statType
|
|
* @returns {string} Distribution shape name
|
|
*/
|
|
function getDistributionShape(statType) {
|
|
return DISTRIBUTION_SHAPES[statType] || 'normal';
|
|
}
|
|
|
|
/**
|
|
* Normal CDF using rational approximation.
|
|
*/
|
|
function normalCDF(x, mean, stddev) {
|
|
if (stddev <= 0) return x >= mean ? 1 : 0;
|
|
const z = (x - mean) / stddev;
|
|
const t = 1 / (1 + 0.2316419 * Math.abs(z));
|
|
const d = 0.3989422804014327;
|
|
const p = d * Math.exp(-z * z / 2) *
|
|
(t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.8212560 + t * 1.3302744)))));
|
|
return z > 0 ? 1 - p : p;
|
|
}
|
|
|
|
/**
|
|
* Poisson CDF: P(X <= x) for Poisson(lambda).
|
|
* @param {number} x - Value (floored to integer)
|
|
* @param {number} lambda - Rate parameter
|
|
* @returns {number} Cumulative probability
|
|
*/
|
|
function poissonCDF(x, lambda) {
|
|
if (lambda <= 0) return 1;
|
|
const k = Math.floor(x);
|
|
if (k < 0) return 0;
|
|
|
|
let cdf = 0;
|
|
let term = Math.exp(-lambda);
|
|
cdf += term;
|
|
|
|
for (let i = 1; i <= k; i++) {
|
|
term *= lambda / i;
|
|
cdf += term;
|
|
}
|
|
|
|
return Math.min(1, cdf);
|
|
}
|
|
|
|
/**
|
|
* Negative Binomial CDF: P(X <= x) for NB(r, p).
|
|
* Uses direct summation of PMF.
|
|
* @param {number} x - Value (floored to integer)
|
|
* @param {number} r - Number of successes
|
|
* @param {number} p - Probability of success per trial
|
|
* @returns {number} Cumulative probability
|
|
*/
|
|
function negativeBinomialCDF(x, r, p) {
|
|
if (r <= 0 || p <= 0 || p > 1) return 0;
|
|
const k = Math.floor(x);
|
|
if (k < 0) return 0;
|
|
|
|
let cdf = 0;
|
|
|
|
// log of binomial coefficient using lgamma approximation
|
|
function logGamma(z) {
|
|
// Stirling approximation for lgamma
|
|
if (z < 0.5) return Math.log(Math.PI / Math.sin(Math.PI * z)) - logGamma(1 - z);
|
|
z -= 1;
|
|
const coeffs = [
|
|
76.18009172947146, -86.50532032941677, 24.01409824083091,
|
|
-1.231739572450155, 0.001208650973866179, -0.000005395239384953,
|
|
];
|
|
let x = 0.99999999999980993;
|
|
for (let i = 0; i < coeffs.length; i++) {
|
|
x += coeffs[i] / (z + i + 1);
|
|
}
|
|
const t = z + coeffs.length - 0.5;
|
|
return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(x);
|
|
}
|
|
|
|
for (let i = 0; i <= k; i++) {
|
|
const logCoeff = logGamma(i + r) - logGamma(i + 1) - logGamma(r);
|
|
const logProb = logCoeff + r * Math.log(p) + i * Math.log(1 - p);
|
|
cdf += Math.exp(logProb);
|
|
}
|
|
|
|
return Math.min(1, Math.max(0, cdf));
|
|
}
|
|
|
|
/**
|
|
* Calculate probability based on distribution shape.
|
|
* @param {string} shape - Distribution type
|
|
* @param {object} params - Distribution parameters
|
|
* @param {number} line - Prop line
|
|
* @param {string} direction - 'over' or 'under'
|
|
* @returns {number} Probability 0-1
|
|
*/
|
|
function calculateProbability(shape, params, line, direction) {
|
|
let cdf;
|
|
|
|
switch (shape) {
|
|
case 'normal':
|
|
cdf = normalCDF(line, params.mean, params.stddev);
|
|
break;
|
|
case 'poisson':
|
|
cdf = poissonCDF(line, params.lambda);
|
|
break;
|
|
case 'negative_binomial':
|
|
cdf = negativeBinomialCDF(line, params.r, params.p);
|
|
break;
|
|
case 'bimodal_mixture':
|
|
// Weighted mixture of two normals
|
|
const w1 = params.weight1 || 0.5;
|
|
const cdf1 = normalCDF(line, params.mean1, params.stddev1);
|
|
const cdf2 = normalCDF(line, params.mean2, params.stddev2);
|
|
cdf = w1 * cdf1 + (1 - w1) * cdf2;
|
|
break;
|
|
default:
|
|
cdf = normalCDF(line, params.mean, params.stddev);
|
|
}
|
|
|
|
return direction === 'over' ? 1 - cdf : cdf;
|
|
}
|
|
|
|
module.exports = {
|
|
DISTRIBUTION_SHAPES,
|
|
getDistributionShape,
|
|
calculateProbability,
|
|
normalCDF,
|
|
poissonCDF,
|
|
negativeBinomialCDF,
|
|
};
|