One of the standard global SST products is readily available from NOAA: http://www.esrl.noaa.gov/psd/data/gridded/data.noaa.oisst.v2.html. We will use the monthly-averaged version. We need two files: the actual SST, and the land mask.
%matplotlib inline
import os
import time
import urllib
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from mpl_toolkits.basemap import Basemap
import netCDF4 as nc
from pycurrents.codas import to_day
from pycurrents.num import rangeslice
from pycurrents.num import eof
fnames = ['sst.mnmean.nc', 'lsmask.nc']
urlbase = 'ftp://ftp.cdc.noaa.gov/Datasets/noaa.oisst.v2/'
for fname in fnames:
if not os.path.exists(fname):
urllib.request.urlretrieve(urlbase + fname, fname)
lsmasknc = nc.Dataset(fnames[1])
# strip the time dimension off the mask
seamask = lsmasknc.variables['mask'][0].astype(bool)
lsmasknc.close()
landmask = np.logical_not(seamask)
print(landmask.shape)
sstnc = nc.Dataset(fnames[0])
# Read all the 1-D dimensional arrays.
time = sstnc.variables['time'][:]
lat = sstnc.variables['lat'][:]
lon = sstnc.variables['lon'][:]
# Read the 3-D SST array, and mask it.
sst = np.ma.array(sstnc.variables['sst'][:])
sst[:, landmask] = np.ma.masked
sstnc.close()
dday2000 = time - to_day(1800, 2000, 1, 1, 0, 0, 0)
yeartime = 2000 + dday2000 / 365.25
print(yeartime[0], yeartime[-1])
# Quick plot of on month against indices (sanity check).
plt.pcolormesh(sst[10, ::-1])
Take out the time mean, and fill the land with zeros.
sstmean = sst.mean(axis=0)
sstdm = sst - sstmean
sstdmz = sstdm.filled(0)
Exercise for the reader: instead of taking out just the mean, take out the mean annual cycle as well.
The grid is uniform in latitude and longitude, so high-latitude cells represent smaller areas than low-latitude cells. We would like the EOF spatial functions to be approximately orthogonal in an area-integrated sense. Because this orthogonality condition involves the product of two EOFs, we weight the data by $\sqrt{\cos\theta}$ so that the product is weighted by $\cos\theta$, where $\theta$ is latitude.
latweights = np.sqrt(np.cos(np.deg2rad(lat)))
latweights = latweights[np.newaxis, :, np.newaxis]
sstdmz *= latweights
ssteof = eof.EOF(sstdmz)
fig, ax = plt.subplots()
ax.plot(ssteof.s, 'o')
ax.margins(0.05)
fig, ax = plt.subplots()
ax.plot(ssteof.percent_var()[:10], 'o')
print(ssteof.percent_var()[:10])
fig, ax = plt.subplots()
ax.plot(yeartime, ssteof.u[:, 5:7])
We now take the reshaped spatial patterns, mask them with the landmask, undo the latitude weighting, and multiply them by the singular values.
pats = np.ma.array(ssteof.v_reshaped)
pats[:, landmask] = np.ma.masked
pats /= latweights
pats = pats * ssteof.s[:, np.newaxis, np.newaxis]
cmap = plt.get_cmap('RdBu_r')
Now make a quick contour plot of the spatial pattern, without bothering to use a map projection.
# quick and dirty: using global pats and ssteof
def contour_pattern(i):
# normalize so that std of time function is unity, and pattern
# is in degrees.
std_t = ssteof.u[:, i].std()
pat = pats[i] * std_t
mag = pat.std()
cticker = mpl.ticker.MaxNLocator(nbins=30, symmetric=True)
cticker.create_dummy_axis()
cticker.set_bounds(-2*mag, 2*mag)
clevs = cticker()
fig, ax = plt.subplots()
cs = ax.contourf(lon, lat, pat, levels=clevs,
cmap=cmap,
extend='both')
ax.patch.set_facecolor('gray')
fig.colorbar(cs, ax=ax, shrink=0.9)
contour_pattern(0)
Last, let's make a nicer plot for each of the first few EOFs, with a map for the spatial pattern.
gmap = Basemap(lon_0=180, projection="kav7", resolution='c')
X, Y = gmap(*np.meshgrid(lon, lat))
def draw_map(gmap, ax):
gmap.drawmapboundary(ax=ax)
gmap.drawcoastlines(ax=ax)
gmap.fillcontinents(ax=ax)
gmap.drawmeridians(np.arange(0, 360.01, 30), ax=ax)
gmap.drawparallels(np.arange(-60, 60.01, 30), ax=ax)
def map_pattern(i, pats, ssteof, yeartime, gmap):
# normalize so that std of time function is unity, and pattern
# is in degrees.
std_t = ssteof.u[:, i].std()
pat = pats[i] * std_t
mag = pat.std()
cticker = mpl.ticker.MaxNLocator(nbins=30, symmetric=True)
cticker.create_dummy_axis()
cticker.set_bounds(-2*mag, 2*mag)
clevs = cticker()
#fig, ax = plt.subplots()
#cs = ax.contourf(lon, lat, pat, levels=clevs,
# cmap=cmap,
# extend='both')
#fig.colorbar(cs, ax=ax, shrink=0.9)
fig, axs = plt.subplots(nrows=2, figsize=(7, 10))
draw_map(gmap, ax=axs[1])
cs = gmap.contourf(X, Y, pat, levels=clevs,
cmap=cmap,
extend='both', ax=axs[1])
cbar = fig.colorbar(cs, ax=axs[1], orientation='vertical',
shrink=0.8,
ticks=plt.MaxNLocator(nbins=6, symmetric=True))
cbar.set_label('$^\circ$C')
ax = axs[0]
ax.plot(yeartime, ssteof.u[:, i] / std_t)
ax.set_title("EOF %d: %.1f%% of variance" %
(i, ssteof.percent_var()[i]))
for i in range(5):
map_pattern(i, pats, ssteof, yeartime, gmap)