Files
training.python.datascience/documentation/01.5-pandas-dataframe.md
Steve Kossouho d06fd5414d Rename documentation files
Removed new- prefix.
Added old- prefix to old files.
2025-07-12 17:03:38 +02:00

53 KiB
Raw Permalink Blame History

title, author
title author
Pandas Steve Kossouho

Manipuler et comprendre les dataframes


Propriétés d'un dataframe

Un objet DataFrame{.python} dans Pandas est un objet représentant un tableau de valeurs. On peut l'imaginer comme une table de base de données, où chaque colonne du document est une Series{.python}, avec son propre type. Les objets DataFrame{.python} possèdent deux index; un pour étiqueter les éléments des colonnes de contenu, et un autre pour étiqueter... les colonnes elles-mêmes.

Exemple de dataframe


Notions et nomenclature

Lorsque vous manipulez un DataFrame{.python}, il faut avoir conscience de certaines notions :

  1. Un DataFrame{.python} possède deux index, un pour les lignes (vertical), et un pour les colonnes (horizontal).
  2. L'index des lignes est un attribut nommé index{.python}.
  3. L'index des colonnes est un attribut nommé columns{.python}.
  4. Le contenu d'un DataFrame{.python} peut être modifié directement.

Quand obtient-on un dataframe ?

Un objet de type DataFrame{.python} s'obtient naturellement en le créant manuellement. Les autres cas de figure où l'on récupère un DataFrame{.python} sont par exemple, les cas où :

  1. On extrait une ou plusieurs lignes d'un DataFrame{.python};
  2. On extrait une ou plusieurs colonnes d'un DataFrame{.python};
  3. On utilise une méthode de transformation sans agrégation sur un DataFrame{.python};
  4. On lit un DataFrame{.python} depuis un document CSV, XLSX, etc…

Types de données dans Pandas (rappel)

Le stockage de données en mémoire avec Pandas proposera de manipuler des entrées, dans des types spécifiques à Pandas :

  • object : données de types divers, dont chaînes de caractères
  • int64/32/16 : nombres entiers (64 bits maximum)
  • Int64/32/16 : nombres entiers (avec prise en charge des nan)
  • float64 : nombres à virgule flottante (64 bits)
  • complex128 : même les nombres complexes (64 et 64 bits)
  • bool : valeurs booléennes
  • boolean : valeurs booléennes (avec prise en charge des nan)
  • datetime64[ns] : représentation d'une date
  • datetime64[ns, UTC] : représentation d'une date, avec décalage horaire (UTC)
  • timedelta64 : représentation d'un intervalle de temps
  • category: choix de valeurs textuelles

La bibliothèque pyarrow propose d'autres types, utilisables par Pandas, pour stocker des données. Entre autres, vous pouvez trouver les types suivant :

  • int8/16/32/64[pyarrow] : nombres entiers (avec prise en charge des nan)
  • uint8/16/32/64[pyarrow] : nombres entiers sans signe (avec prise en charge des nan)
  • float16/32/64[pyarrow] : nombres à virgule flottante (avec prise en charge des nan)
  • timestamp[ns] : date et heure

Type de données d'un DataFrame

Un objet DataFrame{.python} possède plusieurs colonnes que l'on peut considérer comme des Series{.python}. Chaque colonne possède donc son propre type, et il est toujours possible de connaître le type des colonnes d'un dataframe en accédant à son attribut dtypes (data type); l'attribut renvoie une série de types Pandas, étiquetés avec l'index d'une colonne :

import pandas as pd

data = pd.DataFrame(data={"product": ["Pen", "Ink", "Paper", "Ruler"], "price": [.99, 20.49, 5.99, 1.99]})
print(data.dtypes)  # affiche une série ["object", "float64"] avec l'index ["product", "price"]

Créer un DataFrame

Nous avons vu que nous pouvons créer simplement des séries, l'équivalence est vraie pour les dataframes. Nous avons à disposition plusieurs moyens de renseigner leurs données :

  • Des tableaux Numpy à deux dimensions
  • Des séquences de séquences Python (int{.python}, float{.python}, str{.python}, datetime.datetime{.python}…)
  • Des dictionnaires Python (dont les valeurs peuvent être des Series)
  • Des listes de dictionnaires Python
  • Des Series{.python} peuvent servir de source et être concaténées horizontalement

Créer un DataFrame avec des listes

Créer un dataframe nécessite au minimum de passer deux dimensions de données. Ainsi, passer une liste de listes est une option raisonnable; chaque liste contenue dans la liste principale représentera une ligne du document (une valeur par colonne).

import pandas as pd

# Créer un DataFrame via une liste de listes
data = pd.DataFrame(data=[
    ["Disquette 3,5\" 1,39Mo Imation", 2.95, 1996],
    ["Magnétoscope LP Hitachi", 99.95, 1994],
    ["Lecteur Disque Zip Iomega", 169.95, 1994],
])

Les lignes et les colonnes du document posséderont un index par défaut (des index numériques débutant à 0).


Créer un DataFrame avec un dictionnaire (recommandé)

Les dictionnaires ont pour avantage d'être définis avec des clés, qui seront utilisées par Pandas comme valeurs d'index de colonnes.

import pandas as pd

# Créer un DataFrame avec un dictionnaire
data = pd.DataFrame(data={
    "product": ["Disquette 3,5\" 1,39Mo Imation", "Magnétoscope LP Hitachi", "Lecteur Disque Zip Iomega"],
    "price": [2.95, 99.95, 169.95],
    "year": [1996, 1994, 1994]
})

Créer un DataFrame avec une liste de dictionnaires

Passer une liste de dictionnaires, un dictionnaire par ligne, a pour intérêt principal de permettre de renseigner uniquement les colonnes qui nous intéressent.

import pandas as pd

# Créer un DataFrame avec une liste de dictionnaires
data = pd.DataFrame(data=[
    {"product": "Disquette 3,5\" 1,39Mo Imation", "price": 2.95},
    {"product": "Magnétoscope LP Hitachi", "year": 1994},
    {"product": "Lecteur Disque Zip Iomega", "price": 169.95, "year": 1994}
])

Créer un DataFrame avec des Series

Si vous souhaitez créer un DataFrame{.python} avec des objets de type Series{.python}, la méthode la plus accessible consiste à les concaténer... dans le sens des colonnes.

import pandas as pd

products = pd.Series(data=["Disquette 3,5\" 1,39Mo Imation", "Magnétoscope LP Hitachi", "Lecteur Disque Zip Iomega"], name="product")
prices = pd.Series(data=[2.95, 99.95, 169.95], name="price")
years = pd.Series(data=[1996, 1994, 1994], name="year")

data = pd.concat([products, prices, years], axis="columns")

Les noms donnés aux séries (via l'argument name={.python})seront utilisés comme identifiants dans l'index des colonnes du DataFrame{.python} résultant.


Spécifier les index d'un DataFrame

Si le jeu de données que vous passez à la création manuelle d'un DataFrame{.python} ne suffit pas à préciser tout ce dont vous avez besoin, notamment au niveau des index, vous pouvez préciser spécifiquement les valeurs desdits index.

import pandas as pd

# Créer un DataFrame via une liste de listes
data = pd.DataFrame(
    data=[
        ["Disquette 3,5\" 1,39Mo Imation", 2.95, 1996],
        ["Magnétoscope LP Hitachi", 99.95, 1994],
        ["Lecteur Disque Zip Iomega", 169.95, 1994],
    ],
    index=["floppy", "vhs", "zip"],  # index des lignes
    columns=["product", "price", "year"]  # index des colonnes
)

Extraire des informations d'un DataFrame

Si nous avons vu précédemment comment extraire des informations d'une série, et que certains éléments de syntaxe doivent pouvoir s'appliquer de façon similaire aux DataFrame{.python}, nous devons aborder les cas utiles en détail.


Taille d'un DataFrame

Un objet DataFrame{.python} est un objet en deux dimensions. Il possède donc une largeur (nombre de colonnes) et une hauteur (nombre de lignes).

import pandas as pd

# Créer un DataFrame via une liste de listes
data = pd.DataFrame(...)
# Affiche un tuple (lignes, colonnes)
print(data.shape)
# Affiche le nombre de cellules du document
print(data.size)

L'attribut .shape{.python} permet de récupérer un tuple représentant dans l'ordre la hauteur puis le nombre de colonnes du contenu. L'attribut .size{.python}, lui, renvoie le nombre de cellules au total dans le document (largeur * hauteur)


Index des lignes et des colonnes

Récupérer les index des lignes et des colonnes, c'est très simple; il suffit de récupérer des attributs du DataFrame{.python} nommés index{.python} et columns{.python} :

import pandas as pd

# Créer un DataFrame avec un dictionnaire
data = pd.DataFrame(data={
    "product": ["Disquette", "Magnétoscope", "Lecteur Zip"],
    "price": [2.95, 99.95, 169.95],
    "year": [1996, 1994, 1994]
})
# Afficher l'index de colonnes, puis des lignes
# Un index est une séquence et vous pouvez récupérer des valeurs à des positions numériques
print(data.columns)
print(data.index)

Cellule à une position précise

Récupérer une cellule unique est possible grâce à l'attribut at[]{.python} des objets de type DataFrame{.python}. Ce n'est pas une méthode : il s'utilise comme les attributs .loc[]{.python} et .iloc[]{.python}, en passant comme clé un tuple contenant l'index de ligne, puis celui de colonne :

import pandas as pd

# colonnes : "product", "price" et "year"
# index de lignes : 0, 1 et 2
data = pd.DataFrame(data={
    "product": ["Disquette", "Magnétoscope", "Lecteur Zip", "CD"],
    "price": [2.95, 99.95, 169.95, 4.29],
    "year": [1996, 1994, 1994, 1997]
})
# Récupérer la cellule à la ligne 0, colonne "product"
print(data.at[(0, "product")])

Une ligne de données

Quand on pense à une feuille de calcul dans un tableur Excel, on a tout intérêt à pouvoir, pour nos calculs, extraire des lignes ou des colonnes. Commençons par les lignes :


Ligne de données identifiée par un index

Extraire une ligne d'un DataFrame{.python} peut se faire depuis les valeurs d'index associées aux lignes du document. Vous récupérez un objet Series{.python} dont les valeurs d'index correspondent aux colonnes :

import pandas as pd

# Créer un DataFrame via une liste de listes
data = pd.DataFrame(
    data=[
        ["Disquette 3,5\" 1,39Mo Imation", 2.95, 1996],
        ["Magnétoscope LP Hitachi", 99.95, 1994],
        ["Lecteur Disque Zip Iomega", 169.95, 1994],
    ],
    index=["floppy", "vhs", "zip"],  # index des lignes
    columns=["product", "price", "year"]  # index des colonnes
)
# Extraire une série, via l'attribut loc
row = data.loc["vhs"]
print(row, type(row))

Note : Les valeurs par défaut d'index pour un DataFrame{.python} ou une Series{.python} sont toujours des nombres séquentiels démarrant à 0{.python}.


Ligne de données identifiée par une position numérique

Indépendamment des index des lignes, vous pouvez récupérer des lignes d'un DataFrame{.python} par leur position (indexée à 0{.python}) avec l'attribut .iloc{.python}. Vous récupérez également un objet Series{.python} dont les valeurs d'index correspondent aux colonnes :

import pandas as pd

# Créer un DataFrame via une liste de listes
data = pd.DataFrame(
    data=[
        ["Disquette 3,5\" 1,39Mo Imation", 2.95, 1996],
        ["Magnétoscope LP Hitachi", 99.95, 1994],
        ["Lecteur Disque Zip Iomega", 169.95, 1994],
    ],
    index=["floppy", "vhs", "zip"],  # index des lignes
    columns=["product", "price", "year"]  # index des colonnes
)
# Extraire la troisième ligne, via l'attribut iloc
row = data.iloc[2]
print(row, type(row))

Colonne de données

L'API proposée par Pandas sur les DataFrame{.python} est très simple lorsqu'il faut extraire une colonne; il suffit globalement de considérer le DataFrame{.python} comme un simple dictionnaire. La clé à utiliser est la valeur d'index de la colonne à extraire :

import pandas as pd

data = pd.DataFrame(
    data={
        "person": ["Marie Martin", "Pierre Plâtrier", "Jules Joffrin"],
        "city": ["Marseille", "Paris", "Juvisy"],
        "earn": [3000.0, 2800.0, 2600.0]
    }
)

# Extraire la colonne "person" vers une série
people = data["person"]
print(people)

Plusieurs lignes de données

Nous pouvons extraire d'un DataFrame{.python} plusieurs lignes de données de notre choix en se servant des fonctionnalités des attributs .loc{.python} et .iloc{.python}. La différence avec les cas précédents est le type de la clé utilisée pour extraire du contenu :

import pandas as pd

data = pd.DataFrame(
    data={
        "person": ["Marie Martin", "Pierre Plâtrier", "Jules Joffrin"],
        "city": ["Marseille", "Paris", "Juvisy"],
        "earn": [3000.0, 2800.0, 2600.0]
    },
    index=["M", "P", "J"]
)

# Extraire les première et troisième lignes
rows1 = data.loc[["M", "J"]]  # la clé est une LISTE de valeurs d'index
rows2 = data.iloc[[0, 2]]  # la clé est une LISTE de numéros de ligne
print(rows1, rows2, sep="\n")

Pour extraire plusieurs lignes, vous devez utiliser un objet de type list{.python} et uniquement list{.python}. Le résultat obtenu est un objet de type DataFrame{.python}.


Plusieurs colonnes de données

Avec exactement le même état d'esprit que pour extraire plusieurs lignes, si vous utilisez le DataFrame{.python} comme un dictionnaire et utilisez une clé qui est un objet de type list{.python}, vous pouvez extraire plusieurs colonnes à la fois.

import pandas as pd

data = pd.DataFrame(
    data={
        "person": ["Marie Martin", "Pierre Plâtrier", "Jules Joffrin"],
        "city": ["Marseille", "Paris", "Juvisy"],
        "earn": [3000.0, 2800.0, 2600.0]
    },
    index=["M", "P", "J"]
)

# Extraire les première et troisième colonnes
# La clé à utiliser est une LISTE de valeurs d'index de colonne
cols1 = data[["person", "earn"]]
# Je peux extraire les noms de colonne numériquement via .columns
cols2 = data[data.columns[[0, 2]]]
print(cols1, cols2, sep="\n")

Lignes et colonnes

Pandas est une bibliothèque ingénieuse; elle vous autorise, avec peu de changements, à extraire d'un DataFrame{.python} des lignes, mais aussi des colonnes en même temps.

Pour cela vous pouvez utiliser les attributs .loc[]{.python} et .iloc[]{.python}, et utiliser comme clé un objet de type tuple{.python} à deux éléments (et pas une liste) :

import pandas as pd

data = pd.DataFrame(
    data={
        "person": ["Marie Martin", "Pierre Plâtrier", "Jules Joffrin"],
        "city": ["Marseille", "Paris", "Juvisy"],
        "earn": [3000.0, 2800.0, 2600.0]
    },
    index=["M", "P", "J"]
)

# Sélectionner par valeurs d'index
selection1 = data.loc[("M", "person")]
# Sélectionner par positions numériques
selection2 = data.iloc[(0, 0)]
print(selection1, selection2, sep="\n")

Bon à savoir : vues et copies

Lorsque vous extrayez une partie d'un DataFrame{.python}, vous obtenez généralement un objet de type DataFrame{.python}. Cependant, il faut savoir que par défaut, le nouveau DataFrame{.python} est une vue sur le DataFrame{.python} initial. Cela veut dire que si vous modifiez le contenu du DataFrame{.python} initial, le contenu du DataFrame{.python} extrait sera aussi modifié, et vice versa.

Pour obtenir une copie des données du DataFrame{.python} extrait, il faut utiliser la méthode .copy(){.python}.

import pandas as pd

data = pd.DataFrame(
    data={
        "fruit": ["Banane", "Orange", "Mangue"],
        "price": [0.95, 0.85, 1.05],
        "ean": ["1234567890123", "4567890123456", "7890123456789"]
    },
    index=["B", "O", "M"])
# Extraire une partie du DataFrame et obtenir une vue
selection1 = data.loc[("M", "fruit")]
# Extraire une partie du DataFrame et obtenir une copie
selection2 = data.loc[("M", "fruit")].copy()

Lignes avec des conditions

Dans cette section, l'objectif est de pouvoir dire à Pandas : « Filtre-moi les lignes de ce DataFrame{.python} en gardant celles où une condition est remplie ». Par exemple :

import pandas as pd

data = pd.DataFrame(data={
    "product": ["Eau", "Air", "Feu", "Terre"],
    "price": [10, 20, 30, 40]
})

Imaginons que je souhaite filtrer ce DataFrame{.python} pour ne garder que les lignes où la colonne price possède une valeur supérieure à 20. Pour pouvoir bien le faire, il faut comprendre le raisonnement de Pandas.


Appliquer une condition à une colonne

Pandas permet d'appliquer une condition à une Series{.python} et d'obtenir une suite de bool{.python}. Par exemple, je souhaite savoir quelles valeurs de la série sont supérieures à 20 :

import pandas as pd

data = pd.Series(data=[1, 23, 24, 5, 17, 19, 46])
filtered = (data > 20)  # cette comparaison génère une suite de booléens

La variable filtered{.python} contiendra une série avec les valeurs [False, True, True, False, False, False, True]{.python}, et des valeurs d'index récupérées de la série originale. Tous les opérateurs de comparaison de Python peuvent être utilisés sur une Series{.python} et renvoient une série de booléens.


Utiliser une série de booléens pour filtrer un DataFrame

Les séries de booléens, qu'elles soient obtenues via une série ou manuellement, servent à filtrer les lignes d'un DataFrame{.python} :

import pandas as pd

data = pd.DataFrame(data={
    "product": ["Eau", "Air", "Feu", "Terre"],
    "price": [10, 20, 30, 40]
})
# L'expression est une série de booléens de la même taille
# que le DataFrame. Pour chaque valeur True, on garde la ligne
# correspondante.
cheapest = data.loc[data["price"] < 25]  

Cette expression se lit aussi naturellement « Récupérer les lignes de data où la colonne price de data est inférieure à 25 ».


Séries de booléens : opérateurs

Les opérateurs de comparaison fonctionnent pour récupérer des séries de booléens :

import pandas as pd

data = pd.Series(data=[1, 2, 3, 4, 5, 6])
is_even = (data % 2) == 0  # F, T, F, T, F, T
is_high = data > 3  # F, F, F, T, T, T
# Filtrer les données avec les séries de booléens
high_data = data.loc[is_high]  # 4, 5, 6
even_data = data.loc[is_even]  # 2, 4, 6

Séries de booléens : opérateurs booléens

Vous pouvez organiser logiquement vos filtres avec les opérateurs OU et ET; par exemple, garder les lignes où le nombre est pair, et où le même nombre est supérieur à 3 :

import pandas as pd

data = pd.Series(data=[1, 2, 3, 4, 5, 6])
is_even = (data % 2) == 0  # F, T, F, T, F, T
is_high = data > 3  # F, F, F, T, T, T
high_data = data.loc[is_high & is_even]  # 4, 6

On peut utiliser les opérateurs binaires &{.python} (and) et |{.python} (or) pour combiner les séries de booléens. Le résultat est une série de booléens de la même longueur.


Séries de booléens : chaînes

Si on voulait filtrer des lignes de Series{.python} ou de DataFrame{.python} sur un critère de chaîne, on peut imaginer utiliser l'opérateur =={.python} :

import pandas as pd

data = pd.Series(data=["matt", "jenn", "igor", "alex", "mila"])
# Ici on garde True si la valeur est exactement "mila"
mila_data = data.loc[data == "mila"]

C'est bien, mais cela ne suffit pas si on recherche, par exemple, les lignes dans lesquelles le texte contient une chaîne, voire répond à une expression régulière. Il existe pour la série un attribut str qui permet d'appliquer diverses méthodes de chaînes de caractères, dont une méthode contains(){.python} :

import pandas as pd

data = pd.Series(data=["matt", "jenn", "igor", "alex", "mila"])
# Ici on garde True si la valeur contient un "i" minuscule
filter_data1 = data.loc[data.str.contains("i")]
# Ici on garde True si la valeur contient un "i" (ignore la casse)
filter_data2_a = data.loc[data.str.contains("(?i)i")]
filter_data2_b = data.loc[data.str.contains("i", case=False)]
# Si vous connaissez les regex, le reste marche aussi :
# Ici reconnaître les chaînes où un caractère apparaît 2 fois de suite
filter_data3 = data.loc[data.str.contains(r"(\w)\1")]

Apprendre les Regex avec RegexLearn


Séries de booléens : infos de dates

Certaines séries peuvent contenir des valeurs de type datetime64[ns]{.python}, et on peut filtrer là-dessus; par exemple, on peut imaginer filtrer un dataframe sur les lignes où une colonne concerne un mois de juin :

import pandas as pd

data = pd.DataFrame(data={
    "order": ["A15", "B84", "D25", "B43"],
    "date": pd.to_datetime(["2024-06-10", "2021-02-14", "2023-06-25", "2024-11-30"])
})
june_dates = (data["date"].dt.month == 6)
june_data = data.loc[june_dates]

Dans cet exemple, on utilise l'attribut dt{.python} d'une série pour avoir accès à des attributs et des méthodes d'extraction d'informations sur une valeur date. L'attribut est documenté ici :

Attributs de dt


Méthode/Attribut Explication succincte
dt.date Renvoie la date (sans l'heure) pour chaque élément
dt.time Renvoie l'heure (sans la date) pour chaque élément
dt.year Renvoie l'année de chaque élément
dt.month Renvoie le mois de chaque élément (1-12)
dt.day Renvoie le jour du mois de chaque élément (1-31)
dt.hour Renvoie l'heure de chaque élément
dt.minute Renvoie les minutes de chaque élément
dt.second Renvoie les secondes de chaque élément
dt.microsecond Renvoie les microsecondes de chaque élément
dt.nanosecond Renvoie les nanosecondes de chaque élément

Méthode/Attribut Explication succincte
dt.weekday Renvoie le jour de la semaine pour chaque élément (0=Monday, 6=Sunday)
dt.dayofweek Alias de weekday, renvoie le jour de la semaine
dt.dayofyear Renvoie le jour de l'année pour chaque élément (1-365/366)
dt.quarter Renvoie le trimestre de l'année pour chaque élément
dt.is_month_start Renvoie un booléen indiquant si la date est le début du mois
dt.is_month_end Renvoie un booléen indiquant si la date est la fin du mois
dt.is_quarter_start Renvoie un booléen indiquant si la date est le début du trimestre
dt.is_quarter_end Renvoie un booléen indiquant si la date est la fin du trimestre
dt.is_year_start Renvoie un booléen indiquant si la date est le début de l'année

Méthode/Attribut Explication succincte
dt.is_year_end Renvoie un booléen indiquant si la date est la fin de l'année
dt.is_leap_year Renvoie un booléen indiquant si l'année est bissextile
dt.days_in_month Renvoie le nombre de jours dans le mois pour chaque élément
dt.tz Renvoie le fuseau horaire de chaque élément
dt.freq Renvoie la fréquence des éléments si elle est définie
dt.strftime Formate les dates selon une chaîne de format spécifiée
dt.to_period Convertit en période périodique (e.g., année, trimestre)
dt.to_pydatetime Convertit en objets datetime de Python natif
dt.total_seconds Renvoie la durée totale en secondes pour les objets timedelta
dt.normalize Normalise les dates à minuit (enlevant les composants d'heure)

Méthode/Attribut Explication succincte
dt.tz_localize Localise les dates selon un fuseau horaire spécifié
dt.tz_convert Convertit les dates à un autre fuseau horaire
dt.round Arrondit les dates à une fréquence spécifiée
dt.floor Abaisse les dates à la fréquence spécifiée
dt.ceil Elève les dates à la fréquence spécifiée
dt.month_name Renvoie le nom du mois pour chaque élément
dt.day_name Renvoie le nom du jour de la semaine pour chaque élément
dt.isocalendar Renvoie les composants ISO de la date (année, semaine, jour de la semaine)

Les méthodes de cet attribut sont documentées individuellement (voir les liens dans l'index de la page). Vous pouvez obtenir l'autocomplétion de cet attribut uniquement en installant le paquet suivant:

pip install pandas-stubs

Exemple d'utilisation de dt{.python}

import pandas as pd

# Les dates contiennent des minutes et j'aimerais arrondir
# à l'heure la plus proche pour dédupliquer des informations
data = pd.DataFrame(data={
    "order": ["A15", "B84", "D25", "B43"],
    "date": pd.to_datetime(["2024-06-10T04:31", "2021-02-14T13:55", "2023-06-25T06:14", "2024-11-30T00:13"])
})
data["hour"] = data.dt.round("H")
print(data)

Dédupliquer des lignes

Autre type d'extraction : imaginons que vous lisiez des données depuis un fichier Excel dans un DataFrame{.python}, et vous souhaitez en dédupliquer des lignes. Vous considérez par exemple que deux lignes sont un doublon si elles ont la même valeur sur la troisième colonne. C'est possible de le faire grâce à une méthode drop_duplicates{.python}.


Considérons le tableau suivant :

Prénom Nom Âge
Alice Leroy 25
Bob Dupont 30
Alice Leroy 27
David Martin 40
Eva Joly 35
Bob Dupont 26

Dans ce tableau, Bob Dupont et Alice Leroy apparaissent en doublon, même si la colonne Âge n'a pas la même valeur à chaque ligne en doublon.

Nous pouvons dédupliquer les lignes du document avec l'extrait suivant :

import pandas as pd

data = pd.read_csv("names.csv")
# Dédupliquer en considérant uniquement les colonnes Nom et Prénom 
deduplicated = data.drop_duplicates(subset=["Prénom", "Nom"])

Dans ce cas de figure, les lignes considérées comme dupliquées seront… dédupliquées, en conservant par défaut la première ligne dupliquée trouvée. Il est possible via l'argument subset={.python} de choisir quelles colonnes sont considérées pour être comparées dans le DataFrame{.python}. Par exemple :

import pandas as pd

data = pd.read_csv("names.csv")
# Dédupliquer en considérant uniquement les colonnes Nom et Prénom 
deduplicated = data.drop_duplicates(subset=["Prénom", "Nom"])

Un argument keep={.python} permet d'indiquer quelles lignes conserver parmi les doublons, et possède par défaut la valeur "first"{.python}, qui ne conserve que la première ligne en doublon (en partant du sommet du document).


Suppression des doublons

L'argument keep={.python} de la méthode drop_duplicates(){.python} permet d'indiquer à Pandas comment dédupliquer les entrées. Il accepte trois valeurs : "first"{.python}, "last"{.python} et False{.python}.

  • "first"{.python} : seule la première entrée de chaque groupe de doublons est conservée (en partant du haut du document)
  • "last"{.python} : seule la dernière entrée du groupe de doublons est conservée (en partant du bas du document)
  • False{.python} : si une ligne apparaît en doublon, elle et tous ses clones sont retirés du résultat

Conservation des doublons

Vous pourriez être tenté depuis un jeu de données de récupérer uniquement les doublons afin de les analyser. Cette opération peut se faire en deux temps, grâce à la méthode .duplicated(){.python} d'un DataFrame{.python}.

La méthode vous renvoie une série de booléens, indiquant si chaque ligne est considérée comme un doublon. Plusieurs stratégies existent pour considérer un doublon:

  • Toutes les valeurs sauf la première occurrence ("first")
  • Toutes les valeurs sauf la dernière occurrence ("last")
  • Toutes les valeurs (False{.python})
import pandas as pd

data = pd.read_csv("names.csv")
# Dédupliquer en considérant uniquement les colonnes Nom et Prénom 
duplicated = data.duplicated(subset=["Prénom", "Nom"], keep=False)
duplicates = data.loc[duplicated]

Trier les lignes d'un DataFrame

Vous pouvez trier les lignes d'un DataFrame{.python} ou d'une Series{.python} en personnalisant sur quelles colonnes vous souhaitez effectuer le tri, et dans quel sens vous souhaitez effectuer ledit tri (croissant ou décroissant) :

import pandas as pd

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

# Récupérer un dataframe trié sur nom puis prénom, dans l'ordre croissant
result1 = df.sort_values(by=["nom", "prenom"])
print(result1)
# Récupérer un dataframe trié sur nom puis prénom, dans l'ordre croissant, puis décroissant
result2 = df.sort_values(by=["nom", "prenom"], ascending=[True, False])
print(result2)

Récapitulatif

Filtrer un DataFrame{.python} assigné à une variable df{.python} :

Attributs et usages principaux :

  • df.loc[]{.python} : extraire des lignes
  • df.iloc[]{.python} : extraire des lignes
  • df.at[rowindex, colindex]{.python} : extraire une cellule
  • df[]{.python} : extraire des colonnes

Dans tous les cas sauf pour df.at{.python}, la clé peut être un des objets suivants :

  • Index de ligne ou de colonne (a{.python});
  • slice Python (départ:fin:pas{.python});
  • liste de clés de lignes/colonne ou numéro de ligne ([a, b, c]{.python});
  • tuple à deux éléments ((ligne(s), colonne(s)){.python}).

Valeurs vides

Au même titre qu'un document Excel, un DataFrame{.python} peut contenir des cellules vides. Malheureusement, nous sommes en Python et la notion de vide doit être représentée par une valeur. Et il ne s'agit pas de None{.python}, mais d'une valeur numpy.nan{.python} (not a number).

Notez que le type de cette valeur spéciale « vide » est float{.python} et joue sur le type de sa colonne (ex. une valeur NaN{.python} dans une colonne de type int64{.python} transforme la colonne en lui donnant le type float64{.python}).

Not a Number sur Wikipedia


Filtrage des valeurs vides

Pour un objet Series{.python} ou DataFrame{.python}, deux méthodes vous permettent d'obtenir un objet de même type et de mêmes dimensions, rempli d'une carte de booléens indiquant les valeurs vides (NaN{.python}).

import pandas as pd
from numpy import nan

df = pd.DataFrame(data={"nom": ["Jose", nan, "Pepe"], "age": [nan, 10, 24]})
# Dataframe de booléens à deux colonnes où chaque valeur vide de df est True
nan_map = df.isnull()
# Dataframe de booléens à deux colonnes où chaque valeur de df remplie est True
set_map = df.notnull()

À quoi peuvent bien servir les cartes de booléens obtenues sous forme de DataFrame{.python} ?

  1. Utilisées comme clés directement sur un DataFrame{.python} de mêmes dimensions, nous récupérons un DataFrame{.python} où les valeurs à la même position qu'une valeur False{.python} dans la carte sont remplacées par NaN{.python};
  2. si une colonne est utilisée comme clé pour l'attribut .loc{.python} d'un DataFrame{.python}, le filtrage classique est utilisé : une ligne à la même position qu'un False{.python} n'est pas conservée dans le résultat;
  3. utiliser des méthodes d'agrégation numériques comme .sum(){.python} ou .mean(){.python} permet respectivement de connaître le nombre de valeurs True{.python} par colonne de booléens ou le ratio de valeurs True{.python} par colonne de booléens (car False{.python} vaut 0 et True{.python} vaut 1).

Méthodes .any(){.python}, .all(){.python} et .sum()

Les cartes booléennes que vous pouvez obtenir avec les méthodes .isna(){.python} et .notna(){.python} possèdent des méthodes d'agrégation utiles :

import pandas as pd
from numpy import nan

df = pd.DataFrame(data={
    "nom": ["Jose", nan, "Pepe"], 
    "age": [nan, 10, 24]
})
# Dataframe de booléens à deux colonnes où chaque valeur initialement vide
# est représentée par un booléen True
nan_map: pd.DataFrame = df.isnull()
# L'axe à préciser est celui à réduire
print(nan_map.any(axis="columns"))  # Réduire les colonnes d'une ligne
print(nan_map.all())  # Réduire par défaut les lignes d'une colonne
print(nan_map.sum())  # Réduire par défaut les lignes d'une colonne

Supprimer des lignes avec des valeurs inexploitables

Cela peut arriver sur des données récupérées de diverses sources, d'avoir des informations incomplètes. Par exemple, une information d'entreprise n'indiquant pas d'adresse de siège (ce qui serait intéressant pour du publipostage).


Vous pouvez, sur un DataFrame{.python}, retirer des lignes (ou des colonnes) dans lesquelles certaines colonnes ne sont pas renseignées, avec plusieurs stratégies :

import pandas as pd
from numpy import nan

df = pd.DataFrame(data={
    "siret": ["5001234567890", "6001234567890", "7001234567890", "800123456789"], 
    "nom": ["SARL Marks", "SAS Pareil", "SASU Key", "Péri SCOP"],
    "siege": ["Rue des roses", "Rue des baies", nan, nan]
})
# Créer un nouveau DataFrame dans lequel
# Ne garder que les lignes avec un siège ET un SIRET
df2 = df.dropna(axis="index", how="any", subset=["siege", "siret"])
# Ne garder que les lignes avec un siège OU un SIRET
df3 = df.dropna(axis="index", how="all", subset=["siege", "siret"])
# Ne garder que les colonnes sans aucune valeur vide
df4 = df.dropna(axis="columns", how="any")

Conserver les lignes avec des valeurs vides

Si vous souhaitez traiter ou analyser les lignes qui contiennent des valeurs que vous considérez non exploitables, vous pouvez les récupérer, en deux temps, en utilisant les méthodes .isnull(){.python} et .any(){.python} ou .all(){.python}:

import pandas as pd
from numpy import nan

df = pd.DataFrame(data={
    "siret": ["5001234567890", "6001234567890", "7001234567890", "800123456789"], 
    "nom": ["SARL Marks", "SAS Pareil", "SASU Key", "Péri SCOP"],
    "siege": ["Rue des roses", "Rue des baies", nan, nan]
})

# Récupérer les lignes avec du vide
empty_map = df.isna()  # Récupérer la carte qui dit si chaque valeur du dataframe est un NaN
# Dire pour chaque ligne si une des valeurs est True
# Cela donne une série de booléens qui dit pour chaque ligne si elle contenait une valeur vide
empty_rows = empty_map.any(axis="columns") 
# Afficher le résultat du filtre
print(df.loc[empty_rows])

Insérer des valeurs à la place des valeurs vides

Si vous souhaitez conserver des données incomplètes, et que votre stratégie pourrait consister à utiliser des valeurs par défaut lorsqu'une valeur non renseignée est rencontrée, vous avez la possibilité d'utiliser la méthode .fillna(){.python} d'un DataFrame{.python}. Elle offre quelques stratégies de remplissage.


Valeur constante ou dictionnaire

Lorsque vous utilisez la méthode .fillna(){.python} en passant des littéraux, vous récupérez un DataFrame{.python} où toutes les entrées vides de toutes les colonnes sont remplacées.

import pandas as pd
from numpy import nan

df = pd.DataFrame(data={
    "siret": ["5001234567890", nan, "7001234567890", "800123456789"], 
    "nom": ["SARL Marks", "SAS Pareil", "SASU Key", nan],
    "siege": ["Rue des roses", "Rue des baies", nan, nan]
})
# Créer un nouveau DataFrame dans lequel
# Remplacer les valeurs vides par un texte fixe, partout
df2 = df.fillna("Inconnu")
# Remplacer les valeurs vides par colonne
df3 = df.fillna({"siret": "N/A", "siege": "Inconnu"})
# Remplir les colonnes avec la dernière valeur non vide trouvée
df4 = df.ffill()  # forward fill
# Remplir les colonnes avec la prochaine valeur non vide trouvée
df5 = df.bfill()  # backward fill

DataFrame{.python} ou Series

Utiliser la méthode .fillna(){.python} en passant une Series{.python} ou un DataFrame{.python} est possible, dans certains cas. Les cas autorisés sont les suivants :

  • Vous appelez la méthode sur un DataFrame{.python} et vous passez un DataFrame{.python} de mêmes dimensions.
  • Vous appelez la méthode sur une Series{.python} et vous passez une Series{.python} de même dimension.

Dans tous les cas, vous obtenez un DataFrame{.python} ou une Series{.python} dans le ou laquelle les valeurs NaN{.python} sont remplacées par les valeurs à la même position dans le DataFrame{.python} ou la Series{.python} passé en argument (les indexs et noms de colonne doivent correspondre).


import pandas as pd
from numpy import nan

df = pd.DataFrame(data={
    "siret": ["5001234567890", nan, "7001234567890", "800123456789"], 
    "nom": ["SARL Marks", "SAS Pareil", "SASU Key", nan],
    "siege": ["Rue des roses", "Rue des baies", nan, nan]
})

df_fill = pd.DataFrame(data={
    "siret": ["14", "14", "14", "14"], 
    "nom": ["A", "A", "A", "A"],
    "siege": ["B", "B", "B", "B"]
})

# La première colonne contiendra un "14", etc.
print(df.fillna(df_fill))

Modifier les informations d'un DataFrame

Ce sous-chapitre va décrire comment changer le contenu d'un DataFrame{.python} (des fois en modifiant directement le contenu, des fois en récupérant un résultat sous la forme d'un nouveau DataFrame{.python}).


Remplacer une colonne ou une ligne

Cette opération modifie le contenu d'un DataFrame{.python} et utilise la syntaxe de Python pour manipuler les dictionnaires. Cela donne par exemple :

import pandas as pd

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

# Changer la colonne `prenom`
# La valeur peut être une séquence ou un littéral
df["prenom"] = ["Aude", "Maud", "Raoul", "Gilles"]
# Changer la ligne à l'index 0
# La valeur peut être une séquence, un dictionnaire ou un littéral
df.loc[0] = ["Arthur", "Martin", 1]
print(df)

Créer une ligne ou une colonne

Cette opération est très simple, il suffit juste d'utiliser des valeurs d'index de ligne ou de colonne qui n'existent pas déjà pour créer un nouveau contenu (de la même manière qu'avec un dictionnaire Python)

import pandas as pd

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

df["naissance"] = [1971, 1994, 2003, 1986]
df.loc[len(df.index)] = {"prenom": "Éric", "nom": "André", "numero": 19, "naissance": 1991}
print(df)

TODO: df.Append


Définir la valeur d'une cellule

Il existe une façon recommandée d'accéder à, et surtout de modifier une cellule d'un DataFrame{.python} avec Pandas. Il faut passer par l'attribut .at{.python} du DataFrame{.python} et l'utiliser comme un dictionnaire pour retrouver une cellule à un index et une colonne donnée :

import pandas as pd

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

# Modifier la valeur à la ligne 2, colonne "nom"
df.at[2, "nom"] = "Courcelles"
print(df)

Retirer une ligne ou une colonne

Une façon recommandée de "corriger" un DataFrame{.python} en retirant soit une ligne, soit une colonne (ou plusieurs) consiste à utiliser une méthode .drop(){.python}.

import pandas as pd

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

# Retirer la colonne prénom et directement modifier le DataFrame
df.drop(labels=["prenom"], axis="columns", inplace=True)
# Retirer les lignes aux index 0 et 2, mais retourner un nouveau DataFrame
df2 = df.drop(labels=[0, 2], axis="index")

Appliquer une fonction sur un DataFrame{.python} ou Series

Vous pouvez appliquer un calcul personnalisé à tous les éléments d'une Series{.python} ou d'un DataFrame{.python}, et récupérer respectivement une Series{.python} ou un DataFrame{.python}.

Le principe peut s'apparenter aux formules dans un document type Excel, mais dans le cas courant, un calcul est executé une fois sur chaque élément d'une sélection, et on récupère un DataFrame{.python} et une Series{.python} en résultat.

import pandas as pd

def double(s: pd.Series) -> pd.Series:
    return s * 2  # Cela marche aussi sur les chaînes

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

# Appliquer une fonction sur chaque colonne
df2 = df.apply(double, axis="columns")

L'exemple précédent peut être répliqué avec une simple fonction anonyme (lambda{.python}) :

import pandas as pd

df = pd.DataFrame(data={
    "prenom": ["Gisèle", "Jocelyne", "Charles", "Valentin"], 
    "nom": ["Garnier", "Jones", "Chancel", "Verdier"],
    "numero": [8, 31, 23, 14]
})

# Appliquer une fonction sur chaque colonne
df2 = df.apply(lambda s: s * 2, axis="columns")

Équivalents au SQL pour des DataFrame


Éléments SQL

Ce chapitre va aborder des fonctionnalités dans Pandas qui reprennent des concepts trouvables en bases de données SQL, nommément:

  1. les jointures (JOIN{.sql})
  2. les groupements (GROUP BY{.sql}) et
  3. les fonctions de fenêtrage (OVER{.sql}).

Fusion (jointures)

En SQL, une jointure consiste à faire correspondre des données appartenant à plusieurs tables pour obtenir une vue plus complète, dans une seule table de résultat. Prenons comme exemple, une liste de transactions (virements) effectués au bénéfice de personnes diverses.

Notre base de données pourrait contenir les deux tables suivantes:

Tables de base


Ce que nous souhaitons obtenir, comme en SQL, est une table de résultat contenant les informations tirées de la liaison entre la table transactions et la table people, avec un INNER JOIN{.sql}:

Table de fusion


Si nous représentons les deux tables dans un code Python tel que le suivant:

import pandas as pd

transactions = pd.DataFrame(data={
    "id": [1, 2, 3, 4, 5, 6], 
    "date": ["2024-01-01", "2024-01-08", "2024-01-09", "2024-01-23", "2024-01-27", "2024-02-03"],
    "amount": [150, 70, 86, 300, 75, 66],
    "person": [1, 2, 1, 3, 2, 5],
})
transactions["date"] = pd.to_datetime(transactions["date"])

people = pd.DataFrame(data={
    "id": [1, 2, 3, 4], 
    "firstname": ["Henri", "Colette", "Martine", "Jean-Pierre"],
    "lastname": ["Bouvier", "Chaland", "Duvivier", "Escudé"],
})

Nous pouvons obtenir la table de fusion avec la méthode .merge(){.python}. Par défaut, vous obtiendrez le même résultat qu'un INNER JOIN{.sql} en SQL:

import pandas as pd

transactions = pd.DataFrame(...)
people = pd.DataFrame(...)

# Récupérer la fusion avec people où transactions.person = people.id
cartesian_product = transactions.merge(people, left_on="person", right_on="id")

Seules les lignes de transactions{.python} ayant un équivalent dans people{.python} seront présentes dans le résultat.


Autres stratégies de fusion

En SQL, les jointures peuvent se faire d'au moins quatre façons:

  • INNER JOIN{.sql}: correspondance des deux côtés
  • LEFT JOIN{.sql}: toutes les lignes de gauche apparaissent
  • RIGHT JOIN{.sql}: toutes les lignes de droite apparaissent
  • OUTER JOIN{.sql}: toutes les lignes apparaissent

Toutes ces stratégies sont applicables à la méthode .merge(){.python} des DataFrame{.python}, via l'argument how{.python}.

Les valeurs possibles de cet argument sont:

  • how="inner"{.python}
  • how="outer"{.python}
  • how="left"{.python}
  • how="right"{.python}
  • how="cross"{.python}: produit cartésien, pas une jointure

Groupes d'agrégation (GROUP BY)

En SQL, la notion de groupements consiste à considérer un jeu de données, et indiquer au moteur de base de données que ledit jeu doit être découpé en groupes afin de pouvoir appliquer une fonction d'agrégation à chaque groupe.

Par exemple, si nous considérons le document Excel suivant :

Ventes


Nous pouvons matérialiser l'exemple précédent par le code suivant :

import pandas as pd

sales = pd.DataFrame(data={
    "id": [1, 2, 3, 4, 5, 6, 7, 8],
    "country": ["France", "France", "France", "France", "Spain", "Spain", "Spain", "Spain"],
    "city": ["Lille", "Paris", "Paris", "Chartres", "Malaga", "Madrid", "Barcelona", "Barcelona"],
    "product": ["wrench", "hammer", "screws", "drill", "hammer", "screws", "nails", "saw"],
    "sales": [200, 650, 100, 315, 250, 90, 400, 440]
})

Si nous désirons ensuite calculer la somme des ventes par pays:

Ventes par pays


Nous pouvons le faire grâce à la méthode .groupby(){.python}:

import pandas as pd

sales = pd.DataFrame(data={
    "id": [1, 2, 3, 4, 5, 6, 7, 8],
    "country": ["France", "France", "France", "France", "Spain", "Spain", "Spain", "Spain"],
    "city": ["Lille", "Paris", "Paris", "Chartres", "Malaga", "Madrid", "Barcelona", "Barcelona"],
    "product": ["wrench", "hammer", "screws", "drill", "hammer", "screws", "nails", "saw"],
    "sales": [200, 650, 100, 315, 250, 90, 400, 440]
})
# Les groupes sont uniquement utilisables pour des agrégations
# Tenter une agrégation sur toutes les colonnes numériques
sales_by_country = sales.groupby("country").sum(numeric_only=True)
# Tenter une agrégation sur les colonnes qui m'intéressent
sales_by_country = sales.groupby("country")[["sales", "id"]].sum(numeric_only=True)

Fonctions de fenêtrage (OVER{.sql})

En SQL, le fenêtrage permet d'utiliser des fonctions d'agrégation sur toutes les lignes d'une table. Il est possible d'appliquer une fonction qui calcule pour chaque ligne la somme d'une de ses colonnes, mais uniquement à partir des deux lignes précédentes, ex.:

Données de fenêtrage


Fonctions d'agrégation mobiles/glissantes

Si nous appliquons cette idée sur la colonne minutes, nous obtiendrons une colonne avec les valeurs suivantes: 31{.python}, 31 + 47 == 78{.python}, 31+47+26==104{.python}, 47 + 26 + 52 == 125{.python}, 26 + 52 + 34 == 102{.python}, 52 + 34 + 22 == 108{.python}, etc…

import pandas as pd

runs = pd.DataFrame(data={
    "id": [1, 2, 3, 4, 5, 6, 7, 8, 9],
    "date": pd.date_range("2024-01-01", "2024-01-09", periods=9),
    "minutes": [31, 47, 26, 52, 34, 22, 36, 15, 30],
})
# Calculer la nouvelle colonne
runs["3-day sum"] = runs["minutes"].rolling(2, min_periods=0, center=False, closed="both").sum()

Fonctions d'agrégation mobiles cumulatives

Nous pouvons appliquer la même stratégie pour calculer la somme depuis la première ligne lorsque nous appliquons la somme mobile à chaque ligne du DataFrame{.python}:

import pandas as pd

runs = pd.DataFrame(data={
    "id": [1, 2, 3, 4, 5, 6, 7, 8, 9],
    "date": pd.date_range("2024-01-01", "2024-01-09", periods=9),
    "minutes": [31, 47, 26, 52, 34, 22, 36, 15, 30],
})
# Calculer la nouvelle colonne
runs["total sum"] = runs["minutes"].expanding(min_periods=0).sum()
# La même chose peut être faite avec une somme roulante avec un grand nombre d'observations
runs["total sum+"] = runs["minutes"].rolling(len(runs), min_periods=0).sum()

Les méthodes runs.rolling{.python} et runs.expanding{.python} peuvent être précédées d'un .groupby(){.python} et d'un .sort_values(){.python} pour créer des groupes d'agrégation et organiser les éléments dans chaque groupe.