Election Audits is a modern web app for auditing elections to help election officials use risk-limiting audits using ballot-polling, ballot-comparison, canvass, and Bayesian ballot-polling audits; each supporting multiple winners/“vote for n”. Also includes a sample-size demo page, comparing the risk limit of ballot-polling and ballot-comparison audits are arbitrary risk limits using real election data from openelections.net.
The app was implemented with a Python server and audit methods with a static frontend. The frontend communicates with the server using sockets, and the backend delegates requests to the appropriate audit method and returns responses to guide the user through the audit process. The implementations of audit methods are Python translations of the pseudocode provided in the relevant literature. 1 2 3 4 5
As part of this project, I submitted changes to Ron Rivest’s 2018-bptool project, in order to let
BayesianPolling.py be a wrapper around his code with the same interface as the other audit methods.
While the project was once live on election-audits.org, it was taken down after the conclusion of the class due to hosting costs. However, it can still be used by election officials through local installations!
All source for the project is available on Github.
Below are a few of the more interesting functions from
Bravo.py (the ballot-comparison method we implemented), which do most of the audit method’s work.
def get_margins(self): """ Return an array `margins`, for which for each winner and loser, margins[winner][loser] ≡ votes_array[winner]/(votes_array[winner] + votes_array[loser]), is the fraction of votes `winner` was reported to have received among ballots reported to show a vote for `winner` or `loser` or both. """ margins = np.zeros([self.num_candidates, self.num_candidates]) for winner in self.candidates.winners: for loser in self.candidates.losers: margins[winner][loser] = self.votes_array[winner] \ / (self.votes_array[winner] + self.votes_array[loser]) return margins def get_ballot(self): """ Step 2 of the BRAVO algorithm. Randomly picks a ballot to test and returns a list of its votes. The ballot picked is returned to the frontend for the user to input actual votes on the ballot. The ballot should be selected using a random function seeded by a user-generated seed. Note: 'random' should be seeded before this function is called. """ ballot_votes = self.get_votes() if len(ballot_votes) > self.num_winners: return  return ballot_votes def update_hypothesis(self, winner, loser): """ Step 5 of the BRAVO algorithm. Rejects the null hypothesis corresponding to the test statistic of the `winner` and `loser` pair. Increments the null hypothesis rejection count. """ if self.hypotheses.test_stat[winner][loser] >= 1/self.risk_limit: self.hypotheses.test_stat[winner][loser] = 0 self.hypotheses.reject_count += 1 def update_audit_stats(self, vote): """ Steps 3-5 from the BRAVO algorithm. Updates the `test_statistic` and rejects the corresponding null hypothesis when appropriate. """ if vote in self.candidates.winners: # Step 3 for loser in self.candidates.losers: self.hypotheses.test_stat[vote][loser] \ *= 2*self.margins[vote][loser] self.update_hypothesis(vote, loser) elif vote in self.candidates.losers: # Step 4 for winner in self.candidates.winners: self.hypotheses.test_stat[winner][vote] \ *= 2*(1-self.margins[winner][vote]) self.update_hypothesis(winner, vote)
“A gentle introduction to risk-limiting audits” (Lindeman and Stark 2012). ↩︎
“BRAVO: ballot-polling risk-limiting audits to verify outcomes” (Lindeman, Stark, and Yates 2012). ↩︎
“Canvass audits by sampling and testing” (Stark 2009). ↩︎
“Super-simple simultaneous single-ballot risk-limiting audits” (Stark 2010). ↩︎
“Bayesian Tabulation Audits: Explained and Extended” (Rivest 2018). ↩︎