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. The BBC/ITV/Sky exit poll's approach is more sophisticated than plain UNS, but it rests on a related idea: assign each constituency to a type, sample a panel of "representative" seats of each type, and extrapolate the swing observed in that panel to every other seat sharing the label. The published methodology describes it as "coherent multiple-regression modelling of multiparty electoral change" over a panel of a little over 130 constituencies, run by a team led by Sir John Curtice for Ipsos (UK in a Changing Europe explainer; Ipsos methodology note).
Both approaches share an assumption worth challenging directly: that a constituency is well described by one category. UNS assumes every seat moves the same way. The exit poll's panel approach assumes every seat moves the way its assigned "type" seat moves. Both were reasonable simplifications in a stable two-party system. 2024 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| 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
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 better 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. 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 the whole ballgame, 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 Muslim%, high
Pakistani%, high Asian%..."). Each row of W is one seat's weights across those archetypes.
Everything in W and H is constrained to be non-negative — you can add archetypes together,
never subtract them — which is exactly the physical constraint a real population mixture has:
you can't have -12% of a seat be secular graduates.
The intuition that made this click for me: 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 — 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 big 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:
| 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 the census 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.
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"])| 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 quality (R²) across k=2..20 and taking the elbow — the point where adding another archetype stops buying much more reconstruction accuracy. That came out at k=5.
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)k = 5 | ecological R² (2024 vote, all parties pooled) = 0.295
| 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% |
Quick tour before we start pulling threads:
- Archetype 1 (5.6% of GB): high Asian, Muslim, Pakistani, Indian population, skewed young. Implied vote leans heavily to "Other" (independents), then Labour.
- Archetype 2 (37.7% of GB, the single largest group): high routine occupations, no qualifications, social renting — the classic post-industrial working-class profile. Implied vote: Labour first, but Reform second at 29%.
- Archetype 3 (8.5% of GB): high "White Other," high
Superwealthy, high ethnic diversity generally — the "international metropolitan" profile. Implied vote: Labour-led, but with real Conservative, Lib Dem and Green minorities. - Archetype 4 (32.7% of GB): high managerial/professional, owner-occupation, degrees — affluent and professional. Implied vote: Conservative first, Lib Dem a strong second.
- Archetype 5 (15.5% of GB): defined mostly by low scores across the board (low under-18s, low intermediate occupations, low Christian, low owner-occupation) — a young, secular, rented, graduate profile. Implied vote: Labour first, Green a strong second.
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. Two of the five don't.
5. What's actually inside Archetype 1
Archetype 1's headline profile — "high Asian, high Muslim, high Pakistani, high Indian" — is already a hint of trouble: those are four different, only loosely correlated things being described by one archetype. 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 look at seats where Archetype 1 actually dominates.
mem = pd.read_csv("../../clustering/output/archetype_membership.csv")
feat = pd.read_csv("../../clustering/output/features_england.csv")
watch_seats = ["Harrow West", "Harrow East", "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| 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 |
| 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:
- Harrow West and Harrow East are ~30–36% Archetype 1, with a large Hindu and Indian population (24–33%) and comparatively modest Muslim population (15–18%). Their Conservative vote barely moved: Harrow East fell only 1.3 points in a year the party collapsed by over 20 points nationally. No surge to independents either (5.9–10.5%).
- Leicester East, Bethnal Green and Stepney, Dewsbury and Batley, and Blackburn are also high on Archetype 1 (or its close sibling Archetype 2), but with the Muslim population share driving most of the loading, and their 2024 story is the opposite of stable: Bethnal Green and Stepney swung nearly 33 points to "Other" (an independent won the seat); Leicester East swung 23 points the same way. These are the seats behind the well-documented independent surge in Muslim-plurality areas in 2024, several of them explicitly tied to Gaza as a single-issue driver.
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 (a 23-point swing to Other, in a seat that also saw real Hindu–Muslim communal tension after 2022) doesn't sit cleanly in either bucket. That's not a bug in the analysis; it's a seat where the assumption of two clean groups genuinely strains, and it's worth flagging as a case that will need special handling however the final grouping shakes out — rather than quietly forcing it into whichever side looks more convenient.
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(4)
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)England-wide average Conservative swing, 2019→2024: -21.6 points
| 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 |
In the four 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, 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.
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. 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)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
| 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. 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.
The same three seats check out against a completely independent measure: recorded average
house price. Electoral Calculus's constituency house-price figures put Kensington and
Bayswater at £2,047,640, Cities of London and Westminster at £1,934,555, and Chelsea
and Fulham at £1,418,340 — against £631,345 for London as a whole and £313,667 for Great
Britain (Electoral Calculus constituency
data).
Every one of them is, literally, over a million pounds: three to six times the national
average, in the same three seats the Superwealthy variable and the Belgravia ward result
already pointed to independently. Three unrelated measurements — a tax-record-derived census
proxy, a hyper-local council-ward election, and the property market — landing on the same
handful of postcodes is about as much corroboration as this kind of claim gets. On this
evidence, 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 assuming the answer.
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}%")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), which is the case for treating rural residence as a real but heavily discounted signal
rather than the near-blanket assumption it might have earned pre-2019: 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) and shows no sign of having stopped moving.
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, Diaspora-Conservative, Superwealthy, Working-class-Reform, 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"]]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
| 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 read exactly like the "safe bellwether" reputation suggests — Buckingham and Bletchley, Northampton South, Milton Keynes North, Chelmsford, Banbury — 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–7 together, the categories this research converges on look like this:
| 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; £1.4m–£2.0m average house prices; 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 |
| Secular 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 |
Leicester East stays flagged as an unresolved edge case rather than force-fit into either of the first two rows, per Section 5.
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: 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.
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 the million-pounds-plus house prices above), and a flat 20% of the Rural population
(Section 6 found the relationship real but its size roughly halved between 2019 and 2024, hence
the discount rather than a full weighting). This is the one group built from "wherever we found
a specific, well-evidenced Conservative-leaning population," rather than from a single
archetype.
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.
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.
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" than one drawn from private-sector management and finance — the formula reflects a
real, distinct sub-lean rather than assuming all professionals are interchangeable.
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.
Average — a constant baseline score, the same for every seat. The six formulas above all vary with local composition; Average doesn't need to, because — per Section 7 — 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 — which is exactly the "follows whichever way the seat otherwise leans" behaviour Section 7 found in the data.
On the double-counting question
None of the raw scores above are literal, mutually-exclusive headcounts, and they're not meant
to be — 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:
-
The tenure-split terms are a real partition, not a double-count. Every unit of
ManagerialProfessionalis allocated to either Progressives (via the renting fraction) or Liberals (via the owning fraction), andrenting fraction + owning fraction = 1by 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. -
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 fromBlackdirectly, 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, both argued from evidence rather than assumed. Turning each group into an actual per-party preference order or flow table, and using it to allocate 2024 votes and project 2029 seats, is the next post.