Tutorial 6: Large Scale Climate Variability - ENSO#

Week 1, Day 3, Remote Sensing

Content creators: Douglas Rao

Content reviewers: Katrina Dobson, Younkap Nina Duplex, Maria Gonzalez, Will Gregory, Nahid Hasan, Sherry Mi, Beatriz Cosenza Muralles, Jenna Pearson, Agustina Pesce, Chi Zhang, Ohad Zivan

Content editors: Jenna Pearson, Chi Zhang, Ohad Zivan

Production editors: Wesley Banfield, Jenna Pearson, Chi Zhang, Ohad Zivan

Our 2023 Sponsors: NASA TOPS and Google DeepMind

Tutorial Objectives#

In this tutorial, you will build upon the introduction to El Niño-Southern Oscillation (ENSO) from Day 1 and 2. ENSO is recognized as one of the most influential large-scale climate variabilities that impact weather and climate patterns.

By the end of this tutorial, you will:

  • Enhance your comprehension of the concept of ENSO and the three distinct phases associated with it.

  • Utilize satellite-derived sea surface temperature (SST) data to compute an index for monitoring ENSO.

Setup#

# !apt-get install libproj-dev proj-data proj-bin --quiet
# !apt-get install libgeos-dev --quiet
# !pip install cython --quiet
# !pip install cartopy --quiet

# !apt-get -qq install python-cartopy python3-cartopy  --quiet
# !pip uninstall -y shapely  --quiet
# !pip install shapely --no-binary shapely  --quiet
# imports
import xarray as xr
import numpy as np
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import os
import requests
import tarfile
import pooch
import os
import tempfile
import holoviews
from geoviews import Dataset as gvDataset
import geoviews.feature as gf
from geoviews import Image as gvImage

Figure settings#

# @title Figure settings
import ipywidgets as widgets  # interactive display

%config InlineBackend.figure_format = 'retina'
plt.style.use(
    "https://raw.githubusercontent.com/ClimateMatchAcademy/course-content/main/cma.mplstyle"
)

Video 1: Video 1 Name#

# @title Video 1: Video 1 Name
# Tech team will add code to format and display the video
# helper functions


def pooch_load(filelocation="", filename=""):
    shared_location = "/home/jovyan/shared/data/tutorials/W1D3_RemoteSensingLandOceanandAtmosphere"  # this is different for each day
    user_temp_cache = tempfile.gettempdir()

    if os.path.exists(os.path.join(shared_location, filename)):
        file = os.path.join(shared_location, filename)
    else:
        file = pooch.retrieve(
            filelocation, known_hash=None, fname=os.path.join(user_temp_cache, filename)
        )

    return file

Section 1: El Niño-Southern Oscillation (ENSO)#

As you learned in Day 1 and 2, one of the most significant large-scale climate variabilities is El Niño-Southern Oscillation (ENSO). ENSO can change the global atmospheric circulation, which in turn, influences temperature and precipitation across the globe.

Despite being a single climate phenomenon, ENSO exhibits three distinct phases:

  • El Niño: A warming of the ocean surface, or above-average sea surface temperatures, in the central and eastern tropical Pacific Ocean.

  • La Niña: A cooling of the ocean surface, or below-average sea surface temperatures, in the central and eastern tropical Pacific Ocean.

  • Neutral: Neither El Niño or La Niña. Often tropical Pacific SSTs are generally close to average.

In Day 2, you practiced utilizing a variety of Xarray tools to examine variations in sea surface temperature (SST) during El Niño and La Niña events by calculating the Oceanic Niño Index (ONI) from reanalysis data over the time period 2000-2014.

In contrast to previous days, in this tutorial you will use satellite-based SST data to monitor ENSO over a longer time period starting in 1981.

Section 1.1: Calculate SST Anomaly#

Optimum Interpolation Sea Surface Temperature (OISST) is a long-term Climate Data Record that incorporates observations from different platforms (satellites, ships, buoys and Argo floats) into a regular global grid. OISST data is originally produced at daily and 1/4° spatial resolution. To avoid the large amount of data processing of daily data, we use the monthly aggregated OISST SST data provided by NOAA Physical Systems Laboratory.

# download the monthly sea surface temperature data from NOAA Physical System
# Laboratory. The data is processed using the OISST SST Climate Data Records
# from the NOAA CDR program.
# the data downloading may take 2-3 minutes to complete.
# filename=sst.mon.mean.nc
url_sst = "https://osf.io/6pgc2/download/"
filename = "sst.mon.mean.nc"

# we divide the data into small chunks to allow for easier memory manangement. this is all done automatically, no need for you to do anything
ds = xr.open_dataset(
    pooch_load(filelocation=url_sst, filename=filename),
    chunks={"time": 25, "latitude": 200, "longitude": 200},
)
ds
Downloading data from 'https://osf.io/6pgc2/download/' to file '/tmp/sst.mon.mean.nc'.
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[6], line 11
      7 filename = "sst.mon.mean.nc"
      9 # we divide the data into small chunks to allow for easier memory manangement. this is all done automatically, no need for you to do anything
     10 ds = xr.open_dataset(
---> 11     pooch_load(filelocation=url_sst, filename=filename),
     12     chunks={"time": 25, "latitude": 200, "longitude": 200},
     13 )
     14 ds

Cell In[5], line 11, in pooch_load(filelocation, filename)
      9     file = os.path.join(shared_location, filename)
     10 else:
---> 11     file = pooch.retrieve(
     12         filelocation, known_hash=None, fname=os.path.join(user_temp_cache, filename)
     13     )
     15 return file

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/pooch/core.py:239, in retrieve(url, known_hash, fname, path, processor, downloader, progressbar)
    236 if downloader is None:
    237     downloader = choose_downloader(url, progressbar=progressbar)
--> 239 stream_download(url, full_path, known_hash, downloader, pooch=None)
    241 if known_hash is None:
    242     get_logger().info(
    243         "SHA256 hash of downloaded file: %s\n"
    244         "Use this value as the 'known_hash' argument of 'pooch.retrieve'"
   (...)
    247         file_hash(str(full_path)),
    248     )

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/pooch/core.py:803, in stream_download(url, fname, known_hash, downloader, pooch, retry_if_failed)
    799 try:
    800     # Stream the file to a temporary so that we can safely check its
    801     # hash before overwriting the original.
    802     with temporary_file(path=str(fname.parent)) as tmp:
--> 803         downloader(url, tmp, pooch)
    804         hash_matches(tmp, known_hash, strict=True, source=str(fname.name))
    805         shutil.move(tmp, str(fname))

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/pooch/downloaders.py:226, in HTTPDownloader.__call__(self, url, output_file, pooch, check_only)
    224     progress = self.progressbar
    225     progress.total = total
--> 226 for chunk in content:
    227     if chunk:
    228         output_file.write(chunk)

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/requests/models.py:816, in Response.iter_content.<locals>.generate()
    814 if hasattr(self.raw, "stream"):
    815     try:
--> 816         yield from self.raw.stream(chunk_size, decode_content=True)
    817     except ProtocolError as e:
    818         raise ChunkedEncodingError(e)

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/urllib3/response.py:628, in HTTPResponse.stream(self, amt, decode_content)
    626 else:
    627     while not is_fp_closed(self._fp):
--> 628         data = self.read(amt=amt, decode_content=decode_content)
    630         if data:
    631             yield data

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/urllib3/response.py:567, in HTTPResponse.read(self, amt, decode_content, cache_content)
    564 fp_closed = getattr(self._fp, "closed", False)
    566 with self._error_catcher():
--> 567     data = self._fp_read(amt) if not fp_closed else b""
    568     if amt is None:
    569         flush_decoder = True

File ~/miniconda3/envs/climatematch/lib/python3.10/site-packages/urllib3/response.py:533, in HTTPResponse._fp_read(self, amt)
    530     return buffer.getvalue()
    531 else:
    532     # StringIO doesn't like amt=None
--> 533     return self._fp.read(amt) if amt is not None else self._fp.read()

File ~/miniconda3/envs/climatematch/lib/python3.10/http/client.py:466, in HTTPResponse.read(self, amt)
    463 if self.length is not None and amt > self.length:
    464     # clip the read to the "end of response"
    465     amt = self.length
--> 466 s = self.fp.read(amt)
    467 if not s and amt:
    468     # Ideally, we would raise IncompleteRead if the content-length
    469     # wasn't satisfied, but it might break compatibility.
    470     self._close_conn()

File ~/miniconda3/envs/climatematch/lib/python3.10/socket.py:705, in SocketIO.readinto(self, b)
    703 while True:
    704     try:
--> 705         return self._sock.recv_into(b)
    706     except timeout:
    707         self._timeout_occurred = True

File ~/miniconda3/envs/climatematch/lib/python3.10/ssl.py:1274, in SSLSocket.recv_into(self, buffer, nbytes, flags)
   1270     if flags != 0:
   1271         raise ValueError(
   1272           "non-zero flags not allowed in calls to recv_into() on %s" %
   1273           self.__class__)
-> 1274     return self.read(nbytes, buffer)
   1275 else:
   1276     return super().recv_into(buffer, nbytes, flags)

File ~/miniconda3/envs/climatematch/lib/python3.10/ssl.py:1130, in SSLSocket.read(self, len, buffer)
   1128 try:
   1129     if buffer is not None:
-> 1130         return self._sslobj.read(len, buffer)
   1131     else:
   1132         return self._sslobj.read(len)

KeyboardInterrupt: 

The monthly OISST data is available starting from September of 1981. We will use the Niño 3.4 (5N-5S, 170W-120W) region to monitor the ENSO as identified in the map below provided by NOAA Climate portal.

Location of four different nino regions

Credit: NOAA

The data is only available in full years starting 1982, so we will use 1982-2011 as the climatology period.

# get 30-year climatology from 1982-2011
sst_30yr = ds.sst.sel(time=slice("1982-01-01", "2011-12-01"))

# calculate monthly climatology
sst_clim = sst_30yr.groupby("time.month").mean()
sst_clim
# calculate monthly anomaly
sst_anom = ds.sst.groupby("time.month") - sst_clim
sst_anom

Now, we can take a look at the SST anomaly of a given month. We use January of 1998 to show the specific change of SST during that time period.

sst = sst_anom.sel(time="1998-01-01")

# initate plot
fig, ax = plt.subplots(
    subplot_kw={"projection": ccrs.Robinson(central_longitude=180)}, figsize=(9, 6)
)

# focus on the ocean with the central_longitude=180
ax.coastlines()
ax.gridlines()
sst.plot(
    ax=ax,
    transform=ccrs.PlateCarree(),
    vmin=-3,
    vmax=3,
    cmap="RdBu_r",
    cbar_kwargs=dict(shrink=0.5, label="OISST Anomaly (degC)"),
)

Interactive Demo 1.1#

Use the slider bar below to explore maps of the anomalies through the year in 1998.

# note this code takes a while to load. probably an hour
# holoviews.extension('bokeh')

# dataset_plot = gvDataset(sst_anom.sel(time=slice('1998-01-01','1998-12-01'))) # taking only 12 months
# images = dataset_plot.to(gvImage, ['lon', 'lat'], ['sst'], 'time')
# images.opts(cmap='RdBu_r', colorbar=True, width=600, height=400,projection=ccrs.Robinson(),
#             clim=(-3,3),clabel ='OISST Anomaly (degC)') * gf.coastline

Section 1.2: Monitoring ENSO with Oceanic Niño Index#

As you learned in Day 2, the Oceanic Niño Index (ONI) is a common index used to monitor ENSO. It is calculated using the Niño 3.4 region (5N-5S, 170W-120W) and by applying a 3-month rolling mean to the mean SST anomalies in that region.

You may have noticed that the lon for the SST data from NOAA Physical Systems Laboratory is organized between 0°–360°E. Just as in Tutorial 1 of Day 2, we find that the region to subset with our dataset is (-5°–5°, 190–240°).

# extract SST data from the Nino 3.4 region
sst_nino34 = sst_anom.sel(lat=slice(-5, 5), lon=slice(190, 240))
sst_nino34
# calculate the mean values for the Nino 3.4 region
nino34 = sst_nino34.mean(dim=["lat", "lon"])

# Pplot time series for Nino 3.4 mean anomaly
fig, ax = plt.subplots(figsize=(12, 6))
nino34.plot(ax=ax)
ax.set_ylabel("Nino3.4 Anomaly (degC)")
ax.axhline(y=0, color="k", linestyle="dashed")

The ONI is defined as the 3-month rolling mean of the monthly regional average of the SST anomaly for the Nino 3.4 region. We can use .rolling() to calculate the ONI value for each month from the OISST monthly anomaly.

# calculate 3-month rolling mean of Nino 3.4 anomaly for the ONI
oni = nino34.rolling(time=3, center=True).mean()

# generate time series plot
fig, ax = plt.subplots(figsize=(12, 6))
nino34.plot(label="Nino 3.4", ax=ax)
oni.plot(color="k", label="ONI", ax=ax)
ax.set_ylabel("Anomaly (degC)")
ax.axhline(y=0, color="k", linestyle="dashed")
ax.legend()

The different phases of ENSO are nominally defined based on a threshold of \(\pm\) 0.5 with the ONI index.

  • El Niño [ONI values higher than 0.5]: surface waters in the east-central tropical Pacific are at least 0.5 degrees Celsius warmer than normal.

  • La Niña [ONI values lower than -0.5]: surface waters ub the west tropical Pacific are at least 0.5 degrees Celsius cooler than normal.

The neutral phase is when ONI values are in between these two thresholds. We can make the ONI plot that is used by NOAA and other organizations to monitor ENSO phases.

# set up the plot size
fig, ax = plt.subplots(figsize=(12, 6))

# create the filled area when ONI values are above 0.5 for El Nino
ax.fill_between(
    oni.time.data,
    oni.where(oni >= 0.5).data,
    0.5,
    color="red",
    alpha=0.9,
)

# create the filled area when ONI values are below -0.5 for La Nina
ax.fill_between(
    oni.time.data,
    oni.where(oni <= -0.5).data,
    -0.5,
    color="blue",
    alpha=0.9,
)

# create the time series of ONI
oni.plot(color="black", ax=ax)

# add the threshold lines on the plot
ax.axhline(0, color="black", lw=0.5)
ax.axhline(0.5, color="red", linewidth=0.5, linestyle="dotted")
ax.axhline(-0.5, color="blue", linewidth=0.5, linestyle="dotted")
ax.set_title("Oceanic Niño Index")

From the plot, we can see the historical ENSO phases swing from El Nino to La Nina events. The major ENSO events like 1997-1998 shows up very clearly on the ONI plot.

We will use the ONI data to perform analysis to understand the impact of ENSO on precipitation. So you can export the ONI time series into a netCDF file for future use via .to_netcdf(). For our purposes, we will download a dataset that has been previously saved in the next tutorial. If you wanted to save the data when working on your own computer, this is the code you could use.

# oni.to_netcdf('t6_oceanic-nino-index.nc')

Coding Exercises 1.2#

As we learned here, ENSO is monitored using the anomaly of SST data for a specific region (e.g., Nino 3.4). We also learned previously that the reference periods used to calculate climatolgies are updated regularly to reflect the most up to date ‘normal’.

  1. Compare the ONI time series calculated using two different climatology reference periods (1982-2011 v.s. 1991-2020).

#################################################
# Students: Fill in missing code (...) and comment or remove the next line
raise NotImplementedError(
    "Student exercise: Compare the ONI time series calculated using two different climatology reference periods (1982-2011 v.s. 1991-2020)."
)
#################################################
# select data from 1991-2020.
sst_30yr_later = ...

# calculate climatology
sst_clim_later = ...

# calculate anomaly
sst_anom_later = ...

# calculate mean over Nino 3.4 region
nino34_later = ...

# compute 3 month rolling mean
oni_later = ...

# compare the two ONI time series and visualize the difference as a time series plot
fig, ax = plt.subplots(figsize=(12, 6))
oni.plot(color="k", label="ONI (1982-2011)", ax=ax)
oni_later.plot(color="r", label="ONI (1991-2020)", ax=ax)
ax.set_ylabel("Anomaly (degC)")
ax.axhline(y=0, color="k", linestyle="dashed")
ax.legend()

# to_remove solution

# select data from 1991-2020.
sst_30yr_later = ds.sst.sel(time=slice("1991-01-01", "2020-12-30"))

# calculate climatology
sst_clim_later = sst_30yr_later.groupby("time.month").mean()

# calculate anomaly
sst_anom_later = ds.sst.groupby("time.month") - sst_clim_later

# calculate mean over Nino 3.4 region
nino34_later = sst_anom_later.sel(lat=slice(-5, 5), lon=slice(190, 240)).mean(
    dim=["lat", "lon"]
)

# compute 3 month rolling mean
oni_later = nino34_later.rolling(time=3, center=True).mean()

# compare the two ONI time series and visualize the difference as a time series plot
fig, ax = plt.subplots(figsize=(12, 6))
oni.plot(color="k", label="ONI (1982-2011)", ax=ax)
oni_later.plot(color="r", label="ONI (1991-2020)", ax=ax)
ax.set_ylabel("Anomaly (degC)")
ax.axhline(y=0, color="k", linestyle="dashed")
ax.legend()

Questions 1.2: Climate Connection#

  1. What is the main difference you note about this plot?

  2. What does this tell you about the climatology calculated from 1982-2011 versus 1991-2020?

  3. Why is it important to use appropriate climatologies when finding anomalies?

# to_remove explanation

"""
1. The index using the 1982-2011 reference period is always a little larger than the 1991-2020 reference period curve.
2. This means that the climatology values for the 1991-2020 reference period were larger than that of 1982-2011.
3. This can help remove long term trends (e.g. from warming) that can obscure the anomalies and ensures the anomaly is to the correct 'normal'.
""";

Summary#

In this tutorial, you revisted the foundational principles of ENSO and explored how satellite data can be employed to track this phenomenon.

  • As one of the most potent climate influences on Earth, ENSO has the capacity to alter global atmospheric circulation with impacts around the world.

  • You observed the three phases of ENSO by utilizing SST data gathered from satellites and calculating the Oceanic Niño Index.

In the forthcoming tutorial, we will utilize the ONI, calculated in this session, to evaluate the influence of ENSO on precipitation in select regions.

Resources#

Data from this tutorial can be accessed here.