The politics of New Mexico: a brief historical-visual account

In this post, we piece together a brief political history of New Mexico using a host of data sources, including Wikipedia, the US Census, the New Mexico State Legislature (NMSL) & VoteView. A bit of a show/tell post, and one that piggybacks some on a guide I have developed for working with US political data using R.

In the process, we demonstrate some methods for accessing/cleaning online data sets made available in a variety formats. Many of these data (especially those made available by the NMSL) have not really seen the light of day; so, we let these data breathe some. Fully reproducible. Open methods. Open government.

if (!require("pacman")) install.packages("pacman")
pacman::p_load(tidyverse, tigris, nmelectiondatr) 

#devtools::install_github("jaytimm/nmelectiondatr")
options(tigris_use_cache = TRUE, tigris_class = "sf")

New Mexico demographics via tidycensus

We first take a quick look at some socio-demographic indicators in New Mexico (relative to other states in the Union) using the tidycensus package. The violin plots below summarize percentages of the population that are Hispanic (Per_Hispanic), White (Per_White), living below the poverty line (Per_BPL), have a Bachelor’s degree or higher (Per_Bachelors_Plus), and have a high school degree or higher (Per_HS_Plus). Also included are median household incomes (Median_HH_Income).

vars <- c(Per_Hispanic = 'DP05_0071P',
          Per_Bachelors_Plus = 'DP02_0067P', 
          Per_BPL = 'DP03_0128P', 
          Per_White = 'DP05_0077P',
          Per_HS_Plus = 'DP02_0066P',
          Median_HH_Income = 'DP03_0062')

m90 <- tidycensus::get_acs(geography = "state", 
                           variables = vars, 
                           year = 2017,
                           output = "tidy", 
                           survey = 'acs1') %>%
  filter(!GEOID %in% c('11', '72'))

So, New Mexico is browner, less financially heeled, and less educated relative to other states in the USA. A very simple overview of a fairly complicated state.

labs <- m90 %>%
  filter(NAME == 'New Mexico')

m90 %>%
  ggplot(aes(x =1, y = estimate)) +
  geom_violin()+
  geom_point() +
  ggrepel::geom_label_repel(data = labs, 
                           aes(x = 1, y = estimate, 
                               label = 'NM'),
            color = 'steelblue',
            nudge_x = .01)+
  theme_minimal()+
  theme(axis.title.x=element_blank(),
        axis.text.x=element_blank(),
        legend.position = "none") +
  facet_wrap(~variable, scales = 'free') +
  labs(title="Some socio-demographics", 
       caption = 'American Community Survey, 1-Year Estimates, 2017.') 

2016 Presidential Election

To get our bearings, we briefly consider how New Mexico voted in the 2016 presidential election. While Hillary Clinton carried the state by roughly eight points, here we investigate election results at the precinct level. My nmelectiondatr package (available on GitHub) makes available election returns in New Mexico for state & federal elections from 2014 - 2018. These data live on the New Mexico Legislature website as fairly inaccessible spreadsheets. I have cleaned things up some, and collated returns as simple data tables. For non-R users, data also live as simple csv/excel files.

Here, we access precinct-level returns for the 2016 presidential election.

precincts_raw <- #nmelectiondatr::nmel_pol_geos$nm_precincts %>%
  nmelectiondatr::nmel_results_precinct %>%
               filter(Type == 'President and Vice President of the United States' ) %>% #& 
  group_by(County_Name, Precinct_Num) %>%
  mutate(per = round(Votes/sum(Votes), 3)) %>%
  select(County_Name, Precinct_Num, Party, per) %>%
  filter(Party %in% c('DEM', 'REP')) %>%
  spread(Party, per) %>%
  mutate(Trump_Margin = REP - DEM)
base <- nmelectiondatr::nmel_pol_geos$nm_precincts %>%
  inner_join(precincts_raw) %>%
  ggplot() + 
  geom_sf(aes(fill = cut_width(Trump_Margin, 0.2)),
           color = 'darkgray') +
  scale_fill_brewer(palette = 'RdBu', direction = -1, name = 'Margin')

The map below summarizes Trump vote margins by precinct in New Mexico. So, an airplane-red state, much like the country as a whole, with larger, more rural precincts dominating the map.

base +
  ggsflabel::geom_sf_text_repel(data = nmel_pol_geos$nm_places %>% 
                                filter (LSAD == '25'),
                                aes(label = NAME), size = 2.5)  +
  theme_minimal() +
  theme(axis.title.x=element_blank(),
        axis.text.x=element_blank(),
        axis.title.y=element_blank(),
        axis.text.y=element_blank(),
        legend.position = 'right') +
  labs(title = "2016 Trump margins by precinct")

When we zoom in some to New Mexico’s four largest metro areas, the state becomes a bit blue-er. Rio Rancho is the friendliest to 45 – where Trump held a rally in mid-September.

Presidential elections in New Mexico historically

A blue state in 2016, next we consider how New Mexico has voted in presidential elections since its statehood in 1912. We first grab a simple list of US presidents and their party affiliations via Git Hub.

url1 <- 'https://gist.githubusercontent.com/namuol/2657233/raw/74135b2637e624848c163759be9cd14ae33f5153/presidents.csv'

us_pres <- read.csv(url(url1)) %>%
  #select(Year, Party) %>%
  mutate(Party = trimws(Party),
         Party = gsub('/.*$', '', Party),
         year = as.numeric(gsub('^.*/', '', Took.office))-1,
         President = gsub(' \\(.*$', '', President)) %>%
  select(year, President, Party) %>%
  mutate(Party = gsub('Democratic', 'Democrat', Party)) %>%
  bind_rows(data.frame(year = 2016,
                       President = 'Donald Trump',
                       Party = 'Republican'))

Then we access New Mexico’s presidential election voting history via Wikipedia.

url <- 'https://en.wikipedia.org/wiki/United_States_presidential_elections_in_New_Mexico'

nm_returns <- url %>%
  xml2::read_html() %>%
  rvest::html_node(xpath = '//*[@id="mw-content-text"]/div/table[2]') %>%
  rvest::html_table(fill = TRUE)

nm_returns <- nm_returns[,c(1:2, 4:5, 7)]
colnames(nm_returns) <- c('year', 'winner', 'winner_per', 'loser', 'loser_per')

nm_returns1 <- nm_returns %>%
  mutate(winner = gsub('\\[.*\\]', '', winner),
         year = gsub('\\[.*\\]', '', year)) %>%
  filter(year < 2020) %>%
  mutate(winner_per = as.numeric(winner_per),
         loser_per = as.numeric(loser_per),
         year = as.numeric(year),
         state_winner = ifelse(winner_per < loser_per, loser, winner)) %>%
  
  mutate(other = round(100 - winner_per - loser_per, 2)) %>%
  left_join(us_pres %>% select(-year), by = c('winner' = 'President')) 

wins <- nm_returns1 %>%
  select(year, state_winner, Party, winner_per)%>%
  rename(per = winner_per)

loss <- nm_returns1 %>%
  select(year, state_winner, Party, loser_per)%>%
  mutate(Party = ifelse(Party == 'Democrat', 'Republican', 'Democrat')) %>%
  rename(per = loser_per)

others <- nm_returns1 %>%
  select(year, state_winner, Party, other)%>%
  rename(per = other)%>%
  mutate(Party = 'Other')

new <- bind_rows(wins, loss, others)
new$Party <- factor(new$Party, levels = c('Other', 'Democrat', 'Republican')) 

Based on these data, the plot below summarizes historical election results by party affiliation. Labeled are the candidates that won New Mexico. The gray portions of the plot reflect vote shares for “other”/ non-predominant political parties.

flip_dets <- c('Other', 'Democrat', 'Republican')
flip_pal <- c('#b0bcc1', '#395f81', '#9e5055')
names(flip_pal) <- flip_dets

dems <- new %>%
  group_by(year) %>%
  filter(per == max(per)) %>%
  filter(Party == 'Democrat')

pres_labels <- new %>%
  group_by(year) %>%
  filter(Party == 'Democrat') %>%
  mutate(percent1 = ifelse(state_winner %in% dems$state_winner,
                           per + 7, per - 7),
         state_winner = toupper(sub('^.* ', '', state_winner)))

new %>%
  ggplot(aes(x = year, y = per, fill = Party))+
  geom_bar(alpha = 0.85, color = 'white', stat = 'identity') +
  annotate(geom="text", 
           x = pres_labels$year, 
           y = pres_labels$percent1, 
           label = pres_labels$state_winner,
           size = 3, angle = 90, color = 'white')+
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))+
  #geom_hline(yintercept = 50, color = 'white', linetype = 2) +
  theme(legend.position = "none")+
  #guides(fill = guide_legend(reverse=TRUE))+
  scale_fill_manual(values = flip_pal) +
  #ggthemes::scale_fill_stata()+
  scale_x_continuous(breaks=seq(1912,2016,4)) + xlab('') +
  ggtitle('Presidential election results in New Mexico')

Margins historically

For a slightly different perspective, we consider Republican-Democrat vote margins historically. As the plot below attests, a state that swings quite a bit. However, more recently having settled some as blue post-Bush v2.

new %>%
  select(-state_winner) %>%
  spread(Party, per) %>%
  mutate(margin = Republican - Democrat,
         Party = ifelse(margin > 0, 'Republican', 'Democrat')) %>%
  
  ggplot(aes(x=year, y=margin, fill = Party))+
  geom_bar(alpha = 0.85, color = 'white', stat = 'identity') +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))+
  theme(legend.position = "none")+
  ggthemes::scale_fill_stata()+
  scale_x_continuous(breaks=seq(1912,2016,4)) + xlab('') +
  ggtitle('Presidential vote margins in New Mexico since 1912')

Congressional delegation historically

Next, we consider the composition of New Mexico’s congressional delegation historically. Here we access data made available via the VoteView project and the R package Rvoteview.

House of Representatives

The table below details the names & party affiliations of the (3) representatives New Mexico has sent to Washington over the last 15 congresses. District 3, which is comprised of the northern half of the state and includes the Santa Fe metro, has generally gone blue during this time period. District 1 (the ABQ metro area) has flipped from red to blue since the election of Obama in 2008. District 2 (the southern half of the sate) has been a GOP stronghold, with the 111th and 116th congresses being exceptions.

js <- "(/Rep/).test(value) ? '#cf7d7d' : (/Dem/).test(value) ? '#8faabe' : ''"

dat <- Rvoteview:: member_search(chamber= 'House', 
                                 state = 'NM', 
                                 congress = 102:116) %>%
  mutate(bioname = gsub(',.*$', '', bioname),
         bioname = ifelse(party_name == 'Democratic Party',
                          paste0(bioname, ' (Dem)'),
                          paste0(bioname, ' (Rep)'))) %>%
  select(congress, bioname, district_code) %>%
  group_by(congress, district_code)%>%
  slice(1) %>%
  ungroup() %>%
  spread(district_code, bioname)

dat %>%
  DT::datatable(rownames = FALSE,
                options =  list(pageLength = 15, 
                                columnDefs = list(list(className = 'dt-center', targets = 0:3)),
                                dom = 't')) %>% 
  DT::formatStyle(1:ncol(dat), backgroundColor = htmlwidgets::JS(js))

So, 2018 (the 116th) was only the second time in the last thirty years that New Mexico elected an all-Democrat delegation to the House. See this post for some thoughts on how Torres Small carried New Mexico’s second district in 2018.

US Senate

Next we consider the political affiliations & ideologies of US Senators from New Mexico since 1947. I have discussed VoteView’s political ideology scores in previous posts (eg), and have also demonstrated their derivation using roll call data from New Mexico’s 53rd State Legislature as an example.

Here we utilize Nokken-Poole political ideology scores, which are congress-specific scores. These data are not available via the Rvoteview package; instead, we download these scores directly from the VoteView website.

voteview_nokken_poole <- 
  read.csv(url("https://voteview.com/static/data/out/members/HSall_members.csv"),
           stringsAsFactors = FALSE) 

base1 <- voteview_nokken_poole %>%
  filter(!is.na(nokken_poole_dim1),  
    chamber == 'Senate',
           party_code %in% c('100','200')&
           congress > 79) 

nm <- base1 %>%
  filter(state_abbrev == 'NM') 
  
nm_labels <- nm %>%
  group_by(bioname) %>%
  filter(congress == max(congress)) %>%
  ungroup() %>%
  mutate(bioname =  gsub(',.*$', '', bioname)) 

Below, the names and political ideology scores (first dimension) of Senators from New Mexico are presented relative to the median ideology for each major party historically. So, a history of fairly moderate representation in the Senate – dominated until more recently by the split delegation of Domenici (R) and Bingaman (D), both of whom voted center of their respective party medians. Udall (D) and Heinrich (D) may be drifting left, but this would reflect the state’s shifting ideology in general.

base2 <- base1 %>%  
  group_by(congress, party_code) %>%
  summarize(med = median(nokken_poole_dim1)) %>%
  ungroup() %>%
  ggplot() +
  
  geom_line(aes(x = congress, y= med, color = as.factor(party_code)),
            size = 1.25) +
  ylim(-.5, .5) +
  theme_minimal()+
  ggthemes::scale_color_stata() +
  theme(legend.position = 'none') +
  labs(title="Median ideologies for major parties: Houses 80 to 116") 

base2 +
  geom_line(data = nm, 
            aes(x = congress, y= nokken_poole_dim1, color = as.factor(bioname)),
            linetype = 2) +
  geom_text(data = nm_labels, 
            aes(label = bioname,
                x = congress, y =nokken_poole_dim1),
            size = 3)

New Mexico State Government

Finally, we consider the composition of New Mexico’s state government historically, namely the governorship and the bicameral house.

Governors historically

Here, we investigate the party affiliation of New Mexico’s governors since its statehood in 1912. These data are made available as a PDF from the New Mexico State Legislature website.

url <- 'https://www.nmlegis.gov/Publications/Handbook/leadership_since_statehood_17.pdf'
tmp <- tempfile()
curl::curl_download(url, tmp)
tab <- tabulizer::extract_tables(tmp, output = "data.frame") 

xx <- c('year', 'speaker', 'pro_tem', 'governor', 'president')

tab1 <- lapply(tab, function(x) {
  colnames(x) <- xx
  return(x) }) %>%
  bind_rows() %>%
  mutate(governor = gsub('\\(died\\)|\\(resigned\\)', NA, governor),
         president = gsub('\\(died\\)|\\(resigned\\)', NA, president),
         president = gsub('^$', NA, president)) %>%
  tidyr::fill(governor, .direction = 'up') %>%
  tidyr::fill(president, .direction = 'up') %>%
  filter(!is.na(year)) %>%
  mutate(gov_party = gsub('^.*\\(([A-Z])\\)', '\\1', governor),
         pres_party = gsub('^.*\\(([A-Z])\\)', '\\1', president),
         governor = gsub('\\(.\\)', '', governor),
         president = gsub('\\(.\\)', '', president)) %>%
  select(-speaker, -pro_tem)

#Tabulizer is not perfect.  PDF is not up-to-date.
hand_edits <- data.frame (year = c(1912, 1951:1953, 2000, 2018:2019),
                          governor = c('McDonald', 'Horn', 'Horn', 'Stockton', 
                                       'Sanchez', 'Martinez', 'Lujan Grisham'),
                          president = c('Wilson', 'Truman', 'Truman', 'Eisenhower', 
                                        'Clinton', 'Trump, D.', 'Trump, D.'),
                          gov_party = c('D', 'D', 'D', 'R', 'D', 'R', 'D'),
                          pres_party = c('D', 'D', 'D', 'R', 'D', 'R', 'R'))
tab1 <- tab1 %>% bind_rows(hand_edits) %>% arrange(year)

After some cleaning, a sample of our data set is presented below. Included are the names of sitting US Presidents and their political affiliation.

year governor president gov_party pres_party
1912 McDonald Wilson D D
1913 McDonald Wilson D D
1914 McDonald Wilson D D
1915 McDonald Wilson D D
1916 McDonald Wilson D D
1917 C de Baca Wilson D D

The table below summarizes the total number of years (since 1912) that each party has held the governor’s office, cross-tabbed with the political affiliation of the US President during the same time period. First to note is that Democrats have held gubernatorial control in 70/108 years.

Second to note is that in 59 (39 + 20) of those years the New Mexico governor shared party affiliation with the sitting US President; in 49 (18 + 31) of those years, the two were divided. Roughly a 50-50 split historically, which is pretty interesting.

table(tab1$gov_party, tab1$pres_party) %>% 
  data.frame() %>% 
  rename(Gov_Party = Var1, Pres_Party = Var2) %>%
  spread(Pres_Party, Freq) %>% knitr::kable()%>%
  kableExtra::kable_styling("striped", full_width = F) %>%
  kableExtra::add_header_above(c(" " = 1, "Pres_Party" = 2))
Pres_Party
Gov_Party D R
D 39 31
R 18 20

In rank order by total years, then:

  • [Dem Gov/Dem Pres (39)] > [Dem Gov/Rep Pres (31)] > [Rep Gov/Rep Pres (20)] > [Rep Gov/Dem Pres (18)]

The plot below illustrates the political affiliation of New Mexico governors and US presidents since statehood in 1912. Lots of back and forth for sure. It would seem that New Mexicans hedge their bets when it comes to gubernatorial elections, tempering federal leadership with state leadership from the opposing party. With the exception of the ~FDR years.

tab1 %>%
  mutate(gov_val = ifelse(gov_party == 'D', .75, -.75),
         pres_val = ifelse(pres_party == 'D', 1, -1)) %>%

  ggplot() +
  geom_line(aes(x = year, y = gov_val), size = 1.25, color = '#b0bcc1') +
  geom_line(aes(x = year, y = pres_val), size = 1.25, color = '#55752f') +
  ylim(-1.25, 1.25) +
  theme_minimal()+
  annotate("text", x = 1920, y = 1.25, label = "DEMOCRAT") +
  annotate("text", x = 1920, y = -1.25, label = "REPUBLICAN") +
  annotate("text", x = 1914, y = 1.05, label = "President") +
  annotate("text", x = 1914, y = .8, label = "Governor") +
  theme(legend.position = 'none', 
        axis.title.y=element_blank(),
        axis.text.y=element_blank(),
        axis.text.x = element_text(angle = 90, hjust = 1)) +
  scale_x_continuous(breaks=seq(1912,2018,4)) +
  labs(title="Presidential & Gubernatorial Party Affiliation by Year") 

State legislature composition historically

LASTLY, we investigate the party-based composition of the New Mexico state houses historically. Again, we access this data via a PDF made available at the New Mexico State Legislature website.

url_state <- 'https://www.nmlegis.gov/Publications/Handbook/political_control_17.pdf'
tmp <- tempfile()
curl::curl_download(url_state, tmp)
tab <- tabulizer::extract_tables(tmp, 
                          output = "data.frame", 
                          encoding = 'UTF-8')

current <- data.frame(year = c(2019, 2019),
                      count = c(26,16, 46, 24),
                      house = c('senate', 'senate', 'house', 'house'),
                      party = c('dem', 'rep', 'dem', 'rep'))

xx <- c('year', 'house', 'house_dem', 'house_rep', 'house_other', 
        'senate_dem', 'senate_rep', 'senate_other')

tab2 <- lapply(tab, function(x) {
  x <- x[, c(1:4,6, 8:9, 11)]
  colnames(x) <- xx
  x$house_other <- as.numeric(x$house_other)
  x$senate_other <- as.numeric(x$senate_other)
  return(x) }) %>%
  bind_rows() %>%
  filter(year %% 2 == 1 | house == '31st,2nd') %>%
  filter(!grepl('SS', house)) %>%
  mutate(house = gsub(',.*$', '', house)) %>%
  gather(key = 'type', value = 'count', -year, -house) %>%
  separate(type, into = c('house', 'party'), sep = '_') %>%
  mutate(count = ifelse(is.na(count), 0, count)) %>%
  bind_rows(current) %>%
  group_by(year, house) %>%
  mutate(per = round(count/sum(count),2)) %>%
  ungroup()

tab2$party <- factor(tab2$party, levels = c('other', 'dem', 'rep')) 

Per plot below, then, a post-Depression era stronghold for Democrats, with a couple of exceptions – most recently in the 52nd House (which took office in 2015). A bit of a different story relative to the state’s swingy-er tendancies in other offices considered here.

flip_dets <- c('other', 'dem', 'rep')
flip_pal <- c('#b0bcc1', '#395f81', '#9e5055')
names(flip_pal) <- flip_dets

tab2 %>%
  ggplot(aes(x=year, y=per, fill = party))+
  geom_area(alpha = 0.85, color = 'white', stat = 'identity') +
  geom_hline(yintercept = .50, color = 'white', linetype = 2) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))+
  theme(legend.position = "none")+
  scale_fill_manual(values = flip_pal) +
  scale_x_continuous(breaks=seq(1913, 2019, 8)) + xlab('') +
  ggtitle('Composition of New Mexico state houses since statehood') +
  facet_wrap(~house)

Summary

At present, then, New Mexico is a blue state. While Trump rallied in Rio Rancho in September in hopes of capturing the state in 2020, New Mexico has (seemed to have) lost some of the SWING that has defined the state through much of its history. || The state supported Clinton in 2016, sends two Democrats to the Senate, 3/3 Democrats to the House, and has a Democratic state government trifecta. These things are fluid for sure, but the state’s demographics continue to move the state’s political ideology leftwards. So, we’ll see.

Share