Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user