A prorrogação do 2º turno de 2026: o que dizem os arquivos das urnas
1. Introdução
1.1 O problema
No 2º turno da eleição presidencial de 25 de outubro de 2026, a votação deveria terminar às 17h (horário de Brasília). No fim da tarde, o ministro Cristiano Zanin, do Supremo Tribunal Federal, atendeu a um pedido para prorrogar a votação até as 19h e, logo depois, esclareceu que a prorrogação valia apenas para as seções que ainda estavam com as urnas abertas. As urnas que já tinham encerrado não foram reabertas.
A decisão gerou dúvidas nas redes e na imprensa: quantas urnas de fato ficaram abertas depois das 17h? Onde estavam? Quantos votos entraram nesse período? Essas urnas votaram de forma diferente das demais? Os votos depois das 17h poderiam ter mudado o resultado?
Essas perguntas podem ser respondidas com dados oficiais e públicos: o TSE publica, para cada urna do país, o boletim com o resultado e o log com o registro de tudo o que aconteceu na urna durante o dia, segundo a segundo.
1.2 Como pretendo abordar o problema
Os dados são os arquivos de urna publicados pelo TSE para as cerca de 500 mil seções eleitorais do Brasil. De cada urna, uso dois arquivos:
- o boletim de urna (
bu.dat), com os votos por candidato e os horários de abertura e encerramento da urna; - o log da urna (
log.jez), com o horário de cada voto computado, trocas de urna, falhas e o tipo de identificação de cada eleitor.
Os dois arquivos são unidos por seção. O resultado é uma base com uma linha por urna, que permite identificar as urnas que ficaram abertas depois das 17h e comparar o seu comportamento com o das demais.
1.3 Técnica adotada
- Classificar cada urna pelo horário real de encerramento, já convertido para o horário de Brasília: antes das 17h, entre 17h e 17h45 (encerramento normal, com a fila das 17h) e estendida (depois das 17h45).
- Contar os votos registrados depois das 17h a partir dos logs, e comparar com a margem entre os candidatos no pior cenário possível (todos esses votos para um só lado).
- Investigar por que algumas urnas atrasaram: trocas de urna, paradas longas, falhas de biometria e tamanho da seção.
- Comparar o voto de cada urna estendida com o restante do seu próprio município, para separar o efeito da prorrogação do perfil político de cada região.
1.4 A quem esta análise ajuda
A análise interessa a eleitores, jornalistas e pesquisadores que querem dimensionar o efeito da prorrogação com números verificáveis, em vez de impressões. Como todo o processo parte dos arquivos oficiais e o código é aberto, qualquer pessoa pode refazer as contas.
2. Pacotes requeridos
library(dplyr) # manipulação de tabelas (filtrar, agrupar, juntar)
library(tidyr) # mudar o formato das tabelas (pivot_wider, unnest)
library(purrr) # aplicar funções a listas (map, map_dfr)
library(jsonlite) # ler as listas de seções do TSE, em JSON
library(lubridate) # tratar datas e horários
library(parallel) # processar milhares de arquivos em paralelo
library(ggplot2) # gráficos
library(plotly) # gráficos interativos
library(DT) # tabelas interativas
library(scales) # formatar números e porcentagens nos gráficos
library(knitr) # tabelas estáticas (kable)| Pacote | Para que foi usado |
|---|---|
dplyr |
filtrar, agrupar, resumir e juntar tabelas |
tidyr |
transformar os votos do formato longo para o largo e desaninhar o JSON |
purrr |
aplicar as funções de leitura a milhares de arquivos e combinar os resultados |
jsonlite |
ler as listas oficiais de seções, que vêm em JSON aninhado |
lubridate |
converter os horários das urnas e corrigir o fuso de cada estado |
parallel |
ler os ~500 mil boletins e logs usando vários núcleos do processador |
ggplot2 |
gráficos estáticos |
plotly |
gráficos interativos |
DT |
tabelas interativas, com busca e ordenação |
scales |
formatar porcentagens e números nos eixos |
knitr |
tabelas estáticas do relatório |
O download dos arquivos foi feito com um script em Python
(coleta_tse.py), que apenas baixa os arquivos do site do
TSE sem modificá-los. Toda a leitura, limpeza e análise é feita em R,
neste relatório.
caminho_brutos <- "coleta_tse/brutos" # arquivos originais baixados do TSE
caminho_dados <- "coleta_tse/dados" # resultados intermediários (cache da leitura)
data_eleicao <- "25/10/2026"
# números no padrão brasileiro, para o texto
num <- function(x, d = 0) format(round(x, d), big.mark = ".", decimal.mark = ",", nsmall = d)
pct <- function(x, d = 1) paste0(num(100 * x, d), "%")3. Preparação dos dados
3.1 Fonte dos dados
Os arquivos vêm do Portal de Resultados do TSE, seção “Dados de urna”: https://resultados.tse.jus.br/oficial/app/index.html#/eleicao/6258/uf/br/cargo/1/vis/nominal/dados-de-urna/boletim-de-urna.
Cada seção eleitoral tem uma página com os arquivos da sua urna. O
download foi feito entre 26 e 27 de outubro de 2026, com o script
coleta_tse.py, que percorre a lista oficial de seções de
cada estado e salva os arquivos exatamente como o TSE os publica, na
estrutura brutos/UF/município/zona/seção/.
3.2 Os dados de origem
Para cada seção, o TSE publica:
| Arquivo | Conteúdo | Formato |
|---|---|---|
UF-secoes.json |
lista oficial de municípios, zonas e seções de cada estado | JSON aninhado |
*-bu.dat |
boletim de urna: aptos, comparecimento, votos por candidato, abertura e encerramento | binário (ASN.1/DER), assinado digitalmente |
*-log.jez |
log da urna: cada evento do dia, com horário | zip com texto em latin1, separado por TAB |
*-rdv.dat |
registro digital do voto (cada voto, embaralhado) | binário (não usado aqui) |
*-vota.vsc |
assinaturas dos arquivos | binário (não usado aqui) |
O propósito original desses arquivos é a auditoria da eleição: permitir que qualquer pessoa confira o resultado de cada urna. Algumas peculiaridades exigiram cuidado:
- Seções agregadas: seções pequenas são juntadas a
outra e votam na mesma urna. Elas aparecem na lista oficial com o campo
nsp(seção principal), mas não têm arquivos próprios: os votos estão no boletim da seção principal. - Fusos horários: o boletim e o log registram a hora local da urna. No Acre, por exemplo, a votação foi das 6h às 15h locais (8h às 17h de Brasília).
- Urnas de contingência: quando uma urna quebra, ela é trocada. O log da nova urna traz, dentro dele, o log da urna original (um zip dentro do zip), e há linhas repetidas entre os dois.
- Códigos com zeros à esquerda (município
01007, zona0009), que precisam ser lidos como texto. - Formato binário: o boletim não é uma tabela; é uma árvore de blocos codificados em ASN.1/DER, que precisou de um leitor próprio.
- Exterior: as seções no exterior (UF “ZZ”) não foram baixadas e ficam fora da análise.
3.3 Importação e limpeza
3.3.1 Lista de seções
Cada estado tem um JSON aninhado em quatro níveis (estado → município → zona → seção). A função abaixo “desaninha” o JSON em uma tabela com uma linha por seção, e separa as seções agregadas, que não têm arquivos próprios.
ler_lista_secoes <- function(uf) {
j <- fromJSON(file.path(caminho_brutos, uf, paste0(uf, "-secoes.json")))
secoes_uf <- bind_rows(j$abr$mu) |>
as_tibble() |>
select(cd_mun = cd, municipio = nm, zon) |>
unnest(zon) |>
rename(zona = cd) |>
unnest(sec)
if (!"nsp" %in% names(secoes_uf)) secoes_uf$nsp <- NA_character_
secoes_uf |>
transmute(uf = toupper(uf), cd_mun, municipio, zona, secao = ns,
secao_principal = nsp)
}
ufs <- list.dirs(caminho_brutos, recursive = FALSE, full.names = FALSE)
lista_secoes <- map_dfr(ufs, ler_lista_secoes)
agregadas <- filter(lista_secoes, !is.na(secao_principal))
secoes <- filter(lista_secoes, is.na(secao_principal)) |> select(-secao_principal)A lista oficial tem 514.464 seções em 27 estados. Dessas, 16.567 são agregadas (sem arquivos próprios), e 497.897 têm boletim e log.
3.3.2 Leitura do boletim de urna (binário ASN.1)
O boletim é codificado em DER: uma sequência de blocos “tipo,
tamanho, conteúdo”, em que alguns blocos contêm outros blocos dentro. A
função der() transforma os bytes do arquivo nessa árvore, e
ler_bu() percorre a árvore para extrair os campos de
interesse.
# Converte bytes DER em uma árvore de blocos (classe, construído?, tag, valor)
der <- function(b, i = 1, fim = length(b)) {
out <- list()
while (i <= fim) {
t <- as.integer(b[i]); i <- i + 1
cls <- bitwShiftR(t, 6L) # classe: 0 universal, 2 contexto
cons <- bitwAnd(t, 0x20L) > 0 # construído (tem filhos) ou primitivo
tag <- bitwAnd(t, 0x1FL)
if (tag == 0x1FL) { # tag longa (vários bytes)
tag <- 0
repeat { x <- as.integer(b[i]); i <- i + 1
tag <- tag * 128 + bitwAnd(x, 0x7FL); if (x < 128) break }
}
n <- as.integer(b[i]); i <- i + 1
if (n >= 128) { # tamanho longo: os próximos k bytes
k <- n - 128; n <- 0
for (j in seq_len(k)) { n <- n * 256 + as.integer(b[i]); i <- i + 1 }
}
fim_v <- i + n - 1
v <- if (n == 0) raw(0) else if (cons) der(b, i, fim_v) else b[i:fim_v]
out[[length(out) + 1]] <- list(cls = cls, cons = cons, tag = tag, v = v)
i <- fim_v + 1
}
out
}
der_int <- function(v) { # inteiro com sinal, big-endian
if (length(v) == 0) return(0)
x <- as.numeric(v)
val <- sum(x * 256^((length(x) - 1):0))
if (x[1] >= 128) val <- val - 256^length(x)
val
}
cargos <- c("1" = "presidente", "3" = "governador", "5" = "senador", "11" = "prefeito")
tipos_voto <- c("2" = "branco", "3" = "nulo", "5" = "anulado", "6" = "anulado_apuracao")
ler_bu <- function(arquivo) {
b <- readBin(arquivo, "raw", file.size(arquivo))
envelope <- der(b)[[1]]$v
# o boletim vem dentro de um OCTET STRING do envelope assinado
conteudo <- Find(function(x) x$cls == 0 && !x$cons && x$tag == 4 && length(x$v) > 50, envelope)
bu <- der(conteudo$v)[[1]]$v
res <- list(abertura = NA_character_, encerramento = NA_character_,
aptos = NA_real_, comparecimento = NA_real_)
votos <- list()
for (x in bu) {
# bloco [0]: data/hora de abertura e encerramento ("20261025T080001")
if (x$cls == 2 && x$cons && x$tag == 0 && length(x$v) >= 2 &&
all(map_dbl(x$v[1:2], "tag") == 27)) {
res$abertura <- rawToChar(x$v[[1]]$v)
res$encerramento <- rawToChar(x$v[[2]]$v)
}
# sequência com os resultados por eleição
eh_resultados <- x$cls == 0 && x$cons && x$tag == 16 && length(x$v) > 0 &&
all(map_lgl(x$v, "cons")) && length(x$v[[1]]$v) > 0 &&
x$v[[1]]$v[[1]]$cls == 0 && !x$v[[1]]$v[[1]]$cons && x$v[[1]]$v[[1]]$tag == 2 &&
any(map_lgl(x$v[[1]]$v, "cons"))
if (!eh_resultados) next
for (rve in x$v) {
ints <- map_dbl(Filter(function(y) y$cls == 0 && y$tag == 2, rve$v), ~ der_int(.x$v))
if (length(ints) >= 2 && is.na(res$aptos)) res$aptos <- ints[2]
for (rv in Filter(function(y) y$cons, rve$v)) for (resultado in rv$v) {
f <- resultado$v
comp <- Find(function(y) y$cls == 0 && y$tag == 2, f)
if (!is.null(comp)) res$comparecimento <- max(res$comparecimento, der_int(comp$v), na.rm = TRUE)
for (tvc in Filter(function(y) y$cls == 0 && y$cons && y$tag == 16, f)) for (cargo in tvc$v) {
cc <- cargo$v
cod <- as.character(der_int(Find(function(y) y$cls == 2, cc)$v))
nome_cargo <- if (cod %in% names(cargos)) cargos[[cod]] else paste0("cargo", cod)
for (lista in Filter(function(y) y$cls == 0 && y$tag == 16, cc)) for (vv in lista$v) {
campos <- Filter(function(y) y$cls == 2, vv$v)
names(campos) <- map_chr(campos, ~ as.character(.x$tag))
tipo <- if (!is.null(campos[["1"]])) der_int(campos[["1"]]$v) else 1
qtd <- if (!is.null(campos[["2"]])) der_int(campos[["2"]]$v) else 0
votavel <- if (tipo %in% c(1, 4) && !is.null(campos[["3"]])) {
as.character(der_int(tail(campos[["3"]]$v, 1)[[1]]$v)) # número do candidato
} else if (as.character(tipo) %in% names(tipos_voto)) {
tipos_voto[[as.character(tipo)]]
} else paste0("tipo", tipo)
votos[[length(votos) + 1]] <- list(cargo = nome_cargo, votavel = votavel, votos = qtd)
}
}
}
}
}
res$votos <- bind_rows(votos)
res
}Exemplo com a seção 0001, zona 0001, de Salvador (BA):
pasta_exemplo <- file.path(caminho_brutos, "ba", "38490", "0001", "0001")
bu_exemplo <- ler_bu(list.files(pasta_exemplo, pattern = "-bu\\.dat$", full.names = TRUE))
bu_exemplo[c("abertura", "encerramento", "aptos", "comparecimento")] |> as_tibble() |> kable()| abertura | encerramento | aptos | comparecimento |
|---|---|---|---|
| 20261025T080001 | 20261025T190126 | 414 | 335 |
| cargo | votavel | votos |
|---|---|---|
| presidente | 13 | 223 |
| presidente | 22 | 106 |
| presidente | branco | 2 |
| presidente | nulo | 4 |
3.3.3 Leitura do log da urna
O log é um arquivo .jez (um zip) com o texto
logd.dat. Cada linha tem data e hora, nível, número da
urna, aplicativo, mensagem e um código de verificação (hash), separados
por TAB. Veja as primeiras linhas do dia da eleição na seção de
exemplo:
# Lê todas as linhas de texto de um .jez, inclusive de .jez aninhados (urna original
# dentro do log da urna de contingência), convertendo de latin1 para UTF-8
ler_linhas_jez <- function(arquivo) {
membros <- unzip(arquivo, list = TRUE)
linhas <- character()
for (k in seq_len(nrow(membros))) {
nome <- membros$Name[k]
con <- unz(arquivo, nome, open = "rb")
bytes <- readBin(con, "raw", membros$Length[k]); close(con)
if (grepl("\\.jez$", nome, ignore.case = TRUE)) {
tmp <- tempfile(fileext = ".jez"); writeBin(bytes, tmp)
linhas <- c(linhas, tryCatch(ler_linhas_jez(tmp), error = function(e) character()))
unlink(tmp)
} else if (grepl("\\.(dat|log|txt)$", nome, ignore.case = TRUE)) {
txt <- iconv(rawToChar(bytes[bytes != as.raw(0)]), from = "latin1", to = "UTF-8")
linhas <- c(linhas, strsplit(txt, "\n", fixed = TRUE)[[1]])
}
}
linhas
}
log_exemplo <- ler_linhas_jez(list.files(pasta_exemplo, pattern = "-log\\.jez$", full.names = TRUE))
log_exemplo[startsWith(log_exemplo, data_eleicao)] |>
head(8) |>
strsplit("\t", fixed = TRUE) |>
map_dfr(~ tibble(data_hora = .x[1], nivel = .x[2], urna = .x[3],
aplicativo = .x[4], mensagem = .x[5])) |>
kable()| data_hora | nivel | urna | aplicativo | mensagem |
|---|---|---|---|---|
| 25/10/2026 06:28:23 | INFO | 02428153 | LOGD | Início das operações do logd |
| 25/10/2026 06:28:23 | INFO | 02428153 | LOGD | Urna ligada em 25/10/2026 às 06:27:46 |
| 25/10/2026 06:28:46 | INFO | 02428153 | GAP | Estrutura de diretórios da ME: OK |
| 25/10/2026 06:28:49 | INFO | 02428153 | GAP | Iniciando aplicação - 2º turno |
| 25/10/2026 06:28:49 | INFO | 02428153 | GAP | Versão da aplicação: 10.23.0.0 - Praia da Barra do Cahy |
| 25/10/2026 06:28:49 | INFO | 02428153 | GAP | Identificação do Modelo de Urna: UE2022 |
| 25/10/2026 06:28:50 | INFO | 02428153 | LOGD | Iniciando registro do log na MI e na ME |
| 25/10/2026 06:28:50 | INFO | 02428153 | GAP | Resultado da verificação da mídia externa estática : 0 |
A função ler_log() transforma essas milhares de linhas
em um resumo por urna. Ela:
- mantém só as linhas de 25/10/2026 e remove as repetidas (mesmo horário e hash);
- descobre o fuso pelo horário em que a urna ficou pronta: a abertura oficial é às 8h de Brasília, então uma urna que abriu às 6h locais está 2 horas atrás;
- conta os votos computados, inclusive depois das 17h, 18h e 19h de Brasília;
- mede a maior parada entre dois votos e identifica trocas de urna, falhas e o tipo de identificação de cada eleitor (biometria ou manual).
fuso_por_uf <- c(AC = -2, AM = -1, RO = -1, RR = -1, MT = -1, MS = -1)
seg_para_hora <- function(s) ifelse(is.na(s), NA_character_,
sprintf("%02d:%02d:%02d", s %/% 3600, (s %% 3600) %/% 60, s %% 60))
ler_log <- function(arquivo, uf, fuso = NULL) {
linhas <- ler_linhas_jez(arquivo)
linhas <- linhas[startsWith(linhas, data_eleicao)] # só o dia do 2º turno
partes <- strsplit(linhas, "\t", fixed = TRUE)
ok <- lengths(partes) >= 5
partes <- partes[ok]; linhas <- linhas[ok]
campo <- function(k) vapply(partes, `[`, "", k) # k-ésimo campo de cada linha
hora <- substr(linhas, 12, 19)
nivel <- campo(2); urna <- campo(3); msg <- campo(5)
manter <- !duplicated(paste(hora, vapply(partes, function(p) p[length(p)], "")))
ordem <- order(hora[manter])
hora <- hora[manter][ordem]; nivel <- nivel[manter][ordem]
urna <- urna[manter][ordem]; msg <- msg[manter][ordem]
s <- as.integer(substr(hora, 1, 2)) * 3600 + as.integer(substr(hora, 4, 5)) * 60 +
as.integer(substr(hora, 7, 8))
primeiro <- function(prefixo) { k <- which(startsWith(msg, prefixo)); if (length(k)) s[k[1]] else NA }
abertura <- primeiro("Urna pronta para receber votos")
encerramento <- suppressWarnings(min(c(primeiro("Procedimento de encerramento confirmado"),
primeiro("Inicio do Encerramento")), na.rm = TRUE))
if (!is.finite(encerramento)) encerramento <- NA
# fuso: a abertura oficial é às 8h de Brasília; a diferença revela o fuso local
# (se o fuso for informado, ele é usado diretamente; ver seção 3.3.5)
if (is.null(fuso)) {
fuso <- if (!is.na(abertura) && abertura >= 4 * 3600 && abertura <= 11 * 3600) {
min(1, max(-3, round((abertura - 8 * 3600) / 3600)))
} else if (uf %in% names(fuso_por_uf)) fuso_por_uf[[uf]] else 0
}
corr <- -fuso * 3600
votos <- s[startsWith(msg, "O voto do eleitor foi computado")] + corr
justif <- s[startsWith(msg, "Justificativa recebida")] + corr
intervalos <- diff(votos)
maior <- if (length(intervalos)) which.max(intervalos) else NA
tipo_hab <- sub(".*\\[(.*)\\].*", "\\1", msg[startsWith(msg, "Tipo de habilitação do eleitor")])
resp <- msg[grepl("Todas as pessoas presentes já votaram?", msg, fixed = TRUE)]
list(
resumo = tibble(
fuso_h = fuso,
n_urnas = length(unique(urna)),
contingencia = any(grepl("ontingência", msg) & grepl("procedimento|Modo de carga", msg)),
abertura_log_brt = seg_para_hora(abertura + corr),
encerramento_log_brt = seg_para_hora(encerramento + corr),
ultimo_voto_brt = if (length(votos)) seg_para_hora(max(votos)) else NA_character_,
votos_log = length(votos),
votos_pos17 = sum(votos >= 17 * 3600),
votos_pos18 = sum(votos >= 18 * 3600),
votos_pos19 = sum(votos >= 19 * 3600),
justificativas = length(justif),
justificativas_pos17 = sum(justif >= 17 * 3600),
maior_parada_min = if (is.na(maior)) NA else intervalos[maior] / 60,
# maior parada só entre votos dados antes das 17h (sem a espera da prorrogação)
maior_parada_ate17_min = {
iv <- diff(votos[votos < 17 * 3600])
if (length(iv)) max(iv) / 60 else NA_real_
},
inicio_maior_parada_brt = if (is.na(maior)) NA_character_ else seg_para_hora(votos[maior]),
erros = sum(nivel == "ERRO"),
erros_biometria = sum(nivel == "ERRO" & grepl("fingerscanner|digital|biométric|biometric", tolower(msg))),
vezes_ligada = sum(startsWith(msg, "Urna ligada em")),
hab_total = sum(startsWith(msg, "Eleitor foi habilitado")),
hab_biometrica = sum(tipo_hab == "biométrica"),
hab_manual = sum(startsWith(msg, "Solicitação de dado pessoal do eleitor para habilitação manual")),
digital_mesario = sum(startsWith(msg, "Digital coletada bate com uma digital gravada após o registro inicial de mesário")),
todos_votaram = if (length(resp)) trimws(sub(".*\\?", "", resp[1])) else NA_character_),
votos_hora = tabulate(pmin(23, pmax(0, votos %/% 3600)) + 1, nbins = 24))
}
ler_log(list.files(pasta_exemplo, pattern = "-log\\.jez$", full.names = TRUE), "BA")$resumo |>
select(fuso_h, contingencia, votos_log, votos_pos17, maior_parada_min,
inicio_maior_parada_brt, hab_total, hab_biometrica, hab_manual) |>
kable()| fuso_h | contingencia | votos_log | votos_pos17 | maior_parada_min | inicio_maior_parada_brt | hab_total | hab_biometrica | hab_manual |
|---|---|---|---|---|---|---|---|---|
| 0 | TRUE | 335 | 3 | 94.05 | 16:31:22 | 335 | 296 | 23 |
Essa urna de Salvador é um bom exemplo do que o log revela: o leitor biométrico falhou no fim da tarde, a urna ficou parada por mais de uma hora e meia e foi substituída por uma urna de contingência, que recebeu os últimos votos já depois das 17h.
3.3.4 Processamento de todas as urnas
As mesmas funções foram aplicadas às 497.897 seções, em paralelo,
estado por estado. Como a leitura leva cerca de 1 a 2 horas, ela foi
executada uma vez pelo script ler_dados.R (o mesmo código
abaixo), que salva o resultado de cada estado em
dados/cache/. O trecho abaixo não é
executado ao gerar este relatório; ele mostra o processo:
processar_secao <- function(uf, cd_mun, zona, secao) {
pasta <- file.path(caminho_brutos, tolower(uf), cd_mun, zona, secao)
arq <- list.files(pasta)
id <- tibble(uf = uf, cd_mun = cd_mun, zona = zona, secao = secao)
tryCatch({
bu <- ler_bu(file.path(pasta, arq[endsWith(arq, "-bu.dat")][1]))
lg <- ler_log(file.path(pasta, arq[endsWith(arq, "-log.jez")][1]), uf)
list(secao = bind_cols(id, tibble(bu_abertura = bu$abertura, bu_encerramento = bu$encerramento,
aptos = bu$aptos, comparecimento = bu$comparecimento),
lg$resumo),
votos = bind_cols(id[rep(1, nrow(bu$votos)), ], bu$votos),
horas = bind_cols(id, tibble(hora_brt = 0:23, votos = lg$votos_hora)) |> filter(votos > 0))
}, error = function(e) list(secao = mutate(id, erro = conditionMessage(e))))
}
for (u in unique(secoes$uf)) {
s <- filter(secoes, uf == u)
partes <- mclapply(seq_len(nrow(s)), function(k)
processar_secao(s$uf[k], s$cd_mun[k], s$zona[k], s$secao[k]), mc.cores = 12)
saveRDS(list(secao = bind_rows(map(partes, "secao")),
votos = bind_rows(map(partes, "votos")),
horas = bind_rows(map(partes, "horas"))),
file.path(caminho_dados, "cache", paste0(u, "_1.rds")))
}Aqui, o relatório apenas carrega esse resultado:
resultados <- list.files(file.path(caminho_dados, "cache"), pattern = "_1\\.rds$",
full.names = TRUE) |>
map(readRDS)
dados_secao <- bind_rows(map(resultados, "secao")) |>
left_join(select(secoes, uf, cd_mun, zona, secao, municipio),
by = c("uf", "cd_mun", "zona", "secao"))
votos <- bind_rows(map(resultados, "votos")) # formato longo: seção x cargo x votável
votos_hora <- bind_rows(map(resultados, "horas")) # formato longo: seção x hora
if (!"erro" %in% names(dados_secao)) dados_secao$erro <- NA_character_Foram lidas 497.897 seções, das quais 0 com erro de leitura.
3.3.5 Um problema encontrado: o fuso horário de urnas que abriram atrasadas
O fuso de cada urna foi inferido pelo horário em que ela ficou pronta para votar. Na primeira versão da análise, isso gerou um erro: urnas que abriram com atraso (por exemplo, às 9h por algum problema na instalação) eram tratadas como se estivessem uma hora à frente de Brasília, e todos os seus horários eram deslocados em uma hora. O sintoma era um grupo de urnas que pareciam ter encerrado às 16h, uma hora antes do horário oficial.
A correção usa o fato de que todas as urnas de um mesmo município estão no mesmo fuso: para cada município, o fuso adotado é o mais frequente entre as suas urnas (a moda). As urnas cujo fuso individual diverge do fuso do município têm o log relido com o fuso correto.
Aproveitando a releitura, também é calculada a maior parada antes das 17h. Nas urnas estendidas, o maior intervalo entre dois votos costuma ser a própria espera da prorrogação (a urna aberta depois das 17h sem eleitores), o que não é uma falha. Para comparar paradas de forma justa, o log de todas as urnas estendidas e de uma amostra aleatória de 20 mil urnas normais é relido com essa medida.
moda <- function(x) {
x <- x[!is.na(x)]
if (length(x) == 0) return(NA_real_)
as.numeric(names(which.max(table(x))))
}
chaves <- c("uf", "cd_mun", "zona", "secao")
dados_secao <- dados_secao |>
group_by(uf, cd_mun) |>
mutate(fuso_mun = moda(fuso_h)) |>
ungroup() |>
mutate(fuso_mun = coalesce(fuso_mun, unname(fuso_por_uf[uf]), 0),
fuso_divergente = !is.na(fuso_h) & fuso_h != fuso_mun)
# urnas a reler: fuso divergente, todas as estendidas e uma amostra de 20 mil normais
set.seed(2026)
candidatas <- dados_secao |>
filter(is.na(erro)) |>
mutate(enc = ymd_hms(bu_encerramento, tz = "UTC") - hours(fuso_mun),
grupo_relido = case_when(fuso_divergente ~ "fuso divergente",
hour(enc) * 60 + minute(enc) >= 17 * 60 + 45 ~ "estendida",
TRUE ~ "normal"))
reler <- bind_rows(filter(candidatas, grupo_relido != "normal"),
slice_sample(filter(candidatas, grupo_relido == "normal"),
n = min(20000, sum(candidatas$grupo_relido == "normal")))) |>
mutate(grupo_relido = ifelse(grupo_relido == "normal", "amostra normal", grupo_relido))
reler_log <- function(k) {
r <- reler[k, ]
tryCatch({
pasta <- file.path(caminho_brutos, tolower(r$uf), r$cd_mun, r$zona, r$secao)
lg <- ler_log(list.files(pasta, pattern = "-log\\.jez$", full.names = TRUE)[1],
r$uf, fuso = r$fuso_mun)
list(resumo = bind_cols(r[, chaves], lg$resumo),
horas = bind_cols(r[, chaves], tibble(hora_brt = 0:23, votos = lg$votos_hora)) |>
filter(votos > 0))
}, error = function(e) list(falha = TRUE))
}
nucleos <- if (.Platform$OS.type == "windows") 1 else max(1, detectCores() - 4)
relidos <- mclapply(seq_len(nrow(reler)), reler_log, mc.cores = nucleos)
ok <- vapply(relidos, function(p) is.list(p) && (isTRUE(p$falha) || !is.null(p$resumo)), logical(1))
relidos[!ok] <- lapply(which(!ok), reler_log) # refaz em série o que o paralelo perdeu
novos_resumos <- bind_rows(map(relidos, "resumo"))
novas_horas <- bind_rows(map(relidos, "horas"))
# substitui os dados do log das urnas relidas
dados_secao <- dados_secao |>
mutate(maior_parada_ate17_min = NA_real_) |>
rows_update(novos_resumos, by = chaves, unmatched = "ignore") |>
left_join(select(reler, all_of(chaves), grupo_relido), by = chaves)
# substitui a distribuição horária das urnas com fuso divergente
ids_fuso <- filter(reler, grupo_relido == "fuso divergente") |> select(all_of(chaves))
votos_hora <- votos_hora |>
anti_join(ids_fuso, by = chaves) |>
bind_rows(semi_join(novas_horas, ids_fuso, by = chaves))reler |>
count(grupo_relido, name = "urnas") |>
kable(col.names = c("Motivo da releitura", "Urnas"), format.args = list(big.mark = "."))| Motivo da releitura | Urnas |
|---|---|
| amostra normal | 20.000 |
| estendida | 7.349 |
| fuso divergente | 327 |
dados_secao |>
filter(fuso_divergente) |>
count(uf, sort = TRUE, name = "urnas_corrigidas") |>
head(10) |>
kable(col.names = c("UF", "Urnas com fuso corrigido"))| UF | Urnas com fuso corrigido |
|---|---|
| SP | 48 |
| RJ | 47 |
| BA | 42 |
| MG | 30 |
| PA | 22 |
| MA | 20 |
| PE | 19 |
| PR | 18 |
| CE | 13 |
| AM | 9 |
3.3.6 Limpeza e novas variáveis
Com os dados lidos, as etapas de limpeza são:
- Votos do formato longo para o largo: o boletim traz
uma linha por candidato; para comparar urnas, é melhor uma coluna por
candidato (
v_13,v_22,v_branco,v_nulo). - Converter os horários do boletim (texto como
20261025T190126, em hora local) para data-hora e corrigir o fuso, para que todas as urnas fiquem no horário de Brasília. - Classificar cada urna pelo horário de encerramento.
- Juntar os dados da seção com os votos e criar variáveis: votos válidos, % do 13, abstenção, % de eleitores sem biometria e a % do 13 no restante do município.
# 1. votos de presidente: formato longo -> largo
votos_presidente <- votos |>
filter(cargo == "presidente") |>
pivot_wider(id_cols = c(uf, cd_mun, zona, secao), names_from = votavel,
values_from = votos, names_prefix = "v_", values_fill = 0)
regioes <- c(AC = "Norte", AM = "Norte", AP = "Norte", PA = "Norte", RO = "Norte",
RR = "Norte", TO = "Norte", AL = "Nordeste", BA = "Nordeste", CE = "Nordeste",
MA = "Nordeste", PB = "Nordeste", PE = "Nordeste", PI = "Nordeste",
RN = "Nordeste", SE = "Nordeste", DF = "Centro-Oeste", GO = "Centro-Oeste",
MS = "Centro-Oeste", MT = "Centro-Oeste", ES = "Sudeste", MG = "Sudeste",
RJ = "Sudeste", SP = "Sudeste", PR = "Sul", RS = "Sul", SC = "Sul")
urnas <- dados_secao |>
filter(is.na(erro)) |>
mutate(
# 2. hora local do boletim -> horário de Brasília
fuso_h = fuso_mun, # fuso do município (3.3.5)
abertura_brt = ymd_hms(bu_abertura, tz = "UTC") - hours(fuso_h),
encerramento_brt = ymd_hms(bu_encerramento, tz = "UTC") - hours(fuso_h),
enc_min = hour(encerramento_brt) * 60 + minute(encerramento_brt),
# 3. classificação da urna
situacao = cut(enc_min, breaks = c(-Inf, 17 * 60, 17 * 60 + 45, Inf), right = FALSE,
labels = c("antes das 17h", "17h a 17h45", "estendida (após 17h45)")),
estendida = situacao == "estendida (após 17h45)",
regiao = factor(unname(regioes[uf]),
levels = c("Norte", "Nordeste", "Centro-Oeste", "Sudeste", "Sul"))) |>
select(-erro, -bu_abertura, -bu_encerramento) |>
# 4. junção com os votos e novas variáveis
inner_join(votos_presidente, by = c("uf", "cd_mun", "zona", "secao")) |>
mutate(validos = v_13 + v_22,
pct_13 = v_13 / validos,
abstencao = 1 - comparecimento / aptos,
pct_sem_biometria = 1 - hab_biometrica / hab_total) |>
group_by(uf, cd_mun) |>
mutate(pct_13_resto_mun = (sum(v_13) - v_13) / (sum(validos) - validos),
dif_vs_municipio = pct_13 - pct_13_resto_mun) |>
ungroup()Conferência: a soma dos boletins dá 127.854.631 eleitores que compareceram, 64.056.078 votos para o 13 e 59.654.544 para o 22 (sem o exterior), o que permite comparar com o resultado oficial divulgado pelo TSE.
3.4 O conjunto de dados final
A base final, urnas, tem 497.897 linhas
(uma por urna) e 49 colunas. Abaixo, uma amostra de 100
urnas com as principais colunas:
set.seed(2026)
urnas |>
slice_sample(n = 100) |>
transmute(uf, municipio, zona, secao,
encerramento = format(encerramento_brt, "%H:%M"), situacao,
aptos, comparecimento, v_13, v_22, votos_pos17,
contingencia, maior_parada_min = round(maior_parada_min, 1)) |>
datatable(rownames = FALSE, options = list(pageLength = 10, scrollX = TRUE))3.5 Resumo das variáveis de interesse
descricoes <- tribble(
~variavel, ~descricao,
"aptos", "Eleitores aptos a votar na seção",
"comparecimento", "Eleitores que votaram",
"v_13", "Votos para o candidato 13",
"v_22", "Votos para o candidato 22",
"pct_13", "% do 13 nos votos válidos da seção",
"abstencao", "% de aptos que não votaram",
"enc_min", "Encerramento da urna (minutos desde 0h, horário de Brasília)",
"votos_pos17", "Votos computados às 17h ou depois",
"maior_parada_min", "Maior intervalo entre dois votos (minutos)",
"pct_sem_biometria", "% de eleitores liberados sem biometria",
"erros", "Eventos de erro registrados no log")
resumo_num <- function(v) {
x <- urnas[[v]]
tibble(variavel = v, minimo = min(x, na.rm = TRUE), mediana = median(x, na.rm = TRUE),
media = mean(x, na.rm = TRUE), maximo = max(x, na.rm = TRUE), ausentes = sum(is.na(x)))
}
descricoes |>
left_join(map_dfr(descricoes$variavel, resumo_num), by = "variavel") |>
mutate(across(minimo:maximo, ~ ifelse(abs(.x) < 1 & .x != 0, round(.x, 3), round(.x, 1)))) |>
kable(col.names = c("Variável", "Descrição", "Mínimo", "Mediana", "Média", "Máximo", "Ausentes"),
format.args = list(big.mark = ".", decimal.mark = ","))| Variável | Descrição | Mínimo | Mediana | Média | Máximo | Ausentes |
|---|---|---|---|---|---|---|
| aptos | Eleitores aptos a votar na seção | 20,0 | 327,000 | 317,000 | 534,000 | 0 |
| comparecimento | Eleitores que votaram | 9,0 | 263,000 | 256,800 | 481,000 | 0 |
| v_13 | Votos para o candidato 13 | 0,0 | 124,000 | 128,700 | 382,000 | 0 |
| v_22 | Votos para o candidato 22 | 0,0 | 122,000 | 119,800 | 363,000 | 0 |
| pct_13 | % do 13 nos votos válidos da seção | 0,0 | 0,502 | 0,521 | 1,000 | 0 |
| abstencao | % de aptos que não votaram | 0,0 | 0,185 | 0,190 | 0,916 | 0 |
| enc_min | Encerramento da urna (minutos desde 0h, horário de Brasília) | 1.020,0 | 1.021,000 | 1.023,600 | 1.384,000 | 0 |
| votos_pos17 | Votos computados às 17h ou depois | 0,0 | 0,000 | 0,069 | 39,000 | 1 |
| maior_parada_min | Maior intervalo entre dois votos (minutos) | 1,9 | 16,500 | 18,800 | 478,900 | 1 |
| pct_sem_biometria | % de eleitores liberados sem biometria | 0,0 | 0,107 | 0,125 | 1,000 | 1 |
| erros | Eventos de erro registrados no log | 0,0 | 1,000 | 1,100 | 45,000 | 1 |
Em resumo: a urna típica tinha cerca de 327 eleitores aptos, com abstenção mediana de 18,5%. A grande maioria das urnas encerrou poucos minutos depois das 17h, e a mediana de votos depois das 17h por urna é 0: em geral, apenas a fila que já estava formada.
4. Análise exploratória
4.1 Quando as urnas fecharam?
situacao_resumo <- urnas |>
count(situacao, name = "urnas") |>
mutate(percentual = urnas / sum(urnas))
situacao_resumo |>
mutate(percentual = pct(percentual, 2), urnas = num(urnas)) |>
kable(col.names = c("Situação no encerramento", "Urnas", "% das urnas"))| Situação no encerramento | Urnas | % das urnas |
|---|---|---|
| 17h a 17h45 | 490.540 | 98,52% |
| estendida (após 17h45) | 7.357 | 1,48% |
urnas |>
filter(enc_min >= 16 * 60 + 30, enc_min <= 20 * 60) |>
ggplot(aes(x = enc_min / 60)) +
geom_histogram(binwidth = 5 / 60, fill = "#2f6db5") +
geom_vline(xintercept = c(17, 17.75), linetype = "dashed", colour = "grey40") +
scale_x_continuous(breaks = seq(16.5, 20, 0.5),
labels = function(h) sprintf("%02d:%02d", floor(h), round((h %% 1) * 60))) +
scale_y_log10(labels = label_number(big.mark = ".")) +
labs(title = "Horário de encerramento das urnas (horário de Brasília)",
subtitle = "Escala logarítmica; linhas tracejadas em 17h e 17h45",
x = "Horário de encerramento", y = "Número de urnas (escala log)") +
theme_minimal()Quase todas as urnas encerraram entre 17h e 17h10. As 7.357 urnas estendidas (1,48% do total) formam uma cauda pequena, que se estende até perto das 19h.
4.2 Onde ficaram as urnas estendidas?
por_uf <- urnas |>
group_by(uf, regiao) |>
summarise(urnas = n(), estendidas = sum(estendida),
pct_estendidas = mean(estendida),
votos_apos_17h = sum(votos_pos17, na.rm = TRUE), .groups = "drop") |>
arrange(desc(pct_estendidas))
grafico_uf <- por_uf |>
ggplot(aes(x = reorder(uf, pct_estendidas), y = pct_estendidas, fill = regiao,
text = paste0(uf, ": ", num(estendidas), " de ", num(urnas), " urnas (",
pct(pct_estendidas, 2), ")"))) +
geom_col() +
coord_flip() +
scale_y_continuous(labels = label_percent(accuracy = 0.1)) +
labs(title = "Percentual de urnas estendidas por estado", x = NULL,
y = "% das urnas do estado", fill = "Região") +
theme_minimal()
ggplotly(grafico_uf, tooltip = "text")por_municipio <- urnas |>
group_by(uf, municipio) |>
summarise(urnas = n(),
estendidas = sum(estendida),
votos_17h_estendidas = sum(votos_pos17[estendida], na.rm = TRUE),
votos_17h_demais = sum(votos_pos17[!estendida], na.rm = TRUE),
.groups = "drop") |>
filter(estendidas > 0) |>
arrange(desc(estendidas))
datatable(por_municipio, rownames = FALSE, caption = "Municípios com urnas estendidas",
colnames = c("UF", "Município", "Urnas", "Estendidas",
"Votos após 17h (estendidas)", "Votos após 17h (demais urnas)"),
options = list(pageLength = 10))# concentração: quanto das urnas estendidas está nos 4 municípios com mais estendidas
top4 <- head(por_municipio, 4)
pct_top4 <- sum(top4$estendidas) / sum(urnas$estendida)
ufs_concentradas <- por_uf |> filter(pct_estendidas >= 0.005) |> pull(uf)As urnas estendidas estão muito concentradas. Os quatro municípios com mais urnas estendidas (RIO DE JANEIRO (RJ), SÃO PAULO (SP), SALVADOR (BA), NITERÓI (RJ)) reúnem 68,3% de todas elas. Apenas 6 estados (RJ, PE, PB, BA, SP, SE) tiveram mais de 0,5% das urnas estendidas; nos demais, a prorrogação praticamente não teve efeito.
4.3 Quantos votos entraram depois das 17h?
votos_hora |>
group_by(hora_brt) |>
summarise(votos = sum(votos)) |>
filter(hora_brt >= 7, hora_brt <= 20) |>
mutate(periodo = ifelse(hora_brt >= 17, "17h ou depois", "Até 17h")) |>
ggplot(aes(x = factor(hora_brt), y = votos, fill = periodo)) +
geom_col() +
scale_y_continuous(labels = label_number(scale = 1e-6, suffix = " mi", decimal.mark = ",")) +
scale_fill_manual(values = c("Até 17h" = "grey70", "17h ou depois" = "#c0392b")) +
labs(title = "Votos computados por hora do dia (horário de Brasília)",
x = "Hora", y = "Votos", fill = NULL) +
theme_minimal()total_pos17 <- sum(urnas$votos_pos17, na.rm = TRUE)
pos17_estendidas <- sum(urnas$votos_pos17[urnas$estendida], na.rm = TRUE)
margem <- sum(urnas$v_13) - sum(urnas$v_22)Foram registrados 34.330 votos depois das 17h, o que corresponde a 0,027% de todos os votos. Desses, 5.052 foram dados nas urnas estendidas; o restante foi dado nas urnas que encerraram normalmente, com a fila que já estava formada às 17h, o que é previsto pela lei em qualquer eleição. Em média, cada urna estendida recebeu 0,7 votos depois das 17h: a procura durante a prorrogação foi baixa.
O voto é secreto: o log não diz em quem cada eleitor votou. Mas é possível calcular o pior cenário. A diferença entre os dois candidatos nas urnas analisadas foi de 4.401.534 votos. Mesmo que todos os votos depois das 17h tivessem ido para o mesmo candidato, eles representariam 0,78% dessa diferença.
4.4 Por que algumas urnas atrasaram?
As urnas estendidas são diferentes das demais? A tabela compara as urnas estendidas com a amostra aleatória de 20 mil urnas que encerraram normalmente. A parada é medida só até as 17h, para não contar a espera da prorrogação.
comparacao_atraso <- urnas |>
filter(estendida | grupo_relido %in% "amostra normal") |>
mutate(grupo = ifelse(estendida, "Estendida", "Encerramento normal (amostra)")) |>
group_by(grupo) |>
summarise(urnas = n(),
aptos = mean(aptos),
comparecimento = mean(comparecimento / aptos),
troca_de_urna = mean(contingencia, na.rm = TRUE),
parada_30min = mean(maior_parada_ate17_min >= 30, na.rm = TRUE),
parada_mediana = median(maior_parada_ate17_min, na.rm = TRUE),
sem_biometria = mean(pct_sem_biometria, na.rm = TRUE),
erros = mean(erros, na.rm = TRUE))
comparacao_atraso |>
mutate(urnas = num(urnas), aptos = num(aptos),
across(c(comparecimento, troca_de_urna, parada_30min, sem_biometria), ~ pct(.x, 1)),
parada_mediana = num(parada_mediana, 1), erros = num(erros, 2)) |>
kable(col.names = c("Grupo", "Urnas", "Aptos (média)", "Comparecimento",
"Teve troca de urna", "Parou ≥ 30 min antes das 17h",
"Maior parada antes das 17h (mediana, min)",
"Eleitores sem biometria", "Erros no log (média)"))| Grupo | Urnas | Aptos (média) | Comparecimento | Teve troca de urna | Parou ≥ 30 min antes das 17h | Maior parada antes das 17h (mediana, min) | Eleitores sem biometria | Erros no log (média) |
|---|---|---|---|---|---|---|---|---|
| Encerramento normal (amostra) | 20.000 | 316 | 81,0% | 0,3% | 6,7% | 16,4 | 12,5% | 1,08 |
| Estendida | 7.357 | 361 | 80,6% | 0,8% | 2,5% | 14,7 | 12,9% | 1,14 |
Essa comparação ajuda a separar duas explicações: urnas que ficaram abertas porque tinham fila (seções maiores, comparecimento alto) e urnas que atrasaram porque tiveram problemas durante o dia (troca de urna, paradas longas, falhas de biometria).
4.5 As urnas estendidas votaram diferente?
Comparar a % do 13 nas urnas estendidas com a média nacional seria enganoso: se as urnas estendidas estão concentradas em certos estados, elas herdam o perfil político desses lugares. Por isso, cada urna é comparada com o restante do seu próprio município. O valor “esperado” é a % que o 13 teria se essas urnas votassem como as vizinhas.
comparacao <- urnas |>
filter(!is.na(pct_13_resto_mun)) |>
group_by(situacao) |>
summarise(urnas = n(),
pct_observado = sum(v_13) / sum(validos),
pct_esperado = sum(pct_13_resto_mun * validos) / sum(validos)) |>
mutate(diferenca_pp = 100 * (pct_observado - pct_esperado))
comparacao |>
mutate(urnas = num(urnas), pct_observado = pct(pct_observado, 2),
pct_esperado = pct(pct_esperado, 2), diferenca_pp = num(diferenca_pp, 2)) |>
kable(col.names = c("Situação", "Urnas", "% do 13 observado",
"% do 13 esperado (pelo município)", "Diferença (p.p.)"))| Situação | Urnas | % do 13 observado | % do 13 esperado (pelo município) | Diferença (p.p.) |
|---|---|---|---|---|
| 17h a 17h45 | 490.540 | 51,72% | 51,70% | 0,02 |
| estendida (após 17h45) | 7.357 | 55,27% | 56,78% | -1,52 |
urnas |>
filter(!is.na(dif_vs_municipio), situacao != "antes das 17h") |>
ggplot(aes(x = 100 * dif_vs_municipio, fill = situacao)) +
geom_density(alpha = 0.5, colour = NA) +
geom_vline(xintercept = 0, linetype = "dashed") +
coord_cartesian(xlim = c(-40, 40)) +
labs(title = "Diferença entre a % do 13 na urna e no restante do município",
x = "Diferença (pontos percentuais)", y = "Densidade", fill = NULL) +
theme_minimal() +
theme(legend.position = "top")Como ler o gráfico: o eixo horizontal é a diferença, em pontos percentuais, entre a % do 13 na urna e no restante do município (zero = votou igual aos vizinhos). A densidade é um histograma suavizado: cada curva tem área igual a 1, o que permite comparar grupos de tamanhos muito diferentes (algumas mil urnas estendidas contra centenas de milhares de urnas normais). O boxplot abaixo mostra a mesma comparação: a linha do meio de cada caixa é a mediana, e a caixa contém metade das urnas.
urnas |>
filter(!is.na(dif_vs_municipio), situacao != "antes das 17h") |>
ggplot(aes(x = situacao, y = 100 * dif_vs_municipio)) +
geom_boxplot(outlier.shape = NA, fill = "grey90") +
geom_hline(yintercept = 0, linetype = "dashed") +
coord_cartesian(ylim = c(-30, 30)) +
labs(title = "Diferença entre a % do 13 na urna e no restante do município",
x = NULL, y = "Diferença (pontos percentuais)") +
theme_minimal()dif_est <- comparacao$diferenca_pp[comparacao$situacao == "estendida (após 17h45)"]
mediana_est <- 100 * median(urnas$dif_vs_municipio[urnas$estendida], na.rm = TRUE)
leitura_dif <- if (abs(dif_est) < 1) {
"praticamente igual à das urnas vizinhas"
} else if (dif_est > 0) {
"maior que a das urnas vizinhas"
} else {
"menor que a das urnas vizinhas"
}Nas urnas estendidas, a % do 13 foi menor que a das urnas vizinhas: a diferença em relação ao esperado pelo município é de -1,52 pontos percentuais (mediana por urna: -1,20 p.p.). Mesmo uma diferença não nula não significa que a prorrogação “favoreceu” alguém: as urnas estendidas ficam em bairros com fila e com problemas, que podem ter um perfil próprio. E como a decisão chegou no fim da tarde, quase todos os eleitores dessas urnas votaram antes das 17h, quando a votação era igual para todos.
5. Conclusões
n_estendidas <- sum(urnas$estendida)
n_antes <- sum(urnas$situacao == "antes das 17h", na.rm = TRUE)5.1 O problema
A prorrogação da votação até as 19h, decidida no fim da tarde de 25/10/2026, gerou dúvidas sobre quantas urnas e quantos votos foram afetados e se ela favoreceu algum candidato. A análise buscou responder essas perguntas com os arquivos oficiais de cada urna, e não com estimativas ou relatos.
5.2 A abordagem
Foram lidos os boletins e os logs oficiais das 497.897 urnas do país (sem o exterior). Para isso, foram escritos em R um leitor para o formato binário do boletim (ASN.1) e um leitor para os logs compactados, inclusive os de urnas substituídas durante o dia. Cada urna foi classificada pelo horário real de encerramento, convertido para o horário de Brasília, e comparada com as demais urnas do seu próprio município.
5.3 Principais resultados
- Poucas urnas foram estendidas: 7.357 urnas (1,48%) ficaram abertas depois das 17h45. Outras 0 encerraram antes das 17h.
- A prorrogação foi um fenômeno concentrado: os quatro municípios com mais urnas estendidas reúnem 68,3% delas, e só 6 estados tiveram mais de 0,5% das urnas estendidas.
- Poucos votos entraram depois das 17h: 34.330 votos no país, dos quais 5.052 nas urnas estendidas, ou 0,7 por urna estendida.
- Os votos depois das 17h não poderiam mudar o resultado: mesmo que todos fossem para o mesmo candidato, representariam 0,78% da diferença entre os candidatos (4.401.534 votos).
- Perfil das urnas estendidas: ver tabela da seção 4.4.
- No voto, a % do 13 nas urnas estendidas foi menor que a das urnas vizinhas, com diferença de -1,52 p.p. em relação ao esperado pelo município.
5.4 Implicações
Para quem acompanha o debate sobre a prorrogação, os dados permitem dimensionar o seu efeito com números verificáveis: quantas urnas ela atingiu, onde estavam e quantos votos trouxe. A comparação entre os votos depois das 17h e a diferença entre os candidatos mostra se ela teria condição de alterar o resultado. Para a Justiça Eleitoral, a relação entre urnas estendidas, trocas de urna e paradas longas indica onde agir para reduzir atrasos em eleições futuras.
5.5 Limitações
- O voto é secreto: não é possível saber em quem votou quem chegou depois das 17h; a análise trabalha com o pior cenário e com a comparação entre urnas.
- As seções no exterior não foram incluídas.
- O fuso de cada urna foi inferido pelo horário de abertura no log.
- A comparação com o município mostra associação, não causa: urnas estendidas podem diferir das vizinhas por outros motivos (bairro, perfil do eleitorado).
- Uma extensão possível é cruzar os dados com o perfil do eleitorado por seção (idade, escolaridade), também publicado pelo TSE, e com mapas por bairro.
Referências
- TSE, Portal de Resultados – Dados de urna: https://resultados.tse.jus.br/oficial/app/index.html
- Mix Vale (25/10/2026). Zanin esclarece que prorrogação até as 19h vale apenas para seções com urnas abertas. https://www.mixvale.com.br/2026/10/25/apuracao-dos-votos-zanin-esclarece-que-prorrogacao-ate-as-19h-vale-apenas-para-secoes-com-urnas-abertas/
- Poder360. STF prorroga eleição em 2h. https://www.poder360.com.br/poder-eleicoes-2026/zanin-prorroga-votacao-do-2o-turno-ate-19h/