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.
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.
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.
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) |
Os dados foram obtidos no Portal de Dados Abertos da Prefeitura do Recife:
dataset-servidores.csv (mesma pasta deste relatório no
RPubs)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).
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:
"8439,13").NA.YYYY-MM-DD ou YYYY/MM/DD._id em algumas exportações do
DataStore.eselsesituacao no CSV
vs. esalsesituacao no dicionário.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
)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
eselsesituacao → esalsesituacao. 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…
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):
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
)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.
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)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.
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()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)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()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()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()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.
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.
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.
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.
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.
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.