Transformação e Limpeza de Dados

No módulo anterior, conhecemos os Pinguins de Palmer e aprendemos os princípios do Tidy Data. Agora chegou o momento de enfrentar a realidade: dados do mundo real raramente chegam limpos e prontos para análise.

Mesmo nosso dataset científico dos pinguins, coletado com rigor na Antártica, apresenta desafios reais que encontramos constantemente: valores ausentes, medidas biologicamente impossíveis, e inconsistências que podem comprometer nossas análises.

Neste módulo, você dominará o processo de transformação e limpeza usando nossos pinguins como case prático, desenvolvendo intuição para identificar e resolver problemas de qualidade que se aplicam a qualquer dataset.

Preparando o Ambiente e Dataset

Importando os Dados Brutos dos Pinguins

# Importar dados BRUTOS (com valores ausentes e problemas)
penguins_raw = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv')

# Diagnóstico inicial
glimpse(penguins_raw)
Rows: 344
Columns: 8
> species           object   0 (0%) NAs : Adelie, Adelie, Adelie, Adelie, Adelie
> island            object   0 (0%) NAs : Torgersen, Torgersen, Torgersen, Torgersen, Torgersen
> bill_length_mm    float64  2 (1%) NAs : 39.1, 39.5, 40.3, nan, 36.7
> bill_depth_mm     float64  2 (1%) NAs : 18.7, 17.4, 18.0, nan, 19.3
> flipper_length_mm float64  2 (1%) NAs : 181.0, 186.0, 195.0, nan, 193.0
> body_mass_g       float64  2 (1%) NAs : 3750.0, 3800.0, 3250.0, nan, 3450.0
> sex               object  16 (5%) NAs : male, female, female, nan, female
> year              int64    0 (0%) NAs : 2007, 2007, 2007, 2007, 2007

Nossos dados dos pinguins já são organizados (tidy), com poucos valores ausentes. Para aprender técnicas de limpeza, vamos simular problemas típicos encontrados em datasets reais:

Criando e Resolvendo Problemas Reais

Simulando Problemas Típicos

# Criar dataset com problemas típicos para demonstrar soluções
penguins_problematic = penguins_raw.copy()

# Problema 1: Pinguim gigante (erro de digitação: 20kg em vez de 2kg)
penguins_problematic.loc[0, 'body_mass_g'] = 20000  

# Problema 2: Bico impossível (alguém digitou em metros em vez de mm)
penguins_problematic.loc[1, 'bill_length_mm'] = 0.043  # 43mm virou 0.043

# Problema 3: Espécie com grafia inconsistente 
penguins_problematic.loc[2, 'species'] = 'adelie'  # minúscula
penguins_problematic.loc[3, 'species'] = 'ADELIE'  # maiúscula

# Problema 4: Ilha com abreviação
penguins_problematic.loc[4, 'island'] = 'Torg'  # Torgersen abreviado

# Problema 5: Sexo inconsistente
penguins_problematic.loc[5, 'sex'] = 'M'  # M em vez de male
penguins_problematic.loc[6, 'sex'] = 'F'  # F em vez de female

glimpse(penguins_problematic)
Rows: 344
Columns: 8
> species           object   0 (0%) NAs : Adelie, Adelie, adelie, ADELIE, Adelie
> island            object   0 (0%) NAs : Torgersen, Torgersen, Torgersen, Torgersen, Torg
> bill_length_mm    float64  2 (1%) NAs : 39.1, 0.043, 40.3, nan, 36.7
> bill_depth_mm     float64  2 (1%) NAs : 18.7, 17.4, 18.0, nan, 19.3
> flipper_length_mm float64  2 (1%) NAs : 181.0, 186.0, 195.0, nan, 193.0
> body_mass_g       float64  2 (1%) NAs : 20000.0, 3800.0, 3250.0, nan, 3450.0
> sex               object  16 (5%) NAs : male, female, female, nan, female
> year              int64    0 (0%) NAs : 2007, 2007, 2007, 2007, 2007

Detectando Valores Estranhos

# Detectar valores biologicamente impossíveis
print(f"Massa > 8kg: {(penguins_problematic['body_mass_g'] > 8000).sum()} pinguins")
print(f"Bico < 1mm: {(penguins_problematic['bill_length_mm'] < 1).sum()} pinguins")
print(f"Espécies únicas: {penguins_problematic['species'].nunique()} (esperamos 3)")

penguins_problematic['species'].value_counts()
Massa > 8kg: 1 pinguins
Bico < 1mm: 1 pinguins
Espécies únicas: 5 (esperamos 3)
species
Adelie       150
Gentoo       124
Chinstrap     68
ADELIE         1
adelie         1
Name: count, dtype: int64

Padronizando Texto

penguins_clean = penguins_problematic.copy()

# Espécies sempre com primeira letra maiúscula
penguins_clean['species'] = penguins_clean['species'].str.title()

# Sexo sempre minúsculo e expandir abreviações
penguins_clean['sex'] = penguins_clean['sex'].str.lower()
penguins_clean['sex'] = penguins_clean['sex'].replace({'m': 'male', 'f': 'female'})

# Expandir abreviações das ilhas
penguins_clean['island'] = penguins_clean['island'].replace({'Torg': 'Torgersen'})

penguins_clean['species'].value_counts()
species
Adelie       152
Gentoo       124
Chinstrap     68
Name: count, dtype: int64

Corrigindo Valores Estranhos

# Corrigir o pinguim de 20kg (provável erro de digitação)
penguins_clean.loc[penguins_clean['body_mass_g'] > 8000, 'body_mass_g'] = 2000

# Corrigir bico em metros (multiplicar por 1000)
penguins_clean.loc[penguins_clean['bill_length_mm'] < 1, 'bill_length_mm'] *= 1000

# Verificar correções
print(f"Massa > 8kg: {(penguins_clean['body_mass_g'] > 8000).sum()} pinguins")
print(f"Bico < 1mm: {(penguins_clean['bill_length_mm'] < 1).sum()} pinguins")
Massa > 8kg: 0 pinguins
Bico < 1mm: 0 pinguins

Lidando com Valores Ausentes

Removendo Linhas com Dados Ausentes

total_original = len(penguins_clean)
penguins_no_na = penguins_clean.dropna()
sem_na = len(penguins_no_na)
perda = (total_original - sem_na) / total_original * 100

print(f"Registros originais: {total_original}")
print(f"Registros sem NA: {sem_na}")  
print(f"Perda: {perda:.1f}%")
Registros originais: 344
Registros sem NA: 328
Perda: 4.7%

Preenchendo Valores Ausentes

Aviso

Cuidado com imputação! Quando você preenche valores ausentes, está alterando a distribuição real dos dados. Compare sempre resultados antes e depois.

# Exemplo: imputação por média da espécie
penguins_imputed = penguins_clean.copy()
numeric_cols = ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']

for col in numeric_cols:
    penguins_imputed[col] = penguins_imputed.groupby('species')[col].transform(
        lambda x: x.fillna(x.mean())
    )

original_nas = penguins_clean.isnull().sum().sum()
final_nas = penguins_imputed.isnull().sum().sum()
print(f"NAs antes: {original_nas}")
print(f"NAs depois: {final_nas}")
print(f"Valores imputados: {original_nas - final_nas}")
NAs antes: 24
NAs depois: 16
Valores imputados: 8

Entendendo “Outliers” vs Valores Raros

Outliers vs Valores Raros

Definição: Um outlier é um valor que não deveria existir no processo que gerou os dados - diferente de um valor apenas raro mas legítimo.

Aviso

Cuidado! Um valor anômalo só é outlier se você tem conhecimento do domínio para classificá-lo como erro.

A Metáfora do Cisne Negro

Por séculos, europeus acreditavam que todos os cisnes eram brancos - até descobrirem cisnes negros na Austrália em 1697.

Lição: O que parece “impossível” pode ser apenas nossa ignorância sobre a distribuição real dos dados.

Exemplo: Em uma empresa, 99% ganham R$ 4.000-8.000, mas o diretor ganha R$ 30.000: - Visto como outlier: se tratamos como uma distribuição única - Visto como normal: se reconhecemos duas distribuições (operacional vs executiva)

Remover “cisnes negros” torna modelos mais limpos, mas menos capazes de modelar a realidade completa.

Como Distinguir?

Outliers verdadeiros (erros):

  • Pinguim de 200kg (erro de digitação)
  • Bico de -5mm (sensor defeituoso)
  • Espécie “Adelie38” (erro tipográfico)

Cisnes negros (valores raros mas reais):

  • Pinguim Gentoo de 6,5kg (grande, mas biologicamente possível)
  • Adelie em ilha inusual (migração rara)
  • Bico no extremo superior da distribuição (variação natural)

Critérios: Conhecimento do domínio + verificação da fonte + contexto

Detecção Prática

# Simular outliers verdadeiros 
penguins_with_outliers = penguins_no_na.copy()
penguins_with_outliers.loc[0, 'body_mass_g'] = 20000  # Erro de digitação
penguins_with_outliers.loc[1, 'bill_length_mm'] = -5   # Sensor defeituoso
penguins_with_outliers.loc[2, 'species'] = 'Adelie38'  # Erro tipográfico

# Detectar valores impossíveis
print(f"Massa > 10kg: {(penguins_with_outliers['body_mass_g'] > 10000).sum()} casos")
print(f"Bico negativo: {(penguins_with_outliers['bill_length_mm'] < 0).sum()} casos")
especies_com_numeros = penguins_with_outliers['species'].str.contains(r'\d', na=False).sum()
print(f"Espécie com números: {especies_com_numeros} casos")
Massa > 10kg: 1 casos
Bico negativo: 1 casos
Espécie com números: 1 casos
# Detectar valores raros mas legítimos (cisnes negros)
penguins_final = penguins_no_na.copy()
muito_pesados = penguins_final[penguins_final['body_mass_g'] > penguins_final['body_mass_g'].quantile(0.95)]

print(f"Pinguins muito pesados (top 5%): {len(muito_pesados)}")
for _, p in muito_pesados.head(3).iterrows():
    print(f"  {p['species']}: {p['body_mass_g']:.0f}g (biologicamente possível)")

print(f"\nDiferença crucial:")
print(f"6.000g = Cisne negro (raro mas real)")
print(f"20.000g = Outlier (erro de digitação)")
Pinguins muito pesados (top 5%): 17
  Gentoo: 5700g (biologicamente possível)
  Gentoo: 5700g (biologicamente possível)
  Gentoo: 5850g (biologicamente possível)

Diferença crucial:
6.000g = Cisne negro (raro mas real)
20.000g = Outlier (erro de digitação)

Lição: Técnicas estatísticas não distinguem erros de valores raros - é necessário conhecimento do domínio.

Dataset Final Limpo

penguins = penguins_final.copy()

print(f"{len(penguins)} pinguins, {penguins.shape[1]} variáveis")
print(f"{penguins.isnull().sum().sum()} valores ausentes")
print(f"{penguins['species'].nunique()} espécies")

glimpse(penguins)
328 pinguins, 8 variáveis
0 valores ausentes
3 espécies
Rows: 328
Columns: 8
> species           object  0 (0%) NAs : Adelie, Adelie, Adelie, Adelie, Adelie
> island            object  0 (0%) NAs : Torgersen, Torgersen, Torgersen, Torgersen, Torgersen
> bill_length_mm    float64 0 (0%) NAs : 39.1, 43.0, 40.3, 36.7, 39.3
> bill_depth_mm     float64 0 (0%) NAs : 18.7, 17.4, 18.0, 19.3, 20.6
> flipper_length_mm float64 0 (0%) NAs : 181.0, 186.0, 195.0, 193.0, 190.0
> body_mass_g       float64 0 (0%) NAs : 2000.0, 3800.0, 3250.0, 3450.0, 3650.0
> sex               object  0 (0%) NAs : male, female, female, female, male
> year              int64   0 (0%) NAs : 2007, 2007, 2007, 2007, 2007

Conclusão

A transformação e limpeza de dados é onde a ciência de dados encontra a ciência do domínio. Com nossos pinguins, você aprendeu que limpeza eficaz exige conhecimento biológico - saber que ranges são plausíveis, como interpretar valores ausentes, e quando outliers representam variação natural legítima.

Recursos Adicionais