Wracasz z objazdu pięciu rezerwatów, @name, i masz w notesie pięć liczb: 120, 450, 85, 200 oraz 330 sztuk. Pytania, które zaraz zada kierownik ekspedycji, są co sezon te same. Ile średnio przypada na rezerwat? O ile urośnie każda populacja, jeśli przyrost wyniesie dziesięć procent? Które stada zeszły poniżej progu stu sztuk? Zwykła lista Pythona przechowa te liczby bez najmniejszego problemu, ale na żadne z tych pytań nie odpowie wprost.
Zanim sięgniemy po nowe narzędzie, warto zobaczyć, gdzie dokładnie stare przestaje wystarczać. Wbudowana funkcja
sum doda elementy listy, a len policzy, ile ich jest - jedno podzielone przez drugie daje średnią i tu wszystko gra. Gorzej z przeliczaniem: mnożenie listy przez liczbę całkowitą nie mnoży wartości, tylko powiela całą listę, a mnożenie przez ułamek w ogóle nie przechodzi i kończy się wyjątkiem TypeError.1populations = [120, 450, 85, 200, 330]
2
3print(sum(populations) / len(populations)) # 237.0
4
5print(populations * 2)
6# [120, 450, 85, 200, 330, 120, 450, 85, 200, 330]
7
8# populations * 1.1
9# TypeError: can't multiply sequence by non-int of type 'float'Trzy linie i trzy zupełnie różne zachowania. Średnia wyszła poprawna, bo policzyliśmy ją ręcznie, krok po kroku. Drugi wydruk pokazuje, że dla listy znak mnożenia znaczy "powtórz zawartość", więc zamiast podwojonych liczebności dostaliśmy dziesięć elementów zamiast pięciu. Trzecia linia jest zakomentowana nie bez powodu: gdybyś ją uruchomił, program zatrzymałby się na błędzie, bo Python nie umie powielić listy ułamek raza. Zwróć uwagę na to, czego tu nie ma: ani jednej liczby przeliczonej o dziesięć procent. Żeby ją uzyskać, musiałbyś napisać pętlę, a przy pięciu tysiącach odczytów z obroży GPS pętla w Pythonie robi się boleśnie wolna.
NumPy, czyli Numerical Python, to biblioteka, która załatwia oba problemy naraz. Wprowadza własny typ danych - tablicę - w której wszystkie elementy są tego samego rodzaju i leżą w pamięci jeden przy drugim. Dzięki temu działanie arytmetyczne wykonuje się na całej tablicy naraz, bez pętli, a same obliczenia wykonuje skompilowany kod w języku C, a nie interpreter Pythona. NumPy to również fundament, na którym stoi cała reszta ekwipunku badacza danych: Pandas, Matplotlib i biblioteki do uczenia maszynowego liczą pod spodem właśnie tablicami NumPy.
npNumPy nie wchodzi w skład standardowej biblioteki Pythona, więc trzeba go najpierw doinstalować. Robi to
pip, czyli menedżer pakietów Pythona - program, który pobiera bibliotekę z sieci i odkłada ją w Twoim środowisku. Polecenie wpisujesz w terminalu, a nie w pliku z kodem, i wykonujesz je tylko raz na środowisko, a nie przy każdym uruchomieniu skryptu. Jeśli pracujesz w Google Colab albo w dystrybucji Anaconda, NumPy jest już na miejscu i ten krok możesz spokojnie pominąć.1pip install numpyPo tym poleceniu pakiet leży na dysku, ale żaden Twój plik z kodem jeszcze o nim nie wie. Do wprowadzenia biblioteki do programu służy osobne polecenie, tym razem w samym Pythonie. Słowo kluczowe
import wciąga bibliotekę do pamięci, a słowo kluczowe as nadaje jej alias, czyli drugą, krótszą nazwę na dokładnie tę samą rzecz. Przyjęło się skracać NumPy do dwóch liter np i jest to konwencja tak powszechna, że każdy przykład w dokumentacji, każda odpowiedź w sieci i każde zadanie w tym module zakładają właśnie ją.1import numpy as npTa jedna linia składa się z czterech części i kolejność jest w niej sztywna: najpierw słowo
import, potem pełna nazwa pakietu numpy, potem słowo as, a na końcu skrót np. Zapamiętaj tę czwórkę, bo będziesz ją pisał na początku każdego skryptu z analizą. Nic poza tym się nie wydarzyło - nie powstała żadna tablica i nic nie zostało policzone. Wariant import numpy też jest poprawny, tylko wtedy przy każdym wywołaniu musisz pisać pełne numpy.array, co przy trzydziestu wywołaniach w skrypcie robi się męczące. Nie zadziała za to import np: pakiet nazywa się numpy, więc Python zakończy błędem ModuleNotFoundError: No module named 'np'. Skrót nadajesz sam, w tej właśnie linii, i dopiero od niej zaczyna on istnieć.np.arraySkoro biblioteka jest już pod ręką, zamieńmy notes na tablicę. Funkcja, która to robi, nazywa się
array, czyli po prostu "tablica", i przyjmuje jeden argument: listę wartości. Zwraca obiekt klasy ndarray - to skrót od n-dimensional array, czyli tablicy o dowolnej liczbie wymiarów. Sprawdzimy to od razu wbudowaną funkcją type, która odpowiada na pytanie "jakiego rodzaju jest ten obiekt". Nazwę np.array warto wbić sobie do głowy dokładnie w tej postaci, bo to jedyna poprawna droga z listy do tablicy.1import numpy as np
2
3arr = np.array([1, 2, 3, 4, 5])
4
5print(arr) # [1 2 3 4 5]
6print(type(arr)) # <class 'numpy.ndarray'>Pierwsza rzecz, która rzuca się w oczy w wydruku, to brak przecinków. Lista Pythona wypisuje się jako
[1, 2, 3, 4, 5], a tablica NumPy jako [1 2 3 4 5] - elementy rozdzielają same spacje. To najprostszy sposób, żeby na pierwszy rzut oka odróżnić jedno od drugiego w konsoli. Drugi wydruk potwierdza, że dostaliśmy nowy rodzaj obiektu, a nie ulepszoną listę. I rzecz równie ważna: lista, którą podałeś w nawiasach, nie zmieniła się ani trochę. NumPy skopiował z niej wartości do własnej struktury i zostawił oryginał w spokoju.Wokół tworzenia tablic krąży kilka nazw, które brzmią sensownie, a nie istnieją. Nie ma funkcji
np.list, bo lista to typ Pythona, a nie NumPy. Nie ma np.create ani np.new, mimo że w innych bibliotekach takie nazwy bywają spotykane. Nie ma też numpy.new, i to z dwóch niezależnych powodów naraz: takiej funkcji w pakiecie nie ma, a po import numpy as np nazwa numpy nie jest w Twoim programie w ogóle zdefiniowana. Możesz to sprawdzić bez wiary na słowo, funkcją hasattr, która odpowiada na pytanie "czy ten obiekt ma w środku nazwę o takim brzmieniu".1print(hasattr(np, "array")) # True
2print(hasattr(np, "list")) # False
3print(hasattr(np, "create")) # False
4print(hasattr(np, "new")) # FalseJedna prawda i trzy fałsze, czarno na białym. Wywołanie
np.list([1, 2, 3]) kończy się komunikatem AttributeError: module 'numpy' has no attribute 'list' i tak samo skończą się dwa pozostałe warianty. Zapamiętaj więc jedną drogę: tablicę z listy tworzy np.array i tylko np.array. Samo wywołanie hasattr niczego przy okazji nie zbudowało ani nie zmieniło - sprawdziło tylko, co w bibliotece jest, a czego nie ma. Warto też pamiętać o nawiasach kwadratowych w środku: np.array przyjmuje jedną listę, więc np.array(1, 2, 3) da błąd o zbyt wielu argumentach.Tablica NumPy różni się od listy nie tylko wydrukiem. Lista Pythona może pomieścić obok siebie liczbę, tekst i wartość logiczną, a tablica musi mieć jeden wspólny typ dla wszystkich elementów - i to właśnie ten warunek daje jej szybkość, bo komputer wie z góry, ile bajtów zajmuje każdy element. Typ tablicy odczytasz z atrybutu
dtype, czyli data type. Zobaczmy, co się stanie, gdy do samych liczb całkowitych dorzucimy jeden ułamek.1whole = np.array([1, 2, 3])
2mixed = np.array([1, 2, 3.5])
3
4print(whole.dtype) # int64
5print(mixed.dtype) # float64
6print(mixed) # [1. 2. 3.5]Pierwsza tablica dostała typ
int64, czyli liczby całkowite zapisane na sześćdziesięciu czterech bitach. Druga dostała float64, czyli liczby zmiennoprzecinkowe, choć wpisałeś do niej dwie liczby całkowite i tylko jeden ułamek. NumPy wybrał typ, który pomieści wszystkie wartości bez straty, i cicho przerobił jedynkę oraz dwójkę na 1.0 i 2.0 - widać to w ostatnim wydruku po kropkach bez cyfr. Twoje dane nie zostały uszkodzone, ale zmieniły postać, więc warto na to zerkać. Drobna uwaga praktyczna: na niektórych systemach, głównie na Windowsie, domyślnym typem całkowitym bywa int32 zamiast int64. To ta sama liczba w mniejszym pudełku i nie zmienia niczego w Twoim kodzie.Ta zasada jednego typu wygląda niewinnie, dopóki liczysz same pomiary. W następnej lekcji przekonasz się, że dla notesu terenowego, w którym obok liczebności stoi nazwa gatunku i informacja o zagrożeniu, jest ona twardym ograniczeniem - i właśnie dlatego powstała biblioteka Pandas.
shape, ndim, sizeTablica nie musi być pojedynczym szeregiem liczb. Podając listę list, dostajesz tablicę dwuwymiarową, czyli macierz: pierwsza lista wewnętrzna staje się pierwszym wierszem, druga drugim, i tak dalej. Do opisania takiej struktury służą trzy atrybuty. Atrybut
shape zwraca krotkę, czyli niezmienny ciąg wartości zapisywany w nawiasach okrągłych, w kolejności wiersze, kolumny. Atrybut ndim mówi, ile wymiarów ma tablica, a size - ile w sumie ma elementów. Wszystkie trzy są atrybutami, a nie metodami, więc piszesz je bez nawiasów.1arr = np.array([[1, 2, 3], [4, 5, 6]])
2
3print(arr.shape) # (2, 3)
4print(arr.ndim) # 2
5print(arr.size) # 6
6print(arr.dtype) # int64Cztery liczby, które łatwo pomylić, więc przeczytajmy je powoli. Kształt to
(2, 3), co czyta się zawsze tak samo: dwa wiersze na trzy kolumny, nigdy odwrotnie. Podałeś dwie listy wewnętrzne, po trzy liczby każda, więc wierszy jest dwa, a kolumn trzy - odpowiedź (3, 2) opisywałaby zupełnie inną tablicę, taką o trzech wierszach i dwóch kolumnach. Liczba 6 to size, czyli łączna liczba elementów, a nie kształt. Zwróć też uwagę na nawiasy: kształt wraca w nawiasach okrągłych, bo to krotka, a nie w kwadratowych, bo nie jest listą. Sama tablica po tych czterech wydrukach jest dokładnie taka sama jak przed nimi - odczyt atrybutu niczego nie zmienia.Skoro kształt jest krotką, można się do niego odwoływać po pozycji, dokładnie tak jak do listy. Zapis
arr.shape[0] daje liczbę wierszy, a arr.shape[1] liczbę kolumn, i tego pierwszego będziesz używał bez końca, żeby sprawdzić, ile obserwacji masz w zbiorze.1print(type(arr.shape)) # <class 'tuple'>
2print(arr.shape[0]) # 2
3print(arr.shape[1]) # 3
4
5# arr.shape()
6# TypeError: 'tuple' object is not callableOstatnie dwie linie są zakomentowane celowo i warto je zapamiętać jako ostrzeżenie. Dopisanie nawiasów do
shape kończy się błędem, bo próbujesz wywołać jak funkcję coś, co jest gotową krotką. To najczęstsza pomyłka przy pierwszym kontakcie z NumPy: metody wołasz z nawiasami, atrybuty czytasz bez. Trzy pierwsze wydruki potwierdzają natomiast, że w shape naprawdę siedzi zwykła krotka Pythona, ze wszystkim, co z tego wynika - możesz ją rozpakować do dwóch zmiennych albo porównać z inną krotką.zeros i onesNie każda tablica bierze się z pomiarów. Bardzo często potrzebujesz pustego arkusza o zadanym rozmiarze, który dopiero wypełnisz wynikami - na przykład siatki trzech gatunków na cztery kwartały. Funkcja
np.zeros tworzy taką tablicę wypełnioną zerami, a jej jedynym argumentem jest krotka z wymiarami, znowu w kolejności wiersze, kolumny. To ważny szczegół: cała krotka (3, 4) jest jednym argumentem, a nie dwoma, dlatego widzisz w wywołaniu dwie pary nawiasów.1grid = np.zeros((3, 4))
2
3print(grid)
4# [[0. 0. 0. 0.]
5# [0. 0. 0. 0.]
6# [0. 0. 0. 0.]]
7
8print(grid.shape) # (3, 4)
9print(grid.dtype) # float64
10print(type(grid)) # <class 'numpy.ndarray'>Wynik to macierz o trzech wierszach i czterech kolumnach, czyli dwanaście zer ustawionych w prostokąt. Przeczytaj wydruk jeszcze raz i policz: trzy linie po cztery zera każda. Odwrotny odczyt, czyli macierz cztery na trzy, opisywałby inną tablicę, o czterech liniach po trzy zera. Nie jest to też lista trzech list Pythona, mimo że kwadratowe nawiasy wyglądają podobnie - ostatni wydruk pokazuje czarno na białym klasę
numpy.ndarray. Nie ma tu również żadnego błędu: podanie krotki jako jednego argumentu jest dokładnie tym, czego np.zeros oczekuje. Zwróć jeszcze uwagę na kropki przy zerach i na typ float64: zera są zmiennoprzecinkowe, bo taki jest domyślny typ tych funkcji.Bliźniacza funkcja
np.ones działa identycznie, tylko wypełnia tablicę jedynkami, i przydaje się wtedy, gdy budujesz mnożniki albo wagi. Osobno stoi np.eye, która tworzy macierz jednostkową: kwadratową tablicę z jedynkami na przekątnej i zerami wszędzie indziej. Ta ostatnia bierze jedną liczbę, bo macierz jednostkowa z definicji ma tyle samo wierszy co kolumn.1print(np.ones((2, 3)))
2# [[1. 1. 1.]
3# [1. 1. 1.]]
4
5print(np.eye(3))
6# [[1. 0. 0.]
7# [0. 1. 0.]
8# [0. 0. 1.]]Dwa wiersze po trzy jedynki oraz kwadrat trzy na trzy z przekątną jedynek - dokładnie tak, jak zapowiadała proza. Macierz jednostkowa gra w algebrze tę samą rolę, co liczba jeden w zwykłym mnożeniu: pomnożenie przez nią niczego nie zmienia, dlatego wykorzystuje się ją jako punkt wyjścia przy odwracaniu macierzy. Warto zauważyć, że
np.eye przyjmuje samą liczbę, bez krotki, bo drugi wymiar wynika z pierwszego. Żadna z tych funkcji nie potrzebuje Twoich danych - wszystkie budują tablicę od zera, na podstawie samego rozmiaru.arange i linspaceDo opisania osi czasu albo numerów kolejnych obserwacji potrzebujesz nie zer, lecz rosnącego ciągu liczb. NumPy daje na to dwie funkcje i różnica między nimi jest źródłem wielu pomyłek. Funkcja
np.arange działa jak wbudowany range: podajesz początek, koniec i krok, a ona wypełnia tablicę wartościami co tyle, ile podałeś, pomijając koniec zakresu. Funkcja np.linspace pyta o coś innego: początek, koniec i liczbę punktów, a sama dobiera odstęp tak, żeby równo podzielić odcinek, przy czym koniec włącza do wyniku.1print(np.arange(0, 10, 2)) # [0 2 4 6 8]
2print(np.linspace(0, 1, 5)) # [0. 0.25 0.5 0.75 1. ]Pierwsza tablica ma pięć elementów i kończy się na ósemce, bo dziesiątka jako koniec zakresu do wyniku nie wchodzi. Druga też ma pięć elementów, ale ostatnim jest równa jedynka, bo
linspace traktuje koniec jako wartość docelową, a nie granicę. Trzeci argument znaczy w obu funkcjach zupełnie co innego: w arange to odstęp między wartościami, w linspace to liczba wartości. Moja rada, @name: kiedy znasz krok i chcesz numerować pomiary, bierz arange, a kiedy potrzebujesz dokładnie stu punktów rozłożonych równo od zera do jedynki, bierz linspace - inaczej skończysz z ręcznym dzieleniem odcinka i błędem zaokrąglenia na końcu.np.random.randZanim spłyną prawdziwe odczyty z obroży, przydaje się zbiór wymyślony, na którym można przetestować cały skrypt. Losowe liczby generuje moduł
np.random, a najprostsza z jego funkcji to rand, zwracająca wartości z przedziału od zera do jedynki. Jej wymiary podaje się jako osobne argumenty, a nie jako krotkę - to wyjątek od reguły, którą poznałeś przy np.zeros, i łatwo się na nim potknąć. Żeby wyniki dało się powtórzyć, ustawiamy wcześniej ziarno generatora funkcją np.random.seed, a do skrócenia wydruku używamy metody round, która zaokrągla wszystkie wartości tablicy naraz.1np.random.seed(42)
2sample = np.random.rand(2, 3)
3
4print(sample.round(2))
5# [[0.37 0.95 0.73]
6# [0.6 0.16 0.16]]Sześć losowych liczb ułożonych w dwa wiersze po trzy kolumny, każda mniejsza od jedynki. Dzięki ziarnu ustawionemu na czterdzieści dwa dostaniesz dokładnie te same wartości na swoim komputerze - to nie magia, tylko powtarzalny generator, i o to właśnie chodzi przy testowaniu. Bez wywołania
seed liczby byłyby inne przy każdym uruchomieniu, a Ty nie umiałbyś odtworzyć wyniku sprzed tygodnia. Zwróć uwagę, że round niczego nie popsuła w sample - zwróciła nową, zaokrągloną tablicę, a oryginał trzyma dalej pełne rozwinięcia. W nowych projektach polecam nowszy zapis rng = np.random.default_rng(42), bo trzyma stan generatora w osobnym obiekcie zamiast globalnie, ale wersję z seed musisz znać, bo spotkasz ją w tysiącach istniejących skryptów.Wracamy do notesu z pierwszej strony lekcji. Pięć liczebności, jedna tablica i komplet pytań, na które trzeba odpowiedzieć. NumPy ma na każde z nich osobną funkcję.
np.mean liczy średnią arytmetyczną, np.median medianę, czyli wartość środkową po ustawieniu liczb w kolejności, a np.std odchylenie standardowe, mówiące, jak mocno pomiary rozjeżdżają się wokół średniej. Do tego dochodzą trzy oczywiste: np.min, np.max i np.sum. Każda z nich przyjmuje tablicę i zwraca pojedynczą liczbę.1populations = np.array([120, 450, 85, 200, 330])
2
3print(np.mean(populations)) # 237.0
4print(np.median(populations)) # 200.0
5print(np.std(populations)) # 135.77923257994942
6print(np.min(populations)) # 85
7print(np.max(populations)) # 450
8print(np.sum(populations)) # 1185Sześć liczb, które opisują całe stado lepiej niż sam wykaz. Średnia dwieście trzydzieści siedem, a mediana dwieście - różnica bierze się stąd, że słoniowe czterysta pięćdziesiąt ciągnie średnią w górę, a mediany nie rusza, bo ta patrzy tylko na to, co stoi w środku szeregu. Odchylenie standardowe około stu trzydziestu sześciu przy średniej dwustu trzydziestu siedmiu to sygnał, że rezerwaty bardzo się od siebie różnią. Jedna uwaga dla porządku:
np.std domyślnie liczy odchylenie populacyjne, dzieląc przez liczbę pomiarów, a Pandas w tym samym miejscu liczy odchylenie próby, dzieląc przez tę liczbę pomniejszoną o jeden. Dla tych samych pięciu liczb da to 151.81 zamiast 135.78, więc nie zdziw się, gdy zobaczysz dwie różne wartości. Sama tablica populations po tych sześciu wydrukach ma nadal te same pięć liczb.Wywołanie średniej warto rozłożyć na części, bo będziesz je pisał częściej niż jakiekolwiek inne. Składa się z sześciu elementów w sztywnej kolejności:
np, ., mean, (, arr, ). Najpierw skrót biblioteki, potem kropka, potem nazwa funkcji, nawias otwierający, nazwa tablicy i nawias zamykający. Istnieje też druga forma, w której średnia jest metodą samej tablicy.1arr = np.array([1, 2, 3, 4, 5])
2
3print(np.mean(arr)) # 3.0
4print(arr.mean()) # 3.0Ten sam wynik dwiema drogami i to nie przypadek - to dosłownie ta sama operacja zapisana inaczej. Forma
np.mean(arr) ma jednak przewagę, którą polecam wykorzystywać: działa również na zwykłej liście Pythona, więc np.mean([1, 2, 3, 4, 5]) też zwróci trójkę, podczas gdy [1, 2, 3, 4, 5].mean() skończy się błędem, bo lista takiej metody nie ma. Przy pisaniu skryptu, który dostaje dane z różnych miejsc, ta odporność bywa bezcenna. Żadne z tych wywołań nie zmienia tablicy - obliczenie statystyki to czysty odczyt.Teraz wracamy do pytania, na którym potknęła się lista: o ile urosną populacje przy dziesięcioprocentowym przyroście. W NumPy zapisuje się to dokładnie tak, jak to brzmi po polsku - mnożysz tablicę przez 1.1. Taki zapis nazywa się wektoryzacją: pojedyncza liczba zostaje rozciągnięta na wszystkie elementy tablicy i działanie wykonuje się osobno dla każdego z nich, bez żadnej pętli. NumPy nazywa ten mechanizm broadcasting, czyli rozgłaszanie.
1growth = populations * 1.1
2
3print(growth) # [132. 495. 93.5 220. 363. ]
4print(populations) # [120 450 85 200 330]
5print(growth.dtype) # float64Pięć nowych liczebności z jednej linii kodu, bez pętli i bez indeksów. Drugi wydruk jest tu najważniejszy:
populations trzyma nadal pierwotne pomiary, bo działanie arytmetyczne zwraca nową tablicę, a nie poprawia starej. Trzeci wydruk pokazuje skutek uboczny mnożenia przez ułamek - wynik przeszedł na typ float64, bo dziewięćdziesiąt trzy i pół nie mieści się w liczbach całkowitych. Biologicznie pół geparda to bezsens, ale matematycznie tak właśnie działa mnożenie, a od zaokrąglenia jest round. Tak samo zachowają się pozostałe działania: populations + 10 doda dziesiątkę do każdej wartości, a populations / 2 podzieli każdą na pół.Wektoryzacja obsłuży też wyrażenie złożone z kilku kroków naraz. Klasycznym przykładem jest normalizacja, czyli sprowadzenie pomiarów do wspólnej skali: od każdej wartości odejmujesz średnią i dzielisz przez odchylenie standardowe. Wynik mówi, o ile odchyleń dana populacja leży powyżej lub poniżej przeciętnej, więc da się go porównywać między zupełnie różnymi wielkościami - liczebnością stada i opadem w milimetrach.
1normalized = (populations - np.mean(populations)) / np.std(populations)
2
3print(normalized.round(2)) # [-0.86 1.57 -1.12 -0.27 0.68]
4print(np.mean(normalized).round(10)) # 0.0
5print(np.std(normalized).round(10)) # 1.0Pięć liczb, tym razem po obu stronach zera. Wartości ujemne to rezerwaty poniżej średniej, dodatnie powyżej, a słoń z wynikiem 1.57 leży półtora odchylenia nad przeciętną. Dwa ostatnie wydruki potwierdzają, że przekształcenie zadziałało zgodnie z definicją: znormalizowany zbiór ma średnią zero i odchylenie jeden. Cała ta operacja to trzy wywołania i jedno dzielenie na całej tablicy naraz - w czystym Pythonie zajęłaby pętlę i dwa przebiegi po danych. Oryginalne
populations znów pozostało nietknięte, bo każde z tych działań zbudowało nową tablicę obok.Do pojedynczych wartości sięgasz tak samo, jak w liście Pythona: podając pozycję w nawiasach kwadratowych, licząc od zera. Indeks ujemny liczy od końca, więc minus jeden to element ostatni. Wycinek, czyli slice, zapisujesz dwiema liczbami rozdzielonymi dwukropkiem: pierwsza to pozycja początkowa, druga to pozycja, przed którą wycinek się kończy - koniec zakresu nie wchodzi do wyniku. Pominięcie liczby przed dwukropkiem znaczy "od początku", a po dwukropku "do końca". Trzecia liczba, po drugim dwukropku, to krok.
1arr = np.array([10, 20, 30, 40, 50])
2
3print(arr[0]) # 10
4print(arr[-1]) # 50
5print(arr[1:4]) # [20 30 40]
6print(arr[:3]) # [10 20 30]
7print(arr[::2]) # [10 30 50]Pięć zapisów i pięć różnych odpowiedzi z tej samej tablicy. Wycinek
arr[1:4] daje trzy elementy, a nie cztery, bo pozycja czwarta stanowi granicę i sama do wyniku nie wchodzi - to reguła, która myli każdego na początku. Zapis arr[:3] bierze pozycje zero, jeden i dwa, czyli trzy pierwsze wartości. Ostatni, arr[::2], przechodzi przez całą tablicę co drugi element i dlatego zwraca dziesiątkę, trzydziestkę i pięćdziesiątkę. Warto zauważyć, że wszystkie te wydruki są tablicami NumPy, a nie listami, więc możesz na nich od razu policzyć średnią.Jest jednak jedna różnica wobec list Pythona, która potrafi kosztować godzinę szukania błędu. Wycinek listy tworzy kopię, a wycinek tablicy NumPy tworzy widok, czyli okno na te same dane w pamięci. Zmiana wartości w widoku zmienia oryginał. Sprawdźmy to na osobnej tablicy, żeby zobaczyć efekt bez wątpliwości.
1readings = np.array([10, 20, 30, 40, 50])
2window = readings[1:4]
3window[0] = 999
4
5print(readings) # [ 10 999 30 40 50]
6
7safe = readings[1:4].copy()
8safe[0] = 111
9
10print(readings) # [ 10 999 30 40 50]
11print(safe) # [111 30 40]Pierwsza część kodu psuje dane po cichu: przypisanie do
window podmieniło wartość w readings, choć nikt tej tablicy wprost nie dotykał. Druga część pokazuje lekarstwo - metoda copy tworzy niezależną kopię wycinka i od tej chwili obie tablice żyją własnym życiem, co widać po tym, że readings nie zmieniło się po podstawieniu 111. Moja rada jest tu jednoznaczna, @name: jeśli zamierzasz cokolwiek zapisywać do wycinka, dopisz copy od razu, bez zastanawiania się, czy w tym konkretnym miejscu widok zaszkodzi. Przy samym odczytywaniu widok jest lepszy, bo nie kopiuje pamięci i działa błyskawicznie.Pytanie o stada poniżej stu sztuk rozwiązuje się w NumPy dwoma krokami i oba warto zobaczyć osobno. Porównanie tablicy z liczbą nie zwraca jednego
True, tylko wykonuje porównanie dla każdego elementu z osobna i oddaje tablicę wartości logicznych o tej samej długości. Taką tablicę nazywamy maską logiczną. Włożona z powrotem w nawiasy kwadratowe wybiera wyłącznie te elementy, którym odpowiada True.1print(populations < 100)
2# [False False True False False]
3
4print(populations[populations < 100]) # [85]
5print(populations) # [120 450 85 200 330]Pierwszy wydruk to sama decyzja: pięć odpowiedzi tak albo nie, w tej samej kolejności co pomiary, i ani jednej liczebności - maska nie przechowuje danych, tylko werdykt o każdym elemencie. Drugi wydruk stosuje ją i zostawia jedyny rezerwat poniżej progu, czyli gepardzie osiemdziesiąt pięć. Trzeci wydruk mówi rzecz najważniejszą:
populations ma nadal wszystkie pięć wartości, bo filtrowanie zwróciło nową tablicę. Ten sam mechanizm, dosłownie ten sam, zobaczysz w następnej lekcji przy filtrowaniu tabel w Pandas - warto go więc zrozumieć teraz, na pięciu liczbach, a nie później, na trzech tysiącach wierszy.W macierzy jedna pozycja nie wystarczy, bo trzeba wskazać wiersz i kolumnę. Podajesz je w jednych nawiasach kwadratowych, rozdzielone przecinkiem, zawsze w kolejności wiersz, kolumna. Sam dwukropek w miejscu jednego z nich znaczy "wszystkie" - dzięki temu
[:, 0] czyta się jako "wszystkie wiersze, kolumna zerowa", czyli cała pierwsza kolumna.1matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
2
3print(matrix[0, 1]) # 2
4print(matrix[:, 0]) # [1 4 7]
5print(matrix[1, :]) # [4 5 6]Trzy zapytania i trzy różne kształty odpowiedzi. Pierwsze zwraca pojedynczą liczbę, bo wskazałeś dokładnie jedną komórkę: wiersz zerowy, kolumna pierwsza, czyli dwójka. Drugie zwraca tablicę trzech liczb pobranych z góry na dół - to kolumna, więc w wyniku widzisz wartości z trzech różnych wierszy. Trzecie zwraca cały wiersz środkowy. Działa też zapis
matrix[0][1], bo najpierw wyciąga wiersz, a potem z niego element, ale polecam wersję z przecinkiem: jest krótsza, nie tworzy po drodze pośredniej tablicy i pozwala wycinać oba wymiary naraz, na przykład matrix[0:2, 1:3].reshape, flatten i TTe same liczby można ułożyć na różne sposoby i NumPy pozwala to robić bez przepisywania danych. Metoda
reshape przestawia tablicę na zadany kształt, pod jednym warunkiem: iloczyn nowych wymiarów musi się zgadzać z liczbą elementów. Metoda flatten robi rzecz odwrotną - spłaszcza dowolną tablicę do jednego szeregu. Atrybut T to transpozycja, czyli zamiana wierszy z kolumnami, i tak jak shape piszemy go bez nawiasów.1arr = np.arange(12)
2matrix = arr.reshape(3, 4)
3
4print(matrix)
5# [[ 0 1 2 3]
6# [ 4 5 6 7]
7# [ 8 9 10 11]]
8
9print(matrix.flatten()) # [ 0 1 2 3 4 5 6 7 8 9 10 11]
10print(matrix.T.shape) # (4, 3)Dwanaście liczb od zera do jedenastu ułożyło się w trzy wiersze po cztery, wypełniane wierszami od lewej do prawej. Spłaszczenie wróciło do jednego szeregu w tej samej kolejności, a transpozycja odwróciła kształt z
(3, 4) na (4, 3) - to kolejna okazja, żeby przypomnieć sobie, że pierwsza liczba kształtu to zawsze wiersze. Wywołanie arr.reshape(5, 4) zakończyłoby się błędem ValueError: cannot reshape array of size 12 into shape (5,4), bo pięć razy cztery to dwadzieścia, a elementów jest dwanaście. Jest na to wygodny skrót: podaj minus jeden zamiast jednego z wymiarów, a NumPy sam go policzy, więc arr.reshape(3, -1) da ten sam kształt (3, 4). Sama arr po tych operacjach jest nadal płaskim ciągiem dwunastu liczb.Przy dwóch macierzach dochodzi rozróżnienie, które w matematyce jest fundamentalne, a w kodzie sprowadza się do jednego znaku. Dodawanie i mnożenie zapisane zwykłymi znakami działa element po elemencie: NumPy zestawia komórki leżące na tych samych pozycjach. Mnożenie macierzowe, znane z algebry liniowej, zapisuje się operatorem
@ albo funkcją np.dot i liczy zupełnie inaczej: każdy element wyniku powstaje z przemnożenia wiersza przez kolumnę i zsumowania iloczynów.1A = np.array([[1, 2], [3, 4]])
2B = np.array([[5, 6], [7, 8]])
3
4print(A + B)
5# [[ 6 8]
6# [10 12]]
7
8print(A * B)
9# [[ 5 12]
10# [21 32]]
11
12print(A @ B)
13# [[19 22]
14# [43 50]]Porównaj drugi i trzeci wynik, bo to jedna z najczęstszych pułapek w kodzie uczącym maszyny. Mnożenie gwiazdką dało pięć w lewym górnym rogu, czyli jeden razy pięć - proste zestawienie komórek. Mnożenie operatorem
@ dało w tym samym rogu dziewiętnaście, bo policzyło jeden razy pięć plus dwa razy siedem. Oba są poprawnymi działaniami, tylko odpowiadają na różne pytania, a program nie zgłosi błędu, jeśli wybierzesz nie to, o które Ci chodziło. Zapamiętaj: gwiazdka to działanie na parach komórek, @ to mnożenie macierzy. Funkcja np.dot(A, B) zwraca dokładnie to samo co A @ B, ale operator jest czytelniejszy i to jego polecam w nowym kodzie.linalgPoważniejsze narzędzia algebry siedzą w podmodule
np.linalg, czyli linear algebra. Funkcja det liczy wyznacznik macierzy - jedną liczbę, która mówi między innymi, czy macierz da się odwrócić. Funkcja inv zwraca macierz odwrotną, czyli taką, która pomnożona przez oryginał daje macierz jednostkową, tę z jedynkami na przekątnej. Wyznacznik równy zeru znaczy, że odwrotności nie ma, więc w praktyce sprawdza się go przed odwracaniem.1A = np.array([[1, 2], [3, 4]])
2
3print(np.linalg.det(A)) # -2.0000000000000004
4print(round(np.linalg.det(A), 2)) # -2.0
5print(np.linalg.inv(A))
6# [[-2. 1. ]
7# [ 1.5 -0.5]]Pierwszy wydruk to dobra lekcja pokory wobec komputera: wyznacznik tej macierzy wynosi dokładnie minus dwa, a NumPy pokazuje minus dwa i jeszcze odrobinę. To nie błąd biblioteki, tylko skutek zapisu ułamków w postaci dwójkowej, w której wiele liczb nie ma dokładnego odpowiednika. Dlatego wyników zmiennoprzecinkowych nie porównuje się znakiem równości, tylko zaokrągla albo sprawdza z tolerancją - drugi wydruk pokazuje najprostszy sposób. Macierz odwrotna wyszła zgodnie z podręcznikiem, a
A po obu wywołaniach jest nadal tą samą macierzą, którą wpisaliśmy. W tym samym module mieszka jeszcze np.linalg.eig, zwracająca parę wyników naraz: wartości własne i wektory własne macierzy. Spotkasz ją, gdy dojdziesz do redukcji wymiarów w uczeniu maszynowym.Warto na koniec zrozumieć, skąd bierze się przewaga NumPy, bo to tłumaczy wszystkie jego dziwactwa. Lista Pythona przechowuje wskaźniki do obiektów rozrzuconych po pamięci, a każdy z tych obiektów niesie własną informację o typie. Tablica NumPy trzyma surowe liczby jeden przy drugim, w jednym ciągłym bloku pamięci, i typ opisuje raz dla całej tablicy - to właśnie ten
dtype, na który patrzyliśmy wcześniej. Dzięki temu pętla po elementach wykonuje się w skompilowanym kodzie C, a procesor może przetwarzać po kilka liczb w jednym takcie. Stąd wymóg jednego typu, stąd sztywny kształt i stąd widoki zamiast kopii przy wycinkach: każda z tych zasad kupuje szybkość. Przy pięciu liczbach różnicy nie zauważysz, przy pięciu milionach jest to różnica między sekundą a kwadransem.import, numpy, as, np.np.array - funkcji np.list, np.create ani np.new w NumPy nie ma.np.zeros((3, 4)) zwraca tablicę ndarray o trzech wierszach i czterech kolumnach, wypełnioną zerami. Krotka wymiarów to jeden argument, więc żadnego błędu tu nie ma.shape, bez nawiasów, a wynik jest krotką w kolejności wiersze, kolumny: dla np.array([[1,2,3], [4,5,6]]) jest to (2, 3). Liczba 6 to size, a nie kształt.np, ., mean, (, arr, ). Obok niej stoją median, std, min, max i sum.populations < 100, zwraca tablicę wartości True i False, a włożona w nawiasy kwadratowe filtruje dane.copy.@ wykonuje mnożenie macierzowe. To dwa różne działania, nie dwa zapisy tego samego.W następnej lekcji dołożymy do tych liczb nazwy kolumn i różne typy w jednej tabeli, bo notes terenowy to coś więcej niż kolumna pomiarów. Na razie zapamiętaj jedno, @name: tablica NumPy to karabińczyk tropiciela - jeden typ, jeden kształt, żadnych ozdobników, za to całe stado przeliczone jednym ruchem.