Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Cette section vise à vous donner de nouvelles compétences en Python pour manipuler des données structurées et tabulaires.

Objectifs d’apprentissage :

  • Manipuler des tables de données (data frames) : les décrire, les filtrer…
  • Découvrir les fonctions lambda
  • Introduction aux objets datetime
  • Tracer des données issues de tables de données (histogrammes et cartes)
  • Introduction à Plotly
  • Introduction à CSV & Parquet

Ce tutoriel peut être donné en un cours de 2 heures. Les sections sont étiquetées Niveau 1, 2, 3 dans les sections 1, 2, 3, et les enseignants peuvent réserver les niveaux supérieurs à un apprentissage asynchrone, en autonomie.

Nous travaillerons sur plusieurs jeux de données structurés : métadonnées de capteurs, produit de données sismologiques (catalogue de séismes).

Importons d’abord tous les modules nécessaires :

🖥️ Diapositives du cours — Séance 05 (ven. 9 oct.)

import io
import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pooch
import requests

os.makedirs('data', exist_ok=True)

Ce carnet (notebook) utilise plotly pour produire des tracés interactifs

import plotly.express as px
import plotly.io as pio
# pio.renderers.default = 'vscode' # writes as standalone html, 
# pio.renderers.default = 'iframe' # writes files as standalone html, 
# pio.renderers.default = 'png' # writes files as standalone png, 
# try notebook, jupyterlab, png, vscode, iframe

1 Fondamentaux de Pandas

1.1 Bases

Les objets centraux de Pandas sont Series et DataFrame. Une Series est un tableau étiqueté unidimensionnel. Le DataFrame est une table bidimensionnelle dont les colonnes sont des Series.

Nous pouvons créer de toutes pièces un DataFrame composé de séries à partir d’un dictionnaire Python :

data = {
    'temperature' : [36,37,30,50],
    'precipitation':[3,1,0,0]
}
my_pd = pd.DataFrame(data)
print(my_pd)
   temperature  precipitation
0           36              3
1           37              1
2           30              0
3           50              0

Chaque élément (clé, valeur) du DataFrame correspond à une valeur de data. Pour obtenir les clés du DataFrame, tapez :

my_pd.keys()
Index(['temperature', 'precipitation'], dtype='str')

obtenir une Series donnée (différente du tableau)

print(my_pd.temperature[:])
print(type(my_pd.temperature[:]))
0    36
1    37
2    30
3    50
Name: temperature, dtype: int64
<class 'pandas.Series'>

pour obtenir la valeur d’une clé donnée (la température, par exemple), à un indice donné (2, par exemple), tapez :

print(my_pd.temperature[2])
print(type(my_pd.temperature[2]))
30
<class 'numpy.int64'>

1.2 Lire un DataFrame depuis un fichier CSV

Nous pouvons lire un DataFrame pandas directement depuis un fichier standard. Nous téléchargerons un catalogue de séismes avec pooch, qui met le fichier en cache dans le dossier local data/.

ff = pooch.retrieve(
    url="https://raw.githubusercontent.com/UW-MLGEO/MLGeo-dataset/refs/heads/main/data/Global_Quakes_IRIS.csv",
    known_hash=None,
    fname="Global_Quakes_IRIS.csv",
    path="./data",
)
quake = pd.read_csv(ff)

Utilisez maintenant la fonction head pour afficher le contenu du fichier

# enter answer here
quake.head()
Loading...

Affichez la profondeur en utilisant l’objet pandas de deux manières

print(quake.depth)
print(quake['depth'])
0        34400.0
1        30100.0
2        16900.0
3       109400.0
4        25700.0
          ...   
1780     10000.0
1781    105000.0
1782    608510.0
1783     10000.0
1784     35440.0
Name: depth, Length: 1785, dtype: float64
0        34400.0
1        30100.0
2        16900.0
3       109400.0
4        25700.0
          ...   
1780     10000.0
1781    105000.0
1782    608510.0
1783     10000.0
1784     35440.0
Name: depth, Length: 1785, dtype: float64

Calculez des statistiques de base des données avec la fonction describe.

quake.describe()
Loading...

Calculez la moyenne et la médiane d’une Series donnée, par exemple la profondeur.

# answer it here
print(quake.depth.mean())
print(quake.depth.median())
82773.18767507003
24400.0

1.3 Manipuler Pandas avec Python

Fonctions classiques

Nous allons maintenant nous exercer à modifier le contenu du DataFrame à l’aide de fonctions. Prenons l’exemple où nous voulons convertir les valeurs de profondeur de mètres en kilomètres. Nous pouvons d’abord définir cette opération comme une fonction

# this function converts a value in meters to a value in kilometers
m2km = 1000 # this is defined as a global variable
def meters2kilometers(x):
    return x/m2km

# now test it using the first element of the quake DataFrame
meters2kilometers(quake.depth[0])
np.float64(34.4)
# this function converts a value in meters to a value in kilometers
def meters2kilometers2(x):
    m2km = 1000 # this is defined as a global variable
    return x/m2km

Définissons une autre fonction qui utilise une variable locale plutôt que globale

# Apply the meters2kilometers2 function to the 'depth' column and add it as a new column 'depth_km' to the quake DataFrame
quake['depth_km'] = quake['depth'].apply(meters2kilometers2)

# Display the first few rows to verify the new column
quake.head()
Loading...

Fonctions lambda

Nous abordons maintenant les fonctions lambda.

  • Les fonctions lambda sont de petites fonctions anonymes en Python.
  • Elles servent à des opérations rapides et simples, sans qu’il faille définir une fonction complète avec def.
  • En géosciences, où l’on traite de grands jeux de données (données climatiques, mesures sismiques…), les fonctions lambda permettent de traiter les données efficacement.
  • Dans Pandas, les fonctions lambda permettent de transformer, filtrer ou traiter rapidement ces données avec un minimum de code.
  • Lisez aussi les tutoriels de RealPython (en anglais).
lambda x: x * 1.2
<function __main__.<lambda>(x)>

Cette fonction lambda multiplie toute entrée x par 1,2, ce qui peut servir à des tâches comme des conversions d’unités (de km en m, par exemple).

Voici un exemple d’utilisation de lambda dans un DataFrame pandas. Exemple 1 : La fonction lambda x: (x - 32) * 5 / 9 convertit des degrés Fahrenheit en degrés Celsius pour chaque valeur de la colonne Temperature_F.

import pandas as pd

# Sample DataFrame
data = {'Temperature_F': [32, 50, 77, 100]}
df = pd.DataFrame(data)

# Convert Fahrenheit to Celsius
df['Temperature_C'] = df['Temperature_F'].apply(lambda x: (x - 32) * 5 / 9)

print(df)
   Temperature_F  Temperature_C
0             32       0.000000
1             50      10.000000
2             77      25.000000
3            100      37.777778
# Now the equivalent in lambda is:
lambda_meters2kilometers = lambda x:x/1000
# x is the variable

Appliquez maintenant la fonction à la série entière

# apply it to the entire series
lambda_meters2kilometers(quake.depth)
0 34.40 1 30.10 2 16.90 3 109.40 4 25.70 ... 1780 10.00 1781 105.00 1782 608.51 1783 10.00 1784 35.44 Name: depth, Length: 1785, dtype: float64

Les fonctions lambda peuvent prendre plusieurs entrées

# you can add several variables into lambda functions
remove_anything = lambda x,y:x-y
remove_anything(3,2)
1

Cela n’a pas modifié les valeurs du DataFrame, vérifiez-le :

quake.depth
0 34400.0 1 30100.0 2 16900.0 3 109400.0 4 25700.0 ... 1780 10000.0 1781 105000.0 1782 608510.0 1783 10000.0 1784 35440.0 Name: depth, Length: 1785, dtype: float64

Vous pourriez à la place écraser quake.depth=X. Essayez les deux approches, mais ne le faites qu’une seule fois ! Vous pouvez utiliser les fonctions Python map (une fonction pour appliquer des fonctions, générique en Python) et apply (une fonction pour appliquer des fonctions, propre à Pandas).

Essayez map pour appliquer une fonction lambda qui convertit la profondeur de mètres en kilomètres.

#type answer below
quake.depth=quake.depth.map(lambda x:x/1000)

À discuter en classe : qu’est-il arrivé au champ de profondeur ?

Qu’est-il arrivé au DataFrame d’origine ?

Essayez apply pour appliquer une fonction lambda qui convertit la profondeur de mètres en kilomètres. Ici, nous affichons seulement le résultat sans l’assigner, car la cellule précédente a déjà converti la colonne en kilomètres.

# or like this (display only, the column is already in km)
quake.depth.apply(lambda x:x/1000)
0 0.03440 1 0.03010 2 0.01690 3 0.10940 4 0.02570 ... 1780 0.01000 1781 0.10500 1782 0.60851 1783 0.01000 1784 0.03544 Name: depth, Length: 1785, dtype: float64

Tracez un histogramme des distributions de profondeur avec la fonction matplotlib hist.

# answer here
plt.hist(quake.depth,100)
plt.grid(True)
plt.xlabel('Quake depth (km)')
plt.show()
<Figure size 640x480 with 1 Axes>

Vous pouvez utiliser le paquet de visualisation interactive Plotly. Nous montrerons d’abord un histogramme de la profondeur des événements avec la fonction histogram.

fig = px.histogram(quake,   #specify what dataframe to use
             x="depth",  #specify the variable for the histogram 
             nbins=50,       #number of bins for the histogram 
             height=400,     #dimensions of the figure
             width=600);
fig.show()
Loading...

Exemple 2 : logique conditionnelle sur la magnitude des séismes

Vous pouvez utiliser des fonctions lambda pour ajouter de la logique conditionnelle, par exemple pour classer les magnitudes de séismes en catégories.

Scénario : vous avez une colonne Magnitude contenant des magnitudes d’événements sismiques, et vous voulez classer les événements en « Minor » (mineur), « Moderate » (modéré) ou « Severe » (sévère).

# Classify earthquake magnitudes
quake['Category'] = quake['magnitude'].apply(lambda x: 'Minor' if x < 4.0 else ('Moderate' if x < 6.5 else 'Severe'))
quake.head()
Loading...

Nous allons maintenant faire un nouveau tracé de la position des séismes, avec l’outil Plotly.

La taille des marqueurs sera proportionnée à la magnitude du séisme. Pour cela, nous ajoutons une série marker_size au DataFrame

quake['marker_size'] = quake['magnitude'].apply(lambda x: np.trunc(np.exp(x))) # add marker size as exp(mag)
quake['magnitude_bin'] = quake['magnitude'].apply(lambda x: 0.5*np.trunc(2*x)) # bin magnitude in 0.5 steps
# another way to do it
quake['marker_size'] = np.trunc(np.exp(quake['magnitude'])) # add marker size as exp(mag)
quake['magnitude_bin'] = 0.5*np.trunc(2*quake['magnitude']) # bin magnitude in 0.5 steps

1.4 Manipulation intermédiaire avec Pandas

Vous pouvez aussi appliquer des fonctions lambda sur plusieurs colonnes, ce qui est courant dans les jeux de données géoscientifiques, où l’on manipule souvent des données spatiales ou temporelles.

Exemple 3 : calculer un indice à partir de plusieurs mesures Scénario : vous avez des données de précipitations (Rainfall_mm) et d’évaporation (Evaporation_mm), et vous voulez calculer le bilan hydrique net (Net Water Balance) pour chaque enregistrement.

data = {'Rainfall_mm': [100, 80, 120], 'Evaporation_mm': [60, 70, 65]}
df = pd.DataFrame(data)

# Calculate Net Water Balance
df['Net_Water_Balance'] = df.apply(lambda row: row['Rainfall_mm'] - row['Evaporation_mm'], axis=1)

print(df)
   Rainfall_mm  Evaporation_mm  Net_Water_Balance
0          100              60                 40
1           80              70                 10
2          120              65                 55

Dans cet exemple, lambda row: row['Rainfall_mm'] - row['Evaporation_mm'] calcule le bilan hydrique net en soustrayant l’évaporation des précipitations pour chaque enregistrement.

1.5 Avancé : manipuler des séries temporelles avec des fonctions lambda

Les géoscientifiques travaillent fréquemment avec des séries temporelles (données climatiques, par exemple). Les fonctions lambda permettent des transformations de données efficaces au sein des séries temporelles.

Exemple 4 : appliquer un calcul en fenêtre glissante Scénario : supposons que vous disposez de températures journalières et que vous voulez calculer une moyenne glissante sur 3 jours.

# Sample daily temperature data
data = {'Date': pd.date_range(start='2023-09-01', periods=10, freq='D'),
        'Temperature_C': [20, 22, 23, 21, 19, 24, 25, 26, 22, 20]}
df = pd.DataFrame(data)

# Calculate 3-day rolling average using lambda
df['7_day_avg'] = df['Temperature_C'].rolling(window=3).apply(lambda x: x.mean())
df.head()
Loading...

Ici, lambda x: x.mean() calcule la moyenne glissante sur une fenêtre de 3 jours pour des données de température, une opération essentielle en analyse climatique pour lisser les fluctuations de court terme.

1.6 Fonction d’agrégation

La méthode agg est une méthode puissante de Pandas qui permet d’effectuer plusieurs opérations sur des DataFrames et des Series. Vous pouvez appliquer une ou plusieurs fonctions d’agrégation telles que sum, mean, min, max, etc., sur différentes colonnes ou différents groupes de données.

# Sample DataFrame
data = {
    'A': [1, 2, 3, 4],
    'B': [5, 6, 7, 8]
}
df = pd.DataFrame(data)

# Apply aggregation functions
result = df['A'].agg(['sum', 'mean'])
print(result)
sum     10.0
mean     2.5
Name: A, dtype: float64
Agréger plusieurs colonnes avec plusieurs fonctions

Vous pouvez passer à agg un dictionnaire dont les clés sont des noms de colonnes et les valeurs les fonctions à appliquer.

# Apply different functions to different columns
result = df.agg({'A': ['sum', 'mean'], 'B': ['min', 'max']})
print(result)
         A    B
sum   10.0  NaN
mean   2.5  NaN
min    NaN  5.0
max    NaN  8.0

Dans cet exemple, la colonne ‘A’ reçoit sum et mean, tandis que la colonne ‘B’ reçoit min et max.

Vous pouvez aussi utiliser des fonctions personnalisées

# Custom function to calculate range (max - min)
def data_range(x):
    return x.max() - x.min()

# Apply custom function
result = df.agg({'A': ['mean', data_range], 'B': data_range})
print(result)
              A    B
mean        2.5  NaN
data_range  3.0  3.0

2 Cartographier avec Plotly

Nous allons maintenant tracer la position des séismes sur une carte avec le paquet Plotly. Plus de tutoriels sur Plotly. Les arguments de la fonction sont explicites et typiques des fonctions Python. La documentation de scatter_geo de Plotly liste les variables.

fig = px.scatter_geo(quake,
                     lat='latitude',lon='longitude', 
                     range_color=(6,9),
                     height=600, width=600,
                     size='marker_size', color='magnitude',
                     hover_name="description",
                     hover_data=['description','magnitude','depth']);
fig.update_geos(resolution=110, showcountries=True)
fig.update_geos(resolution=110, showcountries=True,projection_type="orthographic")
fig
Loading...

La fonction lambda lambda x: 'Minor' if x < 4.0 else ('Moderate' if x < 6.5 else 'Severe') classe les magnitudes de séismes selon leurs valeurs.

Les données étaient triées par date. Nous voulons maintenant trier et afficher les données par magnitude. Nous utilisons la fonction pandas sort pour créer un nouveau DataFrame aux valeurs triées.

quakes2plot=quake.sort_values(by='magnitude_bin')
quakes2plot.head()
Loading...

Traçons de nouveau avec Plotly

fig = px.scatter_geo(quakes2plot,
                     lat='latitude',lon='longitude', 
                     range_color=(6,9),
                     height=600, width=600,
                     size='marker_size', color='magnitude',
                     hover_name="description",
                     hover_data=['description','magnitude','depth']);
fig.update_geos(resolution=110, showcountries=True)
# fig.update_geos(resolution=110, showcountries=True,projection_type="orthographic")
Loading...

3 Créer un DataFrame pandas depuis un fichier texte générique

Le paquet Python pandas est très utile pour lire des fichiers csv, mais aussi de nombreux fichiers texte plus ou moins formatés en une observation par ligne et une colonne par caractéristique (feature).

À titre d’exemple, nous allons examiner la liste des stations sismiques du réseau sismique de Californie du Nord, disponible ici :

url = 'https://ncedc.org/ftp/pub/doc/NC.info/NC.channel.summary.day'
# this gets the file linked in the URL page and convert it to a string
s = requests.get(url).content
# this will convert the string, decode it , and make it a table
data = pd.read_csv(io.StringIO(s.decode('utf-8')), header=None, skiprows=2, sep=r'\s+', usecols=list(range(0, 13)))
# because columns/keys were not assigned, assign them now
data.columns = ['station', 'network', 'channel', 'location', 'rate', 'start_time', 'end_time', 'latitude', 'longitude', 'elevation', 'depth', 'dip', 'azimuth']

Regardons les données. Elles sont maintenant stockées dans un DataFrame pandas.

data.head()
Loading...

Nous pouvons afficher le premier élément du DataFrame :

data.iloc[0]
station AAR network NC channel EHZ location -- rate 0.0 start_time 1976/07/20,17:38:00 end_time 1977/12/01,22:37:00 latitude 39.27594 longitude -121.02696 elevation 911.0 depth 0.0 dip -90.0 azimuth 0.0 Name: 0, dtype: object
# display the type of each column
data.dtypes
station str network str channel str location str rate float64 start_time str end_time str latitude float64 longitude float64 elevation float64 depth float64 dip float64 azimuth float64 dtype: object
data.iloc[:, 0]
0 AAR 1 AAR 2 AAR 3 AAR 4 AAR ... 33375 WMP 33376 WMP 33377 WMP 33378 WSL 33379 WWVB Name: station, Length: 33380, dtype: str

Les colonnes start_time et end_time sont stockées comme object (chaînes de caractères) : nous ne pouvons donc pas encore faire d’arithmétique de dates dessus. Nous les convertissons avec pd.to_datetime et une chaîne de format explicite.

Une mise en garde sur les assistants IA : un assistant IA peut échouer sur cette conversion si vous lui donnez un prompt (une consigne) vague, par exemple en devinant la mauvaise chaîne de format ou en écartant silencieusement les dates hors plage. Vérifiez toujours dtypes (et quelques valeurs réelles) après tout code d’interprétation écrit par une IA.

# convert start_time to datetime with an explicit format
data['start_time'] = pd.to_datetime(data['start_time'], format='%Y/%m/%d,%H:%M:%S')

Les dates de fin demandent une étape de plus. Le NCEDC marque les canaux encore en service avec la date de fin fictive 3000/01/01. Ce n’est pas une vraie date de fin. Nous passons errors='coerce' pour que toute entrée non interprétable devienne NaT (not-a-time) au lieu de lever une erreur ; sur les versions plus anciennes de pandas, qui stockaient les horodatages à la résolution de la nanoseconde, l’an 3000 était hors plage et se faisait convertir ainsi. Pandas 3 interprète les horodatages à la résolution de la microseconde : l’an 3000 se lit donc désormais sans problème, et nous mettons explicitement la valeur fictive à NaT. C’est délibéré et porteur de sens : après la conversion, NaT dans end_time identifie les canaux encore en fonctionnement.

data['end_time'] = pd.to_datetime(data['end_time'], format='%Y/%m/%d,%H:%M:%S', errors='coerce')
# the year-3000 placeholder is not a real end date: mark those channels as still operating
data.loc[data['end_time'] == pd.Timestamp('3000-01-01'), 'end_time'] = pd.NaT
data.head()
Loading...
# check the conversion worked
print(data.dtypes)
print('still-operating channels (NaT end_time):', data['end_time'].isna().sum())
station                  str
network                  str
channel                  str
location                 str
rate                 float64
start_time    datetime64[us]
end_time      datetime64[us]
latitude             float64
longitude            float64
elevation            float64
depth                float64
dip                  float64
azimuth              float64
dtype: object
still-operating channels (NaT end_time): 5619

Utilisez Plotly pour cartographier les stations. Nous écartons les lignes sans coordonnées et gardons les dates de fin NaT, puisqu’elles portent de l’information.

data = data.dropna(subset=['latitude', 'longitude'])
data = data[data.longitude != 0]
fig = px.scatter_geo(data,
                     lat='latitude',lon='longitude', 
                     range_color=(6,9),
                     height=600, width=600,
                     hover_name="station",
                     hover_data=['network','station','channel','rate']);
fig.update_geos(resolution=110, showcountries=True)
Loading...
fig = px.scatter_map(data,
                     lat='latitude', lon='longitude',
                     range_color=(6,9), map_style="carto-positron",
                     height=600, width=500,
                     hover_name="station",
                     hover_data=['network','station','channel','rate']);
fig.update_layout(title="Northern California Seismic Network")
fig.show()
Loading...

4 Exercice

Nous allons maintenant nous exercer à manipuler pandas. Cet exercice récupère des métadonnées de stations depuis une URL, et les étudiants s’exercent sur des tâches précises.

Téléchargez les données depuis l’URL du NCEDC

url = 'https://ncedc.org/ftp/pub/doc/NC.info/NC.channel.summary.day'

Les noms de colonnes figurent en tête du fichier texte ; faites-en une liste de chaînes de caractères, dans l’ordre, afin de renommer les colonnes une fois le DataFrame créé

# students answer here
# students answer here
# request the data from the URL and use the IO package
# assign the column names
data.columns = ['station', 'network', 'channel', 'location', 'rate', 'start_time', 'end_time', 'latitude', 'longitude', 'elevation', 'depth', 'dip', 'azimuth']

Trouvez la ligne du canal KCPB

# find the row of station KCPB

Sélectionnez maintenant deux stations de votre choix à l’aide de |.

# answer below

Sélectionnez maintenant une station donnée et un code de canal précis, par exemple KCPB et le code de canal HNZ.

# Select two stations, use the typical "AND" 

Vous pouvez aussi choisir la fonction pandas isin pour sélectionner les lignes dont un attribut donné appartient à une liste. Par exemple, utilisez isin pour sélectionner toutes les lignes dont la clé station figure dans une liste ['KCPB','KHBB'].

#  students answer here

Q Utilisez les fonctions natives de pandas pour calculer le nombre de sites uniques (noms de stations) dans le réseau.

# students answer here

Q Utilisez les fonctions natives de pandas pour sélectionner l’ensemble unique des codes de canaux qui se terminent par Z : cela vous dira combien de types de données numérisées le réseau sismique gère.

# students answer here

Q Quel nom de station compte le plus grand nombre de canaux ? indice : vous pouvez utiliser value_counts().

# students answer here

Q Quelle est la différence d’altitude maximale entre les stations, en utilisant des fonctions lambda

# students answer here

Ici, pandas ne reconnaît pas les colonnes start_time et end_time comme un format datetime, nous ne pouvons donc pas leur appliquer d’opérations de dates. Il faut d’abord convertir ces colonnes en format datetime :

# answer here
# Transform column from string into datetime format
# do the same for end times

Nous pouvons maintenant regarder quand chaque station sismique a été installée, avec groupby et un tri par déploiement le plus ancien (c’est-à-dire le minimum de start_time)

# students answer here

Sélectionnez les stations déployées en premier et récupérées en dernier avec agg et des fonctions lambda

# answer here

5 CSV vs Parquet

Parquet est un format de données compressé qui stocke et compresse les colonnes. Il est rapide en entrées-sorties et compact.

Sauvegardez data dans un fichier CSV :

%timeit data.to_csv("data/my_metadata.csv")
!ls -lh data/my_metadata.csv
135 ms ± 4.59 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
-rw-r--r--@ 1 marinedenolle  staff   3.2M Aug 12 09:28 data/my_metadata.csv

Essayez de sauvegarder en Parquet et comparez le temps et la mémoire.

%timeit data.to_parquet("data/my_metadata.pq")
!ls -lh data/my_metadata.pq
11.1 ms ± 253 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
-rw-r--r--@ 1 marinedenolle  staff   338K Aug 12 09:28 data/my_metadata.pq