Crime and Neighborhoods

Crime as a local disamenity

People care about safety, and they pay for it. If buyers expect more crime near a home, they should pay less for it. So house prices give us a way to measure what crime costs. This uses the same logic as Hedonic Pricing: the price of a home bundles its features, and safety is one of them.

The measurement problem

The hard part is that crime is not random. It is higher in places that also differ in many other ways: income, school quality, housing age, and distance to jobs. A simple comparison of prices in high-crime and low-crime areas mixes all of these together. It does not tell us what crime itself does to prices.

Near vs far: distance rings

One fix is to compare homes that are very close to each other. Draw a small ring around each home, say 0.05 miles. Then draw a wider band farther out, say 0.20 to 0.30 miles. Homes in both rings share the same neighborhood: the same schools, the same transit, and the same local trends.

Now ask: when a crime happens in the near ring, does the price fall more than when it happens in the far band? The far band works as a control. It picks up whatever is happening to the whole area. The difference, near minus far, isolates what is special about being right next to the crime.

\[\text{Effect} = \Delta \text{Price}_{\text{near}} - \Delta \text{Price}_{\text{far}}\]

Linden and Rockoff (2008) used this idea when a registered sex offender moved into a neighborhood. Homes very close to the offender’s address lost value. Homes a little farther away did not.

The Model View. This is a made-up city. No real data is used. Some areas are nicer than others, and crime is higher in the less nice areas. You set how much one nearby crime lowers a home’s price. The panel compares two estimates of that effect: a naive one that compares all homes, and a near vs far one that holds crime in the far band fixed. Click any home to see its rings.

#| standalone: true
#| viewerHeight: 700

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("effect", "True effect of one nearby crime on price (%):",
                  min = 0, max = 5, value = 2, step = 0.5),
      sliderInput("confound", "Crime is higher in less nice areas (0 = not at all, 1 = strongly):",
                  min = 0, max = 1, value = 0.7, step = 0.1),
      sliderInput("near", "Near ring radius (miles):",
                  min = 0.03, max = 0.10, value = 0.05, step = 0.01),
      actionButton("go", "New city", class = "btn-primary", width = "100%"),
      uiOutput("info")
    ),
    mainPanel(
      width = 8,
      plotOutput("map", height = "560px", click = "map_click")
    )
  )
)

server <- function(input, output, session) {

  side <- 2; far_in <- 0.20; far_out <- 0.30          # city is 2 x 2 miles
  nice <- function(x, y) sin(3 * x) + cos(3 * y)      # how nice each spot is

  city <- reactive({
    input$go
    nh <- 400; n_inc <- 3000
    hx <- runif(nh, 0.1, side - 0.1); hy <- runif(nh, 0.1, side - 0.1)
    cx <- runif(20 * n_inc, 0, side); cy <- runif(20 * n_inc, 0, side)
    w <- exp(-1.5 * input$confound * nice(cx, cy))     # more crime where it is less nice
    k <- sample(seq_along(cx), n_inc, prob = w)
    list(hx = hx, hy = hy, ix = cx[k], iy = cy[k], noise = rnorm(nh, 0, 0.03))
  })

  res <- reactive({
    c0 <- city()
    d <- sqrt(outer(c0$hx, c0$ix, "-")^2 + outer(c0$hy, c0$iy, "-")^2)
    near_n <- rowSums(d <= input$near)
    far_n <- rowSums(d >= far_in & d <= far_out)
    logp <- log(800) + 0.15 * nice(c0$hx, c0$hy) - (input$effect / 100) * near_n + c0$noise
    naive <- -100 * coef(lm(logp ~ near_n))[2]
    ring <- -100 * coef(lm(logp ~ near_n + far_n))[2]
    list(d = d, near_n = near_n, far_n = far_n, price = exp(logp),
         naive = naive, ring = ring)
  })

  chosen <- reactiveVal(1)
  observeEvent(input$go, chosen(1))
  observeEvent(input$map_click, {
    c0 <- city()
    dd <- (c0$hx - input$map_click$x)^2 + (c0$hy - input$map_click$y)^2
    chosen(which.min(dd))
  })

  circle <- function(x, y, r, ...) {
    th <- seq(0, 2 * pi, length.out = 120)
    polygon(x + r * cos(th), y + r * sin(th), ...)
  }

  output$map <- renderPlot({
    c0 <- city(); r <- res(); h <- chosen()
    pal <- colorRampPalette(c("#d6eaf8", "#1b4f72"))(50)
    pc <- pal[cut(r$price, 50, labels = FALSE)]
    par(mar = c(4, 4, 3, 1))
    plot(NULL, xlim = c(0, side), ylim = c(0, side), asp = 1,
         xlab = "Miles east", ylab = "Miles north",
         main = "Click a home to draw its rings")
    # far band as a donut (only the band is shaded), then the near ring, then the dots on top
    th <- seq(0, 2 * pi, length.out = 120)
    polypath(c(c0$hx[h] + far_out * cos(th), NA, c0$hx[h] + far_in * rev(cos(th))),
             c(c0$hy[h] + far_out * sin(th), NA, c0$hy[h] + far_in * rev(sin(th))),
             col = adjustcolor("#3498db", 0.15), border = "#3498db", lwd = 2, rule = "evenodd")
    circle(c0$hx[h], c0$hy[h], input$near, col = adjustcolor("#e67e22", 0.30), border = "#e67e22", lwd = 2)
    points(c0$hx, c0$hy, pch = 16, cex = 0.75, col = pc)
    points(c0$ix, c0$iy, pch = 16, cex = 0.45, col = adjustcolor("#c0392b", 0.55))
    points(c0$hx[h], c0$hy[h], pch = 21, cex = 1.8, bg = "#2c3e50", col = "white", lwd = 2)
    legend("topright", bty = "o", bg = "white", cex = 0.85,
           legend = c("Home (darker = pricier)", "Crime", "Near ring", "Far band (0.20 to 0.30 mi)"),
           pch = c(16, 16, 15, 15),
           col = c("#2874a6", "#c0392b", adjustcolor("#e67e22", 0.6), adjustcolor("#3498db", 0.4)))
  })

  output$info <- renderUI({
    r <- res(); h <- chosen()
    tags$div(class = "info-box",
      HTML(paste0(
        "<b>This home:</b> $", format(round(r$price[h]), big.mark = ","), "k, ",
        r$near_n[h], " crimes near, ", r$far_n[h], " far<br>",
        "<b>True effect:</b> ", sprintf("%.1f", input$effect), "% per nearby crime<br>",
        "<b>Naive estimate:</b> ", sprintf("%.1f", r$naive), "%<br>",
        "<b>Near vs far estimate:</b> ", sprintf("%.1f", r$ring), "%"
      ))
    )
  })
}

shinyApp(ui, server)

Things to try

  • Start with the defaults. The naive estimate is much bigger than the true effect. Homes near crime are also in less nice areas, so the naive comparison blames crime for both.
  • Set “crime is higher in less nice areas” to 0. Crime is now spread evenly, and both estimates are close to the truth.
  • Set the true effect to 0. The naive estimate still finds a drop in price. The near vs far estimate is close to zero, as it should be.
  • Click homes in dark and light areas. Pricier homes tend to have fewer crimes in both rings. That is why the far band works as a control for the area.

Beyond distance

Timing matters as much as distance. A crime from last week may matter more than one from two years ago, so studies count crimes over different time windows before each sale.

Not every crime is equal either. A burglary next door and a crime that only shows up in police records may not affect buyers the same way.

Connections

Did you know?

  • Gary Becker started the economics of crime with his 1968 paper “Crime and Punishment: An Economic Approach.” He treated crime as a choice that responds to costs and benefits.
  • Megan’s Law, passed by Congress in 1996, made sex offender addresses public. That public list is what let Linden and Rockoff measure how buyers react to a new neighbor.
  • Many US cities now publish geocoded crime data on open data portals. That makes ring designs possible with free data.

Key papers