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_workersbrowsers (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"
)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 tomax_workersparallel Chromium browsers across Georgia’s 159 counties. A full state + county scrape for one year takes 20–60 minutes. Uselevel = "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"orlevel = "state"when precinct data is not needed. -
include_vote_methodsis 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.