Tilbake
4.1

4.1 NumPy-arrays: oppretting, vektorisering og 2D

Arrays, elementvise operasjoner, `linspace`/`zeros`, og et kort dykk i 2D-arrays — verktøyet ODE- og differenslikning-oppgavene hviler på.

50 min
10 oppgaver
NumPy-arraysopprettingvektorisering
Din fremgang i kapitlet
0 / 10 oppgaver

Forkunnskaper

Du trenger tre ting fra Del 1:

- Lister, indeksering og slicing fra kap. 1.2 — hele dette kapitlet er en sammenligning mot lister. «Slicing» betyr å plukke ut et utsnitt, som x[1:3].
- Løkker og range fra kap. 1.3, særlig at stoppverdien i range er eksklusiv.
- Funksjoner fra kap. 1.6, fordi vi skal sende en hel tabell inn i en funksjon du selv har skrevet.

Har du aldri programmert før, er Programmering med Python en mykere inngang til løkker og lister enn dette kapitlet.

Kapitlet er delt i fire løkker med tidsanslag. Ta gjerne en pause mellom løkke 2 og 3.

Løkke 1 — fra liste til tabell (~12 min)

En værstasjon på Blindern måler temperaturen hver time. Med Python-lister lagrer du målingene som [2.1, 3.4, 3.0, 5.2, 4.8]. Skal du så trekke fra en kalibreringsfeil på 0,3 grader i alle fem, må du gå gjennom lista med en løkke og bygge en ny liste.

Med NumPy skriver du i stedet maal - 0.3 og er ferdig. Det er hele poenget: en NumPy-tabell er en rad av tall som regnes på under ett. Å regne på en hel tabell i én operasjon, uten løkke, kalles vektorisering.

Tabellen heter array på engelsk. Vi bruker ordet «array» også på norsk, siden det er ordet du ser i koden og i oppgaveteksten.

`import numpy as np`

Standardlinjen som gjør NumPy tilgjengelig. as np gir biblioteket kallenavnet np, slik at du skriver np.zeros(5) og ikke numpy.zeros(5). Alle NumPy-navn i faget skrives med np.-prefiks, og linjen skal alltid stå øverst i kode som bruker arrays — sensor forventer at nødvendige importer er med.

import numpy as np
print(np.zeros(3))

Utskrift:

[0. 0. 0.]

`np.array(liste)`

Lager et array av en ferdig liste med tall. Lista må inneholde tall (eller nøstede lister av tall), og resultatet er en ny tabell med samme verdier i samme rekkefølge. Legg merke til at Python skriver et array uten komma mellom elementene — [2 4 6], ikke [2, 4, 6]. Det er den sikreste måten å kjenne igjen et array i en utskrift.

import numpy as np
tabell = np.array([2, 4, 6])
print(tabell)

Utskrift:

[2 4 6]

✏️Eksempel 1: samme uttrykk, to helt ulike svar

Tre timesmålinger av temperatur er lagret både som liste og som array. Hva skriver liste + liste, array + array, liste * 2 og array * 2 ut?


Nøkkelen er at + og * betyr noe helt annet for en liste enn for et array.

import numpy as np

maal_liste = [2.1, 3.4, 3.0]
maal_array = np.array([2.1, 3.4, 3.0])

print(maal_liste + maal_liste)
print(maal_array + maal_array)
print(maal_liste * 2)
print(maal_array * 2)

Utskrift:

[2.1, 3.4, 3.0, 2.1, 3.4, 3.0]
[4.2 6.8 6. ]
[2.1, 3.4, 3.0, 2.1, 3.4, 3.0]
[4.2 6.8 6. ]

For lista betyr + skjøting: to lister etter hverandre, seks elementer. For arrayet betyr + elementvis addisjon: hvert element legges til seg selv, fortsatt tre elementer. Samme mønster for *: lista gjentas, arrayet skaleres.

Legg også merke til hvordan Python skriver de to: lista med komma, arrayet uten.

Elementvise operasjoner på array

For to arrays med like mange elementer regner +, -, *, / og ** element for element. x + y gir et nytt array der element nummer i er x[i] + y[i]. Er den ene siden et vanlig tall, brukes tallet på hvert element: x * 2 dobler alle, x ** 2 kvadrerer alle. Ingen løkke trengs, og resultatet har samme lengde som det som gikk inn.

import numpy as np
x = np.array([1.0, 2.0, 3.0])
print(x ** 2 + x)

Utskrift:

[ 2.  6. 12.]

📝Oppgave 1

(Midtveisnivå, sjanger A — kodesporing, altså «hva skriver programmet ut?».) Skriv utskriften nøyaktig, med de mellomrommene og hakeparentesene Python bruker.

import numpy as np

a = [5, 10]
b = np.array([5, 10])
print(a + a)
print(b + b)

`.size` og `.shape`

To opplysninger du kan hente ut av et array uten parenteser, fordi de er egenskaper og ikke funksjoner. .size er antall tall i alt, som et helt tall. .shape er formen, gitt som en tuppel: (5,) for en tabell med fem tall på én rad, (3, 4) for tre rader og fire kolonner. Kommaet i (5,) er Pythons måte å skrive en tuppel med bare ett element.

import numpy as np
t = np.array([0.0, 0.5, 1.0, 1.5])
print(t.size, t.shape)

Utskrift:

4 (4,)

Løkke 2 — lage tabellen før du fyller den (~14 min)

I nesten alle senere oppgaver kjenner du antall verdier før du kjenner verdiene. Skal du simulere en bestand i 20 tidssteg, vet du at du trenger plass til startverdien pluss 20 nye verdier. Da lager du en tabell full av nuller først, og fyller den i en løkke.

Det er derfor np.zeros og np.linspace er de to viktigste NumPy-funksjonene i faget. Den første lager plass, den andre lager et jevnt punktgitter.

Her ligger også fagets vanligste feil, og den er verdt å si i klartekst før du ser koden: et intervall delt i N biter har N+1 delepunkter. Deler du strekningen fra 0 til 6 i 6 biter, er punktene 0, 1, 2, 3, 4, 5, 6 — det er sju tall, ikke seks.

`np.zeros(n)`

Lager et array med n nuller, klart til å fylles. Nullene er flyttall, så de skrives 0. med punktum. Dette er standard første linje i alle simuleringer i boka: du lager plassen, setter startverdien, og skriver de øvrige verdiene inn i en løkke. Trenger du N+1 punkter, skriver du np.zeros(N+1).

import numpy as np
print(np.zeros(4))

Utskrift:

[0. 0. 0. 0.]

`np.zeros_like(t)`

Lager et nullarray med samme lengde som t. Nyttig når du allerede har et tidsarray og vil ha et løsningsarray som passer til det — da kan du ikke bomme på lengden, fordi du aldri skriver et tall. Det er den sikreste måten å unngå feil #1 (av-én på antall punkter).

import numpy as np
t = np.linspace(0, 1, 5)
print(np.zeros_like(t))

Utskrift:

[0. 0. 0. 0. 0.]

`np.ones(n)`

Som np.zeros, men fylt med ettere. Brukes når startverdien i en modell er 1, eller når du trenger et array du skal multiplisere inn i noe annet. Ettallene er flyttall og skrives 1..

import numpy as np
print(np.ones(3) * 5)

Utskrift:

[5. 5. 5.]

`np.linspace(a, b, N+1)`

Lager N+1 jevnt fordelte punkter fra a til b, med begge endepunktene inkludert. Tredje argument er altså antall punkter, ikke antall intervaller. Avstanden mellom nabopunkter blir (ba)/N(b-a)/N, og du kan alltid hente den ut som t[1] - t[0].

Dette er den mest presise setningen i hele kapitlet, og den du må kunne i søvne: N intervaller gir N+1 punkter. Vil du ha 101 punkter i intervallet fra 1 til 10, skriver du np.linspace(1, 10, 101).

import numpy as np
t = np.linspace(0, 6, 7)
print(t)
print(t.size, t[1] - t[0])

Utskrift:

[0. 1. 2. 3. 4. 5. 6.]
7 1.0

✏️Eksempel 2: punkttelling og steglengde

En måleserie skal dekke tidsrommet fra t=0t=0 til t=6t=6 timer med 12 like intervaller. Lag tidsarrayet, og vis at antall punkter og steglengden stemmer.


Tolv intervaller betyr tretten punkter, og steglengden blir 6/12=0.56/12 = 0.5.

import numpy as np

N = 12
t = np.linspace(0, 6, N+1)
print(t)
print('Antall punkter:', t.size)
print('Antall intervaller:', t.size - 1)
print('Steglengde:', t[1] - t[0])

Utskrift:

[0.  0.5 1.  1.5 2.  2.5 3.  3.5 4.  4.5 5.  5.5 6. ]
Antall punkter: 13
Antall intervaller: 12
Steglengde: 0.5

Sensorpoenget: hadde du skrevet np.linspace(0, 6, N), ville steglengden blitt 6/110.5456/11 \approx 0.545 og siste punkt fortsatt 6 — tabellen ser riktig ut, men alle mellomverdiene er feil. Feilen er umulig å se på øyemål, og det er nettopp derfor den er så vanlig.

📝Oppgave 2

(Midtveisnivå, sjanger A — kodesporing.) Hva skriver programmet ut?

import numpy as np

x = np.linspace(2, 5, 4)
print(x)
print(x.size, x[-1])

`np.arange(start, stop, step)`

Lager et array fra start og oppover i steg av step, og stopper før stop — akkurat som range, men med flyttall tillatt. np.arange(0, 2, 0.5) gir derfor [0. 0.5 1. 1.5]: 2 er ikke med.

Bruk np.linspace når du vet hvor mange punkter du vil ha, og np.arange når du vet steglengden. På eksamen er linspace nesten alltid det riktige valget, fordi oppgaven oppgir antall steg.

import numpy as np
print(np.arange(0, 2, 0.5))

Utskrift:

[0.  0.5 1.  1.5]

📝Oppgave 3

(Midtveisnivå, sjanger A — kodesporing.) De to første linjene ser like ut, men gir ulike tabeller. Hva skriver programmet ut?

import numpy as np

a = np.linspace(0, 3, 4)
b = np.arange(0, 3, 1)
print(a)
print(b)
print(a.size - b.size)

Løkke 3 — funksjoner som spiser hele tabellen (~13 min)

— naturlig pausepunkt —

Til nå har vi regnet med +, -, * og **. Men de fleste vitenskapelige formler inneholder også kvadratrot, eksponentialfunksjon, sinus eller logaritme. NumPy har egne versjoner av disse som virker på et helt array om gangen.

En slik funksjon kalles en ufunksjon (kort for «universell funksjon»): den tar et array inn og gir et array av samme lengde ut, med funksjonen brukt på hvert element. np.sqrt(x) er ufunksjonen som svarer til math.sqrt, men math.sqrt klarer bare ett tall om gangen.

Det beste med dette er at det også gjelder funksjoner du har skrevet, så lenge de bare bruker aritmetikk og ufunksjoner.

Ufunksjon (`np.sqrt`, `np.exp`, `np.log`, `np.sin`)

En NumPy-funksjon som virker elementvis på et helt array og returnerer et nytt array av samme lengde. np.exp, np.log (naturlig logaritme), np.sin, np.cos og np.sqrt er de du trenger i faget. De tilsvarer funksjonene i math, men math-versjonene tar bare ett tall: math.sqrt med et array inn gir en feilmelding, mens np.sqrt gir hele tabellen med røtter.

import numpy as np
x = np.array([0.0, 0.25, 1.0])
print(np.sqrt(x))

Utskrift:

[0.  0.5 1. ]

✏️Eksempel 3: din egen funksjon brukt på en hel tabell

Høyden til en ball kastet rett opp er h(t)=4t0,5t2h(t) = 4t - 0{,}5t^2 målt i meter, med tt i sekunder. Skriv funksjonen én gang, og bruk den både på ett tidspunkt og på et helt tidsarray.


Funksjonen skrives helt vanlig, uten et eneste NumPy-navn. Den virker likevel på hele tabellen, fordi *, - og ** er elementvise for arrays.

import numpy as np

def hoyde(t):
    return 4.0*t - 0.5*t**2

t = np.linspace(0, 4, 5)
print(hoyde(2.0))
print(t)
print(hoyde(t))

Utskrift:

6.0
[0. 1. 2. 3. 4.]
[0.  3.5 6.  7.5 8. ]

Første kall gir ett tall, fordi argumentet er ett tall. Andre kall gir fem tall, fordi argumentet er en tabell med fem tall. Funksjonen er den samme — det er argumentet som bestemmer om du får et tall eller en tabell tilbake.

Dette er hele grunnlaget for plotting i kap. 4.3: du lager et tett tidsarray, sender det gjennom funksjonen din, og plotter de to tabellene mot hverandre.

📝Oppgave 4

(Midtveisnivå, sjanger A — kodesporing.) Hva skriver programmet ut?

import numpy as np

def g(x):
    return 2*x + 1

v = np.linspace(0, 1, 3)
u = g(v)
print(u)
print(u[1] + v[2])

Løkke 4 — plukke ut deler av tabellen (~11 min)

Indeksering og slicing virker som for lister: a[0] er første element, a[-1] er siste, og a[1:] er alt fra og med element 1. Forskjellen er at et utsnitt av et array er et array og ikke en liste, så du kan regne elementvis med det.

Det gir et triks du får bruk for i kap. 6.2: a[1:] - a[:-1] er tabellen over endringer fra ett punkt til det neste, regnet ut uten løkke.

Indeksering og slicing av array
a[i] gir ett tall (element nummer i, med 0 som første). a[i:j] gir et array med elementene fra og med i til men ikke med j — sluttindeksen er eksklusiv, som for lister. a[1:] er alt unntatt det første, a[:-1] er alt unntatt det siste, og de to har derfor like mange elementer.

Én viktig forskjell fra lister: et utsnitt av et array er et utsyn inn i det samme minnet, ikke en kopi. Endrer du utsnittet, endrer du originalen.

import numpy as np
a = np.array([2.0, 5.0, 9.0, 14.0])
print(a[1:] - a[:-1])

Utskrift:

[3. 4. 5.]
✏️Eksempel 4: sammenligne to måleserier uten løkke

En sensor logger fem heltallsverdier, og fasitverdiene er kjent fra en kalibrering. Hvor mange av de fem stemmer, og hvilke?


En sammenligning mellom to arrays gir et array av True/False, ett per element. Siden True teller som 1 i en sum, kan du telle treffene direkte med den innebygde sum.

import numpy as np

maalt = np.array([3, 5, 5, 2, 7])
fasit = np.array([3, 4, 5, 2, 8])

print(maalt == fasit)
print(sum(maalt == fasit))

Utskrift:

[ True False  True  True False]
3

Legg merke til at maalt == fasit ikke gir én sann/falsk-verdi for hele tabellen, men fem. Det er dette som gjør sum(x == y) til et kort og lesbart uttrykk for «antall like elementer» — nettopp den korte, idiomatiske stilen sensor belønner.

📝Oppgave 5

(Midtveisnivå, sjanger A — kodesporing.) Hva skriver programmet ut?

import numpy as np

d = np.array([10, 12, 15, 21])
e = d[1:] - d[:-1]
print(e)
print(e.size, sum(e == 3))

Tabeller med to indekser — bør kjenne til

Resten av denne løkka er nivå 3-stoff (kjenne til): 2D-arrays er bare i 2 av de 8 nyere settene, altså 25 %, og da som en mindre deloppgave. Har du kort tid, kan du komme hit tilbake etter at Del 5 og Del 7 sitter.

Du trenger likevel formen u[:, 0] senere, når et ODE-system i kap. 7.3 gir deg løsningen som en tabell med én kolonne per komponent. Det er den ene grunnen til å lese avsnittet nå.

`np.zeros((n, m))` og `a[i, j]`

Med en tuppel som argument lager np.zeros en tabell med flere rader: np.zeros((3, 4)) gir 3 rader og 4 kolonner, altså 12 nuller. Du henter ett element med to indekser, a[i, j] (rad i, kolonne j), en hel kolonne med a[:, j] og en hel rad med a[i, :]. Kolonnen og raden er selv vanlige arrays med én rad. .shape gir (3, 4), og .size gir 12.

Legg merke til de doble parentesene i np.zeros((3, 4)): de ytre hører til funksjonskallet, de indre lager tuppelen.

import numpy as np
A = np.zeros((2, 3))
A[1, 2] = 7.0
print(A[:, 2])

Utskrift:

[0. 7.]

✏️Eksempel 5: bygge en tabell rad for rad

Tre målestasjoner rapporterer nedbør fire ganger i døgnet. Bygg tabellen med np.zeros, fyll den med to nøstede løkker der verdien er stasjonsnummeret pluss en tidel per måletidspunkt, og hent ut kolonne 2 og rad 1.


Den ytre løkka går over rader (stasjoner), den indre over kolonner (måletidspunkt). Dette er den vanlige måten et 2D-array fylles.

import numpy as np

A = np.zeros((3, 4))
for i in range(3):
    for j in range(4):
        A[i, j] = i + 0.1*j

print(A)
print(A[:, 2])
print(A[1, :])
print(A.shape, A.size)

Utskrift:

[[0.  0.1 0.2 0.3]
 [1.  1.1 1.2 1.3]
 [2.  2.1 2.2 2.3]]
[0.2 1.2 2.2]
[1.  1.1 1.2 1.3]
(3, 4) 12

Merk hvordan et 2D-array skrives ut: én rad per linje, med ytterligere hakeparenteser rundt hele. A[:, 2] gir tre tall (én per stasjon), A[1, :] gir fire tall (én per tidspunkt). Blander du de to aksene, får du et svar med feil antall elementer — og det er lett å oppdage nettopp fordi antallet ikke stemmer.

📝Oppgave 6
Eksamensnivå, sjanger A

Hva skriver programmet ut?

import numpy as np

B = np.zeros((2, 3))
for i in range(2):
    for j in range(3):
        B[i, j] = 2*i + j
print(B)
print(B[:, 0])
print(B[1, 1:])

📝Oppgave 7
Eksamensnivå, sjanger A

Programmet regner ut den relative endringen mellom nabomålinger. Hva skriver det ut?

import numpy as np

p = np.array([100.0, 110.0, 99.0])
r = (p[1:] - p[:-1]) / p[:-1]
print(r)
print(r.size)
print(sum(r > 0))

API- og konstruksjonsliste

Begrepsbank

Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.

Array

En tabell av tall som Python regner på under ett. Til forskjell fra en liste har alle elementene samme talltype, lengden er fast, og +, -, *, / og ** virker elementvis. Et array skrives ut uten komma mellom elementene, som [2 4 6].

Vektorisering

Å regne på en hel tabell i én operasjon, uten løkke. y = np.log(x) er den vektoriserte versjonen av å løpe gjennom x med en løkke og kalle math.log på hvert element. På eksamen er det en egen sjanger (M) å skrive samme tabell både med og uten løkke — se kap. 4.2.

Antall punkter mot antall intervaller

Deler du et intervall i NN like biter, får du N+1N+1 delepunkter: startpunktet, N1N-1 punkter inni, og sluttpunktet. Derfor er np.linspace(a, b, N+1) riktig når oppgaven oppgir NN steg, og derfor gir range(N) nøyaktig de NN oppdateringene som fyller punkt 1 til NN. Dette er feil #1 i feilkatalogen og fagets hyppigste feil.

`np.asarray(x)`

Gjør x om til et array hvis det ikke alt er et, og returnerer det uendret hvis det er. Brukes i starten av en funksjon som skal takle både liste og array som argument, slik at resten av koden trygt kan regne elementvis.

Boolsk array

Resultatet av en elementvis sammenligning, for eksempel x > 0 eller x == y: et array med True eller False for hvert element, like langt som x. Det er ikke ett sant/falsk-svar for hele tabellen. Siden True teller som 1 i en sum, gir sum(x > 0) antall positive elementer.

Nøstet liste til 2D-array
np.array([[1.0, 2.0], [3.0, 4.0]]) gir et array med 2 rader og 2 kolonner. Hver indre liste blir en rad, og alle de indre listene må være like lange. Elementet i rad 1, kolonne 0 hentes med M[1, 0] og er 3.0.
`a[i, j]` mot `a[i][j]`

Begge henter samme element i et 2D-array, men a[i, j] er formen boka og sensor bruker: den slår opp direkte med to indekser. a[i][j] henter først hele rad i som et nytt array og indekserer deretter i den. For nøstede lister finnes bare den siste formen, siden en liste ikke godtar to indekser i én klamme.

Utsyn mot kopi
b = a[1:] gir et utsyn inn i det samme minnet som a, så en endring i b vises også i a. b = a.copy() gir en uavhengig kopi. Dette er feil #23 (aliasing) og skiller arrays fra lister, der a[1:] alltid er en ny liste.
Array ganget med et tall
x * 3 multipliserer hvert element med 3 og gir et array av samme lengde. Med en liste betyr liste * 3 at lista gjentas tre ganger etter hverandre, slik at lengden tredobles. Det er samme forskjell som mellom array-addisjon og liste-skjøting (feil #5).
Radvis oppbygging av et 2D-array

Mønsteret er: lag plassen med A = np.zeros((n, m)), og fyll med to nøstede løkker der den ytre går over rader (for i in range(n)) og den indre over kolonner (for j in range(m)). Tilordningen er A[i, j] = .... Utskriften får én rad per linje.

`math` mot `np` på et array

Funksjonene i math tar ett tall: math.sqrt(4.0) er greit, men math.sqrt med et array inn gir en TypeError. NumPy-versjonene tar hele tabellen, så np.sqrt gir et array med røtter. Regelen på eksamen: skal koden virke på et array, skal den bruke np.-funksjonene.

Steglengden i et punktgitter

Med t = np.linspace(a, b, N+1) er steglengden Δt=(ba)/N\Delta t = (b-a)/N, og du kan alltid hente den ut av tabellen selv med t[1] - t[0]. Den siste formen er den tryggeste i en simulering, fordi den er riktig selv om du senere endrer hvordan t lages.

Repetisjonsoppgaver
Din fremgang
0 / 3 oppgaver

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.

Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.