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

Ćwiczenie 1: Twój pierwszy model uczenia maszynowego

Po co to ćwiczenie?

W klasycznym programowaniu Ty piszesz reguły: „jeśli poziom glukozy przekracza X, a BMI przekracza Y, zgłoś podejrzenie cukrzycy". Problem w tym, że przy kilkunastu cechach nikt nie jest w stanie takich reguł wymyślić ręcznie - a nawet gdyby, to przestałyby działać na nowych pacjentach.

W uczeniu maszynowym odwracamy ten kierunek: dajemy algorytmowi przykłady z odpowiedziami, a on sam znajduje regułę. Naszą pracą przestaje być pisanie reguł, a staje się:

  1. przygotowanie danych,
  2. wybór algorytmu,
  3. uczciwa ocena, czy to, co powstało, w ogóle działa.

Punkt 3 jest najtrudniejszy i najczęściej psuty - dlatego zajmiemy się nim już w pierwszym ćwiczeniu.

Czego się nauczysz

  1. Jak wygląda pełny cykl: dane → podział → model → ocena.
  2. Dlaczego nie każda kolumna w pliku nadaje się na cechę.
  3. Dlaczego modelu nigdy nie ocenia się na danych, na których się uczył.
  4. Dlaczego pierwszy model, jaki zbudujesz, powinien być celowo prymitywny.
  5. Dlaczego sama „skuteczność" (accuracy) potrafi kłamać.

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

1. Dane

Pracujemy na zbiorze dane/diabetes.csv: 10 000 anonimowych kart pacjentów przebadanych pod kątem cukrzycy. Dla każdego pacjenta mamy wyniki badań (glukoza, ciśnienie, BMI, wiek i kilka innych) oraz informację, czy cukrzycę stwierdzono.

Zadanie, które postawimy modelowi, to klasyfikacja binarna (ang. binary classification): na podstawie wyników badań rozstrzygnąć, czy pacjent choruje (Diabetic = 1), czy nie (Diabetic = 0).

Ten sam zbiór wraca w kursie Azure Machine Learning prowadzonym w tym repozytorium. Tam nacisk pada na uruchamianie treningu w chmurze; tutaj - na samo uczenie maszynowe. Warto zobaczyć te same dane z obu stron.

In [ ]:
import numpy as np
import pandas as pd

dane = pd.read_csv('dane/diabetes.csv')

print("Kształt danych (wiersze, kolumny):", dane.shape)
print("Braki danych:", dane.isna().sum().sum())
dane.head()

Nie każda kolumna jest cechą

Przyjrzyj się kolumnom. Jest wśród nich PatientID - numer identyfikacyjny pacjenta.

Czy to jest cecha? Nie. To etykietka nadana przez system rejestracji, a nie wynik badania. Numer pacjenta nie ma żadnego związku przyczynowego z chorobą - a mimo to model potrafiłby się go „uczyć": przy 10 000 unikalnych numerów wystarczająco rozbudowane drzewo zapamiętałoby, który numer odpowiada choremu pacjentowi. Na danych uczących wyglądałoby to świetnie, a na nowych pacjentach - tragicznie, bo ich numery model widzi pierwszy raz.

To jeden z najczęstszych błędów początkujących: wrzucenie do modelu wszystkiego, co jest w pliku. W literaturze problem ten opisuje się jako przeciek danych (ang. data leakage). Identyfikatory, daty rejestracji, numery próbek - to trzeba usunąć świadomie.

Dzielimy więc dane na:

  • X - cechy (ang. features): to, na podstawie czego model przewiduje,
  • y - etykietę (ang. label, czasem target): to, co model ma przewidzieć.
In [ ]:
X = dane.drop(columns=['PatientID', 'Diabetic'])   # cechy (bez identyfikatora!)
y = dane['Diabetic']                               # etykieta

print("Cechy, z których korzysta model:")
for nazwa in X.columns:
    print("  -", nazwa)
print()
print("Liczba cech:", X.shape[1])

Jak rozkładają się klasy?

Zanim zbudujemy jakikolwiek model, sprawdzamy proporcje klas. To jedna z tych rzeczy, które zajmują dziesięć sekund, a potrafią uratować przed całkowicie błędnymi wnioskami - za chwilę zobaczysz dlaczego.

In [ ]:
liczebnosc = y.value_counts().sort_index()
opisy = {0: 'brak cukrzycy', 1: 'cukrzyca'}

for klasa, liczba in liczebnosc.items():
    print(f"{klasa} = {opisy[klasa]:14s}: {liczba:5d} pacjentów ({liczba / len(y):.1%})")

Klasy nie są równoliczne: około dwie trzecie pacjentów jest zdrowych, jedna trzecia choruje. Zapamiętaj tę liczbę 66,6% - wróci do nas za moment.

2. Podział na zbiór uczący (ang. training set) i testowy (ang. test set)

Teraz najważniejsza zasada całego ćwiczenia:

Modelu nie wolno oceniać na danych, na których się uczył.

Dlaczego? Bo model może po prostu zapamiętać przykłady zamiast nauczyć się ogólnej reguły. Model, który zapamiętał odpowiedzi, na znanych danych osiągnie wynik bliski ideału - i będzie bezużyteczny w praktyce, bo prawdziwi pacjenci to zawsze nowe przypadki.

Dlatego odkładamy część danych na bok i nie pokazujemy ich modelowi podczas uczenia. Dopiero na nich mierzymy skuteczność - to symuluje sytuację „model spotyka pacjenta, którego nigdy nie widział".

Trzy argumenty, których właśnie użyliśmy, warto rozumieć, bo będą wracać w każdym kolejnym ćwiczeniu:

Argument Co robi Co się stanie, jeśli go pominiesz
test_size=0.2 odkłada 20% danych na test domyślnie odłoży 25% - nic złego, ale warto decydować świadomie
stratify=y zachowuje proporcje klas w obu częściach przy losowym podziale proporcje mogą się rozjechać i wyniki stają się trudne do porównania
random_state=42 ustala ziarno losowości (ang. random seed), więc podział jest powtarzalny przy każdym uruchomieniu dostaniesz inny podział i inne wyniki - nie będziesz wiedzieć, czy zmiana wyniku to zasługa modelu, czy przypadku

3. Model odniesienia - celowo prymitywny

Zanim sięgniemy po prawdziwy algorytm, budujemy model odniesienia (ang. baseline): najprostszy możliwy model, jaki da się wymyślić. Tutaj będzie to model, który kompletnie ignoruje wyniki badań i zawsze odpowiada tą samą, najczęstszą klasą - czyli „ten pacjent jest zdrowy", niezależnie od tego, kogo dostanie.

Po co nam coś takiego? Bo wynik modelu sam w sobie nic nie znaczy. Zdanie „model ma 90% skuteczności" jest bez wartości, dopóki nie wiesz, ile miałby model, który nie robi nic. Model odniesienia wyznacza poprzeczkę - poniżej niej nie ma o czym rozmawiać.

In [ ]:
from sklearn.model_selection import train_test_split

X_ucz, X_test, y_ucz, y_test = train_test_split(
    X, y,
    test_size=0.2,        # 20% odkładamy na test
    stratify=y,           # zachowaj proporcje klas w obu częściach
    random_state=42,      # ustalone ziarno losowości - powtarzalny podział
)

print(f"Zbiór uczący:  {len(X_ucz):5d} pacjentów")
print(f"Zbiór testowy: {len(X_test):5d} pacjentów")
print()
print("Udział chorych w zbiorze uczącym: ", f"{y_ucz.mean():.1%}")
print("Udział chorych w zbiorze testowym:", f"{y_test.mean():.1%}")
In [ ]:
from sklearn.dummy import DummyClassifier

model_odniesienia = DummyClassifier(strategy="most_frequent")
model_odniesienia.fit(X_ucz, y_ucz)

skutecznosc_odniesienia = model_odniesienia.score(X_test, y_test)
print(f"Skuteczność modelu odniesienia: {skutecznosc_odniesienia:.1%}")

Model, który nie patrzy na dane w ogóle, ma około 66,6% skuteczności - dokładnie tyle, ile wynosi udział osób zdrowych w zbiorze.

Zatrzymaj się tu na chwilę, bo to jest sedno. Ten model nie wykrył ani jednego chorego pacjenta. Jest medycznie bezużyteczny, a mimo to jego „skuteczność" brzmi przyzwoicie. Gdyby chorych był 1% zamiast 33%, taki bezmyślny model miałby 99% skuteczności - i nadal przeoczyłby każdy przypadek choroby.

Właśnie dlatego w ćwiczeniu 05 zajmiemy się metrykami, które pokazują to, czego skuteczność nie pokazuje.

4. Prawdziwy model

Teraz model, który faktycznie korzysta z danych: regresja logistyczna (ang. logistic regression). Mimo nazwy służy do klasyfikacji - dla każdego pacjenta wylicza prawdopodobieństwo przynależności do klasy.

Użyjemy jej wewnątrz potoku (Pipeline) razem ze skalowaniem cech. Skalowanie (ang. feature scaling) sprowadza wszystkie cechy do porównywalnego zakresu - bez tego glukoza (wartości rzędu 100) przytłoczyłaby wskaźnik DiabetesPedigree (wartości poniżej 1). Potokami zajmiemy się na poważnie w ćwiczeniu 03; na razie potraktuj to jako „skalowanie i model połączone w jedną całość".

In [ ]:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

model = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=1000, random_state=42),
)

model.fit(X_ucz, y_ucz)   # uczenie: model szuka reguły w danych uczących

skutecznosc = model.score(X_test, y_test)
print(f"Skuteczność modelu odniesienia:    {skutecznosc_odniesienia:.1%}")
print(f"Skuteczność regresji logistycznej: {skutecznosc:.1%}")
print(f"Poprawa: {skutecznosc - skutecznosc_odniesienia:+.1%}")

5. Ocena: co model właściwie myli?

Jedna liczba (skuteczność) nie mówi, jakie błędy popełnia model. A w medycynie rodzaj błędu ma ogromne znaczenie:

  • uznanie chorego za zdrowego → pacjent nie trafia na dalszą diagnostykę i nie dostaje leczenia,
  • uznanie zdrowego za chorego → niepotrzebny stres i dodatkowe badania.

Te dwa błędy nie są równie kosztowne, więc musimy je widzieć osobno. Służy do tego macierz pomyłek (ang. confusion matrix).

In [ ]:
import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay

fig, ax = plt.subplots(figsize=(5.5, 4.5))
ConfusionMatrixDisplay.from_estimator(
    model, X_test, y_test,
    display_labels=['brak cukrzycy', 'cukrzyca'],
    cmap="Blues",
    values_format="d",
    ax=ax,
)
ax.set_title("Macierz pomyłek - regresja logistyczna")
ax.set_xlabel("Przewidziano")
ax.set_ylabel("Rzeczywistość")
plt.tight_layout()
plt.show()

Jak to czytać: wiersze to prawda, kolumny to przewidywanie. Przekątna (lewy górny → prawy dolny) to trafienia, wszystko poza nią to pomyłki.

Najważniejsze pole to wiersz cukrzyca, kolumna brak cukrzycy: to pacjenci chorzy, których model uznał za zdrowych. W zastosowaniu medycznym to najgroźniejszy rodzaj błędu - i zwróć uwagę, że nie widać go w samej skuteczności.


Zajrzyj do środka: jak wygląda wytrenowane drzewo?

Drzewo decyzyjne (ang. decision tree) ma rzadką i bardzo cenną cechę: da się je obejrzeć i zrozumieć. Nie jest czarną skrzynką - to zestaw zagnieżdżonych pytań typu „czy ta cecha jest mniejsza niż X?".

Do narysowania drzewa użyjemy funkcji plot_tree, która rysuje zwykłym matplotlibem. Zwróć uwagę na max_depth=3: rysujemy celowo płytkie drzewo, bo drzewo bez ograniczeń ma kilkadziesiąt poziomów i na rysunku byłoby zupełnie nieczytelne.

In [ ]:
from sklearn.tree import DecisionTreeClassifier, plot_tree

# Płytkie drzewo - tylko po to, żeby dało się je obejrzeć
drzewo_male = DecisionTreeClassifier(max_depth=3, random_state=42)
drzewo_male.fit(X_ucz, y_ucz)

fig, ax = plt.subplots(figsize=(18, 9))
plot_tree(
    drzewo_male,
    feature_names=X.columns,
    class_names=['brak cukrzycy', 'cukrzyca'],
    filled=True,       # koloruje węzły według dominującej klasy
    rounded=True,
    fontsize=9,
    ax=ax,
)
plt.tight_layout()
plt.show()

print("Skuteczność tego płytkiego drzewa na zbiorze testowym:",
      f"{drzewo_male.score(X_test, y_test):.1%}")

Jak czytać ten rysunek

Każdy prostokąt to węzeł (ang. node) - jedno pytanie zadane danym. W węźle znajdziesz:

Wiersz w węźle Co oznacza
PlasmaGlucose <= 122.5 warunek: jeśli prawda, idziesz w lewo; jeśli fałsz - w prawo
gini = 0.44 miara „wymieszania" klas: 0 oznacza węzeł czysty (sami chorzy albo sami zdrowi), 0.5 - idealnie pół na pół
samples = 8000 ilu pacjentów ze zbioru uczącego trafiło do tego węzła
value = [5325, 2675] jak ci pacjenci dzielą się na klasy
class = brak cukrzycy co model odpowie, jeśli zatrzyma się w tym węźle

Liście (ang. leaves; węzły na samym dole) to gotowe odpowiedzi. Kolor pokazuje dominującą klasę, a jego intensywność - jak bardzo węzeł jest „czysty".

Prześledź jedną ścieżkę od korzenia do liścia i przeczytaj ją jak zdanie: „jeśli glukoza ≤ 122,5 oraz BMI ≤ 29,4, to pacjent jest zdrowy". To właśnie jest reguła, której model nauczył się sam - nikt mu jej nie napisał.


Zadania

Poniższe zadania wykonujesz samodzielnie. Wszystko, czego potrzebujesz, pojawiło się w przykładzie powyżej.

Zadanie 1: Inny model odniesienia

DummyClassifier ma także strategię "stratified" - zgaduje losowo, ale zgodnie z proporcjami klas w danych uczących.

Zbuduj taki model, naucz go i sprawdź jego skuteczność na zbiorze testowym. Zastanów się przed uruchomieniem: spodziewasz się wyniku wyższego czy niższego niż 66,6%?

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Utwórz DummyClassifier ze strategią "stratified" i ustaw random_state=42.
  2. Naucz go na zbiorze uczącym.
  3. Zmierz jego skuteczność na zbiorze testowym.
  4. Wypisz obok siebie wynik obu strategii: "most_frequent" i "stratified".

Czym różnią się te dwie strategie: "most_frequent" zawsze odpowiada tą samą klasą - tą, która w danych uczących występuje najczęściej. "stratified" losuje odpowiedź, ale zgodnie z proporcjami klas: skoro chorych jest 33%, to w 33% przypadków zgaduje „chory".

Zgadnij przed uruchomieniem. To nie jest ozdobnik - chodzi o to, żebyś zobaczył różnicę między tym, co intuicja podpowiada, a tym, co wychodzi. Zapamiętaj swoją odpowiedź i sprawdź ją za chwilę.

Jak to napisać - kod z wyjaśnieniem

Krok 1-2: model i trenowanie

model_stratified = DummyClassifier(strategy="stratified", random_state=42)
model_stratified.fit(X_ucz, y_ucz)
  • strategy="stratified" włącza losowanie zgodne z proporcjami klas.
  • random_state=42 jest tu konieczne, a nie kosmetyczne. Ta strategia naprawdę losuje, więc bez ustalonego ziarna dostaniesz inny wynik przy każdym uruchomieniu i nie będziesz wiedzieć, czy różnica bierze się ze zmiany w kodzie, czy z losu.
  • .fit() przy modelu odniesienia nie uczy się niczego o zależnościach - zapamiętuje tylko rozkład klas.

Krok 3-4: pomiar i porównanie

print(f"strategy='most_frequent': {DummyClassifier(strategy='most_frequent').fit(X_ucz, y_ucz).score(X_test, y_test):.1%}")
print(f"strategy='stratified':    {model_stratified.score(X_test, y_test):.1%}")
  • .score(X_test, y_test) dla klasyfikatora zwraca skuteczność - udział poprawnych odpowiedzi.
  • :.1% w f-stringu mnoży przez 100 i dokleja znak procenta, więc 0.665 wypisze się jako 66.5%.
  • Zapis w jednej linii (DummyClassifier(...).fit(...).score(...)) działa, bo .fit() zwraca sam model. Przy dłuższym kodzie lepiej to rozbić, tutaj chodzi o zestawienie obu liczb obok siebie.
Gotowe rozwiązanie i czego się spodziewać
model_stratified = DummyClassifier(strategy="stratified", random_state=42)
model_stratified.fit(X_ucz, y_ucz)

print(f"strategy='most_frequent': {DummyClassifier(strategy='most_frequent').fit(X_ucz, y_ucz).score(X_test, y_test):.1%}")
print(f"strategy='stratified':    {model_stratified.score(X_test, y_test):.1%}")

Czego się spodziewać:

strategy='most_frequent': 66.5%
strategy='stratified':    54.5%

Jak to czytać:

Jeśli spodziewałeś się wyniku wyższego, jesteś w licznym towarzystwie. Intuicja podpowiada, że model, który „bierze pod uwagę proporcje klas", powinien być mądrzejszy od takiego, który zawsze mówi to samo. Jest odwrotnie - i to o 12 punktów procentowych.

Powód: przy dwóch klasach o udziałach 2/3 i 1/3 zgadywanie zgodne z proporcjami trafia średnio w 0,666 × 0,666 + 0,334 × 0,334 ≈ 0,55. Odpowiadanie zawsze klasą większościową trafia w 0,666. Losowanie dokłada pomyłki po obu stronach.

Wniosek, który warto zapamiętać: model odniesienia nie ma być mądry - ma wyznaczać poprzeczkę. I bierze się zawsze tę najwyższą z rozsądnych, czyli tutaj 66,6%. Gdybyś porównywał swój model do 54,5%, byłoby Ci nieuczciwie łatwo.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 2: Drzewo decyzyjne

Wytrenuj klasyfikator DecisionTreeClassifier (bez ograniczenia głębokości) i porównaj jego skuteczność z regresją logistyczną.

Pamiętaj o random_state=42, żeby wynik był powtarzalny. Drzewo nie potrzebuje skalowania - zastanów się, dlaczego (odpowiedź znajdziesz w pytaniach na końcu).

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Utwórz DecisionTreeClassifier z random_state=42, bez ustawiania max_depth.
  2. Naucz go na zbiorze uczącym.
  3. Wypisz obok siebie skuteczność regresji logistycznej i drzewa na zbiorze testowym.

Dlaczego drzewo nie potrzebuje skalowania: drzewo zadaje pytania w rodzaju „czy BMI jest większe niż 30?". Jeśli przeskalujesz tę cechę, próg też się przeskaluje, a podział pacjentów na dwie grupy pozostanie dokładnie ten sam. Regresja logistyczna dodaje cechy do siebie z wagami, więc tam skala zmienia wynik - i dlatego tam StandardScaler jest potrzebny.

Jak to napisać - kod z wyjaśnieniem

Krok 1-2: drzewo

drzewo = DecisionTreeClassifier(random_state=42)
drzewo.fit(X_ucz, y_ucz)

Brak max_depth oznacza, że drzewo rośnie bez ograniczeń - tak długo, aż liście staną się jednorodne albo skończą się dane do podziału. Zapamiętaj to, bo zadanie 3 opiera się właśnie na tym.

Zwróć uwagę, że podajesz tu X_ucz, a nie dane przeskalowane. Drzewo skalowania nie potrzebuje.

Krok 3: porównanie

print(f"Regresja logistyczna: {model.score(X_test, y_test):.1%}")
print(f"Drzewo decyzyjne:     {drzewo.score(X_test, y_test):.1%}")

model to potok z regresją logistyczną zbudowany wcześniej w notatniku - nie musisz go tworzyć od nowa. Spacje w drugim napisie wyrównują liczby w pionie, dzięki czemu różnicę widać od razu.

Gotowe rozwiązanie i czego się spodziewać
drzewo = DecisionTreeClassifier(random_state=42)
drzewo.fit(X_ucz, y_ucz)

print(f"Regresja logistyczna: {model.score(X_test, y_test):.1%}")
print(f"Drzewo decyzyjne:     {drzewo.score(X_test, y_test):.1%}")

Czego się spodziewać:

Regresja logistyczna: 78.8%
Drzewo decyzyjne:     89.0%

Jak to czytać:

Drzewo wygrywa o ponad 10 punktów procentowych. To duża różnica i warto wiedzieć, skąd się bierze: regresja logistyczna szuka jednej prostej granicy między klasami, a drzewo może dzielić przestrzeń na dowolnie poszatkowane obszary. Gdy zależność nie jest liniowa, drzewo ma znacznie większe pole manewru.

Nie wyciągaj stąd wniosku, że drzewa są lepsze od regresji. W zadaniu 3 zobaczysz drugą stronę tej elastyczności - i będzie to najważniejsza rzecz w całym notatniku.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 3: Dowód, że ocena na danych uczących kłamie

To najważniejsze zadanie w tym notatniku.

  1. Weź drzewo wytrenowane w zadaniu 2 (bez ograniczenia głębokości).
  2. Zmierz jego skuteczność na zbiorze uczącym (X_ucz, y_ucz).
  3. Zmierz jego skuteczność na zbiorze testowym (X_test, y_test).
  4. Wypisz obie liczby obok siebie.

Różnica, którą zobaczysz, to przeuczenie (ang. overfitting) - model nauczył się danych uczących niemal na pamięć, zamiast wychwycić ogólną regułę.

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Weź drzewo wytrenowane w zadaniu 2 - nie twórz nowego.
  2. Zmierz jego skuteczność na zbiorze uczącym (X_ucz, y_ucz).
  3. Zmierz jego skuteczność na zbiorze testowym (X_test, y_test).
  4. Wypisz obie liczby oraz ich różnicę.

Na czym polega sedno: zbiór uczący to dane, które model widział podczas nauki. Zbiór testowy to dane, których nie widział. Porównanie tych dwóch liczb odpowiada na pytanie: czy model nauczył się reguły, czy zapamiętał konkretne przypadki?

Jak to napisać - kod z wyjaśnieniem

Krok 1-3: dwa pomiary

wynik_ucz = drzewo.score(X_ucz, y_ucz)
wynik_test = drzewo.score(X_test, y_test)

To ta sama metoda .score(), tylko wywołana na dwóch różnych zbiorach. Cała różnica polega na tym, które dane jej podasz.

Zapisujesz wyniki do zmiennych, bo za chwilę policzysz z nich różnicę - i dlatego, że wrócisz do nich w zadaniu 6.

Krok 4: wypisanie

print(f"Skuteczność na zbiorze UCZĄCYM:  {wynik_ucz:.1%}")
print(f"Skuteczność na zbiorze TESTOWYM: {wynik_test:.1%}")
print(f"Różnica (przeuczenie):           {wynik_ucz - wynik_test:.1%}")

Trzecia linia jest najważniejsza. Sama różnica to liczba, która ma własną nazwę i własne znaczenie - lepiej ją wypisać, niż kazać czytelnikowi odejmować w pamięci.

Gotowe rozwiązanie i czego się spodziewać
wynik_ucz = drzewo.score(X_ucz, y_ucz)
wynik_test = drzewo.score(X_test, y_test)

print(f"Skuteczność na zbiorze UCZĄCYM:  {wynik_ucz:.1%}")
print(f"Skuteczność na zbiorze TESTOWYM: {wynik_test:.1%}")
print(f"Różnica (przeuczenie):           {wynik_ucz - wynik_test:.1%}")

Czego się spodziewać:

Skuteczność na zbiorze UCZĄCYM:  100.0%
Skuteczność na zbiorze TESTOWYM: 89.0%
Różnica (przeuczenie):           11.0%

Jak to czytać - to jest najważniejszy wynik w całym notatniku:

100,0% na zbiorze uczącym. Nie 99,8%. Dokładnie sto procent, czyli ani jednej pomyłki na ośmiu tysiącach pacjentów.

Żaden model medyczny nie jest bezbłędny. Ta liczba nie mówi więc nic o jakości drzewa - mówi tylko tyle, że drzewo rosło dopóty, dopóki nie zapamiętało każdego pacjenta z osobna. Bez ograniczenia głębokości zawsze jest w stanie to zrobić.

Prawdziwy wynik to 89,0% - tyle drzewo osiąga na pacjentach, których nie widziało.

Konsekwencja praktyczna: gdyby ktoś pokazał Ci model ze skutecznością 100% i nie powiedział, na jakich danych ją zmierzył, pierwsze pytanie brzmi: „na uczących czy testowych?". Wynik na danych uczących nie jest wynikiem modelu - jest miarą jego pamięci.

Różnica 11 punktów to przeuczenie (ang. overfitting). W zadaniu 4 zobaczysz, jak je zmniejszyć.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 4: Ograniczenie złożoności drzewa

Drzewo z zadania 3 rozrastało się bez ograniczeń. Ogranicz jego głębokość parametrem max_depth.

Sprawdź wartości max_depth od 1 do 15 i dla każdej wypisz skuteczność na zbiorze uczącym oraz testowym. Znajdź wartość, przy której wynik testowy jest najlepszy.

Zauważ, co się dzieje: wynik na danych uczących rośnie z głębokością niemal zawsze, ale wynik testowy w pewnym momencie przestaje rosnąć i zaczyna spadać. To jest moment, w którym model przestaje uczyć się reguł, a zaczyna zapamiętywać szum.

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Przygotuj pustą listę na wyniki.
  2. W pętli po wartościach max_depth od 1 do 15 zbuduj i naucz drzewo o takiej głębokości.
  3. Dla każdego zapisz głębokość oraz obie skuteczności - uczącą i testową.
  4. Zamień listę na tabelę i znajdź wiersz o najlepszym wyniku testowym.
  5. Narysuj obie krzywe na jednym wykresie.

Czym jest max_depth: maksymalna liczba pytań, jakie drzewo może zadać po drodze od korzenia do decyzji. max_depth=1 to jedno pytanie, max_depth=15 to co najwyżej piętnaście. Im więcej pytań, tym drobniejsze różnice model potrafi wychwycić - i tym łatwiej zapamiętuje przypadki zamiast reguł.

Jak to napisać - kod z wyjaśnieniem

Krok 1-3: pętla po głębokościach

wyniki = []
for glebokosc in range(1, 16):
    d = DecisionTreeClassifier(max_depth=glebokosc, random_state=42)
    d.fit(X_ucz, y_ucz)
    wyniki.append({
        'max_depth': glebokosc,
        'uczacy': d.score(X_ucz, y_ucz),
        'testowy': d.score(X_test, y_test),
    })
  • range(1, 16) daje liczby od 1 do 15 - w Pythonie prawy kraniec nie należy do zakresu.
  • W każdym obrocie pętli powstaje nowe drzewo. Nie da się zmienić max_depth w już wytrenowanym modelu.
  • Zbierasz słowniki do listy, a tabelę zbudujesz raz na końcu. Doklejanie wierszy do DataFrame w pętli jest wolne i mniej czytelne.

Krok 4: tabela i najlepszy wiersz

tabela = pd.DataFrame(wyniki)
najlepszy = tabela.loc[tabela['testowy'].idxmax()]
  • .idxmax() zwraca indeks wiersza o największej wartości, a nie samą wartość.
  • tabela.loc[indeks] wyciąga cały ten wiersz, więc masz od razu i głębokość, i oba wyniki.

Krok 5: wykres

ax.plot(tabela['max_depth'], tabela['uczacy'], marker='o', label='zbiór uczący')
ax.plot(tabela['max_depth'], tabela['testowy'], marker='s', label='zbiór testowy')
ax.axvline(najlepszy['max_depth'], color='gray', linestyle='--', linewidth=1)

axvline rysuje pionową linię w miejscu optimum. Bez niej trzeba szukać wzrokiem, gdzie krzywa testowa ma szczyt.

Gotowe rozwiązanie i czego się spodziewać
wyniki = []
for glebokosc in range(1, 16):
    d = DecisionTreeClassifier(max_depth=glebokosc, random_state=42)
    d.fit(X_ucz, y_ucz)
    wyniki.append({
        'max_depth': glebokosc,
        'uczacy': d.score(X_ucz, y_ucz),
        'testowy': d.score(X_test, y_test),
    })

tabela = pd.DataFrame(wyniki)
najlepszy = tabela.loc[tabela['testowy'].idxmax()]

print(tabela.to_string(index=False, float_format=lambda v: f"{v:.3f}"))
print()
print(f"Najlepszy wynik testowy przy max_depth = {int(najlepszy['max_depth'])}")

# Wykres - bardzo dobrze pokazuje moment, w ktorym model zaczyna sie przeuczac
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(tabela['max_depth'], tabela['uczacy'], marker='o', label='zbiór uczący')
ax.plot(tabela['max_depth'], tabela['testowy'], marker='s', label='zbiór testowy')
ax.axvline(najlepszy['max_depth'], color='gray', linestyle='--', linewidth=1)
ax.set_xlabel('max_depth (głębokość drzewa)')
ax.set_ylabel('skuteczność')
ax.set_title('Im głębsze drzewo, tym lepiej? Nie na danych testowych.')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

Czego się spodziewać:

 max_depth  uczacy  testowy
         1   0.776    0.780
         2   0.852    0.843
         3   0.869    0.876
         4   0.888    0.893
         5   0.901    0.897
         6   0.914    0.900
         7   0.932    0.910
         8   0.942    0.917
         9   0.950    0.907
        10   0.963    0.904
        11   0.971    0.902
        12   0.980    0.904
        13   0.987    0.898
        14   0.991    0.900
        15   0.995    0.893

Najlepszy wynik testowy przy max_depth = 8

Jak to czytać:

Prześledź obie kolumny z góry na dół. uczacy rośnie praktycznie zawsze - z 0,776 do 0,995. Z punktu widzenia danych uczących każde dodatkowe pytanie to poprawa.

testowy zachowuje się inaczej: rośnie do max_depth=8 (0,917), a potem zaczyna spadać - przy 15 wynosi już tylko 0,893, czyli mniej niż przy głębokości 4.

To jest przeuczenie zobaczone w ruchu. Do ósmego pytania drzewo uczy się reguł, które działają także na nowych pacjentach. Od dziewiątego zaczyna dopasowywać się do przypadkowych szczegółów zbioru uczącego - szczegółów, których u nowych pacjentów nie ma.

Na wykresie obie krzywe najpierw idą razem, a potem się rozchodzą. Rosnąca przepaść między nimi to dokładnie ta sama wielkość, którą zmierzyłeś w zadaniu 3.

Uwaga praktyczna: wybór max_depth = 8 jest tu dokonany na zbiorze testowym - i to jest błąd metodologiczny, który popełniamy tu celowo, żeby pokazać zjawisko. Poprawnie robi się to walidacją krzyżową na zbiorze uczącym; wrócisz do tego w ćwiczeniu 06.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 5: Czytelność kontra dokładność

  1. Narysuj drzewo z max_depth=1 (tzw. pień decyzyjny). Jaką ma skuteczność? Która cecha okazała się na tyle silna, że model wybrał ją jako jedyne pytanie?
  2. Spróbuj narysować drzewo bez ograniczenia głębokości. Da się cokolwiek z tego rysunku odczytać?
  3. Odpowiedz sobie: gdyby trzeba było wytłumaczyć decyzję modelu lekarzowi, które drzewo byś wybrał i dlaczego?

To napięcie - model dokładniejszy kontra model zrozumiały - wraca w uczeniu maszynowym nieustannie. W ćwiczeniu 07 zobaczysz jego skrajny przypadek: las losowy bywa wyraźnie skuteczniejszy od pojedynczego drzewa, ale nie da się go już narysować na jednej kartce.

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Zbuduj drzewo z max_depth=1 i naucz je.
  2. Narysuj je przy użyciu plot_tree, podając nazwy cech i nazwy klas.
  3. Wypisz jego skuteczność na zbiorze testowym.
  4. Sprawdź, którą cechę model wybrał jako jedyne pytanie - wykorzystaj feature_importances_.
  5. Spróbuj narysować drzewo bez ograniczenia głębokości i zobacz, co z tego wyjdzie.

Czym jest pień decyzyjny (ang. decision stump): drzewo o jednym pytaniu. To najprostszy możliwy model tego typu - dzieli pacjentów na dwie grupy na podstawie jednej cechy i jednego progu.

Jak to napisać - kod z wyjaśnieniem

Krok 1: pień

pien = DecisionTreeClassifier(max_depth=1, random_state=42)
pien.fit(X_ucz, y_ucz)

Krok 2: rysunek

fig, ax = plt.subplots(figsize=(9, 4))
plot_tree(pien, feature_names=X.columns, class_names=['brak cukrzycy', 'cukrzyca'],
          filled=True, rounded=True, fontsize=10, ax=ax)
  • feature_names=X.columns sprawia, że w węzłach zobaczysz nazwy cech zamiast X[3]. Bez tego rysunek jest prawie nieczytelny.
  • class_names=[...] podpisuje klasy słowami zamiast 0 i 1. Kolejność musi odpowiadać wartościom etykiety: najpierw 0, potem 1.
  • filled=True koloruje węzły według przeważającej klasy - od razu widać, która strona podziału to chorzy.
  • ax=ax kieruje rysunek na przygotowaną oś, dzięki czemu działa figsize.

Krok 4: która cecha

waznosc = pd.Series(pien.feature_importances_, index=X.columns).sort_values(ascending=False)
print(waznosc.head(3).to_string())

feature_importances_ to tablica liczb - po jednej na cechę, w tej samej kolejności co kolumny. Opakowanie jej w pd.Series z indeksem X.columns przypisuje liczby do nazw; bez tego masz osiem liczb i musisz sam pamiętać, która jest która.

Gotowe rozwiązanie i czego się spodziewać
pien = DecisionTreeClassifier(max_depth=1, random_state=42)
pien.fit(X_ucz, y_ucz)

fig, ax = plt.subplots(figsize=(9, 4))
plot_tree(pien, feature_names=X.columns, class_names=['brak cukrzycy', 'cukrzyca'],
          filled=True, rounded=True, fontsize=10, ax=ax)
ax.set_title('Pień decyzyjny - jedno pytanie')
plt.tight_layout()
plt.show()

print(f"Skuteczność pnia decyzyjnego: {pien.score(X_test, y_test):.1%}")
print()
waznosc = pd.Series(pien.feature_importances_, index=X.columns).sort_values(ascending=False)
print("Cecha wybrana przez model jako jedyne pytanie:")
print(waznosc.head(3).to_string())

Czego się spodziewać:

Skuteczność pnia decyzyjnego: 78.0%

Cecha wybrana przez model jako jedyne pytanie:
Pregnancies               1.0
PlasmaGlucose             0.0
DiastolicBloodPressure    0.0

Jak to czytać:

  • Model zadający jedno pytanie osiąga 78,0%. Przypomnij sobie regresję logistyczną z ośmioma cechami: 78,8%. Różnica wynosi 0,8 punktu procentowego.
  • Wszystkie ważności poza Pregnancies wynoszą dokładnie 0,0. To nie zaokrąglenie - drzewo o jednym pytaniu fizycznie nie może użyć drugiej cechy.
  • Pregnancies to ta sama cecha, która wygrała ranking korelacji w ćwiczeniu 02. Dwie różne metody, ten sam wynik.

Odpowiedź na pytanie 3 z zadania: jeśli różnica w skuteczności wynosi niecały punkt procentowy, a jeden model da się wytłumaczyć lekarzowi jednym zdaniem, wybór jest oczywisty. Model, którego nikt nie rozumie, bywa nie do wdrożenia - nie dlatego, że jest gorszy, tylko dlatego, że nikt nie weźmie za niego odpowiedzialności.

Przy drzewie bez ograniczenia głębokości rysunek zamienia się w nieczytelną plamę - to ma setki węzłów. I to jest właśnie odpowiedź na pytanie 2.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 6: Co się stanie, gdy zostawimy PatientID?

Sprawdź empirycznie, dlaczego usunęliśmy identyfikator pacjenta.

  1. Zbuduj X_zle = dane.drop(columns=['Diabetic']) - czyli cechy razem z PatientID.
  2. Podziel dane tak samo jak wcześniej (test_size=0.2, stratify=y, random_state=42).
  3. Wytrenuj DecisionTreeClassifier(random_state=42) bez ograniczenia głębokości.
  4. Porównaj skuteczność na zbiorze uczącym i testowym z wynikami z zadania 3.

Zastanów się nad wynikiem: czy model z identyfikatorem wypadł lepiej, czy gorzej? Co to mówi o pokusie „wrzućmy wszystkie kolumny, model sobie poradzi"?

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Zbuduj X_zle, usuwając z danych tylko kolumnę Diabetic - czyli zostawiając PatientID.
  2. Podziel dane dokładnie tak samo jak wcześniej.
  3. Wytrenuj drzewo bez ograniczenia głębokości.
  4. Wypisz jego skuteczność uczącą i testową obok wyników z zadania 3.
  5. Sprawdź, na którym miejscu w rankingu ważności wylądował PatientID.

Dlaczego to w ogóle sprawdzamy: PatientID to numer nadany przy rejestracji. Nie jest wynikiem badania i nie niesie żadnej informacji o zdrowiu. Model o tym nie wie - dla niego to po prostu kolejna kolumna z liczbami.

Jak to napisać - kod z wyjaśnieniem

Krok 1: dane z identyfikatorem

X_zle = dane.drop(columns=['Diabetic'])   # zostawiamy PatientID jako "cechę"

Usuwasz tylko etykietę. W poprawnej wersji usuwałeś dwie kolumny: PatientID i Diabetic.

Krok 2: ten sam podział

X_zle_ucz, X_zle_test, y_zle_ucz, y_zle_test = train_test_split(
    X_zle, y, test_size=0.2, stratify=y, random_state=42
)

random_state=42 jest tu kluczowe: chcesz, żeby dokładnie ci sami pacjenci trafili do zbioru testowego co poprzednio. Inaczej porównywałbyś dwie rzeczy naraz - wpływ identyfikatora i wpływ innego losowania.

Krok 5: ranking ważności

waznosc_zle = pd.Series(drzewo_zle.feature_importances_, index=X_zle.columns).sort_values(ascending=False)
print(waznosc_zle.to_string())

Wypisujesz cały ranking, nie head(3) - chodzi o to, żeby zobaczyć, gdzie dokładnie znalazł się PatientID.

Gotowe rozwiązanie i czego się spodziewać
X_zle = dane.drop(columns=['Diabetic'])   # zostawiamy PatientID jako "cechę"

X_zle_ucz, X_zle_test, y_zle_ucz, y_zle_test = train_test_split(
    X_zle, y, test_size=0.2, stratify=y, random_state=42
)

drzewo_zle = DecisionTreeClassifier(random_state=42)
drzewo_zle.fit(X_zle_ucz, y_zle_ucz)

print("Z identyfikatorem PatientID:")
print(f"  uczący:  {drzewo_zle.score(X_zle_ucz, y_zle_ucz):.1%}")
print(f"  testowy: {drzewo_zle.score(X_zle_test, y_zle_test):.1%}")
print()
print("Bez identyfikatora (zadanie 3):")
print(f"  uczący:  {wynik_ucz:.1%}")
print(f"  testowy: {wynik_test:.1%}")
print()
waznosc_zle = pd.Series(drzewo_zle.feature_importances_, index=X_zle.columns).sort_values(ascending=False)
print("Ważność cech - na którym miejscu wylądował PatientID?")
print(waznosc_zle.to_string())

Czego się spodziewać:

Z identyfikatorem PatientID:
  uczący:  100.0%
  testowy: 89.2%

Bez identyfikatora (zadanie 3):
  uczący:  100.0%
  testowy: 89.0%

Ważność cech - na którym miejscu wylądował PatientID?
Pregnancies               0.393510
BMI                       0.192674
Age                       0.126940
SerumInsulin              0.094342
PlasmaGlucose             0.072622
TricepsThickness          0.033841
PatientID                 0.030420
DiastolicBloodPressure    0.029382
DiabetesPedigree          0.026269

Jak to czytać - uwaga, wynik jest inny, niż podpowiada intuicja:

Model z identyfikatorem wypadł odrobinę lepiej: 89,2% wobec 89,0%. Jeśli spodziewałeś się katastrofy, to jej nie ma. I właśnie to jest tu lekcją.

Dwie rzeczy, które trzeba z tego wyciągnąć:

1. PatientID dostał ważność 0,030 - czyli model go użył. Nie zignorował go. Trzy procent decyzji tego drzewa opiera się na numerze z rejestracji, który nie ma nic wspólnego z cukrzycą. Wyprzedził nawet DiastolicBloodPressure, czyli prawdziwy pomiar medyczny.

2. Różnica 0,2 punktu procentowego to szum, nie poprawa. Przy 2000 pacjentów w zbiorze testowym to cztery osoby. Gdybyś zmienił random_state, równie dobrze mogłoby wyjść o 0,2 punktu gorzej.

Odpowiedź na pytanie o pokusę „wrzućmy wszystkie kolumny, model sobie poradzi":

Tutaj akurat poradził sobie prawie tak samo - ale to szczęście, nie zasada. Identyfikatory w prawdziwych zbiorach często są nadawane w kolejności zgłoszeń, a ta bywa skorelowana z czymś istotnym: pacjenci z jednego szpitala dostają numery z jednego zakresu, a pacjenci badani później mają inne rozpoznania. Wtedy model uczy się numeru zamiast medycyny i zawodzi dokładnie w momencie, gdy dostanie pacjenta z nowej puli numerów.

Nie wolno tego sprawdzać eksperymentem. Kolumny, które nie są pomiarem, usuwa się dlatego, że wiadomo, czym są - a nie dlatego, że akurat zaszkodziły w tym jednym uruchomieniu.

In [ ]:
# TWÓJ KOD TUTAJ

Zadanie 7 (trudniejsze): Ile kosztuje brak stratyfikacji?

Sprawdź empirycznie, czy stratify=y ma znaczenie.

  1. Wykonaj podział danych bez stratify dla 10 różnych wartości random_state (np. od 0 do 9).
  2. Za każdym razem wytrenuj regresję logistyczną i zanotuj skuteczność testową.
  3. Powtórz to samo ze stratify=y.
  4. Porównaj rozrzut wyników (np. np.std(...)) w obu przypadkach.

Pytanie, na które odpowiadasz: czy stratyfikacja sprawia, że wyniki są stabilniejsze? Zastanów się też, czy przy 10 000 wierszy efekt jest równie wyraźny, jak byłby przy 200 wierszach.

Krok po kroku - co masz zrobić (kliknij, żeby rozwinąć)
  1. Napisz funkcję, która przyjmuje informację, czy stosować stratyfikację.
  2. W środku, dla dziesięciu wartości random_state od 0 do 9: podziel dane, wytrenuj model, zanotuj skuteczność testową.
  3. Zwróć zebrane wyniki jako tablicę numpy.
  4. Wywołaj funkcję dwa razy - z stratyfikacją i bez.
  5. Porównaj średnią, odchylenie standardowe i rozstęp obu serii.

Co robi stratify=y: pilnuje, żeby proporcja chorych do zdrowych była taka sama w zbiorze uczącym i testowym, jak w całych danych. Bez tego podział jest czysto losowy i proporcje mogą się rozjechać.

Dlaczego dziesięć razy, a nie raz: jedno uruchomienie nie odpowie na pytanie o stabilność. Pytasz nie o to, który podział jest lepszy, tylko o to, jak bardzo wynik skacze w zależności od losu.

Jak to napisać - kod z wyjaśnieniem

Krok 1-3: funkcja

def sprawdz(stratyfikacja):
    wyniki = []
    for ziarno in range(10):
        Xu, Xt, yu, yt = train_test_split(
            X, y, test_size=0.2,
            stratify=y if stratyfikacja else None,
            random_state=ziarno,
        )
        m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000, random_state=42))
        m.fit(Xu, yu)
        wyniki.append(m.score(Xt, yt))
    return np.array(wyniki)
  • stratify=y if stratyfikacja else None to wyrażenie warunkowe: wstawia y, gdy chcemy stratyfikacji, i None, gdy nie. Dzięki temu jedna funkcja obsługuje oba warianty.
  • random_state=ziarno zmienia się co obrót - i o to właśnie chodzi. To jedyne źródło różnic między przebiegami.
  • random_state=42 w modelu zostaje stałe. Losowość ma pochodzić wyłącznie z podziału danych, inaczej nie wiedziałbyś, co mierzysz.
  • np.array(...) zamienia listę na tablicę, na której od razu działają .mean(), .std(), .min().

Krok 4-5: porównanie

bez = sprawdz(False)
ze = sprawdz(True)

print(f"BEZ stratyfikacji: średnia {bez.mean():.4f} | odchylenie std {bez.std():.4f} | rozstęp {bez.max() - bez.min():.4f}")

Rozstęp (różnica między najlepszym a najgorszym wynikiem) jest tu czytelniejszy od odchylenia standardowego, bo mówi wprost: „między najlepszym a najgorszym losem jest tyle a tyle".

Gotowe rozwiązanie i czego się spodziewać
def sprawdz(stratyfikacja):
    wyniki = []
    for ziarno in range(10):
        Xu, Xt, yu, yt = train_test_split(
            X, y, test_size=0.2,
            stratify=y if stratyfikacja else None,
            random_state=ziarno,
        )
        m = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000, random_state=42))
        m.fit(Xu, yu)
        wyniki.append(m.score(Xt, yt))
    return np.array(wyniki)

bez = sprawdz(False)
ze = sprawdz(True)

print(f"BEZ stratyfikacji: średnia {bez.mean():.4f} | odchylenie std {bez.std():.4f} | rozstęp {bez.max() - bez.min():.4f}")
print(f"ZE stratyfikacją:  średnia {ze.mean():.4f} | odchylenie std {ze.std():.4f} | rozstęp {ze.max() - ze.min():.4f}")

Czego się spodziewać:

BEZ stratyfikacji: średnia 0.7811 | odchylenie std 0.0092 | rozstęp 0.0380
ZE stratyfikacją:  średnia 0.7855 | odchylenie std 0.0046 | rozstęp 0.0135

Jak to czytać:

  • Średnie są prawie identyczne (0,7811 wobec 0,7855). Stratyfikacja nie sprawia, że model jest lepszy - i nie ma takiego zadania.
  • Odchylenie standardowe jest dwa razy mniejsze ze stratyfikacją: 0,0046 wobec 0,0092.
  • Rozstęp jest niemal trzy razy mniejszy: 1,35 punktu procentowego wobec 3,80.

Ostatnia liczba jest najważniejsza w praktyce. Bez stratyfikacji sama zmiana random_state przesuwa wynik o prawie cztery punkty procentowe. Gdybyś porównywał dwa modele różniące się o dwa punkty, nie wiedziałbyś, czy patrzysz na różnicę między modelami, czy na różnicę między losami.

Odpowiedź na drugie pytanie z zadania - czy przy 200 wierszach efekt byłby równie wyraźny? Byłby znacznie silniejszy. Przy 10 000 wierszy losowy podział i tak trafia w proporcje blisko prawdziwych, bo działa prawo wielkich liczb. Przy 200 wierszach zbiór testowy liczy 40 osób i łatwo o podział, w którym chorych jest 25% albo 45% zamiast 33%.

To dlatego w ćwiczeniu 09, gdzie zbiór ma niecałe dwieście wierszy, stratyfikacja przestaje być kosmetyką.

In [ ]:
# TWÓJ KOD TUTAJ

Pytania do przemyślenia

Na te pytania odpowiadasz słowami, nie kodem. Jeśli potrafisz odpowiedzieć na wszystkie, ćwiczenie spełniło swoje zadanie.

  1. Model osiąga 97% skuteczności. Czy to dobry wynik? Od czego zależy odpowiedź?
  2. Dlaczego drzewo decyzyjne nie wymaga skalowania cech, a regresja logistyczna na skalowaniu zyskuje?
  3. W zadaniu 3 drzewo osiągnęło na danych uczących wynik bliski 100%. Dlaczego nie jest to powód do radości?
  4. Model odniesienia nie wykrył ani jednego chorego pacjenta, a ma 66,6% skuteczności. Jakiej miary użyłbyś zamiast skuteczności, żeby ten problem było widać?
  5. Dlaczego ustawiamy random_state? Co byłoby nie tak z porównywaniem dwóch modeli bez ustalonego ziarna losowości?
  6. Poza PatientID - jakie inne rodzaje kolumn należałoby usunąć z danych medycznych przed trenowaniem modelu? Pomyśl o dacie przyjęcia albo numerze oddziału.

Chcesz wiedzieć więcej

W kolejnym ćwiczeniu (02 - Poznaj swoje dane) cofniemy się o krok: zanim cokolwiek wytrenujemy, nauczysz się badać dane - rozkłady, korelacje, wartości odstające i podejrzane zera. Bo w praktyce to tam kryje się większość problemów, a nie w wyborze algorytmu.

In [ ]: