The Brighton Secondary School System

A strategic view

Author

Adam Dennett, UCL Centre for Advanced Spatial Analysis

Published

September 13, 2026

Abstract

Brighton & Hove is deciding what to do about a secondary school system that is losing children faster than it is losing places. This document assembles what can be established about that system from published data alone: where the schools and the children are, how reachable the schools actually are by bus, how demand has moved over sixteen years, and what a model of the whole system says about the choices in front of the council. It is written to be argued with.

WarningBeta

This simulator is currently in beta test mode - outputs have not been validated fully, so nothing should, at this point, be taken as reliable, however the simulator shows what could be possible to develop and outputs that are possible.

NoteNo pupil-level data is used here

No pupil-level record is used anywhere in this document. Almost every figure comes from data the council, the DfE or the ONS has already published, or from a model built on top of it. The one exception is a table the council gave the Schools Adjudicator in the 2026/27 case: how many first, second and third preferences each catchment’s children gave each school, over three rounds (item 8.1 of its evidence). It was received as a party to that case rather than published; it is aggregated to catchment and identifies no one; and the full model in section 7 is calibrated to it (Section 7.8.3). Where a question can only be answered with data the council holds but has not released, that is said plainly rather than glossed over — and section 9 lists what those releases would be.

1 Executive summary

1.1 The situation

Brighton & Hove has a secondary school system built for more children than it now has, and the shortfall is going to get worse before anything the council decides can affect it. Year 7 offers peaked at 2,509 in 2019 and stood at 2,250 in 2026. The children who will enter Year 7 in 2033 are already in the city’s primary schools, and there are about 1,750 of them — a further fall of 22%. Against roughly 2,515 published places, that implies something like 765 surplus places, or two and a half average secondary schools’ worth.

The council’s own catchment forecasts sit above that projection in almost every year, and the gap is widest in the Longhill catchment — the place where the consequences of being wrong are largest.

1.2 What the evidence says

This is not a school quality problem. Across English secondaries, absence explains 56% of the variation in Attainment 8 and prior attainment 50%, while deprivation explains only 21%. Once intake is accounted for, Brighton’s schools mostly perform above what their intake predicts. The largest lever available to the city is absence, not social mixing — established in detail in How to Pull the Right Lever.

Moving children between schools is a weak instrument, and this is the single most important point for admissions policy. Decomposing the variance in Attainment 8 shows that a school can reach about 12% of what separates its results from another’s — consistent with sixty years of school-effectiveness research. The rest arrives with the children. Crucially, almost all of that reachable share runs through attendance: the workforce a school hires accounts for only 2.5%, while the school-controllable half of absence accounts for 9%. Redistribution touches neither. It changes which building a child attends without changing their attendance or their circumstances. This is not an argument that schools do not matter — it is an argument that the council’s lever is attendance, and attendance is not an admissions policy.

Admissions criteria only bind at some schools. 3 of the 10 schools offered a place to every first-preference applicant. For those schools the catchment rule, the sibling rule and the free school meals quota do nothing at all. The city runs two admissions systems under one set of rules, and public argument about criteria concerns only one of them.

Families are choosing on the wrong number. Headline Attainment 8 explains 84% of the variation in how heavily each school is preferred. The value-added measure — what the school actually contributes — explains 20%. Because the headline score is largely a description of the existing intake, choice becomes self-fulfilling.

Physical access is unequal, and unequal in the worst direction. 29 of 179 neighbourhoods — about 385 cohort-aged children — can reach no secondary school place within 30 minutes by walking and bus. Accessibility and child poverty correlate at -0.33: 25 neighbourhoods sit in both the least-reachable third and the most deprived third.

Longhill has already been shrunk to about what geography supports. A model of where children would go if only distance and school size mattered gives it 197 children — 73% of its former admission number of 270, but 94% of the reduced 210 now in force. The reduction has already done most of what a reduction can do, and the remaining problem is a falling cohort against a reserve that supports about 3.7 more years.

But the money is not a Longhill problem. 9 of the city’s 10 secondary schools have been spending their reserves, 3 are now overdrawn, and the two in the deepest deficit are not Longhill — they are Hove Park and Cardinal Newman, one of them the largest and fastest-growing school in the city. Section 6 sets out every school’s position and which of them the pupil projections put next in line.

1.3 What follows

  1. Decide about Longhill on the financial timetable, not the consultation one. The horizon is about 3.7 years.
  2. Treat the instruments as a package. Moving the school, changing its admission number and redrawing catchments each fall short alone and compose when combined. Picking one and waiting is the worst available option.
  3. Publish a contextualised measure alongside Attainment 8. It costs nothing and begins to unwind a loop the authority currently sustains through its own admissions guide.
  4. Treat the bus network as an admissions instrument. It is the only lever here that widens choice without taking anything from anyone.
  5. Release six tables (§9) that would convert most of the bands in this document into estimates.

1.4 How confident to be

The demographic findings are the firmest: they are counts of children already in school, adjusted by a transfer rate that has varied by under two percentage points in a decade. The accessibility findings are firm in direction and approximate in magnitude — modelled journey times, not measured ones. The model results in sections 7 and 8 are the softest: the model is swept rather than calibrated, because calibration needs data the council has not released. Every comparison there is robust across the swept range; no individual number should be read as a prediction.

2 The secondary school system

Ten secondary schools serve Brighton & Hove: six community schools run by the local authority, two academies (Brighton Aldridge and Portslade Aldridge) and two church schools (King’s and Cardinal Newman) that set their own admissions. An eleventh, Peacehaven Community School, sits just outside the boundary but inside the travel-to-school geography, and is included throughout because families on the eastern edge of the city treat it as a real option.

The city allocates places by catchment area with a lottery tie-break for oversubscribed schools — an arrangement that is very unusual in England, where distance from the school gate usually decides priority. It dates from the closure of CoMArt in the east of the city in 2005, and the catchment system that replaced distance-based allocation in 2008. CoMArt is marked on the map below because the shape of the system is still partly the shape of that closure.

2.1 Where the schools, the catchments and the children are

Show code
# ---- Schools ---------------------------------------------------------
sch <- schools_sf(oi$schools)

sch_popup <- sprintf(
  "<b>%s</b><br>Admission number 2024: %s<br>Planned 2030: %s%s",
  sch$name, sch$pan2024, sch$pan2030,
  ifelse(is.na(sch$catchment),
         "<br><i>Faith school, admits city-wide</i>",
         paste0("<br>Catchment: ", CATCH_LABELS[sch$boundary_catchment])))

# ---- Children per LSOA ----------------------------------------------
# Oi is the cohort-aged child estimate per zone; zones are LSOA x
# catchment, so an LSOA split by a boundary has to be summed back up.
children <- oi$zones %>%
  group_by(lsoa) %>%
  summarise(children = sum(Oi, na.rm = TRUE), .groups = "drop")

lsoa_pts <- lsoa %>%
  left_join(children, by = c("lsoa21cd" = "lsoa")) %>%
  filter(!is.na(children), children > 0) %>%
  st_point_on_surface() %>%
  suppressWarnings()
pt_xy <- st_coordinates(lsoa_pts)
lsoa_pts$lon <- pt_xy[, 1]; lsoa_pts$lat <- pt_xy[, 2]

# Radius on the square root of the count, so circle *area* is
# proportional to the number of children rather than the radius being -
# the latter exaggerates big areas by a factor of the count.
r_scale <- function(n) 3 + 17 * sqrt(n / max(n, na.rm = TRUE))

pal_catch <- colorFactor(unname(CATCH_COLOURS), names(CATCH_COLOURS))

m <- leaflet(width = "100%", height = 620) %>%
  add_basemap() %>%
  setView(lng = -0.10, lat = 50.84, zoom = 12) %>%
  addPolygons(
    data = catch, group = "Catchments",
    fillColor = ~pal_catch(catchment), fillOpacity = 0.13,
    color = ~pal_catch(catchment), weight = 2.5, opacity = 0.85,
    label = ~unname(CATCH_LABELS[catchment]),
    highlightOptions = highlightOptions(weight = 4, fillOpacity = 0.25,
                                        bringToFront = FALSE)) %>%
  addCircleMarkers(
    data = lsoa_pts, lng = ~lon, lat = ~lat, group = "Children",
    radius = ~r_scale(children),
    fillColor = "#1f4e79", fillOpacity = 0.35,
    color = "#1f4e79", weight = 1, opacity = 0.55,
    label = ~sprintf("%s: about %s children in the cohort",
                     lsoa21nm, round(children)))

m <- add_school_layer(m, sch, group = "Schools", popup = sch_popup)

# CoMArt, closed 2005
m <- m %>% addCircleMarkers(
  lng = COMART$lon, lat = COMART$lat, group = "CoMArt (closed 2005)",
  radius = 6, fillColor = "#b2182b", fillOpacity = 0.9,
  color = "white", weight = 2,
  popup = "<b>CoMArt</b><br>Closed 2005. Its closure is why the city moved to catchments with a lottery tie-break.",
  label = COMART$name)
ic <- logo_icon(COMART$logo)
if (!is.null(ic))
  m <- m %>% addMarkers(lng = COMART$lon, lat = COMART$lat, icon = ic,
                        group = "CoMArt (closed 2005)",
                        label = COMART$name)

legend_html <- paste0(
  '<div style="background:white;padding:8px 12px;border-radius:4px;',
  'box-shadow:0 1px 5px rgba(0,0,0,.3);font-family:sans-serif;font-size:12px;line-height:1.6">',
  '<div style="font-weight:bold;margin-bottom:4px">Catchment areas, 2025/26</div>',
  paste0('<div style="display:flex;align-items:center;gap:6px">',
         '<span style="display:inline-block;width:14px;height:14px;background:',
         unname(CATCH_COLOURS), ';border-radius:2px;flex-shrink:0"></span><span>',
         unname(CATCH_LABELS), '</span></div>', collapse = ''),
  '<div style="margin-top:6px;padding-top:6px;border-top:1px solid #ddd">',
  '<span style="display:inline-block;width:14px;height:14px;background:#1f4e79;',
  'opacity:.35;border-radius:50%;margin-right:6px"></span>Children per LSOA',
  ' <span style="color:#777">(area &prop; count)</span></div></div>')

m %>%
  addLayersControl(
    overlayGroups = c("Catchments", "Schools", "Children", "CoMArt (closed 2005)"),
    options = layersControlOptions(collapsed = FALSE)) %>%
  hideGroup("CoMArt (closed 2005)") %>%
  addControl(html = legend_html, position = "bottomright")
Figure 1: Schools, catchment boundaries and the cohort-aged child population. Use the control at the top right to turn each layer on and off.

Three things are worth noticing on that map before any analysis begins.

The catchments are not equivalent objects. Four of them contain a single school; two contain a pair. A child in the Varndean / Dorothy Stringer or Hove Park / Blatchington Mill catchment has two schools to name and a realistic chance of one of them. A child in the Longhill, Patcham, BACA or PACA catchment has one. That asymmetry does more work in this system than almost anything else, and section 5 returns to it.

The children are not spread evenly. The cohort is concentrated in the centre and west; the east of the city, which contains Longhill’s entire catchment, is thin. Longhill’s problem is visible on this map before any model is run.

Two schools sit outside the geography entirely. King’s and Cardinal Newman admit city-wide on faith criteria. Together they hold 540 places, about 21% of the city total. Any statement about what catchments do has to be read against that.

2.2 Where the children actually are

The circles on the map above are LSOA totals, and an LSOA is a blunt unit — about 1,500 people, drawn for administrative convenience rather than to describe a neighbourhood. Two finer views of the same population follow. They are shown side by side because they answer slightly different questions, and it is worth deciding which one this document should carry.

Show code
BW <- 400          # kernel bandwidth in metres - a walkable neighbourhood
GRID <- 320        # grid resolution

# A weighted kernel density: each child contributes one point, so the
# surface is the density of children rather than of postcodes. Counts are
# apportioned estimates and so fractional; rounding for replication
# changes the total by well under a percent.
reps <- round(pcd$children)
kx <- rep(pcd$easting,  reps)
ky <- rep(pcd$northing, reps)

bb <- st_bbox(city)
k <- MASS::kde2d(kx, ky, h = c(BW, BW), n = GRID,
                 lims = c(bb["xmin"] - 500, bb["xmax"] + 500,
                          bb["ymin"] - 500, bb["ymax"] + 500))

# kde2d integrates to 1 over square metres. Scale to children per
# hectare so the legend means something to a reader.
dens <- k$z * sum(reps) * 1e4

brks <- c(0, 2, 5, 10, 15, 22, 30, max(45, ceiling(max(dens))))
pal_d <- viridisLite::viridis(length(brks) - 1, option = "magma",
                              direction = -1, begin = 0.08)

# isoband wants z indexed [y, x]; kde2d returns [x, y].
bands <- isoband::isobands(x = k$x, y = k$y, z = t(dens),
                           levels_low = brks[-length(brks)],
                           levels_high = brks[-1])

band_sf <- st_sf(
  lo = brks[-length(brks)], hi = brks[-1],
  geometry = st_sfc(isoband::iso_to_sfg(bands), crs = 27700)) %>%
  st_make_valid() %>%
  st_intersection(city) %>%
  filter(!st_is_empty(geometry)) %>%
  # Simplify while still in metres. Contour bands off a 320 x 320 grid
  # carry far more vertices than a screen can show; 15 m is invisible at
  # any zoom this map offers and cuts the embedded geometry sharply.
  # This has to happen before the transform - in 4326 the tolerance
  # would be read as degrees and flatten the whole surface.
  st_simplify(dTolerance = 15, preserveTopology = TRUE) %>%
  st_transform(4326)

band_sf$col <- pal_d[seq_len(nrow(band_sf))]
band_sf$lab <- sprintf("%g - %g children per hectare", band_sf$lo, band_sf$hi)

leaflet(width = "100%", height = 600,
        options = leafletOptions(preferCanvas = TRUE)) %>%
  add_basemap() %>%
  addPolygons(data = band_sf, fillColor = ~col, fillOpacity = 0.72,
              color = NA, weight = 0, label = ~lab, group = "Children") %>%
  addPolygons(data = catch, fill = FALSE, color = "#333333",
              weight = 1.6, opacity = 0.7, group = "Catchments",
              label = ~unname(CATCH_LABELS[catchment])) %>%
  add_school_layer(sch, group = "Schools", popup = sch_popup) %>%
  addLayersControl(overlayGroups = c("Children", "Catchments", "Schools"),
                   options = layersControlOptions(collapsed = TRUE)) %>%
  addLegend(colors = band_sf$col, labels = band_sf$lab,
            title = "Children 0-18<br>per hectare",
            position = "bottomright", opacity = 0.8)
Figure 2: Children aged 0-18 per hectare, smoothed from postcode-level estimates with a 400 m kernel and clipped to the built-up area. Census 2021 household composition apportioned to postcode centroids.
Show code
pcd_sf <- pcd %>%
  st_as_sf(coords = c("easting", "northing"), crs = 27700) %>%
  st_transform(4326) %>%
  filter(!is.na(idaci_decile))

pal_idaci <- colorFactor("RdYlBu", domain = sort(unique(pcd_sf$idaci_decile)))

# Radius on the square root of the count so circle area is proportional
# to children. Scaled well down from the LSOA circles in 2.1: there are
# 4,229 postcodes against 179 LSOAs, so the same per-circle size would
# cover the city in a single wash of ink. The median postcode draws at
# about 3.5 px and the largest at about 9.
#
# preferCanvas matters here rather than being a nicety. Leaflet's
# default renderer gives every marker its own SVG element, and four
# thousand of them is enough to hang or crash a browser tab. On canvas
# they are drawn into a single element and the map stays responsive.
leaflet(width = "100%", height = 600,
        options = leafletOptions(preferCanvas = TRUE)) %>%
  add_basemap() %>%
  addPolygons(data = catch, fill = FALSE, color = "#333333",
              weight = 1.6, opacity = 0.7, group = "Catchments",
              label = ~unname(CATCH_LABELS[catchment])) %>%
  addCircleMarkers(
    data = pcd_sf, group = "Postcodes",
    radius = ~0.5 + sqrt(children),
    fillColor = ~pal_idaci(idaci_decile), fillOpacity = 0.6,
    stroke = FALSE,
    label = ~sprintf("%s | %.0f children 0-18 | IDACI decile %s",
                     postcode, children, idaci_decile)) %>%
  add_school_layer(sch, group = "Schools", popup = sch_popup) %>%
  addLayersControl(overlayGroups = c("Postcodes", "Catchments", "Schools"),
                   options = layersControlOptions(collapsed = TRUE)) %>%
  addLegend(pal = pal_idaci, values = sort(unique(pcd_sf$idaci_decile)),
            title = "IDACI decile<br><span style='font-weight:normal'>1 = most deprived</span>",
            position = "bottomright", opacity = 0.9)
Figure 3: Every residential postcode in the city, sized by the number of children aged 0-18 and coloured by income deprivation affecting children. Decile 1 is the most deprived tenth of neighbourhoods nationally.

The surface and the dots are built from the same 4,229 postcodes and the same 47,850 children, and they show the same city — the smoothed surface tracks the number of children within 500 metres of each postcode at a rank correlation of 0.78. What differs is what each one makes easy to see.

The surface shows where children are concentrated: a dense central and western band, thinning sharply east of the marina. It answers “where is the demand?” and it does not pretend to precision it lacks.

The postcode map shows the same concentration but adds the social gradient, and it exposes something the surface cannot — that deprivation in this city is fine-grained. Deprived and comfortable postcodes sit within a few hundred metres of each other, particularly in the centre. That texture matters later: section 4 shows that poor transport access and child poverty coincide, and section 5 that catchment boundaries drawn at this scale will always cut through mixed ground rather than separating rich areas from poor ones.

NoteTwo limits on both maps

These are Census 2021 output-area household counts apportioned to postcode centroids, not a register of children. They are estimates, and at postcode level they are noisy estimates — reliable in aggregate, not for any individual postcode.

They also cover Brighton & Hove only. The Peacehaven and Telscombe area, which contributes about 215 cohort-aged children to the study area, is absent from both. The LSOA-level figures used everywhere else in this document do include it.

2.3 What the attainment data actually says

The debate about Brighton’s schools is usually conducted in the currency of headline attainment — Attainment 8, league table position, “good” and “bad” schools. That currency is close to worthless for the question the council actually faces, and it is worth being precise about why, because the usual explanation is also wrong.

The common account is that attainment tracks deprivation. It does, but weakly, and stopping there points the city at the wrong lever.

The specification below follows the model in How to Pull the Right Lever: Attainment 8 and the rate variables are logged, so their coefficients are elasticities, while prior attainment enters as a mean KS2 score because it is a score and not a rate.

Across 3,188 mainstream secondaries in England, the proportion of disadvantaged pupils explains 37% of the variation in Attainment 8. Prior attainment explains 56%. And the single strongest of the three is neither: absence alone explains 56%. All three together reach 68%.

The elasticities make the point more sharply than the R² values do. In the combined model a one per cent rise in absence is associated with a 0.29 per cent fall in Attainment 8; a one per cent rise in the disadvantaged share with a fall of just 0.04 per cent — roughly 8 times smaller.

That ordering is the finding. Who is poor matters least of the three. What children could already do at eleven matters more. Whether they are in the room matters most.

Show code
# Attainment 8 is on a log axis in every panel, matching the model. The
# x axis is logged for the two rates and left linear for the KS2 score,
# so each panel is drawn on the scale its term is actually fitted on.
panel <- function(xvar, xlab, ttl, logx) {
  d <- nat %>% filter(!is.na(.data[[xvar]]))
  b <- bh_latest %>% filter(!is.na(.data[[xvar]]))
  if (logx) { d <- d %>% filter(.data[[xvar]] > 0); b <- b %>% filter(.data[[xvar]] > 0) }
  p <- ggplot(d, aes(.data[[xvar]], ATT8SCR)) +
    geom_point(colour = "grey80", size = 0.9, alpha = 0.5) +
    geom_smooth(method = "lm", formula = y ~ x, se = FALSE,
                colour = "grey30", linewidth = 0.7, linetype = "22") +
    geom_point(data = b, colour = "#b2182b", size = 2.4) +
    ggrepel::geom_text_repel(
      data = b %>% filter(SCHNAME %in% c("Longhill High School", "King's School")),
      aes(label = SCHNAME), size = 2.9, colour = "#7f0f22",
      min.segment.length = 0, segment.size = 0.3, seed = 1) +
    scale_y_log10(breaks = c(10, 20, 30, 40, 50, 60, 70)) +
    labs(x = xlab, y = "Attainment 8", subtitle = ttl) +
    theme_bh(11)
  if (logx)
    p <- p + scale_x_log10(labels = label_percent(scale = 1),
                           breaks = c(1, 2, 5, 10, 20, 50))
  p
}

(panel("PTFSM6CLA1A", "Disadvantaged pupils",
       sprintf("Deprivation, logged (R² = %.2f)", summary(m_fsm)$r.squared), TRUE) |
 panel("ks2_c", "Mean KS2 prior attainment, centred",
       sprintf("Prior attainment, linear (R² = %.2f)", summary(m_prior)$r.squared), FALSE) |
 panel("PERCTOT", "Overall absence rate",
       sprintf("Absence, logged (R² = %.2f)", summary(m_abs1)$r.squared), TRUE)) +
  plot_annotation(
    title = "Attainment is a description of intake, and of who turns up",
    subtitle = sprintf("Mainstream secondaries in England, %s. Attainment 8 log scaled throughout; rates logged, the KS2 score linear. Red points are Brighton & Hove.",
                       perf$latest),
    caption = "Source: DfE performance tables. Specification follows How to Pull the Right Lever.",
    theme = theme_bh())
Figure 4: Attainment 8 against two features of intake, every mainstream secondary in England in the latest year, with Brighton & Hove schools highlighted. Prior attainment and absence both order schools far more sharply than deprivation does.

Two consequences follow, and they point in the same direction.

A headline score is mostly a description of who walks through the door. Once prior attainment and deprivation are accounted for, 8 of the 10 Brighton & Hove schools score above what the national relationship predicts for their intake. On the measure that isolates the school’s own contribution, this is not a city with a school quality problem.

The lever that moves is attendance, not social mix. Absence and deprivation travel together, and absence is the mechanism through which much of the deprivation effect actually operates — which is why redistributing disadvantaged children between schools is a far weaker instrument than it is usually assumed to be.

ImportantTwo cautions

Absence is partly an outcome as well as a cause. A child who is disengaged attends less and attains less, so a single-equation fit overstates how much of that 68% is absence doing the causal work. The Lever report models this properly — with school, year, region and inspection-rating effects, and a first stage that models absence itself — rather than reading it off a scatterplot. These three panels are an illustration of that model’s premise, not a substitute for it.

These are single-year, single-equation fits. The full model is fitted on a multi-year panel with random effects and a wider set of school characteristics, including English as an additional language, teacher retention and admissions policy. The ordering of the three terms here matches it; the individual R² values will not, and none of them should be quoted as a causal effect.

TipThe detail behind this is published separately

The contextualised analysis — value-added modelling across every local authority in England, and what it says about where Brighton’s schools really sit — is set out in How to Pull the Right Lever. Its central findings are that Brighton’s schools perform well once intake is accounted for, and that absence, not social mixing, is the largest single lever available to the city. This document takes those findings as given and turns to the spatial and demographic questions the Lever report does not address.

Show code
flip <- oi$attract %>%
  filter(!is.na(att8), !is.na(va_lever), name != "Peacehaven Community School") %>%
  mutate(rank_att8 = rank(-att8), rank_va = rank(-va_lever)) %>%
  select(name, rank_att8, rank_va) %>%
  pivot_longer(starts_with("rank_"), names_to = "measure", values_to = "rank") %>%
  mutate(measure = factor(measure, c("rank_att8", "rank_va"),
                          c("Attainment 8\n(headline)", "Value added\n(Lever report)")))

moved <- flip %>%
  pivot_wider(names_from = measure, values_from = rank) %>%
  rename(att8 = 2, va = 3) %>%
  mutate(shift = att8 - va)

ggplot(flip, aes(measure, rank, group = name)) +
  geom_line(aes(colour = name), linewidth = 1.1, alpha = 0.85, show.legend = FALSE) +
  geom_point(aes(colour = name), size = 3, show.legend = FALSE) +
  ggrepel::geom_text_repel(
    data = flip %>% filter(measure == levels(flip$measure)[1]),
    aes(label = name), hjust = 1, nudge_x = -0.08, size = 3,
    direction = "y", segment.size = 0.2, seed = 1) +
  ggrepel::geom_text_repel(
    data = flip %>% filter(measure == levels(flip$measure)[2]),
    aes(label = name), hjust = 0, nudge_x = 0.08, size = 3,
    direction = "y", segment.size = 0.2, seed = 1) +
  scale_y_reverse(breaks = 1:10) +
  scale_x_discrete(expand = expansion(add = c(0.85, 0.85))) +
  labs(x = NULL, y = "Rank (1 = highest)",
       title = "Which schools look good depends on which measure you use",
       subtitle = "Ranked by headline attainment, and by contribution to progress once intake is accounted for",
       caption = "Sources: DfE performance tables; How to Pull the Right Lever.") +
  theme_bh() +
  theme(panel.grid.major.x = element_blank())
Figure 5: The same ten schools ranked two ways: by headline Attainment 8, and by the value-added measure from the Lever report. Lines crossing means the two measures disagree about which schools are doing well.

The lines cross a great deal. Brighton Aldridge Community Academy rises 6 places when you move from the headline measure to the value-added one; Patcham High School and Cardinal Newman Catholic School falls 4. 3 of the ten schools move by three places or more.

The schools that fall are not bad schools, and the schools that rise are not suddenly good ones. The two measures are answering different questions: one asks what pupils at this school achieved, the other asks how much of that the school is responsible for. Families reading the published headline are answering the first question while believing they are answering the second — and section 5 shows that this has a measurable effect on where demand actually goes.

2.4 How much can a school change at all?

There is a prior question underneath all of this, and it is the one that bears most directly on what the council can achieve by moving children between schools.

Since Coleman et al. (1966), study after study has found that the share of variation in pupils’ outcomes attributable to differences between schools — as opposed to differences in the pupils they admit and the circumstances those pupils bring with them — is somewhere in the order of 8 to 15 per cent. The landmark English study settled on about a tenth (Smith and Tomlinson 1989), and the finding has held up across countries and phases (Teddlie and Reynolds 2000).

The multilevel model behind How to Pull the Right Lever decomposes that variance directly, and lands in the same place.

Show code
sed <- bh_data("school_effect_decomp.rds")
lev <- bh_data("school_leverage.rds")
LV  <- lev$leverage

GRP_COL <- c("Inherited (exogenous)"      = "#49a0c4",
             "Absence: structural"        = "#7FB3D5",
             "Absence: school-reachable"  = "#F2A03D",
             "Persistent school effect"   = "#6A4C93",
             "Workforce"                  = "#ED357D",
             "Place / noise"              = "#BBBBBB")

ABS_LAB <- "Absence (part inherited, part school-reachable)"
EXO_LAB <- "Inherited: cohort, prior attainment, neighbourhood"

# Absence is published inside the exogenous block, but it is the one
# term that is partly within a school's control. Split it out as its own
# row. The published totals are kept; the share of the exogenous block
# that is absence is taken from the fitted model, and the split of that
# absence into structural and school-reachable from the stage-1 model.
prep <- function(d, who_lab) {
  r        <- LV[LV$who == who_lab, ]
  abs_frac <- r$absence / r$exogenous
  exo_tot  <- d[["Share of total %"]][grepl("^Exogenous", d$Component)]
  abs_tot  <- exo_tot * abs_frac

  bind_rows(
    d %>%
      filter(!grepl("^Exogenous", Component)) %>%
      transmute(Component, share = `Share of total %`,
                grp_type = case_when(
                  grepl("^Endogenous", Component) ~ "Workforce",
                  Component == "School (persistent, unexplained)" ~ "Persistent school effect",
                  TRUE ~ "Place / noise")),
    tibble(Component = EXO_LAB, share = exo_tot - abs_tot,
           grp_type = "Inherited (exogenous)"),
    tibble(Component = ABS_LAB,
           share = c(abs_tot * lev$struct_share, abs_tot * lev$school_share),
           grp_type = c("Absence: structural", "Absence: school-reachable"))
  ) %>%
    mutate(who = who_lab,
           Component = sub("^Endogenous, within-school \\(workforce\\)$",
                           "Workforce (the part a school hires)", Component))
}

decomp <- bind_rows(prep(sed$decomp_all, "All pupils"),
                    prep(sed$decomp_dis, "Disadvantaged pupils"),
                    prep(sed$decomp_non, "Non-disadvantaged pupils")) %>%
  mutate(who = factor(who, c("All pupils", "Disadvantaged pupils",
                             "Non-disadvantaged pupils")),
         grp_type = factor(grp_type, levels = names(GRP_COL)))

# Order rows by the all-pupil total for each component, so the three
# panels read in the same order and can be compared row by row.
ord <- decomp %>%
  filter(who == "All pupils") %>%
  group_by(Component) %>% summarise(t = sum(share), .groups = "drop") %>%
  arrange(t) %>% pull(Component)
decomp <- decomp %>% mutate(Component = factor(Component, ord))

# One label per row, placed at the row total rather than per segment, so
# the split absence bar reads as a single quantity.
tot_lab <- decomp %>%
  group_by(who, Component) %>% summarise(share = sum(share), .groups = "drop")

ggplot(decomp, aes(share, Component, fill = grp_type)) +
  geom_col(width = 0.72) +
  geom_text(data = tot_lab, inherit.aes = FALSE, hjust = -0.15, size = 3,
            mapping = aes(x = share, y = Component,
                          label = sprintf("%.1f%%", share))) +
  facet_wrap(~ who, ncol = 1) +
  scale_fill_manual(values = GRP_COL, name = NULL) +
  scale_x_continuous(limits = c(0, 52), expand = expansion(mult = c(0, 0.02))) +
  guides(fill = guide_legend(nrow = 2)) +
  labs(x = "Share of total outcome variance (%)", y = NULL,
       title = "Most of what separates schools' results arrives with the children",
       subtitle = "Absence shown separately: it is the one term that is partly inherited and partly within a school's reach",
       caption = "Sources: How to Pull the Right Lever, school effect decomposition; RPE paper two-stage absence model.") +
  theme_bh(11) +
  theme(legend.position = "bottom", panel.grid.major.y = element_blank(),
        axis.text.y = element_text(size = 8),
        strip.text = element_text(face = "bold", hjust = 0))
Figure 6: Where the variance in school-level Attainment 8 sits, decomposed from the multilevel model, for all pupils and for the two pupil groups separately. Absence is shown as its own row rather than buried in the inherited block, split into the structural share a first-stage model predicts from intake and area, and the school-level residual it does not. The persistent school effect is an upper bound: it bundles genuine school ethos and management with anything unmeasured about the catchment, such as cultural capital or parental motivation.

For all pupils, the part a school directly controls through the staff it hires accounts for about 2.5% of the variance. What it inherits with the intake — cohort composition, prior attainment, neighbourhood — accounts for roughly 42%. Even the persistent school effect, at 15.6%, is an upper bound: it is what is left once everything measured is accounted for, so it bundles genuine school ethos and leadership together with anything unmeasured about the catchment — cultural capital, parental motivation, and the rest.

Absence has its own row because it is the one term that will not sit in either camp. The published decomposition files it with the inherited block, but a school’s attendance is partly a fact about its intake and partly a fact about what it does. The next section takes that apart, and it turns out to matter more than anything else on the chart.

For disadvantaged pupils — the group any mixing policy is aimed at — the workforce term falls to 1.1%, against 56.8% inherited, with a persistent school effect of 10.5%. The lever gets smaller, not larger, precisely where the policy is pointed. For non-disadvantaged pupils it is 3.8% against 60.6% — so the pattern is not an artefact of one group, and the school-controllable share is smallest for the children the policy is meant to help.

2.5 Absence is the term that moves

The published decomposition files absence inside the exogenous block — “cohort, prior attainment, absence, neighbourhood” — as though a school simply receives its attendance rate. The two-stage work in the RPE paper shows that it does not. A first-stage model of school absence on intake, area and segregation explains only about half of its variation; the rest is a residual that pastoral systems, attendance officers, family liaison and persistent-absentee follow-up act on directly. That is why it is drawn as its own split row above, and it is worth being precise about the size of each part.

Across 12,199 school-years, that first stage accounts for 53% of the variation in absence. The other 47% is school-level residual. Absence is not one thing: roughly half of it is structural and outside a school’s reach, and roughly half is not.

That changes the arithmetic, because absence is the largest single term in the exogenous block — 20.2% of total variance for all pupils, and 24.8% for disadvantaged pupils, where it is the biggest component of all.

Show code
LV %>%
  transmute(who = factor(who, LV$who),
            `Workforce` = workforce,
            `Absence, school-reachable` = absence_school,
            `Absence, structural` = absence_structural,
            `Other inherited` = exogenous - absence) %>%
  pivot_longer(-who, names_to = "part", values_to = "pct") %>%
  mutate(part = factor(part, c("Workforce", "Absence, school-reachable",
                               "Absence, structural", "Other inherited"))) %>%
  ggplot(aes(pct, fct_rev(who), fill = part)) +
  geom_col(width = 0.62) +
  geom_text(aes(label = ifelse(pct >= 3, sprintf("%.0f%%", pct), "")),
            position = position_stack(vjust = 0.5), size = 3.1,
            colour = "white", fontface = "bold") +
  scale_fill_manual(values = c("Workforce" = "#ED357D",
                               "Absence, school-reachable" = "#F2A03D",
                               "Absence, structural" = "#7FB3D5",
                               "Other inherited" = "#49a0c4"), name = NULL) +
  guides(fill = guide_legend(nrow = 2)) +
  labs(x = "Share of total outcome variance (%)", y = NULL,
       title = "What a school can actually act on is mostly attendance",
       subtitle = "Fixed-effect variance only; the random-effect components are omitted here",
       caption = "Sources: How to Pull the Right Lever; RPE paper two-stage absence model.") +
  theme_bh(11) +
  theme(legend.position = "bottom", panel.grid.major.y = element_blank())
Figure 7: The same variance, re-cut by what a school can actually act on. The absence term is split into the structural share the first-stage model predicts and the school-level residual it does not.

Adding the school-controllable half of absence to the workforce term gives a school-reachable share of 12.0% for all pupils, 12.5% for disadvantaged pupils and 12.2% for non-disadvantaged — strikingly consistent, and sitting squarely inside the 8–15 per cent that sixty years of school-effectiveness research has settled on.

NoteA small discrepancy worth naming

The workforce figures in this section are recomputed from the fitted models rather than read off the published table, so that the parts sum to the totals quoted beside them. They come out slightly lower than the published version — 2.5% against 3.3% for all pupils — because the cached table and the model objects available here are not from quite the same fit. The difference is under a percentage point and changes nothing in the argument, but the two numbers are not identical and it would be worse to present them as if they were.

So the earlier conclusion needs restating, more precisely and more usefully:

ImportantThe corrected version

A school can reach about 12% of the variance in its results — not the 3.3% the workforce term alone suggests. But almost all of that leverage runs through attendance, not through who is sitting in the classroom.

For disadvantaged pupils the point is sharper still. The workforce term is 0.8%; the school-reachable absence term is 11.7% — 14 times larger. The lever that works for the children the policy is aimed at is attendance, and it is not an admissions lever.

Moving children between schools does not touch either term. It changes which building a child attends without changing their attendance, and without changing the intake characteristics that account for the rest. That is the case against expecting much from redistribution — and, equally, the case for expecting a great deal from attendance work.

NoteDoes the shared variance change this?

It is a fair challenge. The two fixed blocks are not independent — schools with difficult intakes also tend to have less stable workforces — so the variance they share has to be allocated somehow, and the answer depends on how.

The decomposition above credits each block with its covariance against the total. An alternative is to split the shared part evenly, as a Shapley decomposition does. On the fitted models the two blocks correlate at about +0.3 to +0.4, and the choice is not immaterial: the workforce share moves from 2.5% to 7.5% for all pupils, from 0.8% to 3.1% for disadvantaged pupils, and from 3.2% to 8.1% for non-disadvantaged pupils — roughly a threefold difference.

It does not, however, change the conclusion. Under the more generous allocation the workforce share is still about eight times smaller than the inherited share for all pupils, and about seventeen times smaller for disadvantaged pupils. The random-effect components — the persistent school effect among them — are untouched either way, because variance components in a multilevel model are orthogonal by construction and have no shared part to argue over.

The figures quoted here are the published ones, on the covariance split. Read the workforce term as at most a few per cent rather than as a precise quantity.

Show code
tibble(term = LAB[three], share = shap_share) %>%
  ggplot(aes(reorder(term, share), share)) +
  geom_col(fill = "#2166ac", width = 0.6) +
  geom_text(aes(label = fmt_pct(share, 0)), hjust = -0.18, size = 3.4) +
  coord_flip() +
  scale_y_continuous(limits = c(0, max(shap_share) * 1.22)) +
  labs(x = NULL, y = "Share of explained variance (%)",
       title = "And within that inherited share, how it divides",
       subtitle = sprintf("Shapley decomposition of school-level Attainment 8 (R² = %.2f)", r2_all),
       caption = "Source: DfE performance tables.") +
  theme_bh(11) +
  theme(panel.grid.major.y = element_blank())
Figure 8: How the explained variance in school-level Attainment 8 divides between the three main intake measures, by Shapley decomposition.

Two things follow, and together they are the most important finding in this section for the decisions in front of the council.

Moving children between schools operates on the small term. Redistributing pupils changes which school a child attends. It does not change the intake characteristics and social circumstances that account for 62.2% of the variance, and it does not touch the workforce that accounts for 3.3%. A policy of social mixing through admissions is therefore working on the smaller term by construction — not because mixing is undesirable, but because the arithmetic limits what it can deliver.

The next section qualifies this in an important way. One large item inside that 62.2% is not inherited at all.

And the measured drivers cannot be cleanly separated anyway. Of the 68% of school-level variation the three measures jointly explain, 74% is variance they share rather than variance any one of them owns. Disadvantage, absence and prior attainment correlate at -0.68, -0.65 and 0.53 because they are substantially the same phenomenon seen three ways. Any policy that targets one of them in isolation — and an admissions policy targets disadvantage in isolation — is pulling on a rope attached to the other two.

ImportantWhat this does and does not say

It does not say schools do not matter. A persistent school effect of 15.6% across three and a half thousand secondary schools is a great deal, and section 2.3 showed Brighton’s schools mostly performing above what their intakes predict. Nor does it say that segregation between schools is acceptable; there are fairness arguments for mixed intakes that have nothing to do with attainment.

What it says is narrower and harder to argue with: the council should not expect a redistribution policy to move attainment much, because the mechanism it operates through is the smallest of the terms available. The instruments that reach the larger terms — attendance above all — are not admissions instruments at all.

2.6 A school’s disadvantaged results are a noisy signal

One row on that chart is easy to skip past and should not be. For disadvantaged pupils, transient cohort-year noise is the second-largest component of all — larger than the persistent school effect, larger than absence, larger than anything except what the children bring with them.

Show code
noise %>%
  pivot_longer(-who, names_to = "part", values_to = "pts") %>%
  mutate(part = factor(part, c("persistent", "transient"),
                       c("Persistent difference between schools",
                         "Year-to-year noise within a school"))) %>%
  ggplot(aes(pts, fct_rev(who), fill = part)) +
  geom_col(position = position_dodge(width = 0.72), width = 0.66) +
  geom_text(aes(label = sprintf("%.1f", pts)),
            position = position_dodge(width = 0.72), hjust = -0.25, size = 3.2) +
  scale_fill_manual(values = c("Persistent difference between schools" = "#6A4C93",
                               "Year-to-year noise within a school" = "#BBBBBB"),
                    name = NULL) +
  scale_x_continuous(limits = c(0, max(noise$transient) * 1.25),
                     expand = expansion(mult = c(0, 0.02))) +
  guides(fill = guide_legend(nrow = 2)) +
  labs(x = "Attainment 8 points (standard deviation)", y = NULL,
       title = "For disadvantaged pupils, the noise is bigger than the signal",
       caption = "Source: How to Pull the Right Lever, school effect decomposition.") +
  theme_bh(11) +
  theme(legend.position = "bottom", panel.grid.major.y = element_blank())
Figure 9: The persistent difference between schools against the year-to-year wobble within them, in Attainment 8 points. For disadvantaged pupils the wobble is the larger of the two.

For all pupils the school signal wins: the persistent difference between schools is 3.4 Attainment 8 points against 2.7 points of year-to-year noise. For disadvantaged pupils it reverses — 2.7 points of persistent difference against 3.6 points of noise.

The reason is arithmetic rather than mysterious. A secondary school’s disadvantaged cohort is small — often forty or fifty pupils — and small numbers move about. A handful of children having a bad year, or a good one, shifts the published figure by more than any real difference in what the school does.

The consequence is uncomfortable and worth stating plainly. Two schools doing genuinely equally well by their disadvantaged pupils will differ by around 5 Attainment 8 points in a given year for no reason at all. That is larger than most of the gaps that get argued about. A single year of disadvantaged results is not evidence about a school; it is one draw from a distribution.

ImportantWhat follows for how the city reads its own data

Never judge a school on one year of disadvantaged results. Three or more years, averaged, or the comparison is mostly noise. This applies to league tables, to consultation documents, and to the case made for or against any individual school.

Do not expect to detect an admissions policy’s effect this way either. If redistribution moves disadvantaged attainment by a point or two — which section 2.4 suggests is optimistic — that signal sits well inside 5 points of year-to-year variation. The policy could work exactly as intended and be invisible for years; it could also fail and appear to succeed. Evaluating it will need attendance and intake measures, not headline outcomes.

This bears directly on the free school meals review. The FSM admissions priority took effect for entry in September 2023. Those children entered Year 7 that autumn and will not sit GCSEs until summer 2028. So a review conducted now is not working with thin outcome evidence — it is working with none: not one cohort admitted under the policy has yet reached an examination. Even in 2028 there will be a single cohort, and a single cohort of disadvantaged pupils in one catchment carries the 3.6-point year-to-year variation described above.

That is not an argument against reviewing the policy. Admission patterns, intake composition by catchment, and the operation of the criteria itself can all be examined now, and section 5 does some of that. It is an argument against reviewing it on attainment, or against any conclusion of the form “results have or have not improved since the policy came in”. No honest analysis can support such a claim from the data that exists, in either direction.

This compounds the problem in section 2.3. Families are choosing on a measure that is largely a description of intake, and that measure is noisiest precisely for the pupils whose schools are most often criticised on it.

3 Demographic futures

The single most important fact about this system is that it is shrinking, and that the shrinkage is already locked in. The children who will enter Year 7 in 2033 have already started primary school. Their number is not a forecast in any meaningful sense — it is a count, adjusted for a transfer rate that has been remarkably stable for a decade.

3.1 What has already happened

Year 7 offers across the city peaked at 2,509 in 2019. In 2026 the figure was 2,250 — a fall of 10.3% from the peak. That decline is not a projection. It has happened.

Show code
bind_rows(
  y7  %>% transmute(year, offers = y7_offers, phase = "Year 7 offers"),
  rec %>% transmute(year, offers = rec_offers, phase = "Reception offers")) %>%
  ggplot(aes(year, offers, colour = phase)) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 2) +
  scale_colour_manual(values = c("Reception offers" = "#2c7fb8",
                                 "Year 7 offers" = "#b2182b"), name = NULL) +
  scale_y_continuous(labels = label_comma(), limits = c(0, NA)) +
  labs(x = NULL, y = "Offers made",
       title = "Both phases are past their peak",
       subtitle = "Reception offers turn down first, and Year 7 follows seven years later",
       caption = "Source: BHCC published allocation factsheets.") +
  theme_bh()
Figure 10: Reception and Year 7 offers made across Brighton & Hove. The reception line leads the Year 7 line by seven years, so it is a preview of secondary demand rather than a separate story.

3.2 The cohort already in school

The transfer rate from a reception cohort to the Year 7 cohort seven years later has been strikingly stable. Across the six cohorts where both ends are observable, 88.4% of reception offers reappear as Year 7 offers, with a standard deviation of just 1.6%. The city loses about one child in nine between ages four and eleven — to independent schools, to moves out of the area, and to schools across the boundary.

Applying that rate to reception cohorts already in school gives a projection that depends on almost no assumptions:

Show code
ggplot() +
  geom_ribbon(data = proj, aes(entry_year, ymin = lo, ymax = hi),
              fill = "#b2182b", alpha = 0.18) +
  geom_line(data = proj, aes(entry_year, central),
            colour = "#b2182b", linewidth = 1.1, linetype = "22") +
  geom_point(data = proj, aes(entry_year, central), colour = "#b2182b", size = 2) +
  geom_line(data = y7, aes(year, y7_offers), colour = "grey25", linewidth = 1.1) +
  geom_point(data = y7, aes(year, y7_offers), colour = "grey25", size = 2) +
  annotate("text", x = 2016, y = 2480, label = "Observed", colour = "grey25",
           hjust = 0, fontface = "bold", size = 3.6) +
  annotate("text", x = 2029.5, y = 2180, label = "Projected from\nchildren already in school",
           colour = "#b2182b", hjust = 0, size = 3.4, lineheight = 0.95) +
  scale_y_continuous(labels = label_comma(), limits = c(0, NA)) +
  labs(x = NULL, y = "Year 7 offers",
       title = sprintf("A further %s fall is already in the primary schools",
                       fmt_pct(abs(drop_pct), 0)),
       subtitle = sprintf("From %s in %s to about %s by %s",
                          fmt_n(now$y7_offers), now$year,
                          fmt_n(last_proj$central), last_proj$entry_year),
       caption = "Sources: BHCC allocation factsheets; transfer rate estimated from six observed cohorts.") +
  theme_bh()
Figure 11: Year 7 demand projected from reception cohorts already in school. The band is the range implied by the observed year-to-year variation in the transfer rate.

By 2033 the city should expect about 1,750 Year 7 children, against 2,250 today — a further fall of 22%. Against a current published admission capacity of about 2,515 places, that is roughly 765 surplus places — the equivalent of two and a half average-sized secondary schools standing empty.

3.3 Where the council’s forecast differs

The council publishes its own catchment-level forecasts. They are consistently higher than what the primary cohorts support, and the gap widens with distance.

Show code
rc$comparison %>%
  mutate(gap = ons_demand - reception_based) %>%
  ggplot(aes(entry_year, gap)) +
  geom_col(fill = "#7b3294", alpha = 0.85, width = 0.65) +
  geom_hline(yintercept = 0, colour = "grey30") +
  geom_text(aes(label = sprintf("%+.0f", gap),
                vjust = ifelse(gap >= 0, -0.4, 1.3)), size = 3.2) +
  scale_y_continuous(expand = expansion(mult = c(0.15, 0.18))) +
  labs(x = "Year of Year 7 entry", y = "Council forecast minus cohort projection",
       title = "The council expects more children than the primary registers hold",
       subtitle = "Difference in Year 7 places, city-wide",
       caption = "Sources: BHCC published forecasts; reception-based projection as above.") +
  theme_bh()
Figure 12: The council’s own demand forecast against a projection built from reception cohorts already in school. Positive values mean the council expects more children than the primary registers contain.

The city-wide figure hides a good deal. Broken down by catchment, and set against the council’s own two successive forecasts, the picture is less tidy.

Show code
# Four series on one set of axes. The council labels its catchments
# differently from the model tables, so everything is converted to model
# keys first - see as_model_catchment() in R/00_core.R.
CATCH_DISPLAY <- c(CATCH_LABELS[unname(CATCH_FROM_MODEL)], "Religious schools")
names(CATCH_DISPLAY) <- c(names(CATCH_FROM_MODEL), "Religious schools")

council25 <- rc$council %>%
  transmute(catchment = as_model_catchment(CatchmentGroup),
            entry_year, offers = council, PAN,
            series = "Council forecast (Oct-25)")

council24 <- bh_data("council_forecast_oct24.csv") %>%
  transmute(catchment = as_model_catchment(CatchmentGroup),
            entry_year, offers = council_oct24,
            series = "Council forecast (Oct-24, superseded)")

series <- bind_rows(
  rc$observed_by_catchment %>%
    transmute(catchment, entry_year, offers = y7_offers,
              series = "Actual offers"),
  rc$by_catchment %>%
    transmute(catchment, entry_year, offers = projected,
              series = "Our projection (from primary cohorts)"),
  council25 %>% select(-PAN),
  council24) %>%
  filter(!is.na(catchment), entry_year >= 2016) %>%
  mutate(panel = factor(unname(CATCH_DISPLAY[catchment]),
                        unname(CATCH_DISPLAY)),
         series = factor(series, c(
           "Actual offers", "Our projection (from primary cohorts)",
           "Council forecast (Oct-25)",
           "Council forecast (Oct-24, superseded)")))

stopifnot(!any(is.na(series$panel)))

pan_lines <- council25 %>%
  distinct(catchment, PAN) %>%
  mutate(panel = factor(unname(CATCH_DISPLAY[catchment]), unname(CATCH_DISPLAY)))

ggplot(series, aes(entry_year, offers, colour = series)) +
  geom_hline(data = pan_lines, aes(yintercept = PAN),
             linetype = "dotted", colour = "grey30", linewidth = 0.45) +
  geom_line(linewidth = 0.75) +
  geom_point(size = 1.2) +
  facet_wrap(~ panel, ncol = 2, scales = "free_y") +
  scale_colour_manual(values = c(
    "Actual offers"                          = "#1f78b4",
    "Our projection (from primary cohorts)"  = "#33a02c",
    "Council forecast (Oct-25)"              = "#e31a1c",
    "Council forecast (Oct-24, superseded)"  = "#fb9a99"), name = NULL) +
  scale_y_continuous(limits = c(0, NA)) +
  guides(colour = guide_legend(nrow = 2)) +
  labs(x = NULL, y = "Year 7 offers",
       title = "Every catchment, four views of the same future",
       subtitle = "Dotted line is the catchment admission number",
       caption = "Sources: BHCC allocation factsheets; BHCC forecasts, Appendices 6 and 7.") +
  theme_bh(11) +
  theme(legend.position = "top", strip.text = element_text(face = "bold"),
        axis.text.x = element_text(angle = 45, hjust = 1))
Figure 13: Year 7 offers for each catchment: what has actually happened, our projection from primary cohorts already in school, and the council’s two most recent forecasts of the same years. Dotted line is the catchment’s admission number.

Two things stand out.

Our projection sits below the council’s in 4 of the 6 catchments — the city-wide gap of the previous chart, distributed. The closest agreement is in Patcham, where the two differ by under a place a year on average.

And the council’s own view has moved a long way in a single year. Between the Oct-24 and Oct-25 forecasts, for the years both cover, the figure for Varndean / Dorothy Stringer moved by an average of -18 places a year, with a largest single-year revision of 30.

The direction is the part worth noting. Every one of the six catchments was revised downward, none upward. That is not forecasting noise; it is a consistent correction in one direction, and it points the same way as the projection from primary cohorts. The council’s own figures are moving toward the lower number — they have not arrived at it yet, and decisions with twenty-year consequences are being taken against a quantity still in motion.

3.4 The forecast is not wrong in total. It is wrong by place

The comparison above sets one projection against another, and a reader is entitled to ask why either should be believed. For 2026 that question can be settled, because the offers have been made. Both the council’s forecast and our projection can be checked against what happened.

Show code
err26 <- rc$observed_by_catchment %>%
  filter(entry_year == 2026) %>%
  select(catchment, actual = y7_offers) %>%
  inner_join(council25 %>% filter(entry_year == 2026) %>%
               select(catchment, PAN, council = offers), by = "catchment") %>%
  left_join(rc$by_catchment %>% filter(entry_year == 2026) %>%
              select(catchment, ours = projected), by = "catchment") %>%
  mutate(council_err = council - actual, ours_err = round(ours - actual)) %>%
  arrange(desc(council_err))

err26 %>%
  transmute(Catchment = unname(CATCH_DISPLAY[catchment]),
            PAN, Actual = actual,
            `Council forecast` = council,
            `Council error` = sprintf("%+d", council_err),
            `Our projection` = round(ours),
            `Our error` = sprintf("%+d", ours_err)) %>%
  knitr::kable(align = "lrrrrrr")
Table 1: Year 7 offers in 2026: what the council forecast, what our projection gave, and what actually happened.
Catchment PAN Actual Council forecast Council error Our projection Our error
Longhill 210 81 162 +81 110 +29
PACA (Portslade Aldridge) 220 183 218 +35 186 +3
Patcham 225 204 194 -10 205 +1
BACA (Brighton Aldridge) 180 146 128 -18 122 -24
Varndean / Dorothy Stringer 630 630 601 -29 610 -20
Hove Park / Blatchington Mill 510 466 424 -42 470 +4
NoteRead our column with more suspicion than the council’s

The council’s forecast is a genuine prediction: published in advance, checked here against what happened. Ours is not, quite. The retention ratio it uses is fitted on cohorts that include the 2019 reception group, which is the 2026 Year 7 group — so 2026 is partly inside the window our method was calibrated on, and its apparent accuracy is flattered. The honest comparison is between the council’s forecast and the outturn; our column is there for orientation, not as a claim to have done better.

City-wide the council’s 2026 forecast was close: it was +17 places out in total, on a cohort of over two thousand. By catchment it was not close at all. The individual errors sum to 215 places in absolute terms — roughly 13 times the net error. The council is forecasting the right number of children and putting a great many of them in the wrong place.

Two catchments carry most of that, and they miss in opposite directions.

Longhill was forecast 162 and received 81 — 2.0 times too high. This is a catchment whose admission number is 210 and which filled 39% of it. The council’s own method already applies the largest leakage deduction in the city to this catchment, at over 22 per cent, and it is still not close.

Hove Park / Blatchington Mill was forecast 424 and received 466 — 42 places too low, the largest under-estimate of any catchment.

WarningQuestion for the council 1: why is the forecast wrong in these two places, and in opposite directions?

The Hove Park / Blatchington Mill under-estimate has a candidate explanation. The neighbouring PACA catchment was over-forecast by 35 places — close to the 42 that Hove Park / Blatchington Mill was under by. Children the model expected to appear in Portslade appearing in Hove instead would account for much of it. That is testable, and the council holds the data to test it.

Longhill has no such explanation. Its neighbours were not over-forecast in a way that could absorb an +81-place error — BACA was -18 and Patcham -10. The children the council expected to enrol at Longhill did not turn up in an adjacent catchment. They did not turn up at all.

So the questions are:

  1. What accounts for the Longhill over-forecast, if not cross-catchment movement? Out-of-city flow to Lewes district and the independent sector are the obvious candidates, and the council holds allocation records that would separate them.
  2. Is the leakage rate for the Longhill catchment being estimated on data old enough to predate the change it is meant to capture? A rate calibrated on historical patterns will lag a shift that is still happening.
  3. Is the Hove Park / Blatchington Mill under-estimate the counterpart of the PACA over-estimate? If so, the model’s catchment-of-residence assumption is diverging from where families actually apply, and the two errors should be corrected together rather than separately.

These matter because the Longhill decision is being taken against the forecast, and the forecast has been wrong about Longhill by a factor of 2.0 in the most recent year it can be checked against.

The city-wide gap is modest in most years. Broken down by catchment it is not, and Longhill is where it opens widest.

Show code
lh_join %>%
  select(entry_year, Council = council, `From primary cohorts` = projected) %>%
  pivot_longer(-entry_year, names_to = "series", values_to = "children") %>%
  ggplot(aes(entry_year, children, colour = series)) +
  geom_hline(yintercept = rc$council_lh_pan, linetype = "31", colour = "grey45") +
  annotate("text", x = min(lh_join$entry_year), y = rc$council_lh_pan + 7,
           label = sprintf("Admission number (%s)", rc$council_lh_pan),
           hjust = 0, size = 3.1, colour = "grey35") +
  geom_line(linewidth = 1.1) + geom_point(size = 2.4) +
  scale_colour_manual(values = c("Council" = "#7b3294",
                                 "From primary cohorts" = "#008837"), name = NULL) +
  scale_y_continuous(limits = c(0, NA)) +
  labs(x = "Year of Year 7 entry", y = "Children",
       title = "Longhill: the two forecasts disagree, and both sit below the admission number",
       subtitle = sprintf("Mean gap of %.0f children a year",
                          mean(lh_join$gap, na.rm = TRUE)),
       caption = "Sources: BHCC catchment forecasts; reception-based projection.") +
  theme_bh()
Figure 14: Longhill catchment: the council’s forecast, the projection from its own primary cohorts, and the school’s admission number.

The council’s forecast for the Longhill catchment runs an average of 40 children a year above what that catchment’s own primary cohorts support. Both series sit below the school’s admission number of 210 in every year. This matters because decisions about Longhill’s future are being taken against the higher of two numbers, and the higher one is the one the primary registers do not support.

3.5 The Peacehaven accession

The catchment system reaches beyond the city boundary. The Peacehaven and Telscombe area contributes 14 LSOAs and about 215 cohort-aged children, roughly 9% of the total the model covers.

In headcount terms this is close to self-supporting: Peacehaven Community School has an admission number of 180, against about 215 children in its area. The difficulty is not the arithmetic but the direction of travel. These families sit at the eastern end of a transport network that runs east–west along the coast, and section 4 shows that they are among the least well served in the whole study area — not because they are far from a school, but because they are far from most schools, and therefore have the fewest alternatives if their first choice is full.

4 How reachable are the schools?

Almost all of the public argument about Brighton’s schools is conducted in terms of admissions rules — catchments, priorities, tie-breaks. Underneath the rules sits a physical constraint that no rule can undo: how long it actually takes a eleven-year-old to get to each school on a bus. This section establishes that constraint before section 5 returns to the rules.

All journey times here are routed with r5r over the merged street network and bus timetable for the area, for a weekday morning arrival. They include walking to the stop, waiting, the ride itself, and any transfer.

4.1 What counts as a long journey to school?

Before mapping anything it is worth fixing what these minutes mean, because a number like “twenty-eight minutes” is only interpretable against something.

Benchmark Figure Source
Average one-way school trip, England 19 minutes National Travel Survey, five-year average to 2019
Average one-way trip distance 2.4 miles as above
Statutory maximum, secondary age 75 minutes each way DfE home-to-school travel guidance
Statutory maximum, primary age 45 minutes each way as above

Two cautions about the 19-minute average. It covers ages 5 to 16 together, and secondary journeys are longer than primary ones, so it understates the secondary figure. And it counts all modes, including the car — which carries 30% of secondary school trips nationally against 18% by local bus. The journeys modelled here are walking and bus only, so they describe the trip facing a family without a car. That is the right comparison for this document, but it is not a like-for-like one with the national number.

The 75-minute figure is firmer. It is the Department for Education’s guidance on how long a home-to-school journey should reasonably take for a child of secondary age, each way, including walking to a pick-up point.

ImportantA correction to the routed times, and why it matters

The routed matrix returns a walk-and-bus itinerary for every neighbourhood-school pair. Where the bus network is awkward it returns a poor one: 19% of the 2,299 neighbourhood-school pairs were modelled as taking longer than simply walking the distance would. In the worst case a journey of about four kilometres came out at 99 minutes.

That is a property of the router, not of the city. A family facing a 99-minute bus ride to a school four kilometres away walks instead. So every journey is now the lesser of the routed bus time and the time it would take to walk, and the bus is used only where it is genuinely faster.

The walking pace is not invented. Kent et al. (2026), a systematic review of active school travel, finds children’s mean walking trip to school is 1,030 metres in 14.2 minutes — 4.35 km/h — and that is the speed used, with a circuity factor of 1.3 to turn straight-line distance into street distance.

NoteThis is a ceiling, not a claim about how children travel

The same review finds children walk about a kilometre on average and seldom beyond 1,800 metres. Capping a four-kilometre journey at its walking time does not assert that a child walks four kilometres. It asserts that no journey should be modelled as costing more than walking would, because a family with that option would take it. The cap is an upper bound on modelled cost, not a mode assumption.

The correction is not cosmetic, and it is applied upstream, in the open model’s own inputs — so every section of this document and of the technical companion now uses the corrected costs. Sections 7 and 8 were re-run against them. It is worth being open that an earlier version of this analysis overstated the headline figures:

Uncorrected With the walk fallback
Journey to the nearest school, child-weighted 22.8 min 19.9 min
Neighbourhoods reaching no place within 30 min 37 29
Children in those neighbourhoods 474 385

The corrected figure is a useful check in itself. A child-weighted mean of 19.9 minutes to the nearest school sits almost exactly on the national average of 19 minutes — which is reassuring for a model built from timetables and street networks rather than from observed journeys.

4.1.1 Against the statutory guidance

The DfE limit applies to the journey a child actually has to make, so the test is the time to the school their catchment entitles them to — not to the nearest school of any kind.

Show code
acc$by_catch_stat %>%
  transmute(Catchment = unname(CATCH_DISPLAY[catchment]),
            `Mean minutes` = sprintf("%.1f", mean_min),
            `Worst zone` = sprintf("%.0f", max_min),
            `Zones over 45 min` = over_45,
            `Children affected` = sprintf("%.0f", children_over_45)) %>%
  knitr::kable(align = "lrrrr")
Table 2: Journey to the nearest school in the child’s own catchment, by catchment, against the DfE guidance.
Catchment Mean minutes Worst zone Zones over 45 min Children affected
Varndean / Dorothy Stringer 26.2 54 6 59
Longhill 25.8 40 0 0
BACA (Brighton Aldridge) 23.0 33 0 0
Hove Park / Blatchington Mill 22.4 38 0 0
Patcham 21.9 74 1 15
PACA (Portslade Aldridge) 18.3 33 0 0
NA 17.9 40 0 0

No neighbourhood in the city exceeds the 75-minute secondary limit to its own catchment school. The worst is 74 minutes. On that test the city passes, and it is worth saying so plainly given how much of this document is critical.

7 zones — about 74 children — exceed the 45-minute figure that applies to primary-age children. That is not a breach for secondary pupils, but it is a reasonable marker of a long journey, and it identifies where the pressure sits.

4.2 Journey time to a single school

Every figure in this section comes from routing each origin to each school over a street network and a bus timetable. The details matter, because they set the limits of what the numbers can support.

What was routed. r5r over a merged OpenStreetMap extract and a GTFS bus timetable, from 7,896 postcode origins to 15 destinations — the eleven schools in the expanded authority plus three East Sussex schools that Peacehaven families genuinely choose between, and one hypothetical site. Postcode results are then aggregated to the LSOA-by-catchment zones used everywhere else, weighted by child population.

The parameters. Walking and public transport combined; up to 60 minutes of walking; trips capped at 120 minutes; the median of a 1-minute departure window. Arrival is set for 08:00 at every school except Longhill, which uses 07:30 because its buses run earlier. That asymmetry is deliberate.

Two extracts, merged. Geofabrik splits its Sussex data along the county boundary, and the split runs straight through this study area: Brighton & Hove sits in the west extract, the Peacehaven expansion area in the east. r5r accepts exactly one .pbf file, so the two were merged into sussex-merged.osm.pbf. Supplying both separately does not error — it silently returns nothing.

The timetable is a timetable. This is the single most important caveat. The GTFS feed (gtfs.zip, from the Department for Transport’s Open Bus Data Service) describes the service that was scheduled, not the service that ran. The routing date is chosen automatically as the weekday with the most services active in the feed, because it is a multi-period export and only one window carries a full timetable. So these are timetabled journey times, not measured ones. They do not know about a bus that failed to arrive, one that was already full, or one running ten minutes late.

What is missing entirely. No school buses, no parental lifts, no cycling, no walking routes children actually prefer to the shortest one. Car journeys are excluded by design — the question here is what a family without a car faces — but nationally the car carries about 30% of secondary school trips, so these times describe a subset of families rather than the average one.

Where the school gate is. r5r snaps each school to the nearest point on the street network, and which point that is can matter more than it should. Dorothy Stringer and Varndean are 470 metres apart, yet the matrix makes Stringer slower from most of the city by a median of about five and a half minutes. That may be a real difference in bus access or an artefact of the snapping; it has not been established which, and it is large enough to affect anything said about Stringer specifically. Longhill’s position is untouched by it, because the anomaly sits between two central schools.

Bus stop coverage was checked at five points across the study area:

Show code
bmeta$gtfs_coverage %>%
  transmute(Place = place, `Stops within 2 km` = stops_within_2km) %>%
  knitr::kable(align = "lr")
Table 3: Bus stops within 2 km of each check point, in the feed used.
Place Stops within 2 km
Brighton 269
Saltdean 90
Peacehaven 55
Newhaven 35
Seaford 40

The feed thins eastwards but does not stop at the city boundary, which is what matters: the expansion area is genuinely served rather than appearing unreachable because the timetable runs out.

Show code
m_pick <- leaflet(width = "100%", height = 600,
                  options = leafletOptions(preferCanvas = TRUE)) %>%
  add_basemap()

layer_names <- c(sort(unique(oi$costs_now$name)), ELM_LABEL)

for (s in layer_names) {
  d <- lsoa_lite %>%
    inner_join(surf_all %>% filter(name == s), by = c("lsoa21cd" = "lsoa")) %>%
    filter(is.finite(mins))

  # The school's own marker, at Elm Grove for the relocation layer.
  if (identical(s, ELM_LABEL)) {
    px <- elm_xy[, 1]; py <- elm_xy[, 2]; plab <- oi$elm_grove$label
  } else {
    r <- sch_pts[sch_pts$name == s, ]
    px <- r$lon; py <- r$lat; plab <- s
  }

  m_pick <- m_pick %>%
    addPolygons(data = d, group = s,
                fillColor = ~pal_time(mins), fillOpacity = 0.8,
                color = "white", weight = 0.3,
                label = ~sprintf("%s: %.0f minutes", lsoa21nm, mins)) %>%
    addCircleMarkers(lng = px, lat = py, group = s,
                     radius = 7, fillColor = "#1a9850", fillOpacity = 1,
                     color = "white", weight = 2, label = plab)
}

m_pick %>%
  addPolygons(data = catch, group = "Catchment boundaries",
              fill = FALSE, color = "#111111", weight = 2.2, opacity = 0.85,
              label = ~unname(CATCH_LABELS[catchment])) %>%
  addLayersControl(baseGroups = layer_names,
                   overlayGroups = "Catchment boundaries",
                   options = layersControlOptions(collapsed = TRUE)) %>%
  hideGroup("Catchment boundaries") %>%
  addLegend(pal = pal_time, values = surf_all$mins, title = "Minutes",
            position = "bottomright", opacity = 0.85)
Figure 15: Walk-and-bus journey time from every neighbourhood, for a weekday morning arrival. Choose a school from the control at the top right. The colour scale is shared across all schools, so the maps are directly comparable. The last option places Longhill at the alternative Elm Grove site.
Show code
jt <- bind_rows(
  oi$costs_now %>% inner_join(zone_w, by = "zone") %>%
    group_by(name) %>%
    summarise(unw = mean(cij), wtd = weighted.mean(cij, Oi), .groups = "drop"),
  oi$costs_elm %>% filter(name == "Longhill High School") %>%
    inner_join(zone_w, by = "zone") %>%
    summarise(name = ELM_LABEL, unw = mean(cij), wtd = weighted.mean(cij, Oi))
) %>%
  arrange(wtd) %>%
  mutate(gap = wtd - unw)

jt %>%
  transmute(School = sub(" AT ELM GROVE \\(alternative site\\)",
                         " (at Elm Grove)", name),
            `Unweighted mean` = sprintf("%.1f", unw),
            `Child-weighted mean` = sprintf("%.1f", wtd),
            `Difference` = sprintf("%+.1f", gap)) %>%
  knitr::kable(align = "lrrr")
Table 4: Average walk-and-bus journey time to each school. The unweighted column treats every neighbourhood equally; the weighted column weights each by the number of cohort-aged children living there, which is the figure that describes what families actually face.
School Unweighted mean Child-weighted mean Difference
Cardinal Newman Catholic School 41.1 43.1 +2.0
Longhill (at Elm Grove) 41.6 45.6 +4.0
Hove Park School 47.3 47.5 +0.2
Varndean School 45.7 48.2 +2.5
Blatchington Mill School 49.9 49.4 -0.5
Dorothy Stringer School 48.5 50.6 +2.1
Patcham High School 51.6 54.1 +2.5
Portslade Aldridge Community Academy 55.3 55.5 +0.2
Brighton Aldridge Community Academy 52.5 56.6 +4.0
King’s School 59.0 58.4 -0.6
Longhill High School 57.9 61.3 +3.4
Peacehaven Community School 66.0 67.8 +1.8

Three things come out of that table.

Longhill is the least accessible school in the city, at 61.3 minutes on the child-weighted measure against 43.1 for Cardinal Newman Catholic School. Only Peacehaven, outside the authority, is further from the city’s children.

Weighting by where children live makes almost every school look worse. The weighted mean exceeds the unweighted one for 10 of the 12 rows. Children are not distributed evenly across the map: they are concentrated in places slightly further from the schools than the average neighbourhood is. The two largest gaps belong to Brighton Aldridge Community Academy and Longhill High School (+4.0 and +3.4 minutes) — which are also the two least full schools in the city. The children in those catchments live further from their own school than an average neighbourhood does, which is a different problem from the school simply being far away, and a harder one to fix with a bus route.

Moving Longhill to Elm Grove changes its position completely. The child-weighted journey time falls from 61.3 minutes to 45.6 — a saving of 15.7 minutes, which would take it from the least accessible school in the city to the second most accessible. That is the single largest effect of any intervention considered anywhere in this document, and section 8 returns to what it would and would not solve.

4.3 What one of those journeys actually looks like

A single number in minutes hides what the journey involves. These are seven real routed journeys, drawn leg by leg: walking legs follow the street network, bus legs follow the service’s own shape.

Show code
rg <- bh_data("route_geometries.rds")
legs <- rg$legs %>% st_transform(4326)

pal_mode <- colorFactor(c("BUS" = "#2166ac", "WALK" = "#e08214"),
                        domain = c("BUS", "WALK"))

mleg <- leaflet(width = "100%", height = 560) %>%
  add_basemap()

for (j in sort(unique(legs$journey))) {
  d <- legs %>% filter(journey == j)
  tot <- rg$summary$total_minutes[rg$summary$journey == j]
  mleg <- mleg %>%
    addPolylines(data = d, group = j,
                 color = ~pal_mode(leg_mode), weight = 5, opacity = 0.85,
                 label = ~sprintf("%s - %s leg, %.0f min%s", j, leg_mode, minutes,
                                  ifelse(nzchar(route), paste0(" (service ", route, ")"), "")),
                 popup = ~sprintf("<b>%s</b><br>%s minutes in total", j, tot))
}

mleg %>%
  addLayersControl(overlayGroups = sort(unique(legs$journey)),
                   options = layersControlOptions(collapsed = FALSE)) %>%
  addLegend(pal = pal_mode, values = c("BUS", "WALK"), title = "Leg",
            position = "bottomright", opacity = 0.9)
Figure 16: Seven worked journeys, routed leg by leg. Walking legs in orange, bus legs in blue. Each can be checked against local knowledge.
Show code
rg$summary %>%
  transmute(Journey = journey,
            `Total minutes` = total_minutes,
            Legs = legs,
            `Bus services used` = ifelse(nzchar(services), services, "walk only")) %>%
  arrange(`Total minutes`) %>%
  knitr::kable()
Table 5: The same seven journeys, summarised.
Journey Total minutes Legs Bus services used
Woodingdean to Longhill 11 3 2
Saltdean to Longhill 19 5 12A, 2
Kemptown to Longhill 25 5 12, 2
Peacehaven to Longhill 39 5 14, 2
Hove to Dorothy Stringer 43 3 5B
Whitehawk to Longhill 44 3 2
Whitehawk to Dorothy Stringer 52 5 21, 5B

Woodingdean to Longhill takes 11 minutes. Whitehawk to Dorothy Stringer takes 52, for a journey of about the same straight-line distance. The network is not symmetric, and neither is opportunity.

4.4 Which school is actually nearest

Show code
near <- lsoa %>%
  left_join(acc$lsoa %>% select(lsoa, nearest_school, nearest_min),
            by = c("lsoa21cd" = "lsoa")) %>%
  filter(!is.na(nearest_school))

pal_near <- colorFactor("Set3", domain = sort(unique(near$nearest_school)))

leaflet(width = "100%", height = 560) %>%
  add_basemap() %>%
  addPolygons(data = near, group = "Nearest school",
              fillColor = ~pal_near(nearest_school), fillOpacity = 0.72,
              color = "white", weight = 0.4,
              label = ~sprintf("%s: %s, %.0f min", lsoa21nm, nearest_school, nearest_min)) %>%
  # This is the map where the overlay earns its place: the colours are
  # the nearest school and the outlines are the catchment a child is
  # actually assigned to, so any mismatch is visible directly.
  addPolygons(data = catch, group = "Catchment boundaries",
              fill = FALSE, color = "#111111", weight = 2.2, opacity = 0.85,
              label = ~unname(CATCH_LABELS[catchment])) %>%
  addLayersControl(overlayGroups = c("Nearest school", "Catchment boundaries"),
                   options = layersControlOptions(collapsed = FALSE)) %>%
  hideGroup("Catchment boundaries") %>%
  addLegend(pal = pal_near, values = near$nearest_school, title = "Nearest school",
            position = "bottomright", opacity = 0.85)
Figure 17: The nearest secondary school to each neighbourhood by routed walk-and-bus time, regardless of catchment. Turn on the catchment boundaries to see where the two disagree.

This map is worth comparing with the catchment map in section 2. They are not the same shape. The nearest school by bus is not always the catchment school, and for 30% of neighbourhoods even the nearest school is more than 25 minutes away.

4.5 Potential accessibility: how much school is within reach

Nearest-school distance answers a narrow question. It says nothing about whether a neighbourhood has one school within reach or five — and that difference is the whole of what a family’s realistic options amount to when their first preference is full.

Two measures are used here, deliberately, because they fail in opposite directions.

4.5.1 First, what makes a school attractive?

Both measures need a weight for each school — some number saying how much school is there. The choice is not obvious, and it is worth setting out before the maps rather than burying it in a footnote afterwards.

There are four candidates, each answering a different question.

Specification What it measures Question it answers
Admission number places the school is allowed to fill What did the authority decide to provide?
First preferences families naming it first What did families ask for?
Places allocated offers actually made What did the system deliver?
Weighted preferences ranks 1–3 with geometric decay What did families want, allowing for the ordering the rules force on them?

The admission number is the obvious choice and the weakest. It is an administrative decision, revised occasionally by the authority, and it says nothing about whether anyone wants the places. Longhill has the third-largest admission number in the city and the fewest first preferences of any school; on a PAN weighting it counts as one of Brighton’s more attractive destinations, which is plainly wrong.

First preferences fix that but introduce a different distortion. In the two paired catchments a family has to rank the two local schools against each other, so the catchment’s first preferences are split between them while its second preferences accumulate. The signature is unmistakable in the data, and it means counting firsts alone marks down precisely the schools whose catchment obliges families to put them second.

Places allocated measures what happened rather than what was wanted. That makes it a poor attractiveness measure for exactly the schools this document is about: an undersubscribed school allocates every place it is asked for, so allocation tracks capacity rather than desire at the bottom of the range.

Weighted preferences — counting all three ranks with a geometric decay of \(\alpha = 0.5\) — is what the maps below use. It keeps the information in the second and third choices without treating them as equal to a first.

Show code
acc$attract %>%
  left_join(oi$schools %>% select(name, catchment), by = "name") %>%
  mutate(kind = case_when(
           is.na(catchment) ~ "Faith, city-wide",
           catchment %in% c("DS_Varndean", "Hove_Blatch") ~ "Paired",
           TRUE ~ "Single-school")) %>%
  arrange(desc(W_pref)) %>%
  transmute(School = name, Catchment = kind,
            `2nd / 1st` = ifelse(imputed, "--", sprintf("%.2f", p2 / p1)),
            `Admission no.` = sprintf("%.2f", W_pan),
            `1st prefs` = sprintf("%.2f", W_p1),
            `Allocated` = sprintf("%.2f", W_alloc),
            `Weighted prefs` = sprintf("%.2f", W_pref)) %>%
  knitr::kable(align = "llrrrrr")
Table 6: The four specifications, normalised so each averages 1 across the eleven schools. The ratio of second to first preferences separates the paired catchments from the single-school ones almost perfectly.
School Catchment 2nd / 1st Admission no. 1st prefs Allocated Weighted prefs
Varndean School Paired 1.00 1.20 1.87 1.36 1.82
Cardinal Newman Catholic School Faith, city-wide 0.45 1.45 2.05 1.62 1.71
Dorothy Stringer School Paired 1.89 1.32 1.15 1.51 1.51
Blatchington Mill School Paired 1.02 1.32 1.23 1.49 1.28
King’s School Faith, city-wide 1.04 0.66 1.05 0.77 1.10
Hove Park School Paired 1.96 0.72 0.55 0.73 0.81
Patcham High School Single-school 0.43 0.90 0.85 0.97 0.79
Peacehaven Community School Single-school 0.72 0.71 0.71 0.72
Portslade Aldridge Community Academy Single-school 0.58 0.88 0.70 0.85 0.59
Brighton Aldridge Community Academy Single-school 0.37 0.72 0.44 0.50 0.35
Longhill High School Single-school 0.38 1.08 0.41 0.49 0.32

The four schools in paired catchments have second-to-first ratios between 1.0 and 2.0; the four in single-school catchments, between 0.4 and 0.6.

Show code
wlong <- acc$attract %>%
  select(name, W_pan, W_p1, W_alloc, W_pref) %>%
  pivot_longer(-name, names_to = "spec", values_to = "w") %>%
  mutate(spec = factor(spec, c("W_pan", "W_p1", "W_alloc", "W_pref"),
                       c("Admission number", "First preferences",
                         "Places allocated", "Weighted preferences")))

ord <- acc$attract %>% arrange(W_pref) %>% pull(name)

wlong %>%
  mutate(name = factor(name, ord)) %>%
  ggplot(aes(w, name)) +
  geom_line(aes(group = name), colour = "grey72", linewidth = 0.8) +
  geom_point(aes(colour = spec), size = 2.8) +
  geom_vline(xintercept = 1, linetype = "31", colour = "grey45") +
  scale_colour_manual(values = c("Admission number"    = "#999999",
                                 "First preferences"   = "#2166ac",
                                 "Places allocated"    = "#4daf4a",
                                 "Weighted preferences" = "#b2182b"),
                      name = NULL) +
  labs(x = "Attractiveness weight (1 = city average)", y = NULL,
       title = "The four specifications broadly agree, and disagree where it matters",
       subtitle = "Each school's four weights, joined by a grey line. The dashed line is the city average.",
       caption = "Sources: BHCC allocation factsheets; published admission numbers.") +
  theme_bh(11) +
  theme(legend.position = "top", panel.grid.major.y = element_blank())
Figure 18: The four specifications compared. Schools are ordered by the weighted-preference weight. Where the points for a school spread out, the choice of specification matters for that school.

The lines are mostly short: for most schools the four specifications agree closely, and the choice would not matter much. They spread furthest for Longhill High School, Varndean School and Cardinal Newman Catholic School, for three different reasons — Longhill because its admission number is generous relative to any measure of demand, Varndean because the paired catchment splits its first preferences with Dorothy Stringer, and Cardinal Newman because it admits city-wide on faith criteria, so what it is asked for and what it allocates diverge from what it is sized for.

Two choices sit inside \(W_j\) that are easy to skate over. Neither is obviously right, and both change what individual schools look like.

The decay, \(\alpha\). A first preference counts 1, a second \(\alpha\), a third \(\alpha^2\). At the value used, \(\alpha = 0.5\), a second choice is worth half a first and a third a quarter. The geometric form is a modelling convenience rather than a measured fact — it says the drop from first to second is the same proportion as the drop from second to third, which nothing in the data establishes.

\(\alpha = 0\) reduces to counting first preferences only; \(\alpha = 1\) counts all three equally.

Show code
w_alpha %>%
  arrange(desc(`0.50`)) %>%
  mutate(across(where(is.numeric), ~ sprintf("%.2f", .x))) %>%
  rename(School = name) %>%
  knitr::kable(align = "lrrrrr")
Table 7: Attractiveness weight under different decay values, five-year window. The paired-catchment schools move most, because their second preferences are the ones the catchment structure generates.
School 0.00 0.25 0.50 0.75 1.00
Varndean School 1.87 1.87 1.82 1.77 1.71
Cardinal Newman Catholic School 2.05 1.85 1.71 1.62 1.57
Dorothy Stringer School 1.15 1.38 1.51 1.58 1.61
Blatchington Mill School 1.23 1.26 1.28 1.30 1.31
King’s School 1.05 1.08 1.10 1.11 1.12
Hove Park School 0.55 0.69 0.81 0.91 0.99
Patcham High School 0.85 0.79 0.79 0.83 0.87
Peacehaven Community School 0.71 0.71 0.72 0.72 0.72
Portslade Aldridge Community Academy 0.70 0.64 0.59 0.56 0.53
Brighton Aldridge Community Academy 0.44 0.39 0.35 0.32 0.30
Longhill High School 0.41 0.36 0.32 0.29 0.27

Hove Park ranges from 0.55 at \(\alpha = 0\) to 0.99 at \(\alpha = 1\) — close to a doubling, and the clearest illustration of what the choice does. Schools whose demand arrives as second preferences gain as \(\alpha\) rises; schools whose demand is overwhelmingly first-choice, like Cardinal Newman, lose. The ordering is fairly stable throughout (Spearman 0.90 between the extremes); the magnitudes are not.

The window. The maps and models below use a five-year average, the 2022–2026 admissions rounds. That is a compromise, and it is worth seeing what the alternatives would give.

Show code
w_win %>%
  arrange(desc(`Last 5 years (used)`)) %>%
  mutate(across(where(is.numeric), ~ sprintf("%.2f", .x))) %>%
  rename(School = name) %>%
  knitr::kable(align = "lrrr")
Table 8: The same weight measured over three different windows. Averaging over the whole series flatters the schools whose demand has fallen, because it includes the years before it fell.
School Latest year only Last 5 years (used) Whole series
Varndean School 1.81 1.82 1.61
Cardinal Newman Catholic School 1.89 1.71 1.51
Dorothy Stringer School 1.56 1.51 1.76
Blatchington Mill School 1.24 1.28 1.51
King’s School 1.11 1.10 0.86
Hove Park School 0.66 0.81 0.94
Patcham High School 0.70 0.79 0.81
Peacehaven Community School 0.71 0.72 0.71
Portslade Aldridge Community Academy 0.60 0.59 0.50
Brighton Aldridge Community Academy 0.47 0.35 0.33
Longhill High School 0.26 0.32 0.45

The differences are not small. Longhill scores 0.26 on the most recent year and 0.45 across the whole series — and the two windows rank the schools differently (Spearman 0.86).

That gap is not noise. It is section 5.3 showing up in a different place: demand has moved so much over the published series that a sixteen-year average describes a city that no longer exists, and systematically flatters the schools that have declined.

So why not use the latest year alone? Because section 2.6’s point about small numbers applies here too. Across the five years used, the year-to-year coefficient of variation in \(W\) is 25% for Brighton Aldridge Community Academy and 20% for Longhill High School — the two smallest and least subscribed schools. A single year would make exactly the schools this document is about the least reliably measured.

Five years is short enough to describe the current system and long enough to damp that. It is a judgement, not a derivation, and a reader who preferred three or seven would not be wrong.

NoteAnd the surface barely notices

It is worth knowing how little of this propagates. Recomputing the accessibility surface under each of the four weightings gives neighbourhood-level rankings that correlate between 0.985 and 0.998 across all six pairs. The weighting matters a great deal for what you say about an individual school and very little for the map, because the measure sums over eleven of them.

Two limitations do matter more than the choice between the four. Peacehaven Community School is in Lewes district and does not appear in the council’s factsheets, so it has no preference or allocation data; its weight is imputed from its admission number at the city-average rate. And none of these counts is divided by the admission number, because what should be within reach is places families want, which scales with the size of the school.

4.5.2 Measure one: gravity accessibility

\[A_i = \sum_j W_j \, c_{ij}^{-\beta}\]

Term Meaning
\(A_i\) the accessibility of neighbourhood \(i\) — the quantity mapped below
\(j\) a school; the sum runs over all 11 in the study area
\(W_j\) the weighted-preference attractiveness set out above
\(c_{ij}\) routed walk-and-bus minutes from \(i\) to \(j\), weekday morning
\(\beta\) distance decay: how sharply demand falls away with travel time, set at 1.7

Every school contributes to every neighbourhood’s score, discounted by how far away it is. A neighbourhood with one school ten minutes away and nothing else scores lower than one with three schools at fifteen minutes, which is the point of the measure. The result has no units, so it is indexed with today’s child-weighted city average at 100.

NoteWhere \(\beta\) comes from

\(\beta\) is set at 1.7. This is a judgement rather than an estimate: calibrating it properly needs pupil-level flows, which the council has not released. It is a reasonable central value for a system of this kind — journeys are short and the alternatives sit close together — and it lies inside the 1.5–3.2 range the open model sweeps in section 7. Nothing in this section turns on the exact figure; the surface is stored at the ends of that range as well, and the ordering of neighbourhoods is stable across it.

Show code
acc_scenario_map("A_index", "A_index_elm", pal_grav,
                 "Gravity index<br>(today's city average = 100)")
Figure 19: Gravity accessibility to secondary school places. Indexed so today’s child-weighted city average is 100, and both scenarios use that same base, so the Elm Grove layer can be read against it directly.

4.5.3 Measure two: cumulative opportunity

\[P_i(t) = \sum_j \mathrm{PAN}_j \cdot \mathbf{1}\!\left(c_{ij} \le t\right)\]

Term Meaning
\(P_i(t)\) school places reachable from neighbourhood \(i\) inside \(t\) minutes
\(j\) a school; the sum again runs over all 11
\(\mathrm{PAN}_j\) school \(j\)’s admission number — the places it actually has
\(\mathbf{1}(\cdot)\) an indicator: 1 if the journey is within \(t\) minutes, 0 if not
\(c_{ij}\) routed walk-and-bus minutes from \(i\) to \(j\), as above
\(t\) the time threshold, set with the slider below

Where the gravity measure discounts smoothly, this one cuts off. A school 29 minutes away counts in full; the same school at 31 minutes counts for nothing. That is crude, but it is also how a threshold actually works for a family judging whether a journey is feasible — and unlike the gravity index, the number means something on its own.

The threshold is doing all the work, so it is worth being able to move it.

Show code
# The polygons are drawn in JavaScript rather than by addPolygons(),
# because the slider has to restyle them on every move. Going through R
# would mean reaching into leaflet's own layer registry, which is an
# undocumented internal; building the layer here means the handles are
# ours and the interaction is straightforward.
#
# Colours are precomputed in R for every combination of threshold,
# scenario and weighting, so the browser only swaps fills.

tl <- acc$thresh_long %>% mutate(scenario = factor(scenario, c("now", "elm")))

pal_t  <- colorNumeric("viridis", domain = c(0, max(tl$places)), reverse = TRUE)
# Places per child are heavily right-skewed, so that scale is built on
# the square root; without it nearly every neighbourhood sits in the
# bottom colour and the map says nothing.
pal_pc <- colorNumeric("viridis", domain = c(0, sqrt(max(tl$per_child))),
                       reverse = TRUE)

tl <- tl %>% mutate(col = pal_t(places), col_pc = pal_pc(sqrt(per_child)))

as_lookup <- function(col) {
  split(tl, tl$scenario) %>%
    lapply(function(s) split(s, s$t) %>%
             lapply(function(d) setNames(as.list(d[[col]]), d$lsoa)))
}

# A coarser simplification than the leaflet maps use, because this
# geometry is embedded in the page as JSON. 60 m is still finer than the
# rendered pixels at any zoom this map offers.
geo_js <- lsoa %>%
  st_transform(27700) %>%
  st_simplify(dTolerance = 60, preserveTopology = TRUE) %>%
  st_transform(4326) %>%
  select(lsoa21cd, lsoa21nm)

lh_pt <- sch_pts[sch_pts$short == "longhill", ]

payload <- list(
  geo        = jsonlite::fromJSON(geojsonsf::sf_geojson(geo_js),
                                  simplifyVector = FALSE),
  thresholds = acc$thresh_grid,
  colours    = as_lookup("col"),
  values     = as_lookup("places"),
  colours_pc = as_lookup("col_pc"),
  values_pc  = as_lookup("per_child"),
  city = split(acc$thresh_city, acc$thresh_city$scenario) %>%
    lapply(function(s) split(s, s$t) %>%
             lapply(function(d) list(mean = round(d$mean_places[1]),
                                     stranded = d$stranded[1],
                                     children = round(d$stranded_children[1])))),
  longhill = list(now = list(lat = lh_pt$lat, lng = lh_pt$lon),
                  elm = list(lat = elm_xy[, 2], lng = elm_xy[, 1])),
  catchments = jsonlite::fromJSON(
    geojsonsf::sf_geojson(catch %>% select(catchment)),
    simplifyVector = FALSE))

m_cum <- leaflet(width = "100%", height = 580) %>%
  add_basemap() %>%
  fitBounds(lng1 = min(sch_pts$lon) - 0.03, lat1 = min(sch_pts$lat) - 0.02,
            lng2 = max(sch_pts$lon) + 0.03, lat2 = max(sch_pts$lat) + 0.02) %>%
  addCircleMarkers(data = sch_pts %>% filter(short != "longhill"),
                   lng = ~lon, lat = ~lat, radius = 4,
                   fillColor = "#e31a1c", fillOpacity = 1,
                   color = "white", weight = 1.2, label = ~name) %>%
  addLegend(pal = pal_t, values = c(0, max(tl$places)),
            title = "Places within<br>the threshold",
            position = "bottomright", opacity = 0.85)

m_cum <- htmlwidgets::onRender(m_cum, "
function(el, x, data) {
  var map = this;

  var layers = {};
  L.geoJSON(data.geo, {
    style: function() {
      return {weight: 0.3, color: 'white', fillOpacity: 0.82};
    },
    onEachFeature: function(f, layer) {
      layers[f.properties.lsoa21cd] = layer;
      layer.bindTooltip('');
    }
  }).addTo(map);

  var lh = L.circleMarker([data.longhill.now.lat, data.longhill.now.lng],
    {radius: 6, fillColor: '#1a9850', fillOpacity: 1,
     color: 'white', weight: 2}).addTo(map)
    .bindTooltip('Longhill High School');

  // Outlines only, so the surface stays readable underneath. Kept off
  // until asked for: the point of the map is that reachability does not
  // follow the boundaries.
  var catchLayer = L.geoJSON(data.catchments, {
    style: function() {
      return {fill: false, color: '#111111', weight: 2.2, opacity: 0.85};
    },
    onEachFeature: function(f, layer) {
      layer.bindTooltip(f.properties.catchment);
    }
  });

  var ui = L.DomUtil.create('div', 'acc-slider');
  ui.style.cssText = 'background:white;padding:10px 14px;border-radius:4px;' +
    'box-shadow:0 1px 5px rgba(0,0,0,.3);font-family:sans-serif;' +
    'font-size:13px;min-width:290px';
  ui.innerHTML =
    '<div style=\"font-weight:bold;margin-bottom:6px\">Within ' +
      '<span id=\"tval\">30</span> minutes</div>' +
    '<input id=\"tslider\" type=\"range\" min=\"0\" max=\"' +
      (data.thresholds.length - 1) + '\" value=\"' +
      data.thresholds.indexOf(30) + '\" step=\"1\" style=\"width:100%\">' +
    '<div style=\"margin-top:8px\">' +
      '<label style=\"margin-right:10px\"><input type=\"radio\" name=\"scn\" ' +
        'value=\"now\" checked> Today</label>' +
      '<label><input type=\"radio\" name=\"scn\" value=\"elm\"> ' +
        'Longhill at Elm Grove</label></div>' +
    '<div style=\"margin-top:6px;padding-top:6px;border-top:1px solid #eee\">' +
      '<label><input type=\"checkbox\" id=\"perchild\"> ' +
        'Divide by children living there</label><br>' +
      '<label><input type=\"checkbox\" id=\"showcatch\"> ' +
        'Catchment boundaries</label></div>' +
    '<div id=\"citystat\" style=\"margin-top:8px;color:#444;line-height:1.45\">' +
    '</div>';

  var ctl = L.control({position: 'topright'});
  ctl.onAdd = function() { return ui; };
  ctl.addTo(map);
  L.DomEvent.disableClickPropagation(ui);
  L.DomEvent.disableScrollPropagation(ui);

  var names = {};
  data.geo.features.forEach(function(f) {
    names[f.properties.lsoa21cd] = f.properties.lsoa21nm;
  });

  function redraw() {
    var idx = +ui.querySelector('#tslider').value;
    var t = data.thresholds[idx];
    var scn = ui.querySelector('input[name=scn]:checked').value;
    var pc = ui.querySelector('#perchild').checked;
    ui.querySelector('#tval').textContent = t;

    var cols = pc ? data.colours_pc[scn][t] : data.colours[scn][t];
    var vals = pc ? data.values_pc[scn][t]  : data.values[scn][t];
    var unit = pc ? ' places per child' : ' places';

    Object.keys(layers).forEach(function(id) {
      if (cols[id] === undefined) return;
      layers[id].setStyle({fillColor: cols[id]});
      layers[id].setTooltipContent(
        names[id] + '<br>' +
        (pc ? (+vals[id]).toFixed(1) : Math.round(vals[id])) +
        unit + ' within ' + t + ' min');
    });

    lh.setLatLng([data.longhill[scn].lat, data.longhill[scn].lng]);
    lh.setTooltipContent(scn === 'elm' ?
      'Longhill (at Elm Grove)' : 'Longhill High School');

    // Restyling the fills can leave the outlines behind them.
    if (map.hasLayer(catchLayer)) catchLayer.bringToFront();

    var c = data.city[scn][t], o = data.city['now'][t];
    ui.querySelector('#citystat').innerHTML =
      '<b>' + c.mean + '</b> places for the average child' +
      (scn === 'elm' ? ' <span style=\"color:#888\">(' + o.mean +
        ' today)</span>' : '') +
      '<br><b>' + c.stranded + '</b> neighbourhoods reach none (' +
      c.children + ' children)' +
      (scn === 'elm' ? ' <span style=\"color:#888\">(' + o.stranded +
        ' today)</span>' : '');
  }

  ui.querySelector('#tslider').addEventListener('input', redraw);
  ui.querySelector('#perchild').addEventListener('change', redraw);
  ui.querySelector('#showcatch').addEventListener('change', function() {
    if (this.checked) { catchLayer.addTo(map); catchLayer.bringToFront(); }
    else { map.removeLayer(catchLayer); }
  });
  ui.querySelectorAll('input[name=scn]').forEach(function(r) {
    r.addEventListener('change', redraw);
  });
  redraw();
}
", data = payload)

m_cum
Figure 20: School places reachable within the chosen number of minutes by walking and bus. Drag the slider to change the threshold, switch Longhill between its current site and Elm Grove, and toggle whether the figure is divided by the number of children living there. Darker means fewer places within reach; the darkest areas can reach none at all.

Two things are worth doing with that slider.

Drag it down to 20 minutes. The map empties. At a threshold most parents would call a reasonable school-run, much of the city can reach almost nothing — and the pattern is not centred on the places the admissions debate concentrates on.

Switch scenarios at different thresholds. Below about 30 minutes, moving Longhill to Elm Grove is unambiguously better for the city. Above about 35 it stops being so: the far south east loses the only school it could previously reach at all, and neighbourhoods start appearing in the “reaching none” count that were not there before. The relocation is not a free improvement — it is a trade that is strongly favourable at short thresholds and turns against a small number of places at long ones.

The two maps agree on the broad shape of the city — they correlate at 0.69 — and disagree substantially in the detail, which is exactly what makes showing both worthwhile. A neighbourhood sits an average of 1.8 deciles apart between the two, and some sit six deciles apart.

The disagreement is not random. The cumulative map has a hard edge the gravity map does not: 0 neighbourhoods score in the better half of the city on the gravity measure and can still reach no school place at all within 30 minutes. Their nearest school sits a few minutes the wrong side of the threshold, so the gravity measure — which discounts smoothly rather than cutting off — records them as adequately served while a parent with a bus timetable would not.

Which map is right depends on the question. If you are asking how much school is notionally within reach, the gravity surface is the better description. If you are asking whether a child can get to a school in a morning, the threshold is the thing that matters, and the cumulative map is showing you something real that the gravity map hides.

4.5.4 What moving one school would do to the whole city

Both maps carry a second layer placing Longhill at Elm Grove. Because accessibility sums over every school, moving one of them changes the figure for every neighbourhood — not only for Longhill’s own catchment.

Show code
ct <- acc$city
tibble::tribble(
  ~Measure, ~Today, ~`At Elm Grove`, ~Change,
  "Gravity accessibility (index)",
    sprintf("%.0f", 100), sprintf("%.1f", 100 * ct$A_elm / ct$A_now),
    sprintf("%+.1f%%", ct$A_pct_change),
  "Places reachable within 30 minutes",
    sprintf("%.0f", ct$p30_now), sprintf("%.0f", ct$p30_elm),
    sprintf("%+.0f", ct$p30_elm - ct$p30_now),
  "Journey to the nearest school (minutes)",
    sprintf("%.1f", ct$near_now), sprintf("%.1f", ct$near_elm),
    sprintf("%+.1f", ct$near_elm - ct$near_now),
  "Neighbourhoods reaching no place in 30 minutes",
    sprintf("%d", ct$zero30_now), sprintf("%d", ct$zero30_elm),
    sprintf("%+d", ct$zero30_elm - ct$zero30_now),
  "Children in those neighbourhoods",
    sprintf("%.0f", ct$zero30_children_now), sprintf("%.0f", ct$zero30_children_elm),
    sprintf("%+.0f", ct$zero30_children_elm - ct$zero30_children_now)
) %>%
  knitr::kable(align = "lrrr")
Table 9: City-wide accessibility today and with Longhill relocated to Elm Grove. All figures are weighted by where cohort-aged children live.
Measure Today At Elm Grove Change
Gravity accessibility (index) 100 101.1 +1.1%
Places reachable within 30 minutes 452 482 +30
Journey to the nearest school (minutes) 19.9 18.8 -1.0
Neighbourhoods reaching no place in 30 minutes 29 15 -14
Children in those neighbourhoods 385 240 -145

The two measures disagree about how much this matters, and the disagreement is informative.

On the gravity measure the effect is small — +1.1% city-wide. That is because Longhill carries a low attractiveness weight: moving an unpopular school closer to people does not add much wanted school to the map.

On the cumulative measure it is substantial. Places reachable within half an hour rise from 452 to 482 for the average child, and the number of neighbourhoods that can reach nothing inside 30 minutes falls from 29 to 15. In children, that is 145 who would gain a reachable school place they do not currently have.

153 of the 179 neighbourhoods are better off under the move and 26 are worse off — the latter being the far south east, which loses its closest school. This is a genuine trade, but it is a lopsided one, and it is the strongest argument in this document for relocation on grounds that have nothing to do with Longhill’s own roll.

Three findings come out of this, and the first is the one that should change the conversation.

29 of the 179 neighbourhoods — about 385 cohort-aged children — cannot reach a single secondary school place within 30 minutes by walking and bus on a weekday morning. Not their catchment school; any school. For those neighbourhoods the admissions debate is somewhat beside the point.

The spread is nearly fivefold. The best-served neighbourhood scores 746 on the index against 26 for the worst. A child in the centre of the city has several schools genuinely within reach; a child at either end has one, or none.

The two measures broadly agree, but not closely. They correlate at 0.69 (Spearman), yet only 44% of neighbourhoods land within one decile of each other on both. Where they agree, the finding is robust to how you define access. Where they disagree, it is because one is counting a school just over the threshold that the other is discounting smoothly.

Show code
acc$lsoa %>%
  mutate(grp = ifelse(places_30 < 1, "No places within 30 min", "Some places within 30 min")) %>%
  ggplot(aes(A_index, places_30, colour = grp, size = Oi)) +
  geom_point(alpha = 0.75) +
  scale_colour_manual(values = c("No places within 30 min" = "#b2182b",
                                 "Some places within 30 min" = "#2166ac"),
                      name = NULL) +
  scale_size_area(max_size = 6, guide = "none") +
  scale_y_continuous(labels = label_comma()) +
  labs(x = "Gravity accessibility index (city average = 100)",
       y = "Places reachable within 30 minutes",
       title = "Two ways of asking the same question",
       subtitle = "Each point is a neighbourhood, sized by the number of cohort-aged children",
       caption = "Routed walk-and-bus times, weekday morning arrival.") +
  theme_bh()
Figure 21: The two measures against each other. Neighbourhoods along the bottom can reach no places at all inside 30 minutes.
WarningTwo caveats that bear on these numbers

Journey times are modelled, not measured. They come from r5r routing over the merged street network and published timetables for one weekday morning. They do not know about school buses, parental lifts, walking routes children actually use, or a bus that is full when it arrives.

One school’s times are suspect. Dorothy Stringer and Varndean are 470 metres apart, yet the routed matrix makes Stringer slower from most of the city by a median of about five and a half minutes. That may be real, or it may be an artefact of where the router snapped each school onto the network. It has not been established which, and it inflates the apparent inaccessibility of the areas whose nearest school is Stringer.

4.6 Accessibility and child poverty together

The question that matters for policy is not where access is poor, nor where deprivation is high, but where the two coincide — because those are the neighbourhoods where a difficult journey is least likely to be solved by a car in the family.

Show code
# Stevens-style bivariate palette, keyed "access-deprivation". Both
# terciles are oriented worst-to-best, so 1-1 is the concerning corner
# and gets the darkest colour, and 3-3 is the comfortable one and gets
# the lightest. Getting this the wrong way round is the classic way to
# misread a bivariate map, so the legend below spells out both axes.
BIV <- c(
  "1-1" = "#3b4994", "1-2" = "#8c62aa", "1-3" = "#be64ac",
  "2-1" = "#5698b9", "2-2" = "#a5add3", "2-3" = "#dfb0d6",
  "3-1" = "#5ac8c8", "3-2" = "#ace4e4", "3-3" = "#e8e8e8")

TER_ACC <- c("least reachable", "middle", "most reachable")
TER_DEP <- c("most deprived", "middle", "least deprived")

biv_map <- lsoa_lite %>%
  inner_join(acc$bivariate %>%
               select(lsoa, acc_t, dep_t, biv_key, idaci_score,
                      A_index, places_30, Oi),
             by = c("lsoa21cd" = "lsoa")) %>%
  mutate(fill = unname(BIV[biv_key]))

biv_lab <- sprintf(
  paste0("<b>%s</b><br>Accessibility: %s (index %.0f)",
         "<br>Deprivation: %s (IDACI %.2f)",
         "<br>%.0f places within 30 min | about %.0f children"),
  biv_map$lsoa21nm, TER_ACC[biv_map$acc_t], biv_map$A_index,
  TER_DEP[biv_map$dep_t], biv_map$idaci_score,
  biv_map$places_30, biv_map$Oi) %>%
  lapply(htmltools::HTML)

# The 3 x 3 key, drawn as HTML so it can sit on the map with both axes
# labelled in words rather than as tercile numbers.
key_cells <- paste0(
  sapply(3:1, function(a) paste0(
    '<tr>',
    if (a == 3) '<td rowspan="3" style="writing-mode:vertical-rl;transform:rotate(180deg);font-size:10px;padding-right:3px;color:#444">More reachable &rarr;</td>' else '',
    paste0(sapply(1:3, function(d) sprintf(
      '<td style="width:20px;height:20px;background:%s" title="%s / %s"></td>',
      BIV[[paste0(a, "-", d)]], TER_ACC[a], TER_DEP[d])), collapse = ''),
    '</tr>'), USE.NAMES = FALSE), collapse = '')

biv_legend <- paste0(
  '<div style="background:white;padding:8px 10px;border-radius:4px;',
  'box-shadow:0 1px 5px rgba(0,0,0,.3);font-family:sans-serif;font-size:11px">',
  '<div style="font-weight:bold;margin-bottom:5px">Both axes: worst &rarr; best</div>',
  '<table style="border-collapse:collapse">', key_cells, '</table>',
  '<div style="font-size:10px;color:#444;margin-top:3px;padding-left:16px">',
  'Less deprived &rarr;</div>',
  '<div style="margin-top:6px;font-size:10px;color:#666;max-width:150px">',
  'Dark blue, bottom left: least reachable <i>and</i> most deprived.',
  '</div></div>')

leaflet(width = "100%", height = 600) %>%
  add_basemap() %>%
  addPolygons(data = biv_map, group = "Accessibility x deprivation",
              fillColor = ~fill, fillOpacity = 0.85,
              color = "white", weight = 0.3,
              label = biv_lab) %>%
  addPolygons(data = catch, group = "Catchment boundaries",
              fill = FALSE, color = "#111111", weight = 2.2, opacity = 0.85,
              label = ~unname(CATCH_LABELS[catchment])) %>%
  addLayersControl(
    overlayGroups = c("Accessibility x deprivation", "Catchment boundaries"),
    options = layersControlOptions(collapsed = FALSE)) %>%
  hideGroup("Catchment boundaries") %>%
  addControl(html = biv_legend, position = "bottomright")
Figure 22: Accessibility and income deprivation affecting children, mapped together. Both axes run worst to best, so the darkest blue is the corner that matters: least reachable school places and highest child poverty at the same time. Hover for a neighbourhood’s figures on both.

There is a real gradient here, and it runs the wrong way. Accessibility and child poverty correlate at -0.33 (Spearman): the more deprived a neighbourhood, the less school it can reach.

25 neighbourhoods — about 400 cohort-aged children — sit in the worst corner, in the least accessible third of the city and the most deprived third at the same time. At the comfortable end, 27 neighbourhoods (441 children) are in the best third on both.

The diagonal is what makes this a finding rather than an artefact. Only 11 neighbourhoods are in the most deprived third and the best-served third — fewer than half the 25 in the worst corner. Deprivation and poor access are not merely coincident in this city; they line up.

This is the strongest argument in the document for treating transport as a school-admissions instrument rather than a separate departmental concern. A city that wanted to widen real choice for its poorest families could do more with a bus timetable than with a catchment boundary — and unlike a boundary change, it would take nothing away from anyone else.

NoteWhat pupil-level data would add here

This section works entirely with modelled journey times between neighbourhood centroids and school gates. The council holds the home postcode of every applicant and the school each was allocated. With that, the same analysis could be run on journeys children actually make rather than journeys they could in principle make — and the gap between the two is precisely the quantity that would tell the city whether its transport network is a real constraint on choice or merely a theoretical one. Section 9 sets out what that request would look like.

5 Admissions rules, and what families actually do

5.1 How a place is allocated

Families name up to three schools in order. Each school then ranks its applicants against its own criteria, and the authority runs an equal-preference matching so that a child is offered the highest-ranked school that will take them. The catchment criterion sits partway down most schools’ lists, below looked-after children and siblings and, since 2023, below a quota for children eligible for free school meals.

The critical point is one the rules themselves obscure. A criterion only does something if a school has to turn someone away.

Show code
conv <- bh_data("adjudicator_conversion.rds")

rat <- conv$conv %>%
  group_by(school) %>%
  summarise(p1_named = sum(p1_named), p1_offered = sum(p1_offered),
            .groups = "drop") %>%
  mutate(rate = p1_offered / p1_named,
         rations = rate < 0.999) %>%
  arrange(rate)

ggplot(rat, aes(reorder(school, rate), rate, fill = rations)) +
  geom_col(width = 0.7) +
  geom_hline(yintercept = 1, colour = "grey40", linetype = "31") +
  geom_text(aes(label = fmt_pct(100 * rate, 0)), hjust = -0.15, size = 3.2) +
  coord_flip() +
  scale_y_continuous(labels = label_percent(), limits = c(0, 1.12),
                     breaks = seq(0, 1, 0.25)) +
  scale_fill_manual(values = c(`TRUE` = "#b2182b", `FALSE` = "#4d9221"),
                    labels = c(`TRUE` = "Rations places",
                               `FALSE` = "Turns nobody away"), name = NULL) +
  labs(x = NULL, y = "First preferences that received an offer",
       title = "Only some of these schools ration anything",
       subtitle = "Pooled across three admissions rounds",
       caption = "Source: BHCC evidence to the Schools Adjudicator.") +
  theme_bh() +
  theme(panel.grid.major.y = element_blank())
Figure 23: For each school, the proportion of first-preference applicants who were offered a place. A school at 100% turned nobody away, so its admission criteria did no work at all.

3 of the 10 schools offered a place to every single first-preference applicant. For those schools — Brighton Aldridge Community Academy, Longhill High School, Portslade Aldridge Community Academy — the catchment criterion, the sibling criterion and the free school meals quota are all inert. They admit everyone who asks.

The city therefore runs two admissions systems under one set of rules. In 7 schools the criteria bind and the lottery matters. In the other 3 they are decoration. Debate about admissions criteria is, in practice, debate about a minority of the city’s schools — and never about the schools with the most spare capacity.

Since 2026/27 one more criterion sits in the list: priority 6, a share of places at the community schools for children from the four single-school catchments, set at 5% after a first proposal of 20%. It is meant to widen access. 1 finds that, in the full model, the larger that share, the more segregated the city’s intakes become.

5.2 Sixteen years of demand

Show code
fp <- bh_data("factsheet_panel.rds")

fs <- fp$factsheets %>%
  filter(!is.na(pref1), name != "Total") %>%
  mutate(kind = ifelse(name %in% c("King's School", "Cardinal Newman Catholic School"),
                       "Faith schools (city-wide)", "Catchment schools"))

ggplot(fs, aes(year, pref1, colour = name)) +
  geom_line(linewidth = 0.95) +
  geom_point(size = 1.4) +
  facet_wrap(~ kind, ncol = 1, scales = "free_y") +
  scale_colour_brewer(palette = "Paired", name = NULL) +
  labs(x = NULL, y = "First preferences",
       title = "Demand has moved a long way between schools",
       subtitle = "First preferences expressed, by school",
       caption = "Source: BHCC published allocation factsheets.") +
  theme_bh() +
  theme(legend.position = "right", legend.text = element_text(size = 8))
Figure 24: First preferences expressed for each school, 2010 onwards. The two church schools admit city-wide and are shown separately.

5.3 Who gained and who lost

Comparing each school’s first five years in the published series with its last, the largest falls are at Blatchington Mill School and Dorothy Stringer School; the largest rises at King’s School and Varndean School. Figure 25 shows how each school got there. This is not a system in equilibrium. It is one where a shrinking cohort is being redistributed towards a small number of schools, and away from others, faster than the cohort itself is shrinking.

Show code
base_w <- fs_w %>%
  filter(year <= yr_rng[1] + 4) %>%
  group_by(name) %>% summarise(base = mean(wpref), .groups = "drop")

traj <- fs_w %>%
  inner_join(base_w, by = "name") %>%
  mutate(idx = 100 * wpref / base,
         kind = ifelse(name %in% c("King's School", "Cardinal Newman Catholic School"),
                       "Faith schools (city-wide)", "Catchment schools"))

ends <- traj %>% group_by(name) %>% slice_max(year, n = 1) %>% ungroup()

ggplot(traj, aes(year, idx, colour = name)) +
  geom_hline(yintercept = 100, linetype = "31", colour = "grey45") +
  geom_line(linewidth = 0.95) +
  ggrepel::geom_text_repel(
    data = ends, aes(label = name), size = 2.9, hjust = 0,
    direction = "y", nudge_x = 0.6, segment.size = 0.25,
    min.segment.length = 0, max.overlaps = 20, seed = 1) +
  facet_wrap(~ kind, ncol = 1, scales = "free_y") +
  scale_colour_brewer(palette = "Paired", guide = "none") +
  scale_x_continuous(expand = expansion(mult = c(0.02, 0.28))) +
  labs(x = NULL, y = "Weighted preferences, indexed to opening average = 100",
       title = "Where demand has moved under the catchment system",
       subtitle = sprintf(
         "Ranks 1-3 with decay %.2f. Above the dashed line is growth on the %d-%d baseline.\nThe whole series post-dates the 2008 move to catchments with a lottery tie-break.",
         DECAY, yr_rng[1], yr_rng[1] + 4),
       caption = "Source: BHCC published allocation factsheets.") +
  theme_bh() +
  theme(strip.text = element_text(face = "bold", hjust = 0))
Figure 25: Weighted preferences over the published series. Preferences are counted at all three ranks with the same geometric decay used in section 4, so a school that families consistently name second is not scored as though nobody wanted it. Each school is indexed to 100 at its own five-year opening average, so the lines show relative movement rather than size.
NoteWhy this uses weighted preferences rather than first ones

Counting only first preferences would give a different and, in two cases, a misleading picture.

Hove Park loses 38 first preferences between the two windows — which looks survivable — but 101 weighted preferences, a fall of 25%. Most of what it lost was second and third choices, and a first-preference count discards exactly that. A school families have stopped naming at all is in a different position from one they have stopped naming first.

Varndean moves the other way. On first preferences it more than doubles, up 104%; on weighted preferences it rises 33%. It has gained firsts partly by converting seconds it already had — real, but a smaller shift than the headline count suggests, and a predictable artefact of sitting in a paired catchment where families must rank two local schools against each other.

5.4 What families are actually choosing on

The council’s admissions guide points families at published performance data. It is worth asking which published number they respond to.

Show code
# Two panels sharing a set of buttons that swap the preference measure.
# Each panel holds all four measures as hidden trace pairs -- a fit line
# then its points -- so a button is a visibility switch rather than a
# redraw. Measure i therefore sits at traces 2i-1 and 2i within each
# panel's block of eight, which is what btn() below relies on.

# Both panels take the same vertical range, so switching measure does
# not silently rescale one against the other.
ch_range <- function(y) log10(range(y) * c(0.60, 1.60))

# plotly has no repel, and these ten schools cluster tightly enough that
# a fixed left/right rule collides three times per panel even at full
# page width. So each label is tried in six positions and takes the one
# that runs into the fewest markers and already-placed labels, most
# crowded point choosing first. Scoring is in normalised panel
# coordinates against a nominal panel size, so a browser that gives the
# widget a different width degrades the placement rather than breaking
# it. Positions are recomputed per measure, because the buttons move
# every point.
CH_PANEL_PX <- c(w = 495, h = 232)

ch_place <- function(x, y, labs) {
  xr <- range(x) + c(-0.13, 0.13) * diff(range(x))
  yr <- ch_range(y)
  nx <- (x - xr[1]) / diff(xr)
  ny <- (log10(y) - yr[1]) / diff(yr)

  wn <- (nchar(labs) * 5.6 + 6) / CH_PANEL_PX[["w"]]
  hn <- 13 / CH_PANEL_PX[["h"]]
  rx <- 6 / CH_PANEL_PX[["w"]]
  ry <- 6 / CH_PANEL_PX[["h"]]

  cand <- list(
    `middle right`  = function(i) c(nx[i] + rx, ny[i] - hn / 2,
                                    nx[i] + rx + wn[i], ny[i] + hn / 2),
    `middle left`   = function(i) c(nx[i] - rx - wn[i], ny[i] - hn / 2,
                                    nx[i] - rx, ny[i] + hn / 2),
    `top center`    = function(i) c(nx[i] - wn[i] / 2, ny[i] + ry,
                                    nx[i] + wn[i] / 2, ny[i] + ry + hn),
    `bottom center` = function(i) c(nx[i] - wn[i] / 2, ny[i] - ry - hn,
                                    nx[i] + wn[i] / 2, ny[i] - ry),
    `top right`     = function(i) c(nx[i], ny[i] + ry,
                                    nx[i] + wn[i], ny[i] + ry + hn),
    `bottom left`   = function(i) c(nx[i] - wn[i], ny[i] - ry - hn,
                                    nx[i], ny[i] - ry))

  ov <- function(a, b) max(0, min(a[3], b[3]) - max(a[1], b[1])) *
                       max(0, min(a[4], b[4]) - max(a[2], b[2]))
  mk <- lapply(seq_along(x), function(i)
    c(nx[i] - rx, ny[i] - ry, nx[i] + rx, ny[i] + ry))

  # The y span is visually shorter than the x span, so crowding is
  # measured with y weighted down before taking nearest neighbours.
  d <- as.matrix(stats::dist(cbind(nx, ny * 0.6)))
  diag(d) <- Inf

  pos <- character(length(x)); placed <- list()
  for (i in order(apply(d, 1, min))) {
    best <- NULL; best_box <- NULL; best_s <- Inf
    for (nm in names(cand)) {
      b <- cand[[nm]](i)
      s <- 3 * sum(vapply(mk[-i], ov, numeric(1), b = b)) +
           3 * sum(vapply(placed, ov, numeric(1), b = b)) +
           max(0, -b[1]) + max(0, b[3] - 1) +
           max(0, -b[2]) + max(0, b[4] - 1)
      if (s < best_s) { best_s <- s; best <- nm; best_box <- b }
    }
    pos[i] <- best
    placed <- c(placed, list(best_box))
  }
  pos
}

ch_panel <- function(pred, show_ylab) {
  x   <- cd[[pred]]
  col <- CH_COL[[pred]]

  p <- plotly::plot_ly()
  for (m in CH_MEASURES) {
    f    <- ch_one(m, pred)
    tpos <- ch_place(x, cd[[m]], cd$short)
    p <- p %>%
      plotly::add_lines(
        x = f$x, y = f$y, visible = (m == CH_DEFAULT), showlegend = FALSE,
        hoverinfo = "skip", color = I("#8c8c8c"),
        line = list(width = 1.4, dash = "dash")) %>%
      plotly::add_trace(
        x = x, y = cd[[m]], visible = (m == CH_DEFAULT), showlegend = FALSE,
        type = "scatter", mode = "markers+text", color = I(col),
        text = cd$short, textposition = tpos,
        textfont = list(size = 10, color = "#333333"),
        marker = list(size = 9, line = list(color = "white", width = 1)),
        customdata = cd$school,
        hovertemplate = paste0("<b>%{customdata}</b><br>", CH_PRED[[pred]],
                               ": %{x:.1f}<br>%{y:.2f} ",
                               if (m == "M5 attractiveness")
                                 "attractiveness (M5, city mean 1)"
                               else "preferences per place",
                               "<extra></extra>"))
  }

  plotly::layout(
    p,
    xaxis = list(title = list(text = CH_PRED[[pred]], font = list(size = 11.5)),
                 zeroline = FALSE, gridcolor = "#eeeeee",
                 range = range(x) + c(-0.13, 0.13) * diff(range(x))),
    yaxis = list(title = list(
                   text = if (show_ylab) "Preferences per place" else "",
                   font = list(size = 11.5)),
                 type = "log", zeroline = FALSE, gridcolor = "#eeeeee",
                 tickvals = c(0.1, 0.15, 0.25, 0.4, 0.6, 1, 1.5, 2.5, 4),
                 ticktext = c("0.1", "0.15", "0.25", "0.4", "0.6",
                              "1.0", "1.5", "2.5", "4.0")))
}

ch_ann <- function(m) list(
  list(x = 0, y = 1.30, xref = "paper", yref = "paper",
       xanchor = "left", yanchor = "top", showarrow = FALSE,
       font = list(size = 11.5, color = "#444444"),
       text = "Preferences counted:"),
  list(x = 0.01, y = 1.01, xref = "paper", yref = "paper",
       xanchor = "left", yanchor = "bottom", showarrow = FALSE,
       font = list(size = 12.5, color = CH_COL[["att8"]]),
       text = sprintf("<b>Headline attainment · R² = %.2f</b>",
                      ch_r2(m, "att8"))),
  list(x = 0.55, y = 1.01, xref = "paper", yref = "paper",
       xanchor = "left", yanchor = "bottom", showarrow = FALSE,
       font = list(size = 12.5, color = CH_COL[["va"]]),
       text = sprintf("<b>Value added · R² = %.2f</b>",
                      ch_r2(m, "va"))))

# Each panel holds a fit line and a point trace per measure, so the
# second panel's block starts after 2 x the number of measures.
ch_btn <- function(i) {
  n <- length(CH_MEASURES)
  vis <- rep(FALSE, 4 * n)
  vis[c((2 * i - 1):(2 * i), 2 * n + (2 * i - 1):(2 * i))] <- TRUE
  rg <- ch_range(cd[[CH_MEASURES[i]]])
  list(method = "update", label = CH_BUTTONS[i],
       args = list(list(visible = vis),
                   list(annotations = ch_ann(CH_MEASURES[i]),
                        yaxis.range = rg, yaxis2.range = rg,
                        yaxis.title.text = if (CH_MEASURES[i] == "M5 attractiveness")
                          "Attractiveness, M5 (city mean 1)" else "Preferences per place")))
}

plotly::subplot(ch_panel("att8", TRUE), ch_panel("va", FALSE),
                nrows = 1, shareY = FALSE, titleX = TRUE, titleY = TRUE,
                margin = 0.055) %>%
  plotly::layout(
    annotations = ch_ann(CH_DEFAULT),
    yaxis = list(range = ch_range(cd[[CH_DEFAULT]])),
    yaxis2 = list(range = ch_range(cd[[CH_DEFAULT]])),
    margin = list(t = 104, b = 54, l = 64, r = 14),
    hoverlabel = list(bgcolor = "white"),
    updatemenus = list(list(
      type = "buttons", direction = "right",
      active = which(CH_MEASURES == CH_DEFAULT) - 1,
      x = 0.135, xanchor = "left", y = 1.36, yanchor = "top",
      pad = list(t = 0, b = 0, l = 2, r = 2),
      bgcolor = "white", bordercolor = "#cccccc",
      font = list(size = 11),
      buttons = lapply(seq_along(CH_MEASURES), ch_btn)))) %>%
  plotly::config(displayModeBar = FALSE)
Figure 26: Preferences per place against headline attainment and against value added, for the ten Brighton & Hove secondary schools. Use the buttons to change which preference rank is counted, or switch to the full model’s attractiveness (M5). Five-year means; the vertical scale is logarithmic and the dashed line is the fitted log-linear relationship.
Show code
CH_FITS %>%
  mutate(r2 = sprintf("%.2f", r2),
         pred = unname(CH_PRED[pred])) %>%
  pivot_wider(id_cols = measure, names_from = pred, values_from = r2) %>%
  mutate(measure = factor(measure, CH_MEASURES)) %>%
  arrange(measure) %>%
  rename(`Preferences counted` = measure) %>%
  knitr::kable(align = "lrr")
Table 10: How much of each preference measure, and of the full model’s attractiveness, the two attainment measures explain. Log-linear fits on ten schools, so these are descriptive rather than inferential.
Preferences counted Attainment 8 (headline) Value added (Lever report)
Weighted (ranks 1-3) 0.84 0.20
First preferences 0.80 0.12
Second preferences 0.69 0.40
Third preferences 0.55 0.01
M5 attractiveness 0.59 0.03

The result is stark. Counting preferences the way section 4.5 does, headline Attainment 8 explains 84% of the variation in how heavily each school is preferred. The value-added measure — the one that actually isolates what the school contributes — explains 20%. Adding value added to the attainment model lifts the fit from 0.84 to 0.85, which is to say barely at all: once you know a school’s headline score, knowing what it contributes tells you almost nothing more about how heavily families ask for it.

Families are choosing on a number that is mostly a description of the existing intake. That makes school choice partly self-fulfilling: a school with a difficult intake posts a low headline score, is chosen less, and receives a still more difficult intake next year. Section 2 showed that Hove Park and BACA are among the schools most penalised by this conflation. This is a communications problem before it is an admissions problem, and the authority is not a neutral party in it, because the authority publishes the guide.

These are the same figures the open Brightopia model publishes, on the same specification and the same five-round window; the render fails if the two ever drift apart.

The same question, asked of the full model’s attractiveness. Section 7 builds a different measure of how much families want each school: the attractiveness M5 balances so that, once distance, the catchment term and competition between schools have done their work, the model’s demand for each school matches its share of the city’s first preferences (Section 7.8.3.2). It is the number the simulator’s attractiveness sliders multiply, and the M5 button on the chart above shows it against the same two scores.

On a log scale it follows headline attainment, R² 0.59, and value added not at all, 0.03. The log scale is the right one. A straight line explains only 0.39, and on the log scale each point of Attainment 8 goes with about 12% more attractiveness — a constant proportion, as with preferences per place, rather than a fixed amount. Refitting without each school in turn gives R² between 0.42 and 0.76. The simulator converts its attractiveness sliders into Attainment 8 points on this fit.

It is a looser fit than preferences per place, and the reason is worth seeing. M5’s attractiveness is what is left once geography and the catchment term have been accounted for, so the two faith schools, which have no catchment term to carry their demand, sit far above the rest — Cardinal Newman at 3.3 times the city mean and King’s at 2.5 — while Dorothy Stringer, at 0.54, and Hove Park, at 0.34, sit low because the catchment term already explains most of their demand. Across the eight catchment schools alone, attainment explains 0.48. Either way the finding stands: families ask for the headline score, not for what a school adds.

NoteOne round is not enough to carry a sign

The model used to fit this regression to the single 2024 admissions round, and on that round value added takes a negative coefficient once attainment is already in the model — which reads as families actively avoiding schools that add value.

Across the five rounds it does not: the coefficient is +0.033, small and positive. The negative sign was one year, ten schools, and no basis for that reading. It is recorded here because it was briefly in an earlier draft, and because the same trap is available to anyone reading a single year’s factsheet.

NoteWhat changes when you count a different preference

Section 4.5 argued that first preferences alone distort the picture, because the paired catchments force families to rank two local schools against each other. The buttons on the chart above are there so that objection can be tested rather than argued about, and Table 10 holds all eight fits at once.

The answer is that the finding survives, it strengthens, and there is one exception worth dwelling on.

It strengthens. The weighted measure gives the strongest association of all with headline attainment, R² 0.84 against 0.80 for first preferences alone. Counting the other ranks does not dilute the finding; it sharpens it. So the association is not an artefact of looking only at firsts — if anything, firsts understate it.

Attainment’s grip weakens down the ranks. It explains 0.80 of first preferences, 0.69 of seconds and 0.55 of thirds. That is what you would expect if the headline score is what families reach for when choosing freely, while lower preferences are increasingly shaped by catchment structure and proximity.

And then the exception. At second preferences, and only there, value added explains a substantial share — 0.40, against 0.12 on firsts and 0.01 on thirds, which is nothing at all.

That is robust to dropping any one school — refitting ten times without each in turn gives R² between 0.29 and 0.59, so it is not one point doing the work. What it means is a different question, and an honest answer is that this cannot settle it. A second preference is chosen with the first already committed, often between two schools in the same paired catchment that a family knows something about beyond the league table. Whether that is families accessing better information at the margin, or something about how paired catchments generate second preferences in the first place, ten schools cannot say.

WarningTen points

Every fit here is on ten schools. That is enough to show an association and nowhere near enough to separate attainment from the things correlated with it — intake, location, reputation, or the Ofsted grade families may actually be reading. Nothing here identifies what causes what.

The point is narrower: the relationship with the headline score is not an artefact of which preference rank you count. That is worth establishing, because it is the obvious objection to the chart above, and it is the objection the council could most easily raise.

The second-preference result should be read as a question rather than a finding. It is the kind of thing the council’s own preference records would settle in an afternoon, and section 9 asks for them.

5.5 What pupil-level data would add here

This is a school-level association across ten schools — ten points. It cannot distinguish families responding to attainment from families responding to something correlated with it, and it cannot say whether different kinds of family respond differently. The council holds the preference ordering of every applicant. With it, the same question becomes a discrete-choice model over thousands of decisions, and the answer would be worth acting on rather than merely worth noting.

6 Money

A school’s finances follow its roll, because funding is overwhelmingly per-pupil. A school losing children is therefore losing income against a cost base that does not shrink at the same rate — the building, the leadership team and the curriculum offer are all substantially fixed.

That is the mechanism. This section asks what it has already done to the city’s ten state secondary schools, and what the projections in Section 3 will do to them next.

6.1 Nine of the ten schools are spending their reserves

Across the three years every school has filed, the city’s ten secondary schools spent £2.3m of their combined revenue reserves — from £5.9m in 2021-22 to £3.6m in 2023-24, a fall of 39%. Schools with a negative reserve went from 1 to 3, and schools spending more in the year than they received went from 2 to 6.

9 of the 10 schools ran their reserve down over the period. 1 did not.

The 7 maintained schools have filed a fourth year, and it is worse: their combined reserve went from £4.6m in 2021-22 to −£395k in 2024-25, and their combined in-year balance from £1.3m to −£2.4m.

NoteThat aggregate is carried by two schools, and it matters which

The combined figure crosses zero because of Cardinal Newman alone. Take it out and the other 6 maintained schools still hold £1.4m between them.

The aggregate is worth reporting because it is the sum the council is responsible for, but the distribution is the finding. Two schools are in serious deficit, one is at the line, and the rest are comfortable and getting less so.

Show code
fex %>%
  arrange(reserve_pct) %>%
  transmute(School = name,
            Year = year_label,
            Roll = fmt_n(roll),
            Income = gbp(income),
            `Per pupil` = paste0("£", fmt_n(income_pp)),
            # Two columns called "— % of income" is one column: the
            # second silently replaced the first, and the table printed
            # the reserve percentage in the in-year column.
            `In-year balance` = gbp(balance_pct * income),
            `In-year, % of income` = pp(balance_pct),
            `Revenue reserve` = gbp(reserve),
            `Reserve, % of income` = pp(reserve_pct),
            `Staff costs, % of income` = sprintf("%.0f%%", 100 * staff_pct)) %>%
  knitr::kable(align = "llrrrrrrrr")
Table 11: Every state secondary school in the city, at its latest published return. Maintained schools report to 31 March on the Consistent Financial Reporting return; academies report to 31 August on the Academies Accounts Return, so their latest published year is one behind. The revenue reserve is the accumulated surplus or deficit carried forward; it is shown as a share of annual income, because a given sum of money means different things at a school of 723 and a school of 2,625. Ranked worst first.
School Year Roll Income Per pupil In-year balance In-year, % of income Revenue reserve Reserve, % of income Staff costs, % of income
Hove Park School and Sixth Form Centre 2024-25 948 £9.6m £10,083 −£546k −5.7% −£1.1m −11.0% 83%
Cardinal Newman Catholic School 2024-25 2,625 £17.6m £6,704 −£1.7m −9.9% −£1.8m −10.1% 91%
Patcham High School 2024-25 1,099 £9.0m £8,221 £371k +4.1% −£82k −0.9% 70%
Brighton Aldridge Community Academy 2023-24 802 £7.2m £8,929 £383k +5.3% £1k +0.0% 71%
Blatchington Mill School 2024-25 1,635 £12.4m £7,611 −£246k −2.0% £492k +4.0% 75%
Varndean School 2024-25 1,497 £12.2m £8,173 −£151k −1.2% £662k +5.4% 70%
Dorothy Stringer School 2024-25 1,626 £13.2m £8,134 £37k +0.3% £831k +6.3% 76%
Portslade Aldridge Community Academy 2023-24 974 £7.7m £7,859 £791k +10.3% £534k +7.0% 68%
Longhill High School 2024-25 723 £7.5m £10,358 −£168k −2.2% £531k +7.1% 76%
King’s School 2023-24 876 £6.5m £7,365 £795k +12.3% £1.0m +15.7% 71%

Source: DfE school income and expenditure — Consistent Financial Reporting (CFR) returns for maintained schools and Academies Accounts Returns (AAR) for academies, both published per school on the schools financial benchmarking service.

NoteWhy the year is not the same for every school

Every school files a return every year. The years differ here for two reasons, neither of them a gap in what schools publish.

They have different year ends. A maintained school reports on the CFR return to 31 March. An academy reports on the AAR to 31 August, because that is its financial year. A 2024-25 figure therefore means something slightly different on the two rows, and there is no way to make them the same without throwing away half the data.

The academy returns are published later in the cycle. The national finance panel behind this section was assembled on 10 September 2026 and contains CFR returns up to 2024-25 but no AAR returns for that year at all — not for Brighton and not for anywhere, nationally. So the 3 academies here (Brighton Aldridge, King’s, and Portslade Aldridge) show 2023-24. If a newer release is out by the time you read this, re-running R/01_assemble.R picks it up and the table closes.

The consequence for reading the table is small but real: the academies’ figures are a year staler, and a year in which several of these schools moved. It is one reason the funding allocations below, which are the same year for everybody, are worth having alongside.

Two schools are in serious deficit. Hove Park’s revenue reserve is −£1.1m, or −11.0% of its annual income, and it spent £546k more than it received in 2024-25. Cardinal Newman’s reserve is −£1.8m, or −10.1%, and its in-year deficit was £1.7m.

They are in trouble for opposite reasons, and the difference is the whole argument of this section. Hove Park’s roll fell from 1,170 to 948 over four years — 222 pupils, each of them carrying funding out of the building. Cardinal Newman’s roll rose, by 175. It is the largest school in the city and it is growing, and it is still the deepest deficit in the city.

The number that separates them from everybody else is the last column. Staff costs are 91% of income at Cardinal Newman and 83% at Hove Park, against 73% across the city. A school whose staffing is a fifth higher, as a share of income, than its neighbours’ has no room to absorb anything, and staffing is the cost that takes years and redundancies to move.

Show code
# Deficit is a status, not a category or a magnitude, so it takes a
# status colour and it ships with a label rather than being left as
# "the red one".
DEFICIT_COL <- "#d03b3b"

ord <- fex %>% arrange(reserve_pct) %>% pull(name)
rp <- fpan %>%
  mutate(school = factor(short_sch(name), short_sch(ord)),
         yr = year,
         neg = pmin(reserve_pct, 0))

ggplot(rp, aes(yr, reserve_pct)) +
  geom_hline(yintercept = 0, colour = "grey40", linewidth = 0.4) +
  geom_ribbon(aes(ymin = neg, ymax = 0), fill = DEFICIT_COL, alpha = 0.28) +
  geom_line(linewidth = 0.8, colour = "grey20") +
  geom_point(size = 1.1, colour = "grey20") +
  facet_wrap(~ school, ncol = 5) +
  scale_x_continuous(breaks = sort(unique(rp$yr)),
                     labels = function(x) paste0(substr(x, 3, 4), "/",
                                                 substr(x + 1, 3, 4))) +
  scale_y_continuous(labels = scales::label_percent()) +
  labs(x = NULL, y = "Revenue reserve as a share of annual income",
       title = "Every school but one has been spending its reserve",
       subtitle = str_wrap(paste("Shaded years are years the school was carrying a deficit.", "The line is the accumulated surplus, not the in-year balance."), 100),
       caption = "Source: DfE school income and expenditure (CFR returns for maintained schools, academy accounts returns for academies).") +
  theme_bh(9) +
  theme(axis.text.x = element_text(size = 6),
        panel.spacing.x = unit(7, "pt"))
Figure 27: Revenue reserve as a share of annual income, for every state secondary school in the city. Panels are ordered by the latest figure, worst first. Shading marks the years a school was carrying a deficit. The academies’ series ends a year earlier because their accounts are published on a different cycle — see the note under the table above.

Published per school on the schools financial benchmarking service.

6.2 Small schools receive more per pupil and are still likelier to be in deficit

The national picture says why a shrinking school is a financial problem rather than merely a smaller one.

Show code
sf_ <- os$size_fin %>%
  mutate(size_band = factor(size_band, levels = size_band))

pa <- ggplot(sf_, aes(size_band, income_pp)) +
  geom_col(fill = "#2166ac", width = 0.7) +
  scale_y_continuous(labels = label_dollar(prefix = "£", accuracy = 1)) +
  labs(x = NULL, y = NULL, subtitle = "Income per pupil") +
  theme_bh(11) + theme(axis.text.x = element_text(angle = 40, hjust = 1))

pb <- ggplot(sf_, aes(size_band, pct_deficit)) +
  geom_col(fill = "#b2182b", width = 0.7) +
  scale_y_continuous(labels = label_percent(scale = 1)) +
  labs(x = NULL, y = NULL, subtitle = "Schools in deficit") +
  theme_bh(11) + theme(axis.text.x = element_text(angle = 40, hjust = 1))

(pa | pb) +
  plot_annotation(
    title = "Small schools receive more per pupil and are still likelier to be in deficit",
    subtitle = "Secondary schools in England, by roll",
    caption = "Source: DfE school income and expenditure data.",
    theme = theme_bh())
Figure 28: Income per pupil and the proportion of schools in deficit, by size band, nationally.

Small schools are not starved of money per head — they receive 1,069 more per pupil than the largest band. They are nonetheless the likeliest to be in deficit: 38% of schools under 500 pupils, against 24% at the most favourable size. Extra funding per head does not compensate for the loss of scale.

6.2.1 The same argument, for Brighton’s own schools

The national picture is a correlation across thousands of schools. The funding formula says why, and it says it in cash, for these ten schools, in a single year.

Show code
fund %>%
  left_join(fex %>% select(urn, name), by = "urn") %>%
  arrange(fund_pupils) %>%
  transmute(School = name,
            `Pupils, 5–16` = fmt_n(fund_pupils),
            `Block allocation` = gbp(block),
            # Two columns with the same name is one column: dplyr keeps
            # the second and drops the first, silently. This table had
            # two called "— per pupil" until the render was read.
            `Follows pupils` = gbp(pupil_led),
            `Pupil-led, per pupil` = paste0("£", fmt_n(marginal_pp)),
            `Does not follow pupils` = gbp(fixed),
            `Fixed, per pupil` = paste0("£", fmt_n(fixed_pp)),
            `of which lump sum` = paste0("£", fmt_n(lump_pp))) %>%
  knitr::kable(align = "lrrrrrrr")
Table 12: The 2025-26 schools block allocation, split into the money that follows pupils and the money that does not. The lump sum is a flat cash amount paid to every secondary school whatever its size; rates, PFI and split-site payments belong to the site rather than the roll. Covers ages 5 to 16, so sixth forms are not in it. Ranked smallest school first.
School Pupils, 5–16 Block allocation Follows pupils Pupil-led, per pupil Does not follow pupils Fixed, per pupil of which lump sum
Brighton Aldridge Community Academy 700 £5.7m £5.5m £7,889 £152k £217 £217
Longhill High School 719 £5.7m £5.3m £7,440 £342k £475 £212
King’s School 867 £5.8m £5.7m £6,548 £152k £175 £175
Hove Park School and Sixth Form Centre 949 £7.5m £7.0m £7,331 £527k £555 £160
Portslade Aldridge Community Academy 950 £6.8m £6.7m £7,026 £152k £160 £160
Patcham High School 1,107 £7.8m £7.4m £6,675 £444k £401 £137
Varndean School 1,493 £10.4m £9.9m £6,643 £511k £342 £102
Blatchington Mill School 1,626 £11.1m £10.7m £6,570 £428k £263 £94
Dorothy Stringer School 1,643 £11.6m £11.1m £6,744 £566k £344 £93
Cardinal Newman Catholic School 1,851 £12.7m £12.4m £6,707 £236k £128 £82

Source: DfE school funding statistics, school-level allocations for 2025-26.

Every secondary school in England gets the same lump sum, and this year it is £152,113. Spread over 1,851 pupils at Cardinal Newman that is £82 a head. Spread over 700 at Brighton Aldridge it is £217. The smaller school is better funded per pupil, which is the formula working as designed — and it still has to run a full curriculum, a leadership team and a building on £5.7m against £12.7m.

This is also the number that matters for what a lost child costs. It is not a school’s average funding per pupil, which is what an income statement shows. A departing Year 7 takes the pupil-led money with them — between £6,548 and £7,889 a year across these ten schools — and leaves every pound of the lump sum, the rates and the PFI behind, to be carried by the children who remain. Section 6.3 uses the pupil-led rate for exactly that reason.

6.3 What the projections do to this

Section 3 projects the cohort. Section 7 turns it into intakes school by school. Putting the two next to the accounts gives the question this section exists to answer: which schools are heading into a smaller roll from a weak financial position.

The arithmetic is deliberately crude, and it is stated so that a reader can redo it. Each Year 7 place a school loses takes 5 pupils off its roll once the smaller cohort has worked through. Multiplied by that school’s own income per pupil, that is the annual income it is heading for in today’s money.

ImportantThe baseline is the intake that happened, not the modelled one

An earlier version of this section took the modelled 2026 intake as each school’s starting point. That was wrong, and the error was not small.

The model places every child in the projected cohort somewhere in the city. The schools that are full absorb only their admission number, so the surplus lands on the schools that are not full — which is exactly where this section is looking. Against the offers the council actually made for September 2026 it puts 183 children into Longhill, which admitted 81, and 197 into Cardinal Newman, which admitted 360.

In total it is close: 2,302 modelled against 2,250 offered across the ten schools, 2% out. The error is in the distribution, which is a fair thing to ask of a model with no calibration data and a fatal thing to build a financial table on.

So the baseline below is the published offer count, and the model supplies only the trajectory — the proportional change in a school’s intake to 2035, driven by the cohort in its catchment shrinking. Read the “modelled” column as a diagnostic, not as an intake.

The city has 2,515 Year 7 places in these ten schools and made 2,250 offers for 2026 entry. On the model’s trajectory that becomes 1,792 by 2035 — a fill rate falling from 89% to 71%, and 723 empty places. Carried through to the whole roll at each school’s own funding rate, that is about £15.7m a year of income gone from a system that currently receives £102.9m.

Show code
xr <- range(fex$intake_pct)
PT_MAX <- 11

ggplot(fex, aes(intake_pct, reserve_pct)) +
  annotate("rect", xmin = -Inf, xmax = 0, ymin = -Inf,
           ymax = sfin$reserve_thin, fill = DEFICIT_COL, alpha = 0.07) +
  # The shaded corner needs naming inside the plot, but the obvious
  # place for the label is on top of Longhill. It goes in the empty band
  # to the left of Patcham instead.
  annotate("text", x = xr[1] - 0.05, y = -0.055,
           label = "Thin reserve,\nand a shrinking intake", hjust = 0,
           vjust = 1, size = 3, lineheight = 0.95, colour = DEFICIT_COL,
           fontface = "bold") +
  geom_hline(yintercept = 0, colour = "grey45", linewidth = 0.4) +
  geom_hline(yintercept = sfin$reserve_thin, colour = "grey65",
             linetype = "31") +
  geom_vline(xintercept = 0, colour = "grey65", linetype = "31") +
  # Filled for the eight schools whose modelled intake can be read as a
  # projection, hollow for the two whose cannot. Setting fill outside
  # aes() drew every circle white and lost the distinction entirely.
  geom_point(aes(size = roll, fill = faith), shape = 21,
             colour = "#1f3b57", stroke = 0.9) +
  # ggrepel does not know how big a point is unless it is told, and it
  # wants that size in the same millimetres the point scale uses, not in
  # pupils: handed the raw roll it treated every circle as enormous and
  # threw the labels to the edges of the panel. scale_size_area() draws
  # a point at max_size * sqrt(value / max(value)), so that is what it
  # is given.
  ggrepel::geom_text_repel(
    aes(label = short, point.size = PT_MAX * sqrt(roll / max(roll))),
    size = 3, seed = 3, min.segment.length = 0, segment.size = 0.25,
    box.padding = 0.5, max.overlaps = Inf, colour = "grey15") +
  scale_fill_manual(values = c(`FALSE` = "#1f3b57", `TRUE` = "white"),
                    guide = "none") +
  scale_size_area(max_size = PT_MAX, guide = "none") +
  scale_x_continuous(labels = scales::label_percent(),
                     limits = c(xr[1] - 0.07, 0.07)) +
  scale_y_continuous(labels = scales::label_percent()) +
  labs(x = "Change in modelled Year 7 intake, 2026 to 2035",
       y = "Revenue reserve as a share of annual income",
       title = "Where the money is, and where the children are going",
       subtitle = str_wrap(paste("Circle area is the school's roll.", "Hollow circles are the two faith schools, whose modelled intake should not be read as a forecast."), 100),
       caption = "Sources: DfE school income and expenditure; modelled intakes, configuration A of the open scenarios.") +
  theme_bh(11)
Figure 29: Financial position against projected intake. The vertical axis is the revenue reserve as a share of annual income at the latest published return; the horizontal axis is the change in modelled Year 7 intake between 2026 and 2035 under the city as it stands. Circle area is proportional to the school’s roll. The shaded corner is a thin or negative reserve together with a shrinking intake. Hollow circles are the two faith schools, whose modelled intake is the least reliable figure in this document.
Show code
fex %>%
  arrange(income_pct) %>%
  transmute(School = paste0(name, if_else(faith, " †", "")),
            `Reserve, % of income` = pp(reserve_pct),
            `Offers 2026` = fmt_n(offers),
            `Modelled 2026` = fmt_n(intake_first),
            `Projected 2035` = fmt_n(intake_proj),
            Change = pp(intake_pct, 0),
            `Roll, steady state` = sub("-", "\u2212",
                                       sprintf("%+.0f", roll_change_ss),
                                       fixed = TRUE),
            `Income a year` = gbp(income_change),
            `— % of income` = pp(income_pct, 0)) %>%
  knitr::kable(align = "lrrrrrrrr")
Table 13: What the projected intakes are worth. The baseline is the offers the council published for 2026 entry. The modelled column is shown only so the gap between the two can be seen — it is the model’s own 2026 level, which is not an intake and is badly out for the schools that are not full. The projection applies the model’s proportional change to 2035 to the published baseline. The income column then applies each school’s own income per pupil to the steady-state change in roll, in today’s money: an order of magnitude, not a budget line.
School Reserve, % of income Offers 2026 Modelled 2026 Projected 2035 Change Roll, steady state Income a year — % of income
King’s School † +15.7% 180 163 94 −48% −432 −£2.8m −44%
Cardinal Newman Catholic School † −10.1% 360 197 190 −47% −849 −£5.7m −32%
Brighton Aldridge Community Academy +0.0% 146 173 88 −40% −292 −£2.3m −32%
Longhill High School +7.1% 81 183 51 −36% −148 −£1.1m −15%
Patcham High School −0.9% 204 225 169 −17% −176 −£1.2m −13%
Dorothy Stringer School +6.3% 330 330 283 −14% −235 −£1.6m −12%
Portslade Aldridge Community Academy +7.0% 183 220 171 −7% −62 −£436k −6%
Blatchington Mill School +4.0% 330 330 311 −6% −96 −£633k −5%
Hove Park School and Sixth Form Centre −11.0% 136 180 136 +0% +0 £0 +0%
Varndean School +5.4% 300 300 300 +0% +0 £0 +0%

† Cardinal Newman and King’s are faith schools. The model admits to them on distance alone, because their actual criteria are not in any published dataset, so their intake columns are the least reliable figures in this document. Cardinal Newman is currently over-subscribed and growing.

4 schools sit in the corner that matters — a reserve under 5% of income and an intake the model expects to fall. They are Blatchington Mill, Brighton Aldridge, Cardinal Newman, and Patcham.

Cardinal Newman is the largest school in the city and already −10.1% on its reserve — though it is also the school whose projected intake is least worth trusting. Patcham is at −0.9% with a projected fall of −17%, worth −£1.2m a year, or −13% of its income.

Blatchington Mill is the fourth, and it is there on a technicality worth stating: its reserve is +4.0%, just under the line, and its projected fall is only −6%. It is the least worrying school in the shaded corner and it is in it because the threshold is a line rather than a judgement.

Brighton Aldridge is the most exposed school whose numbers can be trusted. It has spent its reserve to £1k, its intake is projected to fall −40%, and that is worth −£2.3m a year, or −32% of its income.

Longhill is not, and that is the correction the baseline forced. Its remaining exposure is −15%, worth −£1.1m a year — the fourth largest share in the city, not the first. The reason is that Longhill has already taken most of the fall: it admitted 81 children in 2026 against an admission number of 210, and a school at 39% of its admission number has less left to lose than one that is full. Its reserve is still +7.1%, and Section 6.5 is about how long that lasts.

Hove Park is the same story further along. Its intake is projected not to fall at all, and it has the worst reserve position in the city as a share of income. Its roll has already dropped by 222 in four years and its costs have not followed.

So the two schools with the least to lose from the projections are two of the three already in deficit. The financial damage at both is done rather than coming, and no future intake number fixes it. What is coming lands on Brighton Aldridge, Patcham and Dorothy Stringer — the first two with nothing left to absorb it.

WarningFive things this arithmetic is not

It is not a forecast of any school’s budget. It is a projection of pupil numbers multiplied by a current funding rate. Real budgets move with pay awards, energy, the national funding formula, sixth-form funding, high-needs recoupment and one-off capital, none of which is here.

The baseline is published, the trajectory is modelled. The starting point is the offers the council made for 2026 entry. The path from there is the open model’s configuration A — the city as it stands, with today’s admission numbers held constant to 2035 — applied as a proportional change, not as a level. That is the point of the exercise rather than a prediction: it is what happens if nothing is decided. The council is already reducing admission numbers, which changes every column.

A proportional trajectory is an assumption in its own right. It says a school’s intake falls in step with the cohort the model sends towards it. A school that is already far below its admission number might fall faster, because the families still choosing it are the most local ones, or slower, because it has already lost everyone who was going to leave. Nothing here distinguishes those two.

The per-pupil rate is each school’s own average, and for the four schools with a sixth form it blends 11–16 and 16–18 funding. Their pound figures are rougher than the others’.

Two reporting cycles are being read side by side. Maintained schools file to 31 March and academies to 31 August, so 2024-25 figures sit next to 2023-24 ones. Nothing has been adjusted to hide the gap; the year is in the table.

6.4 Which schools are most at risk, and when

Section 6.3 gives each school’s exposure as a single figure for 2035. It does not say when the money runs out, and that is the question a council has to answer first. This section projects the roll year by year and asks what it does to each school’s annual position.

The arithmetic, in four lines. A school’s roll falls by some number of pupils. Each of them was carrying the pupil-led funding in Section 6.2.1, so the school’s income falls by that number times that rate. It responds by taking cost out — mostly teaching staff — but it cannot take out all of it, because the leadership team, the building and the curriculum offer do not shrink in step. Whatever it cannot take out is added to the deficit it is already running.

The one judgement in that is how much cost a school can shed, so it is shown at three values rather than one.

Show code
rgap <- risk_mid %>% mutate(ends_down = short %in% r_neg$short)

# A share of income rather than pounds. In cash, Cardinal Newman's
# -£3.1m sets the axis and squashes the other nine into the top fifth of
# the panel; as a share of income the range is a fifth as wide and every
# school is legible. It is also the comparison the rest of this section
# makes - reserve, staff costs and balance are all shares of income -
# and the cash figures are in the table below.
#
# Six lines, from the categorical set used elsewhere in this document,
# assigned in order of how bad the 2035 position is. The set's usual
# sixth colour is a second green, which sat indistinguishably next to
# the first here; purple replaces it and the six still validate.
RISK_COL <- setNames(
  c("#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#6b4fbb")[
    seq_len(nrow(r_neg))],
  r_neg$short[order(r_neg$gap_pct)])

# One repel call for every label, not one per group: two calls do not
# know about each other's labels and "Patcham" landed on top of
# "Varndean".
ends <- rgap %>% filter(year == max(RISK_YEARS)) %>%
  mutate(lab_col = if_else(ends_down, unname(RISK_COL[short]), "grey45"))

ggplot(rgap, aes(year, gap_pct, group = short)) +
  geom_hline(yintercept = 0, colour = "grey35", linewidth = 0.4) +
  geom_line(data = rgap %>% filter(!ends_down),
            colour = "grey72", linewidth = 0.7) +
  geom_line(data = rgap %>% filter(ends_down),
            aes(colour = short), linewidth = 1) +
  ggrepel::geom_text_repel(
    data = ends, aes(label = short), colour = ends$lab_col,
    size = 2.6, hjust = 0, direction = "y", nudge_x = 0.3,
    segment.size = 0.22, min.segment.length = 0, seed = 4,
    box.padding = 0.12) +
  scale_colour_manual(values = RISK_COL, guide = "none") +
  scale_x_continuous(breaks = seq(min(RISK_YEARS), max(RISK_YEARS), 2),
                     limits = c(min(RISK_YEARS), max(RISK_YEARS) + 3.8)) +
  scale_y_continuous(labels = function(x) pp(x, 0)) +
  labs(x = NULL, y = "Annual surplus or deficit, as a share of income",
       title = paste(nrow(r_neg), "schools end below the line, and",
                     sum(rgap$gap[rgap$year == min(RISK_YEARS)] < 0),
                     "are already there"),
       subtitle = str_wrap(paste("Grey lines are the schools that stay in surplus.",
                                 "Middle assumption: a school takes three quarters of the cost out when the funding goes."), 105),
       caption = "Sources: DfE school income and expenditure; DfE school funding statistics 2025-26; BHCC allocation factsheets; modelled intakes, configuration A.") +
  theme_bh(9.5)
Figure 30: The annual gap between what each school receives and what it spends, as a share of its income, projected to 2035 on the middle assumption that a school takes out three quarters of the cost when it loses the funding. Each line starts at that school’s most recent in-year balance and deteriorates as the roll falls. The schools that end below zero are coloured; the rest stay in surplus and are drawn in grey.
Show code
risk_end %>%
  filter(scenario == SHED_MID) %>%
  arrange(gap_pct) %>%
  transmute(School = paste0(name, if_else(faith, " †", "")),
            `Roll 2026` = fmt_n(fund_pupils),
            `Roll 2035` = fmt_n(roll),
            `Change` = pp(d_roll / fund_pupils, 0),
            `Funding lost` = gbp(lost_funding),
            `Gap in 2035` = gbp(gap),
            `— % of income` = pp(gap_pct),
            `Reserve now` = gbp(reserve),
            `Years of reserve` = case_when(
              reserve <= 0 ~ "already overdrawn",
              is.na(years_left) ~ "still in surplus",
              TRUE ~ sprintf("%.1f", years_left))) %>%
  knitr::kable(align = "lrrrrrrrr")
Table 14: Where each school ends up by 2035, and how long its reserve lasts. The gap is the projected annual surplus or deficit at that point, on the middle assumption. Years of reserve divides today’s reserve by that gap; a school already overdrawn has none to divide. Ranked by the 2035 gap as a share of income.
School Roll 2026 Roll 2035 Change Funding lost Gap in 2035 — % of income Reserve now Years of reserve
Cardinal Newman Catholic School † 1,851 1,062 −43% £5.3m −£3.1m −17.4% −£1.8m already overdrawn
Longhill High School 719 444 −38% £2.0m −£680k −9.1% £531k 0.8
Hove Park School and Sixth Form Centre 949 823 −13% £924k −£777k −8.1% −£1.1m already overdrawn
Blatchington Mill School 1,626 1,597 −2% £190k −£294k −2.4% £492k 1.7
Dorothy Stringer School 1,643 1,477 −10% £1.1m −£242k −1.8% £831k 3.4
Varndean School 1,493 1,483 −1% £66k −£168k −1.4% £662k 4.0
Patcham High School 1,107 915 −17% £1.3m £51k +0.6% −£82k already overdrawn
Brighton Aldridge Community Academy 700 611 −13% £703k £207k +2.9% £1k still in surplus
King’s School † 867 509 −41% £2.3m £209k +3.2% £1.0m still in surplus
Portslade Aldridge Community Academy 950 900 −5% £348k £704k +9.2% £534k still in surplus

5 of the ten were already spending more than they received in their latest return, and the falling roll makes it worse for every one of them. Two are in a different category from the rest: Cardinal Newman and Hove Park are carrying accumulated deficits of £1.8m and £1.1m — both, on the measure Schools Week uses for the schools in the deepest trouble nationally, million-pound deficits already. Neither is a small school and neither got there by being unpopular: Cardinal Newman is the largest school in the city and its roll has been rising.

Longhill is the school the projection changes most. Its in-year position today is only −£168k, which does not look like a crisis. But its roll falls by 38% by 2035 — the steepest fall of any school whose projection can be trusted — and that takes £2.0m of funding with it. On the middle assumption its annual gap reaches −£680k, which its £531k reserve covers for 0.8 years.

Four schools absorb it. Brighton Aldridge, King’s, Patcham, and Portslade Aldridge are running surpluses large enough that the roll loss does not put them under, even by 2035. Two of them — Brighton Aldridge and Patcham — are doing that from reserves at or below zero, so they are repairing rather than comfortable.

Show code
risk_end %>%
  mutate(scenario = factor(scenario, names(SHED))) %>%
  select(name, faith, scenario, gap_pct) %>%
  tidyr::pivot_wider(names_from = scenario, values_from = gap_pct) %>%
  arrange(.data[[SHED_MID]]) %>%
  transmute(School = paste0(name, if_else(faith, " †", "")),
            across(all_of(names(SHED)), ~ pp(.x, 1))) %>%
  knitr::kable(align = "lrrr")
Table 15: How much the answer depends on the one assumption. Each cell is the projected annual gap in 2035 as a share of the school’s income. Ranked by the middle column.
School Sheds 90% of the cost Sheds 75% of the cost Sheds 50% of the cost
Cardinal Newman Catholic School † −12.9% −17.4% −24.9%
Longhill High School −5.0% −9.1% −15.9%
Hove Park School and Sixth Form Centre −6.7% −8.1% −10.5%
Blatchington Mill School −2.1% −2.4% −2.7%
Dorothy Stringer School −0.6% −1.8% −3.9%
Varndean School −1.3% −1.4% −1.5%
Patcham High School +2.7% +0.6% −3.0%
Brighton Aldridge Community Academy +4.4% +2.9% +0.4%
King’s School † +8.7% +3.2% −5.8%
Portslade Aldridge Community Academy +9.9% +9.2% +8.1%

The assumption is load-bearing and the ranking is not. Whether a school sheds half or nine tenths of the cost moves every number, and at the pessimistic end 8 of the ten are below the line rather than 6. What does not move is the order: the schools at the top of this table are at the top of it under every assumption.

WarningWhat this projection is and is not

It is a measure of pressure, not a forecast of any school’s accounts. Nothing here is allowed to happen. A maintained school whose reserve goes negative enters a licensed deficit and agrees a recovery plan with the council; an academy trust in that position answers to the ESFA. The figures say how large a hole has to be closed and roughly when, not what the balance sheet will read.

It holds everything else still. Today’s admission numbers, today’s funding rates in real terms, today’s in-year balance as the starting point, and no pay award, energy shock, capital receipt or restructuring. Each of those is larger than some of the differences in the table.

The two faith schools are marked, and their rolls are the least reliable figures here. The model admits to Cardinal Newman and King’s on distance alone. Cardinal Newman’s projected fall of −43% drives the largest gap in the table and should be read as an illustration of what that fall would cost, not as a claim that it will happen. Its current deficit is not modelled at all — it is what the school reported.

The starting balance is a single year. A school having one bad year looks permanently worse here than one with a steady small deficit, and Figure 27 is the better guide to which is which.

6.5 Longhill’s position

Show code
os$lh_fin %>%
  transmute(Year = year_label,
            Roll = ifelse(is.na(roll), "--", fmt_n(roll)),
            `Income / pupil` = paste0("£", fmt_n(income_pp)),
            `Spend / pupil` = paste0("£", fmt_n(expenditure_pp)),
            `In-year balance` = paste0("£", fmt_n(balance)),
            `Reserve` = paste0("£", fmt_n(reserve))) %>%
  knitr::kable(align = "lrrrrr")
Table 16: Longhill High School: roll, per-pupil income and expenditure, in-year balance and accumulated reserve.
Year Roll Income / pupil Spend / pupil In-year balance Reserve
2021-22 908 £7,644 £6,980 £602,810 £960,852
2022-23 875 £7,978 £7,998 £-17,469 £943,383
2023-24 825 £8,748 £9,041 £-243,505 £699,878
2024-25 723 £10,358 £10,591 £-168,435 £531,443

Longhill’s roll fell from 908 to 723 across four years. Its per-pupil income rose over the same period — the funding system is doing what it is meant to do — and its per-pupil spending rose faster. The accumulated reserve has fallen from £960,852 to £531,443.

At the recent rate of depletion — about £143,136 a year — the reserve supports roughly 3.7 more years. That is the constraint that makes this a decision with a deadline rather than a decision that can be deferred.

7 Brightopia: a model of the whole system

7.1 Why model at all

Note“Brightopia” means one specific assumption: \(W_j = 1\)

The name is used throughout this section and it is worth pinning down, because the model is run under two different assumptions and they answer different questions.

Brightopia proper sets \(W_j\) to the same value for every school. Same size, same quality, same buildings, same staff, same lunches, no religious character. Children are identical too. The only thing that separates the schools is where they stand. Whenever this document says Brightopia, the distance-only model, or geography alone, that is the assumption in force, and every such figure is labelled \(W_j = 1\).

The runs with \(W_j\) varying are not Brightopia and are labelled separately, as with demand or by the specification they use. They are the same equation with a real attractiveness term.

The distinction matters because Brightopia’s strength is exactly its severity: it cannot be dismissed as an artefact of attainment proxies, Ofsted grades or preference data, because it uses none of them. That is also its limit — it cannot say how many children a school would draw, because it has assumed the question away.

Every argument in the sections above is about one part of the system: where the children are, how far the schools are, what families prefer. A model is what lets you ask what happens when you change one part and everything else adjusts. Close a school and its children do not vanish; they appear somewhere else, displacing others. Shrink one school’s admission number and demand redistributes to its neighbours. Nothing in a spreadsheet of admission numbers captures that.

Brightopia is a production-constrained spatial interaction model. It takes the children in each neighbourhood, the attractiveness of each school, and the travel cost between them, and predicts the flow:

\[T_{ij} = A_i O_i W_j^{\alpha} c_{ij}^{-\beta}\]

\(O_i\) is the number of cohort-aged children in neighbourhood \(i\); \(W_j\) is school \(j\)’s attractiveness; \(c_{ij}\) is the routed walk-and-bus cost; \(\beta\) governs how sharply demand falls away with travel time; and \(A_i\) is a balancing factor ensuring every child goes somewhere.

The model deliberately contains no catchment term. That is the point. It answers the question “where would children go if only geography and school size mattered?” — and the gap between that answer and reality is a measure of what the admissions system is doing.

It is run twice, and the difference between the two runs does a lot of work below. Figure 31 holds \(W_j\) identical for every school, which is the distance-only case: same size, same quality, same buildings, no religious character, and the only thing separating the schools is where they stand. Figure 34 lets \(W_j\) vary, setting it to the rank-weighted preference rate from Section 4.5.1 — what families actually ask for, counting all three ranks. The first is a statement about the geography of the city. The second is about the geography and the demand together, and the gap between them is the whole of what attractiveness contributes.

Show code
# The distance-only run at the bundle's central decay, not at the 1.5 the
# original Brightopia used. The open model keeps the 1.5 run for
# reproduction; here the point is that this figure and @fig-brightopia-demand
# report the same quantity, so they have to be at the same beta.
bt <- brt$geog_only_at_ref %>%
  mutate(fill_rate = modelled / pan2026,
         short = str_remove(name, " (School|High School|Community Academy|Catholic School).*"))

ggplot(bt, aes(pan2026, modelled)) +
  geom_abline(slope = 1, intercept = 0, colour = "grey45", linetype = "31") +
  geom_point(aes(colour = fill_rate >= 1), size = 3.5) +
  ggrepel::geom_text_repel(aes(label = name), size = 2.8, seed = 1,
                           max.overlaps = 20) +
  scale_colour_manual(values = c(`TRUE` = "#2166ac", `FALSE` = "#b2182b"),
                      labels = c(`TRUE` = "Geography would fill it",
                                 `FALSE` = "Geography would not"), name = NULL) +
  scale_x_continuous(limits = c(0, NA)) + scale_y_continuous(limits = c(0, NA)) +
  labs(x = "Published admission number, 2026", y = "Modelled intake",
       title = expression("What the map alone would do:  "*W[j]*" = 1"),
       subtitle = sprintf("Brightopia proper — every school equally attractive. Distance decay β = %.1f.\nThe dashed line is a school exactly full.",
                          brt$beta_ref),
       caption = "Brightopia, open-data specification.") +
  theme_bh()
Figure 31: Brightopia proper, with every school equally attractive: modelled intake against published admission number. A school above the line is one that geography alone would fill; below it, one that geography alone would not.

City-wide there are 2,560 places for 2,267 children — a fill rate of 89% before any child expresses a preference. The system is not short of capacity; it is short of children.

Two schools are worth reading off this chart carefully, and they point in opposite directions.

Longhill has already been resized to roughly what geography supports. Its modelled intake is 201 children. Against its former admission number of 270 that is 74%; against the reduced number of 210 now in force it is 95%. The distinction matters, and it is easy to quote the wrong one. Geography can fill 210 places at Ovingdean; it could not fill 270. The reduction has already done most of the work that a reduction can do — which is why section 8 is about what remains, and why the modelled intake is remarkably insensitive to the decay parameter, varying only between 193 and 247 across the entire swept range. Longhill’s catchment simply does not contain many more children to attract.

Hove Park’s problem is not its location. Geography alone would fill 142% of its places — it is the most over-subscribed school on the map, sitting in the densest part of the child population. Yet section 5 shows it among the largest losers of real demand over sixteen years. A school that the map says should be full and that families are nonetheless leaving is not a geography problem, and no catchment or relocation policy reaches it. That gap between what location predicts and what families do is the clearest evidence in this document that choice is operating on something other than distance.

7.2 How far geography alone actually gets you

The chart above is a model of a city that does not exist. It is worth putting next to the one that does: the offers Brighton & Hove actually made in the 2026 round, school by school.

Brightopia proper, with attractiveness switched off and no ceiling (Section 7.5.1 sets out the full form and what the later models add):

\[ T_{ij} \;=\; A_i\, O_i\; c_{ij}^{-\beta}, \qquad A_i = \Big[\textstyle\sum_j c_{ij}^{-\beta}\Big]^{-1}, \qquad W_j = 1,\;\; \beta = 1.7 \]

Show code
brt$observed_compare %>%
  arrange(diff_17) %>%
  transmute(School = str_remove(name, " (School|High School|Community Academy|Catholic School).*"),
            `PAN` = pan2026,
            `Offers made` = fmt_n(observed),
            `Brightopia` = fmt_n(modelled_17),
            `Difference` = sprintf("%+.0f", diff_17),
            `Offers, % of city` = sprintf("%.1f", share_obs),
            `Brightopia, % of city` = sprintf("%.1f", share_mod)) %>%
  knitr::kable(align = "lrrrrrr")
Table 17: Brightopia (\(W_j = 1\)) against the offers actually made. Every school is equally attractive in the model, so a difference here is everything except geography: reputation, results, faith character, sixth form, siblings, and the catchment rule. Brightopia applies no admission-number ceiling, so a modelled figure above the PAN is demand the real round would have turned away.
School PAN Offers made Brightopia Difference Offers, % of city Brightopia, % of city
Blatchington Mill 330 330 229 -101 14.7 10.1
Dorothy Stringer 330 330 232 -98 14.7 10.2
Cardinal Newman 360 360 286 -74 16.0 12.6
Varndean 300 300 247 -53 13.3 10.9
King’s 180 180 174 -6 8.0 7.7
Patcham 225 204 211 +7 9.1 9.3
Brighton Aldridge 180 146 185 +39 6.5 8.2
Portslade Aldridge 220 183 248 +65 8.1 10.9
Hove Park 180 136 255 +119 6.0 11.2
Longhill 210 81 201 +120 3.6 8.8

Geography alone explains 31% of the variation in offers made, with a root-mean-square error of 79 children per school. For a model that knows nothing except where the buildings are and where the children live, that is neither nothing nor much.

What matters is that the errors are not random. They run in one direction, and it is the direction the rest of this document is about.

The model over-predicts Longhill by +120 and Hove Park by +119. Both are schools that geography says should do well — Hove Park sits in the densest part of the child population — and that families do not choose. It under-predicts Blatchington Mill by -101 and Dorothy Stringer by -98, both of which filled to their admission number and would have taken more.

In share terms the same thing reads more starkly. Longhill takes 3.6% of the city’s offers; geography alone would give it 8.8%. Blatchington Mill takes 14.7% against a geographic 10.1%.

NoteWhy the gaps are not evidence of a broken model

A distance-only model is supposed to miss. It has been told that every school is identical, which is false, and the size of what it misses is the measurement being taken.

Two mechanical points inflate the gaps without meaning anything.

The popular schools are capped and the model is not. Blatchington Mill, Cardinal Newman, Dorothy Stringer, King’s, and Varndean all offered their admission number or more, so their observed figure is a ceiling rather than a measure of demand. The model under-predicts them; the real gap is larger, not smaller. Only the 5 schools that were not rationing places can be judged against their offers at all.

The catchment rule is in the offers and not in the model. Every offer above was made under a catchment system with a lottery tie-break. Brightopia has no catchment term at all. Some of what looks like preference here is the admissions rule doing its job — which is why this table cannot separate “families do not want Longhill” from “the rule does not send them there”, and why Section 5.4 has to answer that question with preference data rather than offers.

Show code
bo %>%
  filter(!at_ceiling) %>%
  arrange(abs(diff_w)) %>%
  transmute(School = str_remove(name, " (School|High School|Community Academy|Catholic School).*"),
            `Offers made` = fmt_n(observed),
            `Geography only` = fmt_n(modelled_17),
            `Gap` = sprintf("%+.0f", diff_17),
            `With demand` = fmt_n(modelled_w),
            `Gap ` = sprintf("%+.0f", diff_w),
            `Closer?` = if_else(abs(diff_w) < abs(diff_17), "yes", "no")) %>%
  knitr::kable(align = "lrrrrrr")
Table 18: The five schools that were not rationing places, against the two runs of the model. A school at its admission number is left out, because an uncapped model exceeding a capped observation is not an error.
School Offers made Geography only Gap With demand Gap Closer?
Portslade Aldridge 183 248 +65 183 +0 yes
Longhill 81 201 +120 76 -5 yes
Patcham 204 211 +7 188 -16 no
Brighton Aldridge 146 185 +39 97 -49 no
Hove Park 136 255 +119 280 +144 no

Putting \(W_j\) back in does not simply fix this. The average gap across those 5 schools falls from 70 children to 43, but only 2 of the 5 actually get closer. Longhill and Portslade Aldridge land almost exactly on their observed figure — which is the striking result, given that geography alone missed Longhill by +120. The rest do not improve.

WarningHove Park breaks the weighted measure, and it is worth knowing why

Hove Park is the one school that gets worse when demand is added: from +119 on geography alone to +144. The model thinks it should be the most sought-after school in the city and it offered 136 places against a PAN of 180.

The reason is structural, and it applies to the weighted measure generally. A second preference only becomes an offer when the first one fails. Across the same five rounds Hove Park receives 123 first preferences and 242 seconds — 2.0 seconds for every first, the highest ratio in the city. Those seconds come largely from the Hove Park / Blatchington Mill paired catchment (Section 5.3), and Blatchington Mill fills, so they are never called on. Weighting a second at half a first says something true about what families would accept; it says much less about what they will be allocated.

So the weighted specification is a good measure of wanting and a poor measure of taking up. That does not undo the Longhill result, which runs the other way: Longhill is named 164 times at any of the three ranks against 270 places — 0.61 per place, the lowest in the city against a next-lowest of 1.03 and a city median of 2.57. There is no reservoir of unconverted seconds behind it. But it is a reason to read the with-demand column as a statement about demand rather than a forecast of offers.

7.3 Where the model sends the children

The tables say how many children each school draws. They do not say where from, and for Longhill that is most of the question: a school at the eastern edge of the city either recruits from its own corner or it does not recruit.

Show code
fl <- fm$net %>%
  filter(name == "Longhill High School") %>%
  mutate(model = factor(model, names(FM_LABELS), FM_LABELS))
fl_bb <- st_bbox(fl)

ggplot() +
  geom_sf(data = catch, fill = NA, colour = "grey80", linewidth = 0.3) +
  geom_sf(data = fl %>% arrange(flow),
          aes(linewidth = flow, colour = leg_mode), alpha = 0.75,
          lineend = "round") +
  geom_sf(data = fm_sch, colour = "grey45", size = 1.1) +
  geom_sf(data = fm_sch %>% filter(name == "Longhill High School"),
          colour = "black", fill = "white", shape = 21, size = 2.8, stroke = 1) +
  facet_wrap(~ model, nrow = 1) +
  scale_linewidth_continuous(range = c(0.15, 2.6), guide = "none") +
  scale_colour_manual(values = FM_COL, name = NULL) +
  coord_sf(xlim = c(fl_bb["xmin"], fl_bb["xmax"]),
           ylim = c(fl_bb["ymin"], fl_bb["ymax"])) +
  labs(title = "Longhill's modelled catchment contracts as the model gets more realistic",
       subtitle = "The white point is Longhill; grey points are the other nine schools. Grey outlines are the current catchments.",
       caption = "Open-data model. Modelled flows, not observed journeys.") +
  theme_bh(11) +
  theme(axis.text = element_blank(), axis.ticks = element_blank(),
        panel.grid = element_blank(), legend.position = "top",
        strip.text = element_text(face = "bold"))
Figure 32: Longhill’s modelled intake, routed over the walking and bus network, under each of the three models. Flows are summed onto shared segments, so a corridor used by several neighbourhoods draws thicker. Blue is a bus leg, orange a walking leg.
Show code
m <- leaflet(width = "100%", height = 620,
             options = leafletOptions(preferCanvas = TRUE)) %>%
  add_basemap() %>%
  addPolygons(data = catch, fill = FALSE, color = "#555555",
              weight = 1, opacity = 0.5, group = "Catchment boundaries")

for (i in seq_len(nrow(fm_groups))) {
  g <- fm_groups$label[i]
  d <- fm_net %>% filter(label == g) %>% arrange(flow)
  m <- m %>%
    addPolylines(data = d, group = g,
                 color = ~ unname(FM_COL[leg_mode]),
                 weight = ~ fm_w(flow), opacity = 0.75,
                 label = ~ sprintf("%s leg · %.0f children", leg_mode, flow)) %>%
    addCircleMarkers(
      data = fm_sch %>% filter(name == fm_groups$name[i]), group = g,
      radius = 6, color = "#111111", weight = 2, opacity = 1,
      fillColor = "#ffffff", fillOpacity = 1, label = ~ name)
}

# Every group is added at once, so leaflet's automatic bounds cover the
# whole routed network and open at a Sussex-wide zoom. Frame the city.
fm_bb <- st_bbox(fm_net)

m %>%
  fitBounds(lng1 = unname(fm_bb["xmin"]) - 0.01, lat1 = unname(fm_bb["ymin"]) - 0.005,
            lng2 = unname(fm_bb["xmax"]) + 0.01, lat2 = unname(fm_bb["ymax"]) + 0.005) %>%
  addLayersControl(baseGroups = fm_groups$label,
                   overlayGroups = "Catchment boundaries",
                   options = layersControlOptions(collapsed = FALSE)) %>%
  hideGroup("Catchment boundaries") %>%
  addLegend("bottomright", colors = unname(FM_COL), labels = names(FM_COL),
            title = "Leg", opacity = 0.8)
Figure 33: The same three panels, interactively, so the corridors can be traced and the segments read. Blue is a bus leg, orange a walking leg; hover a segment for the number of children on it. These are modelled flows from an open-data model: no pupil record is drawn here. The other nine schools are in Figure 35, under the full model.

Longhill’s modelled catchment contracts as the model gets more realistic, and that is the striking thing in this map. Under Brightopia, with every school equally attractive, it draws 201 children and only 53% of them come from its own catchment — the model has it reaching deep into Kemptown, Whitehawk and beyond. Add what families actually ask for and the total falls to 75; add the capacity ceiling and the other terms and it settles at 86 children, 98% of them from the Longhill catchment.

Show code
lh_catch %>%
  mutate(model = recode(model, !!!as.list(FM_LABELS)),
         flow = round(flow)) %>%
  select(catchment, model, flow) %>%
  pivot_wider(names_from = model, values_from = flow, values_fill = 0) %>%
  arrange(desc(.data[[FM_LABELS[["M5"]]]])) %>%
  rename(`Home catchment of the neighbourhood` = catchment) %>%
  knitr::kable(align = "lrrr")
Table 19: Where Longhill’s modelled intake comes from, by the home catchment of the neighbourhood it starts in, under each of the three models.
Home catchment of the neighbourhood Brightopia · Wj = 1 With demand · Wj = weighted preferences Full model · calibrated to catchment preferences
Longhill 107 47 84
BACA 10 3 1
DS_Varndean 48 14 1
Hove_Blatch 23 6 0
PACA 8 2 0
Patcham 6 2 0

Read west to east, the map is an argument about siting. The trunk into Longhill is a single corridor along the coast road and over the downs, and it is thick only at the eastern end. Under the full model there is almost nothing coming from west of the Steine — not because those children are forbidden to go, but because on a routed journey time they have closer schools that are also more wanted.

NoteHow to read the lines, and what they are not

The lines are real routes. Each zone-to-school journey is routed with r5r over the same merged OSM and GTFS network the journey times in Section 4.1 come from, on the same weekday morning. A walking leg follows the street network; a bus leg follows the service’s own shape.

The widths are added along the way. Flows are summed onto shared segments, so a road used by six neighbourhoods carries the sum of all six. That is what produces the trunk-and-branch shape, and it is why a line’s thickness is a statement about a corridor rather than about any one neighbourhood.

The flows are modelled, not observed. Nobody’s journey is drawn here. This is where the model sends children, which is precisely the thing Section 7.2 and Section 7.5 test against the offers actually made — and on that test the full model gets four of the five rationed schools right and misses Cardinal Newman and Hove Park. The map should be read with those two misses in mind.

Journeys that need more than 30 minutes of walking are not drawn. The routing caps walking legs, so a handful of zone-school pairs return no itinerary and drop out of the picture. They are in the tables.

7.4 What happens when you put demand back in

Figure 31 asked what the map alone would do. It is a deliberately severe assumption, and it is the assumption that makes Longhill’s position look most survivable: with every school equally wanted, the only thing that can hurt a school is being far away.

The same model, same geography, same routed times, same production constraint, but with \(W_j\) set to what families actually ask for:

Show code
bd <- brt$demand_compare %>%
  mutate(short = str_remove(name, " (School|High School|Community Academy|Catholic School).*"),
         gains = shift > 0) %>%
  arrange(shift) %>%
  mutate(short = factor(short, short))

ggplot(bd, aes(y = short)) +
  geom_segment(aes(x = geog_only, xend = with_demand, yend = short,
                   colour = gains), linewidth = 1.1,
               arrow = arrow(length = unit(0.16, "cm"), type = "closed")) +
  geom_point(aes(x = geog_only), colour = "grey35", size = 2.6) +
  geom_point(aes(x = pan2026), shape = 124, size = 5, colour = "#444444") +
  scale_colour_manual(values = c(`TRUE` = "#2166ac", `FALSE` = "#b2182b"),
                      labels = c(`TRUE` = "Gains once demand is added",
                                 `FALSE` = "Loses once demand is added"),
                      name = NULL) +
  labs(x = "Modelled intake", y = NULL,
       title = "What the map would do, and what families do to it",
       subtitle = sprintf(
         "Grey dot: every school equally attractive. Arrow: W set to weighted preferences. Tick: 2026 admission number.\nBoth runs at β = %.1f.",
         brt$beta_ref),
       caption = "Open-data specification. No capacity ceiling in either run.") +
  theme_bh() +
  theme(panel.grid.major.y = element_blank(),
        legend.position = "top")
Figure 34: Modelled intake with every school equally attractive, against modelled intake using the rank-weighted preference rate. Both runs are at the same distance decay, so the difference between the two points is attractiveness and nothing else. Neither run applies an admission-number ceiling, so these are natural demand rather than offers.

The same equation with a real attractiveness term and still no ceiling:

\[ T_{ij} \;=\; A_i\, O_i\; W_j^{\alpha}\; c_{ij}^{-\beta}, \qquad \alpha = 1,\;\; \beta = 1.7, \qquad W_j = \text{weighted preferences per place} \]

Show code
brt$observed_compare %>%
  arrange(diff_w) %>%
  transmute(School = str_remove(name, " (School|High School|Community Academy|Catholic School).*"),
            `PAN` = pan2026,
            `Offers made` = fmt_n(observed),
            `Brightopia, Wj = 1` = fmt_n(modelled_17),
            `With demand` = fmt_n(modelled_w),
            `Difference` = sprintf("%+.0f", diff_w),
            `Rationing?` = if_else(at_ceiling, "yes", "")) %>%
  knitr::kable(align = "lrrrrrc")
Table 20: The same comparison as the Brightopia table above, but with \(W_j\) set to weighted preferences rather than held at 1. Still no admission-number ceiling, so a figure above the PAN is demand that the real round would have turned away.
School PAN Offers made Brightopia, Wj = 1 With demand Difference Rationing?
Blatchington Mill 330 330 229 217 -113 yes
Dorothy Stringer 330 330 232 255 -75 yes
Brighton Aldridge 180 146 185 97 -49
Cardinal Newman 360 360 286 330 -30 yes
Patcham 225 204 211 188 -16
Longhill 210 81 201 76 -5
Portslade Aldridge 220 183 248 183 +0
Varndean 300 300 247 366 +66 yes
King’s 180 180 174 274 +94 yes
Hove Park 180 136 255 280 +144

This is the least comfortable figure in the document, and it is about Longhill. On geography alone the school draws 201 children — close to the 210 places now in force, which is what Section 7 reported. Once \(W_j\) carries what families ask for, it draws 76, a fall of 62%. That is the largest fall of any school in the city, and it means the reduction to 210 closed the gap to geography but not the gap to demand.

The same is true at BACA, the other end of the same story: from 185 on geography to 97 with demand.

In the other direction, Varndean and King’s gain 120 and 100 children respectively — both already well above their admission numbers on geography alone, and further above once preference is counted. Neither can actually take them: the ceiling is the admission number, and the children have to go somewhere.

Counting how many schools geography alone would fill to their 2026 admission number gives 3 of 10. Counting how many would fill once demand is added gives 3. The city’s problem is not that it is short of children in aggregate — it is that they are not distributed the way the buildings are.

ImportantWhat this does and does not license

It does not say Longhill would recruit 76 children. The model has no catchment term and no admission ceiling. In reality the catchment rule holds children in the east who would otherwise go west, which is exactly what the rule is for, and section 8 runs the configurations with catchments and ceilings in place.

It does say the two problems are separable, and only one of them moves. Relocation and admission-number changes act on the geography term. Nothing in this document acts on \(W_j\), because \(W_j\) is what families think of the school — and section 5 shows what families think is tracking a headline score that is mostly a description of intake. A school can be moved to where the children are and still not be chosen by them.

And it depends on the preference measure. Counting first preferences alone would put Longhill’s attractiveness at 0.41; counting all three ranks with the geometric discount puts it at 0.32 — so the weighted measure makes Longhill’s position look worse, not better. That is worth stating plainly, because the weighted measure was adopted in Section 4.5.1 for reasons that had nothing to do with Longhill, and it did not turn out to favour the argument.

7.5 What the model actually needs

Both runs above miss, and they miss in a particular way: inside each paired catchment they send children to the wrong one of the two schools. Blatchington Mill is under-predicted and Hove Park over-predicted; Dorothy Stringer is under-predicted and Varndean over-predicted once \(W_j\) is real. That is a specific failure with specific candidate causes, so they are added one at a time.

Three candidates, in the order they are tested.

A capacity ceiling. Schools cannot admit beyond their admission number. Brightopia has no ceiling at all, so it is free to send 255 children to a school with 180 places.

A catchment term. Living in a school’s catchment makes a place far more likely. Entered as \(\gamma\) in the exponent, so being in catchment multiplies a school’s pull by \(e^{\gamma}\).

A competing-destinations term. Fotheringham’s argument that a gravity model is misspecified when destinations cluster: families pick an area and then a school within it. Entered as \(C_j^{\delta}\), where \(C_j = \sum_{k \neq j} W_k d_{jk}^{-\sigma}\) is how much company a school has. A negative \(\delta\) is the competing-destinations prediction — rivals nearby take share.

Show code
lad %>%
  transmute(Model = model, `Term added` = label,
            `` = sprintf("%.2f", r2),
            RMSE = fmt_n(rmse),
            `MAE` = fmt_n(mae),
            `MAE, unrationed only` = fmt_n(mae_free)) %>%
  knitr::kable(align = "llrrrr")
Table 21: Fit against the offers actually made, adding one term at a time. The last column is the one to read: it scores only the five schools that were not rationing places, because from the capacity row onwards the model is told the admission numbers, and five of the ten observed figures are those admission numbers.
Model Term added RMSE MAE MAE, unrationed only
M0 Brightopia: every school identical 0.31 79 68 70
M1 + W = weighted preferences 0.44 75 59 43
M2 + capacity ceiling 0.92 27 21 28
M3 + catchment term (gamma = 0.4) 0.93 26 21 29
M4 + competing destinations (delta = +0.3) 0.94 22 14 25
M5 Calibrated to what each catchment asks for 0.95 22 14 27

The capacity ceiling does almost all of the work. It takes the fit from R² 0.44 to 0.92, and on the schools that were not rationing places it takes the average error from 43 children to 28.

That headline R² is flattered and should be discounted: five of the ten observed figures are the admission numbers, and from that row on the model is given them. But the improvement survives the discount — on the five schools free to be wrong, the error still falls by a third. Brightopia’s largest single omission is not a behavioural term. It is that offers are rationed and the model was not told.

The catchment term adds essentially nothing. Fitted at \(\gamma\) = 0.4, it moves R² from 0.92 to 0.93 and makes the unrationed error slightly worse, 28 to 29. Once children are held to real capacities in a city nine miles across, the catchment rule has little left to do that proximity was not already doing.

That is a finding about offers, and it does not survive being asked about preferences instead. Fitted to what each catchment’s children ask for, the catchment term is the strongest behavioural term in the model (Section 7.8.3).

The competing-destinations term helps a little, with the wrong sign.

Fitted at \(\delta\) = +0.3, it improves both scores. But a positive \(\delta\) is not competition — it says a school does better for having rivals close by, which is agglomeration, the opposite of what the term was added to test. Without the capacity ceiling the term does nothing at all: fitted on its own it lands at +0.1 and the RMSE is unchanged.

So the honest answer to “would a competing-destinations term fix the paired catchments” is no. What fixes them is the capacity ceiling.

7.5.1 The five models, written out

Every model in the ladder is the same equation with terms switched on. In full:

\[ T_{ij} \;=\; A_i\, O_i\; W_j^{\alpha}\; C_j^{\delta}\; e^{\gamma \kappa_{ij}}\; c_{ij}^{-\beta}, \qquad A_i \;=\; \Big[\textstyle\sum_j W_j^{\alpha}\, C_j^{\delta}\, e^{\gamma \kappa_{ij}}\, c_{ij}^{-\beta}\Big]^{-1} \]

\(T_{ij}\) is the flow of children from neighbourhood \(i\) to school \(j\); \(O_i\) the cohort-aged children living in \(i\); \(c_{ij}\) the routed walk-and-bus journey time; \(\kappa_{ij}\) is 1 if \(i\) lies in \(j\)’s catchment and 0 otherwise; and \(A_i\) is the balancing factor that sends every child somewhere. Throughout, \(\alpha = 1\) and \(\beta = 1.7\).

Setting \(W_j = 1\), \(\delta = 0\) and \(\gamma = 0\) collapses it to Brightopia. Each model below turns one more of them on.

7.5.2 M2 — adding the capacity ceiling

M1 lets every school take as many children as its attractiveness and its distance earn it. Real schools stop at their admission number. M2 adds that, and it is worth being precise about how, because the mechanism is not a cap applied after the fact — it changes where the children who cannot get in end up.

Flows are computed as in M1, then two balancing factors are found:

\[ T^{*}_{ij} \;=\; a_i\, b_j\, T_{ij} \]

  • \(a_i\) is the origin factor, one per neighbourhood. It is the number that makes neighbourhood \(i\) send exactly its own children and no more: \(\sum_j T^{*}_{ij} = O_i\). In M1 it is just the normaliser that turns relative attractions into shares; here it has to keep doing that while schools are being cut back underneath it.
  • \(b_j\) is the destination factor, one per school, and it is constrained to \(b_j \le 1\). It can shrink a school’s intake. It can never inflate one.

\(\overline{T}_j\) is school \(j\)’s published admission number for 2026, and the pair of factors is required to satisfy

\[ \sum_j T^{*}_{ij} = O_i \quad \text{(every child placed)}, \qquad \sum_i T^{*}_{ij} \;\le\; \overline{T}_j \quad \text{(no school over its number)} \]

7.6 The ceiling only exists where it binds

The \(b_j \le 1\) restriction is what makes this a destination constraint that switches on and off rather than one that is always active. For every school exactly one of two things is true:

\[ b_j = 1 \quad\text{(the school is left alone)} \qquad\text{or}\qquad \sum_i T^{*}_{ij} = \overline{T}_j \quad\text{(the school is exactly full)} \]

An undersubscribed school is never scaled up to fill itself — that is the difference between this and a doubly-constrained model, where both margins are forced to match and every school would be pushed to its admission number whether anyone wanted it or not. Here the model is production-constrained everywhere, and destination-constrained only at the schools that would otherwise overflow.

7.7 How the two factors are found

There is no closed form, because each factor depends on the other: cutting an oversubscribed school frees children who must go somewhere, which changes what every other school receives. They are found by iterative proportional fitting, which is two steps repeated until nothing moves:

  1. Scale each row. Multiply every flow out of neighbourhood \(i\) by whatever makes its row sum to \(O_i\) again.
  2. Scale each column, by \(\min(\overline{T}_j / \sum_i T_{ij},\, 1)\). A school over its number is cut back to it; a school under its number is multiplied by 1 and left exactly as it was.

Repeat. The published \(a_i\) and \(b_j\) are the accumulated products of those per-step factors. The loop stops when every school is at or under its admission number and every neighbourhood is placing all its children.

Step 1 is what makes this a rationing rule rather than a cull. A child cut from an oversubscribed school in step 2 is not lost from the model: the next row-scaling redistributes that neighbourhood’s whole demand across the schools it can still reach, in proportion to how attractive and how close they are. The displacement cascades, which is why the ceiling moves schools that are nowhere near full.

Show code
toy <- tibble(School = c("A", "B", "C"),
              `M1 flow` = c(60, 30, 10),
              Places = c(40, 200, 200))
toy <- toy %>%
  mutate(`Capped` = pmin(`M1 flow`, Places),
         free = sum(`M1 flow`) - sum(`Capped`),
         slack = if_else(`M1 flow` < Places, `M1 flow`, 0),
         `M2 flow` = `Capped` + free * slack / sum(slack)) %>%
  select(School, `M1 flow`, Places, `M2 flow`)
stopifnot(abs(sum(toy$`M2 flow`) - 100) < 1e-9)
toy %>%
  mutate(across(-School, ~ sprintf("%.0f", .x))) %>%
  knitr::kable(align = "lrrr")
One neighbourhood, three schools, to show the arithmetic. School A is the most attractive but has 40 places. The children it cannot take do not disappear: they are shared between B and C in the ratio those two already had, 3 to 1.
School M1 flow Places M2 flow
A 60 40 40
B 30 200 45
C 10 200 15

With one neighbourhood it settles in a single pass. With 179 of them competing for the same ten schools it takes several, because every redistribution can push another school over its number.

7.8 What it is not

It is not the admissions algorithm. There is no priority order in it: no catchment, no sibling, no distance tie-break. A school over its number is scaled back proportionally, so every neighbourhood sending to it loses the same fraction of its children, whether it is next door or across the city. The real rule takes in-catchment children first and then the nearest — Section 8.4 models that separately, on top of a catchment map. M2 is a capacity ceiling, not an allocation.

It does not know which schools actually rationed places. It applies the published admission number to every school equally. Whether that number binds in the model is an output, and comparing it against the schools that really did ration is one of the things this section is for.

Both constraints hold on the published flows: no school is over its admission number by more than a rounding error, every neighbourhood places all its children, and the ceiling is active at 5 of the 10 schools. At the other 5 it is inert — b_j is exactly 1 and those schools are left wherever M1 put them, the emptiest at 43% of its admission number.

Show code
mt_table("M2", "M1") %>% knitr::kable(align = "lrrrrrc")
Table 22: M2. Weighted preferences with a capacity ceiling, against the offers actually made. The change column is the size of the residual against M1: negative means the ceiling moved that school closer.
School PAN Offers made Modelled Residual Change from M1 Rationing?
Blatchington Mill 330 330 281 -49 -64 yes
Brighton Aldridge 180 146 118 -28 -21
Dorothy Stringer 330 330 312 -18 -56 yes
Cardinal Newman 360 360 360 +0 -30 yes
King’s 180 180 180 +0 -94 yes
Varndean 300 300 300 +0 -66 yes
Longhill 210 81 91 +10 +5
Patcham 225 204 225 +21 +5
Portslade Aldridge 220 183 220 +37 +37
Hove Park 180 136 180 +44 -100

R² 0.92, RMSE 27, and 28 children average error across the five schools that were not rationing places. Almost every school moves closer — 7 of 10 — and the biggest single correction is Hove Park, which the ceiling cuts by 100 children.

But the ceiling does not bind where the real round did. It binds on 6 schools — Cardinal Newman, Hove Park, King’s, Patcham, Portslade Aldridge, and Varndean — while the schools that actually rationed places were Blatchington Mill, Cardinal Newman, Dorothy Stringer, King’s, and Varndean. The overlap is imperfect in both directions, and the interesting half is Hove Park, Patcham, and Portslade Aldridge: the model pushes them to the admission number and none of them filled. That is the same puzzle as Section 7.2 — schools the model wants to send children to, and the city did not.

7.8.1 M3 — adding the catchment term

\(\gamma\) enters the exponent, so living inside a school’s catchment multiplies its pull by \(e^{\gamma}\):

\[ T_{ij} \;=\; A_i\, O_i\; W_j^{\alpha}\; e^{\gamma \kappa_{ij}}\; c_{ij}^{-\beta} \quad\text{then capped as in M2,}\qquad \gamma = 0.4 \]

At \(\gamma\) = 0.4 a catchment school is 49% more attractive than the same school out of catchment, other things equal.

Show code
mt_table("M3", "M2") %>% knitr::kable(align = "lrrrrrc")
Table 23: M3. The same, with a catchment term. The change column is against M2.
School PAN Offers made Modelled Residual Change from M2 Rationing?
Blatchington Mill 330 330 292 -38 -11 yes
Brighton Aldridge 180 146 115 -31 +3
Dorothy Stringer 330 330 314 -16 -2 yes
Cardinal Newman 360 360 347 -13 +13 yes
Varndean 300 300 300 +0 +0 yes
King’s 180 180 180 +0 +0 yes
Patcham 225 204 218 +14 -7
Longhill 210 81 101 +20 +10
Portslade Aldridge 220 183 220 +37 -0
Hove Park 180 136 180 +44 +0

R² 0.93, RMSE 26, and 29 children average error across the five schools that were not rationing places. The term moves almost nothing, and what it moves it moves in both directions. Against offers, this is the rung that fails; against preferences it is the one that matters most (Section 7.8.3).

7.8.2 M4 — adding competing destinations

\(C_j\) is Fotheringham’s competing-destinations term, the accessibility of each school to the other schools:

\[ C_j \;=\; \sum_{k \neq j} W_k\, d_{jk}^{-\sigma}, \qquad \sigma = 1.0 \]

\(d_{jk}\) is the straight-line distance in kilometres between two schools, not a journey time — nobody travels from one school to another, and what is being measured is only how clustered they are. It enters as \(C_j^{\delta}\):

\[ T_{ij} \;=\; A_i\, O_i\; W_j^{\alpha}\; C_j^{\delta}\; e^{\gamma \kappa_{ij}}\; c_{ij}^{-\beta} \quad\text{then capped,}\qquad \delta = +0.3 \]

Show code
mt_table("M4", "M3") %>% knitr::kable(align = "lrrrrrc")
Table 24: M4. The full form. The change column is against M3.
School PAN Offers made Modelled Residual Change from M3 Rationing?
Brighton Aldridge 180 146 99 -47 +16
Cardinal Newman 360 360 349 -11 -2 yes
Longhill 210 81 80 -1 -18
Blatchington Mill 330 330 329 -1 -37 yes
Dorothy Stringer 330 330 330 +0 -16 yes
King’s 180 180 180 +0 -0 yes
Varndean 300 300 300 +0 +0 yes
Patcham 225 204 218 +14 +1
Portslade Aldridge 220 183 201 +18 -19
Hove Park 180 136 180 +44 -0

R² 0.94, RMSE 22, and 25 children average error across the five schools that were not rationing places. The gains are concentrated in the paired catchments, which is where the term has most to work with — but they come from a \(\delta\) of +0.3, and a positive \(\delta\) says clustering helps. Read as a competing-destinations result it has the sign backwards; read as a fit, it is a small improvement bought with a parameter estimated from ten numbers.

The most useful thing in this table is which schools the model now rations. The ceiling binds on 4 schools, and the real round rationed 5. 3 of them are the same: Dorothy Stringer, King’s, and Varndean. The model swaps Blatchington Mill and Cardinal Newman, which filled and which it leaves 1, 11 short, for Hove Park, which did not fill and which it fills.

Getting 3 of 5 right from published data and no admissions records is more than the fit statistics convey. Both errors are also explicable. Cardinal Newman is a faith school and this model has no faith restriction; the variant that adds one is discussed below. Hove Park is the school whose demand is second preferences that never convert (Section 7.2).

Show code
mt$pair_split %>%
  transmute(Pair = pair, Model = model,
            `Modelled total` = fmt_n(mod_total),
            `Offers made` = fmt_n(obs_total),
            `Modelled share` = fmt_pct(100 * mod_share, 0),
            `Observed share` = fmt_pct(100 * obs_share, 0)) %>%
  arrange(Pair, Model) %>%
  knitr::kable(align = "llrrrr")
Table 25: Each paired catchment splits between its two schools. The share is the one going to the second-named school. A model can get a catchment’s total right and still send the children to the wrong school within it.
Pair Model Modelled total Offers made Modelled share Observed share
Hove Park / Blatchington M0 484 466 53% 29%
Hove Park / Blatchington M1 497 466 56% 29%
Hove Park / Blatchington M2 461 466 39% 29%
Hove Park / Blatchington M3 472 466 38% 29%
Hove Park / Blatchington M4 509 466 35% 29%
Hove Park / Blatchington M5 491 466 33% 29%
Stringer / Varndean M0 478 630 52% 48%
Stringer / Varndean M1 622 630 59% 48%
Stringer / Varndean M2 612 630 49% 48%
Stringer / Varndean M3 614 630 49% 48%
Stringer / Varndean M4 630 630 48% 48%
Stringer / Varndean M5 630 630 48% 48%

Stringer and Varndean are fixed. The distance-only model splits that catchment 52% to Varndean against an observed 48%; weighted preferences make it worse at 59%; the capacity ceiling brings it to 49% and the full model lands on 48%. Stringer and Varndean are 470 metres apart and both fill, so what decides the split between them is which one runs out of places first — a constraint, not a preference.

Hove Park and Blatchington Mill are improved and not fixed. The observed split sends 29% to Hove Park. Brightopia says 53%, and the full model still says 35%. Hove Park remains the one school the model cannot place: it is pinned at its admission number of 180 when only 136 were offered.

WarningWhat this is not

These are not estimates. \(\gamma\) and \(\delta\) are fitted to ten destination totals. That is enough to say whether a term moves the model towards the offers and nowhere near enough to say what its value is; the identification problem the open model sets out has not gone away. A \(\delta\) of +0.3 fitted on ten numbers after a capacity constraint has already absorbed most of the variance should be read as “this term has little left to do”, not as a measurement of agglomeration.

M5 below does estimate the catchment term, from a different target: the catchment-level preference table, which has sixty cells rather than ten totals and is not already fixed by the admission numbers.

Adding terms until the fit improves is not validation. Each rung here has more freedom than the last, so some improvement is guaranteed. The reason the capacity result is worth stating is that it is not a fitted parameter at all — the admission numbers are published, and putting them in is a correction rather than a tuning.

The faith split makes things worse here, and that is informative. Holding half the city ineligible for Cardinal Newman and King’s, as Section 5.4’s identification work does, drops R² to 0.79: it leaves Cardinal Newman unable to fill, and Cardinal Newman filled. A refinement that helps against the published second-preference profile hurts against total offers. Both targets are real, and no single specification here is best at both.

7.8.3 M5 — calibrated to what each catchment asks for

Every rung so far is scored against the offers the council made, and from M2 onwards most of those offers are admission numbers the model has been told. That is why the catchment term came out at \(\gamma\) = 0.4: it was asked what is left for catchment to explain once capacity has done its work. That is not the question a catchment policy turns on. The question is how strongly families follow the map when they choose, and it has an answer in data.

The council’s evidence to the Schools Adjudicator includes, for each catchment and each of the 3 rounds from 2023/24 to 2025/26, how many first, second and third preferences its children gave each school. It is a flow matrix at catchment level — sixty cells, where the offers are ten totals, and not already fixed by the admission numbers — and it shows something M4 gets badly wrong.

Show code
cal$own %>%
  arrange(desc(observed)) %>%
  transmute(Catchment = unname(catch_lab[home]),
            `Families' first preferences` = fmt_pct(100 * observed, 0),
            M4 = fmt_pct(100 * M4, 0),
            M5 = fmt_pct(100 * M5, 0)) %>%
  knitr::kable(align = "lrrr")
Table 26: Share of each catchment’s first preferences going to its own catchment school or schools: what families did, where M4 sends that catchment’s demand, and M5. Pooled over three rounds.
Catchment Families’ first preferences M4 M5
Stringer / Varndean 81% 59% 83%
Patcham 65% 42% 70%
Portslade Aldridge 57% 44% 50%
Hove Park / Blatchington 44% 49% 45%
Brighton Aldridge 41% 24% 41%
Longhill 40% 17% 32%

Families follow their catchment far more closely than M4 allows. 81% of first preferences from the Stringer / Varndean catchment go to one of its two schools; M4 sends 59% of that catchment’s demand there and spreads the rest across the city. With the demand spread thin, the pressure that makes Varndean turn catchment children away never builds in the model — and Hove Park, next to the densest part of Hove, collects the demand Stringer and Varndean should have.

M5 fits three things to the table, all on uncapped demand — what families ask for — and then applies the capacity ceiling. Unlike M2, it re-offers the children a full school refuses the way a family’s second preference would, rather than in proportion to their first.

7.8.3.1 A catchment term for each catchment

\[ T_{ij} \;=\; A_i\, O_i\; W_j\; C_j^{\delta}\; e^{\gamma_{h(i)} \kappa_{ij}}\; c_{ij}^{-\beta} \]

where \(h(i)\) is the catchment neighbourhood \(i\) lies in. The six \(\gamma_h\) are fitted by multinomial deviance: how likely the first preferences each catchment’s children actually gave are, under the shares the model gives them. The fit uses the pre-2024 map the preferences were made under.

Show code
tibble(home = names(cal$gamma), gamma = unname(cal$gamma)) %>%
  arrange(desc(gamma)) %>%
  transmute(Catchment = unname(catch_lab[home]),
            `γ` = sprintf("%.1f", gamma),
            `Pull of the catchment school` = sprintf("×%.1f", exp(gamma))) %>%
  knitr::kable(align = "lrr")
Table 27: The catchment term, fitted for each catchment. e to the gamma is how many times more attractive a school is to a family living in its catchment than to an otherwise identical family outside it.
Catchment γ Pull of the catchment school
Stringer / Varndean 2.6 ×13.5
Longhill 2.3 ×10.0
Patcham 1.8 ×6.0
Portslade Aldridge 1.7 ×5.5
Hove Park / Blatchington 1.5 ×4.5
Brighton Aldridge 1.2 ×3.3

The terms run from 1.2 in Brighton Aldridge to 2.6 in Stringer / Varndean, against the single 0.4 fitted to offers. A catchment school is between 3 and 13 times as attractive to a family living in its catchment as to one outside it. Catchment is the strongest behavioural term in the model, not the weakest.

7.8.3.2 Attractiveness balanced to what families ask for

In M1 to M4, \(W_j\) is weighted preferences per place. Once distance, the catchment term and competing destinations act on it, the demand the model generates no longer matches the demand it was built from. M5 re-balances \(W_j\) so the model’s uncapped demand for each school is that school’s share of the city’s first preferences in the same table.

Show code
tibble(name = names(cal$W),
       W4 = unname(cal$W_wprefs[names(cal$W)]), W5 = unname(cal$W)) %>%
  mutate(W4 = W4 / mean(W4), W5 = W5 / mean(W5)) %>%
  left_join(cal$wanted, by = "name") %>%
  left_join(oi$schools %>% select(name, pan2026), by = "name") %>%
  arrange(desc(wanted / pan2026)) %>%
  transmute(School = short_sch(name),
            `W, M1 to M4` = sprintf("%.2f", W4),
            `W, M5` = sprintf("%.2f", W5),
            `Demand before the ceiling` = fmt_n(wanted),
            PAN = fmt_n(pan2026),
            `Demand per place` = sprintf("%.2f", wanted / pan2026)) %>%
  knitr::kable(align = "lrrrrr")
Table 28: Attractiveness before and after balancing, both scaled to a mean of one across the city’s ten schools, and the demand M5 puts on each school before the capacity ceiling, for the 2026 cohort.
School W, M1 to M4 W, M5 Demand before the ceiling PAN Demand per place
Varndean 1.53 1.01 420 300 1.40
King’s 1.67 2.51 231 180 1.29
Cardinal Newman 1.19 3.33 442 360 1.23
Patcham 0.89 0.78 194 225 0.86
Blatchington Mill 0.98 0.65 268 330 0.81
Hove Park 1.13 0.34 126 180 0.70
Dorothy Stringer 1.15 0.54 229 330 0.69
Portslade Aldridge 0.68 0.26 143 220 0.65
Brighton Aldridge 0.48 0.48 84 180 0.47
Longhill 0.30 0.11 79 210 0.38

Two schools move most. Cardinal Newman was under-weighted: families ask for it city-wide and it gets no catchment term, so a weight built from preferences per place left the model wanting fewer places there than it has. M5 puts 442 children’s demand on its 360 places, and it fills — as it always does. Hove Park was the opposite, and now draws 126 against 180 places, which is why it no longer fills in the model. It does not fill in reality either.

Balancing to weighted preferences instead of first preferences was tried. It fills Hove Park, which does not fill, so first preferences are used: they are also one unit per child, which is what the model’s flows are.

7.8.3.3 Families who would take only one of the pair

Every model up to M4 treats a child in a paired catchment as equally content with either school: refused at Varndean, the child falls back on Stringer. Some families would not. A child who names Varndean and not Stringer, and is refused at Varndean, is placed somewhere else — and that child is exactly who is displaced from the catchment. M5 carries those families as populations of their own, whose choice sets leave the other school out.

Show code
cal$exclusive %>%
  transmute(Catchment = unname(catch_lab[catchment]),
            `Would take only` = short_sch(school),
            `Named it, three rounds` = fmt_n(named),
            `Share used` = fmt_pct(100 * share, 1),
            Bounds = sprintf("%s to %s", fmt_pct(100 * share_lo, 1),
                             fmt_pct(100 * share_hi, 1))) %>%
  knitr::kable(align = "llrrr")
Table 29: Families in the two paired catchments who would accept only one school of the pair. The table counts how many children named each school at any rank, not who named both, so the share is bounded rather than observed. The share used sits midway between the most overlap the counts allow and what independent naming would give.
Catchment Would take only Named it, three rounds Share used Bounds
Hove Park / Blatchington Hove Park 1,344 5.2% 0.0% to 18.2%
Hove Park / Blatchington Blatchington Mill 1,919 29.7% 24.5% to 42.7%
Stringer / Varndean Dorothy Stringer 1,818 3.3% 0.0% to 7.4%
Stringer / Varndean Varndean 1,877 6.2% 2.9% to 10.4%

The default is not taken to either end. Families treat a pair as substitutes, so the real overlap is above what independent naming would produce, and the share of exclusive families below it. The simulator lets it be scaled.

7.8.3.4 Children who leave the city

Every rung up to here sends every child to one of the ten city schools. Some go elsewhere, and for Longhill that is most of the question. The council’s answer to a Freedom of Information request, published on WhatDoTheyKnow, gives every school each catchment’s children were offered a place at in the 2024 round, including schools outside the city: Longhill’s catchment was offered 38 places at Priory School in Lewes, and fewer than five each at Peacehaven and Seahaven.

So M5 carries four East Sussex schools — Priory, Peacehaven, Seahaven and Seaford Head — as destinations, each with an attractiveness of its own and one decay on straight-line distance, fitted to each catchment’s share of those offers. Straight-line rather than routed, because the bus network has no Woodingdean to Lewes service: the router sends those families through Brighton, 71 to 95 minutes, and a model on that cost sends Longhill’s leavers to Peacehaven instead of Priory. Counts the council suppressed enter the fit as the interval they are. The fitted decay is steep, 8.0: a school outside the city draws from the neighbourhoods nearest it and from almost nowhere else.

Show code
if (!is.null(cal$outside)) {
  cal$outside$fit %>%
    filter(published != "0" | expected_2024 >= 0.5) %>%
    transmute(Catchment = dplyr::coalesce(unname(catch_lab[catchment]), catchment),
              School = name, `Published, 2024` = published,
              `Modelled` = sprintf("%.1f", expected_2024)) %>%
    knitr::kable(align = "llrr")
}
Table 30: Offers at the four East Sussex schools in the 2024 round, published and modelled. Modelled at the 2024 scale: M5’s share of each catchment’s children, times the children that catchment was offered places. Rows where neither is above half a child are left out.
Catchment School Published, 2024 Modelled
Brighton Aldridge Priory School <5 5.3
Stringer / Varndean Priory School <5 1.2
Hove Park / Blatchington Priory School <5 0.3
Longhill Peacehaven Community School <5 2.2
Longhill Priory School 38 35.2
Longhill Seahaven Academy <5 2.1
Patcham Priory School 0 1.4

In 2026 M5 sends 42 children from Longhill’s catchment to these schools, 37 of them to Priory. Children offered places in West Sussex or London — a handful a year from Hove, Portslade and Stringer / Varndean — are not modelled, nor are those who go to independent schools.

7.8.3.5 Where refused children go

M2’s ceiling cuts every applicant to a full school back by the same share and then scales each neighbourhood’s flows back up until all its children are placed. That spreads a refused child across every other school in proportion to how much the neighbourhood wanted it first. It is a fair average of a random tie-break at the school that is full. It is not a fair account of where the refused child then goes, and in one place it produced a contradiction: in the Stringer / Varndean catchment the model counted families who would take either school as displaced from the catchment while Stringer itself was short of its admission number, because their refused Varndean demand had been spread across the city rather than offered to the school next door.

M5 re-offers refused demand in two steps, and only ever to schools with room:

  1. The other school of the pair first. A family in a paired catchment who would take either school, refused at one, goes to the other while it has places.
  2. Then second preferences. Everything else is shared among schools with room in proportion to the second preferences the family’s home catchment gives each school, in the same catchment preference table, adjusted for how near this neighbourhood is to each school compared with its catchment as a whole.

Demand re-offered to a school with little room can overfill it; the next round cuts that back and re-offers the excess. If no school anywhere has room, the children stay unplaced rather than being forced over an admission number. It is the average of what a round of deferred acceptance does when ties are broken at random, with second preferences standing in for the rest of each family’s list.

Show code
if (!is.null(cal$overflow)) {
  cal$overflow %>%
    mutate(Catchment = unname(catch_lab[catchment]), School = short_sch(school)) %>%
    select(Catchment, School, share) %>%
    group_by(Catchment) %>%
    arrange(desc(share), .by_group = TRUE) %>%
    slice_head(n = 3) %>%
    summarise(`Where its second preferences go, top three` =
                paste(sprintf("%s %s", School, fmt_pct(100 * share, 0)), collapse = ", "),
              .groups = "drop") %>%
    knitr::kable(align = "ll")
}
Table 31: Where each catchment’s second preferences go: the share of the catchment’s second preferences, over three rounds, naming each school. These are the weights refused children are re-offered with, among schools that still have places. Faith schools are included; they are full in every round, so they take none of it.
Catchment Where its second preferences go, top three
Brighton Aldridge Varndean 29%, Patcham 26%, Dorothy Stringer 22%
Hove Park / Blatchington Blatchington Mill 31%, Hove Park 25%, King’s 21%
Longhill Varndean 24%, Dorothy Stringer 21%, Longhill 17%
Patcham Varndean 42%, Dorothy Stringer 31%, Patcham 14%
Portslade Aldridge Hove Park 30%, King’s 27%, Blatchington Mill 18%
Stringer / Varndean Dorothy Stringer 55%, Varndean 32%, Cardinal Newman 6%

The kernel is a trait of where a family lives, like the catchment term, so under a redrawn map it stays with the neighbourhood. The same code runs in the simulator, which is checked against these runs.

7.8.3.6 What M5 changes

Show code
cal$fit %>%
  transmute(Model = model, Deviance = fmt_n(deviance),
            `Cell RMSE, percentage points` = sprintf("%.1f", cell_rmse_pp)) %>%
  knitr::kable(align = "lrr")
Table 32: Fit to the catchment preference table: multinomial deviance, and the root-mean-square gap between modelled and observed shares across the sixty catchment by school cells.
Model Deviance Cell RMSE, percentage points
M4 3,220 8.3
M5 825 3.6

Against what families ask for, the deviance falls from 3,220 to 825 and the typical gap in a catchment’s share for a school from 8.3 percentage points to 3.6.

Show code
mt_table("M5", "M4") %>% knitr::kable(align = "lrrrrrc")
Table 33: M5 against the offers made. The change column is against M4. M5 is not fitted to these numbers, so this is a test it was not built to pass.
School PAN Offers made Modelled Residual Change from M4 Rationing?
Brighton Aldridge 180 146 95 -51 +5
Portslade Aldridge 220 183 153 -30 +12
Blatchington Mill 330 330 327 -3 +2 yes
Cardinal Newman 360 360 360 +0 -11 yes
Dorothy Stringer 330 330 330 +0 +0 yes
King’s 180 180 180 +0 +0 yes
Varndean 300 300 300 +0 +0 yes
Longhill 210 81 89 +8 +6
Patcham 225 204 220 +16 +2
Hove Park 180 136 164 +28 -16

R² 0.95, RMSE 22, and 27 children average error across the five schools that were not rationing places. Against the offers it is not built to reproduce, M5 lands close to M4 overall; the difference is in which schools the ceiling binds on. Cardinal Newman fills and Hove Park does not, as in the real rounds, and the Stringer / Varndean split is 48% to Varndean against an observed 48%.

NoteWhat M5 still does not do

Distance decay is set, not fitted. A catchment-level table cannot separate how far families will travel within a catchment from how strongly they follow it. \(\beta\) stays at 1.7, the decay the accessibility work settles on.

The catchment terms are fitted to one map. They describe how families in each neighbourhood chose under the boundaries they had. Carried to a redrawn map, they assume families would follow a new boundary as closely as the old one, which no data here can confirm.

Two catchments are still under-fitted. Longhill’s children put Longhill first 40% of the time and M5 gives it 32%; Portslade Aldridge 57% against 50%. With one distance decay for the whole city, the model cannot make eastern and western families both follow their catchment this closely and travel as far as they do.

The exclusive families are a bounded assumption. The share is taken from inside the bounds the table allows, not measured.

7.8.4 The modelling choices, and why

Every modelling decision behind the full model and the simulator, in one place. “Set” means chosen and stated rather than estimated.

Component What is used Where it comes from Why, and what else was tried
What a flow is Preferences, not offers Offers are rationed. A model fitted to them learns the admission numbers, which is what happened to \(\gamma\) in M3.
Who is modelled Brighton & Hove children; the ten city schools and four East Sussex schools as destinations Cohort estimates from section 3 Children do leave the city, about 70 a year, three in four of them from Longhill’s catchment and most of those for Priory School in Lewes. Priory, Peacehaven, Seahaven and Seaford Head are destinations. West Sussex and London schools, a handful a year, are not; the simulator shows them as an estimate, the adjudicator’s published count less what the model places in East Sussex. Children who go to independent schools or move away are in neither, so every city school’s intake is slightly high.
Schools outside the city An attractiveness for each, one decay on straight-line km, no catchment term, not rationed The council’s FOI response on WhatDoTheyKnow: 2024 offers by catchment and school Routed walk and bus time was tried and sends Longhill’s leavers to Peacehaven: there is no Woodingdean to Lewes bus in the network. The adjudicator’s catchment totals alone get the numbers right and the destinations wrong.
Journey cost Routed walk and bus minutes, \(c_{ij}^{-\beta}\) r5r over OpenStreetMap and bus timetables Straight-line distance sends families across hills and water no bus crosses.
Distance decay \(\beta\) = 1.7 Set Not identifiable from catchment-level data. It is the value the accessibility work settles on, inside the open model’s swept range.
School attractiveness \(W_j\) balanced so demand matches each school’s share of first preferences Catchment preference table, school totals Weighted preferences per place (M1 to M4) left Cardinal Newman short of its demand and gave Hove Park more than twice its own. Balancing to weighted preferences filled Hove Park, which does not fill.
Catchment A term for each catchment, \(e^{\gamma_h}\), by where a family lives Catchment preference table, by deviance One city-wide term fitted to offers came out at 0.4. Fitted to preferences the terms run from 1.2 to 2.6. Fitted on the pre-2024 map the preferences were made under; run on the map in force. Under a redrawn map the term stays with the neighbourhood.
Paired catchments A share of families who would take only one school of the pair Bounded by the same table; set midway The table counts who named each school, not who named both. Without it, a child refused at one school of the pair always falls back on the other, and displacement from those catchments is understated.
Competing destinations \(C_j^{\delta}\), \(\delta\) = +0.3 Fitted in the ladder, against offers Kept at its ladder value and not re-fitted. It is a small term; Section 7.8.2 sets out why its sign should not be read as a finding.
Faith schools Open to every family Holding half the city ineligible fits the second-preference profile better but stops Cardinal Newman filling, and it fills.
Capacity A one-sided ceiling; refused demand re-offered to the other school of a pair, then by second preference, to schools with room Admission numbers in force; catchment preference table, second preferences Applicants to a full school are cut back in proportion, which is what a random tie-break averages to — and the council breaks ties at random. Re-offering refused children in proportion to their first preferences (M2 to M4) left Stringer short of its number while counting Stringer / Varndean families as displaced.
Admission rules (simulator only) Optionally, the council’s 2026/27 priorities as a tiered ceiling at the six community schools Admissions guide 2027–28 FSM eligibility is the IDACI score scaled by one take-up constant, set so FSM-priority offers at the three schools that ration match the 192 made in September 2026. Priority-6 places are not fitted.
Offer day, not September Every comparison is with national offer day Six months of appeals and movement follow offer day, so September rolls are not what an allocation produces.

7.9 Every school, under the full model

Section 7.3 showed Longhill under each rung of the ladder. This is the other nine, all under the full model, M5 (Section 7.8.3) — attractiveness balanced to first preferences, a catchment term for each catchment, competing destinations, the paired-catchment families who would take only one of the two schools, and the capacity ceiling.

Show code
ma <- leaflet(width = "100%", height = 660,
              options = leafletOptions(preferCanvas = TRUE)) %>%
  add_basemap() %>%
  addPolygons(data = catch, fill = FALSE, color = "#555555",
              weight = 1, opacity = 0.5, group = "Catchment boundaries")

for (i in seq_len(nrow(fa_groups))) {
  g <- fa_groups$label[i]
  d <- fa_net %>% filter(label == g) %>% arrange(flow)
  ma <- ma %>%
    addPolylines(data = d, group = g,
                 color = ~ unname(FM_COL[leg_mode]),
                 weight = ~ fm_w(flow), opacity = 0.75,
                 label = ~ sprintf("%s leg · %.0f children", leg_mode, flow)) %>%
    addCircleMarkers(
      data = fm_sch %>% filter(name == fa_groups$name[i]), group = g,
      radius = 6, color = "#111111", weight = 2, opacity = 1,
      fillColor = "#ffffff", fillOpacity = 1, label = ~ name) %>%
    addCircleMarkers(
      data = fm_sch %>% filter(name != fa_groups$name[i]), group = g,
      radius = 3, color = "#777777", weight = 1, opacity = 0.9,
      fillColor = "#cccccc", fillOpacity = 0.9, label = ~ name)
}

ma %>%
  fitBounds(lng1 = unname(fa_bb["xmin"]) - 0.01, lat1 = unname(fa_bb["ymin"]) - 0.005,
            lng2 = unname(fa_bb["xmax"]) + 0.01, lat2 = unname(fa_bb["ymax"]) + 0.005) %>%
  addLayersControl(baseGroups = fa_groups$label,
                   overlayGroups = "Catchment boundaries",
                   options = layersControlOptions(collapsed = FALSE)) %>%
  hideGroup("Catchment boundaries") %>%
  addLegend("bottomright", colors = unname(FM_COL), labels = names(FM_COL),
            title = "Leg", opacity = 0.8)
Figure 35: Modelled flows into each school under the full model, routed over the walking and bus network and summed onto shared segments. Every panel is the same model; only the school changes. Line widths are on the same scale as the Longhill map above, so a thick line means the same number of children in both.

Switched one at a time, the maps sort the city into two kinds of school, and the sorting is not the one you might expect.

Longhill draws more locally than any other school in the city. 98% of its modelled intake starts inside its own catchment, against 86% for Portslade Aldridge, the next most self-contained, and 62% for Brighton Aldridge, the least. Only 1 catchments supply even a twentieth of it, the narrowest base in the city.

That sounds like a virtue and is not. A school draws locally either because families nearby want it or because nobody else does, and Section 7.2 has already established which applies here: the same model that gives Longhill this tight local star gives it 86 children in total. A narrow base is only good news when it is a full one.

The faith schools have no catchment and draw along every corridor. Cardinal Newman’s largest single source is the Hove Park / Blatchington catchment at 51%, from the far side of the city. That is the geographic shape of the point Section 5.4 makes about how heavily they are preferred.

One more thing to watch for. The paired catchments share their corridors almost exactly. Switch between Blatchington Mill and Hove Park and the same roads carry both — they draw 82% and 75% from the same catchment. That is why Section 7.5 could not separate them: the model can get the pair’s total right and still split it wrongly, because geography barely distinguishes the two.

WarningThis is an open-data model, and it is not calibrated

\(\beta\) cannot be estimated without pupil-level flows, so it is swept across a plausible range rather than fitted. Figures here use the reference value; the range is 1.5 to 3.2, and section 8 reports the band rather than a point wherever the conclusion depends on it. A calibrated model built on the council’s own allocation records would replace every band in this document with an estimate.

8 Redrawing the catchments

The catchments Brighton uses were drawn round schools. Section 7.3 drew where the model actually sends children. This section asks the obvious next question — if you drew the boundaries round something else, what would they look like, and would they be better? — and answers it with four alternatives and the same statistics applied to each.

NoteTwo methods, and the difference between them is the point

The power diagram is a proximity design. Every neighbourhood goes to the catchment with the lowest journey time minus a price, and the prices are then adjusted until every catchment holds the children its schools have places for. It knows how long journeys take and how many places exist. It knows nothing about what families want.

The flow regions are a demand design, and a standard functional regionalisation of the kind used to build travel-to-work areas:

  1. Dominant flow. Every LSOA joins the school it sends most children to — the Nystuen–Dacey construction, the oldest method there is.
  2. Contiguity repair. A catchment has to be one piece. Fragments move to the adjacent region they send most flow to.
  3. Capacity balance. Regions trade LSOAs until each holds roughly as many children as its schools have places, always moving the boundary LSOA that costs the least flow, and never breaking contiguity.

Which flows go in is the choice that matters. The obvious input is the full model of Section 7.5, and it would be the wrong one: it contains a catchment term, so its flows already know the current boundaries and regionalising them would partly rediscover the map this is meant to replace. The input is the capacity-ceiling model with no catchment term — the closest published data comes to where would children go if the rule did not exist but the places still ran out.

The two faith schools are left out of the geography, as they are now. They admit across the city, and giving them a catchment would be a change of policy rather than a change of map.

Show code
fr_pal <- colorFactor("Set2", domain = sort(unique(fr$regions_sf$grp)))
fr_sch <- schools_sf() %>% filter(name != "Peacehaven Community School")
fr_designs <- unique(fr$regions_sf$design)

# Longhill's marker moves with it in the relocation design.
elm_pt <- st_as_sf(tibble(name = "Longhill High School (Elm Grove)",
                          lon = COMART$lon, lat = COMART$lat),
                   coords = c("lon", "lat"), crs = 4326)

# Five base groups make a tall layers control. Left at the default it
# reached down into the legend in the opposite corner and the last
# option could not be clicked, so the control goes top-left and the
# legend bottom-right, with the map given the height to hold both.
# height only takes effect when width is given too, which is why every
# other map in this document passes the pair.
mr <- leaflet(width = "100%", height = 720,
              options = leafletOptions(preferCanvas = TRUE)) %>%
  add_basemap()

for (d in fr_designs) {
  poly <- fr$regions_sf %>% filter(design == d)
  is_elm <- str_detect(d, "Elm Grove")
  pts <- if (is_elm) fr_sch %>% filter(name != "Longhill High School") else fr_sch
  mr <- mr %>%
    addPolygons(data = poly, group = d,
                fillColor = ~ fr_pal(grp), fillOpacity = 0.45,
                color = "#333333", weight = 1.2, opacity = 0.8,
                label = ~ sprintf("%s · %d LSOAs", grp, lsoas)) %>%
    addCircleMarkers(data = pts %>% filter(!name %in% fr$faith), group = d,
                     radius = 5, color = "#111111", weight = 2, opacity = 1,
                     fillColor = "#ffffff", fillOpacity = 1, label = ~ name) %>%
    addCircleMarkers(data = pts %>% filter(name %in% fr$faith), group = d,
                     radius = 5, color = "#111111", weight = 2, opacity = 1,
                     fillOpacity = 0, label = ~ paste0(name, " (no catchment)"))
  if (is_elm)
    mr <- mr %>% addCircleMarkers(data = elm_pt, group = d, radius = 6,
                                  color = "#b2182b", weight = 2, opacity = 1,
                                  fillColor = "#ffffff", fillOpacity = 1,
                                  label = ~ name)
}

mr %>%
  addLayersControl(baseGroups = fr_designs, position = "topleft",
                   options = layersControlOptions(collapsed = FALSE)) %>%
  addLegend("bottomright", pal = fr_pal,
            values = sort(unique(fr$regions_sf$grp)), title = "Region",
            opacity = 0.8)
Figure 36: Five catchment designs. Switch between them with the control. Points are schools; the two faith schools, which have no catchment under any design, are hollow. In the last design Longhill sits at the top of Elm Grove rather than at Ovingdean.

8.1 How the designs compare

Show code
fr$designs %>%
  left_join(fr$changed %>% group_by(design) %>%
              summarise(moved = sum(Oi[moved]) / sum(Oi), .groups = "drop"),
            by = "design") %>%
  transmute(Design = design,
            Regions = regions,
            `Self-containment` = fmt_pct(100 * self_containment, 0),
            `Mean journey` = sprintf("%.1f min", mean_journey),
            `Worst capacity gap` = sprintf("%+.0f%%", 100 * worst_gap),
            `Children moved` = if_else(is.na(moved), "—",
                                       fmt_pct(100 * moved, 0))) %>%
  knitr::kable(align = "lrrrrr")
Table 34: The five designs. Self-containment is the travel-to-work-area statistic: the share of a region’s children whose modelled first choice is inside it. The capacity gap is the worst region’s demand against its fair share of the places, counting only the children the geography could place — the faith schools take about a fifth of the cohort across no catchment at all.
Design Regions Self-containment Mean journey Worst capacity gap Children moved
Current catchments 6 61% 23.5 min +23%
Power diagram (proximity and capacity) 6 62% 23.9 min +11% 15%
Flow regions, one per school 8 60% 24.1 min +22%
Flow regions, pairs kept 6 66% 23.7 min +11% 17%
Flow regions, Elm Grove, PAN 150 6 60% 23.2 min +94% 17%
Flow regions, Elm Grove, PAN 210 6 60% 23.0 min +49% 17%
Show code
fr$designs %>%
  transmute(Design = design, Fragments = fragments, Enclaves = enclaves,
            `Ragged neighbourhoods` = ragged,
            `Schools outside their own catchment` = schools_outside) %>%
  knitr::kable(align = "lrrrr")
Table 35: Whether each design is a usable map. A fragment is a piece of a catchment detached from the rest of it. An enclave is a catchment wholly surrounded by one other, which a contiguity check misses because a ring is connected. A ragged neighbourhood has at most one neighbour in its own catchment — not an island, but a sliver. The last column counts schools that fall outside their own catchment. The current map is scored on its whole-LSOA approximation.
Design Fragments Enclaves Ragged neighbourhoods Schools outside their own catchment
Current catchments 2 1 9 0
Power diagram (proximity and capacity) 0 0 1 0
Flow regions, one per school 0 0 5 2
Flow regions, pairs kept 0 0 0 0
Flow regions, Elm Grove, PAN 150 0 0 4 0
Flow regions, Elm Grove, PAN 210 0 0 3 0

Three of the five designs are clean maps. The power diagram and both paired flow designs have no detached pieces, no islands and every school inside its own catchment. Getting there took three rules that a capacity-balancing algorithm does not enforce on its own, and each was added because the first attempt broke it:

  • A catchment must contain its own school. Without it the relocation design put the Elm Grove site inside BACA’s region and left Longhill with nine neighbourhoods on the far side of the city.
  • An island counts as a fault. A region can be in one piece and still enclose another; a contiguity test passes it because a ring is connected. The power diagram arrived with three.
  • Repair by accessibility, not by flow. When a piece has to move, it joins whichever neighbouring catchment its children can reach quickest. Repairing by modelled flow instead sent boundaries over hills.

Regions on the coast or at the city edge have one land neighbour and are not islands, so the edge of the study area is exempt — a test that did not exempt it flagged Longhill and PACA as enclaves in every design.

The ragged column is the one a reader notices, and it is where the current map is furthest from the alternatives: 9 neighbourhoods against 1 for the power diagram and 0 for the flow regions. Two of them face each other across Elm Grove. A tongue of BACA reaches down past St Luke’s pool, almost surrounded by Stringer/Varndean; two Stringer/Varndean neighbourhoods reach up just north of it, almost surrounded by BACA.

Both redesigns swap them, without being asked to. Under the current map the neighbourhood by the pool is BACA (Brighton Aldridge) and the two north of Elm Grove are Varndean / Dorothy Stringer. Under the power diagram and the flow regions alike they are the other way round — Varndean / Dorothy Stringer and BACA (Brighton Aldridge) — which is what accessibility says they should be, and it removes both slivers at once.

Splitting the paired catchments fails this audit, which is a cleaner answer than the statistics gave. One region per school leaves 2 schools outside their own catchment, because Dorothy Stringer and Varndean are 470 metres apart and no contiguous capacity-balanced boundary can separate them. That is the flows saying what the council already knows.

The proximity design balances capacity best. The power diagram takes the worst capacity gap from +23% to +11% — a quarter of what the current map carries — while moving the fewest neighbourhoods of any alternative, and it adds 0.4 minutes to the average journey to a catchment school.

The flow regions win the statistic they were built to win. Keeping the council’s pairings gives the best self-containment of any design, 66% against 61% now, for 0.2 extra minutes. But it does not improve on the power diagram anywhere else, and the power diagram does not improve on the current map’s self-containment at all — 62% against 61%.

So the two methods answer different questions and neither dominates. Proximity-and-capacity produces the better-balanced, more compact map. Demand produces the map more children’s first choice sits inside.

8.2 The deprivation profile of each design

The consultation profiles the current catchments by putting every postcode inside a catchment boundary and counting households with dependent children by IDACI decile. The same method is applied here to every design, so what follows compares against the consultation’s own figures rather than against some other statistic that happens to be about deprivation.

Show code
bd <- fr$idaci_bands %>%
  mutate(design = factor(design, fr_designs),
         band = factor(band, fr$idaci_band_levels),
         region = fr_lab(grp)) %>%
  group_by(design, region) %>%
  mutate(dep = sum(share[band == fr$idaci_band_levels[1]])) %>%
  ungroup() %>%
  arrange(design, dep)

# The row key has to be unique across designs, because a catchment
# appears in several of them; the label shown is just the catchment.
row_lv <- unique(paste(bd$design, bd$region, sep = "|"))
bd <- bd %>% mutate(row = factor(paste(design, region, sep = "|"), row_lv))
row_lab <- setNames(sub("^.*\\|", "", row_lv), row_lv)

# The share in the most deprived band is printed OUTSIDE the bar. Inside
# it sat on the darkest fill and could not be read.
lab <- bd %>% filter(band == fr$idaci_band_levels[1])

ggplot(bd, aes(share, row, fill = band)) +
  geom_col(width = 0.8, colour = "white", linewidth = 0.5,
           position = position_stack(reverse = TRUE)) +
  geom_text(data = lab, aes(x = 1.03, y = row,
                            label = sprintf("%.0f%%", 100 * share)),
            hjust = 0, size = 2.5, colour = "grey25", inherit.aes = FALSE) +
  facet_grid(design ~ ., scales = "free_y", space = "free_y",
             labeller = label_wrap_gen(18), switch = "y") +
  scale_y_discrete(labels = row_lab) +
  scale_x_continuous(labels = scales::label_percent(),
                     breaks = seq(0, 1, 0.25), limits = c(0, 1.13),
                     expand = expansion(mult = c(0, 0))) +
  scale_fill_manual(values = BAND_COL, name = NULL) +
  labs(x = "Households with dependent children", y = NULL,
       title = "What kind of neighbourhood each catchment holds",
       subtitle = str_wrap(paste("Each bar is one catchment, ordered within its design by how deprived it is.",
                                 "The figure at the right is the share in the three most deprived deciles in England."), 96),
       caption = "Sources: ONS postcode household counts; IMD 2019 IDACI.") +
  theme_bh(8) +
  theme(legend.position = "top",
        panel.grid.major.y = element_blank(),
        strip.placement = "outside",
        strip.text.y.left = element_text(angle = 0, hjust = 1, face = "bold",
                                         size = 7))
Figure 37: How each catchment’s households with dependent children divide between the most deprived three deciles nationally, the middle four, and the least deprived three. Every bar is one catchment; bars are grouped by design and ordered within it by how deprived the catchment is.

BACA’s catchment is the outlier under every design, and under the current map it is extreme: 84% of its households with dependent children are in the three most deprived deciles nationally, against 12% in the least deprived catchment. No alternative has a catchment above 60%.

That range is the obvious summary and it is a poor one, because it says nothing about how many households sit at each end. A design can have a narrow range and still concentrate deprivation, and one here does. The measure that does not have that problem is a segregation curve.

Show code
cv <- fr$idaci_curve %>% mutate(design = factor(design, fr_designs))
ends <- cv %>% group_by(design) %>% slice_max(x, n = 1) %>% ungroup()
worst <- cv %>% group_by(design) %>% slice_max(gap, n = 1) %>% ungroup()

# Six lines is past the point where direct-labelling every one is
# readable - four of them run through the same cluster around 45% and
# the labels sat on top of each other. Legend for identity, direct
# labels for the two that matter: the best and the worst.
mark <- worst %>%
  filter(design %in% c(fr$idaci_summary$design[which.min(fr$idaci_summary$gorard)],
                       fr$idaci_summary$design[which.max(fr$idaci_summary$gorard)]))

ggplot(cv, aes(x, y, colour = design)) +
  geom_abline(slope = 1, intercept = 0, colour = "grey55", linetype = "31") +
  geom_segment(data = mark, aes(x = x, xend = x, y = y, yend = x),
               linewidth = 0.6, alpha = 0.6) +
  geom_line(linewidth = 0.9) +
  geom_point(data = mark, size = 2.6) +
  ggrepel::geom_text_repel(
    data = mark, aes(label = sprintf("%s\nwidest gap %.2f", str_wrap(design, 24), gap)),
    size = 2.8, hjust = 0, nudge_x = 0.07, nudge_y = -0.09,
    segment.size = 0.25, min.segment.length = 0, seed = 1, lineheight = 0.95,
    show.legend = FALSE) +
  scale_colour_manual(values = DESIGN_COL, name = NULL,
                      guide = guide_legend(nrow = 2, byrow = TRUE)) +
  scale_x_continuous(labels = scales::label_percent(),
                     limits = c(0, 1.02), breaks = seq(0, 1, 0.25)) +
  scale_y_continuous(labels = scales::label_percent()) +
  labs(x = "Cumulative share of all households with dependent children",
       y = "Cumulative share of those in the three most deprived deciles",
       title = "How evenly each design spreads disadvantage",
       subtitle = str_wrap(paste("The dashed line is a perfectly even spread.", "The marked points are the most and least even designs, at their widest gap from it - which for catchments ordered this way is Gorard's index exactly."), 100),
       caption = "Sources: ONS postcode household counts; IMD 2019 IDACI.") +
  theme_bh(11) +
  theme(legend.position = "top", legend.text = element_text(size = 8))
Figure 38: Segregation curves. Catchments are ordered from least to most deprived, then the cumulative share of all households with dependent children is plotted against the cumulative share of those in the three most deprived deciles. A design that spread deprivation evenly would trace the diagonal; the further a line bows below it, the more one catchment carries.
Show code
# The widest gap on the curve above is NOT reported as a second column.
# On catchments ordered by deprivation it is arithmetically identical to
# Gorard, to every decimal, so printing both would be one measure
# dressed as two agreeing with each other. R/03_flow_regions.R asserts
# the identity rather than trusting this comment.
fr$idaci_summary %>%
  arrange(gorard) %>%
  transmute(Design = design,
            `Least deprived catchment` = fmt_pct(100 * lo, 0),
            `Most deprived catchment` = fmt_pct(100 * hi, 0),
            `Range` = sprintf("%.0f points", 100 * spread),
            `Gorard index` = sprintf("%.3f", gorard)) %>%
  knitr::kable(align = "lrrrr")
Table 36: How evenly each design spreads disadvantage. The share columns are households with dependent children in the three most deprived deciles, for the least and most deprived catchment of each design. Gorard’s segregation index is the measure the open model uses for the same question: half the sum of the absolute difference between each catchment’s share of the city’s deprived households and its share of all households. Zero would be a perfectly even spread. Ranked best first.
Design Least deprived catchment Most deprived catchment Range Gorard index
Power diagram (proximity and capacity) 10% 60% 50 points 0.195
Flow regions, pairs kept 10% 57% 47 points 0.206
Current catchments 12% 84% 73 points 0.214
Flow regions, Elm Grove, PAN 210 0% 57% 57 points 0.230
Flow regions, Elm Grove, PAN 150 0% 57% 57 points 0.236
Flow regions, one per school 7% 57% 50 points 0.311

The power diagram spreads disadvantage most evenly, and it is not close. Its Gorard index is 0.195 against 0.214 for the map in force — about a 9% reduction — and its curve sits closest to the diagonal at every point.

3 of the alternatives are worse than the current map, which the range column hides. Look at the flow-region design with one catchment per school: its range is 50 points, narrower than the current map’s 73, and its curve bows furthest from the diagonal of any design here. A narrow range across catchments of very different sizes is not evenness.

The current map’s extreme catchment is BACA, and BACA is small. Concentrating disadvantage in a small catchment moves fewer households than spreading a moderate excess across two large ones — which is what the curve shows and the range cannot. Gorard weights by size; maximum minus minimum does not.

8.2.1 A catchment is not an intake

Everything above measures the neighbourhoods a boundary encloses. That is what the consultation profiles and it is the right first question, but it is not the question a parent or a headteacher is actually asking, which is who ends up in the school. The two are not the same thing. A real minority of children cross a boundary, the two faith schools admit across the city with no catchment at all, and any school that fills before its catchment does forces the rest outward.

So the same index is computed a second way: on the modelled intake of each school under the over-subscription rule in Section 8.4, rather than on the population of each catchment.

Show code
# A validated two-colour categorical pair, deliberately outside the six
# used for designs elsewhere in this section: colour here means which
# measure, not which design, and reusing the design hues for a different
# job is how a reader comes to think blue means "current catchments"
# everywhere.
PAIR_COL <- c(`Across the catchments` = "#6b4fbb",
              `Across the school intakes` = "#c2701c")

dm <- fr$score_wide %>%
  transmute(design = factor(design, fr_designs),
            `Across the catchments` = gorard_catch,
            `Across the school intakes` = gorard_schools) %>%
  tidyr::pivot_longer(-design, names_to = "measure", values_to = "g") %>%
  mutate(measure = factor(measure, names(PAIR_COL))) %>%
  # Labels go on the OUTER side of each point rather than above it. Two
  # of these designs differ by four thousandths, and above the points
  # their labels printed on top of each other as "0.31014".
  group_by(design) %>% arrange(g, .by_group = TRUE) %>%
  mutate(side = c(-1, 1)) %>% ungroup()

seg <- dm %>% select(-side) %>%
  tidyr::pivot_wider(names_from = measure, values_from = g)

ggplot(dm, aes(g, fct_rev(design))) +
  geom_segment(data = seg, aes(x = `Across the catchments`,
                               xend = `Across the school intakes`,
                               y = fct_rev(design), yend = fct_rev(design)),
               inherit.aes = FALSE, colour = "grey70", linewidth = 1.1) +
  geom_point(aes(colour = measure), size = 3.4) +
  geom_text(aes(label = sprintf("%.3f", g), colour = measure,
                hjust = if_else(side > 0, -0.3, 1.3)),
            size = 2.8, show.legend = FALSE) +
  scale_colour_manual(values = PAIR_COL, name = NULL) +
  scale_x_continuous(limits = c(0.15, 0.38), breaks = seq(0.15, 0.35, 0.05)) +
  scale_y_discrete(labels = function(x) str_wrap(x, 26)) +
  labs(x = "Gorard's segregation index", y = NULL,
       title = "A catchment is not an intake",
       subtitle = str_wrap(paste("The catchments a design encloses, against the intakes its schools fill once children cross boundaries.", "Further right is more segregated."), 100),
       caption = "Sources: ONS postcode household counts; IMD 2019 IDACI; modelled allocation.") +
  theme_bh(11) +
  theme(legend.position = "top", panel.grid.major.y = element_blank())
Figure 39: Gorard’s segregation index computed two ways for each design: across the catchments the design draws, and across the intakes its schools fill once the over-subscription rule has run. Zero would be a perfectly even spread. Five of the six designs fill their schools less evenly than they draw their map.

Five of the six designs are more segregated at the school gate than on the map, and the current one by the widest margin: 0.214 across the catchments becomes 0.298 across the intakes. Choice and over-subscription do not dilute the geography here. They sharpen it.

The exception is the design that fails everything else. One catchment per school goes from 0.311 to 0.306 — unchanged to within four thousandths — because it is already so segregated on the map that the allocation has nothing left to add.

That is the finding worth carrying out of this section. A boundary review that scores itself on catchment populations will overstate what it has achieved, because the population of a catchment is not the intake of its school, and the gap between the two is large in every design that is worth drawing.

The ranking changes as well. The power diagram is still first — 0.233 against 0.298 now — but the flow regions move up to second on intakes, 0.255, and both relocation designs move down: putting Longhill at Elm Grove leaves 0.332 at an admission number of 150 and 0.304 at 210, both worse than the map in force.

Under the current map the modelled intakes run from 0% of children from the three most deprived deciles at Blatchington Mill to 81% at Brighton Aldridge. No alternative closes that; the best of them narrows it to 4% to 65%.

NoteRead the two faith schools out of this

The index above is computed across the eight schools that have a catchment, so it is like for like with the catchment figure. Adding the two faith schools moves it to 0.314 for the current map and 0.266 for the power diagram — the same ordering, slightly higher levels.

But that column should be read with more suspicion than the rest of this section. The model admits to the faith schools on distance alone, because their actual criteria are faith-based and no published dataset contains them. Their modelled intakes are therefore the least reliable numbers in this document, and they take about a fifth of the cohort.

ImportantA finding that reversed under a better measure

An earlier version of this section reported that every alternative was less segregated than the current map. That was computed as the range between the least and most deprived catchment, and on that statistic it was true.

It does not survive being measured properly. Once catchment size is taken into account, 3 of the alternatives spread disadvantage less evenly than the map in force and 0 matches it. The claim has been withdrawn and the ranking above is by Gorard.

What survives is narrower and still worth having: one design, the power diagram, is clearly better than the current map on this measure, and it is better on capacity and shape too.

And a caution against reading any of this as a solution. 0.195 is a large index, and the best curve here still bows a long way from the diagonal. No redrawing of a catchment map fixes residential segregation, because the segregation is in where families live, not in where the lines are. What a map can do is stop amplifying it, and the distance between the best and worst curves is the size of that amplification.

8.2.2 Opening places to single-school catchments

The 2026/27 arrangements added priority 6: a share of places at each community school for children living in the four single-school catchments — Brighton Aldridge, Longhill, Patcham and Portslade — who apply outside them. The council first consulted on 20% and settled on 5%. The case for it is access: families in catchments with one school, one of them the most deprived in the city, get a route into the popular schools of the two paired catchments.

The simulator runs the council’s priorities as a tiered ceiling on the full model of section 7 (Section 7.8.3): free school meals first, up to 30% of places, then priority 6 up to its share, then the catchment. Moving the priority-6 share shows something the policy does not intend.

Show code
P6_COL <- c(`2026` = "#0d366b", `2030` = "#c2701c")
p6s$sweep %>%
  mutate(year = factor(year), fsm = if_else(fsm, "With the FSM priority", "Without it")) %>%
  ggplot(aes(p6, gorard, colour = year, linetype = fsm)) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 2) +
  scale_colour_manual(values = P6_COL, name = NULL) +
  scale_linetype_manual(values = c(`With the FSM priority` = "solid", `Without it` = "22"), name = NULL) +
  scale_x_continuous(breaks = unique(p6s$sweep$p6), labels = function(x) paste0(x, "%")) +
  labs(x = "Priority-6 share of each community school's places", y = "Gorard's segregation index",
       title = "More places for single-school catchments, more segregated intakes",
       subtitle = "Further up is more segregated. The lines flatten once nobody else wants the places.",
       caption = "Sources: modelled intakes (M5); IMD 2019 IDACI; ONS postcode household counts.") +
  theme_bh(11) +
  theme(legend.position = "top")
Figure 40: Gorard’s segregation index across the ten city schools’ modelled intakes, under the council’s priorities, as the priority-6 share rises. Solid lines keep the free school meals priority; dashed lines switch it off. Full model (M5) with the tiered ceiling the simulator runs. The index here is across all ten city schools, so its level is not comparable with the eight-school figures above; the direction is the point.

The larger the priority-6 share, the more segregated the intakes. In 2026 the index is 0.152 with priority 6 switched off, 0.154 at the 5% in force, 0.162 at 15% and 0.163 at 20%, where the first proposal would have put it. Past about 15% it stops moving, because there are no more families in the single-school catchments who want the places: 166 children at 15%, 169 at 40%. It rises the same way with the free school meals priority switched off, and in 2030. The rise is modest — about 6% of the index between none and 15% — but it runs in the wrong direction for a policy about access.

Show code
p6s$schools %>%
  mutate(d_con = contrib_at - contrib_0) %>%
  arrange(desc(d_con)) %>%
  transmute(School = short_sch(name),
            `Deprived share, off` = fmt_pct(100 * dep_0, 1),
            `At 15%` = fmt_pct(100 * dep_at, 1),
            `Change, points` = sprintf("%+.1f", 100 * (dep_at - dep_0)),
            `Contribution to the index, change` = sprintf("%+.4f", d_con)) %>%
  knitr::kable(align = "lrrrr")
Table 37: Each school’s modelled intake, with priority 6 off and at 15% of places, under the council’s priorities in 2026: the share from the three most deprived deciles, and the school’s contribution to Gorard’s index (half the absolute gap between its share of the city’s deprived children and its share of all children). Ordered by how much the school adds to the index.
School Deprived share, off At 15% Change, points Contribution to the index, change
Varndean 35.6% 38.1% +2.5 +0.0063
Patcham 24.4% 23.1% -1.2 +0.0022
Blatchington Mill 15.2% 14.7% -0.6 +0.0016
Hove Park 14.4% 14.0% -0.4 +0.0011
Cardinal Newman 20.8% 20.8% +0.0 +0.0000
King’s 32.4% 32.4% +0.0 +0.0000
Longhill 22.6% 22.4% -0.1 -0.0001
Portslade Aldridge 38.1% 37.8% -0.3 -0.0003
Dorothy Stringer 27.6% 27.5% -0.2 -0.0005
Brighton Aldridge 65.0% 65.0% +0.0 -0.0006

Two things in the model drive it.

The children who use it still go to the popular school nearest them. Priority 6 lets a child cross a catchment boundary; it does not change how far a family will travel. Brighton Aldridge’s and Longhill’s catchments send most of their priority-6 children to Varndean. Varndean is the school that moves the index most: its intake was already 36% deprived against a city average of 27%, and at 15% it is 38%, while the catchment children it turns away move next door. Portslade’s catchment sends its priority-6 children to Blatchington Mill, and at the other end the schools already below the city average become less deprived still: Patcham from 24.4% to 23.1%; Blatchington Mill from 15.2% to 14.7%; Hove Park from 14.4% to 14.0%. A school above the average moves further above it and one below further below, which is exactly what Gorard’s index measures.

The families who use it are the better-off ones in their catchment. The priority-6 places won from Brighton Aldridge’s catchment go to neighbourhoods 67% deprived, where the catchment as a whole is 78%; from Portslade’s, 18% against 31%. A lottery within the priority does not change that. Who wins a place is set by who applies out of catchment, and in the model that follows how strongly each neighbourhood is drawn to each school.

WarningHow far to take this

The effect is small beside what a catchment redesign does, and it is a model result, not an observed one: the council has published the priority-6 offers by school for 2026 but not which neighbourhoods they went to. Deprivation here is a neighbourhood measure, so a better-off family in a deprived neighbourhood counts as deprived and the second mechanism may be understated or overstated. And segregation is one objective among several. Priority 6 may still widen access for the children who use it — the 70 or so a year at 5% — and that may be judged worth a small rise in the index. What it does not do, on this evidence, is even out the intakes, and a larger share makes that worse rather than better. The simulator shows the same comparison live in its Fairness tab for any setting.

8.3 And if Longhill moves

The last two designs re-run everything with Longhill at the top of Elm Grove, at the two admission numbers the scenario suite considers: 150, which is Section 9’s shrink-and-move, and 210, which is move-only and keeps the number now in force.

NoteThese two are seeded differently, and it matters

Every other design here assigns each neighbourhood to the school it sends most children to. For a school that has just moved, there is no such school. The modelled flows at a new site are shaped by attractiveness, and Longhill’s is 0.32 against a city average of 1. Even with no capacity ceiling it wins the dominant flow almost nowhere, so a flow-seeded design handed the east of the city to Stringer/Varndean — a school those neighbourhoods are not nearest to and cannot reach in under an hour — and the balancer, which can only trade neighbourhoods across an existing boundary, could never reach far enough east to take them back.

A catchment is a statement about geography and capacity, not a popularity contest. A school does not forfeit a catchment for being unpopular; that is what the over-subscription rule is for. So these two designs are seeded on which catchment can this neighbourhood reach quickest, and the capacity balance trades from there. The Ovingdean designs keep the flow seed, because there the flows describe a school that is where it is.

Show code
ec %>%
  group_by(design) %>%
  summarise(`To Longhill` = sum(assigned == "Longhill"),
            `To BACA` = sum(assigned == "BACA"),
            `To Stringer / Varndean` = sum(assigned == "DS_Varndean"),
            `Nearest is Longhill` = sum(nearest == "Longhill"),
            `Mean journey` = sprintf("%.0f min", weighted.mean(t_assigned, Oi)),
            `If each went to its nearest` =
              sprintf("%.0f min", weighted.mean(t_near, Oi)),
            .groups = "drop") %>%
  rename(Design = design) %>%
  knitr::kable(align = "lrrrrrr")
Table 38: Where the twelve easternmost neighbourhoods end up under each relocation, and how long their journey is. ‘Nearest’ is the catchment they could reach quickest.
Design To Longhill To BACA To Stringer / Varndean Nearest is Longhill Mean journey If each went to its nearest
Elm Grove, PAN 150 12 0 0 12 40 min 40 min
Elm Grove, PAN 210 12 0 0 12 40 min 40 min

Every one of the 12 easternmost neighbourhoods is now in Longhill’s catchment, at the journey it could not better. Rottingdean, Saltdean, Ovingdean and Woodingdean all sit with the school they can reach quickest, and the mean journey to an assigned catchment school is 40 minutes — identical to the 40 they would get if each simply went to its nearest. Under the first flow-seeded version they averaged sixty.

That took a second rule beyond the accessibility seed, because the capacity balancer promptly gave them away again.

NoteWhich neighbourhood a catchment gives up when it is over capacity

Ranking candidates by how much worse the receiving catchment is than the donating one is the obvious rule and it behaves badly at the edges of the city. Rottingdean and Saltdean are 30 to 52 minutes from Longhill at Elm Grove and around ten minutes more from Stringer/Varndean — a small difference, so they looked cheap to give away and went first. Somewhere in Hanover, five minutes from Elm Grove and twelve from Varndean, scored worse and was kept.

Two changes fix it, and both are confined to the relocation designs:

  • Rank on the journey the child would actually make, not on the change in it. A neighbourhood with a short alternative is given up before one whose only alternative is an hour away.
  • Never move a neighbourhood whose nearest catchment is already more than 35 minutes away further from it. The far east has no good option and keeps its least bad one. Everywhere with a reasonable alternative stays fully tradeable, which is the point: the inner neighbourhoods absorb the balancing instead.

A blanket tolerance was tried first and was worse than useless. Six minutes blocked the inner moves as well, the balancer could shed almost nothing, and Longhill finished 231% over its admission number.

8.3.1 What this costs, and what the admission number decides

Show code
sw %>%
  transmute(`Admission number` = pan,
            `Easternmost neighbourhoods in its catchment` =
              sprintf("%d of %d", east_in_longhill, n_east),
            `Mean journey for the east` = sprintf("%.0f min", east_minutes),
            `Longhill capacity gap` = sprintf("%+.0f%%", 100 * longhill_gap)) %>%
  knitr::kable(align = "rrrr")
Table 39: Longhill at Elm Grove, swept across admission numbers. The capacity gap is its catchment’s demand against its fair share of the city’s places: a positive figure means the area it is nearest to holds more children than it has room for.
Admission number Easternmost neighbourhoods in its catchment Mean journey for the east Longhill capacity gap
150 12 of 12 40 min +81%
180 12 of 12 40 min +53%
210 12 of 12 40 min +54%
240 12 of 12 40 min +37%
270 12 of 12 40 min +23%
300 12 of 12 40 min +23%
330 12 of 12 40 min +16%
360 12 of 12 40 min +8%

The admission number does not decide the map. It decides whether the map is affordable. The east is in Longhill’s catchment at every number in the sweep, from 150 to 360, because that is where the geography puts it. What changes is the gap between what the catchment holds and what the school can take: +81% at 150 places, +54% at 210, and still +8% at 360.

That is the real result of moving the school. A site in the middle of Brighton is close to a great many children; Ovingdean is close to few. Drawing an honest catchment round a relocated Longhill produces a catchment far larger than 150 or 210 places can serve, and the corridor between Elm Grove and the coast has to be inside it or the catchment is in two pieces.

And the journey times are the best on offer. At 210 places this design has a mean journey to a catchment school of 23.0 minutes, against 23.5 for the map in force — the shortest of any design here, because the east finally sits with the school it is nearest to.

The cost lands in the allocation. A catchment holding 49% more children than its school has places cannot admit them all, so they are bumped to their next preference: 38% of the city’s children cross a boundary under this design against 33% now, the highest of any design tested, and only 62% are placed in their own catchment.

ImportantYou can draw the boundary properly or size the school properly. 210 places cannot do both.

Reducing Stringer’s admission number was the obvious lever and it does not reach: targets are shares of the city’s places, so taking 120 places off the Stringer/Varndean pair moves Longhill’s target by about eleven children. The binding constraint is Longhill’s own number.

Three things, and only two can hold at once:

  • A school of 150 or 210 places at Elm Grove.
  • A catchment containing the children it is nearest to — which means Rottingdean and Saltdean, and the corridor connecting them to the site.
  • A catchment it can actually admit, so that in-catchment children are not bumped across the city.

This section takes the first two, because a boundary that sends Saltdean an hour west to a school it is not nearest to is not a boundary anyone should propose. The arithmetic then shows up in the allocation instead of on the map, which is the honest place for it: the school is too small for the geography it would serve, and that is a decision about the admission number, not about the lines.

On everything else the relocation designs are unremarkable or worse. They move 17% of the cohort, much the largest upheaval on offer; they have the longest journeys of any design, 23.0 minutes against 23.5 now; and on deprivation the PAN 210 version scores 0.230 against 0.214 for the map in force and 0.195 for the power diagram.

So the relocation designs are not the best for the city on any measure here except the capacity gap they create for themselves — and Section 9.0.1 finds that, once catchment terms are fitted to what families ask for, moving is not the best for Longhill either. That is not an argument against moving the school. It is an argument for deciding the admission number and the boundary together, and for asking what happens to Ovingdean, Rottingdean and Saltdean before rather than after.

8.4 What the over-subscription rule does

A catchment map is half a policy. The other half is what happens when a school is over-subscribed, and that is where the cross-catchment movement Brighton actually sees comes from.

The rule modelled here is the one England uses, in the simplified form published data supports: every child has a preference order taken from the model’s own utility; an over-subscribed school admits in-catchment children first, and within each priority group the nearest first; children who miss out cascade to their next preference. Schools hold offers provisionally and can bump a held child when a higher-priority applicant arrives, which is how the coordinated scheme behaves.

Show code
fr$alloc %>%
  mutate(design = factor(design, fr_designs)) %>%
  arrange(design) %>%
  transmute(Design = design,
            `Got their first preference` = fmt_pct(100 * first_pref, 0),
            `Placed in their own catchment` = fmt_pct(100 * local_share, 0),
            `Crossed a boundary` = fmt_pct(100 * cross_share, 0),
            `— of which to a faith school` = fmt_pct(100 * faith_share, 0),
            `Mean journey` = sprintf("%.1f min", mean_journey)) %>%
  knitr::kable(align = "lrrrrr")
Table 40: Where children end up under each design, with an in-catchment-then-distance over-subscription rule. Crossing a boundary includes the two faith schools, which have no catchment at all and take about a fifth of the cohort.
Design Got their first preference Placed in their own catchment Crossed a boundary — of which to a faith school Mean journey
Current catchments 70% 67% 33% 24% 22.7 min
Power diagram (proximity and capacity) 70% 71% 29% 24% 22.7 min
Flow regions, one per school 70% 64% 36% 24% 22.9 min
Flow regions, pairs kept 69% 72% 28% 24% 22.6 min
Flow regions, Elm Grove, PAN 150 68% 61% 39% 24% 24.8 min
Flow regions, Elm Grove, PAN 210 69% 62% 38% 24% 23.5 min

Most children draw locally and a real minority cross, which is what a catchment system with choice in it should produce. The power diagram places 71% in their own catchment against 67% now, and the flow regions 72%. Of the 29% who cross under the power diagram, 24 percentage points go to the two faith schools, which have no catchment to cross — leaving about 5% genuinely moving between geographic catchments.

First-preference success does not move. It is 70% now and between 68% and 70% across every design tested. A better-drawn map gets more children the school nearest to them. It does not get more of them the school they want, and the second is worth more than the first.

8.5 What each design costs in travel

Redrawing a catchment moves children between schools, and the schools are not in the same places. So every design has a travel bill, and it falls on households as time and on the council as home-to-school transport.

The figures here are for the journey to the school each child is actually offered under the rule above — not to the school whose catchment they live in, which is the number the comparison table in Section 8.1 reports and which nobody travels to unless they get that school. Every statistic is weighted by children, so a design gets no credit for shortening the journey of a neighbourhood with four children in it.

Show code
jc <- fr$journey_curve %>% mutate(design = factor(design, fr_designs))

# Six lines on one set of axes was six lines on top of each other: the
# four designs that leave the schools where they are never differ by
# more than a few points at any threshold, and drawing them separately
# said "these are different" when the finding is that they are not. The
# four become a band whose width IS that disagreement, and the two
# designs that do move the distribution keep their own colours from the
# rest of this section.
band <- jc %>% filter(!str_detect(design, "Elm")) %>%
  group_by(t) %>%
  summarise(lo = min(share), hi = max(share), .groups = "drop")
elm <- jc %>% filter(str_detect(design, "Elm")) %>% droplevels()
BAND_LAB <- "The four designs that leave the schools where they are"

ggplot() +
  geom_vline(xintercept = c(fr$nts_min, fr$long_min), colour = "grey65",
             linetype = "31") +
  annotate("text", x = fr$nts_min - 1, y = 0.06,
           label = sprintf("England average, %d min", fr$nts_min),
           hjust = 1, size = 2.7, colour = "grey35") +
  annotate("text", x = fr$long_min + 1, y = 0.06, label = "40 min",
           hjust = 0, size = 2.7, colour = "grey35") +
  geom_ribbon(data = band, aes(t, ymin = lo, ymax = hi, fill = BAND_LAB)) +
  geom_line(data = elm, aes(t, share, colour = design), linewidth = 0.9) +
  # The band is the reference the two lines are read against, so it
  # comes first in the legend rather than after them.
  scale_fill_manual(values = setNames("grey72", BAND_LAB), name = NULL,
                    guide = guide_legend(order = 1)) +
  scale_colour_manual(values = DESIGN_COL[levels(elm$design)], name = NULL,
                      guide = guide_legend(order = 2)) +
  scale_x_continuous(breaks = seq(0, 60, 10),
                     labels = function(x) paste0(x, " min")) +
  scale_y_continuous(labels = scales::label_percent(),
                     limits = c(0, 1), breaks = seq(0, 1, 0.25)) +
  labs(x = "Journey time to the school offered", y = NULL,
       title = "How long the journey is, and for how many children",
       subtitle = str_wrap(paste("Redrawing the boundaries barely moves the distribution - the band is how much the four in-place designs disagree at all.", "Moving a school moves the tail."), 100),
       caption = "Sources: routed walk-and-bus times over OSM and Brighton & Hove GTFS; modelled allocation.") +
  theme_bh(11) +
  theme(legend.position = "top", legend.box = "vertical",
        legend.spacing.y = unit(1, "pt"), legend.text = element_text(size = 8))
Figure 41: The share of children whose offered school is within a given journey time, walking and bus. The four designs that leave the schools where they are are drawn as one band, because they never differ by more than the width of it; the two relocation designs are drawn separately. The dashed marks are the National Travel Survey average one-way school trip and the forty-minute line used in the table below.

Redrawing the boundaries changes the travel bill by almost nothing. The four designs that leave the schools where they are run from 22.6 to 22.9 minutes on the mean, and at no journey time does the share of children served differ between them by more than 5 percentage points — the width of the grey band. The reason is straightforward: a boundary decides which school a child is entitled to, but most children are already at a school close to them, and the schools have not moved.

Moving a school changes it a lot. Longhill at Elm Grove adds 2.1 minutes to the average journey at an admission number of 150 and 0.9 at 210 — and much more in the tail. The longest tenth of journeys starts at 42 minutes now and at 50 under the relocation.

Show code
js %>%
  arrange(design) %>%
  transmute(Design = design,
            `Mean` = sprintf("%.1f min", mean_min),
            `Longest tenth, from` = sprintf("%.0f min", p90_min),
            `Over 40 minutes` = fmt_pct(100 * over_long, 0),
            `Mean distance` = sprintf("%.2f km", mean_km),
            `Child-km a day` = fmt_n(round(child_km_day)),
            `Deprived, against the rest` = sprintf("%+.1f min", dep_gap)) %>%
  knitr::kable(align = "lrrrrrr")
Table 41: Journeys to the school each child is offered. Child-kilometres are both ways, every school day, for every child placed — the quantity a transport budget and a carbon figure are counted in. The last column is the difference between the mean journey of children living in the 39 neighbourhoods in the three most deprived deciles nationally and the mean journey of everyone else; a positive figure means deprived children travel further.
Design Mean Longest tenth, from Over 40 minutes Mean distance Child-km a day Deprived, against the rest
Current catchments 22.7 min 42 min 10% 1.53 km 6,954 +8.8 min
Power diagram (proximity and capacity) 22.7 min 42 min 11% 1.55 km 7,028 +8.4 min
Flow regions, one per school 22.9 min 42 min 12% 1.56 km 7,093 +9.3 min
Flow regions, pairs kept 22.6 min 42 min 11% 1.53 km 6,951 +8.0 min
Flow regions, Elm Grove, PAN 150 24.8 min 50 min 15% 1.89 km 8,555 -1.5 min
Flow regions, Elm Grove, PAN 210 23.5 min 47 min 12% 1.78 km 8,078 -0.8 min

8.5.1 Who makes the long journeys

A mean journey for the city is not a measure of a fair system. The question that matters is whether the long journeys fall on the children least able to absorb them, and under the map in force they do.

Show code
eq <- js %>%
  transmute(design,
            `Everyone else` = min_rest,
            `Most deprived neighbourhoods` = min_deprived) %>%
  tidyr::pivot_longer(-design, names_to = "who", values_to = "min") %>%
  mutate(who = factor(who, c("Everyone else", "Most deprived neighbourhoods")))

# The same validated pair as the intake chart, and used the same way
# round: the purple point is the reference, the amber one is the group
# the section is asking about.
EQ_COL <- setNames(c("#6b4fbb", "#c2701c"), levels(eq$who))
eqw <- eq %>% tidyr::pivot_wider(names_from = who, values_from = min)

ggplot(eq, aes(min, fct_rev(design))) +
  geom_segment(data = eqw, aes(x = `Everyone else`,
                               xend = `Most deprived neighbourhoods`,
                               y = fct_rev(design), yend = fct_rev(design)),
               inherit.aes = FALSE, colour = "grey70", linewidth = 1.1) +
  geom_point(aes(colour = who), size = 3.4) +
  # One decimal, because whole minutes made the two relocation rows read
  # as a one-minute gap when the figure is 0.8.
  geom_text(aes(label = sprintf("%.1f", min), colour = who),
            vjust = -1.2, size = 2.8, show.legend = FALSE) +
  scale_colour_manual(values = EQ_COL, name = NULL) +
  scale_x_continuous(labels = function(x) paste0(x, " min"),
                     limits = c(18, 32)) +
  scale_y_discrete(labels = function(x) str_wrap(x, 26)) +
  labs(x = "Mean journey to the school offered", y = NULL,
       title = "Whose children make the long journeys",
       subtitle = str_wrap(paste("Under every design that leaves the schools where they are,", "children in the most deprived neighbourhoods travel further."), 100),
       caption = "Sources: routed walk-and-bus times; IMD 2019 IDACI; modelled allocation.") +
  theme_bh(11) +
  theme(legend.position = "top", panel.grid.major.y = element_blank())
Figure 42: Mean journey to the school offered, for children living in the 39 neighbourhoods in the three most deprived deciles nationally against everyone else. Both means are over children, so a large deprived neighbourhood counts for more than a small one.

Children in the most deprived neighbourhoods travel 9 minutes longer than everyone else, each way, under the current map — 29 minutes against 20. That is a 43% longer journey, twice a day, for the households with the least slack in them, and no redrawing of the boundaries closes it: the best of the in-place designs gets it to 8.0 minutes.

Moving Longhill to Elm Grove does close it, and that is the strongest argument for the relocation anywhere in this document. The gap goes from +8.8 minutes to -1.5: children in the deprived east stop travelling further than everyone else, because the school they are entitled to has moved towards them instead of sitting at the far edge of the city.

It is also the clearest illustration in this section of why one number is not enough. The relocation costs the city 23% more child-kilometres a day and makes the deprivation gap disappear. Both are true. Which matters more is a judgement and it is the council’s to make — but it should be made knowing that the two point in opposite directions, rather than being told that one design is simply better.

8.6 Every measure side by side

Show code
fmt_val <- function(v, f) dplyr::case_when(
  is.na(v)    ~ "—",
  f == "pct0" ~ fmt_pct(100 * v, 0),
  f == "n0"   ~ fmt_n(round(v)),
  TRUE        ~ sprintf(f, v))

fr$score_long %>%
  mutate(design = factor(design, fr_designs),
         txt = fmt_val(value, fmt),
         txt = if_else(!is.na(value) & best, paste0("**", txt, "**"), txt),
         Measure = paste0(label, if_else(better == "low",
                                         " (lower is better)",
                                         " (higher is better)"))) %>%
  # Row order comes from the spec, not from the alphabet, so the three
  # families stay in the order the section argues them.
  arrange(family, label) %>%
  mutate(Measure = factor(Measure, unique(Measure))) %>%
  select(family, Measure, design, txt) %>%
  tidyr::pivot_wider(names_from = design, values_from = txt) %>%
  arrange(family, Measure) %>%
  select(-family) %>%
  knitr::kable(align = "l")
Table 42: Every measure in this section, for every design. The best figure in each row is in bold. Where a lower number is better the row says so in its name; the direction is set in R/03_flow_regions.R and the ranking is done from it, not by hand.
Measure Current catchments Power diagram (proximity and capacity) Flow regions, one per school Flow regions, pairs kept Flow regions, Elm Grove, PAN 150 Flow regions, Elm Grove, PAN 210
Segregation between catchments (lower is better) 0.214 0.195 0.311 0.206 0.236 0.230
Segregation between school intakes (lower is better) 0.298 0.233 0.306 0.255 0.332 0.304
Mean journey to the school offered (lower is better) 22.7 min 22.7 min 22.9 min 22.6 min 24.8 min 23.5 min
Longest tenth of journeys, from (lower is better) 42 min 42 min 42 min 42 min 50 min 47 min
Children over 40 minutes (lower is better) 10% 11% 12% 11% 15% 12%
Mean distance (lower is better) 1.53 km 1.55 km 1.56 km 1.53 km 1.89 km 1.78 km
Child-kilometres a day, both ways (lower is better) 6,954 7,028 7,093 6,951 8,555 8,078
Deprived children’s journeys, against the rest (lower is better) +8.8 min +8.4 min +9.3 min +8.0 min -1.5 min -0.8 min
Self-containment (higher is better) 61% 62% 60% 66% 60% 60%
Worst capacity gap (lower is better) 23% 11% 22% 11% 94% 49%
Got their first preference (higher is better) 70% 70% 70% 69% 68% 69%
Placed in their own catchment (higher is better) 67% 71% 64% 72% 61% 62%
Shape faults (lower is better) 3 0 2 0 0 0
Neighbourhood children reassigned (lower is better) 15% 17% 17% 17%
Show code
beat %>%
  transmute(Design = design, `Better` = better, `Worse` = worse,
            `The same` = same) %>%
  knitr::kable(align = "lrrr")
Table 43: How each design scores against the map in force, counted over the measures the current map can be scored on. A measure counts as the same where the two differ only in the fourth decimal.
Design Better Worse The same
Flow regions, pairs kept 10 2 1
Power diagram (proximity and capacity) 8 3 2
Flow regions, one per school 2 10 1
Flow regions, Elm Grove, PAN 150 2 11 0
Flow regions, Elm Grove, PAN 210 2 11 0

No design wins outright, and the two that come closest win on different things. Of the 13 measures the current map can be scored on, the flow regions with the council’s pairings kept beat it on 10 and the power diagram on 8. Between them they take 15 of the 22 first places in the table above.

  • The power diagram is the design to take seriously if the criterion is fairness and balance. It is best on both segregation measures, best on capacity, has no shape faults, and moves the fewest neighbourhoods.
  • The flow regions win the measures about where children actually go: self-containment, being placed in your own catchment, and the shortest mean journey of any design.
  • One catchment per school loses on almost everything, and should be read as the negative result it is. The council’s pairings are load bearing.
  • The two relocation designs lose on travel and on segregation, and win the one measure no boundary can touch — who makes the long journeys.

The measure nobody moves is the one families care about most. First-preference success sits between 68% and 70% across all six designs. Every gain in this section is a gain in fairness of entitlement — who is nearest, who is balanced against whom, who travels furthest. None of it is a gain in getting children into the school their family chose, because that is set by how many places the popular schools have, and a boundary does not create places.

WarningWhat this is and is not

It is a demonstration that the geography could be better, not a proposal. The regions are built from modelled flows and an uncalibrated model. A real redesign would use the council’s own preference and allocation records, which would replace every modelled flow here with an observed one.

Nothing here is in the algorithm that should be. Sibling links keep families together and are a large share of real admissions. Transport cost falls on the council and on households. Existing pupils cannot be moved. Feeder-primary patterns matter to families in ways a flow matrix does not see. Any of these could overturn a boundary drawn here.

The comparison is fair, though. Every design is scored with the same statistics on the same flows and profiled with the same postcode method, so the differences between the columns are like for like even where the levels are uncertain.

And the headline is a qualified one. One design — the power diagram — improves capacity balance, spreads disadvantage substantially more evenly, and moves the fewest neighbourhoods of any alternative. That is a real gain and it is worth having. But it changes how many children get their first choice by nothing at all, and two of the other three alternatives are worse than the current map on deprivation. Redrawing the map is worth doing on its own terms, and only if the redrawing is done on the right criterion. It is not an answer to the problem the rest of this document is about.

9 Longhill

Longhill High School sits at Ovingdean, at the eastern edge of both its catchment and the city. Its roll has fallen by 20% in four years and its reserve supports about 3.7 more years.

Section 7 established the constraint this section has to work inside. The school draws about 201 children on geography alone, and that figure barely moves across the swept parameter range. Reducing the admission number to 210 has already closed most of the gap between capacity and catchment. What remains is a cohort that keeps falling (section 3) and a reserve that runs out (section 6).

So the question is not whether something must change, nor whether the admission number should come down again — that lever is largely spent. It is whether any combination of the available changes produces a school that is viable at the end of the projection period rather than merely smaller.

Show code
# os$lh_band holds FILL RATES, not intakes: the modelled intake divided
# by whatever admission number that configuration sets. So configuration
# B fills at 100% on 150 places while A is short of 210, and the two
# numbers are not comparable as recruitment. Natural recruitment, which
# is comparable, is os$natural and is quoted alongside below.
lb <- os$lh_band %>%
  mutate(config = factor(config, levels = os$configs))

ggplot(lb, aes(entry_year, pmin(med, 1))) +
  geom_hline(yintercept = 1, linetype = "31", colour = "grey45") +
  geom_ribbon(aes(ymin = pmin(lo, 1), ymax = pmin(hi, 1)),
              fill = "#2166ac", alpha = 0.2) +
  geom_line(colour = "#2166ac", linewidth = 1) +
  geom_point(colour = "#2166ac", size = 1.8) +
  facet_wrap(~ config, ncol = 2, labeller = label_wrap_gen(38)) +
  scale_y_continuous(limits = c(0, 1.02), labels = scales::label_percent()) +
  labs(x = "Year of Year 7 entry", y = "Fill rate against that option's PAN",
       title = "Seven futures for Longhill",
       subtitle = "Median and full range across the swept parameter space.\nThe dashed line is a school exactly full — on an admission number that differs between options.",
       caption = "Brightopia, open-data specification.") +
  theme_bh(11) +
  theme(strip.text = element_text(size = 8.5))
Figure 43: How full Longhill would be under seven configurations, across the projection period. Bands show the range across the swept parameter space; 100% is a school exactly at its admission number for that configuration, and the admission number differs between them.

By 2030 the configurations separate clearly. The status quo (A. Today: Ovingdean, PAN 210, current catchments) fills to a median of 78%, and draws about 130 children.

4 of the seven options fill completely at the median. That sounds decisive and is not, because there are two ways to fill a school and only one of them is worth having: lower the admission number until the school is full, or raise the number of children it draws. Fill rate alone cannot tell them apart — a school is trivially full at a PAN of one — which is why natural recruitment sits beside it here.

On that test the strongest are E. Shrink + move + redrawn catchments and G. Elm Grove, PAN 120, redrawn catchments, which fill and draw 148 children: 18 more than the status quo, from the same cohort. They tie because they put the school on the same site behind the same catchments, and differ only in the admission number — which is the lever this section has already argued is largely spent.

On the open scenario suite that reads as an argument that the instruments compose: moving the school changes its position in the travel network, reducing its admission number changes what “full” means, redrawing catchments changes which children are steered towards it, and together they reach a configuration that works. But that suite runs one catchment term for the whole city, with attractiveness taken straight from preferences per place. The full model of section 7 does not, and for this question the difference is decisive.

9.0.1 The same options under the full model

Show code
rl5$central %>%
  select(config, entry_year, natural) %>%
  mutate(natural = fmt_n(natural)) %>%
  pivot_wider(names_from = entry_year, values_from = natural) %>%
  rename(Option = config) %>%
  knitr::kable(align = "lrrr")
Table 44: Longhill’s natural recruitment under each option in the full model, M5: catchment terms fitted to what each catchment’s children ask for, attractiveness balanced to first preferences. Children drawn with the school’s own admission number unbinding, so options that differ only in that number draw the same children.
Option 2026 2030 2035
A. Today: Ovingdean, PAN 210, current catchments 89 72 65
B. Shrink only: Ovingdean, PAN 150 89 72 65
C. Move only: Elm Grove, PAN 210 74 60 53
D. Shrink + move: Elm Grove, PAN 150 74 60 53
E. Shrink + move + redrawn catchments 96 78 71
F. Redrawn catchments, Longhill stays at Ovingdean 76 59 53
G. Elm Grove, PAN 120, redrawn catchments 96 78 71

The catchment term is strongest in exactly the two places a move trades between. Longhill’s own families, at \(\gamma\) = 2.3, are the ones the move takes the school away from. Stringer / Varndean’s, at 2.6, are most of the families near Elm Grove it moves towards — and they are the most attached to their own schools of any in the city.

  • Moving on its own costs Longhill about 13 children a year. It draws 72 at Ovingdean in 2030 and 60 at Elm Grove behind today’s catchments.
  • Moving with redrawn catchments gains a few children: 78 in 2030 against 72 staying put. The redraw wins back what the move lost and a little more.
  • Redrawing without moving costs children too, 59 in 2030, because the designed map passes some of Longhill’s hinterland to its neighbours.
  • A move sends more of Longhill’s own catchment out of the city: 36 to the East Sussex schools in 2030 at Ovingdean, most of them to Priory, against 43 at Elm Grove behind today’s catchments.
  • Nothing draws 150 children after 2026. By 2035 the best option draws 71.
Show code
rl5$sens %>%
  mutate(natural = fmt_n(natural), Option = unname(rl5$labels[id])) %>%
  select(Option, Assumption = variant, entry_year, natural) %>%
  pivot_wider(names_from = entry_year, values_from = natural) %>%
  knitr::kable(align = "llrrr")
Table 45: How much the answer rests on two assumptions: the fitted catchment terms halved, and no paired-catchment families who would take only one of the two schools.
Option Assumption 2026 2030 2035
Today: Ovingdean, PAN 210, current catchments Catchment terms halved 58 45 39
Today: Ovingdean, PAN 210, current catchments No paired-school exclusivity 88 71 65
Move only: Elm Grove, PAN 210 Catchment terms halved 57 44 38
Move only: Elm Grove, PAN 210 No paired-school exclusivity 73 58 52
Shrink + move: Elm Grove, PAN 150 Catchment terms halved 57 44 38
Shrink + move: Elm Grove, PAN 150 No paired-school exclusivity 73 58 52
Shrink + move + redrawn catchments Catchment terms halved 74 55 49
Shrink + move + redrawn catchments No paired-school exclusivity 95 77 71
Elm Grove, PAN 120, redrawn catchments Catchment terms halved 74 55 49
Elm Grove, PAN 120, redrawn catchments No paired-school exclusivity 95 77 71

A move looks better the less families follow their catchment, and the evidence is that they follow it closely. Halve the catchment terms and the site stops mattering for a move on its own — 45 at Ovingdean against 44 at Elm Grove in 2030 — and a move with a redraw gains. At the strength families actually show, it does not.

So the instruments do not compose the way the open suite suggests. Size is the one lever that works on every model here, because it changes what “full” means rather than who comes — and under the full model even 150 places is more than Longhill draws after 2026. Relocation’s case is real, but it is the one section 8 makes on behalf of the city, that deprived children in the east would stop travelling further than everyone else (Section 8.5.1). It is not a case that the move would refill the school.

ImportantRead these figures with the models’ limits in mind

The figure and the first comparisons in this section come from the open scenario suite, which runs one catchment term for the whole city and is more optimistic about relocation than the full model. The full model’s figures rest on the assumptions stated in Section 7.8.3: distance decay is set rather than fitted, and the catchment terms carry over to a redrawn map at the strength families follow the map they have. Whether families would follow a new boundary as closely as the old one is the question that decides relocation, and no published data answers it. Nothing in this section is a prediction of a roll.

10 What the council needs to do

10.1 Decisions

Decide about Longhill on a deadline, not on a consultation cycle. The reserve supports about 3.7 more years. A decision deferred past that point is a decision taken by insolvency rather than by the council.

Treat the instruments as a package. Section 8 shows they compose. Section 3 shows the cohort keeps falling regardless. Adjusting a single admission number and waiting to see what happens will consume years the finances do not have.

Stop pointing families at Attainment 8. The council publishes the admissions guide. Section 5 shows families track the headline score at 84% and the value-added measure at 20%. Publishing a contextualised measure alongside the headline would cost nothing and would begin to unwind a self-fulfilling loop the authority currently sustains.

Treat transport as an admissions instrument. Section 4 found 29 neighbourhoods that can reach no school place inside 30 minutes, concentrated in the more deprived third of the city. A bus timetable can widen real choice without taking anything from anyone — which is more than can be said for any boundary change.

10.2 Questions raised in this document

Numbered questions appear in the sections above where the evidence raises them. They are gathered here so they can be put as a set. None requires new research; each is answerable from records the council already holds.

  1. Why is the catchment forecast wrong in Longhill and in Hove Park / Blatchington Mill, in opposite directions? (§3.4) In 2026 Longhill was forecast 162 and received 81, while Hove Park / Blatchington Mill was 42 places under. The second has a candidate explanation in the neighbouring PACA over-forecast; the first does not.
  2. Which neighbourhoods did the priority-6 places go to, and what did they do to the social mix of the community schools’ intakes?
    1. In the full model a larger priority-6 share makes intakes more segregated, because the families who use it are the better-off ones in their catchments and they go to the popular school nearest them. The council holds the offers and the home postcodes to test it.

10.3 Data the council could simply publish

Every “this cannot be done on published data” note in this document resolves to a small number of releases. None requires new collection; all are aggregates of data the council already holds.

# What What it would settle
1 Home LSOA of every applicant, by year and allocated school Calibrate \(\beta\); replace every band in section 8 with an estimate
2 Full preference ordering, anonymised, by year Turn section 5’s ten-point association into a discrete-choice model
3 Allocated school by home LSOA, including schools outside the city Measure the actual outflow the council’s forecasts assume
4 Criterion under which each place was allocated Establish directly which criteria bind, rather than inferring it
5 Children allocated a school they did not name, and which Test the council’s stated “nearest school with places” practice
6 Journey actually made, or at least distance, per allocation Separate potential accessibility from realised accessibility

LSOAs average about 1,500 residents and 650 households. A cohort-year count at LSOA level is not disclosive in the way a postcode-level count would be, and the council already publishes LSOA-level material elsewhere.

10.4 Why now

The cohort projections in section 3 are not forecasts. Those children are in the city’s primary schools today. The financial position in section 6 has a horizon of about 3.7 years. The decisions in front of the council will shape the school system for the next fifteen to twenty years, and they are being taken without a picture of the whole system.

This document is an attempt to supply as much of that picture as published data allows. It stops well short of what the council’s own records would support — and the offer to do that work, properly and in the open, stands.

11 Sources and data

Everything in this document is built from published data. Nothing here uses pupil-level records: no individual child, postcode-level application or named preference appears in any input, and the one place where postcode-level counts are used — the density map in Section 2.2 — is a census household count, not an admissions record.

Every chart and table names its immediate source in the caption. This section gives the links, says which sections rest on which data, and lists what is in the repository’s data/ folder and what made it.

11.1 What each section is built from

Show code
uses %>%
  group_by(number, section) %>%
  summarise(
    derived = paste(sort(unname(ds_title[file])), collapse = "; "),
    sources = paste(purrr::map_chr(sort(unique(unlist(ds_from[file]))),
                                   src_link), collapse = "; "),
    .groups = "drop") %>%
  transmute(Section = paste0(number, ". ", section),
            `Data used` = derived,
            `Published sources` = sources) %>%
  knitr::kable(align = "lll")
Table 46: Which published data each numbered section of this document rests on. Derived files are the ones this repository and the open model build; the sources column is what they are built from. Generated from the document’s own source rather than maintained by hand.
Section Data used Published sources
1. Executive summary Accessibility surfaces and benchmarks; Every school’s finances and exposure; LSOA boundaries; Scenario runs, and Longhill’s accounts School admissions: allocation factsheets; Bus Open Data Service; School income and expenditure; Get Information About Schools; Home-to-school travel and transport guidance; English indices of deprivation 2019; National Travel Survey; ONS Postcode Directory and boundaries; OpenStreetMap
2. The secondary school system Catchments in force; Households with children, by postcode; How much a school can reach; LSOA boundaries; Model inputs: zones, costs, schools; School attainment panel; Variance decomposition School admissions: allocation factsheets; Secondary catchment boundaries; Bus Open Data Service; Census 2021; Explore education statistics; Get Information About Schools; English indices of deprivation 2019; How to Pull the Right Lever; ONS Postcode Directory and boundaries; OpenStreetMap; School performance tables; Small area population estimates
3. Demographic futures LSOA boundaries; Model inputs: zones, costs, schools; Reception cohorts and the projection; The council’s October 2024 forecast School admissions: allocation factsheets; Secondary catchment boundaries; School place planning reports and forecasts; Bus Open Data Service; Get Information About Schools; ONS Postcode Directory and boundaries; OpenStreetMap; Small area population estimates
4. How reachable are the schools? Accessibility surfaces and benchmarks; Catchments in force; LSOA boundaries; Model inputs: zones, costs, schools; One journey, leg by leg; Preferences and offers, 2010 to 2026 School admissions: allocation factsheets; Secondary catchment boundaries; Bus Open Data Service; Get Information About Schools; Home-to-school travel and transport guidance; English indices of deprivation 2019; National Travel Survey; ONS Postcode Directory and boundaries; OpenStreetMap; Small area population estimates
5. Admissions rules, and what families actually do Accessibility surfaces and benchmarks; Preferences and offers, 2010 to 2026; What the adjudicator determined School admissions: allocation factsheets; School place planning reports and forecasts; Bus Open Data Service; Home-to-school travel and transport guidance; English indices of deprivation 2019; National Travel Survey; Schools Adjudicator determinations; OpenStreetMap
6. Money Every school’s finances and exposure; Funding allocations, 2025-26; Model inputs: zones, costs, schools; Preferences and offers, 2010 to 2026; Scenario runs, and Longhill’s accounts School admissions: allocation factsheets; Secondary catchment boundaries; Bus Open Data Service; School income and expenditure; School funding statistics; Get Information About Schools; ONS Postcode Directory and boundaries; OpenStreetMap; Small area population estimates
7. Brightopia: a model of the whole system Catchments in force; Model inputs: zones, costs, schools; Modelled flows along the road network; Preferences and offers, 2010 to 2026; Sensitivity across the parameter band; The distance-only model; The model ladder M0 to M5 School admissions: allocation factsheets; Secondary catchment boundaries; Where each catchment’s children were offered a place, 2024; Catchment-level preferences, evidence to the Schools Adjudicator; Bus Open Data Service; Get Information About Schools; ONS Postcode Directory and boundaries; OpenStreetMap; Small area population estimates
8. Redrawing the catchments Catchments redrawn from the flows; LSOA boundaries; Model inputs: zones, costs, schools; One journey, leg by leg; Priority 6 and the social mix of intakes School admissions: allocation factsheets; Secondary catchment boundaries; Where each catchment’s children were offered a place, 2024; Catchment-level preferences, evidence to the Schools Adjudicator; Bus Open Data Service; Census 2021; Get Information About Schools; English indices of deprivation 2019; ONS Postcode Directory and boundaries; OpenStreetMap; Small area population estimates
9. Longhill Scenario runs, and Longhill’s accounts; The model ladder M0 to M5 School admissions: allocation factsheets; Secondary catchment boundaries; Where each catchment’s children were offered a place, 2024; Catchment-level preferences, evidence to the Schools Adjudicator; Bus Open Data Service; School income and expenditure; Get Information About Schools; OpenStreetMap
10. What the council needs to do Accessibility surfaces and benchmarks; LSOA boundaries; Scenario runs, and Longhill’s accounts School admissions: allocation factsheets; Bus Open Data Service; School income and expenditure; Home-to-school travel and transport guidance; English indices of deprivation 2019; National Travel Survey; ONS Postcode Directory and boundaries; OpenStreetMap

11.2 The published sources

Links go to the publisher’s landing page for the dataset rather than to a particular file. Deep links to statistical releases rot within a year or two, and a landing page that has moved a file still gets a reader to it. The council’s committee papers are cited by title and date for the same reason.

Show code
SOURCES %>%
  arrange(publisher, title) %>%
  transmute(Source = sprintf("[%s](%s)", title, url),
            Publisher = publisher,
            `What it is` = what,
            Licence = licence) %>%
  knitr::kable(align = "llll")
Table 47: Every published source this document draws on.
Source Publisher What it is Licence
Catchment-level preferences, evidence to the Schools Adjudicator Brighton & Hove City Council First, second and third preferences and allocations by home catchment and school, 2023/24 to 2025/26 (item 8.1). Received as a party to the 2026/27 case, not published; aggregated to catchment and non-disclosive. The full model (M5) is calibrated to it. not published; used as a party to the case
School admissions: allocation factsheets Brighton & Hove City Council Preferences and offers by school and rank, published each year after allocation. OGL v3
School place planning reports and forecasts Brighton & Hove City Council Catchment forecasts and admission-number proposals, in committee papers and consultation documents. OGL v3
Secondary catchment boundaries Brighton & Hove City Council The catchment map in force from September 2026 entry, and the map it replaced. OGL v3
Where each catchment’s children were offered a place, 2024 Brighton & Hove City Council Offers on national offer day in the 2024 round by home catchment and school, including schools outside the city, with small counts suppressed. The council’s answer to a Freedom of Information request, published on WhatDoTheyKnow. The full model’s out-of-city destinations are fitted to it. published FOI response
How to Pull the Right Lever Dennett and colleagues, UCL CASA The multilevel model of school-level attainment whose specification and decomposition section 2 follows. author’s own work
Explore education statistics Department for Education The underlying national datasets behind the performance and finance services. OGL v3
Get Information About Schools Department for Education The school register: URN, phase, age range, status, religious character, coordinates. OGL v3
Home-to-school travel and transport guidance Department for Education The statutory maximum journey times used as a benchmark in section 4. OGL v3
School funding statistics Department for Education School-level funding allocations for 2025-26, split into the formula’s own components. OGL v3
School income and expenditure Department for Education Consistent Financial Reporting returns for maintained schools and academy accounts returns. OGL v3
School performance tables Department for Education Attainment 8, Progress 8, absence, cohort characteristics and prior attainment, by school. OGL v3
Bus Open Data Service Department for Transport The Brighton & Hove timetable feed (GTFS) the journey times are routed over. OGL v3
National Travel Survey Department for Transport The England average school trip, used to make a number of minutes interpretable. OGL v3
World Light Gray Canvas Esri The base cartography under every map in this document. Keyless, unlike the CARTO tiles it replaced. Esri terms, attribution required
English indices of deprivation 2019 Ministry of Housing, Communities and Local Government IDACI: income deprivation affecting children, by LSOA. OGL v3
Census 2021 Office for National Statistics Household composition, used for households with dependent children by postcode. OGL v3
ONS Postcode Directory and boundaries Office for National Statistics Postcode centroids, LSOA lookups and the 2021 LSOA boundary geometry. OGL v3
Small area population estimates Office for National Statistics Population by single year of age, used to split each ward’s children between its LSOAs. OGL v3
Schools Adjudicator determinations Office of the Schools Adjudicator The 2025 determinations on Brighton & Hove’s admission arrangements (ADA4423 and others). OGL v3
OpenStreetMap OpenStreetMap contributors The walking and road network the journey times are routed over. ODbL

11.3 What is in data/, and what made it

The repository carries its inputs so that the document renders on any machine. Two of those files are published boundary or statistical files copied unchanged; the rest are derived, and the column says by what.

Show code
n_sec <- purrr::map_int(DATASETS$file,
                        ~ n_distinct(uses$number[uses$file == .x]))

# A file the document loads into an object but never uses is either dead
# code or a drifted register, and both are worth failing on. Two such
# loads were found this way and removed.
stopifnot(all(n_sec[DATASETS$file %in% QMD_VARS$file] > 0))

DATASETS %>%
  mutate(n_sections = n_sec) %>%
  arrange(built_by, file) %>%
  transmute(File = paste0("`", file, "`"),
            `What it is` = title,
            `Built by` = built_by,
            `From` = purrr::map_chr(from, ~ paste(SOURCES$title[match(.x, SOURCES$key)],
                                                  collapse = "; ")),
            Sections = if_else(n_sections == 0, "—", as.character(n_sections))) %>%
  knitr::kable(align = "lllr")
Table 48: Every file this repository reads, what it is, and what built it. The open Brightopia bundle is the open-data model published alongside this document; the school attainment tool is the national panel behind How to Pull the Right Lever. The last column counts the numbered sections that use the file directly; a dash means it is an input to one of the others rather than to the document.
File What it is Built by From Sections
adjudicator_conversion.rds What the adjudicator determined open Brightopia bundle Schools Adjudicator determinations; School place planning reports and forecasts 1
brightopia.rds The distance-only model open Brightopia bundle OpenStreetMap; Bus Open Data Service; School admissions: allocation factsheets 1
deprivation_open.rds IDACI by neighbourhood open Brightopia bundle English indices of deprivation 2019; ONS Postcode Directory and boundaries
factsheet_panel.rds Preferences and offers, 2010 to 2026 open Brightopia bundle School admissions: allocation factsheets 4
flow_map.rds Modelled flows along the road network open Brightopia bundle OpenStreetMap; Bus Open Data Service; School admissions: allocation factsheets 1
model_terms.rds The model ladder M0 to M5 open Brightopia bundle OpenStreetMap; Bus Open Data Service; School admissions: allocation factsheets; Secondary catchment boundaries; Catchment-level preferences, evidence to the Schools Adjudicator; Where each catchment’s children were offered a place, 2024; Get Information About Schools 2
open_inputs.rds Model inputs: zones, costs, schools open Brightopia bundle ONS Postcode Directory and boundaries; Small area population estimates; Get Information About Schools; Secondary catchment boundaries; School admissions: allocation factsheets; OpenStreetMap; Bus Open Data Service 6
open_scenarios.rds Scenario runs, and Longhill’s accounts open Brightopia bundle OpenStreetMap; Bus Open Data Service; School admissions: allocation factsheets; School income and expenditure 4
reception_cohort.rds Reception cohorts and the projection open Brightopia bundle School admissions: allocation factsheets; School place planning reports and forecasts 1
route_geometries.rds One journey, leg by leg open Brightopia bundle OpenStreetMap; Bus Open Data Service 2
school_finance.rds Every school’s finances and exposure open Brightopia bundle School income and expenditure; Get Information About Schools; School admissions: allocation factsheets 2
sensitivity_envelope.rds Sensitivity across the parameter band open Brightopia bundle OpenStreetMap; Bus Open Data Service; School admissions: allocation factsheets 1
catchments_current.geojson Catchments in force published boundary file Secondary catchment boundaries 3
lsoa.geojson LSOA boundaries published boundary file ONS Postcode Directory and boundaries 6
school-funding-statistics_2025-26/data/20260129_School_level_data_csv.csv Funding allocations, 2025-26 published dataset School funding statistics 1
performance_panel.rds School attainment panel school attainment tool School performance tables; Explore education statistics 1
school_effect_decomp.rds Variance decomposition school attainment tool School performance tables; How to Pull the Right Lever 1
school_leverage.rds How much a school can reach school attainment tool School performance tables; How to Pull the Right Lever 1
postcode_children.csv Households with children, by postcode this repository, R/01_assemble.R Census 2021; ONS Postcode Directory and boundaries; English indices of deprivation 2019 1
accessibility.rds Accessibility surfaces and benchmarks this repository, R/02_accessibility.R OpenStreetMap; Bus Open Data Service; National Travel Survey; Home-to-school travel and transport guidance; English indices of deprivation 2019 4
flow_regions.rds Catchments redrawn from the flows this repository, R/03_flow_regions.R ONS Postcode Directory and boundaries; English indices of deprivation 2019; Census 2021; Secondary catchment boundaries; OpenStreetMap; Bus Open Data Service 1
priority6_sweep.rds Priority 6 and the social mix of intakes this repository, R/05_app_inputs.R Catchment-level preferences, evidence to the Schools Adjudicator; Where each catchment’s children were offered a place, 2024; School admissions: allocation factsheets; English indices of deprivation 2019; Census 2021; ONS Postcode Directory and boundaries; OpenStreetMap; Bus Open Data Service 1
council_forecast_oct24.csv The council’s October 2024 forecast transcribed from the published appendix School place planning reports and forecasts 1

deprivation_open.rds carries the dash. The document does not read it; R/02_accessibility.R and R/03_flow_regions.R do, and it reaches these pages through the accessibility surfaces and the redrawn catchments they build.

11.4 Licences, attribution and reuse

Most of this is Open Government Licence v3. The DfE, DfT, ONS, MHCLG and council datasets above are all published under OGL v3, which permits reuse with attribution. The attribution is in the table above.

OpenStreetMap is ODbL. The walking and road network under every routed journey time is © OpenStreetMap contributors, available under the Open Database Licence.

The basemaps are Esri’s World Light Gray Canvas, used because it needs no API key. Every map carries the Esri and OpenStreetMap attribution in its corner, which the terms require, and R/99_verify_render.R fails the build if a map loses it. These tiles replaced CARTO’s, which are keyed: a render from a shell without the key produced watermarked maps and no other symptom, repeatedly.

The analysis is reproducible from these sources. R/01_assemble.R pulls every input into data/; R/02_accessibility.R and R/03_flow_regions.R build the accessibility surfaces and the redrawn catchments; the model outputs come from the open Brightopia bundle, which publishes its own code. What cannot be reproduced from this repository alone is the routed travel matrix, which needs an OSM extract and a GTFS feed and about an hour of compute — the matrix itself is carried in data/travel/, so nothing downstream of it requires the rebuild.

NoteWhat is deliberately not here

The council’s admissions records. Section 10.3 lists the six fields that would replace a modelled flow with an observed one throughout sections 7 and 8. They are not public and this document does not use them.

Anything at individual level. The finest geography used anywhere is the postcode, and only for counts of households from the census. Every model input is at LSOA or catchment level.

Unpublished council material. Where a council figure is quoted it comes from a published report, a published factsheet or a determination of the Schools Adjudicator, and the section says which.

References

Coleman, James S., Ernest Q. Campbell, Carol J. Hobson, James McPartland, Alexander M. Mood, Frederic D. Weinfeld, and Robert L. York. 1966. “Equality of Educational Opportunity.” Washington, DC: U.S. Department of Health, Education,; Welfare, Office of Education.
Kent, Jennifer L., Laya Hossein Rashidi, Emily Moylan, Katherine Owen, Melanie Crane, Aitan M. Militão, Olivia Marrum, and Izabella Santos. 2026. “A Systematic Review of Active School Travel Distances and Durations.” Transport Reviews. https://doi.org/10.1080/01441647.2026.2647794.
Smith, David J., and Sally Tomlinson. 1989. The School Effect: A Study of Multi-Racial Comprehensives. London: Policy Studies Institute.
Teddlie, Charles, and David Reynolds. 2000. The International Handbook of School Effectiveness Research. London: Falmer Press.