Fiszki Online NumPy (Preview)
Darmowy podgląd 15 z 32 dostępnych pytań
Podstawy NumPy
Czym jest NumPy i jakie problemy rozwiązuje w ekosystemie Pythona?
Odpowiedź w 30 sekund:
NumPy (ang. Numerical Python) to fundamentalna biblioteka do obliczeń numerycznych w Pythonie, dostarczająca wydajną, n-wymiarową tablicę ndarray oraz zestaw operacji wektorowych na niej. Rozwiązuje problem wolnych pętli na listach Pythona i braku jednorodnej, ciągłej w pamięci struktury danych do liczb. Jest podstawą całego ekosystemu naukowego: pandas, SciPy, scikit-learn, TensorFlow i PyTorch budują na NumPy.
Odpowiedź w 2 minuty:
Czysty Python świetnie nadaje się do skryptowania i logiki aplikacji, ale do obliczeń numerycznych ma dwie wady: lista Pythona to tablica wskaźników do rozproszonych w pamięci obiektów (każda liczba to pełnoprawny PyObject z narzutem), a operacje na elementach wymagają pętli interpretowanych instrukcja po instrukcji. Dla milionów liczb i operacji macierzowych jest to zarówno wolne, jak i pamięciożerne.
NumPy rozwiązuje to, wprowadzając typ ndarray — jednorodną, ciągłą w pamięci tablicę o ustalonym typie elementów (dtype). Dzięki temu dane mają zwartą reprezentację (jak tablica w C/Fortranie), a operacje są zwektoryzowane: zamiast pisać pętlę po elementach, wykonujemy a + b czy np.dot(A, B), a samo liczenie odbywa się w skompilowanym kodzie C/Fortran, bez narzutu interpretera Pythona.
Poza samym kontenerem danych NumPy dostarcza bogaty zestaw narzędzi: broadcasting (operacje na tablicach o różnych kształtach), funkcje matematyczne i statystyczne (mean, std, sum), algebrę liniową (numpy.linalg), transformaty Fouriera, generatory liczb losowych oraz mechanizmy indeksowania i krojenia (ang. slicing). NumPy definiuje też wspólny protokół tablicowy, dzięki któremu inne biblioteki mogą wymieniać dane bez kopiowania.
W praktyce NumPy jest „wspólnym mianownikiem" naukowego Pythona — to standard, w którym przechowuje się i przekazuje dane numeryczne między bibliotekami. Bez niego pandas, scikit-learn czy biblioteki uczenia głębokiego nie miałyby spójnej, wydajnej podstawy.
Przykład kodu:
import numpy as np
# Tworzymy tablicę z listy Pythona
dane = np.array([1.0, 2.0, 3.0, 4.0])
# Operacja zwektoryzowana — bez pętli, wykonana w C
podwojone = dane * 2 # array([2., 4., 6., 8.])
suma = dane.sum() # 10.0
srednia = dane.mean() # 2.5
# Operacje na całych tablicach naraz
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
wynik = a + b # array([11, 22, 33])
print(podwojone, suma, srednia, wynik)
Materiały
↑ Powrót na góręCzym jest ndarray i czym różni się od wbudowanej listy Pythona?
Odpowiedź w 30 sekund:
ndarray (ang. N-dimensional array) to podstawowy typ NumPy: jednorodna, n-wymiarowa tablica elementów tego samego typu, przechowywana w ciągłym bloku pamięci. W odróżnieniu od listy Pythona, która jest tablicą wskaźników do rozproszonych obiektów dowolnego typu, ndarray ma stały rozmiar, jeden dtype i wspiera szybkie operacje zwektoryzowane.
Odpowiedź w 2 minuty:
Lista Pythona to elastyczny, dynamiczny kontener: może przechowywać elementy różnych typów ([1, "tekst", 3.5, None]), rośnie i kurczy się, a pod spodem jest tablicą wskaźników do obiektów PyObject rozsianych po stercie. Ta elastyczność kosztuje: każdy element to osobny obiekt z nagłówkiem (typ, licznik referencji), a brak ciągłości pamięci szkodzi wydajności i utrudnia optymalizacje sprzętowe.
ndarray projektowany jest pod liczby. Przechowuje elementy jednego typu (dtype) w jednym ciągłym buforze pamięci, podobnie jak tablica w C. Tablica zna swój kształt (shape), liczbę wymiarów (ndim), typ (dtype) oraz tzw. strides — informacje, o ile bajtów przesunąć się w pamięci, by przejść do kolejnego elementu wzdłuż danej osi. Dzięki temu NumPy potrafi udostępniać podtablice i zmieniać kształt bez kopiowania danych (poprzez tzw. widoki).
Najważniejsze różnice praktyczne: rozmiar ndarray jest z reguły stały (zmiana rozmiaru tworzy nową tablicę), operacje wykonują się element po elemencie na całej tablicy naraz (a + b zamiast pętli), a sama tablica jest znacznie bardziej zwarta w pamięci. Lista wygrywa tam, gdzie potrzebna jest heterogeniczność i częste wstawianie/usuwanie elementów; ndarray wygrywa wszędzie tam, gdzie liczą się jednorodne dane numeryczne i wydajność.
flowchart LR
subgraph L["Lista Pythona"]
direction TB
LA["lista = tablica wskaznikow"]
LA --> P1["PyObject (int 1)"]
LA --> P2["PyObject (float 2.0)"]
LA --> P3["PyObject (str 'x')"]
P1 -.rozproszone w pamieci.-> P2
P2 -.-> P3
end
subgraph N["ndarray NumPy"]
direction TB
NA["ciagly blok pamieci, jeden dtype"]
NA --> B["[ 1 | 2 | 3 | 4 | 5 ] (int64, obok siebie)"]
end
Przykład kodu:
import numpy as np
lista = [1, 2, 3, 4]
tablica = np.array([1, 2, 3, 4])
# Lista akceptuje typy mieszane; ndarray ujednolica typ
mieszana = [1, "tekst", 3.5] # OK dla listy
jednorodna = np.array([1, 2, 3]) # dtype=int64
# Mnożenie listy POWIELA jej zawartość...
print(lista * 2) # [1, 2, 3, 4, 1, 2, 3, 4]
# ...a tablicy — mnoży każdy element (operacja matematyczna)
print(tablica * 2) # array([2, 4, 6, 8])
# ndarray zna swoje metadane
print(tablica.shape, tablica.ndim, tablica.dtype) # (4,) 1 int64
Materiały
↑ Powrót na góręDlaczego operacje na tablicach NumPy są zwykle szybsze niż pętle na listach Pythona?
Odpowiedź w 30 sekund: Bo NumPy wykonuje operacje zwektoryzowane w skompilowanym kodzie C/Fortran na ciągłym bloku pamięci o jednorodnym typie, zamiast iterować w interpreterze Pythona. Eliminuje to narzut pętli interpretera, dynamicznego typowania i tworzenia obiektów per element, a dodatkowo zyskuje na lokalności pamięci i instrukcjach SIMD procesora.
Odpowiedź w 2 minuty:
Pętla po liście w Pythonie jest kosztowna z kilku powodów naraz. Po pierwsze, narzut interpretera: każda iteracja to wykonanie bytecode'u w pętli ewaluacyjnej maszyny wirtualnej. Po drugie, dynamiczne typowanie: przy każdej operacji a + b interpreter musi sprawdzić typy operandów i wybrać właściwą implementację. Po trzecie, opakowywanie w obiekty: wyniki to nowe obiekty PyObject alokowane na stercie, z licznikami referencji. Po czwarte, rozproszona pamięć: elementy listy leżą w różnych miejscach, więc procesor traci czas na chybienia w pamięci podręcznej (ang. cache misses).
NumPy usuwa wszystkie te narzuty dzięki wektoryzacji. Operacja taka jak a + b na tablicach uruchamia jedną wywołaną funkcję C (tzw. ufunc), która w zwartej pętli w skompilowanym kodzie przetwarza cały bufor. Typ jest znany z góry (dtype), więc nie ma sprawdzania typów per element ani tworzenia obiektów pośrednich — pętla operuje na surowych liczbach w pamięci.
Dochodzą do tego korzyści sprzętowe. Ciągły blok pamięci o jednorodnym typie oznacza świetną lokalność danych — procesor efektywnie wykorzystuje pamięć podręczną i prefetcher. Co więcej, taka pętla w C nadaje się do wektoryzacji SIMD (ang. Single Instruction, Multiple Data), gdzie jedna instrukcja procesora przetwarza wiele liczb jednocześnie. Operacje algebry liniowej dodatkowo delegowane są do zoptymalizowanych bibliotek BLAS/LAPACK, często wielowątkowych.
Warto pamiętać o niuansach. Przewaga ujawnia się przy większych tablicach — dla kilku elementów narzut samego wywołania funkcji NumPy może przeważyć. Antywzorcem jest też iterowanie po ndarray pętlą Pythona (for x in tablica), co łączy wady obu światów. Klucz to myślenie tablicami: wyrażać obliczenia jako operacje na całych tablicach, używając broadcastingu i funkcji uniwersalnych zamiast jawnych pętli.
Przykład kodu:
import numpy as np
n = 1_000_000
lista_a = list(range(n))
lista_b = list(range(n))
# Podejście "Pythonowe" — pętla interpretowana, wolne
def suma_petla(a, b):
return [x + y for x, y in zip(a, b)]
# Podejście NumPy — wektoryzacja w C, szybkie
a = np.arange(n)
b = np.arange(n)
wynik = a + b # jedna operacja na całej tablicy
# Pomiar (np. w IPythonie / Jupyterze):
# %timeit suma_petla(lista_a, lista_b) -> ~setki ms
# %timeit a + b -> ~kilka ms
# Różnica rzędu 10-100x na korzyść NumPy
print(wynik[:5]) # array([0, 2, 4, 6, 8])
Materiały
- NumPy — Why is NumPy fast?
- NumPy — Universal functions (ufunc) basics
- NumPy — Memory layout / internals
Czym jest dtype i dlaczego jednorodność typów w tablicy ma znaczenie?
Odpowiedź w 30 sekund:
dtype (ang. data type) to obiekt opisujący typ i rozmiar w bajtach pojedynczego elementu tablicy NumPy — np. int64, float32, bool czy complex128. Wszystkie elementy ndarray mają ten sam dtype, co umożliwia ciągłą, zwartą reprezentację w pamięci i wektoryzowane operacje w C bez sprawdzania typu per element.
Odpowiedź w 2 minuty:
Każda tablica NumPy ma przypisany jeden dtype, który mówi, jak interpretować bajty w buforze: czy to liczby całkowite, zmiennoprzecinkowe, logiczne, zespolone, łańcuchy o stałej długości itd., oraz ile bajtów zajmuje element. Stąd nazwy takie jak int32 (4 bajty) czy float64 (8 bajtów). Dzięki znanemu z góry typowi NumPy może przechowywać dane jako surowy, ciągły blok pamięci i wie dokładnie, gdzie kończy się jeden element, a zaczyna następny.
Jednorodność typów jest tu kluczowa wydajnościowo. Skoro każdy element ma identyczny rozmiar i interpretację, pętla w skompilowanym C może przebiegać po buforze bez żadnego sprawdzania typu — to fundament wektoryzacji i SIMD. Gdyby typy były mieszane (jak w liście), trzeba by sprawdzać każdy element osobno, co niweczyłoby przewagę wydajnościową. Jednorodność daje też zwartość pamięci: milion float64 to ok. 8 MB ciągłego bufora, podczas gdy lista tych samych liczb zajmie wielokrotnie więcej przez nagłówki obiektów i wskaźniki.
dtype niesie też ważne konsekwencje semantyczne. Typy całkowite o stałej szerokości mogą się przepełniać (ang. overflow) — int8 przechowa wartości tylko od -128 do 127. Typy zmiennoprzecinkowe mają ograniczoną precyzję (float32 vs float64) i wprowadzają błędy zaokrągleń. Dobór typu to także kompromis pamięć kontra precyzja/zakres: float32 zajmuje połowę miejsca float64, co bywa istotne w uczeniu maszynowym, ale kosztem dokładności.
Przy łączeniu tablic o różnych typach NumPy stosuje reguły promocji typów (ang. type promotion / casting), wybierając wspólny typ zdolny pomieścić oba (np. int + float → float). Warto to kontrolować jawnie przez argument dtype= przy tworzeniu tablicy lub metodę astype(), by uniknąć niespodzianek z zakresem, precyzją i zużyciem pamięci.
Przykład kodu:
import numpy as np
a = np.array([1, 2, 3])
print(a.dtype) # int64 (zależnie od platformy)
# Jawne ustawienie typu — kontrola pamięci i precyzji
b = np.array([1, 2, 3], dtype=np.float32)
print(b.dtype, b.itemsize) # float32 4 (bajty na element)
# Przepełnienie typu o stałej szerokości
c = np.array([127], dtype=np.int8)
print(c + 1) # array([-128]) — overflow!
# Promocja typów przy operacjach mieszanych
i = np.array([1, 2, 3], dtype=np.int64)
f = np.array([0.5, 0.5, 0.5], dtype=np.float64)
print((i + f).dtype) # float64
# Konwersja typu istniejącej tablicy
d = b.astype(np.int32)
print(d.dtype, d) # int32 [1 2 3]
Materiały
↑ Powrót na góręJakie znaczenie ma liczba wymiarów tablicy (skalar, wektor, macierz, tablica n-wymiarowa) i jak NumPy je reprezentuje?
Odpowiedź w 30 sekund:
Liczba wymiarów (osi) tablicy określa jej strukturę: skalar (0D) to pojedyncza wartość, wektor (1D) to ciąg liczb, macierz (2D) to tabela wiersze×kolumny, a tablica n-wymiarowa (nD) uogólnia to na dowolnie wiele osi. NumPy opisuje to atrybutami ndim (liczba wymiarów) i shape (rozmiar wzdłuż każdej osi), a wszystkie te przypadki to ten sam typ ndarray.
Odpowiedź w 2 minuty:
W NumPy „wymiar" to oś (ang. axis), a liczba osi to atrybut ndim. Kształt shape to krotka podająca długość tablicy wzdłuż każdej osi. Skalar (0D) ma shape == () i jest pojedynczą wartością. Wektor (1D) ma jeden wymiar, np. shape == (5,) — to ciąg pięciu liczb. Macierz (2D) ma dwa wymiary, np. shape == (3, 4) — trzy wiersze i cztery kolumny. Tablica n-wymiarowa (nD) ma trzy lub więcej osi, np. (2, 3, 4) — przydatna do obrazów (wysokość×szerokość×kanały), serii czasowych, partii (ang. batch) danych w sieciach neuronowych.
Kluczowe jest to, że niezależnie od liczby wymiarów mamy do czynienia z jednym typem ndarray i tym samym, ciągłym buforem pamięci — wymiary to tylko sposób interpretacji tego płaskiego bufora poprzez kształt i strides. Dlatego można tanio zmieniać kształt tablicy (reshape) bez kopiowania danych: te same bajty raz widzimy jako wektor 12-elementowy, a raz jako macierz 3×4.
Liczba wymiarów ma znaczenie praktyczne, bo wiele operacji działa wzdłuż wybranej osi. Argument axis decyduje, czy np. sum sumuje kolumny, czy wiersze. Również broadcasting (dopasowywanie kształtów przy operacjach) oraz mnożenie macierzy zależą wprost od liczby i rozmiarów wymiarów. Niezrozumienie kształtu to najczęstsze źródło błędów — typowy ValueError o niezgodnych kształtach bierze się z nieświadomości, ile osi i jakiej długości ma tablica.
Warto odróżnić pojęcia: ndim to liczba osi, shape to ich długości, a size to całkowita liczba elementów (iloczyn wartości z shape). NumPy świadomie używa ogólnego ndarray zamiast osobnego typu macierzy — daje to spójny, jednolity model dla danych o dowolnej liczbie wymiarów.
Przykład kodu:
import numpy as np
skalar = np.array(7) # 0D
wektor = np.array([1, 2, 3, 4]) # 1D
macierz = np.array([[1, 2, 3],
[4, 5, 6]]) # 2D
tensor = np.arange(24).reshape(2, 3, 4) # 3D (nD)
for nazwa, t in [("skalar", skalar), ("wektor", wektor),
("macierz", macierz), ("tensor", tensor)]:
print(f"{nazwa:8} ndim={t.ndim} shape={t.shape} size={t.size}")
# skalar ndim=0 shape=() size=1
# wektor ndim=1 shape=(4,) size=4
# macierz ndim=2 shape=(2, 3) size=6
# tensor ndim=3 shape=(2, 3, 4) size=24
# Operacje wzdłuż osi (axis) zależą od liczby wymiarów
print(macierz.sum(axis=0)) # suma po kolumnach -> [5 7 9]
print(macierz.sum(axis=1)) # suma po wierszach -> [ 6 15]
# Zmiana kształtu bez kopiowania danych (ten sam bufor)
print(wektor.reshape(2, 2)) # [[1 2]
# [3 4]]
Materiały
- NumPy — The N-dimensional array (ndim, shape)
- NumPy — Array creation and reshaping (Beginner's Guide)
Tworzenie tablic i ich atrybuty
Jak rzutować typ danych tablicy (astype) i na co uważać przy zmianie dtype?
Odpowiedź w 30 sekund:
Typ danych zmienia się metodą tablica.astype(nowy_dtype), która domyślnie zwraca nową KOPIĘ tablicy z przekonwertowanymi wartościami — oryginał pozostaje nietknięty. Główne pułapki to: utrata precyzji przy float → int (następuje obcięcie w stronę zera, nie zaokrąglenie), oraz przepełnienie/zawijanie wartości przy zbyt małym typie całkowitym (np. uint8 mieści tylko 0–255). Argument copy=False pozwala uniknąć kopii, jeśli typ i tak się nie zmienia.
Odpowiedź w 2 minuty:
astype(dtype) to standardowy sposób rzutowania. Kluczowe: domyślnie tworzy KOPIĘ danych w nowym typie i zwraca nową tablicę — w przeciwieństwie do reshape nie jest to widok. Dla dużych tablic oznacza to realny koszt pamięci, więc nie rzutuj „na wszelki wypadek" w pętli. Argument copy=False mówi NumPy, by nie kopiował, jeśli to możliwe — czyli gdy żądany typ jest identyczny z bieżącym. Gdy konwersja jest faktyczna, kopia i tak musi powstać, bo zmienia się układ bajtów.
Pierwsza pułapka to konwersja float → int. NumPy obcina część ułamkową w stronę zera (truncation), a nie zaokrągla. Zatem 1.9 daje 1, a -1.9 daje -1. Jeśli chcesz zaokrąglenia matematycznego, najpierw zastosuj np.round(...), a dopiero potem astype(int). To częste źródło cichych błędów w obliczeniach — wyniki są „prawie dobre", tylko systematycznie zaniżone.
Druga, groźniejsza pułapka to przepełnienie (overflow) przy zbyt małym typie całkowitym. Typy takie jak int8, uint8, int16 mają ograniczony zakres (uint8: 0–255, int8: −128–127). Wartość spoza zakresu zawija się modulo (wrap-around) zamiast zgłosić błąd. Konwersja 300 na uint8 daje 44 (300 mod 256), a -1 daje 255. To bywa wykorzystywane celowo (np. obrazy), ale gdy jest przypadkowe — prowadzi do trudnych do wykrycia, „magicznych" liczb. Analogicznie float → float32 traci precyzję dla bardzo dużych/małych wartości, a konwersja zespolonych na rzeczywiste odrzuca część urojoną (z ostrzeżeniem).
Praktyczne wskazówki: (1) zawsze świadomie wybieraj typ docelowy, znając zakres danych; (2) przed float → int rozważ np.round; (3) dla pewności sprawdź zakres typu przez np.iinfo(np.uint8) / np.finfo(np.float32); (4) pamiętaj, że wynik jest kopią, więc przypisz go do zmiennej (a = a.astype(...)), bo modyfikacja oryginału „w miejscu" przez astype nie zachodzi.
Przykład kodu:
import numpy as np
a = np.array([1, 2, 3])
# astype ZAWSZE zwraca nową tablicę (domyślnie kopia) — oryginał bez zmian
b = a.astype(np.float64)
print(b.base is None) # True — to niezależna kopia
print(a.dtype, b.dtype) # int64 float64
# PUŁAPKA 1: float -> int OBCINA (w stronę zera), nie zaokrągla
f = np.array([1.9, 2.1, -1.9])
print(f.astype(int)) # [ 1 2 -1] — nie [2, 2, -2]!
# Aby zaokrąglić: najpierw round
print(np.round(f).astype(int)) # [ 2 2 -2]
# PUŁAPKA 2: przepełnienie przy zbyt małym typie (uint8: 0-255)
big = np.array([300, 256, -1])
print(big.astype(np.uint8)) # [ 44 0 255] — zawijanie modulo 256!
# Sprawdzenie zakresu typu przed konwersją
print(np.iinfo(np.uint8)) # min = 0, max = 255
# copy=False unika kopii tylko, gdy typ się NIE zmienia
c = a.astype(np.int64, copy=False)
print(c is a) # True — ten sam obiekt (typ identyczny)
Materiały
↑ Powrót na góręJakie znasz sposoby tworzenia tablic NumPy (array, zeros, ones, full, empty)?
Odpowiedź w 30 sekund:
Tablicę z istniejących danych (lista, krotka) tworzy się funkcją np.array(...). Do tworzenia tablic o zadanym kształcie wypełnionych konkretną wartością służą fabryki: np.zeros (same zera), np.ones (same jedynki), np.full (dowolna wartość) oraz np.empty (niezainicjowana pamięć — śmieci, bez gwarantowanej wartości). Każda z tych funkcji przyjmuje krotkę z kształtem i opcjonalny argument dtype.
Odpowiedź w 2 minuty:
Podstawowym sposobem jest np.array(obj), który konwertuje istniejący obiekt Pythona (zagnieżdżoną listę, krotkę, inną tablicę) na ndarray. NumPy sam wnioskuje wówczas wymiary i wspólny dtype, choć można go wymusić argumentem dtype=. To wybór, gdy masz już dane do umieszczenia w tablicy.
Gdy nie masz jeszcze danych, ale znasz docelowy kształt, korzystasz z fabryk. np.zeros(shape) zwraca tablicę zer (typ domyślny float64), a np.ones(shape) tablicę jedynek — oba bywają punktem startowym do akumulatorów, masek czy macierzy, które będziesz później wypełniać. np.full(shape, fill_value) to ich uogólnienie: wypełnia tablicę dowolną stałą, np. np.nan, -1 czy 255.
np.empty(shape) jest najszybsze, bo jedynie rezerwuje pamięć bez jej inicjalizacji — zawartość to przypadkowe „śmieci" pozostałe w pamięci. Używa się go tylko wtedy, gdy gwarantujesz, że nadpiszesz każdą komórkę przed odczytem; w przeciwnym razie wynik będzie nieprzewidywalny. To częsta pułapka: np.empty nie jest „szybszym np.zeros", bo nie zeruje danych.
Warto znać też warianty *_like (np.zeros_like, np.ones_like, np.full_like, np.empty_like), które tworzą nową tablicę o kształcie i typie istniejącej tablicy wzorcowej, oraz np.eye/np.identity dla macierzy jednostkowej. Domyślnym typem dla zer i jedynek jest float64, co bywa zaskakujące, gdy spodziewamy się liczb całkowitych — wtedy podaj dtype=int.
Przykład kodu:
import numpy as np
# 1. Z istniejących danych — NumPy wnioskuje kształt i dtype
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a.shape, a.dtype) # (2, 3) int64
# 2. Same zera (domyślnie float64)
zera = np.zeros((2, 3))
print(zera)
# 3. Same jedynki, ale jako liczby całkowite
jedynki = np.ones((2, 3), dtype=int)
# 4. Dowolna stała wartość
pelna = np.full((2, 2), fill_value=7.5)
# 5. Niezainicjowana pamięć — UWAGA: zawiera śmieci, nie zera!
puste = np.empty((2, 2))
print(puste) # nieprzewidywalne wartości
# Warianty *_like — kształt i typ jak we wzorcu
print(np.zeros_like(a)) # zera o kształcie (2, 3), dtype int64
Materiały
↑ Powrót na góręCzym różnią się arange i linspace i kiedy użyłbyś którego?
Odpowiedź w 30 sekund:
np.arange(start, stop, step) generuje wartości od start do stop z zadanym krokiem, nie włączając końca przedziału (przedział otwarty z prawej). np.linspace(start, stop, num) generuje zadaną liczbę punktów równomiernie rozłożonych w przedziale, domyślnie włączając koniec. Dla liczb zmiennoprzecinkowych preferuj linspace, bo arange z krokiem ułamkowym potrafi dać nieprzewidywalną liczbę elementów przez błędy zaokrągleń.
Odpowiedź w 2 minuty:
Obie funkcje tworzą równomiernie rozłożone wartości, ale parametryzują problem z dwóch różnych stron. W arange decydujesz o kroku (odległości między sąsiednimi wartościami), a liczba elementów wychodzi z obliczeń. W linspace decydujesz o liczbie punktów (num), a krok jest wyliczany automatycznie. To fundamentalna różnica: gdy zależy ci na konkretnym kroku (np. „co 2"), naturalny jest arange; gdy zależy ci na konkretnej liczbie próbek (np. „100 punktów do wykresu"), naturalny jest linspace.
Druga różnica to włączanie końca przedziału. arange, podobnie jak range w Pythonie, nie zawiera wartości stop. linspace domyślnie zawiera stop (chyba że ustawisz endpoint=False). Dlatego np.arange(0, 10, 2) daje [0, 2, 4, 6, 8], a np.linspace(0, 10, 5) daje [0, 2.5, 5, 7.5, 10] — z końcem 10.
Najważniejszy praktyczny niuans dotyczy liczb zmiennoprzecinkowych. Krok ułamkowy w arange (np. 0.1) nie jest reprezentowany dokładnie w arytmetyce zmiennoprzecinkowej, przez co długość wyniku bywa zaskakująca, a ostatni element może nieznacznie przekroczyć lub nie dotknąć granicy. Z tego powodu dokumentacja NumPy wprost zaleca linspace dla wartości niecałkowitych — tam liczbę elementów podajesz wprost, więc jest deterministyczna. arange rezerwuj dla kroków całkowitych.
| Cecha | np.arange |
np.linspace |
|---|---|---|
| Co podajesz | Krok (step) |
Liczbę punktów (num) |
| Co jest wyliczane | Liczba elementów | Krok |
Koniec przedziału (stop) |
Wykluczony | Domyślnie włączony (endpoint=True) |
| Liczby zmiennoprzecinkowe | Ryzykowne (błędy zaokrągleń) | Zalecane, deterministyczne |
| Typowe zastosowanie | Indeksy, kroki całkowite | Próbki do wykresów, siatki |
Przykład kodu:
import numpy as np
# arange — krok 2, koniec WYKLUCZONY
print(np.arange(0, 10, 2)) # [0 2 4 6 8] — brak 10
# linspace — 5 punktów, koniec WŁĄCZONY
print(np.linspace(0, 10, 5)) # [ 0. 2.5 5. 7.5 10. ]
# linspace bez końca przedziału
print(np.linspace(0, 10, 5, endpoint=False)) # [0. 2. 4. 6. 8.]
# Pułapka arange z krokiem ułamkowym — liczba elementów bywa zaskakująca
print(np.arange(0, 1, 0.1).size) # 10, ale dla innych kroków bywa 9 lub 11
# Bezpieczna alternatywa: 11 punktów od 0 do 1 włącznie
print(np.linspace(0, 1, 11))
# linspace może zwrócić też krok (retstep=True)
wartosci, krok = np.linspace(0, 1, 5, retstep=True)
print(krok) # 0.25
Materiały
↑ Powrót na góręCo opisują atrybuty shape, ndim, size oraz dtype?
Odpowiedź w 30 sekund:
shape to krotka z liczbą elementów wzdłuż każdej osi (np. (3, 4) dla macierzy 3×4). ndim to liczba wymiarów (osi) — tutaj 2. size to całkowita liczba elementów, czyli iloczyn wartości z shape (tutaj 12). dtype to typ danych przechowywanych w tablicy (np. int64, float64), wspólny dla wszystkich elementów.
Odpowiedź w 2 minuty:
Te cztery atrybuty opisują metadane tablicy ndarray i są dostępne bez nawiasów (to właściwości, nie metody).
shape to krotka liczb całkowitych — każda pozycja mówi, ile elementów leży wzdłuż danej osi. Dla wektora to np. (5,), dla macierzy (3, 4) (3 wiersze, 4 kolumny), dla tablicy 3D np. (2, 3, 4). Kolejność osi ma znaczenie i jest fundamentem operacji takich jak reshape, transpose czy broadcasting.
ndim to liczba wymiarów, czyli długość krotki shape. Skalar opakowany w tablicę ma ndim == 0, wektor 1, macierz 2, a tablice wyższych rzędów odpowiednio więcej. To wygodny skrót zamiast liczenia len(a.shape).
size to całkowita liczba elementów, czyli iloczyn wszystkich liczb w shape. Dla kształtu (3, 4) to 12. Nie należy mylić tego z nbytes (rozmiarem w bajtach) ani z len(a), które zwraca jedynie rozmiar pierwszej osi.
dtype opisuje typ danych każdego elementu. NumPy wymaga jednorodności — wszystkie elementy mają ten sam typ, co umożliwia ciągły układ pamięci i szybkie operacje wektorowe. Typ niesie informacje o rodzaju (całkowity, zmiennoprzecinkowy, logiczny) i o rozmiarze w bajtach (int8, int32, int64, float32, float64). Powiązany atrybut itemsize mówi, ile bajtów zajmuje jeden element, więc size * itemsize == nbytes.
Przykład kodu:
import numpy as np
a = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(a.shape) # (3, 4) — 3 wiersze, 4 kolumny
print(a.ndim) # 2 — dwa wymiary (== len(a.shape))
print(a.size) # 12 — 3 * 4 elementów
print(a.dtype) # int64 — wspólny typ wszystkich elementów
# Powiązane atrybuty pamięci
print(a.itemsize) # 8 — bajtów na jeden element (int64)
print(a.nbytes) # 96 — size * itemsize = 12 * 8
# Uwaga: len() to tylko pierwsza oś, nie size!
print(len(a)) # 3
Materiały
↑ Powrót na góręJak działa reshape i co oznacza wymiar -1 przekazany jako argument?
Odpowiedź w 30 sekund:
reshape zmienia kształt tablicy bez zmiany jej danych — nowy kształt musi mieć tę samą liczbę elementów co stary (size się nie zmienia). Wartość -1 w jednym z wymiarów to „placeholder": każesz NumPy samodzielnie wyliczyć ten wymiar na podstawie pozostałych i całkowitej liczby elementów. Można użyć -1 tylko raz. reshape zwykle zwraca widok (tę samą pamięć), a nie kopię.
Odpowiedź w 2 minuty:
reshape(nowy_kształt) reinterpretuje ten sam ciąg danych jako tablicę o innym kształcie. Warunek konieczny: iloczyn wymiarów nowego kształtu musi być równy size tablicy. Próba przekształcenia 12 elementów w kształt (3, 5) (15 elementów) skończy się błędem ValueError. Domyślnie elementy są odczytywane w porządku C (wierszami, row-major) — możesz to zmienić argumentem order='F' (kolumnami).
Wartość -1 to wygodny sposób, by nie liczyć jednego wymiaru ręcznie. NumPy podstawia za -1 taką liczbę, aby iloczyn się zgadzał. Np. dla 12 elementów reshape(3, -1) daje kształt (3, 4), a reshape(-1, 2) daje (6, 2). Najczęstsze użycie to spłaszczenie do jednego wymiaru: reshape(-1) zamienia dowolną tablicę w wektor. -1 można podać tylko w jednym wymiarze — w przeciwnym razie układ jest niejednoznaczny i NumPy zgłosi błąd. Jeśli liczba elementów nie dzieli się równo (np. 13 elementów na reshape(2, -1)), również dostaniesz ValueError.
Istotny niuans wydajnościowy: reshape stara się zwrócić widok (view) — nową „etykietę" wskazującą na tę samą pamięć, bez kopiowania danych. To bardzo tanie. Jednak gdy układ pamięci nie pozwala na widok o żądanym kształcie (np. po wcześniejszym transpose, gdy dane nie są ciągłe), NumPy utworzy kopię. Dlatego nie należy zakładać, że modyfikacja wyniku reshape zawsze wpłynie na oryginał — czasem tak, czasem nie. Jeśli potrzebujesz pewności co do spłaszczonej kopii, użyj flatten() (zawsze kopia) zamiast ravel()/reshape(-1) (zwykle widok).
flowchart TD
A["reshape(3, -1)<br/>size = 12"] --> B{"Iloczyn znanych<br/>wymiarów dzieli size?"}
B -- "Tak: 12 / 3 = 4" --> C["Podstaw -1 = 4<br/>kształt (3, 4)"]
B -- "Nie" --> D["ValueError"]
C --> E{"Dane ciągłe<br/>w pamięci?"}
E -- "Tak" --> F["Zwróć widok<br/>(bez kopiowania)"]
E -- "Nie" --> G["Zwróć kopię"]
Przykład kodu:
import numpy as np
a = np.arange(12) # [0 1 2 ... 11], size = 12
# Jawny kształt — iloczyn musi dać 12
print(a.reshape(3, 4).shape) # (3, 4)
# -1: NumPy wylicza brakujący wymiar (12 / 3 = 4)
print(a.reshape(3, -1).shape) # (3, 4)
print(a.reshape(-1, 2).shape) # (6, 2)
# Spłaszczenie do wektora
print(a.reshape(3, 4).reshape(-1).shape) # (12,)
# -1 tylko raz — to zgłosi błąd:
# a.reshape(-1, -1) # ValueError
# reshape zwykle zwraca WIDOK — zmiana wpływa na oryginał
b = a.reshape(3, 4)
b[0, 0] = 99
print(a[0]) # 99 — to ta sama pamięć
# flatten() to zawsze KOPIA, ravel()/reshape(-1) — zwykle widok
print(a.reshape(3, 4).flatten().base is None) # True (kopia)
Materiały
↑ Powrót na góręJak generować tablice z liczbami losowymi i czym np.random.default_rng (Generator) różni się od starszego API np.random?
Odpowiedź w 30 sekund:
Współczesny, zalecany sposób to utworzenie generatora rng = np.random.default_rng(seed) i wywoływanie na nim metod: rng.random(), rng.integers(), rng.normal() itd. Starsze, legacy API to globalne funkcje np.random.rand, np.random.randint, np.random.seed — działają na jednym, ukrytym stanie globalnym. default_rng oferuje lepszy algorytm PRNG (PCG64), brak ukrytego stanu globalnego i jawny, kontrolowany seed, dlatego od NumPy 1.17 jest preferowane w nowym kodzie.
Odpowiedź w 2 minuty:
Od NumPy 1.17 obowiązuje nowa architektura losowości oparta na dwóch obiektach: BitGenerator (źródło surowych losowych bitów, domyślnie PCG64) i Generator (warstwa zamieniająca bity na rozkłady — jednostajny, normalny, dwumianowy itd.). Funkcja np.random.default_rng(seed) zwraca gotowy Generator. Wywołujesz na nim metody: rng.random(rozmiar) (rozkład jednostajny [0, 1)), rng.integers(low, high, size) (liczby całkowite), rng.normal(loc, scale, size), rng.choice(...), rng.shuffle(...) i inne.
Legacy API to zestaw funkcji wprost w przestrzeni np.random: np.random.rand, np.random.randn, np.random.randint, a determinizm ustawiało się przez np.random.seed(...). Problem polega na tym, że wszystkie te funkcje współdzielą jeden globalny stan. W większym programie czy w wielu modułach trudno wtedy zapanować nad reprodukowalnością — dowolny fragment kodu (albo importowana biblioteka) może niepostrzeżenie „przesunąć" globalny generator i zmienić wyniki gdzie indziej. To klasyczne źródło trudnych do wytropienia błędów w eksperymentach ML.
Przewagi nowego API są konkretne. Po pierwsze, brak ukrytego stanu globalnego — każdy Generator jest niezależnym obiektem, który możesz przekazać tam, gdzie jest potrzebny. Po drugie, jakość i szybkość: domyślny PCG64 ma lepsze własności statystyczne niż stary Mersenne Twister z legacy API. Po trzecie, jawny i bezpieczny seeding — możesz podać liczbę, ale zalecanym wzorcem jest np.random.SeedSequence, co ułatwia rozsiewanie niezależnych strumieni (np. do obliczeń równoległych). Po czwarte, drobne, ale ważne różnice w API, np. rng.integers domyślnie wyklucza górną granicę spójnie z range, a metoda nazywa się integers, nie randint.
Reprodukowalność uzyskujesz tak samo prosto: ten sam seed daje tę samą sekwencję. Po prostu twórz osobny rng z ustalonym ziarnem na początku skryptu i używaj go wszędzie. Stare API nadal działa (dla kompatybilności wstecznej), ale w nowym kodzie dokumentacja NumPy wprost rekomenduje default_rng.
Przykład kodu:
import numpy as np
# === ZALECANE: nowoczesne API (Generator) ===
rng = np.random.default_rng(seed=42) # jawny, lokalny generator
print(rng.random(3)) # 3 liczby z [0, 1)
print(rng.integers(0, 10, size=5)) # 5 liczb całkowitych [0, 10)
print(rng.normal(loc=0, scale=1, size=3)) # rozkład normalny
print(rng.choice([10, 20, 30], size=2)) # losowy wybór z tablicy
# Reprodukowalność: ten sam seed -> ta sama sekwencja
rng_a = np.random.default_rng(0)
rng_b = np.random.default_rng(0)
print(np.array_equal(rng_a.random(5), rng_b.random(5))) # True
# Niezależne strumienie (np. do obliczeń równoległych)
ss = np.random.SeedSequence(12345)
generatory = [np.random.default_rng(s) for s in ss.spawn(3)]
# === LEGACY (starsze, do nowego kodu NIEzalecane) ===
np.random.seed(42) # globalny, współdzielony stan
print(np.random.rand(3)) # rozkład jednostajny
print(np.random.randint(0, 10, 5))
Materiały
↑ Powrót na góręBroadcasting i operacje wektorowe
Czym różni się mnożenie element-wise (*) od iloczynu macierzowego (@ / np.dot)?
Odpowiedź w 30 sekund:
Operator * to mnożenie element po elemencie (iloczyn Hadamarda): mnoży odpowiadające sobie elementy dwóch tablic, korzystając z broadcastingu, a wynik ma kształt jak operandy. Operator @ (oraz np.dot / np.matmul) to iloczyn macierzowy: sumuje iloczyny wierszy pierwszej macierzy z kolumnami drugiej. Wymaga zgodności wymiarów wewnętrznych — dla macierzy (m,n) @ (n,p) wynik ma kształt (m,p).
Odpowiedź w 2 minuty:
To częsta pułapka dla osób przechodzących z MATLAB-a lub matematyki, gdzie * oznacza mnożenie macierzy. W NumPy * jest zawsze operacją element-wise.
Mnożenie element-wise (*, czyli np.multiply):
- mnoży elementy na odpowiadających sobie pozycjach,
- stosuje reguły broadcastingu (kształty nie muszą być identyczne, byle zgodne),
- wynik ma kształt powstały z broadcastingu operandów,
- przykład:
[1,2,3] * [4,5,6]daje[4,10,18].
Iloczyn macierzowy (@, np.matmul, np.dot):
- realizuje algebraiczne mnożenie macierzy: element
(i,j)wyniku to suma iloczynówi-tego wiersza ij-tej kolumny, - wymaga, by wewnętrzne wymiary były równe:
(m,n) @ (n,p)→(m,p); w przeciwnym razieValueError: matmul: ... mismatch, - dla wektorów 1-D
a @ bliczy iloczyn skalarny (zwraca skalar), @inp.matmultraktują tablice >2D jako stosy macierzy (broadcasting po wymiarach „wsadowych"), natomiastnp.dotzachowuje się dla wyższych wymiarów inaczej (suma po ostatniej osi pierwszego i przedostatniej drugiego argumentu) — dlatego do mnożenia macierzy zaleca się@/np.matmul.
Różnica między np.dot a np.matmul: dla 1-D i 2-D dają ten sam wynik, ale np.dot z dwoma skalarami działa jak zwykłe mnożenie, a dla tablic >2D ich semantyka się rozjeżdża. We współczesnym kodzie standardem na mnożenie macierzy jest operator @ (PEP 465).
Przykład kodu:
import numpy as np
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
# --- ELEMENT-WISE (Hadamard): mnożenie pozycja po pozycji ---
print(A * B)
# [[ 5 12]
# [21 32]] (1*5, 2*6, 3*7, 4*8)
# --- ILOCZYN MACIERZOWY: wiersze x kolumny ---
print(A @ B)
# [[19 22]
# [43 50]] (1*5+2*7=19, 1*6+2*8=22, ...)
print(np.matmul(A, B)) # to samo co A @ B
print(np.dot(A, B)) # dla 2-D to samo
# wymiary wewnętrzne muszą się zgadzać: (2,3) @ (3,2) -> (2,2)
M = np.ones((2, 3))
N = np.ones((3, 2))
print((M @ N).shape) # (2, 2)
# niezgodne wymiary -> błąd
try:
M @ np.ones((2, 2)) # (2,3) @ (2,2): 3 != 2
except ValueError as e:
print(e) # matmul: ... size mismatch
# wektory 1-D: @ daje iloczyn skalarny (skalar)
u = np.array([1, 2, 3])
v = np.array([4, 5, 6])
print(u * v) # [ 4 10 18] (element-wise)
print(u @ v) # 32 (iloczyn skalarny: 4+10+18)
Materiały
↑ Powrót na góręWydajność, pamięć i typowe pułapki
Jak unikać niepotrzebnych kopii i kontrolować zużycie pamięci przy dużych tablicach?
Odpowiedź w 30 sekund:
Klucz to świadomy dobór dtype (np. float32 zamiast float64 połowi pamięć), operacje in-place (a += 1, parametr out=) zamiast tworzenia nowych tablic oraz korzystanie z widoków zamiast kopii. Dla danych większych niż RAM stosuje się np.memmap (mapowanie pliku na pamięć) lub formaty out-of-core jak Zarr/HDF5. Zużycie kontrolujemy atrybutem .nbytes, a relacje pamięci sprawdzamy przez np.shares_memory.
Odpowiedź w 2 minuty:
Pierwsza dźwignia to dtype. Domyślny float64 zajmuje 8 bajtów na element; jeśli precyzja na to pozwala, float32 (4 bajty) lub float16 (2 bajty) redukuje pamięć dwu- lub czterokrotnie, a dla liczb całkowitych warto wybrać najmniejszy mieszczący się typ (int8, int16, uint32). Przy dużych tablicach to często największa pojedyncza oszczędność. Pamięć jednej tablicy łatwo zmierzyć: a.nbytes (lub a.size * a.itemsize).
Druga dźwignia to unikanie zbędnych alokacji. Każde wyrażenie typu b = a * 2 + 1 tworzy tablice tymczasowe. Operacje in-place (a *= 2, a += 1) modyfikują istniejący bufor zamiast alokować nowy. Wiele ufunc i funkcji przyjmuje parametr out=, który zapisuje wynik do wskazanej, już zaalokowanej tablicy (np. np.add(a, b, out=a)), eliminując tymczasowe kopie. Warto też preferować widoki (wycinki, reshape, transpozycja) zamiast kopii oraz unikać niejawnych konwersji typu, które potrafią po cichu zduplikować dane (np. np.asarray zamiast np.array, gdy kopia nie jest potrzebna). Relacje współdzielenia bufora weryfikujemy np.shares_memory(a, b).
Trzecia dźwignia dotyczy danych większych niż RAM. np.memmap mapuje plik na dysku jako tablicę — system operacyjny ładuje do pamięci tylko aktualnie potrzebne fragmenty, więc można pracować z tablicą większą niż dostępny RAM bez wczytywania jej w całości. Dla wzorców out-of-core i kompresji dobre są formaty HDF5 (h5py) i Zarr, a do równoległego, kawałkowego przetwarzania — Dask (patrz kolejne pytanie). Gdy w pętli budujemy tablicę, lepiej prealokować ją raz przez np.empty(rozmiar) i wypełniać, niż wielokrotnie konkatenować (np.concatenate w pętli kopiuje całość za każdym razem). Broadcasting również pomaga — pozwala uniknąć fizycznego powielania danych przy operacjach między tablicami o różnych kształtach.
Przykład kodu:
import numpy as np
# 1. Dobór dtype — float32 zajmuje połowę pamięci float64
duza64 = np.ones(10_000_000, dtype=np.float64)
duza32 = np.ones(10_000_000, dtype=np.float32)
print(duza64.nbytes, duza32.nbytes) # 80000000 vs 40000000 bajtów
# 2. Operacje in-place i parametr out= — bez tablic tymczasowych
a = np.arange(1_000_000, dtype=np.float64)
a *= 2 # in-place, brak nowej alokacji
np.add(a, 1, out=a) # wynik zapisany w istniejącym buforze
print(np.shares_memory(a, a)) # True
# 3. Prealokacja zamiast konkatenacji w pętli
wynik = np.empty(1000, dtype=np.float64)
for i in range(1000):
wynik[i] = i * 1.5 # wypełnianie gotowego bufora
# 4. memmap — praca z tablicą większą niż RAM bez wczytywania całości
mm = np.memmap('dane.dat', dtype=np.float32, mode='w+', shape=(100_000, 100))
mm[0] = np.arange(100) # zapis trafia na dysk
mm.flush()
del mm # zwolnienie bez trzymania w RAM
Materiały
- NumPy — numpy.memmap
- NumPy — Data type objects (dtype)
- NumPy — Universal functions: the out argument
Indeksowanie, wycinanie i widoki
Jak działa podstawowe indeksowanie i wycinanie (slicing) w tablicach wielowymiarowych?
Odpowiedź w 30 sekund:
Podstawowe indeksowanie w NumPy używa krotki indeksów oddzielonych przecinkami, po jednym na każdy wymiar, np. a[2, 3] dla tablicy 2D. Wycinanie (slicing) start:stop:step wybiera zakresy elementów wzdłuż każdej osi i — co kluczowe — zwraca widok współdzielący pamięć z oryginałem, a nie kopię.
Odpowiedź w 2 minuty:
W tablicach wielowymiarowych adresujemy elementy podając jeden indeks na wymiar wewnątrz jednej pary nawiasów kwadratowych: a[i, j, k]. Zapis a[i][j] też działa, ale jest wolniejszy i mniej czytelny, bo tworzy pośrednie tablice — w NumPy preferujemy formę z krotką a[i, j]. Indeksy mogą być ujemne (liczone od końca), więc a[-1] to ostatni wiersz, a a[-1, -1] to ostatni element.
Wycinanie ma postać start:stop:step, gdzie stop jest wyłączny. Pominięte wartości oznaczają domyślnie: początek tablicy, koniec tablicy i krok 1. Każdej osi można nadać własny slice, np. a[1:3, ::2] bierze wiersze 1–2 i co drugą kolumnę. Symbol ... (Ellipsis) zastępuje tyle pełnych slice'ów :, ile trzeba, aby dopełnić wymiary — a[..., 0] to ostatni wymiar zredukowany do indeksu 0. Z kolei np.newaxis (czyli None) dodaje nowy wymiar o rozmiarze 1, co przydaje się przy broadcastingu.
Najważniejsza cecha podstawowego slicingu: zwraca widok (view), czyli nowy obiekt tablicy, który wskazuje na ten sam bufor danych co oryginał. Dzięki temu wycinanie jest bardzo tanie (nie kopiuje danych), ale ma to konsekwencję — modyfikacja elementów wycinka zmienia również oryginalną tablicę. To zachowanie różni NumPy od list Pythona, gdzie slicing tworzy płytką kopię. Ważne też, że indeksowanie pojedynczym skalarnym indeksem redukuje wymiar (a[0] z 2D daje 1D), a slice go zachowuje (a[0:1] z 2D daje nadal 2D).
Przykład kodu:
import numpy as np
a = np.arange(12).reshape(3, 4) # tablica 3x4 z liczbami 0..11
print(a)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# Indeksowanie: jeden indeks na wymiar w jednej krotce
print(a[1, 2]) # 6 - wiersz 1, kolumna 2
print(a[-1, -1]) # 11 - ostatni element (indeksy ujemne)
# Wycinanie start:stop:step na każdej osi
print(a[0:2, 1:3]) # wiersze 0-1, kolumny 1-2 -> [[1 2], [5 6]]
print(a[::2, ::2]) # co drugi wiersz i co druga kolumna
# Ellipsis i newaxis
print(a[..., 0]) # pierwsza kolumna jako wektor 1D -> [0 4 8]
print(a[:, None].shape) # dodanie osi -> (3, 1, 4)
# KLUCZOWE: slice zwraca WIDOK, modyfikacja zmienia oryginał
w = a[0, :] # widok pierwszego wiersza
w[0] = 999 # zapis do widoku...
print(a[0, 0]) # 999 - ...zmienił oryginał!
Materiały
↑ Powrót na góręAgregacje, osie i manipulacja kształtem
Jak działają funkcje agregujące (sum, mean, min, max) i co oznacza parametr axis?
Odpowiedź w 30 sekund:
Funkcje agregujące redukują tablicę do mniejszej liczby wartości, łącząc wiele elementów w jeden wynik (suma, średnia, minimum, maksimum). Domyślnie, bez podania axis, agregują po wszystkich elementach i zwracają pojedynczy skalar. Parametr axis mówi, którą oś (wymiar) tablicy chcemy zredukować — agregacja przebiega wzdłuż tej osi, a ona sama "znika" z kształtu wyniku.
Odpowiedź w 2 minuty:
Funkcje agregujące w NumPy (np.sum, np.mean, np.min, np.max, ale też np.std, np.prod, np.median itd.) przyjmują tablicę i sprowadzają ją do mniejszej reprezentacji. Występują zarówno jako funkcje modułu (np.sum(a)), jak i metody tablicy (a.sum()) — działają identycznie. Działają na całej tablicy wektorowo, w skompilowanym kodzie C, dlatego są dużo szybsze niż pętle w Pythonie.
Najważniejszy parametr to axis. Gdy go nie podamy (axis=None, wartość domyślna), agregacja obejmuje wszystkie elementy tablicy i wynikiem jest pojedynczy skalar. Gdy podamy konkretną oś, NumPy "przejdzie" wzdłuż tej osi i zredukuje wszystkie elementy w jej kierunku. Kluczowa intuicja: oś podana w axis znika z kształtu wyniku. Dla tablicy o kształcie (3, 4) agregacja z axis=0 da wynik o kształcie (4,), a z axis=1 — (3,). Można też podać krotkę osi, np. axis=(0, 1), aby zredukować kilka wymiarów naraz.
Warto znać też parametr keepdims=True, który zachowuje zredukowany wymiar jako rozmiar 1 (np. (3, 4) → (3, 1) zamiast (3,)). Jest to bardzo przydatne przy późniejszym broadcastingu, np. przy normalizacji wierszy lub kolumn. Należy uważać na wartości NaN — zwykłe np.sum/np.mean "zatruwają się" wartościami NaN (wynik to NaN), dlatego istnieją warianty odporne: np.nansum, np.nanmean, np.nanmin, np.nanmax. Trzeba też pamiętać, że typ wyniku zależy od typu danych — sumowanie tablicy int8 może się przepełnić, więc czasem warto wymusić szerszy typ przez parametr dtype.
Przykład kodu:
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
# Agregacja po WSZYSTKICH elementach (axis=None) -> skalar
print(a.sum()) # 21
print(a.mean()) # 3.5
print(a.min(), a.max()) # 1 6
# Agregacja wzdłuż osi
print(a.sum(axis=0)) # [5 7 9] -> oś 0 znika, zostaje kształt (3,)
print(a.sum(axis=1)) # [6 15] -> oś 1 znika, zostaje kształt (2,)
# keepdims zachowuje wymiar (przydatne do broadcastingu)
print(a.sum(axis=1, keepdims=True)) # [[ 6] [15]] -> kształt (2, 1)
print(a.shape, a.sum(axis=1, keepdims=True).shape) # (2, 3) (2, 1)
# Odporność na NaN
b = np.array([1.0, np.nan, 3.0])
print(b.sum()) # nan -> zwykła suma "zatruta"
print(np.nansum(b)) # 4.0 -> wariant ignorujący NaN
Materiały
↑ Powrót na górę