Une introduction à Pandas¶

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¶

Documentation officielle

  • 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¶

In [1]:
import pandas as pd
import numpy as np
pd.set_option("display.max_rows", 8)  # Pour limiter le nombre de lignes affichées
In [2]:
print(pd.Series([10, 8, 7, 6, 5]))
print(pd.Series([4, 3, 2, 1, 0.]))
0    10
1     8
2     7
3     6
4     5
dtype: int64
0    4.0
1    3.0
2    2.0
3    1.0
4    0.0
dtype: float64

Une série temporelle¶

Par exemple, les jours qui nous séparent du nouvel an.

In [3]:
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))
Out[3]:
2023-12-08    24
2023-12-09    23
2023-12-10    22
2023-12-11    21
              ..
2023-12-29     3
2023-12-30     2
2023-12-31     1
2024-01-01     0
Freq: D, Length: 25, dtype: int64

Un exemple de traitement¶

On exploite un texte tiré de ce site non officiel : http://www.sacred-texts.com/neu/mphg/mphg.htm

In [4]:
with open("exos/nee.txt") as f:
    nee = f.read()

print(nee)
HEAD KNIGHT:  Nee!
  Nee!
  Nee!
  Nee!
ARTHUR:  Who are you?
HEAD KNIGHT:  We are the Knights Who Say... Nee!
ARTHUR:  No!  Not the Knights Who Say Nee!
HEAD KNIGHT:  The same!
BEDEMIR:  Who are they?
HEAD KNIGHT:  We are the keepers of the sacred words:  Nee, Pen, and
  Nee-wom!
RANDOM:  Nee-wom!
ARTHUR:  Those who hear them seldom live to tell the tale!
HEAD KNIGHT:  The Knights Who Say Nee demand a sacrifice!
ARTHUR:  Knights of Nee, we are but simple travellers who seek the
  enchanter who lives beyond these woods.
HEAD KNIGHT:  Nee!  Nee!  Nee!  Nee!
ARTHUR and PARTY:  Oh, ow!
HEAD KNIGHT:  We shall say 'nee' again to you if you do not appease us.
ARTHUR:  Well, what is it you want?
HEAD KNIGHT:  We want... a shrubbery!
  [dramatic chord]
ARTHUR:  A what?
HEAD KNIGHT:  Nee!  Nee!
ARTHUR and PARTY:  Oh, ow!
ARTHUR:  Please, please!  No more!  We shall find a shrubbery.
HEAD KNIGHT:  You must return here with a shrubbery or else you will
  never pass through this wood alive!
ARTHUR:  O Knights of Nee, you are just and fair, and we will return
  with a shrubbery.
HEAD KNIGHT:  One that looks nice.
ARTHUR:  Of course.
HEAD KNIGHT:  And not too expensive.
ARTHUR:  Yes.
HEAD KNIGHTS:  Now... go!

Dénombrer les occurrences de mots¶

On supprime la ponctuation

In [5]:
for s in '.', '!', ',', '?', ':', '[', ']', 'ARTHUR', 'HEAD KNIGHT', 'PARTY':
    nee = nee.replace(s, '')

On transforme en minuscule et on découpe en une liste de mots

In [6]:
nees = nee.lower().split()
print(nees)
['nee', 'nee', 'nee', 'nee', 'who', 'are', 'you', 'we', 'are', 'the', 'knights', 'who', 'say', 'nee', 'no', 'not', 'the', 'knights', 'who', 'say', 'nee', 'the', 'same', 'bedemir', 'who', 'are', 'they', 'we', 'are', 'the', 'keepers', 'of', 'the', 'sacred', 'words', 'nee', 'pen', 'and', 'nee-wom', 'random', 'nee-wom', 'those', 'who', 'hear', 'them', 'seldom', 'live', 'to', 'tell', 'the', 'tale', 'the', 'knights', 'who', 'say', 'nee', 'demand', 'a', 'sacrifice', 'knights', 'of', 'nee', 'we', 'are', 'but', 'simple', 'travellers', 'who', 'seek', 'the', 'enchanter', 'who', 'lives', 'beyond', 'these', 'woods', 'nee', 'nee', 'nee', 'nee', 'and', 'oh', 'ow', 'we', 'shall', 'say', "'nee'", 'again', 'to', 'you', 'if', 'you', 'do', 'not', 'appease', 'us', 'well', 'what', 'is', 'it', 'you', 'want', 'we', 'want', 'a', 'shrubbery', 'dramatic', 'chord', 'a', 'what', 'nee', 'nee', 'and', 'oh', 'ow', 'please', 'please', 'no', 'more', 'we', 'shall', 'find', 'a', 'shrubbery', 'you', 'must', 'return', 'here', 'with', 'a', 'shrubbery', 'or', 'else', 'you', 'will', 'never', 'pass', 'through', 'this', 'wood', 'alive', 'o', 'knights', 'of', 'nee', 'you', 'are', 'just', 'and', 'fair', 'and', 'we', 'will', 'return', 'with', 'a', 'shrubbery', 'one', 'that', 'looks', 'nice', 'of', 'course', 'and', 'not', 'too', 'expensive', 'yes', 's', 'now', 'go']

On crée un object compteur

In [7]:
from collections import Counter
c = Counter(nees)

On ne retient que les mots qui apparaissent plus de 2 fois

In [8]:
c = Counter({x: c[x] for x in c if c[x] > 2})
c
Out[8]:
Counter({'nee': 16,
         'who': 8,
         'the': 8,
         'you': 7,
         'we': 7,
         'are': 6,
         'and': 6,
         'a': 6,
         'knights': 5,
         'say': 4,
         'of': 4,
         'shrubbery': 4,
         'not': 3})

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).

In [9]:
words = pd.Series(c)
words
Out[9]:
nee          16
who           8
are           6
you           7
             ..
of            4
and           6
a             6
shrubbery     4
Length: 13, dtype: int64

Représentation dans un histogramme¶

On commence par positionner certains paramètres de tracé

In [10]:
%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
In [11]:
words.plot(kind='bar');
No description has been provided for this image

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

In [12]:
print(words.index)  # Pour rappel
words["nee"]
Index(['nee', 'who', 'are', 'you', 'we', 'the', 'knights', 'say', 'not', 'of',
       'and', 'a', 'shrubbery'],
      dtype='object')
Out[12]:
16

Trois dernières données de la série

In [13]:
words[-3:]
Out[13]:
and          6
a            6
shrubbery    4
dtype: int64

Ordonner la série¶

In [14]:
words.sort_values(inplace=True)
words.plot(kind='barh');  # On change pour un histogramme horizontal
No description has been provided for this image

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 :

  1. télécharger les données
  2. les charger dans un Dataframe
  3. les nettoyer/filtrer
  4. 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.

In [15]:
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.

In [16]:
download_unzip("https://www.strasbourg.eu/documents/976405/1168331/CUS_CUS_DEPN_ARBR.zip", "arbres")

On liste le contenu de l'archive

In [17]:
%ls -R arbres
arbres:
CUS_CUS_DEPN_ARBR.csv

On charge le fichier csv comme un Dataframe.

In [18]:
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
Out[18]:
Num point vert point vert NOM_USUEL point vert ADRESSE point vert VILLE Point vert Quartier usuel point vert TYPOLOGIE n°arbre SIG Libellé_Essence Diam fût à 1m Hauteur arbre
0 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15783 Tilia x 'Euchlora' 25.0 8.0
1 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15784 Tilia x 'Euchlora' 8.0 6.5
2 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15785 Tilia x 'Euchlora' 33.0 7.5
3 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15786 Tilia x 'Euchlora' 23.0 9.0
... ... ... ... ... ... ... ... ... ... ...
79134 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG STOCKFELD ACJF - Accompagnement de jardins familiaux 87652 Picea abies 30.0 10.0
79135 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG STOCKFELD ACJF - Accompagnement de jardins familiaux 87653 Picea abies 30.0 10.0
79136 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG STOCKFELD ACJF - Accompagnement de jardins familiaux 87654 Picea abies 30.0 10.0
79137 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG STOCKFELD ACJF - Accompagnement de jardins familiaux 87655 Picea abies 30.0 10.0

79138 rows × 10 columns

In [19]:
print(f"{len(arbres_all)} arbres recensés !")
79138 arbres recensés !

On commence par lister les villes citées.

In [20]:
print(set(arbres_all['point vert VILLE']))
{'LINGOLSHEIM', 'WOLFISHEIM', 'ESCHAU', 'OBERSCHAEFFOLSHEIM', 'ILLKIRCH-GRAFFENSTADEN', 'HOENHEIM', 'FEGERSHEIM', 'MITTELHAUSBERGEN', 'HOLTZHEIM', 'ECKBOLSHEIM', 'WANTZENAU (LA)', 'NIEDERHAUSBERGEN', 'LIPSHEIM', 'BLAESHEIM', 'VENDENHEIM', 'WANTZENAU (La)', 'REICHSTETT', 'STRASBOURG', 'SOUFFELWEYERSHEIM', 'PLOBSHEIM', 'GEISPOLSHEIM', 'LAMPERTHEIM', 'OSTWALD', 'SCHILTIGHEIM', 'BISCHHEIM', 'MUNDOLSHEIM', 'ECKWERSHEIM', 'ENTZHEIM', 'OBERHAUSBERGEN', nan}

On ne s'intéresse qu'à la ville de Strasbourg

In [21]:
arbres = arbres_all[arbres_all['point vert VILLE'] ==  "STRASBOURG"]
print(f"Il ne reste plus que {len(arbres)} arbres.")
Il ne reste plus que 64624 arbres.

On enlève les données incomplètes.

In [22]:
arbres = arbres.dropna(axis=0, how='any')
print(f"Il ne reste plus que {len(arbres)} arbres.")
Il ne reste plus que 61382 arbres.

On veut comptabiliser les essences¶

On extrait la série des essences.

In [23]:
essences = set(arbres['Libellé_Essence'])
print(f"Il y a {len(essences)} essences différentes !")
Il y a 456 essences différentes !

Les 5 premières dans l'ordre alphabétique :

In [24]:
sorted(list(essences))[:5]
Out[24]:
['Abies (sp non determinée)',
 'Abies alba',
 'Abies cephalonica',
 'Abies concolor',
 'Abies grandis']

C'est bientôt Noël, on se limite aux sapins !

In [25]:
sapins = arbres[arbres['Libellé_Essence'].str.match("^Abies")]
sapins
Out[25]:
Num point vert point vert NOM_USUEL point vert ADRESSE point vert VILLE Point vert Quartier usuel point vert TYPOLOGIE n°arbre SIG Libellé_Essence Diam fût à 1m Hauteur arbre
2656 620.0 Parc des Contades Hirschler (rue René) STRASBOURG CONSEIL-XV PARC - Parcs 20379 Abies concolor 26.0 14.0
2657 620.0 Parc des Contades Hirschler (rue René) STRASBOURG CONSEIL-XV PARC - Parcs 20380 Abies concolor 23.0 13.5
9235 704.0 Groupe scolaire Ampère Wattwiller (39, rue de) STRASBOURG NEUDORF EESE2 - Espaces des établissements sociaux et ... 25143 Abies alba 10.0 6.0
9575 1151.0 Groupe scolaire Charles Adolphe Wurtz Rieth (51, rue du) STRASBOURG CRONENBOURG EESE2 - Espaces des établissements sociaux et ... 44237 Abies nordmanniana 10.0 4.0
... ... ... ... ... ... ... ... ... ... ...
75935 318.0 Parc de la Citadelle -(01)- Secteur Centre et Est Belges (quai des) STRASBOURG ESPLANADE PARC - Parcs 11419 Abies nordmanniana 38.0 18.6
75940 318.0 Parc de la Citadelle -(01)- Secteur Centre et Est Belges (quai des) STRASBOURG ESPLANADE PARC - Parcs 11424 Abies concolor 28.0 11.6
75941 318.0 Parc de la Citadelle -(01)- Secteur Centre et Est Belges (quai des) STRASBOURG ESPLANADE PARC - Parcs 11425 Abies concolor 18.0 5.4
78276 997.0 Parc de Pourtalès Mélanie (rue) STRASBOURG ROBERTSAU PARC - Parcs 40616 Abies alba 29.0 16.0

114 rows × 10 columns

On trace leur répartition

In [26]:
ax = sapins['Libellé_Essence'].value_counts().plot(kind="barh");
ax.set_xlabel("nombre d'arbres")
Out[26]:
Text(0.5, 0, "nombre d'arbres")
No description has been provided for this image

On veut faire des stastistiques par essence¶

On veut connaître la hauteur moyenne par essence pour chaque type Abies.

In [27]:
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');
No description has been provided for this image

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.

In [28]:
download_unzip("https://data.strasbourg.eu/explore/dataset/strasbourg-15-quartiers/download/?format=shp&timezone=Europe/Berlin&lang=fr", "quartiers")
%ls -R quartiers
quartiers:
strasbourg-15-quartiers.dbf  strasbourg-15-quartiers.shp
strasbourg-15-quartiers.prj  strasbourg-15-quartiers.shx

C'est le fichier .shp qui nous intéresse.

À ce stade, nous avons besoin des bibliothèques GeoPandas et Folium que l'on installe avec conda.

In [29]:
%pip install geopandas folium
Requirement already satisfied: geopandas in /home/jovyan/.local/lib/python3.11/site-packages (0.14.1)
Requirement already satisfied: folium in /home/jovyan/.local/lib/python3.11/site-packages (0.15.1)
Requirement already satisfied: fiona>=1.8.21 in /opt/conda/lib/python3.11/site-packages (from geopandas) (1.9.4.post1)
Requirement already satisfied: packaging in /opt/conda/lib/python3.11/site-packages (from geopandas) (23.1)
Requirement already satisfied: pandas>=1.4.0 in /home/jovyan/.local/lib/python3.11/site-packages (from geopandas) (2.1.4)
Requirement already satisfied: pyproj>=3.3.0 in /opt/conda/lib/python3.11/site-packages (from geopandas) (3.6.1)
Requirement already satisfied: shapely>=1.8.0 in /opt/conda/lib/python3.11/site-packages (from geopandas) (2.0.1)
Requirement already satisfied: branca>=0.6.0 in /opt/conda/lib/python3.11/site-packages (from folium) (0.6.0)
Requirement already satisfied: jinja2>=2.9 in /opt/conda/lib/python3.11/site-packages (from folium) (3.1.2)
Requirement already satisfied: numpy in /home/jovyan/.local/lib/python3.11/site-packages (from folium) (1.26.2)
Requirement already satisfied: requests in /opt/conda/lib/python3.11/site-packages (from folium) (2.31.0)
Requirement already satisfied: xyzservices in /home/jovyan/.local/lib/python3.11/site-packages (from folium) (2023.10.1)
Requirement already satisfied: attrs>=19.2.0 in /opt/conda/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas) (23.1.0)
Requirement already satisfied: certifi in /opt/conda/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas) (2023.7.22)
Requirement already satisfied: click~=8.0 in /opt/conda/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas) (8.1.7)
Requirement already satisfied: click-plugins>=1.0 in /opt/conda/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas) (1.1.1)
Requirement already satisfied: cligj>=0.5 in /opt/conda/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas) (0.7.2)
Requirement already satisfied: six in /opt/conda/lib/python3.11/site-packages (from fiona>=1.8.21->geopandas) (1.16.0)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.11/site-packages (from jinja2>=2.9->folium) (2.1.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.11/site-packages (from pandas>=1.4.0->geopandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.11/site-packages (from pandas>=1.4.0->geopandas) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.1 in /opt/conda/lib/python3.11/site-packages (from pandas>=1.4.0->geopandas) (2023.3)
Requirement already satisfied: charset-normalizer<4,>=2 in /opt/conda/lib/python3.11/site-packages (from requests->folium) (3.2.0)
Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.11/site-packages (from requests->folium) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.11/site-packages (from requests->folium) (2.0.4)
Note: you may need to restart the kernel to use updated packages.

On charge le fichier comme un GeoDataFrame :

In [30]:
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
gdf_quartiers est de type <class 'geopandas.geodataframe.GeoDataFrame'>.
Out[30]:
code_sct libelle geometry
0 1B CRONENBOURG POLYGON ((7.71391 48.58707, 7.71380 48.58767, ...
1 10B PORT DU RHIN POLYGON ((7.78154 48.57908, 7.78152 48.57925, ...
2 5 BOURSE-ESPLANADE-KRUTENAU POLYGON ((7.75002 48.57454, 7.74983 48.57463, ...
3 11A NEUHOF1 POLYGON ((7.75904 48.56022, 7.75924 48.55992, ...
... ... ... ...
11 6 ORANGERIE-CONSEIL DES XV POLYGON ((7.78101 48.57854, 7.78127 48.57923, ...
12 2A KOENIGSHOFFEN POLYGON ((7.72644 48.57736, 7.72633 48.57730, ...
13 11B NEUHOF2 POLYGON ((7.76317 48.52212, 7.76333 48.52212, ...
14 4 CENTRE POLYGON ((7.75878 48.58799, 7.75891 48.58792, ...

15 rows × 3 columns

Avec Folium, on commence par représenter ces données géographiques sur un fond de carte.

In [31]:
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)
Make this Notebook Trusted to load map: File -> Trust Notebook

À 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.

In [32]:
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)
{'BOURSE-ESPLANADE-KRUTENAU',
 'CENTRE',
 'CRONENBOURG',
 'ELSAU',
 'HAUTEPIERRE',
 'KOENIGSHOFFEN',
 'MEINAU',
 'MONTAGNE-VERTE',
 'NEUDORF',
 'NEUHOF1',
 'NEUHOF2',
 'ORANGERIE-CONSEIL DES XV',
 'PORT DU RHIN',
 'ROBERTSAU',
 'TRIBUNAL-GARE-PORTE DE SCHIRMECK'} -> 15
{'BOURSE',
 'CENTRE',
 'CONSEIL-XV',
 'CRONENBOURG',
 'ELSAU',
 'ESPLANADE',
 'GARE',
 'HAUTEPIERRE',
 'KOENIGSHOFFEN',
 'KRUTENAU',
 'MEINAU',
 'MONTAGNE VERTE',
 'MUSAU',
 'NEUDORF',
 'NEUHOF',
 'ORANGERIE',
 'PLAINE DES BOUCHERS',
 'POLYGONE',
 'PORT DU RHIN',
 'PORTE DE SCHIRMECK',
 'ROBERTSAU',
 'STOCKFELD',
 'TRIBUNAL',
 'WACKEN'} -> 24

Certains noms figurent dans les deux jeux de données :

In [33]:
intersection = set_quartiers.intersection(set_arbres)
print_set_data(intersection)
{'CENTRE',
 'CRONENBOURG',
 'ELSAU',
 'HAUTEPIERRE',
 'KOENIGSHOFFEN',
 'MEINAU',
 'NEUDORF',
 'PORT DU RHIN',
 'ROBERTSAU'} -> 9

D'autres sont différents :

In [34]:
difference = set_arbres.difference(set_quartiers)
print_set_data(difference)
{'BOURSE',
 'CONSEIL-XV',
 'ESPLANADE',
 'GARE',
 'KRUTENAU',
 'MONTAGNE VERTE',
 'MUSAU',
 'NEUHOF',
 'ORANGERIE',
 'PLAINE DES BOUCHERS',
 'POLYGONE',
 'PORTE DE SCHIRMECK',
 'STOCKFELD',
 'TRIBUNAL',
 'WACKEN'} -> 15

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.

In [35]:
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
Out[35]:
Num point vert point vert NOM_USUEL point vert ADRESSE point vert VILLE Point vert Quartier usuel point vert TYPOLOGIE n°arbre SIG Libellé_Essence Diam fût à 1m Hauteur arbre
0 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15783 Tilia x 'Euchlora' 25.0 8.0
1 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15784 Tilia x 'Euchlora' 8.0 6.5
2 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15785 Tilia x 'Euchlora' 33.0 7.5
3 450.0 Rue du Houblon Houblon (rue du) STRASBOURG CENTRE ACCE - Accompagnement de cours d'eau 15786 Tilia x 'Euchlora' 23.0 9.0
... ... ... ... ... ... ... ... ... ... ...
79134 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG NEUHOF2 ACJF - Accompagnement de jardins familiaux 87652 Picea abies 30.0 10.0
79135 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG NEUHOF2 ACJF - Accompagnement de jardins familiaux 87653 Picea abies 30.0 10.0
79136 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG NEUHOF2 ACJF - Accompagnement de jardins familiaux 87654 Picea abies 30.0 10.0
79137 859.0 Krummerort Oberjaegerhof (route de l') STRASBOURG NEUHOF2 ACJF - Accompagnement de jardins familiaux 87655 Picea abies 30.0 10.0

61382 rows × 10 columns

On vérifie que l'ensemble des quartiers est le même pour les deux Dataframes quartiers et arbres.

In [36]:
set(arbres['Point vert Quartier usuel']) == set_quartiers
Out[36]:
True

On construit une série qui contient le nombre d'arbres par quartier.

In [37]:
arbres_quartiers = arbres['Point vert Quartier usuel'].value_counts()

On trace le graphique en barres correspondant.

In [38]:
arbres_quartiers.plot(kind='barh');
No description has been provided for this image

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.

In [39]:
aires = gdf_quartiers.to_crs(2154).area
aires.index = gdf_quartiers["libelle"]
aires.sum()
Out[39]:
78225408.33604598

On calcule la densité d'arbres par hectare.

In [40]:
densite = arbres_quartiers / aires * 10000
densite
Out[40]:
BOURSE-ESPLANADE-KRUTENAU           15.316666
CENTRE                              39.367782
CRONENBOURG                         12.038048
ELSAU                               10.169214
                                      ...    
ORANGERIE-CONSEIL DES XV            19.344847
PORT DU RHIN                         4.874460
ROBERTSAU                            5.607219
TRIBUNAL-GARE-PORTE DE SCHIRMECK     6.536751
Length: 15, dtype: float64

On trace une carte colorée par la densité d'arbres avec l'objet Choropleth.

In [41]:
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)
Make this Notebook Trusted to load map: File -> Trust Notebook

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.

In [42]:
def plot_essence(essence):
    pass
    # Votre code ici

plot_essence("Acer")
In [43]:
# 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).

In [44]:
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

In [45]:
from ipywidgets import interact

interact(plot_essence, essence=sorted(genres));
interactive(children=(Dropdown(description='essence', options=('Abies', 'Acer', 'Aesculus', 'Ailanthus', 'Albi…

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

Annexe : une autre façon de représenter les occurences de mots¶

Cette fois, on n'utilise pas pandas mais le module wordcloud.

In [46]:
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)
No description has been provided for this image