0Pricing
Pandas & NumPy Academy · Lekcja

Unikanie iterrows i pętli Pythona

Zastępuj pętle wykonywane wiersz po wierszu zwektoryzowanymi operacjami na kolumnach, np.where i pd.cut, aby uzyskać przyspieszenie 10–100 razy.

Unikanie iterrows i pętli Pythona to bezpłatna lekcja Pandas & NumPy Academy na CoddyKit. To lekcja 2 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.

Koszt iteracji wiersz po wierszu

Pandas jest zbudowany na bazie NumPy, który przetwarza całe tablice jednocześnie, korzystając ze skompilowanego kodu C. Podczas iterowania po wierszach za pomocą for _, row in df.iterrows() omijają Państwo ten mechanizm i wracają do czystego Pythona — każdy wiersz jest wyodrębniany jako obiekt Series, a pętla działa w interpreterze Pythona, co kosztuje około 100–1000 razy więcej niż równoważna operacja wektorowa. W przypadku DataFrame zawierającego 1 milion wierszy może to oznaczać minuty zamiast milisekund.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})

# Slow: iterrows loop
def loop_version(df):
    result = []
    for _, row in df.iterrows():
        result.append(row['a'] + row['b'])
    return pd.Series(result)

# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)

print(f'Loop (5 runs):       {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')

Zastępowanie pętli warunkowych za pomocą np.where

np.where(condition, value_if_true, value_if_false) to zwektoryzowany odpowiednik instrukcji if/else wykonywanej dla poszczególnych elementów. Zamiast iterować po wierszach i zapisywać instrukcję if, należy przekazać warunek oraz obie wartości wynikowe jako tablice. np.where oblicza wynik w języku C dla całej kolumny jednocześnie, dzięki czemu w przypadku dużych DataFrame jest 50–200 razy szybsze od równoważnej pętli w Pythonie.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})

# Slow: iterrows
def label_loop(df):
    labels = []
    for _, row in df.iterrows():
        if row['price'] > 50:
            labels.append('expensive')
        else:
            labels.append('cheap')
    return labels

# Fast: np.where
def label_vectorised(df):
    return np.where(df['price'] > 50, 'expensive', 'cheap')

# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])

Wiele warunków za pomocą np.select

W przypadku trzech lub większej liczby warunków należy użyć np.select(condlist, choicelist, default=) zamiast zagnieżdżonych wywołań np.where. Należy przekazać listę tablic boolowskich oraz listę odpowiadających im wartości wyjściowych. Pierwszy pasujący warunek określa wynik, a wiersze niespełniające żadnego warunku otrzymują wartość default. Zastępuje to złożone łańcuchy if/elif/else wewnątrz pętli przejrzystym, zwektoryzowanym wyrażeniem.

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})

conditions = [
    df['score'] >= 90,
    df['score'] >= 75,
    df['score'] >= 60
]
choices = ['A', 'B', 'C']

df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))

Wektoryzowane operacje na ciągach znaków za pomocą .str

Manipulowanie ciągami znaków jest częstym źródłem powolnych pętli. Akcesor Pandas .str udostępnia zwektoryzowane odpowiedniki wszystkich metod ciągów znaków w Pythonie: .str.lower(), .str.strip(), .str.replace(), .str.contains() i wiele innych. Używanie metod .str jest 10–50 razy szybsze niż iterowanie po wierszach i ręczne wywoływanie metod ciągów znaków w Pythonie.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', '  carol  ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})

# Slow: loop
def clean_loop(df):
    return [n.strip().title() for n in df['name']]

# Fast: .str accessor
def clean_vectorised(df):
    return df['name'].str.strip().str.title()

t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)

print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())

Używanie pd.cut i pd.qcut zamiast pętli

pd.cut() i pd.qcut() wektoryzują operacje grupowania wartości w przedziały, które często są zapisywane jako pętle z wieloma warunkami if/elif. Jeśli piszą Państwo w pętli po wierszach kod w rodzaju 'if value < 18: age_group = child elif value < 65: age_group = adult', należy zastąpić go pojedynczym wywołaniem pd.cut(), które przetworzy całą kolumnę jednocześnie z szybkością kodu C.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})

# Slow: loop with conditionals
def categorise_loop(df):
    result = []
    for age in df['age']:
        if age < 18:
            result.append('child')
        elif age < 65:
            result.append('adult')
        else:
            result.append('senior')
    return result

# Fast: pd.cut
def categorise_cut(df):
    return pd.cut(df['age'], bins=[0, 18, 65, 100],
                  labels=['child', 'adult', 'senior'],
                  right=False)

assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())

apply() nie zawsze jest odpowiedzią

Wiele poradników zaleca zastępowanie pętli wywołaniem .apply(func), jednak apply również wewnętrznie wykonuje pętlę na poziomie Pythona — jest tylko nieznacznie szybsze od iterrows i znacznie wolniejsze od prawdziwej wektoryzacji. Należy używać apply tylko wtedy, gdy nie istnieje zwektoryzowana alternatywa (na przykład przy złożonej logice obejmującej wiele kolumn). Jeśli operację można wyrazić za pomocą wbudowanej funkcji Pandas lub NumPy, zawsze należy wybrać ją zamiast apply.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(100000)})

# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)

print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')

Zastępowanie pętli groupby za pomocą agg i transform

Typowy wzorzec polega na ręcznym iterowaniu po grupach: for name, group in df.groupby('cat'): do_something(group). Jest to powolne z tych samych powodów co iterrows. Należy zastąpić je wywołaniem groupby().agg() w celu tworzenia statystyk podsumowujących albo groupby().transform() w celu rozpropagowania statystyk na poziomie grupy z powrotem do pozycji oryginalnych wierszy.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'cat': np.random.choice(['A','B','C','D'], 100000),
    'val': np.random.randn(100000)
})

# Slow: manual loop to add group mean
def slow_group_mean(df):
    means = {}
    for name, group in df.groupby('cat'):
        means[name] = group['val'].mean()
    return df['cat'].map(means)

# Fast: transform
def fast_group_mean(df):
    return df.groupby('cat')['val'].transform('mean')

t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop:       {t1/10*1000:.1f} ms')
print(f'transform:  {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')

Kiedy iterrows jest dopuszczalne

Mimo niskiej wydajności istnieją uzasadnione zastosowania iterrows i itertuples: wyświetlanie lub rejestrowanie informacji z wierszy (wydajność nie ma znaczenia), tworzenie danych żądań API, gdy każdy wiersz powoduje wywołanie HTTP (dominuje opóźnienie sieci), oraz debugowanie konkretnych wierszy ze złożonymi warunkami. Jeśli mają Państwo mniej niż 1000 wierszy, a operacja jest wykonywana raz, różnica między pętlą a wektoryzacją wynosi milisekundy — nie uzasadnia to dodatkowej złożoności kodu.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'order_id': [101, 102, 103],
    'product': ['Widget', 'Gadget', 'Doohickey'],
    'amount': [29.99, 49.99, 9.99]
})

# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
    print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')

itertuples jest szybsze niż iterrows

Jeśli muszą Państwo iterować po wierszach w Pythonie (ponieważ nie istnieje zwektoryzowany odpowiednik), należy użyć itertuples() zamiast iterrows(). Zwraca ono każdy wiersz jako krotkę nazwaną, a nie jako Pandas Series, dzięki czemu unika narzutu związanego z tworzeniem obiektu Series. Zwykle sprawia to, że jest 5–10 razy szybsze od iterrows. Do wartości należy odwoływać się za pomocą notacji atrybutów: row.column_name. Należy unikać tego rozwiązania, jeśli nazwy kolumn zawierają spacje (nie są prawidłowymi nazwami atrybutów).

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})

t1 = timeit.timeit(
    lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
    lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)

print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows:   {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')

Lista kontrolna wzorców wektoryzacji

Przed napisaniem pętli należy zadać sobie następujące pytania:

  • Czy operacja jest wykonywana element po elemencie w jednej kolumnie? → Należy użyć wyrażenia arytmetycznego dla kolumny lub funkcji ufunc NumPy.
  • Czy obejmuje logikę warunkową? → Należy użyć np.where (2 warunki) lub np.select (3 lub więcej).
  • Czy przypisuje wartości do przedziałów? → Należy użyć pd.cut lub pd.qcut.
  • Czy wykonuje operacje na ciągach znaków? → Należy użyć akcesora .str.
  • Czy agreguje dane w grupach? → Należy użyć groupby().agg() lub groupby().transform().
Po apply() lub itertuples() należy sięgać tylko wtedy, gdy żadne z powyższych rozwiązań nie ma zastosowania.

Przykład rzeczywistego przyspieszenia: obliczanie ceny

Oto kompletna refaktoryzacja przed i po dla typowego obliczenia biznesowego — zastosowania rabatów progowych zależnych od ilości zamówienia. Wersja z pętlą jest czytelna, ale w przypadku dużych DataFrame niedopuszczalnie powolna. Wersja zwektoryzowana, wykorzystująca np.select, uzyskuje ten sam wynik w jednej dziesiątej czasu.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'price': np.random.uniform(10, 200, 100000),
    'qty': np.random.randint(1, 500, 100000)
})

# BEFORE: loop with conditionals
def slow_discount(df):
    result = []
    for _, row in df.iterrows():
        if row['qty'] >= 100:
            disc = 0.2
        elif row['qty'] >= 50:
            disc = 0.1
        elif row['qty'] >= 10:
            disc = 0.05
        else:
            disc = 0.0
        result.append(row['price'] * (1 - disc))
    return pd.Series(result)

# AFTER: np.select
def fast_discount(df):
    conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
    discounts = [0.20, 0.10, 0.05]
    disc = np.select(conditions, discounts, default=0.0)
    return df['price'] * (1 - disc)

assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')

Szybki test

Sprawdź swoją wiedzę na temat wektoryzacji z tej lekcji.

Podsumowanie lekcji

W tej lekcji poznali Państwo: iterrows jest 100–1000 razy wolniejsze od operacji wektorowych, np.where i np.select zastępują pętle warunkowe, akcesor .str wektoryzuje operacje na ciągach znaków, a groupby().transform() zastępuje ręczne iterowanie po grupach. Gdy iterowanie jest konieczne, należy używać itertuples zamiast iterrows, aby uzyskać przyspieszenie 5–10 razy. W następnej części omówimy wydajne typy danych — zmniejszanie typów liczbowych i używanie Categorical w celu ograniczenia zużycia pamięci nawet o 70%.

Często zadawane pytania

Czy lekcja „Unikanie iterrows i pętli Pythona” jest bezpłatna?

Tak — pełny tekst „Unikanie iterrows i pętli Pythona” 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 „Unikanie iterrows i pętli Pythona”?

Zastępuj pętle wykonywane wiersz po wierszu zwektoryzowanymi operacjami na kolumnach, np.where i pd.cut, aby uzyskać przyspieszenie 10–100 razy. Ć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 2 z 4.

Ile czasu zajmuje lekcja „Unikanie iterrows i pętli Pythona”?

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

  1. Profilowanie za pomocą timeit i memory_profiler
  2. Unikanie iterrows i pętli Pythona
  3. Wydajne typy danych do redukcji pamięci
  4. Odczyt dużych plików partiami
← Powrót do Pandas & NumPy Academy