ENSO

El Niño - Oscilación del Sur (ENOS)


Introducción

En este cuadernillo (Notebook) aprenderemos:

  1. Breve introducción al fenómeno ENOS.

  2. Acceso a datos públicos de la NOAA.

  3. Generación de mapas con anomalías de temperatura superficial del Océano Pacífico Tropical.

  4. Reproducción de la gráfica del índice ONI en la región Niño 3.4

Prerequisitos

Conceptos

Importancia

Notas

Xarray

Necesario

Manejo de datos multidimensionales espacializados

Matplotlib

Necesario

Generación de gráficas

Cartopy

Necesario

Generación de mapas

NetCDF

Útil

Familiaridad con la estructura de datos y metadatos.

  • Tiempo de aprendizaje: 30 minutos.

1. El Niño/La Niña

Para entender los eventos El Niño o La Niña debemos, primero, introducir que es ENOS. De acuerdo con la Organización Meteorológica Mundial (WMO por sus siglas en inglés de World Meteorological Organization) podemos decir que:

El Niño/Oscilación del Sur (ENOS) es un fenómeno natural caracterizado por la fluctuación de las temperaturas del océano en la parte central y oriental del Pacífico ecuatorial, asociada a cambios en la atmósfera. Este fenómeno tiene una gran influencia en las condiciones climáticas de diversas partes del mundo.

Actualmente es de gran interés en nuestra comunidad científica entender y predecir los efectos de corto y largo alcance de estas fluctuaciones, pues estas suelen estar asociadas a fuertes lluvias o sequías en algunas partes del mundo.

Se dice que, cuando ENOS está en su fase cálida hay un evento El Niño, mientras que, su fase fría se denomina La Niña.

1.1 ¿Cómo saber si hay un evento El Niño o La Niña?

Durante los episodios El Niño, la temperatura de la superficie del mar (TSM) en las partes central y oriental del Pacífico Tropical suele ser muy superior a la normal, mientras que, en esas mismas regiones, durante los episodios de La Niña la temperatura es inferior a la normal. Para decir que, efectivamente hay un evento del uno o del otro, se utilizan indices Oceánicos de medición de ENOS que cuantifican el estado de la anomalía de TSM.

Por ejemplo, en la figura se pueden ver varias regiones de cálculo de la anomalía. Nosotros nos concentraremos en la región Niño 3.4 a lo largo de este taller, la cual se define como la región entre +/- 5° latitud y 170° W, 120° W longitud.

ENSO

Las fases cálidas o frías del Índice del Niño Oceánico (ONI) se definen por cinco anomalías medias consecutivas de TSM durante cinco meses consecutivos en la región elegida -en nuestro caso Niño 3.4- que están por encima del umbral de +0,5 °C (cálido), o por debajo del umbral de -0,5°C (frío). Las oscilaciones de episodios ENOS pueden presentarse en intervalos que varían entre 2 y 7 años.


Librerías

Importamos las librerías que usaremos a lo largo de este cuadernillo.

import cartopy.crs as ccrs
import hvplot.xarray  # noqa
import matplotlib.ticker as mticker
import numpy as np
import pandas as pd
import xarray as xr
from matplotlib import pyplot as plt

2. Accesso a los datos de temperatura de la NOAA ERSST

Usaremos los datos de temperaturas superficiales del mar de la Oficina Nacional de Administración Océanica y Atmosférica de Estados Unidos (NOAA por sus siglas en inglés).

# url = 'http://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/noaa.ersst.v5/sst.mnmean.nc'
# # url = "http://psl.noaa.gov/thredds/dodsC/Datasets/noaa.ersst.v5/sst.mnmean.nc"
# ds = xr.open_dataset(url, drop_variables=['time_bnds'], engine='netcdf4')

Algunas veces el servidor de Thredds de NCAR presenta algunos inconvenientes con el bytestreaming. Si el computo de la climatología y la anomalía toma mucho tiempo, utilice el siguiente archivo:

url = "../data/sst.mnmean.nc"
ds = xr.open_dataset(url, engine="netcdf4")
0.3.0
display(ds)
<xarray.Dataset>
Dimensions:  (lat: 89, lon: 180, time: 2037)
Coordinates:
  * lat      (lat) float32 88.0 86.0 84.0 82.0 80.0 ... -82.0 -84.0 -86.0 -88.0
  * lon      (lon) float32 0.0 2.0 4.0 6.0 8.0 ... 350.0 352.0 354.0 356.0 358.0
  * time     (time) datetime64[ns] 1854-01-01 1854-02-01 ... 2023-09-01
Data variables:
    sst      (time, lat, lon) float32 ...
Attributes: (12/37)
    climatology:               Climatology is based on 1971-2000 SST, Xue, Y....
    description:               In situ data: ICOADS2.5 before 2007 and NCEP i...
    keywords_vocabulary:       NASA Global Change Master Directory (GCMD) Sci...
    keywords:                  Earth Science > Oceans > Ocean Temperature > S...
    instrument:                Conventional thermometers
    source_comment:            SSTs were observed by conventional thermometer...
    ...                        ...
    creator_url_original:      https://www.ncei.noaa.gov
    license:                   No constraints on data access or use
    comment:                   SSTs were observed by conventional thermometer...
    summary:                   ERSST.v5 is developed based on v4 after revisi...
    dataset_title:             NOAA Extended Reconstructed SST V5
    data_modified:             2023-10-03

Hagamos algunas visualizaciones básicas de los datos, solo para asegurarnos de que parezcan razonables.

ds.sst[-1].plot(vmin=-2, vmax=30, cmap="coolwarm")
<matplotlib.collections.QuadMesh at 0x7fdae53f3610>
../../_images/7bcdeb16c79eda10c2bd277c4f2d338ce052f20ce7f81d526ab20ee528248c36.png

Podemos usar Cartopy para “embellecer” el gráfico

# Definir el tamaño de la figura
fig = plt.figure(figsize=(9, 4))

# Asignar un eje y projección del mapa
ax = plt.axes(
    projection=ccrs.InterruptedGoodeHomolosine(central_longitude=180, globe=None)
)

# Agregar líneas costeras
ax.coastlines()

# Agregar las líneas de retícula (lon and lat)
ax.gridlines()
ds.sst.sel(time="1998-01").plot(
    vmin=-2, vmax=30, cmap="coolwarm", transform=ccrs.PlateCarree()
)
<cartopy.mpl.geocollection.GeoQuadMesh at 0x7fdadd301d10>
../../_images/0d62b6bc1cb2270300860c9463a85e52a6b91b38a5baed232b42bca8415ca7a8.png

3. Anomalías de temperatura superficial del Océano Pacífico Tropical

“Anomalía” significa que se ha eliminado el ciclo estacional, también llamado “climatología”.

3.1 Climatología

Para estimar la climatología podemos usar la funcionalidad de xarray denominada groupby donde agrupamos por meses del año y luego tomamos la media a lo largo de cada mes usando el metodo mean

%time
sst_clim = ds.sst.groupby("time.month").mean("time")
CPU times: user 2 µs, sys: 1e+03 ns, total: 3 µs
Wall time: 5.25 µs

Ahora podemos visualizar la climatología media zonal

%time
sst_clim.mean(dim="lon").transpose().plot.contourf(
    levels=12, vmin=-2, vmax=30, cmap="coolwarm"
)
CPU times: user 2 µs, sys: 0 ns, total: 2 µs
Wall time: 5.01 µs
<matplotlib.contour.QuadContourSet at 0x7fdadc22a850>
../../_images/b5c8f4b3c254104dd15cfd70c362d2965b936651c9491ddf4013143663136b62.png

3.2 Cálculo de la anomalía

La anomalía esta dada por el valor de la TSM - SST (por sus siglas en inglés) media para cada mes

sst_anom = ds.sst.groupby("time.month") - sst_clim
# Definir el tamaño de la figura
fig = plt.figure(figsize=(9, 4))

# Asignar un eje y proyección del mapa
ax = plt.axes(projection=ccrs.Robinson(central_longitude=180))

# Añadir líneas costeras
ax.coastlines()

# Añadir líneas de la cuadrícula (longitud y latitud)
ax.gridlines()
sst_anom.sel(time="1998-01").plot(
    vmin=-2, vmax=2, cmap="coolwarm", transform=ccrs.PlateCarree()
)
<cartopy.mpl.geocollection.GeoQuadMesh at 0x7fdadc109950>
../../_images/f81a4d07586476403998bf191026726b4b81b4c07ef035574cad140efb97cca8.png

Cuidado!!!

Debemos ponderar la anomalía con respecto a su posición en latitud

ENSO

¿Por qué necesitamos ponderar nuestros datos?

Las celdas de la cuadrícula más cercanas al ecuador son mucho más grandes que las cercanas a los polos, como se ve en la figura anterior (Djexplo, 2011, CC-BY)

weights = np.cos(np.deg2rad(ds.lat)).where(~sst_anom[0].isnull())
weights /= weights.mean()
sst_anom_wgt = sst_anom * weights

Veamos cómo se ve el el promedio global vs el promedio ponderado de la anomalía de la temperatura superficial del océano usando ahora hvplot

# Promedio global de la anomalía
gb_anom = sst_anom.mean(dim=["lon", "lat"])
_anom = gb_anom.hvplot.line(x="time", y="sst", label="Inicial")

# Promedio global de la anomalía ponderada
gb_anom_wtd = sst_anom_wgt.mean(dim=["lon", "lat"])
gb_anom_wtd.name = "sst"
_anom_wtg = gb_anom_wtd.hvplot.line(x="time", label="Ponderado")
# _anom_wtg
_anom * _anom_wtg

Esta gráfica parece tener una tendencia. El incremento en la temperatura superficial del mar se puede observar despues de ~1950 (CC?). Ahora debememos remover la tendencia!!!

from scipy.signal import detrend

sst_anom_wgt_dtd = xr.apply_ufunc(
    detrend, sst_anom_wgt.fillna(0), kwargs={"axis": 0}
).where(~sst_anom.isnull())
sst_anom_wgt_dtd.name = "sst"
# Promedio global de la anomalía ponderada sin tendencia
gb_anom_wtd_dtd = sst_anom_wgt_dtd.mean(dim=["lon", "lat"])
_anom_wtd_dtd = sst_anom_wgt_dtd.mean(dim=["lon", "lat"]).hvplot.line(
    x="time", y="sst", label="Ponderado sin tendencia"
)
_anom * _anom_wtg * _anom_wtd_dtd

Miremos ahora la anomalía de la SST para enero de 1998

fig, ax = plt.subplots(
    figsize=(6, 4),
    subplot_kw={"projection": ccrs.Robinson(central_longitude=180)},
    dpi=150,
)
sst_anom_wgt_dtd.sel(time="1998-01").plot(
    vmin=-2,
    vmax=2,
    cmap="coolwarm",
    transform=ccrs.PlateCarree(),
    cbar_kwargs={
        "label": r"$SST \ Anomaly \ [°C]$",
        "orientation": "horizontal",
        "aspect": 50,
    },
)
ax.coastlines()
gl = ax.gridlines(draw_labels=True, crs=ccrs.PlateCarree())
gl.xlocator = mticker.FixedLocator([-180, -60, 60, 180])
gl.ylocator = mticker.FixedLocator([-60, -30, 0, 30, 60])
plt.title(
    f"Anomalía TSM {pd.to_datetime(sst_anom_wgt_dtd.sel(time='1998-01').time[0].values): %Y-%m}"
)
Text(0.5, 1.0, 'Anomalía TSM  1998-01')
../../_images/1b387683fe3a30313879a7a297372ffaafe77cd980288855a420782ce02b6a3b.png

Veamos qué pasa si cambiamos la proyección del mapa

# Definir el tamaño de la figura
fig = plt.figure(figsize=(12, 6))

# Asignar un eje y proyección del mapa
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180))

# Añadir líneas costeras
ax.coastlines()

# Añadir líneas de cuadricula (lon y lat)
ax.gridlines()
sst_anom_wgt_dtd.sel(time="1998-01").plot(
    vmin=-2, vmax=2, cmap="coolwarm", transform=ccrs.PlateCarree()
)
plt.title(
    f"Anomalía TSM {pd.to_datetime(sst_anom_wgt_dtd.sel(time='1998-01').time[0].values): %Y-%m}"
)
Text(0.5, 1.0, 'Anomalía TSM  1998-01')
../../_images/65d368a5e8ab7d18c709d0fc442934d3f0b24c37323ad95677381dcc3b0cf0c4.png

3.3 Tratemos de reproducir la gráfica de la NOAA

Tratemos de usar nuestros datos para generar una gráfica como esta:

ENSO

sst_anom_sub = sst_anom_wgt_dtd.sel(lat=slice(60, -60), lon=slice(25, 360))
fig, ax = plt.subplots(
    figsize=(6, 4),
    subplot_kw={"projection": ccrs.PlateCarree(central_longitude=180)},
    dpi=150,
)
sst_anom_sub.sel(time="1998-01").plot(
    vmin=-2,
    vmax=2,
    cmap="coolwarm",
    transform=ccrs.PlateCarree(),
    cbar_kwargs={
        "label": r"$SST \ Anomaly \ [°C]$",
        "orientation": "horizontal",
        "aspect": 50,
    },
)
ax.coastlines()
ax.gridlines(draw_labels=True, crs=ccrs.PlateCarree())
plt.title(
    f"Anomalía TSM {pd.to_datetime(sst_anom_wgt_dtd.sel(time='1998-01').time[0].values): %Y-%m}"
)
Text(0.5, 1.0, 'Anomalía TSM  1998-01')
../../_images/079ce1eaf3446d8a52263760a5ae94f4141fbf4f0038b57a95e5bc8724074744.png

4. Índice ONI en la región Niño 3.4

ENSO

La región Niño 3.4 se define como la región entre +/- 5 grados. latitud, 170 W - 120 W longitud.

nino_34 = sst_anom_wgt_dtd.sel(
    lat=slice(5, -5), lon=slice(180 - (180 - 170), 180 + (180 - 120))
)
fig, ax = plt.subplots(
    figsize=(5, 5),
    subplot_kw={"projection": ccrs.PlateCarree(central_longitude=180)},
    dpi=150,
)
nino_34.sel(time="1998-01").plot(
    vmin=-2,
    vmax=2,
    cmap="coolwarm",
    transform=ccrs.PlateCarree(),
    cbar_kwargs={
        "label": r"$SST \ Anomaly \ [°C]$",
        "orientation": "horizontal",
        "aspect": 50,
    },
)
ax.coastlines()
ax.set_extent([120, 290, -30, 30], crs=ccrs.PlateCarree())
ax.gridlines(draw_labels=True, crs=ccrs.PlateCarree())
<cartopy.mpl.gridliner.Gridliner at 0x7fdad5b01c90>
../../_images/0434d220cb637e511311f8f16569272cd4284fe312d3e55908aebdcaf594c0e2.png

Ahora podemos generar nuestro índice ONI con una ventana móvil de 3 meses

oni = nino_34.mean(["lat", "lon"]).rolling(time=3, center=True).mean()
fig, ax = plt.subplots(figsize=(10, 3), dpi=150)
ax.plot(oni.time, oni, lw=1, c="k")
ax.axhline(0, c="grey", lw=0.5, ls="--")
ax.axhline(0.5, c="r", lw=0.5, ls="--", label="El Niño")
ax.axhline(-0.5, c="b", lw=0.5, ls="--", label="La Niña")
ax.set_ylim(-2.5, 2.5)
ax.set_xlabel("$Año$")
ax.set_ylabel(r"$Anomalía \ [°C]$")
ax.legend()
<matplotlib.legend.Legend at 0x7fdad5ed8e50>
../../_images/e07f55328b6c1fc3e5748ff0c67f5d675190e9555e50bab2b5834d917dd0800a.png

Podemos selesccionar los ultimos 70 años para efectos de visualización

oni_sub = oni.sel(time=slice("1950", "2023"))
fig, ax = plt.subplots(figsize=(8, 3), dpi=150)
ax.plot(oni_sub.time, oni_sub, lw=1, c="k")
ax.axhline(0, c="grey", lw=0.5, ls="--")
ax.axhline(0.5, c="r", lw=0.5, ls="--", label="El Niño")
ax.axhline(-0.5, c="b", lw=0.5, ls="--", label="La Niña")
ax.fill_between(oni_sub.time, 0.5, oni_sub.where(oni_sub >= 0.5), color="C01")
ax.fill_between(oni_sub.time, -0.5, oni_sub.where(oni_sub <= -0.5), color="C00")

ax.set_ylim(-3.5, 3.5)
ax.set_xlabel("$Año$")
ax.set_ylabel(r"$Anomalía \ [°C]$")
ax.legend()
plt.show()
../../_images/2de0c247e51a80362277a06ce83b44da95c3d7e84281ee86ba6be4beecf4502e.png

Bonus!

Podemos crear una animación usando xmovie

from xmovie import Movie
def custom_plotfunc(ds, fig, t0, *args, **kwargs):
    ax = fig.subplots(
        1, 1, subplot_kw={"projection": ccrs.PlateCarree(central_longitude=180)}
    )
    ds_sub = ds.isel(time=t0)
    ds_sub.plot(
        vmin=-2,
        vmax=2,
        cmap="coolwarm",
        transform=ccrs.PlateCarree(),
        cbar_kwargs={
            "label": r"$SST \ Anomaly \ [°C]$",
            "orientation": "horizontal",
            "aspect": 50,
        },
        ax=ax,
    )
    ax.coastlines()
    ax.gridlines(draw_labels=True, crs=ccrs.PlateCarree())
    ax.set_title(f"Anomalía TSM {pd.to_datetime(ds_sub.time.values): %Y-%m}")
subset = sst_anom_wgt_dtd.sel(time=slice("2019-08", "2023-08")).chunk({"time": 1})
mov_custom = Movie(
    da=subset,
    plotfunc=custom_plotfunc,
    framedim="time",
    input_check=False,
    pixelwidth=1220,
    pixelheight=920,
    dpi=150,
)
# mov_custom.save('enso1.gif', progress=True, overwrite_existing=True, framerate=10)
from IPython.display import Video

Video("../images/enso1.mov")

Conclusiones

En este cuadernillo brevemente describimos el fenómeno ENOS y sus fases cálida (Niño) y fría (Niña). Apredimos a importar los datos de NOAA y reproducir las gráficas que vemos comunmente para ENOS.

Recursos y referencias

  • Abernathey, R. 2023. An Introduction to Earth and Environmental Data Science. Retrieved from Earth and Environmental Data Science: https://earth-env-data-science.github.io/intro.html

  • Climatematch Acadimy: Computational tools for climate science. Página web: https://academy.climatematch.io/, repositorio GitHub: https://github.com/ClimatematchAcademy/course-content