Quem espera mais? Velocidade de atendimento dos serviços urbanos do Recife em 2026

O que o diário do atendimento ao cidadão revela sobre quem é atendido e quando (jan–jun/2026)

1. Introdução

O problema

Todo dia, moradores do Recife pedem à Prefeitura, pelo 156, que troque uma lâmpada, retire entulho, pode uma árvore ou desentupa uma galeria. Cada pedido vira um registro com serviço, endereço, bairro, data e situação. O arquivo analisado reúne mais de 61 mil registros movimentados entre 1º de janeiro e 19 de junho de 2026.

A pergunta que organiza o relatório:

Todos os pedidos de zeladoria andam na mesma velocidade? Se não, o que explica a diferença: o tipo de serviço, o território ou o momento?

Por que isso interessa? Zeladoria é a parte do serviço público que o cidadão vê todo dia. Quando a resposta é rápida para um tipo de problema e lenta para outro, ou rápida em uma região e lenta em outra, isso é uma questão de eficiência (onde estão os gargalos) e de equidade (quem espera mais).

Como pretendo abordar

  • Dados: diário de demandas do Sistema 156 da Emlurb, dado aberto da Prefeitura do Recife (61.225 linhas × 13 variáveis).
  • Metodologia: importar e limpar a base, criar variáveis (tempo de atendimento, coorte), mesclar com tabelas de apoio e cruzar serviço × região × tempo com tabelas e gráficos interativos.
  • Técnica: comparação de medianas (os tempos são muito assimétricos) e separação entre pedidos de 2026 e pedidos antigos, para não distorcer as comparações.

Como isso ajuda os potenciais clientes

  • Gestão da Emlurb/Prefeitura: mostra em quais serviços e regiões se formam as filas.
  • Vereadores, imprensa e associações de moradores: oferece números comparáveis e avisa o que a base permite (e não permite) afirmar.
  • Quem mantém o dado aberto: aponta problemas de qualidade a corrigir.

2. Pacotes requeridos

Todos os pacotes são carregados aqui, de uma vez. Para replicar: install.packages(c("tidyverse", "lubridate", "scales", "plotly", "DT", "knitr", "rmdformats")).

library(tidyverse)  # dplyr, tidyr, stringr, forcats, readr, ggplot2: manipulação e gráficos
library(lubridate)  # datas (ymd, floor_date, month)
library(scales)     # formatação de números e percentuais
library(plotly)     # gráficos interativos
library(DT)         # tabelas interativas
library(knitr)      # tabelas estáticas e geração do relatório
Pacote Para que serve
tidyverse Importar, limpar e visualizar
lubridate Converter textos em datas
scales Formatar números e percentuais em padrão brasileiro
plotly Gráficos com hover e filtro por legenda
DT Tabelas com busca e ordenação
knitr Tabelas estáticas e geração do relatório
rmdformats Tema readthedown (exigido para publicar no RPubs)
# Formatação brasileira (ponto nos milhares, vírgula nos decimais)
fmt_n <- label_number(big.mark = ".", decimal.mark = ",")
fmt_p <- label_percent(accuracy = 0.1, big.mark = ".", decimal.mark = ",")
fmt_d <- label_number(accuracy = 0.1, big.mark = ".", decimal.mark = ",")

# Rótulos de meses em português (o R do servidor pode estar em inglês)
meses_pt <- c("jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez")
rotulo_mes <- function(x) meses_pt[month(x)]

# Tema único para os gráficos
theme_set(theme_minimal(base_size = 12) +
            theme(plot.title = element_text(face = "bold"),
                  plot.subtitle = element_text(color = "grey35"),
                  legend.position = "bottom"))

# Tabela interativa padrão, com textos em português
dt_pt <- function(dados, pagina = 8, ...) {
  datatable(dados, rownames = FALSE, class = "stripe hover compact",
            options = list(pageLength = pagina, dom = "ftip", scrollX = TRUE,
                           language = list(search = "Buscar:", info = "_START_ a _END_ de _TOTAL_",
                                           infoEmpty = "Sem registros", zeroRecords = "Nada encontrado",
                                           paginate = list(previous = "Anterior", `next` = "Próximo"))),
            ...)
}

3. Preparação dos dados

3.1 Fonte original

Os dados vêm do Portal de Dados Abertos da Prefeitura do Recife, conjunto Central de Atendimento de Serviços da Emlurb – 156, que reúne demandas de arborização, drenagem, iluminação, limpeza, pavimentação, tapa-buracos e outros serviços de reparação da cidade:

3.2 O que são os dados originais

# Usa o arquivo local; se não existir, baixa da URL do recurso CSV no portal
arquivo_local <- "resources_80c5b3cd-d4f7-466e-9031-e636ef56dd99_156_cco_diario_2026__2_.csv"
url_dados <- "https://dados.recife.pe.gov.br/dataset/331e88a9-d878-464b-b0bb-cb803d7091b3/resource/80c5b3cd-d4f7-466e-9031-e636ef56dd99/download/156_cco_diario_2026.csv"   

if (!file.exists(arquivo_local)) {
  stopifnot("Informe url_dados ou coloque o CSV na pasta do projeto" = !is.na(url_dados))
  download.file(url_dados, arquivo_local, mode = "wb")
}

# Lê tudo como texto
bruto <- read_delim(arquivo_local, delim = ";",
                    col_types = cols(.default = col_character()),
                    locale = locale(encoding = "UTF-8"))

Propósito. Cada linha é uma demanda de serviço urbano registrada no 156 e acompanhada pela Emlurb. A base serve à gestão operacional e, ao ser aberta, à transparência.

Dimensão e período. O arquivo tem 61.225 linhas e 13 variáveis. As datas de última situação vão de 01/01/2026 a 19/06/2026: o arquivo reúne demandas movimentadas em 2026, inclusive pedidos abertos anos antes (o mais antigo é de 19/05/2009).

Peculiaridades (a base não traz dicionário; as interpretações são minhas):

  • Separador ;, texto sem acentos e em MAIÚSCULAS (“ILUMINACAO PUBLICA”).
  • Logradouro com o tipo de via colado ao nome e espaços repetidos ("AV Boa Viagem").
  • Latitude/longitude vazias em cerca de metade das linhas, e algumas fora do Recife. Por isso não uso coordenadas; a análise geográfica se baseia em RPA e bairro.
  • Valores ausentes aparecem como campo vazio. Não há identificador, hora, canal de abertura nem descrição do problema.
# Diagnóstico do arquivo bruto: valores distintos, ausentes e um exemplo por variável
tibble(variavel  = names(bruto),
       distintos = map_int(bruto, n_distinct),
       ausentes  = map_int(bruto, ~ sum(is.na(.x))),
       exemplo   = map_chr(bruto, ~ as.character(.x[!is.na(.x)][1]))) %>%
  kable(align = "lrrl", col.names = c("Variável original", "Valores distintos", "Ausentes", "Exemplo"))
Variável original Valores distintos Ausentes Exemplo
GRUPOSERVICO_CODIGO 22 0 2
GRUPOSERVICO_DESCRICAO 22 0 LIMPEZA URBANA
SERVICO_CODIGO 93 0 2
SERVICO_DESCRICAO 93 0 REMOCAO DE ANIMAIS MORTOS PEQUENOS
LOGRADOURO 6667 0 AV Antonio Torres Galvao
NUMERO 4097 0 283
BAIRRO 95 0 Imbiribeira
RPA 6 0 6
DATA_DEMANDA 1697 0 2026-03-03
SITUACAO 7 0 ATENDIDA
DATA_ULT_SITUACAO 170 0 2026-03-09
latitude 20918 30244 -8.015447829409093
longitude 20519 30244 -34.890618876480964

3.3 Importação e limpeza

Passo 1: nomes e tipos. Nomes em minúsculas (estilo R); datas viram Date (para poder subtrair e agrupar); RPA vira inteiro. O stopifnot garante que nenhuma data falhou na conversão. Espaços repetidos são removidos.

dados <- bruto %>%
  rename_with(str_to_lower) %>%
  mutate(rpa = as.integer(rpa),
         across(c(data_demanda, data_ult_situacao), ymd),
         across(c(gruposervico_descricao, servico_descricao, logradouro, bairro), str_squish))

stopifnot(!anyNA(dados$data_demanda), !anyNA(dados$data_ult_situacao))

Passo 2: texto padronizado e acentos nas categorias (mesclagem). Descrições em maiúsculas viram sentence case. Para os 22 grupos de serviço e as 7 situações, uso tabelas de apoio e left_join para recuperar a acentuação. Serviços e bairros ficam como na fonte, porque restaurar acentos sem dicionário geraria erros.

grupos_acentuados <- tribble(
  ~gruposervico_descricao,            ~grupo_servico,
  "ACAO EDUCATIVA",                   "Ação educativa",
  "ARBORIZACAO",                      "Arborização",
  "COLETA SELETIVA",                  "Coleta seletiva",
  "COLETA URBANA",                    "Coleta urbana",
  "CONSERVACAO",                      "Conservação",
  "DENUNCIA VEICULO",                 "Denúncia de veículo",
  "DENUNCIAS",                        "Denúncias",
  "DRENAGEM",                         "Drenagem",
  "ESCADARIA",                        "Escadaria",
  "FISCALIZACAO URBANA",              "Fiscalização urbana",
  "ILUMINACAO PUBLICA",               "Iluminação pública",
  "LIMPEZA URBANA",                   "Limpeza urbana",
  "MACRODRENAGEM",                    "Macrodrenagem",
  "MANUT. URBANA",                    "Manutenção urbana",
  "PAVIMENTACAO",                     "Pavimentação",
  "PLANEJAMENTO DE LIMPEZA URBANA",   "Planejamento de limpeza urbana",
  "POSTE",                            "Poste",
  "PRACAS",                           "Praças",
  "PROJETOS",                         "Projetos",
  "REPOSICAO DE PLACAS",              "Reposição de placas",
  "SEMAFOROS",                        "Semáforos",
  "SEMENTEIRA",                       "Sementeira"
)

# Situações na ordem lógica (presumida) do fluxo de atendimento
situacoes <- tribble(
  ~situacao_orig,   ~situacao,
  "CADASTRADA",     "Cadastrada",
  "PREPARACAO",     "Preparação",
  "EXECUCAO",       "Execução",
  "PENDENCIA",      "Pendência",
  "FISCALIZACAO",   "Fiscalização",
  "MONITORAMENTO",  "Monitoramento",
  "ATENDIDA",       "Atendida"
)

dados <- dados %>%
  mutate(servico = str_to_sentence(servico_descricao)) %>%
  rename(situacao_orig = situacao) %>%
  left_join(grupos_acentuados, by = "gruposervico_descricao") %>%
  left_join(situacoes, by = "situacao_orig") %>%
  mutate(situacao = factor(situacao, levels = situacoes$situacao))

# Conferência: nenhum grupo ou situação ficou sem correspondência
stopifnot(!anyNA(dados$grupo_servico), !anyNA(dados$situacao))

Passo 3: separar o logradouro. O campo junta duas informações ("AV Boa Viagem"). Separo em tipo de via (AV, RUA…) e nome da via.

dados <- dados %>%
  separate(logradouro, into = c("tipo_via", "nome_via"), sep = " ", extra = "merge", fill = "right")

Passo 4: nome das RPAs (mais uma mesclagem) e bairro indefinido. A base só traz o número da RPA (Região Político-Administrativa). O valor "INDEFINIDO" em bairro é um dado ausente e vira NA.

rpa_nomes <- tibble(rpa = 1:6,
                    rpa_nome = c("RPA 1 · Centro", "RPA 2 · Norte", "RPA 3 · Noroeste",
                                 "RPA 4 · Oeste", "RPA 5 · Sudoeste", "RPA 6 · Sul"))

dados <- dados %>%
  left_join(rpa_nomes, by = "rpa") %>%
  mutate(bairro = na_if(bairro, "INDEFINIDO"))

Passo 5: variáveis derivadas e filtros.

  • tempo_dias: dias entre a abertura e a última movimentação.
  • coorte: pedido aberto em 2026 ou legado (aberto até 2025 e movimentado em 2026).
  • atendida: indicador lógico.

Atenção: só para demandas Atendidas tempo_dias se aproxima do tempo de atendimento. Nas demais, é o tempo até a última atualização. Por isso as análises de tempo usam só as atendidas.

dados <- dados %>%
  mutate(id_demanda = row_number(),
         tempo_dias = as.integer(data_ult_situacao - data_demanda),
         coorte     = if_else(year(data_demanda) == 2026, "Aberta em 2026", "Legado (até 2025)"),
         atendida   = situacao == "Atendida") %>%
  select(id_demanda, grupo_servico, servico, tipo_via, nome_via, bairro, rpa, rpa_nome,
         data_demanda, data_ult_situacao, situacao, tempo_dias, coorte, atendida)

# Consistência lógica: a última movimentação nunca é anterior à abertura
stopifnot(all(dados$tempo_dias >= 0))

# Filtro 1: pedidos abertos em 2026. A base só traz demandas MOVIMENTADAS em 2026,
# então o "legado" é um grupo enviesado (seção 4.6); taxas e tempos usam só 2026.
d26 <- dados %>% filter(coorte == "Aberta em 2026")

# Filtro 2: dentro de 2026, só as atendidas (único caso em que tempo_dias = tempo de atendimento)
d26_atend <- d26 %>% filter(atendida)

3.4 O conjunto de dados final

O conjunto limpo tem 61.225 linhas e 14 variáveis. Em vez de imprimir a base, mostro o dicionário e uma amostra de 8 linhas.

tribble(
  ~Variável,                    ~Tipo,        ~Descrição,
  "id_demanda",                 "inteiro",    "Identificador criado na limpeza",
  "grupo_servico / servico",    "texto",      "Grupo (22 categorias, com acentos) e serviço (93 categorias)",
  "tipo_via / nome_via",        "texto",      "Resultado da separação do logradouro",
  "bairro",                     "texto",      "Bairro (NA quando 'Indefinido')",
  "rpa / rpa_nome",             "inteiro/texto", "Região Político-Administrativa (1 a 6) e seu nome",
  "data_demanda",               "data",       "Data de abertura do pedido",
  "data_ult_situacao",          "data",       "Data da última movimentação",
  "situacao",                   "fator",      "Etapa atual (7 níveis)",
  "tempo_dias",                 "inteiro",    "Dias entre abertura e última movimentação",
  "coorte",                     "texto",      "Aberta em 2026 ou Legado (até 2025)",
  "atendida",                   "lógico",     "TRUE se situação = Atendida"
) %>% kable()
Variável Tipo Descrição
id_demanda inteiro Identificador criado na limpeza
grupo_servico / servico texto Grupo (22 categorias, com acentos) e serviço (93 categorias)
tipo_via / nome_via texto Resultado da separação do logradouro
bairro texto Bairro (NA quando ‘Indefinido’)
rpa / rpa_nome inteiro/texto Região Político-Administrativa (1 a 6) e seu nome
data_demanda data Data de abertura do pedido
data_ult_situacao data Data da última movimentação
situacao fator Etapa atual (7 níveis)
tempo_dias inteiro Dias entre abertura e última movimentação
coorte texto Aberta em 2026 ou Legado (até 2025)
atendida lógico TRUE se situação = Atendida
set.seed(2026)
dados %>%
  slice_sample(n = 8) %>%
  select(grupo_servico, servico, bairro, rpa, data_demanda, situacao, tempo_dias) %>%
  mutate(servico = str_trunc(servico, 38)) %>%
  kable(caption = "Amostra aleatória de 8 linhas do conjunto limpo")
Amostra aleatória de 8 linhas do conjunto limpo
grupo_servico servico bairro rpa data_demanda situacao tempo_dias
Iluminação pública Manutencao em lampada apagada Boa Vista 1 2026-03-13 Atendida 19
Iluminação pública Luminarias Ponto de Parada 2 2026-05-18 Pendência 3
Limpeza urbana Remocao de entulhos Dois Unidos 2 2026-04-27 Atendida 38
Drenagem Limpeza de canaleta Imbiribeira 6 2026-04-13 Cadastrada 0
Iluminação pública Manutencao em lampada apagada Madalena 4 2026-04-10 Atendida 4
Iluminação pública Manutencao em lampada apagada Nova Descoberta 3 2026-05-08 Atendida 2
Drenagem Reposicao tampa de poco, galerias,m… Arruda 2 2026-05-05 Cadastrada 0
Iluminação pública Acender circuito Caxanga 4 2026-06-10 Pendência 2

3.5 Resumo das variáveis de interesse

q <- quantile(dados$tempo_dias, c(.5, .9))
top_grupo  <- dados %>% count(grupo_servico, sort = TRUE) %>% slice(1)
top_bairro <- dados %>% filter(!is.na(bairro)) %>% count(bairro, sort = TRUE) %>% slice(1)

tribble(
  ~Variável, ~Resumo,
  "grupo_servico",
  str_glue("{n_distinct(dados$grupo_servico)} grupos; o maior é {top_grupo$grupo_servico} ({fmt_p(top_grupo$n/nrow(dados))} das demandas)"),
  "bairro / rpa",
  str_glue("{n_distinct(dados$bairro, na.rm = TRUE)} bairros em 6 RPAs; o mais frequente é {top_bairro$bairro} ({fmt_n(top_bairro$n)} demandas)"),
  "situacao",
  str_glue("{fmt_p(mean(dados$atendida))} Atendidas; {fmt_p(mean(dados$situacao == 'Cadastrada'))} ainda Cadastradas (primeira etapa)"),
  "data_demanda",
  str_glue("de {format(min(dados$data_demanda), '%d/%m/%Y')} a {format(max(dados$data_demanda), '%d/%m/%Y')}; {fmt_p(mean(dados$coorte == 'Aberta em 2026'))} abertas em 2026"),
  "tempo_dias",
  str_glue("mediana {q[1]} dias; percentil 90 = {q[2]} dias; máximo {fmt_n(max(dados$tempo_dias))} (muito assimétrica: a média é {fmt_d(mean(dados$tempo_dias))})")
) %>% kable(caption = "Resumo consolidado das variáveis de interesse")
Resumo consolidado das variáveis de interesse
Variável Resumo
grupo_servico 22 grupos; o maior é Iluminação pública (45,2% das demandas)
bairro / rpa 94 bairros em 6 RPAs; o mais frequente é Boa Viagem (3.463 demandas)
situacao 70,5% Atendidas; 14,5% ainda Cadastradas (primeira etapa)
data_demanda de 19/05/2009 a 19/06/2026; 92,1% abertas em 2026
tempo_dias mediana 3 dias; percentil 90 = 41 dias; máximo 6.083 (muito assimétrica: a média é 74,5)

O retrato: uma base dominada por poucos serviços (iluminação é quase metade) e com tempos muito assimétricos (mediana de poucos dias e uma cauda de pedidos antigos). Por isso comparo medianas e separo os pedidos de 2026 dos antigos.

4. Análise exploratória

4.1 Panorama: três grupos concentram três quartos dos pedidos

# Distribuição dos grupos de serviço (curva de Pareto)
pareto <- dados %>%
  count(grupo_servico, name = "n", sort = TRUE) %>%
  mutate(pct = n / sum(n), pct_acum = cumsum(pct))

g1 <- pareto %>%
  slice_head(n = 10) %>%
  mutate(grupo_servico = fct_reorder(grupo_servico, n),
         dica = str_glue("{grupo_servico}<br>{fmt_n(n)} demandas ({fmt_p(pct)})")) %>%
  ggplot(aes(grupo_servico, n, text = dica)) +
  geom_col(fill = "#2C7FB8") +
  coord_flip() +
  scale_y_continuous(labels = fmt_n) +
  labs(title = "Iluminação pública domina a demanda por zeladoria",
       subtitle = "10 maiores grupos de serviço, jan–jun/2026", x = NULL, y = "Nº de demandas")

ggplotly(g1, tooltip = "text")
pareto %>%
  transmute(`Grupo de serviço` = grupo_servico, Demandas = n, `% do total` = pct, `% acumulado` = pct_acum) %>%
  dt_pt(pagina = 6) %>%
  formatPercentage(c("% do total", "% acumulado"), 1, dec.mark = ",", mark = ".") %>%
  formatRound("Demandas", 0, mark = ".")

Insight. Iluminação pública responde por 45,2% dos registros e os três maiores grupos (Iluminação pública, Limpeza urbana, Arborização) somam 75,1%. Uma média geral da cidade é, na prática, uma média da iluminação; por isso olho cada serviço separadamente.

4.2 Duas velocidades: serviços de “resposta” e serviços de “fila”

Em que etapa estão os pedidos abertos em 2026, por grupo?

cores_sit <- c("Cadastrada" = "#CBD5E1", "Preparação" = "#99D5CF", "Execução" = "#0284C7",
               "Pendência" = "#F59E0B", "Fiscalização" = "#7C3AED", "Monitoramento" = "#92400E",
               "Atendida" = "#16A34A")

top10 <- pareto$grupo_servico[1:10]

# Percentual de cada situação dentro de cada grupo
sit_grupo <- d26 %>%
  filter(grupo_servico %in% top10) %>%
  count(grupo_servico, situacao) %>%
  group_by(grupo_servico) %>%
  mutate(p = n / sum(n)) %>%
  ungroup()

# Ordena os grupos pela taxa de atendimento
ordem <- d26 %>% filter(grupo_servico %in% top10) %>%
  group_by(grupo_servico) %>% summarise(tx = mean(atendida)) %>% arrange(tx) %>% pull(grupo_servico)

g2 <- sit_grupo %>%
  mutate(grupo_servico = factor(grupo_servico, levels = ordem),
         dica = str_glue("{grupo_servico}<br>{situacao}: {fmt_p(p)} ({fmt_n(n)})")) %>%
  ggplot(aes(grupo_servico, p, fill = situacao, text = dica)) +
  geom_col(width = 0.75) +
  coord_flip() +
  scale_fill_manual(values = cores_sit, name = NULL) +
  scale_y_continuous(labels = label_percent()) +
  labs(title = "Em que etapa estão os pedidos de 2026?",
       subtitle = "Os 10 maiores grupos, ordenados pela taxa de atendimento", x = NULL, y = NULL)

ggplotly(g2, tooltip = "text") %>% layout(legend = list(orientation = "h", y = -0.15))
# Taxa de atendimento por grupo e classificação em "resposta" ou "fila"
regimes <- d26 %>%
  group_by(grupo_servico) %>%
  summarise(demandas = n(), pct_atendida = mean(atendida), .groups = "drop") %>%
  mutate(regime = case_when(demandas < 100       ~ "Amostra pequena (< 100)",
                            pct_atendida >= 0.70 ~ "Resposta (≥ 70% atendidas)",
                            pct_atendida >= 0.30 ~ "Intermediário (30–70%)",
                            TRUE                 ~ "Fila (< 30% atendidas)"))

regimes %>%
  group_by(regime) %>%
  summarise(exemplos = paste(head(grupo_servico[order(-demandas)], 3), collapse = "; "),
            grupos = n(), demandas = sum(demandas), .groups = "drop") %>%
  mutate(`% das demandas` = fmt_p(demandas / sum(demandas)), demandas = fmt_n(demandas)) %>%
  arrange(desc(grupos)) %>%
  select(regime, grupos, demandas, exemplos, `% das demandas`) %>%
  kable(col.names = c("Regime", "Nº de grupos", "Demandas", "Maiores grupos", "% das demandas"), align = "lrrlr")
Regime Nº de grupos Demandas Maiores grupos % das demandas
Fila (< 30% atendidas) 12 10.145 Drenagem; Pavimentação; Manutenção urbana 18,0%
Intermediário (30–70%) 4 7.725 Arborização; Fiscalização urbana; Denúncias 13,7%
Amostra pequena (< 100) 3 158 Semáforos; Coleta seletiva; Conservação 0,3%
Resposta (≥ 70% atendidas) 3 38.364 Iluminação pública; Limpeza urbana; Coleta urbana 68,0%
fila <- regimes %>% filter(regime == "Fila (< 30% atendidas)")
fila_dados <- d26 %>% filter(grupo_servico %in% fila$grupo_servico)
ilum26 <- regimes %>% filter(grupo_servico == "Iluminação pública")

Insight. Iluminação, com 94,1% de pedidos atendidos, está no regime de resposta. Já 12 grupos (entre eles Drenagem, Pavimentação, Manutenção urbana, Praças) estão em fila: são 10.145 pedidos (18,0% de 2026) com menos de 30% de atendimento, e 83,0% deles continuam na primeira etapa (“Cadastrada”).

Cuidado. Isso não prova descaso: drenagem, pavimentação e projetos costumam exigir obra programada ou vistoria, e o andamento pode ser registrado em outro sistema. A base mostra, com segurança, que esses pedidos não avançam de etapa nesse registro, o que já é um problema de transparência para o cidadão.

4.3 Quanto tempo leva? Mediana de 3 dias na iluminação, 12 na limpeza, 22 nas denúncias

Agora só as demandas atendidas em 2026. Como a distribuição é assimétrica, uso medianas; o eixo do gráfico é cortado em 60 dias (os extremos continuam na tabela).

grupos_tempo <- d26_atend %>% count(grupo_servico) %>% filter(n >= 100) %>% pull(grupo_servico)

g3 <- d26_atend %>%
  filter(grupo_servico %in% grupos_tempo) %>%
  mutate(grupo_servico = fct_reorder(grupo_servico, tempo_dias, .fun = median)) %>%
  ggplot(aes(grupo_servico, tempo_dias)) +
  geom_boxplot(fill = "#A6CEE3", outlier.alpha = 0.15, outlier.size = 0.6) +
  coord_flip(ylim = c(0, 60)) +
  labs(title = "Tempo até o atendimento por grupo de serviço",
       subtitle = "Demandas atendidas abertas em 2026 (eixo cortado em 60 dias)",
       x = NULL, y = "Dias entre abertura e última movimentação")

ggplotly(g3)
tab_tempo <- d26_atend %>%
  filter(grupo_servico %in% grupos_tempo) %>%
  group_by(grupo_servico) %>%
  summarise(atendidas = n(), mediana = median(tempo_dias), p90 = quantile(tempo_dias, .9),
            ate_7d = mean(tempo_dias <= 7), mais_30d = mean(tempo_dias > 30), .groups = "drop") %>%
  arrange(mediana)

tab_tempo %>%
  rename(`Grupo de serviço` = grupo_servico, Atendidas = atendidas, `Mediana (dias)` = mediana,
         `P90 (dias)` = p90, `Até 7 dias` = ate_7d, `Mais de 30 dias` = mais_30d) %>%
  dt_pt(pagina = 8) %>%
  formatPercentage(c("Até 7 dias", "Mais de 30 dias"), 1, dec.mark = ",", mark = ".") %>%
  formatRound("Atendidas", 0, mark = ".")
t_ilum <- tab_tempo %>% filter(grupo_servico == "Iluminação pública")
t_limp <- tab_tempo %>% filter(grupo_servico == "Limpeza urbana")
t_arb  <- tab_tempo %>% filter(grupo_servico == "Arborização")
t_den  <- tab_tempo %>% filter(grupo_servico == "Denúncias")

Insight. Metade dos pedidos de iluminação é baixada em até 3 dias (80,0% em até uma semana). Em limpeza urbana a mediana é 12 dias, em arborização 13 e em denúncias 22; em limpeza, 18,5% dos atendimentos passam de 30 dias. A velocidade depende do tipo de problema, e uma média da cidade esconde isso.

4.4 Território: o que muda entre as RPAs?

rpa_tab <- d26 %>%
  group_by(rpa_nome) %>%
  summarise(demandas = n(),
            pct_iluminacao = mean(grupo_servico == "Iluminação pública"),
            pct_atendida = mean(atendida),
            mediana_dias = median(tempo_dias[atendida]), .groups = "drop")

rpa_tab %>%
  rename(RPA = rpa_nome, Demandas = demandas, `% iluminação` = pct_iluminacao,
         `% atendidas` = pct_atendida, `Mediana (dias, atendidas)` = mediana_dias) %>%
  dt_pt(pagina = 6) %>%
  formatPercentage(c("% iluminação", "% atendidas"), 1, dec.mark = ",", mark = ".") %>%
  formatRound("Demandas", 0, mark = ".")
rpa_melhor <- rpa_tab %>% slice_max(pct_atendida, n = 1, with_ties = FALSE)
rpa_pior   <- rpa_tab %>% slice_min(pct_atendida, n = 1, with_ties = FALSE)

À primeira vista, RPA 1 · Centro atende 80,3% dos pedidos e RPA 3 · Noroeste só 63,6%. Mas as RPAs pedem coisas diferentes (coluna “% iluminação”). Para separar o efeito do território do efeito do serviço, comparo a mediana de dias dentro do mesmo serviço:

grupos_heat <- c("Iluminação pública", "Limpeza urbana", "Arborização")

heat <- d26_atend %>%
  filter(grupo_servico %in% grupos_heat) %>%
  group_by(grupo_servico, rpa_nome) %>%
  summarise(mediana = median(tempo_dias), .groups = "drop")

ggplot(heat, aes(rpa_nome, factor(grupo_servico, levels = rev(grupos_heat)), fill = mediana)) +
  geom_tile(color = "white", linewidth = 1) +
  geom_text(aes(label = mediana), fontface = "bold", size = 5) +
  scale_fill_gradient(low = "#E5F5E0", high = "#E6550D", name = "Mediana (dias)") +
  labs(title = "Mesmo serviço, ritmos diferentes entre as RPAs",
       subtitle = "Mediana de dias até o atendimento, pedidos de 2026", x = NULL, y = NULL) +
  theme(axis.text.x = element_text(angle = 20, hjust = 1), panel.grid = element_blank())

# Mediana de um grupo em uma RPA (para citar no texto)
hv <- function(g, r) heat$mediana[heat$grupo_servico == g & heat$rpa_nome == r]
h_il <- heat %>% filter(grupo_servico == "Iluminação pública")

Insight. O território importa, mas não existe uma RPA “lenta” para tudo. Em iluminação, a mediana vai de 1 a 5 dias conforme a RPA. O ranking também muda com o serviço: a RPA 1 · Centro tem a menor mediana em limpeza urbana (7 dias), mas está entre as mais lentas em arborização (21 dias). Sem dados de equipes e população, a base mostra o padrão, não a causa.

4.5 Bairros: o “mix” de serviços engana

Será que bairros com mais pedidos atendidos são simplesmente os que pedem mais iluminação, o serviço mais rápido? Comparo, para bairros com pelo menos 300 pedidos de 2026, a parcela de iluminação com a taxa de atendimento:

bairros <- d26 %>%
  filter(!is.na(bairro)) %>%
  group_by(bairro, rpa_nome) %>%
  summarise(demandas = n(), pct_iluminacao = mean(grupo_servico == "Iluminação pública"),
            pct_atendida = mean(atendida), .groups = "drop") %>%
  filter(demandas >= 300)

corr_mix <- cor(bairros$pct_iluminacao, bairros$pct_atendida)

g5 <- ggplot(bairros, aes(pct_iluminacao, pct_atendida, color = rpa_nome, size = demandas,
                          text = str_glue("{bairro}<br>{fmt_p(pct_iluminacao)} iluminação<br>{fmt_p(pct_atendida)} atendidos<br>{fmt_n(demandas)} demandas"))) +
  geom_point(alpha = 0.8) +
  scale_x_continuous(labels = label_percent()) +
  scale_y_continuous(labels = label_percent()) +
  scale_color_brewer(palette = "Dark2", name = NULL) +
  scale_size_continuous(guide = "none") +
  labs(title = "Mais iluminação no pedido, mais atendimento no bairro",
       subtitle = "Cada ponto é um bairro com 300+ pedidos em 2026",
       x = "% dos pedidos que são de iluminação", y = "% dos pedidos atendidos")

ggplotly(g5, tooltip = "text")
bairros %>%
  transmute(Bairro = bairro, RPA = rpa_nome, Demandas = demandas,
            `% iluminação` = pct_iluminacao, `% atendidos` = pct_atendida) %>%
  arrange(desc(`% atendidos`)) %>%
  dt_pt(pagina = 8) %>%
  formatPercentage(c("% iluminação", "% atendidos"), 1, dec.mark = ",", mark = ".") %>%
  formatRound("Demandas", 0, mark = ".")

Insight. A correlação entre a parcela de iluminação e a taxa de atendimento nos 52 bairros é de 0,76. Isso indica que boa parte da diferença entre bairros vem do que se pede, e não só de quem atende melhor (a correlação não prova causa, e sobra diferença que o mix não explica). Um ranking de bairros só por “% atendidos” seria injusto; seria preciso comparar dentro de cada serviço, como na seção anterior.

4.6 Quando: o crescimento ao longo do semestre

# Semanas completas (segunda a domingo), para não subestimar a primeira e a última
top4 <- pareto$grupo_servico[1:4]
inicio <- as.Date("2026-01-05")
fim_ok <- max(d26$data_demanda) - days(6)

serie <- d26 %>%
  filter(grupo_servico %in% top4) %>%
  mutate(semana = floor_date(data_demanda, "week", week_start = 1)) %>%
  filter(semana >= inicio, semana <= fim_ok) %>%
  count(semana, grupo_servico)

g6 <- ggplot(serie, aes(semana, n, color = grupo_servico, group = grupo_servico,
                        text = str_glue("{grupo_servico}<br>Semana de {format(semana, '%d/%m')}: {fmt_n(n)}"))) +
  geom_line(linewidth = 0.9) +
  scale_color_brewer(palette = "Set1", name = NULL) +
  scale_x_date(date_breaks = "1 month", labels = rotulo_mes) +
  scale_y_continuous(labels = fmt_n) +
  labs(title = "Novos pedidos por semana", subtitle = "Quatro maiores grupos, semanas completas de 2026",
       x = NULL, y = "Pedidos abertos na semana")

ggplotly(g6, tooltip = "text")
# Meses completos (jan–mai); junho é parcial (dados até 19/06)
mensal <- d26 %>%
  filter(data_demanda < as.Date("2026-06-01")) %>%
  count(grupo_servico, mes = floor_date(data_demanda, "month")) %>%
  pivot_wider(names_from = mes, values_from = n)

var_mes <- function(g) {
  linha <- mensal %>% filter(grupo_servico == g)
  linha[["2026-05-01"]] / linha[["2026-01-01"]] - 1
}

Insight. De janeiro a maio, os pedidos de iluminação crescem 41,4% e os de arborização 92,7%. Para arborização, uma hipótese é o início da estação chuvosa; e, para o conjunto, o ponto de partida de janeiro pode estar baixo por férias e feriados. A base não traz o motivo dos pedidos, então ambas ficam como hipóteses.

4.7 O “legado”: uma baixa em massa de pedidos antigos

O arquivo contém demandas movimentadas em 2026, inclusive pedidos abertos anos atrás. Como eles entram porque foram movimentados, o legado não representa todos os pedidos antigos.

legado <- dados %>% filter(coorte == "Legado (até 2025)")

atu_dia <- dados %>% count(data_ult_situacao, coorte)

pico <- atu_dia %>%
  group_by(data_ult_situacao) %>% summarise(total = sum(n), .groups = "drop") %>%
  slice_max(total, n = 1)

ggplot(atu_dia, aes(data_ult_situacao, n, fill = coorte)) +
  geom_col(width = 1) +
  annotate("text", x = pico$data_ult_situacao + days(8), y = pico$total,
           label = paste0(format(pico$data_ult_situacao, "%d/%m"), ": ", fmt_n(pico$total), " atualizações"),
           hjust = 0, size = 3.6) +
  scale_fill_manual(values = c("Aberta em 2026" = "#2C7FB8", "Legado (até 2025)" = "#E6550D"), name = NULL) +
  scale_x_date(date_breaks = "1 month", labels = rotulo_mes) +
  scale_y_continuous(labels = fmt_n) +
  labs(title = "Data da última movimentação de cada demanda",
       subtitle = "Um pico isolado em janeiro, dominado por pedidos antigos", x = NULL, y = "Nº de demandas")

leg_pico <- legado %>% filter(data_ult_situacao == pico$data_ult_situacao)
leg_pico_esc <- leg_pico %>% count(grupo_servico, sort = TRUE) %>% slice(1)

Insight. São 4.833 demandas legadas (7,9% do arquivo), 94,0% delas atendidas. Só o dia 13/01/2026 concentra 1.295 atualizações, 1.030 de pedidos legados (o maior grupo é Escadaria, com 696), 99,7% marcados como “Atendida” e com mediana de 4 anos entre abertura e baixa. O padrão sugere uma baixa em massa da carteira antiga (inferência: a base não registra o motivo). Misturar esses pedidos com os de 2026 inflaria o tempo médio de atendimento, e é por isso que foram separados.

5. Conclusões

5.1 O problema

Perguntei se os pedidos de zeladoria do Recife andam todos na mesma velocidade e, se não, o que explica a diferença: serviço, território ou momento.

5.2 Como abordei

Usei o arquivo 156_cco_diario_2026.csv do Portal de Dados Abertos do Recife (61.225 registros). Limpei e padronizei texto, datas e endereços; mesclei tabelas de apoio (grupos acentuados, situações e RPAs); separei o logradouro; e criei variáveis (tempo, coorte). Cruzei serviço, região e tempo com gráficos e tabelas interativas.

5.3 O que descobri

  1. Duas velocidades. Iluminação (45,2% das demandas) tem 94,1% de atendimento e mediana de 3 dias; um bloco de 12 grupos (drenagem, pavimentação, projetos e outros), com 10.145 pedidos de 2026, quase não registra atendimento.
  2. O ritmo depende do serviço (de 3 dias na iluminação a 22 nas denúncias) e, em menor grau, da região, e não há uma RPA lenta para tudo.
  3. O “mix” de serviços engana: a parcela de iluminação está fortemente associada às diferenças entre bairros (correlação de 0,76).
  4. O volume cresce ao longo do semestre, sobretudo em arborização.
  5. O arquivo traz uma baixa em massa de pedidos antigos (13/01), que distorceria qualquer média se não fosse isolada.

5.4 Implicações para os potenciais clientes

  • Emlurb/Prefeitura: sinalizar para o cidadão os serviços em “fila” (por exemplo, “programado para obra”), para que o pedido não pareça parado; olhar a velocidade por serviço, e não pela média geral.
  • Vereadores, imprensa e associações: não comparar bairros só pela taxa de atendimento; separar pedidos de 2026 do legado.
  • Quem mantém o dado aberto: publicar dicionário de dados, identificador da demanda, data de cada mudança de etapa e coordenadas confiáveis.

5.5 Limitações e como evoluir

  • Sem dicionário: a ordem e o significado das situações são interpretações minhas.
  • tempo_dias é aproximação: só há a última data, não o histórico de etapas.
  • Seleção por movimentação em 2026: pedidos antigos sem movimentação não aparecem; por isso as taxas usam só pedidos de 2026. Os pedidos mais recentes (até 19/06) ainda estão em andamento.
  • Sem população nem equipes: não calculei taxas por habitante nem capacidade de atendimento.
  • Coordenadas não usadas: só metade das linhas as tem, o que impede um mapa fiel.
  • Próximos passos: juntar os arquivos de 2024 e 2025 do mesmo conjunto (série histórica), cruzar com dados de população por bairro e modelar a probabilidade de atendimento (regressão logística).