Machine learning do zero · Aula 4.2 · 21 de 33 aulas prontas · 12 min
Dados faltantes
Em resumo
Remova as linhas com lacunas e ficam 333 de 344 pinguins. Se o IMC falta nos mais pesados, a média nas lacunas enfraquece a relação com a doença.
Aquecimento Duas perguntas de aulas anteriores. Um minuto, e a aula nunca fica bloqueada.
Na lição 4.1 olhamos para os dados antes de construir um modelo. Hoje, o que os dados não contêm. Um dado faltante é uma célula vazia na tabela: alguém não mediu, não anotou ou não quis dizer. A maioria dos modelos não aceita célula vazia, então é preciso fazer algo com ela. Há duas saídas simples: remover a linha ou escrever algum valor na lacuna, o que se chama imputar valores faltantes.
Escolha uma resposta e o seu grau de certeza.
Responda para liberar o experimento.
Experimento interativo
Este experimento interativo funciona em um navegador com JavaScript ativado. O texto ao redor descreve o que ele mostra.
Ver a resposta
Resposta certa: 333, porque os 2 pinguins sem medidas estão entre os 11 aos quais falta o sexo
Ficam 333. As células vazias das medidas estão nas mesmas 2 linhas, e a esses pinguins também falta o sexo. Então você remove 11 linhas, não 19 nem 13. Antes de contar as perdas, veja em que linhas estão as lacunas.
Experimente
- Comece pelos pinguins. A quantos falta o sexo e a quantos as medidas? São as mesmas linhas?
- O modelo deve prever a massa a partir das outras três medidas. Ele precisa do sexo? Quantos pinguins ficam se você remover só as linhas sem as colunas do modelo?
- Passe para o diabetes. Aqui o IMC falta nos pacientes mais pesados. Preencha a lacuna com a média: a relação entre o IMC e a progressão da doença enfraquece ou fica mais forte?
- Acrescente a coluna “falta” e compare o R² na validação cruzada. Depois troque para pacientes ao acaso. O que muda?
Depois de conferir no lab, vamos dar nome ao que você vê.
A ideia em palavras simples
Remover uma linha com lacuna é simples, mas custa. Nos pinguins, as linhas com alguma lacuna são 11 de 344, então ficam 333. Um modelo que prevê a massa a partir das medidas do bico e da nadadeira nunca lê o sexo. Se você remover só as linhas às quais faltam colunas do modelo, ficam 342. Remova linhas por causa das colunas que você realmente usa.
A pergunta mais importante é de quem os dados faltam. No conjunto de diabetes, cada um dos 442 pacientes tem IMC, então o lab o esconde de propósito em 110 deles. Uma vez ao acaso, outra nos 110 mais pesados, ou seja, em todos com IMC de pelo menos 29,4. Uma lacuna assim não é aleatória. Os pacientes sem IMC têm progressão média da doença de 209, e os outros de 133. O simples fato de o IMC faltar diz algo sobre o paciente.
Quando você escreve na lacuna a média dos outros pacientes (24,3), as 110 pessoas com mais progressão caem num IMC médio. A correlação do IMC com a progressão da doença cai de 0,586 para 0,352: a relação enfraquece. A reta do modelo não fica mais plana, porque quem a define são os pacientes com IMC; o que piora é o ajuste, porque os 110 pontos preenchidos ficam longe da reta. O R² de uma regressão linear na validação cruzada cai de 0,328 para 0,106. Uma coluna extra 0/1 “faltava o IMC” ajuda. O modelo dá então à própria lacuna um peso de 76 pontos de progressão, e o R² volta a 0,284. Com lacunas aleatórias essa coluna não acrescenta nada: 0,264 contra 0,267 só com a média.
Jak to policzono
- Pinguins: Palmer Penguins (Gorman, Williams e Fraser, 2014; pacote palmerpenguins: Horst, Hill e Gorman, 2020), licença CC0. 344 linhas, lacunas reais: o sexo em 11 (Adelie 6, Gentoo 5), as quatro medidas nos mesmos 2 pinguins.
- Diabetes: Diabetes (Efron, Hastie, Johnstone e Tibshirani, 2004), distribuído com o scikit-learn sob a licença BSD-3-Clause. 442 pacientes, progressão da doença após um ano. O IMC está nos dados para todos; as lacunas de 110 pacientes fomos nós que fizemos: ao acaso ou nos 110 com o IMC mais alto.
- Modelo: regressão linear da progressão da doença sobre o IMC (com a coluna de lacuna, sobre o IMC e essa coluna), validação cruzada em 5 partes repetida 10 vezes. A média para preencher é calculada com os pacientes que têm IMC; na validação cruzada, em cada divisão, só com a parte de treino, como
SimpleImputerem um pipeline.
Por que funciona
A média preenchida fica no meio do eixo do IMC, mas pertence a pacientes da sua ponta direita. A progressão deles é alta, então o preenchimento cria uma pilha vertical de pontos altos sobre um IMC médio. Essa pilha não acrescenta nada à variação conjunta do IMC e da progressão. Em comparação com os pacientes que têm IMC (correlação 0,459), ela só acrescenta variação à progressão, então a correlação cai para 0,352. Em comparação com os dados completos (0,586), esses 110 pacientes perdem sua parte da variação conjunta: tinham IMC alto e progressão alta, e agora ficam no meio do eixo. O desvio-padrão do IMC encolhe de 4,4 para 2,3, porque 110 valores ficam num só ponto.
Remover esses pacientes também não resolve. Fica uma nuvem sem a ponta direita, e a correlação nos 332 restantes é 0,459 em vez de 0,586. Além disso, para 110 pacientes o modelo não dá resposta nenhuma. A coluna de lacuna funciona porque dá ao modelo um peso próprio para o grupo cujo IMC não conhecemos. Esse grupo difere na progressão da doença, então o peso leva essa diferença para as previsões. Quando a lacuna é aleatória, o grupo sem IMC não difere do resto (156 contra 151), e o peso não tem o que levar.
Um erro comum
Verdadeiro ou falso: “preencha as lacunas com a média e pronto”. Falso. A média encolhe a dispersão da coluna e finge que a lacuna nunca existiu. Quando a lacuna depende do próprio valor, como aqui nos mais pesados, a média distorce a relação: a correlação cai de 0,586 para 0,352 e o R² de 0,328 para 0,106. A coluna “falta” devolve o R² a 0,284. No código abaixo, ela vence a média sozinha em 48 de 50 partes de validação.
A média e a remoção de linhas servem quando as lacunas são poucas e aleatórias. Mas antes verifique se as linhas com lacuna diferem do resto.
Quiz
Três perguntas para você se testar. Escolha uma resposta ou abra a resposta embaixo de cada pergunta. No teclado: as setas escolhem, Enter ou Espaço confirma.
Pontos-chave
- Remova linhas por causa das colunas que você usa: ficam 342 pinguins em vez de 333.
- Antes de fazer qualquer coisa com as lacunas, verifique se as linhas com lacuna diferem do resto.
- A média nas lacunas encolhe a dispersão e, com lacunas não aleatórias, enfraquece a relação: uma correlação de 0,586 cai para 0,352.
- Uma coluna 0/1 “falta” deixa o modelo usar a informação que a própria lacuna carrega: R² 0,284 em vez de 0,106.
Mais a fundo: fórmula, código, prática
A matemática
Preencher com a média: x̃ᵢ = x̄_obs para os pacientes com lacuna, x̃ᵢ = xᵢ para o resto. Os pontos preenchidos têm x̃ᵢ − x̄ = 0, então não acrescentam nada à covariância Σ (x̃ᵢ − x̄)(yᵢ − ȳ) nem à variância Σ (x̃ᵢ − x̄)², mas acrescentam (yᵢ − ȳ)² à variância de y. Em comparação só com os pacientes que têm IMC, a soma Σ (x̃ᵢ − x̄)(yᵢ − ȳ) não muda enquanto a variância de y cresce, então a correlação r = cov(x̃, y) / (s_x̃ · s_y) cai (0,459 → 0,352). Em comparação com os dados completos, a variância de y é a mesma; cai a covariância, porque os pontos preenchidos já não trazem seus termos grandes e positivos (0,586 → 0,352).
A coluna de lacuna: ŷ = a + b · x̃ + c · m, onde m = 1 quando faltava o IMC e m = 0 caso contrário. Para um paciente com lacuna o modelo prevê a + b · x̄_obs + c, então c desloca todo o grupo sem IMC. Aqui c = 76 com as lacunas nos mais pesados.
No código (Python)
O mesmo experimento em pandas e scikit-learn: as lacunas da tabela dos pinguins, depois o IMC escondido em 110 pacientes ao acaso e nos 110 mais pesados, a média nas lacunas e a média com uma coluna de lacuna na validação cruzada 10 × 5.
import numpy as np
import pandas as pd
from sklearn.datasets import load_diabetes
from sklearn.model_selection import cross_val_score, RepeatedKFold
from sklearn.linear_model import LinearRegression
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
df = pd.read_csv("data/raw/penguins.csv")
gaps = df.isna().sum()
print(f"{len(df)} penguins; missing: " + ", ".join(f"{c} {n}" for c, n in gaps[gaps > 0].items()))
measured = df.dropna(subset=["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]) # remove só as linhas sem as colunas do modelo
print(f"dropna() leaves {len(df.dropna())}; dropping only rows without measurements leaves {len(measured)}")
X, y = load_diabetes(return_X_y=True, as_frame=True, scaled=False)
bmi, y = X.bmi.to_numpy(), y.to_numpy()
heavy = bmi >= np.sort(bmi)[-110] # os 110 mais pesados: IMC de pelo menos 29,4
rnd = np.zeros(len(bmi), bool)
rnd[np.random.default_rng(0).choice(len(bmi), 110, replace=False)] = True # 110 ao acaso
cv = RepeatedKFold(n_splits=5, n_repeats=10, random_state=0)
r2 = lambda model, Z: cross_val_score(model, Z, y, cv=cv, scoring="r2")
print(f"{len(bmi)} patients, BMI vs progression: r {np.corrcoef(bmi, y)[0, 1]:.3f}, CV R2 {r2(LinearRegression(), bmi[:, None]).mean():.3f}")
for name, miss in [("random", rnd), ("heaviest", heavy)]:
hidden = np.where(miss, np.nan, bmi)[:, None]
filled = np.where(miss, bmi[~miss].mean(), bmi) # a média só dos pacientes com IMC
mean_r2 = r2(make_pipeline(SimpleImputer(), LinearRegression()), hidden) # a média é aprendida em cada divisão, só com a parte de treino dela
flag_r2 = r2(make_pipeline(SimpleImputer(add_indicator=True), LinearRegression()), hidden) # segunda coluna: 1 = faltava o IMC
print(f"BMI hidden for 110 {name}: progression {y[miss].mean():.0f} vs {y[~miss].mean():.0f}; mean fill: r {np.corrcoef(filled, y)[0, 1]:.3f}, CV R2 {mean_r2.mean():.3f}; mean + flag: CV R2 {flag_r2.mean():.3f}")
print(f"heaviest: mean + flag beats the mean alone in {(flag_r2 > mean_r2).sum()} of 50 folds")
Saída
344 penguins; missing: bill_length_mm 2, bill_depth_mm 2, flipper_length_mm 2, body_mass_g 2, sex 11
dropna() leaves 333; dropping only rows without measurements leaves 342
442 patients, BMI vs progression: r 0.586, CV R2 0.332
BMI hidden for 110 random: progression 159 vs 150; mean fill: r 0.503, CV R2 0.241; mean + flag: CV R2 0.239
BMI hidden for 110 heaviest: progression 209 vs 133; mean fill: r 0.352, CV R2 0.112; mean + flag: CV R2 0.289
heaviest: mean + flag beats the mean alone in 48 of 50 folds
Os pacientes sorteados e as divisões são diferentes dos do lab, então os números também diferem um pouco. A imagem é a mesma: ficam 333 ou 342 pinguins, a média nos mais pesados derruba a correlação para 0,352, e a coluna de lacuna sobe o R² de 0,112 para 0,289. Com lacunas aleatórias a coluna não ajuda: 0,239 contra 0,241.
Baixar o notebook (.ipynb). Abra no Jupyter ou envie para o Google Colab; ele baixa sozinho os dados que faltarem.
Para quem pratica
- No scikit-learn:
SimpleImputer(strategy="mean")ou"median", e comadd_indicator=Truevocê ganha a coluna de lacuna de uma vez. Coloque-o emmake_pipeline, para que a média seja calculada só na parte de treino de cada divisão. df.isna().sum()edf[df.x.isna()].describe()mostram quantas lacunas há e se as linhas com lacuna diferem do resto. É a primeira coisa a verificar antes de escolher um método.IterativeImputereKNNImputerpreveem o valor que falta a partir de outras colunas. Ajudam quando as lacunas dependem de outras colunas, mas não recuperam uma lacuna que depende do próprio valor escondido; aí a coluna de lacuna importa mais.- As árvores do
HistGradientBoostingClassifierlidam sozinhas com as lacunas: em cada divisão aprendem para que lado mandar as linhas com lacuna.
Isto foi útil?
Obrigado! Isso nos ajuda a melhorar.
Vá mais fundo no Atlas
Verbetes da enciclopédia sobre as ideias desta aula: