numpy
-jaOsnovni element - osnovni podatkovni tip - v numpy
-ju je
tabela (array
). Ta je nekoliko podobna Pythonovemu seznamu.
(V slovenščini mu nekaterimi pravimo seznam, ker je njegovo
uradno angleško ime list
, drugi pa to slovenijo v
tabela, ker bi bilo to ime iz določenih razlogov dejansko
boljše. Tu pa nam pride prav, da je ime tabela še nezasedeno in
ga lahko uporabimo za numpy
-jeve array-e.)
Med numpyjevimi tabelami in Pythonovimi seznami je več pomembnih razlik. Za prvo srečanje z numpyjem se bomo posvetili dvema: razlikam v načinu indeksiranja in razlikam med tem, kako nanju delujejo različne operacije.
Pripravimo si dva seznama v Pythonu.
= [3, 8, 9, 2]
p = [8, 0, 1, -3] r
Z indeksiranjem lahko pridemo do posamičnih elementov. Indeksi morajo
biti seveda cela števila (int
).
2] p[
9
Sezname lahko tudi "seštevamo". Besedo seštevanje pravzaprav
uporabljamo zgolj zato, ker uporabimo operator +
. V resnici
ne gre za seštevanje (v matematičnem pomenu), temveč za
stikanje.
+ r p
[3, 8, 9, 2, 8, 0, 1, -3]
Ker +
v resnici ni seštevanje, odštevanje seznamov pa bi
sploh nimelo (zakaj slovenščina nima te besede?!) nobenega smisla.
- r p
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[4], line 1
----> 1 p - r
TypeError: unsupported operand type(s) for -: 'list' and 'list'
Prav tako nima smisla k seznamu prišteti 1, saj ne moremo stakniti seznama in števila.
+ 1 p
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[5], line 1
----> 1 p + 1
TypeError: can only concatenate list (not "int") to list
Pač pa lahko seznam pomnožimo s celim številom; rezultat je tak, kot da bi seznam tolikokrat sešteli.
* 3 p
[3, 8, 9, 2, 3, 8, 9, 2, 3, 8, 9, 2]
numpy
Če hočemo modul uporabiti, ga moramo najprej uvoziti. Modula
numpy
navadno ne uvažamo z import numpy
, ker
bomo njegove funkcije uporabljali tako pogosto, da bi bil ves program
poln numpy.
-- ime numpy
je preprosto
predolgo. Po drugi strani ne uvažamo posamičnih funkcij
from numpy import hstack, sum, max
, ker tipično potrebujemo
preveč različnih funkcij, poleg tega pa imajo nekatere enaka imena kot
vdelane Pythonove funkcije (na primer sum
in
max
; funkcije, uvožene iz numpy
bi jih
izpodrinile in povzročile težave, saj pričakujejo drugačne podatke,
delajo malo drugače in vračajo drugačne rezultate kot Pythonove.
numpy
zato običajno uvozimo z
import numpy as np
To uvozi modul, vendar ga potem ne vidimo pod imenom
numpy
, temveč pod krajšim, prijaznejšim imenom
np
.
Zdaj pa naredimo dve tabeli. Najpreprosteje bo poklicati funkcijo
array
in ji podati seznam elementov.
= np.array([3, 8, 9, 2])
a = np.array([8, 0, 1, -3]) b
Tako.
a
array([3, 8, 9, 2])
Tabele indeksiramo tako kot sezname. Z indeksi, ki morajo biti cela števila.
2] a[
9
Delujejo tudi rezine in vse, kar smo se naučili v zvezi z njimi.
2:4] a[
array([9, 2])
1:] a[
array([8, 9, 2])
-1] a[:
array([3, 8, 9])
Prva razlika primerjavi s seznami: če potrebujemo več elementov, lahko podamo več indeksov. Ne kar tako, da jih naštejemo (s tem bomo dosegli nekaj drugega), temveč tako, da kot indeks podamo seznam ali tabelo indeksov.
Če imamo torej
a
array([3, 8, 9, 2])
je
2, 0, 1, 0, 0, 1]] a[[
array([9, 3, 8, 3, 3, 8])
tabela, ki vsebuje drugi, ničti, prvi, potem še dvakrat ničti in spet
prvi element tabele a
.
Namesto seznama int
-ov, lahko podamo seznam (ali tabelo)
bool
-ov. Ta seznam mora biti enako dolg kot tabela, ki jo
indeksiramo, saj pove, katere elemente bi radi in katerih ne. Takole,
recimo, dobimo prvi in zadnji element tabele:
True, False, False, True]] a[[
array([3, 2])
Oboje - predvsem zadnje - je videti ... ne preveč uporabno. V resnici bo fenomenalno uporabno. Počakajmo na primer.
Imamo torej
a
array([3, 8, 9, 2])
b
array([ 8, 0, 1, -3])
Seštejmo ju.
+ b a
array([11, 8, 10, -1])
Da, to je v resnici seštevanje. Potemtakem lahko tudi v resnici odštevamo.
- b a
array([-5, 8, 8, 5])
In množimo.
* b a
array([24, 0, 9, -6])
Vse operacije nad tabelami, delujejo po elementih. +
,
-
, *
... vsaka operacija se izvede na vsakem
elementu posebej. Zato lahko tabele množimo tudi s števili, jim
prištevamo števila...
a
array([3, 8, 9, 2])
+ 1 a
array([ 4, 9, 10, 3])
* 2.5 a
array([ 7.5, 20. , 22.5, 5. ])
2]) ** range(10) np.array([
array([ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512])
Še več. Celo operacije, kot so <
delujejo nad
posameznimi elementi.
b
array([ 8, 0, 1, -3])
> 0 b
array([ True, False, True, False])
Pa funkcije tudi!
abs(b) np.
array([8, 0, 1, 3])
np.sqrt(a)
array([1.73205081, 2.82842712, 3. , 1.41421356])
Kjer je to smiselno, jasno. Funkcije, kot so sum
in
max
bodo seveda seštevale in množile.
sum(a) np.
22
max(a) np.
9
Tule smo uporabljali numpy
-jeve funkcije
abs
, sqrt
, sum
in
max
. Namesto nekaterih od njih bi lahko uporabili tudi
Pythonove funkcije. sum
zna seštevati, kar jima pride pod
roko (točneje: vse, čez kar lahko nažene zanko for
), pa
tudi max
in min
nista izbirčna.
sum(a)
22
Vendar: ko delamo s tabelami, se nam vedno splača uporabiti
numpy
-jeve ekvivalente funkcije. Če ne drugega, bodo
hitrejše, včasih pa vdelane Pythonove funkcije ne bodo delovale pravilno
ali pa ne sploh (sqrt(a)
javi napako). Poleg tega imajo
numpy
-jeve funkcije pogosto dodatne argumente, specifične
za delo s tabelami.
Ko delamo z numpy
-jem, se poskušamo predvsem izogniti
pisanju zank.
Kako bi dobili vsoto vseh pozitivnih elementov b
-ja?
Najprej sestavimo "masko", tabelo bool
-ov, ki vsebuje
True
na mestih, kjer ima b
pozitivne
elemente.
> 0 b
array([ True, False, True, False])
S to masko lahko izberemo pozitivne elemente.
> 0] b[b
array([8, 1])
Ker nas zanima vsota, jih seštejemo.
sum(b[b > 0]) np.
9
Če bi nas zanimalo samo, koliko pozitivnih elementov ima
b
, bi preprosto sešteli masko, saj tudi v
numpy
velja, da je True
toliko kot
1
, False
pa toliko kot 0
.
sum(b > 0) np.
2
Če bi bili še manj zahtevni in bi nas zanimalo le, ali ima
b
kakšen pozitiven element, bi uporabili any
(spet vzamemo numpy
-je ekvivalent in ne Pyhonovega
vdelanega any
, ki smo ga spoznali prejšnjo uro):
any(b > 0) np.
True
Da b
nima samih pozitivnih elementov, pa nam pove
all
:
all(b > 0) np.
False
Podobno preprosto je poiskati (šteti, preverjati) sode elemente
a
-ja.
% 2 == 0] a[a
array([8, 2])
Ta, zadnji primer je kar zanimivo prebrati: a % 2 == 0
.
Kar ta formula pravi o a
-ju, se v bistvu nanaša na vsak
element a
-ja. Ko rečemo a % 2 == 0
dobimo to,
kar bi v golem Pythonu, s seznami, dosegli z
[x % 2 == 0 for x in a]
. Pogovor o izpeljanih seznamih je
bil - če ne zaradi drugega - potreben zato, da lažje razumemo idejo
"vektorskih operacij" - operacij, ki se, popolnoma enake, zgodijo na
vsakem elementu tabele. Nekje znotraj numpy
-ja se seveda še
vedno skriva neka zanka, vendar je zaradi načina, na katerega je
numpy
narejen takšna zanka veliko (kjer "veliko" zlahka
pomeni petdesetkrat ali celo stokrat) hitrejša, kot če bi zapisali zanko
v Pythonu.
Kolesar je na vsakih sto metrov (neke izgleda kar razgibane :) vožnje zabeležil svojo nadmorsko višino.
= np.array([345, 355, 360, 364, 378, 370, 360, 355, 360, 361]) h
Zdaj ga, kot vsakega kolesarja, ki počne takšne stvari, zanima skupni dvig.
Za začetek je potrebno dobiti seznam sprememb višine med zaporednimi pari meritev. S seznami bi napisali nekaj takšnega:
= [x - y for x, y in zip(h[1:], h)]
d
d
[10, 5, 4, 14, -8, -10, -5, 5, 1]
Vendar se želimo izogniti zanki. Operator -
želimo
uporabiti na tabeli, ne na njenih posamičnih elementih. Gornji
zip
- oziroma njegovi argument - so že pokazali, kaj je
potrebno odšteti.
print(h[1:])
print(h)
[355 360 364 378 370 360 355 360 361]
[345 355 360 364 378 370 360 355 360 361]
Odšteti moramo, preprosto drugo tabelo od prve.
1:] - h h[
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[43], line 1
----> 1 h[1:] - h
ValueError: operands could not be broadcast together with shapes (9,) (10,)
Da, mali detajl: ker -
deluje po elementih, morata biti
tabeli enako dolgi. Druga je za en element predolga. Zadnji element zato
odrežemo.
= h[1:] - h[:-1]
d
d
array([ 10, 5, 4, 14, -8, -10, -5, 5, 1])
Če to seštejemo, bomo seveda dobili 0
, saj je kolesar
končal, kjer je začel.
sum(d) np.
16
Vsota spustov je pač enaka vsoti dvigov. Kolesarja zanimajo le dvigi,
> 0] d[d
array([10, 5, 4, 14, 5, 1])
To je torej tisto, kar želimo sešteti. Za bonus navrzimo še največji dvig. In zložimo vse skupaj, da bomo imeli pravi vtis, kako kratek program smo napisali.
= np.array([345, 355, 360, 364, 378, 370, 360, 355, 360, 361, 345])
h = h[1:] - h[:-1]
d print(np.max(d))
print(np.sum(d[d > 0]))
14
39
Zdaj pa se spomnimo domače naloge Dražba: rešili jo bomo s numpy-ja. Videli bomo, kako bo pokazal mišice!
Za začetek uvozimo podatke. O funkcijah, kot je
genfromtxt
se bomo pogovarjali prihodnjič. Tu jo le
pokličimo.
= np.genfromtxt("../domace-naloge/02-drazba/drazba.txt", dtype=int) cene
cene
array([11, 17, 24, 30, -1, 13, 27, 33, -1, 12, 27, 34, 40, -1, 9, -1, 8,
20, 30, 31, -1])
Na prvi dve vprašanji - koliko izdelkov so prodali in koliko je stal najdražji, lahko že odgovorimo.
Prodali so toliko izdelkov, kolikor je -1
. S
cene == -1
dobimo tabelo False
in
True
; True
, kjer so -1
.
== -1 cene
array([False, False, False, False, True, False, False, False, True,
False, False, False, False, True, False, True, False, False,
False, False, True])
Ker je True
enak 1
in False
enak 0
, elemente seznama preprosto seštejemo.
sum(cene == -1) np.
5
Cena najdražjega izdelka je kar največja številka v tabeli.
max(cene) np.
40
Odgovor na zadnje vprašanje je pravilen, z estetskega vidika pa
nekoliko zmoti, da smo računali maksimum prek vsega, vključno z vmesnimi
ponudbami in celo -1
. Nalogo bomo brez težav rešili tudi
elegantneje, saj nas bo v to prisililo naslednje vprašanje: kakšna je
vsota cen prodanih izdelkov.
Funkcija np.flatnonzero
nam vrne tabelo z indeksi
ne-ničelnih elementov. Če gre za tabelo True
-jev in
False
-ov, vrne indekse True
-jev. V našem
primeru bodo to ravno indeksi elementov z vrednostjo
-1
.
= np.flatnonzero(cene == -1)
indeksi
indeksi
array([ 4, 8, 13, 15, 20])
Hitro se prepričamo, da so na teh indeksih ravno
-1
-ke.
cene[indeksi]
array([-1, -1, -1, -1, -1])
To je nezanimivo: zanimajo nas ravno elementi pred njimi. Od indeksov
torej odštejemo 1
.
= cene[indeksi - 1]
koncne
koncne
array([30, 33, 40, 9, 31])
Zdaj lahko ponovno rešimo prvi dve nalogi in še tretjo:
print(f"Število prodanih predmetov: {len(koncne)}")
print(f"Cena najdražjega predmeta: {np.max(koncne)}")
print(f"Vsota končnih cen: {np.sum(koncne)}")
Število prodanih predmetov: 5
Cena najdražjega predmeta: 40
Vsota končnih cen: 143
Zadnji vprašanji sprašujeta, koliko predmetov je kupila Ana in koliko Berta ter koliko je zapravila katera od njiju.
Na dražbi sta le onidve in prva ponudba je vedno Anina. Za odgovor na
vprašanji moramo prešteti, za koliko predmetov je bilo število ponudb
liho in za koliko sodo. Za to pa moramo za začetek prešteti število
ponudb za vsak predmet. Pravilni odgovor bo
[4, 3, 4, 1, 4]
.
Število ponudb je (skoraj) enako razlikam med indeksi.
indeksi
array([ 4, 8, 13, 15, 20])
Za prvi predmet so bile dane štiri ponudbe. Naprej gledamo razlike:
indeks druge -1 je 8
, indeks prve pa 4
; vmes
so bile 8 - 4 - 1 = 3 ponudbe. (Še 1 je potrebno odšteti, ker imamo v
tabeli poleg cen še vmesne elemente -1
.) Naslednja indeksa
sta 13 in 8; 13 - 8 - 1 = 4.
Število ponudb za posamični predmet bomo izvedeli, če od
indeksi
array([ 4, 8, 13, 15, 20])
odštejemo
-1], indeksi[:-1])) np.hstack(([
array([-1, 4, 8, 13, 15])
in še -1
. Na začetek smo dodali -1. Na ta način bomo po
odštevanju dobili ravno pravo številko za nesrečni, posebni prvi
predmet, saj bomo imeli 4 - (-1) - 1 = 4
. Ne spreglejte
tudi dvojnih oklepajev: funkciji np.hstack
kot argument
podamo terko s tabelama, ki jo želimo speti skupaj.
= indeksi - np.hstack(([-1], indeksi[:-1])) - 1
ponudb
ponudb
array([4, 3, 4, 1, 4])
Natančno, kar potrebujemo.
Ana je kupila tiste predmete, za katere je bilo število ponudb liho; Berta tiste, za katere je bilo sodo.
= ponudb % 2 == 1
ana = ponudb % 2 == 0
berta
ana
array([False, True, False, True, False])
print(f"Ana je kupila {np.sum(ana)}, Berta pa {np.sum(berta)} reči.")
Ana je kupila 2, Berta pa 3 reči.
In koliko je zapravila katera? Tole so cene reči, ki so končale pri Ani:
koncne[ana]
array([33, 9])
Torej, očitno,
print(f"Ana je zapravila {np.sum(koncne[ana])}, Berta pa {np.sum(koncne[berta])}.")
Ana je zapravila 42, Berta pa 101.
Da se zavemo, kako elegantno kratko je vse skupaj, napišimo celoten program v kosu.
import numpy as np
= np.genfromtxt("../domace-naloge/02-drazba/drazba.txt", dtype=int)
cene = np.flatnonzero(cene == -1)
indeksi = cene[indeksi - 1]
koncne = indeksi - np.hstack(([-1], indeksi[:-1])) - 1
ponudb = ponudb % 2 == 1
ana = ponudb % 2 == 0
berta
print(f"Število prodanih predmetov: {len(koncne)}")
print(f"Cena najdražjega predmeta: {np.max(koncne)}")
print(f"Vsota končnih cen: {np.sum(koncne)}")
print(f"Ana je kupila {np.sum(ana)}, Berta pa {np.sum(berta)} reči.")
print(f"Ana je zapravila {np.sum(koncne[ana])}, Berta pa {np.sum(koncne[berta])}.")
Število prodanih predmetov: 5
Cena najdražjega predmeta: 40
Vsota končnih cen: 143
Ana je kupila 2, Berta pa 3 reči.
Ana je zapravila 42, Berta pa 101.