diff --git a/content/course/manipulation/01_numpy/index.qmd b/content/course/manipulation/01_numpy/index.qmd index 38005aada..fd3b8704a 100644 --- a/content/course/manipulation/01_numpy/index.qmd +++ b/content/course/manipulation/01_numpy/index.qmd @@ -58,7 +58,7 @@ afin d'avoir des résultats reproductibles: ```{python} np.random.seed(12345) ``` - +Remarque : Les auteurs de `numpy` [préconisent désormais](https://numpy.org/doc/stable/reference/random/index.html) de privilégier l'utilisation de générateurs via la fonction `default_rng()` plutôt que la simple utilisation de `numpy.random`. Si les scripts suivants sont exécutés dans un `Notebook Jupyter`, il est recommandé d'utiliser les paramètres suivants @@ -221,7 +221,7 @@ avec les arrays. # Correction x[[0,3,5]] x[::2] -x[-0] +x[1:] x[:5] # x2[0,:] # La première ligne ``` diff --git a/content/course/manipulation/02a_pandas_tutorial/index.qmd b/content/course/manipulation/02a_pandas_tutorial/index.qmd index 353480fe0..f3f42af8c 100644 --- a/content/course/manipulation/02a_pandas_tutorial/index.qmd +++ b/content/course/manipulation/02a_pandas_tutorial/index.qmd @@ -35,11 +35,9 @@ Pour essayer les exemples présents dans ce tutoriel : #| output: asis #| include: true #| eval: true - import sys sys.path.insert(1, '../../../../') #insert the utils module from utils import print_badges - #print_badges(__file__) print_badges("content/course/manipulation/02a_pandas_tutorial.qmd") ``` @@ -47,12 +45,14 @@ print_badges("content/course/manipulation/02a_pandas_tutorial.qmd") Le _package_ `pandas` est l'une des briques centrales de l'écosystème de la data-science. Son créateur, Wes McKinney, l'a pensé comme -une surcouche à la librairie `numpy` pour introduire -dans `Python` un objet central dans des langages comme `R` -ou `Stata`, à savoir le _dataframe_. `pandas` est rapidement +une surcouche à la librairie `numpy` pour introduire le _DataFrame_ +qui est un objet central dans des langages comme `R` +ou `Stata` mais qui était absent dans `Python`. `pandas` est rapidement devenu un incontournable de la _data-science_. L'ouvrage de référence de @mckinney2012python présente de manière plus -ample ce _package_. Ce tutoriel vise à introduire aux concepts +ample ce _package_. + +Ce tutoriel vise à introduire aux concepts de base de ce package par l'exemple et à introduire à certaines des tâches les plus fréquentes de (re)structuration des données du _data-scientist_. Il ne s'agit pas d'un ensemble @@ -78,7 +78,7 @@ les données ci-dessus associées à des données de contexte au niveau communal [^1]: Idéalement, on utiliserait les données [disponibles sur le site de l'Insee](https://www.insee.fr/fr/statistiques/3560121) mais celles-ci nécessitent un peu de travail de nettoyage qui n'entre pas dans le cadre de ce TP. -Pour faciliter l'import de données Insee, il est recommandé d'utiliser le package +Pour faciliter l'import de données `Insee`, il est recommandé d'utiliser le package [`pynsee`](https://github.com/InseeFrLab/Py-Insee-Data) qui simplifie l'accès aux données de l'Insee disponibles sur le site web [insee.fr](https://www.insee.fr/fr/accueil) ou via des API. @@ -86,7 +86,7 @@ ou via des API. {{% box status="note" title="Note" icon="fa fa-comment" %}} Le package `pynsee` est relativement jeune et n'est disponible que sur -[Github](https://github.com/InseeFrLab/Py-Insee-Data), pas sur `PyPi`. +[`Github`](https://github.com/InseeFrLab/Py-Insee-Data), pas sur `PyPi`. Idéalement, on utilise les commandes suivantes, pour installer le package et quelques dépendances nécessaires : @@ -94,7 +94,7 @@ pour installer le package et quelques dépendances nécessaires : #| eval: false !pip install pathlib2 !pip install python-Levenshtein -!pip install xlrd +!pip install --upgrade xlrd !pip install git+https://github.com/InseeFrLab/Py-Insee-Data.git ``` @@ -110,7 +110,6 @@ il faut télécharger le package compressé et l'installer localement : ```{python} #| eval: false import requests - url = 'https://github.com/InseeFrLab/Py-Insee-Data/archive/refs/heads/master.zip' r = requests.get(url) with open("pynsee.zip" , 'wb') as zipfile: @@ -141,7 +140,7 @@ les commandes d'import avec le chemin adéquat plutôt que l'url. Nous suivrons les conventions habituelles dans l'import des packages -```{python import pkg} +```{python} import numpy as np import pandas as pd import matplotlib.pyplot as plt @@ -151,37 +150,19 @@ import pynsee.download Pour obtenir des résultats reproductibles, on peut fixer la racine du générateur pseudo-aléatoire. -```{python seed, show = FALSE} +```{python} np.random.seed(123) ``` Au cours de cette démonstration des principales fonctionalités de `pandas`, et -lors du TP - -::: {.cell .markdown} -```{python} -#| echo: false -#| output: 'asis' -#| include: true -#| eval: true - -import sys -sys.path.insert(1, '../../../../') #insert the utils module -from utils import print_badges - -#print_badges(__file__) -print_badges("content/course/manipulation/02b_pandas_TP.qmd") -``` -::: - - -Je recommande de se référer régulièrement aux ressources suivantes: +lors du chapitre suivant, +je recommande de se référer régulièrement aux ressources suivantes: * L'[aide officielle de pandas](https://pandas.pydata.org/docs/user_guide/index.html). Notamment, la [page de comparaison des langages](https://pandas.pydata.org/pandas-docs/stable/getting_started/comparison/index.html) est très utile -* La cheatsheet suivante, [issue de ce post](https://becominghuman.ai/cheat-sheets-for-ai-neural-networks-machine-learning-deep-learning-big-data-678c51b4b463) +* La _cheatsheet suivante_, [issue de ce post](https://becominghuman.ai/cheat-sheets-for-ai-neural-networks-machine-learning-deep-learning-big-data-678c51b4b463) ![Cheasheet pandas](https://cdn-images-1.medium.com/max/2000/1*YhTbz8b8Svi22wNVvqzneg.jpeg) @@ -192,13 +173,14 @@ Il s'agit d'une structure particulière de données à deux dimensions, structurées en alignant des lignes et colonnes. Les colonnes peuvent être de type différent. -Un DataFrame est composé des éléments suivants: +Un `DataFrame` est composé des éléments suivants: * l'indice de la ligne ; * le nom de la colonne ; * la valeur de la donnée ; -Structuration d'un DataFrame pandas, emprunté à : +Structuration d'un DataFrame pandas, +empruntée à : ```{python} #| include: false @@ -215,34 +197,40 @@ with open('featured.png', 'wb') as out_file: ![](featured.png) Le concept de *tidy* data, popularisé par Hadley Wickham via ses packages `R`, -est parfaitement pertinent pour décrire la structure d'un DataFrame pandas. +est parfaitement pertinent pour décrire la structure d'un `DataFrame pandas`. Les trois règles sont les suivantes: -* Chaque variable possède sa propre colonne -* Chaque observation possède sa propre ligne -* Une valeur, matérialisant la valeur d'une observation d'une variable, +* Chaque variable possède sa propre colonne ; +* Chaque observation possède sa propre ligne ; +* Une valeur, matérialisant une observation d'une variable, se trouve sur une unique cellule. ![Concept de tidy data (emprunté à H. Wickham)](https://d33wubrfki0l68.cloudfront.net/6f1ddb544fc5c69a2478e444ab8112fb0eea23f8/91adc/images/tidy-1.png) -:warning: Les DataFrames sont assez rapides en Python[^2] et permettent de traiter en local de manière efficace des tables de +{{% box status="hint" title="Hint" icon="fa fa-lightbulb-o" %}} + +Les `DataFrames` sont assez rapides en `Python`[^2] et permettent de traiter en local de manière efficace des tables de données comportant plusieurs millions d'observations (en fonction de la configuration de l'ordinateur) et dont la volumétrie peut être conséquente (plusieurs centaines de Mo). Néanmoins, passé un certain seuil, qui dépend de la puissance de la machine mais aussi de la complexité de l'opération effectuée, le DataFrame `pandas` peut montrer certaines limites. Dans ce cas, il existe différentes -solutions: `dask` (dataframe aux opérations parallélisés), `SQL` (notamment postgres), `spark` (solution big data) +solutions: `Dask` (dataframe aux opérations parallélisés), `SQL` (notamment `Postgres`), +`Spark` (solution _big data_). Un chapitre spécial de ce cours est consacré à `Dask`. + +{{% /box %}} + ::: {.cell .markdown} [^2]: En `R`, les deux formes de dataframes qui se sont imposées récemment sont les `tibbles` (package `dplyr`) -et les `data.tables` (package `data.table`). `dplyr` reprend la syntaxe SQL de manière relativement +et les `data.tables` (package `data.table`). `dplyr` reprend la syntaxe `SQL` de manière relativement transparente ce qui rend la syntaxe très proche de celle de `pandas`. Cependant, alors que `dplyr` supporte très mal les données dont la volumétrie dépasse 1Go, `pandas` s'en accomode bien. Les performances de `pandas` sont plus proches de celles de `data.table`, qui est connu pour être une approche efficace avec des données de taille importante. ::: -Concernant la syntaxe, une partie des commandes python est inspirée par la logique SQL. On retrouvera ainsi +Concernant la syntaxe, une partie des commandes python est inspirée par la logique `SQL`. On retrouvera ainsi des instructions relativement transparentes. Il est vivement recommandé, avant de se lancer dans l'écriture d'une @@ -422,7 +410,7 @@ df.head() Pour présenter les méthodes les plus pratiques pour l'analyse de données, on peut partir de l'exemple des consommations de CO2 communales issues des données de l'Ademe. Cette base de données est exploitée plus intensément -dans le TP +dans le TP. ::: {.cell .markdown} ```{python} @@ -440,20 +428,28 @@ print_badges("content/course/manipulation/02b_pandas_TP.qmd") ``` ::: +L'import de données depuis un fichier plat se fait avec la fonction `read_csv`: ```{python} df = pd.read_csv("https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert") df ``` -Dans un processus de production, où normalement on connait les types des variables du DataFrame qu'on va importer, +{{% box status="note" title="Note" icon="fa fa-comment" %}} + +Dans un processus de production, où normalement on connait les types des variables du `DataFrame` qu'on va importer, il convient de préciser les types avec lesquels on souhaite importer les données -(argument `dtype`, sous la forme d'un dictionnaire). Cela est particulièrement important lorsqu'on désire utiliser une colonne comme une variable textuelle mais qu'elle comporte des attributs proches d'un nombre qui vont inciter `pandas` à l'importer sous forme de variable numérique. +(argument `dtype`, sous la forme d'un dictionnaire). +Cela est particulièrement important lorsqu'on désire utiliser une colonne +comme une variable textuelle mais qu'elle comporte des attributs proches d'un nombre +qui vont inciter `pandas` à l'importer sous forme de variable numérique. Par exemple, une colonne `[00001,00002,...] ` risque d'être importée comme une variable numérique, ignorant l'information des premiers 0 (qui peuvent pourtant la distinguer de la séquence 1, 2, etc.). Pour s'assurer que `pandas` importe sous forme textuelle la variable, on peut utiliser `dtype = {"code": "str"}` Sinon, on peut importer le csv, et modifier les types avec `astype()`. Avec `astype`, on peut gérer les erreurs de conversion avec le paramètre `errors`. +{{% /box %}} + L'affichage des DataFrames est très ergonomique. On obtiendrait le même *output* avec `display(df)`[^3]. Les premières et dernières lignes s'affichent automatiquement. Autrement, on peut aussi faire: @@ -463,27 +459,11 @@ nom l'indique, de n'afficher que les premières lignes ; * `tail` qui permet, comme son nom l'indique, de n'afficher que les dernières lignes * `sample` qui permet d'afficher un échantillon aléatoire de *n* lignes. -Cette méthode propose de nombreuses options - -::: {.cell .markdown} -```{python} -#| echo: false -#| output: 'asis' -#| include: true -#| eval: true - -import sys -sys.path.insert(1, '../../../../') #insert the utils module -from utils import print_badges - -#print_badges(__file__) -print_badges("content/course/manipulation/02b_pandas_TP.qmd") -``` -::: +Cette méthode propose de nombreuses options. ::: {.cell .markdown} [^3]: Il est préférable d'utiliser la fonction `display` (ou tout simplement -taper le nom du DataFrame qu'utiliser la fonction `print`). Le +taper le nom du `DataFrame`) qu'utiliser la fonction `print`. Le `display` des objets `pandas` est assez esthétique, contrairement à `print` qui renvoie du texte brut. ::: @@ -492,26 +472,31 @@ qui renvoie du texte brut. Il faut faire attention au `display` et aux commandes qui révèlent des données (`head`, `tail`, etc.) -dans un notebook ou un markdown qui exploite -des données confidentielles lorsqu'on utilise `git`. En effet, on peut se +dans un `Notebook` ou un `Markdown` qui exploite +des données confidentielles lorsqu'on utilise `Git`. + +En effet, on peut se retrouver à partager des données, involontairement, dans l'historique -`git`. Avec un `R markdown`, il suffit d'ajouter les sorties au fichier -`gitignore` (par exemple avec une balise de type `*.html`). Avec un -notebook `jupyter`, la démarche est plus compliquée car les fichiers +`Git`. Avec un `R Markdown`, il suffit d'ajouter les sorties au fichier +`.gitignore` (par exemple avec une balise de type `*.html`). Avec un +`Notebook Jupyter`, la démarche est plus compliquée car les fichiers `.ipynb` intègrent dans le même document, texte, sorties et mise en forme. -Techniquement, il est possible d'appliquer des filtres avec `git` + +Techniquement, il est possible d'appliquer des filtres avec `Git` (voir [ici](http://timstaley.co.uk/posts/making-git-and-jupyter-notebooks-play-nice/)) -mais c'est une démarche très complexe - +mais c'est une démarche très complexe. +Ce post de l'équipe à l'origine de [nbdev2](https://www.fast.ai/posts/2022-08-25-jupyter-git.html) +résume bien le problème du contrôle de version avec `Git` et des solutions qui +peuvent y être apportées. + +Une solution est d'utiliser [`Quarto`](https://quarto.org/) qui permet de générer les +`.ipynb` en _output_ d'un document texte, ce qui facilite le contrôle sur les +éléments présents dans le document. {{% /box %}} -On pourra alors préférer convertir systématiquement les `.ipynb` en `.py` grâce -à `jupytext` (`jupytext --to py nom_du_notebook.ipynb`) et mettre l'extension `*.ipynb` -dans le `.gitignore` de son projet git. - ### Dimensions et structure du DataFrame Les premières méthodes utiles permettent d'afficher quelques @@ -519,17 +504,28 @@ attributs d'un DataFrame. ```{python} df.axes +``` + +```{python} df.columns +``` + +```{python} df.index ``` Pour connaître les dimensions d'un DataFrame, on peut utiliser quelques méthodes pratiques: - ```{python} df.ndim +``` + +```{python} df.shape +``` + +```{python} df.size ``` @@ -541,7 +537,8 @@ méthode `nunique`. Par exemple, df['Commune'].nunique() ``` -Voici un premier résumé des méthodes `pandas` utiles, et un comparatif avec `R` +`pandas` propose énormément de méthodes utiles. +Voici un premier résumé, accompagné d'un comparatif avec `R` | Opération | pandas | dplyr (`R`) | data.table (`R`) | |-------------------------------|--------------|----------------|----------------------------| @@ -565,12 +562,26 @@ faire une moyenne ou une somme de l'ensemble des lignes ```{python} df.count() +``` + +```{python} df.mean() +``` + + +```{python} df.sum() +``` + +```{python} df.nunique() +``` + +```{python} df.quantile(q = [0.1,0.25,0.5,0.75,0.9]) ``` + Il faut toujours regarder les options de ces fonctions en termes de valeurs manquantes, car ces options sont déterminantes dans le résultat obtenu. @@ -617,12 +628,8 @@ Les méthodes relatives aux valeurs manquantes peuvent être mobilisées en conjonction des méthodes de statistiques agrégées. C'est utiles lorsqu'on désire obtenir une idée de la part de valeurs manquantes dans un jeu de données -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/linogaliana/python-datascientist/master) -[![Onyxia](https://img.shields.io/badge/SSPcloud-Tester%20via%20SSP--cloud-informational&color=yellow?logo=Python)](https://datalab.sspcloud.fr/launcher/ide/jupyter-python?autoLaunch=true&onyxia.friendlyName=%C2%ABpython-datascientist%C2%BB) -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](http://colab.research.google.com/github/linogaliana/python-datascientist/blob/pandas_intro/static/notebooks/numpy.ipynb) - ([ou depuis github](https://github.com/linogaliana/python-datascientist/blob/master/content/01_data/02_pandas_tp.ipynb)) -```python +```{python} df.isnull().sum() ``` @@ -638,7 +645,7 @@ complexes de maniement de données. En effet, on peut appliquer la méthode `plot()` directement à une `pandas.Series`: -```{python matplotlib-example} +```{python} #| eval: false df['Déchets'].plot() @@ -647,7 +654,7 @@ df['Déchets'].plot(kind = 'hist', logy = True) ``` -```{python matplotlib, include = FALSE} +```{python} plt.figure() fig = df['Déchets'].plot() fig @@ -685,7 +692,7 @@ utiliser plusieurs approches: Cette méthode requiert néanmoins d'avoir des noms de colonnes sans espace. * `dataframe[['variable']]` pour renvoyer la variable sous -forme de `DataFrame` ou dataframe['variable'] pour +forme de `DataFrame` ou `dataframe['variable']` pour la renvoyer sous forme de `Series`. Par exemple, `df[['Autres transports']]` ou `df['Autres transports']`. C'est une manière préférable de procéder. @@ -695,13 +702,17 @@ Pour accéder à une ou plusieurs valeurs d'un `DataFrame`, il existe deux manières conseillées de procéder, selon la forme des indices de lignes ou colonnes utilisés: -* `df.loc`: use labels -* `df.iloc`: use indices +* `df.loc`: utilise les labels +* `df.iloc`: utilise les indices + +{{% box status="danger" title="Warning" icon="fa fa-exclamation-triangle" %}} Les bouts de code utilisant la structure `df.ix` sont à bannir car la fonction est *deprecated* et peut ainsi disparaître à tout moment. +{{% /box %}} + `iloc` va se référer à l'indexation de 0 à *N* où *N* est égal à `df.shape[0]` d'un `pandas.DataFrame`. `loc` va se référer aux valeurs de l'index de `df`. @@ -719,23 +730,13 @@ Alors `df.loc[1, :]` donnera la première ligne de `df` (ligne où l'indice `mon `df.iloc[1, :]` donnera la deuxième ligne (puisque l'indexation en `Python` commence à 0). - # Principales manipulation de données L'objectif du [TP pandas](#pandasTP) est de se familiariser plus avec ces commandes à travers l'exemple des données des émissions de C02. -Les opérations les plus fréquentes en SQL sont résumées par le tableau suivant. +Les opérations les plus fréquentes en SQL `sont` résumées par le tableau suivant. Il est utile de les connaître (beaucoup de syntaxes de maniement de données reprennent ces termes) car, d'une manière ou d'une autre, elles couvrent la plupart @@ -750,36 +751,40 @@ des usages de manipulation des données | Effectuer une opération par groupe | `GROUP BY` | `df.groupby('Commune').mean()` | `df %>% group_by(Commune) %>% summarise(m = mean)` | `df[,mean(Commune), by = Commune]` | | Joindre deux bases de données (*inner join*) | `SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.x` | `table1.merge(table2, left_on = 'id', right_on = 'x')` | `table1 %>% inner_join(table2, by = c('id'='x'))` | `merge(table1, table2, by.x = 'id', by.y = 'x')` | -## Opérations sur les colonnes: select, mutate, drop +## Opérations sur les colonnes: `select`, `mutate`, `drop` -Les DataFrames pandas sont des objets *mutables* en langage `python`, -c'est-à-dire qu'il est possible de faire évoluer le DataFrame au grès +Les DataFrames pandas sont des objets *mutables* en langage `Python`, +c'est-à-dire qu'il est possible de faire évoluer le `DataFrame` au grès des opérations. L'opération la plus classique consiste à ajouter ou retirer des variables à la table de données. +```{python} +df_new = df.copy() +``` -{{% box status="danger" title="warning" icon="fa fa-exclamation-triangle" %}} +{{% box status="danger" title="Warning" icon="fa fa-exclamation-triangle" %}} Attention au comportement de `pandas` lorsqu'on crée une duplication -d'un DataFrame. Par défaut, `pandas` effectue une copie par référence. Dans ce +d'un `DataFrame`. + +Par défaut, `pandas` effectue une copie par référence. Dans ce cas, les deux objets (la copie et l'objet copié) restent reliés. Les colonnes crées sur l'un vont être répercutées sur l'autre. Ce comportement permet de -limiter l'inflation en mémoire de `python`. En faisant ça, le deuxième +limiter l'inflation en mémoire de `Python`. En faisant ça, le deuxième objet prend le même espace mémoire que le premier. Le package `data.table` -en `R` adopte le même comportement, contrairement à `dplyr`. +en `R` adopte le même comportement, contrairement à `dplyr`. Cela peut amener à quelques surprises si ce comportement d'optimisation n'est pas anticipé. Si vous voulez, par sécurité, conserver intact le premier DataFrame, faites appel à une copie profonde (*deep copy*) en -utilisant la méthode `copy`: - -```{python} -df_new = df.copy() -``` +utilisant la méthode `copy`, comme ci-dessus. -Attention toutefois, cela a un coût mémoire. Avec des données volumineuses, c'est une pratique à utiliser avec précaution +Attention toutefois, cela a un coût mémoire. +Avec des données volumineuses, c'est une pratique à utiliser avec précaution. {{% /box %}} + + La manière la plus simple d'opérer pour ajouter des colonnes est d'utiliser la réassignation. Par exemple, pour créer une variable `x` qui est le `log` de la @@ -806,8 +811,7 @@ Cela permet notamment d'enchainer les opérations sur un même `DataFrame` (nota nous verrons plus loin). Cette approche utilise généralement des *lambda functions*. Par exemple le code précédent (celui concernant une - seule variable) prendrait la forme: - +seule variable) prendrait la forme: ```{python} df_new.assign(Energie_log = lambda x: np.log(x['Energie'])) @@ -815,7 +819,7 @@ df_new.assign(Energie_log = lambda x: np.log(x['Energie'])) Dans les méthodes suivantes, il est possible de modifier le `pandas.DataFrame` *en place*, c'est à dire en ne le réassignant pas, avec le paramètre `inplace = True`. -Par défaut, `inplace` est égal à False et pour modifier le `pandas.DataFrame`, +Par défaut, `inplace` est égal à `False` et pour modifier le `pandas.DataFrame`, il convient de le réassigner. On peut facilement renommer des variables avec la méthode `rename` qui @@ -854,7 +858,7 @@ en fonction d'une condition logique (clause `WHERE`). On sélectionne les données sur une condition logique. Il existe plusieurs méthodes en `pandas`. La plus simple est d'utiliser les *boolean mask*, déjà vus dans le chapitre -[`numpy`](#numpy) +[`numpy`](#numpy). Par exemple, pour sélectionner les communes dans les Hauts-de-Seine, on peut utiliser le résultat de la méthode `str.startswith` (qui renvoie @@ -892,11 +896,12 @@ df_copy[df_copy['INSEE commune'].str.startswith("92")] = np.nan Il est conseillé de filtrer avec `loc` en utilisant un masque. En effet, contrairement à `df[mask]`, `df.loc[mask, :]` permet d'indiquer clairement à Python que l'on souhaite appliquer le masque aux labels de l'index. -Ce n'est pas le cas avec `df[mask]`. D'ailleurs, lorsqu'on utilise la syntaxe `df[mask]`, `pandas` renvoie généralement un *warning* +Ce n'est pas le cas avec `df[mask]`. +D'ailleurs, lorsqu'on utilise la syntaxe `df[mask]`, `pandas` renvoie généralement un *warning* ## Opérations par groupe -En SQL, il est très simple de découper des données pour +En `SQL`, il est très simple de découper des données pour effectuer des opérations sur des blocs cohérents et recollecter des résultats dans la dimension appropriée. La logique sous-jacente est celle du *split-apply-combine* qui est repris @@ -950,10 +955,11 @@ df.groupby('dep').mean() A noter que la variable de groupe, ici `dep`, devient, par défaut, l'index du DataFrame de sortie. Si on avait utilisé plusieurs variables de groupe, -on obtiendrait un objet multi-indexé. Sur la gestion des `multiindex`, on -pourra se référer à la référence de `Modern pandas` donnée en fin de cours. +on obtiendrait un objet multi-indexé. Sur la gestion des `multi-index`, on +pourra se référer à l'ouvrage `Modern Pandas` dont la référence est +donnée en fin de cours. -Tant qu'on n'appelle pas une action sur un DataFrame par groupe, du type +Tant qu'on n'appelle pas une action sur un `DataFrame` par groupe, du type `head` ou `display`, `pandas` n'effectue aucune opération. On parle de *lazy evaluation*. Par exemple, le résultat de `df.groupby('dep')` est une transformation qui n'est pas encore évaluée: @@ -977,11 +983,11 @@ propose une grande variété de méthodes optimisées. Cependant, il est fréque d'avoir besoin de méthodes non implémentées. Dans ce cas, on recourt souvent aux `lambda` functions. Par exemple, si -on désire connaître les communes dont le nom fait plus de 10 caractères, +on désire connaître les communes dont le nom fait plus de 40 caractères, on peut appliquer la fonction `len` de manière itérative: ```{python} -# Noms de communes superieurs à 10 caracteres +# Noms de communes superieurs à 40 caracteres df[df['Commune'].apply(lambda s: len(s)>40)] ``` @@ -1031,23 +1037,27 @@ En particulier, il faut noter que `apply` avec le paramètre `axis=1` est en gé Il est commun de devoir combiner des données issues de sources différentes. Nous allons ici nous focaliser sur le cas le plus favorable qui est la situation où une information permet d'apparier de manière exacte deux bases de données (autrement nous -serions dans une situation, beaucoup plus complexe, d'appariement flou). +serions dans une situation, beaucoup plus complexe, d'appariement flou[^6]). + +[^6]: Sur l'appariement flou, se reporter aux chapitres présentant `ElasticSearch`. La situation typique est l'appariement entre deux sources de données selon un identifiant individuel. Ici, il s'agit d'un identifiant de code commune. -Il est recommandé de lire [ce guide assez complet sur la question des jointures avec R](https://www.book.utilitr.org/jointures.html) qui donne des recommandations également utiles en `python`. +Il est recommandé de lire [ce guide assez complet sur la question des jointures avec R](https://www.book.utilitr.org/jointures.html) +qui donne des recommandations également utiles pour un utilisateur de `Python`. + +![](https://pics.me.me/thumb_left-join-right-join-inner-join-full-outer-join-imgflip-com-66845242.png) On utilise de manière indifférente les termes *merge* ou *join*. Le deuxième terme provient de la syntaxe SQL. -En `pandas`, dans la plupart des cas, on peut utiliser indifféremment `df.join` et `df.merge` +En `Pandas`, dans la plupart des cas, on peut utiliser indifféremment `df.join` et `df.merge` ![](pandas_join.png) Il est aussi possible de réaliser un merge en utilisant la fonction `pandas.concat()` avec `axis=1`. Se référer à la documentation de `concat` pour voir les options possibles. - ## Reshape On présente généralement deux types de données: @@ -1055,7 +1065,7 @@ On présente généralement deux types de données: * format __wide__: les données comportent des observations répétées, pour un même individu (ou groupe), dans des colonnes différentes * format __long__: les données comportent des observations répétées, pour un même individu, dans des lignes différentes avec une colonne permettant de distinguer les niveaux d'observations -Un exemple de la distinction entre les deux peut être pris à l'ouvrage de référence d'Hadley Wickham, *R for Data Science*: +Un exemple de la distinction entre les deux peut être emprunté à l'ouvrage de référence d'Hadley Wickham, *R for Data Science*: ![](https://d33wubrfki0l68.cloudfront.net/3aea19108d39606bbe49981acda07696c0c7fcd8/2de65/images/tidy-9.png) @@ -1066,9 +1076,9 @@ L'aide mémoire suivante aidera à se rappeler les fonctions à appliquer si bes Le fait de passer d'un format *wide* au format *long* (ou vice-versa) peut être extrêmement pratique car certaines fonctions sont plus adéquates sur une forme de données ou sur l'autre. -En règle générale, avec `python` comme avec `R`, les formats *long* sont souvent préférables. +En règle générale, avec `Python` comme avec `R`, les formats *long* sont souvent préférables. -Le TP pandas proposera des applications de ces principes +Le chapitre suivant, qui fait office de TP, proposera des applications de ces principes ::: {.cell .markdown} ```{python} @@ -1090,22 +1100,27 @@ print_badges("content/course/manipulation/02b_pandas_TP.qmd") En général, dans un projet, le nettoyage de données va consister en un ensemble de méthodes appliquées à un `pandas.DataFrame`. -On a vu que `assign` permettait de créer une variable dans un DataFrame. +On a vu que `assign` permettait de créer une variable dans un `DataFrame`. Il est également possible d'appliquer une fonction, appelée par exemple `my_udf` au DataFrame grâce à `pipe`: -~~~python +```python df = (pd.read_csv(path2data) .pipe(my_udf)) -~~~ +``` + +L'utilisation des `pipe` rend le code très lisible et peut être très +pratique lorsqu'on enchaine des opérations sur le même +_dataset_. # Quelques enjeux de performance -La librairie `dask` intègre la structure de `numpy`, `pandas` et `sklearn`. +La librairie `Dask` intègre la structure de `numpy`, `pandas` et `sklearn`. Elle a vocation à traiter de données en grande dimension, ainsi elle ne sera pas optimale pour des données qui tiennent très bien en RAM. Il s'agit d'une librairie construite sur la parallélisation. -Pour aller plus loin, se référer à la [documentation de `dask`](https://docs.dask.org/en/latest/). +[Un chapitre dans ce cours](/dask.html) lui est consacré +Pour aller plus loin, se référer à la [documentation de `Dask`](https://docs.dask.org/en/latest/). # Références @@ -1114,6 +1129,8 @@ Pour aller plus loin, se référer à la [documentation de `dask`](https://docs. [pandas.pydata](https://pandas.pydata.org/pandas-docs/stable/user_guide/basics.html) fait office de référence +* Le livre `Modern Pandas` de Tom Augspurger: https://tomaugspurger.github.io/modern-1-intro.html + ::: {#refs} ::: diff --git a/content/course/manipulation/02b_pandas_TP/index.qmd b/content/course/manipulation/02b_pandas_TP/index.qmd index a6f78e2c7..afc3057a5 100644 --- a/content/course/manipulation/02b_pandas_TP/index.qmd +++ b/content/course/manipulation/02b_pandas_TP/index.qmd @@ -20,11 +20,12 @@ summary: | à partir de données d'émissions de gaz à effet de serre de l'[`Ademe`](https://data.ademe.fr/). echo: false +output: false eval: false --- -Les exemples de ce TP sont visualisables sous forme de notebooks: +Les exemples de ce TP sont visualisables sous forme de `Notebooks`: ::: {.cell .markdown} ```{python} @@ -42,13 +43,13 @@ print_badges("content/course/manipulation/02b_pandas_TP.qmd") ``` ::: -Dans ce tutoriel `pandas`, nous allons utiliser deux sources de données : +Dans ce tutoriel `Pandas`, nous allons utiliser deux sources de données : -* Les émissions de gaz à effet de serre estimées au niveau communal par l'ADEME. Le jeu de données est +* Les émissions de gaz à effet de serre estimées au niveau communal par l'`ADEME`. Le jeu de données est disponible sur [data.gouv](https://www.data.gouv.fr/fr/datasets/inventaire-de-gaz-a-effet-de-serre-territorialise/#_) et requêtable directement dans python avec [cet url](https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert). - `pandas` offre la possibilité d'importer des données directement depuis un url. C'est l'option +`pandas` offre la possibilité d'importer des données directement depuis un url. C'est l'option prise dans ce tutoriel. Si vous préfèrez, pour des raisons d'accès au réseau ou de performance, importer depuis un poste local, @@ -65,7 +66,7 @@ ou via des API. [^1]: Toute contribution sur ce package, disponible sur [Github](https://github.com/InseeFrLab/Py-Insee-Data) est bienvenue ! -Nous suivrons les conventions habituelles dans l'import des packages +Après avoir installé la librairie `pynsee` (voir l'[introduction à pandas](course/manipulation/02a_pandas_tutorial)), nous suivrons les conventions habituelles dans l'import des packages : ```{python} @@ -80,18 +81,17 @@ import pynsee.download ## Exploration de la structure des données -Commencer par importer les données de l'Ademe à l'aide du package `pandas`. Vous pouvez les nommer `df`. +Commencer par importer les données de l'Ademe à l'aide du package `pandas`. Vous pouvez nommer le `DataFrame` obtenu `df`. ```{python} #| echo: true -df = pd.read_csv("https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global", sep = ";") +df = pd.read_csv("https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global/convert", sep=",") ``` Pour les données de cadrage au niveau communal (source Insee), le package `pynsee` facilite grandement la vie. La liste des données disponibles est [ici](https://inseefrlab.github.io/DoReMIFaSol/articles/donnees_dispo.html). -En l'occurrence, on va utiliser les données Filosofi (données de revenus) au niveau communal, dans la -dernière version disponible. +En l'occurrence, on va utiliser les données Filosofi (données de revenus) au niveau communal de 2016. Le point d'entrée principal de la fonction `pynsee` est la fonction `download_file`. Le code pour télécharger les données est le suivant : @@ -107,20 +107,23 @@ La fonction `download_file` attend un identifiant unique pour savoir quelle base de données aller chercher et restructurer depuis le site [insee.fr](https://www.insee.fr/fr/accueil). + Pour connaître la liste des bases disponibles, vous -pouvez utiliser la fonction `pynsee.get_file_list()`. +pouvez utiliser la fonction `meta = pynsee.get_file_list()`. Celle-ci renvoie un `DataFrame` dans lequel on peut rechercher, par exemple grâce à une recherche de mot clé: ```{python} +#| echo: true + meta = pynsee.get_file_list() meta.loc[meta['label'].str.contains(r"Filosofi.*2016")] ``` -Ici, `meta['label'].str.contains(r`"Filosofi.*2016"`)` signifie: - _"`pandas` trouve moi tous les labels où sont contenus les termes Filosofi et 2016_" - (`.*` signifiant _"peu m'importe le nombre de mots ou caractères entre"_) +Ici, `meta['label'].str.contains(r"Filosofi.*2016")` signifie: +"_`pandas` trouve moi tous les labels où sont contenus les termes Filosofi et 2016._" + (`.*` signifiant "_peu m'importe le nombre de mots ou caractères entre_") {{% /box %}} @@ -133,24 +136,31 @@ L'objectif de cet exercice est de vous amener à afficher des informations sur l Commencer sur `df`: - * Utiliser les méthodes adéquates pour les 10 premières valeurs, les 15 dernières et un échantillon aléatoire de 10 valeurs - * Tirer 5 pourcent de l'échantillon sans remise - * Ne conserver que les 10 premières lignes et tirer aléatoirement dans celles-ci pour obtenir un DataFrame de 100 données. - * Faire 100 tirages à partir des 6 premières lignes avec une probabilité de 1/2 pour la première observation et une probabilité uniforme pour les autres - -Faire la même chose sur `df_city` +1. Utiliser les méthodes adéquates pour les 10 premières valeurs, les 15 dernières et un échantillon aléatoire de 10 valeurs +2. Tirer 5 pourcent de l'échantillon sans remise +3. Ne conserver que les 10 premières lignes et tirer aléatoirement dans celles-ci pour obtenir un DataFrame de 100 données. +4. Faire 100 tirages à partir des 6 premières lignes avec une probabilité de 1/2 pour la première observation et une probabilité uniforme pour les autres +5. Faire la même chose sur `df_city`. {{% /box %}} ```{python} +# Question 1 df.head(10) df.tail(15) df.sample(10) + +# Question 2 df.sample(frac = 0.05) + +# Question 3 df[:10].sample(n = 100, replace = True) + +# Question 4 df[:6].sample(n = 100, replace = True, weights = [0.5] + [0.1]*5) +# Question 5 df_city.head(10) df_city.tail(15) df_city.sample(10) @@ -159,25 +169,41 @@ df_city[:10].sample(n = 100, replace = True) df_city[:6].sample(n = 100, replace = True, weights = [0.5] + [0.1]*5) ``` -Cette première approche exploratoire donne une idée assez précise de la manière dont les données sont organisées. On remarque ainsi une différence entre `df` et `df_city` quant aux valeurs manquantes: la première base est relativement complète, la seconde comporte beaucoup de valeurs manquantes. Autrement dit, si on désire exploiter `df_city`, il faut faire attention à la variable choisie. +Cette première approche exploratoire donne une idée assez précise de la manière dont les données sont organisées. +On remarque ainsi une différence entre `df` et `df_city` quant aux valeurs manquantes : +la première base est relativement complète, la seconde comporte beaucoup de valeurs manquantes. +Autrement dit, si on désire exploiter `df_city`, il faut faire attention à la variable choisie. {{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} **Exercice 2: structure des données** -La première chose à vérifier est le format des données, afin d'identifier des types de variables qui ne conviennent pas. Ici, comme c'est `pandas` qui a géré automatiquement les types de variables, il y a peu de chances que les types ne soient pas adéquats mais une vérification ne fait pas de mal. +La première chose à vérifier est le format des données, +afin d'identifier des types de variables qui ne conviennent pas + Ici, comme c'est `pandas` qui a géré automatiquement les types de variables, + il y a peu de chances que les types ne soient pas adéquats mais une vérification ne fait pas de mal. -* Vérifier les types des variables. S'assurer que les types des variables communes aux deux bases sont cohérents. +* Vérifier les types des variables. +S'assurer que les types des variables communes aux deux bases sont cohérents. +Pour les variables qui ne sont pas en type `float` alors qu'elles devraient l'être, modifier leur type. ```{python} +print("base df") print(df.dtypes) +print("\nbase df_city") +print(df_city.dtypes) +# Il faut changer les types de toutes les variables de df_city à l'exception des codes et libellés de commune. +df_city[df_city.columns[2:len(df_city.columns)]] = df_city[df_city.columns[2:len(df_city.columns)]].apply(pd.to_numeric) +print("\nbase df_city corrigée") print(df_city.dtypes) ``` -Ensuite, on vérifie les dimensions des `DataFrames` et la structure de certaines variables clés. En l'occurrence, les variables fondamentales pour lier nos données sont les variables communales. Ici, on a deux variables géographiques: un code commune et un nom de commune. +Ensuite, on vérifie les dimensions des `DataFrames` et la structure de certaines variables clés. +En l'occurrence, les variables fondamentales pour lier nos données sont les variables communales. +Ici, on a deux variables géographiques: un code commune et un nom de commune. * Vérifier les dimensions des DataFrames @@ -217,23 +243,26 @@ df_city[df_city['LIBGEO'].isin(x['LIBGEO'])] df_city[df_city['LIBGEO'].isin(x['LIBGEO'])].sort_values('LIBGEO') ``` -* Déterminer la taille moyenne (variable nombre de personnes: `NBPERSMENFISC16`) et quelques statistiques descriptives de ces données. Comparer aux mêmes statistiques sur les données où libellés et codes communes coïncident +* Déterminer la taille moyenne (variable nombre de personnes: `NBPERSMENFISC16`) et quelques statistiques descriptives de ces données. +Comparer aux mêmes statistiques sur les données où libellés et codes communes coïncident ```{python} -df_city[df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe() -df_city[~df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe() +print(df_city[df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe()) +print(df_city[~df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe()) ``` -* Vérifier sur les grandes villes (plus de 100 000 personnes), la proportion de villes où libellés et codes communes ne coïncident pas. Identifier ces observations. +* Vérifier les grandes villes (plus de 100 000 personnes), +la proportion de villes pour lesquelles un même nom est associé à différents codes commune. ```{python} -df_big_city = df_city[df_city['NBPERSMENFISC16']>100000] +df_big_city = df_city[df_city['NBPERSMENFISC16']>100000].copy() df_big_city['probleme'] = df_big_city['LIBGEO'].isin(x['LIBGEO']) df_big_city['probleme'].mean() df_big_city[df_big_city['probleme']] ``` -* Vérifier dans `df_city` les villes dont le libellé est égal à Montreuil. Vérifier également celles qui contiennent le terme 'Saint-Denis' +* Vérifier dans `df_city` les villes dont le libellé est égal à Montreuil. +Vérifier également celles qui contiennent le terme 'Saint-Denis' @@ -245,11 +274,15 @@ df_city[df_city.LIBGEO.str.contains('Saint-Denis')].head(10) {{% /box %}} -Ce petit exercice permet de se rassurer car les libellés dupliqués sont en fait des noms de commune identiques mais qui ne sont pas dans le même département. Il ne s'agit donc pas d'observations dupliquées. On se fiera ainsi aux codes communes, qui eux sont uniques. +Ce petit exercice permet de se rassurer car les libellés dupliqués +sont en fait des noms de commune identiques mais qui ne sont pas dans le même département. +Il ne s'agit donc pas d'observations dupliquées. +On se fiera ainsi aux codes communes, qui eux sont uniques. ## Les indices -Les indices sont des éléments spéciaux d'un DataFrame puisqu'ils permettent d'identifier certaines observations. Il est tout à fait possible d'utiliser plusieurs indices, par exemple si on a des niveaux imbriqués. +Les indices sont des éléments spéciaux d'un DataFrame puisqu'ils permettent d'identifier certaines observations. +Il est tout à fait possible d'utiliser plusieurs indices, par exemple si on a des niveaux imbriqués. {{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} @@ -258,7 +291,8 @@ Les indices sont des éléments spéciaux d'un DataFrame puisqu'ils permettent d A partir de l'exercice précédent, on peut se fier aux codes communes. -* Fixer comme indice la variable de code commune dans les deux bases. Regarder le changement que cela induit sur le *display* du dataframe +* Fixer comme indice la variable de code commune dans les deux bases. +Regarder le changement que cela induit sur le *display* du `DataFrame` ```{python} display(df) @@ -270,21 +304,29 @@ df_city = df_city.set_index('CODGEO') display(df_city) ``` -* Les deux premiers chiffres des codes communes sont le numéro de département. Créer une variable de département `dep` dans `df` et dans `df_city` +* Les deux premiers chiffres des codes communes sont le numéro de département. +Créer une variable de département `dep` dans `df` et dans `df_city` ```{python} df['dep'] = df.index.str[:2] df_city['dep'] = df_city.index.str[:2] ``` -* Calculer les émissions totales par secteur pour chaque département. Mettre en log ces résultats dans un objet `df_log`. Garder 5 départements et produire un barplot +* Calculer les émissions totales par secteur pour chaque département. +Mettre en log ces résultats dans un objet `df_log`. +Garder 5 départements et produire un `barplot` ```{python} df_log = df.groupby('dep').sum().apply(np.log) +print(df_log.head()) df_log.sample(5).plot(kind = "bar") ``` -* Repartir de `df`. Calculer les émissions totales par département et sortir la liste des 10 principaux émetteurs de CO2 et des 5 départements les moins émetteurs. Sans faire de *merge*, regarder les caractéristiques de ces départements (population et niveau de vie) +* Repartir de `df`. +Calculer les émissions totales par département et sortir la liste +des 10 principaux émetteurs de CO2 et des 5 départements les moins émetteurs. +Sans faire de *merge*, +regarder les caractéristiques de ces départements (population et niveau de vie) ```{python} @@ -294,13 +336,12 @@ df_emissions = df.reset_index().set_index(['INSEE commune','dep']).sum(axis = 1) gros_emetteurs = df_emissions.sort_values(ascending = False).head(10) petits_emetteurs = df_emissions.sort_values().head(5) - ## Caractéristiques des départements (df_city) -print(df_city[df_city['dep'].isin(gros_emetteurs.index)][['NBPERSMENFISC16','MED16']].sum()) +print("gros emetteurs") print(df_city[df_city['dep'].isin(gros_emetteurs.index)][['NBPERSMENFISC16','MED16']].mean()) -print(df_city[df_city['dep'].isin(petits_emetteurs.index)][['NBPERSMENFISC16','MED16']].sum()) +print("\npetits emetteurs") print(df_city[df_city['dep'].isin(petits_emetteurs.index)][['NBPERSMENFISC16','MED16']].mean()) - +# Les petits emetteurs sont en moyenne plus pauvres et moins peuplés que les gros emetteurs ``` {{% /box %}} @@ -331,8 +372,10 @@ df_copy2 = df_copy2.reset_index() * Importer le module `timeit` et comparer le temps d'exécution de la somme par secteur, pour chaque département, des émissions de CO2 ```{python} +#| eval: false %timeit df_copy.drop('Commune', axis = 1).groupby('dep').sum() %timeit df_copy2.drop('Commune', axis = 1).groupby('dep').sum() +# Le temps d'exécution est plus lent sur la base sans index par département. ``` @@ -342,7 +385,7 @@ df_copy2 = df_copy2.reset_index() # Restructurer les données -On présente généralement deux types de données: +On présente généralement deux types de données : * format __wide__: les données comportent des observations répétées, pour un même individu (ou groupe), dans des colonnes différentes * format __long__: les données comportent des observations répétées, pour un même individu, dans des lignes différentes avec une colonne permettant de distinguer les niveaux d'observations @@ -356,13 +399,15 @@ L'aide mémoire suivante aidera à se rappeler les fonctions à appliquer si bes ![](../02a_pandas_tutorial/reshape.png) -Le fait de passer d'un format *wide* au format *long* (ou vice-versa) peut être extrêmement pratique car certaines fonctions sont plus adéquates sur une forme de données ou sur l'autre. En règle générale, avec `python` comme avec `R`, les formats *long* sont souvent préférables. +Le fait de passer d'un format *wide* au format *long* (ou vice-versa) +peut être extrêmement pratique car certaines fonctions sont plus adéquates sur une forme de données ou sur l'autre. +En règle générale, avec `Python` comme avec `R`, les formats *long* sont souvent préférables. {{% box status="exercise" title="Exercise" icon="fas fa-pencil-alt" %}} **Exercice 5: Restructurer les données: wide to long** -* Créer une copie des données de l'ADEME en faisant `df_wide = df.copy()` +* Créer une copie des données de l'`ADEME` en faisant `df_wide = df.copy()` ```{python} df_wide = df.copy()