Python - wprowadzenie

03 - Pakiet `NumPy` cz. 1

Pakiet NumPy jest powszechnie stosowany w obliczeniach naukowych i nie tylko. Jego strona domowa znajduje się pod adresem numpy.org. Powinien być zainstalowany wraz ze środowiskiem Anaconda.

Poznajmy kilka użytecznych elementów pakietu. Zacznijmy od ndarray, który jest wielowymiarowym, tablicowym (ang. array) obiektem umożliwiającym przechowywanie i szybką obróbkę dużej ilości danych określonego typu.

Tworzenie tablic

Zacznijmy od poznania kilku sposobów utworzenia jednowymiarowych tablic z liczbami.

# import pakietu numpy
import numpy as np
# Utworzenie tablic z listy
tab_1 = np.array([1, 2, 3, 4])
print(f'tab_1 {tab_1}, typ: {type(tab_1)}')
lis = [5, 6, 7, 8]
tab_2 = np.array(lis)
print(f'tab_2 {tab_2}, typ: {type(tab_2)}')
tab_1 [1 2 3 4], typ: <class 'numpy.ndarray'>
tab_2 [5 6 7 8], typ: <class 'numpy.ndarray'>
# z krotki
tab_3 = np.array((9, 10, 11, 12))
print(f'tab_3 {tab_3}, typ: {type(tab_3)}')
tab_3 [ 9 10 11 12], typ: <class 'numpy.ndarray'>
# Tablica z wygenerowanej sekwencji kolejnych liczb
# analogicznie do funkcji range()
tab_4 = np.arange(10)
print(f'tab_4 {tab_4}, typ: {type(tab_4)}')
tab_4 [0 1 2 3 4 5 6 7 8 9], typ: <class 'numpy.ndarray'>
# (Pseudo)losowe liczby ze standardowego rozkłady normalnego
tab_5 = np.random.randn(5)
print(f'tab_5 {tab_5}, typ: {type(tab_5)}')

tab_5 [ 0.15194022 -1.7265475  -0.57213547  0.31769404  0.80205681], typ: <class 'numpy.ndarray'>
# 10-elementowa tablica wypełniona zerami
tab_6 = np.zeros(10)
print(f'tab_6 {tab_6}, typ: {type(tab_6)}')
tab_6 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.], typ: <class 'numpy.ndarray'>
# 10-elementowa tablica wypełniona jedynkami
tab_7 = np.ones(10)
print(f'tab_7 {tab_7}, typ: {type(tab_7)}')
tab_7 [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.], typ: <class 'numpy.ndarray'>
# 10-elementowa tablica wypełniona siódemkami
tab_8 = np.full(10, 7)
print(f'tab_8 {tab_8}, typ: {type(tab_8)}')
tab_8 [7 7 7 7 7 7 7 7 7 7], typ: <class 'numpy.ndarray'>

Utwórzmy kolejną tablicę (typu ndarray) i porównajmy ją z listą:

# Import modułu
import numpy as np
# Utworzenie listy z kolejnymi wartościami od 0 do 9
lis = list(range(10))
# Utworzenie tablicy z kolejnymi wartościami od 0 do 9
tab = np.arange(10)
# Drukowanie zawartości
print(f'Lista:   {lis}')
print(f'Tablica: {tab}')
# Sprawdzenie typu obiektów
print(f'Typ listy: {type(lis)}\nTyp tablicy: {type(tab)}')
# Mnożenie
print(f"lis * 2: {lis * 2}")
print(f"tab * 2: {tab * 2}")
Lista:   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Tablica: [0 1 2 3 4 5 6 7 8 9]
Typ listy: <class 'numpy.ndarray'>
Typ tablicy: <class 'list'>
lis * 2: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
tab * 2: [ 0  2  4  6  8 10 12 14 16 18]

Zwróć uwagę, że o ile w przypadku listy operator * spowodował wygenerowanie listy z powtórzoną serią danych, dane w tablicy zostały pomnożone i została zwrócona tablica ze zmodyfikowanymi liczbami. Aby uzyskać analogiczny wynik dla listy należałoby użyć np. takiego kodu:

lis2 = [i * 2 for i in lis]
print(lis2)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Jak widać, mimo zastosowania listy składanej, operacja na liście była bardziej skomplikowana. Co więcej, nie będziemy tego sprawdzać, ale obiekty udostępniane przez pakiet numpy umożliwiają nawet kilkudziesięciokrotnie szybszą pracę z danymi niż w przypadku struktur, które poznaliśmy wcześniej (jak listy).

Działania na tablicach jednowymiarowych

Sprawdźmy kilka innych możliwości:

import numpy as np
tab_1 = np.arange(10)
print(f'tab_1: {tab_1}')
# Długość tablicy
print(f'Długość:     {tab_1.size}')
# Proste działania arytmetyczne
print(f'Dodawanie:   {tab_1 + 2}')
print(f'Odejmowanie: {tab_1 - 2}')
print(f'Mnożenie:    {tab_1 * 2}')
print(f'Dzielenie:   {tab_1 / 2}')
print(f'Potęgowanie: {tab_1 ** 2}')
tab_1: [0 1 2 3 4 5 6 7 8 9]
Długość:     10
Dodawanie:   [ 2  3  4  5  6  7  8  9 10 11]
Odejmowanie: [-2 -1  0  1  2  3  4  5  6  7]
Mnożenie:    [ 0  2  4  6  8 10 12 14 16 18]
Dzielenie:   [0.  0.5 1.  1.5 2.  2.5 3.  3.5 4.  4.5]
Potęgowanie: [ 0  1  4  9 16 25 36 49 64 81]

W działaniach można też zastosować dwie (lub więcej) tablice o takiej samej długości:

# Bezpośredni sposób tworzenia tablic
tab_2 = np.arange(10, 101, 10)
print(f'tab_2: {tab_2}')
print(f'Dodawanie tablic:   {tab_1 + tab_2}')
print(f'Odejmowanie tablic: {tab_1 - tab_2}')
print(f'Mnożenie tablic:    {tab_1 * tab_2}')
print(f'Dzielenie tablic:   {tab_1 / tab_2}')
print(f'Potęgowanie tablic: {tab_1 ** tab_2}')
tab_2: [ 10  20  30  40  50  60  70  80  90 100]
Dodawanie tablic:   [ 10  21  32  43  54  65  76  87  98 109]
Odejmowanie tablic: [-10 -19 -28 -37 -46 -55 -64 -73 -82 -91]
Mnożenie tablic:    [  0  20  60 120 200 300 420 560 720 900]
Dzielenie tablic:   [0.         0.05       0.06666667 0.075      0.08       0.08333333
 0.08571429 0.0875     0.08888889 0.09      ]
Potęgowanie tablic: [                   0                    1           1073741824
 -6289078614652622815                    0  8512967443501092241
                    0 -1156583747620310143                    0
  6627890308811632801]

Przy okazji (potęgowanie) zauważ, że przy dużych liczbach pojawił się problem z dużymi liczbami. Wrócimy do tego później.

Dość łatwo można porównać zawartość dwu tablic, otrzymując tablicę z wynikiem porównania (True/False) kolejnych elementów:

tab_3 = np.array([1, 2, 3, 4])
tab_4 = np.array([1, 3, 3, 5])
porownanie = tab_3 == tab_4
print(f'Porównanie: {porownanie}, typ: {type(porownanie)}')
Porównanie: [ True False  True False], typ: <class 'numpy.ndarray'>

Obiekty ndarray mają zdefiniowane metody m. in. przydatne w szybkim przeprowadzaniu prostych operacji matematycznych czy obliczaniu prostych wartości statystycznych. Na przykład:

dane = np.array([2.3, 2.4, 1.9, 2.0, 2.5, 1.8, 2.1, 1.8])
print(dane)
# Suma
print(f'Suma:        {dane.sum()}')
# Wartość największa
print(f'Największa:  {dane.max()}')
# Najmniejsza
print(f'Najmniejsza: {dane.min()}')
# Średnia arytmetyczna
print(f'Średnia:     {dane.mean()}')
# Wariancja
print(f'Wariancja:   {dane.var()}')
# Odchylenie standardowe
print(f'Odch. stand: {dane.std()}')

[2.3 2.4 1.9 2.  2.5 1.8 2.1 1.8]
Suma:        16.799999999999997
Największa:  2.5
Najmniejsza: 1.8
Średnia:     2.0999999999999996
Wariancja:   0.06499999999999997
Odch. stand: 0.2549509756796392

Można też posortować elementy w tablicy, należy jednak pamiętać, że nie jest zwracana kopia tablicy z posortowanymi wartościami, ale zmienia się ich kolejność w tablicy, na której wywołujemy metodę:

# Wartości posortowane 
dane.sort()
print(f'Posortowane: {dane}') 
Posortowane: [1.8 1.8 1.9 2.  2.1 2.3 2.4 2.5]

Odwołanie do elementów tablic jednowymiarowych

Odwoływanie się do poszczególnych elementów tablic jednowymiarowych, wygląda podobnie jak w przypadku list:

import numpy as np
tab = np.arange(10)
print(tab)
# Element pod indeksem 1
print(tab[1])
[0 1 2 3 4 5 6 7 8 9]
1
# Ostatni element
print(tab[-1])
9
# Elementy do indeksu 4 (4 pierwsze)
print(tab[:4])
[0 1 2 3]
# Cztery ostatnie elementy
print(tab[-4:])
[6 7 8 9]
# Elementy między indeksami 2 i 7-1
print(tab[2:7])
[2 3 4 5 6]
# Elementy między indeksami 2 i 7-1, co drugi
print(tab[2:7:2])
[2 4 6]
# Wszystkie elementy odwrotnie
print(tab[::-1])
[9 8 7 6 5 4 3 2 1 0]
# Elementy między indeksami 7 i 2+1 (odwrotnie)
print(tab[7:2:-1])
[7 6 5 4 3]
# Zmiana elementu pod indeksem 3
tab[3] = 99
print(tab)
[ 0  1  2 99  4  5  6  7  8  9]

Tablice wielowymiarowe

Jak wspomniałem na początku, można tworzyć wielowymiarowe tablice ndarray. Zacznijmy od dwu wymiarów. Tablicę dwuwymiarową można utworzyć np. tak:

tab_2d = np.array([[0, 1, 2], 
                   [3, 4, 5], 
                   [6, 7, 8]])
print(tab_2d)
[[0 1 2]
 [3 4 5]
 [6 7 8]]

Jak widać otrzymaliśmy tablicę, zawierającą tablice jednowymiarowe, które możemy interpretować jako rzędy. Możemy się odwołać do poszczególnych elementów na dwa sposoby:

print(tab_2d[2][1])
print(tab_2d[2, 1])
7
7

Rzędy i kolumny pobieramy tak:

# Drugi rząd:
print(tab_2d[1,])
# Druga kolumna:
print(tab_2d[:,1])
[3 4 5]
[1 4 7]

Można też wybrać odpowiednie wycinki tablicy:

print(tab_2d[2,1:])
print(tab_2d[:2,:])
print(tab_2d[:2,:2])
[7 8]
[[0 1 2]
 [3 4 5]]
[[0 1]
 [3 4]]

Zauważ, że liczba kolumn musi się zgadzać w każdym rzędzie:

tab_2d = np.array([[0, 1, 2], 
                   [3, 4, 5], 
                   [6, 7, 8, 9]])
<ipython-input-93-b10b20179f94>:1: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray
  tab_2d = np.array([[0, 1, 2],

Analogicznie do dwuwymiarowych, można utworzyć tablice o większej liczbie wymiarów, zobaczmy to na przykładzie tablicy trójwymiarowej:

tab_3d = np.array([[[1, 2, 3],[4, 5, 6]],[[7, 8, 9],[10, 11, 12]]])
print(tab_3d)
[[[ 1  2  3]
  [ 4  5  6]]

 [[ 7  8  9]
  [10 11 12]]]

Do wartości w takiej tablicy odwołujemy się analogicznie do dwuwymiarowej, dodając indeks(-y) dodatkowego wymiaru:

print(tab_3d[1,0,2])
9
print(tab_3d[1,:2,2])
[ 9 12]
print(tab_3d[0:,:2,2])
[[ 3  6]
 [ 9 12]]

Takich funkcji jak zeros() można używać także przy tworzeniu tablic o wielu wymiarach, w takim przypadku, należy odpowiednie parametry podać w krotce, jako argument:

tab_2d_0 = np.zeros((2, 5, 10))
print(tab_2d_0)
[[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]

 [[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]]

Informację o liczbie wymiarów i kształcie tablicy można uzyskać z odpowiednich atrybutów:

print(f'Liczba wymiarów: {tab_2d_0.ndim}')
print(f'Kształt:         {tab_2d_0.shape}')
Liczba wymiarów: 3
Kształt:         (2, 5, 10)

Zmiana wartości tablicy

Skoro wiemy jak odwołać się do komórek, sprawdźmy jak wygląda modyfikacja ich zawartości. Przy okazji poznamy dwie nowe funkcje ułatwiających tworzenie tablic z określoną zawartością (jest ich znacznie więcej).

Wartości w tablicy jednowymiarowej można zmieniać przypisując nowe wartości do określonej komórki, lub komórek:

import numpy as np

# Generowanie tablicy z równo rozłożonymi 11 wartościami
# (zmiennoprzecinkowymi) między 0 i 20
tab_1d = np.linspace(0, 20, 11)
print(tab_1d)
[ 0.  2.  4.  6.  8. 10. 12. 14. 16. 18. 20.]
# Zmiana pojedynczej wartości
tab_1d[0] = 77
print(tab_1d)
[77.  2.  4.  6.  8. 10. 12. 14. 16. 18. 20.]
#Zmiana zakresu wartości
tab_1d[5:8] = 0
print(tab_1d)
[77.  2.  4.  6.  8.  0.  0.  0. 16. 18. 20.]
#Zmiana wartości w całej tablicy
tab_1d[:] = 1
print(tab_1d)
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

Analogicznie postępujemy z tablicami wielowymiarowymi, podając komórkę, lub wycinek, który chcemy zmienić:

# Generowanie tablicy 5x5 wypełnionej l. całkowitymi:  
# 1 na przekątnej i 0 w pozostałych miejscach
tab_2d = np.identity(5, int)
print(tab_2d)
[[1 0 0 0 0]
 [0 1 0 0 0]
 [0 0 1 0 0]
 [0 0 0 1 0]
 [0 0 0 0 1]]
#Zmiana pojedynczej wartości
tab_2d[0, 2] = 7
print(tab_2d)
[[1 0 7 0 0]
 [0 1 0 0 0]
 [0 0 1 0 0]
 [0 0 0 1 0]
 [0 0 0 0 1]]
#Zmiana w kolumnie 
tab_2d[:, 1] = 6
print(tab_2d)
[[1 6 7 0 0]
 [0 6 0 0 0]
 [0 6 1 0 0]
 [0 6 0 1 0]
 [0 6 0 0 1]]
#Zmiana w rzędzie 
tab_2d[2, :] = 4
print(tab_2d)
[[1 6 7 0 0]
 [0 6 0 0 0]
 [4 4 4 4 4]
 [0 6 0 1 0]
 [0 6 0 0 1]]
#Zmiana w wycinku tabeli 
tab_2d[3:5, 2:] = 8
print(tab_2d)
[[1 6 7 0 0]
 [0 6 0 0 0]
 [4 4 4 4 4]
 [0 6 8 8 8]
 [0 6 8 8 8]]
Last updated on 10 Mar 2021
Published on 10 Mar 2021