Materiały kursów
Materiały / Machine Learning
Pobierz notatnik (.ipynb)

Ćwiczenie 4: Regresja - przewidywanie wartości liczbowej

Po co to ćwiczenie?

Dotąd model odpowiadał na pytanie „która klasa?" - chory czy zdrowy. Ale ogromna część realnych zadań wygląda inaczej: ile będzie kosztowało to mieszkanie, ile energii zużyje budynek jutro, o ile wzrośnie stężenie markera po podaniu leku. Odpowiedzią nie jest wtedy etykieta, tylko liczba.

Takie zadanie nazywa się regresją (ang. regression). Zmiana wygląda na drobną - przecież to nadal „dane wchodzą, przewidywanie wychodzi" - ale pociąga za sobą trzy konsekwencje:

  1. Inne metryki. Nie da się powiedzieć „model trafił" albo „nie trafił", bo przewidywanie 148,3 przy prawdzie 151,0 nie jest ani trafieniem, ani pomyłką. Trzeba mierzyć jak bardzo model się myli.
  2. Inne modele. Zamiast LogisticRegression i DecisionTreeClassifier sięgamy po LinearRegression, Ridge, Lasso, DecisionTreeRegressor.
  3. Inne pułapki. Pojedyncza błędna wartość w danych potrafi przesunąć cały model - i zobaczymy to na własne oczy.

Przy okazji regresja liniowa daje coś, czego klasyfikatory zwykle nie dają tak wprost: współczynnik przy każdej cesze, czyli liczbę, którą da się przeczytać jak zdanie.

Ćwiczenie ma dwie części i to celowe. Przykład prowadzony opiera się na zbiorze, w którym regresja działa: model wyjaśnia sporą część zmienności celu, więc metryki, wykres reszt i regularyzacja mają co pokazać. Ostatnie zadanie robi rzecz odwrotną - wraca do znanego z ćwiczeń 01-03 pliku dane/diabetes.csv i pokazuje, jak wygląda sytuacja, w której w danych po prostu nie ma odpowiedzi na zadane pytanie. Obie sytuacje trzeba umieć rozpoznać, a druga zdarza się w prawdziwych projektach częściej, niż sugerują podręczniki.

Czego się nauczysz

  1. Czym regresja różni się od klasyfikacji i jak rozpoznać, które zadanie masz przed sobą.
  2. Jak zbudować model odniesienia dla regresji (DummyRegressor) - tak jak w ćwiczeniu 01 zaczynamy od poprzeczki.
  3. Co dokładnie mierzą MAE, MSE, RMSE i R² oraz kiedy się rozjeżdżają.
  4. Dlaczego MSE jest wrażliwe na wartości odstające (ang. outliers) - na eksperymencie, nie na wykładzie.
  5. Jak czytać współczynniki regresji liniowej i czego nie wolno z nich wyczytać.
  6. Jak wygląda wykres reszt (ang. residuals) i co z niego wynika.
  7. Czym różni się niedouczenie (ang. underfitting) od przeuczenia (ang. overfitting) - na wielomianach o rosnącym stopniu.
  8. Po co jest regularyzacja (ang. regularization) i dlaczego Lasso zeruje współczynniki, a Ridge nie.
  9. Jak rozpoznać, że problem leży nie w modelu, tylko w danych - i co wtedy zrobić.

Zanim zaczniesz: uruchamiaj komórki po kolei (Shift+Enter). Późniejsze korzystają ze zmiennych zdefiniowanych wcześniej.

1. Regresja a klasyfikacja - co się właściwie zmienia

Różnica sprowadza się do jednego: jakiego typu jest etykieta (ang. label). Wszystko inne wynika z tego automatycznie.

Klasyfikacja Regresja
Etykieta kategoria (0/1, „kot"/„pies") liczba rzeczywista (151,0; 128 500 zł)
Przykładowe pytanie czy pacjent choruje? jak bardzo choroba postąpi przez rok?
Model odniesienia najczęstsza klasa (DummyClassifier) średnia etykiety (DummyRegressor)
Metryki skuteczność, precyzja, czułość, F1 MAE, MSE, RMSE, R²
Modele liniowe LogisticRegression LinearRegression, Ridge, Lasso
Modele drzewiaste DecisionTreeClassifier DecisionTreeRegressor
Co zwraca predict numer klasy liczbę
Co zwraca score skuteczność (im wyżej, tym lepiej, maksimum 1) R² (im wyżej, tym lepiej, maksimum 1, bez dolnej granicy)

Zwróć uwagę na ostatni wiersz: score znaczy co innego w zależności od tego, jakiego typu jest model. To częste źródło nieporozumień - liczba 0,45 zwrócona przez regresor nie jest „45% skuteczności", tylko współczynnikiem R², o którym za chwilę.

Zbiór danych do tego ćwiczenia

Plik dane/diabetes.csv, na którym pracowały ćwiczenia 01-03, ma etykietę binarną (Diabetic), więc do regresji się nie nadaje. Potrzebujemy celu ciągłego, sięgamy więc po zbiór wbudowany w scikit-learn: load_diabetes. Nic nie trzeba pobierać - dane są częścią biblioteki.

  • 442 pacjentów, 10 cech, żadnych braków danych.
  • Cel: liczbowa miara postępu choroby rok po badaniu. Wartości mieszczą się mniej więcej między 25 a 350; twórcy zbioru nie podają jednostki fizycznej tej miary - im większa liczba, tym gorszy stan pacjenta po roku.
  • Cechy: age (wiek w latach), sex (płeć zakodowana jako 1 i 2 - dokumentacja nie mówi, która wartość oznacza którą płeć), bmi, bp (średnie ciśnienie krwi) oraz s1-s6, czyli sześć wyników badań krwi: cholesterol całkowity, frakcja LDL, frakcja HDL, stosunek cholesterolu całkowitego do HDL, stężenie trójglicerydów i poziom cukru.

Uwaga na zbieżność nazw - przeczytaj to, zanim się pomylisz: load_diabetes ze scikit-learn to inny zbiór niż nasz plik dane/diabetes.csv. Oba dotyczą cukrzycy, ale odpowiadają na różne pytania i pochodzą z różnych badań.

| | dane/diabetes.csv (ćwiczenia 01-03) | load_diabetes (to ćwiczenie) | |---|---|---| | Zadanie | klasyfikacja | regresja | | Etykieta | Diabetic: chory / zdrowy (0 lub 1) | liczbowa miara postępu choroby po roku | | Liczba wierszy | 10 000 | 442 | | Skąd pochodzi | plik CSV w katalogu dane/ | wbudowany w scikit-learn |

Mylenie tych dwóch zbiorów jest najczęstszym nieporozumieniem w tym ćwiczeniu. Do pliku CSV wrócimy dopiero w zadaniu 8 - i to w zupełnie innej roli.

Dlaczego scaled=False: domyślnie load_diabetes oddaje cechy już wystandaryzowane (wyśrodkowane i przeskalowane), co jest wygodne dla modelu, ale zabija to, na czym nam tu zależy: możliwość przeczytania współczynnika jak zdania po polsku. Argument scaled=False zwraca wartości w oryginalnych jednostkach - lata, mm Hg, wyniki badań. Jeśli w internetowym przykładzie zobaczysz cechy o wartościach rzędu 0,05, to właśnie dlatego, że ktoś zostawił ustawienie domyślne.

In [ ]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_diabetes

# scaled=False -> cechy w oryginalnych jednostkach (lata, mm Hg, wyniki badan krwi).
# as_frame=True -> dostajemy DataFrame zamiast tablicy numpy, wiec cechy maja nazwy.
zbior = load_diabetes(scaled=False, as_frame=True)

dane = zbior.frame          # cechy i cel w jednej ramce
X = zbior.data              # 10 cech
y = zbior.target            # etykieta ciagla: miara postepu choroby po roku

CEL = 'postęp choroby'

print("Kształt danych:", dane.shape)
print("Cechy podawane modelowi:", list(X.columns))
print()
print(f"Cel: {CEL}")
print(y.describe().round(2).to_string())

Popatrz na podsumowanie celu: wartości rozciągają się od około 25 do około 346, a odchylenie standardowe wynosi około 77 jednostek. Zapamiętaj rząd wielkości tego odchylenia - to naturalna zmienność celu i za chwilę posłuży nam za punkt odniesienia dla błędów modelu. Błąd rzędu 77 oznaczałby, że model nie wie nic ponad to, co wiadomo bez niego.

Zwróć też uwagę, że wśród cech jest kolumna bmi. W tym zbiorze BMI jest jedną z informacji wejściowych, a nie odpowiedzią - to dobry moment, żeby przypomnieć sobie, że o tym, co jest cechą, a co etykietą, decyduje zadane pytanie, a nie sama kolumna.

2. Podział danych i model odniesienia

Podział wygląda jak w ćwiczeniu 01, z jedną różnicą: nie ma argumentu stratify. Stratyfikacja pilnuje proporcji klas, a przy celu ciągłym klas nie ma - nie ma czego zachowywać.

Potem, zanim sięgniemy po jakikolwiek prawdziwy model, stawiamy poprzeczkę: DummyRegressor(strategy="mean"). Ten model ignoruje wszystkie cechy i dla każdego pacjenta odpowiada tą samą liczbą - średnim postępem choroby w zbiorze uczącym.

Dlaczego znowu zaczynamy od atrapy: bo liczba „średni błąd 42,8" sama w sobie nic nie znaczy. Znaczenie dostaje dopiero wtedy, gdy wiadomo, ile wynosi błąd modelu, który nie robi nic. Regresja ma tu dodatkową pułapkę: model odniesienia bywa zaskakująco trudny do pobicia, jeśli cechy niosą mało informacji - zobaczymy to w ostatnim zadaniu.

In [ ]:
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyRegressor

X_ucz, X_test, y_ucz, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,      # ustalone ziarno losowosci - powtarzalny podzial
)

print(f"Zbiór uczący:  {len(X_ucz):4d} pacjentów")
print(f"Zbiór testowy: {len(X_test):4d} pacjentów")
print()

odniesienie = DummyRegressor(strategy="mean")
odniesienie.fit(X_ucz, y_ucz)

print(f"Średni postęp choroby w zbiorze uczącym: {y_ucz.mean():.3f}")
print("Co model odniesienia przewiduje dla pierwszych pięciu pacjentów testowych:")
print(np.round(odniesienie.predict(X_test[:5]), 3))

Ten sam wynik dla każdego pacjenta - dokładnie o to chodziło. Teraz trzeba go zmierzyć.

3. Cztery metryki regresji

Dla każdego pacjenta model popełnia błąd: błąd = wartość prawdziwa - wartość przewidziana. Metryka to sposób na streszczenie kilkuset takich błędów jedną liczbą - a sposobów jest kilka i każdy odpowiada na inne pytanie.

Metryka Pełna nazwa Jak liczy Jednostka Kierunek
MAE mean absolute error, średni błąd bezwzględny średnia z wartości bezwzględnych błędów ta sama co cel (tu: jednostki miary postępu choroby) im mniej, tym lepiej
MSE mean squared error, błąd średniokwadratowy średnia z kwadratów błędów kwadrat jednostki celu im mniej, tym lepiej
RMSE root mean squared error pierwiastek z MSE ta sama co cel im mniej, tym lepiej
R² współczynnik determinacji (ang. coefficient of determination) 1 - MSE modelu / MSE średniej brak (liczba niemianowana) im więcej, tym lepiej; maksimum 1

Trzy rzeczy, które warto rozumieć od razu:

MAE kontra RMSE. Obie liczby wyrażają się w jednostkach celu, więc kuszą, żeby czytać je tak samo. A nie są tym samym: MAE traktuje wszystkie błędy proporcjonalnie (pomyłka o 10 jest dziesięć razy gorsza od pomyłki o 1), RMSE karze duże błędy nieproporcjonalnie mocniej (pomyłka o 10 waży sto razy tyle, co pomyłka o 1, bo liczy się kwadrat). Stąd praktyczna reguła: RMSE jest zawsze większe lub równe MAE, a im bardziej się rozjeżdżają, tym bardziej nierówny jest rozkład błędów.

Po co MSE, skoro RMSE jest czytelniejsze. MSE jest tym, co model faktycznie minimalizuje podczas uczenia (metoda najmniejszych kwadratów), i jest wygodne matematycznie - w przeciwieństwie do wartości bezwzględnej ma pochodną wszędzie. Do raportowania wyników i tak lepiej nadaje się RMSE, bo „błąd 53,9 jednostki" da się porównać z odchyleniem standardowym celu, a „2900 jednostek do kwadratu" - nie.

R² to porównanie z modelem odniesienia, wbudowane w metrykę. Mówi, jaką część zmienności celu model wyjaśnia ponad to, co daje samo przewidywanie średniej:

  • R² = 1 - przewidywania idealne,
  • R² = 0 - model jest dokładnie tak dobry, jak przewidywanie średniej, czyli bezwartościowy,
  • R² < 0 - model jest gorszy od średniej. Tak, to możliwe i wcale nie rzadkie na zbiorze testowym.
In [ ]:
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

def ocen(nazwa, model, X_oceny=None, y_oceny=None):
    # Zwraca slownik z czterema metrykami. Domyslnie ocenia na zbiorze testowym
    # przykladu prowadzonego - przy innych danych trzeba je podac jawnie.
    X_oceny = X_test if X_oceny is None else X_oceny
    y_oceny = y_test if y_oceny is None else y_oceny
    przewidywania = model.predict(X_oceny)
    mse = mean_squared_error(y_oceny, przewidywania)
    return {
        'model': nazwa,
        'MAE': mean_absolute_error(y_oceny, przewidywania),
        'MSE': mse,
        'RMSE': np.sqrt(mse),          # RMSE liczymy recznie - dziala w kazdej wersji scikit-learn
        'R2': r2_score(y_oceny, przewidywania),
    }

wyniki = [ocen('model odniesienia (średnia)', odniesienie)]
print(pd.DataFrame(wyniki).to_string(index=False, float_format=lambda v: f"{v:10.3f}"))
print()
print(f"Dla porównania - odchylenie standardowe celu w zbiorze testowym: {y_test.std():.3f}")

Porównaj RMSE modelu odniesienia z odchyleniem standardowym celu wypisanym poniżej tabeli. Są niemal identyczne - i nie jest to przypadek. Przewidywanie średniej to dokładnie to, co robi odchylenie standardowe: mierzy typowe odchylenie obserwacji od średniej. RMSE modelu odniesienia ≈ odchylenie standardowe celu to tożsamość, którą warto znać, bo pozwala oszacować poprzeczkę bez trenowania czegokolwiek.

Zwróć też uwagę, że R² modelu odniesienia na zbiorze testowym wyszło bliskie zeru, ale niekoniecznie dokładnie zeru. Powód: średnia policzona została na zbiorze uczącym, a R² mierzymy względem średniej zbioru testowego. Te dwie średnie minimalnie się różnią, więc wychodzi liczba nieco poniżej zera.

4. Regresja liniowa

LinearRegression szuka najlepszej kombinacji liniowej cech:

przewidywany postęp choroby = wyraz_wolny
                            + w_1 · age
                            + w_2 · sex
                            + ...
                            + w_10 · s6

„Najlepsza" znaczy: taka, przy której suma kwadratów błędów na zbiorze uczącym jest najmniejsza (stąd nazwa: metoda najmniejszych kwadratów, ang. ordinary least squares). Wyraz wolny (ang. intercept) to wartość, którą model przewiduje, gdy wszystkie cechy są zerowe - tutaj liczba bez sensu fizycznego, bo pacjent o wieku 0 i ciśnieniu 0 nie istnieje.

Dlaczego tym razem nie skalujemy cech: zwykła regresja liniowa nie potrzebuje skalowania - rozwiązanie jest dokładnie to samo, zmieniają się tylko jednostki współczynników. A nam zależy właśnie na tym, żeby współczynniki były w oryginalnych jednostkach, bo dzięki temu da się je przeczytać po polsku. Sytuacja zmieni się w sekcji 8: regularyzacja bez skalowania nie ma sensu i tam skaler wróci.

In [ ]:
from sklearn.linear_model import LinearRegression

regresja = LinearRegression()
regresja.fit(X_ucz, y_ucz)

wyniki.append(ocen('regresja liniowa', regresja))

print(pd.DataFrame(wyniki).to_string(index=False, float_format=lambda v: f"{v:10.3f}"))
print()
poprawa = wyniki[1]['RMSE'] - wyniki[0]['RMSE']
print(f"Zmiana RMSE względem modelu odniesienia: {poprawa:+.3f} jednostki celu")
print(f"Model wyjaśnia {wyniki[1]['R2']:.1%} zmienności celu.")

Tym razem tabela wygląda tak, jak podręcznik obiecuje: regresja liniowa wyraźnie bije model odniesienia. RMSE spada o blisko dwadzieścia jednostek, MAE o ponad dwadzieścia, a R² wychodzi w okolicach 0,45 - model wyjaśnia blisko połowę zmienności celu, korzystając wyłącznie z wieku, BMI, ciśnienia i sześciu wyników badań krwi.

Zachowaj jednak proporcje. R² rzędu 0,45 to nie jest model gotowy do zastosowań klinicznych: typowa pomyłka wynosi wciąż kilkadziesiąt jednostek przy zmienności celu rzędu 77. Taki wynik mówi raczej „w tych badaniach naprawdę jest sygnał i da się go wyciągnąć prostym modelem" niż „problem rozwiązany". Do pytania, co wolno powiedzieć o modelu o niskim R², wrócimy w ostatnim zadaniu.

Co mówią współczynniki

Teraz najciekawsza rzecz w regresji liniowej: każdą cechę da się wycenić jedną liczbą.

In [ ]:
wspolczynniki = pd.Series(regresja.coef_, index=X.columns)

print(f"Wyraz wolny (intercept): {regresja.intercept_:.4f}")
print()
print("Współczynniki (w oryginalnych jednostkach cech):")
print(wspolczynniki.round(4).to_string())
print()
print("Odczyt trzech wybranych współczynników:")
print(f"  age: wzrost wieku o 1 rok            -> przewidywany cel zmienia się o {wspolczynniki['age']:+.4f}")
print(f"  bmi: wzrost BMI o 1 jednostkę        -> przewidywany cel zmienia się o {wspolczynniki['bmi']:+.4f}")
print(f"  sex: przejście z kodu 1 na kod 2     -> przewidywany cel zmienia się o {wspolczynniki['sex']:+.4f}")

Współczynnik przy bmi czyta się tak: jeżeli BMI wzrośnie o jedną jednostkę, a wszystkie pozostałe cechy pozostaną bez zmian, przewidywany postęp choroby zmieni się o tyle jednostek. Zwrot „przy pozostałych cechach bez zmian" (po łacinie ceteris paribus) jest częścią definicji, a nie ozdobnikiem - bez niego zdanie jest po prostu nieprawdziwe.

sex przyjmuje tylko dwie wartości (1 i 2), więc „wzrost o jeden" oznacza po prostu przejście od jednej grupy do drugiej. Współczynnik mówi więc, o ile średnio różni się przewidywany cel między tymi dwiema grupami - przy pozostałych cechach takich samych. Dokumentacja zbioru nie podaje, która wartość odpowiada której płci, więc dalej niż do takiego zdania nie da się uczciwie pójść.

Trzy rzeczy, których ze współczynników wyczytać nie wolno:

Pokusa Dlaczego to błąd
„Wysokie BMI powoduje szybszy postęp choroby" Regresja mierzy współwystępowanie, nie przyczynę. Kierunek może być odwrotny, może istnieć trzeci czynnik wpływający na oba. To ta sama pułapka co przy korelacji w ćwiczeniu 02.
„s5 ma współczynnik największy co do wartości, więc jest najważniejszą cechą" Współczynniki są w jednostkach swoich cech. s5 przyjmuje wartości między 3 a 6, s1 - w setkach; ta pierwsza dostaje z natury współczynnik kilkadziesiąt razy większy. Żeby porównywać, trzeba najpierw cechy wystandaryzować - o tym jest zadanie 3.
„Ujemny współczynnik oznacza, że cecha działa ochronnie" Popatrz na s1: jego korelacja z celem jest dodatnia (około +0,21), a współczynnik w modelu wychodzi ujemny. Powód: s1 i s2 są ze sobą skorelowane na poziomie około 0,90 - to praktycznie ta sama informacja podana dwa razy. Model rozdziela ją między obie kolumny w sposób, który liczy się tylko jako para. Przy silnie skorelowanych cechach pojedynczy współczynnik przestaje mieć samodzielne znaczenie.

Najważniejsza jest trzecia pozycja w tabeli - ta o ujemnym współczynniku przy s1. Warto ją rozpisać, bo wraca w tym kursie jeszcze kilka razy.

Patrząc osobno, s1 rośnie razem z celem (korelacja +0,21). Ale w modelu dostaje współczynnik ujemny. Sprzeczność jest pozorna, bo te dwie liczby odpowiadają na różne pytania:

Pytanie Odpowiada na nie
„Jak s1 zachowuje się względem celu, gdy patrzę tylko na nie?" korelacja: +0,21
„Co wnosi s1 ponad to, co już wnosi s2?" współczynnik: −1,28

Ponieważ s1 i s2 są skorelowane na poziomie 0,90, model dostaje tę samą informację dwa razy i rozdziela ją między obie kolumny - a rozdzielić może ją na wiele sposobów, byle suma się zgadzała.

Stąd zasada: współczynnik opisuje rolę cechy w obecności wszystkich pozostałych, a nie jej samodzielny związek z celem. Przy silnie skorelowanych cechach pojedynczy współczynnik przestaje mieć sens interpretacyjny - sensowna jest dopiero cała para.

5. Dlaczego MSE jest wrażliwe na wartości odstające

To nie jest teoria do zapamiętania, tylko coś, co da się pokazać w dziesięć sekund. Weźmiemy gotowe wyniki i zepsujemy jeden jedyny rekord w zbiorze testowym: wpiszemy pacjentowi wartość celu równą 5000. Przy skali, która kończy się na 346, taka liczba jest niemożliwa - to typowy błąd wpisu albo pomyłka jednostek, coś, co w prawdziwych danych zdarza się stale.

Jeden rekord na 89. Zobacz, co zrobi z każdą z czterech metryk.

In [ ]:
przewidywania = regresja.predict(X_test)

y_test_zepsute = y_test.copy()
indeks_ofiary = y_test_zepsute.index[0]
y_test_zepsute.loc[indeks_ofiary] = 5000.0      # jeden blednie wpisany pomiar

def metryki(y_prawda, y_przewidziane, opis):
    mse = mean_squared_error(y_prawda, y_przewidziane)
    return {'dane': opis,
            'MAE': mean_absolute_error(y_prawda, y_przewidziane),
            'MSE': mse,
            'RMSE': np.sqrt(mse),
            'R2': r2_score(y_prawda, y_przewidziane)}

porownanie = pd.DataFrame([
    metryki(y_test, przewidywania, 'zbiór testowy bez zmian'),
    metryki(y_test_zepsute, przewidywania, 'jeden rekord z celem = 5000'),
])
print(porownanie.to_string(index=False, float_format=lambda v: f"{v:12.3f}"))
print()
print("Wzrost każdej metryki (ile razy):")
for m in ['MAE', 'MSE', 'RMSE']:
    print(f"  {m:5s}: {porownanie[m][1] / porownanie[m][0]:.2f} x")

Popatrz na ostatnie trzy linie. MAE urosło mniej więcej dwukrotnie, MSE - blisko stukrotnie, RMSE gdzieś pośrodku. Arytmetyka jest prosta: model przewidział dla tego pacjenta około 140, więc błąd na zepsutym rekordzie wynosi około 4860. Do MAE dokłada to 4860 / 89, czyli około 55 jednostek. Do MSE dokłada 4860² / 89, czyli ponad ćwierć miliona - prawie sto razy więcej niż wynosił cały MSE przed zepsuciem danych.

Wniosek: jedna obserwacja potrafi zdominować metrykę opartą na kwadratach. Nie dlatego, że coś jest zepsute w MSE - to działa dokładnie tak, jak zaprojektowano. Pytanie brzmi, czy tego chcemy.

Chcę, żeby… Wybieram
każdy błąd ważył proporcjonalnie do swojej wielkości MAE
duże pomyłki bolały wyraźnie bardziej niż małe RMSE (albo MSE)
wyrazić wynik w jednostkach celu MAE albo RMSE (nie MSE)
porównać modele między różnymi zbiorami danych R² (jest niemianowane)

W medycynie zwykle chcemy RMSE: jedna pomyłka o sto jednostek jest nieporównanie groźniejsza niż sto pomyłek po jednej jednostce. Ale gdy dane bywają zanieczyszczone błędnymi wpisami, RMSE mierzy głównie te wpisy - i wtedy MAE jest uczciwsze.

Zwróć jeszcze uwagę na kolumnę R2. Zachowuje się mniej dramatycznie, niż można by sądzić, i to z ciekawego powodu: zepsuty rekord powiększa jednocześnie błąd modelu i zmienność celu, czyli licznik i mianownik tego samego ułamka. Do tego wątku wraca zadanie 4.

To samo od drugiej strony: zepsuty rekord w danych uczących

Powyżej zepsuliśmy dane testowe, więc ucierpiał tylko pomiar. Model pozostał ten sam. A co, jeśli błędna wartość siedzi w danych uczących?

In [ ]:
y_ucz_zepsute = y_ucz.copy()
y_ucz_zepsute.loc[y_ucz_zepsute.index[0]] = 5000.0

regresja_zepsuta = LinearRegression().fit(X_ucz, y_ucz_zepsute)

print("Ocena na CZYSTYM zbiorze testowym:")
print(pd.DataFrame([
    ocen('model uczony na czystych danych', regresja),
    ocen('model uczony z jednym błędnym rekordem', regresja_zepsuta),
]).to_string(index=False, float_format=lambda v: f"{v:10.3f}"))
print()
print("Jak przesunęły się współczynniki:")
print(pd.DataFrame({
    'czysty': regresja.coef_,
    'zepsuty': regresja_zepsuta.coef_,
    'różnica': regresja_zepsuta.coef_ - regresja.coef_,
}, index=X.columns).round(4).to_string())

To jest wynik, który warto zapamiętać: jedna błędna wartość na 353 rekordy uczące zabrała modelowi większość jego skuteczności. R² spadło z okolic 0,45 do kilku setnych - model, który przed chwilą wyjaśniał blisko połowę zmienności celu, po tej jednej zmianie jest ledwie lepszy od przewidywania średniej.

Powód jest ten sam co poprzednio: metoda najmniejszych kwadratów minimalizuje sumę kwadratów błędów, więc opłaca jej się przesunąć całą hiperpłaszczyznę, byle tylko zmniejszyć ten jeden gigantyczny, podniesiony do kwadratu błąd. Reszta pacjentów płaci za to pogorszeniem - i tym razem płaci słono.

To jest praktyczne uzasadnienie dla całego ćwiczenia 02: wartości odstające trzeba znaleźć, zanim zacznie się trenować. Alternatywą są modele odporne (ang. robust), na przykład HuberRegressor, który karze duże błędy liniowo zamiast kwadratowo.

6. Wykres reszt

Reszta (ang. residual) to błąd na pojedynczej obserwacji:

reszta = wartość prawdziwa - wartość przewidziana

Jedna liczba metryki streszcza kilkaset reszt i przy okazji gubi całą informację o ich strukturze. A struktura reszt mówi rzeczy, których żadna metryka nie powie. Standardowy wykres diagnostyczny to: na osi poziomej przewidywania modelu, na osi pionowej reszty.

Co widać Co to znaczy Co z tym zrobić
bezkształtna chmura wokół zera model wyłapał to, co było liniowe; zostaje sam szum nic - tak ma być
wyraźny łuk albo fala model przeoczył nieliniowość dodać cechy wielomianowe albo zmienić model
lejek (rozrzut rośnie ze wzrostem przewidywań) błąd jest proporcjonalny do wielkości celu rozważyć logarytm celu
reszty przesunięte w górę lub w dół dla części zakresu model systematycznie zaniża albo zawyża w tym zakresie brakuje cechy opisującej tę grupę
In [ ]:
reszty = y_test - przewidywania

fig, osie = plt.subplots(1, 2, figsize=(13, 4.8))

osie[0].scatter(przewidywania, reszty, s=22, alpha=0.6, color='#4C72B0')
osie[0].axhline(0, color='red', linewidth=1.5)
osie[0].set_xlabel('przewidywany postęp choroby')
osie[0].set_ylabel('reszta (prawda - przewidywanie)')
osie[0].set_title('Wykres reszt')
osie[0].grid(alpha=0.3)

osie[1].hist(reszty, bins=25, color='#55A868', edgecolor='white')
osie[1].axvline(0, color='red', linewidth=1.5)
osie[1].set_xlabel('reszta')
osie[1].set_ylabel('liczba pacjentów')
osie[1].set_title('Rozkład reszt')
osie[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

print(f"Średnia reszt: {reszty.mean():+.4f}")
print(f"Odchylenie standardowe reszt: {reszty.std():.4f}")
print()
print("Reszty w podziale na cztery przedziały przewidywań:")
print(pd.DataFrame({'reszta': reszty, 'przedział': pd.qcut(przewidywania, 4)})
      .groupby('przedział', observed=True)['reszta']
      .agg(['mean', 'std', 'count']).round(2).to_string())

Ten wykres warto przeczytać powoli, bo tym razem naprawdę jest na nim co oglądać:

  1. Chmura jest szeroka w poziomie. Przewidywania rozciągają się od około 50 do około 290 - model faktycznie różnicuje pacjentów, a nie odpowiada wszystkim mniej więcej to samo. To graficzny odpowiednik przyzwoitego R².
  2. Nie ma wyraźnego łuku ani fali. Model liniowy nie przeoczył tu żadnej oczywistej nieliniowości.
  3. Widać za to lejek. Tabelka pod wykresem pokazuje to liczbowo: odchylenie standardowe reszt rośnie z każdym kolejnym przedziałem przewidywań. Im gorszy przewidywany stan pacjenta, tym bardziej model się myli. To wzorzec z trzeciego wiersza tabeli powyżej.
  4. Przy najniższych przewidywaniach reszty są średnio dodatnie, czyli model systematycznie zaniża wynik w tym zakresie. Częściowo to efekt tego, że cel nie schodzi poniżej 25 - model liniowy o tym nie wie i potrafi przewidzieć mniej.

Ostatnie dwa punkty opierają się na 89 obserwacjach, więc traktuj je jako sygnał do sprawdzenia, a nie jako dowód. Przy tak małej próbce łatwo zobaczyć wzorzec, którego nie ma - to samo ostrzeżenie, które padło przy histogramach w ćwiczeniu 02.

Histogram obok pokazuje rozkład reszt: z grubsza symetryczny, wyśrodkowany blisko zera. Średnia reszt bliska zeru to zresztą właściwość metody najmniejszych kwadratów, a nie zasługa modelu - warto o tym pamiętać, zanim się z tego ucieszy.

Żeby zobaczyć wykres reszt, który krzyczy, potrzebujemy danych z zależnością, którą świadomie zepsujemy. Do tego posłużą dane syntetyczne.

7. Niedouczenie i przeuczenie

Przechodzimy na dane wygenerowane sztucznie. Powód jest prosty: przy danych syntetycznych znamy prawdziwą zależność, bo sami ją zapisaliśmy. Można więc obejrzeć, jak bardzo model się od niej odchyla - komfort, którego z prawdziwymi danymi nigdy nie ma.

Prawdziwa zależność będzie falą sinusoidalną plus szum. Model dostanie tylko punkty; ma zgadnąć kształt.

Narzędziem będzie PolynomialFeatures: klasa, która z jednej cechy x robi zestaw x, x², x³, … aż do zadanego stopnia. Model pozostaje liniowy - liniowy względem współczynników - ale krzywa, którą rysuje, robi się dowolnie pofalowana. Stopień wielomianu staje się więc pokrętłem sterującym złożonością modelu.

In [ ]:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler

losowy = np.random.RandomState(42)

x_syn = losowy.uniform(0, 5, 40)
y_syn = np.sin(1.4 * x_syn) + losowy.normal(0, 0.25, 40)   # prawda + szum
X_syn = x_syn.reshape(-1, 1)                                # scikit-learn chce dwuwymiarowego X

X_syn_ucz, X_syn_test, y_syn_ucz, y_syn_test = train_test_split(
    X_syn, y_syn, test_size=0.3, random_state=42
)

def model_wielomianowy(stopien, estymator=None):
    # PolynomialFeatures -> StandardScaler -> model liniowy.
    # Skaler nie zmienia dopasowania zwyklej regresji, ale ratuje ja
    # przed problemami numerycznymi przy wysokich potegach.
    return make_pipeline(
        PolynomialFeatures(degree=stopien, include_bias=False),
        StandardScaler(),
        LinearRegression() if estymator is None else estymator,
    )

print(f"Punktów uczących: {len(X_syn_ucz)}, testowych: {len(X_syn_test)}")
In [ ]:
siatka = np.linspace(0, 5, 400).reshape(-1, 1)
stopnie = [1, 4, 15]
opisy = ['stopień 1 - niedouczenie', 'stopień 4 - w sam raz', 'stopień 15 - przeuczenie']

fig, osie = plt.subplots(1, 3, figsize=(16, 4.6), sharey=True)
tabela_syn = []

for ax, stopien, opis in zip(osie, stopnie, opisy):
    m = model_wielomianowy(stopien)
    m.fit(X_syn_ucz, y_syn_ucz)

    tabela_syn.append({
        'stopień': stopien,
        'RMSE uczący': np.sqrt(mean_squared_error(y_syn_ucz, m.predict(X_syn_ucz))),
        'RMSE testowy': np.sqrt(mean_squared_error(y_syn_test, m.predict(X_syn_test))),
    })

    ax.plot(siatka, np.sin(1.4 * siatka.ravel()), color='green', linewidth=2,
            label='prawdziwa zależność')
    ax.plot(siatka, m.predict(siatka), color='red', linewidth=2, label='model')
    ax.scatter(X_syn_ucz, y_syn_ucz, s=30, color='#4C72B0', label='dane uczące')
    ax.scatter(X_syn_test, y_syn_test, s=30, color='orange', marker='s', label='dane testowe')
    ax.set_ylim(-2.5, 2.5)
    ax.set_title(opis)
    ax.set_xlabel('x')
    ax.grid(alpha=0.3)

osie[0].set_ylabel('y')
osie[0].legend(fontsize=8, loc='lower left')
plt.tight_layout()
plt.show()

print(pd.DataFrame(tabela_syn).to_string(index=False, float_format=lambda v: f"{v:.4f}"))

Trzy obrazki i trzy zupełnie różne historie:

Niedouczenie (underfitting) W sam raz Przeuczenie (overfitting)
Model za prosty na dane dopasowany do złożoności zjawiska za elastyczny
Błąd na zbiorze uczącym duży mały bardzo mały
Błąd na zbiorze testowym duży mały duży
Objaw oba błędy wysokie i podobne oba niskie i podobne wielka przepaść między nimi
Co model zrobił przeoczył kształt zależności wychwycił kształt dopasował się do szumu
Lekarstwo bogatszy model, więcej cech nic prostszy model, więcej danych, regularyzacja

Zwróć uwagę na wielomian stopnia 15: przechodzi przez punkty uczące niemal idealnie, a między nimi wariuje. Nauczył się nie zależności, lecz konkretnego losowania szumu. To ten sam mechanizm co drzewo bez ograniczenia głębokości z ćwiczenia 01 - inny model, identyczna choroba.

Najważniejszy nawyk diagnostyczny: zawsze patrz na oba błędy naraz. Sam błąd testowy nie mówi, czy model jest za prosty, czy za skomplikowany - a to dwa problemy wymagające przeciwnych reakcji.

8. Regularyzacja: Ridge i Lasso

Przeuczony wielomian ze stopnia 15 ma charakterystyczną cechę: jego współczynniki są ogromne. Żeby krzywa mogła przecisnąć się przez wszystkie punkty, musi gwałtownie zawracać, a to wymaga wielkich dodatnich i ujemnych wag, które prawie się znoszą.

Stąd pomysł regularyzacji (ang. regularization): skoro duże współczynniki są objawem choroby, dołóżmy do tego, co model minimalizuje, karę za wielkość współczynników.

zwykła regresja:   minimalizuj   suma kwadratów błędów
Ridge:             minimalizuj   suma kwadratów błędów + alpha · suma (w_i)²
Lasso:             minimalizuj   suma kwadratów błędów + alpha · suma |w_i|

Parametr alpha steruje siłą kary:

  • alpha = 0 - kara znika, dostajemy zwykłą regresję liniową,
  • alpha małe - model lekko przyhamowany,
  • alpha duże - współczynniki dociśnięte niemal do zera, model zbliża się do przewidywania średniej (czyli do niedouczenia).

Dlaczego regularyzacja wymaga skalowania cech: kara dotyczy współczynników, a wielkość współczynnika zależy od jednostki cechy. Cecha mierzona w gramach ma współczynnik tysiąc razy mniejszy niż ta sama cecha w kilogramach - i tysiąc razy mniejszą karę. Bez standaryzacji alpha karałoby cechy w sposób zależny od tego, w czym akurat je zmierzono. Dlatego StandardScaler przed Ridge i Lasso to nie opcja, tylko obowiązek.

Dlaczego Lasso zeruje współczynniki, a Ridge nie

To najczęściej zadawane pytanie w tej sekcji i jest na nie prosta odpowiedź: chodzi o zachowanie kary blisko zera.

  • Kara Ridge to kwadrat. Gdy współczynnik jest już mały, jego kwadrat jest malutki (0,01² = 0,0001), więc zysk ze zmniejszania go dalej praktycznie znika. Ridge ściska współczynniki w stronę zera, ale nigdy ich nie dociska do samego zera.
  • Kara Lasso to wartość bezwzględna. Jej „nachylenie" jest takie samo niezależnie od tego, jak blisko zera jesteśmy - zmniejszenie współczynnika z 0,01 do 0 daje dokładnie taki sam zysk na karze jak zmniejszenie z 1,01 do 1,00. Jeśli więc cecha nie zarabia na siebie dokładnością, opłaca się wyciąć ją całkowicie.

Skutek praktyczny: Lasso robi selekcję cech (ang. feature selection). Współczynnik wyzerowany oznacza cechę, której model w ogóle nie używa - można ją usunąć ze zbioru i nic się nie zmieni. Ridge zostawia wszystkie cechy, tylko je przycisza.

Sprawdźmy to na przeuczonym wielomianie stopnia 15.

In [ ]:
from sklearn.linear_model import Ridge, Lasso

STOPIEN = 15
warianty = {
    'bez regularyzacji': LinearRegression(),
    'Ridge (alpha=1)': Ridge(alpha=1.0, random_state=42),
    'Lasso (alpha=0.05)': Lasso(alpha=0.05, max_iter=100000, random_state=42),
}

tabela_reg = []
dopasowane = {}

for nazwa, estymator in warianty.items():
    m = model_wielomianowy(STOPIEN, estymator)
    m.fit(X_syn_ucz, y_syn_ucz)
    dopasowane[nazwa] = m

    wagi = m[-1].coef_
    tabela_reg.append({
        'model': nazwa,
        'RMSE uczący': np.sqrt(mean_squared_error(y_syn_ucz, m.predict(X_syn_ucz))),
        'RMSE testowy': np.sqrt(mean_squared_error(y_syn_test, m.predict(X_syn_test))),
        'największy |w|': np.abs(wagi).max(),
        'zerowych w': int(np.sum(np.abs(wagi) < 1e-8)),
        'wszystkich w': len(wagi),
    })

print(pd.DataFrame(tabela_reg).to_string(index=False, float_format=lambda v: f"{v:10.4f}"))
In [ ]:
fig, osie = plt.subplots(1, 2, figsize=(14, 5))

# Lewy panel: jak wygladaja dopasowane krzywe
osie[0].plot(siatka, np.sin(1.4 * siatka.ravel()), color='green', linewidth=2.5,
             label='prawdziwa zależność')
for nazwa, m in dopasowane.items():
    osie[0].plot(siatka, m.predict(siatka), linewidth=1.8, label=nazwa)
osie[0].scatter(X_syn_ucz, y_syn_ucz, s=30, color='#4C72B0', zorder=5, label='dane uczące')
osie[0].set_ylim(-2.5, 2.5)
osie[0].set_xlabel('x')
osie[0].set_ylabel('y')
osie[0].set_title(f'Wielomian stopnia {STOPIEN} - z regularyzacją i bez')
osie[0].legend(fontsize=8)
osie[0].grid(alpha=0.3)

# Prawy panel: same wspolczynniki
szerokosc = 0.27
pozycje = np.arange(STOPIEN)
for i, (nazwa, m) in enumerate(dopasowane.items()):
    osie[1].bar(pozycje + i * szerokosc, m[-1].coef_, width=szerokosc, label=nazwa)
osie[1].axhline(0, color='black', linewidth=0.8)
osie[1].set_xticks(pozycje + szerokosc)
osie[1].set_xticklabels([f'x^{i}' for i in range(1, STOPIEN + 1)], fontsize=8)
osie[1].set_xlabel('cecha wielomianowa')
osie[1].set_ylabel('współczynnik')
osie[1].set_title('Wielkość współczynników')
osie[1].legend(fontsize=8)
osie[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

Zadania

Zadania wykonuje się samodzielnie; wszystko, czego potrzeba, pojawiło się wyżej. Do dyspozycji są gotowe zmienne: dane, X, y, X_ucz, X_test, y_ucz, y_test, funkcje ocen i model_wielomianowy oraz dane syntetyczne X_syn_ucz, y_syn_ucz, X_syn_test, y_syn_test.

Zadanie 1: Drugi model odniesienia

DummyRegressor ma także strategię "median" - zawsze przewiduje medianę etykiety ze zbioru uczącego.

  1. Zbuduj taki model, naucz go i oceń funkcją ocen.
  2. Zestaw wyniki obok strategii "mean" w jednej tabeli - osobno dla zbioru uczącego i osobno dla testowego (funkcja ocen przyjmuje X_oceny i y_oceny).
  3. Odpowiedz: która strategia wygrywa według MAE, a która według RMSE? Czy odpowiedź jest ta sama na obu zbiorach?

Teoria mówi, że stała minimalizująca sumę kwadratów błędów to średnia, a stała minimalizująca sumę błędów bezwzględnych to mediana. Sprawdź, na którym zbiorze ta reguła sprawdza się co do joty, a na którym potrafi się odwrócić - i zastanów się dlaczego. Przyda się porównanie średniej i mediany celu: rozkład nie jest symetryczny.

In [ ]:
# TWÓJ KOD TUTAJ
# Podpowiedź: schemat jest identyczny jak przy zmiennej `odniesienie`,
# zmienia się tylko wartość argumentu strategy.

Zadanie 2: Ile wnosi cecha bmi?

bmi ma najsilniejszą korelację z celem spośród wszystkich cech. Sprawdź, ile model traci, gdy ta kolumna znika.

  1. Zbuduj X_bez = X.drop(columns=['bmi']).
  2. Podziel dane tak samo jak wcześniej (test_size=0.2, random_state=42).
  3. Wytrenuj LinearRegression i oceń go na zbiorze testowym (pamiętaj o podaniu nowych zbiorów do funkcji ocen).
  4. Porównaj R², MAE i RMSE z modelem korzystającym ze wszystkich cech.

Pytanie na koniec: o ile dokładnie spadło R² i jaką część całego wyjaśnienia odpowiada ta jedna cecha? Czy model bez bmi nadal wyraźnie bije model odniesienia - i co to mówi o tym, czy informacja o pacjencie siedzi w jednej kolumnie, czy jest rozłożona na wiele?

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 3: Które cechy naprawdę ważą najwięcej?

W sekcji 4 padło ostrzeżenie: surowych współczynników nie wolno porównywać między sobą, bo każdy jest w innych jednostkach. Napraw to.

  1. Zbuduj potok make_pipeline(StandardScaler(), LinearRegression()) i naucz go na tych samych danych.
  2. Wyciągnij współczynniki modelu (potok[-1].coef_) i zestaw je z nazwami cech.
  3. Posortuj cechy według wartości bezwzględnej współczynnika.
  4. Wypisz obok siebie ranking po standaryzacji i ranking z surowych współczynników z sekcji 4.

Czy oba rankingi dają tę samą kolejność? Która cecha zmieniła pozycję najbardziej i dlaczego akurat ona? Sprawdź też, czy skalowanie zmieniło R² modelu - i zastanów się, czy powinno.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 4: Jak szybko psuje się każda metryka

W sekcji 5 zepsuliśmy jeden rekord, wpisując wartość celu 5000. Zbadaj teraz, jak wielkość błędnego wpisu przekłada się na metryki.

  1. Dla każdej wartości z listy [150, 400, 1000, 5000, 50000] wstaw ją do jednego rekordu w kopii y_test.
  2. Za każdym razem policz MAE, RMSE i R² względem niezmienionych przewidywań przewidywania.
  3. Zbierz wyniki w DataFrame i narysuj wykres: na osi poziomej wartość błędnego wpisu (skala logarytmiczna, ax.set_xscale('log')), na osi pionowej metryka.
  4. Dorysuj wartości metryk dla niezepsutych danych jako linie poziome.

Odpowiedz: która metryka rośnie liniowo z wielkością błędu, a która kwadratowo? I pytanie trudniejsze: dlaczego R² nie leci w dół bez końca, tylko zatrzymuje się w okolicach zera? Rozpisz wzór R² = 1 - SS_res / SS_tot i sprawdź, co dzieje się z licznikiem, a co z mianownikiem, gdy jedna obserwacja robi się gigantyczna.

Zwróć też uwagę na pierwszą wartość z listy: 150 mieści się w normalnym zakresie celu. Co robią wtedy metryki?

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 5: Wykres reszt, który coś pokazuje

Wykres reszt dla modelu BMI był bezkształtną chmurą. Teraz zrób taki, na którym wzorzec widać z drugiego końca sali.

  1. Dopasuj prostą (model_wielomianowy(1)) do danych syntetycznych X_syn_ucz, y_syn_ucz.
  2. Policz reszty na zbiorze uczącym i narysuj je względem wartości x (nie względem przewidywań - przy jednej cesze to czytelniejsze).
  3. Powtórz to samo dla model_wielomianowy(4).
  4. Narysuj oba wykresy obok siebie (plt.subplots(1, 2)) i dodaj poziomą linię na zerze.

Opisz słowami, co widać na pierwszym wykresie, czego nie ma na drugim. Jaki wzorzec z tabeli w sekcji 6 to jest i jaka byłaby właściwa reakcja, gdyby zobaczyć go na prawdziwych danych?

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 6: Krzywa złożoności

Sekcja 7 pokazała trzy stopnie wielomianu. Sprawdź wszystkie.

  1. Dla stopni od 1 do 15 dopasuj model_wielomianowy(stopien) do danych syntetycznych.
  2. Dla każdego stopnia zanotuj RMSE na zbiorze uczącym i testowym.
  3. Narysuj obie krzywe na jednym wykresie (oś pozioma: stopień, oś pionowa: RMSE). Warto ustawić ax.set_yscale('log') - przy stopniu kilkunastu błąd testowy potrafi urosnąć o rzędy wielkości.
  4. Zaznacz pionową linią stopień o najniższym RMSE testowym.

Odpowiedz na trzy pytania:

  • Czy RMSE uczący maleje monotonicznie? Dlaczego tak musi być?
  • Gdzie dokładnie zaczyna się przeuczenie - czyli od którego stopnia obie krzywe idą w przeciwne strony?
  • Czy stopień wybrany na podstawie zbioru testowego to uczciwy wybór? (Odpowiedź znasz z ćwiczenia 01; pełne rozwiązanie problemu przyjdzie w ćwiczeniu 06.)
In [ ]:
# TWÓJ KOD TUTAJ
# Podpowiedź: pętla `for stopien in range(1, 16):` i lista słowników jak w sekcji 7.

Zadanie 7 (trudniejsze): Lasso jako narzędzie selekcji cech

Zbadaj, czy Lasso faktycznie potrafi znaleźć cechy, które mają znaczenie. Do tego potrzebne są dane, w których wiadomo, które cechy są prawdziwe - a to potrafi wygenerować make_regression:

from sklearn.datasets import make_regression

X_mr, y_mr, wagi_prawdziwe = make_regression(
    n_samples=200, n_features=30, n_informative=5,
    noise=10.0, coef=True, random_state=42,
)

Pięć cech z trzydziestu naprawdę wpływa na y; pozostałe dwadzieścia pięć to czysty szum (ich prawdziwa waga wynosi dokładnie 0).

  1. Podziel dane na uczące i testowe (test_size=0.3, random_state=42).
  2. Dla alpha z listy [0.01, 0.1, 1, 5, 10, 50, 100] dopasuj potok StandardScaler + Lasso(alpha=..., max_iter=100000). Dla każdej wartości zanotuj: liczbę niezerowych współczynników, R² testowe oraz to, ile spośród 5 prawdziwie istotnych cech model zachował (indeksy istotnych: np.flatnonzero(wagi_prawdziwe)).
  3. Zrób to samo dla Ridge z tymi samymi wartościami alpha. Ile współczynników Ridge wyzerował?
  4. Narysuj ścieżkę współczynników dla Lasso: oś pozioma alpha w skali logarytmicznej, jedna linia na każdą z 30 cech. Cechy prawdziwie istotne narysuj grubszą linią.

Odpowiedz: przy jakiej wartości alpha Lasso zostawia mniej więcej pięć cech i czy są to te właściwe? Co się dzieje z R², gdy alpha rośnie dalej? Dlaczego Ridge nie wyzerował ani jednego współczynnika, mimo że dwadzieścia pięć cech to bezużyteczny szum?

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 8 (na koniec): Kiedy odpowiedzi nie ma w danych

Wracamy do pliku dane/diabetes.csv - tego z ćwiczeń 01-03, gdzie przewidywaliśmy Diabetic. Tym razem zadamy mu pytanie regresyjne: spróbuj przewidzieć BMI na podstawie pozostałych cech.

  1. Wczytaj dane/diabetes.csv. Zbuduj X_csv = dane_csv.drop(columns=['PatientID', 'BMI']) oraz y_csv = dane_csv['BMI'].
  2. Podziel dane (test_size=0.2, random_state=42).
  3. Wytrenuj DummyRegressor(strategy="mean") i LinearRegression. Oceń oba na zbiorze testowym - pamiętaj, że funkcja ocen domyślnie sięga po zbiór testowy przykładu prowadzonego, więc nowe zbiory trzeba jej podać jawnie.
  4. Policz korelację każdej cechy z BMI (dane_csv.drop(columns=['PatientID']).corr()['BMI']) i zestaw ją z odchyleniem standardowym BMI.

Potem odpowiedz słowami:

  • Dlaczego model liniowy prawie nie poprawia wyniku względem przewidywania średniej?
  • Czy to wina modelu, czy danych? Jaki dowód na swoją odpowiedź znajdziesz w tabeli korelacji?
  • Czy pomogłoby sięgnięcie po model bardziej złożony - las losowy, sieć neuronową, cokolwiek większego?
  • Co zrobić w prawdziwym projekcie, gdyby po kilku dniach pracy okazało się, że zbiór wygląda właśnie tak? Komu i jak to zakomunikować?

O co tu naprawdę chodzi: to zadanie nie ma dobrego zakończenia i tak ma być. Czasem dane po prostu nie zawierają odpowiedzi na zadane pytanie, a żaden model tego nie naprawi - bo model nie tworzy informacji, tylko ją wydobywa. Umiejętność rozpoznania takiej sytuacji wcześnie jest w praktyce warta więcej niż dowolna liczba godzin spędzonych na strojeniu hiperparametrów. Porównaj przy okazji oba zbiory: load_diabetes z przykładu prowadzonego i ten plik CSV dotyczą tej samej choroby, a dają zupełnie różne wyniki - bo mierzą różne rzeczy i odpowiadają na różne pytania.

In [ ]:
# TWÓJ KOD TUTAJ
# Podpowiedź: dane_csv = pd.read_csv('dane/diabetes.csv')
# Pamiętaj o jawnym podaniu zbiorów oceny: ocen('...', model, X_csv_test, y_csv_test)

Pytania do przemyślenia

Na te pytania odpowiada się słowami, nie kodem.

  1. Model z przykładu prowadzonego osiągnął R² rzędu 0,45, a model z zadania 8 - rzędu 0,05. Czy ten drugi jest bezużyteczny? Od czego zależy odpowiedź i czy istnieją zastosowania, w których tak słaby model nadal się przydaje?
  2. Model A ma niższe MAE, model B niższe RMSE. Który wybrać? Co ta sytuacja mówi o rozkładzie błędów obu modeli?
  3. R² modelu odniesienia wynosi dokładnie 0 na zbiorze uczącym, ale na testowym wychodzi liczba nieco ujemna. Skąd ta różnica? I dlaczego żaden model nie ma dolnego ograniczenia R², choć górne wynosi 1?
  4. Dlaczego przed Ridge i Lasso trzeba wystandaryzować cechy, a przed zwykłą regresją liniową nie trzeba? Co dokładnie poszłoby nie tak, gdyby jedną cechę podać w gramach, a drugą w kilogramach?
  5. Lasso wyzerowało dwadzieścia współczynników z trzydziestu. Czy wolno z tego wyciągnąć wniosek, że te dwadzieścia cech nie ma znaczenia dla zjawiska? Zastanów się, co Lasso robi z dwiema cechami niosącymi niemal tę samą informację.
  6. Wielomian stopnia 15 przechodzi przez punkty uczące niemal idealnie. Dlaczego nie jest to powód do radości - i co by się stało, gdyby poprosić ten model o przewidywanie dla x spoza zakresu danych uczących (ekstrapolacja)?
  7. W sekcji 4 współczynnik przy s1 wyszedł ujemny, choć korelacja s1 z celem jest dodatnia. Co trzeba wiedzieć o kolumnie s2, żeby to wyjaśnić? Czy wobec tego wolno powiedzieć „wyższy cholesterol całkowity wiąże się z wolniejszym postępem choroby"?

Chcesz wiedzieć więcej

W kolejnym ćwiczeniu (05 - Klasyfikacja i metryki) wracamy do etykiet binarnych i do pliku dane/diabetes.csv w jego naturalnej roli. Rozprawimy się z obietnicą złożoną w ćwiczeniu 01: pokażemy metryki, które widzą to, czego skuteczność nie widzi - precyzję, czułość, F1 i krzywą ROC - oraz to, że próg decyzyjny jest wyborem, a nie stałą. Schemat zostaje ten sam co tutaj: najpierw poprzeczka, potem model, na końcu uczciwy pomiar.