Poliwave's Methodology
A detailed account of how each projection is built, from raw historical data through the final reconciled forecast.
You give Poliwave a set of input targets. These can be poll numbers, hypothetical scenarios, or any mix. You can set targets at four levels: country-wide (general), region (like Atlantic Canada), province (subnational), or a smaller sub-region (subregional, like Calgary).
For the election you want to project, Poliwave loads the result of the previous election under today's riding boundaries (same redistribution, marked by rep_order in the database). That one prior election is the per-riding baseline. Earlier elections come in only through the historical-reconstruction step, which fills in baselines for parties that did not run in that prior cycle.
Poliwave then runs a swing formula on every riding. The formula mixes proportional swing with a gain/loss-asymmetric additive term. The additive term is scaled by the party's new target, so it can't push a riding below zero when the party loses, and it gives mid-tier ridings room to surge when the party wins.
After the swing, the model can add extra effects on top: voters moving between parties, tactical voting in close races, demographic boosts, and manual regional tweaks. Most of these are optional. In particular, tactical voting is off by default in the formal projections published on this site (real voter behaviour is messy and hard to predict from rules); it's available mainly as a knob for users to play with in the interactive simulator. At the end the model rescales every riding so the geographic averages land close to your targets. If your numbers don't fully agree (say, a national target that doesn't match the average of your regional targets), the broader target wins.
For the official projections we publish on the site, we also run a Monte Carlo simulation on top to get win probabilities and confidence intervals. It takes about five minutes per run, so the interactive seat calculator does not run it on your scenarios; you get the point estimate only.
The rest of the page goes into each piece in detail.
Poliwave runs every riding through a fifteen-step pipeline. Each step does one thing: read the prior result, fill in missing candidates, apply swing, move votes between parties, add demographic and geographic adjustments, and finally rescale everything to match the targets you gave. The swing formula in the middle is the core math, but on its own it would not be enough. The surrounding steps are what turn a national poll into a per-riding forecast you can actually trust.
There are two classical ways to convert a national swing into a riding-level projection, and each one breaks in a different way.
Proportional swing multiplies every riding by the same ratio. If a party doubles nationally, every riding doubles too. This is fine most of the time but breaks during big swings in either direction. A party that was already at 55% in a riding has nowhere to go when you double it, and the formula tries to push it past 100%. More damagingly, when a party collapses nationally (think NDP 2025 going from 17.8% to 6%), pure proportional brutalises strongholds — Edmonton Strathcona's 60.7% gets scaled down to 20% even though incumbent personal vote and local brand strength should preserve much more than that.
Uniform swing adds the same number of points to every riding. If the party drops 10 points nationally, every riding drops 10 points. This breaks the other way: a party at 3% in a riding drops to negative 7%, which is not a thing. Pure uniform also has the opposite collapse problem: a riding where the party had almost no presence keeps a non-zero share that's basically noise.
Poliwave's projection formula takes the geometric mean of both — it multiplies the two predictions together and takes the square root. Each prediction is floored at zero first so the inputs to the square root can't be negative:
The geometric mean is the natural average for percentages. It always sits between the two predictions, and it gets pulled toward whichever one is smaller. That single mathematical property fixes both classical failure modes at once: when proportional explodes high, the smaller uniform value pulls the answer down; when uniform crashes to zero, geometric mean returns zero too (because square root of anything-times-zero is zero), which is the correct answer for a riding where the party has effectively vanished.
Worked example 1: NDP 2025 in Edmonton Strathcona (collapse scenario)
Heather McPherson's seat. NDP nationally went from 17.8% in 2021 to 6.3% in 2025. McPherson ran for re-election, won the riding, held it above 45% — much higher than the national 1/3 collapse would suggest.
Inputs: =60.7, =6, =17.8.
- Proportional only: 60.7 × (6 / 17.8) = 20.5% — way too harsh; ignores the incumbent's hold.
- Uniform only: 60.7 + (6 − 17.8) = 48.9% — closer to reality but doesn't generalise.
- Geometric mean: √(20.5 × 48.9) = 31.6%
The incumbent boost (described later) lifts that further to about 33%. The previous Excel-hybrid formula gave 20.1% in this riding — over 11 points worse than geometric mean. The old formula systematically under-projected NDP strongholds in 2025 by 10-15 points; the new formula cuts that error in half while keeping the same answer almost everywhere else.
Worked example 2: the negative-vote problem
A party at 3.3% in some riding (say Lac-Saint-Jean NDP 2021), with a national drop from 17.8% to 6%.
- Proportional only: 3.3 × (6 / 17.8) = 1.1%
- Uniform only: 3.3 + (6 − 17.8) = −8.5%, floored to 0%
- Geometric mean: √(1.1 × 0) = 0%
The party with almost no presence gets a clean zero, not a phantom fraction. That zero-out behaviour at the bottom is built into the formula by construction: no thresholds, no special cases. Just the same multiply-and-root.
Variable definitions
- The party's projected vote share in this riding. This is the output. Later pipeline steps adjust it further.
- What the party got in this riding in the previous election. Normally the model uses only this one prior election. Earlier elections come in only via the historical-reconstruction step when this number is missing or unreliable.
- Your input target for the party (from polls or scenarios), picked at the most specific level that applies to this riding. The model checks subregional input first, then subnational, then regional, then the general (national) input.
- The matching baseline. If the current input is regional, the baseline is what the party got in that same region in the last election. The two have to be at the same geographic level; otherwise the swing ratio means nothing.
Why this formula is principled, not just empirically tuned
The geometric mean has zero tuning parameters. There are no weights to fit, no thresholds to set, no per-jurisdiction calibration. It is just multiply and square-root. That matters for forecasts that need to stand the test of time: a formula tuned by hand to one era of elections quietly drifts as the political landscape changes. A parameter-free formula does not.
The mathematical justification is that vote shares are ratio data — bounded between 0 and 100, and behaving multiplicatively. Going from 5% to 10% isn't the same kind of change as going from 50% to 55%, even though both are "+5pp." Proportional swing captures the multiplicative nature; uniform swing captures the additive nature; the geometric mean is the standard way to combine the two because it operates in the ratio space natural to percentages.
Empirical evidence: across 47 Canadian elections since 2000,
the geometric-mean formula wins more often than it loses against the previous
Excel hybrid (17 wins, 6 losses, 24 ties) when both are run inside the full
production pipeline. On federal elections specifically — the cleanest historical
data — seat-count error drops by 2 seats per election compared to the previous
formula. Mean winner accuracy across all 47 elections goes from 84.40% to
85.05%. The full backtest is reproducible from tests/study_swing_formulas.py and the production-pipeline
comparison runs from tests/backtest_sweep.py with SWING_FORMULA=geometric_mean versus the previous default.
Every riding goes through the same ordered set of steps. The swing formula above is one of them. The others clean up the input to the formula and make the output line up with everything else the model has to respect.
- Input parsing. The model loads each riding's
previous result, the geographic hierarchy (which riding belongs to which sub-region,
province, region), and any referendum results for the target election's
redistribution (
rep_order). Your hierarchical input is flattened into a lookup so each riding picks the most specific target that applies to it. - Historical normalisation. Missing or unreliable prior results get filled in. A party that did not nominate a candidate at all (a recent Canadian example: Conservatives in Québec Centre, 2025), or that ran with less than 20% of its own national share for the cycle (the cutoff that separates a real campaign from a late-withdrawal residue), gets rebuilt by one of three methods described in the next section. This step runs on whatever prior-cycle baseline the database holds for the riding, including transposed results from older boundaries (a 2025 federal projection's main baseline is the 2021 result transposed onto the 2023 rep_order, for instance).
- Swing projection. The core formula runs on every riding and every party. Special cases handle brand-new parties and parties running nationally but absent from this riding.
- Simple normalisation. The riding's projected percentages are rescaled to add up to 100%. Anything negative is set to zero.
- Vote flow. Loss-only redistribution. When a party is projected to lose share, the lost share is handed to other parties according to a flow matrix you supply.
- Vote intake. Vote transfers that don't depend on whether a party is winning or losing. The formula is in the redistribution section below.
- Tactical voting. Strategic defection in FPTP races, scaled by how close the local race is. Two rule types: block-target voting (anti-X coordination) and the top-two squeeze (wasted-vote logic). This stage is off by default in the formal projections published on the site and is enabled only when you turn it on in the interactive simulator.
- Regional adjustments. Manual knobs. You give the model two tables, one for additive shifts and one for percent multipliers, organised by level (subregional, subnational, regional) and by party. Each cell is the change you want applied at that level for that party. The model just applies what you wrote; nothing is auto-computed here.
- Demographics. Per-category adjustments scaled to the party's existing support. You set a boost for a (party, demographic) pair; the model multiplies it by how far the riding deviates from the national average for that demographic and by the party's current vote share in the riding, so the effect can't blow past what the party actually has to move.
- Incumbent boost. An automatic 5% multiplicative boost goes to the party of any candidate identified as a running incumbent in the riding (sitting MP seeking re-election). A symmetric 5% deboost goes to the previous-winner party when the sitting MP is known to be retiring or otherwise not on the ballot. Both magnitudes are calibrated from a backtest sweep across 38 Canadian elections since 2000; see the dedicated section below.
- Riding boosts. Per-riding manual overrides, either additive or multiplicative. Useful for unusually strong or weak local candidates beyond the auto-incumbent effect, well-known local figures, or one-off scenario tweaks. User boosts stack on top of the auto-incumbent boost.
- Hierarchical normalisation. The rescaling step. Every riding is rescaled level by level (subregional first, general last) so the vote-weighted average at each level matches your input for that level. The general (national) level runs last and is the binding rule the result has to respect.
- Threshold capping. Damps any party that's running hot in a riding compared to its cohort. When the projected share exceeds a multiple of the regional or national average (default cap: 1.5× that average), the excess gets reduced by a set rate (default 50%). The riding is then renormalised to 100%, and hierarchical normalisation is re-run so the capping doesn't break the geographic totals. This isn't a small-party squelch; it applies to any party that runs hot.
- Aggregation. Riding winners are picked, seat totals are added up by party, and geographic summary tables are built for each level of the hierarchy.
- Output. Everything is packaged into the JSON response used by the website and by the Monte Carlo layer further down this page.
A note on the historical window used per projection
A projection only uses prior elections held under the same riding boundaries as
the target election. The database calls a redistribution version rep_order. For the 2025 federal projection (rep_order = 2023), the
usable history is whatever federal elections have results compiled or transposed
into the 2023 boundaries: the 2021 result transposed forward, plus any earlier
elections that have been mapped onto today's boundaries by our upload scripts.
The model does NOT reach back to 1867 when making one projection. The full
1867-onwards record lives in the site's historical archive as a separate
resource, and the upload scripts are what bring some of those older numbers into
the current rep_order when they get boundary-mapped forward.
Inside that history window, the most important prep step is filling in missing or unreliable prior results. A naive model would take "the party got 0% here" at face value and project from that. Poliwave does not, for two reasons.
The first reason is candidate withdrawal. A party might just not have nominated a candidate at all in the last cycle. There are real Canadian cases: the Conservatives did not run a candidate in Québec Centre in 2025, so the prior result is structurally zero, and treating it as a real voter preference would be wrong.
The second reason is late withdrawals that still leave a name on the ballot. The 2019 Brexit Party stand-down in Conservative-held UK seats is the classic example, but the same thing happens in Canada when a candidate withdraws after the nomination deadline. The resulting tenths of a percent look like a vote share, but they don't reflect a real campaign. Either way, this needs to be cleaned up before the swing formula sees the baseline.
When the model has to fill a baseline in, it picks one of three branches depending on what data is available. Note that transposed prior results (for example the 2021 federal result mapped onto the 2023 rep_order) are treated as the real prior result for the riding and go through this stage like any other baseline. They're the main input the projection runs on, not an exceptional case.
Branch 1: footprint-matched proportional swing
The default. The missing value is filled by taking the most-specific available geographic average for the party and applying proportional swing from the historical version of that average to the current version. The averages on both sides of the swing ratio only count ridings where the party ran in both the old and the new cycle. That restriction kills off a whole class of errors. If you didn't restrict it, a party that stood down in a region last cycle but is running fully this cycle would produce a huge swing ratio against a tiny prior denominator, and the rebuilt number would blow past 100%.
The "level" in the formula is picked most-specific-first: subregional, then subnational, then regional, then general (national). This matches the same hierarchy the user-input lookup uses elsewhere in the pipeline. The reasoning is the same too: a riding's behaviour is better predicted by its actual local cohort than by a national rollup.
Branch 2: zero-swing fall-through
When the party has a historical result for the riding but no current-cycle average at any level (usually because the party isn't running this cycle), the swing ratio is zero and the rebuilt baseline collapses to zero. The riding gets a clean zero instead of some arbitrary leftover.
Branch 3: regional-average fallback
When there's no historical result at all (the party is genuinely new), the riding gets a damped version of the current-cycle regional average. The damping factor is set so brand-new parties start meaningfully below their headline regional number, which leaves room for the swing formula to grow them in surge ridings and shrink them where they don't catch on.
Rebuilt baselines are not treated as if they were real. Each baseline carries a confidence weight depending on where it came from: real database results are 1.0, branch-1 reconstructions are 0.7, and the branch-3 regional fallback is 0.4. Before the swing formula reads the baseline, low-confidence baselines get pulled toward the regional mean by a weighted average:
The effect is a careful pull toward the average. When the model trusts the baseline, it respects the riding's individual character. When it doesn't, the riding behaves more like its regional cohort. That keeps a single noisy or guessed number from contaminating the rest of the pipeline.
Voters move between parties in three different ways, and Poliwave handles each one separately. The three run in order, with each one working on what the previous one produced.
Vote Flow (only when a party is losing)
When a party is projected to lose share in a riding compared to its previous result, the lost share gets handed out to other parties using a flow matrix you supply. Each row tells the model, for one source party, what fraction of its losses go to each other party. The rows don't have to add up to 100%; whatever's left over is treated as abstention or as going to parties outside the projection.
Vote Intake (always-on transfer)
Vote intake exists for cases where the "only when losing" condition of vote flow doesn't apply. A party can be gaining nationally and still be drained locally by a new entrant. Each row in the intake matrix tells the model, for one receiving party, what fraction of each source party's current share it pulls in:
This is the case vote intake was built for. Imagine you want to model a new right-wing party (call it Restore) taking voters from Reform UK. Vote flow can't handle this if your projection has Reform gaining or holding steady nationally, because vote flow only fires when the source is losing. But Reform can be up nationally and still bleed support locally to a fresh competitor on the same side of the spectrum. Vote intake lets you say "for every riding, Restore takes a fixed fraction of Reform's current share" regardless of whether Reform itself is up or down. That's why the formula has no loss-condition: it works in both directions.
Within a single riding, all the rules look at the same starting snapshot, so the order of rules in the matrix doesn't matter. Multiple targets pulling from the same source add up. If the total drain would take the source below zero, the rules are scaled down proportionally so the source ends at zero instead of negative.
Tactical Voting (close-race-sensitive)
In first-past-the-post systems, voters often abandon their first-choice party when it has no real chance of winning the local seat. Poliwave models this directly. There are two rule types. A block-target rule names a party that tactical voters want to keep out, a pool of parties whose supporters will defect, and a list of acceptable alternatives. The rule fires in a riding only if the target is in the top two; the pool's voters then move to whichever named blocker is strongest locally. A top-two squeeze rule doesn't name a target. It just drains every party below third place toward the top two, with the destination split decided by the squeezed party's own vote-flow preferences.
Both rule types are damped by how close the race is. The effect scales linearly with the local margin and disappears at a cutoff you can set:
This is what keeps safe seats from moving while close seats get realistic tactical-voting behaviour. The model penalises parties below second place exactly in proportion to how much the local race matters.
Why this is off by default
In the formal projections published on the site, this stage is usually disabled. The reason is below: real tactical-voting behaviour is messy and doesn't reliably match any rule the model can fire, so leaving the rules off is generally more honest than committing to a specific theory of how voters will defect. The stage is mainly here as a tool for users to experiment with in the interactive simulator.
Real-world tactical voting is messier than any rule the model can fire. Voters decide based on a mix of things: past results, recent projections, what they heard on the news, what feels safe, sometimes just gut. The model uses local standing to predict where defection goes; voters in real life often don't.
A good example is Kitchener Centre in 2025. The Greens held the seat with Mike Morrice as the incumbent. Anti-Conservative tactical voters arguably should have stuck with the local Green incumbent to keep the Conservatives out. Plenty of them moved to the Liberals instead, the progressive vote split, and the Conservatives ended up winning. The model's tactical-voting logic, applied naively, would have routed those voters toward whichever non-Conservative was strongest locally (the Greens here). Real voters didn't. People also do the reverse, voting Liberal in a Green-held or NDP-held riding simply because Liberal feels like the safer national choice, even when the incumbent is already the better anti-Conservative bet locally.
You can tune the tactical-voting rules to match a specific theory of how voters are behaving in a given cycle, but no single rule set will catch every real case. Treat the tactical step as one structured input to the projection, not as the last word on what voters will actually do.
Demographic effects in real elections are relative, not absolute. If a party does ten points better with renters, that doesn't mean it picks up ten points everywhere. It picks up ten points in ridings that have a lot more renters than the national average, almost nothing in average ridings, and loses ten points in ridings with way fewer renters than average. Poliwave handles this directly. For each (party, demographic) boost you set, the model multiplies your boost by how far the riding deviates from the national average for that demographic, and by the party's current support in that riding:
The deviation term is what makes the adjustment relative. Above-average ridings get a push proportional to how far above average they are; average ridings barely move; below-average ridings get pushed the opposite way. Multiplying by the party's current support is what keeps the formula honest: a party with no vote share in a riding gets no demographic shift (a 0% party times anything is still 0), and a party already strong in the riding has more vote share available to move. A party sitting at 3% can drift by a few tenths of a point per category; a party at 30% shifts ten times more in absolute terms but the same in relative terms. That self-bounding shape is what stops a small input ever amplifying into an unintended seat flip downstream.
Incumbent candidates outperform their party's swing. The pattern is robust across systems and shows up in almost every academic study of personal-vote effects: name recognition, constituency casework, media presence during the prior term, and a ready-made local volunteer base all add up to a few percentage points of advantage that pure party-level swing math will miss. The flip side is sophomore-surge loss: when a sitting MP retires, the same seat loses the personal vote that helped prop it up, and the party usually slips back toward its normal baseline.
Poliwave applies both effects directly. After demographics and before any user-supplied riding overrides, the model classifies every riding into one of three states and adjusts the projected vote share of the relevant party:
- Incumbent running. Multiplicative +5% boost to the incumbent's party.
- Incumbent retiring. Multiplicative −5% deboost to the previous-winner party. The seat may still be safe, but the personal vote is gone.
- No prior history available. No adjustment.
In the formula, is party 's raw baseline result in riding , so picks out the previous winner; is that party's currently-projected share after the swing and all the redistribution steps above. The boost factor (1 ± b/100) is positive for running incumbents and negative for retiring ones. The riding is then renormalised back to 100% so the within-riding shares still sum cleanly, and the broader hierarchical reconciliation downstream re-anchors regional and national vote-weighted averages to the user's input — so the boost shifts the within-region distribution, not the regional totals.
Identifying the incumbent
Each riding is classified by walking a priority cascade. The model looks at the election we're projecting to (when its candidate data is in the database) and tags each riding using the first signal that fires:
- Explicit incumbent flag. A target-election
row marked
isIncumbent = true. Set when a sitting MP appears on the next ballot. Boost their party. - Candidate-name match. If no flag was set, compare candidate names between the baseline election and the target. A target candidate whose name also appears in the same riding's baseline ballot is treated as an incumbent. Catches cases where the flag was missed during data entry.
- Retiring fallback. When the baseline winner's candidate name does NOT appear in the target's candidate list, the seat is marked as a retiring-incumbent case and gets the negative deboost.
- Baseline-winner heuristic. When no target candidate data is available at all (live forward projection of an upcoming race), the model falls back on the party of the baseline winner and applies the positive boost only. Without target data the model can't tell running from retiring, so the deboost arm is disabled for these runs.
Parties classified as "Other," independent, or Speaker are excluded. The first is a mathematical lump rather than a real party with a sitting MP, and the latter two have their own special-case handling elsewhere in the pipeline.
Where the 5% numbers come from
Both the boost and deboost are calibrated from perfect-input backtest sweeps across 38 Canadian elections since 2000 (federal, provincial, and territorial, each within a single redistribution boundary). For each election we ran the projection with the actual regional vote shares fed in as a "perfect poll" and scored riding-winner accuracy, mean absolute error in vote share, and total seat-count error.
Boost: Winner accuracy rose monotonically with boost size, but vote-share MAE and seat-count error started degrading past about 10%. The joint sweet spot was +5% — roughly half a percentage point of extra riding-winner accuracy across the full sample, with no net change in MAE.
Deboost: A second sweep across the same 38 elections layered a negative multiplier on retiring-incumbent ridings on top of the +5% boost. The −5% point matched the boost magnitude for symmetry and lined up with the joint MAE minimum; going past −10% started rewarding open-seat upsets that don't happen often enough to justify the noise.
Combined effect, federal elections only: winner accuracy rises by roughly 0.8 percentage points and seat-count error drops by about 4.7 seats per election compared to the no-incumbent-adjustment baseline. Across all jurisdictions (provincial data is noisier) the gain is smaller but still clearly positive on every headline metric.
Manual riding boosts in the next step stack on top of this. If you know a specific candidate is exceptionally strong or unusually weak beyond what the party-level incumbency boost captures, you can add a per-riding additive or multiplicative adjustment that runs after the automatic step. The two stack rather than override: the auto-boost lifts (or trims) the projected winner first, your manual boost adjusts further on top.
Every projection has to answer a question you asked, like "what does the country look like if the Liberals are at 26% nationally and the Bloc is at 30% in Quebec?" The reconciliation step is what makes those targets actually stick. For each geographic group at each level, the model takes the current vote-weighted average of the group, compares it to your target for that group, and multiplies every riding in the group by one correction factor:
The in the formula is the riding's total vote count. It weighs each riding's contribution to the group average, so larger ridings have more influence. The four levels run in this fixed order: subregional first, then subnational, then regional, then general (national) last.
Broader targets always win
Important quirk: because the general (national) level runs last, it rescales whatever the lower levels produced. So lower-level inputs act more like a shape than a hard ceiling. Example: you set the Liberals at 50% nationally, and also at 10% in BC, 10% in Alberta, and 10% in Ontario. After the subnational and regional stages, Liberals sit close to 10% in each of those provinces. Then the general stage runs. The vote-weighted national average is way below your 50% target, so the national correction factor is large, and every riding (including BC, AB, ON) gets multiplied up until the national average lands close to 50%. After that, the Liberals are not at 10% in any of those provinces anymore. The regional inputs ended up controlling the relative order and ratios between provinces; the national input ended up controlling the actual level.
This top-down setup has a useful property. The geographic variation that earlier steps built in is preserved, because every rescale is just multiplication: if a riding was double its regional average before reconciliation, it's still double afterwards. Only the absolute level changes. That's what lets the model honour any national target without flattening the riding-by-riding texture that makes the projection useful. It also means that if you genuinely want one province at 10% while the country is at 50%, you have to drop one of the inputs. The two don't agree with each other, and the model trusts the broader target.
This also matters when pollsters get their regional weighting wrong. A pollster's regional crosstabs don't always weight back up to the national headline they reported. If you fed those inconsistent regional numbers in as hard ceilings, the national total in the projection would drift away from the topline. Because Poliwave runs the national level last and rescales whatever the regions produced, the topline you trust the most stays the binding number, and a regionally mis-weighted poll still produces a sensible projection.
Matching is close, not exact
In practice the final vote-weighted averages don't always land exactly on your input. A few things cause small gaps. Riding percentages get clamped at 0% and 100%, so when a correction factor would push some riding past either limit, the lost amount isn't recovered somewhere else in the group. Groups with only one riding are intentionally skipped to avoid instability. Numbers are rounded to two decimals, so a small amount of rounding drift builds up across hundreds of ridings. The model checks the final national-level error for each party against a 0.1 percentage point tolerance and logs a warning if the gap is bigger, but it doesn't iterate to close the gap. Think of the reconciliation as an honest best-effort match, not as a perfect identity.
The simulator on the site adds another reason gaps can show up. You can type whatever numbers you want; the inputs don't have to sum to 100%. At the end, the model still normalises every riding to 100%, which means the final percentages have to share a fixed budget no matter what you typed. If your inputs sum to well over or well under 100%, the deviation between what you typed and what the projection shows can be quite large (5 percentage points or more is common). For a quick worked example: if you set one party at 28% and the rest of your inputs (after the swing runs) total 85%, the combined sum is 113% and every party including the one you set gets shrunk to fit, so the party you typed at 28% lands closer to 25%. The bigger the gap between your input sum and 100, the bigger the drift. Easy fix: keep your inputs summing to roughly 100% across the parties you care about.
For two-round elections like the French legislative, the projection so far has only computed round-1 vote shares. A second pipeline step then takes those round-1 results and projects each riding’s round-2 outcome by deciding which candidates qualify and how the eliminated voters split between them. This whole section runs only when the election is flagged as two-round. First-past-the-post, MMP, PR, STV and other systems skip it entirely.
Qualifier selection
French law lets every candidate who clears 12.5% of registered voters in round 1 advance to round 2. The top two always advance even when fewer than two clear the bar. We apply the same rule: a candidate's qualifying threshold is 12.5% of the riding's eligible voter count, converted into an equivalent percentage of valid votes using each riding's own turnout. So in a riding with 87,000 registered and 60,000 valid votes (about 69% turnout), a candidate needs roughly 18% of valid votes to clear, not 12.5%. This conversion is what produces a realistic mix of duels and triangulaires; applying 12.5% to valid votes directly would let three times as many candidates through and create quadrangulaires that never happen in real life.
If any candidate gets more than 50% of valid votes in round 1, the riding is decided immediately and round 2 is skipped for that seat.
Vote transfer between rounds
Each eliminated party's voters split between the qualifiers according to a transfer matrix. Every cell of the matrix is the affinity of one party's voters for another party on a 0-to-100 scale. Cells are seeded from ideology distance using
where are the source and destination parties’ ideology scores on a -10 to +10 axis and is a decay constant (3.0 by default). A pair of parties at the same ideology gets affinity 100; a pair 7 points apart gets about 10. Users can override individual cells in the Transfer Matrix tab, and those hand-edits take priority over the ideology fallback. For each eliminated party in each riding, the model takes only the row cells for that riding's actual qualifiers, normalises them, multiplies by the source bloc's round-1 vote share, and adds the result to each qualifier's round-2 total. Cells for non-qualifiers are ignored: a transfer rule like "NFP voters dislike LR" only matters in a riding where LR actually made it to round 2.
Per-source abstention
Not every eliminated voter shows up in round 2. A voter whose first choice was a hard-left candidate facing a hard-left versus hard-right runoff has a good option and most of them turn out. The same voter facing a centre versus hard-right runoff has no good option and abstention rises sharply. The model captures this with a per-source abstention rate
is the floor (default 15%, applies even when the voter's favourite qualifier is on the ballot) and the second term is the race sensitivity (default 30%, controls how much abstention rises when no qualifier is appealing). The transfer from each source to each qualifier is then multiplied by (1 − abstention) before being added to the qualifier's round-2 share. After all transfers are applied, qualifier shares are renormalised to sum to 100% so they match how French round-2 results are reported (the abstained chunk is not shown as a percentage point of any party, the way real round-2 tables do).
Both parameters are exposed as sliders in the simulator’s Transfer Matrix tab.
Two-pass aggregation
The aggregation step runs twice. First over the round-1 projected vote shares, producing the pre-runoff view (who would win each riding on a plurality, who leads each region in round 1). Then the round-1 values are snapshotted onto round1_* fields and the model overwrites projected_percentages with the round-2 result, runs aggregation a second time, and stores those as the final-round summary. The R1/R2 toggle on the simulator’s Summary, Map, and Results Table tabs just swaps which set of fields is being displayed.
This is why ridings where the round-1 leader loses the runoff (the classic front-républicain dynamic where a far-right candidate leads on plurality but loses when the centre and left consolidate against them) show up as "FLIP" badges in the Riding Transfers tab.
Current limitations
- Strategic withdrawals are not modelled. In real French elections many third-place qualifiers withdraw between rounds in favour of the front-rép candidate. Our threshold rule produces every legally qualified candidate; in reality some of them step aside. As a result the model slightly over-predicts triangulaires and slightly under-predicts duels for France 2024 (144 modelled triangulaires versus 311 actual, 345 modelled duels versus 191 actual). Total seat counts per party are usually within a few seats of reality because the front-rép transfer math still routes the votes correctly in the runoffs that do happen.
- Ideology is one-dimensional (left-right only). About 85% of French runoff outcomes can be explained on a single ideological axis per the academic literature, but issues like Europe and ecology aren’t captured separately.
- The user-editable transfer matrix is the right escape hatch for any pair of parties whose actual transfer behaviour doesn’t fit a 1D ideology model (regional parties, single-issue parties, etc).
Where Monte Carlo runs, and where it doesn't
Monte Carlo is slow. A single run takes roughly five minutes of wall time, which rules it out of the interactive request path: nobody is going to sit waiting on a five-minute spinner after dragging a polling slider. The Monte Carlo layer therefore runs only for formal projections published on this site (the live federal and provincial forecasts maintained by the team), where the run is initiated offline and the resulting win-probability and confidence-interval data are baked into the page. The interactive simulator that users can drive themselves returns the point estimate produced by the pipeline described above, not a distribution.
Getting a single point estimate is the easy part of a forecast. The hard part is what could happen around it. For formal projections, Poliwave runs Monte Carlo on top of the base projection (40,000 draws by default, more if set higher via the CLI) using a correlated error model. Polling error is split into three pieces: a national piece that moves every riding the same way, a regional piece that moves ridings inside one region together, and a per-riding piece that's independent and whose size depends on the riding's population.
The split exists because real polling misses aren't independent across ridings. They're usually systemic and regional. If you treated every riding as an independent draw, the confidence intervals would be way too narrow and the seat distributions would be too tight around the centre. The correlated split gives back the realistic spread an honest forecast needs.
The third piece, the per-riding term, has its variance scaled inversely by the riding's elector count. A small northern riding with 35,000 electors really is noisier than a Toronto-area riding with 110,000, because a smaller electorate gives more leverage to single events (a popular local candidate, a scandal, a local issue):
is the calibrated noise level at the reference population (the median riding size in the jurisdiction), and is the riding's own elector count. This is what gives small ridings wider win-probability ranges than large ones, and any honest forecast model has to behave this way.
What the simulation actually produces:
- Per-riding win probabilities and 95% confidence intervals for every party.
- Seat distributions at the regional and national level.
- Government formation probabilities: majority, minority, plurality, hung. Each gets its own simulated frequency, based on your inputs and the correlated error structure above.
Methodology is only as good as the track record it produces. On the 2025 Canadian federal election, Poliwave produced the closest absolute seat-count projection among Canadian forecasters: the smallest total gap between projected and actual seats for the top four parties. On riding-level accuracy (the share of seats where the model picked the right winner), Poliwave landed within one percentage point of the long-established national benchmark, despite being a much newer project run by one person instead of an institutional team.
Performance on every prior election the project has covered is published in full on the accuracy page: projected seats next to actual seats, no curation, no cherry-picking.
Every projection combines proportional and uniform swing by taking the geometric mean — multiply both predictions, square-root the product. No tuning parameters. Naturally pulls toward whichever component is smaller, which fixes both classical failure modes at once: runaway proportional projections get damped, near-zero parties cleanly zero out. Wins more elections than it loses against every other formula tested in head-to-head backtests across 47 CA elections since 2000.
When the model needs a prior baseline for a party whose footprint changed between cycles, it only compares ridings the party ran in both times. That kills off the distortion that would otherwise happen when a party stood down one cycle and runs fully the next.
Loss-only flows, always-on intakes, and close-race tactical defection are three separate pipeline steps that add up cleanly without double-counting. Real voters move in different ways, and the model handles each one separately.
How much tactical voting happens depends on how close the local race is. A safe seat sees none; a tight seat sees the full effect. The cutoff and the pool of defecting voters are both user-configurable, so the same engine handles anti-Conservative coordination, anti-Reform coordination, or any future strategic alignment without code changes. Off by default in the formal projections; available as a knob in the interactive simulator.
Each (party, demographic) boost is applied as a deviation-weighted adjustment scaled by the party's current vote share, so the size depends both on how far the riding is from the national average and on how much support the party has to move. Above-average ridings get a positive shift; below-average ridings the opposite; a party at 0% gets nothing because there's nothing to multiply. Relative, not absolute.
Running incumbents get a +5% multiplicative boost; retiring incumbents trigger a symmetric −5% deboost to the previous-winner party. Incumbency is read from the target election's incumbent flag, falling back to candidate-name matches and finally to the baseline winner when no target candidate data exists. Both magnitudes were tuned by backtest sweeps across 38 Canadian elections since 2000.
The final stage rescales every riding so that the vote-weighted averages at each geographic level land as close as possible to the user's input. Clamping at 0 and 100, two-decimal rounding, and single-riding-group skips mean the match is approximate rather than exact; the broadest target acts as the binding constraint and everything upstream provides the per-riding shape the rescale preserves.
Polling error is split into national, regional, and per-riding pieces. This captures the fact that polling misses are usually systemic, not random, and produces realistic seat distributions instead of the implausibly tight ones you get from treating every riding as independent.
Every projection is anchored on the prior election held under today's riding boundaries. Earlier cycles can be brought in when their results have been mathematically transposed onto current boundaries by the database upload scripts. No riding gets a generic baseline; each one starts from its own political history under the boundaries it has today.
This page describes how the model is built and the equations it uses. The specific calibration numbers, jurisdiction-by-jurisdiction tuning, and operational details are kept internal.