5.1 ndarray, shape, axis és dtype

5.1.1 Cél és konkrét helyzet

A Northstar adatszerződésben egy rendelés sora a predikció megfigyelési egysége. Tegyük fel, hogy a distance_km, basket_minor és quantity_sum három oszlopból kiválasztottunk öt rendelést, és NumPy-tömbben tároljuk. Az elemzés előtt tudnunk kell, hogy a táblázatos adatból hogyan lesz tömb: hány dimenzió, milyen alak, melyik tengely mentén összegzünk, és milyen numerikus típust használunk.

A konkrét bemenet egy 5×35 \times 3-as mátrix:

A=(12.4299028.15990421.7129015.08790614.934903) A = \begin{pmatrix} 12.4 & 2990 & 2\\ 8.1 & 5990 & 4\\ 21.7 & 1290 & 1\\ 5.0 & 8790 & 6\\ 14.9 & 3490 & 3 \end{pmatrix}

A sorok rendeléseket, az oszlopok a fenti három feature-t jelentik. A mértékegység: km, EUR-cent, darab.

Induló becslés, mielőtt kódot írunk: mekkora az ndim és a shape? Becsüld meg az oszlopátlagokat: az első oszlop átlaga 1010 és 1313 között lesz, a másodiké 40004000 körül, a harmadiké 33 és 44 között. Írd le azt is, milyen alakú eredményt vársz az oszlopátlag és a sorátlag művelettől.

5.1.2 Fogalmi magyarázat

5.1.2.1 Skalár és vektor

A skalár (scalar) egyetlen szám. NumPy-ban a 0-dimenziós tömb:

s = np.array(3.2)
s.ndim    # 0
s.shape   # ()

A vektor itt egy rendezett számlista, például v=(12.4,2990,2)v = (12.4, 2990, 2). Ez egy 1-dimenziós tömb, shape=(3,):

v = np.array([12.4, 2990.0, 2.0])
v.ndim    # 1
v.shape   # (3,)

A vektor most még csak számlista; a geometriai jelentést később, a lineáris algebra fejezetben kapja meg.

5.1.2.2 ndim és shape

Az ndim a dimenziók száma. A shape egy egész számokból álló tuple, amely megadja az egyes tengelyek hosszát. Az 5×35 \times 3-as mátrixra: ndim=2, shape=(5, 3). A tömbbeli elemek száma a shape elemeinek szorzata: 5⋅3=155 \cdot 3 = 15.

A NumPy sor-major (C-rendű) tárolást használ: a memóriában először az első sor elemei következnek, utána a második soré. Ez akkor válik fontossá, amikor a reshape műveletet vizsgáljuk.

5.1.2.3 axis

A tengely (axis) az aggregáció irányát jelöli. A szabály, amelyet érdemes mechanikusan alkalmazni: a megadott tengely eltűnik az eredményből.

Ha A alakja (r,c)(r, c) és A.mean(axis=0)-t számolunk, akkor a 0. tengely, vagyis a sorok tűnnek el, az eredmény alakja (c,)(c,). Ez az oszloponkénti átlag: minden oszlopra a rögzített jj index mellett átlagolunk az ii sorokon:

(mean(A,axis=0))j=1r∑i=1rAij. (\operatorname{mean}(A, \mathrm{axis}=0))_j = \frac{1}{r}\sum_{i=1}^{r} A_{ij}.

A A.mean(axis=1) esetén az 1. tengely, vagyis az oszlopok tűnnek el, az eredmény alakja (r,)(r,). Ez a soronkénti átlag:

(mean(A,axis=1))i=1c∑j=1cAij. (\operatorname{mean}(A, \mathrm{axis}=1))_i = \frac{1}{c}\sum_{j=1}^{c} A_{ij}.

Ugyanez a szabály igaz az összegre, minimumra, maximumra és más redukciós műveletekre.

5.1.2.4 dtype

A dtype (data type) a tömbben tárolt elemek típusa. A leggyakoribb kezdeti értékek:

np.array([1, 2, 3]).dtype          # int64
np.array([1.0, 2.0, 3.0]).dtype    # float64
np.array([True, False]).dtype      # bool
np.array(["a", "b"]).dtype         # <U1, egykarakteres Unicode string

A tömb létrehozásakor a NumPy egységes típust választ: ha egy listában vegyesen van egész és valós, akkor float64 lesz. Ha szám mellé string keveredik, akkor a tömb stringgé válik, és a numerikus műveletek meghiúsulnak. A dtype a memóriaigényt is meghatározza: egy float64 elem 8 bájt, egy float32 elem 4 bájt, de a float32 kisebb pontosságú.

5.1.2.5 Átalakítás

A reshape megváltoztatja a shape-et, de az elemszámot és a C-rendű sorrendet nem:

a = np.array([[1, 2, 3], [4, 5, 6]])  # shape (2, 3)
a.reshape(6)                          # shape (6,): [1, 2, 3, 4, 5, 6]
a.reshape(3, 2)                       # shape (3, 2)
a.reshape(-1)                         # shape (6,): a -1 automatikusan kiszámolt hossz

A -1 hasznos, ha az egyik dimenzió hosszát a másikból akarjuk kiszámoltatni.

5.1.3 Teljes minimális munkapélda

A példa a Northstar feature-hármasát használja. A kódot a projektgyökérben egy s017_pelda.py fájlba írjuk, futtatása uv run python s017_pelda.py a lab környezetből. A NumPy 2.x verziót feltételezünk; a np.float64 és np.float32 elnevezés ebben a verzióban érvényes, a régi np.float kerülendő.

import numpy as np

# 5 rendelés, 3 rendeléskor ismert feature.
# Oszlopok: distance_km, basket_minor, quantity_sum.
orders = np.array(
    [
        [12.4, 2990.0, 2.0],
        [8.1, 5990.0, 4.0],
        [21.7, 1290.0, 1.0],
        [5.0, 8790.0, 6.0],
        [14.9, 3490.0, 3.0],
    ],
    dtype=np.float64,
)

print("ndim:", orders.ndim)
print("shape:", orders.shape)
print("dtype:", orders.dtype)

column_mean = orders.mean(axis=0)  # oszloponkénti átlag
row_mean = orders.mean(axis=1)     # soronkénti átlag

print("column_mean shape:", column_mean.shape)
print("column_mean:", column_mean)
print("row_mean shape:", row_mean.shape)
print("row_mean:", row_mean)

flat = orders.reshape(-1)
print("flat shape:", flat.shape)
print("flat első hat elem:", flat[:6])

Szemléltető kimenet (a kapcsolódó példaprojektben még nem ellenőrzött; a számokat levezetés és kézi kontroll követi):

ndim: 2
shape: (5, 3)
dtype: float64
column_mean shape: (3,)
column_mean: [  12.42 4510.      3.2 ]
row_mean shape: (5,)
row_mean: [1001.46666667 2000.7         437.56666667 2933.66666667 1169.3       ]
flat shape: (15,)
flat első hat elem: [  12.4 2990.     2.    8.1 5990.     4. ]

5.1.4 Eredmény értelmezése

Az orders tömb ndim=2, shape=(5, 3), dtype=float64. A mean(axis=0) az első tengelyt tünteti el, ezért az eredmény alakja (3,). A kapott három érték rendre a távolság, a kosárérték és a darabszám átlaga:

A mean(axis=1) a második tengelyt tünteti el, ezért az eredmény alakja (5,). Minden sor három elemét átlagolja. Az első sor: (12.4+2990+2)/3≈1001.47(12.4 + 2990 + 2)/3 \approx 1001.47. Ez az érték kevert mértékegységű, mert km, cent és darab számtani átlagát veszi; itt a sorátlag csak shape-gyakorlat, üzletileg nem értelmezhető. Az oszlopátlag viszont mértékegység-helyes, mert minden oszlop ugyanazt a mennyiséget méri.

A reshape(-1) a 5⋅3=155 \cdot 3 = 15 elemet C-sorrendben egyetlen vektorrá fűzi: előbb az első sor, majd a második, és így tovább. Az első hat elem valóban 12.4,2990,2,8.1,5990,412.4, 2990, 2, 8.1, 5990, 4.

A shape-invariáns, amelyet minden redukcióra ellenőrizhetünk:

shape(reduce(A,axis=k))=shape(A) a k. elem nélkül. \text{shape}\big(\operatorname{reduce}(A, \mathrm{axis}=k)\big) = \text{shape}(A) \text{ a } k \text{. elem nélkül}.

Például (5, 3) esetén axis=0 után (3,), axis=1 után (5,).

Alak- és típusinvariáns. A NumPy axis értelmezése mindig a bemeneti tömb dimenzióihoz kötött; egy kétdimenziós (n, p) adatmátrix esetében az axis=0 oszloponként, az axis=1 soronként redukál. Az ndarray dtype-ja a tárolt reprezentációt határozza meg, nem automatikusan a valós világban értelmes mértékegységet.

Ellenpróba: egész típusú tömbön történő műveletnél ellenőrizd a túlcsordulást és a kimeneti típust, az üres tengelyen végzett átlagolásnál pedig a figyelmeztetést vagy nan-t. Minden mintapéldához rögzítsd a várt shape-et és dtype-ot.

5.1.5 Feladatok

1. Értelmezés. Az 5×35 \times 3-as orders tömbre mondd meg: mi az ndim, a shape, és mit jelent képlettel a sum(axis=0)? Mi lesz a kapott tömb shape-e, és mennyi a sum(axis=0) első eleme?

2. Implementálás. Hozz létre egy 4×24 \times 2-es tömböt:

B = np.array([[1, 10], [2, 20], [3, 30], [4, 40]])

Számítsd ki az oszlopátlagot, a sorátlagot és a reshape(-1) eredményét. Ellenőrizd mindhárom eredmény shape-ét és értékeit kézzel.

3. Hibadiagnózis. Az alábbi kód egy elemző szándéka szerint „soronkénti átlagot” akar számolni:

M = np.array([[1, 2], [3, 4]])
sor_atlag = M.mean(axis=0)
print(sor_atlag)  # [2. 3.]

Mi a hiba? Milyen axis kell a soronkénti átlaghoz, és mi lenne a helyes eredmény? Magyarázd el a kimenet alakját is.

4. Megváltozott feltétel. Az eredeti orders tömbhöz adj egy negyedik oszlopot, item_rows-t, a következő értékekkel: [1, 2, 1, 3, 2]. Az új tömb alakja (5,4)(5, 4). Számítsd ki az új sorátlagokat, és add meg az item_rows oszlop átlagát. Mi az új sorátlagok shape-e, és miért változott meg az értékük az eredeti sorátlagokhoz képest?

5.1.6 Megoldások és indoklás

1. megoldás. ndim=2, shape=(5, 3). A sum(axis=0) az első tengelyt tünteti el, tehát oszloponkénti összeg:

(sum(A,axis=0))j=∑i=15Aij. (\operatorname{sum}(A, \mathrm{axis}=0))_j = \sum_{i=1}^{5} A_{ij}.

Az eredmény shape=(3,). Az első elem az első oszlop összege:

12.4+8.1+21.7+5.0+14.9=62.1. 12.4 + 8.1 + 21.7 + 5.0 + 14.9 = 62.1.

Tehát a várt tömb [62.1, 22550.0, 16.0].

2. megoldás. A kód:

B = np.array([[1, 10], [2, 20], [3, 30], [4, 40]])
B.shape                      # (4, 2)
B.mean(axis=0)               # (2,): oszlopátlag [2.5, 25]
B.mean(axis=1)               # (4,): sorátlag [5.5, 11, 16.5, 22]
B.reshape(-1)                # (8,): [1, 10, 2, 20, 3, 30, 4, 40]

Kézi ellenőrzés: az első oszlop átlaga (1+2+3+4)/4=10/4=2.5(1+2+3+4)/4 = 10/4 = 2.5; a másodiké (10+20+30+40)/4=100/4=25(10+20+30+40)/4 = 100/4 = 25. A sorátlagok: (1+10)/2=5.5(1+10)/2=5.5, (2+20)/2=11(2+20)/2=11, (3+30)/2=16.5(3+30)/2=16.5, (4+40)/2=22(4+40)/2=22. A reshape(-1) C-rendben soronként fűzi az elemeket.

3. megoldás. A hiba, hogy axis=0 az oszlopok szerinti átlagot adja, nem a soronkéntit. A M mátrix:

M=(1234). M = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}.

axis=0 az első tengelyt tünteti el, ezért az eredmény alakja (2,), és az oszlopátlagok [2, 3]. A soronkénti átlaghoz axis=1 kell, amely a második tengelyt tünteti el, az eredmény alakja (2,), értéke [1.5, 3.5]. A kimenet alakja mindkét esetben (2,), de az axis választás dönti el, hogy mit átlagolunk.

4. megoldás. Az új tömb:

orders2 = np.array(
    [
        [12.4, 2990.0, 2.0, 1.0],
        [8.1, 5990.0, 4.0, 2.0],
        [21.7, 1290.0, 1.0, 1.0],
        [5.0, 8790.0, 6.0, 3.0],
        [14.9, 3490.0, 3.0, 2.0],
    ],
    dtype=np.float64,
)

orders2.mean(axis=1)
# Szemléltető kimenet: shape (5,)
# [ 751.35  1501.025  328.425  2201.0  877.475]
orders2[:, 3].mean()
# Szemléltető kimenet: 1.8

A sorátlagok shape=(5,), mert a második tengely (most már 4 elem) tűnik el. Az első sor: (12.4+2990+2+1)/4=3005.4/4=751.35(12.4 + 2990 + 2 + 1)/4 = 3005.4/4 = 751.35. Az item_rows oszlop átlaga (1+2+1+3+2)/5=9/5=1.8(1+2+1+3+2)/5 = 9/5 = 1.8. Az értékek azért változtak, mert a korábbi három elemhez egy negyedik, más mértékegységű elem is bekerült; az összesítés továbbra is kevert mértékegységű, tehát a sorátlag most is csak shape-gyakorlat.

5.1.7 Gyakori tévedés ellenpéldával

A leggyakoribb tévedés: „az axis=0 azt jelenti, soronként haladok.” A NumPy szabálya nem a bejárás irányáról, hanem az eltűnő tengelyről szól.

Ellenpélda a 2×32 \times 3-as tömbre:

A=(123456). A = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}.

A.mean(axis=0)  # [2.5, 3.5, 4.5], shape (3,)  -> oszloponkénti átlag
A.mean(axis=1)  # [2.0, 5.0],      shape (2,)  -> soronkénti átlag

Ha a magyar mondat úgy szól, hogy „rendelésenkénti átlag”, akkor a rendelés a sor, tehát a soroknak meg kell maradniuk; a törölt tengely az oszlop, azaz axis=1. Ha viszont „feature-onkénti átlag” kell, akkor az oszlopok maradnak meg, és a sorok tűnnek el, azaz axis=0. Az axis számozás a shape tuple pozíciója, nem a művelet irányának emberi leírása.

Ez a hiba a Northstar elemzésben is számít: az orders tömbön a sor a megfigyelési egység. Ha véletlenül axis=0-t használsz, akkor feature-átlagot kapsz; ha axis=1-et, akkor rendelésenkénti átlagot. A két eredmény más méretű és más jelentésű.

5.1.8 Önellenőrzés

  1. Ha egy tömb shape=(10, 4), akkor mi az ndim, és mi lesz a sum(axis=0) eredményének shape-e?
  2. Mi a különbség a shape=(3,) vektor és a shape=(3, 1) oszlopmátrix között? Próbáld ki np.array([1, 2, 3]) és np.array([[1], [2], [3]]) alakkal.
  3. Melyik dtype-ot választaná a NumPy az np.array([1, 2.5, 3]) listára, és miért?
  4. Mi a reshape(-1) eredménye egy (2, 3) alakú tömbön, és milyen sorrendben követik az elemek?

5.1.9 Rövid összefoglaló

A NumPy-tömb dimenzióit az ndim, alakját a shape adja meg; a dtype a tárolt elemek egységes típusa. A redukciók, például a mean vagy a sum, a megadott tengelyt tüntetik el: axis=0 a sorokat, axis=1 az oszlopokat törli, ezért a kimenet alakja rendre az oszlopok vagy a sorok száma. A reshape az elemszám megtartásával, C-sorrendben rendezi át a tömböt. A sor- és oszlopátlag kézzel ellenőrizhető, és a shape-invariáns segít elkerülni a félrecsúszott tengelyválasztást. A következő alfejezet a broadcasting, az indexelés és a másolás szabályait építi erre az alak- és tengelymodellre.