Segregation and the Schelling Model
Small preferences, big patterns
US cities are highly segregated by race and income. A natural guess is that this requires strong preferences: people who refuse to live near anyone different. Thomas Schelling showed in 1971 that this guess is wrong. Even mild preferences can produce sharp segregation.
The model
Picture a city as a big square made of small squares. Each small square is one house. In the simulation the city is 40 squares across and 40 squares down, so it has 1,600 houses.
A blue or orange square is a house where a family lives. There are two types of families, blue and orange. A white square is an empty house.
A family’s neighbors are the 8 houses touching its house: left, right, above, below, and the four corners. This is Schelling’s own rule. He wrote that “a convenient minimum-sized neighborhood for an individual is his own square plus the eight surrounding.” A family is happy if at least a share \(T\) of its neighbors are its own color. If not, it moves to a random empty house.
The key is that \(T\) can be low. A family with \(T = 0.35\) is fine being in the minority. It only wants about a third of its neighbors to be like it. Yet when everyone acts on this mild rule, the city sorts into large blocks of one type.
Why it happens
Each move changes the neighborhood for others. When one orange family leaves a mostly blue block, the remaining orange families there now have fewer orange neighbors. Some of them become unhappy and leave. This is a chain reaction, or tipping. No one wants full segregation, but the sum of individual choices produces it.
Measuring segregation
The simulation uses Schelling’s measure: the share of a family’s 8 neighbors with its own color. Studies of real cities often use the dissimilarity index instead. Split the city into small areas and compare where each group lives. The index is the share of either group that would have to move to make every area match the citywide mix. 0 means fully even. 1 means complete separation. Large US metro areas often have black-white dissimilarity of 0.5 or higher.
#| standalone: true
#| viewerHeight: 900
library(shiny)
ui <- fluidPage(
tags$head(tags$style(HTML("
.info-box {
background: #eaf2f8; border-radius: 6px; padding: 14px;
margin-top: 12px; font-size: 13px; line-height: 1.8;
}
.info-box b { color: #2c3e50; }
"))),
sidebarLayout(
sidebarPanel(
width = 4,
sliderInput("thresh", "Share of like neighbors wanted (T):",
min = 0, max = 0.8, value = 0.35, step = 0.05),
sliderInput("empty", "Share of houses empty:",
min = 0.05, max = 0.40, value = 0.15, step = 0.05),
sliderInput("blue", "Share of families that are blue (starting mix):",
min = 0.2, max = 0.8, value = 0.5, step = 0.1),
actionButton("run", "Run until settled", class = "btn-primary btn-lg", width = "100%",
style = "margin-bottom: 8px; font-size: 18px;"),
fluidRow(
column(6, actionButton("step", "Step", width = "100%",
style = "padding: 6px; font-size: 13px;")),
column(6, actionButton("reset", "Reset", width = "100%",
style = "padding: 6px; font-size: 13px;"))
),
uiOutput("info")
),
mainPanel(
width = 8,
plotOutput("grid", height = "520px"),
plotOutput("path", height = "300px")
)
)
)
server <- function(input, output, session) {
n <- 40 # 40 x 40 grid
new_grid <- function() {
e <- input$empty
g <- sample(c(0L, 1L, 2L), n * n, replace = TRUE,
prob = c(e, (1 - e) * input$blue, (1 - e) * (1 - input$blue)))
matrix(g, n, n)
}
# count neighbors of each type (wraps around the edges)
shift <- function(m, dr, dc) {
r <- ((seq_len(n) - 1 + dr) %% n) + 1
c <- ((seq_len(n) - 1 + dc) %% n) + 1
m[r, c]
}
like_share <- function(g) {
same <- matrix(0, n, n); occ <- matrix(0, n, n)
for (dr in -1:1) for (dc in -1:1) {
if (dr == 0 && dc == 0) next
s <- shift(g, dr, dc)
occ <- occ + (s > 0)
same <- same + (s == g & g > 0)
}
ifelse(occ > 0, same / occ, 1)
}
unhappy <- function(g) (g > 0) & (like_share(g) < input$thresh)
like_avg <- function(g) mean(like_share(g)[g > 0]) # avg share of a family's 8 neighbors with its color
step_once <- function(g) {
movers <- which(unhappy(g))
if (length(movers) == 0) return(g)
movers <- movers[sample.int(length(movers))]
for (m in movers) {
empties <- which(g == 0)
dest <- empties[sample.int(length(empties), 1)]
g[dest] <- g[m]; g[m] <- 0L
}
g
}
state <- reactiveValues(g = NULL, rounds = 0, D = numeric(0))
init <- function() {
state$g <- new_grid(); state$rounds <- 0
state$D <- like_avg(state$g)
}
observeEvent(list(input$reset, input$empty, input$blue), init(), ignoreNULL = FALSE)
advance <- function(k) {
g <- state$g
for (i in seq_len(k)) {
g <- step_once(g)
state$D <- c(state$D, like_avg(g))
}
state$g <- g; state$rounds <- state$rounds + k
}
observeEvent(input$step, advance(1))
# "Run": move one round at a time so you can watch, and stop once no one wants to move
running <- reactiveVal(FALSE)
observeEvent(input$run, running(TRUE))
observeEvent(list(input$reset, input$empty, input$blue), running(FALSE))
observe({
req(running())
invalidateLater(250)
isolate({
if (!any(unhappy(state$g)) || state$rounds >= 200) running(FALSE) else advance(1)
})
})
output$grid <- renderPlot({
req(state$g)
par(mar = c(0.5, 0.5, 2.5, 0.5))
plot(NULL, xlim = c(-1.5, n + 0.5), ylim = c(-5.5, n + 0.5), asp = 1, axes = FALSE,
xlab = "", ylab = "", main = paste0("City after ", state$rounds, " rounds"))
image(1:n, 1:n, state$g, col = c("#ffffff", "#2980b9", "#e67e22"), zlim = c(0, 2), add = TRUE)
rect(0.5, 0.5, n + 0.5, n + 0.5, border = "#bdc3c7")
# one example neighborhood: the 3 x 3 square of 9 houses around the center house
cx <- n / 2; cy <- n / 2
segments(c(cx - 0.5, cx + 0.5), cy - 1.5, c(cx - 0.5, cx + 0.5), cy + 1.5, col = "#2c3e50", lwd = 1)
segments(cx - 1.5, c(cy - 0.5, cy + 0.5), cx + 1.5, c(cy - 0.5, cy + 0.5), col = "#2c3e50", lwd = 1)
rect(cx - 1.5, cy - 1.5, cx + 1.5, cy + 1.5, border = "#2c3e50", lwd = 3)
text(n / 2 + 0.5, -0.4, paste(n, "houses across"), cex = 0.85, col = "#555555")
text(-0.6, n / 2 + 0.5, paste(n, "houses down"), srt = 90, cex = 0.85, col = "#555555")
labs <- c("Blue family", "Orange family", "Empty house", "Neighborhood (9 houses)")
usr <- par("usr")
legend(mean(usr[1:2]), -2.2, xjust = 0.5, yjust = 1, horiz = TRUE, bty = "n", cex = 0.85,
x.intersp = 1, xpd = TRUE, legend = labs,
text.width = strwidth(labs, cex = 0.85) + 2.5, # space each entry by its own length
pch = c(22, 22, 22, 0), pt.cex = c(2, 2, 2, 2.6), pt.lwd = c(1, 1, 1, 3),
pt.bg = c("#2980b9", "#e67e22", "#ffffff", NA), col = c("#7f8c8d", "#7f8c8d", "#7f8c8d", "#2c3e50"))
})
output$path <- renderPlot({
req(state$g)
par(mar = c(4.5, 4.5, 3, 1))
plot(seq_along(state$D) - 1, 100 * state$D, type = "b", pch = 19, lwd = 2,
col = "#2c3e50", ylim = c(0, 100),
xlab = "Round", ylab = "% of neighbors with the same color",
main = "Segregation over time")
abline(h = 100 * input$thresh, lty = 2, col = "#e67e22", lwd = 2)
abline(h = 100 * state$D[1], lty = 3, col = "#7f8c8d", lwd = 2) # level at the random start
legend("bottomright", bty = "n", cex = 0.9,
legend = c("What families get (average)", "What each family wants (T)", "By chance (random start)"),
col = c("#2c3e50", "#e67e22", "#7f8c8d"), lty = c(1, 2, 3), lwd = 2, pch = c(19, NA, NA))
})
output$info <- renderUI({
req(state$g)
g <- state$g
ls <- like_share(g)[g > 0]
tags$div(class = "info-box",
HTML(paste0(
"<b>Rounds:</b> ", state$rounds,
if (state$rounds > 0 && !any(unhappy(g))) " (settled: no one wants to move)" else "", "<br>",
"<b>Unhappy families:</b> ", round(100 * mean(unhappy(g)[g > 0])), "%<br>",
"<b>Each family wants:</b> ", round(100 * input$thresh), "% like neighbors<br>",
"<b>Families get, on average:</b> ", round(100 * mean(ls)), "% like neighbors"
))
)
})
}
shinyApp(ui, server)
Things to try
- Start at T = 0.35 and press Run until settled. Families only want about a third of neighbors like them, yet the average share of like neighbors climbs well above 70%.
- Set the share of blue families to 0.8. Blue families start with mostly blue neighbors, but orange families start as a small minority. Many orange families are unhappy from the start, so they move a lot.
- Set T = 0. Everyone is happy, no one moves, and the share of like neighbors stays near 50%, what you get by chance.
- Set T = 0.75 and run. Many families can never be happy. The city keeps churning and may never settle.
From the model to real cities
The Schelling model shows that segregation can arise without strong preferences. It does not show that real segregation arose this way. In US history, laws, lending rules, and violence enforced segregation directly. Redlining maps from the 1930s and racial covenants in deeds are well-known examples.
Card, Mas and Rothstein (2008) tested for tipping in real data. They found that once the minority share of a census tract passed a threshold, often between 5% and 20%, the white population tended to fall sharply. The thresholds were higher in cities with more tolerant attitudes.
Segregation matters because neighborhoods shape children’s futures. In the Moving to Opportunity experiment, families got vouchers to move to lower-poverty areas. Chetty, Hendren and Katz (2016) found that children who moved before age 13 earned more as adults. Chetty and coauthors (2022) later found that friendships across income groups are one of the strongest predictors of upward mobility.
Connections
- Tiebout Sorting: households also sort by income and taste for public goods.
- Gentrification & Displacement: changes in who lives where, seen from the other side.
- Crime and Neighborhoods: fear of crime is one reason households move.
Did you know?
- Schelling first ran his model by hand, moving coins on a checkerboard. He shared the 2005 Nobel Prize in Economics with Robert Aumann.
- The dissimilarity index was introduced by Otis Dudley Duncan and Beverly Duncan in 1955. It is still the standard measure in the Census Bureau’s segregation reports.
- The model has spread far beyond economics. It is a classic example in computer science and physics of how simple local rules produce large-scale patterns.
Key papers
- Schelling, T. C. (1971). Dynamic Models of Segregation. Journal of Mathematical Sociology 1(2). Mild preferences, sharp segregation.
- Card, D., Mas, A. and Rothstein, J. (2008). Tipping and the Dynamics of Segregation. Quarterly Journal of Economics 123(1). Tipping points in US neighborhoods.
- Gautier, P. A., Siegmann, A. and van Vuuren, A. (2009). Terrorism and Attitudes towards Minorities: The Effect of the Theo van Gogh Murder on House Prices in Amsterdam. Journal of Urban Economics 65(2). After a shocking murder, prices fell in Amsterdam’s minority neighborhoods.
- Chetty, R., Hendren, N. and Katz, L. F. (2016). The Effects of Exposure to Better Neighborhoods on Children: New Evidence from the Moving to Opportunity Experiment. American Economic Review 106(4). Moving young pays off.
- Chetty, R. and coauthors (2022). Social Capital I: Measurement and Associations with Economic Mobility. Nature 608. Cross-class friendships and upward mobility.