15 minut vs 30 sekund: jak przyspieszyć Pandasa bez kupowania nowego serwera
Twój skrypt do analizy sprzedaży w Allegro przetwarza 100 tysięcy zamówień. Dzisiaj trwa to kwadrans, choć kolega z zespołu robi to samo w pół minuty. Różnic…
Twój skrypt do analizy sprzedaży w Allegro przetwarza 100 tysięcy zamówień. Dzisiaj trwa to kwadrans, choć kolega z zespołu robi to samo w pół minuty. Różnica? On nie używa iterrows() do filtrowania statusów.
Dlaczego Twój kod w Pandas działa wolno, choć "działa"?
Analityk w polskim e-commerce spędził tydzień na pisaniu skryptu do agregacji danych z systemu ERP. Kod "działał" — zwracał poprawne wyniki, ale przetwarzanie 100 tysięcy rekordów zajmowało 15 minut. Po optymalizacji ten sam wynik uzyskiwał w 30 sekund [1]. Różnica nie wynikała z mocy obliczeniowej, ale z trzech powtarzalnych błędów.
Jak drobne błędy w pętlach wydłużają runtime o 95%
Najczęstszy problem: pętle for i iterrows() na DataFrame'ach. Pandas został zaprojektowany do operacji wektorowych, a każda iteracja po wierszach dodaje narzut interpretacji Pythona. W teście na 100 tysiącach rekordów:
iterrows(): 15 minutapply(): 2 minuty- operacje wektorowe (
loc[]): 30 sekund
To przyspieszenie o 95% bez zmiany sprzętu [1]. Problem potęguje się w polskich firmach, gdzie dane często pochodzą z legacy systemów (np. Comarch ERP) i wymagają wielu transformacji przed analizą.
Pułapki w operacjach na DataFrame'ach, które przeoczyłeś
- Filtrowanie w pętli: Zamiast
for idx, row in df.iterrows(): if row['status'] == 'zrealizowane': ...użyjdf.loc[df['status'] == 'zrealizowane']. - Agregacje w Pythonie:
sum([x for x in df['kwota'] if x > 1000])jest 100x wolniejsze niżdf[df['kwota'] > 1000]['kwota'].sum()[4]. - Konwersje typów w środku kodu:
df['data'] = pd.to_datetime(df['data'])wykonane raz na początku jest szybsze niż wielokrotne konwersje w pętli.
Jakie operacje w Pandas są najbardziej kosztowne czasowo?
Nie wszystkie metody Pandas są sobie równe. Benchmarki pokazują, że niektóre operacje są 50-100x wolniejsze niż ich wektorowe odpowiedniki [4].
Row-wise vs vectorized: dlaczego to ma znaczenie
Pandas opiera się na bibliotece NumPy, która wykonuje operacje na całych tablicach jednocześnie (wektoryzacja). Gdy używasz apply() lub pętli, każda operacja jest wykonywana pojedynczo w Pythonie, co dodaje narzut interpretacji.
Przykład: obliczenie podatku VAT dla 100 tysięcy faktur:
# Wolne (row-wise) df['vat'] = df['netto'].apply(lambda x: x * 0.23) # Szybkie (wektorowe) df['vat'] = df['netto'] * 0.23
Różnica w czasie wykonania: 4.2s vs 0.04s [4]. W skali miesiąca to oszczędność 2 godzin przy przetwarzaniu dziennych raportów.
Case study: iterrows() vs loc[] w praktyce
Firma logistyczna z Poznania przetwarzała dane z czujników GPS w pojazdach. Pierwotna wersja kodu:
for idx, row in df.iterrows():
if row['speed'] > 90:
df.at[idx, 'overspeed'] = 1
Czas wykonania na 500 tysiącach rekordów: 22 minuty. Po optymalizacji:
df['overspeed'] = (df['speed'] > 90).astype(int)
Nowy czas: 2.1 sekundy — przyspieszenie o 628x [1]. Kluczowa zmiana: eliminacja pętli na rzecz operacji wektorowej.
Czy Pandas zawsze jest najlepszym narzędziem do przetwarzania danych?
Pandas sprawdza się świetnie dla zbiorów danych mieszczących się w pamięci RAM (do ~10 GB). Powyżej tej granicy zaczynają się problemy.
Kiedy warto rozważyć alternatywy
- Polars: napisany w Rust, wykorzystuje wszystkie rdzenie procesora. Na zbiorze 10 GB:
- Pandas: 48 sekund
- Polars: 7 sekund (przyspieszenie 6.8x) [6]
- Dask: skaluje Pandas na wiele maszyn. Używany przez Allegro do analizy logów z milionami rekordów [4].
- PySpark: gdy dane nie mieszczą się na jednej maszynie (np. w bankowości).
Ograniczenia Pandas, których nie obejdziesz
- Single-threaded: Pandas wykorzystuje tylko jeden rdzeń CPU. Nawet na 64-rdzeniowym serwerze będzie działać na jednym wątku [2].
- Pamięć RAM: Pandas ładuje cały zbiór danych do pamięci. Przy 100 GB danych potrzebujesz maszyny z 128 GB RAM.
- Stringi: Operacje na tekstach są wolniejsze niż na liczbach. Polars radzi sobie z nimi 2-5x szybciej [6].
Jak zoptymalizować kod w Pandas krok po kroku
Optymalizacja to proces, nie jednorazowa akcja. Oto sprawdzony workflow:
Krok 1: Profilowanie — znajdź bottlenecky
Zanim zaczniesz optymalizować, sprawdź, co naprawdę spowalnia kod. Narzędzia:
%timeitw Jupyterze: proste pomiary czasu.
```python
%timeit df['netto'] * 0.23
```
pandas-profiling: generuje raport z czasami wykonania każdej operacji [5].cProfile: dla zaawansowanych, pokazuje czas spędzony w każdej funkcji.
Przykład z polskiej firmy SaaS: profilowanie wykazało, że 70% czasu zajmowało ładowanie danych z CSV. Rozwiązanie? Przejście na format Parquet zmniejszyło czas ładowania z 45s do 3s.
Krok 2: Zastąp wolne operacje szybszymi odpowiednikami
Tabela zamienników (na podstawie benchmarków [3], [4]):
| Wolna operacja | Szybsza alternatywa | Przyspieszenie |
|---|---|---|
iterrows() | loc[] + wektoryzacja | 50-100x |
apply() | Wbudowane funkcje Pandas | 10-50x |
df[df['col'] > x] | query('col > @x') | 2-10x |
merge() | join() | 1.5-3x |
pd.concat() | append() (dla małych) | 1.2-2x |
Krok 3: Chunking i lazy evaluation
Gdy dane nie mieszczą się w pamięci:
- Chunking: przetwarzaj dane partiami.
```python
chunk_size = 10000
for chunk in pd.read_csv('duzy_plik.csv', chunksize=chunk_size):
process(chunk)
```
Pozwala przetwarzać pliki 10x większe niż RAM [2].
- Lazy evaluation (w Polars): zapytania są optymalizowane przed wykonaniem.
```python
# Polars
df = pl.scan_csv('duzy_plik.csv')
df.filter(pl.col('status') == 'zrealizowane').collect()
```
Polars najpierw analizuje całe zapytanie, a potem wykonuje tylko niezbędne operacje [6].
Jakie narzędzia wspierają optymalizację w Pandas?
Pandas-profiling: automatyczna analiza wydajności
Generuje raport HTML z:
- Czasami wykonania każdej operacji
- Rozmiarami kolumn i typami danych
- Sugestiami optymalizacji (np. "zmień typ
objectnacategory")
Przykład: raport wykazał, że kolumna kategoria_produktu (typ object) zajmowała 3x więcej pamięci niż potrzebowała. Zmiana na category zmniejszyła zużycie RAM o 65% [5].
Modin: Pandas na sterydach
Modin to drop-in replacement dla Pandas, który:
- Wykorzystuje wszystkie rdzenie CPU
- Działa na klastrach (np. Ray lub Dask)
- Przyspiesza operacje 2-4x na dużych zbiorach [4]
Instalacja:
pip install modin[all]
Użycie:
import modin.pandas as pd # zamiast pandas
Ograniczenie: Modin dodaje narzut przy małych zbiorach (<100k rekordów), więc nie zawsze warto go używać.
Numba: przyspieszanie niestandardowych funkcji
Numba kompiluje funkcje Pythona do kodu maszynowego. Przykład:
from numba import jit
@jit(nopython=True)
def oblicz_vat(netto):
return netto * 0.23
df['vat'] = oblicz_vat(df['netto'].values)
Przyspieszenie: 10-100x dla funkcji numerycznych [2]. Nie działa z funkcjami zawierającymi Pandas DataFrame'y.
Czy warto inwestować czas w optymalizację Pandas?
Koszty vs korzyści: kiedy optymalizacja ma sens
- Dane <1 GB: optymalizacja rzadko się opłaca. Skup się na czytelności kodu.
- Dane 1-10 GB: warto optymalizować, jeśli skrypt działa dłużej niż 5 minut.
- Dane >10 GB: rozważ alternatywy (Polars, Dask).
Przykład z polskiego banku: optymalizacja skryptu do analizy transakcji (3 GB danych) zmniejszyła czas z 25 minut do 1.5 minuty. Przy 10 uruchomieniach dziennie to oszczędność 4 godzin tygodniowo [4].
Jak przekonać zespół do zmian
- Pokaż konkretne liczby: "Przed: 15 min, po: 30s — oszczędzamy 2h/dzień".
- Zacznij od najwolniejszych fragmentów: profilowanie pokaże, gdzie jest największy zysk.
- Automatyzuj testy: napisz testy wydajnościowe, które uruchamiają się przy każdym commicie.
Ograniczenie, o którym nikt nie mówi
Optymalizacja często prowadzi do mniej czytelnego kodu. Przykład:
# Czytelne, ale wolne df['cena_brutto'] = df['cena_netto'] * (1 + df['stawka_vat']) # Szybkie, ale mniej czytelne df['cena_brutto'] = df['cena_netto'].mul(1 + df['stawka_vat'])
Warto znaleźć balans: optymalizuj krytyczne fragmenty, ale nie kosztem maintainability.
Werdykt
- Dla zbiorów <1 GB: używaj Pandas z operacjami wektorowymi. Unikaj
iterrows()iapply(). - Dla zbiorów 1-10 GB: optymalizuj z użyciem chunkingu, Modin lub Numba. Rozważ przejście na Parquet.
- Dla zbiorów >10 GB: migruj do Polars lub Dask. Pandas nie jest tu efektywny.
Największe przyspieszenia (50-100x) uzyskasz, eliminując pętle i apply() na rzecz operacji wektorowych. To wymaga zmiany myślenia: zamiast "jak przetworzyć każdy wiersz", pytaj "jak przetworzyć całą kolumnę naraz".
Jeśli twój skrypt działa dłużej niż 5 minut, zacznij od profilowania. Najczęściej 80% czasu zajmuje 20% kodu — tam kryje się największy potencjał.
Źródła
[1] I Reduced My Pandas Runtime by 95% — Here’s What I Was Doing Wrong — https://towardsdatascience.com/i-reduced-my-pandas-runtime-by-95-heres-what-i-was-doing-wrong/
[2] Enhancing Performance — pandas documentation — https://pandas.pydata.org/docs/user_guide/enhancingperf.html
[3] Pandas Performance Tips — Ritchie Vink — https://www.ritchie.top/2021/08/16/pandas-performance-tips/
[4] Fast, Flexible, Easy and Intuitive: How to Speed Up Your Pandas Code — Real Python — https://realpython.com/fast-flexible-pandas/
[5] How to Speed Up Your Pandas Code — KDnuggets — https://www.kdnuggets.com/2021/05/speed-up-pandas-code.html
[6] Pandas vs Polars: A Syntax and Speed Comparison — Towards Data Science — https://towardsdatascience.com/pandas-vs-polars-a-syntax-and-speed-comparison-5aa54e27497e