Projeto 2ª VA — Servidores e salários

Introdução

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.

Pacotes

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

Dicionário de dados

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)

Preparação dos dados

Importação

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)

Limpeza

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)

Análise exploratória

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.

Visão geral

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.

Situação funcional e administração

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.

Gênero

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.

Registros por mês

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.

Salário líquido

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.

Top cargos e órgãos

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.

Próximos passos

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.

Conclusões

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.