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:

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.