I live in the worst area in the UK for dating

Analyzing and plotting the 2021 Census data instead of, you know, going outside.

Introduction

Where are all the women at?

I live in Cambridge and it's okay. There are infrastructure and housing problems but where aren't there these days? It's a university city too. Sure, they only started giving degrees to women in 1948 but, 76 years on, you'd think the gender ratio would have balanced out. And anyway, undergraduate students aren't in my dateable range any more and the population that is isn't necessarily just the student population but older.

But I started having this suspicion. Things were just a bit too off. On the streets, on dating apps, in grocery stores. I had to know.

The government had to know too. That's why they do a Census every ten years, with the most recent one being in 2021. It collects plenty of data, not just sex but also demographics, occupation, housing, travel, education, health etc, with over a hundred variables, broken down by local authority, middle layer super output areas and output areas (basically, a block).

But the built-in visualizers, like the one on the Office for National Statistics' website, don't allow you to visualize more than one variable or condition the value on another variable. So, I could only get the gender ratio in my local authority and not the gender ratio in a certain age range. And that 49.9% female-to-male ratio that I saw in the data browser sounded too clean. Too convenient.

It was time to download the data and figure it out myself. How bad was the city of Cambridge really in terms of the gender ratio?

I had no idea.

Acquiring the data

Step one, go to the ONS's website and get the TS009 dataset. There's also a more granular version, RM200, that goes down to the individual output area, but that's way too granular (too noisy and if need be, I can always travel around beyond my street).

import pandas as pd
import seaborn as sns
sns.set_theme()
import geopandas as gpd
import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

csv = pd.read_csv("TS009-2021-2.csv")
csv.columns = ["la_code", "la", "sex_code", "sex", "age_code", "age", "observation"]

Calculating the stats

Step two, draw the rest of the owl.

FEMALE_AGE_RANGE = (25, 30)
MALE_AGE_RANGE = (25, 30)

We're interested in women aged 25-30. We're also interested in our competition, men aged 25-30 (well, that might be different for men than for women but let's do it this way in the name of symmetry).

This obviously has a bunch of other assumptions: that all these people are single or at least equally single and equally monogamous and/or equally homosexual. But it's a start.

Now, let's get our counts and the gender ratio:

def get_stats(grp):
    return pd.Series(
        {
            f"females_{FEMALE_AGE_RANGE[0]}_to_{FEMALE_AGE_RANGE[1]}": grp[
                (
                    (grp["age_code"] >= FEMALE_AGE_RANGE[0])
                    & (grp["age_code"] <= FEMALE_AGE_RANGE[1])
                )
                & (grp["sex"] == "Female")
            ]["observation"].sum(),
            f"males_{MALE_AGE_RANGE[0]}_to_{MALE_AGE_RANGE[1]}": grp[
                (grp["age_code"] >= MALE_AGE_RANGE[0])
                & (grp["age_code"] <= MALE_AGE_RANGE[1])
                & (grp["sex"] == "Male")
            ]["observation"].sum(),
            "total_population": grp["observation"].sum(),
        }
    )


stats = csv.groupby(["la_code", "la"]).apply(get_stats).reset_index()
stats["female_to_male_ratio"] = (
    stats[f"females_{FEMALE_AGE_RANGE[0]}_to_{FEMALE_AGE_RANGE[1]}"]
    / stats[f"males_{MALE_AGE_RANGE[0]}_to_{MALE_AGE_RANGE[1]}"]
)
stats = stats.sort_values("female_to_male_ratio")

Plotting the stats

So, let's plot the top local authorities by gender ratio.

ax = sns.barplot(stats[:-21:-1], y="la", x="female_to_male_ratio")
ax.bar_label(ax.containers[0], fmt="%.2f")

Sounds reasonable. A lot of boroughs in London as well as the Isles of Scilly (population: 2,100).

Now, let's plot the bottom 20 LAs by gender ratio.

ax = sns.barplot(stats[:20], y="la", x="female_to_male_ratio")
ax.bar_label(ax.containers[0], fmt="%.2f")

Cool. Now let's find Cambridge... oh.

Uh oh.

Turns out, there are roughly 9 women aged 25-30 for 10 men aged 25-30 in Cambridge. But it gets worse. If we take the number of men aged 25-30 and subtract it from the number of women, we get this:

stats["surplus"] = (
    stats[f"females_{FEMALE_AGE_RANGE[0]}_to_{FEMALE_AGE_RANGE[1]}"]
    - stats[f"males_{MALE_AGE_RANGE[0]}_to_{MALE_AGE_RANGE[1]}"]
)
ax = sns.barplot(stats.sort_values("surplus")[:20], y="la", x="surplus")
ax.bar_label(ax.containers[0], fmt="%.2f")
plt.xlim(-1000, 0)
plt.title("Bottom 10 local authorities by excess women - men")

Yep, Cambridge is the worst area in the UK for dating, assuming you want to date women aged 25-30.

Plotting the data on a map

Let's plot this on a map, using this blog post as inspiration.

First, get the 2021 local authority boundaries from the Open Geography Portal. We're interested in the Shapefile format.

lad_geom = gpd.read_file("LAD_DEC_2021_GB_BGC.shp")
stats_geom = lad_geom.merge(stats, right_on="la_code", left_on=["LAD21CD"])

Make a nice Choropleth map of the gender ratios:

from matplotlib import pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.colors as colors

fig = plt.figure(figsize=(10, 15))
plt.title(
    f"England, Local Authorities by gender ratio of people aged {FEMALE_AGE_RANGE[0]}-{FEMALE_AGE_RANGE[1]}"
)
ax = fig.gca()
ax.set_axis_off()
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.0)
stats_geom[["female_to_male_ratio", "geometry"]].plot(
    column="female_to_male_ratio",
    linewidth=0,
    ax=ax,
    cax=cax,
    legend=True,
    norm=colors.CenteredNorm(vcenter=1.0),
    cmap="coolwarm",
)

London is king here, just like we saw in the previous visualization.

Now, the map with the absolute difference between the number of women and men:

fig = plt.figure(figsize=(10, 15))
plt.title(
    f"England, Local Authorities by number of women {FEMALE_AGE_RANGE[0]}-{FEMALE_AGE_RANGE[1]} - men {MALE_AGE_RANGE[0]}-{MALE_AGE_RANGE[1]} (excess)"
)
ax = fig.gca()
ax.set_axis_off()
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.0)

stats_geom["surplus"] = (
    stats[f"females_{FEMALE_AGE_RANGE[0]}_to_{FEMALE_AGE_RANGE[1]}"]
    - stats[f"males_{MALE_AGE_RANGE[0]}_to_{MALE_AGE_RANGE[1]}"]
)

stats_geom[["surplus", "geometry"]].plot(
    column="surplus",
    linewidth=0,
    ax=ax,
    cax=cax,
    legend=True,
    cmap="Reds",
)

Not much is surprising here. The one red hot spot in the West Midlands is Birmingham.

Full table

Want to see how your local authority ranks? See the full table under the foldout:

Click for full table
LAD21CDLAD21NMsurplusfemale_to_male_ratio
1E09000032Wandsworth48971.18918
2E09000012Hackney31811.18008
3E09000019Islington27861.17396
4E09000022Lambeth27581.10796
5E09000023Lewisham27371.17635
6E08000025Birmingham24081.04944
7E09000013Hammersmith and Fulham22691.17872
8E09000008Croydon19051.11938
9E09000014Haringey17641.12842
10E09000007Camden16591.13485
11E08000035Leeds15461.043
12E09000028Southwark15451.06973
13E09000006Bromley14321.13678
14E09000020Kensington and Chelsea13981.19912
15E09000010Enfield13831.11439
16E09000003Barnet13801.08462
17E08000032Bradford13081.06454
18E09000011Greenwich12381.08544
19E09000002Barking and Dagenham12281.14246
20E08000030Walsall12171.11739
21E08000001Bolton11251.10854
22E09000031Waltham Forest11201.08304
23E08000008Tameside10941.12678
24E06000035Medway10481.09805
25E08000028Sandwell10411.0786
26E08000034Kirklees10371.06662
27E06000018Nottingham10301.07295
28E06000047County Durham10061.05824
29E09000004Bexley9911.11491
30E09000009Ealing9491.05579
31E09000016Havering9441.09585
32E08000004Oldham9271.10686
33E08000007Stockport9191.09179
34E08000011Knowsley9171.15187
35E09000024Merton9081.09063
36E09000033Westminster9001.06673
37E06000034Thurrock8921.13096
38E08000010Wigan8701.07076
39E06000060Buckinghamshire8671.04801
40E08000005Rochdale8241.10022
41E08000036Wakefield8081.05768
42E09000026Redbridge7751.06016
43E08000031Wolverhampton7601.07541
44E09000029Sutton7521.10341
45E09000017Hillingdon7441.06051
46E09000018Hounslow7371.05963
47E07000066Basildon7281.10203
48E07000107Dartford7251.15941
49E06000061North Northamptonshire6931.05227
50E06000039Slough6561.10567
51E08000024Sunderland6331.06293
52E09000027Richmond upon Thames6301.10879
53E09000030Tower Hamlets5991.02263
54E08000033Calderdale5971.08552
55E07000114Thanet5941.1328
56W06000022Newport5901.09044
57E06000009Blackpool5671.11406
58E08000018Rotherham5601.05725
59E08000015Wirral5601.05253
60E07000073Harlow5521.1498
61E07000170Ashfield5491.12178
62E09000021Kingston upon Thames5481.0856
63E07000115Tonbridge and Malling5451.13358
64E08000013St. Helens5441.08053
65E07000202Ipswich5381.09314
66E08000029Solihull5361.07603
67E08000016Barnsley5351.0559
68E06000010Kingston upon Hull, City of5311.04418
69E06000030Swindon5211.05739
70E06000056Central Bedfordshire5151.04785
71E09000005Brent5081.0292
72E06000052Cornwall5051.02938
73E08000027Dudley5011.04111
74E08000022North Tyneside4911.06802
75E07000086Eastleigh4861.09898
76W06000018Caerphilly4821.07695
77E06000031Peterborough4821.05274
78E07000103Watford4751.10603
79E06000055Bedford4491.06375
80E08000019Sheffield4401.01826
81E06000042Milton Keynes4391.03835
82W06000016Rhondda Cynon Taf4321.04917
83E08000002Bury4291.06224
84E06000021Stoke-on-Trent4211.04093
85E06000033Southend-on-Sea4121.06231
86E06000058Bournemouth, Christchurch and Poole4121.02876
87E06000032Luton4111.04203
88E08000023South Tyneside4011.07847
89E07000149South Norfolk3991.09402
90E07000173Gedling3981.09972
91E08000012Liverpool3891.01712
92E06000024North Somerset3831.05961
93E06000016Leicester3751.02311
94E08000014Sefton3721.04047
95E07000039South Derbyshire3711.09208
96E07000108Dover3541.09331
97E07000070Chelmsford3531.05163
98E06000014York3461.04535
99E07000109Gravesham3461.08975
100E06000044Portsmouth3461.038
101E06000003Redcar and Cleveland3451.07886
102E06000001Hartlepool3431.10807
103E07000141South Kesteven3301.07533
104W06000005Flintshire3201.06025
105E07000105Ashford3191.0679
106E07000169Selby3181.10166
107E07000111Sevenoaks3151.09497
108E07000084Basingstoke and Deane3151.04424
109E07000122Pendle3131.0961
110W06000020Torfaen3131.09217
111E06000043Brighton and Hove3031.02425
112E07000062Hastings3011.09682
113E07000071Colchester3011.04017
114E09000025Newham3011.01393
115E06000006Halton3001.06409
116E06000012North East Lincolnshire2981.05369
117E07000228Mid Sussex2901.06368
118E06000025South Gloucestershire2891.02495
119E07000219Nuneaton and Bedworth2881.05484
120E06000022Bath and North East Somerset2851.0422
121E07000076Tendring2841.06717
122E06000027Torbay2841.06744
123E07000098Hertsmere2801.07761
124E06000004Stockton-on-Tees2791.03908
125E07000207Elmbridge2751.07694
126E06000020Telford and Wrekin2721.03853
127E07000242East Hertfordshire2711.05169
128E07000208Epsom and Ewell2701.11578
129E07000128Wyre2691.08417
130E07000072Epping Forest2651.05882
131E07000226Crawley2631.0507
132W06000019Blaenau Gwent2581.10106
133E06000041Wokingham2511.04518
134E07000067Braintree2491.04585
135E07000243Stevenage2481.06672
136E07000211Reigate and Banstead2441.04825
137E07000095Broxbourne2441.06793
138W06000010Carmarthenshire2431.04103
139E07000061Eastbourne2431.07364
140E07000116Tunbridge Wells2411.06802
141E06000005Darlington2361.06191
142E07000125Rossendale2321.09663
143E07000096Dacorum2251.04108
144E07000117Burnley2221.06047
145E08000009Trafford2221.02917
146W06000012Neath Port Talbot2201.04431
147W06000014Vale of Glamorgan2171.05157
148E07000229Worthing2161.05788
149E07000221Stratford-on-Avon2151.05388
150W06000003Conwy2141.06463
151E07000063Lewes2121.0832
152W06000004Denbighshire2121.07169
153E06000038Reading2111.02415
154E07000213Spelthorne2101.05814
155E07000090Havant2101.05204
156E07000028Carlisle2061.05121
157E07000145Great Yarmouth2061.06151
158E07000036Erewash2051.04953
159W06000024Merthyr Tydfil2041.09475
160E07000120Hyndburn2021.06415
161E07000126South Ribble2011.05437
162E07000083Tewkesbury1911.06052
163E06000036Bracknell Forest1891.03874
164E07000091New Forest1871.04041
165E07000192Cannock Chase1821.04542
166E06000049Cheshire East1781.01339
167E07000093Test Valley1771.04012
168E07000065Wealden1771.04017
169E07000246Somerset West and Taunton1761.03482
170E08000003Manchester1751.00558
171E08000026Coventry1721.01175
172E07000132Hinckley and Bosworth1721.0434
173E06000046Isle of Wight1651.04119
174E08000037Gateshead1641.02156
175E07000215Tandridge1621.0673
176E07000102Three Rivers1591.05546
177E07000012South Cambridgeshire1571.03097
178E07000236Redditch1551.04506
179E07000130Charnwood1531.02313
180E07000244East Suffolk1511.02125
181E07000042Mid Devon1471.06079
182E07000129Blaby1371.03747
183E07000144Broadland1341.03471
184W06000009Pembrokeshire1331.03533
185E07000011Huntingdonshire1331.02065
186E07000239Wyre Forest1321.04068
187E07000142West Lindsey1321.04607
188E07000197Stafford1301.02769
189E07000033Bolsover1301.04124
190E06000057Northumberland1281.01337
191E07000195Newcastle-under-Lyme1231.02986
192E07000081Gloucester1221.02281
193E07000040East Devon1151.02796
194E07000077Uttlesford1151.04374
195E07000212Runnymede1121.03741
196E07000214Surrey Heath1111.03795
197E07000133Melton1111.07166
198E07000112Folkestone and Hythe1101.03168
199E07000199Tamworth1071.03477
200E07000198Staffordshire Moorlands1061.03852
201E06000040Windsor and Maidenhead1041.02142
202E07000032Amber Valley971.02221
203E07000079Cotswold971.04057
204E07000045Teignbridge941.02421
205E07000240St Albans901.02009
206E07000038North East Derbyshire891.02656
207E07000099North Hertfordshire871.01885
208E07000088Gosport841.0288
209E07000075Rochford841.03157
210E06000008Blackburn with Darwen801.01322
211W06000002Gwynedd781.01995
212E07000223Adur771.04113
213E07000134North West Leicestershire761.01997
214E07000237Worcester731.01693
215E07000172Broxtowe731.01703
216E06000002Middlesbrough711.01157
217E06000050Cheshire West and Chester701.00543
218E07000227Horsham691.01511
219E07000140South Holland671.02114
220E06000045Southampton671.0054
221E07000092Rushmoor661.01413
222E07000037High Peak661.02253
223E07000136Boston651.0254
224E07000106Canterbury651.01269
225E07000148Norwich641.00887
226E07000194Lichfield641.01792
227E07000174Mansfield631.01446
228E08000006Salford621.00393
229E07000074Maldon581.02903
230E07000041Exeter571.01067
231E07000069Castle Point571.01981
232W06000011Swansea551.00635
233E07000009East Cambridgeshire541.01904
234E07000176Rushcliffe521.01399
235E07000121Lancaster521.01088
236E07000216Waverley521.01589
237E07000234Bromsgrove511.0173
238E07000068Brentwood501.01739
239E07000064Rother481.02055
240E07000217Woking481.01238
241E06000019Herefordshire, County of461.00785
242E07000085East Hampshire451.01201
243E07000167Ryedale431.02799
244E07000200Babergh421.0161
245E07000224Arun411.00801
246E07000034Chesterfield371.00941
247E07000010Fenland341.00908
248E07000218North Warwickshire321.01405
249E07000147North Norfolk291.01116
250E07000178Oxford281.00308
251E07000168Scarborough201.00598
252E07000087Fareham201.00592
253E07000043North Devon201.00653
254E07000047West Devon191.01291
255E06000053Isles of Scilly191.34545
256E07000082Stroud191.00515
257E07000163Craven181.01164
258E07000131Harborough151.0049
259E06000011East Riding of Yorkshire151.00147
260E07000044South Hams141.0062
261E06000023Bristol, City of141.00052
262E07000139North Kesteven111.00272
263E07000235Malvern Hills101.00478
264E07000238Wychavon81.00194
265E07000177Cherwell41.00061
266E07000187Mendip-20.999422
267E07000180Vale of White Horse-40.99922
268E07000124Ribble Valley-50.997166
269W06000001Isle of Anglesey-70.996562
270W06000015Cardiff-80.999542
271E07000080Forest of Dean-120.995557
272E07000118Chorley-140.996547
273E07000189South Somerset-150.997371
274E07000188Sedgemoor-150.996459
275E07000210Mole Valley-210.991418
276E07000046Torridge-220.988415
277E07000135Oadby and Wigston-240.987603
278E07000146King's Lynn and West Norfolk-280.994652
279E06000007Warrington-280.996355
280E07000119Fylde-310.98669
281W06000013Bridgend-350.993511
282E06000037West Berkshire-380.992822
283E06000013North Lincolnshire-420.992889
284E07000026Allerdale-450.985773
285E07000027Barrow-in-Furness-520.980662
286E07000029Copeland-540.976744
287E07000171Bassetlaw-540.987285
288E09000015Harrow-570.99491
289W06000008Ceredigion-580.97366
290E07000220Rugby-590.986792
291E07000127West Lancashire-630.982815
292E08000017Doncaster-700.994336
293E07000030Eden-720.955473
294E07000241Welwyn Hatfield-720.984752
295E07000179South Oxfordshire-750.985386
296W06000006Wrexham-800.98361
297E07000035Derbyshire Dales-830.954768
298E07000203Mid Suffolk-880.973708
299E07000113Swale-930.983929
300E07000165Harrogate-960.980332
301E07000175Newark and Sherwood-1000.977381
302E06000062West Northamptonshire-1060.993675
303E07000110Maidstone-1160.98311
304E07000164Hambleton-1180.956919
305E06000026Plymouth-1180.989169
306E09000001City of London-1200.847909
307W06000021Monmouthshire-1270.954643
308E07000225Chichester-1280.966483
309E07000123Preston-1370.979488
310E07000137East Lindsey-1400.965466
311E07000138Lincoln-1430.970357
312E07000089Hart-1430.955618
313E07000181West Oxfordshire-1470.964006
314E07000031South Lakeland-1510.951085
315E06000059Dorset-1890.981636
316W06000023Powys-1920.952721
317E06000015Derby-1970.982535
318E08000021Newcastle upon Tyne-2090.985025
319E07000143Breckland-2100.958234
320E07000209Guildford-2120.959465
321E07000094Winchester-2410.939172
322E07000078Cheltenham-2610.948358
323E07000193East Staffordshire-3220.937073
324E07000222Warwick-4290.928832
325E06000017Rutland-4630.696393
326E07000196South Staffordshire-5330.861089
327E07000166Richmondshire-5630.73088
328E06000054Wiltshire-5910.966601
329E06000051Shropshire-6150.94186
330E07000245West Suffolk-7500.906716
331E07000008Cambridge-8870.912378

Conclusion

There's a bunch of other data that's collected by the Census and existing visualizers are mostly about visualizing single variables on a map. Perhaps it would be cool to build something like what I did, but interactive, with filters and dynamic expressions. Or a different visualization method, like the demographic pyramid in a certain area.

But maybe later. I suddenly feel a weird urge to go to the gym or move out.