Kategória: Python.
Alkategória: Adatkezelés Pythonban.
Table of Contents
|
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, egyszerűbbektől egészen a bonyolult lineáris algebra műveltekig.
- 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:
- https://numpy.org/doc/stable/reference/arrays.datetime.html
- https://numpy.org/doc/stable/reference/routines.datetime.html
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 mutat 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. Az 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ólagosan feltesszük az egyenletességet: alapból az eredmény egy 0 és 1 közötti szám (0 elvileg lehet, 1 még elvileg sem), 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 úgy keveri.
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.