Numpy

Introduksjon

NumPy (Numerisk Python) er et åpent Python-bibliotek, ofte brukt i vitenskap og ingeniørfag. Det tillater utviklere å jobbe med flerdimensjonale arrays, og har innebygde funksjoner som fungerer svært effektivt på disse strukturene. Det brukes også i sammenheng med maskinlæring (for eksempel scikit-lean, TensorFlow, Keras) og visualisering (for eksempel Matplotlib, Altair, Seaborn). Det er altså svært nyttig å bli kjent med NumPy, og du vil sannsynligvis komme til å bruke det dersom du fortsetter med programmering i Python.

Her kommer en kjapp introduksjon med det viktigste du trenger å vite om NumPy.

Grunnleggende

Hovedobjektene i NumPy er homogene, flerdimensjonale arrays. På samme måte som en liste i python, kan den altså ha flere dimensjoner. Den største forskjellen er at array-objektene må være homogene - de må altså bestå kun av én datatype (for eksempel int, boolean, float…)

import numpy as np 

# den enkleste måten å lage et numpy-array er å definere det fra en liste
tom_array = np.array([])

liste = [1,2,3,4]
ikke_tom_array = np.array(liste)

print(ikke_tom_array,type(ikke_tom_array)) # utskrift: [1 2 3 4] <class 'numpy.ndarray'>

# som sagt, kan en numpy array kun bestå av én type data
feil_data_array = np.array([1,2,3,"hei"])
# numpy vil allikevel prøve å endre all data til felles type
# den vil derfor heller endre alle dataene til type string istedenfor å gi fielmelding
print(feil_data_array) # utskrift: ['1' '2' '3' 'hei']

# når et array opprettes, må det alltid være kun ett objekt
feil_input_array = np.array([1,2,3], [4,5,6]) # KRASJ --> kan ikke ha to lister som argumenter

riktig_array = np.array([[1,2,3],[4,5,6]]) # RIKTIG! En 2D-liste går helt fint

Det kan være nyttig å ha arrays i flere enn 1 dimensjon.

example of a 3d numpy array visualised as a cube
fra Geek for geeks

to_dim_array = np.array(
    [
        [1,2,3],
        [4,5,6],
        [7,8,9],
        [10,11,12]
    ]
)

tre_dim_array = np.array(
    [
        [
            [1,2],
            [3,4]
        ],
        [
            [2,3],
            [4,5]
        ],
        [
            [3,4],
            [5,6]
        ]
    ]
)

# .shape kan brukes for å få oversikt over dimensjoner til et numpy-array
print(to_dim_array.shape) # utskrift: (4, 3)
print(tre_dim_array.shape) # utskrift: (3, 2, 2)

Noen ganger har eller trenger man ikke dataene for å fylle arrayet

null_array = np.zeros((2,2,2))
print(f'Array med 0:\n{null_array}\n') # utskrift: [[[0. 0.]
                                       #             [0. 0.]]
                                       #             [[0. 0.]
                                       #             [0. 0.]]]

en_array = np.ones((4,5))
print(f'Array med 1:\n{en_array}\n') # utskrift: [[1. 1. 1. 1. 1.]
                                     #            [1. 1. 1. 1. 1.]
                                     #            [1. 1. 1. 1. 1.]
                                     #            [1. 1. 1. 1. 1.]]

# det finnes også en innebygd funksjon for pi
pi_array = np.full((3,3),np.pi)
print(f'Array med pi:\n{pi_array}\n') # utskrift: [[3.14159265 3.14159265 3.14159265]
                                      #            [3.14159265 3.14159265 3.14159265]
                                      #            [3.14159265 3.14159265 3.14159265]]

# man kan også opprette arrays som en følge av tall
arrange_array = np.arange(0,10,1) # start, stopp, step (akkurat som i range)
print(f'Array med arange (0,10,1):\n{arrange_array}\n') # utskrift: [0 1 2 3 4 5 6 7 8 9]

linspace_array = np.linspace(0,1,11) # start, stopp, antall elementer (svært nyttig til å plotte en funksjon)
print(f'Array med linspacce (0,1,11):\n{linspace_array}\n') # utskrift: [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]

Du kan også kopiere eksisterende arrays. Merk at en vanlig tildeling ikke oppretter et nytt array i seg selv; det oppretter en ny variabel som peker til et objekt som allerede eksisterer:

ny_pi_array = pi_array              # ingen nye objekter blir opprettet
ny_pi_array[0, 0] = 0           # endre det første elementet i det nye arrayet
print(ny_pi_array is pi_array)      # True: to navn for samme ndarray-objekt, selv om vi endret verdien

kopi_arrange_array = arrange_array.copy()    # et nytt objekt blir opprettet
kopi_arrange_array[0] = 1000             # endre det første elementet i det nye arrayet
print(kopi_arrange_array is arrange_array)   # False: to forskjellige ndarray-objekter

Man kan også endre eksisterende verdier ved å legge til, fjerne og sortere elementer

arange_1 = np.arange(1, 10, 2)                # [1 3 5 7 9]
arange_2 = np.arange(0, 11, 2)                # [0 2 4 6 8 10]

# sammenslåing - flatet ut
total_seq = np.concatenate((arange_1, arange_2)) 
print(f"Sammenslått sekvens:\n{total_seq}\n")             # [1 3 5 7 9 0 2 4 6 8 10]

# dropping values 
total_seq = np.delete(total_seq, -1) # fjerner siste element
print(f"Sekvens med siste element fjernet:\n{total_seq}\n")  # [1 3 5 7 9 0 2 4 6 8]

# sort sequence
sorted_seq = np.sort(total_seq)  # ulike sorteringsfunksjoner er tilgjengelige for ulike dimensjoner
print(f"Sortert sekvens:\n{sorted_seq}\n")                  # [0 1 2 3 4 5 6 7 8 9]

Indeksering og beskjæring

Generelt fungerer indeksering av arrays på samme måte som med lister:

# Indeksering og skjæring av 1D array fungerer akkurat som for lister:
arange_seq = np.arange(0, 10, 1)                        # [0 1 2 3 4 5 6 7 8 9]    
print("Første element:", arange_seq[0], end = '\n\n')    # 0
print("Siste element:", arange_seq[-1], end = '\n\n')    # 9
print("Elementene from 3rd to 5th:",                     # includert begge
      arange_seq[2:5], end = '\n\n')                    # [2 3 4]

# Flerdimensjonale arrays kan ha en indeksering per dimensjon:
two_d_array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("Første rad av 2D array <- [0]:", two_d_array[0], sep = '\n', end = '\n\n')                 # [1 2 3]
print("Første element av førtste rad <-[0][0]:", two_d_array[0, 0], sep = '\n', end = '\n\n')     # 1

# bonus: kun de siste verdiene fra hver rad
print(f"Siste kolonnen av 2D array:\n{two_d_array[..., -1]}")     # [3 6 9]

Man kan også indeksere med en boolsk array - dette kalles ofte for en mask eller maskering:

print(f'Dette er den originale arrayen:\n{two_d_array}\n')
print(f'Dette er en maske for alle partall:\n{two_d_array % 2 == 0}\n')
# [[False  True False]
#  [ True False  True]
#  [False  True False]]

bool_mask = two_d_array % 2 == 0 # definerer en maske med True for alle partall
print(f'Dette er arrayen filtrert med masken:\n{two_d_array[bool_mask]}\n') # [2 4 6 8]

Attributter

Det kan være nyttig å vite egenskapene til et array.

three_d_array = np.array(
    [
        [
            [1, 2, 3, 4],
            [5, 6, 7, 8],
            [9, 10, 11, 12]
        ],
        [
            [13, 14, 15, 16],
            [17, 18, 19, 20],
            [21, 22, 23, 24]
        ]
    ]
    )

three_d_shape = three_d_array.shape
print(".shape: Formen til arrayet -", three_d_shape)        # (2, 3, 4) - denne tuplen betyr 2 martriser, 3 rader og 4 kolonner

three_d_ndim = three_d_array.ndim
print(".ndim: Antall dimensjoner -", three_d_ndim)          # 3

three_d_size = three_d_array.size
print(".size: Antall elementer totalt -", three_d_size)    # 24 (2*3*4)

three_d_dtype = three_d_array.dtype
print(".dtype: Datatypen til elementene -", three_d_dtype)     # int64

three_d_itemsize = three_d_array.itemsize
print(".itemsize: Størrelsen for hvert element i bytes -", three_d_itemsize) # 8 bytes for int64 (8 bits per byte, int64 = 64 bits)

three_d_data = three_d_array.data
print(".data: Minneadressen til arryayet -", three_d_data)           # <memory at 0x115d2a4d0>

Endre form på array

Noen ganger må man endre formen på et array for å kunne gjøre de opperasjonene man ønsker. Ved å bruke .reshape() endrer man formen til arrayet uten å endre elementene. Her er det viktig å passe på at det nye arrayet har like mange lementer som det originale.

# som vi vet er three_d_array.size = 24: 
three_to_two = three_d_array.reshape(6, 4) # endrer formen til et 2D-array med dimensjoner 6x4
print("Omformet 3D:\n", three_to_two, end='\n\n')

# det går også an å transponere et array
print("Transponert omformet 3D:\n", three_to_two.T)

Operasjoner med arrayer

Når man jobber med NumPy-arrayer, er det viktig å forstå hvordan operasjoner skiller seg fra vanlige Python-objekter. Til forskjell fra lister støtter NumPy vektoriserte operasjoner, som betyr at beregninger på en ndarray utføres elementvis uten behov for eksplisitte løkker. Det er generelt tre kategorier av operasjoner:

# grunnleggende algebra
# addisjon
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print("Addisjon:\n", a + b, end='\n\n')     # [[ 6  8]
                                            #  [10 12]]

# subtraction
print("Subtraksjon:\n", a - b, end='\n\n')  # [[-4 -4]
                                            #  [-4 -4]]

# multiplication (element-wise)
print("Multiplikasjon:\n", a * b, end='\n\n')   # [[ 5 12]
                                                #  [21 32]]
# aggregate opperasjoner
print(f"Sum av alle verdier: {three_d_array.sum()}")  # 300
print(f"Minste verdi: {three_d_array.min()}")      # 1
print(f"Største verdoi: {three_d_array.max()}")      # 24

print(f"Gjennomsnittsverdi: {three_d_array.mean()}")                # 12.5
print(f"Median: {np.median(three_d_array)}")          # 12.5 
print(f"Standardavvik til verdiene (hele populasjonen): {three_d_array.std()}")   # 6.922...
print(f"Standardavvik (av utvalg): {three_d_array.std(ddof=1)}")    # 7.07...
# bonus: sortering
usortert = np.array(
    [[6,5,4],
     [3,2,1]]
)

print(f"Original array:\n{usortert}\n")

# axis forteller langs hvilken akse arrayet skal sorteres
# default er -1, altså den siste aksen
usortert.sort(axis=1)
print(f"Array sortert langs den andre aksen (kolonnene)\n{usortert}\n")
# elementvise opperasjoner
print(f"Elementvis divisjon på 10:\n{three_d_array/10}\n") # [[[0.1 0.2 0.3 0.4]...
print(f"Elementvis addisjon på 10:\n{three_d_array+10}\n") # [[[11 12 13 14]...

# parvise opperasjoner
print(f"Parvis multiplikasjon:\n{three_d_array*three_d_array}\n") # [[[  1   4   9  16]...

Man kan også finne matrise/prikkprodukt:

array2 = np.array([
    [1,0],
    [-1,2],
    [2,1]
])

print(f"Prikkprodukt av \n\n{to_dim_array}\n\nog\n\n{array2}\n\ner\n\n{to_dim_array @ array2}")

Avsluttende merknader

Disse notatene bør gi deg en grunnleggende forståelse av mulighetene som finnes i NumPy. Det er et svært viktig verktøy for alle omfattende beregninger, og det brukes innen mange ulike fagfelt. Her har vi bare dekket det grunnleggende om np.ndarray, men det er mye mer å lære om biblioteket. Det kan derfor være lurt å besøke NumPys omfattende og velskrevne Numpy doc nettside.