# Numpy *[référence](https://numpy.org/)* Il s'agit d'un gros module Python, très utile pour les scientifiques. ```python import numpy as np # on utilise souvent np comme alias # pour ne pas avoir à écrire numpy en entier ``` ## tableau multidimensionnel Un grand intérêt de numpy vient des **tableaux multidimentionnels**. ```python # tableau 2 dimension ordinaire A_normal = [[1,2,3],[4,5,6]] A_normal[1][2] # renvoie 5 # le même version numpy A = np.array([[1,2,3],[4,5,6]]) A[1,2] # renvoie 5 ``` Vous pouvez déjà voir cette notation spéciale `A[1,2]` qui n'existe pas de base en Python. On pourrait se dire que cela n'apporte pas grand chose de plus... mais si ! car numpy fournit adapte aussi la syntaxe de slicing : ```python # avec un tableau plus grand... A[2:7, 3:10] # renvoie la portion de tableau courant des lignes 2 à 6 # et des colonnes 3 à 9 A[::2,::2] # toutes les valeurs en sautant un pas de 2 ! ``` Ce serait beaucoup plus compliqué d'extraire une zone de tableau bi-dimensionnelle avec la syntaxe normale de Python... ## optimisation Donc numpy nous fait gagner du temps côté syntaxe. Mais numpy fait mieux : il fait gagner beaucoup de temps à l'exécution. Prenons une image de 1000 pixels x 1000 pixels. Cette image peut-être vue comme un tableau à deux dimensions. Supposons que l'on veuille appliquer un traitement à cette image. Le traitement prendra la forme d'un calcul à faire sur chaque pixel, c'est à dire sur chacun des 1000x1000 éléments du tableau. Si on ne dispose que des commandes ordinaires de Python, nous devront mettre en des boucles `for` parcourant le tableau et faisant le calcul. Ce sera long car Python est peu performant. Avec numpy, c'est presque la même chose mais avec une énorme différence : numpy est développé en langage C. Les calculs faits avec numpy sont donc optimisés et beaucoup plus rapides que s'ils étaient en Python. Numpy nous permet donc d'écrire notre programme confortablement avec Python (beaucoup plus simple que C) mais, de façon masquée, utilise un code optimisé. On a le meilleur des deux mondes : la programmation facile, l'exécution rapide. ```python # A est un gros tableau... A[100:200,250:390] = 3 # toutes la zone indiquée est modifiée ``` Pour parfaire cette optimisation, numpy donne la possibilité de préciser le type de donnée que contiendra le tableau. Par exemple, une image en noir et blanc contiendra des nombres représentant un niveau de gris et allant de 0 à 255, soit un octet. Un nombre entier Python utilise 4 octets. On gagne en mémoire si on indique que l'on utilisera que des nombres entier de 1 octet. C'est l'option [dtype](https://numpy.org/doc/stable/reference/generated/numpy.dtype.html?highlight=dtype#numpy.dtype) ```python A = np.array([[1,2,3],[4,5,6]], dtype=np.int8) ``` ## masques ```python # A est un tableau numpy A > 5 # produit un tableau spécial qui identifie toutes # les cases vérifiant la condition # on peut qualifier ce tableau de masque au sens d'un pochoir # utilisé par ceux qui peignent à la bombe. A[A > 5] = 10 # toutes les cases respectant la condition sont modifiées ! A[A > 5] # renvoie les éléments du tableau qui respectent la condition # mais ils sont donnés à la suite, on perd la forme # mais cela peut être utile : on pourrait calculer leur moyenne # leur max, leur nombre... A[A>5].mean() # par exemple la moyenne ``` La ligne de code précédente montre comment numpy permet à la fois de réduire énormément le code à écrire, et permet en plus une exécution extrêmement rapide. La recherche des éléments du tableau `> 5` et la modification `= 10` s'exécutera bien plus vite que si on avait voulu le faire en écrivant nous mêmes les boucles `for`. Quelle intérêt ? Exemple : `A` représente une image en niveau de gris. Je veux saturer les blancs. Je cherche toutes les zones claires (par ex, `> 150`) et je les mets en blanc éclatant (`= 255`). Il suffit d'écrire `A[A > 150] = 255`. Autre usage : il n'est pas rare qu'un tableau de données contienne des valeurs indéfinies. On peut par exemple demander à remplacer toutes ces valeurs par `0` : ```python A.isnan() # masque des éléments nan (not a number) A[A.isnan()] = 0 # mets tous les nan à 0 ``` ## Quelques méthodes utiles ```python A.shape # tuple dimension # par exemple (3,2) A.size # nombre d'éléments A.ravel() # produit une version applatie de A # c'est à dire lignes bout à bout ``` Un mot sur `shape`. Si un tableau a une seule dimension : ```python A = np.array([1, 2, 8, 15]) A.shape # renvoie (4,) ce qui est un tuple a un seul élément # normal, A n'a qu'une dimension A = np.array([[1], [2], [8], [15]]) A.shape # renvoie (4,1) ``` Il peut y avoir des cas où on souhaiterait que la dimension de `A` soit plutôt `(4,1)` au lieu de `(4,)`. Il est possible de le forcer. ```python A.reshape((4,1)) ``` On peut souhaiter coller des tableaux ensemble. ```python A = np.array([[1,2,3],[4,5,6]]) B = np.array([[7,8,9],[10,11,12]]) np.concatenate((A, B), axis=0) # collage vertical np.concatenate((A, B), axis=1) # collage horizontal ``` Il faut bien sur que les dimensions soient compatibles. Dans un tableau à deux dimensions, on donne le numéro de ligne en premier. C'est pour cela que l'axe de rang `0` correspond aux lignes, c'est l'axe vertical. L'axe `1` correspond aux colonnes, c'est l'axe horizontal. ```python np.vstack() # concatenate sur l'axe vertical np.hstack() # concatenate sur l'axe horizontal ``` ## Du hasard et des statistiques *références [random](https://numpy.org/doc/stable/reference/random/index.html) et [statistics](https://numpy.org/doc/stable/reference/routines.statistics.html)* ```python A.sum() # somme A.min() # minimum A.max() # maximum A.mean() # moyenne A.std() # écart-type A.corrcoeff() # matrice de corrélation ``` Comme déjà dit, parfois les tableaux contiennent des valeurs `nan`. On dispose de fonctions statistiques qui ignorent ces `nan` : ```python A.nanstd() # écart-type en ignorant les nan A.isnan().sum() # produit le nombre de nan ``` Mais il y a mieux : On peut faire les statistiques selon une direction. ```python A = np.array([ [15, 17, 9], [26, 85, 41], [16, 42, 78] ]) A.max(axis=1) # calcule le max selon les lignes, donc : # [17, 85, 78] ``` Et pour générer des tableaux aléatoires, par exemple : ```python A = np.random.randint(0,10,[20,30]) A = np.random.randn(5,5) # aléa gaussien ``` Il est fréquent que l'on veuille un tableau aléatoire une fois, mais que l'on veuille obtenir toujours le même tableau aléatoire à chaque réexécution. C'est possible en choisissant une valeur pour initialiser les procesus aléatoires (qui sont faussement aléatoires) ```python np.random.seed(0) # permet de répéter de l'aléa identiquement ``` ## Action sur les index Prenons le tableau `[7, 15, 4, 8]` on peut vouloir le trier, ce qui donnerait `[4, 7, 8, 15]`. On pourrait aussi demander les indices des éléments triés. Par exemple l'élément le plus petit dans est 4 et à l'indice 2, 7 vient ensuite avec l'indice 0, puis 8 qui a l'indice 3 et enfin 15 qui a l'indice 1. Si on veut les indices on a `[2, 0, 3, 1]`. La méthode `argsort` fait cela. ```python A.argsort(axis = 0) # fait le travail précédent en précisant l'axe A.argmin(axis = 0) # donne l'index du min ``` Ce dispositif est puissant. Un exemple : ```python v, c = np.unique(A,return_counts=True) # v contient les différentes entités dans le tableau # c est un tableau de compte # c.argsort() est donc un tableau donnant des index # pointant vers les éléments par ordre croissants... v[c.argsort()] # donnera les valeurs triées par effectif ! ``` ## Fonctions diverses Les fonctions [mathématiques](https://numpy.org/doc/stable/reference/routines.math.html) peuvent être appliquées à tout un tableau. ```python np.exp(A) # et si on veut faire x + exp(3*x) avec tous les éléments du tableau... A + 3*np.exp(A) ``` Il y a plus fort : ```python A = np.array([ [4, 7, 18], [11, 25, 2] ]) B = np.array([ [2], [17] ]) A + B # renvoie ''' [ [6, 9, 20], [28, 42, 19] ] ''' ``` Comme on voit, A est un tableau 2x3 tandis que B est un tableau 2x1. Les deux tailles sont normalement incomtatibles. Mais l'addition se fait en répétant B autant de fois que nécessaire pour couvrir A. **Une technique précieuse en analyse de données :** On dispose d'un tableau représentant une série statistique. Chaque ligne représente un individu et chaque colonne un attribut. Par exemple, à chaque ligne une personne, la première colonne est son age, la 2e colonne est son poids, la 3e est sa taille. Des techniques d'analyse consiste à faire comme si ces 3 attributs étaient des coordonnées `(x, y, z)` et à faire des calculs comme des distances. Mais dans ce cas, les 3 axes représentent des grandeurs sans rapport. Est-ce qu'un écart de 1 an doit avoir autant d'importance qu'un écart de 1kg ? Pour contourner ce problème, on peut normaliser chaque colonne de sorte que chaque attribut ait une même moyenne et un même écart-type. Avec numpyn cela se fait très facilement : ```python D = (A - A.mean(axis=0)) / A.std(axis = 0) ``` ## algèbres linéaire, matrices *reference [matrices](https://numpy.org/doc/stable/reference/routines.matlib.html)* ```python A.T # transposée A = np.ones((2,3)) # matrice pleine de 1 A = np.zeros((2,3)) # matrice pleine de 0 A.dot(B) # produit matriciel A.eyes(4) # matrice identité de dimension 4 np.linalg.eig(A) # valeurs propres ```