import numpy as np
from matplotlib import pyplot as plt
contenu de ce notebook (sauter si déjà acquis)¶
- les manières d’accéder à des éléments et de slicer un tableau
numpy
- les slices sont des vues et non des copies
- la notion de
numpy.ndarray.base
- voir les
exercices avancés pour les rapides
accès aux éléments d’un tableau¶
accès à un tableau de dimension 1¶
# le code
tab = np.arange(12)
tab[0] = np.pi
tab[0].dtype, tab[0]
(dtype('int64'), np.int64(3))
# le code
tab1 = tab.astype(np.float64)
tab1[0] = np.pi
tab1[0].dtype, tab1[0]
(dtype('float64'), np.float64(3.141592653589793))
accès à un tableau de dimension > à 1¶
# le code en dimension 2
tab = np.arange(12).reshape((2, 6))
# première ligne, deuxième colonne
line, col = 0, 1
tab[line, col] = 1000
tab
array([[ 0, 1000, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11]])
# le code en dimension 3
tab.resize((2, 3, 2))
# deuxième matrice, troisième ligne, première colonne
mat, line, col = 1, 2, 0
tab[mat, line, col] = 2000
tab
array([[[ 0, 1000],
[ 2, 3],
[ 4, 5]],
[[ 6, 7],
[ 8, 9],
[2000, 11]]])
[tab.shape[i] for i in range(tab.ndim)]
[2, 3, 2]
tab.shape
(2, 3, 2)
exercices¶
accès à un élément
créez un tableau des 30 valeurs paires à partir de 2
donnez lui la forme de 2 matrices de 5 lignes et 3 colonnes
accédez à l’élément qui est à la 3ème colonne de la 2ème ligne de la 1ère matrice
obtenez-vous 12 ?
# votre code
exercice
faites un
np.ndarray
de forme(3, 2, 5, 4)
avec des nombre aéatoires entiers entre 0 et 100affichez-le et vous voyez trois groupes et 2 matrices de 5 lignes et 4 colonnes
affichez le nombre des éléments des deux dernières dimensions
indice
- voyez
np.random.randint
pour créer un tableau aléatoire - tapez
np.random.randint?
pour avoir de l’aide en ligne
# votre code ici
accéder à un sous-tableau (slicing)¶
différence slicing python
et numpy
¶
le slicing numpy
est syntaxiquement équivalent à celui des listes Python
la grande différence est que
quand vous slicez un tableau
numpy
vous obtenez une vue sur le tableau initial
(avec une nouvelle indexation)quand vous slicez une liste
python
vous obtenez une copie de la liste initiale
le slicing numpy
va
- regrouper des éléments du tableau initial
- dans un sous-tableau
numpy.ndarray
avec l’indexation adéquate - la mémoire sous-jacente reste la même
la seule structure informatique qui sera créée est l’indexation
vous pourrez ensuite, par exemple, modifier ces éléments
et donc ils seront modifiés dans le tableau initial
rappel du slicing Python¶
rappel du slicing Python
l[from:to-excluded:step]
paramètres tous optionnels
par défaut:from = 0
to-excluded = len(l)
etstep=1
indices négatifs ok
-1
est le dernier élément,-2
l’avant dernier ...
la liste python des 10 premiers entiers
l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# un élément sur 2 en partant du début de la liste (copie)
l[::2]
# un élément sur 3 en partant du premier élément de la liste (copie)
l[1::3]
# la liste en reverse (copie)
l[::-1]
# la liste entière (copie)
l[::]
# ou
l[:]
# le code
l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
print(l[::2])
print(l[1::3])
print(l[::-1])
print(l[:])
[0, 2, 4, 6, 8]
[1, 4, 7]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
slicing en dimension 1¶
on crée un numpy.ndarray
de dimension 1 de taille 10
- on prend un élément sur 2 en partant du début de la liste
- on modifie les éléments du sous-tableau obtenu
- le tableau initial est modifié
vec = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print(vec[::2]) # [0 2 4 6 8]
vec[::2] = 100
print(vec) # [100, 1, 100, 3, 100, 5, 100, 7, 100, 9]
# le code
vec = np.arange(10)
print(vec[::2])
vec[::2] = 100
vec
[0 2 4 6 8]
array([100, 1, 100, 3, 100, 5, 100, 7, 100, 9])
slicing en dimension > à 1 (a)¶
on crée un numpy.ndarray
en dimension 4, de forme (2, 3, 4, 5)
on l’initialise avec les 120
premiers entiers
tab = np.arange(120).reshape(2, 3, 4, 5)
on a 2 groupes de 3 matrices de 4 lignes et 5 colonnes
- on accède au premier groupe de matrices
tab[0]
- on accède à la deuxième matrice du premier groupe de matrices
tab[0, 1]
- on accède à la troisième ligne de la deuxième matrice du premier groupe de matrices
tab[0, 1, 2]
- on accède à la quatrième colonne de la deuxième matrice du premier groupe de matrices
tab[0, 1, :, 3] # remarquez le ':' pour indiquer toutes les lignes
# le code
tab = np.arange(120).reshape(2, 3, 4, 5)
print( tab )
print( tab[0] )
print( tab[0, 1] )
print( tab[0, 1, 2] )
print( tab[0, 1, :, 3] )
[[[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[ 10 11 12 13 14]
[ 15 16 17 18 19]]
[[ 20 21 22 23 24]
[ 25 26 27 28 29]
[ 30 31 32 33 34]
[ 35 36 37 38 39]]
[[ 40 41 42 43 44]
[ 45 46 47 48 49]
[ 50 51 52 53 54]
[ 55 56 57 58 59]]]
[[[ 60 61 62 63 64]
[ 65 66 67 68 69]
[ 70 71 72 73 74]
[ 75 76 77 78 79]]
[[ 80 81 82 83 84]
[ 85 86 87 88 89]
[ 90 91 92 93 94]
[ 95 96 97 98 99]]
[[100 101 102 103 104]
[105 106 107 108 109]
[110 111 112 113 114]
[115 116 117 118 119]]]]
[[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]]
[[20 21 22 23 24]
[25 26 27 28 29]
[30 31 32 33 34]
[35 36 37 38 39]]
[[40 41 42 43 44]
[45 46 47 48 49]
[50 51 52 53 54]
[55 56 57 58 59]]]
[[20 21 22 23 24]
[25 26 27 28 29]
[30 31 32 33 34]
[35 36 37 38 39]]
[30 31 32 33 34]
[23 28 33 38]
slicing en dimension > à 1 (b)¶
on crée un numpy.ndarray
en dimension 4, de forme (2, 3, 4, 5)
on l’initialise avec les 120
premiers entiers
tab = np.arange(120).reshape(2, 3, 4, 5)
on peut combiner les slicing des 4 dimensions, icitab[from:to:step, from:to:step, from:to:step, from:to_step]
de l’indice from
à l’indice to
(exclus) avec un pas step
à savoir
- quand vous voulez la valeur par défaut de
from
,to
etstep
vous ne mettez rien - quand les valeurs par défaut sont en fin d’expression, elles sont optionnelles
- du coup pour prendre tous les éléments dans une dimension
on peut mettre simplement la slice universelle::
, que généralement on abrège encore en juste:
exemples
la première matrice de tous les groupes de matrice, c’est-à-dire:
- tous les groupes
- la première matrice
- toutes les lignes
- toutes les colonnes
# en version longue où on épelle bien tout
tab[::, 0, ::, ::]
# en version courte on abrège et ça donne simplement
tab[:, 0]
tab = np.arange(120).reshape(2, 3, 4, 5)
# en version longue
tab[::, 0, ::, ::]
array([[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]],
[[60, 61, 62, 63, 64],
[65, 66, 67, 68, 69],
[70, 71, 72, 73, 74],
[75, 76, 77, 78, 79]]])
# en version courte
tab[:, 0]
array([[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]],
[[60, 61, 62, 63, 64],
[65, 66, 67, 68, 69],
[70, 71, 72, 73, 74],
[75, 76, 77, 78, 79]]])
exercices
- extrayez du tableau
tab
précédent
tab = np.arange(120).reshape(2, 3, 4, 5)
la sous-matrice au milieu (i.e. garder deux lignes et 3 colonnes, au centre) des premières matrices de tous les groupes
indices
on a 2 groupes de 3 matrices de 4 lignes et 5 colonnes
donc
- pour les 2 groupes de matrices
- dans la première matrice
- la sous-matrice du milieu (obtenue en enlevant une épaisseur de largeur 1 sur le pourtour)
donc
- tous les groupes
:
- la première matrice (indice
0
) - de la première ligne (indice
1
) à l’avant dernière ligne (indice-1
) step par défaut - idem pour les colonnes
# votre code
les sous-tableaux sont des vues, et non des copies¶
le slicing calcule une nouvelle indexation sur le segment mémoire du tableau existant
si à chaque slicing, numpy
faisait une copie du tableau sous-jacent, les codes seraient inutilisables
parce que coûteux (pénalisés) en place mémoire
donc lors d’un slicing
- un nouvel objet
np.ndarray
est bien créé, - son indexation est différente de celle de l’objet
np.ndarray
initial - mais ils partagent la mémoire (le segment unidimensionnel sous-jacent)
si un utilisateur veut une copie, il la fait avec la méthode copy
tab1 = tab[:, 0, 1:-1, 1:-1].copy()
partage du segment sous-jacent ou non? - avancé¶
un tableau numpy.ndarray
peut être
- un tableau original (on vient de le créer éventuellement par copie)
- une vue sur un tableau (il a été créé par slicing ou indexation)
il partage son segment de mémoire avec au moins un autre tableau
l’attribut numpy.ndarray.base
vaut alors
None
si le tableau est un tableau original
tab = np.arange(10)
print(tab.base)
-> None
tab1 = np.arange(10)
tab2 = tab1.copy()
print(tab2.base)
-> None
- le tableau original qui a servi à créer la vue
quand le tableau est une vue
tab1 = np.array([[1, 2, 3], [4, 5, 6]])
tab2 = tab1[0:2, 0:2] # une vue
tab2.base is tab1
-> True
tab1 = np.arange(120)
tab2 = tab1.reshape(2, 3, 4, 5) # une vue
tab2.base is tab1
-> True
faites attention, dans l’exemple
tab1 = np.arange(10).reshape(2, 5)
tab1.base
est l’objet np.arange(10)
les numpy.ndarray
ayant le même objet numpy.ndarray.base
partagent tous leur segment sous-jacent
sont différentes vues d’un même tableau original
(celui indiqué par leur attributbase
)modifier les éléments de l’un modifiera les éléments des autres
(ils pointent tous sur le même segment de mémoire)
numpy
essaie de créer le moins de mémoire possible
pour stocker les éléments de ses tableaux
# le code
tab1 = np.arange(10)
print(tab1.base)
None
# le code
tab1 = np.arange(10)
tab2 = tab1.copy()
print(tab2.base)
None
# le code
tab1 = np.array([[1, 2, 3], [4, 5, 6]])
tab2 = tab1[0:2, 0:2] # vue
tab2.base is tab1
True
# le code
tab1 = np.arange(120)
tab2 = tab1.reshape(2, 3, 4, 5) # une vue
tab2.base is tab1
True
# le code
tab1 = np.arange(10).reshape(2, 5)
tab1.base
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
exercice
créez un nouveau tableau formé des deux matrices .
affichez sa
base
slicez le tableau pour obtenir
affichez la
base
de la slicevérifiez que les deux
base
sont le même objet
# votre code ici
modification des sous-tableaux¶
pour modifier un sous-tableau, il faut simplement faire attention
- au type des éléments
- et à la forme du tableau
exercices avancés pour les rapides¶
avant d’aborder ces exercices, il existe un utilitaire très pratique (parmi les 2347 que nous n’avons pas eu le temps de couvrir ;); il s’agit de numpy.indices()
commençons par un exemple :
lignes, colonnes = np.indices((3, 5))
lignes
array([[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2]])
colonnes
array([[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4]])
vous remarquerez que dans le tableau qui s’appelle lignes
, la valeur dans le tableau correspond au numéro de ligne; dit autrement :
lignes[i, j] == i
pour tous les(i, j)
,
et dans l’autre sens bien sûr
colonnes[i, j] == j
lignes[1, 4]
np.int64(1)
colonnes[1, 4]
np.int64(4)
Pourquoi est-ce qu’on parle de ça me direz-vous ?
Eh bien en guise d’indice, cela vous renvoie à la notion de programmation vectorielle.
Ainsi par exemple si je veux créer une matrice de taille (3,5) dans laquelle M[i, j] == i + j
, je ne vais surtout par écrire une boucle for
, et au contraire je vais écrire simplement
I, J = np.indices((3, 5))
M = I + J
M
array([[0, 1, 2, 3, 4],
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6]])
les rayures¶
Écrivez une fonction zebre
, qui prend en argument un entier n et qui fabrique un tableau carré de coté n
, formé d’une alternance de colonnes de 0 et de colonnes de 1.
par exemple pour n=4
on s’attend à ceci
0 1 0 1
0 1 0 1
0 1 0 1
0 1 0 1
le damier¶
Écrivez une fonction checkers, qui prend en argument la taille n du damier, et un paramètre optionnel qui indique la valeur de la case (0, 0), et qui crée un tableau numpy
carré de coté n
, et le remplit avec des 0 et 1 comme un damier.
vous devez obtenir par exemple
>>> checkers(4)
array([[1, 0, 1, 0],
[0, 1, 0, 1],
[1, 0, 1, 0],
[0, 1, 0, 1]])
>>> checkers(5, False)
array([[0, 1, 0, 1, 0],
[1, 0, 1, 0, 1],
[0, 1, 0, 1, 0],
[1, 0, 1, 0, 1],
[0, 1, 0, 1, 0]])
# a vous de jouer
def checkers(n, up_left=True):
pass
# pour tester
checkers(4)
checkers(5, False)
le super damier par blocs¶
Il y a beaucoup de méthodes pour faire cet exercice de damier; elles ne vont pas toutes se généraliser pour cette variante du super damier :
Variante écrivez une fonction block_checkers(n, k)
qui crée et retourne
- un damier de coté
k*n x k*n
- composé de blocs de
k x k
homogènes (tous à 0 ou tous à 1) - eux mêmes en damiers
- on décide que le premier bloc (en 0,0) vaut 0
c’est-à-dire par exemple pour n=4
et k=3
cela donnerait ceci :
>>> block_checkers(4, 3)
array([[0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0]])
# vous de jouer
def block_checkers(n, k):
pass
block_checkers(3, 2)
# doit vous donner la figure ci-dessus
# éventuellement avec des False/True au lieu de 0/1
block_checkers(4, 3)
les escaliers¶
Écrivez une fonction escalier, qui prend en argument un entier n, qui crée un tableau de taille 2n+1, et qui le remplit de manière à ce que:
- aux quatre coins du tableau on trouve la valeur 0
- dans la case centrale on trouve la valeur 2n
- et si vous partez de n’importe quelle case et que vous vous déplacez d’une case (horizontalement ou verticalement), en vous dirigeant vers une case plus proche du centre, la valeur augmente de 1
par exemple
>>> stairs(4)
array([[0, 1, 2, 3, 4, 3, 2, 1, 0],
[1, 2, 3, 4, 5, 4, 3, 2, 1],
[2, 3, 4, 5, 6, 5, 4, 3, 2],
[3, 4, 5, 6, 7, 6, 5, 4, 3],
[4, 5, 6, 7, 8, 7, 6, 5, 4],
[3, 4, 5, 6, 7, 6, 5, 4, 3],
[2, 3, 4, 5, 6, 5, 4, 3, 2],
[1, 2, 3, 4, 5, 4, 3, 2, 1],
[0, 1, 2, 3, 4, 3, 2, 1, 0]])
# à vous de jouer
def stairs(n):
pass
# pour vérifier
stairs(4)
https://
calculs imbriqués (avancé)¶
Regardez le code suivant :
# une fonction vectorisée
def pipeline(array):
array2a = np.sin(array)
array2b = np.cos(array)
array3 = np.exp(array2a + array2b)
array4 = np.log(array3+1)
return array4
Les questions : j’ai un tableau X
typé float64
et de forme (1000,)
- j’appelle
pipeline(X)
, combien de mémoire est-ce quepipeline
va devoir allouer pour faire son travail ? - quel serait le minimum de mémoire dont on a besoin pour faire cette opération ?
- voyez-vous un moyen d’optimiser
pipeline
pour atteindre ce minimum ?
indice
- l’exercice vous invite à réfléchir à l’utilisation du paramètre
out=
qui est supporté dans les fonction vectorisées de numpy - dans ce cadre, sachez qu’on peut presque toujours remplacer l’usage d’un opérateur (comme ici
+
) par une fonction vectorisée (icinp.add
)