<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml" href="/cjs/screen.xsl" media="screen"?>
<lecture>

<meta>
  <maintitle>Python</maintitle>
  <author>Jiří Znamenáček</author>
  <title>NumPy – výřezy polí, optimalizace</title>
  <date>2017-04-24</date>
  <link><!--a href="http://vyuka.ookami.cz" rel="external">http://vyuka.ookami.cz</a--></link>
  <!--
  <style>
    /* CSSka */
  </style>
  <use-math/>
  -->
</meta>
<!--
  „“–…
  ←→ ↑↓ ↔↕
  ↵ aneb &#x21B5; aneb \r aneb CR aneb CarriageReturn
-->

<!--
    Nápověda:
        np.info(np.arange)
        np.lookfor('create array')
-->


<slide title="Úvod">

    <p>
        Výřezy <strong>(vícerozměrných) polí</strong> (tedy objektů typu <strong><code>ndarray</code></strong>) patří – po jejich zavedení – ke zcela základním a zdaleka nejdůležitějším operacím nad poli v <em>NumPy</em>. V podstatě se dá skoro bez nadsázky říci, že bez správných výběrů prvků pole nemáte šanci pomocí <em>Numpy</em> napsat optimální kód.
    </p>
    <p>
        Všechno navíc komplikuje, že <strong>je třeba si u každé metody uvědomit, zda vrací pouze jiný pohled na originální pole (takže každá změna v tomto pohledu se projeví i v originálu), nebo zda vrátí kopii příslušné části dat</strong>. Bohužel konkrétní chování se může lišit i v rámci jedné jediné metody v závislosti na vstupních datech (typicky jejich velikosti a tvaru) a pomoci s rozhodováním vám může pouze detailní znalost interní reprezentace dat a metod nad nimi operujících.
    </p>
    <p>
        Nicméně pár užitečných pravidel se vysledovat a naučit dá. Pak už zbývá na ně „jen“ ve vlastním kódu pamatovat…
    </p>
    <p>
        PS: V příkladech budu používat modul <em>numpy</em> naimportovaný podle zvyku jako:
        <example lang="python">
            import numpy as np
        </example>
    </p>

</slide>
<slide title="Klasické výřezy [:] = jiné pohledy na data">

    <p>
        Nepřekvapivě se z polí dají vybírat podpole především velmi podobným způsobem jako u pythoního seznamu. Pouze máme k dispozici více os, ve kterých můžeme operovat, což situaci poněkud komplikuje. Ale zároveň činí zábavnější ^_~
    </p>
    <note>
        Celé je to umožněné <a href="slices.xml">rozšířenou výřezovou notací</a>, která je v Python'u víceméně jen a právě kvůli NumPy.
    </note>
    <p>
        Tyto „klasické“ výřezy se zvláště pro 1D a 2D pole chápou snadno. Nejdůležitější věcí k zapamatování jest:
    </p>
    <blockquote>
        <strong>Klasické výřezy vyrábějí pouze jiné pohledy na data.</strong>
    </blockquote>
    <p>
        Tedy přiřadíte-li něco nového výřezu, projeví se to i v původním poli (a naopak)!
    </p>
    <p>
        PS: Jiným způsobem by se klasické výřezy daly charakterizovat jako ty výřezy, k jejichž provedení stačí použití <a href="arrays.creation.xml?slajd=19"><em>kroků</em></a> (tedy <em>strides</em>).
    </p>

</slide>
<slide title="Získání výřezu">

    <p>
        1D je úplně stejné jako v klasickém Python'u, ve 2D pak představuje první index řádku a druhý sloupec:
    </p>
    <example lang="python">
        >>> a = np.arange(1, 21).reshape(5, 4)
        >>> a
        array([[ 1,  2,  3,  4],
               [ 5,  6,  7,  8],
               [ 9, 10, 11, 12],
               [13, 14, 15, 16],
               [17, 18, 19, 20]])
        
        >>> a[1]
        array([5, 6, 7, 8])
        
        >>> a[1, 2:]
        array([7, 8])
        
        >>> a[1, 2]
        7
    </example>

</slide>
<slide title="Přiřazení do výřezu">

    <p>
        Přiřazení do výřezu pak funguje také tak, jak by člověk intuituvně očekával:
    </p>
    <example lang="python">
        >>> a = np.zeros((3, 4), dtype=int)
        >>> a
        array([[0, 0, 0, 0],
               [0, 0, 0, 0],
               [0, 0, 0, 0]])

        >>> a[1] = 1
        >>> a
        array([[0, 0, 0, 0],
               [1, 1, 1, 1],
               [0, 0, 0, 0]])

        >>> a[2, 2:] = 2
        >>> a
        array([[0, 0, 0, 0],
               [1, 1, 1, 1],
               [0, 0, 2, 2]])

        >>> a[0, 1] = 3
        >>> a
        array([[0, 3, 0, 0],
               [1, 1, 1, 1],
               [0, 0, 2, 2]])
    </example>

</slide>
<slide title="Přiřazení pomocí [...]">

    <p>
        Python (opět právě kvůli <em>Numpy</em>) podporuje mimo jiné i tzv. <strong>výpustkovou notaci</strong>, tedy použití <code>...</code> na místě indexů do pole. V <em>Numpy</em> se takto označují sekvence os, které není třeba blíže určovat:
    </p>
    <example lang="python">
        >>> a = np.full((3, 4), 7)
        >>> a
        array([[7, 7, 7, 7],
               [7, 7, 7, 7],
               [7, 7, 7, 7]])

        >>> a[...] = 3
        >>> a
        array([[3, 3, 3, 3],
               [3, 3, 3, 3],
               [3, 3, 3, 3]])
    </example>

</slide>
<slide title="Komentář">

    <p>
        <em>Výřezy</em> tedy představují <strong>jiné pohledy na stejná data</strong>, pouze podle zavedení buď a nebo s:
    </p>
    <ul>
        <li>
            jiným tvarem (<em>shape</em>);
        </li>
        <li>
            jinými kroky (<em>strides</em>);
        </li>
        <li>
            jiným odsazením (<em>offset</em>) od začátku původních dat.
        </li>
    </ul>
    <p>
        První dvě situace si můžete snadno ověřit pomocí adresy počátku dat, která ovšem bude pro třetí trochu posunutá. Vhodnější je tak rovnou použít řetězec <em>základních polí</em> pomocí atributu <em>base</em>:
    </p>
    <example lang="python">
        >>> a = np.array([[1, 2, 3], [4, 5, 6]])
        >>> a
        array([[1, 2, 3],
               [4, 5, 6]])
        >>> a.__array_interface__['data'][0]
        13087104
        >>> a.base
        None
        >>> a.shape
        (2, 3)
        >>> a.strides
        (24, 8)

        >>> b = a.T
        >>> b
        array([[1, 4],
               [2, 5],
               [3, 6]])
        >>> b.__array_interface__['data'][0]
        13087104
        >>> b.base is a
        True
        >>> b.shape
        (3, 2)
        >>> b.strides
        (8, 24)

        >>> c = a[..., 1:]
        >>> c
        array([[2, 3],
               [5, 6]])
        >>> c.__array_interface__['data'][0]
        13087112
        >>> c.base is a
        True
        >>> c.shape
        (2, 2)
        >>> c.strides
        (24, 8)
    </example>

</slide>
<slide title="„Fancy indexing“ = kopie dat">

    <p>
        Na první pohled možná trochu překvapivě umožňuje <em>Numpy</em> <strong>indexovat prvky polí</strong> dokonce i <strong>pomocí jiných numpyovských polí</strong>! (Čemuž se většinou říká právě <em>fancy indexing</em>.) Nicméně přitom narozdíl od výřezů dochází ke kopírování dat:
    </p>
    <example lang="python">
        >>> a = np.array([2, 4, 3, 5, 6, 7])

        # A) výřez (jiný pohled na data)
        >>> b = a[::2]
        >>> b
        array([2, 3, 6])
        >>> b.base is a
        True

        # B1) seznam indexů (kopie dat)
        >>> c = a[np.arange(0, 6, 2)]
        >>> c
        array([2, 3, 6])
        >>> c.base is a
        False
        # PS:
        >>> np.arange(0, 6, 2)
        array([0, 2, 4])

        # B2) seznam indexů jinak (kopie dat)
        >>> d = a[a % 2]
        >>> d
        array([2, 2, 4, 4, 2, 4])
        >>> d.base is a
        False
        # PS:
        >>> a % 2
        array([0, 0, 1, 1, 0, 1])

        # C) pravdivostní maska (kopie dat)
        >>> e = a[a % 2 == 1]
        >>> e
        array([3, 5, 7])
        >>> e.base is a
        False
        # PS:
        >>> a % 2 == 1
        array([False, False, True, True, False, True], dtype=bool)
    </example>
    <p>
        Tento přístup je sice <strong>velmi mocný</strong> (spousta operací nad poli se pomocí něj dá zapsat velmi kompaktním a pochopitelným způsobem), nicméně je také velmi pomalý (především právě kvůli tomu kopírování dat). Proto, je-li to možné, vyplatí se místo něj použít dedikované metody (ovšem typicky pouze pro výběr nad jednou osou).
    </p>

</slide>
<slide title="„take()“ – výběr pomocí seznamu indexů">

    <p>
        Metoda <em>take()</em> je – pro výběr v jedné ose – rychlejší variantou k indexování pomocí pole indexů do pole:
    </p>
    <example lang="python">
        >>> a = np.array([2, 3, 4, 5, 6, 7])

        # výběrové pole
        >>> idx = np.arange(0, 6, 2)
        >>> idx
        array([0, 2, 4])

        # „fancy indexing“
        >>> b1 = a[idx]
        >>> b1
        array([2, 4, 6])

        # dedikovaná metoda
        >>> b2 = a.take(idx, axis=0)
        >>> b2
        array([2, 4, 6])
    </example>

</slide>
<slide title="„compress()“ – výběr pomocí pravdivostní masky">

    <p>
        Metoda <em>compress()</em> je – pro výběr v jedné ose – rychlejší variantou k indexování pomocí pravdivostního pole:
    </p>
    <example lang="python">
        >>> a = np.array([2, 3, 4, 5, 6, 7])

        # výběrové pole
        >>> idx = (a % 2 == 1)
        >>> idx
        array([False,  True, False,  True, False,  True], dtype=bool)

        # „fancy indexing“
        >>> b1 = a[idx]
        >>> b1
        array([3, 5, 7])

        # dedikovaná metoda
        >>> b2 = a.compress(idx, axis=0)
        >>> b2
        array([3, 5, 7])
    </example>

</slide>
<slide title="Metody měnící tvar pole">

    <p>
        Abyste mohli výběry prvků z pole efektivně používat, musíte samozřejmě vědět, jak ono pole vypadá. A pole v <em>Numpy</em> dokáže být docela proměnlivá mrška.
    </p>
    <p>
        A opět – stejně jako u výřezů je důležité vědět, kdy změna tvaru pole vyrobí jenom jiný pohled na data a kdy je naopak zkopíruje úplně někam jinam.
    </p>
    <note>
        Zdroj: <a href="http://www.scipy-lectures.org/intro/numpy/operations.html#array-shape-manipulation" class="external">Scipy Lectures – Array shape manipulation</a>
    </note>

</slide>
<slide title="„ravel()“ – převod na vektor">

    <p>
        Pomineme-li iterátor přes vektorový pohled <em>flat</em> a vždy-kopírující metodu <em>flatten()</em>, používá se pro převod vícerozměrných polí na pole jednorozměrná především metoda <em>ravel()</em>:
    </p>
    <example lang="python">
        >>> a = np.array([[1, 2, 3], [4, 5, 6]])
        >>> a
        array([[1, 2, 3],
               [4, 5, 6]])

        >>> a.ravel()
        array([1, 2, 3, 4, 5, 6])

        >>> a.T
        array([[1, 4],
               [2, 5],
               [3, 6]])
        >>> a.T.ravel()
        array([1, 4, 2, 5, 3, 6])
    </example>
    <p>
        Její hlavní výhodou je, že – je-li to možné – neprovádí kopie dat, takže je výrazně rychlejší než ostatní způsoby:
    </p>
    <example lang="python">
        >>> a.ravel().base is a
        True

        >>> a.T.ravel().base is a
        False
    </example>

</slide>
<slide title="„reshape()“ – změna tvaru">

    <p>
        Metoda <em>reshape()</em> umí měnit tvary polí, pokud jsou navzájem „kompatibilní“ (neboli dá-li se z jednoho přes celkový počet prvků a rozměry dopočítat na druhý):
    </p>
    <example lang="python">
        >>> a = np.arange(1, 13)
        >>> a
        array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12])

        >>> a.reshape(3, 4)
        array([[ 1,  2,  3,  4],
               [ 5,  6,  7,  8],
               [ 9, 10, 11, 12]])

        >>> a.reshape(3, 4).reshape(2, 6)
        array([[ 1,  2,  3,  4,  5,  6],
               [ 7,  8,  9, 10, 11, 12]])
    </example>
    <p>
        Přitom dokud nenabouráte (typicky transpozicí) možnost pouhé změny pohledu na prvky, nedostanete kopii:
    </p>
    <example lang="python">
        >>> a.reshape(3, 4).reshape(2, 6).base is a
        True

        >>> a.reshape(3, 4).T.reshape(2, 6).base
        array([[ 1,  5,  9],
               [ 2,  6, 10],
               [ 3,  7, 11],
               [ 4,  8, 12]])
        >>> a.reshape(3, 4).T.reshape(2, 6).base is a
        False
    </example>

</slide>
<slide title="„numpy.newaxis“ – přidávání dimenzí I">

    <p>
        Na místě indexů je možno použít i speciální numpyovský operátor <em>numpy.newaxis</em>, který automagicky přidá ke vstupnímu poli další dimenzi na uvedeném místě:
    </p>
    <example lang="python">
        # originál
        >>> a = np.array([1, 2, 3])
        >>> a
        array([1, 2, 3])
        >>> a.shape
        (3,)

        # přidaná dimenze
        >>> b = a[np.newaxis, :]
        >>> b
        array([[1, 2, 3]])
        >>> b.shape
        (1, 3)

        # totéž s pomocí výpustky
        >>> c = a[np.newaxis, ...]
        >>> c
        array([[1, 2, 3]])
        >>> c.shape
        (1, 3)
    </example>

</slide>
<slide title="„numpy.newaxis“ – přidávání dimenzí II">

    <p>
        Tohle si zaslouží i složitější příklad:
    </p>
    <example lang="python">
        # originál
        >>> a = np.array([[1, 2, 3], [4, 5, 6]])
        >>> a
        array([[1, 2, 3],
               [4, 5, 6]])
        >>> a.shape
        (2, 3)

        # přidání „uprostřed“
        >>> d = a[:, np.newaxis, :]
        >>> d
        array([[[1, 2, 3]],

               [[4, 5, 6]]])
        >>> d.shape
        (2, 1, 3)
    </example>

</slide>
<slide title="„numpy.newaxis“ – přidávání dimenzí III">


    <p>
        Asi nejčastější použití nalezne tento operátor v případech, kdy potřebujeme z<em>broadcasting</em>ovat pole nekompatibilního tvaru:
    </p>
    <example lang="python">
        >>> a = np.array([0, 10, 20, 30])
        >>> b = np.array([0, 1, 2])

        >>> a + b
        Traceback (most recent call last):
          File "&lt;stdin>", line 1, in &lt;module>
        ValueError: operands could not be broadcast together with shapes (4,) (3,) 

        >>> am = a[:,np.newaxis]
        >>> am.shape
        (4, 1)
        >>> am
        array([[ 0],
               [10],
               [20],
               [30]])
        >>> am + b
        array([[ 0,  1,  2],
               [10, 11, 12],
               [20, 21, 22],
               [30, 31, 32]])
    </example>

</slide>
<slide title="„transpose()“ &amp; „T“ I">

    <p>
        V jednodušších případech vrací metoda <em>transpose()</em> a atribut <em>T</em> stejný výsledek, a to nový (transponovaný) pohled na originální pole:
    </p>
    <example lang="python">
        >>> a = np.arange(1, 7)
        >>> a
        array([1, 2, 3, 4, 5, 6])

        >>> a.transpose()
        array([1, 2, 3, 4, 5, 6])
        >>> a.T
        array([1, 2, 3, 4, 5, 6])

        >>> a.transpose() == a.T
        array([ True,  True,  True,  True,  True,  True], dtype=bool)
        >>> a.transpose() is a.T
        False
        >>> a.transpose().base is a.T.base is a
        True
    </example>
    <p>
        PS: Nutno připomenout, že pro cokoliv složitějšího než 1D už s největší pravděpodobností nepůjde o jiný pohled na táž data, nýbrž o jejich kopii do nového paměťového umístění!
    </p>

</slide>
<slide title="„transpose()“ &amp; „T“ II">

    <p>
        Metoda <em>transpose()</em> je ovšem univerzálnější, protože ve vícedimenzionálních případech si můžete vybrat, podle kterých os k transpozici dojde (musíte ji pak ale volat na globální úrovni<sup>*</sup>):
    </p>
    <example lang="python">
        # originál
        >>> a = np.arange(1, 13).reshape(2, 3, 2)
        >>> a
        array([[[ 1,  2],
                [ 3,  4],
                [ 5,  6]],

               [[ 7,  8],
                [ 9, 10],
                [11, 12]]])
        >>> a.shape
        (2, 3, 2)

        # atribut „T“
        >>> a.T
        array([[[ 1,  7],
                [ 3,  9],
                [ 5, 11]],

               [[ 2,  8],
                [ 4, 10],
                [ 6, 12]]])
        >>> a.T.shape
        (2, 3, 2)

        # komplexnější použití metody „transpose()“
        >>> np.transpose(a, axes=[2, 0, 1])
        array([[[ 1,  3,  5],
                [ 7,  9, 11]],

               [[ 2,  4,  6],
                [ 8, 10, 12]]])
        >>> np.transpose(a, axes=[2, 0, 1]).shape
        (2, 2, 3)
    </example>
    <note>
        <sup>*</sup> Ano – v duchu „dobré“ tradice Numpy nemají <em>transpose()</em> na poli a ve jmenném prostoru balíčku <em>numpy</em> stejné parametry…
    </note>

</slide>
<slide title="„swapaxes()“ &amp; „moveaxes()“">

    <p>
        Tyto dvě doplňkové metody k univerzální <em>transpose()</em> operují nad zadanou podmnožinou os, takže ve vícedimenzionálních případech by mohly najít své uplatnění.
    </p>

    <p class="enumerate">
        Metoda <em>swapaxes()</em> prohodí dvě zadané osy:
    </p>
    <example lang="python">
        >>> a = np.arange(1, 13).reshape(2, 3, 2)
        >>> a.shape
        (2, 3, 2)

        >>> a.swapaxes(0, 1)
        array([[[ 1,  2],
                [ 7,  8]],

               [[ 3,  4],
                [ 9, 10]],

               [[ 5,  6],
                [11, 12]]])
        >>> a.swapaxes(0, 1).shape
        (3, 2, 2)
    </example>

    <p class="enumerate">
        Metoda <em>moveaxes()</em> dělá v podstatě totéž, ale kromě dvou os dokáže na vstupu přebírat i dva seznamy os:
    </p>
    <example lang="python">
        >>> a = np.arange(1, 13).reshape(2, 3, 2)
        >>> a.shape
        (2, 3, 2)
        
        >>> np.moveaxis(a, 0, 1)
        array([[[ 1,  2],
                [ 7,  8]],

               [[ 3,  4],
                [ 9, 10]],

               [[ 5,  6],
                [11, 12]]])
        >>> np.moveaxis(a, 0, 1).shape
        (3, 2, 2)

        >>> np.moveaxis(a, [2, 0], [0, 1])
        array([[[ 1,  3,  5],
                [ 7,  9, 11]],

               [[ 2,  4,  6],
                [ 8, 10, 12]]])
        >>> np.moveaxis(a, [2, 0], [0, 1]).shape
        (2, 2, 3)
    </example>

</slide>
<slide title="„resize()“ – změna tvaru a velikosti">

    <!--
        http://www.scipy-lectures.org/intro/numpy/operations.html#resizing
        https://docs.scipy.org/doc/numpy/reference/generated/numpy.resize.html
        https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.resize.html#numpy.ndarray.resize
    -->

    <p>
        Další schizofrenní metoda, která se (někdy) chová jinak na poli a jinak v numpyovském jmenném prostoru – <em>resize()</em>. Jejím hlavním úkolem je změnit tvar a počet prvků pole. Ať už směrem k vyšším nebo menším číslům.
    </p>

    <p class="enumerate">
        Jako metoda pole se <em>resize()</em> pokusí změnit jeho velikost v místě, to znamená bez kopírování dat (pokud to půjde), přičemž <strong>při zvětšování doplní do nových míst nuly</strong>:
    </p>
    <example lang="python">
        >>> a = np.array([[1, 2, 3], [4, 5, 6]])
        >>> a.resize((2, 4))
        >>> a
        array([[1, 2, 3, 4],
               [5, 6, 0, 0]])
    </example>
    <p>
        Nicméně jakýkoliv odkaz na toto pole – především pak jakýkoliv jiný pohled na něj (pod svým vlastním jménem) – tomu zabrání, pokud uvedené chování nevypnete nastavením parametru <code>refcheck=False</code>.
    </p>
    <note>
        A bohužel odkazy na vaše pole se často vyrábí na pozadí i bez vašeho zásahu, takže třeba příklad zeshora – ač vyloženě dokumentační – mi bez tohoto parametru nefungoval ani na čistě puštěném interpretru…
    </note>

    <p class="enumerate">
        Narozdíl od předchozí metody <em>resize()</em> v numpyovském jmenném prostoru data téměř vždy kopíruje, ale především <strong>při zvětšování doplňuje kopii původních dat</strong>:
    </p>
    <example lang="python">
        >>> a = np.array([[1, 2, 3], [4, 5, 6]])
        >>> np.resize(a, (2, 4))
        array([[1, 2, 3, 4],
               [5, 6, 1, 2]])
    </example>
    <note>
        Mimochodem zrovna v tomhle případě ke kopii nedošlo – zvětšené pole trůní na stejné adrese jako originální.
    </note>

</slide>


</lecture>
