Insurance redlining analysis

Author

Clay Ford

Published

November 5, 2025

Redlining refers to the practice of refusing to issue insurance to certain types of people or within some geographic area. In the 1970s, the US Commission on Civil Rights examined charges by Chicago community organizations that insurance companies were redlining their neighborhoods. A FAIR plan (Fair Access to Insurance Requirements) was offered by the city of Chicago as a default policy to homeowners rejected by the voluntary market. This serves as a measure of insurance availability. Of interest is the extent to which racial composition of a community affects underwriting practices after controlling for factors that legitimately affect underwriting such as theft and fire damage.

Data was collected for 47 zip codes.

Zip Code Areas of Chicago

Variable definitions:

Row names are zip codes.

chicago <- readRDS("chicago.rds")
head(chicago)
      race fire theft  age involact income
60626 10.0  6.2    29 60.4      0.0  11744
60640 22.2  9.5    44 76.5      0.1   9323
60613 19.6 10.5    36 73.5      1.2   9948
60657 17.3  7.7    37 66.9      0.5  10656
60614 24.5  8.6    53 81.4      0.7   9730
60610 54.0 34.1    68 52.6      0.3   8231

Numerical summaries of data.

summary(chicago)
      race            fire           theft             age       
 Min.   : 1.00   Min.   : 2.00   Min.   :  3.00   Min.   : 2.00  
 1st Qu.: 3.75   1st Qu.: 5.65   1st Qu.: 22.00   1st Qu.:48.60  
 Median :24.50   Median :10.40   Median : 29.00   Median :65.00  
 Mean   :34.99   Mean   :12.28   Mean   : 32.36   Mean   :60.33  
 3rd Qu.:57.65   3rd Qu.:16.05   3rd Qu.: 38.00   3rd Qu.:77.30  
 Max.   :99.70   Max.   :39.70   Max.   :147.00   Max.   :90.10  
    involact          income     
 Min.   :0.0000   Min.   : 5583  
 1st Qu.:0.0000   1st Qu.: 8447  
 Median :0.4000   Median :10694  
 Mean   :0.6149   Mean   :10696  
 3rd Qu.:0.9000   3rd Qu.:11989  
 Max.   :2.2000   Max.   :21480  

What proportion are zeroes?

sum(chicago$involact == 0)
[1] 15
mean(chicago$involact == 0)
[1] 0.3191489

Pairwise correlations

round(cor(chicago), 2)
          race  fire theft   age involact income
race      1.00  0.59  0.26  0.25     0.71  -0.70
fire      0.59  1.00  0.56  0.41     0.70  -0.61
theft     0.26  0.56  1.00  0.32     0.15  -0.17
age       0.25  0.41  0.32  1.00     0.48  -0.53
involact  0.71  0.70  0.15  0.48     1.00  -0.66
income   -0.70 -0.61 -0.17 -0.53    -0.66   1.00

Some pairwise scatter plots. We see some outliers and possibly influential points.

library(ggplot2)
ggplot(chicago) +
  aes(x = race, y = involact) +
  geom_point() +
  stat_smooth()

ggplot(chicago) +
  aes(x = fire, y = involact) +
  geom_point() +
  stat_smooth()

ggplot(chicago) +
  aes(x = theft, y = involact) +
  geom_point() +
  stat_smooth()

ggplot(chicago) +
  aes(x = income, y = involact) +
  geom_point() +
  stat_smooth()

Comparing race with fire.

ggplot(chicago) +
  aes(x = race, y = fire) +
  geom_point() +
  stat_smooth()

Comparing race and theft.

ggplot(chicago) + 
  aes(x = race, y = theft) + 
  geom_point() +
  stat_smooth()

Full model and diagnostics

Model involact (new and renewed FAIR plans per 100 housing units) as function of race controlling for fire, theft, age of home, and median family income. Log transform income since it’s better to consider it on a multiplicative scale instead of an additive scale.

m1 <- lm(involact ~ race + fire + theft + age + log(income), 
         data = chicago)
summary(m1)

Call:
lm(formula = involact ~ race + fire + theft + age + log(income), 
    data = chicago)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.85393 -0.16922 -0.03088  0.17890  0.81228 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -3.573976   3.857292  -0.927 0.359582    
race         0.009502   0.002490   3.817 0.000449 ***
fire         0.039856   0.008766   4.547 4.76e-05 ***
theft       -0.010295   0.002818  -3.653 0.000728 ***
age          0.008336   0.002744   3.038 0.004134 ** 
log(income)  0.345762   0.400123   0.864 0.392540    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.3345 on 41 degrees of freedom
Multiple R-squared:  0.7517,    Adjusted R-squared:  0.7214 
F-statistic: 24.83 on 5 and 41 DF,  p-value: 2.009e-11

Sensitivity analysis

How robust is our conclusion to the choice of covariates?

listcombo <- unlist(sapply(0:4, function(x)combn(1:4, x, simplify = FALSE)),
                    recursive = FALSE)
predterms <- lapply(listcombo, function(x)
   paste(c("race", c("fire", "theft", "age", "log(income)")[x]),
   collapse = " + "))
coefm <- matrix(NA, 16, 2)
for(i in 1:16){
   lmi <- lm(as.formula(paste("involact ~ ", predterms[[i]])),
         data = chicago)
   # extract race coefficient and p-value
   coefm[i,] <- summary(lmi)$coef[2, c(1, 4)] 
}
rownames(coefm) <- predterms
colnames(coefm) <- c("race", "pvalue")
round(coefm, 4)
                                          race pvalue
race                                    0.0139 0.0000
race + fire                             0.0089 0.0002
race + theft                            0.0141 0.0000
race + age                              0.0123 0.0000
race + log(income)                      0.0082 0.0087
race + fire + theft                     0.0082 0.0002
race + fire + age                       0.0089 0.0001
race + fire + log(income)               0.0070 0.0160
race + theft + age                      0.0128 0.0000
race + theft + log(income)              0.0084 0.0083
race + age + log(income)                0.0099 0.0017
race + fire + theft + age               0.0081 0.0001
race + fire + theft + log(income)       0.0073 0.0078
race + fire + age + log(income)         0.0085 0.0041
race + theft + age + log(income)        0.0106 0.0010
race + fire + theft + age + log(income) 0.0095 0.0004

Pretty robust. It doesn’t matter what combination of covariates we control for, the race coefficient is positive.

Does our conclusion depend on a few cases? Do we have influential observations?

plot(m1, which = 5)

High theft and fire zip codes.

chicago["60607",]
      race fire theft age involact income
60607 50.2 39.7   147  83      0.9   7459

Fit a model without this observation.

i <- which(rownames(chicago) == "60607")
m2 <- lm(involact ~ race + fire + theft + age + log(income),
         data = chicago, subset = -i)
summary(m2)

Call:
lm(formula = involact ~ race + fire + theft + age + log(income), 
    data = chicago, subset = -i)

Residuals:
     Min       1Q   Median       3Q      Max 
-0.72943 -0.19062 -0.04279  0.18546  0.79648 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -4.658229   4.103618  -1.135 0.263068    
race         0.010416   0.002748   3.790 0.000497 ***
fire         0.039506   0.008815   4.481 6.08e-05 ***
theft       -0.012856   0.004269  -3.012 0.004488 ** 
age          0.009073   0.002906   3.123 0.003326 ** 
log(income)  0.463082   0.427708   1.083 0.285423    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.336 on 40 degrees of freedom
Multiple R-squared:  0.7545,    Adjusted R-squared:  0.7239 
F-statistic: 24.59 on 5 and 40 DF,  p-value: 3.117e-11

Compare models.

car::compareCoefs(m1, m2)
Calls:
1: lm(formula = involact ~ race + fire + theft + age + log(income), data = 
  chicago)
2: lm(formula = involact ~ race + fire + theft + age + log(income), data = 
  chicago, subset = -i)

             Model 1  Model 2
(Intercept)    -3.57    -4.66
SE              3.86     4.10
                             
race         0.00950  0.01042
SE           0.00249  0.00275
                             
fire         0.03986  0.03951
SE           0.00877  0.00882
                             
theft       -0.01029 -0.01286
SE           0.00282  0.00427
                             
age          0.00834  0.00907
SE           0.00274  0.00291
                             
log(income)    0.346    0.463
SE             0.400    0.428
                             

Conclusion seems robust to the exclusion of one data point.

Discussion

Race coefficient is statistically significant but small. Every 10 percent increase in the minority composition leads to about a 0.09 increase in FAIR plan policies and renewals per 100 housing units

The predicted difference between 0% and 100% minority is about 1.

library(emmeans)
emmeans(m1, specs = "race", at = list(race = c(0, 100)))
 race emmean     SE df lower.CL upper.CL
    0  0.293 0.0935 41    0.104    0.482
  100  1.243 0.1770 41    0.886    1.600

Confidence level used: 0.95 

We can visualize this association with an effect plot.

library(ggeffects)
ggpredict(m1, terms = "race") |> 
  plot(show_data = TRUE)

The analysis is also at the aggregate level. The results may not hold at the individual level.

Session information

The analysis was done using the R Statistical language (v4.5.2; R Core Team, 2025) on Windows 11 x64, using the packages emmeans (v1.11.2.8), ggeffects (v2.3.1) and ggplot2 (v4.0.0).

References