---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Xarray — données multidimensionnelles

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

## Pourquoi Xarray ?

NumPy est une bibliothèque remarquablement efficace pour manipuler des tableaux multidimensionnels. Cependant, ses tableaux sont **anonymes** : les axes n'ont pas de nom, les positions n'ont pas de coordonnées, et rien n'empêche de confondre l'axe des latitudes avec celui des longitudes. Pour un tableau de forme `(12, 180, 360)`, il est impossible de savoir, sans documentation externe, si la première dimension représente les mois, les latitudes ou autre chose.

Ce problème s'aggrave dès que les données ont des coordonnées irrégulières, des trous, ou que l'on veut effectuer des opérations d'alignement entre plusieurs jeux de données provenant de sources différentes. C'est précisément le cas des données scientifiques les plus courantes : données climatiques (température en fonction du temps, de la latitude et de la longitude), données de télédétection, simulations numériques, imagerie médicale.

**Xarray** résout ces problèmes en ajoutant une couche d'abstraction au-dessus de NumPy : les dimensions ont des noms, les coordonnées sont des valeurs explicites associées à chaque dimension, et les opérations respectent ces métadonnées. Xarray s'inspire directement de Pandas — dont il adopte la philosophie des données étiquetées — mais l'étend au cas multidimensionnel.

```{admonition} Xarray
:class: tip
**Xarray** est une bibliothèque Python conçue pour manipuler des données multidimensionnelles **étiquetées**. Elle fournit deux structures principales : `DataArray` (un tableau N-dimensionnel avec dimensions nommées et coordonnées) et `Dataset` (un dictionnaire de `DataArray` partageant les mêmes coordonnées). Xarray supporte nativement le format NetCDF, le calcul parallèle avec Dask et le stockage cloud avec Zarr.
```

Les cas d'usage typiques de Xarray incluent :

- **Données climatiques et météorologiques** : température, pression, précipitations en fonction du temps et de la position géographique.
- **Données géospatiales raster** : images satellitaires avec bandes spectrales, résolution spatiale et horodatage.
- **Simulations numériques** : sorties de modèles de physique ou de chimie en plusieurs dimensions.
- **Données de séries temporelles multivariées** avec coordonnées irrégulières ou alignement nécessaire.

## `DataArray` — le tableau étiqueté

Le `DataArray` est l'élément de base de Xarray. Il encapsule un tableau NumPy en y associant :

- des **dimensions** nommées (ex. : `'time'`, `'lat'`, `'lon'`) ;
- des **coordonnées** : des tableaux de valeurs associées à chaque dimension ;
- des **attributs** : métadonnées libres (unités, source, description...).

```{code-cell} python
import xarray as xr
import numpy as np
import pandas as pd

# Création d'un DataArray de températures fictives
np.random.seed(42)

temps = pd.date_range('2024-01', periods=12, freq='ME')
latitudes = np.arange(-90, 91, 15, dtype=float)      # 13 valeurs
longitudes = np.arange(-180, 181, 30, dtype=float)   # 13 valeurs

# Données : 12 mois × 13 lat × 13 lon
data_temp = (
    15                                      # température de base
    - 0.5 * np.abs(latitudes)[np.newaxis, :, np.newaxis]   # gradient latitudinal
    + 5 * np.sin(np.arange(12)[:, np.newaxis, np.newaxis] * np.pi / 6)  # cycle saisonnier
    + np.random.randn(12, 13, 13) * 2      # bruit aléatoire
)

da_temp = xr.DataArray(
    data_temp,
    dims=['time', 'lat', 'lon'],
    coords={
        'time': temps,
        'lat': latitudes,
        'lon': longitudes,
    },
    attrs={
        'units': '°C',
        'long_name': 'Température de surface',
        'source': 'Simulation fictive',
    },
    name='temperature',
)

print(da_temp)
```

```{note}
Le `DataArray` affiche automatiquement son résumé : dimensions, coordonnées, attributs et un aperçu des données. Cette transparence est l'un des grands avantages de Xarray sur NumPy : on sait toujours avec quoi on travaille, sans avoir à consulter de documentation externe. Les attributs (`attrs`) permettent de conserver les métadonnées (unités, source, conventions) tout au long du pipeline de traitement.
```

## `Dataset` — plusieurs variables partageant les mêmes coordonnées

Un `Dataset` est un conteneur de plusieurs `DataArray` partageant les mêmes coordonnées. Il est analogue à un DataFrame Pandas, mais pour des données multidimensionnelles : là où Pandas associe des colonnes à des valeurs 1D, Xarray associe des variables à des tableaux ND.

```{code-cell} python
# Ajout d'une variable précipitations
data_precip = (
    50
    + 30 * np.cos(latitudes)[np.newaxis, :, np.newaxis]
    + 20 * np.random.rand(12, 13, 13)
).clip(min=0)

da_precip = xr.DataArray(
    data_precip,
    dims=['time', 'lat', 'lon'],
    coords={'time': temps, 'lat': latitudes, 'lon': longitudes},
    attrs={'units': 'mm/mois', 'long_name': 'Précipitations'},
    name='precipitation',
)

# Création du Dataset
ds = xr.Dataset({
    'temperature': da_temp,
    'precipitation': da_precip,
})

print(ds)
```

```{code-cell} python
# Accéder à une variable
print("Variable 'temperature' :")
print(ds['temperature'])
print()

# Informations sur le Dataset
print("Dimensions :", dict(ds.sizes))
print("Variables  :", list(ds.data_vars))
print("Coordonnées:", list(ds.coords))
```

```{admonition} Dataset Xarray
:class: tip
Un **Dataset** Xarray est un dictionnaire ordonné de `DataArray` partageant un système de coordonnées commun. Il peut contenir des variables de dimensions différentes — par exemple, une variable 3D `(time, lat, lon)` et une variable 1D `(lat)` représentant l'altitude — à condition que les coordonnées partagées soient identiques. Le Dataset est le conteneur naturel pour représenter un fichier NetCDF ou une sortie de modèle climatique.
```

## Sélection et indexation

Xarray propose deux méthodes de sélection complémentaires : `.sel()` pour sélectionner par **valeur de coordonnée** et `.isel()` pour sélectionner par **position entière** (comme NumPy).

```{code-cell} python
# Sélection par valeur de coordonnée
temp_paris = da_temp.sel(lat=45, lon=0, method='nearest')
print("Température à lat≈45°N, lon≈0° :")
print(temp_paris.values.round(2))
print()

# Sélection d'une plage
temp_europe = da_temp.sel(lat=slice(30, 75), lon=slice(-15, 45))
print("Shape sous-région Europe :", temp_europe.shape)
print()

# Sélection par position
temp_jan = da_temp.isel(time=0)
print("Température en janvier (isel) :", temp_jan.shape)
print()

# Sélection temporelle intuitive
temp_ete = da_temp.sel(time=slice('2024-06', '2024-08'))
print("Température été 2024 :", temp_ete.shape)
```

### Interpolation

`.interp()` interpole les valeurs à des coordonnées arbitraires, ce qui est très utile pour regriller des données :

```{code-cell} python
# Interpolation à une latitude et longitude précises
temp_interpol = da_temp.interp(lat=48.85, lon=2.35)   # Paris approximatif
print("Températures interpolées (Paris) :")
print(temp_interpol.values.round(2))
```

## Opérations

### Agrégation le long d'une dimension

```{code-cell} python
# Moyenne temporelle (climatologie annuelle)
temp_climatologie = da_temp.mean(dim='time')
print("Climatologie (mean sur time) :", temp_climatologie.shape)

# Moyenne zonale (sur les longitudes)
temp_zonale = da_temp.mean(dim='lon')
print("Profil zonal (mean sur lon)  :", temp_zonale.shape)

# Écart-type interannuel
temp_std = da_temp.std(dim='time')
print("Variabilité (std sur time)   :", temp_std.shape)
```

### Broadcasting automatique

Xarray effectue automatiquement le broadcasting en alignant les dimensions par leur nom, ce qui évite les erreurs classiques de NumPy dues aux dimensions implicites :

```{code-cell} python
# Anomalie par rapport à la moyenne temporelle (broadcasting automatique)
anomalie = da_temp - da_temp.mean(dim='time')
print("Anomalie shape :", anomalie.shape)
print("Min anomalie :", float(anomalie.min().values.round(2)))
print("Max anomalie :", float(anomalie.max().values.round(2)))
```

### `groupby` sur les coordonnées

Xarray supporte `groupby` sur n'importe quelle coordonnée, y compris des composantes dérivées des dates :

```{code-cell} python
# Climatologie mensuelle (moyenne de chaque mois sur tous les pixels)
# Ici, comme nous n'avons qu'une année, on regroupe par mois
temp_par_saison = da_temp.groupby('time.season').mean('time')
print("Température par saison :", temp_par_saison)
```

```{admonition} Cycle saisonnier
:class: note
L'extraction du cycle saisonnier est une opération fondamentale en climatologie :

```python
# Température moyenne par mois (cycle saisonnier)
cycle_mensuel = da_temp.groupby('time.month').mean('time')

# Anomalie saisonnière
anomalie_saisonniere = da_temp.groupby('time.month') - cycle_mensuel
```

Cette syntaxe, qui ressemble à `groupby` de Pandas, effectue automatiquement l'alignement sur la dimension `time` et retourne un `DataArray` de même forme que l'entrée.

## Entrées/sorties

### NetCDF — le format standard des sciences de la Terre

NetCDF (*Network Common Data Form*) est le format de référence pour les données scientifiques multidimensionnelles. Xarray le lit et l'écrit nativement :

```python
# Lecture d'un fichier NetCDF
ds = xr.open_dataset('donnees_climatiques.nc')

# Écriture
ds.to_netcdf('sortie.nc')

# Lecture de plusieurs fichiers en un seul Dataset (données réparties par année)
ds_multi = xr.open_mfdataset('donnees_*.nc', combine='by_coords')
```

### Zarr — pour les données cloud

Zarr est un format de stockage optimisé pour le cloud et le calcul parallèle. Contrairement à NetCDF, Zarr stocke les données en **chunks** pouvant être lus indépendamment, ce qui permet de ne charger que les parties nécessaires :

```python
# Écriture au format Zarr
ds.to_zarr('donnees.zarr', mode='w')

# Lecture (lazy, avec Dask)
ds_zarr = xr.open_zarr('donnees.zarr')
```

### Intégration avec Pandas

Xarray s'intègre naturellement avec Pandas pour les analyses 1D ou 2D :

```{code-cell} python
# Convertir un DataArray 1D en Series Pandas
temp_serie = da_temp.sel(lat=45, lon=0, method='nearest').to_series()
print(type(temp_serie))
print(temp_serie.round(2))
```

## Visualisation — Dataset climatique fictif

```{code-cell} python
:tags: [hide-input]

fig, axes = plt.subplots(2, 2, figsize=(16, 11))
fig.suptitle('Dataset climatique fictif — Xarray', fontsize=15,
             fontweight='bold')

# ---- Carte de la température moyenne annuelle ----
ax = axes[0, 0]
temp_annuelle = da_temp.mean(dim='time')
im = ax.pcolormesh(
    longitudes, latitudes, temp_annuelle.values,
    cmap='RdYlBu_r', vmin=-30, vmax=35
)
plt.colorbar(im, ax=ax, label='°C')
ax.set_xlabel('Longitude (°)')
ax.set_ylabel('Latitude (°)')
ax.set_title('Température moyenne annuelle', fontweight='bold')
ax.axhline(0, color='black', linewidth=0.5, linestyle='--', alpha=0.5)
ax.set_facecolor('#d6e4f0')

# ---- Série temporelle à une position ----
ax = axes[0, 1]
temp_nord = da_temp.sel(lat=60, lon=0, method='nearest')
temp_equat = da_temp.sel(lat=0, lon=0, method='nearest')
temp_sud = da_temp.sel(lat=-60, lon=0, method='nearest')

ax.plot(temps, temp_nord.values, 'o-', label='60°N (lon=0)', linewidth=2)
ax.plot(temps, temp_equat.values, 's-', label='0° (lon=0)', linewidth=2)
ax.plot(temps, temp_sud.values, '^-', label='60°S (lon=0)', linewidth=2)
ax.set_xlabel('Mois')
ax.set_ylabel('Température (°C)')
ax.set_title('Cycle saisonnier — séries temporelles', fontweight='bold')
ax.legend(fontsize=8)
ax.tick_params(axis='x', rotation=30, labelsize=7)

# ---- Profil latitudinal ----
ax = axes[1, 0]
temp_profil_ete = da_temp.sel(time='2024-07', method='nearest').mean(dim='lon')
temp_profil_hiv = da_temp.sel(time='2024-01', method='nearest').mean(dim='lon')
temp_profil_ann = da_temp.mean(dim=['time', 'lon'])

ax.plot(latitudes, temp_profil_ete.values, 'r-', linewidth=2,
        label='Juillet 2024')
ax.plot(latitudes, temp_profil_hiv.values, 'b-', linewidth=2,
        label='Janvier 2024')
ax.plot(latitudes, temp_profil_ann.values, 'k--', linewidth=2,
        label='Moyenne annuelle')
ax.axvline(0, color='gray', linestyle=':', alpha=0.5)
ax.set_xlabel('Latitude (°)')
ax.set_ylabel('Température (°C)')
ax.set_title('Profil zonal moyen (latitude × T°)', fontweight='bold')
ax.legend(fontsize=8)
ax.set_xticks(latitudes[::2])

# ---- Carte des précipitations ----
ax = axes[1, 1]
precip_annuelle = da_precip.mean(dim='time')
im2 = ax.pcolormesh(
    longitudes, latitudes, precip_annuelle.values,
    cmap='Blues', vmin=0, vmax=120
)
plt.colorbar(im2, ax=ax, label='mm/mois')
ax.set_xlabel('Longitude (°)')
ax.set_ylabel('Latitude (°)')
ax.set_title('Précipitations moyennes annuelles', fontweight='bold')
ax.axhline(0, color='black', linewidth=0.5, linestyle='--', alpha=0.5)
ax.set_facecolor('#f9f3e3')

plt.tight_layout()
plt.show()
```

## Résumé

Ce chapitre a présenté Xarray, la bibliothèque de référence pour les données multidimensionnelles étiquetées :

- **Pourquoi Xarray** : les tableaux NumPy sont anonymes — les axes n'ont pas de nom et les positions n'ont pas de coordonnées. Xarray résout ce problème pour les données scientifiques (climatologie, géospatial, simulations) en ajoutant des dimensions nommées et des coordonnées explicites.
- Le **`DataArray`** est l'unité de base : un tableau N-dimensionnel avec `dims`, `coords` et `attrs`. Il conserve les métadonnées tout au long du pipeline.
- Le **`Dataset`** regroupe plusieurs variables partageant les mêmes coordonnées, à l'image d'un fichier NetCDF ou d'une sortie de modèle.
- La **sélection** se fait par valeur avec `.sel()` (intuitive) ou par position avec `.isel()` (NumPy-style). `.interp()` permet l'interpolation à des coordonnées arbitraires.
- Les **opérations** (agrégation, broadcasting, `groupby`) respectent automatiquement les dimensions nommées, éliminant les erreurs classiques de NumPy.
- Xarray lit et écrit **NetCDF** nativement, supporte **Zarr** pour le cloud, et s'intègre naturellement avec Pandas.

Le chapitre suivant introduit **Scikit-learn** et son API cohérente `fit`/`transform`/`predict`, point d'entrée vers le machine learning en Python.
