NumPy

Kategória: Python.
Alkategória: Adatkezelés Pythonban.

Bevezető

A NumPy egy tömbökkel foglalkozó Python könyvtár. Első ránézésre felesleges, pl. a listák is felfoghatóak tömbként, de legalább 3 okát látom annak, hogy érdemes ezzel foglalkozni:

  • A tömb műveletek sokkal hatékonyabbak NumPy-ban mint a beépített listában, ami nagyobb méretű adatok esetén fontos lehet.
  • Sokkal több művelet van megvalósítva.
  • A később bemutatandó Pandas adat keret alapját képezi.

Disztribúció függő, hogy fel van-e alapból telepítve, vagy nekünk kell. Ha nincs, akkor a szokásos módon telepíthetjük.

pip install numpy

Konvencióként kialakult, hogy a numpy-t np rövidítéssel importáljuk:

import numpy as np

A verziót az alábbi módon tudjuk kiírni:

print(np.__version__) # 1.20.3

A legfontosabb művelet a tömb létrehozása:

arr = np.array([3, 2, 7, 5, 4])
print(arr) # [3 2 7 5 4]

A Numpy tömb típusa numpy.ndarray:

print(type(arr)) # <class 'numpy.ndarray'>

A Pythonban megszokott hagyományos módon tudunk végiglépkedni a tömb elemein:

arr = np.array([3, 2, 7, 5, 4])
for elem in arr:
    print(elem)

Az arange() a hagyományos Python range()-hez hasonlít, melynek segítségével Numpy tömböt tudunk generálni.

np_range = np.arange(2, 10, 2)
print(np_range) # [2 4 6 8]

Több dimenziós tömbök

A tömb tetszőleges dimenziójú lehet. A 0 dimenziós a skalár, az 1 dimenziós a "hagyományos" tömb, a 2 dimenziós a mátrix stb. Az ndim függvénnyel lehet lekérdezni a dimenziószámot:

zero_dim = np.array(5)
print(zero_dim) # 5
print(np.ndim(zero_dim)) # 0
 
one_dim = np.array([1, 2, 3, 4])
print(one_dim) # [1 2 3 4]
print(np.ndim(one_dim)) # 1
 
two_dim = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
print(two_dim)
# [[1 2 3 4]
#  [5 6 7 8]]
print(np.ndim(two_dim)) # 2

A reshape() függvénnyel lehet átalakítani:

original = np.array([1, 2, 3, 4, 5, 6, 7, 8])
reshaped = original.reshape(4, 2)
print(reshaped)
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]

A több dimenziós tömbök "kilapítása" egy dimenzióssá a flatten() függvénnyel történik:

two_dim = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
one_dim = two_dim.flatten()
print(one_dim) # [1 2 3 4 5 6 7 8]

Indexelés, szeletelés

Az indexelés 0-tól történik. Például:

arr = np.array([3, 2, 7, 5, 4])
print(arr[2]) # 7

Ily módon értéket is adhatunk, pl.:

arr[1] = 4
print(arr) # [3 4 7 5 4]

Egy példa több dimenziós esetre:

two_dim = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
print(two_dim[1, 2]) # 7

Az indexelésnél szakaszokat is megadhatunk, pl.:

print(two_dim[0:2, 1:3])
# [[2 3]
#  [6 7]]

A szokásos szakasz megadási módok (eleje vagy vége lehagyása, lépték, negatív index) itt is működik.

Típusok, átalakítások

A tömnek van egy attribútuma, a dtype, ami megmondja, hogy milyen típusú elemek vannak a tömbben. Pl.

arr = np.array([3, 2, 7, 5, 4])
print(arr.dtype) # int32

A Python alaptípusokon felül vannak még egyéb típusok is a Numpy-ban, pl. 64 bites egész. A létrehozáskor paraméterként adhatjuk meg a dtype-ot:

arr = np.array([3, 2, 7, 5, 4], dtype='int64')
print(arr.dtype) # int64

Átalakítani az astype()-pal tudunk:

str_arr = np.array(['3', '2', '5'])
num_arr = str_arr.astype(int)
print(num_arr) # [3 2 5]
print(num_arr.dtype) # int32

Itt megadhatjuk az alap Python típusokat is, de string formában a Numpy típusokat is.

Dátumkezelés

A dátumkezelés minden programozási rendszer neuralgikus pontja, és ez nincs másképp a Numpy-ban sem. A Numpy definiál egy datetime64 típust a dátum megadásához, egy timedelta64() függvényt pedig a módosításhoz. Pl. egy skalár dátum létrehozása, majd két nap hozzáadás:

my_date = np.datetime64('2021-05-31')
print(my_date) # 2021-05-31
my_date_plus_two_days = my_date + np.timedelta64(2, 'D')
print(my_date_plus_two_days) # 2021-06-02

Egy dátum tömb megadása:

date_arr = np.array(['2021-05-31', '2021-06-02', '2021-06-05'], dtype='datetime64')
print(date_arr) # ['2021-05-31' '2021-06-02' '2021-06-05']

Az arange() itt is működik:

date_range = np.arange('2021-05-30', '2021-06-03', dtype='datetime64')
print(date_range) # ['2021-05-30' '2021-05-31' '2021-06-01' '2021-06-02']

A dátumkezelés a Numpy-ban egy elég nagy terület; itt épp, hogy csak karcoltuk a felszínét. Ezzel kapcsolatos specifikációs oldalak:

Másolat és nézet

Egy olyan témához érkeztünk, ami a gyakorlatban összetettebb esetekben elég sok fejfájást tud okozni: a másolat (copy) és nézet (view) témája. Egy művelet eredménye lehet az eredeti tömb másolata, vagy annak egy nézete. Az előbbi esetben a további módosítás a másolaton nincs hatással az eredetire, míg ez utóbbi esetben igen.

Példa a másolatra:

arr = np.array([3, 2, 5])
arr_copy = arr.copy()
arr_copy[1] = 4
print(arr) # [3 2 5]
print(arr_copy) # [3 4 5]

Ld. a kódban a copy() hívást. A másolat megváltoztatása nincs kihatással az eredetire.

Példa a nézetre:

arr = np.array([3, 2, 5])
arr_view = arr.view()
arr_view[1] = 4
print(arr) # [3 4 5]
print(arr_view) # [3 4 5]

Minden ugyanaz, mint a másolatnál, az eltérés csak a view() (copy() helyett), itt viszont a nézeten történő módosítás megjelenik az eredetin is, mivel ugyanarra a memóriacímre mutt mindkettő.

A gyakorlatban ez nem ennyire plasztikusan jelentkezik, mint itt, ami jobb esetben nagyon sok bosszantó figyelmeztetéshez, rosszabb esetben nehezen felderíthető hibákhoz vezet.

Összekapcsolás

Kettő vagy több tömb összekapcsolása számos módon lehetséges. A számos lehetőség közül lássunk párat!

Tömbök összefűzése

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
arr3 = np.array([7, 8, 9])
print(np.concatenate((arr1, arr2, arr3)))
# [1 2 3 4 5 6 7 8 9]

Egy dimenzióban ezzel ekvivalens:

print(np.hstack((arr1, arr2, arr3)))
# [1 2 3 4 5 6 7 8 9]

Tömbök egymás alá helyezése

print(np.vstack((arr1, arr2, arr3)))
# [[1 2 3]
#  [4 5 6]
#  [7 8 9]]

Az előző transzponáltja

print(np.dstack((arr1, arr2, arr3))[0])
# [[1 4 7]
#  [2 5 8]
#  [3 6 9]]

Mátrixok egymás alá helyezése

arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])
 
print(np.concatenate((arr1, arr2)))
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]

Ezzel ekvivalens a vstack():

print(np.vstack((arr1, arr2)))
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]

Mátrixok egymás mellé helyezése

print(np.concatenate((arr1, arr2), axis=1))
# [[1 2 5 6]
#  [3 4 7 8]]

(Az axis=0 az előzővel ekvivalens. Az axis=None először "kilapítaná".)

Ezzel ekvivalens:

print(np.hstack((arr1, arr2)))
# [[1 2 5 6]
#  [3 4 7 8]]

Mátrixok egymás fölé helyezése

print(np.dstack((arr1, arr2)))
# [[[1 5]
#   [2 6]]
#
#  [[3 7]
#   [4 8]]]

Szétbontás

Az összekapcsolás inverz művelete a szétbontás, amit az array_split() függvény segítségével tudunk végrehajtani.

arr = np.array([1, 2, 3, 4, 5, 6])
arr_split = np.array_split(arr, 3)
print(arr_split) # [array([1, 2]), array([3, 4]), array([5, 6])]
print(arr_split[1]) # [3 4]

Paraméterként a szétbontandó tömböt kell megadni és azt, hogy hány részre ossza. Ar eredmény tömbök tömbje lesz.

2 dimenziós példa:

arr2d = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]])
arr2d_split = np.array_split(arr2d, 3)
print(arr2d_split)
# [array([[1, 2],
#        [3, 4]]), array([[5, 6],
#        [7, 8]]), array([[ 9, 10],
#        [11, 12]])]
print(arr2d_split[1])
# [[5 6]
#  [7 8]]

Függőleges szétbontás:

arr2d_split_row = np.array_split(arr2d, 2, axis=1)
print(arr2d_split_row[1])
# [[ 2]
#  [ 4]
#  [ 6]
#  [ 8]
#  [10]
#  [12]]

Keresés és szűrés

Az adatstruktúrák egyik alapvető művelete a szűrés. A Numpy erre több lehetőséget biztosít.

Az np.where() azoknak az elemeknek az indexét adja vissza, amire a feltétel igaz. Az így kapott indexeket a szögletes zárójeles jelöléssel le is tudjuk kérni:

arr = np.array([4, 2, 7, 5, 3])
indices = np.where(arr > 3)
print(indices) # (array([0, 2, 3], dtype=int64),)
print(arr[indices]) # [4 7 5]

Ehhez hasonló művelet a szűrőfeltétel létrehozása, melyhez nem kell külön függvény: az eredmény egy logikai értékeket tartalmazó tömb. Itt is használhatjuk a kapcsos zárójelet:

filter_criteria = arr > 3
print(filter_criteria) # [ True False  True  True False]
print(arr[filter_criteria]) # [4 7 5]

Gyakran nem is hozunk létre szűrőfeltételt, hanem rögtön megadjuk. Itt többféle logikai műveletet is végrehajthatunk:

print(arr[arr > 3]) # [4 7 5]
print(arr[(arr > 3) & (arr % 2 == 1)]) # [7 5]
print(arr[(arr > 3) | (arr % 2 == 1)]) # [4 7 5 3]
print(arr[~(arr > 3)]) # [2 3]

Érdemes megfigyelni az egyszeres jelölést: & ill. |: ez azt jelenti, hogy a tömbön egyesével hajtja végre a műveletet. A (arr > 3) & (arr % 2 == 1) jelentése:

  • arr > 3: elkészít egy olyan tömböt, melynek elemei logikai értékek, és akkor igaz, ha az adott elem nagyobb háromnál. Ez tehát egy 5 elemű, logikai értékekből álló tömb lesz.
  • arr % 2 == 1: az eredmény hasonlóan egy 5 elemű logikai értékekből álló tömb lesz, amelyek azt jelzik, hogy az adott elem páratlan-e (igaz) vagy páros (hamis).
  • &: a fenti két tömb elemein hajtjuk végre a logikai és műveletet. Tehát az eredmény itt is egy 5 elemű logikai tömb lesz, a megfelelő indexű elemeken végrehajtva a logikai és műveletet. A fenti példában: [igaz, hamis, igaz, igaz, hamis] & [hamis, hamis, igaz, igaz, igaz] = [hamis, hamis, igaz, igaz, hamis].
  • []: a logikai tömbbel tudunk indexelni, így végül az eredmény a 7 és az 5 lesz.

A tagadás a hullám jellel (~) történik.

A Numpy tartalmaz számos függvényt, ami hasonlóan logikai tömböt ad vissza. Pl. ha vannak NaN (not a number = nem szám) elemek, akkor az np.isnan() hívással tudjuk kiszűrni, pl:

arr = np.array([4, 2, np.nan, 5, np.nan]) # [4. 2. 5.]
print(~np.isnan(arr))

(A tizedespontok amiatt kerültek oda, mert a NaN lebegőpontként van ábrázolva, így a tömb elemei lebegőpontosnak minősülnek).

A fenti és a lenti módszer között a fő eltérés:

  • A fenti a kérdéses indexeket kérdezi le, és index alapján tudunk címezni. Olyan műveletekre lehet ez hasznos, hogy pl. hányas indexű az eredeti tömbben a második olyan elem, amely a feltételnek eleget tesz.
  • A lenti egy logikai tömböt ad vissza, amely azt jelzi, hogy mely elemekre igaz és melyekre hamis a feltétel. Ez a módszer bonyolultabb logikai feltételek megadásánál lehet hasznos.

Rendezés

A tömbön végrehajtott sort() függvénnyel tudunk rendezni. A rendezés helyben történik:

arr = np.array([4, 2, 7, 5, 3])
arr.sort()
print(arr) # [2 3 4 5 7]

Műveletek

A fent bemutatott logikai műveletekhez hasonló logikájú aritmetikai műveleteket is végre tudunk hajtani:

arr1 = np.array([4, 2, 6, 5, 3])
arr2 = np.array([2, 3, 3, 1, 6])
print(arr1 + arr2)     # [6 5 9 6 9]
print(2 * arr1 - arr2) # [6 1 9 9 0]

Univerzális függvények

Az univerzális függvények a műveleteknél bemutatott logikát terjesztik ki: a bemenetük egy vagy több Numpy tömb, és a kimenetük is az.

Lássunk egy példát! Az np.add() függvény egy univerzális függvény, amelynek két Numpy tömb a bemenete, a kimenete pedig egy olyan tömb, amely elemenként összeadja az értékeket:

arr1 = np.array([4, 2, 6, 5, 3])
arr2 = np.array([2, 3, 3, 1, 6])
print(np.add(arr1, arr2)) # [6 5 9 6 9]

Ez tehát pont ugyanazt csinálja, mint a fent bemutatott arr1 + arr2.

A Numpy tartalmaz nem univerzális függvényeket is. Pl. a sum() nem univerzális, az az összes elemet összeadja:

print(np.sum([arr1, arr2])) # 35

Azt, hogy egy függvény univerzális-e, a típusából lehet megtudni:

print(type(np.add)) # <class 'numpy.ufunc'>
print(type(np.sum)) # <class 'function'>

A leggyakoribb aritmetikai műveletek meg vannak valósítva univerzális függvényként:

print(np.subtract(np.multiply(2, arr1), arr2)) # [6 1 9 9 0]

Egy példa trigonometrikus függvényre és kerekítésre (némileg egyszerűsítve az eredményt):

print(np.around(np.sin(np.pi * np.arange(13) / 12), 3)) # [0.0 0.259 0.5 0.707 0.866 0.966 1.0 0.966 0.866 0.707 0.5 0.259 0.0]

A szokásos műveletek (aritmetika, trigonometria, exponenciális stb.) meg vannak valósítva. E leírásnak nem célja ezeknek a részletes ismertetése; a lehetőségekről a specifikációban tájékozódhatunk (https://numpy.org/doc/stable/reference/ufuncs.html).

Mi magunk is írhatunk univerzális függvényt:

def my_operation(x, y):
    return 2 * x - y
 
my_ufunc_operation = np.frompyfunc(my_operation, 2, 1)
print(my_ufunc_operation(arr1, arr2)) # [6 1 9 9 0]

Véletlen szám generálás

Áttekintés

A véletlen szám generálásnál az esetek többségében hallgatólagos feltesszük az egyenletességet: alapból az eredmény egy 0 és 1 közötti szám (0 elvileg lehet, 1 elvileg se), a gyakorlatban pedig gyakrabban használunk egész számokat adott intervallumon belül, ahol mindegyik érték valószínűsége egyforma. Viszont nem feltétlenül kell, hogy ez így legyen! Nagyon okféle statisztikai eloszlás van, és a valóságban az egyenletesség a legritkább esetben fordul elő. Pl. ha visszük az emberek testmagasságát, akkor nem igaz az, hogy a legalacsonyabb és a legmagasabb ember testmérete között egyenletes lenne az eloszlás: a szélsőségekhez közel nyilván sokkal kevesebben vannak, mint az átlaghoz közel.

A véletlen szám generálásnak tehát nagyobbak a mélységei, mint amire felületesen gondolunk. Ezen kívül megkülönböztetünk valódi és álvéletlen számot. Az álvéletlen számok az értékesebbek, mivel reprodukálhatóak. A számítógép alapból nem reprodukálható álvéletlen számot generál, viszont valami külső információ (pl. a pillanatnyi rendszeróra, az egérpozíció pillanatnyi helye vagy megváltozása stb.) alapján viszont valódi véletlen számot generálhatunk.

A Numpy véletlen szám generátorának egész kiterjedtek a lehetőségei. A reprodukálhatóság érdekében először állítsunk be egy ún. magot (seed):

np.random.seed(1000)

Egyenletes eloszlás

Kezdjük a "klasszikus" egyenletes lehetőségekkel:

print(np.random.rand())  # lebegőpontos véletlen szám a [0...1) intervallumban: 0.6535895854646095
print(np.random.rand(5)) # véletlen számokból álló 5 elemű lebegőpontos tömb: [0.11500694 0.95028286 0.4821914  0.87247454 0.21233268]
print(np.random.randint(10)) # egész véletlen a [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] számok közül: 8
print(np.random.randint(low = 1, high = 7, size = 10)) # 10 dobókocka dobás eredményei: [2 5 4 5 5 6 3 3 2 5]
print(np.random.randint(low = 1, high = 7, size = [2, 3])) # véletlen egészek mátrixa
# [[5 3 2]
#  [1 3 5]]

A számok összekeverése

A számok összekeverése is véletlenítés. A random.shuffle() helyben kever, míg a random.permutation() meghagyja az eredeti tömböt, és

arr = np.array([1, 2, 3, 4, 5, 6])
np.random.shuffle(arr) # a számok összekeverése helyben
print(arr) # [4 6 2 1 3 5]
 
arr = np.array([1, 2, 3, 4, 5, 6])
arr_perm = np.random.permutation(arr) # a számok összekeverése az eredeti tömb meghagyásával
print(arr) # [1 2 3 4 5 6]
print(arr_perm) # [6 2 1 4 5 3]

Normális eloszlás

print(np.random.normal(loc=0, scale=1, size=5)) # standard normális: [-0.33483545 -0.0994817   0.4071921   0.91938754  0.31211801]
print(np.random.normal(loc=100, scale=15, size=5)) # IQ: [122.99741598  91.74739202  94.25278884  87.65588555 124.00125052]

Az első példa standard normális eloszlású véletlen számokat hozott létre, míg a második a 100-as várható értékű 15-ös szórású IQ-ra jellemző eloszlást.

Binomiális eloszlás

print(np.random.binomial(n=10, p=0.5, size=5)) # fej vagy írás 10-szer: [7 4 5 2 6]

A fenti példát úgy lehet elképzelni, hogy veszünk egy szabályos érmét, azt feldobjuk 10-szer, és megszámoljuk, hányszor kapunk fejet. Ezt megismételjük ötször. "Cinkelt" érmét szimulálhatunk azzal, ha a második paraméter nem 0.5.

Poisson eloszlás

print(np.random.poisson(lam=4, size=10)) # két esemény közötti várakozási idő: [4 2 4 2 2 6 2 7 6 4]

Példa: tegyük fel, hogy naponta átlag 4 telefonhívás érkezik. A generált számok 10 napnyi beérkezett hívások számát jelenthetik.

Folytonos egyenletes eloszlás

print(np.random.uniform(low=1, high=4, size=5)) # egyenletes 1 és 4 között: [2.86454989 2.31894435 3.73997112 2.97605771 2.96089434]

Tegyük fel, hogy a metró 3 percenként érkezik, és 1 perc, amíg leérünk a peronig. A generált számok olyan időket jelentenek, amennyi eltelik, mire metróra szállunk.

Exponenciális eloszlás

print(np.random.exponential(scale=3, size=5)) # exponenciális 3-as várható értékkel: [1.33164086 2.22034888 0.13840611 0.29717346 7.63826066]

Tegyük fel, egy alkatrészt átlag 3 havonta kell cserélni. A generált szám két csere között eltelt hónapok számát jelenti.

További eloszlások

Számos egyéb lehetőség van, amelyekről a specifikációból tájékozódhatunk, pl. itt: https://numpy.org/doc/stable/reference/random/generator.html.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License