Jakie biblioteki do przetwarzania danych wybrać w projektach z uczeniem maszynowym

0
16
Rate this post

Nawigacja:

Jakie problemy rozwiązuje „warstwa danych” w projektach ML

Uczenie maszynowe jako „klient” dobrze przygotowanych danych

Model uczenia maszynowego można traktować jak bardzo wymagającego klienta: przyjmie tylko dane czyste, spójne, odpowiednio przeskalowane i w ściśle określonym formacie. Sama sieć neuronowa czy model drzew decyzyjnych to jedynie ostatni etap długiego łańcucha przetwarzania. Ogromna część jakości wyników zależy nie od tego, czy używasz XGBoost czy sieci transformer, ale od tego, jak wygląda warstwa danych, która je zasila.

Warstwa danych to zestaw bibliotek, narzędzi i praktyk, które zajmują się:

  • wczytywaniem danych z różnych źródeł (CSV, bazy SQL/NoSQL, magazyny plików, API),
  • czyszczeniem i normalizacją (braki, duplikaty, niespójne formaty dat, różne kodowania),
  • łączeniem wielu zbiorów (joiny, merge, union, okna czasowe),
  • transformacjami (feature engineering, tworzenie agregatów, pivoty, kodowania kategorii),
  • weryfikacją jakości (walidacja schematów, zakresów, rozkładów, detekcja driftu danych).

Bez przemyślanej warstwy danych projekty ML bardzo szybko zamieniają się w zbiór jednorazowych skryptów, których nikt nie rozumie i których nikt nie jest w stanie utrzymać w dłuższym okresie. Dobór bibliotek do przetwarzania danych to wprost decyzja o tym, jak stabilny będzie cały pipeline i jak trudno będzie go rozwijać.

Typowe zadania: od wczytania do gotowej macierzy cech

Niezależnie od branży schemat jest podobny: trzeba dane wczytać, oczyścić, połączyć, przekształcić i przygotować w takiej formie, by można je było bezpośrednio podać do modelu (feature matrix plus etykiety). Każda z tych faz korzysta z nieco innych bibliotek i ma inne wymagania wydajnościowe.

Przykład typowego przepływu:

  1. Wczytanie – odczyt plików CSV/Parquet, zapytań SQL, strumieni z systemu kolejkowego (Kafka, Pulsar), eksportów z narzędzi BI.
  2. Standaryzacja – normalizacja nazw kolumn, formatów dat, typów liczbowych, mapowanie kodów kategorii na przyjazne etykiety.
  3. Łączenie danych – łączenie logów z systemu z CRM, danych transakcyjnych z danymi o kliencie, danych telemetrycznych z informacjami o konfiguracji urządzeń.
  4. Feature engineering – tworzenie nowych cech: rolling window, agregaty po użytkownikach, cechy tekstowe, cechy sekwencyjne, cechy z sygnałów czasowych.
  5. Walidacja i eksport – sprawdzenie poprawności i zapis do formatu, który będzie wykorzystywany przez modele w treningu i produkcji.

Każdy etap może wykorzystywać inne narzędzia – czasem wystarczy pandas, innym razem pojawia się potrzeba użycia Spark, Dask albo bibliotek GPU. Klucz polega na świadomym dobraniu narzędzi do konkretnych wymogów: wielkości danych, latency, częstotliwości uruchamiania, zasobów infrastruktury.

Notatnik analityka kontra produkcyjny pipeline danych

Notebook (Jupyter, Colab) to świetne środowisko do eksploracji, testowania hipotez i szybkiego prototypowania. Najczęściej używa się tam pandas i kilku dodatkowych pakietów. Jednak kod tworzony w notatniku:

  • zwykle nie jest idempotentny (ten sam kod może dawać różne wyniki przy innym stanie środowiska),
  • nie ma jasno zdefiniowanych wejść i wyjść,
  • trudno go monitorować, logować, testować jednostkowo,
  • nie nadaje się bezpośrednio do uruchamiania w harmonogramie (cron, Airflow, Prefect).

Produkcyjny pipeline danych działa według innej logiki: ma wejście (źródła danych), jasno opisane kroki, wyjście (magazyn cech, tabela, plik Parquet) oraz monitoring i alerty. Biblioteki wybierane pod taki pipeline muszą nie tylko być wydajne, lecz także stabilne, dobrze logować błędy, łatwo się integrować z narzędziami orkiestracji i CI/CD.

Dobrą praktyką jest traktowanie kodu w notatnikach jako warstwy eksperymentalnej. Narzędzia jak pandas czy scikit-learn są tam idealne. Po ustaleniu logiki transformacji przenosi się ją do bardziej „twardych” rozwiązań: modułów Pythona, jobów Spark, DAG-ów w Airflow czy Prefect. Ten sam kod logiki biznesowej może wtedy pracować na znacznie większych wolumenach danych.

Konsekwencje złego wyboru bibliotek i narzędzi

Jeżeli wybór bibliotek do przetwarzania danych jest przypadkowy, skutki prędzej czy później stają się bolesne. Najczęstsze problemy:

  • Wąskie gardła wydajnościowe – pandas na pojedynczej maszynie „dusi się” przy setkach milionów wierszy, robi się out-of-memory, a czas przetwarzania rośnie z godzin do dni.
  • Niestabilność i trudne debugowanie – używanie mało dojrzałych bibliotek, słabo udokumentowanych funkcji lub miksowanie niespójnych formatów danych utrudnia znalezienie przyczyny błędów.
  • Brak skalowalności – jednorazowy eksperyment działa, ale re-trening codzienny czy real-time scoring już nie, bo cała warstwa danych nie jest dostosowana do trybu produkcyjnego.
  • Zależność od jednej osoby – egzotyczne, mało znane projekty open source wymagają specjalisty, którego trudno zastąpić. Przy popularnych bibliotekach łatwiej znaleźć nowych członków zespołu.

Zamiast domyślnie sięgać po najgłośniejsze biblioteki, lepiej postawić na przemyślany dobór według kilku klarownych kryteriów: charakter danych, typ obciążenia, ekosystem technologiczny, umiejętności zespołu i dojrzałość projektu.

Kryteria wyboru bibliotek do przetwarzania danych

Rozmiar danych a dostępna pamięć

Pierwszym pytaniem, które warto sobie zadać, jest to, czy cały zbiór danych mieści się w pamięci RAM pojedynczej maszyny. To prosty, ale kluczowy podział:

  • Dane „mieszczą się w RAM” – z reguły wystarczy pandas, NumPy, Polars albo cuDF (na GPU). Można budować logikę w notatnikach, potem przenosić do skryptów lub pipeline’ów.
  • Dane „większe niż RAM” – trzeba myśleć o przetwarzaniu rozproszonym lub przynajmniej o przetwarzaniu partiami (chunking) i lazy evaluation: Dask, PySpark, Vaex, Polars w trybie lazy, narzędzia SQL/lakehouse.

Kluczowe obserwacje:

  • Jeśli od dawna walczysz z OutOfMemoryError w pandas i ciągle zwiększasz RAM – to znak, że czas przejść na Dask, Spark lub przenieść część logiki bliżej bazy danych.
  • Jeśli dane są „na styk”, migracja do Polars lub cuDF może rozwiązać problem lepszym wykorzystaniem pamięci, ale tylko do pewnego poziomu.

Typ obciążenia: analizy jednorazowe, batch, strumienie

Drugie kryterium dotyczy jak często i w jakim trybie przetwarzane są dane:

  • Analizy jednorazowe, ad-hoc – analiza danych historycznych, eksploracja, research. Tu rządzą pandas, Polars, SQL, czasem Dask do pracy na większych zbiorach, ale bez pełnej orkiestracji.
  • Cykliczne batch’e – codzienne przeliczanie cech, tygodniowe raporty, regularny re-trening modeli. Potrzebne są narzędzia, które działają przewidywalnie i integrują się z harmonogramem: pandas/Polars + Airflow/Prefect dla mniejszych wolumenów, Spark/Dask dla większych.
  • Strumienie i near real-time – przetwarzanie danych w czasie zbliżonym do rzeczywistego (np. scoring zachowania użytkownika na stronie). Tutaj pojawiają się biblioteki do przetwarzania strumieniowego (Flink, Spark Structured Streaming, Kafka Streams), a pythonowa warstwa danych zwykle jest cieńsza.

Inne biblioteki spisują się dobrze w trybie batch, inne w streaming. Wiele projektów zaczyna w trybie batch, a dopiero później, gdy potrzeba niskich opóźnień, przenosi część logiki do narzędzi strumieniowych i minimalizuje ilość kodu Pythona w krytycznym path.

Ekosystem technologiczny i istniejąca infrastruktura

Kod ML nie żyje w próżni – działa w kontekście określonej infrastruktury: chmury, hurtowni danych, klastrów Hadoop/Spark, narzędzi BI, policy bezpieczeństwa. Ten kontekst ogranicza (ale też ułatwia) dobór bibliotek do przetwarzania danych.

Kilka wyraźnych scenariuszy:

  • Silna orientacja na SQL / hurtownię danych (BigQuery, Snowflake, Redshift, Synapse) – logikę transformacji danych opłaca się trzymać możliwie blisko bazy: widoki materializowane, procedury, dbt, a Python służy raczej do finalnego przygotowania pod model niż do ciężkiego ETL.
  • Środowisko Hadoop/Spark – jeśli firma ma już klaster Spark i kompetencje w tym obszarze, naturalnym wyborem są PySpark DataFrames, Spark SQL i integracje z MLlib. Pandas bywa wtedy głównie warstwą eksperymentalną.
  • Kubernetes i mikroserwisy – w takim środowisku biblioteki, które dobrze skalują się horyzontalnie (Dask na K8s, Ray, Spark on K8s), ułatwiają równoległe przetwarzanie danych i trening modeli.

Ekosystem determinuje też dostępne zasoby: czy łatwo o GPU, czy dane głównie siedzą w S3/Blob Storage, czy w relacyjnej bazie na własnym serwerze. Biblioteki GPU (cuDF, RAPIDS) mają sens tam, gdzie rzeczywiście są dostępne karty graficzne i da się nimi sterować w pipeline’ach.

Skład zespołu i jego doświadczenie

Narzędzia są tylko tak dobre, jak zespół, który potrafi ich użyć. Dwóm podobnym projektom można zarekomendować różne biblioteki wyłącznie na podstawie składu zespołu:

  • Zespół mocno SQL-owy, mało Pythona – główna logika w SQL, Python jako cienka warstwa do modeli ML. Warto inwestować w dbt, widoki, funkcje okienkowe i integracje z narzędziami BI, zamiast od razu rzucać się na Spark.
  • Zespół pythonowy, analitycy danych, data scientists – naturalny wybór to pandas/Polars, NumPy, Dask, potem ewentualne przejście na Spark. Łatwiej jest rozbudować to, co już jest w użyciu, niż wprowadzać radykalnie inne paradygmaty.
  • Silny komponent DevOps / data engineering – można śmielej sięgać po narzędzia klasy enterprise: Spark, Flink, Airflow, K8s, zaawansowane orkiestracje i monitorowanie, narzędzia do walidacji danych (Great Expectations, Soda).

Biblioteki z ogromnym ekosystemem (pandas, PySpark) mają tę zaletę, że znalezienie nowych osób, które je znają, jest znacznie prostsze niż w przypadku niszowych projektów. Mniejsze ale nowoczesne biblioteki (Polars, cuDF) dają przewagę wydajnościową, ale wymagają inwestycji w naukę i migrację.

Kryteria praktyczne: dojrzałość, społeczność, debugowalność

Poza czysto technicznymi parametrami dobrze uwzględnić kilka kryteriów „miękkich”:

  • Dojrzałość projektu – jak długo biblioteka istnieje, czy ma stabilne wydania, czy pojawiają się regularne aktualizacje bezpieczeństwa.
  • Dokumentacja i przykłady – czy można od razu zobaczyć praktyczne scenariusze użycia, czy istnieją poradniki migracji (np. z pandas do Polars).
  • Społeczność i wsparcie – aktywny GitHub, StackOverflow, blogi, tutoriale – to wszystko skraca czas rozwiązywania problemów.
  • Debugowalność – czy błędy są zrozumiałe, czy można podglądać pośrednie wyniki, czy da się dość łatwo ograniczyć problem do mniejszego zbioru danych.

Na etapie prototypu można sobie pozwolić na więcej eksperymentów z nowymi bibliotekami. W produkcji lepiej stawiać na narzędzia sprawdzone, które ktoś już „przetarł” w podobnym środowisku.

Rozmyty kolorowy kod na ekranie symbolizujący programowanie i biblioteki
Źródło: Pexels | Autor: Markus Spiske

Fundamenty w Pythonie: NumPy i biblioteki tablicowe

Rola NumPy jako fundamentu stosu danych

NumPy to biblioteka, która pod spodem obsługuje większość stosu danych w Pythonie: pandas, scikit-learn, częściowo PyTorch i TensorFlow (przynajmniej w podejściu do macierzy), wiele bibliotek statystycznych i naukowych. Jej główna rola to wydajne operacje na wielowymiarowych tablicach – coś, co w czystym Pythonie z listami byłoby tysiące razy wolniejsze.

W kontekście uczenia maszynowego NumPy przydaje się szczególnie:

  • do własnych transformacji numerycznych, których nie ma w wyższych warstwach (np. customowe funkcje okienkowe, transformacje sygnałów);
  • do generowania i wstępnego przetwarzania danych syntetycznych;
  • w sytuacjach, gdy trzeba manipulować już gotowymi macierzami cech przed podaniem ich do modelu (np. łączenie batchy, niestandardowe skalowanie).

Alternatywne biblioteki tablicowe: CuPy, JAX i spółka

NumPy ustawił standard interfejsu do tablic, ale coraz częściej potrzebne są obliczenia poza klasycznym CPU. Dlatego pojawiły się biblioteki, które naśladują API NumPy, jednocześnie korzystając z GPU lub z kompilacji just-in-time.

  • CuPy – „NumPy na GPU”. Większość kodu napisanego w NumPy można przepisać niemal 1:1, zmieniając import i typ tablicy. Przydaje się, gdy:
    • masz wiele ciężkich obliczeń macierzowych (np. symulacje, liczenie odległości, duże mnożenia macierzy),
    • dane i tak lądują na GPU (np. pod model deep learningowy), więc szkoda czasu na transfery CPU → GPU i z powrotem.
  • JAX – biblioteka od Google, łącząca interfejs podobny do NumPy z kompilacją i automatycznym różniczkowaniem (przydatne w trenowaniu modeli). Potrafi:
    • kompilować funkcje do bardzo szybkiego kodu (XLA),
    • przenosić te same operacje między CPU, GPU i TPU.
  • PyTorch / TensorFlow jako „tablice z bonusami” – oba frameworki potrafią wykonywać sporo typowych operacji tablicowych (sumy, agregacje, nadawanie kształtu, maskowanie). W części projektów zamiast NumPy używa się od razu tensora z PyTorch, żeby nie kopiować danych między różnymi reprezentacjami.

W projektach ML, w których główny koszt to sam trening modelu na GPU, użycie CuPy czy JAX ma sens wtedy, gdy przedsionek do modelu (feature engineering, symulacje, augmentacje) też da się uruchomić na GPU. Jeśli dane i tak trzeba długo ściągać z bazy, a transformacje są lekkie, prostszy będzie czysty NumPy lub pandas.

Jedna praktyczna uwaga: GPU nie rozwiązuje problemów I/O. Jeśli wąskim gardłem jest wolny dostęp do plików lub bazy, przyspieszenie samej arytmetyki na kartach graficznych prawie nic nie zmieni.

Kiedy NumPy wystarcza, a kiedy potrzebna jest wyższa warstwa

Tablice NumPy są świetne do operacji numerycznych, ale bardzo surowe, jeśli chodzi o metadane: nie ma nazw kolumn, jawnych typów kategorycznych, wygodnych operacji na czasie. Dlatego większość zespołów:

  • używa NumPy „pod spodem” – tam, gdzie liczy się prędkość i prostota struktury (obrazy, sygnały, macierze cech),
  • i pandas/Polars „na górze” – do pracy z danymi tabelarycznymi, ETL, łączenia źródeł danych.

Jeśli w kodzie zaczyna się pojawiać mnóstwo ręcznego zarządzania indeksami, reshape’owania i pilnowania kolejności kolumn, dobry sygnał, że przyda się ramka danych (DataFrame), a nie sama tablica. NumPy błyszczy tam, gdzie struktura jest naturalnie macierzowa: siatki, tensory, obrazy, sekwencje.

Pandas – kiedy klasyk wciąż jest najlepszym wyborem

Dlaczego pandas stał się standardem de facto

Pandas to wciąż podstawowe narzędzie pracy dla większości data scientistów. Główne powody są proste:

  • Intuicyjny model danych – DataFrame z nazwami kolumn, typami, indeksami czasowymi. To dużo bliższe tabelom z Excela czy SQL-a niż surowe tablice NumPy.
  • Bogaty zestaw operacji – grupowania, łączenia, pivoty, operacje czasowe, rolling windows, łatwe filtrowanie i agregacje.
  • Integracja z ekosystemem – większość narzędzi do ML przyjmuje pandas DataFrame’y lub potrafi je łatwo skonwertować.

Do eksploracji danych, szybkich analiz, czyszczenia kolumn, przygotowywania cech dla modeli klasycznych (XGBoost, LightGBM, scikit-learn) pandas bywa po prostu najszybszą drogą: mało kodu, bardzo ekspresyjne operacje, dobre wsparcie społeczności.

Typowe zastosowania pandas w projektach ML

Kilka obszarów, gdzie pandas sprawdza się szczególnie dobrze:

  • Eksploracyjna analiza danych (EDA) – szybkie podsumowania .describe(), rozkłady wartości, wykrywanie braków danych, wstępne korelacje.
  • Przygotowywanie cech – łączenie kilku tabel, wyliczanie feature’ów opartych na agregacjach (np. liczba zamówień w ostatnim tygodniu), tworzenie zmiennych kategorycznych i binarnych.
  • Prototypowanie pipeline’ów – szybkie sprawdzenie, czy dana sekwencja transformacji ma sens, zanim zostanie przeniesiona do narzędzia rozproszonego lub do SQL.
  • Analizy raportowe – generowanie tabel do wykresów, raportów PDF/Excel, dashboardów BI.

W wielu firmach ścieżka jest podobna: analityk przygotowuje w pandas „wersję 0” logiki, a potem inżynier danych przenosi ją np. do dbt/Spark, zachowując ten sam zestaw testów walidacyjnych.

Ograniczenia pandas: pamięć, wydajność, bezpieczeństwo

Choć pandas jest wygodny, ma kilka istotnych słabości:

  • Wszystko w pamięci – DataFrame’y trzymane są w RAM-ie. Przy dziesiątkach milionów wierszy pojawiają się problemy z zużyciem pamięci i czasem GC (garbage collectora).
  • Operacje pojedynczym watkiem – większość transformacji działa na jednym rdzeniu CPU. Nawet jeśli serwer ma wiele rdzeni, nie zawsze są wykorzystywane.
  • Ryzyko niejawnych konwersji typów – np. automatyczne zamiany na float przy obecności wartości NaN, co bywa bolesne przy kategoriach.
  • API z pułapkami – klasyczny przykład to łańcuchowe indeksowanie (df[a][b]), które potrafi dawać trudne do debugowania efekty.

Jeżeli doświadczasz częstych zgonów kernela, długich czasów ładowania CSV czy niestabilnych czasów wykonania, sygnał, że trzeba sięgnąć po inną klasę narzędzi: Polars, Dask, PySpark albo przenieść część logiki do bazy.

Good practices przy użyciu pandas

Kilka drobnych nawyków bardzo poprawia jakość i stabilność kodu w pandas:

  • Jawna kontrola typów – po wczytaniu danych rzutuj kolumny tam, gdzie to ma znaczenie (np. category dla kodów, datetime64 dla dat).
  • Unikanie łańcuchowego indeksowania – zamiast df[df["x"] > 0]["y"] = ... używaj .loc: df.loc[df["x"] > 0, "y"] = ....
  • Praca na kopiach, gdy trzeba – komunikat SettingWithCopyWarning to nie drobiazg, potrafi prowadzić do bardzo nieoczywistych błędów.
  • Wczesne filtrowanie – ograniczanie danych jak najbliżej źródła (np. w SQL) zmniejsza presję na pamięć w pandas.

Takie detale mają znaczenie zwłaszcza tam, gdzie ten sam kod ma później trafić na produkcję i żyć latami.

Kolorowy kod programistyczny na monitorze komputera
Źródło: Pexels | Autor: Seraphfim Gallery

Polars, cuDF i inne nowoczesne biblioteki kolumnowe

Model kolumnowy kontra wierszowy

Pandas, podobnie jak wiele klasycznych narzędzi, przechowuje dane w sposób, który jest w praktyce kompromisem między modelem wierszowym i kolumnowym. Nowsze biblioteki, takie jak Polars czy cuDF, bazują bardziej na kolumnowym układzie danych (podobnie jak hurtownie danych).

Intuicja jest prosta: jeśli najczęściej liczysz sumy, średnie czy filtry po kolumnach, to łatwiej mieć dane ułożone „kolumnami” w pamięci. Procesor (lub GPU) może wtedy czytać długie, ciągłe fragmenty RAM-u i bardzo szybko wykonywać operacje wektorowe.

Polars – szybki DataFrame na CPU (i nie tylko)

Polars to biblioteka DataFrame’ów napisana w Rust, z API wzorowanym na pandas, ale zaprojektowana od początku z myślą o:

  • wydajności – operacje wykonywane są w C/Rust, bardzo dobrze wykorzystują cache procesora i wiele rdzeni,
  • lazy evaluation – zamiast wykonywać każde polecenie od razu, Polars buduje plan zapytania i optymalizuje go jako całość,
  • skalowalności w pojedynczym procesie – lepsze zarządzanie pamięcią, mniejsze zużycie RAM niż w pandas.

W praktyce migracja z pandas do Polars może dać wielokrotne przyspieszenie przy analizach i pipeline’ach, które działają na granicy pamięci RAM. Zyski są największe tam, gdzie dominuje ciężkie grupowanie, joiny, agregacje.

Tryb eager i lazy w Polars

Polars ma dwa główne style pracy:

  • Eager – podobny do pandas: operacje wykonywane są „od ręki”, a wynik widać natychmiast po wywołaniu metody. Dobry do interaktywnej pracy i debugowania.
  • Lazy – zamiast DataFrame’a dostajesz obiekt zapytania, na którym budujesz kolejne transformacje. Nic się nie dzieje, dopóki nie wywołasz .collect() lub podobnej metody. Pozwala to:
    • unikać zbędnych kopii pośrednich,
    • łączenie kilku filtrów czy projekcji w jedną, zoptymalizowaną ścieżkę.

Dla projektów ML tryb lazy jest ciekawy wtedy, gdy ten sam pipeline trzeba wykonać wielokrotnie (np. różne okresy danych, różne rynki), a logika transformacji bywa rozbudowana. Optymalizator Polars potrafi wtedy oszczędzić sporo czasu i pamięci.

cuDF i RAPIDS – kolumnowe przetwarzanie na GPU

cuDF to DataFrame działający na GPU, będący częścią ekosystemu RAPIDS od NVIDII. Interfejs przypomina pandas, ale pod spodem:

  • dane znajdują się w pamięci GPU,
  • operacje korzystają z tysięcy wątków równoległych.

Taki model ma sens, gdy:

  • dane są bardzo duże, powtarza się na nich podobne transformacje,
  • i tak używasz GPU do treningu modeli (szczególnie deep learning i gradient boosting na GPU),
  • chcesz zminimalizować czas między „dane w magazynie” a „dane w modelu na GPU”.

Ekosystem RAPIDS zawiera też m.in.:

  • cuML – klasyczne algorytmy ML na GPU,
  • cuGraph – przetwarzanie grafów,
  • cugraph-ops i integracje z PyTorch/TensorFlow,
  • narzędzia do integracji z Dask, by rozciągać przetwarzanie na wiele GPU.

W praktyce cuDF ma największy sens w organizacjach, które już mają stabilną infrastrukturę GPU i zespoły obyte z tym środowiskiem. W małych projektach różnica w stosunku do dobrze napisanego kodu CPU nie zawsze uzasadnia złożoność.

Inne biblioteki kolumnowe i ekosystem Arrow

W tle wiele nowoczesnych narzędzi korzysta z Apache Arrow – kolumnowego formatu danych w pamięci. Używają go m.in. Polars, pyarrow, DuckDB, część integracji pandas. Arrow zapewnia:

  • wspólny format danych między językami (Python, R, Java, Rust),
  • szybkie „zero-copy” przekazywanie danych między bibliotekami,
  • dobrą kompresję i zgodność z formatem Parquet na dysku.

Na Arrow bazują też mniejsze biblioteki, np. Vaex (analiza dużych zbiorów „większych niż RAM” bez pełnej kopii w pamięci). To dobry wybór, gdy chcesz wykonywać szybkie, interaktywne analizy na wielu milionach wierszy, ale nie masz jeszcze infrastruktury rozproszonej.

Gdy dane nie mieszczą się w pamięci: Dask, PySpark i spółka

Dlaczego „większe niż RAM” zmienia zasady gry

Do pewnego momentu można zwiększać RAM albo przełączać się na bardziej wydajne biblioteki. Ale gdy dane przekraczają pamięć pojedynczej maszyny, pojawiają się nowe problemy:

  • trzeba podzielić dane na partycje,
  • zadbać o kolejność operacji, żeby nie przerzucać terabajtów między węzłami,
  • obsłużyć awarie węzłów, retry, monitoring i logowanie.

W tym miejscu wchodzą biblioteki i silniki rozproszone: Dask, Spark, Ray i podobne rozwiązania. Rozszerzają one model pandas/NumPy na wiele maszyn lub procesów, często zachowując znajome API.

Dask – „rozszerzony” pandas/NumPy na klaster

Dask to biblioteka, która:

  • udostępnia DataFrame’y i tablice o API zbliżonym do pandas/NumPy,
  • dzieli dane na partycje i wykonuje operacje równolegle,
  • Typowe wzorce użycia Dask w projektach ML

    Dask najczęściej pojawia się wtedy, gdy kod w stylu pandas zaczyna się „dusić”, ale przepisywanie wszystkiego na Spark byłoby przesadą. Kilka scenariuszy powtarza się szczególnie często:

  • przetwarzanie wielu plików – np. dziesiątki tysięcy plików CSV lub Parquet dzielonych po dacie czy regionie,
  • „większe niż RAM” DataFrame’y – zbiór, który nie wchodzi w pamięć pojedynczego procesu, ale mieści się w sumie na kilku maszynach,
  • klasyczne zadania ML – tworzenie cech (feature engineering) dla modeli trenowanych offline, np. predykcja popytu czy scoring kredytowy.

Dobry przykład: pipeline, który codziennie przetwarza logi z kilkunastu serwisów, liczy statystyki użytkowników i zapisuje gotowe tablice cech do Parquet. Sama logika jest „pandasowa”, ale dane dzienne mają już dziesiątki gigabajtów – Dask potrafi rozbić je na partycje i policzyć w rozsądnym czasie na kilku węzłach.

Architektura Dask: graf zadań zamiast ręcznego „klejenia” kroków

Pod spodem Dask buduje graf zadań (ang. task graph). To po prostu opis, co trzeba policzyć i w jakiej kolejności. Kiedy zapisujesz ciąg operacji na dask.DataFrame czy dask.array, tworzysz taki graf; faktyczne wykonanie następuje dopiero przy wywołaniu metod typu .compute().

Przekłada się to na kilka korzyści:

  • leniwe wykonanie – Dask może połączyć wiele operacji w mniejszą liczbę kroków,
  • planowanie na klastrze – scheduler rozsyła zadania na dostępne maszyny i dba o to, by dane podróżowały jak najmniej,
  • odporność na awarie – jeśli pojedynczy worker padnie, zadania można przeliczyć na innym.

Do debugowania przydaje się dashboard Daska – w przeglądarce widać, co dokładnie się dzieje: jak długa jest kolejka zadań, które partycje są w pamięci, gdzie robi się wąskie gardło.

Jak łączyć Dask z narzędziami ML

Dask nie jest sam w sobie frameworkiem do trenowania modeli, ale dobrze dogaduje się z popularnymi bibliotekami ML. Typowy schemat to:

  1. wczytanie i wstępne przetwarzanie feature’ów w dask.dataframe lub dask.array,
  2. zapis efektu do Parquet / NumPy / formatu własnego,
  3. trening modelu już na „zredukowanym” zbiorze (np. scikit-learn, XGBoost, LightGBM).

Są też bardziej zintegrowane ścieżki. XGBoost i LightGBM mają tryby rozproszone współpracujące z Dask, dzięki czemu można trenować modele wprost na danych rozbitych na klaster, bez manualnego „składania” ich w jeden plik. Przyda się to tam, gdzie pojedynczy model musi widzieć naprawdę duży wolumen przykładów.

Kiedy Dask, a kiedy Spark lub Ray

Na pierwszy rzut oka Dask i Spark robią podobne rzeczy, ale wybór między nimi zwykle zależy od otoczenia technicznego:

  • Spark wygrywa tam, gdzie już istnieje ekosystem Hadoop/Databricks i zespół zna SQL oraz pyspark. Ma bardzo dobrą integrację z jeziorem danych (data lake), streamingiem i systemami kolejkowymi.
  • Dask bywa prostszy do dołożenia w środowisku „pythonowym”, gdzie nie ma rozbudowanego klastra big data. Łatwiej go uruchomić na Kubernetesie, w chmurze czy nawet na kilku większych maszynach bare metal.
  • Ray celuje z kolei w rozproszone uczenie maszynowe i serving modeli, ma też własny ekosystem (np. Ray Train, Ray Serve). Do przetwarzania tabelarycznego używa się w nim często integracji z Pandas/Polars czy Spark.

Jeżeli głównym problemem jest „pandas się nie mieści i trzeba coś tylko trochę większego”, Dask jest naturalnym pierwszym krokiem. Jeśli wymagana jest ścisła integracja z istniejącym jeziorem danych, katalogiem danych, streamingiem – Spark bywa bezpieczniejszym wyborem.

Pułapki przy migracji kodu pandas do Dask

Przejście z pandas na Dask w teorii jest proste, bo API jest podobne, ale w praktyce pojawiają się drobiazgi:

  • nie wszystkie operacje są wspierane – szczególnie te bardzo „imperatywne” lub silnie zależne od kolejności wierszy,
  • koszt przesyłania danych – częsta konwersja między Dask a pandas (np. .compute() na małych etapach) może zabić zysk z równoległości,
  • nieprzemyślana liczba partycji – zbyt wiele małych partycji generuje ogromny narzut na zarządzanie zadaniami, zbyt mało – prowadzi do nierównego obciążenia węzłów.

Przydaje się zasada: na początku traktować Dask jak „większe pandas”, ale w kluczowych fragmentach myśleć już kategoriami rozproszonych systemów – śledzić ruch danych, liczbę shuffle’y i rozmiary partycji.

PySpark – gdy ML siedzi blisko dużego klastra

PySpark to interfejs do Apache Spark dla Pythona. Sam Spark jest silnikiem przetwarzania rozproszonego zaprojektowanym pod:

  • bardzo duże zbiory danych,
  • obliczenia batched (hurtowe) i strumieniowe,
  • integrację z ekosystemem Hadoop / jeziorem danych.

W kontekście ML jego mocną stroną jest możliwość napisania jednego pipeline’u, który:

  • czyta dane z data lake (S3, HDFS, lakehouse),
  • czyści, joinuje i agreguje dane w Spark SQL lub DataFrame API,
  • zapisuje gotowe cechy do bordera (np. Parquet, Delta),
  • a następnie odpala trening modeli albo na samym Spark MLlib, albo eksportuje dane do frameworków pythonowych.

Spark MLlib ma własne algorytmy (m.in. regresja, drzewa, klasyfikacja wieloklasowa), ale w wielu organizacjach stosuje się hybrydy: feature engineering w Spark, trening w scikit-learn czy XGBoost na wybranym wycinku danych.

Ray i inne silniki ogólnego przeznaczenia

Poza Daskiem i Sparkiem rozwija się grupa narzędzi ogólnego przeznaczenia do obliczeń rozproszonych. Najbardziej widoczny jest Ray, którego profil wygląda nieco inaczej:

  • zamiast ściśle „tabelarycznego” API ma model zdalnych funkcji i aktorów (dłużej żyjących obiektów w klastrze),
  • udostępnia gotowe moduły do trenowania modeli na wielu GPU, hyper-param tuning, serwowanie modeli,
  • potrafi współpracować z Polars, Pandas czy Spark jako „źródłem” danych.

W większych systemach bywa tak, że warstwa danych stoi na Spark lub Dask, natomiast typowo „ML-owe” zadania (trenowanie setek modeli, eksploracja hiperparametrów, orkiestracja eksperymentów) są oparte na Rayu. Dzięki temu każde narzędzie robi to, w czym jest najlepsze.

SQL, bazy danych analitycznych i lakehouse – kiedy trzymać przetwarzanie „bliżej magazynu danych”

Dlaczego nie wszystko musi trafić do Pythona

Łatwo wpaść w schemat: „wszystko pobieram do pandas i tam robię logikę”. W wielu projektach ML to jednak ślepa uliczka. Silniki bazodanowe i hurtownie danych mają za sobą dziesiątki lat optymalizacji – filtrują, joinują i agregują dane zdecydowanie szybciej niż pojedynczy proces w Pythonie.

Jeśli zestaw cech dla modelu wynika z kilku dużych tabel, a transformacje da się zapisać w miarę „prostym” SQL-u, znacznie rozsądniej:

  1. przygotować dane jak najbliżej źródła (w hurtowni lub jeziorku danych),
  2. a do Pythona wciągać już mocno zredukowany zestaw – np. jedną tabelę z kilkudziesięcioma cechami na rekord.

Takie podejście odciąża warstwę ML-ową i pozwala inżynierom danych korzystać z narzędzi, które znają najlepiej: SQL, dbt, systemy ETL.

Klasyczne hurtownie i nowoczesne silniki analityczne

Hurtownie danych (Snowflake, BigQuery, Redshift, ClickHouse, systemy MPP on-premise) są zoptymalizowane pod zapytania typu:

  • „policz agregaty dla tej populacji użytkowników według segmencie”
  • „złączenia kilku dużych faktów z wymiarami, z filtrami po czasie i kraju”
  • „policz rolling window – ostatnie N dni aktywności na użytkownika”.

Takie rzeczy świetnie nadają się na featury. W wielu firmach największy kawał „inteligencji” modelu to właśnie dobrze policzone cechy typu: liczba transakcji w ostatnich 7 dniach, średnia wartość koszyka w ostatnim kwartale, czas od ostatniej wizyty itd. Te obliczenia da się stabilnie utrzymywać jako widoki materializowane, modele dbt lub zaplanowane joby SQL.

Python wchodzi wtedy na scenę dopiero jako konsument: pobiera już gotową, przetworzoną tabelę, dzieli na train/validation/test i trenuje model.

Lakehouse i silniki zapytań nad plikami

Coraz popularniejszy model to tzw. lakehouse – połączenie jeziorka danych (pliki Parquet/ORC w S3, GCS, HDFS) z warstwą zarządzającą schematem i wersjonowaniem (Delta Lake, Apache Iceberg, Apache Hudi). Na wierzchu działają różne silniki:

  • Spark SQL,
  • Presto/Trino,
  • BigQuery lub Snowflake skonfigurowane jako external tables,
  • silniki typu DuckDB lub nawet Polars, czytające Parquet bezpośrednio.

Dla ML oznacza to, że źródłem prawdy o danych są pliki w lakehouse, a pipeline’y featurów buduje się jako zestaw zapytań SQL i jobów działających „blisko danych”. Python po tej stronie często ogranicza się do orkiestracji (Airflow, Prefect, Dagster) i końcowego etapu modelowania.

DuckDB – „SQLite do analityki” w ekosystemie ML

DuckDB to ciekawy przykład silnika analitycznego, który działa w jednym procesie, ale oferuje sporo możliwości znanych z hurtowni:

  • kolumnowy silnik wykonawczy,
  • obsługa Parquet i Arrow,
  • pełnoprawny SQL,
  • integracja z Pandas/Polars.

Dla zespołów ML DuckDB jest wygodny do:

  • szybkiej prototypowej analizy plików Parquet lub CSV bez stawiania bazy,
  • budowania lokalnych feature store’ów – lekkich tabel cech trzymanych w plikach, zapytaniach SQL i używanych zarówno do treningu, jak i do lokalnych testów inferencji,
  • łączenia świata SQL (łatwe joiny i agregacje) z kodem w Pandas/Polars.

Wyobraź sobie notebooka, w którym wczytujesz dziesiątki plików Parquet z S3, robisz joiny i okna czasowe w SQL, a wynik przekazujesz wprost do modelu XGBoost w Pythonie. DuckDB bardzo często pozwala zamknąć taki scenariusz na pojedynczej maszynie, przy dobrej wydajności.

Feature store i kontrakt między warstwą danych a ML

W większych organizacjach, gdzie modeli jest wiele, a zespoły i środowiska są rozdzielone, pojawia się potrzeba standaryzacji cech. Tu wchodzą feature store’y – systemy, które:

  • definiują cechy w jednym miejscu (często w SQL-u lub DSL-u bliskim SQL),
  • pozwalają używać tej samej definicji cechy podczas treningu i inferencji (online/offline),
  • trzymają historię wartości cech, co ułatwia odtwarzanie eksperymentów.

Popularne przykłady to Feast, Tecton, Vertex AI Feature Store, Databricks Feature Store. Niezależnie od narzędzia, idea jest podobna: ML przestaje sam liczyć featury w skryptach Pythona, a zamiast tego korzysta z kontraktu na dane – jasno określonej tabeli cech, której jakość, testy i wydajność leżą po stronie inżynierii danych.

W praktyce często oznacza to, że:

  • warstwa danych wykorzystuje hurtownię, Spark, Dask czy inne narzędzia do konstrukcji cech i zapisuje je w feature store,
  • warstwa ML pobiera stamtąd dane jednym wywołaniem API lub zapytaniem i skupia się na modelach, kalibracji, ocenie i deploymencie.

Kiedy logika powinna zostać w SQL, a kiedy przenieść ją do Pythona

Granica między „logiką danych” a „logiką ML” bywa płynna. Proste kryteria pomagają w decyzji:

Najczęściej zadawane pytania (FAQ)

Jaką bibliotekę wybrać do przetwarzania danych w projektach uczenia maszynowego?

Podstawowy wybór zależy od wielkości danych i tego, czy mieszczą się w pamięci RAM jednej maszyny. Dla małych i średnich zbiorów najczęściej wystarcza pandas, NumPy albo Polars; jeśli korzystasz z GPU – cuDF. To dobre narzędzia do notatników, eksploracji i pierwszych wersji pipeline’ów.

Gdy dane są większe niż RAM albo przetwarzanie trwa już godziny, wchodzą w grę narzędzia rozproszone: Dask, PySpark (Apache Spark), czasem Vaex lub SQL/lakehouse w chmurze. Tam ta sama logika przekształceń może obsłużyć znacznie większe wolumeny przy akceptowalnym czasie wykonania.

Kiedy pandas przestaje wystarczać i trzeba przejść na Spark lub Dask?

Sygnalizacją problemu są powtarzające się błędy OutOfMemory, „zawieszanie się” notatników przy prostych operacjach i rosnący czas wykonywania kroków z godziny na wiele godzin. Jeśli za każdym razem ratuje Cię tylko dokładanie RAM do maszyny, to zwykle znak, że pora na narzędzia rozproszone.

Przejście na Dask lub Spark ma sens, gdy:

  • jedna tabela ma dziesiątki/setki milionów wierszy,
  • pipeline trzeba uruchamiać regularnie (np. codziennie),
  • w zespole pojawia się potrzeba równoległego przetwarzania na wielu węzłach lub w klastrze chmurowym.

Prostym krokiem pośrednim bywa też migracja z pandas na Polars (lepsza wydajność w RAM) lub na SQL wykonywany „bliżej danych”.

Czym różni się notatnik (Jupyter/Colab) od produkcyjnego pipeline’u danych?

Notatnik jest narzędziem do eksperymentów: można w nim szybko sprawdzić hipotezę, narysować wykres, przetestować transformację na wycinku danych. Kod bywa uruchamiany w przypadkowej kolejności, nie ma jasno zdefiniowanych wejść i wyjść, a ten sam notebook po kilku dniach potrafi dawać inne wyniki, bo zmieniły się dane lub stan środowiska.

Produkcyjny pipeline to uporządkowany proces: ma określone źródła danych, sekwencję kroków (np. czyszczenie, łączenie, feature engineering), uzgodniony format wyjścia oraz monitoring i alerty. Uruchamia się go regularnie w harmonogramie (np. Airflow, Prefect), musi być powtarzalny, testowalny i możliwy do debugowania bez „klikania” w notatnik.

Jakie biblioteki są najlepsze do feature engineeringu w ML?

Na pojedynczej maszynie najczęściej wykorzystuje się kombinację: pandas/Polars do transformacji tabelowych, NumPy do operacji numerycznych, scikit-learn do standardowych przekształceń (skalery, kodery kategorii, pipeline’y) oraz wyspecjalizowane biblioteki do tekstu czy czasów (np. biblioteki NLP, narzędzia do szeregów czasowych).

Przy dużych wolumenach ta sama logika może być zaimplementowana w Spark (Spark SQL, DataFrame API, Spark MLlib) lub Dask. Wiele zespołów prototypuje cechy w pandas w notatniku, a po ustaleniu ich zestawu przenosi przekształcenia do pipeline’u działającego już w Spark/Dask, aby obsłużyć całą produkcyjną skalę danych.

Jak wybrać między przetwarzaniem batch a streamingiem przy ML?

Jeżeli model musi być aktualizowany lub używany tylko co jakiś czas (np. raport dzienny, scoring raz na godzinę, cotygodniowy re-trening), najprościej jest pozostać przy przetwarzaniu wsadowym (batch) z harmonogramem. Wtedy spokojnie wystarczą pandas/Polars + narzędzie orkiestracji albo Spark/Dask uruchamiane regularnie.

Streaming wchodzi w grę, gdy liczy się opóźnienie rzędu sekund lub minut, np. reakcja na zachowanie użytkownika w sklepie online czy monitoring anomalii na żywo. Wówczas ciężar przetwarzania przenosi się na systemy strumieniowe (Flink, Spark Structured Streaming, Kafka Streams), a kod Pythona często stanowi tylko cienką warstwę wokół modelu lub specyficznych transformacji.

Jakie są skutki złego doboru bibliotek do przetwarzania danych w ML?

Najczęściej pojawiają się wąskie gardła wydajnościowe: to, co działało na małej próbce, na pełnych danych nagle trwa wielokrotnie dłużej albo w ogóle się nie kończy z powodu braku pamięci. Pipeline zaczyna się psuć w losowych miejscach, a debugowanie jest trudne, bo użyto mało dojrzałych, słabo udokumentowanych narzędzi lub mieszanki niespójnych formatów.

Drugi typ problemów to brak skalowalności organizacyjnej: tylko jedna osoba w zespole „zna” egzotyczną bibliotekę czy niestandardowy framework. Wtedy każdy drobny change wymaga jej udziału, a rozwój projektu zwalnia. Postawienie na popularne, dobrze wspierane biblioteki (pandas, Spark, Dask, Polars, SQL) ułatwia rekrutację, utrzymanie i długoterminowy rozwój warstwy danych.