Proč byste měli přestat psát for cykly v NumPy
Ruku na srdce — pokud pracujete s daty v Pythonu, určitě jste si někdy všimli, že for cykly nad velkými poli jsou bolestně pomalé. Zpracování milionu čísel cyklem klidně zabere stovky milisekund. NumPy totéž zvládne pod dvě milisekundy. To není vylepšení o pár procent, to je úplně jiná liga: 50× až 200× rychlejší výpočty, a to bez jediného řádku C kódu.
Tajemství se jmenuje vektorizace.
Místo toho, abyste iterovali přes jednotlivé prvky, popíšete operaci nad celým polem najednou. NumPy ji pak provede interně v optimalizovaném C kódu s využitím SIMD instrukcí a cache-friendly paměťového rozložení. V tomhle článku si ukážeme, jak vektorizaci prakticky využít, kdy sáhnout po broadcasting, a taky kde narazíte na její limity (protože ty samozřejmě existují).
Co je vektorizace a proč je NumPy tak rychlé
Vektorizace v podstatě znamená nahrazení explicitních Python cyklů operacemi nad celými poli. Místo procházení prvků jeden po druhém řeknete NumPy „vynásob tato dvě pole" — a knihovna celou operaci provede naráz v kompilovaném C kódu.
Ale proč jsou ty Python cykly vůbec tak pomalé? Python je dynamicky typovaný jazyk. Každý prvek v běžném Python seznamu je objekt s vlastní hlavičkou, typovou informací a počítadlem referencí. Při průchodu cyklem musí interpret pro každý prvek:
- Rozbalit objekt a zjistit jeho typ
- Vyhledat příslušnou metodu pro požadovanou operaci
- Provést samotnou operaci
- Zabalit výsledek zpět do Python objektu
To je spousta režie na každé jedno číslo.
NumPy pole (ndarray) naproti tomu ukládá data v souvislém bloku paměti jako homogenní C typy (třeba float64). Vektorizovaná operace přeskočí veškerou režii Python interpretru a zpracuje celý blok dat jedním voláním optimalizované C funkce. Rozdíl je dramatický.
Benchmark: cyklus vs. vektorizace
Pojďme si ten rozdíl změřit na jednoduché operaci — umocnění 10 milionů čísel na druhou:
import numpy as np
import time
n = 10_000_000
data = np.random.rand(n)
# Python cyklus
start = time.perf_counter()
result_loop = [x ** 2 for x in data]
loop_time = time.perf_counter() - start
# NumPy vektorizace
start = time.perf_counter()
result_vec = data ** 2
vec_time = time.perf_counter() - start
print(f"Python cyklus: {loop_time:.3f} s")
print(f"NumPy vektorizace: {vec_time:.4f} s")
print(f"Zrychlení: {loop_time / vec_time:.0f}×")
Typický výstup na moderním stroji (NumPy 2.4, Python 3.12):
Python cyklus: 2.410 s
NumPy vektorizace: 0.0189 s
Zrychlení: 128×
128× zrychlení. A to je ještě poměrně konzervativní číslo — s rostoucí velikostí dat se běžně dostanete na 100× a více. Osobně jsem na jednom projektu viděl i 250× na operaci nad řídkou maticí, ale to už záleží na konkrétním hardware.
Základní vektorizované operace
Většina matematických operací v NumPy je nativně vektorizovaná. Stačí je aplikovat přímo na pole — žádné cykly, žádné komplikace:
Aritmetické operace po prvcích
import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])
# Všechny operace probíhají po prvcích — žádný cyklus
soucet = a + b # [11, 22, 33, 44, 55]
rozdil = b - a # [ 9, 18, 27, 36, 45]
soucin = a * b # [10, 40, 90, 160, 250]
podil = b / a # [10., 10., 10., 10., 10.]
mocnina = a ** 2 # [ 1, 4, 9, 16, 25]
Agregační funkce
Místo vlastního cyklu pro výpočet součtu nebo průměru prostě použijte vestavěné metody. Je to jednodušší a řádově rychlejší:
data = np.random.rand(1_000_000)
prumer = data.mean() # průměr
smerodatna = data.std() # směrodatná odchylka
soucet = data.sum() # součet
minimum = data.min() # minimum
maximum = data.max() # maximum
kumulativni = data.cumsum() # kumulativní součet
# Podél konkrétní osy u vícerozměrných polí
matice = np.random.rand(1000, 50)
prumery_sloupcu = matice.mean(axis=0) # průměr každého sloupce
maxima_radku = matice.max(axis=1) # maximum každého řádku
Všechny tyhle funkce běží v C a jsou dramaticky rychlejší než ekvivalentní Python kód s cykly. Tady opravdu není důvod vynalézat kolo.
Broadcasting: operace s poli různých tvarů
Tady to začíná být opravdu zajímavé. Broadcasting je jeden z nejvýkonnějších konceptů v NumPy — a upřímně, když jsem ho poprvé pochopil, změnil mi způsob, jakým přemýšlím o numerických výpočtech.
Broadcasting umožňuje provádět aritmetické operace nad poli různých rozměrů, aniž byste museli data kopírovat nebo ručně rozšiřovat.
Jak broadcasting funguje
Když NumPy narazí na operaci mezi dvěma poli s rozdílnými tvary, pokusí se je „natáhnout" tak, aby byly kompatibilní. Pravidla jsou vlastně docela jednoduchá:
- Pokud mají pole různý počet dimenzí, menší pole se doplní jedničkami zleva
- Pole s rozměrem 1 v dané ose se „natáhne" na rozměr druhého pole
- Pokud rozměry nejsou v žádné ose stejné ani rovné 1, operace selže
# Příklad 1: Přičtení skaláru k poli
a = np.array([1, 2, 3])
vysledek = a + 10 # [11, 12, 13] — skalár se "natáhne"
# Příklad 2: Přičtení vektoru ke každému řádku matice
matice = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
vektor = np.array([10, 20, 30])
vysledek = matice + vektor
# [[11, 22, 33],
# [14, 25, 36],
# [17, 28, 39]]
Praktický příklad: Z-score normalizace
Normalizace dat je jedna z nejčastějších operací v datové vědě. Díky broadcasting ji zapíšete na jeden řádek — a to není přehánění:
# Dataset: 1000 vzorků, 50 příznaků
data = np.random.rand(1000, 50) * 100
# Z-score normalizace — broadcasting automaticky aplikuje
# průměry a odchylky (tvar 50,) na každý řádek (tvar 1000×50)
prumery = data.mean(axis=0) # tvar (50,)
odchylky = data.std(axis=0) # tvar (50,)
normalizovano = (data - prumery) / odchylky
# Ověření: každý sloupec má průměr ≈ 0 a odchylku ≈ 1
print(normalizovano.mean(axis=0)[:5]) # [≈0, ≈0, ≈0, ≈0, ≈0]
print(normalizovano.std(axis=0)[:5]) # [≈1, ≈1, ≈1, ≈1, ≈1]
Bez broadcasting byste museli psát cyklus přes 50 sloupců. S ním? Jeden řádek a NumPy zbytek vyřeší interně — bez kopírování dat. To je docela elegantní, nemyslíte?
Podmíněná logika bez cyklů: np.where() a boolean indexing
Jedna z nejčastějších situací, kde programátoři reflexivně sahají po for cyklech, je podmíněná úprava hodnot. „Když je hodnota záporná, nastav nulu." „Když je teplota nad 25, označ jako teplo." Klasika.
NumPy nabízí dva elegantní nástroje, které cykly zcela eliminují.
np.where() — vektorizovaný if/else
data = np.array([15, -3, 42, -8, 0, 27, -1])
# Nahradit záporné hodnoty nulou — BEZ cyklu
vysledek = np.where(data < 0, 0, data)
# [15, 0, 42, 0, 0, 27, 0]
# Klasifikace: "vysoké" vs "nízké" teploty
teploty = np.random.uniform(-10, 40, size=1_000_000)
kategorie = np.where(teploty > 25, 1, 0) # 1 = teplo, 0 = chladno
Vnořený np.where() pro více kategorií
Tohle vypadá trochu šíleně, ale funguje to spolehlivě:
skore = np.array([92, 67, 45, 88, 73, 51, 95, 30])
znamky = np.where(skore >= 90, "A",
np.where(skore >= 75, "B",
np.where(skore >= 60, "C",
np.where(skore >= 50, "D", "F"))))
# ['A', 'C', 'F', 'B', 'C', 'D', 'A', 'F']
Boolean indexing — filtrování a úprava podmnožin
ceny = np.array([120, 450, 89, 1200, 55, 890, 340])
# Vybrat pouze ceny nad 100
vysoke_ceny = ceny[ceny > 100] # [120, 450, 1200, 890, 340]
# Kombinace podmínek
stredni_ceny = ceny[(ceny >= 100) & (ceny <= 500)] # [120, 450, 340]
# Modifikace podmnožiny: zastropovat ceny na 500
ceny_kopie = ceny.copy()
ceny_kopie[ceny_kopie > 500] = 500
# [120, 450, 89, 500, 55, 500, 340]
Obě techniky jsou plně vektorizované a běží v C. Na milionu prvků jsou typicky 50–100× rychlejší než ekvivalentní Python cyklus s if/else. To prostě stojí za to.
Fancy indexing a pokročilé výběry
NumPy umožňuje indexovat pole jiným polem — takzvaný fancy indexing. Zní to možná zvláštně, ale je to mimořádně užitečné pro přeuspořádání dat, výběr náhodných vzorků nebo práci s lookup tabulkami.
# Výběr konkrétních prvků podle indexů
data = np.array([10, 20, 30, 40, 50, 60, 70])
indexy = np.array([0, 3, 5])
vyber = data[indexy] # [10, 40, 60]
# Přeuspořádání řádků matice
matice = np.arange(20).reshape(5, 4)
poradi = np.array([4, 2, 0, 3, 1])
preusporadano = matice[poradi]
# Praktické využití: mapování kategorií na číselné hodnoty
kategorie = np.array([2, 0, 1, 2, 1, 0, 2])
nazvy = np.array(["nízká", "střední", "vysoká"])
popisky = nazvy[kategorie]
# ['vysoká', 'nízká', 'střední', 'vysoká', 'střední', 'nízká', 'vysoká']
Ten poslední příklad s mapováním kategorií je něco, co v praxi používám pořád. Žádné slovníky, žádné cykly — prostě pole jako index do jiného pole.
Reálné příklady z datové vědy
Dost teorie, pojďme se podívat na několik praktických scénářů, kde vektorizace opravdu zazáří.
Euklidovská matice vzdáleností
Výpočet vzdáleností mezi všemi páry bodů je běžný úkol v klastrovací analýze. Naivní přístup s dvěma vnořenými cykly? O(n²) iterací v pomalém Pythonu. S broadcasting to ale zvládnete na jeden řádek:
# 1000 bodů ve 3D prostoru
body = np.random.rand(1000, 3)
# Broadcasting: (1000,1,3) - (1,1000,3) → (1000,1000,3)
rozdily = body[:, np.newaxis, :] - body[np.newaxis, :, :]
vzdalenosti = np.sqrt((rozdily ** 2).sum(axis=2))
print(vzdalenosti.shape) # (1000, 1000)
print(f"Průměrná vzdálenost: {vzdalenosti.mean():.4f}")
Ten trik s np.newaxis je klíčový. Přidáním nové dimenze vytvoříte tvar kompatibilní pro broadcasting a NumPy se postará o zbytek.
One-hot kódování bez cyklů
kategorie = np.array([0, 2, 1, 0, 3, 2, 1])
pocet_trid = 4
# Broadcasting: porovnání vektoru s rozsahem tříd
one_hot = (kategorie[:, np.newaxis] == np.arange(pocet_trid)).astype(int)
print(one_hot)
# [[1 0 0 0]
# [0 0 1 0]
# [0 1 0 0]
# [1 0 0 0]
# [0 0 0 1]
# [0 0 1 0]
# [0 1 0 0]]
Klouzavý průměr
Klouzavý průměr je klasika v časových řadách. Tady jsou dva čistě vektorizované přístupy:
casova_rada = np.random.rand(10000)
okno = 50
# Pomocí np.convolve — plně vektorizované
jadro = np.ones(okno) / okno
klouzavy_prumer = np.convolve(casova_rada, jadro, mode="valid")
# Alternativa s cumsum (ještě rychlejší pro velká okna)
cumsum = np.cumsum(casova_rada)
cumsum = np.insert(cumsum, 0, 0)
klouzavy_prumer_2 = (cumsum[okno:] - cumsum[:-okno]) / okno
Ten trik s cumsum je mimochodem geniálně jednoduchý — místo opakovaného sčítání okna využijete předpočítaný kumulativní součet. Na velkých oknech je to znatelně rychlejší.
np.vectorize() — kdy ano a kdy ne
Funkce np.vectorize() vypadá na první pohled lákavě — vezme libovolnou Python funkci a „zvektorizuje" ji. Jenže pozor, tohle je past: np.vectorize() nepřináší žádné zrychlení. Pod kapotou stále volá vaši funkci prvek po prvku v Python interpretru.
# NEEFEKTIVNÍ — nepřináší zrychlení!
def klasifikuj(x):
if x > 0.8:
return "výborný"
elif x > 0.5:
return "dobrý"
else:
return "slabý"
vfunkce = np.vectorize(klasifikuj)
vysledky = vfunkce(np.random.rand(100000))
# Funguje, ale je stejně pomalé jako for cyklus
np.vectorize() má smysl pouze pro přehlednost kódu, nikoliv pro výkon. Je to v podstatě jen hezčí zápis for cyklu. Pokud potřebujete skutečnou rychlost, přepište logiku pomocí np.where(), np.select() nebo boolean indexing:
# EFEKTIVNÍ — skutečná vektorizace
data = np.random.rand(100000)
vysledky = np.select(
[data > 0.8, data > 0.5],
["výborný", "dobrý"],
default="slabý"
)
Tohle je reálná vektorizace a rozdíl ve výkonu pozná i laik.
Měření výkonu: jak správně benchmarkovat
Mluvíme tu pořád o zrychlení, ale jak ho vlastně správně změřit? Jednorázové time.time() volání nestačí — výsledky jsou příliš nestabilní. Použijte raději modul timeit nebo (v Jupyter Notebooku) magic příkaz %timeit:
import timeit
import numpy as np
n = 1_000_000
data = np.random.rand(n)
# Benchmark: Python cyklus
cas_cyklus = timeit.timeit(
"sum(x ** 2 for x in data)",
globals={"data": data},
number=10
) / 10
# Benchmark: NumPy vektorizace
cas_numpy = timeit.timeit(
"np.sum(data ** 2)",
globals={"data": data, "np": np},
number=10
) / 10
print(f"Python cyklus: {cas_cyklus*1000:.1f} ms")
print(f"NumPy: {cas_numpy*1000:.2f} ms")
print(f"Zrychlení: {cas_cyklus/cas_numpy:.0f}×")
Přehled typických zrychlení
Následující tabulka shrnuje typická zrychlení pro běžné operace na poli o milionu prvků (měřeno na Apple M2, NumPy 2.4):
| Operace | Python cyklus | NumPy | Zrychlení |
|---|---|---|---|
| Součet prvků | ~85 ms | ~0.7 ms | ~120× |
| Umocnění na druhou | ~240 ms | ~1.9 ms | ~128× |
| Podmíněný výběr | ~190 ms | ~2.5 ms | ~76× |
| Skalární součin | ~310 ms | ~0.8 ms | ~390× |
| Z-score normalizace | ~520 ms | ~5.1 ms | ~102× |
Ten skalární součin je obzvlášť impozantní — 390× zrychlení. NumPy tam využívá BLAS knihovny, které jsou optimalizované až na úroveň jednotlivých procesorových instrukcí.
Novinky ve výkonu NumPy 2.4
NumPy 2.4 (vydaný v prosinci 2025, aktuální patch 2.4.3 z března 2026) přinesl několik zajímavých vylepšení, o kterých stojí za to vědět:
- numpy.ndindex zrychlení — interní přechod na
itertools.productpřináší až 5,2× zrychlení při iteraci přes vícerozměrná pole - Hash-based np.unique pro komplexní typy — nový hashovací algoritmus je 1,4–5× rychlejší než řazení pro pole s
complex128hodnotami - Masivní zrychlení np.unique pro stringy — na velkých řetězcových polích je nový algoritmus řádově rychlejší (benchmark ukazuje 33,5 s vs. 498 s na miliardě prvků, to je pořádný skok)
- Příprava na free-threaded Python — ufunce nyní používají lock-free dispatch tabulku, což výrazně zlepšuje škálovatelnost na vícevláknových buildech CPythonu
# Využití zrychleného np.unique na velkých řetězcových polích
import numpy as np
# Simulace logových dat — 1M záznamů s 100 unikátními hodnotami
logy = np.random.choice(
[f"event_type_{i}" for i in range(100)],
size=1_000_000
)
unikatni = np.unique(logy)
print(f"Nalezeno {len(unikatni)} unikátních hodnot")
Kdy vektorizace nestačí: Numba a další nástroje
Bylo by naivní tvrdit, že vektorizace v NumPy vyřeší všechno. Existují situace, kdy na ni prostě nemůžete spoléhat:
- Iterativní algoritmy — výpočet závisí na předchozím kroku (simulace, některé optimalizační algoritmy, dynamické programování)
- Složitá větvení — když podmíněná logika nelze rozumně vyjádřit pomocí
np.where()nebonp.select() - Paměťové omezení — vektorizace vytváří dočasná pole, což u velmi velkých dat může vyčerpat RAM (a to se stane rychleji, než byste čekali)
V těchto případech je skvělou volbou Numba:
# Numba — JIT kompilace Python kódu do strojového kódu
from numba import njit
import numpy as np
@njit
def simulace_nahodne_prochazky(n_kroku):
pozice = 0.0
maximum = 0.0
for i in range(n_kroku):
krok = np.random.randn()
pozice += krok
if pozice > maximum:
maximum = pozice
return maximum
# První volání kompiluje — další volání jsou bleskurychlá
vysledek = simulace_nahodne_prochazky(10_000_000)
Numba přeloží Python cyklus do nativního strojového kódu. Výsledek je srovnatelný s čistým C — a přitom pořád píšete normální Python. Pro iterativní algoritmy je to v podstatě nejlepší nástroj, jaký máte k dispozici.
Praktická pravidla pro vektorizaci
Na závěr shrnutí postupů, které vám pomohou psát rychlý NumPy kód. Berte to jako takový checklist:
- Myslete v polích, ne v prvcích — Ptejte se „jakou transformaci chci aplikovat na celé pole", ne „co udělám s každým prvkem"
- Vyhněte se Python cyklům nad NumPy poli — Kdykoli napíšete
for x in np_array, zastavte se a hledejte vektorizovanou alternativu - Využívejte broadcasting — Místo ručního rozšiřování polí nechte NumPy, ať si rozměry vyřeší sám
- np.where() místo if/else — Pro podmíněné operace na polích, vždycky
- Pozor na np.vectorize() — Nezrychlí váš kód, jenom zjednoduší syntaxi
- Měřte, než optimalizujete — Použijte
timeitnebo%timeit, ne odhady od oka - Sledujte paměť — Vektorizace může vytvářet velká dočasná pole; u obřích datasetů zvažte zpracování po částech
- Numba pro cykly, kterým se nevyhnete — Když vektorizace nestačí,
@njitje nejsnazší cesta k nativnímu výkonu
Často kladené otázky (FAQ)
Je NumPy vždy rychlejší než Python cyklus?
Ne vždy. Pro velmi malá pole (desítky prvků) může být režie volání NumPy funkce vyšší než samotný cyklus. Výhoda vektorizace se naplno projeví od stovek prvků výše a roste s velikostí dat. U milionů prvků je NumPy typicky 50–200× rychlejší.
Jaký je rozdíl mezi vektorizací a broadcasting?
Vektorizace je obecný princip — provádění operací nad celými poli místo jednotlivých prvků. Broadcasting je specifický mechanismus v NumPy, který umožňuje vektorizované operace nad poli s různými tvary. Takže broadcasting je v podstatě podmnožina vektorizace — nástroj, který ji rozšiřuje na pole s odlišnými rozměry.
Může np.vectorize() nahradit skutečnou vektorizaci?
Upřímně? Ne. Funkce np.vectorize() je pouze syntaktická pomůcka — interně stále volá vaši Python funkci prvek po prvku. Pro skutečné zrychlení použijte nativní NumPy operace (np.where(), np.select(), boolean indexing) nebo JIT kompilátor jako Numba.
Kolik paměti navíc vektorizace potřebuje?
Vektorizované operace typicky vytvářejí dočasná pole stejné velikosti jako vstupní data. Výraz (data - mean) / std vytvoří dvě dočasná pole. Pro pole o milionu float64 hodnot to znamená cca 16 MB navíc. U velmi velkých dat zvažte zpracování po částech nebo použití parametru out= pro in-place operace.
Jak si vybrat mezi NumPy vektorizací a Numba?
Jednoduché pravidlo: pokud se vaše operace dá vyjádřit jako kombinace NumPy funkcí a broadcasting — použijte NumPy. Pokud potřebujete složitou iterativní logiku, kde každý krok závisí na předchozím (simulace, dynamické programování), sáhněte po Numba s dekorátorem @njit. Numba přeloží Python cyklus do strojového kódu a výkon bude srovnatelný s C.