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 nmlegis.gov, 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(wnomadds)#devtools::install_github("jaytimm/wnomadds") library(tidyverse) library(DT) library(tidycensus); options(tigris_use_cache = TRUE, tigris_class = "sf") library(tigris) library(sf) library(knitr) library(ggthemes) library(data.table)
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.
|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) %>% rowwise()%>% mutate(Per_Dem = round(Dem/sum(Dem,Rep)*100,1)) %>% kable()
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())%>% kable() ## Joining, by = "Bill_Code"
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() %>% kable()
|Bill_Code||Result||Dem: Yea||Rep: Yea||Dem: Nay||Rep: Nay|
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) %>% summarise(count=n())
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|
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()
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")) + ggthemes::theme_fivethirtyeight()+ geom_density(alpha = 0.4)+ facet_wrap(~Chamber)+ 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")) + ggthemes::theme_fivethirtyeight()+ geom_density(alpha = 0.4)+ facet_wrap(~Chamber)+ 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,
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) %>% kable()
Roll call details
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
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:
names(bill_dets) ##  "Title" "Sponsors" "Description" "Motion" "Result" ##  "Actions" "Vote" "Rollcall"
Description element provides a brief bill description as detailed below. The prefix “R18” specifies a Regular session vote in 2018.
|R18_HB0079: THANKSGIVING SATURDAY GROSS RECEIPTS|
|AN ACT RELATING TO TAXATION; PROVIDING A DEDUCTION FROM GROSS RECEIPTS FOR RETAIL SALES MADE ON THE FIRST SATURDAY AFTER THANKSGIVING BY CERTAIN BUSINESSES.|
Sponsors element summarizes bill sponsors as a simple data frame.
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)))+ coord_flip()+ labs(title = paste0("R18_HB0079: ", bill_dets$Result), caption = bill_dets$Title) + xlab("") + ylab("") + ggthemes::theme_fivethirtyeight()+ theme(legend.position = "none", plot.title = element_text(size=12))
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.
library(leaflet) pal <- colorFactor(palette = wnomadds::voteview_pal, domain = house_shape$Party_Member_Vote) house_shape %>% leaflet(width="450") %>% addProviderTiles(providers$OpenStreetMap, options = providerTileOptions (minZoom = 5, maxZoom = 8)) %>% addPolygons(popup = ~ Representative, fill = TRUE, stroke = TRUE, weight=1, fillOpacity = 1, color="white", fillColor=~pal(Party_Member_Vote)) %>% addLegend("topleft", pal = pal, values = ~ Party_Member_Vote, title = "Roll call", opacity = 1)