PageSourceSearch

https://www.pollsmax.com/assets/forecast-calibration.js?v=forecast-cache-gov-rebuild-20260620

js pollsmax.com collected 2026-09-28 06:38:17 UTC 9,974 bytes, 237 lines download raw bytes

1/* global ET */
2/**
3 * Per-race forecast simulation calibration (Option B + fundamentals blend).
4 * Load after assets/forecast-model.js (uses ET.forecastModelMarginToWinProb, ET.forecastModelNormalInv).
5 */
6(function () {
7    if (!window.ET) window.ET = {};
8
9    /** Senate / governor simulation calibration curve. */
10    var ODDS_TO_MARGIN_ANCHORS = [
11        { p: 0.50, margin: 0 },
12        { p: 0.55, margin: 1 },
13        { p: 0.65, margin: 2.5 },
14        { p: 0.75, margin: 5 },
15        { p: 0.85, margin: 7.5 },
16        { p: 0.90, margin: 10 },
17        { p: 0.95, margin: 12.5 },
18        { p: 0.99, margin: 25 },
19        { p: 0.999, margin: 40 },
20        { p: 0.9999, margin: 50 }
21    ];
22
23    /**
24     * House margin ↔ favored-party win % (exact engineering curve).
25     * 0.5→57.5%, 1→65%, 3→72.5%, 5→80%, 10→87.5%, 15→95%, 20→98%, 30→99.9%.
26     */
27    var HOUSE_ODDS_TO_MARGIN_ANCHORS = [
28        { p: 0.50, margin: 0 },
29        { p: 0.575, margin: 0.5 },
30        { p: 0.65, margin: 1 },
31        { p: 0.725, margin: 3 },
32        { p: 0.80, margin: 5 },
33        { p: 0.875, margin: 10 },
34        { p: 0.95, margin: 15 },
35        { p: 0.98, margin: 20 },
36        { p: 0.999, margin: 30 },
37        { p: 0.9999, margin: 40 }
38    ];
39
40    ET.FORECAST_SIM_BLEND_WEIGHTS = {
41        senate: { calibrated: 0.65, fundamentals: 0.35 },
42        house: { calibrated: 1, fundamentals: 0 },
43        gov: { calibrated: 0.60, fundamentals: 0.40 }
44    };
45
46    ET.FORECAST_SIM_SIGMA_LIMITS = {
47        senate: { min: 2, max: 20, tossup: 7 },
48        house: { min: 2, max: 22, tossup: 7 },
49        gov: { min: 2, max: 20, tossup: 7.5 }
50    };
51
52    /** Legacy anchor extrapolation cap — not applied to per-draw histogram margins. */
53    ET.FORECAST_SIM_MARGIN_SANITY = null;
54
55    ET.FORECAST_MARGIN_CALIBRATION_ANCHORS = ODDS_TO_MARGIN_ANCHORS;
56    ET.FORECAST_HOUSE_MARGIN_CALIBRATION_ANCHORS = HOUSE_ODDS_TO_MARGIN_ANCHORS;
57
58    function anchorsForChamber(chamber) {
59        return normalizeChamber(chamber) === 'house'
60            ? HOUSE_ODDS_TO_MARGIN_ANCHORS
61            : ODDS_TO_MARGIN_ANCHORS;
62    }
63
64    if (typeof ET.forecastClampProb !== 'function') {
65        ET.forecastClampProb = function (p) {
66            if (p == null || !isFinite(p)) return 0.5;
67            return Math.min(Math.max(p, 0.001), 0.999);
68        };
69    }
70
71    ET.forecastClampProbForMargin = function (p) {
72        if (p == null || !isFinite(p)) return 0.5;
73        return Math.min(Math.max(p, 0.0001), 0.9999);
74    };
75
76    function smoothstep(t) {
77        return t * t * (3 - 2 * t);
78    }
79
80    function normalizeChamber(chamber) {
81        if (chamber === 'senate' || chamber === 'gov' || chamber === 'governor') {
82            return chamber === 'governor' ? 'gov' : chamber;
83        }
84        return 'house';
85    }
86
87    function sigmaLimitsForChamber(chamber) {
88        var ch = normalizeChamber(chamber);
89        return ET.FORECAST_SIM_SIGMA_LIMITS[ch] || ET.FORECAST_SIM_SIGMA_LIMITS.house;
90    }
91
92    function blendWeightsForChamber(chamber) {
93        var ch = normalizeChamber(chamber);
94        return ET.FORECAST_SIM_BLEND_WEIGHTS[ch] || ET.FORECAST_SIM_BLEND_WEIGHTS.house;
95    }
96
97    function extrapolateMarginBeyondAnchors(favoredProb, sign, anchors) {
98        anchors = anchors || ODDS_TO_MARGIN_ANCHORS;
99        var last = anchors[anchors.length - 1];
100        var prev = anchors[anchors.length - 2];
101        var dp = last.p - prev.p;
102        var dm = last.margin - prev.margin;
103        var slope = dp > 0 ? dm / dp : 50;
104        var m = last.margin + (favoredProb - last.p) * slope;
105        return sign * m;
106    }
107
108    ET.forecastProbabilityToTargetMargin = function (demProb, chamber) {
109        if (demProb == null || !isFinite(demProb)) return 0;
110        var anchors = anchorsForChamber(chamber);
111        var clampedProb = ET.forecastClampProbForMargin(demProb);
112        var sign = clampedProb >= 0.5 ? 1 : -1;
113        var favoredProb = clampedProb >= 0.5 ? clampedProb : 1 - clampedProb;
114        if (favoredProb <= anchors[0].p + 1e-12) return 0;
115        var i;
116        var a;
117        var b;
118        for (i = 0; i < anchors.length - 1; i++) {
119            a = anchors[i];
120            b = anchors[i + 1];
121            if (favoredProb >= a.p && favoredProb <= b.p) {
122                var t = smoothstep((favoredProb - a.p) / (b.p - a.p));
123                return sign * (a.margin + t * (b.margin - a.margin));
124            }
125        }
126        return extrapolateMarginBeyondAnchors(favoredProb, sign, anchors);
127    };
128
129    /** Inverse of forecastProbabilityToTargetMargin — used for House composite win-odds. */
130    ET.forecastMarginToTargetWinProb = function (margin, chamber) {
131        if (margin == null || !isFinite(margin)) return 0.5;
132        var anchors = anchorsForChamber(chamber);
133        var absM = Math.abs(margin);
134        if (absM <= anchors[0].margin + 1e-12) return 0.5;
135        var sign = margin > 0 ? 1 : -1;
136        var i;
137        var a;
138        var b;
139        for (i = 0; i < anchors.length - 1; i++) {
140            a = anchors[i];
141            b = anchors[i + 1];
142            if (absM >= a.margin && absM <= b.margin + 1e-12) {
143                var dm = b.margin - a.margin;
144                var t = dm > 1e-12 ? (absM - a.margin) / dm : 0;
145                var favoredProb = a.p + t * (b.p - a.p);
146                return sign > 0 ? favoredProb : 1 - favoredProb;
147            }
148        }
149        var last = anchors[anchors.length - 1];
150        var prev = anchors[anchors.length - 2];
151        var dp = last.p - prev.p;
152        var dmLast = last.margin - prev.margin;
153        var slope = dmLast > 0 ? dp / dmLast : 0.0001;
154        var favoredBeyond = last.p + (absM - last.margin) * slope;
155        favoredBeyond = Math.min(0.9999, Math.max(last.p, favoredBeyond));
156        return sign > 0 ? favoredBeyond : 1 - favoredBeyond;
157    };
158
159    ET.forecastSimVolatilityMultiplier = function (det, chamber, pollCount) {
160        if (normalizeChamber(chamber) === 'house') return 1;
161        var mult = 1;
162        var n = pollCount != null ? pollCount : 0;
163        if (n >= 8) mult *= 0.88;
164        else if (n >= 4) mult *= 0.95;
165        else if (n <= 1) mult *= 1.18;
166        else mult *= 1.06;
167        var ch = normalizeChamber(chamber);
168        if (ch === 'gov') mult *= 1.08;
169        if (ch === 'house') mult *= 1.0;
170        if (det && det.hasIncumbency === false) mult *= 1.1;
171        return mult;
172    };
173
174    ET.forecastSimSigmaFromFinalMargin = function (targetWinD, finalTargetMargin, chamber, volatilityMultiplier) {
175        var limits = sigmaLimitsForChamber(chamber);
176        var p = ET.forecastClampProb(targetWinD);
177        var favoredProb = p >= 0.5 ? p : 1 - p;
178        if (favoredProb <= 0.5 + 1e-9) return limits.tossup;
179        var absM = Math.abs(finalTargetMargin != null && isFinite(finalTargetMargin) ? finalTargetMargin : 0);
180        if (absM < 1e-6) return limits.tossup;
181        var inv = typeof ET.forecastModelNormalInv === 'function' ? ET.forecastModelNormalInv : null;
182        if (!inv) return limits.tossup;
183        var z = inv(favoredProb);
184        if (!z || !isFinite(z) || Math.abs(z) < 1e-6) return limits.tossup;
185        var sigma = (absM / Math.abs(z)) * (volatilityMultiplier != null ? volatilityMultiplier : 1);
186        return Math.max(limits.min, Math.min(limits.max, sigma));
187    };
188
189    ET.forecastSimSigmaFromTargetWinD = function (targetWinD, finalTargetMargin, chamber, volatilityMultiplier) {
190        return ET.forecastSimSigmaFromFinalMargin(targetWinD, finalTargetMargin, chamber, volatilityMultiplier);
191    };
192
193    ET.forecastSimParamsFromDetail = function (det, chamber, opts) {
194        opts = opts || {};
195        var ch = normalizeChamber(chamber || (det && det.forecastKind) || 'house');
196        var compositeM = det && det.margin != null && isFinite(det.margin) ? det.margin : 0;
197        var targetWinD = opts.targetWinD != null && isFinite(opts.targetWinD)
198            ? ET.forecastClampProb(opts.targetWinD)
199            : (ch === 'house' && typeof ET.forecastMarginToTargetWinProb === 'function'
200                ? ET.forecastMarginToTargetWinProb(compositeM, ch)
201                : (typeof ET.forecastModelMarginToWinProb === 'function'
202                    ? ET.forecastModelMarginToWinProb(compositeM)
203                    : 0.5));
204        var calibratedFromOdds = ET.forecastProbabilityToTargetMargin(targetWinD, ch);
205        var blend = blendWeightsForChamber(ch);
206        if (opts.pureCalibration) blend = { calibrated: 1, fundamentals: 0 };
207        if (ch === 'house') blend = { calibrated: 1, fundamentals: 0 };
208        var finalTarget = blend.calibrated * calibratedFromOdds + blend.fundamentals * compositeM;
209        var pollCount = opts.pollCount != null ? opts.pollCount : 0;
210        var volMult = ET.forecastSimVolatilityMultiplier(det, ch, pollCount);
211        var sigma = ET.forecastSimSigmaFromFinalMargin(targetWinD, finalTarget, ch, volMult);
212        return {
213            chamber: ch,
214            targetWinD: ET.forecastClampProb(targetWinD),
215            originalCompositeMargin: compositeM,
216            originalCompositeMarginAdjusted: compositeM,
217            calibratedMarginFromOdds: calibratedFromOdds,
218            finalTargetMargin: finalTarget,
219            centerMargin: finalTarget,
220            sigma: sigma,
221            volatilityMultiplier: volMult,
222            favoredProb: targetWinD >= 0.5 ? targetWinD : 1 - targetWinD
223        };
224    };
225
226    ET.forecastSimParamsFromTargetWinD = function (targetWinD, chamber, opts) {
227        opts = opts || {};
228        var det = { margin: 0, forecastKind: normalizeChamber(chamber) };
229        opts.targetWinD = targetWinD;
230        opts.pureCalibration = opts.pureCalibration !== false;
231        return ET.forecastSimParamsFromDetail(det, chamber, opts);
232    };
233
234    ET.forecastSimParamsFromCompositeMargin = function (compositeMargin, chamber) {
235        return ET.forecastSimParamsFromDetail({ margin: compositeMargin }, chamber || 'house', {});
236    };
237})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.