In [1]:
# setup
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

Python w obliczeniach naukowych

Język python dzięki wielu dodatkowym bibliotekom stał się potężnym narzędziem do obliczeń numerycznych, analizy i eksploracji danych, wizualizacji itp. Do tej grupy w pierwszej kolejności wchodzą:

  • numpy - podstawowy pakiet dostarczający narzędzi do zarządzania wielowymiarowymi tablicami oraz algebry liniowej (macierze)
  • scipy - pakiet zawierajacy funkcje do zaawansowanych obliczeń numerycznych. Jest obecnie nadrzędnym pakietem zbioru
  • pandas - pakiet przeznaczony do zarządzania tabelarycznymi strukturami danych "w stylu R"
  • matplotlib - kompletna biblioteka do wizualizacji 2D
  • sympy - pakiet do obliczeń symbolicznych (matematycznych)
  • scikit- grupa pakietów przeznaczonych do realizacji specyficznych zadań naukowych oparte na powyższych bibliotekach

W ramach tej części kursu zapoznamy się z podstawami działania pakietów numpy, scipy i matplotlib. Pozostałe pakiety pojawią si ę w kolejnych wersjach kursu.

numpy

Jest to podstawowy pakiet do obliczeń numerycznych koncentrujący się na zarządzaniu macierzami dwuwymiarowymi. Podstawową zaletą numpy jest silna typiczność - to znaczy że tablice numpy mają z góry określony typ danych.

tworzenie tablicy

Tablice tworzy się najczęściej na bazie list zawierających dane numeryczne, lub bezpośrednio wczytując wartości binarne.

In [2]:
import numpy as np
# utworzenie tablicy, typ rozpoznany z danych
arr = np.array([1,2,3,4,5,6])
arr
arr.dtype

# utworzenie tablicy z wymuszonym typem danych
arr = np.array([1,2,3,4,5,6],dtype='int8')
arr
arr.dtype

# utworzenie tablicy z wymuszonym typem danych zmiennoprzecinkowych
arr = np.array([1,2,3,4,5,6],dtype='float32')
arr
arr.dtype
Out[2]:
array([1, 2, 3, 4, 5, 6])
Out[2]:
dtype('int64')
Out[2]:
array([1, 2, 3, 4, 5, 6], dtype=int8)
Out[2]:
dtype('int8')
Out[2]:
array([1., 2., 3., 4., 5., 6.], dtype=float32)
Out[2]:
dtype('float32')

Istnieje wiele funkcji pozwalających na definiowanie tablic na podstawie algorytmów

In [3]:
# utworzenie tablicy o określonym zasięgu wartości i kroku 
np.arange(10)
np.arange(10,30,2)

# utworzenie tablicy o określonym zasięgu wartości i liczbie elementów
np.linspace(10,20,3)

# utworzenie tablicy wypełnionej określoną liczbą (0,1,NaN, dowolna)
np.zeros(10)
np.full(10,np.nan)
np.full(10,3,dtype='int8') # wymuszenie typu

# pusta tablica, zawiera losowe wartości
np.empty(10,dtype='int16') 
Out[3]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Out[3]:
array([10, 12, 14, 16, 18, 20, 22, 24, 26, 28])
Out[3]:
array([10., 15., 20.])
Out[3]:
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
Out[3]:
array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])
Out[3]:
array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3], dtype=int8)
Out[3]:
array([     0,      0,      0,      0,  -5216,    424,      0,      0,
       -25592,  31070], dtype=int16)

Można również generować tablice liczb losowych. Więcej na https://docs.scipy.org/doc/numpy-1.14.0/reference/routines.random.html

In [4]:
np.random.random((5,2))
np.random.randint(10,40,(3,4)) # w przedziale
Out[4]:
array([[0.90720731, 0.19812154],
       [0.97995222, 0.06785779],
       [0.62252761, 0.75946765],
       [0.863131  , 0.19451431],
       [0.47426975, 0.09863673]])
Out[4]:
array([[14, 11, 34, 38],
       [20, 28, 15, 13],
       [21, 31, 18, 27]])

Aby zmienić wymiary macierzy należy zmodyfikować jej atrybut shape (to nie jest funkcja, tylko atrybut!), przy pomocy krotki zawierającej tyle wymiarów ile ma liczyć nowa wersja tablicy. W przypadku tablicy dwuwymiarowej jako pierwszy podaje się liczbę wierszy a jako drugi liczbę kolumn.

In [5]:
arr = np.arange(12)
arr.shape = (3,4) # krotka definiująca tablicę dwuwymiarową
arr
arr.ndim
arr.data # miejsce gdzie znajdują się dnae
Out[5]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
Out[5]:
2
Out[5]:
<memory at 0x7f30c007fdc8>

Można też utworzyć macierz bezwymiarową lub z jednym wymiarem zerowym. Takie macierze są przydatne jeżeli zamierzamy je rozszerzać w kolejnych krokach. Dla przykładu utworzymy macierz która ma 10 kolumn i 0 wierszy:

In [6]:
barr = np.empty((0,10))
barr
Out[6]:
array([], shape=(0, 10), dtype=float64)

Podstawowe operacje

Tablice numpy pozwalają na stosowanie rachunku wektorowego, tj modyfikacji każdego elementu tablicy bez konieczności stosowania pętli. Dzięki temu operacje na tablicach wykonuje się tak jak na pojedynczych wartościach skalarnych

In [7]:
arr = np.arange(15)
arr.shape = (3,5)
arr

# mnożenie przez skalar
arr*3

# dodanie dwóch macierzy
narr = np.arange(2,17)
narr.shape = (3,5)
print("B przed dodaniem")
narr
print("A+B po dodaniu")
arr+narr
Out[7]:
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
Out[7]:
array([[ 0,  3,  6,  9, 12],
       [15, 18, 21, 24, 27],
       [30, 33, 36, 39, 42]])
B przed dodaniem
Out[7]:
array([[ 2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16]])
A+B po dodaniu
Out[7]:
array([[ 2,  4,  6,  8, 10],
       [12, 14, 16, 18, 20],
       [22, 24, 26, 28, 30]])

Transpozycja albo inaczej przestawianie macierzy to operacja obrotu wymiarów, tak, że wiersze tablicy stają się kolumnami a kolumny wierszami. Jest to jedna z najważniejszych operacji macierzowych, stosuje się ją wszędzie tam gdzie są ściśle zdefiniowane wymogi co do roli wierszy i kolumn. Do transpozycji macierzy służy funkcja transpose(), która pozwala dodatkowo wybrać osie transpozycji natomiast w praktyce stosuje się atrybut T, do wykonania prostego przekształcenia w dwóch wymiarach.

In [8]:
# transpozycja macierzy (obrót wymiarów)
arr
tarr=arr.transpose()
tarr
ttarr=arr.T
ttarr
Out[8]:
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
Out[8]:
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])
Out[8]:
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])

Mnożenie macierzy przez skalar jest proste do zrozumienia, o tyle mnożenie macierzy przez wektor jest mniej zrozumiałe i polega na przemnożeniu pól wektora przez kolumny. Może się okazać że niezbędne jest wcześniejsze odwrócenie macierzy, tak aby liczba kolumn była zgodna z długością wektora.

In [9]:
# mnożenie macierzy przez wektor
tarr.shape
vect = np.array([10,20,30]) # wektor musi mieć długość 3
np.multiply(tarr,vect)
Out[9]:
(5, 3)
Out[9]:
array([[  0, 100, 300],
       [ 10, 120, 330],
       [ 20, 140, 360],
       [ 30, 160, 390],
       [ 40, 180, 420]])

Niektóre operacje mają charakter metod obiektu - najczęściej wtedy gdy nie są symetryczne. Na przykład przecięcie (dot product) może być zastosowane tylko do macierzy przyjmując wektor jako argument. Przecięcie macierzy i wektora to suma wszystkich wierszy macierzy przemnożonej przez wektor.

In [10]:
# przecięcie wektora z macierzą (dot product)
tarr.dot(vect)
Out[10]:
array([400, 460, 520, 580, 640])

Argument axis jest bardzo ważny w operacjach macierzowych jeżeli operacje wektorowe mają być wykonywane wzdłóż tylko jednej osi (wymiaru). Kontynuując poprzedni przykład poznamy dwie nowe możliwości operacji numpy. W poniższym przykładzie złożyliśmy wykonanie dwóch funkcji: przemnożenia macierzy i wektora i wynik tego przemnożenia - też macierz - zostanie zsumowany wzdłuż kolumn - dlatego używamy wymiaru 1. Sumowanie wzdłóż wierszy byłby to wymiar 0.

W przypadku, gdy nie podamy żadnej osi, należy sprawdzić jak dana funkcja/metoda działa. Niektóre przyjmują jako domyślą oś 0, inne (w tym sum()) wykonują działanie wzdłuż wszystkich osi - a dokładnie na wersji spłaszczonej.

In [11]:
np.multiply(tarr,vect).sum(axis=1)
np.multiply(tarr,vect).sum(axis=0)
np.multiply(tarr,vect).sum()
Out[11]:
array([400, 460, 520, 580, 640])
Out[11]:
array([ 100,  700, 1800])
Out[11]:
2600

Macierz to tylko sposób uporządkowania wartości zdefiniowany przez wymiary. W praktyce jest to ciąg liczb. Każdą macierz można spłaszczyć funkcją flatten() do takiego ciągu. W zależności czy macierz poddamy wcześniej transpozycji kolejność wartości w spłaszczonym wektorze będzie różna. Spłaszczona macierz posiada tylko jeden wymiar.

In [12]:
tarr.flatten()
tarr.T.flatten()
tarr.T.flatten().shape=(5,3)
tarr
tarr.flatten().shape
Out[12]:
array([ 0,  5, 10,  1,  6, 11,  2,  7, 12,  3,  8, 13,  4,  9, 14])
Out[12]:
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])
Out[12]:
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])
Out[12]:
(15,)

Łączenie macierzy

Do łączenia macierzy służy grupa funkcji np.*stack. Do łączenia wzdłóż osi pionowej vstack, poziomej hstack na stosie wzdłuż nowego wskazanego wymiaru stack.

In [13]:
arr = np.arange(15)
arr.shape = (3,5)
arr
Out[13]:
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
In [14]:
np.vstack((arr,arr))
np.hstack((arr,arr))
np.stack((arr,arr))
Out[14]:
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
Out[14]:
array([[ 0,  1,  2,  3,  4,  0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9,  5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14, 10, 11, 12, 13, 14]])
Out[14]:
array([[[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14]],

       [[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14]]])

Funkcjami odwrotnymi dzielącymi macierze wzdłuż określonego wymiaru są odpowiednie funkcje split oraz vsplit i hsplit

In [15]:
np.split(arr,indices_or_sections=3)
np.split(arr,indices_or_sections=5,axis=1)
Out[15]:
[array([[0, 1, 2, 3, 4]]),
 array([[5, 6, 7, 8, 9]]),
 array([[10, 11, 12, 13, 14]])]
Out[15]:
[array([[ 0],
        [ 5],
        [10]]), array([[ 1],
        [ 6],
        [11]]), array([[ 2],
        [ 7],
        [12]]), array([[ 3],
        [ 8],
        [13]]), array([[ 4],
        [ 9],
        [14]])]

Dostęp do elementów macierzy

In [16]:
arr = np.arange(15)
arr.shape = (3,5)
arr
Out[16]:
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])

Dostęp elementów macierzy definiujemy poprzez indeksy. Kolejność indeksów jest taka sama jak wymiarów tablicy. Indeksowanie rozpoczynamy od 0. Możemy również wyznaczać zasięgi indeksów. Jeżeli omijamy zasiegi indeksów, oznacza do że domyślnie jest to początek lub koniec zasięu. Taki fragment macierzy nazywamy wycinkiem

In [17]:
arr[1,3]
arr[1:3,2:4]
arr[0:2,0:3]
print('to samo co powyżej')
arr[:2,:3] 
print('trzecia kolumna') 
arr[:,1]
Out[17]:
8
Out[17]:
array([[ 7,  8],
       [12, 13]])
Out[17]:
array([[0, 1, 2],
       [5, 6, 7]])
to samo co powyżej
Out[17]:
array([[0, 1, 2],
       [5, 6, 7]])
trzecia kolumna
Out[17]:
array([ 1,  6, 11])

W tablicy możemy wybierać również wartości na podtawie testu logicznego. Test logiczny zwraca tablicę wartości True/False a następnie jeżeli taką tablicę zastosujemy jako selekor, ograniczymy liczęb zwracanych wartości tylko do tych które spełniają test logiczny

In [18]:
art = np.random.random(20)
art
art<0.5
art[art>0.7]
Out[18]:
array([0.37771136, 0.32446564, 0.96727064, 0.4345882 , 0.7865981 ,
       0.81068222, 0.14881165, 0.72508893, 0.41064851, 0.5843248 ,
       0.24469452, 0.50944695, 0.44830526, 0.97777776, 0.43293418,
       0.65429203, 0.46109588, 0.43182416, 0.66941301, 0.76311035])
Out[18]:
array([ True,  True, False,  True, False, False,  True, False,  True,
       False,  True, False,  True, False,  True, False,  True,  True,
       False, False])
Out[18]:
array([0.96727064, 0.7865981 , 0.81068222, 0.72508893, 0.97777776,
       0.76311035])

Stosując selekcję można modyfikować wybrane elementy a nawet całe zasięgi

In [19]:
arr[1,4]=100
arr
arr[1:3,4]=[200,300]
arr
Out[19]:
array([[  0,   1,   2,   3,   4],
       [  5,   6,   7,   8, 100],
       [ 10,  11,  12,  13,  14]])
Out[19]:
array([[  0,   1,   2,   3,   4],
       [  5,   6,   7,   8, 200],
       [ 10,  11,  12,  13, 300]])

Wycinanie macierzy dwu- (i więcej) wymiarowych

W przypadku, gdy chcemy wyciąć nieciągły fragment macierzy na podstawie dowolnej tablicy numpy należy użyć funkcji np.ix_ do konwersji wymiarów, odpowiednio dla osi 0 i 1. W przeciwnym wypadku przycięcie nie będzie skuteczne.

In [20]:
arr = np.arange(100)
arr.shape = (10,10)
arr
selector=np.array([3,5,8])
Out[20]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
       [30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
       [40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
       [50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
       [60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
       [70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
       [80, 81, 82, 83, 84, 85, 86, 87, 88, 89],
       [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]])
In [21]:
arr[np.ix_(selector,selector)]
Out[21]:
array([[33, 35, 38],
       [53, 55, 58],
       [83, 85, 88]])

Wczytywanie danych do tablicy

Dane tabelaryczne można wczytać bezpośrednio do tablicy

In [22]:
arr = np.loadtxt("dane.txt", dtype='uint8')
In [23]:
arr
Out[23]:
array([[1, 2, 3, 4, 5],
       [5, 6, 7, 8, 9]], dtype=uint8)

matplotlib

Jest to bardzo rozbudowana biblioteka do wizualizacji danych numerycznych. Jest rozszerzeniem biblioteki numpy. Biblioteka ma wiele wbudowanych funkcji pozwalających na wykonanie praktycznie dowolnego wykresu oraz jego modyfikację. Niestety składania stosowana w matplotlib nie jest przejrzysta, i ma raczej charakter niskopoziomowy co oznacza że jest raczej przeznaczona dla programistów piszących narzędzia wizualizacyjne, niż do codziennego użytku.

Wykres liniowy

Podstawowy wykres opiera się na wykorzystaniu dwóch tablic numpy. Polecenie plot utworzy linię na podstawie danych.

In [24]:
import matplotlib.pyplot as plt
from numpy.random import normal,rand
x = np.linspace(0,10,100)
y = np.exp(-x)
plt.plot(x,y)
Out[24]:
[<matplotlib.lines.Line2D at 0x7f30768e4c88>]

Histogram

Dla danych jednowymiarowych (zawierających tylko jeden wymiar) można utwoyć histogram poprzedz podział ciągłych danych na przedziały

In [25]:
x = normal(size=200)
plt.hist(x,bins=10)
plt.show()
Out[25]:
(array([ 2.,  2., 12., 22., 44., 58., 32., 14., 11.,  3.]),
 array([-3.12759593, -2.54493138, -1.96226683, -1.37960228, -0.79693773,
        -0.21427318,  0.36839136,  0.95105591,  1.53372046,  2.11638501,
         2.69904956]),
 <a list of 10 Patch objects>)

Prosty wykres punktowy

Pobiera wspórzędne z dwóch kolumn i wyświetla je w postaci punktów o współrzędych zdefiniowanych przez punkty

In [26]:
x = rand(100)
x = rand(100)
plt.scatter(x,y)
plt.show()
Out[26]:
<matplotlib.collections.PathCollection at 0x7f3074291fd0>

Modyfikacja wykresu

Elementy wykresu można modyfikować. Wymaga to jednak wyjaśnienia na starcie kilku terminów:

  1. Najwyższy poziom to Figure. Obejmuje on wszystko to co tworzy pojedynczy obraz
  2. Pojedynczy wykres zawierający dane nazywa się Axes figure może zawierać wiele Axes.
  3. Każda z osi Axes to Axis
  4. Każda z Axes może posiadać własny Title oraz odpwiednio xlabel i ylabel

Istnieje wiele innych elementów wykresu, o których można przeczytać: [https://matplotlib.org/examples/showcase/anatomy.html]

W poniższym przykładzie uzupełnimy powyższy wykres o kilka dodatkowych elementów:

In [27]:
plt.scatter(x,y)
plt.axis([-1, 2, -2, 3]) # zakres osi (xmin,xmax,ymin,ymax)
plt.xlabel('Oś X')
plt.ylabel('Oś Y')
plt.title('Wykres rozproszony z przesunietym zakresem osi')
Out[27]:
<matplotlib.collections.PathCollection at 0x7f3074256a58>
Out[27]:
[-1, 2, -2, 3]
Out[27]:
Text(0.5,0,'Oś X')
Out[27]:
Text(0,0.5,'Oś Y')
Out[27]:
Text(0.5,1,'Wykres rozproszony z przesunietym zakresem osi')

Podwykresy

Matplotlib pozwala również na tworzenie wykresów składających się z pod-wykresów (axes). W tym celu należy utworzyć Figure (tu: fig) zawierającą macierz podwykresów z jednym wierszem i dwoma kolumami. Parametr "sharey=True" oznacza, że oba wykresy dzielą zasięg osi y. Funkcja plt.subplots() zwraca obiekt Figure oraz macierz obietów Axes pod nazwą ax. Dostęp do poszczególnych pod-wykresów mamy przez indeksowanie. Dla pierwszego pod wykresu wykonamy histogram liczący 12 przedziałów typu schodkowego a dla drugiego wypełniony, czerwony o 8 przedziałach. Następnie dla każdego sub-wykresu ustawimy tytuły. Ustawianie parametrów dla sub-wykresów rozpoczyna się z reguły przedrostkiem set_. W ostatnim kroku ustawimy tytuł główny sup(er)title dla całej figury, o określonym rozmiarze tekstu. W ostatniej części ustawimy szerokość Figure na 9 cali.

In [28]:
x = normal(size=200)
fig, ax = plt.subplots(1,2,sharey=True)
ax[0].hist(x,bins=12,histtype='step')
ax[1].hist(x,bins=8,color='#FF1100')
ax[0].set_title("wykres schodkowy")
ax[1].set_title("wykres pełny")
fig.suptitle("Dwa wykresy", fontsize=14)
fig.set_figwidth(9)
Out[28]:
(array([ 1.,  3.,  6.,  9., 34., 36., 35., 30., 21., 19.,  3.,  3.]),
 array([-3.29647776, -2.80017355, -2.30386935, -1.80756514, -1.31126093,
        -0.81495672, -0.31865251,  0.1776517 ,  0.67395591,  1.17026012,
         1.66656432,  2.16286853,  2.65917274]),
 <a list of 1 Patch objects>)
Out[28]:
(array([ 2.,  8., 26., 53., 53., 33., 22.,  3.]),
 array([-3.29647776, -2.55202145, -1.80756514, -1.06310882, -0.31865251,
         0.4258038 ,  1.17026012,  1.91471643,  2.65917274]),
 <a list of 8 Patch objects>)
Out[28]:
Text(0.5,1,'wykres schodkowy')
Out[28]:
Text(0.5,1,'wykres pełny')
Out[28]:
Text(0.5,0.98,'Dwa wykresy')