From 2812ef405beb539c60f90a2b66655a06dbc6cc17 Mon Sep 17 00:00:00 2001 From: Lino Galiana <33896139+linogaliana@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:58:58 +0200 Subject: [PATCH] Petite viz sympa des prenoms (#242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * prépare partie deces * plus d'éléments * animation * labesl * ajoute minio * get from minio * get * bucket * bucket * modif * ajoute lien * ajoute lien * add * change access rule * use url * convert * animation * change ordre * Automated changes * Automated changes * un peu de texte * Automated changes * Automated changes Co-authored-by: github-actions[bot] --- .../course/visualisation/matplotlib/index.qmd | 275 ++++++++++++++++++ .../matplotlib/script_save_minio.py | 86 ++++++ 2 files changed, 361 insertions(+) create mode 100644 content/course/visualisation/matplotlib/script_save_minio.py diff --git a/content/course/visualisation/matplotlib/index.qmd b/content/course/visualisation/matplotlib/index.qmd index b4c5053a0..41449cede 100644 --- a/content/course/visualisation/matplotlib/index.qmd +++ b/content/course/visualisation/matplotlib/index.qmd @@ -460,6 +460,281 @@ Alors qu'avec le thème sombre (question 2), on obtient : {{< chart data="plotly2" >}} +# Exercices supplémentaires + +Pour ces exercices, il est recommandé de s'inspirer +des modèles présents dans la librairie +de graphiques `Python` présentée +dans https://www.python-graph-gallery.com/ + +## Les lollipop chart + +Cet exercice permet de s'entraîner +sur le fichier des naissances et des +décès de l'Insee. Il s'inspire d'une +excellente visualisation faite +par [Jean Dupin](https://www.jdupin.com/) +sur `Twitter` mettant en avant l'évolution, +année par année, des décomptes des +personnes nommées _"Jean"_ parmi les +personnes nées ou décédées: + +{{< tweet 1539567143972487169 >}} + +L'animation de Jean Dupin +est beaucoup plus raffinée que +celle que nous allons mettre en +oeuvre. + +### Récupération des données + +La récupération des données étant un peu complexe, +le code est donné pour vous permettre de vous +concentrer sur l'essentiel (si vous +voulez vous exercer avec le package `requests`, +essayez de le faire vous-même). + +Les données des décès sont disponibles de manière +historique dans des zip pour chaque année. + +```{python} +#| label: import-death +#| eval: false + +import shutil +import requests +import zipfile +import os +import glob +import pandas as pd + +def import_by_decade(decennie = 1970): + + url = f"https://www.insee.fr/fr/statistiques/fichier/4769950/deces-{decennie}-{decennie+9}-csv.zip" + + req = requests.get(url) + + with open(f"deces_{decennie}.zip",'wb') as f: + f.write(req.content) + + with zipfile.ZipFile(f"deces_{decennie}.zip", 'r') as zip_ref: + zip_ref.extractall(f"deces_{decennie}") + + csv_files = glob.glob(os.path.join(f"deces_{decennie}", "*.csv")) + + df = [pd.read_csv(f, sep = ";", encoding="utf-8").assign(annee = f) for f in csv_files] + df = pd.concat(df) + df[['nom','prenom']] = df['nomprenom'].str.split("*", expand=True) + df['prenom'] = df['prenom'].str.replace("/","") + df['annee'] = df['annee'].str.rsplit("/").str[-1].str.replace("(Deces_|.csv|deces-)","").astype(int) + + shutil.rmtree(f"deces_{decennie}") + os.remove(f"deces_{decennie}.zip") + + return df + + +dfs = [import_by_decade(d) for d in [1970, 1980, 1990, 2000, 2010]] +deces = pd.concat(dfs) +``` + +Le fichier des naissances est plus simple à récupérer. +Voici le code pour l'obtenir: + +```{python} +#| label: import-birth +#| eval: false + +year = 2021 +url_naissance = f"https://www.insee.fr/fr/statistiques/fichier/2540004/nat{year}_csv.zip" + +req = requests.get(url_naissance) + +with open(f"naissance_{year}.zip",'wb') as f: + f.write(req.content) + +with zipfile.ZipFile(f"naissance_{year}.zip", 'r') as zip_ref: + zip_ref.extractall(f"naissance_{year}") + +naissance = pd.read_csv(f"naissance_{year}/nat{year}.csv", sep = ";") +naissance = naissance.dropna(subset = ['preusuel'] ) +``` + +On peut enfin restructurer les `DataFrames` pour obtenir un +seul jeu de données, en se restreignant aux _"JEAN"_: + +```{python} +#| label: restructure +#| eval: false + +jean_naiss = naissance.loc[naissance['preusuel'] == "JEAN"].loc[:, ['annais', 'nombre']] +jean_naiss = jean_naiss.rename({"annais": "annee"}, axis = "columns") +jean_naiss = jean_naiss.groupby('annee').sum().reset_index() +jean_deces = deces.loc[deces["prenom"] == "JEAN"] +jean_deces = jean_deces.groupby('annee').size().reset_index() +jean_deces.columns = ['annee', "nombre"] +jean_naiss.columns = ['annee', "nombre"] +df = pd.concat( + [ + jean_deces.assign(source = "deces"), + jean_naiss.assign(source = "naissance") + ]) +df = df.loc[df['annee'] != "XXXX"] +df['annee']=df['annee'].astype(int) +df = df.loc[df['annee'] > 1971] + +df.head(3) +``` + +```{python} +#| echo: false +#| label: get-from-minio + +import pyarrow as pa +import pyarrow.parquet as pq +from pyarrow import fs + +#bucket = "lgaliana" +#s3 = fs.S3FileSystem(endpoint_override="https://"+"minio.lab.sspcloud.fr") +#df = pq.read_pandas(f'{bucket}/diffusion/prenoms.parquet', filesystem=s3).to_pandas() + +import requests +req = requests.get("https://minio.lab.sspcloud.fr/lgaliana/diffusion/prenoms.parquet") + +with open(f"prenoms.parquet",'wb') as f: + f.write(req.content) + +df = pq.read_pandas("prenoms.parquet").to_pandas() +df.head(3) +``` + + +## Représentation graphique + +Vous pouvez vous aider du modèle présent +dans https://www.python-graph-gallery.com + +{{% box status="exercise" title="Exercice" +icon="fas fa-pencil-alt" %}} + +Pour commencer, on va se concentrer sur la +production d'un seul graphique +(décès ou naissance, vous choisissez) + +1. Créer un objet `df_plot` qui se restreint à une +source +2. Fixer une année sous le nom `max_year` (par exemple +votre année de naissance). Elle servira ensuite de paramètre +à une fonction +3. Restreindre `df_plot` aux années antérieures à `max_year` +4. Créer une variable `my_range` fixant la séquence des années +entre la plus petite année du dataset et `max_year` (inclus) +5. Créer un array numpy qui vaut `orange` lorsque l'observation +en question est `max_year` et `skyblue` sinon +6. Utiliser les fonctions adéquates de `matplotlib` pour créer +le _lollipop chart_ +{{% /box %}} + +A ce stade, vous devriez avoir une version fonctionnelle +qui peut servir de +base à la généralisation. + +```{python} +#| echo: false +import numpy as np +import matplotlib.pyplot as plt + +def plot_jean_year(df, max_year, savefig = True): + + fig, axes = plt.subplots(1, 2, sharey = True) + + for x, source in enumerate(df.source.value_counts().index.values): + df_plot = df.loc[df['source'] == source] + # Reorder it based on values: + ordered_df = df_plot.copy()#.sort_values(by='nombre') + + ordered_df = ordered_df.loc[ordered_df['annee']<=max_year] + my_range=range(ordered_df.annee.min(), max_year + 1) + my_color=np.where(ordered_df['annee']==max_year, 'orange', 'skyblue') + + # The horizontal plot is made using the hline() function + axes[x].hlines(y=my_range, xmin=0, xmax=ordered_df['nombre'], color = my_color, alpha=0.4) + axes[x].scatter(ordered_df['nombre'], my_range, color = my_color, alpha=1) + axes[x].set_title(f'{source.upper()}', + fontsize = 14) + axes[x].set_ylim([1972, 2016]) + axes[x].set_xlim([0, 5000]) + if savefig: + fig.savefig(f'figure_{max_year}.png', bbox_inches='tight') +``` + + +{{% box status="exercise" title="Exercice" +icon="fas fa-pencil-alt" %}} + + +1. A partir du code précédent, généraliser en utilisant +une boucle `for` à partir du résultat de +`enumerate(df.source.value_counts().index.values)` pour +créer un graphique pour une année donnée de `maxyear`. +Avant cette boucle, ne pas oublier de créer un objet +`matplotlib` vide à remplir dans la boucle + +```python +fig, axes = plt.subplots(1, 2, sharey = True) +``` + +2. Encapsuler ce code dans une fonction qui +prend en argument un `DataFrame` et une +année `max_year` + +Voici un exemple d'output pour `max_year = 2010`: + +```{python} +#| echo: false +#| label: example-function +plot_jean_year(df, 2010, savefig = False) +``` + +3. Pour créer une animation, on propose +d'utiliser la solution présentée +dans https://www.python-graph-gallery.com/animation/. +et qui nécessite le logiciel `imagemagick`. +Sauvegarder chaque itération dans un fichier +dont le nom a la structure `figure_{year}.png`. + +```{python} +#| echo: false +#| output: false +[plot_jean_year(df, t) for t in range(1972,2016)] +``` + +Enfin, pour animer les images, on peut utiliser +la librairie `imageio`: + +```{python} +import glob +import imageio.v2 as imageio +#os.system("convert -delay 15 figure_*.png animation.gif") + +filenames=glob.glob("figure_*.png") +filenames.sort() + +with imageio.get_writer('animation.gif', mode='I') as writer: + for filename in filenames: + image = imageio.imread(filename) + writer.append_data(image) +``` + +L'animation obtenue est la suivante: + +![Animation](animation.gif) + +{{% /box %}} + + + + # Exercices supplémentaires https://plotly.com/python/v3/3d-network-graph/ diff --git a/content/course/visualisation/matplotlib/script_save_minio.py b/content/course/visualisation/matplotlib/script_save_minio.py new file mode 100644 index 000000000..f9025a379 --- /dev/null +++ b/content/course/visualisation/matplotlib/script_save_minio.py @@ -0,0 +1,86 @@ +import shutil +import requests +import zipfile +import os +import glob +import pandas as pd + +import pyarrow as pa +import pyarrow.parquet as pq +from pyarrow import fs + + +def import_by_decade(decennie = 1970): + + url = f"https://www.insee.fr/fr/statistiques/fichier/4769950/deces-{decennie}-{decennie+9}-csv.zip" + + req = requests.get(url) + + with open(f"deces_{decennie}.zip",'wb') as f: + f.write(req.content) + + with zipfile.ZipFile(f"deces_{decennie}.zip", 'r') as zip_ref: + zip_ref.extractall(f"deces_{decennie}") + + csv_files = glob.glob(os.path.join(f"deces_{decennie}", "*.csv")) + + df = [pd.read_csv(f, sep = ";", encoding="utf-8").assign(annee = f) for f in csv_files] + df = pd.concat(df) + df[['nom','prenom']] = df['nomprenom'].str.split("*", expand=True) + df['prenom'] = df['prenom'].str.replace("/","") + df['annee'] = df['annee'].str.rsplit("/").str[-1].str.replace("(Deces_|.csv|deces-)","").astype(int) + + shutil.rmtree(f"deces_{decennie}") + os.remove(f"deces_{decennie}.zip") + + return df + + +dfs = [import_by_decade(d) for d in [1970, 1980, 1990, 2000, 2010]] +deces = pd.concat(dfs) + + +# NAISSANCES ----------------- + +year = 2021 +url_naissance = f"https://www.insee.fr/fr/statistiques/fichier/2540004/nat{year}_csv.zip" + +req = requests.get(url_naissance) + +with open(f"naissance_{year}.zip",'wb') as f: + f.write(req.content) + +with zipfile.ZipFile(f"naissance_{year}.zip", 'r') as zip_ref: + zip_ref.extractall(f"naissance_{year}") + +naissance = pd.read_csv(f"naissance_{year}/nat{year}.csv", sep = ";") +naissance = naissance.dropna(subset = ['preusuel'] ) + + +# RESTRUCTURE -------------- + +jean_naiss = naissance.loc[naissance['preusuel'] == "JEAN"].loc[:, ['annais', 'nombre']] +jean_naiss = jean_naiss.rename({"annais": "annee"}, axis = "columns") +jean_naiss = jean_naiss.groupby('annee').sum().reset_index() +jean_deces = deces.loc[deces["prenom"] == "JEAN"] +jean_deces = jean_deces.groupby('annee').size().reset_index() +jean_deces.columns = ['annee', "nombre"] +jean_naiss.columns = ['annee', "nombre"] +df = pd.concat( + [ + jean_deces.assign(source = "deces"), + jean_naiss.assign(source = "naissance") + ]) +df = df.loc[df['annee'] != "XXXX"] +df['annee']=df['annee'].astype(int) +df = df.loc[df['annee'] > 1971] + + +# SAVE IN MINIO -------------- + +s3 = fs.S3FileSystem(endpoint_override="http://"+"minio.lab.sspcloud.fr") + +bucket = "lgaliana" +table = pa.Table.from_pandas(df, preserve_index=False) +pq.write_table(table, f'{bucket}/diffusion/prenoms.parquet', filesystem=s3) +