new mexico's 53rd state legislature

In this post, we introduce a new R data package, nmlegisdatr, that makes available roll call data for New Mexico’s 53rd (2017-18) State Legislature (NMSL53). While these data are publicly available via, they are wrapped up in thousands of PDFs and, hence, largely inaccessible.

I have scraped roll calls and legislation details from these PDFs, and tidied things up some as a collection of data frames1; a full code-through of how the package was built is available here. The package can be downloaded here.

In this post, then, we demonstrate the contents/structure of the package, as well as its utility as an analytical tool. Ultimately, the goal is to make voting histories available (in an actionable format) for interested folks in New Mexico, as general elections for all 70 House seats are in November. This is the first in a series of posts investigating NMSL53.

library(nmlegisdatr)# devtools::install_github("jaytimm/nmlegisdatr")
library(tidycensus); options(tigris_use_cache = TRUE, tigris_class = "sf")

Package descriptives

The table below details the different data frames included in the nmlegisdatr package. Column names are consistent across tables, facilitating easy joining. We will demonstrate their specific contents as we go.

Table_Name Description
nml_legislation All introduced legislation, including bill id, title, and bill description
nml_legislators All legislators in both chambers, including party affiliation and district id
nml_rollcall Roll calls for all legislation reaching either chamber for vote
nml_sponsors Sponsors for each bill
nml_rollcall_results Summary roll call results
nml_legislator_descs Summary legislator voting patterns and attendance

NMSL53: an overview

We first consider some high level characteristics of NMSL53. Based on the nml_legislators data frame included in nmlegisdatr, the table below summarizes chamber composition by political affiliation. As can be noted, a slight majority in the House for Democrats, and a more sizable one in the Senate. So, unified party control in New Mexico’s legislative branch. (See postscript for a code-through of visualizing congressional compositions.)

nmlegisdatr::nml_legislators %>%
  filter(Representative != 'LT. GOV') %>%
  group_by(Chamber, Party) %>%
  summarize(Count = n()) %>%
  ungroup() %>%
  spread(Party, Count) %>%
  mutate(Per_Dem = round(Dem/sum(Dem,Rep)*100,1)) %>%
Chamber Dem Rep Per_Dem
House 38 32 54.3
Senate 26 16 61.9

Roll call results are summarized in the nml_rollcall_results table. Counts of legislation reaching a vote in NMSL53 are summarized below by chamber and session/year. The first session (2017) is two months in duration while the second session (2018) is only one month in duration.

nmlegisdatr::nml_rollcall_results %>%
  left_join(nml_legislation) %>%
  group_by(Chamber, Session, Year) %>%
  summarise(rollcalls = n())%>%
## Joining, by = "Bill_Code"
Chamber Session Year rollcalls
House Regular 2017 509
House Regular 2018 289
House Special 2017 5
Senate Regular 2017 490
Senate Regular 2018 233
Senate Special 2017 5

A sample of the nml_rollcall_results table is presented below. As can be noted, the table breaks down each vote by party affiliation. It also summarizes how each party voted in the aggregate. Examples below are for House votes only.

nmlegisdatr::nml_rollcall_results %>% filter(Chamber == 'House') %>% 
  select(-Chamber, -Dem_Vote, -Rep_Vote, -Roll_URL, -Motion) %>% 
  head() %>%
Bill_Code Result Dem: Yea Rep: Yea Dem: Nay Rep: Nay
R17_HB0001 65-0 35 30 0 0
R17_HB0002 37-32 37 0 0 32
R17_HB0004 40-26 36 4 1 25
R17_HB0005 37-29 37 0 0 29
R17_HB0008 60-0 33 27 0 0
R17_HB0009 62-0 33 29 0 0

Based on this table, we can get a sense of the degree of bi-partisanship in both houses. Here, we classify each roll call as one of the following:

  • Full consensus: All yea’s
  • Bi-partisan: Over 50% yea’s in both parties
  • Party line: Over 90% yea’s in one party and over 90% nay’s in the other
  • Competitive: All other roll calls
votes <- nmlegisdatr::nml_rollcall_results %>%
  mutate(dem_den = `Dem: Yea`+`Dem: Nay`,
         rep_den = `Rep: Yea`+`Rep: Nay`) %>%
  mutate(`Dem: Yea`= `Dem: Yea`/dem_den,
         `Dem: Nay`= `Dem: Nay`/dem_den,
         `Rep: Yea`= `Rep: Yea`/rep_den,
         `Rep: Nay`= `Rep: Nay`/rep_den) %>%
  mutate(line_vote = ifelse(`Dem: Yea` > 0.5 & `Rep: Yea` > 0.5, 
                            'Bi-partisan', 'Competitive')) %>%
  mutate(line_vote = ifelse(`Dem: Nay` == 0 & `Rep: Nay` == 0, 
                            'Full Consensus',line_vote)) %>%
  mutate(line_vote = ifelse((`Dem: Nay` > 0.9 & `Rep: Yea` > 0.9) | 
                            (`Dem: Yea` > 0.9 & `Rep: Nay` > 0.9), 
                            'Party Line',line_vote)) %>%
  group_by(Chamber, line_vote) %>%

The table below summarizes these distributions. Both chambers, then, vote largely in full consensus. I am not sure if this is typical of state legislatures, or if this is good or bad. That said, quite a bit of legislation is largely symbolic, eg. memorials, for which there is little contention.

Chamber Party Line Competitive Bi-partisan Full Consensus
House 34 56 134 579
Senate 24 68 132 504

Attendance & party loyalty

Next, we consider some details about the aggregate voting records of lawmakers in NMSL53. The nm_legislator_descs table summarizes the total votes cast, party loyalty, and attendance rates for each lawmaker. A portion of the table is presented below.

Votes Cast is the number of times a given legislator voted ‘Yea’ or ‘Nay’ in the legislature. Party Loyalty is the percentage of total roll calls that a given legislator voted in the same direction as the majority of his/her party. Attendance is the percentage of total roll calls for which a given legislator was not absent (ie, being excused/recused is counted as being present).

nmlegisdatr::nml_legislator_descs %>%
  select(Chamber, Representative, Party, Votes_Cast, Party_Loyalty, Attendance) %>%
  head() %>%  kable()
Chamber Representative Party Votes_Cast Party_Loyalty Attendance
House ADKINS Rep 768 95.1 97.8
House ALCON Dem 780 97.9 99.3
House ARMSTRONG, D. Dem 764 99.1 98.0
House ARMSTRONG, GAIL Rep 776 96.6 99.0
House BALDONADO Rep 768 96.4 97.0
House BANDY Rep 701 95.4 88.9

The plot below summarizes attendance rate distributions by political affiliation for each house. While attendance rates are generally quite high, Democratic lawmakers are less likely to miss roll calls than their Republican counterparts.

nmlegisdatr::nml_legislator_descs %>%
ggplot( aes(Attendance, fill = Party, colour = Party)) +
  wnomadds::scale_color_rollcall(aesthetics = c("fill","color")) + 
  geom_density(alpha = 0.4)+
  labs(title="Attendance rate distributions in NMSL53")+
  theme(legend.position = "none",
        plot.title = element_text(size=12))

The plot below summarizes party loyalty rate distributions by political affiliation for each house. Again, party loyalty rates are quite high in each party; however, in both the Senate and the House, Republicans are less loyal to party than Democrats.

This discrepancy is clearly a complicated one. Certainly relevant is Democratic majorities (and fairly sizable ones) in both chambers. Also relevant is potential variation of political ideologies among Republicans in a cash-strapped state that has become solidly blue.

nmlegisdatr::nml_legislator_descs %>%
ggplot( aes(Party_Loyalty, fill = Party, colour = Party)) +
  wnomadds::scale_color_rollcall(aesthetics = c("fill","color")) + 
  geom_density(alpha = 0.4)+
  labs(title="Party loyalty rate distributions in NMSL53")+
  theme(legend.position = "none",
        plot.title = element_text(size=12))

Roll call data underlying these aggregate voting records can be quickly accessed for individual legislators in NMSL53 using the nml_get_legislator function from nmlegisdatr. The function takes two parameters, legislator and chamber, and returns a list of three elements.

Below we access the Voting_Record element of a search for Republican Congressman James Townsend from House District 54. Member_Vote values in all caps indicate votes in which a lawmaker voted against the majority of his/her party.

nmlegisdatr::nml_get_legislator(legislator = 'TOWNSEND', 
                                chamber = 'House')$Voting_Record %>%
  slice(17:22) %>%
Chamber Bill_Code Motion Party_Vote Member_Vote Result
House R17_HB0032 Passage Yea NAY 50-11
House R17_HB0033 Passage Yea Yea 68-0
House R17_HB0034 Passage Yea Yea 67-0
House R17_HB0035 Passage Yea NAY 47-12
House R17_HB0036 Passage Yea Yea 57-0
House R17_HB0040 Passage Yea Excused 48-11

Roll call details

The nmlegisdatr package includes a simple roll call search function, nml_get_bill, that allows users to quickly obtain details about a given piece of legislation. The year, session, bill, and chamber parameters can be used to identify legislation. The function more or less just filters the multiple tables included in nmlegisdatr and collates bill details in one place (akin to the nml_get_legislators function presented above).

bill_dets <- nmlegisdatr::nml_get_bill(year = '2018',
                                       session = 'Regular',
                                       bill = 'HB0079',
                                       chamber = 'House')

The resulting object contains seven elements:

## [1] "Title"       "Sponsors"    "Description" "Motion"      "Result"     
## [6] "Actions"     "Vote"        "Rollcall"

The Description element provides a brief bill description as detailed below. The prefix “R18” specifies a Regular session vote in 2018.


The Sponsors element summarizes bill sponsors as a simple data frame.

Chamber District Representative Party
House 29 ADKINS Rep
Senate 2 NEVILLE Rep

The Result element summarizes roll call results by political affiliation:

bill_dets$Vote %>% 
  ggplot(aes(x=Party_Member_Vote, y=Count, fill= Party_Member_Vote)) +
    geom_col(width=.65, color = 'lightgray') +  
    wnomadds::scale_color_rollcall(aesthetics = c("fill")) +
    scale_x_discrete(limits = rev(levels(bill_dets$Vote$Party_Member_Vote)))+
    labs(title = paste0("R18_HB0079: ", bill_dets$Result),
         caption = bill_dets$Title) +
    xlab("") + ylab("") +
    theme(legend.position = "none",
          plot.title = element_text(size=12))

The Rollcall element details how each legislator voted on a given piece of legislation. Here, we map votes by party affiliation and legislative district. Via the tigris package, we obtain a shape file for lower house legislative districts in New Mexico, and join the Rollcall data frame.

house_shape <- tigris::state_legislative_districts("New Mexico", 
                                                   house = "lower", 
                                                   cb = TRUE)%>% 
  inner_join(bill_dets$Rollcall, by = c('NAME'='District')) %>%
  st_set_crs('+proj=longlat +datum=WGS84')

We then map roll call results by legislative district using the leaflet package, again using the nml_fill_vote function to classify votes by party affiliation. Among other things, the map illustrates an aversion to gross receipt tax exemptions among Republicans in southeastern New Mexico.

pal <- colorFactor(palette = wnomadds::voteview_pal, 
  domain = house_shape$Party_Member_Vote)

house_shape %>%
leaflet(width="100%") %>%
                 options = providerTileOptions (minZoom = 5, maxZoom = 8)) %>%
addPolygons(popup = ~ Representative,
                fill = TRUE, 
                stroke = TRUE, weight=1,
                fillOpacity = 1,
                fillColor=~pal(Party_Member_Vote)) %>%
              pal = pal, 
              values = ~ Party_Member_Vote,
              title = "Roll call",
              opacity = 1)

Incorporating census data

Lastly, we investigate the potential relationship between (1) how legislators voted in the House Bill 79 roll call and (2) selected socio-demographic characteristics of the legislative districts (ie, constituents) they represent. Ultimately, the goal is to provide an aggregate socio-demographic characterization of constituents based on the vote and party affiliation of legislators.

We use the tidycensus package to gather several socio-economic variables by lower house district from the American Community Survey (5-Year estimates, 2012-2016). We collect estimates as counts to facilitate aggregation and the computation of margin of errors for each vote/party affiliation combination. Variables included are largely arbitrary, and simply for demonstration purposes.

census_labs <- c('Hispanic', 'AmericanIndian', 'Under5',
                 "Unemployed", "BelowFPL", "Renters")

census_vars <- list( c('DP05_0066','DP05_0074', 'DP05_0004'),
                     c("B25003_003") )

denoms <- list( c('DP05_0001'),
                c('B25001_001') )
geo <- "state legislative district (lower chamber)"

Here, we apply the tidycensus::get_acs function across each census variable and denominator pair.

census_data <- lapply(1:length(census_vars), function(x)
  tidycensus::get_acs (geography = geo, 
                                    variables = census_vars[[x]], 
                                    summary_var = denoms[[x]],
                                    state = 'NM') ) %>%

Then we do some cleaning.

census_data1 <- census_data%>%
  left_join (data.frame(cbind(census_labs,variable=unlist(census_vars)), 
                        stringsAsFactors = FALSE)) %>%
  mutate(District = as.character(as.numeric(gsub('^...', '',GEOID)))) 

Next, we aggregate census estimates by vote/party affiliation and convert counts to percentages (via denominators from above). We use a combination of the moe_sum and moe_prop functions from the tidycensus package to calculate margins of error for aggregated estimates and (then) for percentages.

census_moe <- census_data1%>%
  left_join(bill_dets$Rollcall) %>%
  group_by(census_labs, Party_Member_Vote) %>%
  dplyr::summarize(new_est = sum(estimate), 
                   new_moe = moe_sum(moe, estimate),
                   new_denom = sum(unique(summary_est)),
                   new_d_moe = moe_sum((summary_moe), (summary_est))) %>%
  mutate(per = new_est/new_denom*100, 
         per_moe = moe_prop(new_est, new_denom, new_moe, new_d_moe)*100) 

The plot below summarizes the aggregate socio-demographic characteristics of constituents based on the vote and party affiliation of their legislators. Values are percentages, and margins of error included as black lines.

So, lots going on. Aside from unemployment rates, some systematic differences between the constituents of Republican House legislators who voted ‘Yea’ on HB0079 (2018 Regular session) relative to the constituents of Republican House legislators who voted ‘Nay’.

census_moe %>%
  ggplot(aes(x=factor(Party_Member_Vote), y=per, fill= Party_Member_Vote,
             ymin = per - per_moe, ymax= per + per_moe)) + 
    geom_col(width=.65, color = 'lightgray') + 
    geom_errorbar(width=0.3) +
    wnomadds::scale_color_rollcall(aesthetics = c("fill")) +
    scale_x_discrete(limits = rev(levels(census_moe$Party_Member_Vote)))+
    coord_flip() +
    theme(legend.position = "none",
          plot.title = element_text(size=12))+
    facet_wrap(~census_labs, scales = 'free_x')+
    labs(title = "Some socio-demographics by party & vote for HB0079")


So, a bit of a walk-about through some roll call data in New Mexico. Hopefully folks in New Mexico will put this package to smarter uses to help inform local politics. General elections for the 54th Legislature in the House of Representatives are in November. Many incumbents (with voting records) are up for re-election. Know how your representative votes.

Postscript: Vizualizing congressional composition

Here we demonstrate how to create a visual representation of the composition of each house that displays congressional seats color-coded by political affiliation (per, eg, Wikipedia).

legs <- nmlegisdatr::nml_legislators %>%
  filter(Representative != 'LT. GOV') %>%
  group_by(Chamber, Party) %>%
  summarize(Count = n()) %>%

To do so, we utilize two functions modified from code made available here. The first function “draws” congressional seats for N legislators and M rows in a half-circle.

 seats <- function(N,M, r0=2){ 
  radii <- seq(r0, 1, len=M)
  counts <- numeric(M)
  pts =,
            lapply(1:M, function(i){
              counts[i] <<- round(N*radii[i]/sum(radii[i:M]))
              theta <- seq(0, pi, len = counts[i])
              N <<- N - counts[i]
              data.frame(x=radii[i]*cos(theta), y=radii[i]*sin(theta), r=i,
   pts = pts[order(-pts$theta,-pts$r),]

The second function assigns each seat to a party affiliation based on the composition of a given congress.

election <- function(seats, counts){
  seats$party <- rep(1:length(counts),counts)
  seats$party <- ifelse(seats$party == 1, "Dem", "Rep")

We apply these two functions to the summary composition of each congressional chamber.

house <- election(seats(70,5), subset(legs$Count, legs$Chamber == 'House')) %>%
  mutate(Chamber = "House")
senate <- election(seats(42,4), subset(legs$Count, legs$Chamber == 'Senate')) %>%
  mutate(Chamber = "Senate")

Lastly, we plot the results. The nmlegisdatr package includes several different color palettes that can be used within the ggplot framework to add colors to plots based on political affiliation, eg, Republican/red and Democrat/blue. Here, we use the nml_color_party function to color congressional seats.

bind_rows(house, senate) %>%
  ggplot() +
  geom_point(aes(x,y, color=as.factor(party)), size = 6)+
  wnomadds::scale_color_rollcall(aesthetics = c("color")) +
        legend.position = 'none',
        plot.title = element_text(size=12)
        ) +
  facet_wrap(~Chamber, ncol =1) +
  coord_fixed(ratio=1) +
  labs(title="Composition of New Mexico's 53rd Congress") 

  1. Roll call data for older legislatures are also available online. However, roll call tables in these PDFs are structured poorly/idiosyncratically, making the scraping process challenging. If the state makes historical roll call data available in a nice, shiny spreadsheet, joke’s on me.