Numpy je ena taka knjižnica za delo s poljubnodimenzionalnimi tabelami. V takih časteh jo imajo, da so svojčas modrovali, ne bi li je bilo smiselno med vdelane knjižnice dodati in vkup s Pythonom distribuirati. Ako bi avtor Pythona ne sklenil, da ne bode tako, današnji dan bi ... (Kaj pa, če bi nehal? Prav, bom jenjal. Ni naporno samo brati, tudi pisati je nemogoče. Ne vem, kako je Trubar to zdržal.)
Python je v osnovi počasen in kuri ogromno pomnilnika. Toliko pomnilnika:
import sys
3.14) sys.getsizeof(
24
42) sys.getsizeof(
28
28 bajtov za en int
me pravzaprav nič ne boli. 28 MB za
milijon int
-ov je pa veliko. Ampak milijon
int
-ov bo vedno milijon int
-ov v seznamu ali
neki večdimenzionalni tabelo. Podobno je z zankami. Bolijo me zanke, ki
se velikokrat obrnejo. To pa so pogosto zanke prek nekih daljših
seznamov in tabel.
Numpy reši prvi problem in, v večini primerov, tudi drugega. Zato je del obvezne opreme Pythona. Vsak, ki resno dela s pythonom, bo prej ko slej potreboval tudi numpy.
Numpy navadno uvozimo z
import numpy as np
To je oblika importa, ki modul ob importu še preimenuje. Namesto da
bi ga potem videli pod imenom numpy
, ga vidimo kot
np
. To je kar konvencija, vsi počnejo tako. Tudi mi
bomo.
Osnovni podatkovni tip v numpy
-ju je
np.array
, to je, poljubno dimenzionalna tabela, ki vsebuje
elemente istega tipa. (Poljubno dimenzionalna: lahko je tudi
0-dimenzionalna. 0-dimenzionalna tabela je število. :)
Tabelo lahko sestavimo na več načinov. Dobimo jo lahko iz seznama; v spodnjem primeru iz seznama seznamov.
1, 2, 3], [4, 5, 6]]) np.array([[
array([[1, 2, 3],
[4, 5, 6]])
Lahko pa sestavimo tabelo samih ničel, enic ali česa drugega, ali pa neinicializirano tabelo. Kot argument podamo terko z dimenzijami. Če terka vsebuje dva elementa, bo tabela dvodimenzionalna. Če pet, bo petdimenzionalna.
2, 4)) np.zeros((
array([[0., 0., 0., 0.],
[0., 0., 0., 0.]])
2, 4)) np.ones((
array([[1., 1., 1., 1.],
[1., 1., 1., 1.]])
2, 4), 42) np.full((
array([[42, 42, 42, 42],
[42, 42, 42, 42]])
2, 4)) np.empty((
array([[1., 1., 1., 1.],
[1., 1., 1., 1.]])
Slednje, np.empty
vrne neinicializirano tabelo. V njej
je lahko karkoli, kar se pač slučajno nahaja v koščku pomnilnika, ki je
bil dodeljen tabeli. Če dobimo slučajno same enice, je to ... pač
slučajno.
Pogosto potrebujemo tudi tabelo zaporednih števil.
12) np.arange(
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
Ali, recimo, 15 enako razmaknjenih števil med 0 in 3.5.
0, 3.5, 15) np.linspace(
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 ])
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
numpy
ne pozna samo enega int
-a in enega
float
-a.Tip določamo z dodatnim argumentom dtype
. Njegova
vrednost je lahko np.int8
, np.int16
,
np.int32
, np.int64
, np.uint8
,
np.uint16
, ..., np.float16
, ... OK, razumemo
idejo - številka je število bitov, u
je unsigned in tako
naprej. Samo vseh skalarnih tipov je 24; lahko si ogledate seznam.
Tule je tridimenzionalna tabela 16-bitnih floatov.
2, 4, 3), dtype=np.float16) np.ones((
array([[[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]],
[[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]]], dtype=float16)
Mimogrede spoznajmo še en način za ustvarjanje tabele. Kar sprašujemo in izpisujemo zatem, je menda očitno.
= np.random.randint(0, 100, (3, 4))
a a
array([[20, 35, 82, 94],
[73, 67, 42, 52],
[89, 3, 46, 33]])
a.ndim
2
a.shape
(3, 4)
a.size
12
a.dtype
dtype('int64')
a.itemsize
8
itemsize
vrne velikost posameznega elementa v bajtih.
64-bitni int
-i očitno vzamejo 8 bajtov.
Tabele lahko preoblikujemo.
= a.reshape(2, 6)
b b
array([[20, 35, 82, 94, 73, 67],
[42, 52, 89, 3, 46, 33]])
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 zdajle še ni tako pomembno, zares bo postalo pomembno ob
indeksiranju.
Dvoje sem obljubil v začetku. Prvo: numpy
bode prijazen
do pomnilnika. (Pravzaprav sem obljubil troje: prvo je bilo, da ne bom
več imitiral Trubarja.) In vidimo: je prijazen. Vsak element tabele
vzame le toliko pomnilnika, kot je potrebno. Drugo: rešil nas bo
počasnih Pythonovih zank. To pride zdaj.
Namreč: operacije nad tabelami se izvajajo po elementih, z zanko, ki je napisana v C-ju (v katerem je napisan numpy).
= np.arange(12).reshape(3, 4)
a a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
* 10 a
array([[ 0, 10, 20, 30],
[ 40, 50, 60, 70],
[ 80, 90, 100, 110]])
To množenje vseh elementov z 10 se je zgodilo tako hitro, kot če bi to naredili v C-ju.
To se da poljubno zaplesti.
- 5) * 10 + 1000 (a
array([[ 950, 960, 970, 980],
[ 990, 1000, 1010, 1020],
[1030, 1040, 1050, 1060]])
** 2 a
array([[ 0, 1, 4, 9],
[ 16, 25, 36, 49],
[ 64, 81, 100, 121]])
Delo po elementih velja tudi za operatorje, kot so ==
,
<
in >
.
** 2 > 40 a
array([[False, False, False, False],
[False, False, False, True],
[ True, True, True, True]])
S tem smo seveda dobili novo tabelo, katere dtype
je
bool
.
Numpy ima tudi vse žive matematične funkcije. Namesto
math.sqrt
, recimo, pokličemo np.sqrt
, pa bo
kot argument sprejela in pokorenila celo tabelo.
np.sqrt(a)
array([[0. , 1. , 1.41421356, 1.73205081],
[2. , 2.23606798, 2.44948974, 2.64575131],
[2.82842712, 3. , 3.16227766, 3.31662479]])
= np.random.randint(0, 100, (3, 4))
r r
array([[51, 12, 16, 75],
[46, 61, 37, 46],
[27, 65, 78, 35]])
max(r) np.
78
sum(r) np.
549
np.mean(r)
45.75
Funkcije, ki takole seštevajo in podobno "akumulirajo" elemente, lahko izvajamo tudi po oseh". Os 0 je prve dimenzija, os 1 druga, in tako naprej.
max(r, axis=0) np.
array([51, 65, 78, 75])
max(r, axis=1) np.
array([75, 61, 78])
sum(r, axis=1) np.
array([154, 190, 205])
Spomnimo se, kaj dela operator >
.
= r > 30
t t
array([[ True, False, False, True],
[ True, True, True, True],
[False, True, True, True]])
Torej z np.all(r > 30, axis=1)
izvemo, v katerih
vrsticah so vsi elementi večji od 30.
all(r > 0.3, axis=1) np.
array([ True, True, True])
Množenje, primerjanje ... tabele s številom je samo poseben primer množenja in primerjanja tabel.
= np.array([2, 6, 3])
a = np.array([8, 2, 1]) b
+ b a
array([10, 8, 4])
* b a
array([16, 12, 3])
> b a
array([False, True, True])
** b a
array([256, 36, 3])
Spet: vse te operacije so enako hitre, kot če bi to počeli v C-ju. Saj v resnici počnemo v C-ju. Razlika se seveda pozna predvsem, ko tabele postanejo velike.
Enako se vedejo večdimenzionalne tabele.
= np.array([[4, 5, 1], [1, 2, 4]])
a = np.array([[1, 2, 8], [3, 1, 5]]) b
a
array([[4, 5, 1],
[1, 2, 4]])
b
array([[1, 2, 8],
[3, 1, 5]])
+ b a
array([[5, 7, 9],
[4, 3, 9]])
* b a
array([[ 4, 10, 8],
[ 3, 2, 20]])
> b a
array([[ True, True, False],
[False, True, False]])
a
array([[4, 5, 1],
[1, 2, 4]])
K tabeli lahko prištejemo tudi tabelo (ali kar seznam), katerega dimenzija ustreza zadnji dimenziji tabele.
+ [100, 200, 300] a
array([[104, 205, 301],
[101, 202, 304]])
Za primer zdaj vzemimo, da imamo koordinate nekih točk na ravnini.
= np.array([
k 5, 2],
[4, 1],
[-1, 4],
[5, 6],
[0, 2]
[ ])
Kakšne so njihove razdalje od koordinatnega izhodišča? Za vsako točko je potrebno izračunati $\sqrt{x^2 + y^2}$, pri čemer sta x in y prvi in drugi stolpec. Prav. Najprej kvadriramo.
** 2 k
array([[25, 4],
[16, 1],
[ 1, 16],
[25, 36],
[ 0, 4]])
Seštejemo vsako vrstico, da dobimo x2 + y2.
sum(k ** 2, axis=1) np.
array([29, 17, 17, 61, 4])
Korenimo.
sum(k ** 2, axis=1)) np.sqrt(np.
array([5.38516481, 4.12310563, 4.12310563, 7.81024968, 2. ])
Kaj pa, če nas zanima razdalja točk od (1, 2)? In moramo torej računati $\sqrt{(x - 1)^2 + (y - 2)^2}$? Pač odštejemo (1, 2) od prvega in drugega stolpca, ne?
- (1, 2) k
array([[ 4, 0],
[ 3, -1],
[-2, 2],
[ 4, 4],
[-1, 0]])
sum((k - (1, 2)) ** 2, axis=1)) np.sqrt(np.
array([4. , 3.16227766, 2.82842712, 5.65685425, 1. ])
Lepota vsega tega je, spet, učinkovitost. Četudi bi imeli milijon točk, bi se tole izračunalo enako hitro, kot če bi programirali v Cju. Le da bi tam morali alocirati pomnilnik, pisati zanke ... tu pa se vse zanke skrivajo v numpyju, ki, kakor se zdi iz Pythona, dela z vsemi elementi tabele hkrati.
Še zadnji košček sestavljanke, zaradi katere je numpy tako učinkovit in praktičen.
= np.arange(12).reshape(4, 3)
a a
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
a[3]
bo, očitno, vrnil tretjo vrstico matrike.
3] a[
array([ 9, 10, 11])
Da pridemo do drugega elementa tretje vrstice, bi, če bi bil to Pythonov seznam seznamov, potrebovali dvojne indekse.
3][2] a[
11
V numpyju pa smemo v iste oglate oklepaje napisati več indeksov.
3, 2] a[
11
In, najbolj imenitno: ti indeksi so lahko tudi rezine. In celo samo
:
. Začnimo kar z njim.
3, :] # sicer isto kot a[3] ... a[
array([ 9, 10, 11])
2] a[:,
array([ 2, 5, 8, 11])
Razumemo? a[3, :]
vrne vse, kar je v tretji vrstici,
a[:, 2]
vrne vse (vrstice, ki so) v tretjem stolpcu.
Seveda delujejo tudi običajne rezine.
1:3] a[
array([[3, 4, 5],
[6, 7, 8]])
1:3, 1:] a[
array([[4, 5],
[7, 8]])
Obljubil sem, da bo indeksiranje in rezanje pomagalo k Pythonovi učinkovisto in praktičnosti. Tole je bila praktično. Kje je učinkovitost? Oh, še začeli nismo!
Prvo, kar se tiče učinkovitosti je: podatki se ne kopirajo. Ko
napišemo a[1:3, 1:]
dobimo le nov pogled na iste
podatke.
a
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
= a[1:3, 1:]
b += 100
b a
array([[ 0, 1, 2],
[ 3, 104, 105],
[ 6, 107, 108],
[ 9, 10, 11]])
Seveda gre tudi brez b
.
1:3, 1:] = 0
a[ a
array([[ 0, 1, 2],
[ 3, 0, 0],
[ 6, 0, 0],
[ 9, 10, 11]])
Kaj pa to: vsako drugo vrstico a
-ja hočemo spremeniti v
[1, 2, 3]
?
2] = [1, 2, 3]
a[:: a
array([[ 1, 2, 3],
[ 3, 0, 0],
[ 1, 2, 3],
[ 9, 10, 11]])
Indeksi v numpyju niso nujno samo števila ali rezine. Indeksiramo lahko tudi s tabelo (celih) števil.
= np.random.random(5)
a a
array([0.12857635, 0.44611773, 0.80872688, 0.73088645, 0.35649974])
= np.array([1, 2, 4])
b a[b]
array([0.44611773, 0.80872688, 0.35649974])
= 42
a[b] a
array([ 0.12857635, 42. , 42. , 0.73088645, 42. ])
No, v resnici ni potrebno da so indeksi tabela. Lahko je tudi običajen seznam.
1, 2, 4]] -= 13
a[[ a
array([ 0.12857635, 29. , 29. , 0.73088645, 29. ])
Če je potrebno, se lahko isti indeks pojavi tudi večkrat.
0, 1, 0, 0, 4, 0, 1]] a[[
array([ 0.12857635, 29. , 0.12857635, 0.12857635, 29. ,
0.12857635, 29. ])
Tabele, ki jih dobimo, ko tabelo indeksiramo s tabelo, pa zahtevajo kopiranje podatkov. Drugače ne gre.
Posebej imenitno pa je indeksiranje s tabelo bool
-ov.
Takšna tabela mora biti enako dolga, kot tabela, ki jo indeksiramo.
Izbrala bo tiste elemente (vrstice, karkoli), pri katerih imamo vrednost
True
.
= np.array([5, 3, 10, 1, 12])
a = a >= 5
t t
array([ True, False, True, False, True])
a[t]
array([ 5, 10, 12])
V praksi seveda ne delamo tako, temveč pišemo
>= 5] a[a
array([ 5, 10, 12])
To seveda deluje s poljubno dimenzionalnimi tabelami. Vrnimo se k primeru s točkami. Znali smo izračunati oddaljenost točk od točke (1, 2).
k
array([[ 5, 2],
[ 4, 1],
[-1, 4],
[ 5, 6],
[ 0, 2]])
= np.sqrt(np.sum((k - (1, 2)) ** 2, axis=1))
dist dist
array([4. , 3.16227766, 2.82842712, 5.65685425, 1. ])
Zdaj me zanimajo vse točke, ki so oddaljene manj kot 4.
< 4 dist
array([False, True, True, False, True])
< 4] k[dist
array([[ 4, 1],
[-1, 4],
[ 0, 2]])
Zdaj poznamo osnovno orožarno. Odtod naprej se moramo naučiti dvoje.
numpy
ima ogromno funkcij. Sam ga uporabljam že
dolgo, a stalno odkrivam nove. Avtorje imam celo na sumu, da jih sproti
dodajajo. :) Res pa je, da so bile nekatere, ki jih odkrijem na novo, v
numpy
-ju že od začetka. Očitno bomo numpy uporabljali tem
boljše, čim več funkcij bomo poznali.
Urjenje spretnosti vektorskih operacij. Vektorska operacija je
tisto, kar naredimo v Pythonu brez zanke. Recimo a + b
, če
sta a
in b
tabeli. Ko programiramo z velikimi
podatki in v numpy
-ju, se za vsako ceno izognemo temu, da
bi napisali zanko v Pythonu. Dokler so se zanke v "skrite" v numpyju, se
bodo izvedle v C-ju in bodo hitre.
Prvo in drugo gre seveda z roko v roki. Nekaj izjemno uporabnih funkcij (ki se take morda ne zdijo na prvi pogled) je
nonzero
,
ki vrne indekse elementov, različnih od 0. Praktičnejše uporabna sestra
te funkcije je flatnonzero
.< 4) np.flatnonzero(dist
array([1, 2, 4])
argmin
, argmax
vrneta indekse najmanjših
in največih elementov, argsort
pa indekse urejene po
velikosti elementov. V resnici je argsort
veliko
uporabnejša od sort
. sort
ne uporabim skoraj
nikoli, argsort
uporabljam stalno.hstack
in vstack
sklapljata tabele
horizontalno oz. vertikalno, pri čemer morajo biti dimenzije seveda
skladne.tranpose
transponira tabelo (vrstice v stolpce in
obratno). Enako dosežemo tudi s .T
(kot npr.
a.T
),Vse knjižnice, povezane z uporabo Pythona v znanosti, temeljijo na
numpy
-ju.
Condo so naredili predvsem, da bi bilo lažje nameščati in nadzorovati vse te mnoge knjižnice. Anaconda pa se od miniconde razlikuje po tem, da je skupaj z Anacondo že poberemo stotine knjižnic, tako da jih ob nameščanju ni potrebno downloadati, saj so že na disku.