1. Introdução

1.1 O problema

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.

1.2 Como o problema será abordado

São usadas duas bases públicas da PRF:

  • Infrações de trânsito (autos lavrados pela PRF) de 2018 e 2019, para medir quanto a fiscalização eletrônica de velocidade de fato caiu;
  • Acidentes atendidos pela PRF de 2017 a 2019, para medir o que aconteceu com acidentes, feridos e mortes.

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.

1.3 Técnica proposta

  1. Importar, harmonizar e limpar as duas bases (que vêm em formatos diferentes a cada ano);
  2. Confirmar, nos próprios dados de multas, que a política reduziu a fiscalização;
  3. Comparar acidentes e mortes entre períodos e anos, separando os acidentes causados por velocidade dos demais, que funcionam como um “grupo de controle” interno;
  4. Testar se o resultado resiste a comparações mais exigentes: com as semanas logo antes da suspensão e com a tendência que cada ano vinha seguindo, usando 2017 e 2018 como placebo (anos sem suspensão);
  5. Unir multas e acidentes por estado para testar se onde a fiscalização caiu mais os acidentes também subiram mais (uma relação “dose-resposta”).

1.4 A quem a análise interessa

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.

2. Pacotes requeridos

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

3. Preparação dos dados

3.1 Fonte

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.

3.2 Os dados originais

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:

  • Encoding e separadores: os acidentes estão em 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;
  • Layout diferente a cada ano nas infrações: o arquivo de 2018 é separado por vírgula, com nomes de coluna abreviados (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;
  • Valores ausentes codificados: a UF da placa vem como -1 quando não é conhecida;
  • Volume: as infrações de 2018 e 2019 somam cerca de 2,7 GB descompactados e mais de 10 milhões de linhas. Não faz sentido carregá-las inteiras: elas precisam ser filtradas (só infrações de velocidade) e agregadas antes de serem unidas aos acidentes.

3.3 Importação

Download

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")
  }
}

Por que não usar read.csv direto

Olhando 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

Acidentes

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

Infrações

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.

3.4 Limpeza e criação de variáveis

Acidentes

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)

Infrações

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)

3.5 Os dados limpos

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áveis de interesse
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.

4. Análise exploratória

4.1 A política reduziu mesmo a fiscalização?

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)
Multas de velocidade por radar: média por dia
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:

  • A queda não foi a zero. Os dados não dizem que equipamento gerou cada multa, mas a explicação mais provável é que parte das multas “sem abordagem” vem de equipamentos fixos, que não foram alcançados pelo despacho;
  • As multas de velocidade com abordagem (em que o agente para o veículo) praticamente desapareceram: de 14 por dia em 2018 para 1 por dia em 2019, na mesma janela. Os números são pequenos, mas indicam que a fiscalização de velocidade como um todo, e não só a eletrônica, enfraqueceu no período.

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.

4.2 Primeira leitura: os acidentes aumentaram

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")
Razão entre a média diária na janela da suspensão e a média diária antes dela
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.

4.3 Efeito da suspensão ou tendência de 2019?

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.

  • Teste 1, vizinhança: compara a janela da suspensão (semanas 33 a
    1. só com as 8 semanas imediatamente anteriores, em vez do ano inteiro. Uma tendência longa pesa pouco em um intervalo tão curto;
  • Teste 2, tendência projetada: ajusta uma reta aos acidentes de cada semana antes da janela e a estende pelo período da suspensão. O desvio entre o que aconteceu e o que a reta previa é o “excesso” de acidentes.
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")
Variação na janela da suspensão segundo os dois testes
Teste 1: vs. 8 semanas anteriores
Teste 2: vs. tendência projetada
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.

  • Acidentes em geral: pela tendência projetada, 2019 ficou -0,3% em relação ao esperado, ou seja, praticamente sobre a reta. Contra as 8 semanas anteriores, a variação foi de 3,6%, muito perto de 2017 (1,8%) e de 2018 (2,3%);
  • Mortes: em todos os anos ficaram abaixo da tendência, inclusive em 2019 (-7,2%). Não há sinal de excesso de mortes;
  • Acidentes por velocidade: sobem no segundo semestre em todos os anos. Contra as 8 semanas anteriores, o aumento de 2019 (21,1%) fica entre os de 2017 (27,9%) e 2018 (16,9%), anos sem suspensão. A reta projetada exagera esse aumento em todos os anos, o que mostra que esses acidentes têm uma sazonalidade forte que uma reta não captura.

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.

4.4 Quando acontecem os acidentes por velocidade

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.

4.5 Onde a fiscalização caiu mais, os acidentes subiram mais?

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.

4.6 Onde morreram as vítimas de acidentes por velocidade

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).

5. Conclusões

5.1 O problema

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.

5.2 Como foi abordado

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.

5.3 O que a análise mostrou

  • A fiscalização caiu de verdade e de imediato. Descontada a diferença que já existia entre os anos, as multas por radar caíram 34% durante a suspensão e zeraram em 16 estados. As multas de velocidade com abordagem praticamente desapareceram;
  • A comparação simples sugere um efeito: 12,8% a mais de acidentes, 8,4% a mais de mortes e 19,0% a mais de acidentes por velocidade na janela da suspensão;
  • Mas esse efeito não resiste a comparações mais rigorosas. 2019 já era um ano de acidentes em alta desde janeiro. Contra a própria tendência de 2019, a janela da suspensão ficou praticamente onde se esperava (-0,3%), sem excesso de mortes; contra as semanas imediatamente anteriores, 2019 se comportou como 2017 e 2018, anos sem suspensão;
  • E o aumento não foi maior onde o radar sumiu de vez (correlação de 0,13 entre a queda da fiscalização e a variação dos acidentes por estado).

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.

5.4 Implicações

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:

  • Desconfiar de comparações antes/depois que não olham para a tendência e para anos de controle;
  • Planejar a avaliação antes de mudar uma política (definir indicadores, grupos de comparação e duração), em vez de tentar reconstruí-la depois com os dados que sobraram;
  • Para a PRF, os dados mostram que a fiscalização de velocidade dependia quase inteiramente do radar: sem ele, as autuações com abordagem também desapareceram.

5.5 Limitações e próximos passos

  • A janela é curta. Quatro meses podem não bastar para mudar o comportamento dos motoristas, e eventuais efeitos depois da retomada não foram estudados (2020 é afetado pela pandemia);
  • Sem dados de fluxo de veículos: não foi possível controlar quantos carros passaram em cada rodovia. A alta de 2019 pode refletir mais tráfego, e a análise não consegue confirmar isso;
  • Registro de acidentes: a “causa principal” é atribuída pelo policial e o critério pode mudar; a grande diferença entre 2017 e 2018 indica mudanças no registro;
  • Escala da análise: a comparação foi feita no país e por estado. Os radares móveis operavam em trechos específicos; o efeito, se existiu, pode estar concentrado neles e diluído nos agregados;
  • Próximos passos: cruzar a localização (BR e km) das multas por radar de 2018 com os acidentes, comparando os trechos que perderam fiscalização com os que não perderam; incluir dados de fluxo do DNIT; e estimar um modelo formal de diferença em diferenças, com intervalos de confiança.