Projeto 2ª VA — Servidores e salários

Introdução

1.1 Declaração do problema

A folha de pagamento do município concentra uma parcela relevante do orçamento público e define, na prática, quem sustenta serviços essenciais — educação, saúde, segurança e urbanismo. Quando a Prefeitura do Recife publica, em dados abertos, a relação mensal de servidores da administração direta e indireta, abre-se a possibilidade de controle social: qualquer pessoa pode verificar como cargos, lotações e remunerações se distribuem na cidade.

Este relatório responde à pergunta: como se organizam perfil, lotação e salários entre os servidores municipais em 2025? Em especial, investigamos diferenças entre administração direta e indireta, entre situações funcionais (ativo ou desligado) e entre gêneros. Por que isso importa? Porque comparações salariais só fazem sentido quando se entende quem compõe a folha e em que condições — misturar vínculos distintos pode distorcer conclusões sobre equidade ou uso do recurso público.

1.2 Dados e metodologia

Utilizamos o conjunto Servidores do Portal de Dados Abertos do Recife, recurso de 2025, disponibilizado em dataset-servidores.csv (versão publicada no RPubs). Cada linha é um registro mensal de folha (servidor × mês), com mais de 500 mil registros no arquivo completo.

A metodologia segue quatro etapas narradas neste relatório: (1) documentar fonte e dicionário de campos; (2) importar e limpar os dados, justificando cada transformação; (3) explorar padrões com gráficos e tabelas interativas; (4) concluir com insights, implicações e limitações. O objetivo é contar uma história coerente, não apenas listar estatísticas.

1.3 Abordagem técnica

A análise emprega o ecossistema tidyverse (readr, dplyr, tidyr) para leitura e transformação, ggplot2 e plotly para visualizações, DT para tabelas interativas e jsonlite para o dicionário oficial em JSON. Números no padrão brasileiro e datas em formatos distintos são padronizados antes da exploração; variáveis categóricas passam a fator quando entram em gráficos e contagens.

1.4 Potenciais clientes da análise

  • Gestores e equipes de RH identificam concentrações por órgão, cargo ou situação funcional.
  • Jornalistas e pesquisadores obtêm base quantitativa para reportagens sobre transparência e desigualdades.
  • Cidadãos acompanham, em linguagem acessível, como a folha se distribui ao longo de 2025. Para todos, o valor está em tornar visíveis padrões que, no CSV bruto, permanecem opacos entre centenas de milhares de linhas.

Pacotes Requeridos

Para replicar esta análise, todos os pacotes necessários são carregados no início do fluxo (requisito 2.1 do projeto). Abaixo, o código de instalação condicional e library(); em seguida, a finalidade de cada biblioteca (requisito 2.2).

# Paleta visual alinhada ao CSS do relatório
cores_relatorio <- c(
  primaria = "#1a5f7a",
  secundaria = "#2E86AB",
  destaque = "#159895",
  accento = "#A23B72"
)

pkgs <- c(
  "readr", "dplyr", "tidyr", "ggplot2", "scales",
  "skimr", "DT", "jsonlite", "plotly"
)
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)
library(plotly)

# Tema ggplot alinhado ao CSS do relatório
tema_relatorio <- function() {
  theme_minimal() +
    theme(
      plot.title = element_text(
        color = unname(cores_relatorio["primaria"]),
        face = "bold",
        size = 14
      ),
      plot.subtitle = element_text(color = "#5a7a8a", size = 11),
      axis.title = element_text(color = unname(cores_relatorio["primaria"])),
      panel.grid.minor = element_blank(),
      panel.grid.major = element_line(color = "#d0e3eb", linewidth = 0.4)
    )
}

paleta_categorias <- c(
  unname(cores_relatorio["secundaria"]),
  unname(cores_relatorio["destaque"]),
  unname(cores_relatorio["accento"]),
  "#5a9bd5",
  "#7bc9be",
  "#c77dff"
)

# Tabelas DT com o mesmo visual do CSS (listras e cabeçalho colorido)
classe_dt <- "display cell-border stripe hover compact nowrap"
opcoes_dt <- list(pageLength = 10, scrollX = TRUE, dom = "lfrtip")

# Rótulos de eixos no padrão brasileiro (milhar ".", decimal ",")
label_moeda_br <- label_number(
  prefix = "R$ ",
  big.mark = ".",
  decimal.mark = ",",
  accuracy = 0.01
)
label_numero_br <- label_number(big.mark = ".", decimal.mark = ",")
Pacote Propósito no relatório
readr Importar o CSV local com encoding e separador corretos
dplyr Filtrar, agrupar, criar variáveis derivadas e resumos
tidyr Reorganizar dados (longo/largo), se necessário
ggplot2 Gráficos estáticos com títulos, eixos e escalas adequados
scales Formatar eixos em reais (R$) e percentuais
skimr Resumo compacto das variáveis após a limpeza
DT Tabelas interativas para exploração pelo leitor
jsonlite Ler o dicionário de dados oficial (metadados.campos)
plotly Gráficos interativos (requisito de visualização exploratória)

Preparação dos dados

3.1 Fonte original

Os dados foram obtidos no Portal de Dados Abertos da Prefeitura do Recife:

A fonte é pública e gratuita, mantida pela Secretaria de Administração e Gestão de Pessoas, com disponibilização técnica pela Emprel, sob licença Open Database License (ODbL).

3.2 Descrição da base original

Segundo a descrição oficial do portal:

Informações sobre os servidores da administração direta e indireta da Prefeitura de Recife; contém informações dos cargos, funções, perfil dos servidores, lotação e salários.

Propósito original: transparência da folha e do funcionalismo municipal (vínculos, lotação, remuneração e perfil de servidores ativos e desligados, administração direta e indireta).

Período e atualização: registros de 2025 (ano, mes). O portal não informa data única de coleta; o recurso foi atualizado pela última vez em 15/05/2026, com atualização mensal prevista na série.

Granularidade: uma linha = registro mensal de folha (ano + mês + matrícula); o mesmo servidor pode aparecer até 12 vezes.

Variáveis: o dicionário dicionario de dados.json documenta 34 campos — tabela abaixo.

Peculiaridades dos dados de origem:

  • CPF anonimizado (asteriscos).
  • Valores monetários muitas vezes como texto ("8439,13").
  • Ausências como campo vazio, não sempre NA.
  • Datas em YYYY-MM-DD ou YYYY/MM/DD.
  • Coluna extra _id em algumas exportações do DataStore.
  • Campo eselsesituacao no CSV vs. esalsesituacao no dicionário.
  • Separador do CSV: vírgula ou ponto e vírgula.
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 = paste0("Dicionário oficial — ", nrow(campos_dict), " campos"),
    rownames = FALSE,
    class = classe_dt,
    options = opcoes_dt
  )
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)

variaveis_data <- unname(mapa_colunas[campos_data])

# Numéricos que não são valores em reais (não arredondamos na limpeza)
cols_num_nao_monetarios <- c("ano", "mes", "jornada_mensal")
variaveis_monetarias <- setdiff(
  unname(mapa_colunas[campos_numericos]),
  cols_num_nao_monetarios
)

3.3 Importação e limpeza

Importação: lemos apenas dataset-servidores.csv (RPubs). Detectamos o separador na primeira linha porque exportações usam , ou ;. Removemos _id (não consta no dicionário) e corrigimos eselsesituacaoesalsesituacao. Renomeamos colunas conforme mapa_colunas para tornar o código legível.

Limpeza: convertemos valores monetários do padrão brasileiro para numérico (parse_br_num) porque o PDF da fonte mistura texto e número; arredondamos apenas colunas monetárias (proventos, descontos, salário líquido etc.) para duas casas decimais (round(..., 2)), coerente com a folha em reais — ano, mes e jornada_mensal permanecem sem esse arredondamento; padronizamos datas (parse_data_flex e parse_data_hora_flex) pelos formatos observados na fonte; definimos fatores em variáveis categóricas para contagens e gráficos corretos.

Exibição numérica: nas tabelas DT, no glimpse e nos eixos dos gráficos, valores monetários aparecem no padrão brasileiro (ex.: 1.389,57 — ponto para milhar, vírgula para decimal); proporções usam vírgula como separador decimal na visualização. A base analítica permanece numérica para cálculos.

Exibição de datas: internamente, as colunas de data permanecem como Date ou POSIXct para filtros e cálculos. Em todas as saídas do relatório (tabelas DT, glimpse e demais visualizações tabulares), elas são apresentadas no padrão brasileiro dd-MM-yyyy; data_hora_envio inclui hora (dd-MM-yyyy HH:MM:SS). As variáveis afetadas são: data_hora_envio, data_admissao, data_desligamento, data_aposentadoria.

arquivo_local <- "dataset-servidores.csv"

if (!file.exists(arquivo_local)) {
  stop(
    "Arquivo 'dataset-servidores.csv' não encontrado. ",
    "Coloque o CSV na pasta do projeto antes do knit."
  )
}

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

round_monetario <- function(x) round(x, 2)

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
}

parse_data_hora_flex <- function(x) {
  x <- as.character(x)
  x[x %in% c("", "NA", "NULL")] <- NA_character_
  out <- suppressWarnings(
    as.POSIXct(x, format = "%Y-%m-%d %H:%M:%S", tz = "America/Recife")
  )
  need_data <- is.na(out) & !is.na(x)
  if (any(need_data)) {
    out[need_data] <- suppressWarnings(
      as.POSIXct(
        substr(x[need_data], 1, 10),
        format = "%Y-%m-%d",
        tz = "America/Recife"
      )
    )
  }
  need_alt <- is.na(out) & !is.na(x)
  if (any(need_alt)) {
    out[need_alt] <- suppressWarnings(
      as.POSIXct(
        substr(x[need_alt], 1, 10),
        format = "%Y/%m/%d",
        tz = "America/Recife"
      )
    )
  }
  out
}

format_br_data <- function(x) {
  if (inherits(x, "POSIXt")) {
    out <- format(x, "%d-%m-%Y %H:%M:%S")
    out[is.na(x)] <- NA_character_
    return(out)
  }
  if (inherits(x, "Date")) {
    out <- format(x, "%d-%m-%Y")
    out[is.na(x)] <- NA_character_
    return(out)
  }
  parsed <- parse_data_flex(x)
  out <- format(parsed, "%d-%m-%Y")
  out[is.na(parsed)] <- NA_character_
  out
}

format_br_numero <- function(x, digits = 2) {
  if (!is.numeric(x)) return(x)
  ifelse(
    is.na(x),
    NA_character_,
    format(
      round(x, digits),
      nsmall = digits,
      big.mark = ".",
      decimal.mark = ",",
      trim = TRUE,
      scientific = FALSE
    )
  )
}

format_br_moeda <- function(x) {
  ifelse(is.na(x), NA_character_, paste0("R$ ", format_br_numero(x, digits = 2)))
}

formatar_datas_para_exibicao <- function(df) {
  cols <- intersect(variaveis_data, names(df))
  if (length(cols) == 0) return(df)
  df |>
    mutate(across(all_of(cols), format_br_data, .names = "{.col}"))
}

formatar_para_exibicao <- function(df) {
  df <- formatar_datas_para_exibicao(df)

  cols_moeda <- intersect(
    c(variaveis_monetarias, "media", "mediana", "p95", "maximo"),
    names(df)
  )
  cols_moeda <- cols_moeda[vapply(df[cols_moeda], is.numeric, logical(1))]
  if (length(cols_moeda) > 0) {
    df <- df |> mutate(across(all_of(cols_moeda), format_br_numero))
  }

  cols_prop <- intersect(
    c("prop", "proporcao_descontos", "mediana_prop_descontos", "media_prop_descontos", "prop_top15"),
    names(df)
  )
  cols_prop <- setdiff(cols_prop, cols_moeda)
  cols_prop <- cols_prop[vapply(df[cols_prop], is.numeric, logical(1))]
  if (length(cols_prop) > 0) {
    df <- df |> mutate(across(all_of(cols_prop), ~ format_br_numero(.x, digits = 4)))
  }

  df
}

dt_exibir <- function(df, ...) {
  df_fmt <- formatar_datas_para_exibicao(df)
  dt <- DT::datatable(df_fmt, ...)

  cols_moeda <- intersect(
    c(variaveis_monetarias, "media", "mediana", "p95", "maximo"),
    names(df_fmt)
  )
  cols_moeda <- cols_moeda[vapply(df_fmt[cols_moeda], is.numeric, logical(1))]
  if (length(cols_moeda) > 0) {
    dt <- DT::formatRound(
      dt,
      columns = which(names(df_fmt) %in% cols_moeda),
      digits = 2,
      mark = ".",
      dec = ","
    )
  }

  cols_prop <- intersect(
    c("prop", "proporcao_descontos", "mediana_prop_descontos", "media_prop_descontos", "prop_top15"),
    names(df_fmt)
  )
  cols_prop <- setdiff(cols_prop, cols_moeda)
  cols_prop <- cols_prop[vapply(df_fmt[cols_prop], is.numeric, logical(1))]
  if (length(cols_prop) > 0) {
    dt <- DT::formatRound(
      dt,
      columns = which(names(df_fmt) %in% cols_prop),
      digits = 4,
      mark = ".",
      dec = ","
    )
  }

  dt
}

cols_num <- intersect(unname(mapa_colunas[campos_numericos]), names(servidores))
cols_monetarias <- intersect(variaveis_monetarias, names(servidores))
cols_data <- intersect(variaveis_data, names(servidores))
cols_data_simples <- setdiff(cols_data, "data_hora_envio")

servidores <- servidores |>
  mutate(across(all_of(cols_num), parse_br_num)) |>
  mutate(across(all_of(cols_monetarias), round_monetario)) |>
  mutate(across(all_of(cols_data_simples), 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)
  )

if ("jornada_mensal" %in% names(servidores)) {
  servidores <- servidores |>
    mutate(jornada_mensal = as.integer(jornada_mensal))
}

if ("data_hora_envio" %in% names(servidores)) {
  servidores <- servidores |>
    mutate(data_hora_envio = parse_data_hora_flex(data_hora_envio))
}

servidores |> formatar_para_exibicao() |> glimpse()
## 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           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ gratificacao_funcao        <chr> NA, NA, "902,06", "374,09", "3.027,73", "1.…
## $ remuneracao                <chr> "0,00", "0,00", "902,06", "374,09", "3.027,…
## $ ferias                     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, "3.391,…
## $ gratificacao_natalina      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ outras_vantagens           <chr> "9.947,99", "3.558,96", "11.500,67", "16.87…
## $ total_proventos            <chr> "9.947,99", "3.558,96", "12.402,73", "17.25…
## $ desconto_excedentes        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ desconto_faltas            <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ desconto_previdencia       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ desconto_irrf              <chr> "1.508,86", "33,77", "2.148,78", "3.430,23"…
## $ total_descontos            <chr> "150.886,00", "3.377,00", "214.878,00", "34…
## $ salario_liquido            <chr> "8.439,13", "3.525,19", "10.253,95", "13.82…
## $ diferenca_meses_anteriores <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ data_hora_envio            <chr> "15-05-2026 16:33:37", "15-05-2026 16:33:37…
## $ unidade_lotacao            <chr> "DEPARTAMENTO DE DESENVOLVIMENTO I", "UNIDA…
## $ data_admissao              <chr> "09-07-2007", "09-07-2007", "06-08-2007", "…
## $ data_desligamento          <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ jornada_mensal             <int> 220, 220, 220, 220, 220, 220, 220, 220, 220…
## $ instrucao                  <chr> "Superior Completo", "Superior Completo", "…
## $ genero                     <fct> Masculino, Feminino, Masculino, Masculino, …
## $ data_aposentadoria         <chr> 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…

3.4 Conjunto de dados final (visão condensada)

Após a limpeza, a base servidores contém 248500 registros e 34 variáveis. Valores monetários ficam com até duas casas decimais, exibidos no formato 1.389,57; demais numéricos (ano, mes, jornada_mensal) mantêm tipo adequado sem arredondamento monetário. Não exibimos mais de 200 linhas; abaixo, cinco registros das colunas centrais da análise (datas no formato dd-MM-yyyy):

servidores |>
  select(
    ano, mes, matricula, orgao, cargo, situacao, genero, administracao,
    total_proventos, salario_liquido, desconto_irrf,
    data_admissao
  ) |>
  slice_head(n = 5) |>
  dt_exibir(rownames = FALSE, class = classe_dt, options = opcoes_dt)

3.5 Resumo das variáveis de interesse

Consolidamos o dicionário oficial com os nomes usados na análise e contamos ausências nas variáveis que estruturam a EDA — sem repetir blocos soltos de str() ou summary() em cada coluna.

vars_interesse <- c(
  "ano", "mes", "matricula", "orgao", "cargo", "situacao", "genero",
  "administracao", "categoria_vinculo", "total_proventos", "salario_liquido",
  "desconto_irrf", "data_admissao", "data_desligamento"
)

tabela_variaveis <- campos_dict |>
  mutate(
    variavel = unname(mapa_colunas[codigo]),
    .before = 1
  ) |>
  filter(variavel %in% vars_interesse) |>
  select(variavel, descricao, tipo, tamanho)

tabela_variaveis |> dt_exibir(
  caption = "Variáveis de interesse: nome na análise e descrição oficial",
  rownames = FALSE,
  class = classe_dt,
  options = opcoes_dt
)
servidores |>
  select(any_of(vars_interesse)) |>
  summarise(across(everything(), ~ sum(is.na(.)))) |>
  pivot_longer(everything(), names_to = "variavel", values_to = "qtd_na") |>
  dt_exibir(
    caption = "Quantidade de valores ausentes por variável",
    rownames = FALSE,
    class = classe_dt,
    options = opcoes_dt
  )

Análise exploratória dos dados

Esta seção reúne a exploração em três blocos — use as sub-abas abaixo para navegar entre descobertas, visualizações e insights.

Descobertas além do óbvio

Não limitamos a análise a médias globais. Exploramos quem compõe a folha (situação, administração, gênero), como o volume evolui mês a mês e onde se concentram cargos e órgãos. Criamos também a variável proporcao_descontos (total de descontos ÷ total de proventos), para comparar o peso dos descontos na remuneração bruta — informação que não aparece pronta no CSV original.

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_exibir(rownames = FALSE, class = classe_dt, options = opcoes_dt)
servidores <- servidores |>
  mutate(
    proporcao_descontos = if_else(
      !is.na(total_proventos) & total_proventos > 0,
      total_descontos / total_proventos,
      NA_real_
    )
  )

servidores |>
  filter(!is.na(proporcao_descontos)) |>
  summarise(
    mediana_prop_descontos = median(proporcao_descontos, na.rm = TRUE),
    media_prop_descontos = mean(proporcao_descontos, na.rm = TRUE)
  ) |>
  dt_exibir(rownames = FALSE, class = classe_dt, options = opcoes_dt)

Gráficos, tabelas e recursos interativos

Apresentamos a exploração visual em etapas: primeiro quem compõe a folha (situação, administração e gênero), depois como o volume evolui ao longo de 2025, em seguida remuneração líquida e, por fim, onde se concentram cargos e órgãos. Cada bloco traz uma leitura orientada pelos números da própria base.

Composição da folha: situação e administração

Antes de comparar salários, precisamos saber em que condição cada registro aparece na base. Entre 248.500 registros de folha, a situação Ativo concentra 87,0% do total (216.094 linhas), enquanto Desligado responde por 13,0% (32.406 registros). Isso indica que a folha publicada é majoritariamente de vínculos em exercício, e eventuais comparações salariais devem considerar esse recorte.

Quanto ao tipo de administração, Direta predomina com 72,1% dos registros (179.119 linhas), contra 27,9% na administração Indireta. Servidores da administração indireta (autarquias, fundações etc.) aparecem, portanto, em menor volume nesta base — mas não necessariamente com o mesmo padrão salarial.

tab_situacao |> dt_exibir(
  caption = "Situação funcional",
  rownames = FALSE,
  class = classe_dt,
  options = opcoes_dt
)
tab_admin |> dt_exibir(
  caption = "Tipo de administração",
  rownames = FALSE,
  class = classe_dt,
  options = opcoes_dt
)

O gráfico abaixo traduz a tabela de situação funcional em barras: a distância entre categorias deixa claro o quanto ‘Ativo’ supera as demais situações, reforçando que análises futuras sobre desligamentos ou aposentadorias precisariam de recortes específicos.

ggplot(tab_situacao, aes(x = reorder(situacao, n), y = n, fill = situacao)) +
  geom_col(show.legend = FALSE) +
  coord_flip() +
  scale_fill_manual(values = paleta_categorias) +
  scale_y_continuous(labels = label_numero_br) +
  labs(
    title = "Registros por situação funcional",
    x = NULL, y = "Quantidade de registros"
  ) +
  tema_relatorio()

Perfil por gênero

A variável gênero descreve o recorte dos registros mensais (não a população única de servidores). Feminino aparece em 70,8% das linhas (175.843 registros), e Masculino em 29,2%. Essa diferença sugere maior presença feminina na folha publicada, o que dialoga com áreas historicamente feminizadas (educação e saúde), mas não prova, isoladamente, diferença salarial entre gêneros.

p_genero <- ggplot(tab_genero, aes(x = genero, y = prop, fill = genero)) +
  geom_col(show.legend = FALSE) +
  scale_fill_manual(values = paleta_categorias) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(
    title = "Distribuição por gênero (registros mensais)",
    x = NULL, y = "Proporção"
  ) +
  tema_relatorio()

ggplotly(p_genero)

Evolução mensal em 2025

Cada mês de 2025 adiciona um conjunto de registros à base. O menor volume ocorre em junho (30.110 registros) e o maior, em maio (45.018 registros). A linha temporal ajuda a distinguir efeito de admissões ou atualizações da publicação de variações pontuais — útil para quem for comparar meses isolados sem contexto.

mes_registros |>
  ggplot(aes(mes, registros)) +
  geom_line(linewidth = 1, color = unname(cores_relatorio["secundaria"])) +
  geom_point(size = 3, color = unname(cores_relatorio["destaque"])) +
  scale_x_continuous(breaks = 1:12) +
  scale_y_continuous(labels = label_numero_br) +
  labs(
    title = "Volume de registros de folha por mês (2025)",
    x = "Mês", y = "Registros"
  ) +
  tema_relatorio()

Salário líquido: resumo e distribuição

O salário líquido é o valor percebido após descontos. Entre registros válidos (248.500 linhas), a mediana é R$ 4.458,19 e a média, R$ 5.478,96. Como a média supera a mediana, poucos valores altos puxam a média para cima. O percentil 95 (R$ 11.902,23) e o máximo (R$ 791.295,80) indicam a cauda superior da distribuição.

resumo_salario |> dt_exibir(
  caption = "Resumo do salário líquido",
  rownames = FALSE,
  class = classe_dt,
  options = opcoes_dt
)

O histograma limita o eixo a R$ 30 mil apenas para legibilidade (valores maiores permanecem na base e entram na tabela acima). A concentração de barras à esquerda confirma uma folha em que a maioria recebe valores moderados, com poucos extremos à direita.

servidores |>
  filter(!is.na(salario_liquido), salario_liquido > 0, salario_liquido < 30000) |>
  ggplot(aes(salario_liquido)) +
  geom_histogram(
    bins = 60,
    fill = unname(cores_relatorio["secundaria"]),
    color = "white"
  ) +
  scale_x_continuous(labels = label_moeda_br) +
  labs(
    title = "Distribuição do salário líquido (até R$ 30 mil)",
    subtitle = "Valores acima do corte permanecem na base; só o gráfico é limitado",
    x = "Salário líquido", y = "Frequência"
  ) +
  tema_relatorio()

Concentração por cargo

A folha se distribui por centenas de cargos distintos; na tabela e no gráfico abaixo destacamos os 15 com mais registros. O cargo PROFESSOR I lidera com 42.924 linhas, seguido por ESTAGIARIO (15.896) e AGENTE COMUNITRIO DE SADE (13.989). Esse padrão aponta para a forte presença de magistério, saúde e funções operacionais no funcionalismo municipal.

top_cargos |> select(cargo, n, prop_top15) |> dt_exibir(
  caption = "15 cargos com mais registros",
  rownames = FALSE,
  class = classe_dt,
  options = opcoes_dt
)
ggplot(top_cargos, aes(x = reorder(cargo, n), y = n)) +
  geom_col(fill = unname(cores_relatorio["accento"])) +
  coord_flip() +
  scale_y_continuous(labels = label_numero_br) +
  labs(title = "15 cargos com mais registros", x = NULL, y = "Registros") +
  tema_relatorio()

Concentração por órgão

Por fim, olhamos onde os servidores estão lotados. PREFEITURA DA CIDADE DO RECIFE concentra 179.119 registros — o maior volume entre os dez primeiros órgãos — o que reflete tanto a estrutura administrativa da Prefeitura quanto a publicação de dados por secretaria. A tabela interativa permite ordenar e buscar outros órgãos além do topo.

top_orgaos |> dt_exibir(
  colnames = c("Órgão/Secretaria", "Registros"),
  rownames = FALSE,
  class = classe_dt,
  options = opcoes_dt
)

Insights da exploração

  • A maioria dos registros é de servidores Ativos; Desligados formam minoria — relevante se no futuro modelarmos classes desbalanceadas.
  • A administração Direta predomina sobre a Indireta.
  • mais registros associados ao gênero feminino do que ao masculino (não implica, sozinho, diferença salarial).
  • O volume de registros cresce ao longo de 2025, sugerindo admissões ou ampliação da publicação.
  • O salário líquido é assimétrico (mediana abaixo da média); poucos valores altos puxam a média.
  • A folha concentra-se em educação, saúde e segurança (professores, agentes comunitários, segurança municipal) e na Prefeitura da Cidade do Recife como órgão principal.

Conclusões

5.1 Problema revisitado

Este relatório examinou como se distribuem perfil, lotação e remunerações na folha de servidores da Prefeitura do Recife em 2025, usando dados abertos para apoiar transparência e discussão sobre equidade no serviço público municipal.

5.2 Metodologia em síntese

Partimos do recurso oficial de 2025, documentado com dicionário JSON, importamos dataset-servidores.csv, limpamos tipos e formatos brasileiros, e exploramos a base com gráficos (ggplot2, plotly) e tabelas interativas (DT), incluindo uma variável derivada de proporção de descontos.

5.3 Principais insights

A folha é volumosa e mensal por servidor; a maioria dos vínculos está ativa e na administração direta; há forte presença feminina nos registros; salários líquidos seguem distribuição assimétrica; cargos de magistério e saúde e a Prefeitura concentram a maior parte dos registros.

5.4 Implicações para os clientes

Gestores podem priorizar auditorias em órgãos e cargos de maior volume; jornalistas têm números para contextualizar reportagens sobre folha; cidadãos ganham um retrato acessível de para onde vai a remuneração pública em Recife — desde que leiam os recortes (situação, administração, mês) e não apenas totais brutos.

5.5 Limitações e melhorias futuras

Limitações: CPF mascarado impede cruzamentos externos; registros mensais repetem servidores; arquivo grande exige ambiente com memória adequada; análise descritiva, sem modelagem preditiva; publicação no RPubs depende do CSV local.

Melhorias: cruzar gênero × cargo × salário; analisar desligamentos por mês; comparar Direta vs Indireta com mediana salarial por grupo; incorporar séries de outros anos do mesmo portal.