Em 15 de agosto de 2019, um despacho do Presidente da República suspendeu o uso de radares estáticos, móveis e portáteis nas rodovias federais. A justificativa oficial foi evitar o uso “meramente arrecadatório” da fiscalização eletrônica. A medida só foi revertida por decisão judicial: em 11 de dezembro de 2019, a 1ª Vara Federal Cível do DF, atendendo ao Ministério Público Federal, mandou a Polícia Rodoviária Federal (PRF) voltar a usar os radares (Agência Brasil).
Durante cerca de quatro meses, portanto, o país viveu um experimento natural: a mesma polícia, nas mesmas rodovias, com muito menos fiscalização eletrônica de velocidade. A pergunta deste trabalho é simples de enunciar e importante para quem desenha políticas públicas:
Quando a fiscalização eletrônica de velocidade diminuiu, os acidentes e as mortes nas rodovias federais aumentaram?
Esse é um debate que costuma ser travado com opiniões (“radar é indústria da multa” versus “radar salva vidas”). Os dados abertos da PRF permitem trocar a opinião por evidência.
São usadas duas bases públicas da PRF:
A estratégia é comparar, em cada ano, a janela de 15/08 a 11/12 (o período em que os radares ficaram suspensos em 2019) com o período anterior do mesmo ano (01/01 a 14/08). Em anos normais (2017 e 2018), essa razão “fim do ano / começo do ano” reflete apenas a sazonalidade. Se em 2019 ela for diferente, a diferença é um indício do efeito da suspensão. É a lógica de uma diferença em diferenças, aplicada de forma descritiva. Como uma comparação desse tipo pode enganar, o resultado é depois submetido a testes mais rigorosos.
Gestores de trânsito, a própria PRF, o Ministério Público, a imprensa e qualquer cidadão interessado em saber se uma mudança de política de fiscalização teve consequências mensuráveis em segurança viária. A análise também mostra o que os dados conseguem e o que não conseguem afirmar, o que é tão importante quanto o resultado.
library(data.table) # leitura rápida de arquivos grandes (milhões de linhas)
library(dplyr) # manipulação de dados (filter, mutate, group_by, summarise, joins)
library(tidyr) # reformatação de dados (pivot_longer, pivot_wider, separate)
library(stringr) # tratamento de textos
library(lubridate) # tratamento de datas
library(ggplot2) # gráficos
library(plotly) # gráficos interativos
library(DT) # tabelas interativas
library(kableExtra) # tabelas estáticas formatadas
library(leaflet) # mapas interativos
library(scales) # formatação de números e percentuais nos gráficos
# percentuais no padrão brasileiro (vírgula decimal)
percent <- function(x, accuracy = 1, ...) {
return(scales::percent(x, accuracy = accuracy, decimal.mark = ",", big.mark = ".", ...))
}
| Pacote | Para que foi usado |
|---|---|
data.table |
Ler os arquivos de infrações, que somam mais de 10 milhões
de linhas, em segundos (com read.csv seriam vários
minutos) |
dplyr |
Filtrar, criar variáveis, agrupar, resumir e juntar tabelas |
tidyr |
Separar colunas e converter tabelas entre os formatos largo e longo |
stringr |
Padronizar textos (municípios, causas) |
lubridate |
Converter datas e extrair semana, mês e dia do ano |
ggplot2 |
Construir os gráficos |
plotly |
Tornar os gráficos interativos (zoom, tooltip) |
DT |
Tabelas interativas com busca e ordenação |
kableExtra |
Tabelas de resumo formatadas |
leaflet |
Mapa interativo dos acidentes |
scales |
Formatar eixos em milhares e percentuais |
Todos os dados vêm do Portal de Dados Abertos da PRF, seções “Acidentes” (arquivos agrupados por ocorrência) e “Infrações”. O significado de cada coluna está no dicionário de dados de acidentes.
Acidentes. A PRF registra cada acidente que atende nas rodovias federais por meio do Boletim de Acidente de Trânsito. Cada linha é uma ocorrência, com 30 colunas: data, hora, estado (UF), rodovia (BR), quilômetro, município, causa principal, tipo de acidente, condições da pista e do tempo, contagem de pessoas por desfecho (ilesos, feridos leves, feridos graves, mortos), número de veículos e coordenadas geográficas. Há um arquivo por ano, de 2007 em diante; aqui são usados 2017, 2018 e 2019.
Infrações. Cada linha é um auto de infração lavrado pela PRF, com 22 colunas: data, hora, UF, rodovia, quilômetro, código e descrição da infração, se houve abordagem do condutor e medições (como a velocidade e o excesso verificados). A PRF publica um arquivo compactado por ano, com um CSV por mês.
As peculiaridades que exigem tratamento:
latin1, separados por ; e usam vírgula
como separador decimal (em quilômetro, latitude e longitude).
Lidos com as configurações padrão do R, esses números viram texto e os
acentos quebram;dat_infracao, tip_abordagem…), um CSV por mês
nomeado jan.csv, fev.csv…; o de 2019 é
separado por ;, em UTF-8 com marca de BOM, com nomes longos
e acentuados (Data da Infração (DD/MM/AAAA)), em arquivos
infracoes_2019_01.csv… Curiosamente, a coluna se chama
“DD/MM/AAAA”, mas as datas estão no formato AAAA-MM-DD;-1 quando não é conhecida;Os arquivos são baixados apenas se ainda não existirem na pasta
dados/brutos. Os endereços ficam em uma tabela
(data.frame), e um laço for percorre cada
linha.
pasta_brutos <- "dados/brutos"
dir.create(pasta_brutos, recursive = TRUE, showWarnings = FALSE)
url_drive <- "https://drive.usercontent.google.com/download?export=download&confirm=t&id="
arquivos <- data.frame(
arquivo = c("acidentes_2017.zip", "acidentes_2018.zip", "acidentes_2019.zip",
"infracoes_2018.zip", "infracoes_2019.zip"),
id = c("1HPLWt5f_l4RIX3tKjI4tUXyZOev52W0N", "1cM4IgGMIiR-u4gBIH5IEe3DcvBvUzedi",
"1pN3fn2wY34GH6cY-gKfbxRJJBFE0lb_l", "1eeGZv7CUzSYS1KTq4If0QLOmaoQ8J3zB",
"1R5LiAULtB22HP0EkG8GSwzTTSAvbp35L")
)
options(timeout = 1200) # os arquivos de infrações têm ~150 MB cada
for (i in seq_len(nrow(arquivos))) {
destino <- file.path(pasta_brutos, arquivos$arquivo[i])
if (!file.exists(destino)) {
download.file(paste0(url_drive, arquivos$id[i]), destino, mode = "wb")
}
}
read.csv diretoOlhando o arquivo bruto de acidentes, aparecem os três problemas de
uma vez: o separador é ;, os números decimais usam vírgula
(136,9, -4,766018) e o texto está em
latin1, não em UTF-8 (abaixo, os caracteres acentuados que
o R não reconhece aparecem como ?).
zip_2019 <- file.path(pasta_brutos, "acidentes_2019.zip")
csv_2019 <- unzip(zip_2019, list = TRUE)$Name[1]
linhas <- readLines(unz(zip_2019, csv_2019), n = 2)
linhas_ascii <- iconv(linhas, from = "latin1", to = "ASCII", sub = "?")
substr(linhas_ascii, 1, 170)
## [1] "\"id\";\"data_inversa\";\"dia_semana\";\"horario\";\"uf\";\"br\";\"km\";\"municipio\";\"causa_acidente\";\"tipo_acidente\";\"classificacao_acidente\";\"fase_dia\";\"sentido_via\";\"condicao_metereo"
## [2] "182256;2019-01-01;\"ter?a-feira\";\"04:00:00\";\"CE\";116;136,9;\"RUSSAS\";\"Animais na Pista\";\"Atropelamento de Animal\";\"Sem V?timas\";\"Amanhecer\";\"Decrescente\";\"Vento\";\"Simples\";"
Com as configurações padrão, o read.csv nem consegue ler
o arquivo: as vírgulas decimais são confundidas com separadores de
coluna, e as linhas passam a ter mais colunas do que o cabeçalho.
read.csv(unz(zip_2019, csv_2019), nrows = 3)
## Error in `read.table()`:
## ! more columns than column names
A função abaixo lê um ano de acidentes já com as configurações
corretas (separador ;, decimal , e encoding
latin1). Ela é aplicada aos três anos com
lapply, e os resultados são empilhados.
ler_acidentes <- function(ano) {
zip <- file.path(pasta_brutos, paste0("acidentes_", ano, ".zip"))
csv <- unzip(zip, exdir = tempdir())
dados <- fread(csv, sep = ";", dec = ",", encoding = "Latin-1", showProgress = FALSE)
file.remove(csv)
dados$ano <- ano
return(dados)
}
acidentes_brutos <- rbindlist(lapply(2017:2019, ler_acidentes))
dim(acidentes_brutos)
## [1] 226458 31
Para lidar com os dois layouts, as diferenças entre anos ficam descritas em uma lista: para cada ano, o separador, o encoding e quais colunas correspondem a data, abordagem, UF e código da infração. Assim, uma única função lê os dois anos.
Só interessam as infrações de excesso de velocidade, que no Código de Trânsito têm três códigos, conforme o tamanho do excesso:
codigos_velocidade <- c(
"74550" = "até 20% acima do limite",
"74630" = "entre 20% e 50% acima",
"74710" = "mais de 50% acima"
)
layouts <- list(
"2018" = list(sep = ",", enc = "Latin-1",
colunas = c("dat_infracao", "tip_abordagem", "uf_infracao", "cod_infracao")),
"2019" = list(sep = ";", enc = "UTF-8",
colunas = c(2, 3, 8, 12)) # nomes acentuados/BOM: seleção por posição
)
ler_infracoes <- function(ano) {
spec <- layouts[[as.character(ano)]]
zip <- file.path(pasta_brutos, paste0("infracoes_", ano, ".zip"))
csvs <- unzip(zip, exdir = file.path(tempdir(), ano))
csvs <- csvs[grepl("\\.csv$", csvs)]
meses <- lapply(csvs, function(arq) {
d <- fread(arq, sep = spec$sep, encoding = spec$enc, select = spec$colunas,
colClasses = "character", showProgress = FALSE)
setnames(d, c("data", "abordagem", "uf", "codigo"))
total <- nrow(d)
d <- d[codigo %in% names(codigos_velocidade)] # só velocidade
attr(d, "total_linhas") <- total
return(d)
})
linhas_lidas <- sum(sapply(meses, function(m) attr(m, "total_linhas")))
unlink(file.path(tempdir(), ano), recursive = TRUE)
# agrega por dia, UF, abordagem e faixa de excesso
agregado <- rbindlist(meses)[, .(multas = .N), by = .(data, uf, abordagem, codigo)]
agregado$ano <- ano
return(list(agregado = agregado, linhas_lidas = linhas_lidas))
}
infracoes_lidas <- lapply(2018:2019, ler_infracoes)
linhas_infracoes <- sapply(infracoes_lidas, function(x) x$linhas_lidas)
multas_brutas <- rbindlist(lapply(infracoes_lidas, function(x) x$agregado))
Foram lidas 13.098.269 linhas de infrações (7.356.159 em 2018 e 5.742.110 em 2019). Depois do filtro de velocidade e da agregação por dia, UF, abordagem e faixa de excesso, restam 37.044 linhas: uma redução de mais de 99% sem perder nenhuma informação necessária para a pergunta.
Primeiro, a qualidade da base: valores ausentes ou vazios e registros duplicados.
ausentes <- sapply(acidentes_brutos, function(x) sum(is.na(x) | x == ""))
ausentes[ausentes > 0]
## <NA> tipo_acidente classificacao_acidente
## NA 40 3
sum(duplicated(acidentes_brutos$id))
## [1] 0
A base está bem preenchida: só tipo_acidente e
classificacao_acidente têm algumas dezenas de vazios,
irrelevantes para a análise, e não há identificadores duplicados.
Já as coordenadas pedem cuidado: algumas caem fora do território brasileiro (erros de digitação ou de sinal). Elas são marcadas como ausentes em vez de excluir o acidente, que continua valendo para as contagens.
fora_do_brasil <- with(acidentes_brutos,
latitude < -34 | latitude > 6 | longitude < -74 | longitude > -34)
sum(fora_do_brasil, na.rm = TRUE)
## [1] 53
Agora as variáveis de que a análise precisa:
periodo: em que trecho do ano o
acidente aconteceu, segundo o calendário da política (antes da
suspensão, durante a suspensão ou depois da retomada). O mesmo
calendário é aplicado aos anos de controle, 2017 e 2018;causa_velocidade: se a causa principal
registrada foi “Velocidade Incompatível”;hora: extraída de horario
com separate;semana: semana do ano, para as séries
temporais.acidentes <- acidentes_brutos %>%
mutate(
data = as.Date(data_inversa),
latitude = ifelse(fora_do_brasil, NA, latitude),
longitude = ifelse(fora_do_brasil, NA, longitude),
municipio = str_to_title(municipio),
causa_velocidade = causa_acidente == "Velocidade Incompatível",
dia_mes = format(data, "%m-%d"),
periodo = case_when(
dia_mes < "08-15" ~ "Antes (01/01 a 14/08)",
dia_mes <= "12-11" ~ "Suspensão (15/08 a 11/12)",
TRUE ~ "Retomada (12/12 a 31/12)"
),
periodo = factor(periodo, levels = c("Antes (01/01 a 14/08)",
"Suspensão (15/08 a 11/12)",
"Retomada (12/12 a 31/12)")),
semana = week(data),
ano = factor(ano)
) %>%
separate(horario, into = c("hora", "minuto", "segundo"), sep = ":", convert = TRUE) %>%
select(id, data, ano, periodo, semana, hora, dia_semana, uf, br, km, municipio,
causa_acidente, causa_velocidade, tipo_acidente, classificacao_acidente,
fase_dia, condicao_metereologica, pessoas, mortos, feridos_graves,
feridos_leves, veiculos, latitude, longitude)
Na base de infrações, a data vira Date, a UF
desconhecida (-1) é descartada, a abordagem recebe rótulos
legíveis e a faixa de excesso vira um fator. Infrações sem
abordagem são as registradas por equipamento (radar), sem que o
condutor seja parado; são elas que a suspensão deveria afetar.
table(multas_brutas$abordagem)
##
## C S
## 1934 35110
multas <- multas_brutas %>%
filter(uf != "-1") %>%
mutate(
data = as.Date(data),
tipo = ifelse(abordagem == "S", "Radar (sem abordagem)", "Com abordagem"),
faixa = factor(codigos_velocidade[codigo], levels = codigos_velocidade),
dia_mes = format(data, "%m-%d"),
periodo = case_when(
dia_mes < "08-15" ~ "Antes (01/01 a 14/08)",
dia_mes <= "12-11" ~ "Suspensão (15/08 a 11/12)",
TRUE ~ "Retomada (12/12 a 31/12)"
),
periodo = factor(periodo, levels = levels(acidentes$periodo)),
semana = week(data),
ano = factor(ano)
) %>%
select(data, ano, periodo, semana, uf, tipo, faixa, multas)
As duas bases finais, de forma condensada (uma amostra de 100 acidentes; as infrações já estão agregadas):
set.seed(2019)
datatable(slice_sample(acidentes, n = 100), rownames = FALSE,
options = list(pageLength = 5, scrollX = TRUE),
caption = "Amostra de 100 acidentes (base limpa)")
datatable(slice_sample(multas, n = 100), rownames = FALSE,
options = list(pageLength = 5, scrollX = TRUE),
caption = "Amostra de 100 linhas da base de infrações de velocidade (agregada por dia)")
Resumo das variáveis de interesse:
resumo <- data.frame(
Variável = c("data", "periodo", "uf", "causa_velocidade", "mortos",
"feridos_graves", "hora", "multas (infrações)", "tipo (infrações)"),
Tipo = c("Date", "fator ordenado", "texto", "lógico", "inteiro", "inteiro",
"inteiro", "inteiro", "texto"),
Descrição = c("Dia do acidente",
"Trecho do ano segundo o calendário da suspensão",
"Estado onde ocorreu",
"Causa principal foi velocidade incompatível",
"Mortos no acidente",
"Feridos graves no acidente",
"Hora do acidente (0 a 23)",
"Número de autos de velocidade no dia/UF/tipo/faixa",
"Radar (sem abordagem) ou com abordagem do condutor"),
Resumo = c(
paste(min(acidentes$data), "a", max(acidentes$data)),
paste(levels(acidentes$periodo), collapse = " / "),
paste(n_distinct(acidentes$uf), "UFs"),
percent(mean(acidentes$causa_velocidade), accuracy = 0.1),
paste("total:", format(sum(acidentes$mortos), big.mark = ".")),
paste("total:", format(sum(acidentes$feridos_graves), big.mark = ".")),
paste("mais comum:", names(which.max(table(acidentes$hora))), "h"),
paste("total:", format(sum(multas$multas), big.mark = ".")),
paste(percent(mean(multas$tipo == "Radar (sem abordagem)")), "das linhas são de radar")
)
)
kable(resumo, caption = "Variáveis de interesse") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| Variável | Tipo | Descrição | Resumo |
|---|---|---|---|
| data | Date | Dia do acidente | 2017-01-01 a 2019-12-31 |
| periodo | fator ordenado | Trecho do ano segundo o calendário da suspensão | Antes (01/01 a 14/08) / Suspensão (15/08 a 11/12) / Retomada (12/12 a 31/12) |
| uf | texto | Estado onde ocorreu | 27 UFs |
| causa_velocidade | lógico | Causa principal foi velocidade incompatível | 10,3% |
| mortos | inteiro | Mortos no acidente | total: 16.862 |
| feridos_graves | inteiro | Feridos graves no acidente | total: 55.071 |
| hora | inteiro | Hora do acidente (0 a 23) | mais comum: 18 h |
| multas (infrações) | inteiro | Número de autos de velocidade no dia/UF/tipo/faixa | total: 7.879.283 |
| tipo (infrações) | texto | Radar (sem abordagem) ou com abordagem do condutor | 95% das linhas são de radar |
A base de acidentes tem 226.458 ocorrências entre 2017 e 2019, com 16.862 mortos. Uma observação importante: 2017 tem 29% mais registros que 2018, uma diferença grande demais para ser real, o que sugere uma mudança na forma de registro. Por isso, a análise compara períodos dentro de cada ano, e não os totais de um ano contra outro.
Antes de procurar efeitos nos acidentes, é preciso confirmar que a política “pegou”. O gráfico mostra as multas de velocidade por semana em 2018 e 2019. As linhas tracejadas vermelhas marcam o início e o fim da suspensão (o mesmo vale para os gráficos seguintes).
semana_inicio <- week(as.Date("2019-08-15"))
semana_fim <- week(as.Date("2019-12-11"))
multas_semana <- multas %>%
filter(semana <= 52) %>%
group_by(ano, semana, tipo) %>%
summarise(multas = sum(multas), .groups = "drop")
g <- ggplot(multas_semana, aes(semana, multas, color = ano)) +
geom_vline(xintercept = c(semana_inicio, semana_fim), linetype = "dashed", color = "#c0392b") +
geom_line(linewidth = 0.8) +
facet_wrap(~ tipo, ncol = 1, scales = "free_y") +
scale_y_continuous(labels = label_number(big.mark = ".")) +
scale_color_manual(values = c("2018" = "grey50", "2019" = "#c0392b")) +
labs(title = "Multas de velocidade por semana",
x = "Semana do ano", y = "Multas", color = "Ano") +
theme_minimal()
ggplotly(g)
taxa_periodo <- function(dados, valor) {
# média diária de 'valor' por ano e período
dias <- dados %>% group_by(ano, periodo) %>% summarise(dias = n_distinct(data), .groups = "drop")
somas <- dados %>% group_by(ano, periodo) %>% summarise(total = sum({{ valor }}), .groups = "drop")
resultado <- left_join(somas, dias, by = c("ano", "periodo")) %>% mutate(por_dia = total / dias)
return(resultado)
}
radar <- taxa_periodo(filter(multas, tipo == "Radar (sem abordagem)"), multas) %>%
select(ano, periodo, por_dia) %>%
pivot_wider(names_from = ano, values_from = por_dia, names_prefix = "ano_") %>%
mutate(variacao = ano_2019 / ano_2018 - 1)
abordagem <- taxa_periodo(filter(multas, tipo == "Com abordagem"), multas) %>%
select(ano, periodo, por_dia) %>%
pivot_wider(names_from = ano, values_from = por_dia, names_prefix = "ano_") %>%
mutate(variacao = ano_2019 / ano_2018 - 1)
queda_radar <- radar$variacao[radar$periodo == "Suspensão (15/08 a 11/12)"]
var_radar_antes <- radar$variacao[radar$periodo == "Antes (01/01 a 14/08)"]
# queda atribuível à suspensão: desconta a diferença que já existia antes dela
queda_radar_ajustada <- (1 + queda_radar) / (1 + var_radar_antes) - 1
queda_abordagem <- abordagem$variacao[abordagem$periodo == "Suspensão (15/08 a 11/12)"]
abordagem_dia_2018 <- abordagem$ano_2018[abordagem$periodo == "Suspensão (15/08 a 11/12)"]
abordagem_dia_2019 <- abordagem$ano_2019[abordagem$periodo == "Suspensão (15/08 a 11/12)"]
radar %>%
mutate(across(starts_with("ano_"), ~ format(round(.x), big.mark = ".")),
variacao = percent(variacao, accuracy = 1)) %>%
kable(col.names = c("Período", "2018", "2019", "Variação 2019/2018"),
caption = "Multas de velocidade por radar: média por dia") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| Período | 2018 | 2019 | Variação 2019/2018 |
|---|---|---|---|
| Antes (01/01 a 14/08) | 13.554 | 9.746 | -28% |
| Suspensão (15/08 a 11/12) | 12.132 | 5.730 | -53% |
| Retomada (12/12 a 31/12) | 16.285 | 7.557 | -54% |
A política teve efeito imediato na fiscalização. Na semana de 15/08 as multas por radar despencam, e só voltam a subir depois da decisão judicial de 11/12. Durante a suspensão, a média diária de multas por radar ficou 53% abaixo do mesmo período de 2018. Mas parte dessa diferença já existia: antes da suspensão, 2019 já registrava 28% menos multas por radar que 2018. Descontando essa diferença prévia, a queda atribuível à suspensão é de 34%.
Dois detalhes não são óbvios:
O efeito foi muito desigual entre estados:
fiscalizacao_uf <- multas %>%
filter(tipo == "Radar (sem abordagem)", periodo == "Suspensão (15/08 a 11/12)") %>%
group_by(uf, ano) %>%
summarise(multas = sum(multas), .groups = "drop") %>%
pivot_wider(names_from = ano, values_from = multas, names_prefix = "radar_", values_fill = 0) %>%
mutate(queda_fiscalizacao = radar_2019 / radar_2018 - 1) %>%
arrange(queda_fiscalizacao)
datatable(fiscalizacao_uf, rownames = FALSE,
colnames = c("UF", "Multas radar 2018", "Multas radar 2019", "Variação"),
caption = "Multas por radar na janela 15/08–11/12, por UF",
options = list(pageLength = 10)) %>%
formatCurrency(2:3, currency = "", digits = 0, mark = ".") %>%
formatPercentage(4, digits = 0) %>%
formatStyle("queda_fiscalizacao",
background = styleColorBar(c(-1, 0.3), "#f5b7b1"),
backgroundSize = "100% 80%", backgroundRepeat = "no-repeat",
backgroundPosition = "center")
Em 16 estados as multas por radar zeraram (todo o radar era móvel), enquanto em outros, como Bahia e Espírito Santo, a queda foi pequena. Essa diferença vai ser usada na seção 4.5.
A comparação mais direta, e a que costuma aparecer no debate público, é olhar o que aconteceu durante a suspensão em relação ao resto do ano. Para cada ano, divide-se a média diária de 15/08 a 11/12 pela média diária de 01/01 a 14/08. Em anos sem suspensão (2017 e 2018), essa razão mostra a sazonalidade “normal”; se 2019 fugir desse padrão, seria um sinal de efeito da política.
indicadores <- acidentes %>%
filter(periodo != "Retomada (12/12 a 31/12)") %>%
mutate(acidentes = 1,
acidentes_velocidade = as.numeric(causa_velocidade),
acidentes_outras = as.numeric(!causa_velocidade),
mortos_velocidade = mortos * causa_velocidade)
medidas <- c("acidentes", "mortos", "feridos_graves",
"acidentes_velocidade", "mortos_velocidade", "acidentes_outras")
# para cada medida: média diária por ano e período, razão suspensão/antes
razoes <- bind_rows(lapply(medidas, function(m) {
taxa_periodo(indicadores, .data[[m]]) %>%
select(ano, periodo, por_dia) %>%
pivot_wider(names_from = periodo, values_from = por_dia) %>%
mutate(medida = m, razao = `Suspensão (15/08 a 11/12)` / `Antes (01/01 a 14/08)`) %>%
select(medida, ano, razao)
})) %>%
pivot_wider(names_from = ano, values_from = razao, names_prefix = "razao_") %>%
mutate(efeito = razao_2019 / ((razao_2017 + razao_2018) / 2) - 1)
nomes_medidas <- c(acidentes = "Acidentes (todos)", mortos = "Mortos",
feridos_graves = "Feridos graves",
acidentes_velocidade = "Acidentes por velocidade",
mortos_velocidade = "Mortos em acidentes por velocidade",
acidentes_outras = "Acidentes por outras causas")
razoes %>%
mutate(medida = nomes_medidas[medida],
across(starts_with("razao_"), ~ round(.x, 3)),
efeito = percent(efeito, accuracy = 0.1)) %>%
kable(col.names = c("Medida", "2017", "2018", "2019", "2019 vs. média 2017–2018"),
caption = "Razão entre a média diária na janela da suspensão e a média diária antes dela") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
row_spec(c(4, 5), bold = TRUE, background = "#fdecea")
| Medida | 2017 | 2018 | 2019 | 2019 vs. média 2017–2018 |
|---|---|---|---|---|
| Acidentes (todos) | 0,970 | 0,932 | 1,072 | 12,8% |
| Mortos | 0,971 | 0,988 | 1,062 | 8,4% |
| Feridos graves | 1,025 | 1,017 | 1,048 | 2,7% |
| Acidentes por velocidade | 1,044 | 0,884 | 1,148 | 19,0% |
| Mortos em acidentes por velocidade | 1,016 | 0,980 | 1,203 | 20,5% |
| Acidentes por outras causas | 0,961 | 0,937 | 1,065 | 12,2% |
grafico_efeito <- razoes %>%
filter(medida %in% c("acidentes_velocidade", "acidentes_outras",
"mortos_velocidade", "mortos")) %>%
mutate(medida = factor(nomes_medidas[medida], levels = rev(nomes_medidas)),
destaque = grepl("velocidade", medida))
g <- ggplot(grafico_efeito, aes(efeito, medida, fill = destaque,
text = paste0(medida, ": ", percent(efeito, accuracy = 0.1)))) +
geom_col(width = 0.6) +
geom_vline(xintercept = 0) +
scale_x_continuous(labels = percent) +
scale_fill_manual(values = c(`TRUE` = "#c0392b", `FALSE` = "grey60"), guide = "none") +
labs(title = "Primeira leitura: variação na janela da suspensão (2019 vs. 2017–2018)",
x = "Variação em relação ao padrão sazonal", y = NULL) +
theme_minimal()
ggplotly(g, tooltip = "text")
À primeira vista, os dados confirmam a hipótese. Em 2017 e 2018, a janela de agosto a dezembro teve menos acidentes por dia que o começo do ano (razões abaixo de 1); em 2019, teve mais. Contra o padrão dos anos anteriores, o período da suspensão teria 12,8% a mais de acidentes e 8,4% a mais de mortes, e o aumento seria maior justamente nos acidentes causados por velocidade (19,0%, contra 12,2% nas demais causas), que é o que o radar fiscaliza.
Seria uma conclusão fácil, e é o tipo de número que circula na imprensa. Mas essa comparação tem uma premissa escondida: a de que, sem a suspensão, 2019 teria repetido a sazonalidade de 2017 e 2018. A próxima seção testa essa premissa.
O gráfico mostra os acidentes semana a semana, suavizados por uma média móvel de 4 semanas.
acidentes_semana <- acidentes %>%
filter(semana <= 52) %>%
count(ano, semana, name = "acidentes") %>%
group_by(ano) %>%
mutate(media_movel = as.numeric(stats::filter(acidentes, rep(1 / 4, 4), sides = 2))) %>%
ungroup()
g <- ggplot(acidentes_semana, aes(semana, media_movel, color = ano)) +
geom_vline(xintercept = c(semana_inicio, semana_fim), linetype = "dashed", color = "#c0392b") +
geom_line(linewidth = 0.8) +
scale_color_manual(values = c("2017" = "grey75", "2018" = "grey45", "2019" = "#c0392b")) +
labs(title = "Acidentes por semana (média móvel de 4 semanas)",
x = "Semana do ano", y = "Acidentes", color = "Ano") +
theme_minimal()
ggplotly(g)
O nível de 2017 é mais alto pela mudança de registro já comentada; o que importa é a forma das curvas. A de 2019 não dá um salto quando a suspensão começa: ela sobe de forma contínua desde janeiro, sete meses antes da política. Se o ano já vinha em alta, comparar o fim do ano com o começo (como na seção 4.2) mede a tendência, e não a política. Já a curva de 2018 tem um vale fundo por volta da semana 21, que coincide com a greve dos caminhoneiros de maio de 2018.
Para separar uma coisa da outra, foram feitos dois testes mais rigorosos, aplicados aos três anos. Em 2017 e 2018 não houve suspensão, então eles funcionam como placebo: se o teste “encontra” efeito nesses anos, o resultado de 2019 também não pode ser atribuído à política.
semanal <- acidentes %>%
filter(semana <= 51) %>%
group_by(ano, semana) %>%
summarise(acidentes = n(),
acidentes_velocidade = sum(causa_velocidade),
acidentes_outras = sum(!causa_velocidade),
mortos = sum(mortos), .groups = "drop")
# Teste 1: janela da suspensão vs. as 8 semanas imediatamente anteriores
teste_vizinhanca <- function(dados, var) {
durante <- dados %>% filter(semana >= semana_inicio, semana <= semana_fim)
vizinhas <- dados %>% filter(semana >= semana_inicio - 8, semana < semana_inicio)
return(mean(durante[[var]]) / mean(vizinhas[[var]]) - 1)
}
# Teste 2: reta ajustada às semanas anteriores, projetada para a janela
teste_tendencia <- function(dados, var) {
antes <- dados %>% filter(semana < semana_inicio)
durante <- dados %>% filter(semana >= semana_inicio, semana <= semana_fim)
modelo <- lm(reformulate("semana", var), data = antes)
return(sum(durante[[var]]) / sum(predict(modelo, newdata = durante)) - 1)
}
medidas_teste <- c("acidentes", "acidentes_velocidade", "acidentes_outras", "mortos")
testes <- expand.grid(ano = levels(acidentes$ano), medida = medidas_teste,
stringsAsFactors = FALSE)
testes$vizinhanca <- NA
testes$tendencia <- NA
for (i in seq_len(nrow(testes))) {
dados_ano <- filter(semanal, ano == testes$ano[i])
testes$vizinhanca[i] <- teste_vizinhanca(dados_ano, testes$medida[i])
testes$tendencia[i] <- teste_tendencia(dados_ano, testes$medida[i])
}
testes %>%
pivot_wider(names_from = ano, values_from = c(vizinhanca, tendencia)) %>%
mutate(medida = nomes_medidas[medida],
across(-medida, ~ percent(.x, accuracy = 0.1))) %>%
kable(col.names = c("Medida", rep(c("2017", "2018", "2019"), 2)),
caption = "Variação na janela da suspensão segundo os dois testes") %>%
add_header_above(c(" " = 1, "Teste 1: vs. 8 semanas anteriores" = 3,
"Teste 2: vs. tendência projetada" = 3)) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
column_spec(c(4, 7), bold = TRUE, background = "#fdecea")
| Medida | 2017 | 2018 | 2019 | 2017 | 2018 | 2019 |
|---|---|---|---|---|---|---|
| Acidentes (todos) | 1,8% | 2,3% | 3,6% | 1,0% | 30,9% | -0,3% |
| Acidentes por velocidade | 27,9% | 16,9% | 21,1% | 48,4% | 210,3% | 35,0% |
| Acidentes por outras causas | -0,9% | 0,9% | 2,1% | -3,2% | 23,5% | -2,9% |
| Mortos | -10,5% | -7,5% | -1,6% | -12,1% | -3,6% | -7,2% |
O gráfico mostra o Teste 2: a linha tracejada é a tendência que cada ano vinha seguindo antes da janela da suspensão.
projecoes <- bind_rows(lapply(levels(acidentes$ano), function(a) {
dados_ano <- filter(semanal, ano == a)
modelo <- lm(acidentes ~ semana, data = filter(dados_ano, semana < semana_inicio))
dados_ano$tendencia <- predict(modelo, newdata = dados_ano)
return(dados_ano)
}))
g <- ggplot(projecoes, aes(semana)) +
geom_vline(xintercept = c(semana_inicio, semana_fim), linetype = "dashed", color = "#c0392b") +
geom_line(aes(y = acidentes, color = ano), linewidth = 0.6) +
geom_line(aes(y = tendencia), linetype = "dashed", color = "black") +
facet_wrap(~ ano, ncol = 1, scales = "free_y") +
scale_color_manual(values = c("2017" = "grey55", "2018" = "grey40", "2019" = "#c0392b"),
guide = "none") +
labs(title = "Acidentes por semana e tendência anterior à janela da suspensão",
x = "Semana do ano", y = "Acidentes por semana") +
theme_minimal()
ggplotly(g)
Com os testes mais rigorosos, o efeito desaparece.
Um cuidado com o placebo de 2018 no Teste 2: a reta daquele ano é puxada para baixo pela queda do início do ano e pela greve dos caminhoneiros, então ela prevê bem menos acidentes do que ocorreram (30,9%). Por isso, o Teste 1 é a comparação mais confiável entre os três anos, e nele 2019 se comporta como os outros dois.
Em resumo, o aumento da seção 4.2 vinha de 2019 ser um ano de acidentes em alta desde janeiro. Quando a comparação respeita essa tendência, a janela da suspensão não se diferencia do que acontece em anos sem suspensão.
Para entender o perfil desses acidentes, o mapa de calor compara dia da semana e hora dos acidentes por velocidade na janela da suspensão, em 2018 e em 2019.
dias_ordem <- c("segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
"sexta-feira", "sábado", "domingo")
calor <- acidentes %>%
filter(causa_velocidade, ano %in% c("2018", "2019"),
periodo == "Suspensão (15/08 a 11/12)") %>%
mutate(dia_semana = factor(str_to_lower(dia_semana), levels = dias_ordem)) %>%
count(ano, dia_semana, hora)
ggplot(calor, aes(hora, dia_semana, fill = n)) +
geom_tile(color = "white") +
facet_wrap(~ ano, ncol = 2) +
scale_fill_gradient(low = "#fdecea", high = "#922b21") +
scale_x_continuous(breaks = seq(0, 23, 3)) +
labs(title = "Acidentes por velocidade na janela 15/08–11/12",
x = "Hora do dia", y = NULL, fill = "Acidentes") +
theme_minimal()
Nos dois anos, os acidentes por velocidade se concentram no fim de semana (36% em 2018 e 37% em 2019), com o pico nas tardes de domingo, das 12h às 19h, horário típico de volta de viagem (8% e 9% do total, em apenas 7 das 168 horas da semana). Em 2019 há mais acidentes por velocidade na janela, o que é coerente com a alta do ano inteiro (seção 4.3), mas o padrão de quando eles acontecem é o mesmo: nada indica um tipo novo de acidente durante a suspensão.
Há uma terceira forma de testar a política, que usa a desigualdade vista na seção 4.1. Se a suspensão causou aumento de acidentes, os estados em que o radar praticamente desapareceu deveriam ter aumentos maiores do que os estados em que quase nada mudou. Para testar isso, a variação da fiscalização por UF é unida à variação dos acidentes por UF, calculada com a mesma lógica da seção 4.2 (2019 contra 2018).
acidentes_uf <- acidentes %>%
filter(ano %in% c("2018", "2019"), periodo != "Retomada (12/12 a 31/12)") %>%
count(uf, ano, periodo) %>%
pivot_wider(names_from = c(ano, periodo), values_from = n) %>%
mutate(variacao_acidentes =
(`2019_Suspensão (15/08 a 11/12)` / `2019_Antes (01/01 a 14/08)`) /
(`2018_Suspensão (15/08 a 11/12)` / `2018_Antes (01/01 a 14/08)`) - 1) %>%
select(uf, variacao_acidentes)
uf_unido <- inner_join(fiscalizacao_uf, acidentes_uf, by = "uf")
correlacao <- cor(uf_unido$queda_fiscalizacao, uf_unido$variacao_acidentes)
ufs_com_aumento <- sum(uf_unido$variacao_acidentes > 0)
g <- ggplot(uf_unido, aes(queda_fiscalizacao, variacao_acidentes,
text = paste0(uf, "<br>Fiscalização: ", percent(queda_fiscalizacao, 1),
"<br>Acidentes: ", percent(variacao_acidentes, 1)))) +
geom_hline(yintercept = 0, color = "grey60") +
geom_smooth(aes(group = 1), method = "lm", se = FALSE, color = "grey40", linetype = "dashed") +
geom_point(aes(size = radar_2018), color = "#c0392b", alpha = 0.7) +
geom_text(aes(label = uf), size = 3, vjust = -1.2) +
scale_x_continuous(labels = percent) +
scale_y_continuous(labels = percent) +
scale_size(range = c(2, 10), guide = "none") +
labs(title = "Queda da fiscalização x variação dos acidentes, por UF",
x = "Variação das multas por radar (2019 vs. 2018, na janela)",
y = "Variação dos acidentes (ajustada pela sazonalidade)") +
theme_minimal()
ggplotly(g, tooltip = "text")
Não há relação. A correlação entre a queda da fiscalização e a variação dos acidentes é de apenas 0,13: os acidentes subiram em 22 dos 27 estados, mas não subiram mais onde o radar sumiu de vez. Estados que perderam 100% das multas por radar aparecem tanto entre os maiores aumentos quanto entre as quedas.
Esse resultado reforça a seção 4.3: o aumento de 2019 se espalhou pelo país de forma parecida, independentemente de quanto cada estado perdeu de fiscalização, o que é o que se espera de uma tendência nacional e não de um efeito da suspensão. Duas ressalvas impedem descartar a política por completo: o efeito do radar pode não ser local (a notícia da suspensão, de alcance nacional, pode ter mudado o comportamento de motoristas em todo lugar), e 27 estados formam uma amostra pequena e ruidosa.
O mapa mostra os acidentes fatais causados por velocidade na janela da suspensão, em 2018 e 2019. Use o seletor no canto para comparar os dois anos.
fatais <- acidentes %>%
filter(causa_velocidade, mortos > 0, ano %in% c("2018", "2019"),
periodo == "Suspensão (15/08 a 11/12)", !is.na(latitude))
popup <- function(d) {
paste0("<b>", d$municipio, " (", d$uf, ")</b><br>BR-", d$br, ", km ", d$km,
"<br>", format(d$data, "%d/%m/%Y"), "<br>Mortos: ", d$mortos)
}
leaflet() %>%
addTiles() %>% # mapa base do OpenStreetMap
addCircleMarkers(data = filter(fatais, ano == "2018"), lng = ~longitude, lat = ~latitude,
radius = ~3 + 2 * mortos, color = "grey40", stroke = FALSE,
fillOpacity = 0.6, popup = popup(filter(fatais, ano == "2018")),
group = "2018") %>%
addCircleMarkers(data = filter(fatais, ano == "2019"), lng = ~longitude, lat = ~latitude,
radius = ~3 + 2 * mortos, color = "#c0392b", stroke = FALSE,
fillOpacity = 0.6, popup = popup(filter(fatais, ano == "2019")),
group = "2019") %>%
addLayersControl(baseGroups = c("2019", "2018"),
options = layersControlOptions(collapsed = FALSE))
Foram 204 acidentes fatais por velocidade na janela de 2019, contra 193 em 2018. Os estados com mais casos em 2019 foram MG (49), SC (27), PR (23).
A suspensão dos radares nas rodovias federais, entre agosto e dezembro de 2019, foi uma mudança de política tomada sem nenhum plano de avaliação. Este trabalho usou dados abertos para responder se a redução da fiscalização eletrônica levou a mais acidentes e mortes.
Foram combinadas duas bases da PRF: infrações (mais de 13 milhões de registros, filtrados e agregados) e acidentes (cerca de 226 mil ocorrências). Os dois anos de infrações vêm em formatos diferentes e foram harmonizados por uma única função. A janela da suspensão foi comparada com o resto do ano em 2019 e, como controle, nos mesmos dias de 2017 e 2018. A primeira comparação foi submetida a três verificações: vizinhança (8 semanas anteriores), tendência projetada e relação dose-resposta entre estados.
A conclusão é que os dados não mostram que a suspensão de cerca de quatro meses aumentou os acidentes nas rodovias federais. Isso não é o mesmo que dizer que os radares não funcionam: significa que o efeito, se existiu, foi pequeno demais para ser separado da tendência do ano com esses dados.
A principal lição é de método. A comparação mais intuitiva, “depois da suspensão houve 13% mais acidentes”, parece uma prova e é o tipo de número que circula no debate público, mas a maior parte dela era tendência. Para gestores e para quem fiscaliza a política pública, isso significa: