M55 - Data Science & Scientific Computing 5
  1.   Cours
  2. Series
  • M55 - Data Science and Scientific Computing 5

  • Syllabus

  •   Cours
    • Introduction
    • Series
    • Data Frames
    • Une analyse complète : le Titanic

  •   Exercices
    • Premières manipulations
    • Notes L3 Math
    • Population France
    • Netflix User
    • Presidentielles USA 2016

  •   Fichiers sources

Contenu de la page

  • 1 Création d’une Series
    • 1.1 À partir d’une liste/array de valeurs
    • 1.2 À partir de deux listes/array : une liste de valeurs et une liste d’index associés
    • 1.3 À partir d’un dictionnaire { label1: valeur1, label2: valeur2, ...}
  • 2 Slicing \(\leadsto\) sélection avec loc et iloc
  • 3 Ajouter des éléments à une Series / Fusionner deux Series
  • 4 Supprimer des éléments à une Series
  1.   Cours
  2. Series

Introduction aux structures de données pandas : les Series (tableaux unidimensionnels)

Auteur·rice

Gloria FACCANONI

Date de publication

24 novembre 2024

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 :

import pandas as pd
import numpy as 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.

?pd.Series
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

ma_series = pd.Series( data=[0.25, 0.5, 0.75, 1.0] )
ma_series
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):

ma_series[1] 
0.5

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.

ma_serie1["France"]
70

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:

etiquettes[1]
'France'

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, ...}

ma_serie2 = pd.Series( { "France" : 70, "USA" : 320, "Suisse" : 8,"Chine" : 1200})
ma_serie2
France      70
USA        320
Suisse       8
Chine     1200
dtype: int64
ma_serie2["France"]
70

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().

ma_serie2.sort_index()
Chine     1200
France      70
Suisse       8
USA        320
dtype: int64

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 :

data = pd.Series(  data  = [0.25, 0.5, 0.75, 1.0], 
                   index = ['a' , 'b', 'c' , 'd'])
data
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 :

data2 = pd.Series( data=[0.25, 0.5, 0.75, 1.0], 
                   index=[1000, 1100, 1200, 1300] )
data2
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.
data2.loc[1100] # la valeur de l'élément de label 1100
0.5
data2.iloc[1] # la valeur de l'élément d'indice 1
0.5
data2.iloc[:3] # les 3 premiers éléments, donc les éléments d'indice 0, 1 et 2
1000    0.25
1100    0.50
1200    0.75
dtype: float64

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.

ma_serie3.drop("France")
Suisse       8
USA        320
Chine     1200
USA          4
Suisse       3
Italie      10
dtype: int64

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.

ma_serie3
Suisse       8
France      70
USA        320
Chine     1200
France       1
USA          4
Suisse       3
Italie      10
dtype: int64
ma_serie3.drop("France", inplace=True)
ma_serie3
Suisse       8
USA        320
Chine     1200
USA          4
Suisse       3
Italie      10
dtype: int64
Retour au sommet
Introduction
Data Frames

© Copyright 2024, Gloria Faccanoni

 

This page is built with ❤️ and Quarto.