This vignette presents ready-to-use labour market analyses using
datacaged. Each use case is independent — you can run just
the section of interest.
Required data (run once):
# 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")Analysis of the monthly flow of entry and exit in the formal labour market.
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 = "Formal Employment Flow — Novo CAGED 2023",
subtitle = "Blue bars = hires | red bars = dismissals | line = balance",
x = NULL,
y = "Movements",
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Identifies which sectors created and destroyed the most formal jobs in the period.
# CNAE 2.0 section dictionary
cnae_secoes <- tibble::tribble(
~secao, ~descricao,
"A", "Agriculture",
"B", "Extractive Industries",
"C", "Manufacturing",
"D", "Electricity and Gas",
"E", "Water and Sanitation",
"F", "Construction",
"G", "Trade",
"H", "Transport",
"I", "Accommodation and Food",
"J", "Information and Communication",
"K", "Financial Activities",
"L", "Real Estate",
"M", "Professional and Technical",
"N", "Administrative Activities",
"O", "Public Administration",
"P", "Education",
"Q", "Health",
"R", "Arts and Culture",
"S", "Other Activities",
"T", "Domestic Services",
"U", "International Organisations"
)
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 = "Employment Balance by Economic Sector — 2023",
x = "Balance (thousand jobs)",
y = NULL,
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 11) +
theme(panel.grid.major.y = element_blank())Visualisation of which state and month had the strongest formal employment.
# State dictionary (IBGE code -> abbreviation)
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 = "Employment Balance by State and Month — 2023",
x = NULL,
y = NULL,
fill = "Saldo",
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 10) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "right"
)Demographic profile of new formal employment by gender, education and race/colour.
# Category dictionaries
grau_instrucao <- tibble::tribble(
~graudeinstrucao, ~escolaridade,
1L, "Illiterate",
2L, "Incomplete primary",
3L, "Complete primary",
4L, "Incomplete secondary",
5L, "Complete secondary",
6L, "Incomplete higher education",
7L, "Complete higher education",
8L, "Master's degree",
9L, "Doctorate"
)
raca_dic <- tibble::tribble(
~racacor, ~raca,
1L, "Indigenous",
2L, "White",
4L, "Black",
6L, "Yellow",
8L, "Mixed",
9L, "Not declared"
)
# Hires by education level and gender
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, "Male", "Female"),
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("Male" = "#1565C0", "Female" = "#AD1457")) +
labs(
title = "Hires by Education Level and Gender — 2023",
x = NULL,
y = "Hires (thousand)",
fill = NULL,
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 11) +
theme(
axis.text.x = element_text(angle = 40, hjust = 1),
legend.position = "top"
)# Hires by race/colour
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 = "Distribution of Hires by Race/Colour — 2023",
x = NULL,
y = "Admissões",
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 11)Analysis of wages declared for new hires.
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, "Male", "Female"),
log_salario = log10(salario)
)
# Wage percentiles
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("Male" = "#1565C0", "Female" = "#AD1457")) +
scale_color_manual(values = c("Male" = "#0D47A1", "Female" = "#880E4F")) +
labs(
title = "Wage Distribution of New Hires by Gender — 2023",
subtitle = "Dashed lines indicate the median wage for each group",
x = "Starting wage (BRL)",
y = "Density",
fill = NULL,
color = NULL,
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 11) +
theme(legend.position = "top")Construction of a long historical series joining legacy CAGED (up to 2019) and Novo CAGED (from 2020 onwards).
# Legacy CAGED: competencia + saldomovimentacao
antigo <- tbl(con, "caged_antigo") |>
filter(!is.na(competencia)) |>
group_by(competencia) |>
summarise(saldo = sum(saldomovimentacao, na.rm = TRUE)) |>
collect() |>
mutate(serie = "Legacy CAGED")
# Adjustments: added to legacy to correct the series
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 = "Legacy CAGED (adjusted)"
) |>
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")
# Combine series
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 = "COVID-19\nPandemic", 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(
"Legacy CAGED (adjusted)" = "#795548",
"Novo CAGED" = "#1565C0"
)) +
labs(
title = "Formal Employment Balance — Historical Series 2015-2023",
subtitle = "Legacy CAGED corrected by Adjustments + Novo CAGED",
x = NULL,
y = "Monthly balance",
color = NULL,
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "top")Turnover rate (dismissals / estimated stock) by economic sector.
# cnae_secoes also defined here so Case 7 can be run independently
cnae_secoes <- tibble::tribble(
~secao, ~descricao,
"A", "Agriculture", "B", "Extractive Industries",
"C", "Manufacturing", "D", "Electricity and Gas",
"E", "Water and Sanitation", "F", "Construction",
"G", "Trade", "H", "Transport",
"I", "Accommodation and Food", "J", "Information and Communication",
"K", "Financial Activities", "L", "Real Estate",
"M", "Professional and Technical", "N", "Administrative Activities",
"O", "Public Administration", "P", "Education",
"Q", "Health", "R", "Arts and Culture",
"S", "Other Activities", "T", "Domestic Services",
"U", "International Organisations"
)
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 = "Turnover by Economic Sector — 2023",
subtitle = "Points above the diagonal = more dismissals than hires",
x = "Admissões",
y = "Desligamentos",
color = "Rotatividade",
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 11)Analysis of which company size concentrates the most job creation.
porte_dic <- tibble::tribble(
~tamestabjan, ~porte,
0L, "Not declared",
1L, "Up to 4 jobs",
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 or more"
)
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 = "Movement by Establishment Size — 2023",
subtitle = "Azul = admissões | Vermelho = desligamentos | Ponto = saldo",
x = "Size range (jobs in Jan/year)",
y = "Movements",
caption = "Source: MTE/CAGED via datacaged"
) +
theme_minimal(base_size = 11) +
theme(axis.text.x = element_text(angle = 40, hjust = 1))Comparison of the formal labour market across Brazil’s 5 regions.
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 = "Formal Employment Balance by Region — 2023",
x = NULL,
y = "Balance (thousand jobs)",
color = NULL,
fill = NULL,
caption = "Source: 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")
)Set of key indicators in a consolidated dashboard.
# Annual KPIs
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("
=== FORMAL LABOUR MARKET DASHBOARD 2023 ===
Hires: {scales::number(kpis$admissoes, big.mark = '.')}
Dismissals: {scales::number(kpis$desligamentos, big.mark = '.')}
Balance: {scales::number(kpis$saldo, big.mark = '.')}
Average wage: R$ {scales::number(kpis$salario_medio, big.mark = '.', decimal.mark = ',')}
Women (%): {kpis$pct_mulheres}%
Higher education: {kpis$pct_superior}%
"))# Export any result to Parquet via caged_to_parquet()
caged_to_parquet(
"caged.duckdb",
output_dir = "~/exports_caged"
)
# Or partitioned by state for regional analyses
caged_to_parquet(
"caged.duckdb",
output_dir = "~/exports_caged",
tables = "caged_mov",
partition_by = "uf"
)Data note: CAGED data is subject to revisions by the MTE. For analyses requiring historical accuracy, consider also incorporating adjustments via
caged_adjustments_load().