fix: align ev-calculator tests with decimal (0-1) probability format
The implementation and rest of the system (ICM calculator, probability updater) use decimals (0-1) for probabilities, but the tests were using percentages (0-100). Updated tests and docs to match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
24126fe389
commit
96b4c5a350
2 changed files with 144 additions and 148 deletions
|
|
@ -22,56 +22,56 @@ describe("calculateEV", () => {
|
|||
|
||||
it("should calculate EV for equal probabilities", () => {
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 12.5,
|
||||
probSecond: 12.5,
|
||||
probThird: 12.5,
|
||||
probFourth: 12.5,
|
||||
probFifth: 12.5,
|
||||
probSixth: 12.5,
|
||||
probSeventh: 12.5,
|
||||
probEighth: 12.5,
|
||||
probFirst: 0.125,
|
||||
probSecond: 0.125,
|
||||
probThird: 0.125,
|
||||
probFourth: 0.125,
|
||||
probFifth: 0.125,
|
||||
probSixth: 0.125,
|
||||
probSeventh: 0.125,
|
||||
probEighth: 0.125,
|
||||
};
|
||||
|
||||
const ev = calculateEV(probabilities, defaultScoring);
|
||||
|
||||
// EV = 12.5% × (100+70+50+40+25+25+15+15) = 12.5% × 340 = 42.5
|
||||
// EV = 0.125 × (100+70+50+40+25+25+15+15) = 0.125 × 340 = 42.5
|
||||
expect(ev).toBe(42.5);
|
||||
});
|
||||
|
||||
it("should calculate EV for favorite (high probability of 1st)", () => {
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 50,
|
||||
probSecond: 30,
|
||||
probThird: 10,
|
||||
probFourth: 5,
|
||||
probFifth: 3,
|
||||
probSixth: 1,
|
||||
probSeventh: 0.5,
|
||||
probEighth: 0.5,
|
||||
probFirst: 0.50,
|
||||
probSecond: 0.30,
|
||||
probThird: 0.10,
|
||||
probFourth: 0.05,
|
||||
probFifth: 0.03,
|
||||
probSixth: 0.01,
|
||||
probSeventh: 0.005,
|
||||
probEighth: 0.005,
|
||||
};
|
||||
|
||||
const ev = calculateEV(probabilities, defaultScoring);
|
||||
|
||||
// EV = 50% × 100 + 30% × 70 + 10% × 50 + 5% × 40 + 3% × 25 + 1% × 25 + 0.5% × 15 + 0.5% × 15
|
||||
// EV = 0.50×100 + 0.30×70 + 0.10×50 + 0.05×40 + 0.03×25 + 0.01×25 + 0.005×15 + 0.005×15
|
||||
// = 50 + 21 + 5 + 2 + 0.75 + 0.25 + 0.075 + 0.075 = 79.15
|
||||
expect(ev).toBe(79.15);
|
||||
});
|
||||
|
||||
it("should calculate EV for underdog (low probability of 1st)", () => {
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 2,
|
||||
probSecond: 5,
|
||||
probThird: 8,
|
||||
probFourth: 10,
|
||||
probFifth: 15,
|
||||
probSixth: 20,
|
||||
probSeventh: 20,
|
||||
probEighth: 20,
|
||||
probFirst: 0.02,
|
||||
probSecond: 0.05,
|
||||
probThird: 0.08,
|
||||
probFourth: 0.10,
|
||||
probFifth: 0.15,
|
||||
probSixth: 0.20,
|
||||
probSeventh: 0.20,
|
||||
probEighth: 0.20,
|
||||
};
|
||||
|
||||
const ev = calculateEV(probabilities, defaultScoring);
|
||||
|
||||
// EV = 2% × 100 + 5% × 70 + 8% × 50 + 10% × 40 + 15% × 25 + 20% × 25 + 20% × 15 + 20% × 15
|
||||
// EV = 0.02×100 + 0.05×70 + 0.08×50 + 0.10×40 + 0.15×25 + 0.20×25 + 0.20×15 + 0.20×15
|
||||
// = 2 + 3.5 + 4 + 4 + 3.75 + 5 + 3 + 3 = 28.25
|
||||
expect(ev).toBe(28.25);
|
||||
});
|
||||
|
|
@ -89,26 +89,26 @@ describe("calculateEV", () => {
|
|||
};
|
||||
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 20,
|
||||
probSecond: 20,
|
||||
probThird: 15,
|
||||
probFourth: 15,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 5,
|
||||
probFirst: 0.20,
|
||||
probSecond: 0.20,
|
||||
probThird: 0.15,
|
||||
probFourth: 0.15,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.05,
|
||||
};
|
||||
|
||||
const ev = calculateEV(probabilities, customScoring);
|
||||
|
||||
// EV = 20% × 200 + 20% × 150 + 15% × 100 + 15% × 80 + 10% × 50 + 10% × 50 + 5% × 30 + 5% × 30
|
||||
// EV = 0.20×200 + 0.20×150 + 0.15×100 + 0.15×80 + 0.10×50 + 0.10×50 + 0.05×30 + 0.05×30
|
||||
// = 40 + 30 + 15 + 12 + 5 + 5 + 1.5 + 1.5 = 110
|
||||
expect(ev).toBe(110);
|
||||
});
|
||||
|
||||
it("should handle 100% probability of one placement (finished participant)", () => {
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 100,
|
||||
probFirst: 1.0,
|
||||
probSecond: 0,
|
||||
probThird: 0,
|
||||
probFourth: 0,
|
||||
|
|
@ -119,24 +119,24 @@ describe("calculateEV", () => {
|
|||
};
|
||||
|
||||
const ev = calculateEV(probabilities, defaultScoring);
|
||||
expect(ev).toBe(100); // 100% × 100 = 100
|
||||
expect(ev).toBe(100); // 1.0 × 100 = 100
|
||||
});
|
||||
|
||||
it("should handle probabilities with decimal places", () => {
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 15.75,
|
||||
probSecond: 14.25,
|
||||
probThird: 13.50,
|
||||
probFourth: 12.75,
|
||||
probFifth: 11.00,
|
||||
probSixth: 10.50,
|
||||
probSeventh: 11.25,
|
||||
probEighth: 11.00,
|
||||
probFirst: 0.1575,
|
||||
probSecond: 0.1425,
|
||||
probThird: 0.1350,
|
||||
probFourth: 0.1275,
|
||||
probFifth: 0.1100,
|
||||
probSixth: 0.1050,
|
||||
probSeventh: 0.1125,
|
||||
probEighth: 0.1100,
|
||||
};
|
||||
|
||||
const ev = calculateEV(probabilities, defaultScoring);
|
||||
|
||||
// EV = 15.75% × 100 + 14.25% × 70 + ... = 46.29
|
||||
// EV = 0.1575×100 + 0.1425×70 + ... = 46.29
|
||||
expect(ev).toBeCloseTo(46.29, 2);
|
||||
});
|
||||
|
||||
|
|
@ -158,31 +158,31 @@ describe("calculateEV", () => {
|
|||
});
|
||||
|
||||
describe("validateProbabilities", () => {
|
||||
it("should validate probabilities that sum to 100", () => {
|
||||
it("should validate probabilities that sum to 1.0", () => {
|
||||
const valid: ProbabilityDistribution = {
|
||||
probFirst: 20,
|
||||
probSecond: 20,
|
||||
probThird: 15,
|
||||
probFourth: 15,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 5,
|
||||
probFirst: 0.20,
|
||||
probSecond: 0.20,
|
||||
probThird: 0.15,
|
||||
probFourth: 0.15,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.05,
|
||||
};
|
||||
|
||||
expect(validateProbabilities(valid)).toBe(true);
|
||||
});
|
||||
|
||||
it("should accept probabilities within tolerance (default 0.1%)", () => {
|
||||
it("should accept probabilities within tolerance (default 0.01)", () => {
|
||||
const nearlyValid: ProbabilityDistribution = {
|
||||
probFirst: 20.05,
|
||||
probSecond: 20,
|
||||
probThird: 15,
|
||||
probFourth: 15,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 4.95,
|
||||
probFirst: 0.2005,
|
||||
probSecond: 0.20,
|
||||
probThird: 0.15,
|
||||
probFourth: 0.15,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.0495,
|
||||
};
|
||||
|
||||
expect(validateProbabilities(nearlyValid)).toBe(true);
|
||||
|
|
@ -190,14 +190,14 @@ describe("validateProbabilities", () => {
|
|||
|
||||
it("should reject probabilities that sum too high", () => {
|
||||
const tooHigh: ProbabilityDistribution = {
|
||||
probFirst: 20,
|
||||
probSecond: 20,
|
||||
probThird: 20,
|
||||
probFourth: 20,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 5,
|
||||
probFirst: 0.20,
|
||||
probSecond: 0.20,
|
||||
probThird: 0.20,
|
||||
probFourth: 0.20,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.05,
|
||||
};
|
||||
|
||||
expect(validateProbabilities(tooHigh)).toBe(false);
|
||||
|
|
@ -205,14 +205,14 @@ describe("validateProbabilities", () => {
|
|||
|
||||
it("should reject probabilities that sum too low", () => {
|
||||
const tooLow: ProbabilityDistribution = {
|
||||
probFirst: 10,
|
||||
probSecond: 10,
|
||||
probThird: 10,
|
||||
probFourth: 10,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 5,
|
||||
probFirst: 0.10,
|
||||
probSecond: 0.10,
|
||||
probThird: 0.10,
|
||||
probFourth: 0.10,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.05,
|
||||
};
|
||||
|
||||
expect(validateProbabilities(tooLow)).toBe(false);
|
||||
|
|
@ -220,88 +220,87 @@ describe("validateProbabilities", () => {
|
|||
|
||||
it("should allow custom tolerance", () => {
|
||||
const probabilities: ProbabilityDistribution = {
|
||||
probFirst: 21,
|
||||
probSecond: 20,
|
||||
probThird: 15,
|
||||
probFourth: 15,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 4,
|
||||
probFirst: 0.21,
|
||||
probSecond: 0.20,
|
||||
probThird: 0.15,
|
||||
probFourth: 0.15,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.04,
|
||||
};
|
||||
|
||||
// Sum is 100, but with 1% tolerance
|
||||
expect(validateProbabilities(probabilities, 1)).toBe(true);
|
||||
// With stricter 0.1% tolerance
|
||||
expect(validateProbabilities(probabilities, 0.1)).toBe(true);
|
||||
// Sum is 1.0, within any tolerance
|
||||
expect(validateProbabilities(probabilities, 0.01)).toBe(true);
|
||||
expect(validateProbabilities(probabilities, 0.001)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeProbabilities", () => {
|
||||
it("should normalize probabilities that sum to more than 100", () => {
|
||||
it("should normalize probabilities that sum to more than 1.0", () => {
|
||||
const input: ProbabilityDistribution = {
|
||||
probFirst: 22,
|
||||
probSecond: 22,
|
||||
probThird: 17,
|
||||
probFourth: 17,
|
||||
probFifth: 11,
|
||||
probSixth: 11,
|
||||
probSeventh: 5.5,
|
||||
probEighth: 5.5,
|
||||
probFirst: 0.22,
|
||||
probSecond: 0.22,
|
||||
probThird: 0.17,
|
||||
probFourth: 0.17,
|
||||
probFifth: 0.11,
|
||||
probSixth: 0.11,
|
||||
probSeventh: 0.055,
|
||||
probEighth: 0.055,
|
||||
};
|
||||
// Sum = 111
|
||||
// Sum = 1.11
|
||||
|
||||
const normalized = normalizeProbabilities(input);
|
||||
|
||||
// Each should be scaled down by 100/111
|
||||
expect(normalized.probFirst).toBeCloseTo(19.82, 2);
|
||||
expect(normalized.probSecond).toBeCloseTo(19.82, 2);
|
||||
// Each should be scaled down by 1/1.11
|
||||
expect(normalized.probFirst).toBeCloseTo(0.1982, 3);
|
||||
expect(normalized.probSecond).toBeCloseTo(0.1982, 3);
|
||||
|
||||
// Sum should be exactly 100 (within rounding)
|
||||
// Sum should be exactly 1.0 (within rounding)
|
||||
const sum = Object.values(normalized).reduce((a, b) => a + b, 0);
|
||||
expect(sum).toBeCloseTo(100, 1);
|
||||
expect(sum).toBeCloseTo(1.0, 3);
|
||||
});
|
||||
|
||||
it("should normalize probabilities that sum to less than 100", () => {
|
||||
it("should normalize probabilities that sum to less than 1.0", () => {
|
||||
const input: ProbabilityDistribution = {
|
||||
probFirst: 18,
|
||||
probSecond: 18,
|
||||
probThird: 13.5,
|
||||
probFourth: 13.5,
|
||||
probFifth: 9,
|
||||
probSixth: 9,
|
||||
probSeventh: 4.5,
|
||||
probEighth: 4.5,
|
||||
probFirst: 0.18,
|
||||
probSecond: 0.18,
|
||||
probThird: 0.135,
|
||||
probFourth: 0.135,
|
||||
probFifth: 0.09,
|
||||
probSixth: 0.09,
|
||||
probSeventh: 0.045,
|
||||
probEighth: 0.045,
|
||||
};
|
||||
// Sum = 90
|
||||
// Sum = 0.90
|
||||
|
||||
const normalized = normalizeProbabilities(input);
|
||||
|
||||
// Each should be scaled up by 100/90
|
||||
expect(normalized.probFirst).toBeCloseTo(20, 1);
|
||||
expect(normalized.probSecond).toBeCloseTo(20, 1);
|
||||
// Each should be scaled up by 1/0.90
|
||||
expect(normalized.probFirst).toBeCloseTo(0.20, 2);
|
||||
expect(normalized.probSecond).toBeCloseTo(0.20, 2);
|
||||
|
||||
const sum = Object.values(normalized).reduce((a, b) => a + b, 0);
|
||||
expect(sum).toBeCloseTo(100, 1);
|
||||
expect(sum).toBeCloseTo(1.0, 3);
|
||||
});
|
||||
|
||||
it("should handle probabilities that already sum to 100", () => {
|
||||
it("should handle probabilities that already sum to 1.0", () => {
|
||||
const input: ProbabilityDistribution = {
|
||||
probFirst: 20,
|
||||
probSecond: 20,
|
||||
probThird: 15,
|
||||
probFourth: 15,
|
||||
probFifth: 10,
|
||||
probSixth: 10,
|
||||
probSeventh: 5,
|
||||
probEighth: 5,
|
||||
probFirst: 0.20,
|
||||
probSecond: 0.20,
|
||||
probThird: 0.15,
|
||||
probFourth: 0.15,
|
||||
probFifth: 0.10,
|
||||
probSixth: 0.10,
|
||||
probSeventh: 0.05,
|
||||
probEighth: 0.05,
|
||||
};
|
||||
|
||||
const normalized = normalizeProbabilities(input);
|
||||
|
||||
// Should remain essentially unchanged
|
||||
expect(normalized.probFirst).toBeCloseTo(20, 1);
|
||||
expect(normalized.probSecond).toBeCloseTo(20, 1);
|
||||
expect(normalized.probFirst).toBeCloseTo(0.20, 3);
|
||||
expect(normalized.probSecond).toBeCloseTo(0.20, 3);
|
||||
});
|
||||
|
||||
it("should handle all zeros by returning equal probabilities", () => {
|
||||
|
|
@ -318,10 +317,10 @@ describe("normalizeProbabilities", () => {
|
|||
|
||||
const normalized = normalizeProbabilities(input);
|
||||
|
||||
// Should return 12.5% for each (equal distribution)
|
||||
expect(normalized.probFirst).toBe(12.5);
|
||||
expect(normalized.probSecond).toBe(12.5);
|
||||
expect(normalized.probEighth).toBe(12.5);
|
||||
// Should return 0.125 for each (equal distribution)
|
||||
expect(normalized.probFirst).toBe(0.125);
|
||||
expect(normalized.probSecond).toBe(0.125);
|
||||
expect(normalized.probEighth).toBe(0.125);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ export interface ScoringRules {
|
|||
|
||||
/**
|
||||
* Probability distribution for a participant's placements
|
||||
* All values should be percentages (0-100) and sum to ~100
|
||||
* All values should be decimals (0-1) and sum to ~1.0
|
||||
*/
|
||||
export interface ProbabilityDistribution {
|
||||
probFirst: number; // e.g., 15.5 means 15.5%
|
||||
probFirst: number; // e.g., 0.155 means 15.5%
|
||||
probSecond: number;
|
||||
probThird: number;
|
||||
probFourth: number;
|
||||
|
|
@ -39,14 +39,14 @@ export interface ProbabilityDistribution {
|
|||
*
|
||||
* EV = Σ (probability × points) for each placement
|
||||
*
|
||||
* @param probabilities - Probability distribution (percentages 0-100)
|
||||
* @param probabilities - Probability distribution (decimals 0-1)
|
||||
* @param scoringRules - League's point values for each placement
|
||||
* @returns Expected value (average projected points)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ev = calculateEV(
|
||||
* { probFirst: 20, probSecond: 15, probThird: 12, ..., probEighth: 5 },
|
||||
* { probFirst: 0.20, probSecond: 0.15, probThird: 0.12, ..., probEighth: 0.05 },
|
||||
* { pointsFor1st: 100, pointsFor2nd: 70, ..., pointsFor8th: 15 }
|
||||
* );
|
||||
* // Returns: 45.5 (expected points)
|
||||
|
|
@ -56,7 +56,6 @@ export function calculateEV(
|
|||
probabilities: ProbabilityDistribution,
|
||||
scoringRules: ScoringRules
|
||||
): number {
|
||||
// Probabilities are already decimals (0-1), no need to divide by 100
|
||||
const ev =
|
||||
probabilities.probFirst * scoringRules.pointsFor1st +
|
||||
probabilities.probSecond * scoringRules.pointsFor2nd +
|
||||
|
|
@ -71,11 +70,11 @@ export function calculateEV(
|
|||
}
|
||||
|
||||
/**
|
||||
* Validate that probability distribution sums to approximately 100%
|
||||
* Validate that probability distribution sums to approximately 1.0
|
||||
* (allows small rounding errors)
|
||||
*
|
||||
* @param probabilities - Probability distribution to validate
|
||||
* @param tolerance - Acceptable deviation from 100% (default 0.1%)
|
||||
* @param tolerance - Acceptable deviation from 1.0 (default 0.01)
|
||||
* @returns true if valid, false otherwise
|
||||
*/
|
||||
export function validateProbabilities(
|
||||
|
|
@ -92,7 +91,6 @@ export function validateProbabilities(
|
|||
probabilities.probSeventh +
|
||||
probabilities.probEighth;
|
||||
|
||||
// Validate sum is close to 1.0 (decimals, not percentages)
|
||||
return Math.abs(sum - 1.0) <= tolerance;
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +99,7 @@ export function validateProbabilities(
|
|||
* Useful when importing odds or dealing with rounding errors
|
||||
*
|
||||
* @param probabilities - Probability distribution to normalize
|
||||
* @returns Normalized probabilities that sum to 1.0 (decimals, not percentages)
|
||||
* @returns Normalized probabilities that sum to 1.0
|
||||
*/
|
||||
export function normalizeProbabilities(
|
||||
probabilities: ProbabilityDistribution
|
||||
|
|
@ -117,7 +115,7 @@ export function normalizeProbabilities(
|
|||
probabilities.probEighth;
|
||||
|
||||
if (sum === 0) {
|
||||
// Avoid division by zero - return equal probabilities (1/8 = 0.125 = 12.5%)
|
||||
// Avoid division by zero - return equal probabilities (1/8 = 0.125)
|
||||
return {
|
||||
probFirst: 0.125,
|
||||
probSecond: 0.125,
|
||||
|
|
@ -130,7 +128,6 @@ export function normalizeProbabilities(
|
|||
};
|
||||
}
|
||||
|
||||
// Normalize to sum = 1.0 (decimals, not percentages)
|
||||
const factor = 1.0 / sum;
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue