• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 21/09/2019

Effectuez les opérations d'algèbre relationnelle sur les DataFrames

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Dans ce chapitre, nous continuons à investiguer l'objet dataframe. Plus précisément, nous allons voir comment effectuer les opérations de base de l'algèbre relationnelle.

L'algèbre relationnelle

L'algèbre relationnelle est une théorie permettant de manipuler des données disposées sous forme de tableau ; et ça tombe bien : un dataframe, c'est justement un tableau !

Dans ce domaine, un tableau, on appelle cela une relation.

Plus précisément, l'algèbre relationnelle définit des opérations sur ces tableaux. Elles sont regroupables en 5 grandes catégories :

Si vous avez déjà pratiqué le SQL, alors je vous conseille de lire en parallèle de ce chapitre cette page de la documentation officielle, qui donne la "traduction" du SQL vers pandas ;) : https://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html

Les structures de données de Pandas

La librairie Pandas fournit deux structures de données fondamentales, la "Series" et le "DataFrame". On peut voir ces structures comme une généralisation des tableaux et des matrices de Numpy. La différence fondamentale entre ces structures et les versions de Numpy est que les objets Pandas possèdent des indices explicites. Là où on ne pouvait se référer à un élément d'un tableau Numpy que par sa position dans le tableau, chaque élément d'une Series ou d'un DataFrame peut avoir un indice explicitement désigné par l'utilisateur.

Commençons par un rappel pour voir comment créer ces structures et nous en servir pour quelques opérations de base.

import numpy as np
import pandas as pd

# On peut créer une Series à partir d'une list
data = pd.Series([0.25, 0.5, 0.75, 1.0])
print("data ressemble à un tableau Numpy: ", data)

# On peut spécifier des indices à la main
data = pd.Series([0.25, 0.5, 0.75, 1.0],
         index=['a', 'b', 'c', 'd'])
print("data ressemble à un dict en Python: ", data)
print(data['b'])

# On peut même créer une Serie directement à partir d'une dict
population_dict = {'California': 38332521,
                   'Texas': 26448193,
                   'New York': 19651127,
                   'Florida': 19552860,
                   'Illinois': 12882135}
area_dict = {'California': 423967, 
             'Texas': 695662,
             'New York': 141297, 
             'Florida': 170312,
             'Illinois': 149995}
population = pd.Series(population_dict)
area = pd.Series(area_dict)
print(population)
# Que pensez vous de cette ligne?
print(population['California':'Florida'])

Les DataFrame permettent de combiner plusieurs Series en colonnes, un peu comme dans un tableau SQL. Construire un DataFrame est chose aisée :

# A partir d'une Series
df = pd.DataFrame(population, columns=['population'])
print(df)

# A partir d'une list de dict
data = [{'a': i, 'b': 2 * i}
        for i in range(3)]
df = pd.DataFrame(data)
print(df)

# A partir de plusieurs Series
df = pd.DataFrame({'population': population,
              'area': area})
print(df)

# A partir d'un tableau Numpy de dimension 2
df = pd.DataFrame(np.random.rand(3, 2),
             columns=['foo', 'bar'],
             index=['a', 'b', 'c'])
print(df)

# Une fonction pour générer facilement des DataFrame. 
# Elle nous sera utile dans la suite de ce chapitre.
def make_df(cols, ind):
    """Crée rapidement des DataFrame"""
    data = {c: [str(c) + str(i) for i in ind]
            for c in cols}
    return pd.DataFrame(data, ind)

# exemple
make_df('ABC', range(3))

La projection et la restriction

Commençons par 2 opérations de l'algèbre relationnelle : la projection et la restriction. Elles sont très simples : la première est une sélection de certaines colonnes, et la seconde une sélection de certaines lignes.

On peut référer aux éléments des objets Pandas en utilisant soit leurs index implicites (de la même façon que les tableaux Numpy) soit les index explicites (comme dans les dict). Pour éviter toute confusion, il est conseillé d'utiliser les attributs  loc  (qui référence par l'index) et  iloc  (qui référence par la position) de chaque objet.

data = pd.Series([0.25, 0.5, 0.75, 1.0],
                 index=['a', 'b', 'c', 'd'])

print(data)

# On peut désigner un élément d'une Series par son index
print(data.loc['b'])

# Ou bien par sa position
print(data.iloc[1])

La différence entre les deux devrait être claire après avoir exécuté ces lignes. Effectuer ces mêmes opérations sur les Dataframe se fait de manière analogue :

data = pd.DataFrame({'area':area, 'pop':population})
print(data)

data.loc[:'Illinois', :'pop']

L'union

L'union grâce à pd.concat

L'une des opérations les plus simples en algèbre relationnelle est l'union de données. Unir deux tableaux, c'est simplement créer un troisième tableau qui contient toutes les lignes du premier et toutes les lignes du second.

Dans notre cas, nous allons nous intéresser à l'union de Series ou de DataFrame. Cette opération consiste en l'assemblage de plusieurs structures pour en créer une nouvelle. Avec Pandas, cette opération s'accomplit grâce à la fonction  pd.concat.

ser1 = pd.Series(['A', 'B', 'C'], index=[1, 2, 3])
ser2 = pd.Series(['D', 'E', 'F'], index=[4, 5, 6])
pd.concat([ser1, ser2])
1     A
2     B
3     C
4     D
5     E
6     F
dtype: object

Pour une Series, cela paraît facile. Mais pour un DataFrame ?

df1 = make_df('AB', [1, 2])
df2 = make_df('AB', [3, 4])
pd.concat([df1, df2])
    A   B
1  A1  B1
2  A2  B2
3  A3  B3
4  A4  B4

Étudiez bien ce morceau de code. Fait-il ce que vous en attendiez ?

Par défaut,  pd.concat  assemble ses arguments "verticalement". Pour changer ce comportement, on peut utiliser l'argument  axis.

Problèmes des index avec l'union

C'est souvent un problème. Pour y pallier, on peut utiliser les index hiérarchiques.

x = make_df('AB', [0, 1])
y = make_df('AB', [2, 3])
y.index = x.index  # Rend les index identiques
# Nous avons alors des index dupliqués
print(pd.concat([x, y]))

# Nous pouvons spécifier des index hiérarchiques
hdf = pd.concat([x, y], keys=['x', 'y'])
print(hdf)
    A   B
0  A0  B0
1  A1  B1
0  A2  B2
1  A3  B3
      A   B
x 0  A0  B0
  1  A1  B1
y 0  A2  B2
  1  A3  B3

Comment utiliser les index hiérarchiques ?

Pour accéder à un élément d'un objet Pandas avec un index hiérarchique, il suffit de spécifier plusieurs index. Dans notre exemple, essayez par exemple de voir le résultat de   hdf.loc[('x', 1),]  .

La jointure

Une autre fonction très utile pour manipuler les Dataframe est  pd.merge  . Elle effectue une jointure.

Une jointure, c'est assembler les informations d'un tableau A avec celles d'un autre tableau B selon un  critère choisi. Ce critère s'appelle la condition de jointure. Cette condition est composée de une ou plusieurs colonnes communes à A et B qui effectuent une correspondance entre les 2 tableaux.

Un petit exemple. Imaginons que nous disposons de deux Dataframes :

  • df1  contenant une liste d'employés et le nom des départements dans lequel ils travaillent,

  • df2  contenant la même liste d'employés et leurs dates d'entrée dans l'entreprise.

La fonction  pd.merge  nous permet de transformer ces deux Dataframes en un seul, contenant les deux informations.

Jointure entre deux dataframes selon la colonne
Jointure entre deux dataframes selon la colonne "employee"
df1 = pd.DataFrame({'employee': ['Bob', 'Jake', 'Lisa', 'Sue'],
                    'department': ['Accounting', 'Engineering', 'Engineering', 'HR']})
df2 = pd.DataFrame({'employee': ['Lisa', 'Bob', 'Jake', 'Sue'],
                    'date': [2004, 2008, 2012, 2014]})
df3 = pd.merge(df1, df2)

La cardinalité

En effectuant une jointure, il est préférable de toujours faire attention à la cardinalité de la relation entre A et B afin d'évider certaines erreurs. Un exemple d'une telle erreur est donnée dans la section "Pourquoi les clés sont-elles si importantes ?" de ce chapitre.

La fonction  pd.merge  ne fait pas la différence entre ces 3 cardinalités : elle s'utilise exactement de la même manière dans les 3 cas.

Cardinalité un-à-un

L'exemple que nous avons pris tout à l'heure correspond à une cardinalité un-à-un. En effet, un employé ne travaille que dans un seul département, et n'a qu'une seule année d'embauche.

df1 = pd.DataFrame({'employee': ['Bob', 'Jake', 'Lisa', 'Sue'],
                    'department': ['Accounting', 'Engineering', 'Engineering', 'HR']})
df2 = pd.DataFrame({'employee': ['Lisa', 'Bob', 'Jake', 'Sue'],
                    'date': [2004, 2008, 2012, 2014]})

df3 = pd.merge(df1, df2)
df3
Cardinalité un-à-plusieurs (ou plusieurs-à-un)

Maintenant nous voulons ajouter une autre colonne. Chaque département a un chef. Cette information est contenue dans un Dataframe. Nous voulons ajouter une colonne à  df3  pour y ajouter le chef de chaque employé.

Ici, c'est une cardinalité un-à-plusieurs. En effet, df3  représente des employés, et  df4  représente des chefs. Un employé n'a qu'un seul chef, mais un chef peut diriger plusieurs empoyés.

df4 = pd.DataFrame({'department': ['Accounting', 'Engineering', 'HR'],
                    'supervisor': ['Carly', 'Guido', 'Steve']})
pd.merge(df3, df4)

Remarquez que Guido apparaît plusieurs fois dans le résultat.

Cardinalité plusieurs-à-plusieurs

Pour continuer avec notre exemple, supposons que nous disposions d'un autre Dataframe contenant les compétences nécessaires pour travailler dans chaque département :

df5 = pd.DataFrame({'department': ['Accounting', 'Accounting',
                              'Engineering', 'Engineering', 'HR', 'HR'],
                    'competence': ['math', 'spreadsheets', 'coding', 'linux',
                               'spreadsheets', 'organization']})

Maintenant, nous souhaitons associer à chaque employé les compétences qu'il doit posséder pour travailler dans son département. Ici, c'est une cardinalité plusieurs-à-plusieurs car un employé a besoin de plusieurs compétences, et une compétence peut être partagée par plusieurs employés.

pd.merge(df1, df5)

La jointure externe

Reprenons df1  et ajoutons-y Lea, employée dans le département Engineering. Appelons ce nouveau dataframe  df6  . Cependant, nous n'ajoutons pas Lea à  df2  :

df6 = pd.DataFrame({'employee': ['Bob', 'Jake', 'Lisa', 'Sue', 'Lea'],
                    'department': ['Accounting', 'Engineering', 'Engineering', 'HR', 'Engineering']})
df2 = pd.DataFrame({'employee': ['Lisa', 'Bob', 'Jake', 'Sue'],
                    'date': [2004, 2008, 2012, 2014]})
Ajoutons Lea !
Ajoutons Lea !

Si on écrit cette jointure...

pd.merge(df6, df2)

... alors on obtient ceci :

pd.merge(df6, df2)
pd.merge(df6, df2)

Effectivement. Comme Lea est présente dans df6 mais pas dans df2, elle n’apparaît pas dans le résultat de la jointure... Pour spécifier que l'on veut garder tous les éléments de dataframe de gauche (ici df6), il faut alors écrire la ligne suivante :

pd.merge(df6, df2, how="left")

Ainsi, Lea est présente dans la table finale ! Bien entendu, pour garder toutes les lignes de la table de droite (df2), on écrit  how="right"  , et pour garder toutes les lignes à la fois à gauche et à droite, on écrit  how="outer"  . On appelle ces jointures respectivement : jointure externe à gauche, jointure externe à droite et jointure externe totale. Si ce n'est pas très clair, faites un petit tour dans la section "Jointure externe" de ce chapitre.

Le produit cartésien

Nous pouvons utiliser la jointure pour réaliser une autre opération d'algèbre relationnelle, le produit cartésien.

# Nous ajoutons une nouvelle colonne à df1 et df2, qui contient toujours
# la même valeur, ici 0.
df1['key'] = 0
df2['key'] = 0

# La jointure plusieurs-à-plusieurs
produit_cartesien = pd.merge(df1, df2, on='key')

# Effaçons la colonne key qui n'est plus utile
produit_cartesien.drop('key',1, inplace=True)

Avec ce que vous avez vu sur la jointure plusieurs-à-plusieurs, comprenez-vous ce qui se passe ici ?

Le produit cartésien est composé de toutes les associations possibles entre les lignes de df1 et celles de df2. Ici, avec nos données, le produit cartésien n'a cependant pas beaucoup de sens...

L'agrégation

Passons maintenant à l'agrégation !

Comme les tableaux Numpy, nous pouvons facilement effectuer des opérations sur l'ensemble des éléments d'une Series ou un Dataframe, comme par exemple  sum  ou  mean  :

rng = np.random.RandomState(42)

# Une Series avec cinq nombres aléatoires
ser = pd.Series(rng.rand(5))
print(ser.sum())
print(ser.mean())

Pour un Dataframe, ces calculs sont aussi possibles, et ils sont réalisés par colonne :

df = pd.DataFrame({'A': rng.rand(5),
                   'B': rng.rand(5)})
                   
# Par colonne
print(df.mean())

# Par ligne
print(df.mean(axis='columns'))

Pandas nous permet d'accomplir une agrégation par groupe, semblable à ce qu'on peut obtenir en utilisant le mot clé  GROUP BY en SQL.

df = pd.DataFrame({'key': ['A', 'B', 'C', 'A', 'B', 'C'],
                   'data1': range(6),
                   'data2': [10,11,10,9,10,10]})                  
print(df)

Dans Pandas, cette opération se fait en deux étapes. Nous allons d'abord créer un objet de type  DataFrameGroupBy  , que nous appelons  gb  :

gb = df.groupby('key')

Sur cet objet gb, on peut ensuite appliquer les fonctions d'agrégation :

print(gb.sum())
print(gb.mean())

Si on veut par exemple calculer la somme des colonnes  data1  et  data2, et calculer la moyenne de  data2  uniquement, sachez qu'il est possible de sélectionner les colonnes nécessaires sur notre objet  gb  :

s = gb['data1','data2'].sum()
m = gb['data2',].mean()

groupped = pd.concat([s,m], axis=1)
groupped.columns = ["data1_somme","data2_somme","data2_moyenne"]

Résultat d'une agrégation
Résultat d'une agrégation

Et voilà !

Exemple de certificat de réussite
Exemple de certificat de réussite