Competition and Representation in New Mexico’s State House
Introduction
This analysis looks at elections for New Mexico’s State House from 2004 to 2024, covering all 70 districts over 11 election cycles.
The core question is whether votes translate into seats proportionally – if a party wins 55% of the statewide vote, do they win roughly 55% of the seats? When they don’t, that gap is partisan bias: one party getting more representation than its raw vote share would justify. Finding bias is only the first step. The more useful question is what produces it – district boundaries, geography, or structural features of how races are contested.
Two structural features get most of the attention here: how often races go uncontested, and how competitive districts are when both parties do field candidates. Both shape the distribution of votes across districts in ways that can generate or amplify bias independently of how the map was drawn.
Data
Election results come from newmexico-political-data, a GitHub resource I’ve put together that collates results from the New Mexico Secretary of State. It covers elections from 2000-2024 with consistent formatting and party names. Here we focus on State House races from 2004-2024, covering 11 election cycles and all 70 House districts.
pacman::p_load(dplyr, ggplot2, tidyr, viridis, scales, patchwork, purrr, data.table, sf, ggrepel, tidycensus, tigris, DT)results <- read.csv("https://raw.githubusercontent.com/jaytimm/newmexico-political-data/main/data/elections/nm_election_results_2000-24.csv",
stringsAsFactors = FALSE)
house_results <- results |>
filter(office_name == "State Representative") |>
filter(election_date >= 2004 & election_date <= 2024)
race_status <- house_results |>
group_by(election_date, district_name) |>
summarise(
n_candidates = n_distinct(candidate_name),
winner_party = candidate_party_name[is_winner == TRUE][1],
.groups = "drop"
) |>
mutate(unopposed = n_candidates == 1)
unopposed_summary <- race_status |>
group_by(election_date) |>
summarise(
total_races = n(),
unopposed_count = sum(unopposed),
unopposed_pct = (unopposed_count / total_races) * 100,
.groups = "drop"
)Representational Fairness
The seats-votes curve is the first diagnostic. Each point plots a party’s statewide vote share against its seat share for a given cycle – proportional representation sits on the diagonal, and deviation from it is the measure of bias. When one party consistently sits above the line, that gap is also called a seat bonus: the extra representation it extracts beyond what its raw vote share would justify.
Points above the diagonal mean Democrats won more seats than their vote share would predict. Points below would favor Republicans. The consistent above-diagonal clustering confirms a modest but durable pro-Democratic bias – present in nearly every cycle, and most pronounced in 2020.
calculate_seats_votes <- function(election_data) {
election_data |>
group_by(district_name) |>
mutate(
total_votes = sum(votes, na.rm = TRUE),
dem_votes = sum(votes[candidate_party_name == "Democratic"], na.rm = TRUE),
rep_votes = sum(votes[candidate_party_name == "Republican"], na.rm = TRUE),
dem_pct = (dem_votes / total_votes) * 100,
rep_pct = (rep_votes / total_votes) * 100
) |>
slice(1) |>
ungroup() |>
summarise(
dem_seats = sum(dem_pct > 50, na.rm = TRUE),
rep_seats = sum(rep_pct > 50, na.rm = TRUE),
dem_seat_share = (sum(dem_pct > 50, na.rm = TRUE) / n()) * 100,
dem_vote_share = (sum(dem_votes, na.rm = TRUE) / sum(total_votes, na.rm = TRUE)) * 100,
.groups = "drop"
)
}
all_elections <- unique(house_results$election_date)
seats_votes <- map_dfr(all_elections, function(year) {
sv_result <- calculate_seats_votes(house_results |> filter(election_date == year))
sv_result$election_date <- year
sv_result
}) |>
arrange(election_date)
ggplot(seats_votes, aes(x = dem_vote_share, y = dem_seat_share)) +
geom_point(color = "#DD8E58", size = 1.5, alpha = 0.8) +
ggrepel::geom_text_repel(aes(label = election_date), size = 3.5, min.segment.length = 0.5) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey50", linewidth = 1) +
labs(
title = "Seats-Votes Curve: All Election Years",
x = "Democratic Vote Share (%)",
y = "Democratic Seat Share (%)"
) +
theme_minimal() +
theme(plot.title = element_text(size = 16)) +
xlim(40, 65) +
ylim(40, 65)Seats-Votes Summary Table
A more detailed view is presented in the table below, with the Democratic advantage ranging from 2-7 points in most years. The only exception was 2014, when Republicans briefly gained a slight seat advantage.
seats_votes_table <- seats_votes |>
mutate(
dem_vote_share = round(dem_vote_share, 1),
dem_seat_share = round(dem_seat_share, 1),
seat_vote_diff = round(dem_seat_share - dem_vote_share, 1)
) |>
select(election_date, dem_vote_share, dem_seat_share, seat_vote_diff, dem_seats, rep_seats) |>
arrange(election_date)
max_abs_diff <- max(abs(seats_votes_table$seat_vote_diff), na.rm = TRUE)
color_func <- scales::col_numeric(
palette = "RdBu",
domain = c(-max_abs_diff, max_abs_diff),
reverse = FALSE
)
seats_votes_table <- seats_votes_table
seats_votes_table |>
DT::datatable(
colnames = c("Year", "Dem Vote Share (%)",
"Dem Seat Share (%)", "Seat-Vote Diff", "Dem Seats", "Rep Seats"),
caption = "Seats-Votes Relationship: State House, All Election Years",
rownames = FALSE,
options = list(pageLength = 25, dom = 't', scrollX = TRUE)
) |>
DT::formatStyle(
"seat_vote_diff",
backgroundColor = DT::styleInterval(
cuts = seq(-max_abs_diff, max_abs_diff, length.out = 20),
values = color_func(seq(-max_abs_diff, max_abs_diff, length.out = 21))
)
) |>
DT::formatRound(
columns = c("dem_vote_share", "dem_seat_share", "seat_vote_diff"),
digits = 1)Partisan Distribution and the “Seat Bonus”
While the historical seat bonus has been modest, the 2024 election results—where Democrats held a 43-27 advantage—reveal a deeper structural story. To understand why the seat share remains disconnected from the statewide vote, we have to look at how the actual 2024 victory margins were distributed across the 70 districts.
The chart below ranks every State House district by its final 2024 Democratic margin. The dashed lines mark the ±10-point threshold, creating a visual “competitive zone.” The reality of the New Mexico map is a competitive void: almost every district falls well outside that center lane, with bars extending toward the poles to illustrate just how few races were even remotely close.
The “cliffs” at the extremes of the distribution are a result of the high frequency of unopposed races, where the margin is fixed at 100 points by default rather than by active contest. These uncontested seats account for a significant share of the total map, effectively anchoring the distribution at its poles and locking in the seat bonus regardless of the statewide mood.
house_2024 <- house_results |>
filter(election_date == 2024) |>
group_by(district_name) |>
mutate(
total_votes = sum(votes, na.rm = TRUE),
dem_votes = sum(votes[candidate_party_name == "Democratic"], na.rm = TRUE),
rep_votes = sum(votes[candidate_party_name == "Republican"], na.rm = TRUE),
dem_pct = (dem_votes / total_votes) * 100,
rep_pct = (rep_votes / total_votes) * 100,
dem_margin = dem_pct - rep_pct
) |>
slice(1) |>
ungroup() |>
arrange(desc(dem_margin)) |>
mutate(
rank = row_number(),
winner = ifelse(dem_margin > 0, "Democratic", "Republican"),
district_num = gsub(".*District (\\d+).*", "\\1", district_name)
)
ggplot(house_2024, aes(x = rank, y = dem_margin, fill = winner)) +
geom_col() +
geom_hline(yintercept = 0, color = "black", linewidth = 0.5) +
geom_hline(yintercept = c(-10, 10), linetype = "dashed", color = "grey50", alpha = 0.7) +
scale_fill_manual(values = c("Democratic" = "#1f77b4", "Republican" = "#d62728")) +
labs(
title = "Partisan Lean Distribution: 2024 State House",
x = "District Rank (by Democratic Margin)",
y = "Democratic Margin (percentage points)",
fill = "Winner"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 16),
legend.position = "bottom",
axis.text.x = element_blank(),
axis.ticks.x = element_blank()
) +
scale_y_continuous(breaks = seq(-100, 100, by = 20), limits = c(-100, 100))Unopposed Races
The 2024 distribution isn’t an anomaly, but a structural mainstay of the state’s political system. The plot below tracks these uncontested districts since 2004, highlighting a consistent floor where a significant portion of the chamber is decided by default.
unopposed_by_party <- race_status |>
filter(unopposed) |>
group_by(election_date, winner_party) |>
summarise(n = n(), .groups = "drop")The notable drop in unopposed races in 2020 is corroborated by Ballotpedia’s coverage of that cycle.
ggplot(unopposed_by_party,
aes(x = as.numeric(election_date), y = n, fill = winner_party)) +
geom_col(alpha = 0.85) +
geom_text(aes(label = n), position = position_stack(vjust = 0.5),
color = "white", size = 3.5, fontface = "bold") +
scale_fill_manual(
values = c("Democratic" = "#1f77b4", "Republican" = "#d62728"),
name = "Party running unopposed"
) +
labs(
title = "Unopposed Races Over Time",
x = "Election Year",
y = "Number of Unopposed Districts"
) +
theme_minimal() +
theme(plot.title = element_text(size = 16),
legend.position = "bottom") +
scale_x_continuous(breaks = seq(2004, 2024, by = 2))The Geography of Uncontested Seats
These lopsided margins reflect the actual map of New Mexico’s political strongholds. As the map below confirms, uncontested seats are concentrated in specific regional clusters—namely rural, lower-population areas and deep urban pockets.
In these regions, partisan dominance is high enough that the minority party often fails to field a candidate. This geographic reality anchors the seat bonus in place, regardless of how the rest of the map shifts in a given cycle.
options(tigris_use_cache = TRUE)
invisible(capture.output({
suppressMessages({
suppressWarnings({
house_districts_raw <- tigris::state_legislative_districts(
state = "NM", house = "lower", year = 2021)
})
})
}))
if("SLDLST" %in% names(house_districts_raw)) {
house_districts <- house_districts_raw |>
mutate(district_num = as.character(as.numeric(SLDLST))) |>
select(district_num, geometry)
} else if("NAME" %in% names(house_districts_raw)) {
house_districts <- house_districts_raw |>
mutate(
district_num = gsub(".*District (\\d+).*", "\\1", NAME),
district_num = as.character(as.numeric(district_num))
) |>
select(district_num, geometry)
} else {
house_districts <- house_districts_raw |>
mutate(district_num = as.character(row_number())) |>
select(district_num, geometry)
}
unopposed_map_house <- race_status |>
filter(election_date == 2024) |>
mutate(
district_num = gsub(".*District (\\d+).*", "\\1", district_name),
district_num = as.character(as.numeric(district_num)),
unopposed_status = case_when(
unopposed & winner_party == "Democratic" ~ "Democrat Unopposed",
unopposed & winner_party == "Republican" ~ "Republican Unopposed",
TRUE ~ "Contested"
)
)
house_map_data <- house_districts |>
left_join(unopposed_map_house, by = "district_num") |>
mutate(
unopposed_status = ifelse(is.na(unopposed_status), "Contested", unopposed_status)
)
ggplot(house_map_data) +
geom_sf(aes(fill = unopposed_status), color = "white", linewidth = 0.3) +
scale_fill_manual(
values = c("Democrat Unopposed" = "#1f77b4",
"Republican Unopposed" = "#d62728",
"Contested" = "grey90"),
name = "Status"
) +
labs(title = "2024 State House: Unopposed Districts") +
theme_void() +
theme(
plot.title = element_text(size = 16),
legend.position = "bottom"
)Competitiveness
This geographic clustering leaves only a small fraction of the map in play. The plot below tracks the share of contested races that fall within the “Competitive Zone,” showing that even when both parties file, a true toss-up remains a rarity in the state’s political landscape.
contested_districts <- race_status |>
filter(!unopposed) |>
select(election_date, district_name)
house_margins <- house_results |>
inner_join(contested_districts, by = c("election_date", "district_name")) |>
group_by(election_date, district_name) |>
mutate(
total_votes = sum(votes, na.rm = TRUE),
dem_votes = sum(votes[candidate_party_name == "Democratic"], na.rm = TRUE),
rep_votes = sum(votes[candidate_party_name == "Republican"], na.rm = TRUE),
dem_pct = (dem_votes / total_votes) * 100,
rep_pct = (rep_votes / total_votes) * 100,
margin_abs = abs(dem_pct - rep_pct),
competitive_5 = margin_abs < 5,
competitive_10 = margin_abs < 10
) |>
filter(is_winner == TRUE) |>
ungroup()
comp_summary <- house_margins |>
group_by(election_date) |>
summarise(
very_competitive = sum(competitive_5, na.rm = TRUE),
competitive = sum(competitive_10, na.rm = TRUE),
.groups = "drop"
) |>
left_join(unopposed_summary |> select(election_date, total_races, unopposed_count), by = "election_date") |>
pivot_longer(cols = c(very_competitive, competitive),
names_to = "metric", values_to = "count") |>
mutate(
pct = (count / total_races) * 100,
metric = case_when(
metric == "very_competitive" ~ "Very Competitive (<5%)",
metric == "competitive" ~ "Competitive (<10%)"
)
)
ggplot(comp_summary, aes(x = as.numeric(election_date), y = pct, color = metric)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
scale_color_manual(values = c("Very Competitive (<5%)" = "#9467bd",
"Competitive (<10%)" = "#5ba3a0")) +
labs(
title = "Competitiveness in State House Races Over Time",
x = "Election Year",
y = "Percentage of All Districts",
color = NULL
) +
theme_minimal() +
theme(
plot.title = element_text(size = 16),
legend.position = "bottom"
) +
scale_x_continuous(breaks = seq(2004, 2024, by = 2)) +
scale_y_continuous(labels = scales::percent_format(scale = 1))Competitiveness Summary by Year
The table below provides a more detailed perspective on competitiveness, breaking out counts by cycle – with unopposed races included for context. All counts sum to 70.
house_margins |>
group_by(election_date) |>
summarise(
`Very Competitive (<5%)` = sum(competitive_5, na.rm = TRUE),
`Competitive (5-10%)` = sum(competitive_10 & !competitive_5, na.rm = TRUE),
`Non-Competitive (>10%)` = sum(!competitive_10, na.rm = TRUE),
.groups = "drop"
) |>
left_join(unopposed_summary |> select(election_date, total_races, unopposed_count), by = "election_date") |>
rename(Year = election_date, Unopposed = unopposed_count) |>
select(-total_races) |>
DT::datatable(
caption = "Competitiveness in State House Races by Year (out of all districts)",
rownames = FALSE,
options = list(pageLength = 15, dom = 't', scrollX = TRUE)
)Summary
As we have seen, a substantial share of districts in New Mexico’s State House go unopposed each cycle, most are decided by comfortable margins, and genuinely competitive races are rare. The result is functionally a bimodal map – split between deep blue and deep red, with a fairly hollowed-out middle.
The seats-votes relationship holds up because this distribution is a structural mainstay. While Democrats consistently win a seat bonus, it typically stays in the 2-7 point range. This partisan bias is less a product of map-drawing and more a reflection of the state’s lopsided geographic sorting, which seems to anchor the seat bonus in place regardless of the statewide mood.