5 min read

Metro Area Migration Trends With 2019 Census Data

It’s the most wonderful time of the year. The Census releases their 5-Year ACS estimates each time around this year, revealing trends afoot in our most wonderful United States of America. These estimates are broader (smaller areas covered) and are more stable estimates. Here’s the official release announcement.

First question to answer, and the subject of this blog post. Which metro areas have seen their populations burgeon as a result of out of state/out of country new movers?

Well, without further ado, here’s your answer!

Shocking to think that nearly 1 in 12 Colorado Springians is fresh off the proverbial boat (well, I guess they could have driven…).

This begs a natural question, one which the delights of tidycensus make us well prepared to answer. What would a map show?

Alas! The answer:

Major trends to observe? Hopefully the map makes trends abundantly clear!

  1. California, NYC, much of rural America and the Midwest are not attracting out of state movers
  2. Nevada, Arizona, Wyoming, Colorado, Florida and North Dakota saw lots of out of state movers in 2019

A few notes here needed on methodology. Unlike our MSA table, this map shows counties (make the mapping easier). To generate the colors, the county-level share of out of state movers were sorted into 20 buckets and colored accordingly.

Perhaps more to come on this data in the future? Thanks for reading!! Code below for the curious.

library(tidycensus)
library(tidyverse)
library(extrafont)
library(sf)
library(usmap)
library(ggtext)

# set theme

theme_set(
  theme_minimal(base_family = "Gill Sans MT") +
    theme(axis.line = element_line(color = "black"),
          axis.text = element_text(color = "black"),
          axis.title = element_text(color = "black"),
          panel.grid.minor = element_blank(),
          plot.caption = element_text(hjust = 0)
    )
)

# set census key & tigris options

census_api_key(Sys.getenv("CENSUS_API_KEY"))
options(tigris_use_cache = TRUE)

# get variables 

lookup_vars <- load_variables(year = 2019, dataset = "acs1", cache = TRUE)

# filter my relevant data

pop_vars <- lookup_vars %>% 
  filter(str_detect(name, "B07001")) %>% 
  mutate(label = str_remove_all(label, "Estimate!!Total!!")) %>% 
  select(variable = name, demographic = label)

# get population info

years_of_int <- 2012:2019

acs_import <- map_df(
  years_of_int, 
  ~ get_acs(
    geography = "metropolitan statistical area/micropolitan statistical area",
    table = "B07001",
    year = .x,
    survey = "acs5",
    cache_table = TRUE
  ), 
  .id = "year"
) %>% 
  mutate(year = min(years_of_int) - 1 + as.numeric(year)) %>% 
  filter(!is.na(estimate))

# datasets for geometry

geo_data <- get_acs(
  geography = "county",
  variable = c("B07001_001", "B07001_065", "B07001_081"),
  year = 2019,
  geometry = TRUE,
  shift_geo = TRUE
) 

state_geo <- get_acs(
  geography = "state",
  variable = "B07001_001",
  year = 2019,
  geometry = TRUE,
  shift_geo = TRUE
) 

# clean population info

clean_pop <- acs_import %>%
  select_all(tolower) %>% 
  select(-geoid) %>% 
  left_join(popVars, by = "variable") %>% 
  mutate(
    variable = str_extract(demographic, "(?<=:!!).+?(?=:)"),
    variable = if_else(is.na(variable), "Total", variable),
    age = str_extract(demographic, "(?<=:!!)\\d+.*"),
    age = if_else(is.na(age), "Total", age),
    name = ifelse(name %in% "Texarkana, TX-Texarkana, AR Metro Area", "Texarkana, TX-AR Metro Area", name),
    state = str_extract(name, "(?<=, ).+?(?= M)"),
    msa = gsub(" (Micro|Metro) Area", "", name, perl = T)
  ) %>% 
  select(-demographic)


# top 20 for out of state migration in 2019

chg_2019 <- clean_pop %>% 
  filter(
    variable %in% c("Total", "Moved from different state", "Moved from abroad"),
    year %in% 2019,
    age == "Total"
    ) %>% 
  mutate(variable = if_else(variable == "Total", variable, "New Mover")) %>% 
  group_by(msa, variable) %>% 
  summarize(name = unique(name),
            estimate = sum(estimate),
            .groups = "drop") %>% 
  pivot_wider(names_from = "variable", values_from = "estimate") %>% 
  mutate(pct_total = `New Mover` / Total) %>% 
  select(name, everything())

hi_lo_msa <- chg_2019 %>% 
  filter(Total > 500000) %>% 
  arrange(-pct_total) %>% 
  mutate(top_bottom = case_when(
    row_number() %in% 1:20 ~ "top",
    row_number() %in% (nrow(.) - 19):nrow(.) ~ "bottom",
    TRUE ~ NA_character_
    )
  ) %>% 
  filter(!is.na(top_bottom))

ggplot(hi_lo_msa %>% filter(top_bottom == "top"), aes(fct_reorder(msa, pct_total), pct_total)) +
  geom_col(fill = "#9aacbc", width = 0.75, color = "white") +
  geom_text(aes(label = scales::percent(pct_total, accuracy = 0.1)),
            hjust = 1.1,
            family = "Gill Sans MT",
            fontface = "bold",
            color = "white") +
  coord_flip() +
  scale_y_continuous(expand = c(0, 0, 0.1, 0), labels = scales::percent) +
  labs(x = "",
       y = "% of Total Population",
       title = "Where Are Out Of State Movers Moving? Top 20 New Mover MSAs* in 2019",
       subtitle = "Out of State/Country New Movers as a % of Total MSA Population",
       caption  = "verbumdata.netlify.com\nSources: U.S. Census Bureau 5-Year ACS 2019\n*Includes MSAs with > 0.5MM People") +
  theme(plot.caption.position = "plot",
        plot.title.position = "plot")

# now mapping

geo_data_chg <- geo_data %>% 
  as_tibble() %>% 
  select(NAME, variable, estimate) %>% 
  mutate(variable = if_else(variable == "B07001_001", "Total", "New Mover")) %>% 
  group_by(name = NAME, variable) %>% 
  summarize(name = unique(name),
            estimate = sum(estimate),
            .groups = "drop") %>% 
  pivot_wider(names_from = "variable", values_from = "estimate") %>% 
  mutate(pct_total = `New Mover` / Total) %>% 
  select(name, everything())

geo_data_full <- geo_data %>% 
  left_join(geo_data_chg %>% select(NAME = name, pct_total), by = "NAME") %>% 
  mutate(pct_mover_tile = ntile(pct_total, 20))

# map output

ggplot() +
  geom_sf(data = geo_data_full, aes(fill = pct_mover_tile), color = NA, show.legend = FALSE) +
  geom_sf(data = state_geo, aes(geometry = geometry), fill = NA, color = "white") +
  coord_sf() +
  scale_fill_viridis_c() +
  labs(title = "Where Are Out Of State Movers Moving?",
       subtitle = paste0(
         "<span style = 'color:#fde725'>**Lighter Colors**</span>",
         " Show Higher % Out of State Movers in 2019"
         ),
       caption  = "verbumdata.netlify.com\nSources: U.S. Census Bureau 5-Year ACS 2019"
       ) +
  theme_void(base_family = "Gill Sans MT") +
  theme(plot.title = element_text(hjust = 0.08, face = "bold"),
        plot.subtitle = element_markdown(hjust = 0.1),
        plot.caption = element_text(hjust = 0.01))