This is the second in a series of posts investigating voting patterns in New Mexico’s 53rd State Legislature (NMSL53). In this post, we detail the use of K. T. Poole and Rosenthal (2011) ’s ideal point estimation procedure (aka NOMINATE) to examine political ideologies in NMSL53 using the R package
wnominate (K. Poole et al. 2011).
NOMINATE is a multidimensional scaling procedure that represents legislators in two-dimensional political “space” based on roll call voting records for a given legislature. Throughout the history of the US Congress, the first dimension of this space has captured ideological differences along the traditional liberal-conservative continuum, while the second dimension has captured ideological differences based in social conservatism that crosscut party affiliation (K. T. Poole and Rosenthal 2011).
Here we apply these methods to roll call data from NMSL53 made available in my R package
nmlegisdatr to get of sense of the political ideologies informing voting behavior among New Mexico State Senators. We also introduce a simple package that I have developed called
wnomadds that facilitates finer-grained exploration of
library(wnomadds)#devtools::install_github("jaytimm/wnomadds") library(nmlegisdatr)#devtools::install_github("jaytimm/nmlegisdatr") library(tidyverse) library(wnominate) library(pscl) library(knitr)
We quickly run through the ideal points estimation procedure using
wnominate, and then spend most of the post unpacking and visualizing model results.
Ideal points estimation
Roll call data for New Mexico’s 53rd State Legislature are made available as a data frame called
nml_rollcall in my
nmlegisdatr package. As can be noted below, roll call data are in a long format, with each row representing a unique legislator-bill vote.
nmlegisdatr::nml_rollcall %>% filter (Chamber == 'Senate') %>% head() %>% select(Bill_Code, Representative, Member_Vote) %>% kable()
Before using the
wnominate package to run an ideal point estimation, we first need to reshape roll call data from long to wide format. Resulting data structure casts each roll call as an individual column and each legislator as an individual row.
wide_rolls <- nmlegisdatr::nml_rollcall %>% filter(Chamber =='Senate' & !grepl('^LT', Representative)) %>% mutate(Bill_Unique = paste0(Bill_Code, substr(Motion, 1,1))) %>% dplyr::select(Representative, Bill_Unique, Member_Vote) %>% mutate(Member_Vote = case_when(Member_Vote == "Yea" ~ 1, Member_Vote == "Nay" ~ 6, Member_Vote %in% c("Excused", "Absent", "Rec") ~ 9)) %>% spread(key= Bill_Unique, value = Member_Vote)
Per this new data structure, we then build a roll call object using the
roll_obj <- pscl::rollcall(wide_rolls [,-1], yea = 1, nay = 6, missing = 9, notInLegis = NA, vote.names = colnames(wide_rolls)[2:ncol(wide_rolls)], legis.names = wide_rolls$Representative)
Next, we perform ideal point estimation using the
wnominate::wnominate function. The
polarity parameter is used to specify a legislator who is conservative on each dimension. This does not “train” (or influence) the model in any way; it only affects the polarity of legislator coordinates in two-dimensional space such that conservatives are to the right (ie, positive coordinates) and liberals to the left (ie, negative coordinates), per the standard political metaphor. This will become clearer shortly.
ideal_2D <- wnominate::wnominate (roll_obj, polarity=c("BURT","BURT")) # ## ## Preparing to run W-NOMINATE... ## ## Checking data... ## ## All members meet minimum vote requirements. ## ## Votes dropped: ## ... 507 of 728 total votes dropped. ## ## Running W-NOMINATE... ## ## Getting bill parameters... ## Getting legislator coordinates... ## Starting estimation of Beta... ## Getting bill parameters... ## Getting legislator coordinates... ## Starting estimation of Beta... ## Getting bill parameters... ## Getting legislator coordinates... ## Getting bill parameters... ## Getting legislator coordinates... ## Estimating weights... ## Getting bill parameters... ## Getting legislator coordinates... ## Estimating weights... ## Getting bill parameters... ## Getting legislator coordinates... ## ## ## W-NOMINATE estimation completed successfully. ## W-NOMINATE took 5.42 seconds to execute.
The above notifications indicate that 507 out of the total 728 roll calls in the State Senate were excluded from the model by virtue of being lopsided, ie, legislation that is largely agreed upon does not shed light on how the political ideologies of legislators differ.
The resulting NOMINATE object consists of seven elements. We will unpack each as we go.
names(ideal_2D) ##  "legislators" "rollcalls" "dimensions" "eigenvalues" "beta" ##  "weights" "fits"
Legislators in political space
legislator element contains model results-proper, ie, the ideal point estimates. Again, these estimates represent coordinates in a two-dimensional space bounded by a unit circle. Below we extract these coordinates, and add legislator details (eg, party affiliation and legislative district) for subsequent analyses.
row.names(ideal_2D$rollcalls) <- colnames(wide_rolls)[2:ncol(wide_rolls)] senate_data <- ideal_2D$legislators %>% bind_cols(nml_legislators %>% filter(Chamber == 'Senate' & !grepl('^LT', Representative)))
Sample output of legislator coordinates:
|Gregory A. Baca||29||Rep||0.8773707||-0.3158852|
|William F. Burt||33||Rep||0.4949957||0.3735212|
First, we consider the first dimension of model results independently. As noted in K. T. Poole and Rosenthal (2011) (and the NOMINATE literature generally), variation along the first dimension captures traditional partisan divisions in a given legislature.
The figure below illustrates 1D scores plotted against rank for New Mexico’s 53rd State Senate. Legislators with scores closer to 1 are described as more conservative and those with scores closer to -1 are described as more liberal. Legislators with scores close to zero can be described as moderate.
ggplot(senate_data, aes(x=reorder(Representative, coord1D), y=coord1D, label=Representative)) + geom_point(stat='identity', aes(col=Party), size=4.5) + wnomadds::scale_color_rollcall()+ geom_text(size=2.5, nudge_y = -0.1) + labs(title="Figure 1: Legislator ideal point estimates in one-dimensional political space") + ggthemes::theme_fivethirtyeight()+ theme(axis.title.y=element_blank(), axis.text.y=element_blank(), legend.position = 'none', plot.title = element_text(size=12)) + coord_flip()
Per model results along 1D, then, William Soules from District 37 can be described as the most liberal Democrat in the Senate and William Sharer from District 1 the most conservative Republican. In general, Republicans are more moderate in their voting patterns — State Senators Rue, Kernan, and Neville in particular.
Next, we consider a two-dimensional model. Again, per K. T. Poole and Rosenthal (2011), variation along the second dimension in the US Congress has historically captured ideological differences based in social conservatism that crosscut party affiliation — reflecting different stances on social issues of the day, eg, the abolition of slavery, civil rights, and lifestyle choices.
library(ggforce) circle <- data.frame( x0 = 0, y0 = 0, r = 1) d2 <- ggplot(senate_data, aes(x=coord1D, y=coord2D)) + geom_point(aes(color = Party), size= 3, shape= 17) + scale_color_rollcall() + theme(legend.position = 'bottom') + geom_text(aes(label=Representative), size=2.5, check_overlap = TRUE, hjust = "inward", nudge_y = -0.03)+ labs(title="Figure 2: Legislator ideal point estimates in two-dimensional political space") + ggforce::geom_circle (data = circle, aes(x0=x0, y0=y0, r=r), color = 'light gray', inherit.aes=FALSE) + coord_fixed()
As the figure illustrates, the added second dimension captures variation in voting patterns among legislators that crosscut party affiliation, most notably, dividing Senators in the Republican party. We will investigate the ideology underlying the second dimension in the NMSL53 Senate as we go.
Legislation in 2D political space
Figure 2, then, represents how legislators voted on all pieces of legislation in a given legislature. Per the NOMINATE procedure, the underlying structure of this political space is based on individual roll call cutting lines. A cutting line bisects the political space, dividing legislators who voted ‘Yea’ for a given piece of legislation from those who voted ‘Nay’.
Long story short, the procedure positions legislator ideal points and cutting line coordinates to optimize correct classification of all votes cast (ie, Yea v. Nay) by all legislators in a given legislature. Figures 1 & 2 are ultimately the product of this optimization process/algorithm.
wnominate package provides two functions for investigating these cutting lines:
plot.angles(). The plots below illustrate (the first 50) cutting lines (along with legislator coordinates) and the distribution of cutting line angles, respectively.
par(mfrow=c(1, 2)) wnominate::plot.cutlines(ideal_2D) ## NULL points(ideal_2D$legislators$coord1D, ideal_2D$legislators$coord2D, col="blue", font=2, pch=16) wnominate::plot.angles(ideal_2D)
As the histogram demonstrates, the large proportion of cutting lines have angles in the 90 degree range, ie, more vertical cutting lines that divide legislators along the first dimension. We can investigate the efficacy of these cutting lines in correctly classifying votes via the
fits element of the NOMINATE object. Per the output below, a one-dimensional model correctly classifies 90.2% of all cast votes. A second dimension accounts for an additional 1.3% of cast votes.
ideal_2D$fits ## correctclass1D correctclass2D apre1D apre2D gmp1D ## 90.2219086 91.5391006 0.5242494 0.5883372 0.7607572 ## gmp2D ## 0.8176903
What does this mean? Well, we have a fairly good model. We also have a State Senate in NMSL53 that is quite uni-dimensional in its voting patterns; in other words, the traditional partisan (ie, liberal-conservative) divide (as represented in Figure 1) can account for most of the voting behavior in the NM Senate.
While we do not have historical data for the NMSL, K. T. Poole and Rosenthal (2011) have noted the shrinking utility of the second dimension in accounting for voting patterns in the US Congress historically — which they interpret (paraphrased very roughly here) as the recasting of certain social issues in the national political debate (ca 1980s) as economic ones.
Despite the relatively small contribution of the second dimension in accounting for voting patterns in NMSL53, we carry on with our 2D model.
Cutting line coordinates & roll call polarity
wnominate::plot.angles()are super convenient functions for quickly visualizing model results, they hide away underlying data and, hence, limit subsequent analyses and visualizations. To address these limitations, I have developed a simple R package called
wnomadds. Functions included in the package, dubbed
wnm_get_angles(), are based on existing
wnominate code, and have been tweaked some to output data frames as opposed to base R plots.
Here, we take a bit more detailed perspective on cutting line coordinates and roll call polarity using the
wnomadds::get_cutlines() function. The function takes a
nomObj object and a
rollcall object (from the previous call to
pscl::rollcall). In addition to cutting line coordinates, the function returns the coordinates of two points perpendicular to cutting line ends that specify the polarity of the roll call, ie, the direction of the ‘Yea’ vote in political space.
with_cuts <- wnomadds::wnm_get_cutlines(ideal_2D, rollcall_obj = roll_obj, arrow_length = 0.05)
Output contains four sets of (x,y) points. These four sets of points can be used to create three line segments via
geom_segment: the actual cutting line and two line arrow segments denoting the polarity of the roll call.
head(with_cuts) ## Bill_Code x_1 y_1 x_2 y_2 x_1a ## 1: R17_HB0001P 0.7659736 0.64287197 0.684223776 -0.7292721 0.6973664 ## 2: R17_HB0002P 0.9757444 0.21891277 -0.197162242 -0.9803709 0.9157803 ## 3: R17_HB0063P 0.7779014 -0.62838635 0.474137214 0.8804510 0.7024595 ## 4: R17_HB0080P 0.9966121 0.08224562 -0.633982079 -0.7733477 0.9538324 ## 5: R17_HB0086P 0.6468509 -0.76261647 -0.001531652 0.9999988 0.5587202 ## 6: R17_HB0087P 0.2567056 0.96648966 -0.149109277 -0.9888207 0.1589400 ## y_1a x_2a y_2a ## 1: 0.6469595 0.61561657 -0.7251846 ## 2: 0.2775581 -0.25712642 -0.9217255 ## 3: -0.6435746 0.39869535 0.8652628 ## 4: 0.1637753 -0.67676175 -0.6918180 ## 5: -0.7950356 -0.08966242 0.9675797 ## 6: 0.9867804 -0.24687480 -0.9685300
The plot below illustrates cutting lines, legislator ideal points, and roll call polarity.
ggplot () + wnomadds::scale_color_rollcall() + theme(legend.position = 'bottom') + geom_point(data=senate_data, aes(x=coord1D, y=coord2D,color = Party), size= 3, shape= 17) + geom_segment(data=with_cuts, #cutting start to end aes(x = x_1, y = y_1, xend = x_2, yend = y_2)) + geom_segment(data=with_cuts, #cutting end to opposite arrow aes(x = x_2, y = y_2, xend = x_2a, yend = y_2a), arrow = arrow(length = unit(0.2,"cm"))) + geom_segment(data=with_cuts, #cutting start to opposite arrow aes(x = x_1, y = y_1, xend = x_1a, yend = y_1a), arrow = arrow(length = unit(0.2,"cm")))+ geom_text(data=with_cuts, aes(x = x_1a, y = y_1a, label = Bill_Code), size=2.5, nudge_y = 0.03, check_overlap = TRUE) + coord_fixed(ratio=1) + labs(title="Figure 3: Cutting lines, roll call polarity & legislator coordinates")
While a bit chaotic, the plot provides a nice high-level perspective on how legislation positions legislators in political space per the NOMINATE procedure. Again, most cutting lines are vertical in nature, dividing the legislature along partisan lines — perhaps most notable is the prevalence of vertical cutting lines on the right (ie, Republican) side of the plot. That said, there are some more horizontal cutting lines that reflect voting ideologies that crosscut political affiliation.
Cutting line angles & legislative bills
To get a sense of the types of legislation that divide the legislature along each dimension of political space (and the political ideologies underlying them), we use the
wnm_get_angles() function to extract cutting line angles from the NOMINATE object.
angles <- wnomadds::wnm_get_angles(ideal_2D)
Sample output includes legislation ID and cutting line angles:
## Bill_Code angle ## 1 R17_HB0001P 86.59045 ## 2 R17_HB0002P 45.63706 ## 3 R17_HB0063P 101.38282 ## 4 R17_HB0080P 27.68656 ## 5 R17_HB0086P 110.19618 ## 6 R17_HB0087P 78.27501
We can identify legislation that positions legislators in 1D by filtering the above output to roll calls with more vertical cutting lines. Here, we take a random sample of legislation with cutting line angles between 85 and 95 degrees.
set.seed(99) angles %>% mutate(Bill_Code = gsub('.$', '', Bill_Code)) %>% left_join(nml_legislation) %>% filter (angle > 85 & angle < 95) %>% sample_n(10) %>% select(Bill_Code, angle, Bill_Title)%>% kable(digits = 1, row.names = FALSE)
|R17_SB0277||93.3||PREGNANT/ LACTATING ALTERNATIVE SENTENCING|
|R17_HB0199||87.6||DISTRIBUTED GENERATION CONSUMER PROTECTION|
|R17_SB0374||89.6||HUNGER-FREE STUDENTS’ BILL OF RIGHTS ACT|
|R18_HB0235||89.4||RAISE MUNICIPAL COURT AUTOMATION FEE|
|R17_SB0139||91.8||AUTO RECYCLER REPORTING TO TAX & REV. DEPT.|
|R18_HB0098||90.0||LOCAL ELECTION ACT|
|R17_SB0192||89.5||TRANSFER OF LOTTERY FUNDS|
|R17_HB0484||88.7||SCHOOL INDIAN STUDENT NEEDS ASSESSMENTS|
|R17_SB0011||87.4||CHILD EARLY INTERVENTION REIMBURSEMENT BASIS|
|R18_SB0040||94.9||WASTEWATER PROJECT FUNDING ELIGIBILITY|
Legislation with more horizontal cutting lines position legislators in 2D. A random sample of legislation with cutting line angles greater than 135 degrees or less than 45 degrees is presented below.
set.seed(99) angles %>% mutate(Bill_Code = gsub('.$', '', Bill_Code)) %>% left_join(nml_legislation) %>% filter (angle > 135 | angle < 45) %>% sample_n(10) %>% select(Bill_Code, angle, Bill_Title)%>% kable(digits = 1, row.names = FALSE)
|R17_SB0349||15.5||LIVESTOCK RUNNING AT LARGE|
|R17_HB0249||18.9||COLLEGE SPECIAL EVENT GROSS RECEIPTS|
|R17_SB0420||136.7||LOTTERY SCHOLARSHIP GRACE PERIOD|
|R18_SB0018||28.7||IMPOSITION OF AVIATION LANDING FEES|
|R17_SB0188||24.6||DISABILITIES STUDENTS LOTTERY SCHOLARSHIPS|
|R18_HB0079||158.3||THANKSGIVING SATURDAY GROSS RECEIPTS|
|R17_SB0258||154.6||DECREASE MARIJUANA PENALTIES|
|R17_SB0046||38.4||E911 SURCHARGES ON COMMUNICATIONS SERVICES|
|R17_SB0052||8.8||RONALD MCDONALD HOUSE LICENSE PLATES|
|R18_SB0176||20.0||INCREASE STATE OFFICER COMPENSATION|
So, the two (random sub-) sets of legislation provide a super simple perspective on potential political ideologies underlying each dimension of our model. The sample of 1D legislation does in fact seem to speak to the traditional liberal-conservative divide.
The sample of 2D legislation, on the other hand, is a bit of a mixed bag, and one that does not strictly speak to social conservatism (as per K. T. Poole and Rosenthal (2011) in the case of US Congress). But certainly a collection of legislation that does not intuitively align with political affiliation.
Individual roll calls & ideal points
Next, we combine the utility of the two functions included in
wnomadds to demonstrate how individual roll calls bisect political space. Having extracted cutting line angles, we can now investigate how individual pieces of legislation divide the legislature in different ways. First, we consider (some cherry-picked) roll calls that divide the legislature along 1D.
D1_cuts <- c('R17_HB0484', 'R17_SB0140', 'R17_HJR10', 'R18_SM023', 'R17_SB0155', 'R18_SM003') sub <- nmlegisdatr::nml_rollcall %>% filter(Bill_Code %in% D1_cuts) %>% inner_join(senate_data) %>% inner_join(nml_legislation) cut_sub <- subset(with_cuts, Bill_Code %in% paste0(D1_cuts,'P')) %>% mutate(Bill_Code = gsub('.$', '', Bill_Code)) %>% inner_join(nml_legislation)
Then we plot/facet individual roll calls and cutting lines utilizing some shape/color palettes made available via
wnomadds to visualize party affiliation and vote type per legislator.
d1 <- sub %>% ggplot(aes(x=coord1D, y=coord2D)) + geom_point(aes(color = Party_Member_Vote, shape= Party_Member_Vote, fill = Party_Member_Vote), size= 1.5) + wnomadds::scale_color_rollcall() + wnomadds::scale_fill_rollcall() + wnomadds::scale_shape_rollcall() + theme(legend.position = 'bottom') + geom_text(aes(label=Representative), size=1.75, check_overlap = TRUE, hjust = "inward", nudge_y = -0.03)+ ggforce::geom_circle (data = circle, aes(x0=x0, y0=y0, r=r), color = 'light gray', inherit.aes=FALSE) + coord_fixed() + geom_segment(data=cut_sub, aes(x = x_1, y = y_1, xend = x_2, yend = y_2)) + geom_segment(data=cut_sub, aes(x = x_2, y = y_2, xend = x_2a, yend = y_2a), arrow = arrow(length = unit(0.2,"cm"))) + geom_segment(data=cut_sub, aes(x = x_1, y = y_1, xend = x_1a, yend = y_1a), arrow = arrow(length = unit(0.2,"cm")))+ geom_text(data=cut_sub, aes(x = .7, y = -1, label = Bill_Code), size=2.25, nudge_y = 0.1, check_overlap = TRUE) + labs(title="Figure 4: Selected 1D cutting lines and roll calls") + facet_wrap(~Bill_Title, labeller = label_wrap_gen(), ncol = 3) + theme(strip.text = element_text(size=7))
So, some nice examples that demonstrate how individual pieces of legislation divide legislators along 1D space and, more specifically, position legislators along the liberal-conservative continuum.
Next, we consider (some cherry-picked) roll calls that divide the legislature along 2D.
D2_cuts <- c('R18_SB0176','R18_HB0079', 'R18_SB0030', 'R17_SB0055', 'R18_SB0018', 'R17_HB0080')
Using the same
ggplot pipe from above, we plot roll calls and cutting lines for legislation that crosscuts political affiliation in varying degrees. Roll calls below illustrate a curious faction comprised of the most conservative Republicans and some of the more moderate Democrats, one that collectively votes against more bureaucratic legislation. (Or some more informed explanation.)
Political space and marijuana
Here, we take a quick look at roll calls for legislation related to marijuana. As a political issue, marijuana-friendly legislation is fun, especially for Republican legislators, as it pits social conservatism against small government & libertarianism. An ideological cage-rattler, as it were. Again, we use the same
ggplot pipe from above, and consider two pieces of marijuana-related legislation from the first session of NMSL53.
weed <-c('R17_HB0527', 'R17_SB0258')
The plot illustrates a fairly clean division among Republicans in the Senate with respect to marijuana-based legislation. Republican State Senators with higher scores on 2D are generally less likely to support legislation friendly to marijuana, ie, social conservatism > small government. The opposite is true for Republican State Senators with lower scores on 2D. Democrats, in contrast, stand united on the issue. The plot sheds some additional light on the political ideology underlying the second dimension, and supports a social conservatism element to 2D.
A quick geographical perspective
Lastly, we investigate a potential relationship between geography and second dimension scores. Using the super convenient
tigris package, we obtain a shape file for upper house legislative districts in New Mexico. Then we join our table containing ideal point estimates for each legislator.
library(tigris); options(tigris_use_cache = TRUE, tigris_class = "sf") library(leaflet); library(sf) senate_shape <- tigris::state_legislative_districts("New Mexico", house = "upper", cb = TRUE)%>% inner_join(senate_data, by = c('NAME'='District')) %>% st_set_crs('+proj=longlat +datum=WGS84')
We plot the 2D scores by upper house legislative district using the
leaflet package. As the map illustrates, legislators with lower scores represent districts in the Albuquerque area (where roughly a third of the state’s population resides), as well as districts in the western part of the state. In contrast, legislators with higher 2D scores represent districts on the state’s periphery (north and south), which tend to be more rural districts.
pal <- colorNumeric(palette ="Purples", domain = senate_data$coord2D) senate_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(coord2D)) %>% addLegend("topleft", pal = pal, values = ~ coord2D, title = "coord2D", opacity = 1)