In this document I look at the “penetration rate” for new vehicles (cars and light trucks), based on the distribution of household income (what percentages of households fall into certain income ranges) versus the distribution of vehicle prices (what percentages of new vehicles sold fall into certain price ranges).
The 2018 Howard County Rental Survey defines the penetration rate as follows in the context of rental apartments:
The penetration rate is calculated by dividing the total number of units targeting a particular income range by the number of renter households with incomes that fall within that range. A penetration rate of 100 percent would indicate that there is equal number of multifamily units in an affordability classification and renter households with income sufficient to afford rents at that level. A penetration rate over 100 percent would indicate an oversupply of units, while a penetration rate of less than 100 percent would indicate an inadequate supply of units relative to the number of renter households in that income range.
I adapt this definition to the context of new vehicle sales, and do the analysis at the level of the United States as a whole, using U.S. Census household income data and third-party reports on new vehicle sales and prices.
For those readers unfamiliar with the R statistical software and the additional Tidyverse software I use to manipulate and plot data, I’ve included some additional explanation of various steps. For more information check out the the tutorial “Getting started with the Tidyverse”.
I use the following packages for the following purposes:
library(tidyverse)
library(tidycensus)
library(knitr)
I use data from the following sources:
The first set of data is from U.S. Census surveys of household income, and is obtained via the Census API.
The next two sets of data are taken from the CSV file vehicle-sales.csv
. Each row of the file contains data values as follows:
See the References section below for more information, including the data sources from which I compiled the data in vehicle-sales.csv
.
I use the Census API to obtain household income data for 2018 from the American Community Survey 1-year survey estimates. I first get the median household income for the U.S. as a whole, using the Census variable B19013_001
.
mhi_us <- get_acs(survey = 'acs1', year = 2018, variable = 'B19013_001', geography = 'us') %>%
select(estimate) %>%
as.integer()
## Getting data from the 2018 1-year ACS
## The one-year ACS provides data for geographies with populations of 65,000 and greater.
I next read in the Census table B19001
for the entire U.S. This table contains data on the number of households in each of 16 different income ranges, with one row per range, plus an initial row giving the estimated number of all households for the geography. I retain only the Census variable names used for each income range and the estimates for the number of households in each range. (For data at the level of the entire U.S. the margins of error are less than 1% of the estimates, and can be safely ignored.)
households_by_income <- get_acs(survey = 'acs1', year = 2018, table = 'B19001', geography = 'us') %>%
select(variable, estimate)
## Getting data from the 2018 1-year ACS
## The one-year ACS provides data for geographies with populations of 65,000 and greater.
## Loading ACS1 variables for 2018 from table B19001. To cache this dataset for faster access to ACS tables in the future, run this function with `cache_table = TRUE`. You only need to do this once per ACS dataset.
I next read in the vehicle sales and price data, and retain only those models for which I have price data.
veh_prices <- read_csv(
"vehicle-sales.csv",
col_names = c("model", "units", "min_price", "max_price"),
col_types = "cidd"
) %>%
filter(!is.na(min_price))
I have to do additional preparation of the data before I can analyze it.
First, the household income data provides estimates on the number of households in each income range, but I want the percentage of households in each range relative to the total number of U.S. households. The total number of households is stored in the first row of the households_by_income
table, under the Census variable B19001_001
. To compute the percentages I therefore do the following to create a new table pct_by_income
:
households_by_income
table read in above.spread()
function to convert each row of the table into a column, with the column names taken from the Census variable names and the column values taken from the estimates for those variables.mutate_at()
function to apply to each column a function that takes the column value (the number of households in a given income range) and replaces it by that value as a percentage of the value in the column corresponding to the Census variable B19001_001
(the total number of households).gather()
function to convert each column back into a row, with the column names of the new table being variable
(for the Census variable) and h_pct
(for the percentage of households in each income range).pct_by_income <- households_by_income %>%
spread(variable, estimate) %>%
mutate_at(1:ncol(.), function(x) 100 * x / .[["B19001_001"]]) %>%
gather(variable, h_pct)
I next extend the vehicle sales data to cover additional price points. The rationale for doing this is as follows:
If a vehicle model has only one list price in the data then I can reasonably assume that all vehicles of that model are sold at that price. (This ignores dealer discounts and sales of additional vehicle options, but I don’t have any data on those, and for purposes of this analysis I’ll ignore them.)
If a vehicle has a range of list prices, I could assume that all vehicles of that model are sold at the average of the minimum and maximum prices. This discards useful information though: a vehicle model with minimum and maximum prices of $30,000 and $70,000 respectively would be treated the same in terms of affordability as another vehicle model with minimum and maximum prices of $49,000 and $51,000 respectively, since the average price in both cases is $50,000. However, in practice the former vehicle model would be more affordable to those of lower incomes than the latter, since its minimum price (the list price of the base model) is lower.
To account for this factor, I divide the price range for a given vehicle model into five price points: the minimum price, the price intermediate between the minimum price and the average price, the average price, the price intermediate between the average price and the maximum price, and the maximum price. I then apportion vehicle sales among these price points in the ratio 1:2:4:2:1, to account for the typical scenario where sales of the base vehicle package and the most expensive package are relatively low compared to sales of the mid-range packages.
For example, for a vehicle model with sales of 50,000 units, a minimum price of $30,000, and a maximum price of $70,000, I apportion the units sold as follows:
I follow this scheme in creating a new table veh_prices_extended
, as follows:
veh_prices
table. If the minimum and maximum list prices are the same for a given model then I just add the model name and prices to the vectors that will make up the columns of the new table.veh_prices
table, I use the resulting vectors for model names, prices, and units to construct a new table veh_prices_extended
.model_vec = NULL
units_vec = NULL
price_vec = NULL
for (row in 1:nrow(veh_prices)) {
if (veh_prices$min_price[row] == veh_prices$max_price[row]) {
model_vec <- c(model_vec, veh_prices$model[row])
units_vec <- c(units_vec, veh_prices$units[row])
price_vec <- c(price_vec, veh_prices$min_price[row])
} else {
price_interval <- (veh_prices$max_price[row] - veh_prices$min_price[row]) / 4
new_prices <- seq(veh_prices$min_price[row], veh_prices$max_price[row], price_interval)
units_tenth <- (veh_prices$units[row] + 5) %/% 10
new_model <- veh_prices$model[row]
model_vec <- c(model_vec, rep(new_model, 5))
units_vec <- c(units_vec, units_tenth, 2 * units_tenth, 4 * units_tenth, 2 * units_tenth, units_tenth)
price_vec <- c(price_vec, new_prices[1], new_prices[2], new_prices[3], new_prices[4], new_prices[5])
}
}
veh_prices_extended <- tibble(model = model_vec, units = units_vec, price = price_vec)
In this section I create additional tables and functions for use in the analysis.
I create a table income_ranges
that maps the Census variable used for each range (e.g., B19001_002
) to the corresponding text description of the range (e.g., “Less than $10,000”). I define the income range descriptions as factors in order to force them to be displayed in a desired order when doing graphs.
hi_ranges <- c(
"Less than $10,000",
"$10,000 to $14,999",
"$15,000 to $19,999",
"$20,000 to $24,999",
"$25,000 to $29,999",
"$30,000 to $34,999",
"$35,000 to $39,999",
"$40,000 to $44,999",
"$45,000 to $49,999",
"$50,000 to $59,999",
"$60,000 to $74,999",
"$75,000 to $99,999",
"$100,000 to $124,999",
"$125,000 to $149,999",
"$150,000 to $199,999",
"$200,000 or more"
)
hi_ranges <- factor(hi_ranges, levels = hi_ranges)
hi_variables <- c(
"B19001_002",
"B19001_003",
"B19001_004",
"B19001_005",
"B19001_006",
"B19001_007",
"B19001_008",
"B19001_009",
"B19001_010",
"B19001_011",
"B19001_012",
"B19001_013",
"B19001_014",
"B19001_015",
"B19001_016",
"B19001_017"
)
income_ranges <- tibble(variable = hi_variables, income_range = hi_ranges)
I also create a function assign_to_range()
to take a particular income value and return the (text description of the) range in which that income falls.
assign_to_range <- function(x) {
ifelse(x < 10000, "Less than $10,000",
ifelse(x < 15000, "$10,000 to $14,999",
ifelse(x < 20000, "$15,000 to $19,999",
ifelse(x < 25000, "$20,000 to $24,999",
ifelse(x < 30000, "$25,000 to $29,999",
ifelse(x < 35000, "$30,000 to $34,999",
ifelse(x < 40000, "$35,000 to $39,999",
ifelse(x < 45000, "$40,000 to $44,999",
ifelse(x < 50000, "$45,000 to $49,999",
ifelse(x < 60000, "$50,000 to $59,999",
ifelse(x < 75000, "$60,000 to $74,999",
ifelse(x < 100000, "$75,000 to $99,999",
ifelse(x < 125000, "$100,000 to $124,999",
ifelse(x < 150000, "$125,000 to $149,999",
ifelse(x < 175000, "$150,000 to $199,999",
"$200,000 or more"
)))))))))))))))
}
Finally, I define a function weighted_median
to compute the weighted median of vehicle prices weighted by the number of vehicle models sold at each price. (See the References section for the source of the code for this function.) For example, if one vehicle of a given model were sold at a price of $20,000, 9 units of a second vehicle model sold at a price of $50,000, and one other vehicle of a third model sold at a price of $100,000, the weighted median price would be the same as the median value of the data values {20000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000, 100000}, or $50,000.
weighted_quantile <- function(x, w, probs=seq(0,1,0.25), na.rm=TRUE) {
x <- as.numeric(as.vector(x))
w <- as.numeric(as.vector(w))
if(anyNA(x) || anyNA(w)) {
ok <- !(is.na(x) | is.na(w))
x <- x[ok]
w <- w[ok]
}
stopifnot(all(w >= 0))
if(all(w == 0)) stop("All weights are zero", call.=FALSE)
#'
oo <- order(x)
x <- x[oo]
w <- w[oo]
Fx <- cumsum(w)/sum(w)
#'
result <- numeric(length(probs))
for(i in seq_along(result)) {
p <- probs[i]
lefties <- which(Fx <= p)
if(length(lefties) == 0) {
result[i] <- x[1]
} else {
left <- max(lefties)
result[i] <- x[left]
if(Fx[left] < p && left < length(x)) {
right <- left+1
y <- x[left] + (x[right]-x[left]) * (p-Fx[left])/(Fx[right]-Fx[left])
if(is.finite(y)) result[i] <- y
}
}
}
names(result) <- paste0(format(100 * probs, trim = TRUE), "%")
return(result)
}
weighted_median <- function(x, w, na.rm=TRUE) {
unname(weighted_quantile(x, probs=0.5, w=w, na.rm=na.rm))
}
I do analyses to answer the following questions:
I want to get a sense for what price vehicle is affordable to a household with a given income. My goal is to come up with a measure analogous to that used in housing affordability discussions. In those discussions people use 30% of household income as a criterion of affordability for renters: anyone spending more than that for rent is assumed to be “rental cost burdened”.
As noted below, for my purposes it’s simpler to relate new vehicle purchase price to household income: for a given household income, what vehicle purchase price can we consider affordable? Or, to put it another way, for a given vehicle purchase price, what household income is required to buy that vehicle and not be over-burdened?
I approach this problem from multiple directions. First, I can look to various third-party recommendations on how much one should spend on a vehicle:
Expressed in terms of the income needed to purchase a given vehicle, these recommendations span from requiring household income of 2 times the vehicle purchase price (Dave Ramsey) up to 5 times the purchase price (Break Free).
These recommendations also for the most part assume purchase of a single vehicle. According to the University of Michigan Transportation Research Institure, U.S. households have just under two vehicles on average. That means that under Dave Ramsey’s criterion (for example) a two-car household should not purchase a vehicle with a price of more than 25% of their household income.
Second, I can compare a “typical” new vehicle price to the U.S. median household income. (Even though the underlying data I’m using is from different years—2017 for vehicles sold, 2018 for household income, and 2019 for new vehicle prices—I ignore inflation in these calculations.)
I calculate a “typical” new vehicle price in three different ways:
veh_price_extended
table.veh_price_extended
table.kbb_mean_price = 35359
total_units <- sum(veh_prices_extended$units)
total_sales <- veh_prices_extended %>%
mutate(sales_per_model = units * price) %>%
summarize(total_sales = sum(sales_per_model)) %>%
as.numeric()
weighted_mean_price <- total_sales / total_units
weighted_median_price <- weighted_median(veh_prices_extended$price, veh_prices_extended$units)
This gives three different values: the Kelly Blue Book average of $35,359, a weighted mean price of $37,944, and a weighted median price of $33,230. I can therefore take $35,000 as an approximate typical price for a new vehicle.
How do these prices relate to median household income? In 2018 the median U.S. household income was $61,937, so a $35,000 vehicle would be about 56.5% of the median household income. Put another way, it we adopt the relationship between median household income and the typical vehicle price as a criterion for affordability, buying a $35,000 vehicle would require having an income of 1.77 times that price. This is a looser criterion even than Dave Ramsey’s, which requires having an income more than twice the vehicle purchase price.
For purposes of this analysis I’ll arbitrarily decree that for a vehicle to be affordable its price must not be more than 50% of household income, or (to put it another way) a new vehicle with a given price will require an income of at least twice that price to be affordable. To allow for future tweaking I capture this criterion in the form of a variable required_income_vs price
:
required_income_vs_price <- 2
Before looking at vehicle affordability, I first want to get a sense of how many households are in each of the U.S. Census income ranges. I already computed the percentages of households in each income range, so I can simply graph that data as follows:
pct_by_income
.B19001_001
corresponding to all households, since it will just be 100%.income_ranges
containing text descriptions for each range, using the common variable variable
.h_pcts
.h_pcts
table as a bar chart (using geom_col
) with the x-axis being the various income ranges and the y-axis the percentage of households with incomes in those ranges.h_pcts <- pct_by_income %>%
filter(variable != "B19001_001") %>%
right_join(income_ranges, by = "variable") %>%
replace_na(list(h_pct = 0))
h_pcts %>%
ggplot(mapping = aes(x = income_range, y = h_pct)) +
geom_col() +
xlab("Household Income Range") +
ylab("% of Households in Range") +
scale_y_continuous(limits = c(0, 15), breaks = seq(0, 15, 5)) +
labs(
title = "Percentages of U.S. Households by Income Range in 2018",
caption = paste0(
"Data sources:",
"\n U.S. Census Bureau, 2018 American Community Survey 1-Year Estimates, Table B19001",
"\nCreated using the tidyverse and tidycensus R packages"
)
) +
theme_minimal() +
theme(axis.title.x = element_text(margin = margin(t = 10))) +
theme(axis.title.y = element_text(margin = margin(r = 10))) +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(plot.caption = element_text(margin = margin(t = 15), hjust = 0))
Since the income ranges are not evenly spaced, the above graph is not the same as an income distribution graph. That’s why there are higher percentages of households in the higher income ranges, since those ranges are so wide compared to the income ranges below $50,000.
I now calculate the percentage of new vehicles sold that are affordable to households in each income range, using the affordability criterion above. I do this as follows, creating a new table veh_pcts
:
veh_prices_extended
containing imputed numbers of vehicles sold for each pricepoint for all vehicle models.required_income_vs_price
) to get the income level required to afford a vehicle of that price.assign_to_range()
function to take the resulting income and assign it to one of the Census income ranges.units_per_range
of the number of vehicles sold whose prices require incomes in that range.total_units
was computed above.)income_ranges
table, using the full_join()
function so that the resulting table has rows for all income ranges.veh_pcts <- veh_prices_extended %>%
mutate(income_range = assign_to_range(required_income_vs_price * price)) %>%
mutate(income_range = factor(income_range, levels = hi_ranges)) %>%
group_by(income_range) %>%
summarize(units_per_range = sum(units)) %>%
mutate(veh_pct = 100 * (units_per_range / total_units)) %>%
full_join(income_ranges, by = "income_range") %>%
replace_na(list(veh_pct = 0))
I plot a bar chart of the percentage of cars affordable to each income range, using the table veh_pcts
created above.
veh_pcts %>%
ggplot(mapping = aes(x = income_range, y = veh_pct)) +
geom_col() +
xlab("Household Income Ranges") +
ylab("% of New Vehicles for Range") +
scale_y_continuous(limits = c(0, 30), breaks = seq(0, 30, 5)) +
labs(
title = "Percentage of New Vehicles Targeted to Each Income Range",
subtitle = "(Assumes Required Income of Twice the Vehicle Price)",
caption = paste0(
"Data sources:",
"\n U.S. Census Bureau, 2018 American Community Survey 1-Year Estimates, Table B19001",
"\n GoodCarBadCar.net, 2017 Year End U.S. Vehicle Sales Rankings",
"\n Consumer Reports, 2019 New Car Guide",
"\nCreated using the tidyverse and tidycensus R packages"
)
) +
theme_minimal() +
theme(axis.title.x = element_text(margin = margin(t = 10))) +
theme(axis.title.y = element_text(margin = margin(r = 10))) +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(plot.caption = element_text(margin = margin(t = 15), hjust = 0))
Based on this graph it appears that most new vehicles are priced to sell to households with incomes between $50,000 and $100,000.
I now calculate the penetration rate for new vehicles by income range. The penetration rate is calculated by analogy to the penetration rate for rental apartments, by dividing the percentage of vehicles targeted to a particular income range (based on my affordability criterion) by the percentage of households with incomes that fall within that range.
A penetration rate of 100 percent would indicate that there are equal numbers of vehicles targeted to an income range and households within that range. A penetration rate over 100 percent would indicate an oversupply of vehicles (for that income range), while a penetration rate of less than 100 percent would indicate an inadequate supply of vehicles relative to the number of households in that income range.
I create the table pen_rates
as follows:
h_pcts
table.veh_pcts
tables, using the common column income_range
.pen_rates <- h_pcts %>%
full_join(veh_pcts, by = 'income_range') %>%
mutate(pen_rate = 100 * (veh_pct / h_pct))
Finally I plot a bar chart showing the penetration rate for each household income range.
pen_rates %>%
ggplot(mapping = aes(x = income_range, y = pen_rate)) +
geom_bar(stat = "identity") +
geom_hline(yintercept = 100) +
xlab("Household Income Ranges") +
ylab("Penetration Rate (%)") +
scale_y_continuous(limits = c(0, 300), breaks = seq(0, 300, 50)) +
labs(
title = "Penetration Rate of New Vehicles by Income Range",
subtitle = "Percentage Sold vs. Percentage Affordable to Each Income Range",
caption = paste0(
"Data sources:",
"\n U.S. Census Bureau, 2018 American Community Survey 1-Year Estimates, Table B19001",
"\n GoodCarBadCar.net, 2017 Year End U.S. Vehicle Sales Rankings",
"\n Consumer Reports, 2019 New Car Guide",
"\nCreated using the tidyverse and tidycensus R packages"
)
) +
theme_minimal() +
theme(axis.title.x = element_text(margin = margin(t = 10))) +
theme(axis.title.y = element_text(margin = margin(r = 10))) +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
theme(plot.caption = element_text(margin = margin(t = 15), hjust = 0))
There are several points worth making about this graph. First, the details of the graph are obviously dependent on the assumptions made about vehicle affordability. If the criterion for affordability, i.e., a vehicle price no more than 50% of household income, were changed then the penetration rates for the various income groups would also change. However the overall shape of the graph should remain the same, with under-penetration in the lowest and highest income ranges, and over-penetration in the middle income ranges.
This general shape can be explained as follows:
In the higher-income ranges there are fewer vehicles sold than one might expect. For example, a household in the income range “$200,000 or more” should be able to afford a vehicle costing $100,000 or more, but relatively few such vehicles are sold (about a 25% penetration rate). This is probably due to two factors:
In the lower-income ranges there are also fewer vehicles sold than one might expect. Again this is likely due to two factors:
The flip-side of under-penetration in the lowest and highest household income ranges is over-penetration in the middle income ranges. This is easily explained by lower-income households buying vehicles more expensive than they could afford by my criterion and by higher-income households buying vehicles less expensive than they could afford by this criterion, for reasons discussed above.
Values from the American Community Survey are estimates based on survey samples, with associated margins of error. However at the national level the margins of error are relatively small even for ACS 1-year estimates.
To obtain the 2017 ACS data I used the tidycensus package, which provides an easy-to-use interface to U.S. Census Bureau data made available via a set of public APIs. As its name suggests, the tidycensus package is designed to be compatible with the tidyverse approach to representing and manipulating data.
I obtained data for 2019 new car prices from the Consumer Reports guide to cars. (Consumer Reports does not apparently store archived versions of the guide for previous years, and there are no usable copies archived by the Wayback Machine.) Consumer Reports typically lists a range of prices for each vehicle. In those cases I assume that sales of a particular model were distributed between the minimum and maximum price in a 1-2-4-2-1 distribution, as discussed above.
I obtained data for the number of vehicles sold from “2017 Year End U.S. Vehicle Sales Rankings – Top 296 Best-Selling Vehicles In America – Every Vehicle Ranked” on the goodcarbadcar.net website.
I obtained the average sales price for new vehicles in July 2018 from the press release “Demand Quickly Backing Away from Cars, Pushing Average New-Car Transaction Prices Up for July 2018, According to Kelley Blue Book”.
The value for the average number of cars per household is from Table 1 of the University of Michigan Sustainable Worldwide Transportation Report No. SWT-2018-2, “Has Motorization in the U.S. Peaked? Part 10: Vehicle Ownership and Distance Driven, 1984 TO 2016”.
Recommendations for how much to spend on a vehicle are from the following sources:
The code for the weighted_median()
function is taken from the code for the weighted.median()
function in the spatstat package, as discussed in the StackOverflow question “Is there a weighted.median() function?”
It would be nice to have data on household income, vehicle sales by model, and vehicle prices all from the same year.
I used the following R environment in doing the analysis above:
sessionInfo()
## R version 3.6.0 (2019-04-26)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 16.04.6 LTS
##
## Matrix products: default
## BLAS: /usr/lib/atlas-base/atlas/libblas.so.3.0
## LAPACK: /usr/lib/atlas-base/atlas/liblapack.so.3.0
##
## locale:
## [1] LC_CTYPE=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
## [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
## [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
## [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.24 tidycensus_0.9.2 forcats_0.4.0 stringr_1.4.0
## [5] dplyr_0.8.3 purrr_0.3.2 readr_1.3.1 tidyr_0.8.3
## [9] tibble_2.1.3 ggplot2_3.2.1 tidyverse_1.2.1
##
## loaded via a namespace (and not attached):
## [1] tidyselect_0.2.5 xfun_0.8 sf_0.7-7
## [4] haven_2.1.1 lattice_0.20-38 colorspace_1.4-1
## [7] generics_0.0.2 vctrs_0.2.0 htmltools_0.3.6
## [10] yaml_2.2.0 rlang_0.4.0 e1071_1.7-2
## [13] pillar_1.4.2 foreign_0.8-71 glue_1.3.1
## [16] withr_2.1.2 DBI_1.0.0 rappdirs_0.3.1
## [19] sp_1.3-1 uuid_0.1-2 modelr_0.1.5
## [22] readxl_1.3.1 munsell_0.5.0 gtable_0.3.0
## [25] cellranger_1.1.0 rvest_0.3.4 tigris_0.8.2
## [28] evaluate_0.14 maptools_0.9-5 curl_4.0
## [31] class_7.3-15 broom_0.5.2 Rcpp_1.0.2
## [34] KernSmooth_2.23-15 scales_1.0.0 backports_1.1.4
## [37] classInt_0.4-1 jsonlite_1.6 hms_0.5.0
## [40] digest_0.6.20 stringi_1.4.3 grid_3.6.0
## [43] rgdal_1.4-4 cli_1.1.0 tools_3.6.0
## [46] magrittr_1.5 lazyeval_0.2.2 crayon_1.3.4
## [49] pkgconfig_2.0.2 zeallot_0.1.0 xml2_1.2.2
## [52] lubridate_1.7.4 assertthat_0.2.1 rmarkdown_1.14
## [55] httr_1.4.1 rstudioapi_0.10 R6_2.4.0
## [58] units_0.6-3 nlme_3.1-139 compiler_3.6.0
You can find the source code for this analysis and others at my hocodata public code repository. This document and its source code are available for unrestricted use, distribution and modification under the terms of the Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. Stated more simply, you’re free to do whatever you’d like with it.