Séries Pandas

Cet article fait partie d’une série d’articles sur la syntaxe de base Python.

Une série pandas est une liste mutable d’objets dont les index peuvent être personnalisés. Le type des objets n’est pas forcément le même.

Les séries pandas permettent de stocker tout type d’objets. L’intérêt de cette structure est l’utilisation d’index personnalisables permettant un accès performant aux objets. Les séries pandas ne permettent de stocker des objets que suivant une dimension. Pour stocker suivant 2 dimensions, il faut utiliser des dataframes.

Pandas peut être importé de cette façon pour utiliser les objets dans la bibliothèque:

import pandas as pd

Les séries pandas sont mutables c’est-à-dire qu’on peut modifier la valeur des éléments après instanciation.

Initialisation

On peut initialiser une série pandas à partir d’un tableau Python ou d’un tableau numpy.

Par exemple, à partir d’un tableau Python:

>>> a = pd.Series([1, 4, 5, 8])
>>> a
0    1
1    4
2    5
3    8
dtype: int64

On peut voir les valeurs ainsi que les index correspondant. Comme les index n’ont pas été précisés à l’initialisation, ce sont des index par défaut qui sont utilisés.

Pour initialiser une série pandas à partir d’un tableau numpy:

>>> a = np.array([1, 4, 5, 8])
>>> b = pd.Series(a)

Sans précision sur le type des éléments, pandas déduit le type des objets dans le cas où les objets ont le même type et sont de type float, int et bool sinon c’est le type object qui sera affecté:

>>> a = np.array(['1', '4', '5', '8'])
>>> a
0    1
1    4
2    5
3    8
dtype: object

De même si les types des éléments sont différents alors le type affecté sera object.

Indiquer explicitement le type des valeurs (argument dtype)

Pandas reconnait les types numpy donc la même syntaxe que numpy peut être utilisée, par exemple:

>>> a = pd.Series([1, 4, 5, 8], dtype='i8')
>>> a
0    1
1    4
2    5
3    8
dtype: int64

La syntaxe plus haut est équivalente à:

>>> a = pd.Series([1, 4, 5, 8], dtype=np.int8)

A condition d’avoir importé la bibliothèque numpy avec:

import numpy as np

Initialiser sans effectuer de copies (argument copy)

Par défaut, quand une série pandas est initialisée à partir d’un tableau Python ou numpy, une copie des éléments est effectuée. Il est possible d’effectuer une initialisation de la série en utilisant des références vers les objets de la structure d’origine avec l’argument copy:

>>> a = np.array([1, 4, 5, 8])
>>> b = pd.Series(a, copy=False)
>>> a
array([1, 4, 5, 8])
>>> b[2]=1000
>>> a
array([   1,    4, 1000,    8])

L’initialisation de la série pandas étant faite avec des références, si on modifie une valeur dans la série alors les éléments dans la structure d’origine sont aussi modifiés.

Indiquer explicitement des index (argument index)

Par défaut, les index des éléments sont des entiers à partir de 0. Avec l’argument index, on peut explicitement préciser des index. Le type indiqué de l’objet doit être un tableau de même taille que la liste des valeurs:

>>> i = range(4, 8)
>>> list(i)
[4, 5, 6, 7]
>>> a = pd.Series([1, 4, 5, 8], index=i)
>>> a
4    1
5    4
6    5
7    8
dtype: int64

Pour créer plus directement une série:

>>> a = pd.Series([1, 4, 5, 8], range(4, 8))

Si la série contient la même valeur:

>>> a = pd.Series(5, range(4))
>>> a
0    5
1    5
2    5
3    5
dtype: int64

On peut affecter un index particulier après initialisation avec la propriété <série>.index. Il faut que la taille du tableau de l’index soit la même que celle de la série.

Par exemple:

>>> a = pd.Series([1, 4, 5, 8])
>>> a.index = ['a', 'b', 'c', 'd']
>>> a
a    1
b    4
c    5
d    8
dtype: int64

Accéder à une valeur à partir de l’index

Pour atteindre une valeur particulière, il suffit d’utiliser l’index:

>>> a = pd.Series([1, 4, 5, 8])
>>> a[2]
5

Si l’index n’existe pas, une exception est levée:

>>> a[5]
KeyError: 5

Si on considère la série suivante:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> a
a    1
b    4
c    5
d    8
dtype: int64

Il existe d’autres méthodes pour obtenir une valeur dans une série:

  • <série>.at[<index>]: par exemple
    >>> a.at['c']
    5
    

    Une exception est levée si l’index n’existe pas.

  • <série>.loc[<index>]:
    >>> a.loc['c']
    5
    

    Une exception est levée si l’index n’existe pas.

  • <série>.get(<index>):
    >>> a.get('c')
    5
    

    Si l’index n’existe pas, None est renvoyé.

Ces 3 syntaxes sont équivalentes.

Même si un index personnalisé est utilisé (différent d’un entier à partir de 0), on peut accéder aux valeurs en utilisant l’index par défaut avec les syntaxes:

  • <série>[<index numérique>]:
    >>> a[1]
    4
    

    Une exception est levée si l’index n’existe pas.

  • <série>.iat[<index numérique>]:
    >>> a.iat[1]
    4
    

    Une exception est levée si l’index n’existe pas.

  • <série>.iloc[<index numérique>]:
    >>> a.iat[1]
    4
    

    Une exception est levée si l’index n’existe pas.

  • <série>.get(<index numérique>):
    >>> a.get(1)
    4
    

    Si l’index n’existe pas, None est renvoyé.

Les syntaxes <série>[<index numérique>] et <série>.get(<index numérique>) sont sources d’ambiguïtés car:

  • Si l’index existe alors elles renvoient la valeur correspondant à l’index sinon
  • Si l’index n’existe pas, elles renvoient la valeur correspondant à l’index numérique.

Si on utilise ces syntaxes, il faut donc s’assurer du type d’index qu’on manipule.

Les autres syntaxes ne sont pas concernées par ces problèmes d’ambiguïté.

Par exemple, si on considère les séries suivantes:

>>> i1 = list(range(3, -1, -1))
>>> a = pd.Series([1, 4, 5, 8], index=i1)
>>> a
3    1
2    4
1    5
0    8
dtype: int64
>>> b = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> b
a    1
b    4
c    5
d    8
dtype: int64

>>> a[0]
8 

0 existe en tant qu’index, la valeur renvoyée est la dernière valeur de la série.

>>> b[0]
1

0 n’existe pas en tant qu’index donc la valeur renvoyée correspond à l’index numérique 0.

Sous-série et slicing

On peut extraire des sous-séries à partir d’une série existante en indiquant explicitement les index numériques à extraire ou en utilisant la syntaxe de slicing.

Par exemple, si on considère la série suivante:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> a
a    1
b    4
c    5
d    8
dtype: int64

Pour extraire une sous-série en indiquant explicitement les index numériques à extraire:

>>> b = a[['b', 'd', 'c']]
>>> b 
b    4
d    8
c    5
dtype: int64

On peut utiliser les règles de slicing habituelles en utilisant les index numériques, par exemple:

>>> c =  a[1:3]
>>> c
b    4
c    5
dtype: int64

Enfin, iloc[] peut être utilisé pour extraire la sous-série en utilisant les index numériques. iloc[] évite les ambiguïtés décrites plus haut puisqu’il ne traite que les index numériques:

>>> a.iloc[1:3]
b    4
c    5
dtype: int64
>>> a.iloc[[1,3]]
b    4
d    8
dtype: int64

Suivant la façon dont la sous-série est extraite, il peut s’agir d’une copie ou d’une référence vers la série d’origine. Dans le cas de références, les modifications dans la sous-série entraînent des modifications dans la série d’origine:

  • Si on indique explicitement les index de la série, la sous-série est une copie.
  • Si on utilise la syntaxe de slicing sur les index numériques, la sous-série contient des références.

Par exemple, si on considère la série suivante:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> a
a    1
b    4
c    5
d    8
dtype: int64

Si on extrait une sous-série en indiquant explicitement les index de la série d’origine:

>>> b = a[['a', 'b']]
>>> b[0] = 1000
>>> b
a    1000
b       4
dtype: int64

>>> a
a    1
b    4
c    5
d    8
dtype: int64

La série d’origine n’est pas modifiée.

Si on effectue un slicing avec les index numériques:

>>> c = a[1:3] 
>>> c[0] = 1000
>>> c
b    1000
c       5
dtype: int64
>>> a
a       1
b    1000
c       5
d       8
dtype: int64

La série d’origine est modifiée.

Pour éviter de modifier la structure d’origine, on peut effectuer une copie avec copy():

c = a[1:3].copy()

Changement de type

Il est possible de changer le type des éléments d’une série en appliquant la fonction <série>.astype(<nouveau type>). La série résultante contient les mêmes index que la série d’origine. Pour indiquer le type, on peut utiliser la même syntaxe qu’à l’initialisation.

Par exemple:

>>> a = pd.Series(['5', '4', '3', '2', '1']) 
>>> a.astype(float)
0    5.0
1    4.0
2    3.0
3    2.0
4    1.0
dtype: float64

Par défaut, si le changement de type n’est pas possible, une erreur est renvoyée:

>>> a = pd.Series(['5', '4', '3', np.NaN, 'Oups', '1'])
>>> a.astype(float)
ValueError: could not convert string to float: 'Oups'

Cette erreur peut être ignorée en faisant:

>>> a.astype(float, errors='ignore')
0       5
1       4
2       3
3     NaN
4    Oups
5       1
dtype: object

En cas d’erreur, l’objet original est renvoyé.

Tester l’existence d’un index et d’une valeur

Tester l’existence d’un index

L’opérateur in peut être utilisé pour tester l’existence d’un index dans une série pandas, par exemple:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> 'b' in a
True
>>> 'e' in a 
False

Si on utilise in directement sur une série, on teste l’existence d’un index dans la série et non l’appartenance de la valeur aux valeurs de la série.

Tester l’existence d’une valeur

Pour tester l’appartenance d’une valeur aux valeurs de la série, il faut utiliser in avec <série>.values:

>>> 4 in a.values
True
>>> 7 in a.values
False

Itération sur les éléments de la structure

On peut itérer directement parmi les valeurs d’une série avec une boucle “for“:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> for item in a:
    print(item)
1
4
5
8

La fonction <série>.iteritems() peut être utilisée pour obtenir un itérable contenant pour chaque élément son index et sa valeur, par exemple:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> for item in a.iteritems():
    print('Index: %s - valeur: %d' % item)
Index: a - valeur: 1
Index: b - valeur: 4
Index: c - valeur: 5
Index: d - valeur: 8

Opérations sur les séries pandas

Des opérations mathématiques peuvent être appliquées directement sur des séries pandas.

Par exemple, si on considère la série:

>>> a = pd.Series([1, 4, 5, 8])
>>> a
0    1
1    4
2    5
3    8
dtype: int64
>>> 2*a
0     2
1     8
2    10
3    16
dtype: int64

On peut aussi appliquer des opérations entre 2 séries pandas mais contrairement aux tableaux numpy, il n’est pas obligatoire que les 2 séries soient de même dimension. Toutefois, il faut que les types des éléments des séries permettent l’application de l’opération.

Par exemple si on considère les séries suivantes:

>>> a = pd.Series([1, 4, 5, 8])
>>> b = pd.Series([5, 4, 3, 2])
>>> a+b
0     6
1     8
2     8
3    10
dtype: int64

L’opération est appliquée sur tous les éléments des séries en préservant le type de ces derniers.

Si le type n’est pas le même, il peut être modifié pour rendre l’opération possible, par exemple:

>>> a = pd.Series([1, 4, 5, 8])
>>> b = pd.Series([5.0, 4.0, 3.0, 2.0])
>>> a+b
0    5.0
1    4.0
2    3.0
3    2.0
dtype: float64

a est une série contenant des entiers et b contient des flottants, en appliquant l’opération les éléments de a sont transformés en flottants pour rendre l’opération possible. Le résultat est une série de flottants.

La modification du type n’est pas tout le temps possible, par exemple si on considère une série de chaînes de caractères:

>>> c = pd.Series(['5', '4', '3', '2'])
>>> a+c
TypeError: unsupported operand type(s) for +: 'int' and 'str'

En revanche, si les éléments de 2 séries sont des chaînes de caractères alors l’opération est possible, le résultat est la concaténation des chaînes:

>>> d = pd.Series(['1', '4', '5', '8'])
>>> c+d
0    51
1    44
2    35
3    28
dtype: object

Quand on applique l’opération *, tous les éléments des séries sont multipliés:

>>> a = pd.Series([1, 4, 5, 8])
>>> b = pd.Series([5, 4, 3, 2])
>>> a*b
0     5
1    16
2    15
3    16
dtype: int64

Si les tailles des séries ne sont pas les mêmes

Si les tailles des séries ne sont pas identiques, l’opération est quand même appliquée toutefois quand il n’existe pas d’éléments dans une série permettant l’opération, la valeur résultante est NaN:

>>> a = pd.Series([1, 4, 5, 8])
>>> b = pd.Series([5, 4, 3, 2, 1])
>>> a+b
0     6.0
1     8.0
2     8.0
3    10.0
4     NaN
dtype: float64

Dans ce cas, la 5e valeur est NaN et les valeurs sont transformées en flottants à cause de la valeur manquante dans a.

Si les index ne sont pas les mêmes

Dans le cas où les index des séries ne sont pas les mêmes:

  • Pour les index communs: l’opération est appliquée.
  • Pour les index qui ne sont pas communs: le résultat de l’opération est NaN.

Par exemple:

>>> a = pd.Series([1, 4, 5, 8], ['a', 'b', 'c', 'd'])
>>> b = pd.Series([5, 4, 3, 2], ['a', 'e', 'c', 'f'])
>>> a+b
a    6.0
b    NaN
c    8.0
d    NaN
e    NaN
f    NaN
dtype: float64

La fonction dropna() peut être utilisée pour supprimer les valeurs NaN:

>>> c = a+b
>>> c.dropna()
a    6.0
c    8.0
dtype: float64

Pour résumer, on peut appliquer les opérations comme:

  • +, -, / ou *. Ces opérations sont appliquées sur les éléments des tableaux avec le même index. Pour les éléments dont les index ne sont pas les mêmes, le résultat est NaN. On peut s’aider de dropna() pour supprimer les éléments dont la valeur est NaN.
  • Les opérations booléennes entre séries pandas peuvent être effectuées en utilisant:
    • & pour “and“,
    • | pour “ou“,
    • ~ pour “not“,
    • ^ pour le “ou exclusif“.
  • Appliquer des opérateurs de comparaison comme ==, <, >, <=, >= et !=.

Fonctions particulières

Quelques fonctions utiles

On peut appliquer les fonctions mathématiques numpy à une série pandas. Le résultat est une série.

Par exemple si on importe numpy avec import numpy as np:

>>> a = pd.Series([1, 4, 5, 8])
>>> np.log(a)
0    0.000000
1    1.386294
2    1.609438
3    2.079442
dtype: float64

De la même façon, les fonctions suivants peuvent être appliquées:

  • np.add(a, b); np.subtract(a, b); np.divide(a, b) ou np.multiply(a, b) pour respectivement ajouter, soustraire, diviser ou multiplier les éléments de séries pandas.
  • np.sum(a) ou np.prod(a) pour respectivement ajouter ou multiplier tous les éléments d’une série.
  • np.floor(a), np.ceil(a) ou np.trunc(a) pour effectuer des arrondis ou troncatures sur les éléments d’une série.
  • np.amin(a), np.amax(a) pour obtenir le minimum ou maximum parmi les éléments de la série.
  • np.argmin(a), np.argmax(a) pour obtenir l’index du minimum ou du maximum des éléments de la série.
  • np.mean() pour obtenir la moyenne des éléments de la série.

Une liste plus exhaustive des opérations numpy possibles peut être retrouvée sur: numpy.org/doc/stable/reference/routines.math.html.

D’autres fonctions permettent d’éviter d’itérer sur les éléments de la série:

  • <série>.index permet d’obtenir un itérable (de type RangeIndex ou Index) contenant les index de la série.
  • <série>.values pour obtenir un tableau numpy contenant les valeurs de la série.
  • <série>.unique pour obtenir un tableau numpy contenant les valeurs uniques de la série.
  • <série>.value_counts() permet d’obtenir une série avec les mêmes index que la série d’origine et le nombre d’occurence pour chaque valeur.
  • <série>.isna() ou <série>.isnull() renvoie une série avec les mêmes index que la série d’origine et des booléens pour indiquer si les valeurs correspondantes sont NaN.
  • <série>.inotna() ou <série>.notnull() renvoie une série avec les mêmes index que la série d’origine et des booléens pour indiquer si les valeurs correspondantes ne sont pas égales à NaN.
  • pd.isnull(<série>) renvoie une série dont les index sont les mêmes que la série d’origine et dont les valeurs sont True si les valeurs correspondantes sont égales à NaN ou None.
  • pd.notnull(<série>) renvoie une série dont les index sont les mêmes que la série d’origine et dont les valeurs sont True si les valeurs correspondantes ne sont pas égales à NaN ou None.
  • <série>.min(), <série>.max(), <série>.mean(), <série>.median() pour respectivement renvoyer le minimum, maximum, la moyenne et la moyenne médiane des valeurs de la série.
  • <série>.all() indique si toutes les valeurs de la série sont égales à True au sens Truthy/Falsy (voir Truthy vs Falsy).
  • <série>.any() indique si au moins une valeur de la série est égale à True au sens Truthy/Falsy.
  • <série>.sort_index() renvoie une série avec les index ordonnés.
  • <série>.sort_values() renvoie une série avec les valeurs ordonnées.
  • <série>.apply(<fonction>) renvoie une série où la fonction est exécutée pour toutes les valeurs.

    Par exemple avec une lambda:

    >>> a = pd.Series([1, 4, 5, 8])
    >>> a.apply(lambda x: 3 * x)
    a     3
    b    12
    c    15
    d    24
    dtype: int64
    
  • <série>.to_frame() renvoie un dataframe avec une seule colonne contenant les valeurs de la série en ligne.

<série>.str

L’objet <série>.str permet d’appliquer des traitements sur les éléments d’une série lorsque ce sont des chaînes de caractères.

Par exemple:

  • <série>.str.startswith(<chaine de caractères>): renvoie une série dont les index sont les mêmes que la série d’origine et dont les valeurs contiennent True si la chaine de caractères correspondante commence par la chaîne donnée.
  • <série>.str.len() renvoie une série dont les index sont les mêmes que la série d’origine et dont les valeurs sont les longueurs des chaînes de caractères correspondantes.
  • <série>.str.match(<regex>) renvoie une série dont les valeurs sont True si la valeur correspondante dans la série d’origine satisfait la regex donnée.
  • <série>.str.contains(<regex>) renvoie une série dont les valeurs sont True si la valeur correspondante dans la série d’origine contient une sous-chaine satisfaisant la regex donnée.
  • <série>.str.contains(<chaîne de caractères>, regex=False) renvoie une série dont les valeurs sont True si la valeur correspondante dans la série d’origine contient une sous-chaine donnée.
  • <série>.str.find(<chaine>) renvoie une série dont les valeurs sont les index dans la chaîne de caractère de la 1ère occurence de la chaine donnée. Si la chaîne ne contient pas la chaine donnée, la valeur retournée est -1.
  • <série>.str.get(<index>) renvoie une série dont les valeurs contiennent le caractère correspondant à l’index donnée dans la chaîne de caractères correspondante.
  • <série>.str.slice(<index début>, <nombre de caractères>) renvoie une série dont les valeurs sont des sous-chaînes de la chaine correspondante dans la série d’origine.
  • <série>.str[<argument slicing>] renvoie une série dont les valeurs proviennent d’un slicing appliquée sur la chaîne correspondante dans la série d’origine.
  • <série>.str.count(<regex>) renvoie une série dont les valeurs contiennent le nombre d’ocurrences de la regex dans la chaîne correspondante dans la série d’origine.
  • <série>.str.replace(<regex>, <chaine de remplacement>) renvoie une série dont les valeurs contiennent un remplacement des chaines d’origine suivant la regex.

Leave a Reply