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 :
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, iframe1 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()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()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/m2kmDé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()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 variableAppliquez 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: float64Les 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)1Cela n’a pas modifié les valeurs du DataFrame, vérifiez-le :
quake.depth0 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: float64Vous 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: float64Tracez 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()
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()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()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 steps1.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()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")
figLa 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()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")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()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.dtypesstation 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: objectdata.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: strLes 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.NaTdata.head()# 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)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()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 KCPBSélectionnez maintenant deux stations de votre choix à l’aide de |.
# answer belowSé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 hereQ Utilisez les fonctions natives de pandas pour calculer le nombre de sites uniques (noms de stations) dans le réseau.
# students answer hereQ 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 hereQ Quel nom de station compte le plus grand nombre de canaux ? indice : vous pouvez utiliser value_counts().
# students answer hereQ Quelle est la différence d’altitude maximale entre les stations, en utilisant des fonctions lambda
# students answer hereIci, 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 timesNous 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 hereSélectionnez les stations déployées en premier et récupérées en dernier avec agg et des fonctions lambda
# answer here5 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.csv135 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.pq11.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