Skip to contents

What it covers

The Georgia scraper returns election results from the Georgia Secretary of State at results.sos.ga.gov. It covers all elections available on the site — primaries, runoffs, generals, and special elections — from 2000 to the present.

Results are available at three levels: statewide candidate totals, county breakdowns, and precinct-level results. Precinct data is obtained by navigating each county page and following “View results by precinct” links for each contest.

Optionally, it can also return a vote-method breakdown (Advance in Person, Election Day, Absentee by Mail, Provisional) for each contest.

Note: The Georgia SOS site is a JavaScript-rendered Angular SPA. All scraping uses a headless Chromium browser via Playwright. County and precinct scraping is parallelised across up to max_workers browsers (default 4). Georgia has 159 counties; a full state + county scrape for one year typically takes 20–60 minutes. Adding precinct data significantly increases runtime.


Arguments

Argument Default Description
state "GA", "georgia", or "Georgia"
year_from NULL Start year, inclusive; NULL = no lower bound
year_to NULL End year, inclusive; NULL = no upper bound
level "all" "all" — statewide + county + precinct; "state" — statewide only (fastest); "county" — county-level only; "precinct" — precinct-level only
max_workers 4L Parallel Chromium browsers for county and precinct scraping
include_vote_methods FALSE If TRUE, also return vote-method breakdown tables

Examples

Statewide + county results for a single year

res <- scrape_elections(state = "GA", year_from = 2024, year_to = 2024)

# Statewide candidate totals
res$state %>%
  filter(office == "President of the United States") %>%
  select(election_name, candidate, party, votes, vote_pct)

# County-level breakdown
res$county %>%
  filter(office == "President of the United States") %>%
  select(county, candidate, party, votes, vote_pct) %>%
  arrange(county, desc(votes))

Statewide totals only (faster — skips county scraping)

state_df <- scrape_elections(
  state     = "GA",
  year_from = 2024,
  year_to   = 2024,
  level     = "state"
)

state_df %>%
  select(election_name, office, candidate, party, votes, vote_pct)

With vote-method breakdown

res <- scrape_elections(
  state                = "GA",
  year_from            = 2024,
  year_to              = 2024,
  include_vote_methods = TRUE
)

# Statewide vote-method totals
res$vote_method_state %>%
  filter(office == "United States Senator") %>%
  select(candidate, party, votes_advance_voting, votes_election_day,
         votes_absentee, votes_provisional, votes_total)

# County vote-method breakdown
res$vote_method_county %>%
  filter(office == "United States Senator", county == "Fulton") %>%
  select(candidate, votes_advance_voting, votes_election_day,
         votes_absentee, votes_provisional, votes_total)

Faster county scraping with more parallel browsers

Georgia has 159 counties. By default, county pages are scraped with 4 parallel Chromium browsers. Increasing max_workers can cut runtime significantly on machines with enough CPU/RAM.

res <- scrape_elections(
  state       = "GA",
  year_from   = 2024,
  year_to     = 2024,
  level       = "all",
  max_workers = 8L
)

Multi-year scrape

res <- scrape_elections(
  state       = "GA",
  year_from   = 2020,
  year_to     = 2024,
  level       = "all",
  max_workers = 4L
)

Precinct-level results

# level = "all" includes precinct data automatically
res <- scrape_elections(state = "GA", year_from = 2024, year_to = 2024)
res$precinct %>%
  filter(office == "President of the United States", county == "Fulton") %>%
  select(precinct, candidate, party, votes, vote_pct) %>%
  arrange(precinct, desc(votes))

# Precinct only (skips statewide + county scraping)
precinct_df <- scrape_elections(
  state     = "GA",
  year_from = 2024,
  year_to   = 2024,
  level     = "precinct"
)

Filter to a specific county

res <- scrape_elections(state = "GA", year_from = 2022, year_to = 2022)

res$county %>%
  filter(county == "Fulton") %>%
  select(election_name, office, candidate, party, votes, vote_pct) %>%
  arrange(office, desc(votes))

Output columns

$state (when level = "all" or "state")

Column Description
state State abbreviation ("GA")
election_name Human-readable election name (e.g. "November General Election")
election_type Election type (e.g. "General", "Primary")
election_year Calendar year of the election
election_date Date string from the page header
office_level "Federal", "State", or "Local"
office Office name
district District identifier; NA when not applicable
candidate Candidate name
party Party abbreviation
votes Vote total
vote_pct Percentage of votes within the contest
winner TRUE if the candidate won the statewide contest
url URL of the election results page scraped

$county (when level = "all" or "county")

Column Description
state State abbreviation ("GA")
election_name Human-readable election name
election_type Election type (e.g. "General", "Primary")
election_year Calendar year of the election
election_date Date string from the page header
office_level "Federal", "State", or "Local"
office Office name
district District identifier; NA when not applicable
county County name
candidate Candidate name
party Party abbreviation
votes County-level vote total
vote_pct Percentage of votes within the contest for this county
county_winner TRUE if the candidate won within this county
url URL of the election results page scraped

$precinct (when level = "all" or "precinct")

Column Description
state State abbreviation ("GA")
election_name Human-readable election name
election_type Election type (e.g. "General", "Primary")
election_year Calendar year of the election
election_date Date string from the page header
office_level "Federal", "State", or "Local"
office Office name
district District identifier; NA when not applicable
county County name
precinct Precinct name
candidate Candidate name
party Party abbreviation
votes Precinct-level vote total
vote_pct Percentage of votes within the contest for this precinct
precinct_winner TRUE if the candidate won within this precinct
url URL of the precinct results page scraped

$vote_method_state / $vote_method_county (when include_vote_methods = TRUE)

Same identifying columns as above, with votes split by method instead of a single votes + pct:

Column Description
votes_advance_voting Advance in Person ballots
votes_election_day Election Day ballots
votes_absentee Absentee by Mail ballots
votes_provisional Provisional ballots
votes_total Sum of all methods

Performance notes

  • level = "state" is much faster. County scraping opens up to max_workers parallel Chromium browsers across Georgia’s 159 counties. A full state + county scrape for one year takes 20–60 minutes. Use level = "state" when county breakdowns are not needed.
  • Precinct scraping is significantly slower. It navigates each county page, fetches one ballot-item page per contest (in parallel), and then scrapes each individual precinct page. Expect a full state + county + precinct run to take several hours for a general election year. Use level = "county" or level = "state" when precinct data is not needed.
  • include_vote_methods is significantly slower. Each contest panel requires an extra Playwright click-and-re-render step. Budget an extra 5–15 minutes per election page.
  • Virtual scroll. The GA SOS site uses Angular virtual scrolling, rendering only panels visible in the viewport. The scraper automatically scrolls to load all panels before parsing — without this, pages with 100+ contests would silently return incomplete data.
  • Multiple elections per year. The GA SOS site lists all elections for a year (primaries, runoffs, generals, specials). All are scraped and combined in the output.