Na dnešní lekci si do virtuálního prostředí nainstalujte následující balíčky:
$ python -m pip install --upgrade pip
$ python -m pip install notebook numpy scipy matplotlib pillow
Mezitím, co se to bude instalovat, si stáhněte do adresáře static
tyto soubory:
A až bude nainstalováno, spusťte si nový Notebook. (Viz lekce o Notebooku.)
NumPy je základní knihovna pro vědce a analytiky, kteří pracují s Pythonem. Definuje typ pro n-rozměrné homogenní pole (nejčastěji čísel) a API pro práci s takovým polem.
Téměř všechny knihovny, kde se objevují větší matice či tabulky, jsou buď postavené na NumPy, nebo podporují numpy.array
: od pandas
pro datovou analýzu a matplotlib
pro grafy, přes scipy
, kde najdete základní algoritmy pro interpolaci, integraci aj., astrofyzikální astropy
, librosa
pro analýzu hudby, až po integraci v knihovnách jako Pillow
nebo Tensorflow
.
Podobně jako „Djangonauti” kolem webového frameworku Django tvoří vědci a datoví analytici podskupinu pythonní komunity s vlastními konferencemi (PyData), organizacemi (NumFocus, Continuum Analytics) a knihovnami jako NumPy, Pandas, SciPy, Matplotlib či Astropy. Potřeby této komunity se samozřejmě odrážejí i v Pythonu samotném (např. ...
a @
, které si ukážeme dále, byly do jazyka přidány pro ulehčení výpočtů) a naopak (na rozdíl od specializovaných jazyků jako R nebo Matlab se tu stále indexuje od nuly). Většina těchto knihoven ale má jednu zvláštnost, kterou ve zbytku pythonního světa tolik nevidíme: důraz na použití v interaktivním režimu.
Čísla můžeme buď prozkoumávat, hrát si s nimi, zjišťovat zajímavé souvislosti; anebo můžeme připravovat programy, které nějaké výpočty provedou automaticky.
Na obojí se používají podobné nástroje.
Automaticky pouštěné skripty musí být samozřejmě robustní. Nástroje ke zkoumání dat ale bývají přívětivé k vědcům, často na úkor robustnosti nebo „dobrých programátorských mravů”. Například některé funkce tak trochu „hádají”, co uživatel chtěl, a v tutoriálech se setkáte se zkratkami jako import numpy as np
či dokonce from numpy import *
.
Toto je kurz programovací, kde nám záleží více na znovupoužitelném kódu než na jednom konkrétním výsledku. Budeme proto preferovat explicitní a jednoznačné operace. Ty jsou v použitých knihovnách vždy vedle zkratek k dispozici a popsány v dokumentaci.
Jak s polem pracovat? Nejprve si ho vytvoříme, nejlépe ze seznamu čísel:
import numpy
array = numpy.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array
Každé pole má dvě základní, neměnné vlastnosti: tvar (shape
), neboli velikost, a datový typ (dtype
).
array.shape
array.dtype
Tvar je n-tice, kde n je počet dimenzí pole; shape=(3, 3) dtype='int64'
znamená pole 3×3 celých čísel.
Dimenzí může být libovolně mnoho, např. trojrozměrnou matici můžeme vytvořit z trojnásobně vnořených seznamů a NumPy ji „rozumně” vypíše:
cube = numpy.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(cube.shape)
cube
Základní vlastnost NumPy polí je to, že aritmetické operace se skalárními hodnotami (např. čísly) fungují po prvcích.
array
array - 1
array / 2
-(array % 4)
Kromě aritmetických operací takto funguje i porovnávání. Následujícím výrazem dostanete pravdivostní tabulku, která má True
na místech, kde pro příslušný prvek platí podmínka:
array > 5
Protože Python neumožňuje předefinovat chování operátorů and
a or
, logické spojení operací se tradičně dělá přes bitové operátory &
(a) a |
(nebo). Ty mají ale neintuitivní prioritu, proto se jednotlivé výrazy hodí uzavřít do závorek:
(array > 3) & (array < 7)
Operace s jinými poli pracují po prvcích. Obě pole musí být stejně velké.
array + array
array * numpy.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
Sekvence (jako seznamy) jsou před operací převedeny na pole.
array * [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
NumPy pole jde různými způsoby indexovat. Základní způsoby známe už ze samotného Pythonu – pole se chová jako sekvence menších polí:
array
array[0]
array[0:-1]
array[0][1]
Na rozdíl od Pythonu se ale dá vícerozměrné pole indexovat přímo n-ticí. Toto je dokonce preferovaný způsob – přehlednější a mnohem rychlejší:
array[0, 1]
array[0:-1, 1:]
Chceme-li vybrat určitý sloupec, řekneme si „kompletním intervalem“ (:
) o všechny řádky:
array[:, 2]
Další způsob specifický pro NumPy je indexování pravdivostní tabulkou.
Když potřebujete z matice vybrat prvky, pro které platí nějaká podmínka, napřed si vytvořte pole hodnot True
/False
:
array > 4
To pak použijte jako index:
array[array > 4]
Výsledek je jednorozměrný – informace o původních pozicích prvků se ztratí.
Pro úplnost uvedu ještě dva způsoby indexování. První je seznamem indexů, kterým můžete vybírat, přehazovat nebo i duplikovat konkrétní řádky:
array[[0, 2, 1, 1]] # Řádky 0, 2, 1 a 1
array[:, [2, 2, 0, 0]] # Podobně pro sloupce
Druhý je indexování pomocí n-tice „řádkových souřadnic“ a n-tice odpovídajících „sloupcových souřadnic“:
array[(0, 1, 2), (1, 2, 0)] # Vybere prvky (0, 1), (1, 2), (2, 0)
Trochu specifické je indexování vícerozměrných polí. U nich se často využije „kompletní interval“ (:
):
cube
cube[0, :, :] # První „vrstva“ - to samé jako cube[0]
cube[:, 0, :] # První řádky - to samé jako cube[:, 0]
cube[:, :, 0] # První sloupce
Má-li pole hodně rozměrů, je psaní spousty :,
zdlouhavé a nepřehledné. Existuje proto speciální hodnota ...
(Ellipsis
), která doplní tolik „kompletních intervalů“ (:
), aby souhlasil počet dimenzí:
cube[..., 0] # První sloupce – ekvivalent [:, :, 3]
Už jsme si ukázali, že aritmetické operace se skalárními hodnotami se provede pro všechny prvky, zatímco operace mezi dvěma stejně velkými poli se provede po prvcích:
array
array * 3
array * [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
Jak je to ale s různě velkými poli?
Nemá-li sekvence, se kterou pracujeme, dost dimenzí, poslední dimenze se „rozšíří“, jako bychom pracovali v každém sloupci se skalární hodnotou. Tomuto „rozšiřování” se obecně říká broadcasting.
array
array * [0, 1, 10] # vynásobí 1. sloupec nulou, 2. jedničkou, 3. deseti
Podobné rozšiřování nastane, má-li některá dimenze velikost 1:
array * [[0], [1], [10]] # vynásobí 1. *řádek* nulou, atd.
Jednotlivé hodnoty v poli lze měnit:
array[0, 0] = 123
array
...a i na měnění se vztahuje broadcasting:
array[0] = 123
array
array[:] = 123
array
Obecně platí, že lze-li něčím vybírat prvky, lze tím i pole měnit:
array[(1, 2, 0), (0, 2, 1)] = 0
array
Další způsob, jak pole měnit, je rozšířeným přiřazením.
array *= 2
array
Tato operace není totéž co array = array * 2
, ačkoli má stejný výsledek.
array *= 2
změní existující pole, zatímco array = array * 2
vytvoří nové pole, které pak přiřadí do původní proměnné.
Pozor na to, že není možné měnit typ pole:
try:
array /= 2
except Exception as e:
print("Chyba!!", type(e), e)
Časté druhy matic se dají vytvořit pomocí pomocných funkcí. Výsledky se dají použít přímo nebo naplnit vypočítanými daty:
numpy.zeros((4, 4))
numpy.ones((4, 4))
numpy.full((4, 4), 12.34)
numpy.eye(4) # Jednotková matice (je čtvercová – n×n)
numpy.diag([1, 2, 3, 4]) # Diagonální matice
U těchto funkcí lze obecně použít argument dtype
, kterým specifikujeme datový typ:
int_zeros = numpy.zeros((4, 4), dtype='int8')
print(int_zeros.dtype)
int_zeros
Další funkce tvoří jednorozměrné matice. Základní je arange
, která bere stejné argumenty jako range
v Pythonu:
numpy.arange(50) # Celočíselné – argumenty jako range() v Pythonu
Navíc umí pracovat s reálnými čísly (float
). Pozor ale na to, že reálná čísla jsou nepřesná! arange
k začátku sekvence postupně přičítá „krok”, takže chyba narůstá celkem rychle:
numpy.arange(0.0, 50.0, 0.3)[-1]
V krajních případech takto dokonce můžeme dostat pole jiné velikosti, než jsme zamýšleli. Proto arange
používejte jen pro celá čísla; pro reálná je tu linspace
, která bere začátek a konec intervalu, plus počet prvků:
numpy.linspace(0, 50, num=11) # vždy 11 prvků
Ačkoli indexování polí v NumPy je dost mocné, v paměti jsou jednotlivé hodnoty reprezentovány jako (metadata a) jednorozměrné pole, známé z jazyka C (ačkoli samotné rozmístění prvků může být jiné než po řádcích, jak jsme zvyklí u C).
Je jednoduché změnit tvar pole, nezmění-li se tím celkový počet prvků:
array = numpy.arange(12)
array
reshaped = array.reshape((3, 4))
reshaped
Pozor na to, že reshape
sice vrací nový objekt, ale může (ale nemusí!) to být jen nový pohled na existující data. Změny v pohledu se projeví i v původním poli:
reshaped[2, 2] = -99
reshaped
array
Podobně tvoří pohledy i jednoduché indexování:
a_slice = array[2:4]
a_slice[:] = -99, -99
array
Potřebujete-li kopii dat, použijte metodu copy
:
array.reshape((3, 4)).copy()
Podobně jako reshape
funguje transpozice, což je tak častá operace, že má jednopísmennou zkratku – atribut T
. (Tohle hodně napomáhá tomu, že zápis maticových výpočtů v NumPy se podobá odpovídajícím matematickým vzorcům.)
reshaped.T
Až budete NumPy zkoušet, doporučuji si u nových funkcí najít, zda tvoří nová pole, vracejí pohled nebo modifikují původní pole. U některých funkcí najdete pojmenovaný argument inplace
(modifikuje původní pole), případně out
, („naplní“ jiné, existující pole).
Jak už bylo řečeno, matice v NumPy mají určené datové typy. Ty jdou nastavit ve většině funkcí, které matice tvoří, argumentem dtype
:
numpy.zeros(4, dtype=int)
numpy.zeros(4, dtype=float)
numpy.zeros(4, dtype=bool)
Nejobecnější typ je object
(jehož použitím ale přicházíme o většinu výhod NumPy).
numpy.array([0, 1.3, "foobar"], dtype=object)
Kromě pythonních typů bere dtype
i řetězcové specifikace typu:
numpy.array([1, 8, 500], dtype='int8') # 8-bitové číslo
Znáte-li modul array
ze standardní knihovny, můžete jeho specifikaci použít i tady:
numpy.zeros(4, dtype='<I')
Navíc dtype
umí řetězcové a bytestring typy. Tyto mají danou maximální velikost a nesmí obsahovat \0
(resp. znakem '\0'
jsou ukončeny):
numpy.full(4, 'abcdef', dtype=('U', 10)) # Unicode
numpy.full(4, 'abcdef', dtype=('a', 3)) # "ASCII"
Typy v NumPy můžou být poměrně složité; např. existují i složené datové typy (records
). Ty nebudeme používat, ale je dobré o nich aspoň tušit:
record_type = numpy.dtype([('a', int), ('b', float), ('c', ('U', 3))])
numpy.array([(1, 2, 'abc')] * 4, record_type)
Kromě základních aritmetických operací se u vícerozměrných polí často setkáme s maticovým násobením. Předpokládám, že jako bakaláři jste se s ním už setkali a tušíte co dělá – jestli ne, tuto sekci ignorujte.
V Pythonu 3.5 byl na výzvu vědecké komunity do jazyka přidán operátor @
(mATrix multiplication), který je vyhrazen pro maticové násobení. V samotném Pythonu ani ve standardní knihovně není typ, který ho podporuje, ale matice v NumPy tuto operaci samozřejmě umí.
array1 = numpy.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
array2 = numpy.array([[1, 0, 0], [0, 2, 0], [0, 0, 3]])
array1 @ array2
Ve starších verzích Pythonu je potřeba používat metodu nebo funkci dot
.
Důvod přidání operátoru @
byl prostý – zjednodušení zápisu maticových operací. Jako příklad uvedený v návrhu je uveden tento vzorec pro testování hypotéz v lineárním regresním modelu:
$ S=(H\beta-r)^T(HVH^T)^{-1} (H\beta-r) $
V NumPy se dá přepsat jako:
from numpy import dot
from numpy.linalg import inv, solve
Pomocí funkce dot
:
S = dot((dot(H, beta) - r).T,
dot(inv(dot(dot(H, V), H.T)), dot(H, beta) - r))
Pomocí metoody dot
:
S = (H.dot(beta) - r).T.dot(inv(H.dot(V).dot(H.T))).dot(H.dot(beta) - r)
Pomocí operátoru @
:
S = (H @ beta - r).T @ inv(H @ V @ H.T) @ (H @ beta - r)
Poslední varianta nápadně připomíná původní vzorec; u prvních dvou se člověk snadno ztratí ve změti závorek.
Použijeme-li pole v příkazu if, NumPy nám vynadá. Standardní pythonní seznam je „pravdivý“ pokud obsahuje nějaké prvky, ale u pole, které má fixní velikost, je tahle informace téměř vždy zbytečná.
try:
if numpy.eye(3):
pass
except ValueError as e:
print("Chyba!", type(e), e)
Musíme říct přesně, co chceme.
if numpy.eye(3).any():
print('Alespoň jeden prvek je nenulový')
if numpy.eye(3).all():
print('Všechny prvky jsou nenulové')
if numpy.eye(3).size:
print('Pole obsahuje nějaké prvky')
Z historických důvodů existují dvě výjimky: pole s právě jedním prvkem má pravdivostní hodnotu podle daného prvku a prázdné pole je „nepravdivé“:
if numpy.ones((1, 1, 1, 1)):
print('Ano')
if numpy.zeros((1, 1, 1, 1)):
print('Ne')
if numpy.ones((0, 0)):
print('Ano')
Modul numpy
obsahuje spoustu základních funkcí, které pracují s maticemi; mimo jiné většinu funkcií z pythonního modulu math
. Oproti math
zvládají funkce z NumPy broadcasting.
array = numpy.linspace(0, numpy.pi, num=1000)
array[:10]
sine = numpy.sin(array)
sine[:10]
Další operace doporučuji hledat buď v Notebooku přes tab, v dokumentaci, nebo obecně na Internetu (kde najdete i případné knihovny, které implementují operace, které v NumPy nejsou).
Dost teorie. Tahle n-rozměrná pole se používají na spoustu zajímavých věcí. Podívejme se na některé příklady.
Jak se používají matice, jistě znáte z matematiky a cílem tohoto kurzu není vás to naučit. Ukážu ale pár ochutnávek.
Použijeme knihovnu Matplotlib, která vykresluje grafy. Jak ji použít dohledáte v dokumentaci nebo – často efektivněji – v galerii příkladů.
Matplotlib nemá automatickou integraci s Jupyter Notebookem, proto ji je potřeba po importu zapnout:
from matplotlib import pyplot
# Zapnutí integrace s notebookem – `%` je "magický" příkaz IPythonu, podobně jako `!` pro shell
%matplotlib inline
A teď můžeme nakreslit třeba graf funkce:
x = numpy.linspace(0, numpy.pi * 4) # definiční obor
y = numpy.sin(x) # odpovídající hodnoty funkce
pyplot.plot(x, y)
s = numpy.linspace(-10, 10, num=100)
# meshgrid vrátí dvě 100×100 matice:
# - jednu, kde v každém řádku jsou čísla od -10 do 10,
# - druhou, kde v každém sloupci jsou čísla od -10 do 10.
x, y = numpy.meshgrid(s, s)
# vyhodnotíme (x**2 + y**2) pro každý prvek
z = x ** 2 + y ** 2
# výsledek vykreslíme jako obrázek
pyplot.imshow(z)
# přidáme legendu
pyplot.colorbar()
# Ta samá data můžeme vykreslit i ve 3D
from mpl_toolkits.mplot3d import Axes3D
fig = pyplot.figure()
axes = fig.gca(projection='3d')
surf = axes.plot_surface(x, y, z, cmap='viridis')
Typický barevný obrázek není nic než matice $m \times n \times 3$ čísel: $m \times n$ pixelů na šířku a výšku a 3 kanály pro červenou, zelenou a modrou barvu.
Knihovna pillow
(nástupce knihovny PIL, který se stále importuje jako PIL) obsahuje nástroje na práci s obrázky, např. „nakresli čáru“ nebo „převeď na černobílý obrázek“ nebo „načti PNG“. Není postavena přímo na NumPy, ale umí obrázky převádět z a na NumPy pole, pokud máme NumPy nainstalované.
V knihovně scipy.ndimage
existuje spousta nástrojů na analýzu obrazových dat jako 2D signálů, např. konvoluce nebo Sobelův filtr. Jako celé SciPy je postavená přímo na NumPy.
Nás bude na začátku zajímat funkce scipy.ndimage.imread
, která pomocí Pillow/PIL načte obrázek jako 3D matici 8-bitových čísel. Já načtu obrázek hada, vy najděte na internetu jakýkoli barevný obrázek a načtěte si ten.
Použitý obrázek je stažený z Wikimedia Commons a je pod licencí CC-BY-SA 3.0. Autor je uživatel Mokele na anglické Wikipedii.
import scipy.ndimage
img = scipy.ndimage.imread('static/python.jpg', mode='RGB')
img
Pomocí nám už známé knihovny matplotlib
takovou matici můžeme zobrazit:
pyplot.imshow(img)
Podívejme se teď na strukturu matice:
img.shape
První rozměr jsou řádky (y); můj obrázek je 887 pixelů vysoký. Druhý jsou sloupce (x); tento obrázek má na šířku 1037 px. Třetí rozměr jsou barevné kanály.
Pomocí indexování se můžeme na jednotlivé barevné kanály dostat: je to poslední index, takže řádky a sloupce nahradíme buď dvěma kompletními intervaly (:, :
) nebo vynechávkou (...
). Červený kanál tedy bude [..., 1]
, modrý [..., -1]
.
Zobrazení chceme černobílé; na to má matplotlib pojmenovaný argument cmap
. Výchozí způsob obarvování je vhodný spíše pro grafy funkcí.
blue_channel = img[..., -1]
pyplot.imshow(blue_channel, cmap='gray')
Zajímavé využití obrázku jako matice je steganografie: ukrytí informace v obrazových datech.
Načteme jiný obrázek stejné velikosti, tentokrát černobílý (s módem L
). Informace v něm schováme do posledního bitu modrého kanálu.
secret = scipy.ndimage.imread('static/secret.png', mode='L')
img[..., -1] = (img[..., -1] & 0b11111110) + (secret.astype(bool))
Obrázek vypadá na první pohled stejně...
pyplot.imshow(img)
... ale v posledím modrém bitu se skrývá tajná informace.
pyplot.imshow(img[..., -1] & 1, cmap='gray')
Výsledek je dobré uložit v bezztrátovém formátu (PNG), aby se informace neztratila:
scipy.misc.imsave('python.png', img)
Jako pole lze reprezentovat i zvukový záznam. Mám záznam, na kterém zkouším zpívat; pomocí funkce scipy.io.wavfile
ho můžu načíst jako NumPy pole:
import scipy.io.wavfile
sample_rate, sound = scipy.io.wavfile.read('static/sample.wav')
print(sample_rate)
sound
Zvuk je stereo, má dvě stopy; jednu z nich si vykreslím:
channel = sound[..., 1]
pyplot.plot(channel)
Případně můžu využít možností Jupyter Notebooku a HTML a zvuk si přehrát:
from IPython.display import Audio
Audio(data=channel, rate=sample_rate)
print('(Zkuste si to sami; tento print vymažte)')
Podívám se na detail první „noty”, kde je patrná vlna s nějakou frekvencí:
segment = channel[50000:55000]
pyplot.plot(segment)
from IPython.display import Audio
Audio(data=segment, rate=sample_rate)
Jaká to je frekvence? Znáte-li analýzu signálů, tušíte, že na podobné otázky odpovídá Fourierova transformace.
NumPy obsahuje diskrétní Fourierovu transformaci v modulu numpy.fft
spolu s funkcí, která spočítá odpovídající frekvence v Hz:
spectrum = numpy.fft.fft(segment)
freqs = numpy.fft.fftfreq(len(spectrum), 1/sample_rate)
pyplot.xlabel('Frekvence (Hz)')
pyplot.plot(freqs, numpy.abs(spectrum))
V tomto grafu hledám maximum. Můžu se zaměřit na prvních pár hodnot spektra:
pyplot.xlabel('Frekvence (Hz)')
pyplot.plot(freqs[:100], numpy.abs(spectrum[:100]))
Maximum je někde kolem 120 Hz; abych to zjistil přesně, použiji funkci argmax
:
amax = numpy.argmax(abs(spectrum))
amax
... a najdu odpovídající frekvenci:
freqs[amax]
Což je podle seznamu not skoro H$_2$ (123,5 Hz).