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 fourteen-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 standard ways to convert a national swing into a riding-level projection, and each 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 surges: 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%.
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 5% in a riding drops to negative 5%, which is not a thing.
Poliwave uses a hybrid that combines both so the failure modes cancel each other out:
The first part is just proportional swing: take the riding's old result and multiply by (new target / old baseline). The second part is the trick. It looks like uniform swing (new target minus old baseline), but it is multiplied by the new target itself. This is what makes the formula behave well in both gain and loss scenarios.
Worked example: the negative-vote problem, fixed
Take the case from above. A party was at 50% nationally and 5% in some riding. Now we are projecting it down to 40% nationally.
Pure uniform swing: 5% + (40 − 50) = −5%. Broken.
Poliwave's formula:
- Proportional part: 5 × (40 / 50) × 1.0 = 4%
- Additive part: (40 − 50) × 40 × 0.5 / 100 = −2%
- Total: 4 − 2 = 2%. Reasonable.
The additive part can never push the riding below zero, because it is multiplied by the new target (40 here), which is small when the party is losing. When the party is winning, the new target is large, so the additive part amplifies the gain in mid-tier ridings. That is the asymmetry: small effect on losers, big effect on winners. This is the formula that correctly projected Greens wins in Manchester Gorton and Denton when other variants of the model did not.
If the formula somehow still tries to produce a negative number, the output is floored at zero. Ridings where the party had no real presence stay near zero; strongholds get the full proportional treatment.
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.
- Weight on the proportional part, set to 1.0 by default. Lets the riding's history pass through the swing ratio at full strength.
- Weight on the additive part, set to 0.5 by default. Half-weight so the gain/loss correction gives a measured push without overpowering the proportional part in normal cycles.
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. Z-score-based adjustments per category. 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 (the z-score) and by how much of the demographic the riding actually has.
- Riding boosts. Per-riding manual overrides, either additive or multiplicative. Useful for incumbency, well-known local candidates, or one-off scenario tweaks.
- 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 finds the riding's z-score for that demographic (how far it is from the national average) and multiplies your boost by both the z-score and the share of that demographic in the riding:
The z-score 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. The prevalence multiplier scales the effect by how much of the demographic the riding actually has: a 0.5 z-score on a tiny demographic group shouldn't move the projection as much as a 0.5 z-score on a group that makes up most of the riding. The two multipliers don't double-count because they measure different things about the same observation.
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.
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 mixes a proportional part, which lets strongholds ride the party's overall trend, with a gain/loss-asymmetric additive part that captures surge dynamics in mid-tier ridings. Pure uniform swing and pure proportional swing each fail half the test cases on their own. The hybrid passes both.
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 z-score-weighted adjustment, so the size depends on how far the riding is from the national average. Above average ridings get a positive shift proportional to how far they're above average; below average ridings get the opposite. The effect is relative, not absolute.
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.