A Prefeitura do Recife publica, em dados abertos, a relação mensal de servidores da administração direta e indireta, com informações de cargo, lotação e remuneração. Essa transparência permite que qualquer cidadão — e gestores públicos — compreendam como a folha de pagamento está distribuída na cidade.
Neste relatório, partimos de uma pergunta central: como se organizam perfil, lotação e salários entre os servidores municipais em 2025? Em especial, buscamos saber se há diferenças relevantes entre administração direta e indireta, entre situações funcionais (ativo ou desligado) e entre gêneros.
Os dados foram obtidos no Portal de Dados Abertos do Recife, no recurso de 2025:
https://dados.recife.pe.gov.br/dataset/4af3f483-53f6-4e5a-91c4-8d486449e183/resource/e408f811-c6a7-48da-8e9e-e5d62a77fa7e/download/-2025-relacao-dos-servidores-e-salarios-da-prefeitura-do-recife.csv
O arquivo local (dataset-servidores.csv) tem cerca de
174 MB e mais de 500 mil registros,
pois cada linha representa um mês de folha de um servidor. A metodologia
segue três etapas: preparar os pacotes e entender o dicionário, importar
e limpar os dados, e por fim explorar padrões que respondam à pergunta
inicial.
Antes de qualquer análise, carregamos as bibliotecas que sustentam
todo o fluxo: leitura do CSV, manipulação com a gramática do
tidyverse, gráficos, tabelas interativas e leitura do
dicionário oficial em JSON. Instalamos apenas o que ainda não estiver
disponível no ambiente, para manter o relatório reproduzível.
pkgs <- c("readr", "dplyr", "tidyr", "ggplot2", "scales", "skimr", "DT", "jsonlite")
to_install <- pkgs[!pkgs %in% rownames(installed.packages())]
if (length(to_install) > 0) install.packages(to_install, dependencies = TRUE)
library(readr)
library(dplyr)
library(tidyr)
library(ggplot2)
library(scales)
library(skimr)
library(DT)
library(jsonlite)
| Pacote | Papel na narrativa deste relatório |
|---|---|
readr |
Importar o CSV com encoding e separador corretos |
dplyr |
Filtrar, agrupar e criar variáveis para as histórias |
tidyr |
Reorganizar dados, se precisarmos de formato longo/largo |
ggplot2 |
Contar a história em gráficos estáticos |
scales |
Apresentar valores em reais e percentuais de forma legível |
skimr |
Resumir tipos e distribuições após a limpeza |
DT |
Tabelas interativas para o leitor explorar os números |
jsonlite |
Ler o dicionário de dados publicado pela Prefeitura |
Não analisamos colunas sem saber o que significam. O arquivo
dicionario de dados.json, fornecido no portal, descreve
cada campo em metadados.campos: código técnico, descrição
em português, tipo e tamanho. Essa etapa é o “mapa” da nossa história —
sem ela, códigos como vsalseliqd seriam apenas siglas.
A tabela abaixo reproduz o dicionário oficial. Em seguida, definimos
nomes amigáveis para usar no restante do relatório (por exemplo,
vsalseliqd passa a ser salario_liquido).
arquivo_dicionario <- "dicionario de dados.json"
dict <- jsonlite::fromJSON(arquivo_dicionario, simplifyVector = TRUE)
campos_dict <- dict$metadados$campos
campos_dict |>
select(codigo, descricao, tipo, tamanho) |>
DT::datatable(
caption = "Campos documentados em metadados.campos",
rownames = FALSE,
options = list(pageLength = 10, scrollX = TRUE)
)
Com base nesse dicionário, montamos o mapeamento entre códigos
originais e nomes usados na análise. Também separamos quais campos são
numéricos (Num) e quais são datas (Date), para
aplicar a limpeza correta na etapa seguinte.
mapa_colunas <- c(
asalseanoo = "ano",
asalsemess = "mes",
csalseccpf = "cpf",
csalsematr = "matricula",
nsalsenome = "nome",
nsalseempr = "orgao",
nsalsecate = "categoria_vinculo",
nsalsecarg = "cargo",
nsalsefunc = "funcao",
vsalsecarg = "vencimento_cargo",
vsalsefunc = "gratificacao_funcao",
vsalseremu = "remuneracao",
vsalseferi = "ferias",
vsalsenatl = "gratificacao_natalina",
vsalseoutr = "outras_vantagens",
vsalseprov = "total_proventos",
vsalsedxcd = "desconto_excedentes",
vsalsedrst = "desconto_faltas",
vsalsedprv = "desconto_previdencia",
vsalsedrrf = "desconto_irrf",
vsalsedtot = "total_descontos",
vsalseliqd = "salario_liquido",
vsalsedife = "diferenca_meses_anteriores",
tslserulat = "data_hora_envio",
esalseunidade = "unidade_lotacao",
dslderadmissao = "data_admissao",
dslserdesligamento = "data_desligamento",
aslserjornadamensal = "jornada_mensal",
esalseinstrucao = "instrucao",
esalsegenero = "genero",
dsalseaposentadoria = "data_aposentadoria",
esalseadministracao = "administracao",
eslserlotacao = "lotacao",
esalsesituacao = "situacao"
)
campos_numericos <- campos_dict |>
filter(tipo == "Num") |>
pull(codigo)
campos_data <- campos_dict |>
filter(tipo == "Date") |>
pull(codigo)
Chegamos aos dados brutos. Priorizamos o arquivo
dataset-servidores.csv nesta pasta; se ele não existir,
usamos uma cópia na pasta pai ou baixamos o CSV oficial (~174 MB). Como
o portal pode exportar o arquivo com vírgula ou ponto e vírgula,
detectamos o separador automaticamente na primeira linha — assim o
relatório funciona com qualquer uma das versões.
Depois da leitura, fazemos dois ajustes pontuais: removemos a coluna
_id (presente só em exportações do DataStore) e corrigimos
o nome eselsesituacao para esalsesituacao,
alinhando-o ao dicionário. Por fim, renomeamos todas as colunas para os
nomes amigáveis definidos acima.
if (!requireNamespace("readr", quietly = TRUE)) {
install.packages("readr")
}
library(readr)
data_url <- paste0(
"https://dados.recife.pe.gov.br/dataset/",
"4af3f483-53f6-4e5a-91c4-8d486449e183/resource/",
"e408f811-c6a7-48da-8e9e-e5d62a77fa7e/download/",
"-2025-relacao-dos-servidores-e-salarios-da-prefeitura-do-recife.csv"
)
arquivo_local <- "dataset-servidores.csv"
arquivo_alternativo <- "../servidores_recife_2025.csv"
if (!file.exists(arquivo_local) && file.exists(arquivo_alternativo)) {
arquivo_local <- arquivo_alternativo
}
if (!file.exists(arquivo_local)) {
message("Baixando CSV (~174 MB). Isso pode levar alguns minutos...")
download.file(data_url, "dataset-servidores.csv", mode = "wb", quiet = TRUE)
arquivo_local <- "dataset-servidores.csv"
}
primeira_linha <- readLines(arquivo_local, n = 1, warn = FALSE, encoding = "UTF-8")
delim <- if (grepl(";", primeira_linha, fixed = TRUE)) ";" else ","
servidores_raw <- read_delim(
arquivo_local,
delim = delim,
locale = locale(encoding = "UTF-8"),
show_col_types = FALSE,
guess_max = 10000
)
if ("_id" %in% names(servidores_raw)) {
servidores_raw <- servidores_raw |> select(-`_id`)
}
if ("eselsesituacao" %in% names(servidores_raw)) {
servidores_raw <- servidores_raw |> rename(esalsesituacao = eselsesituacao)
}
codigos_presentes <- intersect(names(servidores_raw), names(mapa_colunas))
faltando <- setdiff(names(mapa_colunas), names(servidores_raw))
if (length(faltando) > 0) {
message("Colunas do dicionário ausentes no CSV: ", paste(faltando, collapse = ", "))
}
mapa_presente <- mapa_colunas[codigos_presentes]
renomear <- setNames(names(mapa_presente), unname(mapa_presente))
servidores <- servidores_raw |> rename(!!!renomear)
Os dados brutos ainda não estão prontos para gráficos e comparações.
Muitos valores monetários chegam como texto no padrão brasileiro
("8.439,13"), e as datas podem vir em formatos distintos.
Criamos funções para converter números e datas de forma consistente e
aplicamos essa conversão a todos os campos numéricos e de data indicados
no dicionário.
Também padronizamos tipos: ano e mês como inteiros, matrícula como texto, e variáveis categóricas (gênero, situação, administração, categoria de vínculo) como fatores — isso evita surpresas em contagens e gráficos mais adiante.
parse_br_num <- function(x) {
if (is.numeric(x)) return(x)
x <- as.character(x)
x[x %in% c("", "NA", "NULL")] <- NA_character_
x <- gsub("\\.", "", x)
x <- gsub(",", ".", x)
suppressWarnings(as.numeric(x))
}
parse_data_flex <- function(x) {
x <- as.character(x)
x[x %in% c("", "NA", "NULL")] <- NA_character_
out <- suppressWarnings(as.Date(substr(x, 1, 10), format = "%Y-%m-%d"))
need_alt <- is.na(out) & !is.na(x)
if (any(need_alt)) {
out[need_alt] <- suppressWarnings(
as.Date(substr(x[need_alt], 1, 10), format = "%Y/%m/%d")
)
}
out
}
cols_num <- intersect(unname(mapa_colunas[campos_numericos]), names(servidores))
cols_data <- intersect(unname(mapa_colunas[campos_data]), names(servidores))
servidores <- servidores |>
mutate(across(all_of(cols_num), parse_br_num)) |>
mutate(across(all_of(cols_data), parse_data_flex)) |>
mutate(
ano = as.integer(ano),
mes = as.integer(mes),
matricula = as.character(matricula),
genero = factor(genero),
administracao = factor(administracao),
situacao = factor(situacao),
categoria_vinculo = factor(categoria_vinculo)
)
glimpse(servidores)
## Rows: 248,500
## Columns: 34
## $ ano <int> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 2…
## $ mes <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ cpf <chr> "***753374**", "***488744**", "***206094**"…
## $ matricula <chr> "11398", "11703", "11789", "11851", "11860"…
## $ nome <chr> "ANDRE LUIZ DE O LOPES GASPAR", "THALYTA SA…
## $ orgao <chr> "EMPRESA MUNIC. DE INFORMATICA", "EMPRESA M…
## $ categoria_vinculo <fct> CELETISTAS, CELETISTAS, CELETISTAS, CELETIS…
## $ cargo <chr> "ANALISTA DE SISTEMAS", "ASS DESENV I ADM R…
## $ funcao <chr> "SEM INFORMACAO", "SEM INFORMACAO", "AUXILI…
## $ vencimento_cargo <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ gratificacao_funcao <dbl> NA, NA, 902.06, 374.09, 3027.73, 1122.25, 2…
## $ remuneracao <dbl> 0.00, 0.00, 902.06, 374.09, 3027.73, 1122.2…
## $ ferias <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, 3391.35…
## $ gratificacao_natalina <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ outras_vantagens <dbl> 9947.99, 3558.96, 11500.67, 16878.06, 16091…
## $ total_proventos <dbl> 9947.99, 3558.96, 12402.73, 17252.15, 19119…
## $ desconto_excedentes <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ desconto_faltas <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ desconto_previdencia <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ desconto_irrf <dbl> 1508.86, 33.77, 2148.78, 3430.23, 3943.64, …
## $ total_descontos <dbl> 150886, 3377, 214878, 343023, 394364, 15876…
## $ salario_liquido <dbl> 8439.13, 3525.19, 10253.95, 13821.92, 15175…
## $ diferenca_meses_anteriores <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ data_hora_envio <date> 2026-05-15, 2026-05-15, 2026-05-15, 2026-0…
## $ unidade_lotacao <chr> "DEPARTAMENTO DE DESENVOLVIMENTO I", "UNIDA…
## $ data_admissao <date> 2007-07-09, 2007-07-09, 2007-08-06, 2007-1…
## $ data_desligamento <date> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ jornada_mensal <dbl> 220, 220, 220, 220, 220, 220, 220, 220, 220…
## $ instrucao <chr> "Superior Completo", "Superior Completo", "…
## $ genero <fct> Masculino, Feminino, Masculino, Masculino, …
## $ data_aposentadoria <date> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ administracao <fct> Indireta, Indireta, Indireta, Indireta, Ind…
## $ lotacao <chr> "EMPREL", "EMPREL", "EMPREL", "EMPREL", "EM…
## $ situacao <fct> Ativo, Ativo, Ativo, Ativo, Ativo, Ativo, A…
Com a base limpa, mostramos abaixo uma amostra das colunas que conduzirão a exploração: identificação temporal, órgão, cargo, perfil e valores de proventos e salário líquido.
servidores |>
select(
ano, mes, matricula, orgao, cargo, situacao, genero, administracao,
total_proventos, salario_liquido, desconto_irrf
) |>
slice_head(n = 5) |>
DT::datatable(rownames = FALSE)
Com os dados prontos, começamos a responder à pergunta da introdução. Cada aba desta seção revela um capítulo da história: o tamanho da base, quem compõe a folha, como ela evolui no ano e como se distribuem os salários.
Primeiro, precisamos entender a escala do conjunto: quantos registros existem, quantos servidores distintos aparecem e quantos meses de 2025 estão cobertos. Isso define o “universo” de todas as conclusões seguintes.
n_registros <- nrow(servidores)
n_servidores_unicos <- n_distinct(servidores$matricula)
n_meses <- n_distinct(servidores$mes)
tibble(
registros = n_registros,
servidores_unicos = n_servidores_unicos,
meses_distintos = n_meses,
variaveis = ncol(servidores)
) |>
DT::datatable(rownames = FALSE)
Cada linha é um registro mensal de folha (combinação de ano, mês e matrícula). O mesmo servidor pode aparecer até doze vezes — uma por mês — o que é esperado em dados de folha de pagamento e deve ser lembrado ao interpretar totais e proporções.
Agora olhamos quem está na base em termos de vínculo com a Prefeitura: servidores ativos ou já desligados, e se pertencem à administração direta ou indireta. Essas duas dimensões estruturam quase qualquer comparação salarial justa — não faz sentido misturar, sem critério, quem já saiu do serviço com quem ainda está ativo, ou órgãos com regimes distintos.
tab_situacao <- servidores |>
count(situacao, sort = TRUE) |>
mutate(prop = n / sum(n))
tab_admin <- servidores |>
count(administracao, sort = TRUE) |>
mutate(prop = n / sum(n))
tab_situacao |> DT::datatable()
tab_admin |> DT::datatable()
ggplot(tab_situacao, aes(x = reorder(situacao, n), y = n, fill = situacao)) +
geom_col(show.legend = FALSE) +
coord_flip() +
scale_y_continuous(labels = label_number(big.mark = ".")) +
labs(
title = "Registros por situação funcional",
x = NULL, y = "Quantidade de registros"
) +
theme_minimal()
A grande maioria dos registros refere-se a servidores Ativos; uma parcela menor corresponde a Desligados — relevante se no futuro modelarmos desligamento como evento raro (classe desbalanceada). Quanto à administração, a Direta concentra mais registros que a Indireta, o que reflete a estrutura do funcionalismo municipal em Recife, mas não diminui a importância de analisar o subset indireto à parte.
Em seguida, examinamos a composição por gênero nos registros de folha. Esse recorte apoia discussões sobre representatividade e equidade no serviço público local — temas que costumam interessar gestores, sindicatos e a sociedade que consulta os dados abertos.
tab_genero <- servidores |>
count(genero, sort = TRUE) |>
mutate(prop = n / sum(n))
ggplot(tab_genero, aes(x = genero, y = prop, fill = genero)) +
geom_col(show.legend = FALSE) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
labs(
title = "Distribuição por gênero (registros mensais)",
x = NULL, y = "Proporção"
) +
theme_minimal()
Os registros mostram mais linhas associadas ao gênero feminino do que ao masculino. Isso não significa, por si só, desigualdade salarial — para isso seria necessário cruzar gênero com cargo e remuneração —, mas já indica um perfil demográfico marcante da folha municipal em 2025.
A folha não é estática ao longo do ano. Plotamos o volume de registros por mês para captar admissões, desligamentos ou mudanças na forma de publicação dos dados entre janeiro e dezembro de 2025.
servidores |>
count(mes, name = "registros") |>
ggplot(aes(mes, registros)) +
geom_line() +
geom_point(size = 2) +
scale_x_continuous(breaks = 1:12) +
scale_y_continuous(labels = label_number(big.mark = ".")) +
labs(
title = "Volume de registros de folha por mês (2025)",
x = "Mês", y = "Registros"
) +
theme_minimal()
Observa-se um crescimento gradual do número de registros ao longo de 2025. Esse padrão pode refletir novas admissões, atualização cadastral ou ampliação da cobertura da publicação — ponto a detalhar em análises futuras com foco em admissões e desligamentos por mês.
Chegamos ao núcleo da transparência salarial: o valor líquido recebido após descontos. Calculamos medidas resumo (média, mediana, percentil 95 e máximo) e o histograma da distribuição, limitando a visualização a valores até R$ 30 mil para facilitar a leitura — valores maiores permanecem na base para as estatísticas.
resumo_salario <- servidores |>
filter(!is.na(salario_liquido), salario_liquido >= 0) |>
summarise(
n = n(),
media = mean(salario_liquido),
mediana = median(salario_liquido),
p95 = quantile(salario_liquido, 0.95),
maximo = max(salario_liquido)
)
resumo_salario |> DT::datatable()
servidores |>
filter(!is.na(salario_liquido), salario_liquido > 0, salario_liquido < 30000) |>
ggplot(aes(salario_liquido)) +
geom_histogram(bins = 60, fill = "#2E86AB", color = "white") +
scale_x_continuous(labels = label_currency(prefix = "R$ ")) +
labs(
title = "Distribuição do salário líquido (até R$ 30 mil, para leitura)",
subtitle = "Valores acima do corte permanecem na base; só o gráfico é limitado",
x = "Salário líquido", y = "Frequência"
) +
theme_minimal()
A distribuição é assimétrica: há muitos servidores em faixas intermediárias e uma cauda à direita com remunerações bem mais altas. A mediana costuma ser menor que a média — sinal de que poucos valores elevados puxam a média para cima. Esse formato é típico em folhas públicas e motiva análises por cargo e órgão, não apenas estatísticas globais.
Por fim, identificamos onde a folha se concentra: quais cargos e quais órgãos ou secretarias geram mais registros no ano. Isso ajuda o leitor a contextualizar os números — a folha de Recife é fortemente moldada por educação, saúde e segurança, entre outras áreas.
top_cargos <- servidores |>
count(cargo, sort = TRUE) |>
slice_head(n = 15) |>
mutate(prop = n / sum(n))
top_cargos |> DT::datatable()
ggplot(top_cargos, aes(x = reorder(cargo, n), y = n)) +
geom_col(fill = "#A23B72") +
coord_flip() +
scale_y_continuous(labels = label_number(big.mark = ".")) +
labs(title = "15 cargos com mais registros", x = NULL, y = "Registros") +
theme_minimal()
servidores |>
count(orgao, sort = TRUE) |>
slice_head(n = 10) |>
DT::datatable(colnames = c("Órgão/Secretaria", "Registros"))
Cargos ligados a magistério, saúde e segurança aparecem entre os mais frequentes; a Prefeitura da Cidade do Recife concentra a maior parte dos registros, seguida por fundos e empresas municipais. Essa estrutura confirma que qualquer narrativa sobre salários em Recife precisa falar explicitamente dessas áreas estratégicas.
Esta versão do relatório é um rascunho analítico com
storytelling inicial. Para a entrega final do projeto, planejamos: obter
aprovação formal do dataset pelo professor; aprofundar a narrativa
(equidade salarial por gênero e administração); documentar decisões de
limpeza (zeros, outliers, CPF mascarado); explorar desbalanceamento em
classes raras, se houver modelagem; e incorporar gráficos interativos
com plotly, além das tabelas DT já
utilizadas.
Percorremos um caminho claro: da pergunta sobre perfil e remuneração dos servidores de Recife, passando pelo dicionário e pela preparação cuidadosa dos dados, até uma exploração que mostra quem compõe a folha, como ela evolui no tempo e como os salários líquidos se distribuem.
A base é rica e adequada ao projeto da disciplina: volume muito acima do mínimo exigido, variáveis diversas (texto, número, data, categoria) e necessidade real de limpeza e interpretação. Os primeiros resultados sugerem uma folha majoritariamente ativa e de administração direta, com forte presença feminina nos registros, crescimento mensal ao longo de 2025 e concentração em cargos de educação e saúde — um ponto de partida sólido para uma história completa sobre transparência e equidade no serviço público recifense.
Para gestores e cidadãos, o valor desta análise está em tornar visíveis padrões que, nos dados brutos, permaneceriam escondidos entre centenas de milhares de linhas. O próximo passo é refinar essa visão, sempre amarrando cada gráfico e cada tabela a uma pergunta concreta — como manda um bom relatório de data science.