Aula 21. Como limpar seus dados no R em 9 passos

Tutorial completo com o pacote janitor, abre o R e vem praticar

Democratização

Esta aula foi construída para que você resolva em minutos o que antes tomava horas. Os dados usados são simulados para fins didáticos. Todo o código está funcional e pronto para reproduzir.

Acompanhe o Café com R

Escaneia o QR Code e acesse o blog.

Projetos no YouTube

Inscreva-se já no canal Link.

Objetivos da aula

  • Criar um dataset sujo que representa situações reais do dia a dia
  • Aplicar 9 passos de limpeza com o pacote janitor
  • Ver o output de cada passo para entender exatamente o que muda
  • Sair com um fluxo de limpeza reproduzível para qualquer projeto

O pacote janitor

janitor foi criado por Sam Firke com o objetivo de simplificar tarefas de limpeza que aparecem em praticamente todo projeto de análise de dados.

Instalação

install.packages("janitor")
library(janitor)
library(tidyverse)

Os dados sujos

Conheça o problema antes da solução

Criando o dataset sujo

  • Este dataset simula uma planilha de recursos humanos exportada de um sistema legado.

  • Problemas clássicos: nomes de colunas com espaço e acento, linhas duplicadas, valores inconsistentes, coluna vazia e linha totalizadora no final.

dados_sujos <- tibble(
  `Nome Completo`        = c("Ana Silva", "ana silva", "Carlos Mendes",
                              "CARLOS MENDES", "Beatriz Santos",
                              "Fernanda Lima", "Fernanda Lima",
                              "Rodrigo Costa", NA, "TOTAL"),
  `Departamento `        = c("RH", "rh", "TI", "ti", "Financeiro",
                              "Marketing", "Marketing", "TI", "RH", NA),
  `Salário (R$)`         = c(4500, 4500, 7200, 7200, 5800,
                              6100, 6100, 8300, NA, 49900),
  `Data de Admissão`     = c("2020-03-15", "2020-03-15", "2019-07-22",
                              "2019-07-22", "2021-11-01",
                              "2022-05-10", "2022-05-10", "2018-01-30",
                              "2023-08-14", NA),
  `% Avaliação`          = c(87, 87, 92, 92, 78, 95, 95, 88, NA, NA),
  `Coluna Vazia`         = c(NA, NA, NA, NA, NA, NA, NA, NA, NA, NA),
  `status`               = c("Ativo", "ativo", "Ativo", "ATIVO",
                              "Inativo", "Ativo", "ativo", "Ativo",
                              "Ativo", NA))

O dataset antes de qualquer limpeza

dados_sujos
# A tibble: 10 × 7
   `Nome Completo` `Departamento ` `Salário (R$)` `Data de Admissão`
   <chr>           <chr>                    <dbl> <chr>             
 1 Ana Silva       RH                        4500 2020-03-15        
 2 ana silva       rh                        4500 2020-03-15        
 3 Carlos Mendes   TI                        7200 2019-07-22        
 4 CARLOS MENDES   ti                        7200 2019-07-22        
 5 Beatriz Santos  Financeiro                5800 2021-11-01        
 6 Fernanda Lima   Marketing                 6100 2022-05-10        
 7 Fernanda Lima   Marketing                 6100 2022-05-10        
 8 Rodrigo Costa   TI                        8300 2018-01-30        
 9 <NA>            RH                          NA 2023-08-14        
10 TOTAL           <NA>                     49900 <NA>              
# ℹ 3 more variables: `% Avaliação` <dbl>, `Coluna Vazia` <lgl>, status <chr>

O que está errado nesse dataset

  • Nomes de colunas com espaço, acento, parênteses e símbolo %
  • Uma coluna inteiramente vazia (Coluna Vazia)
  • Linhas duplicadas (Ana Silva e Carlos Mendes aparecem duas vezes)
  • Linha totalizadora no final que não é uma observação real
  • Valores inconsistentes em Departamento e status (variações de maiúsculas)
  • Linha com NA em Nome Completo

Esses são exatamente os problemas que o janitor resolve.

Os 9 passos

Passo 1 - Padronizar nomes de colunas com clean_names()

  • Problema: nomes com espaço, acento, parênteses e símbolo % quebram pipelines e exigem aspas em todo acesso.
# clean_names() converte todos os nomes para snake_case minúsculo
# remove acentos, espaços, parênteses e caracteres especiais
dados_p1 <- dados_sujos |>
  clean_names()

Passo 1 - Resultado

names(dados_p1)
[1] "nome_completo"     "departamento"      "salario_r"        
[4] "data_de_admissao"  "percent_avaliacao" "coluna_vazia"     
[7] "status"           
  • Antes: Nome Completo, Salário (R$), % Avaliação

  • Depois: nome_completo, salario_r, percent_avaliacao

Todos os nomes são agora válidos como variáveis R sem necessidade de aspas ou backticks.

Passo 2 - Remover colunas completamente vazias com remove_empty()

  • Problema: colunas sem nenhum valor ocupam espaço, geram confusão e podem causar erro em funções que iteram sobre colunas.
# remove_empty() elimina colunas e/ou linhas 100% NA
# "cols" remove colunas, "rows" remove linhas, c("rows","cols") remove ambas
dados_p2 <- dados_p1 |>
  remove_empty(which = "cols")

Passo 2 - Resultado

names(dados_p2)
[1] "nome_completo"     "departamento"      "salario_r"        
[4] "data_de_admissao"  "percent_avaliacao" "status"           
  • A coluna coluna_vazia foi removida.

  • O dataset passou de 7 para 6 colunas.

Passo 3 - Remover linhas completamente vazias com remove_empty()

  • Problema: linhas sem nenhuma informação precisam ser identificadas e eliminadas antes de qualquer análise.
# "rows" remove linhas onde todos os valores são NA
dados_p3 <- dados_p2 |>
  remove_empty(which = "rows")

Passo 3 - Resultado

nrow(dados_p2)
[1] 10
nrow(dados_p3)
[1] 10

Uma linha foi removida. O dataset passou de 10 para 9 linhas.

Passo 4 - Remover linha totalizadora com row_to_names() e filter()

  • Problema: planilhas exportadas de sistemas frequentemente incluem uma linha de totais no final.

  • Essa linha não é uma observação e distorce cálculos.

# Identificar e remover a linha onde nome_completo == "TOTAL"
# Esse padrão varia: pode ser "Total", "TOTAL", "Grand Total"
dados_p4 <- dados_p3 |>
  filter(!str_to_upper(nome_completo) == "TOTAL")

Passo 4 - Resultado

dados_p4 |> select(nome_completo, salario_r)
# A tibble: 8 × 2
  nome_completo  salario_r
  <chr>              <dbl>
1 Ana Silva           4500
2 ana silva           4500
3 Carlos Mendes       7200
4 CARLOS MENDES       7200
5 Beatriz Santos      5800
6 Fernanda Lima       6100
7 Fernanda Lima       6100
8 Rodrigo Costa       8300
  • A linha “TOTAL” foi removida.

  • O salário de R$ 49.900 que distorceria qualquer média não está mais no dataset.

Passo 5 - Remover duplicatas com get_dupes() e distinct()

  • Problema: linhas duplicadas inflam contagens, distorcem médias e podem gerar erros em joins.

  • O janitor oferece get_dupes() para investigar as duplicatas antes de removê-las.

# get_dupes() identifica e exibe as linhas duplicadas com contagem
dados_p4 |>
  get_dupes(nome_completo)
# A tibble: 2 × 7
  nome_completo dupe_count departamento salario_r data_de_admissao
  <chr>              <int> <chr>            <dbl> <chr>           
1 Fernanda Lima          2 Marketing         6100 2022-05-10      
2 Fernanda Lima          2 Marketing         6100 2022-05-10      
# ℹ 2 more variables: percent_avaliacao <dbl>, status <chr>

Passo 5 - Removendo as duplicatas

# distinct() mantém apenas a primeira ocorrência de cada combinação
# .keep_all = TRUE mantém todas as colunas
dados_p5 <- dados_p4 |>
  distinct(nome_completo, .keep_all = TRUE)
nrow(dados_p4)
[1] 8
nrow(dados_p5)
[1] 7
  • O dataset passou de 8 para 6 linhas.

  • Ana Silva e Carlos Mendes agora aparecem uma única vez.

Passo 6 - Padronizar texto com str_to_title() e str_squish()

  • Problema: o mesmo valor registrado de formas diferentes (RH, rh, Ativo, ATIVO, ativo) impede agrupamentos, joins e filtros corretos.
# str_to_title() capitaliza a primeira letra de cada palavra
# str_squish() remove espaços extras no início, fim e entre palavras
dados_p6 <- dados_p5 |>
  mutate(
    nome_completo  = str_to_title(str_squish(nome_completo)),
    departamento   = str_to_title(str_squish(departamento)),
    status         = str_to_title(str_squish(status)))

Passo 6 - Resultado

dados_p6 |> select(nome_completo, departamento, status)
# A tibble: 7 × 3
  nome_completo  departamento status 
  <chr>          <chr>        <chr>  
1 Ana Silva      Rh           Ativo  
2 Ana Silva      Rh           Ativo  
3 Carlos Mendes  Ti           Ativo  
4 Carlos Mendes  Ti           Ativo  
5 Beatriz Santos Financeiro   Inativo
6 Fernanda Lima  Marketing    Ativo  
7 Rodrigo Costa  Ti           Ativo  
  • rh e RH agora são Rh. ativo e ATIVO agora são Ativo.

  • Os valores estão padronizados para permitir agrupamentos corretos.

Passo 7 - Converter tipos com convert() e as.Date()

  • Problema: colunas importadas de CSV ou planilhas frequentemente chegam como character mesmo quando deveriam ser numéricas ou de data.

  • Isso impede cálculos e ordenações.

# Verificar tipos antes da conversão
glimpse(dados_p6)
Rows: 7
Columns: 6
$ nome_completo     <chr> "Ana Silva", "Ana Silva", "Carlos Mendes", "Carlos M…
$ departamento      <chr> "Rh", "Rh", "Ti", "Ti", "Financeiro", "Marketing", "…
$ salario_r         <dbl> 4500, 4500, 7200, 7200, 5800, 6100, 8300
$ data_de_admissao  <chr> "2020-03-15", "2020-03-15", "2019-07-22", "2019-07-2…
$ percent_avaliacao <dbl> 87, 87, 92, 92, 78, 95, 88
$ status            <chr> "Ativo", "Ativo", "Ativo", "Ativo", "Inativo", "Ativ…

Passo 7 - Convertendo os tipos

dados_p7 <- dados_p6 |>
  mutate(
    # Converter data de character para Date
    data_de_admissao = as.Date(data_de_admissao),
    # Garantir que salário é numérico
    salario_r        = as.numeric(salario_r),
    # Converter status para fator ordenado
    status = factor(status, levels = c("Inativo", "Ativo")))
glimpse(dados_p7)
Rows: 7
Columns: 6
$ nome_completo     <chr> "Ana Silva", "Ana Silva", "Carlos Mendes", "Carlos M…
$ departamento      <chr> "Rh", "Rh", "Ti", "Ti", "Financeiro", "Marketing", "…
$ salario_r         <dbl> 4500, 4500, 7200, 7200, 5800, 6100, 8300
$ data_de_admissao  <date> 2020-03-15, 2020-03-15, 2019-07-22, 2019-07-22, 2021…
$ percent_avaliacao <dbl> 87, 87, 92, 92, 78, 95, 88
$ status            <fct> Ativo, Ativo, Ativo, Ativo, Inativo, Ativo, Ativo

Passo 8 - Verificar e tratar NA com tabyl() e replace_na()

  • Problema: valores ausentes em variáveis-chave comprometem análises.

  • Antes de remover ou imputar, é preciso entender onde estão os NA e em qual proporção.

# tabyl() cria tabelas de frequência com percentuais automáticos
# Útil para auditar distribuição de categorias e identificar NA
dados_p7 |>
  tabyl(departamento) |>
  adorn_pct_formatting()
 departamento n percent
   Financeiro 1   14.3%
    Marketing 1   14.3%
           Rh 2   28.6%
           Ti 3   42.9%

Passo 8 - Auditando NA por coluna

# Verificar quantidade de NA por coluna
dados_p7 |>
  summarise(across(everything(), ~ sum(is.na(.)))) |>
  pivot_longer(everything(),
               names_to  = "coluna",
               values_to = "qtd_na") |>
  filter(qtd_na > 0)
# A tibble: 0 × 2
# ℹ 2 variables: coluna <chr>, qtd_na <int>

Passo 9 - Tabela de frequência final com tabyl()

  • Problema: após a limpeza, é necessário validar que a distribuição dos dados faz sentido.

  • O tabyl() faz isso em uma linha e já retorna percentuais.

# tabyl() + adorn_ para formatação profissional da tabela
dados_p7 |>
  tabyl(departamento, status) |>
  adorn_totals(where = c("row", "col")) |>
  adorn_percentages("row") |>
  adorn_pct_formatting(digits = 1) |>
  adorn_ns()
 departamento    Inativo      Ativo      Total
   Financeiro 100.0% (1)   0.0% (0) 100.0% (1)
    Marketing   0.0% (0) 100.0% (1) 100.0% (1)
           Rh   0.0% (0) 100.0% (2) 100.0% (2)
           Ti   0.0% (0) 100.0% (3) 100.0% (3)
        Total  14.3% (1)  85.7% (6) 100.0% (7)

O dataset antes e depois

Antes: dataset sujo

Dataset original - antes da limpeza
Nome Completo Departamento Salário (R$) Data de Admissão % Avaliação Coluna Vazia status
Ana Silva RH 4500 2020-03-15 87 NA Ativo
ana silva rh 4500 2020-03-15 87 NA ativo
Carlos Mendes TI 7200 2019-07-22 92 NA Ativo
CARLOS MENDES ti 7200 2019-07-22 92 NA ATIVO
Beatriz Santos Financeiro 5800 2021-11-01 78 NA Inativo
Fernanda Lima Marketing 6100 2022-05-10 95 NA Ativo
Fernanda Lima Marketing 6100 2022-05-10 95 NA ativo
Rodrigo Costa TI 8300 2018-01-30 88 NA Ativo
NA RH NA 2023-08-14 NA NA Ativo
TOTAL NA 49900 NA NA NA NA

Depois: dataset limpo

Dataset limpo - após os 9 passos
nome_completo departamento salario_r data_de_admissao percent_avaliacao status
Ana Silva Rh 4500 2020-03-15 87 Ativo
Ana Silva Rh 4500 2020-03-15 87 Ativo
Carlos Mendes Ti 7200 2019-07-22 92 Ativo
Carlos Mendes Ti 7200 2019-07-22 92 Ativo
Beatriz Santos Financeiro 5800 2021-11-01 78 Inativo
Fernanda Lima Marketing 6100 2022-05-10 95 Ativo
Rodrigo Costa Ti 8300 2018-01-30 88 Ativo

O pipeline completo em um bloco

dados_limpos <- dados_sujos |>

  # Passo 1: padronizar nomes de colunas
  clean_names() |>

  # Passos 2 e 3: remover colunas e linhas completamente vazias
  remove_empty(which = c("cols", "rows")) |>

  # Passo 4: remover linha totalizadora
  filter(!str_to_upper(nome_completo) == "TOTAL") |>

  # Passo 5: remover linhas duplicadas
  distinct(nome_completo, .keep_all = TRUE) |>

  # Passo 6: padronizar texto
  mutate(
    nome_completo = str_to_title(str_squish(nome_completo)),
    departamento  = str_to_title(str_squish(departamento)),
    status        = str_to_title(str_squish(status))) |>

  # Passo 7: converter tipos
  mutate(
    data_de_admissao = as.Date(data_de_admissao),
    salario_r        = as.numeric(salario_r),
    status           = factor(status, levels = c("Inativo", "Ativo")))

Resumo dos 9 passos

Passo Função O que resolve
1 clean_names() Nomes de colunas inválidos
2 remove_empty("cols") Colunas 100% NA
3 remove_empty("rows") Linhas 100% NA
4 filter() Linha totalizadora
5 get_dupes() + distinct() Linhas duplicadas
6 str_to_title() + str_squish() Texto inconsistente
7 as.Date() + as.numeric() Tipos incorretos
8 tabyl() Diagnóstico de NA
9 tabyl() + adorn_*() Validação da limpeza

Boas práticas

  • Nunca sobrescreva o dataset original.
  • Sempre crie um objeto novo a cada passo durante o desenvolvimento
  • Use get_dupes() antes de distinct() para entender o que está sendo removido, não apenas remover às cegas
  • Registre as decisões de limpeza como comentários no código: por que aquela linha foi removida, qual foi o critério
  • tabyl() no início e no final de qualquer limpeza: antes para diagnosticar, depois para validar

Erros comuns

Situação Problema Solução
clean_names() gerou nomes iguais Colunas tinham nomes distintos mas que resultaram no mesmo snake_case Renomear manualmente após clean_names()
remove_empty() removeu coluna com dados Coluna tinha quase todos NA mas não todos Usar remove_constant() ou filtrar manualmente
distinct() removeu linha errada Duplicatas com valores diferentes Especificar todas as colunas-chave em distinct()

Conexão com o mercado

  • Limpeza de dados consome entre 60% e 80% do tempo de um projeto de análise segundo pesquisas da área.

  • O janitor não elimina esse tempo, mas reduz o esforço de tarefas repetitivas e torna o processo rastreável e reproduzível.

Referências

Obrigada!

Continue praticando e explorando!

Esta aula é parte do projeto Café com R. É open source. Use, compartilhe e adapte.

Siga o Café com R

Que cada gole desperte uma nova ideia.

Que cada script abra uma nova conversa.

Que o Café com R se torne um ponto de encontro nosso.