## Večdimenzionalne tabele

Tabele so lahko tudi večdimenzionalne. "Več-" naj bo, za začetek, kar "dvo-". (Ker je dve ravno število dimenzij, ki jih imajo sodobni zasloni in projektorji, se dvodimenzionalne tabele ravno še dobro vidi. Za tridimenzionalne bi potrebovali posebna očala, za štiri pa drugačno vesolje.)

    import numpy as np

    a = np.array([[2, -7, 2], [5, 9, 1], [1, -0, 0], [-1, -2, -8]])

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

V Pythonu bi bil to seznam seznamov. Do tretje vrstice bi prišli z

    a[3]

    array([-1, -2, -8])

in do drugega elementa v njej z

    a[3][2]

    np.int64(-8)

Numpyjeve tabele lahko indeksiramo z več indeksi v istih oklepajih. Namesto `a[3][2]` lahko pišemo

    a[3][2]

    np.int64(-8)

Za oba indeksa veljajo enake čarovnije kot za Pythonove indekse sicer. Tako lahko dobimo vse vrstice od druge do četrte in stolpce do zadnjega:

    a[2:4, :-1]

    array([[ 1,  0],
           [-1, -2]])

Ali pa, recimo, vse vrstice drugega stolpca - z drugimi besedami, drugi stolpec.

    a[:, 2]

    array([ 2,  1,  0, -8])

Ker je stolpec enodimenzionalna reč, ga je Python pač prevrnil v enodimenzionalno tabelo.

Ko smo ravno pri prevračanju, omenimo še splošno prevračanje: tabela ima "metodo" `T`, ki vrne transponirano tabelo. Narekovaji so potrebni, ker `T` v resnici ni metoda temveč nekaj drugega, o čemer se pri predmetu niti slučajno ne bomo učili. `T`-ja ni potrebno poklicati. Malo zato, ker bi oklepaji vzeli več prostora kot samo ime funkcije, malo zato, ker T prihaja iz matematike, kjer bi transponirano matriko $\matrix{A}$ zapisali kot $\matrix{A}^T$.

Če je torej `a` takšna

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

je `a.T` takšna:

    a.T

    array([[ 2,  5,  1, -1],
           [-7,  9,  0, -2],
           [ 2,  1,  0, -8]])

Stolpci so postali vrstice, vrstice stolpci. Tabelo bomo presenetljivo pogosto takole zasukali, saj bo določene reči lažje narediti z vrsticami, kot bi jih bilo s stolpci.

Tridimenzionalne tabele se vedejo podobno kot dvodimenzionalne. Imajo pač en indeks več, pa izpišejo se bolj nerodno. In štiri- ali petdimenzionalne enako. `.T` pa obrne vrstni red dimenzij. (Vendar ga pri takih tabelah vsaj jaz še nisem uporabil. No, pa take tabele tudi.)

## Še par načinov sestavljanja tabel

Tabele smo doslej sestavljali tako, da smo poklicali `np.array` in podali pripravljen seznam (ali seznam seznamov, če smo želeli dve dimenziji) v Pythonu. (Prejšnji teden pa tudi tako, da smo poklicali `np.genfromtxt`, ki je prebral datoteko v tabelo, še več tabel pa smo dobili z raznimi načini indeksiranja tako prebrane tabele.)

Včasih si moramo pripraviti prazno tabelo - tabelo ničel, enic ali česa drugega. Kot argument podamo terko z dimenzijami. Če terka vsebuje dva elementa, bo tabela dvodimenzionalna. Če pet, bo petdimenzionalna.

    np.zeros((2, 4))

    array([[0., 0., 0., 0.],
           [0., 0., 0., 0.]])

    np.ones((2, 4))

    array([[1., 1., 1., 1.],
           [1., 1., 1., 1.]])

    np.full((2, 4), 42)

    array([[42, 42, 42, 42],
           [42, 42, 42, 42]])

Pogosto potrebujemo tudi tabelo zaporednih števil.

    np.arange(12)

    array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

Ali, recimo, 15 enako razmaknjenih števil med 0 in 3.5.

    np.linspace(0, 3.5, 15)

    array([0.  , 0.25, 0.5 , 0.75, 1.  , 1.25, 1.5 , 1.75, 2.  , 2.25, 2.5 ,
           2.75, 3.  , 3.25, 3.5 ])

Mimogrede spoznajmo še en način za ustvarjanje tabele: pripravimo tabelo s petimi vrsticami in tremi stolpci naključnih števil med -1 in 1.

    r = np.random.uniform(-1, 1, (5, 2))

    r

    array([[-0.25293564, -0.94072709],
           [ 0.35411039,  0.1389441 ],
           [-0.89286703, -0.55631392],
           [-0.68174291,  0.26498722],
           [-0.72935741,  0.51457491]])

V gornjih tabelah so bili `int`-i ali `float`-i; kdaj se je zgodilo kaj, vidimo iz izpisa. Če sestavimo tabelo iz seznama ali z `np.full`, je tip odvisen od argumenta. Če je v seznamu kak `float`, bo to tabela `float`-ov, če sami `int`-i, tabela `int`-ov.

Seveda pa

1. Lahko o tem sami odločamo
2. `numpy` ne pozna samo enega `int`-a in enega `float`-a.

Čas za nov mednaslov. :)

## Lastnosti tabele

Spomnimo se na `a`.

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

    a.ndim

    2

    a.shape

    (4, 3)

    a.size

    12

    a.dtype

    dtype('int64')

    a.itemsize

    8

`ndim` je povedal, da je tabela dvodimenzionalna, `shape` pove njeno obliko (dolžina `shape` je ravno `ndim`) in `size` pove število elementov (kar je ravno produkt tega, kar imamo v `shape`). Če tako pogledamo, bi bilo dovolj imeti `shape`, saj je možno vse drugo izračunati iz njega. Da, vendar `size` včasih potrebujemo in `ndim` včasih preverimo, zato je čisto praktično, da obstajata.

Bolj zanimivi sta zadnji stvari. `dtype` pove podatkovni tip elementov. Numpy ima svoje podatkovne tipe, tako številske kot ... druge. Elementi tabele `a` so tipa `int64`, kar pomeni `int`, shranjen s 64 biti. `itemsize` je število bajtov, ki jih zasede posamezni element. Ker je vsaj bajt sestavljen iz 8 bitov (kar se zdi danes logično, zgodovina računalništva pa beleži tudi drugačne modele), je 64 bitov 8 bajtov.

Zakaj ravno `int64`? Zakaj ne `int40`? (Ker ne obstaja. :) Zakaj ne `int32`? V osnovi so v tabeli `int`-i, katera različica (širina) `int`-a bo privzeta, pa je odvisno od, hm, operacijskega sistema oziroma vedenja `numpy`-ja na posameznem operacijskem sistemu. Na MS Windows so bili privzeti `int`-i še nedavno (ali pa še vedno?) 32-bitni, četudi je operacijski sistem že davno 64-biten. Na macOS in Linux so 64-bitni. (Kako vem? Ker nam je že povzročalo veselje pri programiranju programov, ki bi morali delovati tudi na MS Windows, vendar so skrivnostno crkovali.)

Najboljše je, da se glede tega ne vznemirjate preveč, saj bo za vaše potrebe dovolj, da se delate, da so tabele bodisi `int` ali `float`.

Kakšnega tipa bo tabela, je odvisno od tega, kaj zapišemo vanjo.

    np.array([2, 8, -1]).dtype

    dtype('int64')

    np.array([2, 1, 3.14, 8]).dtype

    dtype('float64')

Tabele, ki jih sestavimo z `zeros` in `ones` so praviloma `float`, razen če zahtevamo drugače. Prav tako lahko eksplicitno zahtevamo drugačen tip, kadar tabelo sestavimo z `np.array` ali katero drugo funkcijo.

    np.ones(4)

    array([1., 1., 1., 1.])

    np.ones(4, dtype=int)

    array([1, 1, 1, 1])

    np.ones(4, dtype=float)

    array([1., 1., 1., 1.])

    np.ones(4, dtype=bool)

    array([ True,  True,  True,  True])

    np.array([1, 2, 3], dtype=float)

    array([1., 2., 3.])

## Spreminjanje lastnosti tabele

Spremenimo lahko obliko tabele ali njen tip. Točneje: iz tabele lahko naredimo drugo tabelo, ki ima drugačno obliko ali tip elementov.

Obliko spreminjamo z `reshape`.

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

    b = a.reshape(2, 6)

    b

    array([[ 2, -7,  2,  5,  9,  1],
           [ 1,  0,  0, -1, -2, -8]])

In tu se začne numpy-jeva magija. `b` je le drugačen pogled na pomnilnik, v katerem je shranjen `a`. Podatki se tu niso kopirali, torej nismo izgubljali ne časa ne dodatnega pomnilnika. A to za nas ni posebej pomembno - vsaj dokler te tabele ne spreminjamo. Spreminjanje `b`-ja bi namreč spremenilo tudi `a`.

Ko obliko tabele, mora imeti nova tabela enako število elementov. Tabelo 3x4 lahko spremenimo v 2x6 ali celo 1x12, ne pa v 3x5.

Eno od dimenzij lahko nastavimo na `-1` pa bo numpy sam izračunal, kakšna mora biti. Če hočemo spraviti `a` v dva stolpca in se nam danes ne da računati, koliko je 3 x 4 / 2, napišemo kar

    a.reshape(-1, 2)

    array([[ 2, -7],
           [ 2,  5],
           [ 9,  1],
           [ 1,  0],
           [ 0, -1],
           [-2, -8]])

`a` se pri tem seveda ni spremenil. To je nova tabela (oz. nov pogled na obstoječo tabelo).

Tip spreminjamo z `astype`. Tu dobimo novo tabelo.

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

    a.astype(float)

    array([[ 2., -7.,  2.],
           [ 5.,  9.,  1.],
           [ 1.,  0.,  0.],
           [-1., -2., -8.]])

    a.astype(bool)

    array([[ True,  True,  True],
           [ True,  True,  True],
           [ True, False, False],
           [ True,  True,  True]])

    a.astype("U5")

    array([['2', '-7', '2'],
           ['5', '9', '1'],
           ['1', '0', '0'],
           ['-1', '-2', '-8']], dtype='<U5')

Ups, kakšen tip je `U5`? Niz (U kot Unicode) z največ petimi znaki.

Dovolj naštevanja, čas je za primer.

## Numpy na dražbi brez anonimnosti

Najprej preberimo podatke.

    zapisnik = np.genfromtxt(
        "../domace-naloge/03-drazba-brez-anonimnosti/zapisnik.txt",
        delimiter=",")

Funkciji `np.getfromtxt` lahko podamo bodisi ime datoteke bodisi odprto datoteko. Slednje je posebej praktično, če smo na Windows in moramo še določati `encoding`.

    zapisnik = np.genfromtxt(
        open("../domace-naloge/03-drazba-brez-anonimnosti/zapisnik.txt",
             encoding="utf-8"),
        delimiter=",")

Začetek tabele je tak.

    zapisnik[:5]

    array([[nan, nan, 31.],
           [nan, nan, 33.],
           [nan, nan, 35.],
           [nan, nan, 37.],
           [nan, nan, 40.]])

`nan`? `nan` pomeni not a number. Da, seveda, prva dva stolpca vsebujeta imena predmetov in ljudi, not a number.

Določimo, naj bo `dtype` niz.

    zapisnik = np.genfromtxt(
        "../domace-naloge/03-drazba-brez-anonimnosti/zapisnik.txt",
        delimiter=",",
        dtype=str)

    zapisnik[:5]

    array([['slika', 'Berta', '31'],
           ['slika', 'Ana', '33'],
           ['slika', 'Berta', '35'],
           ['slika', 'Fanči', '37'],
           ['slika', 'Ana', '40']], dtype='<U21')

(Preden nadaljujemo: numpy pravi `dtype='U12'`. To pomeni niz z največ 20 znaki.)

Pa imamo dvodimenzionalno tabelo. Vendar bi bilo pravzaprav bolj praktično imeti tri spremenljivke, `predmeti`, `osebe` in `cene`, vsaka bi imela svoj stolpec.

Lahko bi se šli

    predmeti = zapisnik[:, 0]
    osebe = zapisnik[:, 1]
    cene = zapisnik[:, 2]

vendar obstaja prikladen trik: tabelo s tremi stolpci in bogvekoliko vrsticami prevrnimo v tabelo s tremi vrsticami in bogveliko stolpci.

    zapisnik.T[:, :5]

    array([['slika', 'slika', 'slika', 'slika', 'slika'],
           ['Berta', 'Ana', 'Berta', 'Fanči', 'Ana'],
           ['31', '33', '35', '37', '40']], dtype='<U21')

Da ne bi bilo predolgo, smo, tako kot prej pet vrstic, zdaj izpisali le pet stolpcev. (Poglej kako! Prvi indeks je `:`, da dobimo vse tri vrstice, drugi `:5`, da dobimo le prvih pet stolpcev.

V tako prevrnjeni tabeli je `zapisnik.T[0]` prva vrstica, `zapisnik.T[1]` druga in `zapisnik.T[2]` tretja. To je imenitno, ker lahko te tri vrstice preprosto razpakiramo v tri spremenljivke!

    predmeti, osebe, cene = zapisnik.T

    predmeti[:15]

    array(['slika', 'slika', 'slika', 'slika', 'slika', 'slika',
           'pozlačen dežnik', 'Meldrumove vaze', 'Meldrumove vaze',
           'Meldrumove vaze', 'Meldrumove vaze', 'Meldrumove vaze',
           'Meldrumove vaze', 'Meldrumove vaze', 'Meldrumove vaze'],
          dtype='<U21')

    osebe[:15]

    array(['Berta', 'Ana', 'Berta', 'Fanči', 'Ana', 'Fanči', 'Ema', 'Greta',
           'Ana', 'Greta', 'Ana', 'Fanči', 'Ana', 'Greta', 'Ana'],
          dtype='<U21')

    cene[:15]

    array(['31', '33', '35', '37', '40', '45', '29', '44', '46', '48', '53',
           '57', '60', '61', '63'], dtype='<U21')

Aha, cene so nizi, potrebno jih bo spremeniti v številke. Zamahnemo s čarobno paličko numpyja, pa je.

    cene = cene.astype(int)

    cene

    array([ 31,  33,  35,  37,  40,  45,  29,  44,  46,  48,  53,  57,  60,
            61,  63,  67,  71,  76,  78,  50,  55,  60,  61,  62,  65,  68,
            70,  74,  76,  80,  83,  30,  32,  37,  39,  43,  44,  45,  50,
            53,  55,  58,  61,  63,  68,  72,  76,  77,  81,  85,  86,  90,
            92,  94,  97,  98,  99, 100, 103, 107,  15,  27,  30,  35,  39,
            40,  45,  47,  49,  53,  55,  58,  59,  62,  63,  16,  21])

Zdaj pa le po domači nalogi.

### 1. Izpiši, kateri predmet je dosegel najvišjo ceno, kakšno in kdo ga je kupil.

Najvišjo ceno je trivialno najti,

    np.max(cene)

    np.int64(107)

Nas pa zanima tudi, za kateri predmet gre in kdo ga je kupil. Zanima nas torej, kaj je v tabelah `predmeti` in `osebe` na tistem mestu, kjer je v `cene` številka 107. Torej ne potrebujemo (ali pa vsaj: ne potrebujemo samo) največjega števila v `cene` temveč tudi (in predvsem) njegov indeks.

Pomnite li, kako smo pisali funkcijo `argmax`? Python je nima, Numpy pač.

    np.argmax(cene)

    np.int64(59)

Potem pa ni problema:

    naj_i = np.argmax(cene)

    print(f"Najdražji predmet, {predmeti[naj_i]}, je za {cene[naj_i]} kupila {osebe[naj_i]}.")

    Najdražji predmet, kip, je za 107 kupila Dani.

### 2. Izpiši končne cene vseh predmetov

Prejšnji teden je bilo preprosto: imeli smo le seznam cen in cene različnih predmetov so bile razmejene z `-1`. Zdaj imamo seznam predmetov in ugotoviti je treba, v katerih vrsticah se predmet zamenja.

Trik poznamo že od prejšnjič: zadnjič smo računali razlike med zaporednimi vrsticami (ko nas je zanimalo, za koliko se kolesar dvigne ali spusti med dvema zaporednima meritvama). Zdaj nas zanima preprosto, ali sta dve zaporedni vrstici različni.

    predmeti[1:] != predmeti[:-1]

    array([False, False, False, False, False,  True,  True, False, False,
           False, False, False, False, False, False, False, False, False,
            True, False, False, False, False, False, False, False, False,
           False, False, False,  True, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False,  True,  True, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False,  True, False])

Poiščimo indekse, kjer pride do sprememb.

    spremembe = np.flatnonzero(predmeti[1:] != predmeti[:-1])

    spremembe

    array([ 5,  6, 18, 30, 59, 60, 74])

Ker smo izpustili prvo vrstico, so tole v bistvu indekse zadnjih vrstic vsakega predmeta. Manjka le zadnji indeks; ta je pač `len(predmeti) - 1`.

Če k spremembam prištejemo `1`, pa dobimo prve vrstice za vsak predmet. Zdaj nam zmanjka prva vrstica prvega predmeta; te ja preprosto `0`.

Pripravimo tabeli z indeksi začetnih (ki jih bomo potrebovali kasneje) in končnih (ker jih bomo potrebovali zdaj) vrstic, ki opisujejo posamezni predmet. Manjkajoči prvi in zadnji element pripnemo z `np.hstack`. Spet ne spreglejmo: `hstack` kot argument pričakuje terko s tabelami, ki naj jih spne, zato dvojni oklepaji.

    zacetni = np.hstack(([0], spremembe + 1))
    koncni = np.hstack((spremembe, [len(predmeti) - 1]))

Zdaj, ko imamo končne indekse, vemo, da so končne cene

    cene[koncni]

    array([ 45,  29,  78,  83, 107,  15,  63,  21])

Pripadajoči predmeti pa

    predmeti[koncni]

    array(['slika', 'pozlačen dežnik', 'Meldrumove vaze', 'skodelice', 'kip',
           'čajnik', 'srebrn jedilni servis', 'perzijska preproga'],
          dtype='<U21')

Tako lahko izpišemo:

    for predmet, cena in zip(predmeti[koncni], cene[koncni]):
        print(f"{predmet:25} {cena:3}")

    slika                      45
    pozlačen dežnik            29
    Meldrumove vaze            78
    skodelice                  83
    kip                       107
    čajnik                     15
    srebrn jedilni servis      63
    perzijska preproga         21

Seveda bi lahko poprej pripravili tabelo končnih predmetov in cen.

    konpred = predmeti[koncni]
    koncene = cene[koncni]

To malo poenostavi `zip`:

    for predmet, cena in zip(predmeti[koncni], cene[koncni]):
        print(f"{predmet:25} {cena:3}")

    slika                      45
    pozlačen dežnik            29
    Meldrumove vaze            78
    skodelice                  83
    kip                       107
    čajnik                     15
    srebrn jedilni servis      63
    perzijska preproga         21

Kaj pa, če bi želeli te predmete urediti po cenah?

Urediti cene - znamo:

    np.sort(koncene)

    array([ 15,  21,  29,  45,  63,  78,  83, 107])

Vendar je problem, da bi zdaj radi preuredili imena predmetov enako, kot so tu preurejene cene. Se pravi, morali bi, hm, poznati indekse, ki preslikajo te številke iz originalne tabele, `koncene` v takole preurejeno tabelo, da bi potem enako preslikali imena predmetov.

Ne razumemo gornjega stavka? Ne? OK, gremo počasi.

Pomnite li imenitno našo funkcijo `argmax`, ki namesto največjega elementa vrne njegov indeks. Enako - in še bolj - imeniten je `argsort`, ki namesto urejene tabele vrne indekse, ki uredijo tabelo.

    red = np.argsort(koncene)

    red

    array([5, 7, 1, 0, 6, 2, 3, 4])

Primerjajmo to s

    koncene

    array([ 45,  29,  78,  83, 107,  15,  63,  21])

`red` nam pove tole: če hočeš seznam z urejenimi elementi `koncene`, moraš najprej vzeti element z indeksom `5`,

    koncene[5]

    np.int64(15)

nato tistega z indeksom 7,

    koncene[7]

    np.int64(21)

potem onega z indeksom 1,

    koncene[1]

    np.int64(29)

... ali, če skrajšamo zgodbo tako, da se spomnimo, da lahko tabelo indeksiramo z indeksi iz druge tabele:

    koncene[red]

    array([ 15,  21,  29,  45,  63,  78,  83, 107])

Ker so predmeti v `konpred` našteti tako, da jim pripadajo istoležne cene, moramo po enakem vrstnem redu pobrati še predmete.

    konpred[red]

    array(['čajnik', 'perzijska preproga', 'pozlačen dežnik', 'slika',
           'srebrn jedilni servis', 'Meldrumove vaze', 'skodelice', 'kip'],
          dtype='<U21')

Če želimo urediti padajočo, pač obrnemo vrstni `red`.

Pa imamo:

    red = np.argsort(koncene)[::-1]

    for predmet, cena in zip(konpred[red], koncene[red]):
        print(f"{predmet:25} {cena:3}")

    kip                       107
    skodelice                  83
    Meldrumove vaze            78
    srebrn jedilni servis      63
    slika                      45
    pozlačen dežnik            29
    perzijska preproga         21
    čajnik                     15

### 3. Izpiši, koliko ponudb je bil deležen vsak izmed predmetov

K nalogi dodajmo še, da morajo biti predmeti izpisani po padajočem številu ponudb.

Če vemo, da so prve vrstice, ki se nanašajo na določen predmet v `koncni`, začetne pa v `zacetni`,

    print(koncni)
    print(zacetni)

    [ 5  6 18 30 59 60 74 76]
    [ 0  6  7 19 31 60 61 75]

nam je le odšteti tidve tabeli in prišteti 1, pa imamo število ponudb za vsak predmet. Število ponudb arguredimo in potem nadaljujemo tako kot prej.

    ponudb = koncni - zacetni + 1

    red = np.argsort(ponudb)[::-1]

    for predmet, pon in zip(konpred[red], ponudb[red]):
        print(f"{predmet:25} {pon:3}")

    kip                        29
    srebrn jedilni servis      14
    skodelice                  12
    Meldrumove vaze            12
    slika                       6
    perzijska preproga          2
    čajnik                      1
    pozlačen dežnik             1

### 4. Izpiši, za kateri predmet je bilo največ ponudb. Če si prvo mesto deli več predmetov, izpiši vse.

Tole smo skoraj že naredili, pa še malo več. Le tisti del o izpisu več predmetov, ki si deli prvo mesto bi lahko dodali. Pa bomo zdaj naredili drugače: poiskali bomo predmet z največ ponudbami in izpisali vse, ki imajo toliko ponudb. To bo sicer le eden, a iz programa bo očitno, da bi jih lahko bilo več.

    ponudbe = koncni - zacetni + 1

    maska = ponudbe == np.max(ponudbe)

Zdaj vsebuje `maska` `True` za vse elemente, ki imajo toliko ponudb, kolikor je `np.max(ponudbe)`.

    ponudbe

    array([ 6,  1, 12, 12, 29,  1, 14,  2])

    maska

    array([False, False, False, False,  True, False, False, False])

Sicer je `True` le eden (tam, kjer je 29), lahko pa bi jih bilo tudi več.

Zdaj izpišimo te predmete: masko, ki smo jo sestavili iz `ponudbe`, uporabimo na `predmeti`.

    for predmet in konpred[maska]:
        print(predmet)

    kip

## Še malo numpy-ja: osi

Nazaj k dvodimenzionalni matriki `a`.

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

Kaj vrnejo funkcije, kot sta `np.max` in `np.sum`?

    np.max(a)

    np.int64(9)

    np.sum(a)

    np.int64(2)

To je včasih v resnici uporabno, vendar redko. Pogosteje želimo vsoto po vrsticah ali po stolpcih.

Matrika ima dve osi. Prva gre dol, po vrsticah (pač: prvi indeks so vrstice), druga po stolpcih (ker: drugi indeks). Funkcije `np.max`, `np.sum` in druge, kjer je to smiselno (pri `argsort`, recimo, ni) sprejmejo dodatni argument `axis`.

    np.max(a, axis=0)

    array([5, 9, 2])

    np.max(a, axis=1)

    array([ 2,  9,  1, -1])

Prva, `axis=0`, poišče največji element vzdolž indeksa `0`, torej vrne tabelo z največjim elementom v vsakem stolpcu. Druga poišče največje element vsake vrstice, torej vzdolž indeksa `1`.

Enako `sum`:

    np.sum(a, axis=0)

    array([ 7,  0, -5])

    np.sum(a, axis=1)

    array([ -3,  15,   1, -11])

Kaj pa tole: obdržati želimo le vrstice, ki vsebujejo same nenegativne elemente.

    a

    array([[ 2, -7,  2],
           [ 5,  9,  1],
           [ 1,  0,  0],
           [-1, -2, -8]])

    a >= 0

    array([[ True, False,  True],
           [ True,  True,  True],
           [ True,  True,  True],
           [False, False, False]])

`np.all(a >= 0)` nam pove, ali so vsi elementi `a` nenegativni. (Niso.)

    np.all(a >= 0)

    np.False_

Vendar nas to zanima v smeri stolpcev, v smeri 1.

    np.all(a >= 0, axis=1)

    array([False,  True,  True, False])

To uporabimo kot masko za vrstice `a`-ja.

    a[np.all(a >= 0, axis=1)]

    array([[5, 9, 1],
           [1, 0, 0]])

Bi znali prešteti, koliko pozitivnih elementov vsebuje posamični stolpec?

    np.sum(a > 0, axis=0)

    array([3, 1, 2])

## Numpy, koliko je π?

Če slučajno pozabimo, koliko je π, je to preprosto izračunati.

Predstavljamo si krog s središčem v (0, 0) in polmerom 1. Ploščina tega kroga je π r2=π12=π.

Potem si predstavljamo kvadrat, ki ga očrtamo temu krogu. Postavimo ga vzporedno s koordinatami, z drugimi besedami, po x in y gre ta kvadrat od -1 do 1. Njegova stranica je 2 in ploščina, očitno 4.

Zdaj naključno izžrebamo N (recimo 1000) točk znotraj kvadrata. Kakšen delež teh točk je znotraj kroga? Ker je razmerje med ploščino kroga in kvadrata π/4, je pričakovati, da bo v krogu k=N π/4 točk.

Če smo torej pozabili π, sprogramiramo takšen poskus. Naključno izžrebamo koordinate N točk, pogledamo koliko jih je v krogu (to označimo s k) in potem iz gornje formule izrazimo π: π= 4k/N.

Gremo. Točke bomo izžrebali z `np.random.uniform(-1, 1, (N, 2))`, ki bo vrnil tabelo velikosti (N, 2) z naključnimi števili med -1 in 1.

Da bo pregledneje bomo za začetek delali z desetimi točkami.

    N = 10

    tocke = np.random.uniform(-1, 1, (N, 2))

    tocke

    array([[ 0.00948207, -0.07791707],
           [ 0.15539343,  0.21180141],
           [ 0.71511941,  0.37776169],
           [ 0.23962235,  0.52776044],
           [ 0.45650711, -0.1539899 ],
           [ 0.85403227, -0.76246257],
           [-0.27235405, -0.64166477],
           [-0.55670031,  0.26139958],
           [ 0.79770905, -0.73099231],
           [ 0.37104341, -0.93574917]])

Prvi stolpec so koordinate x, druge y. Razdaljo od točke do središča koordinatnega sistema (in s tem kroga) dobimo tako, da seštejemo kvadrate x in y ter to korenimo. Hvala, πtagora.

    np.sum(tocke ** 2, axis=1)

    array([0.00616098, 0.06900696, 0.65409967, 0.33594995, 0.23211163,
           1.31072029, 0.4859104 , 0.37824498, 1.17068948, 1.01329972])

Vidimo? `np.sum(..., axis=1)` sešteva "vodoravno", dobimo torej ravno vsoto (kvadratov) prvega in drugega stolpca.

To korenimo.

    np.sqrt(np.sum(tocke ** 2, axis=1))

    array([0.0784919 , 0.26269175, 0.80876428, 0.5796119 , 0.48177965,
           1.14486693, 0.69707274, 0.61501624, 1.08198405, 1.00662789])

Točka je znotraj kroga s polmerom `1`, če je njena razdalja od izhodišča manjša od 1.

    np.sqrt(np.sum(tocke ** 2, axis=1)) < 1

    array([ True,  True,  True,  True,  True, False,  True,  True, False,
           False])

(Na tem mestu opazimo, da je korenjenje nepotrebno. Koren je manjši od 1 natančno takrat, ko je število že samo manjše od 1.)

In zdaj samo preštejemo, koliko `True`-jev imamo.

    np.sum(np.sum(tocke ** 2, axis=1) < 1)

    np.int64(7)

To je naš `k`. Ponovimo na 1000 točkah in izračunajmo π.

    N = 1000

    k = np.sum(np.sum(np.random.uniform(-1, 1, (N, 2)) ** 2, axis=1) < 1)

    4 * k / N

    np.float64(3.096)

No, recimo. Brez težav lahko ponovimo reč z več točkami.

    N = 1000000

    k = np.sum(np.sum(np.random.uniform(-1, 1, (N, 2)) ** 2, axis=1) < 1)

    4 * k / N

    np.float64(3.145504)

## Unikatni elementi

Spoznajmo še eno zanimivo funkcijo: `np.unique` vrne tabelo vseh različnih elementov neke tabele. Takole, recimo, dobimo tabelo vseh, ki so sodelovale na dražbi.

    np.unique(osebe)

    array(['Ana', 'Berta', 'Cilka', 'Dani', 'Ema', 'Fanči', 'Greta', 'Helga'],
          dtype='<U21')

Funkcija je posebej zanimiva zaradi tega, kar lahko vrne poleg te tabele. Če ji dodamo argument `return_counts=True`, vrne še število pojavitev vsakega od teh imen - torej, kolikokrat je posamična oseba dvigovala ceno (vštevši prvo ponudbo).

    np.unique(osebe, return_counts=True)

    (array(['Ana', 'Berta', 'Cilka', 'Dani', 'Ema', 'Fanči', 'Greta', 'Helga'],
           dtype='<U21'),
     array([ 6, 11, 13, 13, 11,  5, 15,  3]))

Ker vrne dve stvari, je to najbolj praktično razpakirati v dve spremenljivki. In že lahko povemo, kdo je bil najbolj zagret.

    imena, visanja = np.unique(osebe, return_counts=True)

    imena[np.argmax(visanja)]

    np.str_('Greta')

Drug zanimiv dodatni argument je `return_inverse=True`. Klic `np.unique(a, return_inverse=True)` bo poleg tabele unikatov vrnil tabelo, ki bo imela toliko elementov kot `a`. Vsak element pove, na katero mesto unikatne tabele se preslika posamični element `a`-ja.

    imena, indeksi = np.unique(osebe, return_inverse=True)

    osebe

    array(['Berta', 'Ana', 'Berta', 'Fanči', 'Ana', 'Fanči', 'Ema', 'Greta',
           'Ana', 'Greta', 'Ana', 'Fanči', 'Ana', 'Greta', 'Ana', 'Cilka',
           'Greta', 'Fanči', 'Cilka', 'Dani', 'Berta', 'Dani', 'Berta',
           'Dani', 'Berta', 'Dani', 'Berta', 'Dani', 'Berta', 'Dani', 'Berta',
           'Cilka', 'Ema', 'Berta', 'Ema', 'Cilka', 'Berta', 'Cilka', 'Dani',
           'Cilka', 'Greta', 'Dani', 'Cilka', 'Dani', 'Greta', 'Cilka',
           'Greta', 'Ema', 'Dani', 'Greta', 'Cilka', 'Dani', 'Greta', 'Ema',
           'Dani', 'Ema', 'Greta', 'Ema', 'Greta', 'Dani', 'Berta', 'Ema',
           'Helga', 'Ema', 'Cilka', 'Helga', 'Greta', 'Ema', 'Cilka', 'Ema',
           'Greta', 'Cilka', 'Greta', 'Cilka', 'Greta', 'Fanči', 'Helga'],
          dtype='<U21')

    indeksi

    array([1, 0, 1, 5, 0, 5, 4, 6, 0, 6, 0, 5, 0, 6, 0, 2, 6, 5, 2, 3, 1, 3,
           1, 3, 1, 3, 1, 3, 1, 3, 1, 2, 4, 1, 4, 2, 1, 2, 3, 2, 6, 3, 2, 3,
           6, 2, 6, 4, 3, 6, 2, 3, 6, 4, 3, 4, 6, 4, 6, 3, 1, 4, 7, 4, 2, 7,
           6, 4, 2, 4, 6, 2, 6, 2, 6, 5, 7])

    imena

    array(['Ana', 'Berta', 'Cilka', 'Dani', 'Ema', 'Fanči', 'Greta', 'Helga'],
          dtype='<U21')

Z `imena` in `indeksi` lahko (če bi želeli, a nam seveda ni treba) rekonstruiramo osebe. Prvi trije elementi `indeksi` so 1, 0, 1, 5. Pripadajoči elementi `imena` so (prvi) Berta, (ničti) Ana, prvi (Berta) in (peti) Fanči - kar so natančno prvi elementi `osebe`. Torej

    imena[indeksi]

    array(['Berta', 'Ana', 'Berta', 'Fanči', 'Ana', 'Fanči', 'Ema', 'Greta',
           'Ana', 'Greta', 'Ana', 'Fanči', 'Ana', 'Greta', 'Ana', 'Cilka',
           'Greta', 'Fanči', 'Cilka', 'Dani', 'Berta', 'Dani', 'Berta',
           'Dani', 'Berta', 'Dani', 'Berta', 'Dani', 'Berta', 'Dani', 'Berta',
           'Cilka', 'Ema', 'Berta', 'Ema', 'Cilka', 'Berta', 'Cilka', 'Dani',
           'Cilka', 'Greta', 'Dani', 'Cilka', 'Dani', 'Greta', 'Cilka',
           'Greta', 'Ema', 'Dani', 'Greta', 'Cilka', 'Dani', 'Greta', 'Ema',
           'Dani', 'Ema', 'Greta', 'Ema', 'Greta', 'Dani', 'Berta', 'Ema',
           'Helga', 'Ema', 'Cilka', 'Helga', 'Greta', 'Ema', 'Cilka', 'Ema',
           'Greta', 'Cilka', 'Greta', 'Cilka', 'Greta', 'Fanči', 'Helga'],
          dtype='<U21')

Tega seveda ne počnemo, saj osebe že imamo. Indekse uporabimo za kaj drugega.

## Numpy na dodatni dražbi

### 5. Za vsako osebo izpiši, koliko je porabila na dražbi

Tole bo zahtevalo nekaj norega indeksiranja. Dovolj norega - in, predvsem, zelo potratnega - da bi to nalogo v praksi rešili s slovarji in zanko v Pythonu. Vendar bo poučno z vidika numpyja.

Pripravimo si tabelo ničel, ki ima toliko vrstic, kolikor je prodanih predmetov, in toliko stolpcev, kolikor je oseb. Torej: za vsako osebo drug stolpec.

    imena, indeksi = np.unique(osebe, return_inverse=True)

    nakupi = np.zeros((len(koncni), len(imena)), dtype=int)
    nakupi

    array([[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]])

In zdaj nori del. Unique je vsaki osebi priredil indeks. Ana je 0, Berta je 1 in tako naprej. `indeksi` za vsako vrstico iz podatkov pove, kakšen je indeks osebe, na katero se vrstica nanaša. `imena[indeksi]` so, vemo, pač kar imena oseb. Tabela `koncni` vsebuje indekse tistih vrstic, ki se nanašajo na končne cene. `indeksi[koncni]` so potem indeksi imen oseb, ki so dejansko kupile posamični predmet.

    indeksi[koncni]

    array([5, 4, 2, 1, 3, 1, 6, 7])

Prvi predmet je kupila oseba 5, drugega oseba 4 in tako naprej. Oseba 1 je kupila dva predmeta. Kdo je to? Berta.

    imena[1]

    np.str_('Berta')

Sicer pa so končni kupci, po vrsti po predmetih,

    imena[indeksi[koncni]]

    array(['Fanči', 'Ema', 'Cilka', 'Berta', 'Dani', 'Berta', 'Greta',
           'Helga'], dtype='<U21')

V `cene[koncni]` najdemo, kot vemo že dolgo, končne cene predmetov.

    cene[koncni]

    array([ 45,  29,  78,  83, 107,  15,  63,  21])

Zdaj pa izpolnimo tabelico nakupi. Vsaka vrstica se nanaša na nek predmet. Ti so unikatni in jih oštevilčimo kar od `0` do `len(koncni)` (brez `len(koncni)`). Vsak stolpec se nanaša na neko osebo. Za vsako številko od 0 do len(koncni) in osebo v `indeksi[koncni]` (indeksi oseb, ki so kupile nek predmet) v pripadajočo celico vpišemo ceno.

    nakupi[np.arange(len(koncni)), indeksi[koncni]] = cene[koncni]

    nakupi

    array([[  0,   0,   0,   0,   0,  45,   0,   0],
           [  0,   0,   0,   0,  29,   0,   0,   0],
           [  0,   0,  78,   0,   0,   0,   0,   0],
           [  0,  83,   0,   0,   0,   0,   0,   0],
           [  0,   0,   0, 107,   0,   0,   0,   0],
           [  0,  15,   0,   0,   0,   0,   0,   0],
           [  0,   0,   0,   0,   0,   0,  63,   0],
           [  0,   0,   0,   0,   0,   0,   0,  21]])

Prvi stolpec je Anin. Ta ni kupila ničesar. Drugi je Bertin: kupila je nekaj za 83 in nekaj za 15. In tako naprej. Koliko je zapravil kdo, izvemo, če seštejemo stolpce.

    poraba = np.sum(nakupi, axis=0)

    poraba

    array([  0,  98,  78, 107,  29,  45,  63,  21])

Imena, ki pripadajo stolpcem, dobimo v `imena`. In že lahko izpišemo.

    for ime, pora in zip(imena, poraba):
        print(f"{ime:8}{pora:4}")

    Ana        0
    Berta     98
    Cilka     78
    Dani     107
    Ema       29
    Fanči     45
    Greta     63
    Helga     21

Še enkrat: uporabiti tako veliko tabelo za tako malo dela je nesmisel. Če bi vsaka oseba kupila več stvari in če bi lahko vsak izdelek prodali več osebam, pa bi posta(ja)lo smiselno. Vendar smo se tega dvojnega, vzporednega indeksiranja naučili, ker nam bo prišlo prav kasneje.

### 6. Za vsak predmet izpiši, za koliko je bila končna cena višja od prve

Po telovadbi iz prejšnjega razdelka je to trivialno. Začetne cene so v `cene[zacetni]`, koncne v `cene[koncni]`. Odštejemo ju, pa dobimo razlike, po katerih sprašuje naloga.

    print(cene[koncni])
    print(cene[zacetni])

    cene[koncni] - cene[zacetni]

    [ 45  29  78  83 107  15  63  21]
    [31 29 44 50 30 15 27 16]

    array([14,  0, 34, 33, 77,  0, 36,  5])

Imena predmetov najdemo v `predmeti[zacetni]` ali `predmeti[koncni]` (oboje bo enako). Ker dobra vaga pomaga v nebesa, bomo dodali še število ponudb in ponovili, kako oblikujemo izpis.

    ponudb = koncni - zacetni + 1
    zvisanja = cene[koncni] - cene[zacetni]

    print(f"{'Predmet':25}{'ponudb':6}{'zvišanje':>12}")
    print("-" * (25 + 6 + 12))
    for predmet, pon, zvis in zip(predmeti[koncni], ponudb, zvisanja):
        print(f"{predmet:25}{pon:6}{zvis:12}")

    Predmet                  ponudb    zvišanje
    -------------------------------------------
    slika                         6          14
    pozlačen dežnik               1           0
    Meldrumove vaze              12          34
    skodelice                    12          33
    kip                          29          77
    čajnik                        1           0
    srebrn jedilni servis        14          36
    perzijska preproga            2           5

### Kdo največkrat viša za Berto?!

V neki naloga je Berta ugotovila, da ima najbrž sovražnika, ki vedno viša njeno ponudbo. Morali smo ji ga pomagati razkrinkati. V osnovi bi šlo tako:

- najprej ugotovimo, v katerih vrsticah se oglaša Berta. Zadnjo vrstico odstranimo: če je tam slučajno Berta, bi nam to kasneje (videli bomo, kdaj) povzročalo sitnosti.

    osebe[:-1] == "Berta"

    array([ True, False,  True, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False,  True, False,  True, False,  True, False,  True,
           False,  True, False,  True, False, False,  True, False, False,
            True, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False,  True, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False])

    np.flatnonzero(osebe[:-1] == "Berta")

    array([ 0,  2, 20, 22, 24, 26, 28, 30, 33, 36, 60])

Zanimala nas bodo imena v vrsticah, ki so za temi vrsticami. K indeksom torej prištejemo `1` (in to je razlog, da smo odstranili zadnjo vrstico - če bi bila v zadnji vrstici slučajno Berta, bi dobili prevelik indeks).

    np.flatnonzero(osebe[:-1] == "Berta") + 1

    array([ 1,  3, 21, 23, 25, 27, 29, 31, 34, 37, 61])

    osebe[np.flatnonzero(osebe[:-1]== "Berta") + 1]

    array(['Ana', 'Fanči', 'Dani', 'Dani', 'Dani', 'Dani', 'Dani', 'Cilka',
           'Ema', 'Cilka', 'Ema'], dtype='<U21')

In zdaj le še preštejemo, kolikokrat se na tem spisku pojavi posamično ime.

    imena, za_berto = np.unique(osebe[np.flatnonzero(osebe[:-1] == "Berta") + 1], return_counts=True)

    imena

    array(['Ana', 'Cilka', 'Dani', 'Ema', 'Fanči'], dtype='<U21')

    za_berto

    array([1, 2, 5, 2, 1])

Največji je števec na indeksu

    np.argmax(za_berto)

    np.int64(2)

In pripadajoče ime - ha, pa jo imamo, sovražnico Berte - je

    imena[np.argmax(za_berto)]

    np.str_('Dani')

### Logične operacije na tabelah in popravek: kdo je res Bertina sovražnica?

Vendar žal ni tako preprosto. Berta je kupila dva izdelka. Za njima je nekdo ponudil prvo ceno za naslednji izdelek, mi pa smo ga prišteli k Bertinim sovražnikom. Vrniti se bo treba malo nazaj.

Tole je maska, ki predstavlja vrstice, v katerih se je oglašala Berta (brez zadnje).

    (osebe == "Berta")[:-1]

    array([ True, False,  True, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False,  True, False,  True, False,  True, False,  True,
           False,  True, False,  True, False, False,  True, False, False,
            True, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False,  True, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False])

Tole so vrstice, ki ne predstavljajo zadnje ponudbe za nek izdelek - spet brez zadnje.

    predmeti[1:] == predmeti[:-1]

    array([ True,  True,  True,  True,  True, False, False,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
           False,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True, False,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True, False, False,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True, False,  True])

Da smo naredili pravilno, se prepričamo, če pogledamo zapisnik: ta se začne z

    slika,Berta,31
    slika,Ana,33
    slika,Berta,35
    slika,Fanči,37
    slika,Ana,40
    slika,Fanči,45
    pozlačen dežnik,Ema,29
    Meldrumove vaze,Greta,44
    Meldrumove vaze,Ana,46

Šesti in sedmi element gornje tabele sta `False`; pripadajoči vrstici sta zadnji ponudbi, torej tisti, v katerih Bertine vrstice ne smemo šteti kot ponudbo, ki jo je nekdo kasneje zviševal.

Zanimajo nas torej le vrstice, kjer se oglaša Berta in so ne-zadnja ponudba, torej, v bistvu `(osebe == "Berta")[:-1] and predmeti[1:] == predmeti[:-1]`. Žal `and` nad tabelami ne deluje po elementih; razlogi so tehnični in zgodovinski. Uporabiti moramo operator `&`, ki ga poznamo iz množic. Vlogo `or` in `not` pa prevzameta `|` in `~`. Na hitro jih preverimo na preprostem primeru.

    a = np.array([True, True, False, False])
    b = np.array([True, False, False, True])

    a & b

    array([ True, False, False, False])

    a | b

    array([ True,  True, False,  True])

    ~b

    array([False,  True,  True, False])

Če bi kdaj prišlo prav, je tu še "ekskluzivni ali", ki vrne `True`, če je `True` natanko eden od elementov, ne pa oba.

    a ^ b

    array([False,  True, False,  True])

S temi operatorji je povezana še ena komplikacija: premočni so. Previsoko prioriteto imajo. Izraz `a == b and c == d` bi seveda pomeni `(a == b) and (c == d)`, saj `==` veže močneje kot `and`. Operator `&` pa je močnejši `a == b & c == d` bi pomenilo `a == (b & c) == d`. (Zakaj jih niso naredili šibkejših? Ker prihajajo iz drugega vica. Če bi nas zanimalo, ali je presek množic `a` in `b` enak `c`, bi napisali `a & b == c` in zdelo bi se nam logično, da to pomeni `(a & b) == c`. Sploh pa `&` izvorno niti ni operacija nad množicami, temveč nad števili. O tem nismo in ne bomo govorili, vendar je to (in ne množice) razlog, da med tabelami v numpyju uporabljamo `&`.)

Izvedši torej vse o tem, kako izračunati konjunkcijo tabel, brž sestavimo pravo masko:

    (osebe == "Berta")[:-1] & (predmeti[1:] == predmeti[:-1])

    array([ True, False,  True, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False,  True, False,  True, False,  True, False,  True,
           False,  True, False, False, False, False,  True, False, False,
            True, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False])

In odtod vse steče po prejšnji poti.

    imena, za_berto = np.unique(
        osebe[np.flatnonzero((osebe == "Berta")[:-1] & (predmeti[1:] == predmeti[:-1])) + 1],
        return_counts=True)

    za_berto

    array([1, 1, 5, 1, 1])

Da, da, prej je bilo `[1, 2, 5, 2, 1]`. Ampak najhujša je pa še vedno Dani.

### Najvišje zvišanje v času zadnjih sedem ponudb

V neki domači nalogi je bilo potrebno ugotoviti, za koliko se je cena posamičnega predmeta dvignila v zadnjih sedmih ponudbah; če je bilo ponudb manj, pa pač v zadnjih toliko, kolikor jih je bilo.

Če so indeksi vrstic z zadnjo ponudb v

    koncni

    array([ 5,  6, 18, 30, 59, 60, 74, 76])

potem moramo za vsakega za sedem vrstic nazaj,

    koncni - 7

    array([-2, -1, 11, 23, 52, 53, 67, 69])

Seveda pa je to narobe, ker za nekatere predmete niti nimamo sedmih ponudb. Da je tako, nas posebej ostro opozorita prva izdelka, ki nas potisneta celo v negativne indekse. Indekse z vrsticami končnih ponudb moramo "omejiti" z indeksi vrstic s prvo ponudbo. Vzeti moramo tisto od njiju, ki je večja.

    print(koncni - 7)
    print(zacetni)

    [-2 -1 11 23 52 53 67 69]
    [ 0  6  7 19 31 60 61 75]

Kako to storiti? Preprosto; pravzaprav čisto očitno iz tega, kako smo ju izpisali v zgornji celici: zložimo ju v eno samo tabelo in izračunamo maksimum.

    np.array([koncni - 7, zacetni])

    array([[-2, -1, 11, 23, 52, 53, 67, 69],
           [ 0,  6,  7, 19, 31, 60, 61, 75]])

    koncni_7 = np.max([koncni - 7, zacetni], axis=0)

    koncni_7

    array([ 0,  6, 11, 23, 52, 60, 67, 75])

Za vsak slučaj preverimo, da se vrstice `koncni` in `koncni_7` res nanašajo na iste predmete.

    predmeti[koncni]

    array(['slika', 'pozlačen dežnik', 'Meldrumove vaze', 'skodelice', 'kip',
           'čajnik', 'srebrn jedilni servis', 'perzijska preproga'],
          dtype='<U21')

    predmeti[koncni_7]

    array(['slika', 'pozlačen dežnik', 'Meldrumove vaze', 'skodelice', 'kip',
           'čajnik', 'srebrn jedilni servis', 'perzijska preproga'],
          dtype='<U21')

Ah, čemu bi to primerjali sami! Naj nam to naredi `numpy`.

    predmeti[koncni] == predmeti[koncni_7]

    array([ True,  True,  True,  True,  True,  True,  True,  True])

Če smo še bolj leni, pa kar

    np.all(predmeti[koncni] == predmeti[koncni_7])

    np.True_

Zdaj nam `cene[koncni]` pove končne cene, `cene[koncni-7]` pa cene za sedem ponudb prej. Zanima pa nas razlika.

    cene[koncni] - cene[koncni_7]

    array([14,  0, 21, 21, 15,  0, 16,  5])

In izpišimo.

    for predmet, razlika in zip(predmeti[koncni], cene[koncni] - cene[koncni_7]):
        print(f"{predmet:25}{razlika:3}")

    slika                     14
    pozlačen dežnik            0
    Meldrumove vaze           21
    skodelice                 21
    kip                       15
    čajnik                     0
    srebrn jedilni servis     16
    perzijska preproga         5

#### Funkcija kar tako: `clip`

Tole je za vtis. Ko sem programiral gornji primer, sem naredil, kot sem naredil. Ko sem ga predeloval v zapiske, pa sem se spomnil na funkcijo `clip`. Ta prejme tabelo in meji; vse elemente, ki so pod spodnjo mejo, nastavi na spodnjo mejo in te, ki so nad zgornjo, potlači do zgornje.

Nek profesor izračuna oceno tako, da dosežene odstotke na pisnem izpitu deli z 10 in prišteje 1. Odstotki so lahko tudi večji kot 100, ker lahko študenti rešujejo dodatne naloge.

    odstotki = np.array([63, 82, 45, 25, 125, 89])

    ocene = odstotki // 10 + 1
    ocene

    array([ 7,  9,  5,  3, 13,  9])

Pravila UL velevajo, da študentom ne dajemo ocen manjših od 5 in večjih od 10. In tu uporabimo `np.clip`.

    np.clip(ocene, 5, 10)

    array([ 7,  9,  5,  5, 10,  9])

Za funkcijo sem vedel, ni pa mi prišlo na misel, da bi lahko bili meji tudi tabeli. Recimo, da so študenti poleg tega delali domače naloge in iz nekega razloga končna ocena ne more biti višja od ocen domačih nalog. In, tako kot prej, ne more biti nižja od 5.

    domace = np.array([8, 6, 5, 5, 9, 10])

    print(ocene)
    print(domace)
    print(np.clip(ocene, 5, domace))

    [ 7  9  5  3 13  9]
    [ 8  6  5  5  9 10]
    [7 6 5 5 9 9]

Potem bi lahko zgoraj namesto

    np.max([koncni - 7, zacetni], axis=0)

    array([ 0,  6, 11, 23, 52, 60, 67, 75])

napisali

    np.clip(koncni - 7, zacetni, None)

    array([ 0,  6, 11, 23, 52, 60, 67, 75])

Zgornje meje ni, zato `None`.

Je to bistveno krajše, boljše? Če bi bili tabeli veliki in bi nam bilo to mar, bi lahko s tem prihranili precej pomnilnika. Poleg tega je iz imena funkcije `np.clip` bolj jasno, kaj tule počnemo; `np.max` je pač ... nek maksimum.

Sicer pa tole pišem z drugim namenom: da pokažem, da ima numpy res ogromno funkcij, ki jih poznamo ali pa tudi ne in se nanje spomnimo ali pa tudi ne. Dalj kot ga uporabljamo, bolj bomo spretni in bolj elegantni bodo naši programi. Sicer pa pravzprav enako velja tudi za Python.