# Array-Operationen mit NumPy

> For AI agents: use [llms.txt](https://nakafa.com/llms.txt) for the site index. Markdown versions are available by appending `.md` to content URLs or sending `Accept: text/markdown`.

URL: https://nakafa.com/de/faecher/ki-und-data-science/ki-programmierung/array-operationen-mit-numpy
Source: https://raw.githubusercontent.com/nakafaai/aksara/16d6b8e869d1a277313c65bbfc4b4a83efe77a46/packages/corpus/material/lesson/ai-ds/ai-programming/array-operation-numpy/de.mdx

Lerne Broadcasting, elementweise Arithmetik, Reduktionen, Achsen, Formoperationen und Z-Standardisierung mit NumPy kennen.

---

## Broadcasting in NumPy

Broadcasting ist NumPys Regelsystem zum Verknüpfen von Arrays mit unterschiedlichen, aber kompatiblen Shapes. Dimensionen der Größe eins werden dabei nur gedanklich erweitert, ohne Werte in ein größeres Array zu kopieren. Der [NumPy-Leitfaden zum Broadcasting](https://numpy.org/doc/stable/user/basics.broadcasting.html) zeigt weitere Beispiele.

Bei der Vektorisierung wirkt ein Ausdruck auf ein ganzes Array. NumPy führt die Schleife im Hintergrund als optimierten, kompilierten Code aus. Das ist oft schneller als eine ausdrücklich geschriebene Python-Schleife.

File: vectorization_example.py
```python
import numpy as np

# Zwei Arrays erstellen
a = np.array([0, 1, 2])
b = np.array([2, 2, 2])

# Vektorisierungsoperation (elementweise)
result = a + b
print(result)
# Ausgabe: [2 3 4]
```

Im Beispiel addiert NumPy die Elemente an den jeweils entsprechenden Positionen. Wir formulieren die Operation einmal, statt jedes Element in einer Python-Schleife einzeln zu indizieren.

### Broadcasting-Regeln

Um zu entscheiden, ob zwei Shapes kompatibel sind, vergleicht NumPy ihre Dimensionen von rechts nach links.

Beim Broadcasting gelten drei Hauptregeln:

1. **Regel** $$1$$: Unterscheidet sich die Anzahl der Dimensionen, wird der Shape mit weniger Dimensionen so behandelt, als stünden links zusätzliche Dimensionen der Größe $$1$$
2. **Regel** $$2$$: Zwei ausgerichtete Dimensionen sind kompatibel, wenn ihre Größen gleich sind oder eine von ihnen die Größe $$1$$ hat. Eine Dimension der Größe eins wird virtuell auf die Größe ihres Gegenstücks erweitert
3. **Regel** $$3$$: Ist auch nur ein ausgerichtetes Dimensionspaar inkompatibel, löst NumPy einen `ValueError` aus

Visible text: 1. **Regel** : Unterscheidet sich die Anzahl der Dimensionen, wird der Shape mit weniger Dimensionen so behandelt, als stünden links zusätzliche Dimensionen der Größe 
2. **Regel** : Zwei ausgerichtete Dimensionen sind kompatibel, wenn ihre Größen gleich sind oder eine von ihnen die Größe hat. Eine Dimension der Größe eins wird virtuell auf die Größe ihres Gegenstücks erweitert
3. **Regel** : Ist auch nur ein ausgerichtetes Dimensionspaar inkompatibel, löst NumPy einen `ValueError` aus

### Array mit Skalar

File: broadcasting_1d_scalar.py
```python
import numpy as np

# 1D-Array mit Skalar
a = np.arange(3)  # [0, 1, 2]
b = 5

result = a + b
print(f"Array a: {a}")
print(f"Skalar b: {b}")
print(f"Ergebnis a + b: {result}")

# Erklärung der Formen:
# a hat die Form (3,)
# b hat die Form () - Skalar
# Nach dem Broadcasting wird b zu [5, 5, 5]
# Ausgabe: [5 6 7]
```

### Zweidimensionales Array mit eindimensionalem Array

Bei Arrays mit unterschiedlicher Dimensionszahl prüft NumPy die Shapes anhand dieser Regeln. So lässt sich beispielsweise dieselbe Operation auf jede Zeile oder jede Spalte einer Matrix anwenden.

File: broadcasting_2d_1d.py
```python
import numpy as np

# 2D-Array mit 1D-Array
a = np.ones((3, 3))  # 3x3-Matrix mit Einsen
b = np.arange(3)     # [0, 1, 2]

result = a + b
print("Array a (3x3):")
print(a)
print(f"Array b (1D): {b}")
print("Ergebnis a + b:")
print(result)

# Broadcasting findet statt:
# a: Form (3, 3)
# b: Form (3,) -> erweitert zu (1, 3) -> (3, 3)
# b wird zu jeder Zeile von a hinzugefügt
```

### Fall mit fehlgeschlagenem Broadcasting

File: broadcasting_error.py
```python
import numpy as np

try:
  # Arrays mit inkompatiblen Formen
  a = np.arange(6).reshape(2, 3)  # Form (2, 3)
  b = np.arange(2)                # Form (2,)

  print(f"Form von Array a: {a.shape}")
  print(f"Form von Array b: {b.shape}")

  # Dies führt zu einem Fehler
  result = a + b
except ValueError as e:
  print(f"Error: {e}")
  print("Die Array-Formen sind für Broadcasting nicht kompatibel")
```

## Arithmetische Array-Operationen

Arithmetische NumPy-Operatoren arbeiten elementweise, sofern eine Operation nicht ausdrücklich eine andere Bedeutung besitzt, etwa die Matrixmultiplikation.

Wird ein Array mit einem Skalar verknüpft, wendet Broadcasting den Skalar auf jedes Element an. Eine manuelle Indexschleife ist dafür nicht nötig.

File: arithmetic_scalar.py
```python
import numpy as np

a = np.array([0, 1, 2, 3, 4])

# Addition mit Skalar
print("Addition:")
print(f"a + 1 = {a + 1}")
# Ausgabe: [1 2 3 4 5]

# Multiplikation mit Skalar
print("Multiplikation:")
a *= 2
print(f"a *= 2: {a}")
# Ausgabe: [0 2 4 6 8]

# Potenz
print("Potenz:")
print(f"2**a = {2**a}")
# Ausgabe: [  1   4  16  64 256]
```

### Operationen zwischen Arrays

File: arithmetic_arrays.py
```python
import numpy as np

a = np.array([0, 1, 2, 3, 4])
b = np.array([4, 3, 2, 1, 0])

# Elementweise Subtraktion
print("Subtraktion:")
print(f"a - b = {a - b}")
# Ausgabe: [-4 -2  0  2  4]

# Elementweise Multiplikation
print("Elementweise Multiplikation:")
print(f"a * b = {a * b}")
# Ausgabe: [0 3 4 3 0]

# Matrixmultiplikation (Skalarprodukt)
print("Matrixmultiplikation:")
print(f"a @ b = {a @ b}")
# Ausgabe: 10 (Skalarproduktergebnis)
```

Bei `*` werden Elemente an entsprechenden Positionen multipliziert. `@` führt dagegen eine Matrixmultiplikation nach den Regeln der linearen Algebra aus. `np.dot()` hat je nach Dimension der Operanden eigene, dokumentierte Regeln und sollte nicht pauschal mit `@` gleichgesetzt werden.

### Vergleich und Logik

Vergleiche erzeugen boolesche Arrays. Sie können anschließend zum Filtern von Daten oder zum Verknüpfen mehrerer Bedingungen verwendet werden.

File: comparison_logical.py
```python
import numpy as np

a = np.array([0, 1, 2, 3, 4])
b = np.array([0, 0, 2, 4, 4])

# Vergleichsoperationen
print("Größer-als-Vergleich:")
print(f"a > 2: {a > 2}")
# Ausgabe: [False False False  True  True]

print("Gleichheitsvergleich:")
print(f"a == b: {a == b}")
# Ausgabe: [ True False  True False  True]

# Logische Operationen
print("Logische ODER-Verknüpfung:")
print(f"(a > 2) | (a == b): {(a > 2) | (a == b)}")
# Ausgabe: [ True False  True  True  True]
```

Für elementweise boolesche Logik verwendest du `~` für NOT, `&` für AND und `|` für OR oder die entsprechenden `np.logical_*`-Funktionen. Setze jeden Vergleich in Klammern, weil die Operatoren eine andere Priorität als Vergleiche haben. Die skalaren Python-Operatoren `not`, `and` und `or` führen keine elementweise Array-Logik aus.

## Statistische Funktionen und Reduktionen

Reduktionsfunktionen fassen viele Elemente zu weniger Werten zusammen. Ohne Angabe einer Achse können sie das gesamte Array auf einen Wert reduzieren. Mit einer Achse reduzieren sie nur diese Dimension. Bei einer Tabelle mit Prüfungsergebnissen kann so etwa ein Mittelwert pro Fach oder pro Person entstehen.

NumPy stellt dafür statistische Funktionen bereit, die entweder auf dem gesamten Array oder entlang ausgewählter Achsen arbeiten.

File: basic_statistics.py
```python
import numpy as np

# 2D-Beispielarray erstellen
data = np.array([[3, 0, -1, 1],
               [2, -1, -2, 4],
               [1, 7, 0, 4]])

print("Data array:")
print(data)

# Statistiken zum gesamten Array
print(f"Gesamtsumme: {np.sum(data)}")
print(f"Mittelwert: {np.mean(data):.2f}")
print(f"Kleinster Wert: {np.min(data)}")
print(f"Maximalwert: {np.max(data)}")
print(f"Standardabweichung: {np.std(data):.2f}")
# Ausgabe:
# Gesamtsumme: 18
# Mittelwert: 1.50
# Kleinster Wert: -2
# Maximalwert: 7
# Standardabweichung: 2.50
```

### Operationen mit Achsen

Bei einem zweidimensionalen Array reduziert `axis=0` die Zeilendimension und liefert einen Wert pro Spalte. `axis=1` reduziert die Spaltendimension und liefert einen Wert pro Zeile.

Wenn Sie Achsen verstehen, können Sie steuern, wie statistische Funktionen auf mehrdimensionale Daten wirken. Wenn Sie beispielsweise monatliche Verkaufsdaten für verschiedene Produkte haben, können Sie den Gesamtumsatz pro Produkt oder pro Monat berechnen.

File: axis_operations.py
```python
import numpy as np

data = np.array([[3, 0, -1, 1],
               [2, -1, -2, 4],
               [1, 7, 0, 4]])

# Operationen entlang der Achse=0 (für jede Spalte)
print("Maximum jeder Spalte (Achse=0):")
print(f"max(axis=0): {np.max(data, axis=0)}")
# Ausgabe: [3 7 0 4]

print("Index des Maximums in jeder Spalte:")
print(f"argmax(axis=0): {np.argmax(data, axis=0)}")
# Ausgabe: [0 2 2 1]

# Operationen entlang der Achse=1 (für jede Zeile)
print("Maximum jeder Zeile (Achse=1):")
print(f"max(axis=1): {np.max(data, axis=1)}")
# Ausgabe: [3 4 7]

print("Index des Maximums in jeder Zeile:")
print(f"argmax(axis=1): {np.argmax(data, axis=1)}")
# Ausgabe: [0 3 1]
```

## Array-Form-Manipulation

Shape-Operationen verändern, wie dieselben Elemente indiziert werden. Ein Reshape behält die Anzahl der Elemente bei, ordnet sie aber anderen Dimensionen zu.

Neu erzeugte NumPy-Arrays sind häufig C-kontiguierlich. Dann ändert sich der letzte Index im Speicher am schnellsten. Views aus Slices oder Transpositionen können jedoch andere Strides besitzen, sodass Arrays nicht immer in einem einzigen einfachen Zeilenlayout gespeichert sind. Beim Reshape oder Flatten spielen deshalb die Regeln für `order` und Strides eine Rolle.

File: internal_storage.py
```python
import numpy as np

# 2D-Array erstellen
a = np.array([[0, 1], [2, 3]])
print("2D-Array:")
print(a)
print(f"Form: {a.shape}")

# Sehen Sie, wie es im Speicher gespeichert wird
print(f"Im Speicher gespeichert als: {a.ravel()}")
# Ausgabe: [0 1 2 3] (row-major order)
```

### Flatten und Ravel

`flatten()` und `ravel()` liefern beide ein eindimensionales Array. `flatten()` erzeugt immer eine Kopie. `ravel()` liefert wenn möglich eine View und erstellt eine Kopie, wenn das Speicherlayout es erfordert.

File: flatten_ravel.py
```python
import numpy as np

# Diagonales Array erstellen
a = np.diag([1, 2, 3])
print("Diagonales Array:")
print(a)

# flatten - erstellt eine unabhängige Kopie
b_flatten = a.flatten()
print(f"Ergebnis von flatten: {b_flatten}")

# Das Ändern der Flatten-Werte hat keinen Einfluss auf das ursprüngliche Array
b_flatten[0] = 9
print(f"Nach Änderung der flatten-Kopie: {b_flatten}")
print("Das ursprüngliche Array bleibt gleich:")
print(a)

print()

# ravel - versucht eine View zu erstellen und ist dadurch effizienter
b_ravel = a.ravel()
print(f"Ravel-Ergebnis: {b_ravel}")

# Das Ändern der Ravel-Werte wirkt sich auf das ursprüngliche Array aus
b_ravel[0] = 9
print(f"Nach Änderung der ravel-Ansicht: {b_ravel}")
print("Ursprüngliches Array geändert:")
print(a)
```

### Umformen und Größe ändern

`reshape()` ändert den Shape, solange die Anzahl der Elemente gleich bleibt, und liefert wenn möglich eine View. Die unten verwendete Methode `ndarray.resize()` verändert dagegen das ursprüngliche Array direkt.

File: reshape_resize.py
```python
import numpy as np

# Diagonales Array erstellen und mit flatten abflachen
a = np.diag([1, 2, 3])
a_flat = a.flatten()
print(f"Flaches Array: {a_flat}")

# reshape - ändert die Form bei gleicher Elementanzahl
b = a_flat.reshape(3, 3)
print("Ergebnis von reshape(3, 3):")
print(b)

# Werte in der reshape-Ansicht ändern
b[0, 0] = 9
print("Nach Wertänderung:")
print(b)
print(f"Ursprüngliches flaches Array: {a_flat}")  # Ändert sich, weil reshape eine Ansicht erstellt

# resize - ändert die Form direkt und gibt keinen Wert zurück
a_flat.resize(3, 3)
print("Nach der Größenänderung:")
print(a_flat)  # Jetzt ist a_flat 2D
```

### Transponieren

Eine Transposition vertauscht die Reihenfolge der Array-Achsen. Bei einem zweidimensionalen Array werden Zeilen und Spalten getauscht. NumPy bietet dafür die Methode `transpose()` und das kürzere Attribut `.T`.

File: transpose.py
```python
import numpy as np

# 2x4-Array erstellen
a = np.linspace(1, 8, 8).reshape(2, 4)
print("Ursprüngliches Array (2x4):")
print(a)

# Mit Methode transponieren
b = a.transpose()
print("Transponierungsergebnis (4x2):")
print(b)

# Mit .T-Attribut transponieren (kürzer)
c = a.T
print("Verwendung von .T:")
print(c)

# Prüfen, ob die Transponierung eine Ansicht ist
print("Ist die Transponierung eine Ansicht?", np.shares_memory(a, b))
```

## Datenstandardisierung mit Z-Transformation

Die Z-Standardisierung zentriert jedes Merkmal auf den Mittelwert $$0$$ und skaliert es auf die Standardabweichung $$1$$. Sie ist nur für Merkmale mit einer von null verschiedenen Standardabweichung definiert und bringt Merkmale auf vergleichbare numerische Skalen.

Visible text: Die Z-Standardisierung zentriert jedes Merkmal auf den Mittelwert und skaliert es auf die Standardabweichung . Sie ist nur für Merkmale mit einer von null verschiedenen Standardabweichung definiert und bringt Merkmale auf vergleichbare numerische Skalen.

Die Formel lautet $$Z = \frac{X - \mu}{\sigma}$$. Dabei gilt:

Visible text: Die Formel lautet . Dabei gilt:

- $$X$$ ist die Merkmalsmatrix der Größe $$n \times k$$
- $$n$$ ist die Anzahl der Beobachtungen, also der Zeilen
- $$k$$ ist die Anzahl der Merkmale, also der Spalten
- $$\mu$$ ist der Mittelwertvektor der Spalten
- $$\sigma$$ ist der Vektor ihrer Standardabweichungen

Visible text: - ist die Merkmalsmatrix der Größe 
- ist die Anzahl der Beobachtungen, also der Zeilen
- ist die Anzahl der Merkmale, also der Spalten
- ist der Mittelwertvektor der Spalten
- ist der Vektor ihrer Standardabweichungen

File: z_transform.py
```python
import numpy as np

# Beispieldaten erstellen: fünf Beobachtungen mit je drei Merkmalen
np.random.seed(42)
X = np.random.randn(5, 3) * 10 + 50  # Daten mit Mittelwert~50, std~10

print("Originaldaten:")
print(X)
print(f"Datenform: {X.shape}")

# Mittelwert und Standardabweichung für jede Spalte berechnen
mu = np.mean(X, axis=0)  # Mittelwert jeder Spalte
sigma = np.std(X, axis=0)  # Standardabweichung jeder Spalte

print(f"Mittelwert jedes Merkmals: {mu}")
print(f"Standardabweichung jedes Merkmals: {sigma}")

# Z-Transformation durchführen
Z = (X - mu) / sigma

print("Daten nach Z-Transformation:")
print(Z)

# Ergebnis der Standardisierung prüfen
print("Überprüfung der Standardisierung:")
print(f"Neuer Mittelwert: {np.mean(Z, axis=0)}")  # Sollte in der Nähe von 0 liegen
print(f"Neue Standardabweichung: {np.std(Z, axis=0)}")  # Sollte in der Nähe von 1 liegen
# Neue Mittelwerte: [ 1.24344979e-15  8.88178420e-17 -1.77635684e-16] (nahe 0)
# Neue Standardabweichungen: [1. 1. 1.]
```

Die Standardisierung verhindert, dass allein unterschiedliche Einheiten große Skalenunterschiede verursachen. Sie garantiert weder gleiche Bedeutung noch gleichen Einfluss, denn beides hängt weiterhin vom Modell und von den Daten ab. Körpergröße in Zentimetern und Gewicht in Kilogramm werden nach der Standardisierung beispielsweise numerisch vergleichbar.

Weitere Erklärungen und Beispiele findest du in der [offiziellen NumPy-Dokumentation](https://numpy.org/doc/stable/user/basics.html).