
El Niño - Oscilación del Sur (ENOS)¶
Introducción¶
En este cuadernillo (Notebook) aprenderemos:
Breve introducción al fenómeno ENOS.
Acceso a datos públicos de la NOAA.
Generación de mapas con anomalías de temperatura superficial del Océano Pacífico Tropical.
Reproducción de la gráfica del índice ONI en la región Niño 3.4
📚 Descripción general¶
En este cuaderno explorarás el fenómeno El Niño-Oscilación del Sur (ENOS), uno de los patrones climáticos más importantes que afecta el clima global y regional. Aprenderás a acceder y procesar datos de temperatura superficial del mar (TSM) de NOAA para identificar eventos El Niño y La Niña mediante el cálculo del índice ONI (Oceanic Niño Index) en la región Niño 3.4.
Utilizarás técnicas de análisis climático estándar como el cálculo de climatologías, anomalías, y ponderación por latitud. Al final, reproducirás gráficas operacionales similares a las que usa NOAA para monitorear el estado actual de ENOS, una habilidad fundamental para estudios de variabilidad climática y predicción estacional.
✅ Requisitos previos¶
| 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 estimado de aprendizaje: 30 minutos
✍️ Formato: Interactivo
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 índices 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.

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# Configuración de Dask para procesamiento paralelo (opcional)
# Esto acelera el cálculo de climatologías con datos grandes
from dask.distributed import Client, LocalCluster
cluster = LocalCluster(
n_workers=4, # Número de procesos paralelos
memory_limit='2GB' # Límite de memoria por worker
)
client = Client(cluster)
client2. Acceso 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).
# Cargar datos de temperatura superficial del mar (NOAA ERSST v5)
url = '../data/sst.mnmean.nc'
ds = xr.open_dataset(
url,
engine='netcdf4',
chunks={'time': 120} # Chunks grandes para soportar rolling window de 3 meses
)dsdsHagamos algunas visualizaciones básicas de los datos, solo para asegurarnos de que parezcan razonables.
ds.sst[-1].plot(vmin=-2, vmax=30, cmap="coolwarm", robust=True);
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 0x7f02588cd810>/home/runner/micromamba/envs/cdh-python/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_coastline.zip
warnings.warn(f'Downloading: {url}', DownloadWarning)

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", skipna=True)CPU times: user 46.6 ms, sys: 2.18 ms, total: 48.7 ms
Wall time: 47.9 ms
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 330 ms, sys: 44.2 ms, total: 374 ms
Wall time: 2.19 s

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()
);
Cuidado
Debemos ponderar la anomalía con respecto a su posición en latitud
¿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 * weightsVeamos cómo se ve 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_wtgEsta gráfica parece tener una tendencia. El incremento en la temperatura superficial del mar se puede observar después de ~1950 (¿CC?). Ahora debemos remover la tendencia
from scipy.signal import detrend
sst_anom_wgt_dtd = xr.apply_ufunc(
detrend, sst_anom_wgt.fillna(0).load(), 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_dtdMiremos 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}"
);/home/runner/micromamba/envs/cdh-python/lib/python3.13/site-packages/distributed/client.py:3374: UserWarning: Sending large graph of size 125.22 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(

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}"
);/home/runner/micromamba/envs/cdh-python/lib/python3.13/site-packages/distributed/client.py:3374: UserWarning: Sending large graph of size 125.22 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(

3.3 Tratemos de reproducir la gráfica de la NOAA¶
Tratemos de usar nuestros datos para generar una gráfica como esta:
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}"
);/home/runner/micromamba/envs/cdh-python/lib/python3.13/site-packages/distributed/client.py:3374: UserWarning: Sending large graph of size 125.37 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(

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

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());/home/runner/micromamba/envs/cdh-python/lib/python3.13/site-packages/distributed/client.py:3374: UserWarning: Sending large graph of size 125.37 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(

Ahora podemos generar nuestro índice ONI con una ventana móvil de 3 meses
# Calcular índice ONI con ventana móvil de 3 meses
# Agregamos .compute() para evitar problemas con chunks pequeños
oni = nino_34.mean(['lat', 'lon']).compute().rolling(time=3, center=True).mean()/home/runner/micromamba/envs/cdh-python/lib/python3.13/site-packages/distributed/client.py:3374: UserWarning: Sending large graph of size 125.45 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(
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();
Podemos seleccionar los últimos 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()
Actividades Prácticas¶
🏋️ Práctica: Identificando eventos ENOS¶
Usa el gráfico del índice ONI para responder:
¿Cuántos eventos El Niño (ONI > 0.5°C) observas en el período 1950-2023?
¿Cuál fue el evento El Niño más intenso? ¿En qué año ocurrió?
Identifica un evento La Niña reciente (después de 2010)
Pistas:
El Niño: Barras rojas que superan +0.5°C
La Niña: Barras azules que caen bajo -0.5°C
Un evento requiere al menos 5 meses consecutivos sobre el umbral
Solución
Eventos El Niño (1950-2023): Aproximadamente 15-18 eventos
Criterio: ONI > +0.5°C por 5+ meses consecutivos
El Niño más intenso: 1997-1998
Pico ONI: ~2.3°C
Duración: ~1 año
Impactos globales severos
La Niña reciente: 2020-2023
Evento triple-dip (3 años consecutivos)
ONI llegó a ~-1.3°C
Nota: Los eventos varían en intensidad, duración y patrones espaciales. El índice ONI es solo una medida promedio de la región Niño 3.4.
Resumen¶
En este cuadernillo brevemente describimos el fenómeno ENOS y sus fases cálida (El Niño) y fría (La Niña). Aprendimos a:
✅ Acceder a datos de TSM desde servidores OPENDAP de NOAA
✅ Calcular climatologías y anomalías de temperatura superficial del mar
✅ Aplicar correcciones (ponderación por latitud, remoción de tendencia)
✅ Calcular el índice ONI para identificar eventos El Niño y La Niña
✅ Reproducir gráficas operacionales similares a las de NOAA
¿Qué sigue?¶
Con estos conocimientos sobre ENOS, puedes explorar:
[3.3. Datos de reanálisis ERA5] - Análisis de patrones atmosféricos durante eventos ENSO
[2.1. Datos de estaciones] - Validar impactos de ENSO en precipitación local
Pronóstico estacional - Usar índices ENSO para predicción climática
Proyecto sugerido:¶
Analiza cómo ENSO afecta la precipitación en Colombia:
Descarga datos de estaciones del IDEAM durante eventos El Niño y La Niña
Compara anomalías de precipitación con el índice ONI
Identifica patrones regionales (Pacífico vs Caribe vs Andino)