Adott egy Northstar-szerű, négy rendelést leíró tömb:
delivery_km a rendelésenkénti szállítási távolság,
center_km pedig ugyanezen rendelésekhez tartozó,
oszlopvektorként érkező bázistávolság. A cél az elemenkénti különbség
kiszámítása, vagyis az
-edik
rendelésnél a
érték. A megfigyelési egység a rendelés, ezért az elvárt kimeneti alak
.
Előzetes becslés: ha a második tömb véletlenül
alakú, a NumPy nem hibát ad, hanem egy
-es
mátrixot állít elő. A 4.2-ben tanult, debuggerrel végzett
shape-vizsgálat itt beépített alakellenőrzéssé válik; a 5.1-ben
definiált shape és axis pedig a tengelyhiba
pontos nyelvét adja.
A NumPy elemenkénti (elementwise) műveleteket végez az
+, -, *, /
operátorokkal. Ha a két operandus alakja eltér, a
broadcasting (kiterjesztés) dönti el, hogy a kisebb
tömb hogyan „ismétlődik” virtuálisan. A szabály:
shape-et jobbról balra igazítjuk.Ez nem matematikai szándékfelismerés, hanem tiszta alak-egyeztetés. A leggyakoribb esetek:
Bal oldali shape |
Jobb oldali shape |
Kimeneti shape |
Értelmezés |
|---|---|---|---|
(4,) |
() |
(4,) |
skalár + vektor |
(4,3) |
(3,) |
(4,3) |
oszloponkénti igazítás |
(4,3) |
(4,1) |
(4,3) |
soronkénti igazítás |
(4,3) |
(4,) |
hiba | a 3 és a 4 nem kompatibilis |
(4,) |
(4,1) |
(4,4) |
csendes, gyakran nem kívánt külső művelet |
A matematikai híd itt a koordinátánkénti művelet és
a skaláris szorzat (dot product) különbsége. Az 1D
u és v vektorokra:
A NumPy * operátora koordinátánkénti szorzatot jelent, a
@ vagy np.dot pedig skaláris szorzatot. A
(n,) és (n,1) alakú tömbök kompatibilisek a
broadcasting szabályai szerint, de a * ilyenkor nem a
páronkénti szorzat, hanem egy
-es
külső szorzat-jellegű mátrix. A shape tehát nem csupán
technikai részlet: azt mondja meg, milyen matematikai műveletet
kapunk.
Boolean mask alatt olyan logikai tömböt értünk, amely megmondja, mely elemeket válasszuk ki, vagy melyeket módosítsuk. A feltétel ugyanazt a broadcasting szabályt követi; a kiválasztás azonban lapított (flattened), 1D tömböt ad, mert a kijelölt pozíciók halmaza nem feltétlenül téglalap.
import numpy as np
# Megfigyelési egység: rendelés (4 sor).
distance_km = np.array([12.0, 8.0, 20.0, 5.5])
mask = distance_km > 10 # shape (4,), dtype bool
print(mask) # [ True False True False] elméletileg várt
print(distance_km[mask]) # [12. 20.] elméletileg vártTöbb feltétel kombinálásához a & és |
operátor kell, zárójelekkel, mert a Python
and/or kulcsszava nem működik elemekre. A
maszk által kijelölt elemeken értéket is adhatunk:
distance_km[mask] = 10.0
print(distance_km) # [10. 8. 10. 5.5] elméletileg vártEz a művelet az eredeti tömböt mutálja, tehát a maszk kiválasztása itt nem hoz létre másolatot a jobb oldal értelemben, hanem az eredeti memóriaterületet írja.
A view (nézet) ugyanarra a memóriaterületre mutat, új
ndarray objektummal; a copy (másolat) új
memóriát foglal. Az alap indexelés (basic indexing) —
például a slice, egész index vagy reshape —
általában view-t ad, ha a memóriaelrendezés engedi. Az advanced
indexing — boolean mask, egész tömbös indexelés — mindig copy-t
ad. A különbség akkor válik láthatóvá, ha módosítunk:
a = np.arange(5) # [0 1 2 3 4]
b = a[1:4] # view: a memoria egy szelete
b[0] = 99
print(a) # [ 0 99 2 3 4] elméletileg várt
c = a[[1, 3]] # advanced indexing: copy
c[0] = -1
print(a) # [ 0 99 2 3 4] elméletileg várt, a változatlanA np.shares_memory(a, b) ellenőrzi, hogy két tömb
osztozik-e memórián. Az a.base attribútum view esetén
megadja az eredeti tömböt, copy esetén általában None, de a
gyors ellenőrzéshez a np.shares_memory a robusztusabb
eszköz. Ha az elemzést meg akarjuk őrizni, explicit
copy() kell, mielőtt a származtatott tömböt
módosítjuk. A későbbi pandas-fejezetben más másolási szabályokat fogunk
tanulni; a NumPy szabályai nem öröklődnek automatikusan a
DataFrame-ekre.
Az advanced indexing (fancy indexing) két fontos formája: az egész tömbös indexelés és az imént látott boolean mask.
A = np.arange(20).reshape(4, 5) # 4 sor, 5 oszlop
print(A)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]]
A[[0, 2], [1, 3]] # koordináta-párok: A[0,1], A[2,3] -> [1, 13]
A[np.ix_([0, 2], [1, 3])] # 2x2 blokk: [[1, 3], [11, 13]]Az első esetben a két index tömb páronként
kapcsolódik; az alak a broadcasting működését követi. A második esetben
a np.ix_ nyílt mátrixszerű kiválasztást (open mesh) ad. Az
A[[0,2]][:, [1,3]] láncolás ugyanazt a blokkot adja, de két
copy-t hoz létre; a np.ix_ egyértelműbb, és akkor is
helyes, ha a kijelölés tengelyenként eltérő.
A mutáció (mutation) az eredeti tömb tartalmának
felülírása, új objektum létrehozása nélkül. A view-n
keresztül végzett módosítás az alap tömböt is módosítja; a
copy() izolációt ad. A dtype-csapda különösen fontos: az
in-place művelet nem változtathatja meg a tároló típust úgy, ahogy az új
értékadás teszi.
arr = np.array([1, 2, 3], dtype=np.int64)
# Nem várt viselkedés: az int tárba float nem írható be.
# A pontos hibaüzenet a NumPy verziójától függ; szemléltetően:
# arr += 0.5 # UFuncOutputCastingError vagy hasonló
arr = arr + 0.5 # új float tömb, a régi megmarad
print(arr.dtype) # float64 elméletileg vártA maszkkal vagy egész tömbös indexeléssel végzett
arr[index] = value mutálja az eredeti tömböt, akkor is, ha
a jobb oldali kiválasztás copy-t adna. Ezért ha az eredeti adat kell, a
módosítandó munkatömböt másoljuk le.
Az alábbi példa a bevezető helyzet hibás és javított változatát mutatja. A kód kimenetei a NumPy szabályaiból levezetett, szemléltető értékek; ezt a kéziratot a generáló környezet nem futtatta, ezért a futást a saját laborkörnyezetben kell ellenőrizni.
import numpy as np
# Megfigyelési egység: rendelés. Szándék: elemenkénti különbség.
delivery_km = np.array([12.0, 8.0, 20.0, 5.5]) # shape (4,)
center_km = np.array([[12.5], [7.0], [18.0], [6.0]]) # shape (4,1)
print(delivery_km.shape) # (4,)
print(center_km.shape) # (4,1)
wrong = delivery_km - center_km
print(wrong.shape) # (4,4) elméletileg várt, nem kívánt
print(wrong)
# [[ -0.5 -4.5 7.5 -7. ]
# [ 5. 1. 13. -1.5]
# [ -6. -10. 2. -12.5]
# [ 6. 2. 14. -0.5]]
# Javítás: a (4,1) oszlopvektorból 1D vektor, majd explicit shape-teszt.
center_1d = center_km.ravel() # shape (4,)
if center_1d.shape != delivery_km.shape:
raise ValueError(f"Alak-eltérés: {delivery_km.shape} vs {center_1d.shape}")
diff = delivery_km - center_1d
print(diff.shape) # (4,) elméletileg várt
print(diff) # [-0.5 1. 2. -0.5] elméletileg várt
# Ellenőrzés: kézi kontroll és numerikus invariant.
expected = np.array([-0.5, 1.0, 2.0, -0.5])
assert diff.shape == (4,)
assert np.allclose(diff, expected, atol=1e-12)A hibás wrong mátrix
-edik
sorának
-edik
oszlopában a
különbség áll. A cél ehelyett csak az azonos indexű párok különbsége
volt. A ravel() a
-es
tárolót az elemek sorrendjében 1D-vé alakítja; mivel az oszlopvektornak
egyetlen oszlopa van, a lapítás pontosan a kívánt négy elemet adja. A
np.allclose a 5.1-ben bevezetett dtype-hoz
igazodó numerikus összehasonlítás; a nullához közeli eltérésekkel itt
nem kell számolni, mert a kivonás pontosan reprezentálható.
Ha a műveletet újrahasználható függvénybe zárjuk, a shape-teszt hibaüzenete rögzíti a szerződést:
def subtract_checked(a, b, name_b="b"):
if b.ndim == 2 and b.shape[1] == 1 and a.ndim == 1:
raise ValueError(
f"{name_b} oszlopvektor {b.shape}, de a {a.shape}; "
"használjon explicit .ravel()-t a koordinátánkénti művelethez."
)
out = a - b
if out.shape != a.shape:
raise AssertionError(f"Váratlan kimeneti alak: {out.shape}")
return out1. Értelmezés. Legyen
a = np.array([1, 2, 3]) és
b_col = np.array([[10], [20], [30]]). (a) Számolja ki
kézzel a * b_col értékét és adja meg az alakját. (b) Milyen
matematikai művelet jött létre, és miért nem az elemenkénti szorzat? (c)
Milyen alakú eredményt ad a * b_col.ravel(), és mennyi az
a @ b_col.ravel() skaláris szorzat?
2. Implementálás. Írjon
clip_cap_basic(values, cap) függvényt, amely a
values > cap maszk alapján a felső plafont alkalmazza
egy új tömbre, az eredeti módosítása nélkül. Tesztelje
x = np.array([2.0, 7.0, 12.0, 4.0]) és
cap = 10.0 bemenettel. Ellenőrző assert: az output
[2.0, 7.0, 10.0, 4.0], az eredeti x pedig
[2.0, 7.0, 12.0, 4.0]. Miért fontos a copy(),
ha az outputot később módosítani fogjuk?
3. Hibadiagnózis. Egy kolléga oszloponkénti központozást szeretne végezni, de a kódja ezt írja:
X = np.arange(12, dtype=float).reshape(4, 3) + 1
col_mean = X.mean(axis=1, keepdims=True) # a szándék szerint oszlopátlag?
X_centered = X - col_mean
print(X_centered.shape) # miért fut le?Lefut-e a kód? Milyen alakú col_mean? Melyik átlagot
vonja ki? Javítsa úgy, hogy a kimenet oszloponkénti központozás legyen,
és assert segítségével ellenőrizze, hogy
col_mean.shape == (3,). Mit mutasson
X_centered.mean(axis=0)?
4. Transzfer megváltozott feltételre. Adott
A = np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]]).
Készítse el B = A.copy()-t, majd azokban a sorokban, ahol
az első oszlop értéke nagyobb, mint 5, állítsa a harmadik oszlopot
-1-re. Adja meg az elvárt B mátrixot, és
bizonyítsa asseerttel, hogy A változatlan maradt. Milyen
indexelési forma történik a maszk és az oszlopindex együttes
használatakor?
1. (a) A broadcasting a (3,) vektort
(1,3) sorrá, a (3,1) oszlopot
(3,1) oszloppá egészíti ki; a kimenet alakja
(3,3), az
elem
.
Kézzel:
a * b_col.ravel() az
elemenkénti szorzat [10, 40, 90]; az
a @ b_col.ravel() skaláris szorzat
.2. Egy lehetséges megoldás:
def clip_cap_basic(values, cap):
out = values.copy() # izoláció a mutációhoz
out[out > cap] = cap
return out
x = np.array([2.0, 7.0, 12.0, 4.0])
y = clip_cap_basic(x, 10.0)
assert np.allclose(y, [2.0, 7.0, 10.0, 4.0])
assert np.allclose(x, [2.0, 7.0, 12.0, 4.0]) # az eredeti nem változottA values.copy() nélkül az out = values csak
nézetként ugyanarra a memóriára mutatna; a maszkkal végzett értékadás az
eredeti tömböt is módosítaná. Ezért a másolat itt a függvény kimenetének
és az inputnak az elválasztását biztosítja.
3. A kód lefut, mert a (4,3) és
(4,1) alakok kompatibilisek a broadcasting szerint. A
col_mean alakja (4,1), és az
axis=1 miatt soronkénti átlag: minden
sorból a saját sorátlagot vonja ki. Az oszloponkénti központozáshoz
axis=0 kell, keepdims=False mellett:
col_mean = X.mean(axis=0)
assert col_mean.shape == (3,)
X_centered = X - col_mean # shape (4,3)
assert np.allclose(X_centered.mean(axis=0), 0.0, atol=1e-12)A X_centered.mean(axis=0) nullához közeli vektor
[0, 0, 0], mert a mean(axis=0) a 5.1-ben
tanult redukciós tengely mentén törli a sorokat, és pontosan az imént
kivont oszlopátlagot adja vissza.
4. A maszk és az oszlopindex együttes használata
advanced indexing assignment: a boolean maszk az első tengelyt, az egész
oszlopindex a második tengelyt jelöli ki; a bal oldalon ez a kijelölt
elemek eredeti memóriáját írja. A várható B:
# B:
# [[ 1 2 3]
# [ 4 5 6]
# [ 7 8 -1]
# [10 11 -1]]
B = A.copy()
mask = A[:, 0] > 5
B[mask, 2] = -1
assert np.array_equal(B, [[1,2,3],[4,5,6],[7,8,-1],[10,11,-1]])
assert np.array_equal(A, [[1,2,3],[4,5,6],[7,8,9],[10,11,12]])A lefutó kód nem bizonyítja, hogy a matematikai
művelet a szándékunk szerinti. A
és
alakú delivery_km - center_km hiba nélkül lefut, de a várt
négy rendelés helyett 16 párosításos értéket hoz létre. Ha ezt valaki
ellenőrzés nélkül továbbítja egy aggregációba, például
.mean()-et számol rá, az átlag már az összes párosítás
átlaga lesz, nem a rendelések szerinti különbségeké. A 5.1-ben tanult
shape-invariáns itt az első védelmi vonal: rögzítsük előre a kimenet
elvárt alakját, és a művelet után assert vagy explicit
if formában ellenőrizzük. A broadcasting szándékos külső
műveletnél legitim eszköz; a hiba forrása nem a szabály, hanem a
hallgatólagos feltételezés, hogy a NumPy kitalálja a matematikai
célunkat.
Önellenőrzés: (1) Miért ad a[1:3] view-t, de
a[[1,2]] copy-t? (2) Mi a különbség a * b és
a @ b között 1D tömböknél? (3) Egy boolean maszk
kiválasztás után az eredeti tömb módosul-e, ha a kiválasztott tömböt
módosítom? Válaszok: (1) Az alap slice ugyanarra a memóriára mutathat;
az advanced integer indexing új tömböt foglal. (2) Az *
elemenkénti szorzatot ad, a @ összegzi a páronkénti
szorzatok összegét, skalárt eredményezve. (3) Nem, mert a maszk advanced
indexing, tehát a kiválasztott tömb copy; de az
arr[mask] = value assignment az eredeti tömböt mutálja.
Nézet, másolat, írási mellékhatás. A NumPy alapindexelése rendszerint
nézetet, a haladó indexelés rendszerint másolatot ad; a kapott értékek
egyezéséből ez nem derül ki. Ellenőrzési példában az eredeti és a
kiválasztott tömb módosítása után vizsgáld meg, terjed-e a változás. A
np.shares_memory a memóriamegosztás tesztjére használható,
de speciális elrendezéseknél az eredményt körültekintően kell
értelmezni.
Broadcastingnál mindig írd fel a dimenziókat jobbról balra igazítva; csak egyenlő méretek vagy valamelyik oldalon 1-es dimenzió esetén kompatibilisek. A kompatibilis alak nem jelenti automatikusan, hogy a művelet statisztikailag értelmes.
A broadcasting a kisebb alak jobbra igazított, 1 dimenzió mentén
történő virtuális ismétlése; a kimenet a dimenziónkénti hosszak
maximuma. A (n,) és (n,1) alakok
kompatibilisek, de elemenkénti művelettel n×n-es mátrixot
adnak, ezért a shape-teszt a szándék szerinti művelet része. A boolean
mask és az egész tömbös indexelés advanced indexinget jelent, amely
copy-t ad, míg az alap slice view-t; a mutáció izolálásához explicit
copy() kell. A következő alfejezetben a lebegőpontos
ábrázolás, a hiányzó érték és a stabil számítás épül ezekre az alak- és
másolásismeretekre.