Part 2 · Latent groups

The trouble with giving a seat one label

With Uniform National Swing (UNS) as dead as a dodo, every UK election model eventually has to decide what a constituency ‘is,’ part 1 found that doing so with a single label was about as accurate as describing a person by their star sign. Here we stop doing that, and start letting each of Britain's 632 seats be several different kinds of place at once.

April 2026 · 24 min read

1. The problem with giving a constituency one label

Every UK election model has to solve the same problem eventually: you know how a constituency voted last time, you know roughly how the national mood has shifted, and you need to turn that into a vote share for this seat. The classic tool for that is uniform national swing (UNS): take the national swing from Party A to Party B and apply it evenly to every seat, on the reasonable-sounding assumption that the whole country is basically one seat wearing 650 different hats.

A more sophisticated real-world approach exists too, and it's the one this post's own method has the most in common with: MRP (multilevel regression and post-stratification), the technique behind YouGov's constituency-by-constituency projections since 2017, the year it correctly called a hung parliament while nearly every UNS-based model didn't. MRP fits a model of individual vote intention from a national survey, broken down by respondents' demographic and geographic traits (the "multilevel regression" half), then re-weights those modelled preferences for every seat by that seat's actual population count in each demographic cell (the "post-stratification" half) to produce a constituency estimate. Strip away the survey machinery and that second step is the same move this post is about to make: take a small set of reusable group-level preferences and blend them per seat according to how much of each group is actually present locally, rather than assuming the seat is one thing. The difference lies in where the groups and their preferences come from. MRP estimates both from a national survey's individual responses, while this project estimates the groups themselves from unsupervised structure in constituency-level census data, with no survey at all. Even so, the underlying logic, a local blend of shared types rather than one label per seat, is the same idea this post arrives at from a different direction.

MRP's real limitation isn't that it collapses to one number per seat: it's two things bundled together. First, a published MRP result is almost always just the final post-stratified estimate; the demographic-cell-level preferences the model fits on the way there, which group leans which way and by how much, get used internally and then thrown away, never reported as a first-class output in their own right the way this post's archetypes and groups are about to be. Second, and this is the one that actually bites: those cell-level preferences come from fitting a multilevel model against one specific, expensively-fielded national survey. That's fine for a single snapshot, but there's no cheap way to re-estimate a group's preferences every time the national mood shifts, because doing that properly means commissioning a fresh national survey and refitting the whole model each time. A method that can only be run economically once isn't well-suited to tracking how national opinion moves between one snapshot and the next, which is exactly the thing a live election model needs to keep doing. This project's structure sidesteps that by splitting the same two ingredients apart on purpose. A seat's group composition comes from the census (stable, survey-free, and barely changing between elections, which is what Section 4 onward is entirely about), while each group's own political preferences are a separate, far cheaper input to update: an ordinary poll or a fresh flow assumption, not a fresh multilevel model, that can move with the national mood without ever re-deriving what's actually inside each seat.

There's a second inversion on top of that split, and it's arguably the more fundamental one: which direction the fit actually runs. MRP fits inward, from the survey out: it regresses individual survey respondents' stated preferences against their demographic and geographic traits, and whatever national and seat-level totals fall out of that fitted model are simply accepted as the answer. The survey is ground truth, and the group-level breakdown is a byproduct of fitting it. This project's pipeline (covered in full in the next post) runs the fit the other way around: it starts from an evidenced hypothesis of how each latent group is actually moving (the Muslim-to-independent swing, the stable Jewish/diaspora Conservative floor, and so on, argued case-by-case in Sections 5–9), encodes each one as a group-specific vote-flow assumption, and then adjusts those flows until the seats they aggregate up to reproduce the national polling topline that's actually being observed. The group-level flows are fit to the national numbers; the national numbers aren't read off a fit to individual-level polling. MRP treats the survey as the fixed point and the groups as whatever falls out of fitting it; this project treats the groups as the thing under direct scrutiny, and treats matching the current national polling as a constraint those group-level flows have to satisfy, not the object being estimated.

Back to UNS, assuming the national mood shifting meant that it shifted somewhat evenly was a reasonable thing to do in a stable two-party system, where one national vote number told you almost everything anyway. 2024, memorably, was not that system. Part one's own clustering experiment found the same thing from a completely different angle: even the best available hard partition of Britain's 632 seats couldn't agree with itself, let alone with reality, on where the lines between types should fall.

import json
import pandas as pd
from pathlib import Path

DATA = Path("../../web/data/elections")
PARTIES = ["LAB", "CON", "REF", "LD", "GRN", "OTHER", "SNP", "PLAID"]

def national_shares(fname):
    records = json.load(open(DATA / fname, encoding="utf-8"))
    totals = {p: 0 for p in PARTIES}
    grand_total = 0
    for r in records:
        t = pd.to_numeric(r.get("Total"), errors="coerce")
        if pd.isna(t):
            continue
        grand_total += t
        for p in PARTIES:
            v = pd.to_numeric(r.get(p), errors="coerce")
            if pd.notna(v):
                totals[p] += v
    return {p: round(v / grand_total * 100, 1) for p, v in totals.items()}

shares_2019 = national_shares("au2021.json")   # notional 2019 result on 2024 boundaries
shares_2024 = national_shares("2024.json")

summary = pd.DataFrame({"2019 (notional)": shares_2019, "2024": shares_2024})
summary.loc["Lab+Con (two-party)"] = summary.loc[["LAB", "CON"]].sum()
summary
Output
2019 (notional) 2024
LAB 32.9 34.6
CON 44.7 24.4
REF 2.1 14.7
LD 11.8 12.6
GRN 2.8 6.9
OTHER 1.2 3.2
SNP 4.0 2.6
PLAID 0.5 0.7
Lab+Con (two-party) 77.6 59.0

That's computed directly from this project's own 632-seat GB dataset (au2021.json / 2024.json), so it will differ slightly from the official UK-wide totals (Great Britain only, no Northern Ireland), but the shape is unmistakable: the combined Labour+Conservative vote share fell from 77.6% to 59.0% in one election. Officially, across the whole UK, the two-party share hit 57.4% (Full Fact; House of Commons Library, CBP-10009), the lowest on record. Reform UK took 14.3% of the national vote for 5 seats. Five pro-Gaza independents, including a former Labour leader, won seats that "should" have been safe Labour by any category-based model. A seat that used to be well-approximated by "safe Labour, urban, working-class" might now be some blend of long-time Labour loyalists, a lingering white working class, Reform-curious minority, a small but decisive bloc of voters motivated primarily by Gaza, and a handful of Green-curious graduates who moved in during the last decade. None of those groups is the constituency's "type"; they're all in it, simultaneously, in different proportions to the seat next door.

That's the empirical case for the rest of this post: if a constituency's electorate is a mixture of a smallish number of recurring voter types, rather than a single type, the right tool isn't a sharper category label. It's something that can express how much of each type is present.

2. From "which bucket" to "how much of which bucket"

Part one's glossary covered what K-means, HAC, DBSCAN and GMM each assume, and if you skipped it, the short version is: they all sort. Worth restating the one thing all four have in common, because it's exactly what needs to change: every one of them, including GMM's "soft" probabilities, answers the question which single cluster does this whole row most plausibly belong to, and how sure are we? A GMM saying "Bethnal Green and Stepney is 60% likely to be in cluster 3, 40% likely to be in cluster 5" is expressing uncertainty about a single assignment, not "60% of this seat's population is one thing and 40% is another." That distinction sounds pedantic until you try to build a vote model on top of it, at which point it's suddenly extremely important, because what we actually want is the second one: a real compositional breakdown that sums to exactly 100% per seat, the same way a census breakdown of ethnicity or age already does for the underlying population. Getting there needs a different family of technique entirely, not a better-tuned member of the family part one already exhausted.

The matrix approach: unmixing instead of sorting

Non-negative Matrix Factorization (NMF) starts from a different question: instead of "which single group does this row belong to," it asks "can every row be reconstructed as a non-negative weighted sum of a small number of shared 'parts'?" Concretely, if X is our 632-seats-by-34-census-variables matrix, NMF finds two smaller matrices W (632 × k) and H (k × 34) such that

X ≈ W @ H

Each row of H is one latent archetype's demographic profile (e.g. "high Rural%, high Pakistani%, high Graduate%..."). Each row of W is one seat's weights across those archetypes. Everything in W and H is constrained to be non-negative: archetypes can be added together but never subtracted, exactly the physical constraint a real population mixture has, since you can't have -12% of a seat be veterans.

A helpful intuition is that it's like reconstructing every shade in a paint chart from a handful of primary pigments, mixed in different proportions. NMF doesn't need to be told what the primaries are; it finds a set of a few reusable pigments that reconstruct every observed shade about as well as possible, then reports how much of each pigment went into each shade.

One extra step is needed to get all the way to a proper percentage breakdown: NMF's own W isn't constrained to sum to 1 per row (there's a scale ambiguity between W and H: you can multiply a column of W by 2 and divide the matching row of H by 2 and get the identical product). So after NMF finds the archetype profiles (H), each seat's exact mixture weight is re-solved as a small constrained least-squares problem: find the non-negative weights that best reconstruct that seat's actual census numbers from the fixed archetype profiles, subject to the weights summing to 100%. This is a standard trick from hyperspectral image analysis (unmixing a pixel's spectrum into a blend of known minerals) called Fully Constrained Least Squares Unmixing (FCLSU), and it's what actually produces "this seat is 62% Segment 3, 25% Segment 1, 13% Segment 5" as a genuine, literal percentage breakdown, not a clustering confidence score.

Categories are only useful if their members actually vote similarly. If a "category" is secretly two different electorates with two different loyalties bundled together by an oversimplified label, no single description of how it votes can represent it, since the two electorates will just pull in opposite directions in whichever seats mix them in different proportions. That's the test the rest of this post applies to every category it proposes: not just "does this describe the census data well," but "do the seats dominated by this category actually behave like one electorate."

How each category then translates into an actual vote prediction (turnout, swing, tactical voting) is a large enough question to get its own write-up. This one stays focused on the categories themselves: what they are, and what evidence supports each one.

3. The data

Every demographic variable feeding the archetype model comes from the 2021 Census (Office for National Statistics, for England & Wales; National Records of Scotland for the equivalent Scottish figures), at constituency level, with the exception of Public Sector and Rural/Urban classification, which come from other ONS sources:

Category Variables
Ethnicity White, White British, White Other, Asian, Indian, Pakistani, Bangladeshi, Chinese, Black, Arab, Mixed, Other
Religion Christian, No Religion, Muslim, Hindu, Jewish, Sikh, Buddhist
Age 0–17, 18–24, 65+
Education No qualifications, Degree-educated
Occupation (NS-SEC) Public sector, Managerial/Professional, Intermediate, Routine
Housing tenure Social rent, Private rent, Owner-occupied
Other Veterans, LGBT population, Rural/urban classification

One variable doesn't come from government data at all: Superwealthy, a proxy for the super-rich/non-domiciled population, isn't something the census asks about. It's sourced from research using de-identified HMRC tax records: Advani, Burgherr, Savage and Summers (2022), "Who are the non-doms?", CAGE Policy Briefing No. 36 (University of Warwick / LSE International Inequalities Institute), which analysed everyone who claimed non-dom tax status between 1997 and 2018. Its headline constituency-level finding is exactly what this variable is trying to capture: more than one in ten residents of Kensington, and of Cities of London and Westminster, had claimed non-dom status at some point, concentrated overwhelmingly in a small number of very specific, very expensive postcodes, not spread evenly across "affluent" Britain generally. That concentration, not just "rich" but this specific kind of international-money rich clustered in a handful of London streets, turns out to matter a lot in Section 6.

As a supporting proxy, the calculation also incorporates expensive housing sales from the land registry, helping capture the geographic concentration of this wealth where direct population-level data isn't available.

import openpyxl

wb = openpyxl.load_workbook("../../demographics.xlsx", read_only=True, data_only=True)
ws = wb["Demographics"]
rows = list(ws.iter_rows(values_only=True))
header = rows[0]
seat_i, sw_i = header.index("Seat"), header.index("Superwealthy")

superwealthy = sorted(
    ((r[seat_i], r[sw_i]) for r in rows[1:] if r[seat_i] and isinstance(r[sw_i], (int, float))),
    key=lambda t: -t[1],
)
pd.DataFrame(superwealthy[:8], columns=["Seat", "Superwealthy score"])
Output
Seat Superwealthy score
0 Cities of London and Westminster 28.0
1 Kensington and Bayswater 25.6
2 Chelsea and Fulham 20.4
3 Hampstead and Highgate 16.4
4 Finchley and Golders Green 10.4
5 Richmond Park 8.4
6 Wimbledon 8.0
7 Hammersmith and Chiswick 6.8

Exactly the two seats the non-dom study flags by name come out on top of this project's own data, a good sign the variable is measuring what it claims to.

4. Running the matrix: five archetypes

NMF was fit once, across all 632 GB constituencies pooled together (England, Scotland and Wales at once, since this step uses no electoral data, there's no reason to fit each nation separately the way an earlier, vote-informed clustering pass might). The number of archetypes, k, was chosen by sweeping NMF's reconstruction R² (how much of the 632×34 census matrix's variance k archetypes can rebuild) across a range of k, then taking the elbow: the point where adding another archetype stops buying much more reconstruction accuracy.

import json

archetypes = json.load(open("../../clustering/output/archetypes.json", encoding="utf-8"))
curve = archetypes["k_search_curve"]

curve_df = pd.DataFrame({
    "k": [int(k) for k in curve],
    "reconstruction_R2": list(curve.values()),
})
curve_df["gain_over_k-1"] = curve_df["reconstruction_R2"].diff()
curve_df
Output
k reconstruction_R2 gain_over_k-1
0 2 0.4083 NaN
1 3 0.5579 0.1496
2 4 0.6493 0.0914
3 5 0.6962 0.0469
4 6 0.7245 0.0283
5 7 0.7664 0.0419
6 8 0.7794 0.0130
7 9 0.8031 0.0237
8 10 0.8396 0.0365
9 11 0.8607 0.0211
10 12 0.8803 0.0196
11 13 0.8916 0.0113

Reconstruction R² rises steeply through the first few archetypes (0.41 at k=2, 0.56 at k=3, 0.65 at k=4), then visibly flattens from k=5 onward (0.70, then 0.72, 0.77, ...): each additional archetype after the fifth buys a few extra points of reconstruction rather than the double-digit jumps seen earlier. Locating the point of maximum curvature on that exact curve (a standard kneedle-style perpendicular-distance-from-the-chord calculation, see find_elbow in 06_archetype_unmixing.py) picks out k=5 automatically, which is reassuring, because I would have gone with five anyway and then had to pretend it was principled.

import json

archetypes = json.load(open("../../clustering/output/archetypes.json", encoding="utf-8"))
print(f"k = {archetypes['k']}  |  ecological R² (2024 vote, all parties pooled) = {archetypes['ecological_r2_2024']}")

rows = []
for a in archetypes["archetypes"]:
    rows.append({
        "Archetype": a["archetype"],
        "% of GB": a["avg_share_of_gb"],
        "Dominant in n seats": a["n_seats_dominant"],
        "Distinguishing profile": a["label"],
        "Implied 2024 vote": ", ".join(f"{p} {v:.0f}%" for p, v in
                                        sorted(a["implied_vote_2024"].items(), key=lambda kv: -kv[1]) if v > 1),
    })
pd.DataFrame(rows)
Output
k = 5  |  ecological R² (2024 vote, all parties pooled) = 0.295
Output
Archetype % of GB Dominant in n seats Distinguishing profile Implied 2024 vote
0 1 5.59 18 high Asian (z=+9.1); high Age0to17 (z=+8.8); h... OTHER 37%, LAB 22%, CON 21%, REF 10%
1 2 37.67 310 high Routine (z=+3.4); low ManagerialProfessio... LAB 52%, REF 29%, CON 11%, SNP 4%, GRN 4%, OTH...
2 3 8.52 41 high WhiteOther (z=+5.6); high Superwealthy (z... LAB 54%, CON 22%, LD 13%, GRN 10%, REF 6%
3 4 32.74 224 high ManagerialProfessional (z=+4.4); high Own... CON 51%, LD 35%, REF 8%
4 5 15.48 39 low Age0to17 (z=-6.2); low Intermediate (z=-6.... LAB 68%, GRN 30%, SNP 9%, OTHER 3%

That ecological R² of 0.295 deserves a direct read, not a fly-by: these five archetypes, built from census demographics with zero information about how any seat actually voted, account for a bit under a third of the cross-party variance in 2024 vote shares on their own. That's a real number worth sitting with, and it isn't the R² of the finished model, it's the score for archetype composition alone, before Sections 5–9 do anything with it. Two things keep it from being disqualifying. First, this is a harder test than it looks: a model that gets to see a seat's own electoral history (as UNS does, by starting from the seat's actual last result and applying a swing) starts with far more information than one working from demographics alone, so 0.295 from census data with no electoral history at all is a different, lower bar than a "know what happened last time" model needs to clear. Second, and more to the point: nothing here claims 0.295 is the finished product. It's a diagnostic showing the compositional idea already carries real signal before Sections 5–9 spend the rest of the post deliberately correcting the two archetypes (1 and 3) where that signal is weakest, precisely because a single archetype-level number like 0.295 hides real internal splits (Harrow vs. Bethnal Green; Kensington vs. the rest of Archetype 3) that the following sections dig out by hand. The archetypes are raw material for the group-level scoring formula in Section 9, not a standalone forecasting model being presented as one, however much I'd like to stop here and call it a day.

A brief overview of the archetypes identified:

That's a clean, plausible five-way split of Britain's electorate, and also, on inspection, not quite the end of the story. The "implied vote" column above is a single number per archetype; it only tells the truth if every seat dominated by that archetype behaves similarly. As the implied vote shares above already hint, some don't.

5. What's actually inside Archetype 1

Archetype 1's headline profile ("high Asian, high Muslim, high Pakistani, high Indian") looks cohesive as a category at first, for representing Britain's ethnic minorities. NMF found them co-occurring often enough across 632 seats to treat them as one recurring "part," but that's a statement about the census data's correlation structure, not a claim that these communities vote the same way. Let's go and look at the seats where Archetype 1 actually dominates, and see how long the tidy story survives contact with them.

mem = pd.read_csv("../../clustering/output/archetype_membership.csv")
feat = pd.read_csv("../../clustering/output/features_england.csv")

watch_seats = ["Harrow West", "Harrow East", "Brent West","Leicester East", "Leicester South",
               "Bethnal Green and Stepney", "Dewsbury and Batley", "Blackburn"]

cols = ["Indian", "Pakistani", "Muslim", "Hindu", "Sikh",
        "y2019_CON_share", "y2024_CON_share", "swing_CON", "y2024_OTHER_share", "swing_OTHER"]

merged = mem.merge(feat[["Seat"] + cols], on="Seat", how="left")
view = merged[merged["Seat"].isin(watch_seats)][
    ["Seat", "Archetype_1", "Archetype_2", "Archetype_5"] + cols
].round(1)

view
Output
Seat Archetype_1 Archetype_2 Archetype_5 Indian Pakistani Muslim Hindu Sikh y2019_CON_share y2024_CON_share swing_CON y2024_OTHER_share swing_OTHER
23 Bethnal Green and Stepney 32.8 0.0 56.4 2.2 1.1 46.6 1.3 0.3 10.1 4.1 -6.0 33.7 32.8
39 Blackburn 38.7 38.3 16.0 21.4 23.5 46.8 0.4 0.1 23.3 8.9 -14.4 19.4 18.7
60 Brent West 40.2 3.5 22.4 35.2 4.9 17.9 28.3 0.8 33.8 32.5 -1.3 6.7 6.1
133 Dewsbury and Batley 35.2 39.3 15.8 16.7 25.4 43.7 0.3 0.2 28.7 11.0 -17.7 0.0 -5.2
207 Harrow East 35.9 0.0 10.0 32.9 3.6 15.1 28.0 0.6 54.6 53.3 -1.3 5.9 5.9
208 Harrow West 32.1 0.0 14.7 24.4 4.5 17.8 24.3 1.2 34.8 29.2 -5.7 10.5 10.5
263 Leicester East 63.0 18.4 18.6 59.2 4.2 25.6 37.6 6.8 38.5 31.1 -7.4 23.6 22.9
264 Leicester South 38.8 14.8 42.1 30.7 4.7 34.8 8.3 3.7 21.8 11.5 -10.3 1.9 1.9

Two very different stories are both hiding inside "Archetype 1," and the seats above draw the line cleanly:

Leicester East is the genuinely awkward middle case: it has both a large Hindu population (37.6%) and a large Muslim population (25.6%), and its result (Conservatives down a below-average 7 points, Labour down heavily with 'Other' candidates surging, the Conservatives' only seat gain of the election) doesn't sit cleanly in either bucket, in a seat that also saw real Hindu-Muslim communal tension after 2022. It's worth pausing on: both the Conservatives holding up relatively well in highly Hindu areas, and Labour's significant losses amongst Muslim communities, played out in the same seat, and between them produced a genuinely unusual result.

The evidence points to (at least) two distinct categories along the Hindu/Indian vs. Muslim axis: a South Asian, Conservative-diaspora-leaning group (Harrow-shaped) and a Muslim, Labour-to-independent group (Bethnal Green/Dewsbury/Blackburn-shaped), rather than one archetype with an implied vote that's a meaningless average of two opposed electorates.

A candidate for the same Conservative-diaspora group: British Jewish voters

If we're building a group defined partly by "minority community with a real, well-documented, above-average Conservative lean," British Jewish voters are the best-studied case in the country, and they point the same direction. The Institute for Jewish Policy Research's polling around the 2019 general election, run with Survation, found that 61% of British Jews voted Conservative and just 11% voted Labour, "almost certainly the lowest level of Jewish support for the Labour Party in history" (JPR: How British Jews vote and why). A pre-election Survation poll had it even more stark: 64% Conservative to 6% Labour. That collapse tracks the Corbyn-era antisemitism crisis specifically, and 2024 polling shows some reversion toward the national picture (JPR, 2024), but the underlying lean is well-established enough, over enough elections, to be a real electoral signature rather than a one-off reaction. This project's own seat-level data agrees:

top_jewish = feat.sort_values("Jewish", ascending=False)[
    ["Seat", "Jewish", "y2019_CON_share", "y2024_CON_share", "swing_CON"]
].head(6)
england_avg_swing = feat["swing_CON"].mean()

print(f"England-wide average Conservative swing, 2019→2024: {england_avg_swing:.1f} points\n")
top_jewish.round(1)
Output
England-wide average Conservative swing, 2019→2024: -21.6 points

Output
Seat Jewish y2019_CON_share y2024_CON_share swing_CON
175 Finchley and Golders Green 20.8 43.8 35.1 -8.7
221 Hertsmere 16.9 64.9 44.7 -20.2
82 Bury South 15.8 43.5 23.4 -20.0
216 Hendon 14.2 48.9 38.4 -10.5
196 Hackney North and Stoke Newington 12.6 13.0 8.4 -4.6
106 Chipping Barnet 9.7 47.5 36.7 -10.8

In the six highest-Jewish-population seats in England, the Conservative vote held up meaningfully better than the national collapse in two of the four (Finchley and Golders Green: -8.7 points; Hendon: -10.5 points, Chipping Barnet -10.8~ against an England-wide average of roughly -21.6), smaller falls than almost anywhere else in the country, in a year the party lost seats it had held for generations. That's consistent with British Jewish voters being a real, distinct, Conservative-leaning constituency within whatever broader "Archetype 1"-adjacent group ends up housing them, a strong candidate to fold into the same Conservative-diaspora group as the Harrow-shaped seats above, on electoral behaviour rather than ethnicity or religion as such, which is about the least controversial sentence I'll manage to write in this section.

6. What's actually inside Archetype 3

Archetype 3 ("international metropolitan," high Superwealthy, high ethnic diversity, implied vote Labour 54% / Conservative 22%) has the same kind of problem as Archetype 1, for a related reason: Superwealthy, as the non-dom research above already told us, is extremely spatially concentrated. A handful of seats drive almost all of the archetype's Superwealthy loading, and if their Conservative vote behaves differently from the rest of the archetype, averaging them in dilutes a real, distinct signal.

Worth a wry footnote before the numbers: Superwealthy is, at bottom, a non-dom proxy, and plenty of non-doms are foreign nationals who aren't eligible to vote in a UK general election at all (not to mention that non-dom status no longer exists as a tax arrangement). Whatever Conservative lean turns up here isn't literally non-doms turning out to vote non-dom; it's that a genuine non-dom enclave doesn't arrive alone. It comes with a great many very-domestic, very-enfranchised, very Conservative-voting neighbours, because that's who else can afford to live next door to that kind of money. The seat votes Tory even where the specific population the variable is naming, mostly, can't.

merged3 = mem.merge(
    feat[["Seat", "Superwealthy", "y2019_CON_share", "y2024_CON_share", "swing_CON"]],
    on="Seat", how="left",
)
archetype3_seats = merged3[merged3["Archetype_3"] > 25]
print(f"Seats with Archetype 3 > 25% membership: {len(archetype3_seats)}")
print(f"Correlation(Superwealthy, 2024 Conservative share) within this group: "
      f"{archetype3_seats['Superwealthy'].corr(archetype3_seats['y2024_CON_share']):.3f}\n")

hi = archetype3_seats[archetype3_seats["Superwealthy"] > 10]
lo = archetype3_seats[archetype3_seats["Superwealthy"] <= 10]
print(f"High-Superwealthy subgroup  (n={len(hi)}): mean 2024 CON = {hi['y2024_CON_share'].mean():.1f}%, "
      f"mean swing = {hi['swing_CON'].mean():.1f}")
print(f"Rest of Archetype 3         (n={len(lo)}): mean 2024 CON = {lo['y2024_CON_share'].mean():.1f}%, "
      f"mean swing = {lo['swing_CON'].mean():.1f}")

archetype3_seats.sort_values("Archetype_3", ascending=False).head(6)[
    ["Seat", "Archetype_3", "Superwealthy", "y2024_CON_share", "swing_CON"]
].round(1)
Output
Seats with Archetype 3 > 25% membership: 56
Correlation(Superwealthy, 2024 Conservative share) within this group: 0.364

High-Superwealthy subgroup  (n=5): mean 2024 CON = 31.5%, mean swing = -6.7
Rest of Archetype 3         (n=51): mean 2024 CON = 17.8%, mean swing = -10.3
Output
Seat Archetype_3 Superwealthy y2024_CON_share swing_CON
247 Kensington and Bayswater 77.2 25.6 33.7 -4.6
109 Cities of London and Westminster 75.5 28.0 32.1 -8.2
175 Finchley and Golders Green 71.3 10.4 35.1 -8.7
202 Hampstead and Highgate 60.8 16.4 17.4 -5.7
97 Chelsea and Fulham 59.5 20.4 39.1 -6.4
216 Hendon 58.9 3.5 38.4 -10.5

A real, if moderate, correlation (r ≈ 0.36) between Superwealthy and 2024 Conservative vote share within Archetype 3, and it's driven by exactly the seats you'd expect from the non-dom study: Kensington and Bayswater, Cities of London and Westminster, Chelsea and Fulham. Splitting Archetype 3 at Superwealthy > 10 isolates a 5-seat pocket averaging 31.5% Conservative with a 6.7-point swing away, against 17.8% and a 10.3-point swing for the other 51 seats in the same archetype: the Conservative-leaning pocket didn't just poll better, it also fell less.

Constituency-level results understate how solid this pocket actually is, because a parliamentary seat blends the enclave with everything around it (in the case of Kensington and Westminster, a much more Labour friendly mix of renters, graduates, and ethnic minorities). Local election results don't: in the 2022 Westminster City Council election, the Knightsbridge and Belgravia ward, sitting inside Cities of London and Westminster, returned all three of its councillors as Conservatives, with the top candidate taking 62.7% of the vote outright (results via Wikipedia). That's not a marginal lean; it's a landslide, at exactly the geographic resolution the Superwealthy variable is trying to target.

On this evidence (three separate methods all pointing at the same three postcodes, which is more agreement than I get from my own data most days), the small number of Superwealthy-heavy seats within Archetype 3 are safe to peel off into the same Blue (Conservative-leaning) group as the diaspora and Jewish-vote seats from Section 5, a genuinely different electorate from the rest of Archetype 3's Labour-leaning metropolitan mass, hiding inside the same broad ethnicity-and-tenure profile that NMF (reasonably) treated as one thing.

What about rural areas?

Rural areas are the traditional shorthand for "safe Conservative," so it's worth checking whether that shorthand still earns a place in the Blue category, and how much weight it deserves, rather than nodding along because everyone else does.

print(f"Correlation(Rural, 2024 Conservative share), England: {feat['Rural'].corr(feat['y2024_CON_share']):.3f}")
print(f"Correlation(Rural, Conservative swing), England:      {feat['Rural'].corr(feat['swing_CON']):.3f}\n")

hi_rural = feat[feat["Rural"] > 50]
lo_rural = feat[feat["Rural"] <= 10]
print(f"High-rural seats (Rural>50, n={len(hi_rural)}): mean CON 2019 = {hi_rural['y2019_CON_share'].mean():.1f}%, "
      f"mean CON 2024 = {hi_rural['y2024_CON_share'].mean():.1f}%")
print(f"Low-rural seats  (Rural<=10, n={len(lo_rural)}): mean CON 2019 = {lo_rural['y2019_CON_share'].mean():.1f}%, "
      f"mean CON 2024 = {lo_rural['y2024_CON_share'].mean():.1f}%")
Output
Correlation(Rural, 2024 Conservative share), England: 0.495
Correlation(Rural, Conservative swing), England:      -0.408

High-rural seats (Rural>50, n=67): mean CON 2019 = 59.5%, mean CON 2024 = 32.7%
Low-rural seats  (Rural<=10, n=302): mean CON 2019 = 38.7%, mean CON 2024 = 20.4%

Rural seats really are meaningfully more Conservative (r ≈ 0.50 with 2024 vote share, a stronger relationship than either Jewish or Hindu population share managed on their own), so routing some of the rural population toward the Blue category is the right call. But the size of that lean has moved a lot in a short time: the average high-rural seat here went from 59.5% Conservative in 2019 to 32.7% in 2024, a 26.8-point collapse, and that's not a quirk of this dataset. An independent CLA/Survation poll of the 100 most rural constituencies in Britain found Conservative support at 34% going into the 2024 election, "down 25 points from 2019" (CLA: General election 2024: the rural reaction), essentially the same number, from a completely different source. Rural seats are also swinging away from the Conservatives somewhat faster than urban ones (r ≈ -0.41 between Rural and swing), though this may be more so due to the fact that rural areas had much higher Conservative votes in 2019 compared to urban areas, with the latter having fewer initial votes for the Conservatives to lose. All told, the model routes a flat 20% of a seat's Rural population into Blue: enough to register the relationship that's still there (r ≈ 0.50), deliberately conservative given that the historic rural Tory lean has roughly halved in one election (59.5% → 32.7% above).

7. Average: politically disengaged, follows the national mood

It's tempting, having just spent two sections finding real signal inside supposedly generic archetypes, to expect that enough digging turns every seat into a clean, named, explicable electorate: Muslim, Young progressives, White-Working-class, and so on. It doesn't; A large share of Britain's constituencies simply don't lean on any one distinguishing trait: no unusual ethnic or religious composition, no notably young or old population, no striking tenure or class skew. If none of the specific categories above describes a seat well, what does?

The most direct test: does a seat's demographic distinctiveness predict how unusual its voting is? If "Average" seats are a real category, not just leftover statistical noise, they should be the ones whose vote share and swing sit closest to the national numbers, while the seats with a strong, distinctive demographic signature should be the ones that deviate furthest from the national trend.

demo_cols = [c for c in feat.columns if c not in ("Seat", "County", "Region")
             and not c.startswith(("y2019_", "y2024_", "swing_"))]
z = (feat[demo_cols] - feat[demo_cols].mean()) / feat[demo_cols].std()
feat["distinctiveness"] = z.abs().mean(axis=1)   # average |z-score| across every demographic variable

national_con, national_swing = feat["y2024_CON_share"].mean(), feat["swing_CON"].mean()
feat["dev_swing"] = (feat["swing_CON"] - national_swing).abs()

print(f"Correlation(demographic distinctiveness, |deviation from national swing|): "
      f"{feat['distinctiveness'].corr(feat['dev_swing']):.3f}\n")

least = feat.sort_values("distinctiveness").head(50)
most = feat.sort_values("distinctiveness").tail(50)
print(f"50 LEAST distinctive seats: mean 2024 CON = {least['y2024_CON_share'].mean():.1f}% "
      f"(national {national_con:.1f}%), mean |swing deviation| = {least['dev_swing'].mean():.1f}pp")
print(f"50 MOST distinctive seats:  mean 2024 CON = {most['y2024_CON_share'].mean():.1f}% "
      f"(national {national_con:.1f}%), mean |swing deviation| = {most['dev_swing'].mean():.1f}pp")

least.nsmallest(6, "distinctiveness")[["Seat"]]
Output
Correlation(demographic distinctiveness, |deviation from national swing|): 0.491

50 LEAST distinctive seats: mean 2024 CON = 28.2% (national 25.1%), mean |swing deviation| = 5.4pp
50 MOST distinctive seats:  mean 2024 CON = 15.8% (national 25.1%), mean |swing deviation| = 14.0pp
Output
Seat
78 Buckingham and Bletchley
344 Northampton South
307 Milton Keynes North
96 Chelmsford
9 Banbury
384 Rochester and Strood

The relationship holds (r ≈ 0.49 between distinctiveness and swing deviation), and the gap between the two groups is stark: the 50 least distinctive seats swing within about 5 points of the national average; the 50 most distinctive seats swing nearly three times as far off it. The least-distinctive seats themselves include some well known "bellwether" seats: Northampton South, Milton Keynes North, Rochester and Strood, medium English towns with no strong ethnic, religious, generational or class skew in any direction, and votes that track the national mood almost by definition.

That's what Average actually captures: not "we couldn't characterise them," but a real, predictable behavioural pattern: a voter, or a seat, without a strong independent pull of its own, who or which follows wherever the national conversation goes that year. It's the largest single category almost everywhere, for the same reason a "typical" reading is more common than an extreme one: most places aren't unusual on any single axis at once. The specific categories in Sections 5 and 6 are where the distinctive stories live; Average is what's left when none of those stories apply, and empirically it behaves exactly the way that description predicts.

8. Where this leaves the group structure

Putting Sections 4 to 7 together, here's where all that digging actually lands:

Group Where it comes from Core evidence
Muslim / independent-leaning Archetype 1, Muslim-plurality seats Bethnal Green, Dewsbury, Blackburn: 20–33pt swings to independents
South Asian Conservative-diaspora Archetype 1, Hindu/Indian-plurality seats Harrow West/East: Conservative vote barely moved against a 20pt national collapse
Jewish / Conservative evidence-led addition JPR/Survation: 61% Conservative, 11% Labour (2019); Finchley & Hendon held up in 2024
Superwealthy / Conservative Archetype 3, high-Superwealthy seats Kensington, Cities of London & Westminster, Chelsea & Fulham; Sticky conservative vote in inner London, Belgravia ward 62.7% Conservative locally
Metropolitan / cosmopolitan Archetype 3, the rest Labour-leaning, ethnically diverse, not superwealthy
Working-class / Reform-curious Archetype 2 Labour 52%, Reform 29% implied vote
Affluent professional Archetype 4 Conservative 51%, Lib Dem 35% implied vote
Student / graduate / renter Archetype 5 Labour 68%, Green 30% implied vote
Average no strong distinguishing trait r≈0.49 between demographic distinctiveness and swing deviation from the national trend

That's the group structure conceptually. Section 9 makes it arithmetic: the actual per-seat scoring formula settled on for each group, and why each piece of each formula is there.

9. Turning the groups into a formula, seat by seat

Sections 5–8 established which groups belong in the model and why, from evidence. This section is the last mile, and the one where the vague talk of "groups" has to turn into arithmetic I can actually be held to: an explicit, per-seat scoring formula for each group, built directly from the census variables already in the dataset, with every design choice justified against either this project's own correlations or external research.

Muslim: used directly as the Muslim religion percentage, unmodified. Unlike every other group below, this one needs no construction: religious self-identification in the census already is the exact population this group is trying to represent, at exactly the right resolution.

Left: Age18to24, used alone (Archetype 5's defining feature). YouGov's post-election analysis found the Green vote share among 18–24s roughly doubled in 2024 versus 2019 to 14%, while Labour led every age group except 65+, "the oldest crossover point since 1997" (YouGov: How Britain voted in the 2024 general election). This project's own correlations agree and go further: Age18to24 correlates at r=+0.477 with 2024 Green share (England), a stronger relationship than Labour's own +0.345, and at r=-0.517 with Conservative share, the single strongest predictor of the Conservative collapse found anywhere in this dataset.

Progressives: Black + Sikh + ManagerialProfessional × (PrivateRent / (PrivateRent + OwnerOccupation)). Black British voters have polled at roughly 70–80% Labour for decades, remaining Labour's most popular choice among Black voters even in 2024 (The Conversation). British Sikhs lean the same way, if less overwhelmingly: a 2024 survey found 43% Labour against 20% Conservative among Sikh respondents, more than two-to-one (Religion Media Centre: General Election 2024: the Sikh vote), a genuinely different profile from the Hindu-leaning Conservative-diaspora signal folded into Blue above. The model is deliberately not treating "South Asian" as one bloc. The renting professional term is Archetype 3's remaining, non-superwealthy urban-cosmopolitan population, captured through tenure: this project's own data shows PrivateRent correlates at r=+0.457 with 2024 Green share and +0.351 with Labour, against -0.478 with Conservative: renters lean markedly left of owners in the same occupational class.

Average: a group for the "leftover" section of voters who don't neatly fit anywhere else; it's specifically the category for seats and voters with no strong pull of their own. The final renormalisation step (rescaling all seven raw scores to sum to 100%, described next) automatically hands it a larger share in seats where none of the six specific signals above are strong, and a smaller share where they are, exactly the "follows whichever way the seat otherwise leans" behaviour Section 7 found in the data.

Liberals ("Blue Wall", Archetype 4): ManagerialProfessional × (OwnerOccupation / (OwnerOccupation + PrivateRent)) × (1 - PublicSector). This is Progressives' formula mirrored: the same professional population, split the other way by tenure, isolating the owner-occupying half instead of the renting half. OwnerOccupation is this project's single strongest Conservative correlate of any variable tested, at r=+0.669 (England, 2024), far stronger than any ethnicity or religion variable. The × (1 - PublicSector) term then removes public sector professionals specifically: this project's data shows PublicSector correlates at -0.369 with Conservative share and +0.319 with Labour, so a professional-class population that happens to be teachers, doctors and civil servants behaves measurably less like "Blue Wall" (See, Tooting, Cardiff North) than one drawn from private-sector management and finance. The formula reflects a real, distinct sub-lean rather than assuming all professionals are interchangeable.

Blue (Conservative-leaning): Hindu + Jewish + Superwealthy + 0.2 × Rural. Each term is one of the specific, evidenced sub-populations from Sections 5 and 6, added together rather than derived from a single archetype: the Hindu population (Harrow-shaped diaspora-Conservative seats: near-zero swing against a seat's own religion/ethnicity baseline), the Jewish population (JPR/Survation: 61% Conservative in 2019, and the smallest Conservative collapses in the country in 2024 running through the highest-Jewish seats), the Superwealthy population (r ≈ 0.36 with 2024 Conservative share within Archetype 3, backed independently by the Belgravia ward result), and a flat 20% of the Rural population (Section 6). This is the one group built from "wherever we found a specific, well-evidenced Conservative-leaning population," rather than from a single archetype.

Reform (Archetype 2): ((3 × Routine + NoQualifications) / 4) × WhiteBritish. The routine-occupation/no-qualifications blend (weighted 3:1 toward occupation, since it's the stronger and more stable of the two working-class markers) captures the classic post-industrial profile; multiplying by WhiteBritish restricts it to the specific electorate Reform actually draws from. This project's data makes the case for that restriction directly: WhiteBritish correlates at r=+0.508 with 2024 Reform share, the single strongest correlate of Reform's vote found for any variable in this dataset, stronger even than Routine occupation alone. An ethnically-diverse working-class seat and a white-British working-class seat can have identical NS-SEC profiles and very different Reform ceilings; this term is what keeps the model from conflating them.

On the double-counting question

None of the raw scores above are literal, mutually-exclusive headcounts, and they're not meant to be, since the census only publishes marginal percentages (what fraction of a seat is Black; what fraction is a homeowner) rather than the full joint cross-tabulation (what fraction is both) at constituency level. A real person who is a Black professional homeowner is entirely possible, and is genuinely counted toward more than one of these raw terms: the Black term directly, and a share of the ManagerialProfessional term depending on this seat's tenure mix. That's unavoidable with marginal data, and it's the reason this is a two-stage design rather than a one-shot formula:

  1. The tenure-split terms are a real partition, not a double-count. Every unit of ManagerialProfessional is allocated to either Progressives (via the renting fraction) or Liberals (via the owning fraction), and renting fraction + owning fraction = 1 by construction, so the professional population is fully and exactly split between the two groups, never counted twice between them. The one real assumption baked in here is that professionals rent and own in roughly the same proportion as the seat's population overall, i.e. that occupation class and tenure are independent within a seat. That's a simplification (professionals plausibly own at a higher rate than routine workers even within the same seat), and a genuine target for refinement once household-level or small-area tenure-by-occupation data can be brought in. But it's a considerably better approximation than assuming no relationship between tenure and lean at all, and it costs nothing extra to compute from data already on hand.

  2. The additive terms (Black, Sikh, Age18to24) are deliberately allowed to overlap with the professional terms, because that overlap is signal, not noise. A seat with a large population of Black professional renters gets two reinforcing contributions toward Progressives (one from Black directly, one from the renting-professional term), and that's the correct behaviour if the goal is a propensity score, not a headcount: such a seat really does carry two independent pieces of evidence pointing the same way, and a model that let them reinforce each other should score it more confidently Progressive than a seat with only one. What these raw scores are not, at this stage, is a percentage of anything: Black% + Sikh% + a fraction of Professional% has no reason to sum to a sensible number on its own, and isn't supposed to.

That's what the final step is for. Exactly like the NMF archetypes in Section 4, these seven raw propensity scores get rescaled so they sum to 100% per seat, the same "renormalise a set of non-negative scores into a genuine percentage breakdown" move as the FCLSU step in Section 4, just with evidence-derived weights standing in for NMF's data-discovered ones instead of a constrained least-squares solve. The double-counting inside a single raw score before that rescaling isn't a bug to be fixed; it's exactly how the score is allowed to accumulate confidence from more than one piece of overlapping evidence before being turned into a proportion.

That's the group structure and the arithmetic behind it. Turning each group into an actual per-party preference order or flow table, and using that to allocate 2024 votes and project 2029 seats, is the next post: the one where all this careful category-building finally has to go and predict something, in public, where I can be proven wrong.