Casos de Uso: Análise do Mercado de Trabalho

library(datacaged)
library(dplyr)
library(ggplot2)
library(lubridate)
library(scales)

Esta vinheta apresenta análises prontas do mercado de trabalho formal usando o datacaged. Cada caso de uso é independente — você pode executar apenas o trecho de interesse.

Dados necessários (executar uma vez):

# Novo CAGED 2023
caged_load(years = 2023, months = seq_len(12L), db_path = "caged.duckdb")

# Para casos com série histórica, adicionar:
caged_load(years = 2020:2022, months = seq_len(12L), db_path = "caged.duckdb")
caged_load(years = 2015:2019, months = seq_len(12L), db_path = "caged.duckdb")
caged_adjustments_load(years = 2015:2019, months = seq_len(12L), db_path = "caged.duckdb")
con <- caged_connect("caged.duckdb")

Caso 1 — Saldo de empregos: admissões vs desligamentos

Análise do fluxo mensal de entrada e saída no mercado de trabalho formal.

fluxo <- tbl(con, "caged_mov") |>
  group_by(competenciamov) |>
  summarise(
    admissoes     = sum(saldomovimentacao ==  1, na.rm = TRUE),
    desligamentos = sum(saldomovimentacao == -1, na.rm = TRUE),
    saldo         = sum(saldomovimentacao,       na.rm = TRUE)
  ) |>
  collect() |>
  mutate(
    data = ym(as.character(competenciamov))
  ) |>
  arrange(data)
ggplot(fluxo, aes(x = data)) +
  geom_col(aes(y = admissoes),      fill = "#2196F3", alpha = 0.8) +
  geom_col(aes(y = -desligamentos), fill = "#F44336", alpha = 0.8) +
  geom_line(aes(y = saldo), color = "#212121", linewidth = 1.2) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
  scale_y_continuous(labels = label_number(scale = 1e-6, suffix = "M")) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b/%y") +
  labs(
    title    = "Fluxo de Emprego Formal — Novo CAGED 2023",
    subtitle = "Barras azuis = admissões | barras vermelhas = desligamentos | linha = saldo",
    x        = NULL,
    y        = "Movimentações",
    caption  = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 12) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Caso 2 — Saldo por setor econômico (CNAE)

Identifica quais setores geraram e destruíram mais empregos formais no período.

# Dicionário de seções da CNAE 2.0
cnae_secoes <- tibble::tribble(
  ~secao, ~descricao,
  "A",  "Agricultura",
  "B",  "Indústrias Extrativas",
  "C",  "Indústria de Transformação",
  "D",  "Eletricidade e Gás",
  "E",  "Água e Saneamento",
  "F",  "Construção",
  "G",  "Comércio",
  "H",  "Transporte",
  "I",  "Alojamento e Alimentação",
  "J",  "Informação e Comunicação",
  "K",  "Atividades Financeiras",
  "L",  "Atividades Imobiliárias",
  "M",  "Profissionais e Técnicas",
  "N",  "Atividades Administrativas",
  "O",  "Adm. Pública",
  "P",  "Educação",
  "Q",  "Saúde",
  "R",  "Artes e Cultura",
  "S",  "Outras Atividades",
  "T",  "Serviços Domésticos",
  "U",  "Organismos Internacionais"
)

setor <- tbl(con, "caged_mov") |>
  filter(!is.na(secao)) |>
  group_by(secao) |>
  summarise(saldo = sum(saldomovimentacao, na.rm = TRUE)) |>
  collect() |>
  left_join(cnae_secoes, by = "secao") |>
  mutate(
    descricao = coalesce(descricao, paste("Setor", secao)),
    cor       = if_else(saldo >= 0, "#2196F3", "#F44336")
  ) |>
  arrange(saldo)
ggplot(setor, aes(x = saldo, y = reorder(descricao, saldo), fill = cor)) +
  geom_col(show.legend = FALSE) +
  geom_vline(xintercept = 0, color = "grey30") +
  scale_fill_identity() +
  scale_x_continuous(labels = label_number(scale = 1e-3, suffix = "k")) +
  labs(
    title   = "Saldo de Empregos por Setor Econômico — 2023",
    x       = "Saldo (mil vínculos)",
    y       = NULL,
    caption = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11) +
  theme(panel.grid.major.y = element_blank())

Caso 3 — Mapa de calor: saldo por UF e mês

Visualização de qual UF e em qual mês o emprego formal foi mais aquecido.

# Dicionário de UFs (código IBGE -> sigla)
data("uf_codigos")

uf_mensal <- tbl(con, "caged_mov") |>
  group_by(uf, competenciamov) |>
  summarise(saldo = sum(saldomovimentacao, na.rm = TRUE)) |>
  collect() |>
  left_join(uf_codigos, by = c("uf" = "codigo")) |>
  filter(!is.na(sigla)) |>
  mutate(
    data = ym(as.character(competenciamov)),
    mes  = format(data, "%b")
  )
ggplot(uf_mensal, aes(x = mes, y = reorder(sigla, saldo), fill = saldo)) +
  geom_tile(color = "white", linewidth = 0.3) +
  scale_fill_gradient2(
    low      = "#F44336",
    mid      = "white",
    high     = "#2196F3",
    midpoint = 0,
    labels   = label_number(scale = 1e-3, suffix = "k")
  ) +
  labs(
    title   = "Saldo de Empregos por UF e Mês — 2023",
    x       = NULL,
    y       = NULL,
    fill    = "Saldo",
    caption = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 10) +
  theme(
    axis.text.x    = element_text(angle = 45, hjust = 1),
    legend.position = "right"
  )

Caso 4 — Perfil do trabalhador admitido

Caracterização demográfica dos novos vínculos formais por sexo, escolaridade e raça/cor.

# Dicionários de categorias
grau_instrucao <- tibble::tribble(
  ~graudeinstrucao, ~escolaridade,
  1L, "Analfabeto",
  2L, "Fundamental incompleto",
  3L, "Fundamental completo",
  4L, "Médio incompleto",
  5L, "Médio completo",
  6L, "Superior incompleto",
  7L, "Superior completo",
  8L, "Mestrado",
  9L, "Doutorado"
)

raca_dic <- tibble::tribble(
  ~racacor, ~raca,
  1L, "Indígena",
  2L, "Branca",
  4L, "Preta",
  6L, "Amarela",
  8L, "Parda",
  9L, "Não informada"
)

# Admissões por escolaridade e sexo
perfil_escol <- tbl(con, "caged_mov") |>
  filter(saldomovimentacao == 1, !is.na(graudeinstrucao)) |>
  group_by(graudeinstrucao, sexo) |>
  summarise(n = n()) |>
  collect() |>
  left_join(grau_instrucao, by = "graudeinstrucao") |>
  mutate(
    sexo_label = if_else(sexo == 1, "Masculino", "Feminino"),
    escolaridade = factor(escolaridade, levels = grau_instrucao$escolaridade)
  )
ggplot(perfil_escol, aes(x = escolaridade, y = n, fill = sexo_label)) +
  geom_col(position = "dodge") +
  scale_y_continuous(labels = label_number(scale = 1e-3, suffix = "k")) +
  scale_fill_manual(values = c("Masculino" = "#1565C0", "Feminino" = "#AD1457")) +
  labs(
    title   = "Admissões por Escolaridade e Sexo — 2023",
    x       = NULL,
    y       = "Admissões (mil)",
    fill    = NULL,
    caption = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    axis.text.x    = element_text(angle = 40, hjust = 1),
    legend.position = "top"
  )
# Admissões por raça/cor
perfil_raca <- tbl(con, "caged_mov") |>
  filter(saldomovimentacao == 1, !is.na(racacor)) |>
  group_by(racacor) |>
  summarise(n = n()) |>
  collect() |>
  left_join(raca_dic, by = "racacor") |>
  mutate(pct = n / sum(n))
ggplot(perfil_raca, aes(x = reorder(raca, n), y = n, fill = raca)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = percent(pct, accuracy = 0.1)), hjust = -0.1, size = 3.5) +
  coord_flip() +
  scale_y_continuous(
    labels = label_number(scale = 1e-6, suffix = "M"),
    expand = expansion(mult = c(0, 0.15))
  ) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title   = "Distribuição de Admissões por Raça/Cor — 2023",
    x       = NULL,
    y       = "Admissões",
    caption = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11)

Caso 5 — Distribuição salarial das admissões

Análise dos salários declarados nas novas contratações.

salarios <- tbl(con, "caged_mov") |>
  filter(
    saldomovimentacao == 1,
    !is.na(salario),
    salario > 0,
    salario < 30000   # remove outliers extremos
  ) |>
  select(salario, sexo, graudeinstrucao) |>
  collect() |>
  mutate(
    sexo_label = if_else(sexo == 1, "Masculino", "Feminino"),
    log_salario = log10(salario)
  )

# Percentis salariais
salarios |>
  group_by(sexo_label) |>
  summarise(
    p10    = quantile(salario, 0.10),
    mediana = median(salario),
    media   = mean(salario),
    p90    = quantile(salario, 0.90)
  )
ggplot(salarios, aes(x = salario, fill = sexo_label)) +
  geom_histogram(
    aes(y = after_stat(density)),
    bins     = 60,
    alpha    = 0.6,
    position = "identity"
  ) +
  geom_vline(
    data = salarios |>
      group_by(sexo_label) |>
      summarise(med = median(salario)),
    aes(xintercept = med, color = sexo_label),
    linewidth = 1.2, linetype = "dashed"
  ) +
  scale_x_continuous(
    labels = label_dollar(prefix = "R$", big.mark = ".", decimal.mark = ","),
    limits = c(0, 10000)
  ) +
  scale_fill_manual(values  = c("Masculino" = "#1565C0", "Feminino" = "#AD1457")) +
  scale_color_manual(values = c("Masculino" = "#0D47A1", "Feminino" = "#880E4F")) +
  labs(
    title    = "Distribuição Salarial das Admissões por Sexo — 2023",
    subtitle = "Linhas tracejadas indicam a mediana salarial de cada grupo",
    x        = "Salário de admissão (R$)",
    y        = "Densidade",
    fill     = NULL,
    color    = NULL,
    caption  = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11) +
  theme(legend.position = "top")

Caso 6 — Evolução do emprego formal: série 2015-2023

Construção de uma série histórica longa unindo CAGED antigo (até 2019) e Novo CAGED (2020 em diante).

# CAGED antigo: competencia + saldomovimentacao
antigo <- tbl(con, "caged_antigo") |>
  filter(!is.na(competencia)) |>
  group_by(competencia) |>
  summarise(saldo = sum(saldomovimentacao, na.rm = TRUE)) |>
  collect() |>
  mutate(serie = "CAGED Antigo")

# Ajustes: somados ao antigo para corrigir a série
ajustes <- tbl(con, "caged_ajustes") |>
  filter(!is.na(competencia)) |>
  group_by(competencia) |>
  summarise(saldo_ajuste = sum(saldomovimentacao, na.rm = TRUE)) |>
  collect()

antigo_corrigido <- left_join(antigo, ajustes, by = "competencia") |>
  mutate(
    saldo = saldo + coalesce(saldo_ajuste, 0L),
    serie = "CAGED Antigo (corrigido)"
  ) |>
  select(competencia, saldo, serie)

# Novo CAGED: competenciamov + saldomovimentacao
novo <- tbl(con, "caged_mov") |>
  group_by(competencia = competenciamov) |>
  summarise(saldo = sum(saldomovimentacao, na.rm = TRUE)) |>
  collect() |>
  mutate(serie = "Novo CAGED")

# União das séries
serie_longa <- bind_rows(antigo_corrigido, novo) |>
  filter(!is.na(competencia)) |>
  mutate(data = ym(as.character(competencia))) |>
  filter(data >= ym("201501")) |>
  arrange(data)
ggplot(serie_longa, aes(x = data, y = saldo, color = serie)) +
  geom_line(linewidth = 1) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
  annotate("rect",
    xmin = ym("202001"), xmax = ym("202012"),
    ymin = -Inf, ymax = Inf,
    alpha = 0.08, fill = "orange"
  ) +
  annotate("text",
    x = ym("202006"), y = Inf, vjust = 1.5,
    label = "Pandemia\nCOVID-19", size = 3, color = "darkorange"
  ) +
  scale_y_continuous(labels = label_number(scale = 1e-6, suffix = "M")) +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_color_manual(values = c(
    "CAGED Antigo (corrigido)" = "#795548",
    "Novo CAGED"               = "#1565C0"
  )) +
  labs(
    title    = "Saldo de Empregos Formais — Série Histórica 2015-2023",
    subtitle = "Antigo CAGED corrigido pelos Ajustes + Novo CAGED",
    x        = NULL,
    y        = "Saldo mensal",
    color    = NULL,
    caption  = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")

Caso 7 — Rotatividade por setor

Taxa de rotatividade (desligamentos / estoque estimado) por setor econômico.

# cnae_secoes também definido aqui para que o Caso 7 possa ser executado de forma independente
cnae_secoes <- tibble::tribble(
  ~secao, ~descricao,
  "A",  "Agricultura",       "B", "Indústrias Extrativas",
  "C",  "Ind. de Transformação", "D", "Eletricidade e Gás",
  "E",  "Água e Saneamento", "F", "Construção",
  "G",  "Comércio",          "H", "Transporte",
  "I",  "Alojamento e Alimentação", "J", "Informação e Comunicação",
  "K",  "Atividades Financeiras",   "L", "Atividades Imobiliárias",
  "M",  "Profissionais e Técnicas", "N", "Atividades Administrativas",
  "O",  "Adm. Pública",      "P", "Educação",
  "Q",  "Saúde",             "R", "Artes e Cultura",
  "S",  "Outras Atividades", "T", "Serviços Domésticos",
  "U",  "Organismos Internacionais"
)

rotatividade <- tbl(con, "caged_mov") |>
  filter(!is.na(secao)) |>
  group_by(secao, competenciamov) |>
  summarise(
    admissoes     = sum(saldomovimentacao ==  1, na.rm = TRUE),
    desligamentos = sum(saldomovimentacao == -1, na.rm = TRUE)
  ) |>
  collect() |>
  left_join(cnae_secoes, by = "secao") |>
  mutate(descricao = coalesce(descricao, paste("Setor", secao))) |>
  group_by(descricao) |>
  summarise(
    total_admissoes     = sum(admissoes),
    total_desligamentos = sum(desligamentos),
    rotatividade_pct    = total_desligamentos / (total_admissoes + total_desligamentos)
  ) |>
  arrange(desc(rotatividade_pct))
ggplot(
  rotatividade |> filter(!is.na(descricao)),
  aes(x = total_admissoes, y = total_desligamentos,
      size = rotatividade_pct, color = rotatividade_pct,
      label = descricao)
) +
  geom_point(alpha = 0.7) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey50") +
  ggrepel::geom_text_repel(size = 3, max.overlaps = 12) +
  scale_x_continuous(labels = label_number(scale = 1e-6, suffix = "M")) +
  scale_y_continuous(labels = label_number(scale = 1e-6, suffix = "M")) +
  scale_size_continuous(range = c(2, 10), guide = "none") +
  scale_color_gradient(low = "#2196F3", high = "#F44336", labels = percent) +
  labs(
    title    = "Rotatividade por Setor Econômico — 2023",
    subtitle = "Pontos acima da diagonal = mais desligamentos que admissões",
    x        = "Admissões",
    y        = "Desligamentos",
    color    = "Rotatividade",
    caption  = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11)

Caso 8 — Emprego formal por porte de estabelecimento

Análise de qual porte de empresa concentra mais geração de empregos.

porte_dic <- tibble::tribble(
  ~tamestabjan, ~porte,
  0L,  "Não informado",
  1L,  "Até 4 vínculos",
  2L,  "5 a 9",
  3L,  "10 a 19",
  4L,  "20 a 49",
  5L,  "50 a 99",
  6L,  "100 a 249",
  7L,  "250 a 499",
  8L,  "500 a 999",
  9L,  "1000 ou mais"
)

porte <- tbl(con, "caged_mov") |>
  filter(!is.na(tamestabjan)) |>
  group_by(tamestabjan) |>
  summarise(
    saldo       = sum(saldomovimentacao, na.rm = TRUE),
    admissoes   = sum(saldomovimentacao ==  1, na.rm = TRUE),
    desligamentos = sum(saldomovimentacao == -1, na.rm = TRUE)
  ) |>
  collect() |>
  left_join(porte_dic, by = "tamestabjan") |>
  mutate(porte = factor(porte, levels = porte_dic$porte))
ggplot(porte |> filter(!is.na(porte)), aes(x = porte)) +
  geom_col(aes(y = admissoes),       fill = "#1565C0", alpha = 0.8) +
  geom_col(aes(y = -desligamentos),  fill = "#B71C1C", alpha = 0.8) +
  geom_point(aes(y = saldo), color = "#212121", size = 3) +
  geom_hline(yintercept = 0, color = "grey30") +
  scale_y_continuous(labels = label_number(scale = 1e-6, suffix = "M")) +
  labs(
    title    = "Movimentação por Porte de Estabelecimento — 2023",
    subtitle = "Azul = admissões | Vermelho = desligamentos | Ponto = saldo",
    x        = "Faixa de tamanho (vínculos em jan/ano)",
    y        = "Movimentações",
    caption  = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11) +
  theme(axis.text.x = element_text(angle = 40, hjust = 1))

Caso 9 — Emprego formal nas regiões brasileiras

Comparação do mercado de trabalho formal entre as 5 regiões do Brasil.

data("uf_codigos")

regiao_mensal <- tbl(con, "caged_mov") |>
  group_by(uf, competenciamov) |>
  summarise(saldo = sum(saldomovimentacao, na.rm = TRUE)) |>
  collect() |>
  left_join(uf_codigos, by = c("uf" = "codigo")) |>
  filter(!is.na(regiao)) |>
  group_by(regiao, competenciamov) |>
  summarise(saldo = sum(saldo), .groups = "drop") |>
  mutate(data = ym(as.character(competenciamov)))
ggplot(regiao_mensal, aes(x = data, y = saldo, color = regiao, fill = regiao)) +
  geom_line(linewidth = 1) +
  geom_area(alpha = 0.1) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
  scale_y_continuous(labels = label_number(scale = 1e-3, suffix = "k")) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b/%y") +
  scale_color_brewer(palette = "Set1") +
  scale_fill_brewer(palette  = "Set1") +
  facet_wrap(~regiao, scales = "free_y", ncol = 2) +
  labs(
    title   = "Saldo de Empregos Formais por Região — 2023",
    x       = NULL,
    y       = "Saldo (mil vínculos)",
    color   = NULL,
    fill    = NULL,
    caption = "Fonte: MTE/CAGED via datacaged"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    legend.position  = "none",
    axis.text.x      = element_text(angle = 45, hjust = 1),
    strip.text       = element_text(face = "bold")
  )

Caso 10 — Painel executivo: resumo do mercado de trabalho

Conjunto de indicadores-chave em um painel consolidado.

# KPIs gerais do ano
kpis <- tbl(con, "caged_mov") |>
  summarise(
    admissoes     = sum(saldomovimentacao ==  1, na.rm = TRUE),
    desligamentos = sum(saldomovimentacao == -1, na.rm = TRUE),
    saldo         = sum(saldomovimentacao,       na.rm = TRUE),
    salario_medio = round(mean(salario[salario > 0], na.rm = TRUE), 2),
    pct_mulheres  = round(mean(sexo == 3, na.rm = TRUE) * 100, 1),
    pct_superior  = round(mean(graudeinstrucao >= 7, na.rm = TRUE) * 100, 1)
  ) |>
  collect()

cat(glue::glue("
=== PAINEL MERCADO DE TRABALHO FORMAL 2023 ===

Admissões:       {scales::number(kpis$admissoes,     big.mark = '.')}
Desligamentos:   {scales::number(kpis$desligamentos, big.mark = '.')}
Saldo:           {scales::number(kpis$saldo,         big.mark = '.')}
Salário médio:   R$ {scales::number(kpis$salario_medio, big.mark = '.', decimal.mark = ',')}
Mulheres (%):    {kpis$pct_mulheres}%
Com nível sup.:  {kpis$pct_superior}%
"))
# Exportar qualquer resultado para Parquet via caged_to_parquet()
caged_to_parquet(
  "caged.duckdb",
  output_dir = "~/exports_caged"
)

# Ou particionado por UF para análises regionais
caged_to_parquet(
  "caged.duckdb",
  output_dir   = "~/exports_caged",
  tables       = "caged_mov",
  partition_by = "uf"
)
DBI::dbDisconnect(con, shutdown = TRUE)

Nota sobre os dados: os dados do CAGED estão sujeitos a revisões pelo MTE. Para análises que exigem precisão histórica, considere incorporar também os ajustes via caged_adjustments_load().