Kurs uczenia maszynowego od zera · Lekcja 4.2 · 21 z 33 gotowych lekcji · 12 min
Braki danych
W skrócie
Usuń wiersze z brakiem, a z 344 pingwinów zostaje 333. Gdy BMI brakuje u najcięższych, średnia w lukach osłabia związek z chorobą, a sam brak coś mówi.
Rozgrzewka Dwa pytania z wcześniejszych lekcji. Minuta, a lekcji nie blokuje.
W lekcji 4.1 patrzyliśmy na dane, zanim powstał model. Dziś to, czego w danych nie ma. Brak danych to pusta komórka w tabeli: ktoś nie zmierzył, nie zapisał albo nie chciał podać. Większość modeli nie przyjmie pustej komórki, więc trzeba coś z nią zrobić. Są dwa proste wyjścia: usunąć wiersz albo wpisać w lukę jakąś wartość, co nazywa się uzupełnianiem braków.
Wybierz odpowiedź i pewność.
Odpowiedz, żeby odblokować lab.
Eksperyment
Ten interaktywny eksperyment działa w przeglądarce z włączonym JavaScriptem. Tekst wokół opisuje, co w nim widać.
Pokaż odpowiedź
Poprawna odpowiedź: 333, bo 2 pingwiny bez pomiarów są wśród tych 11, którym brakuje też płci
Zostaje 333. Puste komórki pomiarów leżą w tych samych 2 wierszach, a tym pingwinom brakuje też płci. Usuwasz więc 11 wierszy, nie 19 i nie 13. Zanim policzysz straty, sprawdź, w których wierszach leżą braki.
Spróbuj
- Zacznij od pingwinów. Ilu pingwinom brakuje płci, a ilu pomiarów? Czy to te same wiersze?
- Model ma przewidywać masę z trzech pozostałych pomiarów. Czy płeć jest mu potrzebna? Ile pingwinów zostanie, jeśli usuniesz tylko wiersze bez kolumn modelu?
- Przejdź do cukrzycy. BMI brakuje tu u najcięższych pacjentów. Wpisz w lukę średnią: czy związek BMI z postępem choroby osłabnie, czy się wzmocni?
- Dodaj kolumnę „brak” i porównaj R² w walidacji krzyżowej. Potem przełącz na losowych pacjentów. Co się zmienia?
Gdy już sprawdzisz to w labie, nazwijmy, co widzisz.
Pomysł w prostych słowach
Usunięcie wiersza z brakiem jest proste, ale kosztuje. W pingwinach wiersze z jakimkolwiek brakiem to 11 z 344, więc zostaje 333. Model przewidujący masę z pomiarów dzioba i płetwy nie czyta płci. Jeśli usuniesz tylko wiersze, którym brakuje kolumn modelu, zostaje 342. Usuwaj z powodu kolumn, których naprawdę używasz.
Ważniejsze jest pytanie, komu brakuje danych. W zbiorze cukrzycy BMI ma każdy z 442 pacjentów, więc lab ukrywa je celowo u 110 z nich. Raz losowo, raz u 110 najcięższych, czyli u wszystkich z BMI co najmniej 29,4. Taki brak nie jest przypadkowy. Pacjenci bez BMI mają średni postęp choroby 209, a pozostali 133. Sam fakt, że BMI brakuje, mówi więc coś o pacjencie.
Gdy w lukę wpiszesz średnią z pozostałych pacjentów (24,3), 110 osób z największym postępem ląduje przy przeciętnym BMI. Korelacja BMI z postępem choroby spada z 0,586 do 0,352: związek słabnie. Prosta modelu wcale nie robi się płaska, bo wyznaczają ją pacjenci z BMI; słabnie dopasowanie, bo 110 wpisanych punktów leży daleko od prostej. R² regresji liniowej w walidacji krzyżowej spada z 0,328 do 0,106. Pomaga dodatkowa kolumna 0/1 „BMI brakowało”. Model dostaje wtedy wagę 76 punktów postępu za sam brak, a R² wraca do 0,284. Przy brakach losowych ta kolumna nic nie daje: 0,264 wobec 0,267 przy samej średniej.
Jak to policzono
- Pingwiny: Palmer Penguins (Gorman, Williams i Fraser, 2014; pakiet palmerpenguins: Horst, Hill i Gorman, 2020), licencja CC0. 344 wiersze, braki prawdziwe: płeć u 11 (Adelie 6, Gentoo 5), cztery pomiary u 2 tych samych pingwinów.
- Cukrzyca: Diabetes (Efron, Hastie, Johnstone i Tibshirani, 2004), w scikit-learn na licencji BSD-3-Clause. 442 pacjentów, postęp choroby po roku. BMI jest w danych u wszystkich, a braki u 110 pacjentów zrobiliśmy sami: losowo albo u 110 o największym BMI.
- Model: regresja liniowa postępu choroby na BMI (przy kolumnie braku na BMI i tej kolumnie), walidacja krzyżowa na 5 części powtórzona 10 razy. Średnia do wpisania liczona z pacjentów z BMI; w walidacji krzyżowej w każdym podziale tylko z części treningowej, jak
SimpleImputerw pipeline'ie.
Dlaczego to działa
Wpisana średnia leży w środku osi BMI, a należy do pacjentów z jej prawego końca. Ich postęp choroby jest wysoki, więc po wpisaniu powstaje pionowy słupek wysokich punktów nad przeciętnym BMI. Taki słupek nie dodaje nic do wspólnej zmienności BMI i postępu. W porównaniu z samymi pacjentami z BMI (korelacja 0,459) dokłada tylko zmienności postępowi, więc korelacja spada do 0,352. W porównaniu z pełnymi danymi (0,586) tych 110 pacjentów traci swój wkład do wspólnej zmienności: mieli wysokie BMI i wysoki postęp, a teraz stoją w środku osi. Odchylenie standardowe BMI maleje z 4,4 do 2,3, bo 110 wartości siedzi w jednym punkcie.
Usunięcie tych pacjentów też nie ratuje sprawy. Zostaje chmura bez prawego końca, a korelacja na pozostałych 332 to 0,459 zamiast 0,586. Do tego dla 110 pacjentów model nie da żadnej odpowiedzi. Kolumna braku działa, bo daje modelowi osobną wagę dla grupy, której BMI nie znamy. Ta grupa różni się postępem choroby, więc waga przenosi tę różnicę do przewidywań. Gdy brak jest losowy, grupa bez BMI nie różni się od reszty (156 wobec 151) i waga nie ma czego przenieść.
Częsty błąd
Prawda czy fałsz: „braki wypełnij średnią i po sprawie”. Fałsz. Średnia zaniża rozrzut kolumny i udaje, że braku nie było. Gdy brak zależy od samej wartości, jak tu u najcięższych, średnia wykrzywia związek: korelacja spada z 0,586 do 0,352, a R² z 0,328 do 0,106. Kolumna „brak” przywraca R² do 0,284. W kodzie niżej wygrywa z samą średnią w 48 z 50 części walidacyjnych.
Średnia i usuwanie wierszy są w porządku, gdy braki są nieliczne i losowe. Najpierw jednak sprawdź, czy wiersze z brakiem różnią się od reszty.
Quiz
Trzy pytania na sprawdzenie się. Wybierz odpowiedź albo rozwiń odpowiedź pod pytaniem. Z klawiatury: strzałki wybierają, Enter albo spacja zatwierdza.
Najważniejsze wnioski
- Usuwaj wiersze z powodu kolumn, których używasz: zostają 342 pingwiny zamiast 333.
- Zanim coś zrobisz z brakami, sprawdź, czy wiersze z brakiem różnią się od reszty.
- Średnia w lukach zaniża rozrzut, a przy brakach nielosowych osłabia związek: korelacja 0,586 spada do 0,352.
- Kolumna 0/1 „brak” pozwala modelowi użyć informacji, którą niesie sam brak: R² 0,284 zamiast 0,106.
Głębiej: wzór, kod, praktyka
Matematyka
Wpisanie średniej: x̃ᵢ = x̄_obs dla pacjentów z brakiem, x̃ᵢ = xᵢ dla reszty. Wpisane punkty mają x̃ᵢ − x̄ = 0, więc nie dodają nic do kowariancji Σ (x̃ᵢ − x̄)(yᵢ − ȳ) ani do wariancji Σ (x̃ᵢ − x̄)², ale dodają (yᵢ − ȳ)² do wariancji y. W porównaniu z samymi pacjentami z BMI suma Σ (x̃ᵢ − x̄)(yᵢ − ȳ) się nie zmienia, a wariancja y rośnie, więc korelacja r = cov(x̃, y) / (s_x̃ · s_y) maleje (0,459 → 0,352). W porównaniu z pełnymi danymi wariancja y jest ta sama; maleje kowariancja, bo wpisane punkty nie wnoszą już swoich dużych, dodatnich składników (0,586 → 0,352).
Kolumna braku: ŷ = a + b · x̃ + c · m, gdzie m = 1, gdy BMI brakowało, i m = 0 w przeciwnym razie. Dla pacjenta z brakiem model przewiduje a + b · x̄_obs + c, więc c przesuwa całą grupę bez BMI. Tu c = 76 przy brakach u najcięższych.
W kodzie (Python)
Ten sam eksperyment w pandas i scikit-learn: braki w tabeli pingwinów, potem BMI ukryte u 110 pacjentów losowo i u 110 najcięższych, średnia w lukach i średnia z kolumną braku w walidacji krzyżowej 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"]) # usuń tylko wiersze bez kolumn modelu
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] # 110 najcięższych: BMI co najmniej 29,4
rnd = np.zeros(len(bmi), bool)
rnd[np.random.default_rng(0).choice(len(bmi), 110, replace=False)] = True # 110 losowych
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) # średnia liczona tylko z pacjentów z BMI
mean_r2 = r2(make_pipeline(SimpleImputer(), LinearRegression()), hidden) # średnia liczona w każdym podziale tylko z jego części treningowej
flag_r2 = r2(make_pipeline(SimpleImputer(add_indicator=True), LinearRegression()), hidden) # druga kolumna: 1 = BMI brakowało
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")
Wynik
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
Losowanie pacjentów i podziały są inne niż w labie, więc liczby też są trochę inne. Obraz jest ten sam: pingwinów zostaje 333 albo 342, średnia u najcięższych obniża korelację do 0,352, a kolumna braku podnosi R² z 0,112 do 0,289. Przy brakach losowych ta kolumna nie pomaga: 0,239 wobec 0,241.
Pobierz notebook (.ipynb). Otwórz go w Jupyterze albo wgraj do Google Colab; brakujące dane pobierze sam.
Dla praktyka
- W scikit-learn:
SimpleImputer(strategy="mean")albo"median", a zadd_indicator=Truedostajesz od razu kolumnę braku. Wkładaj go domake_pipeline, żeby średnia liczyła się tylko na części treningowej każdego podziału. df.isna().sum()idf[df.x.isna()].describe()pokażą, ile jest braków i czy wiersze z brakiem różnią się od reszty. To pierwsza rzecz do sprawdzenia, zanim wybierzesz metodę.IterativeImputeriKNNImputerprzewidują brakującą wartość z innych kolumn. Pomagają przy brakach zależnych od innych kolumn, ale braku zależnego od samej ukrytej wartości nie odtworzą; wtedy kolumna braku jest ważniejsza.- Drzewa w
HistGradientBoostingClassifiersame obsługują braki: przy każdym podziale uczą się, w którą stronę wysłać wiersze z brakiem.
Czy to było pomocne?
Dzięki! To pomaga nam poprawiać Atlas.
Więcej w Atlasie
Hasła encyklopedii o pojęciach z tej lekcji: