Profilowanie za pomocą timeit i memory_profiler
Mierz czas wykonania za pomocą %timeit i szczytowe zużycie pamięci za pomocą memory_profiler, aby znaleźć najwolniejsze części potoku.
Profilowanie za pomocą timeit i memory_profiler to bezpłatna lekcja Pandas & NumPy Academy na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Pandas & NumPy Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Pandas & NumPy Academy zawiera 4 lekcji w sumie.
Dlaczego profilować przed optymalizacją?
Częstym błędem jest intuicyjna optymalizacja kodu — przepisywanie funkcji, która wydaje się powolna, bez sprawdzenia, czy rzeczywiście stanowi wąskie gardło. W potokach danych 90% czasu wykonania często przypada na 10% kodu. Profilowanie dokładnie wskazuje, gdzie zużywany jest czas i pamięć, dzięki czemu mogą Państwo skupić wysiłki optymalizacyjne tam, gdzie mają one największe znaczenie. Złota zasada brzmi: najpierw zmierz, potem optymalizuj. Narzędzia takie jak timeit mierzą czas, a memory_profiler mierzy pamięć RAM.
import pandas as pd
import numpy as np
import timeit
# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})
t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)
print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')timeit: wbudowany miernik czasu języka Python
Moduł timeit mierzy czas wykonania, uruchamiając instrukcję wiele razy i zwracając całkowity czas, który upłynął. timeit.timeit(stmt, number=n) uruchamia stmt n razy i zwraca łączny czas w sekundach. Aby uzyskać czas pojedynczego wykonania, należy podzielić wynik przez number. Dla dokładnych pomiarów należy wybrać wartość number, tak aby całkowity czas wykonania wynosił co najmniej 0,1 sekundy — pojedyncze wykonanie szybkiej operacji nie jest miarodajne ze względu na zakłócenia związane z planowaniem zadań przez system.
import timeit
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})
# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)
print(f'Operator **2 per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square() per run: {time_c/1000*1000:.3f} ms')Magia %timeit w Jupyterze
W notatnikach Jupyter magia %timeit automatycznie dobiera liczbę powtórzeń i uruchomień, aby uzyskać statystycznie stabilne oszacowanie, a następnie wyświetla najlepszy i średni czas wraz z odchyleniem standardowym. Jest to najwygodniejszy sposób szybkiego testowania pojedynczego wiersza. Aby zmierzyć czas wykonania całej komórki, należy użyć %%timeit (podwójny znak procentu). Format wyniku to: X ns/μs/ms ± Y ns per loop (mean ± std. dev. of 7 runs, N loops each).
# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()
# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')cProfile do profilowania na poziomie funkcji
Podczas gdy timeit mierzy pojedyncze wyrażenie, cProfile profiluje cały stos wywołań — pokazuje, ile czasu zajmuje każda funkcja wywołana podczas wykonania. Jest to niezbędne do identyfikowania wąskiego gardła w złożonym potoku. Należy uruchomić python -m cProfile -s cumulative script.py z wiersza poleceń albo użyć cProfile.run('function()') w skrypcie. W Jupyterze należy użyć %prun function().
import cProfile
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})
def slow_pipeline(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] * row['b'])
return pd.Series(result)
# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')Pomiar zużycia pamięci
Użyj df.memory_usage(deep=True), aby sprawdzić, ile pamięci RAM zużywa każda kolumna. Argument deep=True mierzy rzeczywistą pamięć zajmowaną przez kolumny typu object (ciągi znaków), które w przeciwnym razie raportują tylko rozmiar wskaźników. Użyj .sum(), aby uzyskać całkowity rozmiar DataFrame w bajtach. Podziel wynik przez 1e6, aby otrzymać MB, lub przez 1e9, aby otrzymać GB. To punkt wyjścia do optymalizacji pamięci — nie można zmniejszyć czegoś, czego wcześniej nie zmierzono.
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({
'id': range(100000),
'name': ['user_' + str(i) for i in range(100000)],
'score': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})
mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')memory_profiler do pomiaru pamięci w kolejnych wierszach
Pakiet memory_profiler (pip install memory-profiler) mierzy zużycie pamięci RAM w kolejnych wierszach funkcji. Należy udekorować funkcję za pomocą @profile i uruchomić ją poleceniem python -m memory_profiler script.py. W Jupyterze można użyć magii %memit (z rozszerzenia ipython), aby zmierzyć szczytowe zużycie pamięci przez pojedyncze wyrażenie. Pozwala to wskazać struktury danych powodujące nagłe skoki zużycia pamięci — często jest to konkatenacja w pętli zamiast zebrania danych i wykonania jednej konkatenacji.
# Install: pip install memory-profiler
# Usage as a decorator:
# from memory_profiler import profile
# @profile
# def process_data():
# import pandas as pd, numpy as np
# df = pd.DataFrame({'x': np.random.randn(1000000)})
# result = df['x'].rolling(100).mean() # <-- sees if this spikes RAM
# return result
# Run: python -m memory_profiler script.py
# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()
print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')sys.getsizeof a memory_usage dla obiektów
sys.getsizeof(obj) zwraca rozmiar pamięci obiektu Pythona, ale w przypadku DataFrame pandas raportuje tylko narzut kontenera (kilkaset bajtów), a nie rzeczywiste dane. Dla DataFrame zawsze należy używać df.memory_usage(deep=True).sum(). W przypadku pojedynczych obiektów Pythona, takich jak listy i słowniki, sys.getsizeof jest dokładne, ale nie przechodzi rekurencyjnie przez zagnieżdżone obiekty — do pomiaru pełnego rozmiaru obiektów należy użyć biblioteki pympler (asizeof).
import sys
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.randn(100000)})
# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')
# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')Wzorzec menedżera kontekstu do pomiaru czasu
W przypadku dłuższych etapów potoku, dla których timeit nie jest odpowiedni (np. zapytań do bazy danych lub operacji wejścia-wyjścia), należy użyć prostego menedżera kontekstu z time.perf_counter(). Każdy etap potoku należy opakować w kontekst mierzący czas i rejestrujący jego trwanie. Jest to praktyczniejsze niż timeit w kodzie produkcyjnym, ponieważ rejestruje rzeczywisty czas zegarowy wraz z oczekiwaniem na operacje wejścia-wyjścia, który timeit również mierzy, ale trudniej zintegrować go ze strukturami logowania.
import time
import contextlib
import pandas as pd
import numpy as np
@contextlib.contextmanager
def timer(name):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f'[{name}] {elapsed*1000:.1f} ms')
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})
with timer('rolling mean'):
result1 = df['x'].rolling(30).mean()
with timer('groupby mean'):
df['group'] = df['x'].round(0)
result2 = df.groupby('group')['y'].mean()Systematyczne profilowanie operacji Pandas
Systematyczny proces profilowania: 1) Rozpocznij od reprezentatywnej próbki danych (10 000 wierszy wystarczy do porównywania czasów). 2) Zidentyfikuj najwolniejsze kroki za pomocą kontekstu pomiaru czasu lub cProfile. 3) Przepisz najwolniejszy krok, używając szybszego podejścia (wektoryzacja, typ Categorical, rozszerzenie C). 4) Zweryfikuj poprawność za pomocą instrukcji assert. 5) Zmierz czas ponownie, aby potwierdzić przyspieszenie. 6) Powtarzaj te czynności dla kolejnego wąskiego gardła, aż potok spełni wymagania SLA.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'value': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})
# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)
print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype: {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')Kiedy profilować: praktyczne progi
Nie każdy skrypt wymaga profilowania. Warto profilować, gdy: wykonanie potoku trwa ponad 30 sekund i jest on uruchamiany często (codziennie lub częściej), funkcja przetwarza ponad 100 000 wierszy albo zużycie pamięci przekracza 50% dostępnej pamięci RAM. W przypadku skryptów uruchamianych jednorazowo lub wykonujących się w ciągu kilku sekund czytelność jest ważniejsza niż optymalizacja do mikrosekund. Koszt utrzymywania silnie zoptymalizowanego, nieczytelnego kodu często przewyższa czas zaoszczędzony dzięki optymalizacji.
Podsumowanie narzędzi do profilowania
Szybka ściąga:
- timeit.timeit — mierzy czas wykonania pojedynczej instrukcji (skryptu)
- %timeit / %%timeit — mierzy czas w Jupyterze (automatycznie powtarza pomiar)
- %prun / cProfile — profilowanie stosu wywołań na poziomie funkcji
- df.memory_usage(deep=True) — pomiar użycia pamięci RAM dla poszczególnych kolumn
- memory_profiler @profile — pomiar użycia pamięci RAM w podziale na wiersze kodu
- time.perf_counter() — ręczny pomiar czasu kroków potoku
Szybki test
Sprawdź swoją wiedzę na temat narzędzi do profilowania z tego урокu.
Podsumowanie lekcji
W tej lekcji poznali Państwo: timeit.timeit i %timeit służą do mierzenia czasu wykonania konkretnych operacji, df.memory_usage(deep=True) dokładnie mierzy użycie pamięci RAM przez poszczególne kolumny, a cProfile / memory_profiler zapewniają bardziej szczegółowe profilowanie złożonych potoków. Zawsze profiluj kod przed optymalizacją i sprawdzaj poprawność po każdej zmianie. W następnej części omówimy sposoby unikania powolnych pętli iterrows oraz zastępowania ich operacjami wektorowymi.
Często zadawane pytania
Czy lekcja „Profilowanie za pomocą timeit i memory_profiler” jest bezpłatna?
Tak — pełny tekst „Profilowanie za pomocą timeit i memory_profiler” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Pandas & NumPy Academy, przejdź na CoddyKit PRO. Kurs Pandas & NumPy Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Profilowanie za pomocą timeit i memory_profiler”?
Mierz czas wykonania za pomocą %timeit i szczytowe zużycie pamięci za pomocą memory_profiler, aby znaleźć najwolniejsze części potoku. Ćwiczysz Pandas & NumPy Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Pandas & NumPy Academy?
Nie wymagamy żadnego doświadczenia. Pandas & NumPy Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.
Ile czasu zajmuje lekcja „Profilowanie za pomocą timeit i memory_profiler”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Pandas & NumPy Academy?
Tak. Każda lekcja Pandas & NumPy Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Profilowanie za pomocą timeit i memory_profiler
- Unikanie iterrows i pętli Pythona
- Wydajne typy danych do redukcji pamięci
- Odczyt dużych plików partiami