GIS Research Project Blog

Author

Brian Surratt

Published

April 26, 2023

Blog Post 1

Research project idea (500 words). Here you will describe the idea for your research project, including some background on the topic, a description of your data source(s) and a description of your variable(s). You will also state your research questions and goals related to your project.

In December 2014, Minneapolis, Minnesota passed a city ordinance allowing for the construction of accessory dwelling units (ADUs, also known as “granny flats”) on existing single-family lots. [1] The ordinance was the first in a series of steps to address the costs of housing in the city by increasing housing supply. In 2018, Minneapolis continued this approach by banning single-family zoning, thus requiring a 3-unit minimum on each lot for new construction.

The 2014 ordinance allows single-family homeowners to construct ADUs on their property. Theoretically, this should add housing supply to the city and apply downward pressure on housing costs. Critics argued ADUs wouldn’t make a significant impact on housing supply since they are relatively expensive to build (around $100,000 each at the time) and the property owner had to continue to reside in either the existing home or the ADU. In other words, they would primarily be used by family members and would not enter the general rental market.

The 2018 ordinance banning single-family zoning was one of the first of its kind. Single-family zoning is highly valued by homeowners but advocates of high-density housing argue it should be phased out. It is highly unusual for existing single-family zoning to be modified, so Minneapolis is entering uncharted territory for housing policy.

This research project will analyze changes in rental affordability in Minneapolis between 2014, when the ADU ordinance was passed, and 2020. Comparing housing costs before and after the ADU ordinance and single-family zoning ban will provide evidence of the effects of these changes in housing policy. The research question is how did rental affordability change in Minneapolis, Minnesota, five years after the city passed an ordinance allowing ADUs on single family lots in December 2014 and two years after the city banned single family zoning in 2018?

I will map 3 variables from the U.S. Census Bureau American Community Survey Public Use Microdata (2014 and 2020) related to housing costs in Minneapolis, Minnesota, in 2014 and 2020, by census tract. The variables are percent of renters in each tract (derived from the TEN variable, housing tenure), the percent of renters who are cost burdened (derived from GRPIP, gross rental as a percentate of household income), and the change in percent of renters who are cost burdened from 2014 to 2020. “Cost burdened” is defined as paying greater than 30% of income towards housing costs.

I will produce the following maps and tables:

  • Map 1: Percent of all residents who are renters in each census tract in 2014.
  • Map 2: Precent of all residents who are renters in each census tract in 2020.
  • Map 3: Percent of renters in each census tract who are cost burdened in 2014.
  • Map 4: Percent of renters in each census tract who are cost burdened in 2020.
  • Map 5: Change in percent of renters who are cost burdened from 2014 to 2019.
  • Table 1: List of census tracts with percent cost burdened in 2014, in 2020, and change in percentage over the 5-year period.

References

  1. ACCESSORY DWELLING UNIT ZONING CODE TEXT AMENDMENT PASSES IN MINNEAPOLIS. (2014, Dec 09). US Fed News Service, Including US State News Retrieved from https://login.libweb.lib.utsa.edu/login?url=https://www.proquest.com/wire-feeds/accessory-dwelling-unit-zoning-code-text/docview/1634265244/se-2

Blog Post 2

Description of data and GIS processes (250-500 words, 1-2 tables, 2-3 figures). Here you will describe your data, including the source and origin, as well as a plan for what GIS operations you will be conducting during the course of your project.

Description of data

The source of data is the U.S. Census Bureau’s American Community Survey Public Use Microdata from 2014 and 2019. I will access the data via the get_pums() function in r. The variables I will use are the following:

  • PUMA: Public use area microdata area code. The PUMAs for Minneapolis are 1405, 1406, and 1407. In the 2014 data, this is a 4 character variable. In the 2019 data, this is a 5 character variable.
  • TYPE: Type of unit, filtered for “1” which is a housing unit. This removes group quarters.
  • TEN: This variable is housing tenure and can have four values, 1 = “owned with mortgage,” 2 = “owned free and clear,” 3 = “rented,” and 4 = “occupied without payment of rent.”
  • GRPIP: Gross rent as a percentage of household income past 12 months.
  • RELP: This is the relationship of the respondent to the household reference person. For the 2014 data this must be filtered to “00” to limit to one response per household. For the 2019 data, the variable name is RELSHIPP and the filter must be set to “20”.

Plan for GIS operations

For the GIS operations, I will download the shapefiles using the purrr::map() function and merge the data with the Minneapolis housing data. I will produce the following maps:

  • Map 1: Percent of all residents who are renters in each census tract in 2014
  • Map 2: Percent of all residents who are renters in each census tract in 2019
  • Map 3: Percent of renters in each census tract who are cost burdened in 2014
  • Map 4: Percent of renters in each census tract who are cost burdened in 2019
  • Map 5: Change in percent of renters who are cost burdened from 2014 to 2019

Cleaning the data and initial summary statistics

First, let’s download and clean Minnesota PUMS data for 2014.

Code
mnpums2014 <- get_pums(
  variables = c("PUMA", "TYPE", "TEN", "GRPIP", "HHT", "RELP"),
  state = "MN",
  variables_filter = list(SPORDER = 1, TYPE = 1), # SPORDER = 1 gets households, TYPE = 1 gets housing units and eliminates group quarters.
  #puma = c(1405, 1406, 1407), # I can't figure out how to select by puma.  Maybe capitalize "PUMA"?
  survey = "acs1",
  year = 2014
  )
Getting data from the 2014 1-year ACS Public Use Microdata Sample

This is a sample of all respondents in Minnesota for 2014. Let’s check the sample size.

Code
nrow(mnpums2014)
[1] 21524

Let’s create a new dataframe with only the PUMAs that cover Minneapolis (1405, 1406, and 1407) in 2014.

Code
dat2014 <- mnpums2014 %>%
  filter(PUMA %in% c("1405", "1406", "1407"))

Let’s modify the PUMA variable in the 2014 dataframe so the PUMA is 5 digits.

Code
dat2014 <- dat2014 %>%
  mutate(PUMA = case_when(.$PUMA == "1405" ~ "01405",
                          .$PUMA == "1406" ~ "01406",
                          .$PUMA == "1407" ~ "01407",
                          )
         )

Let’s check the sample size.

Code
nrow(dat2014)
[1] 1023

Let’s check the distribution by PUMA.

Code
tabyl(dat2014$PUMA)
 dat2014$PUMA   n   percent
        01405 344 0.3362659
        01406 331 0.3235582
        01407 348 0.3401760

In order to get just one observation per household, we need to ensure RELP == 0 for every record.

Code
tabyl(dat2014$RELP)
 dat2014$RELP    n percent
            0 1023       1

So now we know there is only one observation per household. Let’s see the distribution of renters vs. non-renters. 1 is owned with a mortgage, 2 is owned free and clear, 3 is rented, 4 is occupied without payment of rent.

Code
tabyl(dat2014$TEN)
 dat2014$TEN   n     percent
           1 401 0.391984360
           2 158 0.154447703
           3 461 0.450635386
           4   3 0.002932551

Let’s recode the TEN variable categories to reflect renters and non-renters.

Code
dat2014 <- dat2014 %>%
  mutate(tenure = (ifelse(.$TEN == 3, 'Renter', 'Non-renter')))

tabyl(dat2014$tenure)
 dat2014$tenure   n   percent
     Non-renter 562 0.5493646
         Renter 461 0.4506354

Making a data frame with percent of renters in 2014 and 2019

Code
renters2014 <- dat2014%>%
  group_by(PUMA, tenure) %>%
  summarise(N = sum(WGTP)) %>%
  mutate(Proportion = N/sum(N)) %>% 
  filter(tenure == 'Renter')
`summarise()` has grouped output by 'PUMA'. You can override using the
`.groups` argument.
Code
renters2014
# A tibble: 3 × 4
# Groups:   PUMA [3]
  PUMA  tenure     N Proportion
  <chr> <chr>  <dbl>      <dbl>
1 01405 Renter 25280      0.477
2 01406 Renter 21451      0.409
3 01407 Renter 40292      0.608

Let’s filter only renters and check the distribution of rent as a percentage of income.

Code
dat2014 <- dat2014 %>%
  filter(tenure == "Renter")

dat2014$GRPIP <- (as.numeric(dat2014$GRPIP))/100

hist(dat2014$GRPIP)

Let’s create a new variable called “cost_burden” with three levels:

  • Paying less than 30% of income on rent is “Not cost burdened”.
  • Paying between 30% of income on rent and 49.4% of income on rent is “Cost burdened”.
  • Paying greater than 50% of income on rent is “Extremely cost burdened”.
Code
dat2014 <- dat2014 %>%
  mutate(cost_burden = case_when(.$GRPIP <.30 ~ "Not cost burdened",
                                 .$GRPIP >=.30 & .$GRPIP <.50 ~ "Cost burdened",
                                 .$GRPIP >=.50 ~ "Extremely cost burdened",
                                 )
         )

Let’s check the distribution of rent cost burden in Minneapolis in 2014.

Code
tabyl(dat2014$cost_burden)
     dat2014$cost_burden   n   percent
           Cost burdened  95 0.2060738
 Extremely cost burdened 136 0.2950108
       Not cost burdened 230 0.4989154

Now, let’s download and clean Minnesota PUMS data for 2019. (PUMS data was not released for 2020 and PUMS geographies are not available for 2021, so those years are not available.)

Code
mnpums2019 <- get_pums(
  variables = c("PUMA", "TYPE", "TEN", "GRPIP", "HHT", "RELSHIPP"),
  state = "MN",
  variables_filter = list(SPORDER = 1, TYPE = 1), # SPORDER = 1 gets households, TYPE = 1 gets housing units and eliminates group quarters.
  #puma = c(1405, 1406, 1407), # I can't figure out how to select by puma.  Maybe capitalize "PUMA".
  survey = "acs1",
  year = 2019
  )
Getting data from the 2019 1-year ACS Public Use Microdata Sample

This is a sample of PUMS respondents in Minnesota for 2019. Let’s check the sample size.

Code
nrow(mnpums2019)
[1] 22576

First, let’s create a new dataframe with only the PUMAs that cover Minneapolis (1405, 1406, and 1407) in 2019.

Code
dat2019 <- mnpums2019 %>%
  filter(PUMA %in% c("01405", "01406", "01407"))

Let’s check the sample size.

Code
nrow(dat2019)
[1] 1054

Let’s check the distribution by PUMA.

Code
tabyl(dat2019$PUMA)
 dat2019$PUMA   n   percent
        01405 350 0.3320683
        01406 336 0.3187856
        01407 368 0.3491461

In order to get just one observation per household, we need to ensure RELSHIPP == 20 for every record.

Code
tabyl(dat2019$RELSHIPP)
 dat2019$RELSHIPP    n percent
               20 1054       1

So now we know there is only one observation per household. Let’s see the distribution of renters vs. non-renters. 1 is owned with a mortgage, 2 is owned free and clear, 3 is rented, 4 is occupied without payment of rent.

Code
tabyl(dat2019$TEN)
 dat2019$TEN   n   percent
           1 413 0.3918406
           2 174 0.1650854
           3 461 0.4373814
           4   6 0.0056926

Let’s recode the TEN variable categories to reflect renters and non-renters.

Code
dat2019 <- dat2019 %>%
  mutate(tenure = (ifelse(.$TEN == 3, 'Renter', 'Non-renter')))

tabyl(dat2019$tenure)
 dat2019$tenure   n   percent
     Non-renter 593 0.5626186
         Renter 461 0.4373814

Making a data frame with percent of renters in 2019.

Code
renters2019 <- dat2019%>%
  group_by(PUMA, tenure) %>%
  summarise(N = sum(WGTP)) %>%
  mutate(Proportion = N/sum(N)) %>% 
  filter(tenure == 'Renter')
`summarise()` has grouped output by 'PUMA'. You can override using the
`.groups` argument.
Code
renters2019
# A tibble: 3 × 4
# Groups:   PUMA [3]
  PUMA  tenure     N Proportion
  <chr> <chr>  <dbl>      <dbl>
1 01405 Renter 27542      0.495
2 01406 Renter 23169      0.428
3 01407 Renter 44253      0.596

Let’s filter only renters and check the distribution of rent as a percentage of income.

Code
dat2019 <- dat2019 %>%
  filter(tenure == "Renter")

dat2019$GRPIP <- (as.numeric(dat2019$GRPIP))/100

hist(dat2019$GRPIP)

Let’s create a new variable called “cost_burden” with three levels:

  • Paying less than 30% of income on rent is “Not cost burdened”.
  • Paying between 30% of income on rent and 49.4% of income on rent is “Cost burdened.
  • Paying greater than 50% of income on rent is “Extremely cost burdened”.
Code
dat2019 <- dat2019 %>%
  mutate(cost_burden = case_when(.$GRPIP <.30 ~ "Not cost burdened",
                                 .$GRPIP >=.30 & .$GRPIP <.50 ~ "Cost burdened",
                                 .$GRPIP >=.50 ~ "Extremely cost burdened",
                                 )
         )

Let’s check the distribution of rent cost burden in Minneapolis in 2019.

Code
tabyl(dat2019$cost_burden)
     dat2019$cost_burden   n   percent
           Cost burdened  86 0.1865510
 Extremely cost burdened  94 0.2039046
       Not cost burdened 281 0.6095445

Now I need to merge the 2014 and 2019 dataframes.

Let’s rename RELSHIPP as RELP in the 2019 dataframe.

Code
dat2019 <- dat2019  %>%
  rename_at('RELSHIPP', ~'RELP')

Let’s add a year column to both dataframes.

Code
dat2014 <- dat2014 %>%
  mutate(year = "2014")

dat2019 <- dat2019 %>%
  mutate(year = "2019")

Let’s merge these dataframes into one.

Code
dat <- rbind(dat2014, dat2019)

head(dat)
# A tibble: 6 × 14
  SERIALNO  WGTP PWGTP PUMA  TEN   GRPIP HHT   RELP  SPORDER TYPE  ST    tenure
  <chr>    <dbl> <dbl> <chr> <chr> <dbl> <chr> <chr> <chr>   <chr> <chr> <chr> 
1 37569       92    92 01405 3      0.24 3     0     1       1     27    Renter
2 77963      196   195 01405 3      0.39 4     0     1       1     27    Renter
3 89323      137   137 01406 3      0.24 6     0     1       1     27    Renter
4 103528      88    88 01405 3      0.29 4     0     1       1     27    Renter
5 107132     106   106 01406 3      0.06 1     0     1       1     27    Renter
6 114983     145   145 01407 3      0.19 1     0     1       1     27    Renter
# ℹ 2 more variables: cost_burden <chr>, year <chr>

Let’s make a table showing the change in rent cost burden in the city from 2014 to 2019. This code weighs the data with WGTP, the variable for household weight.

Code
tab1 <- dat %>% 
  group_by(year, cost_burden) %>% 
  summarise(N = sum(WGTP)) %>%
  mutate(Proportion = N/sum(N))
`summarise()` has grouped output by 'year'. You can override using the
`.groups` argument.
Code
gt(tab1)
cost_burden N Proportion
2014
Cost burdened 17160 0.1971892
Extremely cost burdened 28138 0.3233398
Not cost burdened 41725 0.4794709
2019
Cost burdened 16108 0.1696222
Extremely cost burdened 20488 0.2157449
Not cost burdened 58368 0.6146329

This table shows the share of cost burdened renters declined from 19.7% in 2014 to 17.0% in 2019. The proportion of “Extremely cost burdened” renters declined from 32.2% in 2014 to 21.6% in 2019. The share of “Not cost burdened” renters rose from 47.9% in 2014 to 61.5% in 2019.

Let’s create a new variable called “cost_burden2” with two levels:

  • Paying less than 30% of income on rent is “Not cost burdened”.
  • Paying 30% or greater of income on rent is “Cost burdened.
Code
dat <- dat %>%
  mutate(cost_burden2 = case_when(.$GRPIP <.30 ~ "Not cost burdened",
                                  .$GRPIP >=.30 ~ "Cost burdened",
                                 )
         )

Let’s make a table showing the change in rent cost burden (cost_burden2) in the city from 2014 to 2019. This code weighs the data with WGTP, the variable for household weight.

Code
tab2 <- dat %>% 
  group_by(PUMA, year, cost_burden2) %>% 
  summarise(N = sum(WGTP)) %>%
  mutate(Proportion = N/sum(N))
`summarise()` has grouped output by 'PUMA', 'year'. You can override using the
`.groups` argument.
Code
gt(tab2)
cost_burden2 N Proportion
01405 - 2014
Cost burdened 14819 0.5861946
Not cost burdened 10461 0.4138054
01405 - 2019
Cost burdened 12676 0.4602425
Not cost burdened 14866 0.5397575
01406 - 2014
Cost burdened 12346 0.5755443
Not cost burdened 9105 0.4244557
01406 - 2019
Cost burdened 9089 0.3922914
Not cost burdened 14080 0.6077086
01407 - 2014
Cost burdened 18133 0.4500397
Not cost burdened 22159 0.5499603
01407 - 2019
Cost burdened 14831 0.3351411
Not cost burdened 29422 0.6648589

Downloading the shape files for 2014 and 2019.

Downloading the shapefile for Minnesota for 2014. I tried to use the FIPS code for Hennepin County, but the shape file is for all of Minnesota.

Code
# What package is the map function from (purrr)?  Why are we calling tigris::pumas?  What is 'cb'?  Can I use this sf file with tmap?

hmap2014 <- map("27053", # Getting PUMA geography for Hennepin County.
                tigris::pumas,
                class = "sf",
                cb = TRUE,
                year = 2014) %>%
  reduce(rbind) # This changes the list to a dataframe.  left_join won't work if you don't do this.

hcpumas2014 <- as.vector(hmap2014$PUMACE10)

Downloading the shapefile for Minnesota for 2019. I tried to use the FIPS code for Hennepin County, but the shape file is for all of Minnesota.

Code
hmap2019 <- map("27053",
                tigris::pumas,
                class = "sf",
                cb = TRUE,
                year = 2019) %>%
  reduce(rbind)

hcpumas2019 <- as.vector(hmap2019$PUMACE10)

# Cartographic boundary PUMAs are not yet available for years after 2019. Use the argument `year = 2019` instead to request your data.

Use mapview to view the shapefile. It shows all the PUMAs in the state of Minnesota.

Code
mapview(hmap2014)

Filter for the 3 PUMAs I am interested in for 2014 and create a quick map.

Code
mlps2014_sf <- hmap2014 %>%
  filter(PUMACE10 %in% c('01405', '01406', '01407'))

mapview(mlps2014_sf)

Should I expand the mapping/analysis to all of Hennepin County?

Using ggplot to make a map showing percent of renters in 2014.

Code
# These PUMAS are missing the 0 in the front.
map1 <- mlps2014_sf %>%
  left_join(renters2014, by = c("PUMACE10" = "PUMA")) %>%
  ggplot(aes(fill = Proportion)) +
  geom_sf() +
  scale_fill_viridis_b(
    name = NULL,
    option = "magma",
    labels = scales::label_percent(1)
    ) +
  # labs(title = "Mean Gross Rent as a Percentage of Houshold Income \nMapped by Public Use Microdata Area") +
  ggtitle("Percent of households who rent, 2014",
          subtitle = "Source: U.S. Census Bureau, 2014 ACS 1 year") +
  theme_void()

map1

Using ggplot to make a map showing percent of renters in 2019.

Code
map2 <- mlps2014_sf %>%
  left_join(renters2019, by = c("PUMACE10" = "PUMA")) %>%
  ggplot(aes(fill = Proportion)) +
  geom_sf() +
  scale_fill_viridis_b(
    name = NULL,
    option = "magma",
    labels = scales::label_percent(1)
    ) +
  # labs(title = "Mean Gross Rent as a Percentage of Houshold Income \nMapped by Public Use Microdata Area") +
  ggtitle("Percent of households who rent, 2019",
          subtitle = "Source: U.S. Census Bureau, 2014 ACS 1 year") +
  theme_void()

map2

Next, use ggplot to make maps of the percent who are cost burdened in 2014 and 2019

Make a dataframe grouped by PUMA showing percent of renters who are cost burdened in 2014.

Code
cb2014 <- dat %>%
  filter(year == 2014) %>%
  group_by(PUMA, cost_burden2) %>%
  summarise(N = sum(WGTP)) %>%
  mutate(Proportion = N/sum(N)) %>% 
  filter(cost_burden2 == 'Cost burdened')
`summarise()` has grouped output by 'PUMA'. You can override using the
`.groups` argument.
Code
cb2014
# A tibble: 3 × 4
# Groups:   PUMA [3]
  PUMA  cost_burden2      N Proportion
  <chr> <chr>         <dbl>      <dbl>
1 01405 Cost burdened 14819      0.586
2 01406 Cost burdened 12346      0.576
3 01407 Cost burdened 18133      0.450

Join my 2014 cost burden data with the shapefile and make a map.

Make a dataframe grouped by PUMA showing percent of renters who are cost burdened in 2019.

Code
cb2019 <- dat %>%
  filter(year == 2019) %>%
  group_by(PUMA, cost_burden2) %>%
  summarise(N = sum(WGTP)) %>%
  mutate(Proportion = N/sum(N)) %>% 
  filter(cost_burden2 == 'Cost burdened')
`summarise()` has grouped output by 'PUMA'. You can override using the
`.groups` argument.
Code
cb2019
# A tibble: 3 × 4
# Groups:   PUMA [3]
  PUMA  cost_burden2      N Proportion
  <chr> <chr>         <dbl>      <dbl>
1 01405 Cost burdened 12676      0.460
2 01406 Cost burdened  9089      0.392
3 01407 Cost burdened 14831      0.335

Join my 2019 cost burden data with the shapefile and make a map.

Blog Post 3

Preliminary results (500-750 words, 1-2 tables, 1-2 figures). This is where you will describe the preliminary results of your project. Here you will describe the justification of which techniques you used, and describe the preliminary results of your analysis based on the procedures you conducted.

Percent of renters in each PUMA in 2014

Code
gt(renters2014)
tenure N Proportion
01405
Renter 25280 0.4766394
01406
Renter 21451 0.4089254
01407
Renter 40292 0.6082179

In 2014, 48% of households in PUMA 1405 were renters. 40.1% of households in PUMA 1406 were renters. 60.1% of households in PUMA 1407 were renters.

Code
tmap1 <- mlps2014_sf %>%
  left_join(renters2014, by = c("PUMACE10" = "PUMA"))

tm_shape(tmap1)+
  tm_polygons("Proportion",
              title="Percent of renters in each PUMA",
              palette="Reds"
              )+
  tm_format("World",
            title="Percent of renters in 2014",
            legend.format = list(digits = 2),
            legend.outside=T)+
  tm_scale_bar()+
  tm_compass()

Percent of renters in each PUMA in 2019

Code
gt(renters2019)
tenure N Proportion
01405
Renter 27542 0.4952706
01406
Renter 23169 0.4280805
01407
Renter 44253 0.5964981

In 2019, 49.6% of households in PUMA 1405 were renters. 42.8% of households in PUMA 1406 were renters. 59.6% of households in PUMA 1407 were renters.

Code
tmap2 <- mlps2014_sf %>%
  left_join(renters2019, by = c("PUMACE10" = "PUMA"))

tm_shape(tmap2)+
  tm_polygons("Proportion",
              title="Percent of renters in each PUMA",
              palette="Reds"
              )+
  tm_format("World",
            title="Percent of renters in 2019",
            legend.outside=T)+
  tm_scale_bar()+
  tm_compass()

Percent of renters who are cost burdened in each PUMA in 2014.

Code
gt(cb2014)
cost_burden2 N Proportion
01405
Cost burdened 14819 0.5861946
01406
Cost burdened 12346 0.5755443
01407
Cost burdened 18133 0.4500397

In 2014, 58.6% of renters were cost burdened in PUMA 1405. 57.6% of renters were cost burdened in PUMA 1406. 45.0% of renters were cost burdened in PUMA 1407.

Code
tmap3 <- mlps2014_sf %>%
  left_join(cb2014, by = c("PUMACE10" = "PUMA"))

tm_shape(tmap3)+
  tm_polygons("Proportion",
              title="Percent of renters who are cost burdened",
              palette="Reds"
              )+
  tm_format("World",
            title="Percent who are cost burdened in 2014",
            legend.outside=T)+
  tm_scale_bar()+
  tm_compass()

Percent of renters who are cost burdened in each PUMA in 2019.

Code
gt(cb2019)
cost_burden2 N Proportion
01405
Cost burdened 12676 0.4602425
01406
Cost burdened 9089 0.3922914
01407
Cost burdened 14831 0.3351411

In 2019, 46.0% of renters were cost burdened in PUMA 1405. 39.2% of renters were cost burdened in PUMA 1406. 33.6% of renters were cost burdened in PUMA 1407.

Code
tmap4 <- mlps2014_sf %>%
  left_join(cb2019, by = c("PUMACE10" = "PUMA"))

tm_shape(tmap4)+
  tm_polygons("Proportion",
              title="Percent of renters who are cost burdened",
              palette="Reds"
              )+
  tm_format("World",
            title="Percent who are cost burdened in 2019",
            legend.outside=T)+
  tm_scale_bar()+
  tm_compass()

Create a data frame to find the difference in cost burden between 2014 and 2019.

Code
fiveyr <- cb2019 %>% 
  select(PUMA, Proportion)

fiveyr <- rename(fiveyr, Prop2019 = Proportion)

fiveyr <- merge(fiveyr, cb2014)

fiveyr <- fiveyr %>% 
  select(PUMA, Prop2019, Proportion)

fiveyr <- rename(fiveyr, Prop2014 = Proportion)

fiveyr <- fiveyr %>% 
  mutate(diff = round(((Prop2019 - Prop2014)*100), digits = 1))

gt(fiveyr)
PUMA Prop2019 Prop2014 diff
01405 0.4602425 0.5861946 -12.6
01406 0.3922914 0.5755443 -18.3
01407 0.3351411 0.4500397 -11.5

Between 2014 and 2019, the percent of renters who were costs burdened declined by 12.6% in PUMA 1405.

The proportion of renters who were cost burdened declined by 18.3% in PUMA 1406.

The proportion of renters who were cost burdened declined by 11.5% in PUMA 1407.

Change in renters who are cost burdened from 2014 to 2019

Code
tmap5 <- mlps2014_sf %>%
  left_join(fiveyr, by = c("PUMACE10" = "PUMA"))

tm_shape(tmap5)+
  tm_polygons("diff",
              title="Change in renters who are cost burdened from 2014 to 2019",
              palette="Reds"
              )+
  tm_format("World",
            title="Change in renters who are cost burdened from 2014 to 2019",
            legend.outside=T)+
  tm_scale_bar()+
  tm_compass()

In this project, I analyzed the change in affordability in Minneapolis, Minnesota at the PUMA level. I downloaded microdata from the US Census Bureau and analyzed rent as a percentage of income. “Cost burdened” was defined as paying greater than 30% of income on housing. I calculated summary statistics for the three PUMAS in Minneapolis. The city is comprised of three PUMAs, 1405, 1406, and 1407.

The preliminary results are:

  • In 2014, 48% of households in PUMA 1405 were renters. 40.1% of households in PUMA 1406 were renters. 60.1% of households in PUMA 1407 were renters.
  • In 2019, 49.6% of households in PUMA 1405 were renters. 42.8% of households in PUMA 1406 were renters. 59.6% of households in PUMA 1407 were renters.
  • In 2014, 58.6% of renters were cost burdened in PUMA 1405. 57.6% of renters were cost burdened in PUMA 1406. 45.0% of renters were cost burdened in PUMA 1407.
  • In 2019, 46.0% of renters were cost burdened in PUMA 1405. 39.2% of renters were cost burdened in PUMA 1406. 33.6% of renters were cost burdened in PUMA 1407.
  • Between 2014 and 2019, the percent of renters who were costs burdened declined by 12.6% in PUMA 1405.
  • Between 2014 and 2019, the proportion of renters who were cost burdened declined by 18.3% in PUMA 1406.
  • Between 2014 and 2019, the proportion of renters who were cost burdened declined by 11.5% in PUMA 1407.

Next steps

  • Project maps.
  • Conduct chi squared tests to see if the change in cost burden in Minneapolis is statistically significant.
  • Improve formatting of maps as needed.

Observations and future directions of research

  • Maps would be better suited to analyzing a larger area than just three PUMAS, such as a county or state.
  • There are many other factors at play in determining affordability, such as proportion of renters, total population, housing supply, employment, and wages.
  • The change in cost burden from 2014 to 2019 can be tested for statistical significance. Categorical = chi squared test.
  • Tables showing change in cost burden can be better formatted, such as showing cost burden in rows and the year in columns.
  • Graphs can be made showing the change over time.
  • Tables and graphs can be faceted by PUMS.