Introduction aux structures de données pandas : les Series (tableaux unidimensionnels)
Nous allons découvrir deux structures de données de base de la bibliothèque Pandas : les Series
, les DataFrames
(éventuellement les Index
).
Commençons par importer la bibliothèque Pandas (et la bibliotèque NumPy) avec les alias classiques pd
et np
:
Un objet Series
(une série) est un tableau à une dimension capable de stocker n’importe quel type de données (entiers, chaînes de caractères, flottants, objets Python, etc.).
Les éléments d’une Series sont indexés, c’est-à-dire que un label est associé à chaque élément. Ce label est un nom ou un numéro. Par défaut, les labels sont des entiers allant de \(0\) à \(N-1\), où \(N\) est le nombre d’éléments de la Series. On peut aussi spécifier des labels personnalisés.
Init signature:
pd.Series(
data=None,
index=None,
dtype: 'Dtype | None' = None,
name=None,
copy: 'bool | None' = None,
fastpath: 'bool' = False,
) -> 'None'
Docstring:
One-dimensional ndarray with axis labels (including time series).
Labels need not be unique but must be a hashable type. The object
supports both integer- and label-based indexing and provides a host of
methods for performing operations involving the index. Statistical
methods from ndarray have been overridden to automatically exclude
missing data (currently represented as NaN).
Operations between Series (+, -, /, \*, \*\*) align values based on their
associated index values-- they need not be the same length. The result
index will be the sorted union of the two indexes.
Parameters
----------
data : array-like, Iterable, dict, or scalar value
Contains data stored in Series. If data is a dict, argument order is
maintained.
index : array-like or Index (1d)
Values must be hashable and have the same length as `data`.
Non-unique index values are allowed. Will default to
RangeIndex (0, 1, 2, ..., n) if not provided. If data is dict-like
and index is None, then the keys in the data are used as the index. If the
index is not None, the resulting Series is reindexed with the index values.
dtype : str, numpy.dtype, or ExtensionDtype, optional
Data type for the output Series. If not specified, this will be
inferred from `data`.
See the :ref:`user guide <basics.dtypes>` for more usages.
name : Hashable, default None
The name to give to the Series.
copy : bool, default False
Copy input data. Only affects Series or 1d ndarray input. See examples.
Notes
-----
Please reference the :ref:`User Guide <basics.series>` for more information.
Examples
--------
Constructing Series from a dictionary with an Index specified
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> ser = pd.Series(data=d, index=['a', 'b', 'c'])
>>> ser
a 1
b 2
c 3
dtype: int64
The keys of the dictionary match with the Index values, hence the Index
values have no effect.
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> ser = pd.Series(data=d, index=['x', 'y', 'z'])
>>> ser
x NaN
y NaN
z NaN
dtype: float64
Note that the Index is first build with the keys from the dictionary.
After this the Series is reindexed with the given Index values, hence we
get all NaN as a result.
Constructing Series from a list with `copy=False`.
>>> r = [1, 2]
>>> ser = pd.Series(r, copy=False)
>>> ser.iloc[0] = 999
>>> r
[1, 2]
>>> ser
0 999
1 2
dtype: int64
Due to input data type the Series has a `copy` of
the original data even though `copy=False`, so
the data is unchanged.
Constructing Series from a 1d ndarray with `copy=False`.
>>> r = np.array([1, 2])
>>> ser = pd.Series(r, copy=False)
>>> ser.iloc[0] = 999
>>> r
array([999, 2])
>>> ser
0 999
1 2
dtype: int64
Due to input data type the Series has a `view` on
the original data, so
the data is changed as well.
File: /usr/lib/python3/dist-packages/pandas/core/series.py
Type: type
Subclasses: SubclassedSeries
1 Création d’une Series
On peut crééer une série à partir
- d’une liste de valeurs ou d’un tableau NumPy (les labels sont alors générées automatiquement)
- deux listes : une liste (ou array NumPy) de valeurs et une liste de labels
- d’un dictionnaire
1.1 À partir d’une liste/array de valeurs
0 0.25
1 0.50
2 0.75
3 1.00
dtype: float64
On voit qu’une Series contient une séquence de valeurs et une séquence d’indices (ici générés automatiquement). Pour accéder à ces deux séquences, on peut utiliser les attributs values
et index
de la Series.
display(ma_series.values) # c'est un tableau numpy
display(ma_series.index) # c'est un objet de type Index, créé ici automatiquement
array([0.25, 0.5 , 0.75, 1. ])
RangeIndex(start=0, stop=4, step=1)
On peut accéder à un élément de la série par son index
(qui est un entier crée automatiquemnt ou passé explicitement):
1.2 À partir de deux listes/array : une liste de valeurs et une liste d’index associés
ma_serie1 = pd.Series( data=[8, 70, 320, 1200],
index=["Suisse", "France", "USA", "Chine"])
ma_serie1
Suisse 8
France 70
USA 320
Chine 1200
dtype: int64
valeurs = ma_serie1.values # c'est un tableau numpy
etiquettes = ma_serie1.index # c'est un objet de type Index
display(valeurs)
display(etiquettes)
array([ 8, 70, 320, 1200])
Index(['Suisse', 'France', 'USA', 'Chine'], dtype='object')
On peut accéder à une valeur en utilisant son label, un peu comme dans un dictionnaire.
Les objets Index
(i.e. les labels) sont immutables et ne peuvent donc pas être modifiés par l’utilisateur. On peut accéder à une label selon sa position:
Un Index
peut contenir des étiquettes dupliquées. Les sélections avec des étiquettes en double renverront toutes les occurrences de cette étiquette.
ma_serie1 = pd.Series( data=[8, 70, 320, 1200 , 2000],
index=["Suisse", "France", "USA", "Chine", "France"])
display(ma_serie1)
display(ma_serie1["France"]) # renvoie les deux valeurs associées à l'étiquette "France"
Suisse 8
France 70
USA 320
Chine 1200
France 2000
dtype: int64
France 70
France 2000
dtype: int64
1.3 À partir d’un dictionnaire { label1: valeur1, label2: valeur2, ...}
France 70
USA 320
Suisse 8
Chine 1200
dtype: int64
Attention, comme les éléments d’un dictionnaire ne sont pas ordonnés, dans l’objet Series les entrées seront ordonnées selon l’ordre d’ajout. On peut ensuite ordonner les éléments de la Series en utilisant la méthode sort_index()
.
2 Slicing \(\leadsto\) sélection avec loc
et iloc
Pour l’indexation par étiquettes des DataFrame sur les lignes, les opérateurs d’indexation spéciaux loc
et iloc
permettent de sélectionner un sous-ensemble de lignes et de colonnes d’un DataFrame avec une notation de type NumPy en utilisant soit des étiquettes d’axes (loc
), soit des entiers (iloc
).
Une serie peut être vue comme un dictionnaire ordonné ou comme une liste, ainsi on peut accéder à un élément en utilisant son label ou son indice.
Considérons le cas suivant :
a 0.25
b 0.50
c 0.75
d 1.00
dtype: float64
# Si on voit la Series comme un dictionnaire :
display(data['b']) # On peut accéder à un élément par son index
display('a' in data) # On peut tester l'existence d'une clé
display(data.keys()) # On peut récupérer les index
display(list(data.items())) # On peut récupérer les couples (index, valeur)
data['e'] = 1.25 # On peut ajouter un élément
display(data)
0.5
True
Index(['a', 'b', 'c', 'd'], dtype='object')
[('a', 0.25), ('b', 0.5), ('c', 0.75), ('d', 1.0)]
a 0.25
b 0.50
c 0.75
d 1.00
e 1.25
dtype: float64
# Si on voit la Series comme un tableau numpy/liste dont les indices peuvent être des chaînes :
display(data[1]) # On peut accéder à un élément par sa position
display(data[ ['a', 'e'] ]) # les éléments dont les indices sont 'a' et 'e'
display(data[(data > 0.3) & (data < 0.8)]) # masque : `&` pour le `and`, `|` pour le `or` et `!` pour le `not` ; parenthèses obligatoires
/tmp/ipykernel_17043/3297636479.py:3: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`
display(data[1]) # On peut accéder à un élément par sa position
0.5
a 0.25
e 1.25
dtype: float64
b 0.50
c 0.75
dtype: float64
Cette double vision des indices peut être source de confusion avec le slicing :
- si on utilise des indices automatiques, le slicing se fait comme dans Numpy (premier inclus, dernier exclu),
- si on utilise comme indices des labels, le dernier est aussi inclus.
display(data)
display(data[0:2]) # slicing explicite donc on prend les éléments de 0 inclus à 2 exclus
display(data['a':'c']) # slicing implicite donc on prend les éléments de 'a' à 'c' inclus
a 0.25
b 0.50
c 0.75
d 1.00
e 1.25
dtype: float64
a 0.25
b 0.50
dtype: float64
a 0.25
b 0.50
c 0.75
dtype: float64
Pire, si les indices sont des entiers, on ne sait pas si pandas interprètes ces nombre comme des indices ou des labels pour le slicing :
1000 0.25
1100 0.50
1200 0.75
1300 1.00
dtype: float64
display(data2[0:2]) # slicing explicite donc on prend les éléments de la poisition 0 incluse à la position 2 excluse
display(data2[1000:1200]) # slicing qui semble implicite, mais comme les indices sont des entiers, il agit comme un slicing explicite donc on prend les éléments de 1000 inclus à 1200 exclu
1000 0.25
1100 0.50
dtype: float64
Series([], dtype: float64)
La solution : loc
et iloc
:
loc
permet d’accéder aux éléments en utilisant les labels,iloc
permet d’accéder aux éléments en utilisant les indices numériques.
3 Ajouter des éléments à une Series / Fusionner deux Series
Pour ajouter un élément à une Series, on peut utiliser la méthode concat()
:
ma_serie1 = pd.Series( data=[8, 70, 320, 1200],
index=["Suisse", "France", "USA", "Chine"] )
ma_serie2 = pd.Series( data=[1,4,3,10],
index=["France", "USA", "Suisse", "Italie"] )
ma_serie3 = pd.concat([ma_serie1, ma_serie2])
ma_serie3
Suisse 8
France 70
USA 320
Chine 1200
France 1
USA 4
Suisse 3
Italie 10
dtype: int64
On note que certaines labels sont dupliquées, ce qui est autorisé dans une Series.
4 Supprimer des éléments à une Series
Pour supprimer un élément d’une Series, on peut utiliser la méthode drop()
et spécifier l’index de l’élément à supprimer.
Par default la méthode drop ne modifie pas la Series, mais renvoie une nouvelle Series sans l’élément supprimé. Pour modifier la Series, on peut utiliser l’argument inplace=True
.