Une introduction à Pandas¶
- les Series
- les Dataframes
- Des exemples de traitement de données publiques
Contenu sous licence CC BY-SA 4.0, inspiré de https://github.com/pnavaro/big-data
Un outil pour l'analyse de données¶
- première version en 2011
- basé sur NumPy
- largement inspiré par la toolbox R pour la manipulation de données
- structures de données auto-descriptives
- Fonctions de chargement et écriture vers les formats de fichiers courants
- Fonctions de tracé
- Outils statistiques basiques
Les Pandas series¶
Une series Pandas :
- un tableau 1D de données (éventuellement hétérogènes)
- une séquence d'étiquettes appelée index de même longueur que le tableau 1D
l'index peut être du contenu numérique, des chaînes de caractères, ou des dates-heures.
si l'index est une valeur temporelle, alors il s'agit d'une time series
l'index par défaut est
range(len(data))
Illustration¶
import pandas as pd
import numpy as np
pd.set_option("display.max_rows", 8) # Pour limiter le nombre de lignes affichées
print(pd.Series([10, 8, 7, 6, 5]))
print(pd.Series([4, 3, 2, 1, 0.]))
Une série temporelle¶
Par exemple, les jours qui nous séparent du nouvel an.
today = pd.Timestamp.today()
next_year = today.year + 1
time_period = pd.period_range(today, f'01/01/{next_year}', freq="D")
pd.Series(index=time_period, data=range(len(time_period) - 1, -1, -1))
Un exemple de traitement¶
On exploite un texte tiré de ce site non officiel : http://www.sacred-texts.com/neu/mphg/mphg.htm
with open("exos/nee.txt") as f:
nee = f.read()
print(nee)
Dénombrer les occurrences de mots¶
On supprime la ponctuation
for s in '.', '!', ',', '?', ':', '[', ']', 'ARTHUR', 'HEAD KNIGHT', 'PARTY':
nee = nee.replace(s, '')
On transforme en minuscule et on découpe en une liste de mots
nees = nee.lower().split()
print(nees)
On crée un object compteur
from collections import Counter
c = Counter(nees)
On ne retient que les mots qui apparaissent plus de 2 fois
c = Counter({x: c[x] for x in c if c[x] > 2})
c
Création d'une série Pandas à partir de l'objet c
¶
Notons que la série est ordonnée avec un index croissant (dans l'ordre alphabétique).
words = pd.Series(c)
words
Représentation dans un histogramme¶
On commence par positionner certains paramètres de tracé
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
# Pour un rendu plus abouti https://seaborn.pydata.org/introduction.html
import seaborn as sns
sns.set()
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (9, 6) # Pour obtenir des figures plus grandes
words.plot(kind='bar');
Indexation et slicing¶
L'indexation et le slicing est une sorte de mélange entre les listes et les dictionnaires :
series[index]
pour accéder à la donnée correspondant Ãindex
series[i]
oùi
est un entier qui suit les règles de l'indexation en python
Nombre d'occurrences de la chaîne nee
print(words.index) # Pour rappel
words["nee"]
Trois dernières données de la série
words[-3:]
Ordonner la série¶
words.sort_values(inplace=True)
words.plot(kind='barh'); # On change pour un histogramme horizontal
Les Pandas Dataframes¶
- C'est la structure de base de Pandas
- un Dataframe est une structure de données tabulées à deux dimensions, potentiellement hétérogène
- un Dataframe est constitué de lignes et colonnes portant des étiquettes
- C'est en quelque sorte un "dictionnaire de Series".
Un exemple avec les arbres de la ville de Strasbourg¶
Conformément à l'ordonnance du 6 juin 2005 (qui prolonge la loi CADA), la ville de Strasbourg a commencé à mettre en ligne ses données publiques.
En particulier des données sur ses arbres : https://www.strasbourg.eu/arbres-alignements-espaces-verts
On veut exploiter ces données. Pour ce faire, on va :
- télécharger les données
- les charger dans un Dataframe
- les nettoyer/filtrer
- les représenter graphiquement
On télécharge et on nettoie¶
On commence par définir une fonction qui télécharge et extrait une archive zip.
from io import BytesIO
from urllib.request import urlopen
from zipfile import ZipFile
def download_unzip(zipurl, destination):
"""Download zipfile from URL and extract it to destination"""
with urlopen(zipurl) as zipresp:
with ZipFile(BytesIO(zipresp.read())) as zfile:
zfile.extractall(destination)
On l'utilise pour télécharger l'archive des données ouvertes de la ville de Strasbourg.
download_unzip("https://www.strasbourg.eu/documents/976405/1168331/CUS_CUS_DEPN_ARBR.zip", "arbres")
On liste le contenu de l'archive
%ls -R arbres
On charge le fichier csv comme un Dataframe.
arbres_all = pd.read_csv("arbres/CUS_CUS_DEPN_ARBR.csv",
encoding='latin', # Pour prendre en compte l'encoding qui n'est pas utf-8
delimiter=";", # Le caractère séparateur des colonnes
decimal=',') # Pour convertir les décimaux utilisant la notation ,
arbres_all
print(f"{len(arbres_all)} arbres recensés !")
On commence par lister les villes citées.
print(set(arbres_all['point vert VILLE']))
On ne s'intéresse qu'à la ville de Strasbourg
arbres = arbres_all[arbres_all['point vert VILLE'] == "STRASBOURG"]
print(f"Il ne reste plus que {len(arbres)} arbres.")
On enlève les données incomplètes.
arbres = arbres.dropna(axis=0, how='any')
print(f"Il ne reste plus que {len(arbres)} arbres.")
On veut comptabiliser les essences¶
On extrait la série des essences.
essences = set(arbres['Libellé_Essence'])
print(f"Il y a {len(essences)} essences différentes !")
Les 5 premières dans l'ordre alphabétique :
sorted(list(essences))[:5]
C'est bientôt Noël, on se limite aux sapins !
sapins = arbres[arbres['Libellé_Essence'].str.match("^Abies")]
sapins
On trace leur répartition
ax = sapins['Libellé_Essence'].value_counts().plot(kind="barh");
ax.set_xlabel("nombre d'arbres")
On veut faire des stastistiques par essence¶
On veut connaître la hauteur moyenne par essence pour chaque type Abies.
hauteurs_sapins = sapins.groupby(['Libellé_Essence'])["Hauteur arbre"]
pd.concat([hauteurs_sapins.min().rename('min'),
hauteurs_sapins.mean().rename('moyenne'),
hauteurs_sapins.max().rename('max')],
axis=1).plot(kind='barh');
Représentation géographique¶
On voudrait maintenant représenter la répartition des arbres par quartiers.
On utilise à nouveau les données ouvertes de la ville de Strasbourg, cette fois-ci concernant les quartiers : https://data.strasbourg.eu/explore/dataset/strasbourg-15-quartiers/information/
On télécharge, on extrait l'archive et on liste son contenu.
download_unzip("https://data.strasbourg.eu/explore/dataset/strasbourg-15-quartiers/download/?format=shp&timezone=Europe/Berlin&lang=fr", "quartiers")
%ls -R quartiers
%pip install geopandas folium
On charge le fichier comme un GeoDataFrame
:
import geopandas as gpd
gdf_quartiers = gpd.read_file("quartiers/strasbourg-15-quartiers.shp")
print(f"gdf_quartiers est de type {type(gdf_quartiers)}.")
gdf_quartiers
Avec Folium, on commence par représenter ces données géographiques sur un fond de carte.
import folium
# On crée une carte initialement centrée sur Strasbourg
STRASBOURG_COORD = (48.58, 7.75)
stras_map = folium.Map(STRASBOURG_COORD, zoom_start=11, tiles='cartodbpositron')
# On ajoute les données des quartiers
folium.GeoJson(gdf_quartiers).add_to(stras_map)
# On enregistre dans un fichier html
stras_map.save('stras_map.html')
# On trace dans le notebook
display(stras_map)
À l'emplacement de ces quartiers, on souhaite représenter une échelle de couleur en fonction de la densité d'arbres.
On constate que les noms de quartiers sont différents de ceux du jeu de données sur les arbres.
from pprint import pformat
set_quartiers = set(gdf_quartiers['libelle'])
set_arbres = set(arbres['Point vert Quartier usuel'])
def print_set_data(s: set):
"""Print set and its length"""
print(f"{pformat(s)} -> {len(s)}")
print_set_data(set_quartiers)
print_set_data(set_arbres)
Certains noms figurent dans les deux jeux de données :
intersection = set_quartiers.intersection(set_arbres)
print_set_data(intersection)
D'autres sont différents :
difference = set_arbres.difference(set_quartiers)
print_set_data(difference)
Afin d'obtenir de faire correspondre parfaitement les noms des deux jeux de données, on convertit les noms dans le Dataframe arbres
en supposant les correspondances ci-dessous.
convertion_dict = {
'BOURSE': 'BOURSE-ESPLANADE-KRUTENAU',
'CONSEIL-XV': 'ORANGERIE-CONSEIL DES XV',
'ESPLANADE': 'BOURSE-ESPLANADE-KRUTENAU',
'GARE': 'TRIBUNAL-GARE-PORTE DE SCHIRMECK',
'KRUTENAU': 'BOURSE-ESPLANADE-KRUTENAU',
'MONTAGNE VERTE': 'MONTAGNE-VERTE',
'MUSAU': 'NEUDORF',
'NEUHOF': 'NEUHOF1',
'ORANGERIE': 'ORANGERIE-CONSEIL DES XV',
'PLAINE DES BOUCHERS': 'MEINAU',
'POLYGONE': 'NEUHOF1',
'PORTE DE SCHIRMECK': 'TRIBUNAL-GARE-PORTE DE SCHIRMECK',
'STOCKFELD': 'NEUHOF2',
'TRIBUNAL': 'TRIBUNAL-GARE-PORTE DE SCHIRMECK',
'WACKEN': 'ROBERTSAU'
}
for k, v in convertion_dict.items():
arbres['Point vert Quartier usuel'] = \
arbres['Point vert Quartier usuel'].replace(to_replace=k, value=v)
arbres
On vérifie que l'ensemble des quartiers est le même pour les deux Dataframes quartiers
et arbres
.
set(arbres['Point vert Quartier usuel']) == set_quartiers
On construit une série qui contient le nombre d'arbres par quartier.
arbres_quartiers = arbres['Point vert Quartier usuel'].value_counts()
On trace le graphique en barres correspondant.
arbres_quartiers.plot(kind='barh');
On construit une nouvelle Series correspondant à l'aire de chaque quartier en $m^2$.
Pour que le calcul des aires soit fiables, les données de gdf_quartier
doivent être projetées.
Pour la France métropolitaine, on utilise la projection EPSG:2154, c'est-à -dire Lambert 93.
aires = gdf_quartiers.to_crs(2154).area
aires.index = gdf_quartiers["libelle"]
aires.sum()
On calcule la densité d'arbres par hectare.
densite = arbres_quartiers / aires * 10000
densite
On trace une carte colorée par la densité d'arbres avec l'objet Choropleth
.
folium.Choropleth(geo_data=gdf_quartiers,
data=densite,
key_on='feature.properties.libelle',
fill_color='YlGn',
fill_opacity=0.5,
line_opacity=0.2,
legend_name=r"Nombre d\'arbres par hectare").add_to(stras_map)
stras_map.save('stras_tree.html')
display(stras_map)
Exercice¶
Ecrire la fonction plot_essence()
qui prend en argument une essence d'arbres et qui trace le nombre d'arbres correspondant par quartier en utilisant choropleth
.
def plot_essence(essence):
pass
# Votre code ici
plot_essence("Acer")
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/plot_essence.py
Utilisation des widgets ipython¶
On souhaite proposer à l'utilisateur un menu de sélection pour afficher le nombre d'essences par quartier. Pour limiter la taille du menu, on regroupe les essences par genre (première partie du nom latin).
genres = set([nom.split()[0] for nom in essences])
La bibliothèque ipywidgets permet de générer très facilement un menu déroulant.
La fonction plot_essence()
est alors appelée avec comme
from ipywidgets import interact
interact(plot_essence, essence=sorted(genres));
Vers des applications web¶
ipywidgets permet de faire beaucoup plus que l'exemple ci-dessus. De plus, on peut transformer facilement un notebook en application avec voilà . Par exemple, transformons le notebook exos/stras_arbres.ipynb :
Dans un terminal, installer voila
:
pip install voila
Exécuter voila
sur le notebook :
voila exos/stras_arbres.ipynb
Références¶
- La documentation officielle
- Le cours de Pierre Navaro
- Le cours de Jake Vanderplas
- Des sites personnels de développeurs :
Annexe : une autre façon de représenter les occurences de mots¶
Cette fois, on n'utilise pas pandas
mais le module wordcloud
.
from wordcloud import WordCloud
# On crée un objet Wordcloud
wcloud = WordCloud(background_color="white", width=480, height=480, margin=0).generate(nee)
# On affiche l'image avec matplotlib
plt.imshow(wcloud, interpolation='bilinear')
plt.axis("off")
plt.margins(x=0, y=0)