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
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.
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.
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.
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.
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(year, cost_burden2) %>%summarise(N =sum(WGTP)) %>%mutate(Proportion = N/sum(N))
`summarise()` has grouped output by 'year'. You can override using the
`.groups` argument.
Code
gt(tab2)
cost_burden2
N
Proportion
2014
Cost burdened
45298
0.5205291
Not cost burdened
41725
0.4794709
2019
Cost burdened
36596
0.3853671
Not cost burdened
58368
0.6146329
Blog Post 3
Downloading the shape files for 2014 and 2019.
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.
Using first two digits of 27053 - '27' (minnesota) - for FIPS code.FALSE
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.
Code
mapview(hmap2014)
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Message 1: Sub-geometry 0 has coordinate dimension 2, but container has 3
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Message 1: Sub-geometry 1 has coordinate dimension 2, but container has 3
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Message 1: Sub-geometry 2 has coordinate dimension 2, but container has 3
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Message 1: Sub-geometry 3 has coordinate dimension 2, but container has 3
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Message 1: Sub-geometry 4 has coordinate dimension 2, but container has 3
Filter for the 3 PUMAs I am interested in for 2014.
# 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
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
Make a dataframe grouped by PUMA showing percent of renters who are cost burdened in 2014.
Join my 2019 cost burden data with the shapefile and make a map.
This raises some questions. - How can I modify a shape file to show only certain PUMAs? OK, I did this by filtering the shapefile by PUMA. - Did the PUMAs change between 2014 and 2019? Could I overlay these to compare? Or could I research how often USCB updates PUMAs? - Should I just use the shapefile for one year or for both years? - Should I map the whole county rather than just the 3 PUMAs?
Next steps
Change maps to tmaps and mapview to improve formatting.
Make a map showing cost burden change over time.
Should I make maps for the whole county? How would I do this?
Instead of having three cost burden levels, it would make more sense to dichotomize the variable into “Cost burdened” and “Not cost burdened.” The data would be better formatted for creating choropleth maps of change in cost burden from 2014 to 2019.
The change in # renters and cost burden from 2014 to 2019 can be tested for statistical significance.
Tables showing change in cost burden can be better formatted, such as showing cost burden in rows and the year in columns.