#!/usr/bin/env python # coding: utf-8 # Na dnešní lekci si do virtuálního prostředí nainstalujte následující balíčky: # # ```console # $ python -m pip install --upgrade pip # $ python -m pip install notebook numpy scipy imageio matplotlib pillow # ``` # Mezitím, co se to bude instalovat, si stáhněte do adresáře `static` tyto soubory: # # * [python.jpg](static/python.jpg) # * [sample.wav](static/sample.wav) # * [secret.png](static/secret.png) # # A až bude nainstalováno, spusťte si nový Notebook. (Viz [lekce o Notebooku](../notebook/).) # --- # # NumPy # 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. # # # ## Nejednoznačnost a zkratky # # Čí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: # In[1]: 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`). # In[2]: array.shape # In[3]: 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: # In[4]: cube = numpy.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) print(cube.shape) cube # ## Základní operace # Základní vlastnost NumPy polí je to, že aritmetické operace se skalárními hodnotami (např. čísly) fungují po prvcích. # In[5]: array # In[6]: array - 1 # In[7]: array / 2 # In[8]: -(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: # In[9]: 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: # In[10]: (array > 3) & (array < 7) # Operace s jinými poli pracují po prvcích. Obě pole musí být stejně velké. # In[11]: array + array # In[12]: array * numpy.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) # Sekvence (jako seznamy) jsou před operací převedeny na pole. # In[13]: array * [[0, 1, 0], [1, 0, 1], [0, 1, 0]] # ## Indexování # 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í: # In[14]: array # In[15]: array[0] # In[16]: array[0:-1] # In[17]: 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ší: # In[18]: array[0, 1] # In[19]: array[0:-1, 1:] # Chceme-li vybrat určitý sloupec, řekneme si „kompletním intervalem“ (`:`) o všechny řádky: # In[20]: 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`: # In[21]: array > 4 # To pak použijte jako index: # In[22]: 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: # In[23]: array[[0, 2, 1, 1]] # Řádky 0, 2, 1 a 1 # In[24]: 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“: # In[25]: 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“ (`:`): # In[26]: cube # In[27]: cube[0, :, :] # První „vrstva“ - to samé jako cube[0] # In[28]: cube[:, 0, :] # První řádky - to samé jako cube[:, 0] # In[29]: 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í: # In[30]: cube[..., 0] # První sloupce – ekvivalent [:, :, 0] # ## Broadcasting a změny # 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: # In[31]: array # In[32]: array * 3 # In[33]: 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*. # In[34]: array # In[35]: 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: # In[36]: array * [[0], [1], [10]] # vynásobí 1. *řádek* nulou, atd. # Jednotlivé hodnoty v poli lze měnit: # In[37]: array[0, 0] = 123 array # ...a i na měnění se vztahuje *broadcasting*: # In[38]: array[0] = 123 array # In[39]: array[:] = 123 array # Obecně platí, že lze-li něčím vybírat prvky, lze tím i pole měnit: # In[40]: array[(1, 2, 0), (0, 2, 1)] = 0 array # Další způsob, jak pole měnit, je rozšířeným přiřazením. # In[41]: 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: # In[42]: try: array /= 2 except Exception as e: print("Chyba!!", type(e), e) # ## Tvoření matic, část 2 # Č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: # In[43]: numpy.zeros((4, 4)) # In[44]: numpy.ones((4, 4)) # In[45]: numpy.full((4, 4), 12.34) # In[46]: numpy.eye(4) # Jednotková matice (je čtvercová – n×n) # In[47]: numpy.diag([1, 2, 3, 4]) # Diagonální matice # U těchto funkcí lze obecně použít argument `dtype`, kterým specifikujeme datový typ: # In[48]: 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: # In[49]: 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: # In[50]: 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ů: # In[51]: numpy.linspace(0, 50, num=11) # vždy 11 prvků # ## Reshape # 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ů: # In[52]: array = numpy.arange(12) array # In[53]: 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: # In[54]: reshaped[2, 2] = -99 reshaped # In[55]: array # Podobně tvoří pohledy i jednoduché indexování: # In[56]: a_slice = array[2:4] a_slice[:] = -99, -99 array # Potřebujete-li kopii dat, použijte metodu `copy`: # In[57]: 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.) # In[58]: 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). # ## Datové typy # 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`: # In[59]: numpy.zeros(4, dtype=int) # In[60]: numpy.zeros(4, dtype=float) # In[61]: 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). # In[62]: numpy.array([0, 1.3, "foobar"], dtype=object) # Kromě pythonních typů bere `dtype` i řetězcové specifikace typu: # In[63]: numpy.array([1, 8, 500], dtype='int8') # 8bitové celé číslo # Znáte-li modul [`array`](https://docs.python.org/3/library/array.html) ze standardní knihovny, můžete jeho specifikaci použít i tady: # In[64]: numpy.zeros(4, dtype='tab, v dokumentaci, nebo obecně na Internetu (kde najdete i případné knihovny, které implementují operace, které v NumPy nejsou). # # Příklady použití # 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. # ## Matematika a grafy # 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](https://matplotlib.org/), která vykresluje grafy. Jak ji použít dohledáte v [dokumentaci](https://matplotlib.org/contents.html) nebo – často efektivněji – v [galerii příkladů](https://matplotlib.org/gallery/index.html). # # Matplotlib nemá automatickou integraci s Jupyter Notebookem, proto ji je potřeba po importu zapnout: # In[75]: from matplotlib import pyplot # Zapnutí integrace s notebookem – `%` je "magický" příkaz IPythonu, podobně jako `!` pro shell get_ipython().run_line_magic('matplotlib', 'inline') # A teď můžeme nakreslit třeba graf funkce: # In[76]: x = numpy.linspace(0, numpy.pi * 4) # definiční obor y = numpy.sin(x) # odpovídající hodnoty funkce pyplot.plot(x, y) # In[77]: 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() # In[78]: # 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') # ## Obrázky # 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é. # # Knihovna `imageio` slouží k načítání a ukládání různých (njen obrázkových) formátů do/z NumPy matic. # # Nás bude zajímat funkce `imageio.imread`, která pomocí Pillow/PIL načte obrázek jako 3D matici 8bitových čísel. Já načtu obrázek hada, vy najděte na internetu jakýkoli RGB obrázek a načtěte si ten. # # *Použitý obrázek je stažený [z Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Ball_python_lucy.JPG) a je pod licencí [CC-BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/deed.en). Autor je uživatel [Mokele](https://en.wikipedia.org/wiki/User:HCA) na [anglické Wikipedii](https://en.wikipedia.org/wiki/).* # In[79]: import imageio img = imageio.imread('static/python.jpg') img # Pomocí nám už známé knihovny `matplotlib` takovou matici můžeme zobrazit: # In[80]: pyplot.imshow(img) # Podívejme se teď na strukturu matice: # In[81]: 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í. # In[82]: 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. # In[83]: secret = imageio.imread('static/secret.png', pilmode='L') img[..., -1] = (img[..., -1] & 0b11111110) + (secret.astype(bool)) # Obrázek vypadá na první pohled stejně... # In[84]: pyplot.imshow(img) # ... ale v posledím modrém bitu se skrývá tajná informace. # In[85]: pyplot.imshow(img[..., -1] & 1, cmap='gray') # Výsledek je dobré uložit v bezztrátovém formátu (PNG), aby se informace neztratila: # In[86]: imageio.imsave('python.png', img) # ## Zvuk # 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: # In[87]: 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: # In[88]: channel = sound[..., 1] pyplot.plot(channel) # Případně můžu využít možností Jupyter Notebooku a HTML a zvuk si přehrát: # In[89]: 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í: # In[90]: segment = channel[50000:55000] pyplot.plot(segment) # In[91]: 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: # In[92]: 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: # In[93]: 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`: # In[94]: amax = numpy.argmax(abs(spectrum)) amax # ... a najdu odpovídající frekvenci: # In[95]: freqs[amax] # Což je podle [seznamu not](https://en.wikipedia.org/wiki/Piano_key_frequencies) skoro H$_2$ (123,5 Hz).