Buracos nos dados
Tabelas de preços reais têm buracos: uma ação não negociou naquele dia, um feed perdeu uma cotação, um mercado estava de feriado. O pandas os marca como NaN ("not a number"). Antes de escolher o que fazer, olhe: df.isna() dá uma tabela de Verdadeiro/Falso, .sum() conta os buracos por coluna e .mean() dá a proporção. Uma coluna 40% vazia é um problema diferente de uma que perdeu um único valor.
import pandas as pd
nan = float("nan")
df = pd.DataFrame(
{"AAPL": [184.0, nan, 186.5, 187.0, nan], "MSFT": [410.0, 411.0, nan, nan, 415.0]},
index=pd.bdate_range("2026-03-02", periods=5),
)
print(df.isna().sum().to_dict()) # {'AAPL': 2, 'MSFT': 2} holes per column
print(df.isna().mean().round(2).to_dict()) # {'AAPL': 0.4, 'MSFT': 0.4} share of holesLevar adiante ou descartar
Para preços, o preenchimento natural é o último preço conhecido: até chegar uma nova cotação, o mercado continua no valor antigo. Isso é ffill() (preenchimento para a frente). Uma lacuna longa provavelmente significa dados velhos, então ffill(limit=2) preenche no máximo dois dias seguidos. Os NaN logo no começo continuam NaN, porque não há preço anterior para levar adiante. A alternativa é descartar: dropna() remove toda linha com buraco, e dropna(subset=["AAPL"]) olha só as colunas que importam. Descartar é seguro, mas reduz os dados e muda o espaçamento entre as datas.
msft = df["MSFT"]
print(msft.ffill().tolist()) # [410.0, 411.0, 411.0, 411.0, 415.0]
print(msft.ffill(limit=1).tolist()) # [410.0, 411.0, 411.0, nan, 415.0] only one day carried
print(len(df.dropna())) # 1 only one row is complete
print(len(df.dropna(subset=["AAPL"]))) # 3 rows where AAPL is knownInterpolação e o que nunca preencher
interpolate() preenche uma lacuna com uma reta entre os valores conhecidos dos dois lados. Serve para grandezas suaves (uma curva de juros, um nível de volatilidade) e raramente para preços, que dão saltos. Cuidado nas bordas: por padrão interpolate() também copia o último valor nos NaN finais, o que inventa dados. limit_area="inside" o restringe às lacunas entre dois valores conhecidos. Nunca preencha às cegas com 0: um preço 0 é um colapso, um retorno 0 é um dia parado, e fillna(0) na tabela errada muda em silêncio todas as médias calculadas depois.
gappy = pd.Series([nan, nan, 10.0, nan, nan, 16.0, nan])
print(gappy.interpolate().tolist())
# [nan, nan, 10.0, 12.0, 14.0, 16.0, 16.0] the last 16.0 is invented
print(gappy.interpolate(limit_area="inside").tolist())
# [nan, nan, 10.0, 12.0, 14.0, 16.0, nan] only between known values# Filling with 0 is a decision, not a default: it changes the statistics
print(gappy.fillna(0).tolist()) # [0.0, 0.0, 10.0, 0.0, 0.0, 16.0, 0.0]
print(gappy.mean()) # 13.0 NaN are skipped
print(gappy.fillna(0).mean()) # 3.7142857142857144 the zeros drag the average downbfill() e interpolate() usam valores posteriores à lacuna, então na data da lacuna empregam informação que ainda não existia. Em um backtest isso é viés de antecipação. Use ffill() para tudo o que uma estratégia vê em tempo real e reserve a interpolação para limpar dados históricos analisados como um todo.df.isna().sum()conta os buracos por coluna; olhe os dados antes de decidir como tratá-los.ffill(limit=n)leva adiante o último valor conhecido (a escolha certa para preços);dropna(subset=...)remove as linhas incompletas.interpolate(limit_area="inside")cobre só as lacunas internas; nunca preencha com 0 nem com o futuro por hábito.
A seguir: a detecção de valores atípicos, achar os valores presentes que não podem estar certos.
Lab: o que é avaliado
- Contar os buracos de cada coluna com
count_missing25 pts - Levar o último preço adiante com
forward_fill25 pts - Manter apenas as linhas confiáveis com
complete_rows25 pts - Preencher as lacunas dentro dos dados com
fill_gaps25 pts