03/01/2026
Some of the CVRs were redacted to protect voter anonymity. The redacted ballots were grouped (apparently) by BallotType and just the pool subtotals were published. These redacted ballot pools are a good fit for using OneAudit. Unfortunately, the total number of cards in each pool is (apparently) not published, so we have to estimate it, and adjust the contest Nc to be consistent. See below for details.
We are interested in comparing the CLCA audit which has full CVRs against the OneAudit which has the redacted ballots in OneAudit pools.
Here we show 10 OneAudit’s (with different PRN seeds each time), and compare it to the CLCA audits. In all cases there are no errors (but some of the contests have a small number of phantoms). Over 62 contests there are ? assertions, each with a different margin. Here is the spread of the OneAudits reletive to the CLCA, for each of the ? assertions:
The total mvrs needed are dominated by the assertions with the lowest margin. Here we explore how CLCA and OneAudit differ when n contests with the largest estimated nmvrs are removed from the audit:
In practice, even for CLCA, contests with very small margins and/or that require a large percentage of the ballots for that contest are likely to be removed. For Boulder2024, the top 2 contests are under the Colorado automatic recount margin of .005, and would go to a full hand count immediately. So its only after the first 2 contests are removed that the results become interesting.
With OneAudit, the other two contests in the top 4 require more than 85% of the available ballots, and would also probably just go to a hand count. The top 3 or 4 contests always fail and are removed from the audit, which is why the number of successful contests for OneAudit (OA nsuccess) are less than the number of successful CLCA contests.
The takeaway is that by the time you remove these 4 contests, OneAudit needs about 2x more nmvrs than CLCA for Boulder24. This is a much better result than for SanFrancisco, and is due to the lower percentage of ballots in pools (3% vs 13%), and also because the Boulder pools have a single Ballot Style, and so the margin is not diluted.
| n | nsuccess | OA nsuccess | CLCA est | OA est avg | ratio | One Audit Spread |
|---|---|---|---|---|---|---|
| 0 | 62 | 58.8 | 16292 | 53706 | 3.3 | [46930, 53432, 53553, 53706, 54285, 55184, 56259, 60528, 62340, 62341] |
| 1 | 61 | 58.3 | 8804 | 21121 | 2.4 | [20885, 20901, 20965, 21079, 21097, 21185, 21352, 21412, 21453, 21454] |
| 2 | 60 | 58.2 | 5147 | 17093 | 3.3 | [12662, 12719, 17608, 18511, 18557, 18745, 18810, 18832, 22338, 22339] |
| 3 | 59 | 58.4 | 3070 | 10517 | 3.4 | [9297, 9558, 9721, 10184, 10307, 10496, 10646, 11862, 14157, 14158] |
| 4 | 58 | 57.6 | 1343 | 5011 | 3.7 | [4419, 4717, 4892, 4964, 5018, 5025, 5377, 5453, 5938, 5939] |
| 5 | 57 | 57.0 | 708 | 1386 | 2.0 | [1123, 1230, 1253, 1374, 1449, 1539, 1551, 1663, 1776, 1777] |
| 6 | 56 | 56.0 | 706 | 1346 | 1.9 | [1102, 1230, 1242, 1360, 1439, 1484, 1485, 1568, 1672, 1673] |
| 7 | 55 | 55.0 | 665 | 1283 | 1.9 | [958, 1139, 1226, 1361, 1382, 1442, 1448, 1472, 1627, 1628] |
| 8 | 54 | 54.0 | 625 | 1008 | 1.6 | [667, 681, 709, 849, 1060, 1262, 1359, 1401, 1476, 1477] |
| 9 | 53 | 53.0 | 516 | 753 | 1.5 | [539, 544, 587, 605, 607, 648, 987, 1099, 1368, 1369] |
| 10 | 52 | 52.0 | 415 | 481 | 1.2 | [442, 457, 460, 475, 485, 490, 501, 511, 541, 542] |
where
n = remove top n estimated-nmvrs contests
nsuccess = number of contests successfully audited by CLCA
OA nsuccess = average number of contests successfully audited by OneAudit
CLCA est = estimated nmvrs needed by CLCA
OA est avg = average estimated nmvrs needed by OneAudit
ratio = OA est avg / CLCA est
One Audit Spread = spread of estimated nmvrs needed by OneAudit
Findings so far:
The following files have already been downloaded into cases/src/test/data/Boulder2024 and converted to csv files by reading into Libre Office and exporting to csv:
From: https://bouldercounty.gov/elections/results/
https://assets.bouldercounty.gov/wp-content/uploads/2024/11/2024G-Boulder-County-Official-Statement-of-Votes.xlsx
https://assets.bouldercounty.gov/wp-content/uploads/2025/01/2024-Boulder-County-General-Redacted-Cast-Vote-Record.xlsx
https://assets.bouldercounty.gov/wp-content/uploads/2024/12/2024G-Boulder-County-Amended-Statement-of-Votes.xlsx
https://assets.bouldercounty.gov/wp-content/uploads/2025/01/2024-Boulder-County-General-Recount-Redacted-Cast-Vote-Record.xlsx
Using cases/src/test/kotlin/org/cryptobiotic/rlauxe/util/TestGenerateAllUseCases.kt:
The XXX_Cast-Vote-Record.xlsx files may be a standard “export to excel” function from Dominion. These are read by readDominionCvrExportCsv(). (the CvrExport_xxxxx.json files seem to be no longer available on the website)
The XXX_Statement-of-Votes.xlsx files may be in a bespoke format specific to Boulder County. Each election Ive looked at is slightly different. These are read by readBoulderStatementOfVotes(), which uses the filename to choose the variation.
DominionCvrExportCsv
CvrNumber, TabulatorNum, BatchId, RecordId, ImprintedId, CountingGroup, BallotType,
CastVoteRecord(cvrNumber: Int, tabulatorNum: Int, batchId: String, recordId: Int, imprintedId: String, ballotType: String)
cvrs have ballotType == cardStyle. So does redacted group, which mostly agree.
BoulderStatementOfVotes has
(2023R) "Precinct Code","Precinct Number","Active Voters","Contest Title","Candidate Name","Total Ballots","Round 1 Votes","Round 2 Votes","TotalVotes","TotalBlanks","TotalOvervotes","TotalExhausted"
(2023) "Precinct Code","Precinct Number","Active Voters","Contest Title","Choice Name","Total Ballots","Total Votes","Total Undervotes","Total Overvotes"
(2024) "Precinct Code","Precinct Number","Contest Title","Choice Name","Active Voters","Total Ballots","Total Votes","Total Undervotes","Total Overvotes"
createBoulder24oa() creates a OneAudit by assuming that each pool has a single CardStyle, which allows us to use style based sampling. This constrains the way that undervotes are added to the pools. We need to know the number of cards in each batch. We dont, so we approximate it, and adjust Nc.
createBoulder24clca(): creates a CLCA by simulating cvrs in each pool and adding to the regular cvrs. This allows us to characterize how OneAudit and CLCA compare.
Its not possible to run an IRV audit with redacted CVRs. There are lines called “RCV Redacted & Randomly Sorted”, but they dont make much sense so far. To do IRV with OneAudit you need to create a VoteConsolidator for each pool from the real cvrs.
We have two source of information: sovo is Boulder’s StatementOfVotes, and DominionCvrExportCsv contain the CVRs and the redacted pool counts.
Some of these assumptions may not be right, but they are sufficient for our purposes. In a real audit, we would work with the EA to track each down. We would advocate that the number of cards in each pool be published.
On contest 20, sovo.totalVotes and sovo.totalBallots is wrong vs the cvrs. (only one where voteForN=3, but may not be related)
'Town of Superior - Trustee' (20) candidates=[0, 1, 2, 3, 4, 5, 6] choiceFunction=PLURALITY nwinners=3 voteForN=3
contestTitle, precinctCount, activeVoters, totalBallots, totalVotes, totalUnderVotes, totalOverVotes
sovoContest=Town of Superior - Trustee, 7, 9628, 8254, 16417, 8246, 33
cvrTabulation={0=3121, 1=3332, 2=3421, 3=2097, 4=805, 5=657, 6=3137} nvotes=16570 ncards=7865 undervotes=7025 overvotes=0 novote=1484 underPct= 29%
redTabulation={0=130, 1=87, 2=111, 3=50, 4=25, 5=36, 6=101} nvotes=540 ncards=180 undervotes=0 overvotes=0 novote=0 underPct= 0%
sovoCards= 8254 = (sovoContest.totalVotes + sovoContest.totalUnderVotes) / info.voteForN + sovoContest.totalOverVotes
phantoms= 0 = sovoContest.totalBallots - sovoCards
sovoUndervotes= 8345 = sovoContest.totalUnderVotes + sovoContest.totalOverVotes * info.voteForN
cvrUndervotes= 7025
redUndervotes= 1320 = sovoUndervotes - cvr.undervotes
redVotes= 540 = redacted.votes.map { it.value }.sum()
redNcards= 620 = (redVotes + redUndervotes) / info.voteForN
totalCards= 8485 = redNcards + cvr.ncards
diff= -231 = sovoContest.totalBallots - totalCards
Assume sovo.totalBallots is wrong, so Nc = max(totalCards, sovoContest.totalBallots)
We assume all ballots in each pool contain the same set of contests, called the Ballot Style. Seems true except for RedactedGroup ‘06, 33, & 36-A’. We can compare the BallotStyle for CV RS vs the redacted group:
RedactedGroup=[0, 1, 2, 3, 5, 10, 11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
CVRS=[0, 1, 2, 3, 5, 10, 11, 13, 14, 15, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
Note that contest 12 is in RedactedGroup but not CVRs. Its value in the RedactedGroup is zero, so we assume that it is a mistake to be there. This matters for the undervote count of that contest.