10. NumPy#

« Soyons clairs : le travail de la science n’a rien à voir avec le consensus. Le consensus est l’affaire de la politique. La science, au contraire, ne requiert qu’un seul chercheur qui se trouve avoir raison, ce qui signifie qu’il ou elle dispose de résultats vérifiables par référence au monde réel. En science, le consensus n’a aucune importance. Ce qui importe, ce sont des résultats reproductibles. » – Michael Crichton

En plus de ce qui est inclus dans Anaconda, ce cours nécessitera les bibliothèques suivantes :

!pip install quantecon

Hide code cell output

Requirement already satisfied: quantecon in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.4)
Requirement already satisfied: numba>=0.49.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (0.65.1)
Requirement already satisfied: numpy>=1.17.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.4.6)
Requirement already satisfied: requests in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.34.2)
Requirement already satisfied: scipy>=1.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.17.1)
Requirement already satisfied: sympy in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.14.0)
Requirement already satisfied: llvmlite<0.48,>=0.47.0dev0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numba>=0.49.0->quantecon) (0.47.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2026.5.20)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sympy->quantecon) (1.3.0)

10.1. Vue d’ensemble#

NumPy est une bibliothèque de premier ordre pour la programmation numérique

  • Largement utilisée dans le monde universitaire, la finance et l’industrie.

  • Mature, rapide, stable et en développement continu.

Nous avons déjà vu du code impliquant NumPy dans les cours précédents.

Dans ce cours, nous allons entamer une discussion plus systématique de

  1. les tableaux NumPy et

  2. les opérations fondamentales de traitement de tableaux fournies par NumPy.

(Pour une référence alternative, consultez la documentation officielle de NumPy.)

Nous utiliserons les importations suivantes.

import numpy as np
import random
import quantecon as qe
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.axes3d import Axes3D
from matplotlib import cm

10.2. Tableaux NumPy#

Le problème essentiel que NumPy résout est le traitement rapide de tableaux.

La structure la plus importante que NumPy définit est un type de données tableau, formellement appelé numpy.ndarray.

Les tableaux NumPy alimentent une très grande proportion de l’écosystème scientifique de Python.

10.2.1. Notions de base#

Pour créer un tableau NumPy ne contenant que des zéros, nous utilisons np.zeros

a = np.zeros(3)
a
array([0., 0., 0.])
type(a)
numpy.ndarray

Les tableaux NumPy ressemblent quelque peu aux listes natives de Python, sauf que

  • Les données doivent être homogènes (tous les éléments du même type).

  • Ces types doivent être l’un des types de données (dtypes) fournis par NumPy.

Les plus importants de ces dtypes sont :

  • float64 : nombre à virgule flottante sur 64 bits

  • int64 : entier sur 64 bits

  • bool : True ou False sur 8 bits

Il existe également des dtypes pour représenter les nombres complexes, les entiers non signés, etc.

Sur les machines modernes, le dtype par défaut pour les tableaux est float64

a = np.zeros(3)
type(a[0])
numpy.float64

Si nous voulons utiliser des entiers, nous pouvons le spécifier comme suit :

a = np.zeros(3, dtype=int)
type(a[0])
numpy.int64

10.2.2. Forme et dimension#

Considérons l’affectation suivante

z = np.zeros(10)

Ici z est un tableau plat — ni vecteur ligne ni vecteur colonne.

z.shape
(10,)

Ici, le tuple de forme n’a qu’un seul élément, qui est la longueur du tableau (les tuples à un seul élément se terminent par une virgule).

Pour lui donner une dimension supplémentaire, nous pouvons modifier l’attribut shape

z.shape = (10, 1)   # Convertit le tableau plat en vecteur colonne (bidimensionnel)
z
array([[0.],
       [0.],
       [0.],
       [0.],
       [0.],
       [0.],
       [0.],
       [0.],
       [0.],
       [0.]])
z = np.zeros(4)     # Tableau plat
z.shape = (2, 2)    # Tableau bidimensionnel
z
array([[0., 0.],
       [0., 0.]])

Dans le dernier cas, pour créer le tableau 2x2, nous pourrions aussi passer un tuple à la fonction zeros(), comme dans z = np.zeros((2, 2)).

10.2.3. Création de tableaux#

Comme nous l’avons vu, la fonction np.zeros crée un tableau de zéros.

Vous pouvez probablement deviner ce que crée np.ones.

Une fonction apparentée est np.empty, qui crée des tableaux en mémoire qui peuvent ensuite être remplis de données

z = np.empty(3)
z
array([0., 0., 0.])

Les nombres que vous voyez ici sont des valeurs parasites.

(Python alloue 3 morceaux contigus de mémoire de 64 bits, et le contenu existant de ces emplacements mémoire est interprété comme des valeurs float64)

Pour établir une grille de nombres régulièrement espacés, utilisez np.linspace

z = np.linspace(2, 4, 5)  # De 2 à 4, avec 5 éléments

Pour créer une matrice identité, utilisez soit np.identity, soit np.eye

z = np.identity(2)
z
array([[1., 0.],
       [0., 1.]])

De plus, les tableaux NumPy peuvent être créés à partir de listes Python, de tuples, etc. en utilisant np.array

z = np.array([10, 20])                 # ndarray à partir d'une liste Python
z
array([10, 20])
type(z)
numpy.ndarray
z = np.array((10, 20), dtype=float)    # Ici 'float' est équivalent à 'np.float64'
z
array([10., 20.])
z = np.array([[1, 2], [3, 4]])         # Tableau 2D à partir d'une liste de listes
z
array([[1, 2],
       [3, 4]])

Voir aussi np.asarray, qui remplit une fonction similaire, mais ne fait pas de copie distincte des données déjà présentes dans un tableau NumPy.

Pour lire les données d’un tableau à partir d’un fichier texte contenant des données numériques, utilisez np.loadtxt — voir la documentation pour plus de détails.

10.2.4. Indexation de tableaux#

Pour un tableau plat, l’indexation est la même que pour les séquences Python :

z = np.linspace(1, 2, 5)
z
array([1.  , 1.25, 1.5 , 1.75, 2.  ])
z[0]
np.float64(1.0)
z[0:2]  # Deux éléments, en commençant par l'élément 0
array([1.  , 1.25])
z[-1]
np.float64(2.0)

Pour les tableaux 2D, la syntaxe d’indexation est la suivante :

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

Et ainsi de suite.

Les colonnes et les lignes peuvent être extraites comme suit

z[0, :]
array([1, 2])
z[:, 1]
array([2, 4])

Les tableaux NumPy d’entiers peuvent aussi être utilisés pour extraire des éléments

z = np.linspace(2, 4, 5)
z
array([2. , 2.5, 3. , 3.5, 4. ])
indices = np.array((0, 2, 3))
z[indices]
array([2. , 3. , 3.5])

Enfin, un tableau de dtype bool peut être utilisé pour extraire des éléments

z
array([2. , 2.5, 3. , 3.5, 4. ])
d = np.array([0, 1, 1, 0, 0], dtype=bool)
d
array([False,  True,  True, False, False])
z[d]
array([2.5, 3. ])

Nous verrons ci-dessous pourquoi cela est utile.

Une remarque en passant : tous les éléments d’un tableau peuvent être fixés à un même nombre en utilisant la notation de tranche

z = np.empty(3)
z
array([2. , 3. , 3.5])
z[:] = 42
z
array([42., 42., 42.])

10.2.5. Méthodes des tableaux#

Les tableaux disposent de méthodes utiles, toutes soigneusement optimisées

a = np.array((4, 3, 2, 1))
a
array([4, 3, 2, 1])
a.sort()              # Trie a sur place
a
array([1, 2, 3, 4])
a.sum()               # Somme
np.int64(10)
a.mean()              # Moyenne
np.float64(2.5)
a.max()               # Max
np.int64(4)
a.argmax()            # Renvoie l'indice de l'élément maximal
np.int64(3)
a.cumsum()            # Somme cumulée des éléments de a
array([ 1,  3,  6, 10])
a.cumprod()           # Produit cumulé des éléments de a
array([ 1,  2,  6, 24])
a.var()               # Variance
np.float64(1.25)
a.std()               # Écart-type
np.float64(1.118033988749895)
a.shape = (2, 2)
a.T                   # Équivalent à a.transpose()
array([[1, 3],
       [2, 4]])

Une autre méthode qui vaut la peine d’être connue est searchsorted().

Si z est un tableau non décroissant, alors z.searchsorted(a) renvoie l’indice du premier élément de z qui est >= a

z = np.linspace(2, 4, 5)
z
array([2. , 2.5, 3. , 3.5, 4. ])
z.searchsorted(2.2)
np.int64(1)

10.3. Opérations arithmétiques#

Les opérateurs +, -, *, / et ** agissent tous élément par élément sur les tableaux

a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])
a + b
array([ 6,  8, 10, 12])
a * b
array([ 5, 12, 21, 32])

Nous pouvons ajouter un scalaire à chaque élément comme suit

a + 10
array([11, 12, 13, 14])

La multiplication scalaire est similaire

a * 10
array([10, 20, 30, 40])

Les tableaux bidimensionnels suivent les mêmes règles générales

A = np.ones((2, 2))
B = np.ones((2, 2))
A + B
array([[2., 2.],
       [2., 2.]])
A + 10
array([[11., 11.],
       [11., 11.]])
A * B
array([[1., 1.],
       [1., 1.]])

En particulier, A * B n’est pas le produit matriciel, c’est un produit élément par élément.

10.4. Multiplication matricielle#

Nous utilisons le symbole @ pour la multiplication matricielle, comme suit :

A = np.ones((2, 2))
B = np.ones((2, 2))
A @ B
array([[2., 2.],
       [2., 2.]])

La syntaxe fonctionne avec des tableaux plats — NumPy devine intelligemment ce que vous voulez :

A @ (0, 1)
array([1., 1.])

Comme nous effectuons une post-multiplication, le tuple est traité comme un vecteur colonne.

10.5. Broadcasting#

(Cette section prolonge une excellente discussion du broadcasting fournie par Jake VanderPlas.)

Note

Le broadcasting est un aspect très important de NumPy. En même temps, le broadcasting avancé est relativement complexe et certains des détails ci-dessous peuvent être survolés en première lecture.

Dans les opérations élément par élément, les tableaux peuvent ne pas avoir la même forme.

Lorsque cela se produit, NumPy étendra automatiquement les tableaux à la même forme chaque fois que possible.

Cette fonctionnalité utile (mais parfois déroutante) de NumPy s’appelle broadcasting.

L’intérêt du broadcasting est que

  • les boucles for peuvent être évitées, ce qui aide le code numérique à s’exécuter rapidement et

  • le broadcasting peut nous permettre de mettre en œuvre des opérations sur des tableaux sans réellement créer certaines dimensions de ces tableaux en mémoire, ce qui peut être important lorsque les tableaux sont grands.

Par exemple, supposons que a soit un tableau \(3 \times 3\) (a -> (3, 3)), tandis que b est un tableau plat de trois éléments (b -> (3,)).

Lors de leur addition, NumPy étendra automatiquement b -> (3,) en b -> (3, 3).

L’addition élément par élément produira un tableau \(3 \times 3\)

a = np.array(
        [[1, 2, 3], 
         [4, 5, 6], 
         [7, 8, 9]])
b = np.array([3, 6, 9])

a + b
array([[ 4,  8, 12],
       [ 7, 11, 15],
       [10, 14, 18]])

Voici une représentation visuelle de cette opération de broadcasting :

Hide code cell source

# Adapté et modifié à partir du code du livre écrit par Jake VanderPlas (voir https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Broadcasting)
# Provient à l'origine d'astroML : voir https://www.astroml.org/book_figures/appendix/fig_broadcast_visual.html


def draw_cube(ax, xy, size, depth=0.4,
              edges=None, label=None, label_kwargs=None, **kwargs):
    """draw and label a cube.  edges is a list of numbers between
    1 and 12, specifying which of the 12 cube edges to draw"""
    if edges is None:
        edges = range(1, 13)

    x, y = xy

    if 1 in edges:
        ax.plot([x, x + size],
                [y + size, y + size], **kwargs)
    if 2 in edges:
        ax.plot([x + size, x + size],
                [y, y + size], **kwargs)
    if 3 in edges:
        ax.plot([x, x + size],
                [y, y], **kwargs)
    if 4 in edges:
        ax.plot([x, x],
                [y, y + size], **kwargs)

    if 5 in edges:
        ax.plot([x, x + depth],
                [y + size, y + depth + size], **kwargs)
    if 6 in edges:
        ax.plot([x + size, x + size + depth],
                [y + size, y + depth + size], **kwargs)
    if 7 in edges:
        ax.plot([x + size, x + size + depth],
                [y, y + depth], **kwargs)
    if 8 in edges:
        ax.plot([x, x + depth],
                [y, y + depth], **kwargs)

    if 9 in edges:
        ax.plot([x + depth, x + depth + size],
                [y + depth + size, y + depth + size], **kwargs)
    if 10 in edges:
        ax.plot([x + depth + size, x + depth + size],
                [y + depth, y + depth + size], **kwargs)
    if 11 in edges:
        ax.plot([x + depth, x + depth + size],
                [y + depth, y + depth], **kwargs)
    if 12 in edges:
        ax.plot([x + depth, x + depth],
                [y + depth, y + depth + size], **kwargs)

    if label:
        if label_kwargs is None:
            label_kwargs = {}
        ax.text(x + 0.5 * size, y + 0.5 * size, label,
                ha='center', va='center', **label_kwargs)

solid = dict(c='black', ls='-', lw=1,
             label_kwargs=dict(color='k'))
dotted = dict(c='black', ls='-', lw=0.5, alpha=0.5,
              label_kwargs=dict(color='gray'))
depth = 0.3

# Dessine une figure et un axe sans bordure
fig = plt.figure(figsize=(5, 1), facecolor='w')
ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False)

# premier bloc
draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid)
draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid)
draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid)

draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid)
draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid)
draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid)

draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid)
draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid)
draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid)

# deuxième bloc
draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid)
draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid)
draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid)

draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted)
draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted)
draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted)

draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted)
draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted)
draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted)

# troisième bloc
draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid)
draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid)
draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '12', **solid)

draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '7', **solid)
draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid)
draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '15', **solid)

draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '10', **solid)
draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '14', **solid)
draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid)

ax.text(5, 7.0, '+', size=12, ha='center', va='center')
ax.text(10.5, 7.0, '=', size=12, ha='center', va='center');
_images/f78ac25a907cd4127f9d36ec7563bb78949a11c5db71995f7afc0a093b394882.png

Qu’en est-il de b -> (3, 1) ?

Dans ce cas, NumPy étendra automatiquement b -> (3, 1) en b -> (3, 3).

L’addition élément par élément produira alors une matrice \(3 \times 3\)

b.shape = (3, 1)

a + b
array([[ 4,  5,  6],
       [10, 11, 12],
       [16, 17, 18]])

Voici une représentation visuelle de cette opération de broadcasting :

Hide code cell source

fig = plt.figure(figsize=(5, 1), facecolor='w')
ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False)

# premier bloc
draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid)
draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '2', **solid)
draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **solid)

draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid)
draw_cube(ax, (2, 6.5), 1, depth, [2, 3], '5', **solid)
draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 7, 10], '6', **solid)

draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid)
draw_cube(ax, (2, 5.5), 1, depth, [2, 3], '8', **solid)
draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10], '9', **solid)

# deuxième bloc
draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '3', **solid)
draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted)
draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '3', **dotted)

draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '6', **solid)
draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted)
draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted)

draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '9', **solid)
draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted)
draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted)

# troisième bloc
draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '4', **solid)
draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '5', **solid)
draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '6', **solid)

draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '10', **solid)
draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '11', **solid)
draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid)

draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '16', **solid)
draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '17', **solid)
draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '18', **solid)

ax.text(5, 7.0, '+', size=12, ha='center', va='center')
ax.text(10.5, 7.0, '=', size=12, ha='center', va='center');
_images/e9f8b3fa8db8ad15ee1181f45d4708f1c66ca33d940b7f36584c3764d9ab94ba.png

Dans certains cas, les deux opérandes seront étendus.

Lorsque nous avons a -> (3,) et b -> (3, 1), a sera étendu en a -> (3, 3), et b sera étendu en b -> (3, 3).

Dans ce cas, l’addition élément par élément produira une matrice \(3 \times 3\)

a = np.array([3, 6, 9])
b = np.array([2, 3, 4])
b.shape = (3, 1)

a + b
array([[ 5,  8, 11],
       [ 6,  9, 12],
       [ 7, 10, 13]])

Voici une représentation visuelle de cette opération de broadcasting :

Hide code cell source

# Dessine une figure et un axe sans bordure
fig = plt.figure(figsize=(5, 1), facecolor='w')
ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False)

# premier bloc
draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid)
draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid)
draw_cube(ax, (3, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid)

draw_cube(ax, (1, 6.5), 1, depth, range(2, 13), '3', **dotted)
draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted)
draw_cube(ax, (3, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted)

draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted)
draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted)
draw_cube(ax, (3, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted)

# deuxième bloc
draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 7, 9, 10], '2', **solid)
draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted)
draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **dotted)

draw_cube(ax, (6, 6.5), 1, depth, [2, 3, 4, 7, 10], '3', **solid)
draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted)
draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '3', **dotted)

draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 10], '4', **solid)
draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted)
draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '4', **dotted)

# troisième bloc
draw_cube(ax, (12, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '5', **solid)
draw_cube(ax, (13, 7.5), 1, depth, [1, 2, 3, 6, 9], '8', **solid)
draw_cube(ax, (14, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '11', **solid)

draw_cube(ax, (12, 6.5), 1, depth, [2, 3, 4], '6', **solid)
draw_cube(ax, (13, 6.5), 1, depth, [2, 3], '9', **solid)
draw_cube(ax, (14, 6.5), 1, depth, [2, 3, 7, 10], '12', **solid)

draw_cube(ax, (12, 5.5), 1, depth, [2, 3, 4], '7', **solid)
draw_cube(ax, (13, 5.5), 1, depth, [2, 3], '10', **solid)
draw_cube(ax, (14, 5.5), 1, depth, [2, 3, 7, 10], '13', **solid)

ax.text(5, 7.0, '+', size=12, ha='center', va='center')
ax.text(10.5, 7.0, '=', size=12, ha='center', va='center');
_images/3f471ff12e5720be105ce6fcfb0d667699b0b0a4fa3abfe190c81e11681550d4.png

Bien que le broadcasting soit très utile, il peut parfois sembler déroutant.

Par exemple, essayons d’additionner a -> (3, 2) et b -> (3,).

a = np.array(
      [[1, 2],
       [4, 5],
       [7, 8]])
b = np.array([3, 6, 9])

a + b
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[62], line 7
      3        [4, 5],
      4        [7, 8]])
      5 b = np.array([3, 6, 9])
      6 
----> 7 a + b

ValueError: operands could not be broadcast together with shapes (3,2) (3,) 

La ValueError nous indique que les opérandes n’ont pas pu être broadcastées ensemble.

Voici une représentation visuelle pour montrer pourquoi ce broadcasting ne peut pas être exécuté :

Hide code cell source

# Dessine une figure et un axe sans bordure
fig = plt.figure(figsize=(3, 1.3), facecolor='w')
ax = plt.axes([0, 0, 1, 1], xticks=[], yticks=[], frameon=False)

# premier bloc
draw_cube(ax, (1, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '1', **solid)
draw_cube(ax, (2, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '2', **solid)

draw_cube(ax, (1, 6.5), 1, depth, [2, 3, 4], '4', **solid)
draw_cube(ax, (2, 6.5), 1, depth, [2, 3, 7, 10], '5', **solid)

draw_cube(ax, (1, 5.5), 1, depth, [2, 3, 4], '7', **solid)
draw_cube(ax, (2, 5.5), 1, depth, [2, 3, 7, 10], '8', **solid)

# deuxième bloc
draw_cube(ax, (6, 7.5), 1, depth, [1, 2, 3, 4, 5, 6, 9], '3', **solid)
draw_cube(ax, (7, 7.5), 1, depth, [1, 2, 3, 6, 9], '6', **solid)
draw_cube(ax, (8, 7.5), 1, depth, [1, 2, 3, 6, 7, 9, 10], '9', **solid)

draw_cube(ax, (6, 6.5), 1, depth, range(2, 13), '3', **dotted)
draw_cube(ax, (7, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '6', **dotted)
draw_cube(ax, (8, 6.5), 1, depth, [2, 3, 6, 7, 9, 10, 11], '9', **dotted)

draw_cube(ax, (6, 5.5), 1, depth, [2, 3, 4, 7, 8, 10, 11, 12], '3', **dotted)
draw_cube(ax, (7, 5.5), 1, depth, [2, 3, 7, 10, 11], '6', **dotted)
draw_cube(ax, (8, 5.5), 1, depth, [2, 3, 7, 10, 11], '9', **dotted)


ax.text(4.5, 7.0, '+', size=12, ha='center', va='center')
ax.text(10, 7.0, '=', size=12, ha='center', va='center')
ax.text(11, 7.0, '?', size=16, ha='center', va='center');
_images/9f08cba987f6199dbe21a318bb189181de5ef9404c6e5d18498528b9ae3adb26.png

Nous pouvons voir que NumPy ne peut pas étendre les tableaux à la même taille.

C’est parce que, lorsque b est étendu de b -> (3,) à b -> (3, 3), NumPy ne peut pas faire correspondre b avec a -> (3, 2).

Les choses deviennent encore plus délicates lorsque nous passons à des dimensions supérieures.

Pour nous aider, nous pouvons utiliser la liste de règles suivante :

  • Étape 1 : Lorsque les dimensions de deux tableaux ne correspondent pas, NumPy étend celui qui a le moins de dimensions en ajoutant une ou plusieurs dimensions à gauche des dimensions existantes.

    • Par exemple, si a -> (3, 3) et b -> (3,), alors le broadcasting ajoutera une dimension à gauche de sorte que b -> (1, 3) ;

    • Si a -> (2, 2, 2) et b -> (2, 2), alors le broadcasting ajoutera une dimension à gauche de sorte que b -> (1, 2, 2) ;

    • Si a -> (3, 2, 2) et b -> (2,), alors le broadcasting ajoutera deux dimensions à gauche de sorte que b -> (1, 1, 2) (on peut aussi voir ce processus comme un passage par l”Étape 1 deux fois).

  • Étape 2 : Lorsque les deux tableaux ont la même dimension mais des formes différentes, NumPy tentera d’étendre les dimensions où l’indice de forme est 1.

    • Par exemple, si a -> (1, 3) et b -> (3, 1), alors le broadcasting étendra les dimensions de forme 1 dans a et b de sorte que a -> (3, 3) et b -> (3, 3) ;

    • Si a -> (2, 2, 2) et b -> (1, 2, 2), alors le broadcasting étendra la première dimension de b de sorte que b -> (2, 2, 2) ;

    • Si a -> (3, 2, 2) et b -> (1, 1, 2), alors le broadcasting étendra b sur toutes les dimensions de forme 1 de sorte que b -> (3, 2, 2).

  • Étape 3 : Après les étapes 1 et 2, si les deux tableaux ne correspondent toujours pas, une ValueError sera levée. Par exemple, supposons a -> (2, 2, 3) et b -> (2, 2)

    • Par l”Étape 1, b sera étendu en b -> (1, 2, 2) ;

    • Par l”Étape 2, b sera étendu en b -> (2, 2, 2) ;

    • Nous pouvons voir qu’ils ne correspondent pas l’un à l’autre après les deux premières étapes. Ainsi, une ValueError sera levée

10.6. Mutabilité et copie de tableaux#

Les tableaux NumPy sont des types de données mutables, comme les listes Python.

En d’autres termes, leur contenu peut être modifié (muté) en mémoire après l’initialisation.

C’est pratique mais, combiné au modèle de nommage et de référence de Python, cela peut conduire à des erreurs chez les débutants en NumPy.

Dans cette section, nous passons en revue quelques points clés.

10.6.1. Mutabilité#

Nous avons déjà vu des exemples de mutabilité ci-dessus.

Voici un autre exemple de mutation d’un tableau NumPy

a = np.array([42, 44])
a
array([42, 44])
a[-1] = 0  # Change le dernier élément en 0
a
array([42,  0])

La mutabilité conduit au comportement suivant (qui peut être choquant pour les programmeurs MATLAB…)

rng = np.random.default_rng()
a = rng.standard_normal(3)
a
array([ 0.82706403,  1.36260144, -1.67770637])
b = a
b[0] = 0.0
a
array([ 0.        ,  1.36260144, -1.67770637])

Ce qui s’est passé, c’est que nous avons modifié a en modifiant b.

Le nom b est lié à a et devient simplement une autre référence au tableau (le modèle d’affectation de Python est décrit plus en détail plus loin dans le cours).

Il a donc des droits égaux pour apporter des modifications à ce tableau.

C’est en fait le comportement par défaut le plus sensé !

Cela signifie que nous ne faisons circuler que des pointeurs vers les données, plutôt que de faire des copies.

Faire des copies est coûteux à la fois en termes de vitesse et de mémoire.

10.6.2. Faire des copies#

Il est bien sûr possible de faire de b une copie indépendante de a lorsque cela est nécessaire.

Cela peut être fait en utilisant np.copy

a = rng.standard_normal(3)
a
array([-1.99303188,  0.91306177, -0.58199588])
b = np.copy(a)
b
array([-1.99303188,  0.91306177, -0.58199588])

Maintenant b est une copie indépendante (appelée copie profonde)

b[:] = 1
b
array([1., 1., 1.])
a
array([-1.99303188,  0.91306177, -0.58199588])

Notez que la modification de b n’a pas affecté a.

10.7. Fonctionnalités supplémentaires#

Examinons quelques autres fonctionnalités utiles de NumPy.

10.7.1. Fonctions universelles#

NumPy fournit des versions des fonctions standard log, exp, sin, etc. qui agissent élément par élément sur les tableaux

z = np.array([1, 2, 3])
np.sin(z)
array([0.84147098, 0.90929743, 0.14112001])

Cela élimine le besoin de boucles explicites élément par élément telles que

n = len(z)
y = np.empty(n)
for i in range(n):
    y[i] = np.sin(z[i])

Parce qu’elles agissent élément par élément sur les tableaux, ces fonctions sont parfois appelées fonctions vectorisées.

Dans le jargon de NumPy, elles sont aussi appelées ufuncs, ou fonctions universelles.

Comme nous l’avons vu ci-dessus, les opérations arithmétiques usuelles (+, *, etc.) fonctionnent également élément par élément, et combiner celles-ci avec les ufuncs donne un très large ensemble de fonctions rapides opérant élément par élément.

z
array([1, 2, 3])
(1 / np.sqrt(2 * np.pi)) * np.exp(- 0.5 * z**2)
array([0.24197072, 0.05399097, 0.00443185])

Toutes les fonctions définies par l’utilisateur n’agissent pas élément par élément.

Par exemple, passer un tableau NumPy à la fonction f définie ci-dessous provoque une ValueError

def f(x):
    return 1 if x > 0 else 0

La fonction NumPy np.where fournit une alternative vectorisée :

x = rng.standard_normal(4)
x
array([-1.04361928, -1.46393278, -0.16153239, -0.16967398])
np.where(x > 0, 1, 0)  # Insère 1 si x > 0 est vrai, sinon 0
array([0, 0, 0, 0])

Vous pouvez aussi utiliser np.vectorize pour vectoriser une fonction donnée

f = np.vectorize(f)
f(x)                # Passe le même vecteur x que dans l'exemple précédent
array([0, 0, 0, 0])

Cependant, cette approche n’atteint pas toujours la même vitesse qu’une fonction vectorisée plus soigneusement conçue.

(Plus tard, nous verrons que JAX dispose d’une version puissante de np.vectorize qui peut générer, et génère généralement, du code hautement efficace.)

10.7.2. Comparaisons#

En règle générale, les comparaisons sur les tableaux sont effectuées élément par élément

z = np.array([2, 3])
y = np.array([2, 3])
z == y
array([ True,  True])
y[0] = 5
z == y
array([False,  True])
z != y
array([ True, False])

La situation est similaire pour >, <, >= et <=.

Nous pouvons aussi effectuer des comparaisons avec des scalaires

z = np.linspace(0, 10, 5)
z
array([ 0. ,  2.5,  5. ,  7.5, 10. ])
z > 3
array([False, False,  True,  True,  True])

Ceci est particulièrement utile pour l”extraction conditionnelle

b = z > 3
b
array([False, False,  True,  True,  True])
z[b]
array([ 5. ,  7.5, 10. ])

Bien sûr, nous pouvons — et faisons souvent — effectuer ceci en une seule étape

z[z > 3]
array([ 5. ,  7.5, 10. ])

10.7.3. Sous-packages#

NumPy fournit des fonctionnalités supplémentaires liées à la programmation scientifique via ses sous-packages.

Nous avons déjà vu comment nous pouvons générer des variables aléatoires en utilisant le Generator aléatoire de NumPy.

z = rng.standard_normal(10000)  # Génère des variables normales centrées réduites
y = rng.binomial(10, 0.5, size=1000)    # 1 000 tirages de Bin(10, 0.5)
y.mean()
np.float64(4.982)

Un autre sous-package couramment utilisé est np.linalg

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

np.linalg.det(A)           # Calcule le déterminant
np.float64(-2.0000000000000004)
np.linalg.inv(A)           # Calcule l'inverse
array([[-2. ,  1. ],
       [ 1.5, -0.5]])

Une grande partie de ces fonctionnalités est aussi disponible dans SciPy, une collection de modules qui sont construits par-dessus NumPy.

Nous couvrirons les versions SciPy plus en détail bientôt.

Pour une liste complète de ce qui est disponible dans NumPy, consultez cette documentation.

10.7.4. Multithreading implicite#

Précédemment, nous avons abordé le concept de parallélisation via le multithreading.

NumPy tente d’implémenter le multithreading dans une grande partie de son code compilé.

Examinons un exemple pour voir cela en action.

Le morceau de code suivant calcule les valeurs propres d’un grand nombre de matrices générées aléatoirement.

Son exécution prend quelques secondes.

n = 20
m = 1000
for i in range(n):
    X = rng.standard_normal((m, m))
    λ = np.linalg.eigvals(X)

Maintenant, examinons la sortie du moniteur système htop sur notre machine pendant que ce code s’exécute :

_images/htop_parallel_npmat.png

Nous pouvons voir que 4 des 8 processeurs fonctionnent à pleine vitesse.

Cela est dû au fait que la routine eigvals de NumPy divise proprement les tâches et les distribue à différents threads.

10.8. Exercices#

Exercice 10.1

Considérons l’expression polynomiale

(10.1)#\[p(x) = a_0 + a_1 x + a_2 x^2 + \cdots a_N x^N = \sum_{n=0}^N a_n x^n\]

Plus tôt, vous avez écrit une fonction simple p(x, coeff) pour évaluer (10.1) sans tenir compte de l’efficacité.

Écrivez maintenant une nouvelle fonction qui effectue le même travail, mais qui utilise des tableaux NumPy et des opérations sur tableaux pour ses calculs, plutôt qu’une quelconque forme de boucle Python.

(Une telle fonctionnalité est déjà implémentée sous la forme de np.poly1d, mais pour les besoins de l’exercice, n’utilisez pas cette classe)

Exercice 10.2

Soit q un tableau NumPy de longueur n avec q.sum() == 1.

Supposons que q représente une fonction de masse de probabilité.

Nous souhaitons générer une variable aléatoire discrète \(x\) telle que \(\mathbb P\{x = i\} = q_i\).

En d’autres termes, x prend des valeurs dans range(len(q)) et x = i avec probabilité q[i].

L’algorithme standard (de transformation inverse) est le suivant :

  • Divisez l’intervalle unité \([0, 1]\) en \(n\) sous-intervalles \(I_0, I_1, \ldots, I_{n-1}\) tels que la longueur de \(I_i\) soit \(q_i\).

  • Tirez une variable aléatoire uniforme \(U\) sur \([0, 1]\) et renvoyez le \(i\) tel que \(U \in I_i\).

La probabilité de tirer \(i\) est la longueur de \(I_i\), qui est égale à \(q_i\).

Nous pouvons implémenter l’algorithme comme suit

from random import uniform

def sample(q):
    a = 0.0
    U = uniform(0, 1)
    for i in range(len(q)):
        if a < U <= a + q[i]:
            return i
        a = a + q[i]

Si vous ne voyez pas comment cela fonctionne, essayez de suivre le flux pour un exemple simple, tel que q = [0.25, 0.75] Il est utile de dessiner les intervalles sur papier.

Votre exercice consiste à l’accélérer en utilisant NumPy, en évitant les boucles explicites

Si vous le pouvez, implémentez la fonctionnalité sous la forme d’une classe appelée DiscreteRV, où

  • les données pour une instance de la classe sont le vecteur de probabilités q

  • la classe dispose d’une méthode draw(), qui renvoie un tirage selon l’algorithme décrit ci-dessus

Si vous le pouvez, écrivez la méthode de sorte que draw(k) renvoie k tirages de q.

Exercice 10.3

Rappelez-vous notre discussion précédente sur la fonction de répartition empirique.

Votre tâche consiste à

  1. Rendre la méthode __call__ plus efficace en utilisant NumPy.

  2. Ajouter une méthode qui trace la FDE sur \([a, b]\), où \(a\) et \(b\) sont des paramètres de la méthode.

Exercice 10.4

Rappelez-vous que le broadcasting dans NumPy peut nous aider à effectuer des opérations élément par élément sur des tableaux ayant un nombre de dimensions différent sans utiliser de boucles for.

Dans cet exercice, essayez d’utiliser des boucles for pour reproduire le résultat des opérations de broadcasting suivantes.

Partie 1 : Essayez de reproduire cet exemple simple en utilisant des boucles for et comparez vos résultats avec l’opération de broadcasting ci-dessous.

rng = np.random.default_rng(123)
x = rng.standard_normal((4, 4))
y = rng.standard_normal(4)
A = x / y

Voici la sortie

print(A)

Hide code cell output

[[-0.64562663  0.55727833 -4.13068155  0.57428108]
 [ 0.60065994 -0.87444021  2.04128976  1.60450491]
 [-0.20665053  0.488491   -0.31163862 -4.51767283]
 [ 0.77815951  1.01684966 -3.20810109  0.40359261]]

Partie 2 : Passez à la reproduction du résultat de l’opération de broadcasting suivante. Parallèlement, comparez les vitesses du broadcasting et de la boucle for que vous implémentez.

Pour cette partie de l’exercice, vous pouvez utiliser les fonctions tic/toc de la bibliothèque quantecon pour chronométrer l’exécution.

Assurons-nous que cette bibliothèque est installée.

!pip install quantecon

Hide code cell output

Requirement already satisfied: quantecon in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.4)
Requirement already satisfied: numba>=0.49.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (0.65.1)
Requirement already satisfied: numpy>=1.17.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.4.6)
Requirement already satisfied: requests in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.34.2)
Requirement already satisfied: scipy>=1.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.17.1)
Requirement already satisfied: sympy in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.14.0)
Requirement already satisfied: llvmlite<0.48,>=0.47.0dev0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numba>=0.49.0->quantecon) (0.47.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2026.5.20)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sympy->quantecon) (1.3.0)

Nous pouvons maintenant importer le package quantecon.

rng = np.random.default_rng(123)
x = rng.standard_normal((1000, 100, 100))
y = rng.standard_normal(100)

with qe.Timer("Broadcasting operation"):
    B = x / y
Broadcasting operation: 0.0109 seconds elapsed

Voici la sortie

print(B)

Hide code cell output

[[[ 1.11459186e+00  6.49554639e-01 -1.39222984e+00 ...  1.29919811e+00
    4.05186703e+00 -7.40287138e+01]
  [ 5.81192956e-01 -2.92842384e+00  3.21336857e-01 ...  1.20991613e+00
    2.27427587e+00  1.44385102e+02]
  [ 1.05593413e+00  1.42938670e+00  4.45508796e-01 ...  4.01152744e-01
    2.84732007e+00  8.92867644e+01]
  ...
  [-2.74886903e-02  1.26977472e+00  3.14108995e-01 ...  1.11710121e-01
    1.42995646e+00 -3.11820183e+01]
  [ 2.37232237e-02 -1.07344880e+00 -6.84853323e-02 ...  1.34568899e-01
   -1.03734701e-01 -1.06565258e+02]
  [-1.74039841e+00 -3.01228229e+00 -9.49433502e-01 ...  1.44494147e-01
   -7.23108571e+00 -2.90325461e+01]]

 [[ 5.31486218e-01  2.93920784e+00 -2.44783577e-01 ... -6.32917660e-01
   -2.52543447e+00 -5.31568329e+01]
  [-4.73496558e-01  1.49038936e-01  1.68780916e+00 ...  2.05750194e-01
   -1.40524927e+00 -6.14593964e-01]
  [-2.74888585e+00 -2.33570490e+00  3.41515715e-01 ...  1.24702883e-01
   -1.68468042e+00  8.33959001e+01]
  ...
  [-8.36506904e-01 -1.86665236e+00  6.00226688e-01 ...  8.54789843e-01
   -1.28668710e+00 -8.83692748e+01]
  [-6.70119507e-01  8.95293052e-02 -5.62763392e-01 ...  1.06747869e+00
    8.32890981e-01  1.32305974e+02]
  [-2.35822588e-01  1.71686185e+00 -6.38798692e-01 ...  7.17992258e-01
    5.11995938e+00  4.32277717e+01]]

 [[-8.17691283e-01 -9.69805518e-01  1.20574471e-02 ... -2.29327510e-01
   -6.87440789e-01 -1.08708489e+02]
  [ 1.32946051e-01  2.53886133e+00  1.00358890e+00 ...  1.52451468e+00
   -7.96146833e+00  4.73065584e+01]
  [ 1.39118740e+00  2.42744267e+00  8.99415032e-01 ... -1.98940955e+00
   -4.09483421e+00  1.58863413e+02]
  ...
  [-2.00083480e+00 -2.39858291e+00 -1.63204799e+00 ...  1.03229680e+00
   -2.92527147e+00  4.97550232e+01]
  [ 2.13075183e-01 -6.94472260e-01 -1.27241435e+00 ... -4.72444580e-02
   -1.36097040e+00 -2.69662919e+01]
  [-6.53406813e-01  1.10261376e+00 -8.36617414e-02 ...  1.43523014e+00
   -2.71989098e+00  4.76036526e+01]]

 ...

 [[ 2.10702762e-01 -2.73232476e+00 -9.47422331e-01 ...  5.53820263e-01
    3.67876353e+00  1.50165202e+00]
  [ 2.01068194e+00  1.23328379e+00  5.01861693e-01 ... -6.20423733e-01
    1.82152397e+00  3.91329194e+00]
  [ 7.84176143e-01 -2.10192441e-02 -7.69297715e-01 ... -1.13855768e-03
    2.80468628e+00 -5.61973382e+01]
  ...
  [-1.24033449e+00  5.90826010e-01 -1.58604105e+00 ... -1.06447092e+00
    4.35766108e+00 -6.65157650e+01]
  [-1.37676146e+00 -2.44562663e+00  3.03635548e-01 ...  1.09674555e+00
    8.51724363e-01  7.62734749e+01]
  [ 2.65023743e-01 -1.60418989e+00  8.92588170e-01 ... -1.52970264e+00
    3.35794715e+00  7.66550051e+01]]

 [[-9.76665830e-01 -2.52931081e-01  9.34827331e-01 ...  1.34729867e+00
   -9.49752368e+00 -5.40493267e+01]
  [-6.99969930e-01  7.16309528e-01 -4.87147176e-01 ...  3.84737379e-01
   -4.62349866e+00  6.57667401e+01]
  [-5.35240129e-01  8.29149751e-01 -1.38663673e-01 ... -5.26523746e-01
   -2.78363901e+00  3.97278864e+01]
  ...
  [ 5.90892302e-01  1.10587017e+00 -1.35634710e+00 ... -1.10155962e+00
    4.79718847e+00  1.38630977e+01]
  [-6.30986628e-01 -3.12261837e-02  1.84942661e-01 ...  2.85786218e+00
   -9.26774215e-01  7.36155149e+01]
  [ 2.35257249e+00 -3.14639965e-01  6.29523883e-01 ... -1.14837361e+00
   -1.06301767e+00  1.30911044e+01]]

 [[-4.05344988e-01 -2.22802559e+00  1.01293008e+00 ... -2.84828321e-01
    4.76669493e+00 -5.22389356e+01]
  [-3.01680393e-01 -3.08400312e-01 -1.13845805e+00 ... -3.54402179e-01
    2.69111708e+00 -9.15315118e+01]
  [ 4.75838349e-01 -3.26362244e+00 -4.72222604e-01 ...  2.99854221e-01
    2.37944637e+00 -1.17700185e+01]
  ...
  [ 1.03535657e+00 -5.74611584e-02  1.97073682e-01 ... -1.09443930e+00
    4.66582536e+00 -2.25831644e+02]
  [-6.71551286e-01  6.27890445e-01 -2.96405648e-01 ...  1.32173888e+00
    3.90295626e+00 -7.43606070e+00]
  [ 1.83056628e+00  4.61028305e-01  9.21541992e-01 ...  6.08721297e-01
    4.43683093e+00 -1.49100072e+02]]]