Is it possible to obtain time-series/timestamped election count statistics at the precinct or county level?
After seeing Draza Smith and Edward Solomon's work analyzing the changes in ballot batch counts over time, some very interesting anomalies showed up -- in particular many batches, despite differing numbers of ballots seem to have identical percentages for Trump/Biden. Sometimes dozens of batches have the same percentage.
The time series data can be obtained from the New York Times, which got it from Edison. Here's a few URLs:
Description | URL |
---|---|
Alaska Presendential Race | https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/race-page/alaska/president.json |
Alaska All Ballot Races | https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/state-page/alaska.json |
New York Presendential Race | https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/race-page/new-york/president.json |
New York All Ballot Races | https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/state-page/new-york.json |
To find your state, replace the state name in the URL above, using lowercase, and hypens between multiple words.
The above URLs provide timestamped counts at the state level, but to get county/precinct level, you have to load a different URL which does NOT have the time series data, only the final counts:
Description | URL |
---|---|
Alaska County/Precinct Counts | https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/precincts/AKGeneral-latest.json |
Florida County/Precint Counts | https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/precincts/FLGeneralConcatenator-latest.json |
Notice how these ones use the two-letter abbreviation for the state, in capitals, and some have "General" while others have "GeneralConcatenator", for some reason. So you might need to try both versions to find your state.
Some people have found that some of these files have historical timestamped versions, here are a few examples:
But unfortunately there's no way to query ALL of these historical data files.
Someone did find 1,237 of them on the Wayback Machine, see https://web.archive.org/web/*/https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/precincts*
But that's not a complete list, unfortunately.
Some of us, including myself, have tried to brute-force search the NYT server, but they have no index pages, and there are 1000 possible URLs for every second of the counting period for each state. So that's a heck of a lot of URL permutations to try.
Anyway, here's the Javascript (Node) script I used to attempt this, in case anyone else wants to give it a go:
- Save the code below into
run.mjs
, in its own folder - Ensure you have node installed
- Inside the folder, in a command prompt, type:
npm install queue node-fetch
- Type
node run.mjs
Show code
// There appear to be some timestamped JSON files containing precinct/county
// level information at that specific time. If we could fetch them all,
// then we'd be able to recreate the counts as they occurred.
// This is the URL format:
// https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/precincts/FLGeneralConcatenator-2021-01-25T20:08:14.680Z.json
//
// Some stats omit the word "Concatenator".
// The problem is that we don't know what timestamps are available.
import fetch from 'node-fetch';
import queue from 'queue';
// The base URL we're going to use for this set
var urlBase = 'https://static01.nyt.com/elections-assets/2020/data/api/2020-11-03/precincts/IDGeneral-';
// Create an array with all the timestamps we're going to try to load
// I normally opt for a starting time in seconds, and try 0-999ms after
// and 1-500ms earlier (in case of being rounded up)
var dates = [];
for (var i = 0; i <= 999; i++) {
dates.push('2020-11-04T06:58:29.' + String(i).padStart(3, '0') + 'Z');
}
for (var i = 500; i <= 999; i++) {
dates.push('2020-11-04T06:58:28.' + String(i).padStart(3,'0') + 'Z');
}
// Setup our request queue, running 8 HTTP requests simultaneously
var q = queue({concurrency: 8});
var errorCount = 0;
// Add all URLs to a queue, and handle each result
dates.forEach((item, i) => {
q.push(() => {
let time = new Date();
let url = urlBase + item + '.json';
return fetch(url)
// .then(resp => resp.text())
// .then(resp => resp.ok)
.then(response => {
// Print a message after every 25 requests
if (i % 25 == 0)
console.log('Finished ' + i, url);
// Print if we've found a successful match!
if (response.ok)
console.log(item, response.ok, time)
// Halt if unusual error, such as being banned/limited
if (response.status > 404) {
console.log('Failed with error:', response.status, item);
errorCount++;
if (errorCount > 4)
q.end();
}
})
.catch(resp => console.error(resp));
})
})
// Run the queue
q.start();