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

512 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Pandas
author: Steve Kossouho
---
# Manipuler et comprendre les séries
----
## Propriétés d'une série
Une `Series` dans Pandas est un objet contenant une séquence de valeurs. La série tout entière possède un type unique (voir les types à la fin du chapitre précédent) et tous les éléments de cette structure sont d'un type cohérent avec celui de la série. _Une série avec des éléments de types trop divers (ou avec des chaînes de caractères) sera de type `object`._
Une série peut également posséder un nom.
![Exemple de série](assets/images/structures-series-intro.png)
----
## Quand obtient-on une série ?
Un objet de type `Series` s'obtient soit manuellement, soit en extrayant précisément une colonne ou une ligne de données depuis des données tabulaires.
----
## Types de données dans Pandas
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
Les types `timedelta64` et `category` sont rarement rencontrés.
----
## Type de données d'une `Series`
Un objet `Series` possède toujours un type appliqué à tous ses éléments. Selon le type associé, des traitements différents sont possibles sur une série. Il est toujours possible de connaître le type d'une série en accédant à son attribut `dtype` (data type) :
```python {.numberLines}
import pandas as pd
series = pd.Series(data=[1, 2, 3, 4])
print(series.dtype) # affiche "int64" car les données sont toutes compatibles
```
----
## Créer une série
Nous avons vu que nous pouvons créer simplement des séries, et nous pouvons le faire en passant des informations variées :
- Des tableaux Numpy à une dimension (voir sous-chapitre précédent)
- Des listes ou des tuples de données Python (`int`{.python}, `float`{.python}, `str`{.python}, `datetime.datetime`{.python}…)
- Des `Series`{.python} sont aussi acceptées
----
```python {.numberLines}
import numpy as np
import pandas as pd
# Créer une série avec une somme cumulative générée avec Numpy
series1 = pd.Series(data=np.cumsum([1, 2, 3, 4, 5, 6, 7, 8, 9]))
```
Exemple de série créée depuis un tableau Numpy (lui-même généré via des données Python).
----
```python {.numberLines}
import pandas as pd
# Créer une série avec des entiers, passés via des données standard Python
series2 = pd.Series(data=[1, 2, 3, 4, 5, 6, 7, 8, 9]) # liste
series3 = pd.Series(data=(1, 2, 3, 4, 5, 6, 7, 8, 9)) # tuple
series4 = pd.Series(data=series2) # série Pandas (pas très utile)
```
Exemple de série créée depuis des listes ou tuples Python, ou encore des `Series`{.python}
----
### Créer une série avec un index
Un index est un ensemble de valeurs associées à chaque élément d'une série pour l'identifier.
Une série possède **toujours** un index, par défaut des nombres successifs commençant par zéro.
Par exemple, si l'on considère la série suivante :
![Structure d'une série et de l'index](assets/images/structures-series-index.png)
----
![Série](assets/images/structures-series-index.png)
Les valeurs d'index `"U1"` et `"U3"`, par exemple, sont associées respectivement aux valeurs `"Alain"` et `"Gilles"` de la série. Il sera également possible d'extraire la valeur `"Alain"` avec le code suivant :
```python {.numberLines}
import pandas as pd
s1 = pd.Series(data=["Alain", "Lucie", "Gilles", "André", "Zoé", "Paul"], index=["U1", "U2", "U3", "U4", "U5", "U6"])
print(s1["U1"]) # Affiche la valeur "Alain" en extrayant depuis l'index "U1"
```
----
### Créer une série en forçant le type
Vous pouvez créer une série avec Pandas en précisant le type (`dtype`) à appliquer à ses valeurs; par exemple,
vous pourriez définir votre série en passant uniquement des nombres entiers, et considérer que le type de la série
devrait malgré tout être `float64`. Ou, plus intéressant, vous pouvez par exemple créer une série en passant des chaînes
de caractères représentant des dates, et indiquer que ces chaînes doivent être converties vers le type `datetime64[ns]`
(seul le [format ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601) des dates est interprété) :
```python {.numberLines}
import pandas as pd
s1 = pd.Series(data=["2023-01-02", "2024-03-17"]) # Une série de chaînes de caractères
s2 = pd.Series(data=["2023-01-02", "2024-03-17"], dtype="datetime64[ns]") # Une série d'objets de type date
s3 = pd.Series(data=["2023-01-02", "2024-03-17"], dtype="float64") # Une erreur va se produire : la conversion n'a pas de sens
```
----
## Extraire des informations d'une série
Un objet de type `Series` dans Pandas possède toujours 3 informations distinctes :
- Les valeurs stockées dans l'objet (attribut `.values : ndarray`{.python})
- Le nom de l'objet (attribut `s.name`{.python}, peut être `None`{.python})
- L'index de l'objet (par défaut une séquence de valeurs numériques, attribut `s.index`)
**Note** : On peut récupérer les valeurs de la série sous forme de tableau numpy via
la méthode `.to_numpy()`{.python}.
----
## Opérations sur les séries
Pandas propose d'effectuer des opérations diverses sur des séries, pour obtenir des valeurs scalaires, ou
des séries. Par exemple, vous pouvez appliquer des opérations arithmétiques :
```python {.numberLines}
import pandas as pd
s1 = pd.Series(data=[1, 2, 3, 4], index=[1, 2, 3, 4], name="counter")
s2 = s1 * 2 # génère une série avec les valeurs 2, 4, 6, 8
s3 = s1 * s1 # génère une série avec les valeurs 1, 4, 9, 16
s4 = s1 + 4 # génère une série avec les valeurs 5, 6, 7, 8
```
----
Les séries dans Pandas prennent également en charge l'usage d'opérateurs de comparaison, qui permettent de
récupérer des séries de valeurs booléennes. En voici quelques exemples d'illustration :
```python {.numberLines}
import pandas as pd
s1 = pd.Series(data=[1, 2, 3, 4], index=[6, 7, 8, 9], name="counter")
s2 = s1 > 2 # génère une série avec False, False, True, True
s3 = (s1 * 5) != 10 # génère une série avec True, False, True, True
print(4 in s1) # vérifie si la valeur 4 fait partie... de l'index
print(3 in s1.values) # vérifie si la valeur 3 fait partie... des données
print(s1.isin([1, 4])) # Dit pour chaque valeur si elle fait partie des valeurs 1 et 4
```
Nous verrons que les séries contenant des valeurs booléennes pourront être utiles plus tard
afin de filtrer des éléments de séries ou de dataframes.
----
### Modifier les valeurs de séries
Les séries Pandas ne proposent généralement pas d'outils sous forme de méthodes pour en modifier
le contenu. Pour autant, la méthode la plus simple, disponible en Python sur les objets de type `list`{.python}
ou `dict`{.python}, fonctionnera très bien sur une série :
```python {.numberLines}
import pandas as pd
fruit = pd.Series(data=["apple", "watermelon", "grapefruit", "lemon"])
fruit[0] = "Orange" # Remplacer la valeur à l'index 0
fruit["total"] = "Cocktail" # Ajouter une valeur à un nouvel index
```
----
## Valeurs vides
Si l'on considère un document Excel, par exemple, un document peut contenir des valeurs non renseignées. Les cellules desdites
valeurs apparaissent naturellement vides. Dans Pandas, lorsqu'une série (ou un `DataFrame`) possède des cellules vides, la valeur
qui y est contenue est une valeur spéciale nommée `NaN` (Not a number).
```python {.numberLines}
import pandas as pd
# Vous pouvez utiliser la valeur dans vos séries. Elle n'est disponible que dans Numpy.
from numpy import nan # NaN non disponible depuis numpy 2.0
series_with_holes = pd.Series(data=["apple", "watermelon", nan, "grapefruit", nan])
```
> Notez que le type de cette valeur est `float64`, et que de telles valeurs vont influencer notamment le
`dtype` des séries de nombres entiers, qui prendront ainsi le type `float64`.
----
### Méthodes pour les valeurs vides
Les séries possèdent plusieurs outils pratiques pour obtenir des informations sur leurs valeurs vides. Une première série de
méthodes vous permet d'obtenir, depuis votre série, une série indiquant si une valeur est vide, ou le contraire. Voyons un
exemple :
```python {.numberLines}
import pandas as pd
from numpy import nan
s1 = pd.Series(data=[1, 2, nan, 4, nan, 6, 7])
s_na1 = s1.isna() # donne une série contenant [False, False, True, False, True, False, False]
s_na2 = s1.isnull() # synonyme : donne exactement le même résultat
s_nn1 = s1.notna() # donne une série contenant [True, True, False, True, False, True, True]
s_nn2 = s1.notnull() # synonyme de notna
has_na = s1.hasnans # renvoie True : des valeurs sont vides
```
----
#### Retirer les valeurs vides
Une méthode vous permet de retirer les valeurs vides dans une série. Notez que les valeurs d'index
associées ne sont naturellement pas conservées; l'index associé à la série aura certaines valeurs non contiguës.
```python {.numberLines}
import pandas as pd
from numpy import nan
s1 = pd.Series(data=[1, 2, nan, 4, nan, 6, 7])
sc1 = s1.dropna(inplace=False) # donne une série contenant [1, 2, 4, 6, 7]
```
----
#### Remplir les valeurs vides
Vous pouvez également remplacer les valeurs vides d'une série, et ce de plusieurs manières, avec la méthode
`.fillna()`{.python} :
```python {.numberLines}
import pandas as pd
from numpy import nan
s1 = pd.Series(data=[1, 2, nan, 4, nan, 6, 7])
# Remplir les valeurs avec une valeur fixe
sc1 = s1.fillna(-1, inplace=False) # donne une série contenant [1, 2, -1, 4, -1, 6, 7]
# Une série où on fait coincider les index pour remplacer les valeurs vides
sc2 = s1.fillna(pd.Series([9, 8, 7, 6, 5, 4, 3])) # donne [1, 2, 7, 4, 5, 6, 7]
```
----
#### Connaître le nombre de valeurs non vides
Si l'on peut récupérer le nombre d'éléments d'une série avec la fonction Python `len()`{.python}, la méthode
`count()` d'une série permettra d'obtenir le nombre de valeurs **renseignées**. Pour obtenir le nombre de valeurs vides,
il faudra ruser un peu :
```python {.numberLines}
import pandas as pd
from numpy import nan
s1 = pd.Series(data=[1, 2, nan, 4, nan, 6, 7])
filled_count = s1.count() # Renvoie 5
empty_count_a = len(s1) - s1.count() # Renvoie 2
empty_count_b = s1.isna().sum() # sum() compte `True` comme 1, `False` comme 0. Renvoie 2
```
----
### Séries booléennes et existence de valeurs `True`
En Python, les fonctions `any()`{.python} et `all()`{.python} prennent un itérable en argument
et indiquent respectivement si ledit itérable contient un élément équivalent à `True`{.python}, et
si ledit itérable contient uniquement des éléments équivalents à `True`{.python}.
Le principe est identique avec les méthodes `.any()`{.python} et `.all()`{.python} des séries de
booléens :
```python {.numberLines}
import pandas as pd
s1 = pd.Series([False, True, True, False])
print(s1.any(), s1.all()) # Affiche True et False
```
`any()`{.python} renvoie `True` si au moins une valeur est équivalente à `True`{.python}, et `all()`{.python} renvoie
si toutes les valeurs non vides sont équivalentes à `True`{.python}.
----
## Valeurs uniques
Cela peut être intéressant de savoir dans une série quelles valeurs distinctes apparaissent, ou même
supprimer des valeurs afin de ne garder que des valeurs uniques d'une série.
```python {.numberLines}
import pandas as pd
numbers = pd.Series(data=[1, 4, 7, 3, 4, 8, 2, 4, 7, 5, 6, 8, 9, 1, 1, 3, 2])
uniques = numbers.unique() # valeurs distinctes, tableau numpy
unique2 = numbers.drop_duplicates(keep="first") # ou "last", ou False pour tout supprimer
duplicates = numbers.duplicated(keep="first") # série de booléens indiquant si une valeur est un doublon
frequencies = numbers.value_counts() # série indiquant pour chaque valeur son nombre d'apparitions
print(frequencies[4]) # Récupérer le nombre d'apparitions de la valeur 4
print(frequencies.get(4)) # Idem, mais renvoie `None` si la valeur n'apparait pas
```
- [Documentation de `drop_duplicates`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.drop_duplicates.html#pandas.Series.drop_duplicates)
- [Documentation de `value_counts`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.value_counts.html#pandas.Series.value_counts)
----
## Appliquer une fonction aux valeurs d'une série
Occasionnellement, les outils de traitement et de transformations de séries ne suffisent pas. Vous
pouvez avoir besoin de votre propre algorithme à appliquer sur des valeurs d'une série, ex. vous avez
besoin de simplifier une série de valeurs textuelles, en **retirant les accents** et en mettant les caractères
**en minuscules**. La méthode `apply()`{.python} des séries vous permettra ce genre de traitement :
```python {.numberLines}
import unidecode # pip install unidecode
import pandas as pd
def unaccent_lower(text: str):
"""Retire les accents et convertit en minuscules."""
return unidecode.unidecode(text).lower()
names = pd.Series(data=["Élodie", "Jérémy", "Céleste", "Urünundür"])
# Appliquer la fonction élément par élément pour obtenir une nouvelle série
simple = names.apply(unaccent_lower, by_row="compat")
```
[Documentation de `apply`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.apply.html)
----
Vous pouvez également appliquer une fonction et lui donner connaissance de tous les éléments de
la série, en indiquant la valeur `False` à l'argument `by_row=` de la méthode `apply`.
La fonction devra renvoyer une série :
```python {.numberLines}
import pandas as pd
def nullify_below_avg(numbers: pd.Series):
"""Met à 0 les nombres inférieurs à la moyenne."""
average = numbers.mean()
values = [(n if n >= average else 0) for n in numbers]
return pd.Series(data=values)
values = pd.Series(data=[19, 33, 12, 5.7, 61, 14])
# Appliquer la fonction élément par élément pour obtenir une nouvelle série
simple = values.apply(nullify_below_avg, by_row=False)
```
----
## Filtrer les éléments d'une série
Si récupérer un élément unique d'une série fonctionne comme avec un dictionnaire Python, récupérer une partition
de série est aussi possible, via certaines techniques. Vous pouvez récupérer :
- Plusieurs éléments par la valeur d'index associée
- Plusieurs éléments par un index numérique
- Slicing, par index ou par index numérique
- Éléments filtrés par série de booléens
----
### Récupérer plusieurs éléments par index
Récupérer plusieurs éléments consiste à passer en tant que clé un objet de type `list`{.python} (et uniquement ce type).
Chaque valeur de ladite liste représente une valeur d'index à extraire. De préférence, on filtrera avec l'index en utilisant
l'attribut `.loc`{.python} (_line of content_ ou _location_ ?) de la série, et on filtrera par index numérique avec l'attribut `.iloc`{.python} :
```python {.numberLines}
import pandas as pd
names = pd.Series(data=["Élodie", "Hubert", "Céleste", "Jacques", "Albéric"], index=["E", "H", "C", "J", "A"])
names_cha = names.loc[["C", "H", "A"]] # récupère Céleste, Hubert et Albéric
names_chb = names[["C", "H", "A"]] # la même opération peut être effectuée directement sur `names`
names_ch2 = names.iloc[[2, 1, 4]] # même résultat que ci-dessus
```
----
### Slicing
Vous pouvez récupérer une partition d'une série avec la syntaxe du slicing (`[start:openstop:step]`{.python}) :
```python {.numberLines}
import pandas as pd
from numpy import r_ # trucs d'indexation
names = pd.Series(data=["Élodie", "Hubert", "Céleste", "Jacques", "Albéric"], index=["E", "H", "C", "J", "A"])
elodie_to_celeste = names.loc["E":"C"] # Attention : l'index de fin est inclus
elodie_to_celeste2 = names.iloc[0:3] # Attention : l'index de fin n'est pas inclus
every_two = names.iloc[::2] # L'argument step fonctionne comme prévu
# Appliquer plusieurs slices : outil fourni par Numpy
multiple_slice = names.iloc[r_[0:2, 3:5]] # Note : ne pas préciser l'index de fin ne fonctionne pas
```
----
### Filtrage par série de booléens
```python {.numberLines}
import pandas as pd
values = pd.Series(data=[19, 33, 12, 5.7, 61, 14])
gt_eighteen = values > 18.0 # booléens : T, T, F, F, T, F
filtered1 = values.loc[gt_eighteen] # 19, 33, 61
filtered2 = values.loc[[True, True, False, False, True, False]] # Équivalent
```
----
### Filtrer sur plusieurs conditions
Vous pouvez filtrer sur plusieurs conditions, en les assemblant avec des opérateurs `ET` et `OU`.
Par exemple, si je souhaite filtrer une série en gardant les valeurs entre 0 et 50, et qui sont divisibles
par 3, je peux effectuer l'opération logique de la façon suivante:
```python {.numberLines}
import pandas as pd
import numpy as np
numbers = pd.Series(data=np.arange(0, 101)).sample(frac=1)
# Récupérer les nombres entre 0 et 30 divisibles par 3
output = numbers.loc[numbers.between(0, 30, inclusive="both") & (numbers % 3 == 0)]
```
Pour des questions d'ordre d'évaluation des opérateurs, vous devrez quasi systématiquement utiliser des parenthèses pour discriminer vos conditions.
Les opérateurs à utiliser pour assembler vos propositions sont les opérateurs logiques binaires:
- `&`{.python}: ET binaire
- `|`{.python}: OU binaire
- `~`{.python}: NON binaire
- `^`{.python}: OU exclusif binaire
----
### Méthodes par type de séries
On peut très simplement appliquer des opérateurs à des séries, et obtenir des séries. Rappel :
```python {.numberLines}
import pandas as pd
values = pd.Series(data=[19, 33, 12, 5.7, 61, 14])
double = values * 2 # 38, 66, 24, 11.4, 122, 28
even = values % 2 == 0 # F, F, T, F, F, T
```
----
#### Séries de type `object` (chaînes)
Sur une série de type chaîne de caractères, on aura davantage envie d'appliquer
simplement des méthodes de base disponibles sur le type `str`{.python}.
Le moyen d'utiliser lesdites méthodes consiste à passer par un attribut de la
série nommé `.str`{.python} :
```python {.numberLines}
import pandas as pd
names = pd.Series(data=["Élodie", "Hubert", "Céleste", "Jacques", "Albéric", "42", "1137"])
lower = names.str.lower() # récupérer une série avec les noms en minuscule
digits = names.str.isdigit()
containing_plop = names.str.contains("plop") # Renvoie une série de booléens
```
----
#### Séries de type `datetime64[ns]` (dates)
Sur une série de type dates, il y a aussi des outils qui permettent d'extraire des
informations intéressantes depuis des dates; vous pouvez extraire le jour du mois,
le numéro de jour dans l'année, etc.
Le moyen d'utiliser lesdites méthodes consiste à passer par un attribut de la
série nommé `.dt`{.python} (pour **datetime**) :
```python {.numberLines}
import pandas as pd
dates = pd.Series(data=["2001-01-19", "2012-12-21", "2022-04-30", "2016-10-16", "2024-06-01"], dtype="datetime64[ns]")
days = dates.dt.day # récupérer une série avec le numéro de jour
ordinals = dates.dt.day_of_year # récupérer une série avec le numéro de jour de l'année
day_names = dates.dt.day_name(locale="") # récupérer une série avec le nom de jour dans la langue du système
```