import numpy as np
Pour essayer les exemples présents dans ce tutoriel :
Il est recommandé de régulièrement se référer à la cheatsheet numpy et à la doc officielle en cas de doute sur une fonction.
Dans ce chapitre, on ne dérogera pas à la convention qui s’est imposée
d’importer Numpy
de la
manière suivante :
Nous allons également fixer la racine du générateur aléatoire de nombres afin d’avoir des résultats reproductibles :
12345) np.random.seed(
Note
Les auteurs
de numpy
préconisent désormais
de privilégier l’utilisation de
générateurs via la fonction default_rng()
plutôt que la simple utilisation de numpy.random
.
Afin d’être en phase avec les codes qu’on peut trouver partout sur internet, nous
conservons encore np.random.seed
mais cela peut être amené à évoluer.
Si les scripts suivants sont exécutés dans un Notebook Jupyter
,
il est recommandé d’utiliser les paramètres suivants
pour contrôler le rendu:
from IPython.core.interactiveshell import InteractiveShell
= "all" InteractiveShell.ast_node_interactivity
Le concept d’array
Le concept central de NumPy
(Numerical Python
) est
l’array
qui est un tableau de données multidimensionnel.
L’array numpy
peut être unidimensionnel et s’apparenter à un
vecteur (1d-array
),
bidimensionnel et ainsi s’apparenter à une matrice (2d-array
) ou,
de manière plus générale,
prendre la forme d’un objet
multidimensionnel (Nd-array
).
Les tableaux simples (uni ou bi-dimensionnels) sont faciles à se représenter et seront particulièrement utilisés dans le paradigme des DataFrames mais la possibilité d’avoir des objets multidimensionnels permettra d’exploiter des structures très complexes.
Un DataFrame sera construit à partir d’une collection d’array uni-dimensionnels (les variables de la table), ce qui permettra d’effectuer des opérations cohérentes (et optimisées) avec le type de la variable.
Par rapport à une liste,
- un array ne peut contenir qu’un type de données (
integer
,string
, etc.), contrairement à une liste. - les opérations implémentées par
numpy
seront plus efficaces et demanderont moins de mémoire
Les données géographiques constitueront une construction un peu plus complexe qu’un DataFrame
traditionnel.
La dimension géographique prend la forme d’un tableau plus profond, au moins bidimensionnel
(coordonnées d’un point).
Créer un array
On peut créer un array
de plusieurs manières. Pour créer un array
à partir d’une liste,
il suffit d’utiliser la méthode array
:
1,2,5]) np.array([
array([1, 2, 5])
Il est possible d’ajouter un argument dtype
pour contraindre le type du array :
"a","z","e"],["r","t"],["y"]], dtype="object") np.array([[
array([list(['a', 'z', 'e']), list(['r', 't']), list(['y'])], dtype=object)
Il existe aussi des méthodes pratiques pour créer des array:
- séquences logiques :
np.arange
(suite) ounp.linspace
(interpolation linéaire entre deux bornes) - séquences ordonnées : array rempli de zéros, de 1 ou d’un nombre désiré :
np.zeros
,np.ones
ounp.full
- séquences aléatoires : fonctions de génération de nombres aléatoires :
np.rand.uniform
,np.rand.normal
, etc. - tableau sous forme de matrice identité :
np.eye
Ceci donne ainsi, pour les séquences logiques:
0,10) np.arange(
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
0,10,3) np.arange(
array([0, 3, 6, 9])
0, 1, 5) np.linspace(
array([0. , 0.25, 0.5 , 0.75, 1. ])
Pour un array initialisé à 0:
10, dtype=int) np.zeros(
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
ou initialisé à 1:
3, 5), dtype=float) np.ones((
array([[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.]])
ou encore initialisé à 3.14:
array([[3.14, 3.14, 3.14, 3.14, 3.14],
[3.14, 3.14, 3.14, 3.14, 3.14],
[3.14, 3.14, 3.14, 3.14, 3.14]])
Enfin, pour créer la matrice \(I_3\):
3) np.eye(
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
Exercice 1
Générer:
- \(X\) une variable aléatoire, 1000 répétitions d’une loi \(U(0,1)\)
- \(Y\) une variable aléatoire, 1000 répétitions d’une loi normale de moyenne nulle et de variance égale à 2
- Vérifier la variance de \(Y\) avec
np.var
Indexation et slicing
Logique dans le cas d’un array unidimensionnel
La structure la plus simple est l’array unidimensionnel:
= np.arange(10)
x print(x)
[0 1 2 3 4 5 6 7 8 9]
L’indexation est dans ce cas similaire à celle d’une liste:
- le premier élément est 0
- le énième élément est accessible à la position \(n-1\)
La logique d’accès aux éléments est ainsi la suivante :
x[start:stop:step]
Avec un array unidimensionnel, l’opération de slicing (garder une coupe du array) est très simple. Par exemple, pour garder les K premiers éléments d’un array, on fera:
-1)] x[:(K
En l’occurrence, on sélectionne le K\(^{eme}\) élément en utilisant
-1] x[K
Pour sélectionner uniquement un élément, on fera ainsi:
= np.arange(10)
x 2] x[
2
Les syntaxes qui permettent de sélectionner des indices particuliers d’une liste fonctionnent également avec les arrays.
Exercice 2
Prenez x = np.arange(10)
et…
- Sélectionner les éléments 0,3,5 de
x
- Sélectionner les éléments pairs
- Sélectionner tous les éléments sauf le premier
- Sélectionner les 5 premiers éléments
Sur la performance
Un élément déterminant dans la performance de numpy
par rapport aux listes,
lorsqu’il est question de
slicing est qu’un array ne renvoie pas une
copie de l’élément en question (copie qui coûte de la mémoire et du temps)
mais simplement une vue de celui-ci.
Lorsqu’il est nécessaire d’effectuer une copie,
par exemple pour ne pas altérer l’array sous-jacent, on peut
utiliser la méthode copy
:
= x[:2, :2].copy() x_sub_copy
Filtres logiques
Il est également possible, et plus pratique, de sélectionner des données à partir de conditions logiques (opération qu’on appelle un boolean mask). Cette fonctionalité servira principalement à effectuer des opérations de filtre sur les données.
Pour des opérations de comparaison simples, les comparateurs logiques peuvent être suffisants. Ces comparaisons fonctionnent aussi sur les tableaux multidimensionnels grâce au broadcasting sur lequel nous reviendrons :
= np.arange(10)
x = np.array([[-1,1,-2],[-3,2,0]])
x2 print(x)
print(x2)
[0 1 2 3 4 5 6 7 8 9]
[[-1 1 -2]
[-3 2 0]]
==2
x<0 x2
array([[ True, False, True],
[ True, False, False]])
Pour sélectionner les observations relatives à la condition logique,
il suffit d’utiliser la logique de slicing de numpy
qui fonctionne avec les conditions logiques
Exercice 3
Soit
= np.random.normal(size=10000) x
- Ne conserver que les valeurs dont la valeur absolue est supérieure à 1.96
- Compter le nombre de valeurs supérieures à 1.96 en valeur absolue et leur proportion dans l’ensemble
- Sommer les valeurs absolues de toutes les observations supérieures (en valeur absolue) à 1.96
et rapportez les à la somme des valeurs de
x
(en valeur absolue)
Lorsque c’est possible, il est recommandé d’utiliser les fonctions logiques de numpy
(optimisées et
qui gèrent bien la dimension).
Parmi elles, on peut retrouver:
count_nonzero
isnan
any
;all
; notamment avec l’argumentaxis
np.array_equal
pour vérifier, élément par élément, l’égalité
Soit
= np.random.normal(0, size=(3, 4)) x
un array multidimensionnel et
= np.array([np.nan, 0, 1]) y
un array unidimensionnel présentant une valeur manquante.
Exercice 4
- Utiliser
count_nonzero
sury
- Utiliser
isnan
sury
et compter le nombre de valeurs non NaN - Vérifier que
x
comporte au moins une valeur positive dans son ensemble, en parcourant les lignes puis les colonnes.
Note : Jetez un oeil à ce que correspond le paramètre axis
dans numpy
en vous documentant sur internet. Par exemple ici.
Manipuler un array
Dans cette section, on utilisera un array multidimensionnel:
= np.random.normal(0, size=(3, 4)) x
Statistiques sur un array
Pour les statistiques descriptives classiques,
Numpy
propose un certain nombre de fonctions déjà implémentées,
qui peuvent être combinées avec l’argument axis
Exercice 5
- Faire la somme de tous les éléments d’un
array
, des éléments en ligne et des éléments en colonne. Vérifier la cohérence - Ecrire une fonction
statdesc
pour renvoyer les valeurs suivantes : moyenne, médiane, écart-type, minimum et maximum. L’appliquer surx
en jouant avec l’argument axis
Fonctions de manipulation
Voici quelques fonctions pour modifier un array,
Opération | Implémentation |
---|---|
Aplatir un array | x.flatten() (méthode) |
Transposer un array | x.T (méthode) ou np.transpose(x) (fonction) |
Ajouter des éléments à la fin | np.append(x, [1,2]) |
Ajouter des éléments à un endroit donné (aux positions 1 et 2) | np.insert(x, [1,2], 3) |
Supprimer des éléments (aux positions 0 et 3) | np.delete(x, [0,3]) |
Pour combiner des array, on peut utiliser, selon les cas,
les fonctions np.concatenate
, np.vstack
ou la méthode .r_
(concaténation rowwise).
np.hstack
ou la méthode .column_stack
ou .c_
(concaténation column-wise)
= np.random.normal(size = 10) x
Pour ordonner un array, on utilise np.sort
= np.array([7, 2, 3, 1, 6, 5, 4])
x
np.sort(x)
array([1, 2, 3, 4, 5, 6, 7])
Si on désire faire un ré-ordonnement partiel pour trouver les k valeurs les plus petites d’un array
sans les ordonner, on utilise partition
:
3) np.partition(x,
array([2, 1, 3, 4, 6, 5, 7])
Broadcasting
Le broadcasting désigne un ensemble de règles permettant
d’appliquer des opérations sur des tableaux de dimensions différentes. En pratique,
cela consiste généralement à appliquer une seule opération à l’ensemble des membres d’un tableau numpy
.
La différence peut être comprise à partir de l’exemple suivant. Le broadcasting permet
de transformer le scalaire 5
en array de dimension 3:
= np.array([0, 1, 2])
a
= np.array([5, 5, 5])
b
+ b
a + 5 a
array([5, 6, 7])
Le broadcasting peut être très pratique pour effectuer de manière efficace des opérations sur des données à la structure complexe. Pour plus de détails, se rendre ici ou ici.
Une application: programmer ses propres k-nearest neighbors
Exercice (un peu plus corsé)
- Créer
X
un tableau à deux dimensions (i.e. une matrice) comportant 10 lignes et 2 colonnes. Les nombres dans le tableau sont aléatoires. - Importer le module
matplotlib.pyplot
sous le nomplt
. Utiliserplt.scatter
pour représenter les données sous forme de nuage de points. - Constuire une matrice 10x10 stockant, à l’élément \((i,j)\), la distance euclidienne entre les points \(X[i,]\) et \(X[j,]\). Pour cela, il va falloir jouer avec les dimensions en créant des tableaux emboîtés à partir par des appels à
np.newaxis
:
- En premier lieu, utiliser
X1 = X[:, np.newaxis, :]
pour transformer la matrice en tableau emboîté. Vérifier les dimensions - Créer
X2
de dimension(1, 10, 2)
à partir de la même logique - En déduire, pour chaque point, la distance avec les autres points pour chaque coordonnées. Elever celle-ci au carré
- A ce stade, vous devriez avoir un tableau de dimension
(10, 10, 2)
. La réduction à une matrice s’obtient en sommant sur le dernier axe. Regarder dans l’aide denp.sum
comme effectuer une somme sur le dernier axe. - Enfin, appliquer la racine carrée pour obtenir une distance euclidienne en bonne et due forme.
- Vérifier que les termes diagonaux sont bien nuls (distance d’un point à lui-même…)
- Il s’agit maintenant de classer, pour chaque point, les points dont les valeurs sont les plus similaires. Utiliser
np.argsort
pour obtenir, pour chaque ligne, le classement des points les plus proches - On va s’intéresser aux k-plus proches voisins. Pour le moment, fixons k=2. Utiliser
argpartition
pour réordonner chaque ligne de manière à avoir les 2 plus proches voisins de chaque point d’abord et le reste de la ligne ensuite - Utiliser le morceau de code ci-dessous
Un indice pour représenter graphiquement les plus proches voisins
0], X[:, 1], s=100)
plt.scatter(X[:,
# draw lines from each point to its two nearest neighbors
= 2
K
for i in range(X.shape[0]):
for j in nearest_partition[i, :K+1]:
# plot a line from X[i] to X[j]
# use some zip magic to make it happen:
*zip(X[j], X[i]), color='black') plt.plot(
Pour la question 2, vous devriez obtenir un graphique ayant cet aspect :
Le résultat de la question 7 est le suivant :
Ai-je inventé cet exercice corsé ? Pas du tout, il vient de l’ouvrage Python Data Science Handbook. Mais, si je vous l’avais indiqué immédiatement, auriez-vous cherché à répondre aux questions ?
Par ailleurs, il ne serait pas une bonne idée de généraliser cet algorithme à de grosses données. La complexité de notre approche est \(O(N^2)\). L’algorithme implémenté par Scikit-Learn
est
en \(O[NlogN]\).
De plus, le calcul de distances matricielles en utilisant la puissance des cartes graphiques serait plus rapide. A cet égard, la librairie faiss offre des performances beaucoup plus satisfaisantes que celles que permettraient numpy
sur ce problème précis.
Exercices supplémentaires
Comprendre le principe de l'algorithme PageRank
Google
est devenu célèbre grâce à son algorithme PageRank
. Celui-ci permet, à partir
de liens entre sites web, de donner un score d’importance à un site web qui va
être utilisé pour évaluer sa centralité dans un réseau.
L’objectif de cet exercice est d’utiliser Numpy
pour mettre en oeuvre un tel
algorithme à partir d’une matrice d’adjacence qui relie les sites entre eux.
- Créer la matrice suivante avec
numpy
. L’appelerM
:
\[ \begin{bmatrix} 0 & 0 & 0 & 0 & 1 \\ 0.5 & 0 & 0 & 0 & 0 \\ 0.5 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0.5 & 0 & 0 \\ 0 & 0 & 0.5 & 1 & 0 \end{bmatrix} \]
- Pour représenter visuellement ce web minimaliste,
convertir en objet
networkx
(une librairie spécialisée dans l’analyse de réseau) et utiliser la fonctiondraw
de ce package.
Il s’agit de la transposée de la matrice d’adjacence qui permet de relier les sites entre eux. Par exemple, le site 1 (première colonne) est référencé par les sites 2 et 3. Celui-ci ne référence que le site 5.
- A partir de la page wikipedia anglaise de
PageRank
, tester sur votre matrice.
Le site 1 est assez central car il est référencé 2 fois. Le site 5 est lui également central puisqu’il est référencé par le site 1.
array([[0.25419178],
[0.13803151],
[0.13803151],
[0.20599017],
[0.26375504]])
D’autres idées :
- Simulations de variables aléatoires ;
- TCL ;
Citation
@book{galiana2023,
author = {Galiana, Lino},
title = {Python Pour La Data Science},
date = {2023},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {en}
}