Od „chciałbym coś z AI” do pierwszego konkretnego problemu
Programista Python wraca z poniedziałkowego stand-upu z jednym krótkim zdaniem w głowie: „Zróbmy coś z AI, klienci o to pytają”. Repo jest, dane gdzieś „na pewno mamy”, ale brakuje tego najważniejszego – konkretu, co właściwie model ma zrobić. Zaczyna się szukanie tutoriali, a kończy na dziesiątym otwartym artykule o sieciach neuronowych, które średnio pasują do faktycznego problemu firmy.
Pierwszy krok w uczeniu maszynowym nie ma nic wspólnego z doborem algorytmu. Zaczyna się od przeformułowania ogólnego „AI” na możliwie precyzyjne zadanie: jakie są dane wejściowe, jaki efekt ma być na wyjściu i w jakiej postaci ma z nich korzystać aplikacja lub zespół. Dla programisty myślącego funkcjami to naturalne – uczenie maszynowe można potraktować jak funkcję, której parametry zamiast ręcznego strojenia if-ami i heurystykami ustalane są automatycznie na podstawie danych.
Różnica między klasyczną funkcją a modelem ML jest taka, że w tej drugiej nie piszesz ręcznie całej logiki. W klasycznym kodzie robisz np.:
- jeśli user nie logował się 30 dni → wyślij przypomnienie,
- jeśli nie otworzył 5 ostatnich maili → ogranicz częstotliwość kampanii,
- jeśli kraj = X i kanał = Y → użyj wariantu Z.
W modelu ML przekazujesz dane wejściowe (np. aktywność użytkownika z ostatnich tygodni, kraj, urządzenie, poprzednie zakupy), dodajesz informację, co się stało (czy odszedł, czy kupił, czy kliknął) i pozwalasz algorytmowi znaleźć własne wzorce. Funkcja nadal istnieje – ma sygnaturę zbliżoną do predict(x) → y – ale zamiast logiki warunkowej używa wewnętrznie wyuczonych parametrów.
Kiedy uczenie maszynowe ma sens, a kiedy wystarczy zwykły skrypt
Uczenie maszynowe jest przydatne, gdy:
- masz dużo przykładów z przeszłości (logi, transakcje, eventy),
- potrafisz oznaczyć, które z nich są „dobre”, a które „złe” (np. zakup / brak zakupu),
- logika zależy od wielu zmiennych jednocześnie i trudno ją opisać kilkoma prostymi regułami,
- ważna jest jakość prognozy, a nie stuprocentowa interpretowalność każdej decyzji.
Zwykły skrypt i reguły if/else nadal wygrywają, gdy:
- jest jasna, twarda logika biznesowa (np. przepisy podatkowe, kontrola uprawnień),
- danych jest mało lub są kompletnie niereprezentatywne,
- każdą decyzję trzeba szczegółowo uzasadnić prostymi regułami, a nie statystyką.
Warto też odróżnić główne typy zadań ML, bo to pomaga od razu wykluczyć pewne kierunki:
- Klasyfikacja – przewidujesz jedną z kilku klas (spam/nie spam, odejdzie/nie odejdzie, typ produktu). Wyjście jest dyskretne.
- Regresja – przewidujesz wartość ciągłą (cena mieszkania, liczba kliknięć, czas trwania sesji). Wyjście jest liczbą.
- Klasteryzacja – grupujesz podobne obiekty bez etykiet (segmentacja klientów, grupowanie artykułów).
Jeśli biznesowy problem da się osadzić w jednym z tych typów, jesteś blisko użytecznego projektu.
Jak dobrze postawić problem uczenia maszynowego
Przykład: SaaS z abonamentem. Zespół czuje, że „sporo klientów odchodzi, przydałoby się coś z AI”. Zamiast ogólnego „przewiduj rezygnacje” warto postawić problem precyzyjnie:
- Wejście: ostatnie 90 dni zachowania użytkownika – liczba logowań, ilość wykonanych akcji kluczowych, typ planu, kraj, urządzenie, czy kontaktował się z supportem, historia płatności.
- Wyjście: czy użytkownik zakończył subskrypcję w ciągu następnych 30 dni (1 – tak, 0 – nie).
- Cel: poprawa skuteczności kampanii retencyjnych – chcemy listy użytkowników o najwyższym ryzyku rezygnacji.
Tak zdefiniowany problem to klasyczna binarny klasyfikacja. Od razu wiadomo:
- jakie dane trzeba wyciągnąć z bazy,
- czym będą etykiety uczące (czy faktycznie zrezygnował),
- jak sprawdzić skuteczność (np. precision, recall dla klasy „odejdzie”).
Po kilku takich ćwiczeniach łatwo zauważyć, że najważniejsze jest zdanie w stylu: „Na podstawie X spróbuj przewidzieć Y w horyzoncie Z”. Dopiero do tego dobiera się konkretny algorytm i całą resztę.
Lepsze pytanie niż lepszy algorytm
Typowy błąd na starcie: przeskakiwanie od razu do wyboru biblioteki i modelu („random forest czy XGBoost?”) zamiast przejścia przez definicję problemu, danych wejściowych i metryki jakości. Z biznesowego punktu widzenia lepsze pytanie w rodzaju „jak zmniejszyć liczbę fałszywych alarmów o fraudach o połowę” często przyniesie więcej korzyści niż przesiadka z jednego algorytmu na inny.
W praktyce czas inwestowany w doprecyzowanie problemu procentuje przy każdym kolejnym kroku: łatwiej dobrać dane, łatwiej wybrać metrykę, łatwiej wytłumaczyć wyniki zespołowi produktowemu. Algorytm jest ważny, ale rzadko jest „magicznie” decydujący na samym początku drogi.
Niezbędne fundamenty: matematyka, Python i myślenie w kategoriach danych
Większość programistów, którzy odwlekają wejście w uczenie maszynowe, ma z tyłu głowy ten sam lęk: „za słaby jestem z matematyki”. W praktyce do pierwszych modeli produkcyjnych potrzebna jest raczej umiejętność logicznego myślenia o danych niż biegła znajomość twierdzeń i dowodów.
Jaki poziom matematyki naprawdę wystarczy na początek
Na start przydają się trzy obszary:
- Algebra liniowa – intuicyjne rozumienie wektorów i macierzy (dane to po prostu wektory cech), dodawanie, mnożenie macierzy, pojęcie wymiaru. Bez formalnych dowodów, bardziej „co to robi” niż „dlaczego to działa”.
- Statystyka i prawdopodobieństwo – rozkład, średnia, mediana, wariancja, odchylenie standardowe, prawdopodobieństwo warunkowe. To podstawa zrozumienia metryk, walidacji, błędów.
- Funkcje i logarytmy – po co logarytm w regresji logistycznej, dlaczego funkcja sigmoidalna, co oznacza „skala logarytmiczna”.
Jeśli umiesz policzyć średnią, rozumiesz, czym jest „rozrzut danych”, potrafisz z grubsza odczytać histogram i wiesz, że 0.8 dokładności to „80 na 100 trafionych przypadków”, masz potrzebne minimum. Do głębszej teorii można wracać stopniowo, kiedy faktycznie pojawia się potrzeba.
Elementy Pythona, które faktycznie są kluczowe
Uczenie maszynowe w Pythonie nie wymaga znajomości każdego zakamarka języka, ale są obszary, bez których praca będzie męcząca:
- Praca z listami i słownikami – list comprehensions, filtrowanie, sortowanie, zagnieżdżone struktury.
- Funkcje – pisanie własnych transformacji, prostych funkcji pomocniczych, czysta sygnatura (wejście/wyjście).
- Moduły i pakiety – dzielenie kodu na pliki, import, organizacja w foldery.
- Praca interaktywna – REPL lub Jupyter Notebook, szybkie eksperymenty na małych fragmentach kodu.
W ML stale trzeba coś testować „na szybko”: fragment transformacji, działanie metryki, mały kawałek Pandas. Środowisko interaktywne (np. Jupyter) przyspiesza to dramatycznie w porównaniu do klasycznego cyklu „zapisz plik → uruchom → print-debug”.
NumPy i Pandas – fundamenty, bez których trudno ruszyć
NumPy i Pandas to kręgosłup ekosystemu data science w Pythonie. NumPy daje wydajne tablice numeryczne (np. wektory cech), a Pandas dostarcza tabelaryczne dane (DataFrame), które można łatwo filtrować, agregować, łączyć.
Krótki, praktyczny przykład w Pandas – załóżmy, że masz DataFrame z kolumną age i chcesz policzyć średni wiek oraz zbudować wystandaryzowaną wersję tej kolumny:
import pandas as pd
df = pd.DataFrame({"age": [20, 25, 30, 35, 40]})
mean_age = df["age"].mean()
std_age = df["age"].std()
df["age_std"] = (df["age"] - mean_age) / std_age
print(df)
W kilku linijkach liczysz statystyki i przygotowujesz cechę w postaci, którą modele liniowe „lubią” dużo bardziej. Takie operacje są w ML codziennością – bez Pandas byłyby znacznie bardziej żmudne.
Gdy zaczniesz łączyć dane z kilku źródeł, usuwać duplikaty, filtrować po warunkach i budować cechy na podstawie istniejących kolumn, znajomość Pandas działa jak mnożnik produktywności. Debugowanie dziwnych efektów w modelu często sprowadza się do pytania: „czy dobrze przygotowałem dane?”, a to zwykle widać w paru prostych wywołaniach Pandas.
Solidne podstawy skracają czas debugowania
Na początku drogi większość problemów to nie „błędy algorytmu”, tylko:
- źle wczytane dane (string zamiast liczby),
- niezrozumiałe braki wartości,
- zły podział na zbiory,
- mylenie metryk (accuracy vs precision/recall).
Im lepiej rozumiesz statystykę opisową i strukturę danych w Pandas, tym szybciej wyłapujesz takie błędy. Nawet prosty nawyk sprawdzania df.info(), df.describe() i kilku wykresów potrafi oszczędzić godziny walki z „dziwnym modelem, który nic nie widzi”.
Dlatego rozsądnie jest zainwestować kilka wieczorów w spokojne przećwiczenie NumPy/Pandas oraz podstaw statystyki na małych datasetach, zanim ruszysz do cięższej artylerii typu sieci neuronowe.
Środowisko pracy programisty ML: narzędzia, setup, higiena projektu
Pierwszy projekt ML często zaczyna się od „wrzućmy wszystko do jednego notebooka, najwyżej potem się ogarnie”. Po tygodniu masz 800 linii kodu z nazwami komórek „In [92]”, parę modeli, kilka magicznych ścieżek do plików i brak powtarzalności eksperymentów. W pracy zespołowej taki styl szybko mści się chaosem.
Organizacja środowiska: pyenv/conda, wirtualne środowiska, zależności
Solidny start to osobne środowisko virtualenv lub conda na każdy większy projekt. Pozwala to:
- izolować wersje bibliotek (np. scikit-learn, pandas, numpy),
- uniknąć konfliktów między projektami,
- łatwo odtworzyć setup na innym komputerze lub serwerze.
Prosty schemat pracy:
- instalacja conda lub pyenv (w zależności od preferencji),
- utworzenie środowiska: np. conda create -n ml-env python=3.11,
- aktywacja: conda activate ml-env,
- instalacja biblioteki: pip install numpy pandas scikit-learn matplotlib seaborn jupyter.
Na początku warto też od razu wprowadzić requirements.txt lub pyproject.toml (jeśli korzystasz z Poetry), żeby inni mogli odtworzyć środowisko jednym poleceniem. Dla prostego projektu wystarczy:
pip freeze > requirements.txt
i potem na innym stanowisku:
pip install -r requirements.txt
Jupyter Notebook czy zwykłe moduły? Kiedy co wybrać
Jupyter Notebook doskonale nadaje się do:
- eksploracji danych,
- tworzenia pierwszych wykresów,
- szybkiego sprawdzania hipotez,
- demonstracji wyników innym osobom (PM, analitycy).
Ma jednak swoje wady: kod łatwo się rozrasta, trudniej zachować czystość zależności, łatwo o „zapomniane” komórki, które zmieniały stan środowiska. Przy wdrażaniu i powtarzalnych pipeline’ach lepiej korzystać z czystych modułów Pythona w katalogu src/, gdzie kod ma postać funkcji/klas, testów i skryptów CLI.
Rozsądny kompromis na start:
- do eksploracji danych i pierwszych prób modelu – notebooki w folderze notebooks/,
- do produkcyjnego pipeline’u i powtarzalnych kroków – moduły w folderze src/, uruchamiane skryptami.
Ważne, żeby nie trzymać całego życia projektu w jednym „notebook_final_final2.ipynb”. Fajny nawyk: gdy coś w notebooku zaczyna być „często używane” – przenieść to do modułu Pythona i importować.
Struktura repozytorium i „higiena” kodu
Wyobraź sobie, że po miesiącu wracasz do swojego projektu, a tam: dane w katalogu domowym, skrypty porozrzucane po pulpicie, trzy wersje tego samego modelu o nazwach model_new.py, model_new2.py, model_final.py. Niby wszystko działało, ale nie wiesz już, który plik jest „tym właściwym”. To typowy moment, w którym prosta struktura repo nagle okazuje się bezcenna.
Przykładowy układ katalogów, który nie boli w utrzymaniu:
data/– surowe dane (raw/), dane przetworzone (processed/),notebooks/– eksperymenty, eksploracja,src/– moduły z kodem: przygotowanie danych, definicje modeli, funkcje pomocnicze,models/– zapisane modele, np. w formacie.pkl,reports/– wykresy, podsumowania wyników,tests/– proste testy (choćby szczątkowe) do kluczowych funkcji.
Taki porządek nie jest sztuką dla sztuki. Gdy trzeba coś zmienić w preprocessingu albo odtworzyć konkretny eksperyment, nie szukasz po całym dysku, tylko od razu wiesz, gdzie zajrzeć. Dodatkowo inni (lub ty za pół roku) widzą z marszu, jak projekt jest zorganizowany.
W codziennej pracy pomaga też kilka drobiazgów: spójne nazewnictwo (bez tmp1.py), krótkie opisy w README, prosty skrypt startowy (np. python -m src.train), który uruchamia główny pipeline. To małe rzeczy, ale one decydują, czy projekt „niesie się” dalej, czy zaczyna tonąć w chaosie już po kilku sprintach.
Jeśli pierwsze eksperymenty robisz świadomie: z podstawami matematyki, sensownym korzystaniem z Pandas i choćby minimalnym porządkiem w środowisku, uczenie maszynowe przestaje być czarną skrzynką. Zostaje rzemiosło: systematyczne próby, lepsze dane, czytelny kod – i model, który da się naprawdę wykorzystać, zamiast tylko pokazać na jednym slajdzie.
Jeśli chcesz pogłębić temat i zobaczyć więcej przykładów z tej niszy, zajrzyj na PGMYS.
Pierwsze dane: skąd je wziąć i jak je “oswoić” w Pandas
Programista siada do „pierwszego projektu ML” i po godzinie klepania kodu okazuje się, że największym problemem nie jest model, tylko… brak sensownych danych. Albo są, ale w pięciu różnych Excelach, z różnymi separatorami i nazwami kolumn. Wtedy szybko wychodzi, że umiejętność zdobywania i porządkowania danych jest cenniejsza niż znajomość trzech egzotycznych algorytmów.
Skąd wziąć dane na start: gotowe zbiory vs dane z pracy
Na początek najrozsądniejsze są gotowe, publiczne zbiory danych. Mają tę zaletę, że:
- są już w miarę czyste (choć nie idealne),
- łatwo znaleźć do nich tutoriale i przykładowe rozwiązania,
- nie dotykasz RODO, NDA i innych miłych tematów prawnych.
Kilka sprawdzonych źródeł:
- scikit-learn – wbudowane datasety:
load_iris,load_breast_cancer,fetch_california_housing. Idealne do przerobienia pełnego pipeline’u bez walki z formatami. - Kaggle – klasyczne konkursy typu Titanic, House Prices, a także sporo prawdziwych danych biznesowych (sprzedaż, logi, dane tekstowe). Do nauki wystarczy darmowe konto.
- UCI Machine Learning Repository – starszy, ale nadal użyteczny zbiór datasetów o różnej skali trudności.
Jeśli masz dostęp do danych w pracy (np. logi aplikacji, dane sprzedażowe, tikety z supportu), to świetnie – ale na początek lepiej zbudować sobie „mięśnie” na datasetach publicznych. Dopiero gdy czujesz się pewnie z pipeline’em, wejście w brudne dane firmowe ma sens.
Wczytywanie danych w Pandas: CSV, Excel, SQL
Standardowy scenariusz: dostajesz plik .csv albo .xlsx i chcesz go wciągnąć do DataFrame. W Pandas sprowadza się to do jednego-dwóch wywołań, ale diabeł tkwi w szczegółach.
import pandas as pd
# CSV
df = pd.read_csv("data/raw/customers.csv")
# Excel (pierwszy arkusz)
df_xls = pd.read_excel("data/raw/sales.xlsx")
# SQL (np. PostgreSQL przez sqlalchemy)
from sqlalchemy import create_engine
engine = create_engine("postgresql+psycopg2://user:pass@host:5432/dbname")
df_sql = pd.read_sql("SELECT * FROM orders LIMIT 10000", con=engine)
Po wczytaniu pierwsze co robisz to krótki przegląd struktury:
print(df.head())
print(df.info())
print(df.describe(include="all"))
To trzy komendy, które potrafią zaoszczędzić masę czasu. Od razu widać, czy kolumny liczbowe nie wczytały się jako object, czy nie ma dziwnych wartości typu „-” albo „brak”.
Typowe pułapki przy wczytywaniu danych
Przy pierwszych projektach kilka błędów powtarza się niemal zawsze. Dobrze je mieć z tyłu głowy.
- Zły separator w CSV – w Europie często używany jest
;zamiast,. Wtedy:
df = pd.read_csv("data.csv", sep=";")
- Inne kodowanie znaków – polskie znaki potrafią się posypać. Często pomaga:
df = pd.read_csv("data.csv", encoding="utf-8") # lub "latin-1", "cp1250"
- Liczby jako tekst – gdy w kolumnie oprócz liczb pojawi się choć jeden „dziwny” wpis (np. „-”, „brak”), Pandas zrzuci wszystko do typu
object. Szybki ratunek:
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
Zwróć uwagę na parametr errors="coerce" – wszystko, czego nie da się zrzutować, stanie się NaN. Potem można to ogarnąć w procesie czyszczenia.
Pierwsze „oswajanie” danych: filtrowanie, selekcja, proste agregacje
Po wczytaniu danych dobrze jest zrobić kilka prostych operacji, żeby nabrać „czucia” do datasetu. Nie trzeba od razu budować wykresów – wystarczy kilka linii Pandas.
# wybór kilku kolumn
df_small = df[["customer_id", "age", "country", "churned"]]
# filtrowanie po warunku
df_pl = df_small[df_small["country"] == "Poland"]
# prosta agregacja: średni wiek klientów wg kraju
age_by_country = (
df_small
.groupby("country")["age"]
.mean()
.sort_values(ascending=False)
)
print(age_by_country.head())
Takie operacje są kluczowe przed pierwszym modelem – widzisz rozkłady, liczby, zależności na poziomie „intuicyjnym”. Gdy coś jest bardzo nielogiczne (np. średni wiek klientów to 300 lat), wiesz, że pipeline będzie się sypał zanim zaczniesz trenować model.
Sample, czyli pracuj na wycinku, nie na całości
Przy większych datasetach rozsądniej jest zacząć od wycinka. Łatwiej debugować, szybciej się liczy, a błędy i tak wychodzą już na małej próbce.
# losowe 10 000 wierszy (bez zamieszania w oryginalnym DataFrame)
df_sample = df.sample(n=10_000, random_state=42)
print(df_sample.shape)
Gdy pipeline działa na próbce, zwykle wystarczy podmienić df_sample na pełne df i sprawdzić wydajność (czas/ram). Dzięki temu nie czekasz pięciu minut przy każdej drobnej poprawce.
Podstawowy przepływ pracy w projekcie ML – od surowych danych do pierwszego modelu
Wyobraź sobie, że dostajesz zadanie: „Przewidzieć, czy klient odejdzie w ciągu 3 miesięcy”. Masz tabelę z historią klientów i bardzo ogólne oczekiwanie, że „sztuczna inteligencja coś z tym zrobi”. Jeśli nie rozbijesz tego na kroki, utkniesz między 50 zakładkami tutoriali.
Definiowanie problemu: co dokładnie model ma przewidywać
Zanim otworzysz Jupytera, odpowiadasz na kilka prostych, ale kluczowych pytań:
- Jaka jest jednostka predykcji? Klient? Transakcja? Sesja w aplikacji?
- Jaki jest target (y)? Flaga 0/1 (klasyfikacja), wartość ciągła (regresja), etykieta z kilku klas?
- Na jakim momencie w czasie „zamrażasz” dane wejściowe? To jest szczególnie ważne przy szeregach czasowych – nie możesz dać modelowi informacji z przyszłości.
Przykład: dla „churnu” jednostką jest klient, targetem – zmienna churned (0/1), a featury to informacje z określonego okresu, np. ostatnich 3 miesięcy. Taki opis od razu ustawia, jakie dane są potrzebne i jak wygląda DataFrame wejściowy.
Podział na zbiory: treningowy, walidacyjny, testowy
Klasyczny błąd na początku to ocena modelu na tych samych danych, na których się uczył. Wynik wygląda świetnie, dopóki nie spróbujesz użyć modelu „na żywo”. Dlatego standardem jest podział na kilka zbiorów:
- train – do uczenia modelu,
- validation – do strojenia hiperparametrów, wyboru cech, porównywania modeli,
- test – do końcowej, uczciwej oceny po wszystkich decyzjach.
W najprostszym przypadku wystarczy na start train/test, z czasem dojdzie walidacja lub cross-validation. W scikit-learn robisz to jednym wywołaniem:
from sklearn.model_selection import train_test_split
X = df.drop(columns=["churned"])
y = df["churned"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
Parametr stratify jest ważny przy klasyfikacji – zachowuje podobny rozkład klas w obu zbiorach. Bez tego łatwo o sytuację, w której w testowym zbiorze masz zupełnie inny stosunek klas niż w treningowym i metryki zaczynają dziwnie skakać.
Wybór pierwszego modelu: prosto i interpretowalnie
Na start kuszące są sieci neuronowe i fancy architektury, ale w praktyce dużo szybciej dojdziesz do wniosków z prostymi modelami: regresją logistyczną, drzewami decyzyjnymi, prostym Random Forestem.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
acc = accuracy_score(y_test, y_pred)
print("Accuracy:", acc)
To jest „pierwszy, naiwny baseline”. Jego rola nie polega na tym, żeby wygrać Kaggle, ale żebyś miał punkt odniesienia. Każda kolejna zmiana w danych, cechach, hiperparametrach powinna być porównywana właśnie z tym baseline’em.
Iteracyjna pętla: dane → model → metryki → poprawki
Praca nad projektem ML przypomina pętlę, a nie liniowy proces od A do Z. W prostym ujęciu:
- Przygotowujesz dane wejściowe (feature engineering, czyszczenie).
- Trenujesz prosty model.
- Mierzysz metryki na zbiorze walidacyjnym/testowym.
- Analizujesz błędy (gdzie model się myli, na jakich grupach użytkowników).
- Wracasz do kroku 1 z nowymi hipotezami: inne cechy, inne przetwarzanie, może inny model.
Jeśli trzymasz tę pętlę krótką i powtarzalną (np. jeden skrypt, który wczytuje dane, robi preprocessing i trenuje model), rozwijasz projekt znacznie szybciej niż ktoś, kto każde podejście robi „ręcznie” w notebooku bez porządku.
Logowanie eksperymentów: minimum dyscypliny
Przy trzecim modelu pamiętasz jeszcze, co i jak robiłeś. Przy dziesiątym wszystko zaczyna się mieszać. Nie trzeba od razu stawiać MLflow czy Weight&Biases, ale proste logowanie eksperymentów pomaga bardziej niż się wydaje.
Na początek wystarczy:
- jeden plik
experiments.mdalboexperiments.csv, - dla każdego podejścia zapisujesz: datę, nazwę eksperymentu, główne parametry (np. model, zestaw cech, główne modyfikacje) oraz najważniejsze metryki.
Przykład mini-logu w CSV:
experiment_id,date,model,features,notes,metric,metric_value
1,2024-04-01,LogReg,basic,"baseline",roc_auc,0.71
2,2024-04-02,RandomForest,basic+usage,"more trees",roc_auc,0.79
Po kilku tygodniach taki plik to złoto – wiesz, co już sprawdzałeś, co działa, a co było ślepą uliczką.
Przygotowanie danych krok po kroku: czyszczenie, kodowanie, skalowanie
W pewnym momencie każdy dochodzi do ściany: „model nie działa”, a parametry zmieniane na oślep niewiele dają. Zwykle przyczyną są dane – brudne, źle zakodowane, w różnych skalach. Zadbany preprocessing potrafi poprawić wynik bardziej niż wymiana modelu na „coś bardziej zaawansowanego”.
Czyszczenie danych: wartości brakujące, duplikaty, outliery
Na początek trzeba zobaczyć, co w ogóle jest w środku, szczególnie jeśli dane pochodzą z „prawdziwego świata”, a nie z tutoriala.
# ile braków na kolumnę
print(df.isna().sum())
# czy są duplikaty wierszy
print("Duplikaty:", df.duplicated().sum())
Typowe kroki czyszczenia:
- Usuwanie duplikatów – jeśli wiesz, że każdy wiersz powinien być unikalny (np. po
customer_id):
df = df.drop_duplicates()
- Uzupełnianie braków – strategia zależy od typu zmiennej i jej znaczenia:
# liczby: średnia, mediana lub specjalna wartość
df["age"] = df["age"].fillna(df["age"].median())
# kategorie: najczęstsza wartość lub osobna kategoria "Unknown"
df["city"] = df["city"].fillna("Unknown")
W projektach produkcyjnych uzupełnianie braków robi się zwykle w pipeline’ach scikit-learn (np. SimpleImputer), żeby te same reguły działały na train i test. Na etapie nauki możesz zrobić to ręcznie w Pandas, ważne, żeby rozumieć, co i dlaczego się dzieje.
Porządkowanie typów: daty, kategorie, liczby
Wiele problemów z feature engineeringiem bierze się z tego, że Pandas widzi wszystko jako object. Prostym krokiem można to naprawić.
# konwersja daty
df["signup_date"] = pd.to_datetime(df["signup_date"], errors="coerce")
# kolumny kategoryczne
cat_cols = ["country", "device_type"]
for col in cat_cols:
df[col] = df[col].astype("category")
Gdy daty są prawdziwymi datami, łatwo z nich wyciągnąć przydatne cechy:
df["signup_year"] = df["signup_date"].dt.year
df["signup_month"] = df["signup_date"].dt.month
df["signup_dayofweek"] = df["signup_date"].dt.dayofweek
Dla modeli liniowych często przydają się takie „rozpakowane” cechy zamiast surowej daty.
Ktoś wrzuca do modelu surowe daty i dziwi się, że nic z nich nie korzysta. Modele drzewiaste jeszcze jakoś sobie poradzą, ale regresja logistyczna potraktuje taką kolumnę jak losową liczbę z kosmosu. Kilka prostych transformacji potrafi zamienić „nagą” datę w zestaw sygnałów, które faktycznie pomagają w przewidywaniu.
Oprócz roku, miesiąca czy dnia tygodnia przydają się też cechy opisujące odległość w czasie. Zamiast podawać surową datę rejestracji, liczysz np. „ile dni od rejestracji do dziś” albo „ile dni od ostatniej aktywności do momentu predykcji”. W churnie takie „time since last activity” bywa jedną z najmocniejszych zmiennych, bo bezpośrednio mierzy „ochładzanie się” relacji z użytkownikiem.
Kodowanie zmiennych kategorycznych
Typowy projekt biznesowy ma ich pełno: kraj, typ urządzenia, kanał pozyskania, plan taryfowy. Dopóki nie zamienisz ich na liczby, większość modeli nie będzie mogła ich wykorzystać. Sposób kodowania zależy od tego, jaki model chcesz trenować i jak dużo kategorii masz w kolumnie.
Dla niewielkiej liczby kategorii dobrym startem jest one-hot encoding:
from sklearn.preprocessing import OneHotEncoder
cat_cols = ["country", "device_type"]
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
X_cat = ohe.fit_transform(df[cat_cols])
# nazwy nowych kolumn
ohe_cols = ohe.get_feature_names_out(cat_cols)
X_cat_df = pd.DataFrame(X_cat, columns=ohe_cols, index=df.index)
# łączymy z cechami numerycznymi
num_cols = ["age", "sessions_last_30d"]
X_num_df = df[num_cols]
X = pd.concat([X_num_df, X_cat_df], axis=1)
One-hot zwiększa liczbę kolumn, ale daje modelowi prostą, czytelną reprezentację. Dla modeli liniowych to często najbardziej przewidywalna i bezpieczna technika. Jeśli jednak kolumna ma dziesiątki czy setki unikalnych wartości (np. identyfikator kampanii), takie rozwinięcie może eksplodować wymiarem i spowolnić wszystko do bólu.
Wtedy sensowniejsze są inne podejścia. Przy drzewach decyzyjnych i gradient boosting możesz spokojnie użyć target encodingu (mapujesz kategorię na średni target z odpowiednim wygładzaniem) lub prostego „top N + reszta”: zostawiasz kilka najpopularniejszych kategorii osobno, a pozostałe łączysz w jedną grupę „Other”. Taki kompromis często daje większość informacji, a rozmiar macierzy cech pozostaje pod kontrolą.
Skalowanie i standaryzacja cech numerycznych
Jeśli kiedyś puściłeś regresję logistyczną na danych, gdzie jedna kolumna była w złotówkach, a druga w tysiącach, łatwo było złapać się na tym, że model przywiązuje się do „dużych” liczb. Nie dlatego, że są ważniejsze, tylko dlatego, że ich zakres jest większy. Skalowanie sprowadza wszystkie cechy numeryczne do porównywalnych zakresów.
Jeśli interesują Cię konkrety i przykłady, rzuć okiem na: Co nowego w standardzie Matter: czy smart home w końcu działa bez bólu?.
Dla modeli liniowych i metod opartych na odległości (kNN, SVM) skalowanie jest krytyczne, dla drzew – dużo mniej, ale nawet tam potrafi ułatwić optymalizację. Klasyczne podejście to standaryzacja (średnia 0, odchylenie standardowe 1):
from sklearn.preprocessing import StandardScaler
num_cols = ["age", "income", "sessions_last_30d"]
scaler = StandardScaler()
X_num_scaled = scaler.fit_transform(df[num_cols])
X_num_scaled_df = pd.DataFrame(
X_num_scaled, columns=num_cols, index=df.index
)
W prostych projektach kusi, żeby „na szybko” przeskalować cały DataFrame przed podziałem na train/test. To prosta droga do wycieku informacji: parametry skalera policzone na pełnych danych zawierają wiedzę o rozkładzie testu. Bezpieczniej jest trenować scaler wyłącznie na zbiorze treningowym, a potem użyć go do transformacji walidacji i testu.
Kiedy pierwszy raz zrobisz to poprawnie, efekt bywa zaskakujący: bez zmiany modelu wynik na walidacji potrafi przeskoczyć o kilka punktów w górę. Klucz tkwi w tym, żeby skalowanie było częścią powtarzalnego procesu, a nie jednorazową operacją „na brudno” w notatniku. Dlatego zamiast ręcznie wywoływać scaler na DataFrame, lepiej spiąć wszystko w jeden, spójny pipeline.
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
num_cols = ["age", "income", "sessions_last_30d"]
cat_cols = ["country", "device_type"]
preprocess = ColumnTransformer(
transformers=[
("num", StandardScaler(), num_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
]
)
model = Pipeline(
steps=[
("preprocess", preprocess),
("clf", LogisticRegression(max_iter=1000))
]
)
X = df[num_cols + cat_cols]
y = df["churn"]
model.fit(X, y)
Taki układ załatwia kilka problemów naraz. Te same kroki przygotowania danych są używane przy trenowaniu, walidacji i w produkcji, więc nie ma rozjazdu między notebookiem a kodem aplikacji. Dodatkowo wszystkie parametry preprocessingowe (średnie, odchylenia, słowniki encoderów) żyją razem z modelem – nie musisz ich nigdzie osobno zapisywać ani odtwarzać z pamięci.
W pewnym momencie dojdziesz też do decyzji: ile pracy włożyć w ręczny preprocessing, a ile zostawić modelom „end-to-end”. W projektach tablicowych (tabele z kolumnami, jak w Excelu) większość realnych zysków pochodzi z porządnego przygotowania danych, doboru sensownych cech i stabilnego pipeline’u. Z kolei w obrazach czy tekście więcej ciężaru przenosi się na architekturę sieci, a wstępne kroki są stosunkowo proste. Mądrzej jest więc na początku przesadzić z jakością danych niż złożonością modelu.
Cały proces sprowadza się do powtarzalnego rytmu: wybrać konkretny problem biznesowy, przygotować środowisko, spokojnie obejrzeć dane, oczyścić je i opisać, zbudować pierwszy pipeline i dopiero na tym fundamencie eksperymentować z modelami. Taki sposób pracy daje nie tylko lepsze metryki, ale też coś cenniejszego – zrozumienie, skąd bierze się wynik i które decyzje naprawdę mają znaczenie.

Trening pierwszego modelu: prosty baseline zamiast fajerwerków
Wyobraź sobie, że po tygodniu walki z Pandas masz wreszcie czysty, przygotowany zestaw cech. Otwierasz nowy notebook, importujesz pół scikit-learn i… pół dnia schodzi na wybieraniu modelu, zamiast na sprawdzeniu, czy w ogóle da się coś sensownego przewidzieć. Zanim ruszy karuzela hiperparametrów, wystarczy jeden, spokojny baseline.
Dlaczego baseline ratuje projekty
Baseline to pierwszy, naiwny model, który ma odpowiedzieć wyłącznie na jedno pytanie: „czy w ogóle jest sygnał w danych?”. Nie chodzi o to, żeby wygrać Kaggle, tylko żeby:
- złapać rząd wielkości wyniku (czy to 0.6, czy 0.9 accuracy);
- sprawdzić, czy nie ma poważnego błędu w danych lub etykietach;
- mieć punkt odniesienia dla kolejnych iteracji.
Dobry baseline jest:
- prosty – jedna linia z
LogisticRegressionczyRandomForestClassifierbez kombinowania; - powtarzalny – trenujesz go zawsze w ten sam sposób (podział train/test, te same metryki);
- szybki – liczony w sekundach, nie godzinach.
W zespole ML wygodnym rytuałem jest: nowy problem → 1–2 dni na przygotowanie danych → następnego dnia stoi baseline z notatką „kto przebije ten wynik?”. Brzmi banalnie, ale mocno trzyma projekt na ziemi.
Podział na train i test: oddzielenie nauki od egzaminu
Najczęstszy grzech początkujących: trenowanie i ocenianie modelu na tych samych danych, a potem zachwyt nad „accuracy 99%”. To tak, jakby uczyć się do egzaminu wyłącznie z gotowych odpowiedzi, a potem tym samym kluczem sprawdzać wynik.
Podstawowy krok to podział na zbiór treningowy i testowy:
from sklearn.model_selection import train_test_split
X = df.drop(columns=["churn"])
y = df["churn"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
test_size=0.2– klasyczne 80/20 sprawdza się w większości przypadków;stratify=y– przy problemach klasyfikacji dba, żeby proporcje klas w train i test były podobne.
Przy danych czasowych (np. prognoza popytu, churn liczony miesiąc do przodu) zamiast losowego podziału trzeba trzymać się osi czasu: train z wcześniejszych okresów, test z nowszych. W przeciwnym razie model „patrzy w przyszłość” i zawyża wynik.
Pierwszy model klasyfikacyjny: regresja logistyczna
Przy problemach typu churn, fraud, kliknięcia w reklamę – regresja logistyczna daje często zaskakująco mocny baseline. Z pipeline’em z poprzedniej sekcji kod sprowadza się do kilku linii:
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, accuracy_score
num_cols = ["age", "income", "sessions_last_30d"]
cat_cols = ["country", "device_type"]
preprocess = ColumnTransformer(
transformers=[
("num", StandardScaler(), num_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
]
)
clf = LogisticRegression(max_iter=1000)
model = Pipeline(
steps=[
("preprocess", preprocess),
("clf", clf),
]
)
X_train = df.loc[X_train.index, num_cols + cat_cols]
X_test = df.loc[X_test.index, num_cols + cat_cols]
model.fit(X_train, y_train)
y_pred_proba = model.predict_proba(X_test)[:, 1]
y_pred = model.predict(X_test)
print("Accuracy:", accuracy_score(y_test, y_pred))
print("ROC AUC:", roc_auc_score(y_test, y_pred_proba))
Regresja logistyczna ma kilka zalet na start: szybko się trenuje, daje sensowne prawdopodobieństwa, a przy dobrze przygotowanych cechach bywa zadziwiająco konkurencyjna wobec bardziej złożonych modeli.
Jaka metryka ma sens w praktyce
Kiedy dataset jest ładnie zbalansowany, accuracy zwykle wystarcza. W realnych projektach rzadko tak bywa – churn często dotyczy mniejszości klientów, fraud to promile transakcji, rzadkie błędy produktowe też.
Wtedy przydają się inne metryki:
- ROC AUC – mierzy, jak dobrze model rozróżnia klasy niezależnie od progu. Dobre na start;
- Precision/Recall – ważne, kiedy koszt false positive i false negative jest bardzo asymetryczny;
- F1 – kompromis między precision i recall, gdy obie strony są równie ważne.
from sklearn.metrics import classification_report, confusion_matrix
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred))
Jedno spojrzenie na macierz pomyłek często mówi więcej niż pół strony liczb. Jeśli widzisz, że model prawie nigdy nie przewiduje klasy pozytywnej, to nawet wysoka accuracy jest bezużyteczna – po prostu odgadł, że „prawie zawsze jest 0” i wygrał statystyką.
Regresja: kiedy przewidujesz liczby zamiast klas
Jeśli zamiast „czy klient odejdzie?” chcesz przewidzieć „ile wyda w następnym miesiącu?”, problem zamienia się w regresję. Schemat pracy wygląda podobnie: train/test split, pipeline, model. Zamiast regresji logistycznej możesz użyć prostego RandomForestRegressor albo LinearRegression:
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error
target = "next_month_revenue"
X = df.drop(columns=[target])
y = df[target]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
preprocess = ColumnTransformer(
transformers=[
("num", StandardScaler(), num_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
]
)
reg = RandomForestRegressor(
n_estimators=200,
random_state=42,
n_jobs=-1
)
model_reg = Pipeline(
steps=[
("preprocess", preprocess),
("reg", reg),
]
)
model_reg.fit(X_train, y_train)
y_pred = model_reg.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
rmse = mean_squared_error(y_test, y_pred, squared=False)
print("MAE:", mae)
print("RMSE:", rmse)
MAE (średni błąd bezwzględny) mówi wprost: „o ile średnio się mylimy w jednostkach targetu?”. RMSE (błąd średniokwadratowy) mocniej karze duże pomyłki, więc przydaje się, jeśli skrajne błędy są szczególnie bolesne biznesowo.
Walidacja krzyżowa i pierwsze eksperymenty z modelami
Przy małych zbiorach jeden podział train/test potrafi być zdradliwy: mały przypadek losowy w teście i wynik skacze o kilka punktów. Ktoś zmienia drobiazg w preprocessing, patrzy na +3% accuracy i ogłasza sukces, choć tak naprawdę trafił w bardziej „sprzyjający” podział danych.
Cross-validation: bardziej stabilny obraz
Walidacja krzyżowa dzieli dane treningowe na kilka „foldów” i trenuje model tyle razy, ile jest foldów. Za każdym razem inna część danych pełni rolę walidacji, a reszta – treningu. W scikit-learn sprowadza się to do jednego wywołania:
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(max_iter=1000)
pipeline = Pipeline(
steps=[
("preprocess", preprocess),
("clf", clf),
]
)
scores = cross_val_score(
pipeline,
X_train,
y_train,
cv=5,
scoring="roc_auc",
n_jobs=-1
)
print("ROC AUC (średnia):", scores.mean())
print("ROC AUC (odchylenie):", scores.std())
Średni wynik i odchylenie standardowe mówią, jak „pewny” jest model: czy wynik jest powtarzalny, czy zależy mocno od podziału danych. Przy problemach klasyfikacji często używa się StratifiedKFold, który dba o podobne proporcje klas w każdym foldzie.
Porównywanie kilku modeli w tym samym setupie
Zamiast przeskakiwać co godzinę na „nowy, lepszy algorytm”, lepiej ustawić eksperyment w kontrolowanych warunkach. Ten sam preprocessing, te same foldy, ta sama metryka, a podmieniasz tylko model:
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier # jeśli masz zainstalowane
models = {
"logreg": LogisticRegression(max_iter=1000),
"rf": RandomForestClassifier(
n_estimators=200,
max_depth=None,
n_jobs=-1,
random_state=42,
),
"xgb": XGBClassifier(
n_estimators=300,
learning_rate=0.1,
max_depth=5,
subsample=0.8,
colsample_bytree=0.8,
eval_metric="logloss",
n_jobs=-1,
random_state=42,
)
}
for name, clf in models.items():
pipe = Pipeline(steps=[("preprocess", preprocess), ("clf", clf)])
scores = cross_val_score(
pipe, X_train, y_train, cv=5, scoring="roc_auc", n_jobs=-1
)
print(name, "ROC AUC:", scores.mean(), "+/-", scores.std())
Taki prosty benchmark ratuje przed „optymalizacją hałasu”. Gdy różnica między dwoma modelami mieści się w jednym odchyleniu standardowym, trudno mówić, że któryś „wygrywa” – oba są w praktyce podobnie dobre i można kierować się innymi kryteriami (czas trenowania, prostota, interpretowalność).
Hiperparametry: małe kroki zamiast grid-search apokalipsy
Modele drzewiaste (RandomForest, GradientBoosting, XGBoost, LightGBM) potrafią dużo, ale reagują na hiperparametry. Zamiast odpalać gigantyczny GridSearchCV, lepiej zacząć od z grubsza sensownych wartości i spokojnie je doustawić.
from sklearn.model_selection import GridSearchCV
param_grid = {
"clf__n_estimators": [100, 300],
"clf__max_depth": [3, 5, None],
"clf__min_samples_split": [2, 10],
}
rf = RandomForestClassifier(
n_jobs=-1,
random_state=42,
)
pipe = Pipeline(
steps=[
("preprocess", preprocess),
("clf", rf),
]
)
grid = GridSearchCV(
pipe,
param_grid=param_grid,
cv=3,
scoring="roc_auc",
n_jobs=-1,
)
grid.fit(X_train, y_train)
print("Najlepsze parametry:", grid.best_params_)
print("Najlepszy wynik:", grid.best_score_)
W mniejszych projektach często wystarczy jedna, dobrze przemyślana siatka. Przy większych warto przeskoczyć na RandomizedSearchCV albo zewnętrzne narzędzia typu Optuna – losowe przeszukiwanie przestrzeni parametrów bywa dużo efektywniejsze niż mechaniczne sprawdzanie każdej kombinacji.
Interpretacja i sanity-check: czy model „myśli” w rozsądny sposób
W jednym z projektów scoringu kredytowego model dawał świetne wyniki na walidacji, ale w feature importances wysoko wylądowała kolumna „hasło do konta zmieniane w ciągu ostatnich 7 dni”. Niby sygnał był mocny, lecz biznesowo brzmiało to dziwnie – dopóki nie okazało się, że front aplikacji popychał do zmiany hasła tylko klientów z jednym specyficznym problemem w systemie.
Wagi regresji i ważność cech w drzewach
Do modeli liniowych można zajrzeć przez pryzmat współczynników:
clf = model.named_steps["clf"]
ohe = model.named_steps["preprocess"].named_transformers_["cat"]
scaler = model.named_steps["preprocess"].named_transformers_["num"]
feature_names_cat = ohe.get_feature_names_out(cat_cols)
feature_names = num_cols + list(feature_names_cat)
coef = clf.coef_[0]
feature_importance = pd.Series(coef, index=feature_names).sort_values()
print(feature_importance.tail(10))
print(feature_importance.head(10))
Wagi pokazują, które cechy najmocniej pchają model w kierunku klasy pozytywnej, a które w przeciwną stronę. Trzeba je oczywiście czytać w kontekście skalowania i kodowania kategorii, ale nawet prosty ranking pozwala wyłapać dziwne, potencjalnie „leaksujące” zmienne.
Przy drzewach i lasach można użyć feature_importances_:
rf = grid.best_estimator_.named_steps["clf"]
importances = pd.Series(
rf.feature_importances_,
index=feature_names
).sort_values(ascending=False)
print(importances.head(15))
Proste „top 10” cech daje szybką intuicję, czy model bazuje na sensownych sygnałach (aktywność, wiek konta, historia płatności) czy na czymś kuriozalnym (techniczne ID, timestamp utworzenia rekordu).
Krzywe ROC, Precision-Recall i dobór progu
W wielu produktach nie wystarczy klasa 0/1; potrzeba progów decyzyjnych. Inny próg chcesz mieć do „wysyłamy kampanię przypominającą”, a inny do „blokujemy transakcję”.
from sklearn.metrics import roc_curve, precision_recall_curve
y_scores = model.predict_proba(X_test)[:, 1]
fpr, tpr, thresholds_roc = roc_curve(y_test, y_scores)
precision, recall, thresholds_pr = precision_recall_curve(y_test, y_scores)
W praktyce często wybiera się próg tak, aby:
- utrzymać minimalny poziom recall (np. „łapiemy co najmniej X% fraudów”),
- albo minimalny precision (np. „nie wysyłamy więcej niż Y% pustych ofert”).
Czasem prosty wykres trade-offu między precision i recall (nawet naszkicowany w matplotlib) bardziej przekonuje biznes niż setka slajdów o architekturze modelu.
Sprawdzanie stabilności: segmenty użytkowników i drift
Model może mieć 0.9 ROC AUC w całości, a jednocześnie fatalnie działać na jednym ważnym segmencie – np. nowych klientach z określonego kraju. Warto rozbić ocenę po prostych filtrach:
Jedna z ekip zbudowała model churnu, który genialnie przewidywał odejścia „średnich” klientów, a kompletnie gubił się na kontach enterprise – firmy dowiedziała się o tym dopiero, gdy największy klient przeszedł do konkurencji. Da się tego uniknąć, rozbijając wyniki na prostsze segmenty i sprawdzając, czy nie ma grup, które model ewidentnie krzywdzi lub ignoruje.
segments = {
"new_users": X_test["account_age_days"] < 30,
"old_users": X_test["account_age_days"] >= 365,
"country_pl": X_test["country"] == "PL",
"country_other": X_test["country"] != "PL",
}
for name, mask in segments.items():
if mask.sum() < 100:
continue # za mało danych, żeby coś sensownie powiedzieć
auc = roc_auc_score(y_test[mask], y_scores[mask])
print(name, "ROC AUC:", round(auc, 3), "n:", mask.sum())
Nierówne wyniki między segmentami nie są od razu katastrofą, ale są sygnałem, że trzeba się przyjrzeć danym: może w jednym kraju brakuje kluczowych cech, może nowi użytkownicy mają zupełnie inną ścieżkę w produkcie, a może zmienił się proces zbierania informacji (tzw. data drift). Takie „przekroje” często ujawniają też zwykłe bugi w featurach, których nie widać w globalnej metryce.
Na koniec warto zerknąć również na: Kubernetes dla początkujących: pierwsze wdrożenie aplikacji i podstawy skalowania — to dobre domknięcie tematu.
Kolejny element to czas. Jeżeli masz dane z wielu miesięcy, sensownym nawykiem jest trenowanie na starszym wycinku, a testowanie na nowszym – zamiast losowego mieszania wszystkiego. Jeśli wynik na „ostatnich dwóch miesiącach” leci w dół, to znaczy, że rzeczywistość zaczyna odbiegać od tego, na czym uczył się model. Wtedy zamiast śrubować hiperparametry, lepiej zadać sobie pytanie, co zmieniło się w produkcie, użytkownikach albo procesach biznesowych.
W dojrzałych projektach monitoring stabilności staje się częścią systemu: proste dashboardy z metrykami po segmentach, alerty na duży spadek skuteczności, okresowe retreningi z nowszymi danymi. Nawet w małym projekcie side’owym możesz zasymulować taki tryb – odłożyć na bok najnowszy miesiąc danych jako „przyszłość” i sprawdzić, czy model w ogóle żyje poza swoim komfortowym sandboxem.
Jeśli przejdziesz drogę od dobrze zadanego problemu, przez sensownie przygotowane dane, prosty, powtarzalny pipeline i uczciwą ocenę modelu z sanity-checkami, to masz w rękach konkretny, działający kawałek ML – coś, co można bez wstydu podpiąć pod prawdziwy produkt, a nie tylko pokazać na slajdzie. Dalej zostaje już „tylko” powtarzanie tej ścieżki na kolejnych problemach, z każdą iteracją odrobinę lepiej rozumiejąc zarówno dane, jak i ludzi, dla których ten model ma coś realnie zmienić.
Od notatnika do produkcji: jak nie zepsuć działającego modelu przy wdrożeniu
Model w notatniku śmiga, metryki wyglądają świetnie, demo na spotkaniu z zespołem też. Pierwsza wersja ląduje w produkcji i nagle support zgłasza, że „czasem system daje kompletnie z kosmosu rekomendacje”. Okazuje się, że w trzech miejscach „po drodze” zmieniło się przetwarzanie danych i model w praktyce dostaje zupełnie inne wejście niż na treningu.
Ten sam pipeline w treningu i w predykcji
Najczęstsza pułapka: jeden kod do przygotowania danych w notatniku, drugi w API. Wystarczy, że drobny szczegół się rozjedzie (inna lista kategorii, inny sposób uzupełniania braków) i cały sens dopieszczonego modelu znika. Dlatego tak mocno pomaga podejście „jeden pipeline rządzi wszystkimi etapami”.
Jeżeli bazujesz na sklearn, to dobrym wzorcem jest zbudowanie kompletnego obiektu Pipeline lub ColumnTransformer i zapisanie go po treningu:
import joblib
# załóżmy, że `model` to Pipeline(preprocess -> clf)
model.fit(X_train, y_train)
joblib.dump(model, "model_churn.pkl")
W API nie odtwarzasz ręcznie kroków, tylko ładujesz całość i wołasz predict lub predict_proba:
model = joblib.load("model_churn.pkl")
def predict_churn(user_features: dict) -> float:
X = pd.DataFrame([user_features])
proba = model.predict_proba(X)[0, 1]
return float(proba)
Cała magia: preprocessing, kodowanie kategorii, skalowanie – siedzi w środku. Mniej kodu oznacza mniej miejsc, gdzie coś może się rozjechać.
Kontrakt na dane: schema zamiast „jakoś to będzie”
Model ma swoje oczekiwania co do danych wejściowych: które kolumny, jakie typy, czy mogą być puste, jakie wartości przyjmuje kategoria. Jeśli ten kontrakt nie jest nigdzie spisany, to prędzej czy później ktoś zmieni nazwę kolumny w bazie albo doda nową wartość do pola statusu i cały system zaczyna się zachowywać chaotycznie.
Nawet prosta, ręcznie utrzymywana specyfikacja pomaga:
- lista kolumn z typami (int/float/string, timestamp),
- opis dopuszczalnych wartości dla kategorii,
- informacja, które pola mogą być puste i jak są uzupełniane.
Technicznie można to zamknąć w jednym module Pythona z walidacją, np. przy użyciu pydantic lub pandera. Dane przychodzące do API przechodzą przez walidator; jeśli coś jest nie tak, dostajesz błąd dużo wcześniej niż w trakcie wywołania predict:
from pydantic import BaseModel, Field
from typing import Optional
class UserFeatures(BaseModel):
age: int = Field(ge=18, le=120)
country: str
account_age_days: int = Field(ge=0)
last_login_days_ago: Optional[int] = Field(ge=0)
def predict_churn(features: UserFeatures) -> float:
X = pd.DataFrame([features.dict()])
proba = model.predict_proba(X)[0, 1]
return float(proba)
Taki kontrakt eliminuje sporą klasę bugów: przypadkowe stringi w liczbach, nieznane kraje, brakujące pola po zmianach w upstreamie.
Batch vs real-time: inne tempo, te same zasady
Nie każdy model musi żyć w API z odpowiedzią w milisekundach. Częsty scenariusz w produktach SaaS: raz dziennie odpalasz job, który przelicza scoring churnu, leadów albo fraudu dla całej bazy użytkowników i zapisuje wyniki do tabeli. Frontend czy backend tylko odczytują gotowe liczby.
Przy batchu jest prościej z wydajnością, a trudniej z kontrolą wersji: łatwo „nadpisać” wyniki nową wersją modelu i stracić informację, czym były liczone. Kilka prostych praktyk zmniejsza bałagan:
- każdy batch zapisuje się z identyfikatorem wersji modelu,
- wyniki idą do tabeli z datą i godziną obliczenia (a nie nadpisują starej),
- parametry modelu i metryki walidacyjne zapisujesz w jednym, łatwo dostępnym miejscu (np. tabela
ml_models).
Przy real-time API głównym wąskim gardłem staje się latency i skalowanie. W typowych zastosowaniach biznesowych klasyczny model sklearn czy xgboost spokojnie wyrabia, jeśli nie każesz mu liczyć tysięcy predykcji per request. Problemem częściej jest nie sam model, tylko całe otoczenie: łańcuch zapytań do baz, ciężkie transformacje danych pisane ad hoc, blokujące I/O.
Debugowanie predykcji: „dlaczego model podjął taką decyzję?”
Po wdrożeniu szybko pojawia się pytanie: dlaczego ten konkretny użytkownik dostał taką ocenę? Jeśli nie umiesz tego wytłumaczyć, zaufanie do systemu spada – szczególnie u zespołów nietechnicznych.
Zamiast od razu sięgać po ciężkie SHAP-y, można zacząć od prostych, lokalnych wyjaśnień. Przy modelach liniowych to niemal darmowe: wystarczy policzyć wkład każdej cechy w logit lub wynik:
def explain_linear(model, x: pd.Series) -> pd.Series:
clf = model.named_steps["clf"]
preprocess = model.named_steps["preprocess"]
X_trans = preprocess.transform(x.to_frame().T)
coef = clf.coef_[0]
contrib = pd.Series(coef * X_trans.toarray()[0], index=feature_names)
return contrib.sort_values(ascending=False)
Dostajesz listę cech, które najmocniej podbiły wynik w górę i w dół. To często wystarcza, żeby odpowiedzieć na pytanie typu: „klient dostał wysoki churn-score, bo jest relatywnie nowy, dawno się nie logował i korzysta tylko z jednej funkcji produktu”.
Przy modelach drzewiastych można wejść krok głębiej z SHAP-em, ale nawet zwykłe feature importances w połączeniu z konkretnymi wartościami cech potrafią zbudować sensowną narrację.
Uczenie się na małych projektach: jak planować własną ścieżkę rozwoju
Jedna osoba po przeczytaniu kilku kursów kupiła dostęp do drogiego bootcampu i próbowała zbudować „system predykcji kursów akcji”. Druga wzięła trzy proste zbiory danych z Kaggle, zrobiła do nich małe notebooki i repozytoria, a potem zaaplikowała do pracy jako data scientist. Po roku różnica w praktycznych umiejętnościach była ogromna – na korzyść tej drugiej.
Projekt-ćwiczenie zamiast „przerabiania kursów”
Kursy i książki dają słownictwo oraz intuicję, ale skilla buduje samodzielne „dociskanie” projektów od A do Z. Nie musi to być nic wielkiego. Wystarczy kilka małych problemów, które przeprowadzisz przez pełny cykl:
- jasne pytanie („czy użytkownik wróci?”, „czy transakcja jest fraudem?”, „czy klient kupi premium?”),
- zasilenie się realnymi danymi (z publicznego źródła albo syntetycznymi, ale rozsądnymi),
- prosty baseline, pipeline, raport z wyników.
Przykładowe pomysły:
- predykcja przeżycia na Titanicu (klasyk, ale idealny do ogarnięcia kategorii i braków danych),
- klasyfikacja recenzji filmów na pozytywne/negatywne (NLP, proste przetwarzanie tekstu),
- predykcja prawdopodobieństwa kliknięcia reklamy (CTR) na jakimś publicznym zbiorze klików,
- regresja ceny mieszkań w danym mieście (ciągła zmienna, sporo feature engineeringu).
Każdy taki projekt zamknij w osobnym repozytorium: README z opisem problemu, folder notebooks albo src, plik z wymaganiami, krótka notka o wynikach. To jest jednocześnie nauka i budowanie portfolio.
Jak dobrać trudność problemu do poziomu
Za trudne projekty demotywują, za proste – nie uczą nic nowego. Da się to w miarę prosto wyważyć, patrząc na trzy wymiary:
- Stopień brudności danych – od idealnych CSV bez braków do logów produkcyjnych z dziurami.
- Liczba cech i typów danych – zaczynasz od liczb i kilku kategorii, dopiero potem dorzucasz tekst, sekwencje, time series.
- Oczekiwanie na wynik – od „cokolwiek lepszego niż losowe” do modelu, który da się sensownie wyjaśnić i obronić biznesowo.
Dobrze działa prosty schemat:
- projekt 1–2: mały, czysty dataset, klasyfikacja binarna, jeden notatnik;
- projekt 3–4: większy zestaw, trochę braków, pipeline w
sklearn, proste API lub batch; - projekt 5+: Twój własny problem z pracy lub hobby – np. analiza zachowań użytkowników w Twojej aplikacji, ranking treści, prosty system rekomendacji.
Cel „jedna nowa rzecz na projekt”
Naturalny odruch to chcieć „zrobić wszystko naraz”: nowe modele, zaawansowany feature engineering, hiperparametry, monitoring. Zdecydowanie lepiej zadziała zasada: w każdym kolejnym projekcie uczysz się jednej większej nowej rzeczy.
Przykładowo:
- Projekt A – pierwszy raz używasz porządnego
PipelineiColumnTransformer. - Projekt B – to samo, ale dorzucasz walidację czasową zamiast losowej.
- Projekt C – pierwszy kontakt z SHAP-em i interpretacją lokalną.
- Projekt D – pierwsze wdrożenie w formie małego API w FastAPI.
Po kilku iteracjach masz zbudowany zestaw klocków, które potem układasz w innych kontekstach. Nie ma potrzeby „opanowania deep learningu” przed ogarnięciem porządnego logowania metryk.
Organizacja kodu w projektach ML: jak nie utknąć w spaghetti-notebookach
Po paru tygodniach pracy zostaje dziesięć notatników z nazwami typu final2.ipynb, kilka plików CSV, trzy wersje modelu i zupełny brak pewności, która kombinacja kodu dała najlepszy wynik. Zmiana jednego szczegółu staje się ruletką, bo nie wiadomo, co jeszcze się przy okazji zmodyfikowało.
Warstwy: eksploracja, biblioteka, skrypty
Dobrze uporządkowany projekt ML zwykle dzieli się na trzy poziomy:
- Eksploracja (EDA, prototypy) – notatniki w
notebooks/, dużo testów, wykresów, doraźnego kodu. - Biblioteka funkcji – moduły Pythona w
src/lubml/: ładowanie danych, preprocessing, trenowanie, ewaluacja. - Skrypty wykonawcze –
train.py,evaluate.py,batch_predict.py, które składają wszystko w całość.
Notatnik służy wtedy głównie do szybkich eksperymentów i wizualizacji, a kod, który zaczyna się powtarzać, ląduje w modułach. Jeżeli dwa razy wklejasz ten sam blok do innego notatnika – to dobry sygnał, że czas przenieść go do src/features.py albo src/models.py.
Parametryzacja eksperymentów
Ręczne zmienianie wartości w kodzie (test_size = 0.2 → 0.3, n_estimators = 100 → 300) jest szybkie, ale trudno potem odtworzyć, co właściwie zostało wytrenowane. Pomaga podejście „parametry w jednym miejscu”, np. w pliku YAML.
# config.yaml
data:
path: "data/raw/transactions.csv"
target: "is_fraud"
model:
type: "random_forest"
params:
n_estimators: 200
max_depth: 5
random_state: 42
training:
test_size: 0.2
cv_folds: 5
import yaml
with open("config.yaml") as f:
cfg = yaml.safe_load(f)
rf = RandomForestClassifier(**cfg["model"]["params"])
Dzięki temu masz jasno opisane ustawienia treningu, możesz dodać nową konfigurację jako osobny plik, a kod pozostaje prosty. W większych projektach wchodzą narzędzia typu Hydra, ale na start wystarczy zwykły YAML i kilka linijek do jego wczytania.
Minimalne logowanie eksperymentów
Zaawansowane systemy typu MLflow czy Weights & Biases potrafią dużo, ale zanim po nie sięgniesz, prosta tabela z eksperymentami bywa wystarczająca. To może być nawet zwykły CSV:
import csv
from datetime import datetime
def log_experiment(params: dict, metrics: dict, fname="experiments.csv"):
row = {
"timestamp": datetime.utcnow().isoformat(),
**{f"param_{k}": v for k, v in params.items()},
**{f"metric_{k}": v for k, v in metrics.items()},
}
file_exists = os.path.exists(fname)
with open(fname, "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=row.keys())
if not file_exists:
writer.writeheader()
writer.writerow(row)
Po każdym treningu dopisujesz wiersz z hiperparametrami i metrykami. Później ładujesz ten plik do Pandas i jednym grupowaniem widzisz, które ustawienia działają lepiej. To niewiele kodu, a oszczędza mnóstwo „grzebania” w starych notebookach.
Kiedy pojawia się trzeci plik final_really_last.ipynb, zwykle jest już za późno – gubią się nie tylko metryki, ale też intuicje: dlaczego ta wersja zadziałała lepiej, a tamta gorzej. Prosty log eksperymentów wymusza na Tobie nazywanie rzeczy po imieniu: jakie cechy były użyte, jaki był podział na zbiory, jaką metrykę optymalizowałeś. Po kilku dniach przerwy możesz wrócić do projektu i bez bólu głowy odtworzyć najlepszy zestaw ustawień, zamiast skrolować historię w Jupyterze.
Dodatkowy krok, który bardzo pomaga, to krótki notatnik lub plik EXPERIMENTS.md z ręcznymi komentarzami. Nie chodzi o esej, raczej 1–2 zdania: „Eksperyment 12: dodałem cechy z logów – poprawa ROC AUC, ale duże przeuczenie na użytkownikach z małą liczbą transakcji”. Taka mikro-dokumentacja sprawia, że projekt jest zrozumiały nie tylko dla Ciebie sprzed tygodnia, lecz także dla kolegi z zespołu, który wejdzie do niego za miesiąc.
W pewnym momencie może się okazać, że prosty CSV to za mało: liczba eksperymentów rośnie, modele lądują w produkcji, trzeba śledzić wersje danych. Wtedy przychodzi czas na MLflow czy Weights & Biases, ale wejście w te narzędzia będzie dużo łagodniejsze, jeśli wcześniej wyrobisz sobie nawyk: każdy eksperyment ma jasno zapisane parametry, metryki i krótką interpretację wyniku.
Na końcu sprowadza się to do kilku zdrowych przyzwyczajeń: formułowania konkretnych pytań, pracy na prawdziwych danych, porządnego przygotowania feature’ów, prostych, ale rzetelnych modeli i przejrzystej organizacji kodu. Z takim fundamentem uczenie maszynowe przestaje być zbiorem magicznych trików, a staje się rzemiosłem, które można spokojnie rozwijać z projektu na projekt – dokładnie tak, jak każdy inny kawałek inżynierii oprogramowania.
Najważniejsze wnioski
- „Zróbmy coś z AI” trzeba jak najszybciej zamienić na precyzyjne zadanie: jasno określ, jakie dane wchodzą, co ma wyjść i jak wynik zostanie użyty w aplikacji – bez tego każdy model będzie strzelaniem na oślep.
- Model ML to nadal funkcja predict(x) → y, tylko zamiast setek if/else parametry są uczone z danych; logikę decyzyjną projektujesz pośrednio przez dobór cech i etykiet, a nie przez ręczne zasady.
- Uczenie maszynowe ma sens dopiero wtedy, gdy masz sporo historycznych przykładów, sensowne etykiety (dobre/złe przypadki) i złożoną logikę, której nie da się łatwo rozpisać prostymi regułami – w przeciwnym razie zwykły skrypt będzie skuteczniejszy i tańszy.
- Dobrze postawiony problem brzmi wprost: „Na podstawie X przewidź Y w horyzoncie Z” – jak w przykładzie churnu: zachowanie z 90 dni → rezygnacja w 30 dni – dzięki temu od razu wiadomo, jakie dane wyciągnąć, jak oznaczyć etykiety i jak mierzyć skuteczność.
- Kluczowe jest dobranie typu zadania (klasyfikacja, regresja, klasteryzacja) do problemu biznesowego; to od razu zawęża sensowne rozwiązania i chroni przed uczeniem modelu „do wszystkiego i do niczego”.
- Lepsze pytanie biznesowe (np. „zmniejsz o połowę fałszywe alarmy fraudowe”) daje więcej niż dłubanie w algorytmach – klarowny cel upraszcza wybór danych, metryk i późniejsze tłumaczenie wyników zespołowi.




Bardzo ciekawy artykuł! Chociaż mam pewne podstawy w Pythonie, to uczenie maszynowe nadal wydaje mi się czarną magią. Przewodnik pomógł mi zrozumieć podstawowe koncepcje i narzędzia, które mogą mi być przydatne w dalszym rozwoju. Teraz mam ochotę eksperymentować z własnymi modelami i zobaczyć, co uda mi się osiągnąć. Dzięki autorom za przystępne wyjaśnienia i praktyczne wskazówki!
Możliwość dodawania komentarzy nie jest dostępna.