|
|
## C) Atelier 2 : un graphique avec 3 variables d'unités différentes : 2e axe des y, subplots ; les objets d'une figure
|
|
|
|
|
|
**Objectif :** représenter sur une même figure des variables de nature différente mais tracées selon un même axe des x, avec une légende contenant toutes les variables
|
|
|
**Notions principales** : Figure, Axes, subplots, sharex, twinx, ax.legend et fig.legend
|
|
|
**Notions accessoires** : types de courbe plot, vlines, step (plus de détails [page focus step](focus_step)), scatter (plus de détails [page focus scatter](focus_scatter)) ; dictionnaires ; créer une légende totalement customisée
|
|
|
**Objectif :** représenter sur une même figure des variables de nature différente mais tracées selon un même axe des x, avec une légende contenant toutes les variables **Notions principales** : Figure, Axes, subplots, sharex, twinx, ax.legend et fig.legend **Notions accessoires** : types de courbe plot, vlines, step (plus de détails [page focus step](focus_step)), scatter (plus de détails [page focus scatter](focus_scatter)) ; dictionnaires ; créer une légende totalement customisée
|
|
|
|
|
|
Cet objectif ne semble pas hyper ambitieux, et vous trouverez des tutos pour réaliser facilement cette figure, et pourtant la syntaxe peut sembler étrange... et les légendes sont capricieuses...
|
|
|
Un peu de théorie est indispensable pour comprendre la notion de *subplots" et d' "Axes", liés ou non, et la construction d'une légende.
|
|
|
La démarche proposée est en plusieurs étapes, pour manipuler les notions importantes progressivement.
|
|
|
Le code fourni crée finalement la figure ci-dessous, composée de 3 "vignettes" dont deux portent une ou deux courbes. Elle n'est pas parfaite, vous pourrez vous exercer à l'améliorer. On remarque une légende commune aux deux vignettes en bas et une légende des couleurs des mois construites "à la main" au milieu.
|
|
|
Cet objectif ne semble pas hyper ambitieux, et vous trouverez des tutos pour réaliser facilement cette figure, et pourtant la syntaxe peut sembler étrange... et les légendes sont capricieuses... Un peu de théorie est indispensable pour comprendre la notion de \*subplots" et d' "Axes", liés ou non, et la construction d'une légende. La démarche proposée est en plusieurs étapes, pour manipuler les notions importantes progressivement. Le code fourni crée finalement la figure ci-dessous, composée de 3 "vignettes" dont deux portent une ou deux courbes. Elle n'est pas parfaite, vous pourrez vous exercer à l'améliorer. On remarque une légende commune aux deux vignettes en bas et une légende des couleurs des mois construites "à la main" au milieu.
|
|
|
|
|
|
<img src="uploads/f26fc46a5c4bc9f072820291a73d1cbd/Figure_3_graphique_PTQ_subplots_avec-legende.png" width="240" >
|
|
|
|
|
|
<i> figure composée de deux sous-figures liées par un unique axe des x ; la vignette du haut a deux axes des y. Au milieu, une légende des couleurs des mois ; en bas, une légende commune aux deux vignettes </i>
|
|
|
![](uploads/f26fc46a5c4bc9f072820291a73d1cbd/Figure_3_graphique_PTQ_subplots_avec-legende.png)
|
|
|
|
|
|
_figure composée de deux sous-figures liées par un unique axe des x ; la vignette du haut a deux axes des y. Au milieu, une légende des couleurs des mois ; en bas, une légende commune aux deux vignettes_
|
|
|
|
|
|
Les diapos introduisant l'Atelier ont déjà abordé ce cas, en expliquant ce qui se "cache derrière" un plt.plot, et le pourquoi de la syntaxe plus explicite **" fig, ax = plt.subplots() "**, On y explique aussi ce qu'est vraiment le type de "ax", qui n'est pas que un axe mais se caractérise par une surface dans la figure munie d'un système d'axes... Les diapos de cette partie reviennent sur ce point théorique de manière plus détaillée.
|
|
|
|
... | ... | @@ -25,20 +19,11 @@ On vous a mis plusieurs versions du fichier, qui était initialement un fichier |
|
|
Si vous voulez le faire "à la main", vous trouverez en commentaire plusieurs manières de décoder les dates, à partir de la ligne lue, toujours en format "string".
|
|
|
|
|
|
```python
|
|
|
ligne_dateTPQ = "01/04/2000;9,3;49;8,6"
|
|
|
```
|
|
|
|
|
|
Comme dans la partie A, le découpage se fait avec l'instruction "split". Par défaut, le découpage se fait selon les "blancs" (espaces, tabulations...). La date est le premier de ces éléments, qui est comme les autres une chaîne de caractères (string). Pour en extraire les informations, le plus simple est de "re-splitter" avec comme argument le séparateur "/". On passe ensuite les éléments de la date, convertis en entiers, dans le bon ordre.
|
|
|
On peut profiter de cet exercice pour s'exercer sur les **tuples** (notions que l'on reverra plus loin)
|
|
|
Comme dans la partie A, le découpage se fait avec l'instruction "split". Par défaut, le découpage se fait selon les "blancs" (espaces, tabulations...). La date est le premier de ces éléments, qui est comme les autres une chaîne de caractères (string). Pour en extraire les informations, le plus simple est de "re-splitter" avec comme argument le séparateur "/". On passe ensuite les éléments de la date, convertis en entiers, dans le bon ordre. On peut profiter de cet exercice pour s'exercer sur les **tuples** (notions que l'on reverra plus loin)
|
|
|
|
|
|
```python
|
|
|
# récupération des jour, mois année dans une chaîne de caractères de format connu
|
|
|
jma = "01/04/2000".split('/') # jma est donc une liste de strings ['01', '04', '2000']
|
|
|
j, m, a = jma # unpacking : en une seule opération on affecte j= '01', m='04', a = '2000'
|
|
|
# on pourrait même écrire directement
|
|
|
j, m, a = "01/04/2000".split('/')
|
|
|
# et ensuite on crée un objet au format datetime
|
|
|
date = datetime(int(a), int(m), int(j))
|
|
|
```
|
|
|
|
|
|
### C2) on demande à Pandas de faire tout le boulot : lire le fichier et faire un graphique
|
... | ... | @@ -46,13 +31,11 @@ date = datetime(int(a), int(m), int(j)) |
|
|
Dans un premier temps, on va profiter de la bibliothèque pandas, qui crée et travaille sur des **DataFrames**.
|
|
|
|
|
|
```python
|
|
|
import pandas as pd
|
|
|
```
|
|
|
|
|
|
On vous a donné du code déjà écrit, **donnees_TD_ETP_csv_panda_seul** ; vous pouvez sans doute l'améliorer. On va s'appuyer sur la méthode read_csv pour lire le fichier et le mettre sous forme de tableau (DataFrame). Il devrait être possible de tout récupérer au format voulu en une seule opération, mais ici on a dû écrire quelques étapes. Pistes pour amélioration :
|
|
|
|
|
|
```python
|
|
|
pd.read_csv(chemin_complet_du_fichier, parse_dates=True, index_col="date")
|
|
|
```
|
|
|
|
|
|
On utilise des instructions comme "print" pour vérifier que l'on a bien lu correctement, et header pour jeter un oeil aux premières lignes du DF. Dans un deuxième temps, on va convertir la première colonne (donc d'indice 0) de texte en date, et avec pd.to_datetime ; l'argument day_first permet de lever une ambiguité : le jour ou le mois est écrit en premier
|
... | ... | @@ -60,14 +43,9 @@ On utilise des instructions comme "print" pour vérifier que l'on a bien lu corr |
|
|
Cette colonne doit devenir l'index de la table. On a donc au final un DF avec un index date et 3 colonnes T, P et Q. Le module pandas permet de tracer en une opération les données : mon_data_frame.plot().
|
|
|
|
|
|
```python
|
|
|
DF_TD4.plot()
|
|
|
plt.suptitle("En utilisant les possibilités de pandas")
|
|
|
plt.legend()
|
|
|
plt.show()
|
|
|
```
|
|
|
|
|
|
<img src="uploads/4438d78280976838e8b0742b5bf84653/Figure_2_graphique_pandas.PNG" width="240">
|
|
|
<i> figure obtenue avec DF_TD4.plot() </i>
|
|
|
![](uploads/4438d78280976838e8b0742b5bf84653/Figure_2_graphique_pandas.PNG) _figure obtenue avec DF_TD4.plot()_
|
|
|
|
|
|
L'outil zoom montre que l'affichage des étiquettes de date est correct quel que soit le niveau de zoom, contrairement à la première expérience. Dans la suite, vous trouverez les instructions qui donnent ce rendu même en dehors de Pandas. La mauvaise nouvelle : les trois variables, qui s'expriment dans des unités différentes, sont toutes les 3 tracées dans le même repère. Cela permet de voir les données rapidement, mais évidemment la mise en forme n'est pas du tout satisfaisante pour un rapport. On va donc retravailler cela dans la suite.
|
|
|
|
... | ... | @@ -75,47 +53,15 @@ L'outil zoom montre que l'affichage des étiquettes de date est correct quel que |
|
|
|
|
|
Vous pouvez partir de la fonction précédente ou de **donnees_TD_ETP_2subplots** déjà écrit; dans le développement on trouve souvent en ligne des bouts de code qu'il faut s'approprier et adapter. Cette fonction reprend le début de la fonction précédente (lecture avec pd.read_csv), on trace aussi avec la méthode pd plot pour vérifier que la lecture s'est bien passée (phase que l'on peut ensuite mettre en commentaire). Ensuite on a du code spécifique. Si vous l'exécutez, vous verrez que ça marche, le rendu n'est pas forcément à votre goût, donc ce sera l'occasion de vous exercer à mettre en forme. On proposera par la suite une variante, vous pourrez donc étoffer votre palette.
|
|
|
|
|
|
<img src="uploads/1fd2a2524cc6ebf27504f8cab0d326b8/Figure_3_graphique_PTQ_subplots.png" width="240">
|
|
|
![](uploads/1fd2a2524cc6ebf27504f8cab0d326b8/Figure_3_graphique_PTQ_subplots.png)
|
|
|
|
|
|
<i> figure structurée en 2 vignettes partageant le même axe des x, sur le graphique du haut chaque variable a son axe ; début de mise en forme </i>
|
|
|
_figure structurée en 2 vignettes partageant le même axe des x, sur le graphique du haut chaque variable a son axe ; début de mise en forme_
|
|
|
|
|
|
De prime abord, certaines syntaxes peuvent paraître vraiment bizarres. Pour comprendre, on n'échappera pas à un peu de théorie ; certains sites expliquent très bien la structure des figures, et cela aide vraiment à progresser et à s'approprier les notions.
|
|
|
|
|
|
Voir les diapos.
|
|
|
|
|
|
```python
|
|
|
# on découpe la figure en deux subplots, sur 1 colonne et 2 lignes.
|
|
|
# La méthode subplots renvoie un objet figure et un tuple de deux subplots
|
|
|
# sharex = True lie l'axe des x des deux subplots : on zoome sur les 2 en même temps
|
|
|
# on va nommer la figure fig et les subplots ax_pluie et ax_q
|
|
|
fig, (ax_pluie, ax_q) = plt.subplots(ncols=1, nrows=2, sharex=True)
|
|
|
fig.subplots_adjust(bottom=0.15) # marge en bas
|
|
|
#titre de la figure
|
|
|
fig.suptitle("Forçages en haut, réponse du bassin en bas")
|
|
|
|
|
|
# première variable : LA PLUIE, bien sûr dans le système ax_pluie
|
|
|
etiquette_pluie = nom_colonnes[1]
|
|
|
ax_pluie.set_ylabel(etiquette_pluie, color='lightblue') # on agit sur l'axe ax_pluie
|
|
|
#on va utiliser des lignes verticales, ce n'est pas forcément la meilleure solution ni la meilleure couleur
|
|
|
#il faut transformer la colonne numéro 1 en objet numpy que mpl comprend, avec la méthode tolist()
|
|
|
ax_pluie.vlines(x=liste_dates, ymin=0, ymax=DF_TD4[etiquette_pluie].tolist(), color='lightblue', lw=2,
|
|
|
label=etiquette_pluie)
|
|
|
ax_pluie.invert_yaxis() # pour respecter une convention ; peut se faire aussi via ylim
|
|
|
|
|
|
# 2e variable : LA TEMPERATURE, dans la même vignette la pluie en partageant son axe des x
|
|
|
ax_t = ax_pluie.twinx() # nouvel "ax" partageant l'axe des x de ax_pluie
|
|
|
etiquette_temperatures = nom_colonnes[0]
|
|
|
ax_t.set_ylabel(etiquette_temperatures)
|
|
|
ax_t.plot(liste_dates, DF_TD4[etiquette_temperatures].tolist(), marker='*', color='orange', ls=':',
|
|
|
label=etiquette_temperatures)
|
|
|
# todo : le tracé d'une variable à pas de temps fixe est plus correct avec step
|
|
|
# ax_t.step(liste_dates, DF_TD4[etiquette_temperatures].tolist(), marker='None', color='orange', ls=':',
|
|
|
# label=etiquette_temperatures + " (step)", where="post")
|
|
|
|
|
|
# 3e variable : Le DEBIT, dans l'autre vignette
|
|
|
etiquette_debit = nom_colonnes[2]
|
|
|
ax_q.set_ylabel(etiquette_debit, color='blue')
|
|
|
ax_q.plot(liste_dates, DF_TD4[etiquette_debit].tolist(), marker='>', color='blue', ls=':', label=etiquette_debit)
|
|
|
```
|
|
|
|
|
|
Les messages essentiels sont :
|
... | ... | @@ -132,69 +78,53 @@ On met donc en pratique sur ce graphique deux notions importantes : |
|
|
Dans le code, on a utilisé une méthode legend appliquée à l'objet fig. Ainsi, cette légende regroupe les symboles et étiquettes de l'ensemble des plots de la figure. Les diapos s'étendent davantage sur ses arguments.
|
|
|
|
|
|
```python
|
|
|
fig.legend(bbox_to_anchor=(1, 0), loc="lower right",
|
|
|
bbox_transform=fig.transFigure, ncol=4)
|
|
|
```
|
|
|
|
|
|
Pour se rendre compte de la différence, mettez cette instruction en commentaire (faire précéder de # et **essayez les syntaxes suivantes** :
|
|
|
|
|
|
```python
|
|
|
plt.legend()
|
|
|
```
|
|
|
|
|
|
```python
|
|
|
ax_pluie.legend()
|
|
|
ax_t.legend()
|
|
|
ax_q.legend()
|
|
|
```
|
|
|
|
|
|
### C5) Variantes "esthétiques" et tests d'autres types de courbes (step, scatter...)
|
|
|
|
|
|
**Code pour obtenir la figure du haut de page**
|
|
|
|
|
|
La figure tracée par la fonction **donnees_TD_ETP_2subplots** souffre de défauts esthétiques (couleurs qui se voient mal...) et de plus elle représente par des lignes des variables qui sont en fait des moyennes à pas de temps fixe. On va donc utiliser la méthode **step** sur laquelle j'attire votre attention car j'ai eu du mal à la trouver !
|
|
|
:warning: on remarque que le dernier point n'est pas suivi d'un trait horizontal, ce qui se comprend car dans les arguments il n'y pas d'information qui permettrait de savoir où l'arrêter ; ici cela ne pose pas de problème, mais cela peut parfois être trompeur.
|
|
|
La figure tracée par la fonction **donnees_TD_ETP_2subplots** souffre de défauts esthétiques (couleurs qui se voient mal...) et de plus elle représente par des lignes des variables qui sont en fait des moyennes à pas de temps fixe. On va donc utiliser la méthode **step** sur laquelle j'attire votre attention car j'ai eu du mal à la trouver ! :warning: on remarque que le dernier point n'est pas suivi d'un trait horizontal, ce qui se comprend car dans les arguments il n'y pas d'information qui permettrait de savoir où l'arrêter ; ici cela ne pose pas de problème, mais cela peut parfois être trompeur.
|
|
|
|
|
|
<details>
|
|
|
<summary>Cliquez pour en savoir plus sur \*\*step\*\* et \*\*bar\*\*</summary>
|
|
|
|
|
|
Au moment de l'écriture, les tests avec \*\*bar\*\* ont donné des résultats complètement inutilisables : les valeurs étaient placées en fonction de leur rang (indépendamment des valeurs de x), avec l'intégralité des étiquettes en x ! Ainsi, avec plusieurs courbes qui ne partagent pas exactement la même liste de dates (par exemple, la série originale puis la même mais rééchantillonnée au mois), le rendu est absurde. Des posts de blog et autres discussions de forums ont fait le même constat, et proposaient des expédients pas forcément transposables à nos données (imposer la liste des étiquettes, valable avec une seule courbe et sans zoomer, ou... utiliser plot). En se référant à la doc (le bon réflexe ! ) la raison est apparue : cette méthode convient à des valeurs \*\*par catégories\*\*. \*\*Depuis, la méthode bar a évolué\*\* : elle permet maintenant de respecter la chronologie. C'est encore un bon exemple de la difficulté de trouver des informations : à part dans la doc officielle, qui existe pour chaque version, vous n'êtes pas à l'abri de trouver une information erronée ou incomplète (par exemple ; "bar ne convient pas donc il faut utiliser plot", alors que step est bien meilleur) ou une information correcte au moment de sa rédaction mais obsolète pour votre version.
|
|
|
|
|
|
<details><summary>Cliquez pour en savoir plus sur **step** et **bar**</summary>
|
|
|
Au moment de l'écriture, les tests avec **bar** ont donné des résultats complètement inutilisables : les valeurs étaient placées en fonction de leur rang (indépendamment des valeurs de x), avec l'intégralité des étiquettes en x ! Ainsi, avec plusieurs courbes qui ne partagent pas exactement la même liste de dates (par exemple, la série originale puis la même mais rééchantillonnée au mois), le rendu est absurde. Des posts de blog et autres discussions de forums ont fait le même constat, et proposaient des expédients pas forcément transposables à nos données (imposer la liste des étiquettes, valable avec une seule courbe et sans zoomer, ou... utiliser plot). En se référant à la doc (le bon réflexe ! ) la raison est apparue : cette méthode convient à des valeurs **par catégories**.
|
|
|
**Depuis, la méthode bar a évolué** : elle permet maintenant de respecter la chronologie.
|
|
|
C'est encore un bon exemple de la difficulté de trouver des informations : à part dans la doc officielle, qui existe pour chaque version, vous n'êtes pas à l'abri de trouver une information erronée ou incomplète (par exemple ; "bar ne convient pas donc il faut utiliser plot", alors que step est bien meilleur) ou une information correcte au moment de sa rédaction mais obsolète pour votre version.
|
|
|
|
|
|
(éléments développés actuellement [dans une autre page, consacrée au traitement des chroniques en hydrologie](https://gitlab.irstea.fr/orchyds-visualisation-et-traitement-de-chroniques-pas-de-temps-fixe/echasses/-/wikis/visualisations,-annexe-technique)
|
|
|
</details>
|
|
|
|
|
|
Pour étoffer l'arsenal des courbes, on va aussi utiliser **scatter** à titre d'exercice, même si ce n'est pas vraiment pertinent ici. Scatter permet de passer comme argument color et size soit une valeur unique soit in vecteur de même taille de x et y. Ici, il n'y a pas matière à faire varier la taille (sauf pour s'exercer), on propose de jouer sur la couleur. On en profite au passage pour manipuler un dictionnaire qui vous propose une relation entre les entiers de 1 à 12 et des couleurs.
|
|
|
|
|
|
</details>Pour étoffer l'arsenal des courbes, on va aussi utiliser **scatter** à titre d'exercice, même si ce n'est pas vraiment pertinent ici. Scatter permet de passer comme argument color et size soit une valeur unique soit in vecteur de même taille de x et y. Ici, il n'y a pas matière à faire varier la taille (sauf pour s'exercer), on propose de jouer sur la couleur. On en profite au passage pour manipuler un dictionnaire qui vous propose une relation entre les entiers de 1 à 12 et des couleurs.
|
|
|
|
|
|
```python
|
|
|
DICO_COULEURS_MOIS = {1: "dimgrey", 2: "black", 3: "palegreen", 4: 'mediumspringgreen', 5: 'forestgreen', 6: 'gold', 7: 'orange',
|
|
|
8:'orangered', 9:'deepskyblue', 10:'royalblue', 11:'navy', 12:'silver'}
|
|
|
```
|
|
|
|
|
|
Evidemment, vous pouvez aussi intervenir dans ce dictionnaire. Les données étant dans le pandas DataFrame DF_PTQ, on va remplir une liste en parcourant son index, dont les éléments sont au format date, et en allant chercher la valeur qui correspond à la clé "numéro de mois de cette date".
|
|
|
|
|
|
```python
|
|
|
# couleurs par mois : prétexte pour manipuler dictionnaires et scatter
|
|
|
liste_couleurs = []
|
|
|
for date in DF_PTQ.index:
|
|
|
liste_couleurs.append(DICO_COULEURS_MOIS[date.month])
|
|
|
```
|
|
|
|
|
|
Evidemment, on peut écrire la même chose avec une liste en intension (comprehension list). Il ne reste ensuite qu'à appliquer **scatter** en précisant pour l'argument **c** la liste de couleurs.
|
|
|
|
|
|
```python
|
|
|
liste_couleurs = [ DICO_COULEURS_MOIS[date.month] for date in DF_PTQ.index]
|
|
|
|
|
|
ax_t.scatter(DF_PTQ.index, DF_PTQ[etiquette_temperatures].tolist(), marker='*', ls=':',
|
|
|
label=etiquette_temperatures, c=liste_couleurs)
|
|
|
```
|
|
|
|
|
|
Si vous avez bien assimilé cette manip avec les dictionnaires, vous pourriez écrire une fonction adaptée au fichier dans sa forme initiale, avec des "janv-01" et autres "sept-02".
|
|
|
|
|
|
## soucis possibles avec **pandas** :panda_face: :cactus: ? :interrobang:
|
|
|
|
|
|
Des tests effectués par ailleurs montrent des soucis de gestion de 2 axes des y avec pandas.plot sur des pandas.Series, même en ne faisant intervenir que pandas.plot, et c'est très étonnant.
|
|
|
Dans certains bouts de code, on a défini un objet "Axes" puis un second avec twinx et les 2 axes des y sont placés à gauche et se superposent... En modulant le code avec des commentaires, on constate que la figure a bien 2 axes des y quand on trace une courbe sur chaque axe, et si on ajoute un troisième plot les deux axes des y se superposent...
|
|
|
|
|
|
## soucis possibles avec **pandas** :panda_face: :cactus: ? :interrobang:
|
|
|
|
|
|
Des tests effectués par ailleurs montrent des soucis de gestion de 2 axes des y avec pandas.plot sur des pandas.Series, même en ne faisant intervenir que pandas.plot, et c'est très étonnant. Dans certains bouts de code, on a défini un objet "Axes" puis un second avec twinx et les 2 axes des y sont placés à gauche et se superposent... En modulant le code avec des commentaires, on constate que la figure a bien 2 axes des y quand on trace une courbe sur chaque axe, et si on ajoute un troisième plot les deux axes des y se superposent...
|
|
|
|
|
|
[Retour à la page principale](Home)
|
|
|
| 2 objets Axes liés par twinx, un pandas.plot sur chacun | on ajoute un troisième tracé avec pandas.plot |
|
|
|
|---------------------------------------------------------|-----------------------------------------------|
|
|
|
| | ![pandas_plot_2axes_y_superposes](uploads/df02bd56ca0fddf3c21a408f7d55ac5a/pandas_plot_2axes_y_superposes.png) |
|
|
|
|
|
|
[Retour à la page principale](Home) |
|
|
\ No newline at end of file |
|
|
![pandas_plot_2axes_y_ok](uploads/61732ceae315884fb9e620d82e436298/pandas_plot_2axes_y_ok.png) |
|
|
\ No newline at end of file |