#2 - Analisando e Limpando Dados com Pandas

Como inspecionar rapidamente um DataFrame e tratar os problemas mais comuns de dados sujos - células vazias, formatos errados, valores incorretos e duplicatas.

Imagem de capa

No post anterior você viu como criar e carregar um DataFrame. Só que, na prática, dificilmente os dados que chegam até você já estão prontos pra análise. Células vazias, datas em formatos diferentes dentro da mesma coluna, valores absurdos e linhas duplicadas são a regra, não a exceção — e é aí que entra a limpeza de dados.

Neste post você vai aprender primeiro a dar uma “olhada rápida” num DataFrame recém-carregado, e depois a resolver os quatro problemas de dados sujos mais comuns: células vazias, formato errado, dados errados e duplicatas.

Inspecionando um DataFrame rapidamente

Antes de sair limpando qualquer coisa, o primeiro passo é entender o que você tem em mãos. Vamos usar como exemplo um DataFrame com o registro de treinos de uma pessoa ao longo de alguns dias:

import pandas as pd

dados = {
    "duracao": [30, 45, 60, 45, None, 60, 450],
    "pulso": [110, 117, 103, 109, 117, 102, 104],
    "calorias": [280.0, 340.0, 380.5, None, 320.0, 400.0, 300.0]
}

df = pd.DataFrame(dados)

head() e tail()

.head() mostra as primeiras linhas do DataFrame (5 por padrão, mas você pode passar outro número):

print(df.head(3))

A saída é:

   duracao  pulso  calorias
0     30.0    110     280.0
1     45.0    117     340.0
2     60.0    103     380.5

.tail() faz o mesmo, só que a partir do final:

print(df.tail(2))
   duracao  pulso  calorias
5     60.0    102     400.0
6    450.0    104     300.0

Já dá pra notar algo estranho: uma duração de 450 no meio de valores que giram em torno de 30-60. Vamos voltar nisso mais adiante.

info()

.info() não mostra os dados em si, mostra metadados sobre a estrutura da tabela — quantidade de linhas, colunas, tipo de cada coluna e quantos valores não-nulos existem em cada uma:

print(df.info())

A saída é:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7 entries, 0 to 6
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype
---  ------    --------------  -----
 0   duracao   6 non-null      float64
 1   pulso     7 non-null      int64
 2   calorias  6 non-null      float64
dtypes: float64(2), int64(1)

Repare que duracao e calorias têm só 6 valores não-nulos, contra 7 linhas no total — ou seja, cada uma tem uma célula vazia. É exatamente esse tipo de sinal que o .info() ajuda a enxergar rápido, sem precisar vasculhar a tabela inteira na mão.

Nota: rodar .head(), .tail() e .info() logo depois de carregar qualquer dataset novo é um hábito que economiza muito tempo de debug mais adiante.

Tratando células vazias

Células vazias (representadas como NaN) atrapalham cálculos estatísticos e podem até quebrar algumas operações. O Pandas dá duas saídas principais: remover as linhas com valor vazio, ou preenchê-las com algum valor.

Removendo linhas com dropna()

df_limpo = df.dropna()
print(df_limpo)

Por padrão, .dropna() devolve um novo DataFrame, sem alterar o original. Se quiser modificar o próprio df, use inplace=True:

df.dropna(inplace=True)

Preenchendo valores vazios com fillna()

Às vezes remover a linha inteira é desperdício — melhor preencher só a célula vazia. Dá pra aplicar em todo o DataFrame ou numa coluna específica:

df["calorias"].fillna(300, inplace=True)

# ou preenchendo por coluna, num único comando
df.fillna({"calorias": 300}, inplace=True)

Preenchendo com média, mediana ou moda

Chutar um número fixo (como o 300 acima) raramente é a melhor ideia. É mais comum calcular um valor a partir da própria coluna:

import pandas as pd

dados = {
    "duracao": [30, 45, 60, 45, None, 60, 450],
    "pulso": [110, 117, 103, 109, 117, 102, 104],
    "calorias": [280.0, 340.0, 380.5, None, 320.0, 400.0, 300.0]
}
df = pd.DataFrame(dados)

media = df["calorias"].mean()
df.fillna({"calorias": media}, inplace=True)

A média costuma ser um bom padrão pra colunas numéricas contínuas; a moda é mais indicada pra colunas categóricas.

Corrigindo o formato dos dados

Outro problema comum é uma coluna que deveria ter um tipo consistente (datas, por exemplo) mas guarda valores em formatos diferentes — "2026/03/10" numa linha, "10-03-2026" em outra.

import pandas as pd

dados = {
    "data": ["2026/03/01", "2026/03/02", "10-03-2026", None],
    "duracao": [30, 45, 60, 45]
}
df = pd.DataFrame(dados)

df["data"] = pd.to_datetime(df["data"], format="mixed")
print(df)

O parâmetro format="mixed" diz pro pd.to_datetime() tentar reconhecer formatos diferentes dentro da mesma coluna, em vez de exigir um padrão único. Valores que não conseguem ser convertidos (como um None) viram NaT (“Not a Time” — o equivalente a NaN, só que pra datas).

Depois de converter, dá pra remover as linhas que ficaram com data inválida:

df.dropna(subset=["data"], inplace=True)

O parâmetro subset restringe a busca por valores vazios só à coluna data, em vez de olhar o DataFrame inteiro.

Corrigindo dados errados

Diferente de uma célula vazia, um dado errado é um valor presente, só que implausível — lembra daquela duração de 450 minutos lá em cima, cercada de valores entre 30 e 60?

Corrigindo um valor pontualmente

Se você sabe exatamente qual linha e qual deveria ser o valor correto, o .loc resolve direto:

df.loc[6, "duracao"] = 45

Corrigindo em lote com uma regra

Pra datasets maiores, não dá pra corrigir linha por linha na mão. Uma abordagem comum é definir um limite e substituir tudo que ultrapassa ele:

for indice in df.index:
    if df.loc[indice, "duracao"] > 120:
        df.loc[indice, "duracao"] = 120

Removendo a linha em vez de corrigir

Se não dá pra saber qual seria o valor certo, às vezes a saída mais simples é descartar a linha inteira:

for indice in df.index:
    if df.loc[indice, "duracao"] > 120:
        df.drop(indice, inplace=True)

Vale lembrar que remover dados sempre tem um custo — você está jogando fora informação que pode fazer falta depois. Use com critério.

Removendo linhas duplicadas

Por fim, o problema mais direto de resolver: linhas repetidas inteiras, que costumam entrar no dataset por erro de coleta ou de junção de fontes diferentes.

Encontrando duplicatas com duplicated()

print(df.duplicated())

O retorno é uma Series de booleanos, uma posição por linha: True quando aquela linha é idêntica a alguma anterior, False caso contrário.

Removendo com drop_duplicates()

df.drop_duplicates(inplace=True)

Assim como nos outros métodos de limpeza, inplace=True aplica a remoção direto no df, sem precisar reatribuir o resultado a uma nova variável.

Com o DataFrame inspecionado e limpo, ele já está em condições de ir pra próxima etapa: entender como as colunas se relacionam entre si — que é justamente o assunto do próximo post da série.

Fonte adaptada: Pandas - Analyzing DataFrames, Pandas - Cleaning Data, Pandas - Cleaning Empty Cells, Pandas - Cleaning Data of Wrong Format, Pandas - Fixing Wrong Data, Pandas - Removing Duplicates