
Podstawy pakietu Numpy - część 1
Wstęp
Numpy jako wstęp do data science
Na blogu postanowiłem zacząć opisywać nowy większy temat związany ze wstępem do zagadnień data science. Pakiet Numpy, od numeric python, jest według mnie pierwszym etapem na drodze do zgłębienia tematyki analizy danych i tematów z nią związanych. Opiera się na nim bezpośrednio wiele pakietów, które będziemy tu w późniejszym czasie opisywać, np. Pandas, Matplotlib, czy Scikit-Learn.
Głównym obiektami, z które korzystamy w całym pakiecie, są wielowymiarowe, jednorodne tablice (ndarrays od n-dimension array). Jednorodność oznacza, że wszystkie elementy tablicy muszą być tego samego typu. W zależności od wymiaru tabeli mówimy o jej osiach. Tablice jednowymiarowe, odzwierciedlające wektory, mają jedną oś. Tablice dwuwymiarowe mają 2 osie, jedną odpowiadającą wierszą, a drugą kolumną.
Podstawy obiektu ndarray
Po zaimportowaniu pakietu do jupyter notebook'a możemy sprawdzić podstawowe właściwości obiektu ndarray. Na początek utworzymy dwie tablice, jedno i dwuwymiarową, odpowiednio a i b. Pominę na razie opis tworzenia tablic, skupimy się na nim w następnym podrozdziale.
Kod tego i kolejnych wpisów dotyczących data science będzie przedstawiany jako fragmenty jupyter notbook-ów. Są to fragmenty notatnika wyeksportowanego do formatu html, stąd też czytelnik może kopiować poszczególne komórki i odpalać je u siebie w celu przećwiczenia omawianych tu zagadnień.
import numpy as np
a = np.array([1.3, 2., 3.9, 4., 5.1])
a
b = np.array([[1, 3, 5], [2, 4, 6]])
b
Podstawowymi metodami służącymi do opisu tablicy są ndarray.shape zwracający tuplę z ilością wierszy i kolumn tablicy oraz ndarray.ndim która zwraca ilość wymiarów tablicy.
a.shape
b.shape
b.ndim
Dodatkowo przydatna często okazuje się możliwość sprawdzenia sumarycznej ilości elementów w tablicy, czyli ilości wierszy pomnożonej przez ilość kolumn, służy do tego ndarray.size. Typ elementów w tablicy zwraca ndarray.dtype. Jest to jedna liczna, gdyż wszystkie elementy w tablicy muszą być tego samego typu.
b.size
a.dtype
Typy danych NumPy
Pakiet NumPy rozszerza zbiór typów danych znanych z pythona, wiele z nich zaczerpniętych jest z C i różnią się możliwą długością zapisanych w nich danych, oto przykłady:
- np.int8 - Byte (-128 to 127)
- np.int16 - Integer (-32768 to 32767)
- np.int32 - Integer (-2147483648 to 2147483647)
- np.uint8 - Unsigned integer (0 to 255)
- ...
- np.float32 - float
- np.float64 - double float - ta sama precyzja co wbudowany float pythona
- np.complex64 - float complex - liczba zespolona
Najczęściej używanymi typami są int64 i float64.
Tworzenie tablic
Tablice w NumPy możemy tworzyć na wiele sposobów, podstawowym jest przekazanie do metody np.array() obiektu, po którym można iterować. Mogą to być np. pythonowe listy czy tuple, przekazane bezpośrednio, lub jako zmienna na nie wskazująca.
np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
a = np.array([1, 5, 8])
a.dtype
b = np.array([1.9, 0.2, 11.])
b.dtype
Tablica przyjmie typ danych, które zostały przekazane w momencie jej tworzenia. Jeśli wartości będą liczbami całkowitymi, typem danych po utworzeniu tablicy będzie int64, a dla liczb zmiennoprzecinkowych float64. Warto pamiętać, że podstawowym typem dla ndarray jest właśnie float64, stąd też, jeśli wartości przekazane przy tworzeniu tablicy będą mieszane (float i int), wynikowa tablica będzie miała dtype='float64', a wszystkie wartości całkowite zostaną przekonwertowane.
W momencie tworzenia tablicy możemy również zdefiniować jakiego typu dane mają się w niej znaleźć, za pomocą parametru dtype. NumPy postara się przekonwertować wszystkie wartości do zadanego typu.
c = np.array([1, 2, 3], dtype='float32')
c
Pakiet NumPy udostępnia nam również wiele metod automatycznego tworzenia tablic według konkretnych szablonów. Przykładowe to np.zeros() tworząca tablicę o zadanym rozmiarze, wypełnioną samymi zerami (jest analogiczna metoda p.ones() z jedynkami), czy p.eye(), zwracająca kwadratową tablicę, na której głównej przekątnej są jedynki, a reszta to zera.
Za pomocą metody np.full((row, col), fill_value) stworzymy tablicę, o zadanym kształcie, której wszystkie pola będą równe zadanej w parametrze fill_value wartości.
Nieco bardziej skomplikowana jest ostatnia z pokazanych poniżej metod, czyli np.empty(). Jako parametry przyjmuje ona podobnie jak wyżej wymiary wynikowej tablicy. Różni się ona od np.fill() tym, że wypełnia tablicę przypadkowymi wartościami zależnymi od stanu pamięci w momencie jej wywołania.
np.zeros((3, 3))
np.eye(2)
np.full((3, 4), 2)
np.empty((3,3))
Często będziemy potrzebowali wytworzyć tablicę (zazwyczaj jednowymiarową - wektor), posiadającą dokładnie określoną ilość elementów z jakiegoś konkretnego przedziału. Przychodzą nam wtedy z pomocą dwie metody: np.arange() i np.linspace(). Pierwsza działa podobnie jak pythonowe range() przyjmując start, stop i step, i zwracając jednowymiarową tablicę liczb zdefiniowanych tymi parametrami.
Nieznacznie inaczej działa np.linspace(), w której spośród trzech parametrach definiujemy również start i stop, ostatni parametr jednak określa na ile równo oddalonych od siebie wartości wydzielić z przedziału [start, stop]. Bardzo przydatna metoda przy generowaniu zbioru wartości potrzebnego do zbudowania wykresu.
np.arange(5)
np.arange(1, 10, 2)
np.linspace(0, np.pi, 10)
Na koniec pozostało tworzenie tablic wypełnionych randomowymi wartościami. Pakiet NumPy posiada wiele metod spełniających to zadanie, chciałbym się tu jednak skupić na dwóch, różniących się od siebie zwracanym zakresem, z jakiego wybierana jest randomowa wartość.
Metoda np.random.rand() zwraca tablicę o zadanym kształcie wypełnioną przypadkowymi wartościami z przedziału [0, 1). Natomiast metoda np.random.randint() poza wymiarami tablicy przyjmuje ponadto zakres, z jakiego mają zostać wygenerowane losowe wartości całkowite. Górna wartość nie wchodzi do zakresu.
np.random.rand(3,2)
np.random.randint(2, 5, (3,2))
np.random.rand()
np.random.randint(0, 10)
Obie powyższe metody mogą również służyć do generowania pojedynczych wartości, jeśli tylko pominiemy w parametrach wymiary wynikowej tablicy.
Zmiana wymiarów tablicy
Kolejną ważną funkcjonalności NumPy jest możliwość zmiany kształtu tablic. Jak już wiemy parametr ndarray.shape pozwala nam sprawdzić długość osi tablicy, czyli dla tablicy 2D ilość jej kolumn i wierszy. Za pomocą metody ndarray.reshape() możemy wygenerować nową tablicę na podstawie innej tablicy, zadając jej konkretne wymiary. Trzeba jednak pamiętać, że rows * columns muszą dawać sumaryczną ilość elementów w tablicy.
a = np.arange(1, 10)
a
b = a.reshape(3,3)
b
Metodą bardzo podobną do ndarray.reshape() jest metoda ndarray.resize(), przyjmuje ona również ilość wierszy i kolumn oczekiwanej tablicy. Różnica polega na tym, że .reshape() zwraca nową tablicę, którą trzeba zapisać do zmiennej w celu zachowania, .resize() natomiast zmienia wymiary tablicy, na której zostało wykonane.
a.resize(3,3)
a
Często będziemy korzystali z NumPy do wykonywania operacji związanych z macierzami, bardzo przydatna wtedy okaże się transpozycja macierzy (naszej tablicy), czyli odwrócenie jej przez główną oś. W numpy wykonujemy ją za pomocą metody ndarray.T.
Prostą i przydatną metodą jest również ndarray.ravel(), która spłaszcza tablicę do jednowymiarowej.
a.T
a.ravel()
Często, przy dużych tablicach, zwłaszcza tych z większą niż dwa liczbą wymiarów, nie jesteśmy w stanie prosto i szybko określić jakie wartości powinniśmy zadać do .reshape() aby idealnie pokryły ilość elementów tablicy. Wiemy np., że potrzebujemy 3 wiersze. Możemy wtedy zadać metodzie .reshape() wartość -1, w miejsce wymiaru, którego nie znamy, a metoda sama dopasuje pasującą wartość.
np.linspace(0, 3, 12).reshape(3, -1)
Podstawowe operacje
Cechą tablic NumPy, która definitywnie pokazuje ich wyższość nad zwykłymi tablicami z pythona, jest sposób, w jaki przeprowadzane są na nich operacje matematyczne. Mianowice operacje przeprowadzana są kolejno na wszystkich elementach.
a = np.arange(5)
a
b = np.array([3.2, 4., 0.8, 1., 3.1])
b
a + b
Opisując to na przykładzie dodawanie dwóch tablic, każdy element wynikowej tablicy jest sumą elementów obu tablic na tej samej pozycji. Warunkiem takiego zachowania, są te same wymiary obu tablic. Odstępstwa od tej zasady zostaną omówione, gdy będę opisywał "Broadcasting".
Możemy również wykonywać operacje na tablicach, gdzie drugim składnikiem operacji jest skalar. Potęgowania tablicy spowoduje, że w wynikowej tablicy każdy z elementów będzie podniesiony do potęgi.
a**2
Natomiast wykonanie operacji logicznych, typu większy niż, mniejszy niż itp., zwraca tablicę o tym samym kształcie, gdzie każdy z jej elementów odpowiada wynikowi logicznej operacji przeprowadzonej na elemencie wejściowej tablicy.
b > 3
Zasadniczą cechą powyższych operacji jest tworzenie nowej tablicy, którą, aby zachować trzeba zapisać do zmiennej. Da się jednak w prosty sposób zmienić to zachowanie, używając do tych prostych operacji zapisu +=, *= itp. znanych z pythona.
b += a
b
Pakiet NumPy daje nam również możliwość wykonać w prosty sposób tak zwane operacje jednoargumentowe na całych tablicach. Mowa to o np. znalezieniu sumy wszystkich elementów - ndarray.sum(), wartości największej i najmniejszej - ndarray.max(), ndarray.min() itp.
c = np.arange(0, 12).reshape(3, 4)
c
c.sum()
c.min()
c.max(axis=1)
Każda z powyższych operacji zwraca pojedynczy wynik dla całej tablicy. Można to zachowanie zmienić, podając która oś ma zostać zredukowana. W powyższym wypadku zadając axis=1, mówimy metodzie .max(), aby zredukowała kolumny do jednej i przedstawiła w niej największe wartości dla wszystkich wierszy. Pamiętajmy, że w NumPy oś 0 to oś wierszy, a 1 oś kolumn.
Warto również dodać, że gdy przeprowadzamy operacje na dwóch tablicach lub tablicy i wartości skalarnej i posiadają one różne typy danych, wynikowa tablica odziedziczy dokładniejszy typ danych. Czyli gdy np. pomnożymy tablicę z dtype=int64 przez pojedynczą wartość mającą dtype=float64, cała wynikowa tablica będzie zawierała liczby zmiennoprzecinkowe.
Uniwersalne funkcje NumPy
Pakiet NumPy udostępnia również użytkownikom zestaw przygotowanych funkcji, które przyjmują tablicę i zwracają wynik określonej operacji matematyczne, przeprowadzonej element po elemencie, jako tablicę o tych samych wymiarach. W skrócie nazywane są unfunc od Universal Functions. Możemy dzięki nim przeprowadzić wiele przydatnych operacji jak liczenie potęg, pierwiastków czy wartości funkcji trygonometrycznych.
Większość z nich pozwala również na podanie jako argument wartości skalarnej, zwracają wtedy również wynik obliczeń jako wartość skalarną.
np.exp(a)
np.sqrt(a)
np.sin(a)
Lista wszystkich dostępnych funkcji z odnośnikami do ich specyfikacji dostępna jest tutaj
Indexing & Slicing
Kolejnym niezmiernie ważnym elementem pracy z tablicami NumPy jest możliwość wybierania pojedynczych elementów bądź podtabel na podstawie konkretnych indeksów. W przypadku jednowymiarowych tablic indeksowanie wygląda dokładnie tak jak w przypadku tabel pythona. Możemy wybierać pojedyncze elementy bądź całe fragmenty podając index początku i końca wycinka oddzielone dwukropkiem my_array[1:3]. Pamiętajmy, że końcowy index nie wchodzi do wycinka - [start, stop). Podobnie jak w przypadku tablic możemy pominąć pierwszy element, gdy wycinek zaczyna się od 0 (np. my_array[:2]) i analogicznie, gdy kończy się na ostatnim elemencie.
a = np.arange(10)
a
a[3]
a[5:7]
a[:4]
a[-1]
Gdy mamy do czynienia z tablicami o większej niż jeden liczbie wymiarów, do indeksowania i tworzenia wycinków nie używamy znanej z pythona składni array[x][y]. W NumPy indeksy dla poszczególnych osi zapisujemy w pojedynczym kwadratowym nawiasie, oddzielone od siebie przecinkiem.
Dla dwuwymiarowych tablic pierwsza wartość określa wiersza, a druga kolumny. Tutaj również wycinki określamy, oddzielając początek i koniec znakiem dwukropka.
b = np.array([[1, 2, 3, 4], [11, 12, 13, 14], [21, 22, 23, 24]])
b
b[0,0]
b[1:, 1:3]
Jeśli natomiast podamy mniej indeksów, niż tablica ma wymiarów, brakujące indeksy zostaną uzupełnione znakiem :, czyli pobrane zostaną wszystkie elementy.
b[2]
Nie wyczerpuje to oczywiście możliwości indeksowania i wycinania fragmentów tablic. Po bardziej szczegółowe informacje i więcej przykładów odsyłam do dokumentacji. Tematy związane z indeksowaniem zostaną również poruszone w kolejnej części wpisu o NumPy, gdzie zajmiemy się trochę bardziej skomplikowanymi zagadnieniami, jak np. fancy indexing czy broadcasting.
Podsumowanie
W tym wpisie skupiłem się na przedstawieniu bardzo ogólnego zarysu możliwości pakietu NumPy. Tematy tu omówione pozwolą zacząć z nim pracę, lecz zdecydowanie nie wyczerpują tematu. Skupiam się wyłącznie na najprostszych zagadnieniach oraz omawiam tylko nieliczne bardzo wielu możliwości pakietu. Z tego też względu powstanie kolejny wpis, będący kontynuacją i rozszerzeniem opisu podstaw NumPy.