Pacific variability and ENSO¶
Here, we will do a similar analysis than last week, but focussing on the equatorial Pacific. The dominant process along the equator is upwelling, due to the westward tradewinds pushing surface waters northward in the northern hemisphere, and southward in the southern hemisphere due to the earth rotation, resulting in upwelling of cold water from below along the equator. While that upwelling is easily seen in satellite sea surface temperature images, can we see it in drifter tracks?
The global database of drifter tracks is widely available over the internet; the instructions on how to obtain the data for any region are given at the end of this lecture. We have extracted the data for the area and saved as a cvs file.
[1]:
import pandas as pd
import numpy as np
drifters = pd.read_csv('/home/sunset0/htdocs/ges/ocn463-data/woce/drift/pacdrift.gz', compression='gzip', na_values = 999.999)
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
Input In [1], in <cell line: 4>()
1 import pandas as pd
2 import numpy as np
----> 4 drifters = pd.read_csv('/home/sunset0/htdocs/ges/ocn463-data/woce/drift/pacdrift.gz', compression='gzip', na_values = 999.999)
File /opt/tljh/user/lib/python3.9/site-packages/pandas/util/_decorators.py:311, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
305 if len(args) > num_allow_args:
306 warnings.warn(
307 msg.format(arguments=arguments),
308 FutureWarning,
309 stacklevel=stacklevel,
310 )
--> 311 return func(*args, **kwargs)
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/parsers/readers.py:586, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, squeeze, prefix, mangle_dupe_cols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, error_bad_lines, warn_bad_lines, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options)
571 kwds_defaults = _refine_defaults_read(
572 dialect,
573 delimiter,
(...)
582 defaults={"delimiter": ","},
583 )
584 kwds.update(kwds_defaults)
--> 586 return _read(filepath_or_buffer, kwds)
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/parsers/readers.py:482, in _read(filepath_or_buffer, kwds)
479 _validate_names(kwds.get("names", None))
481 # Create the parser.
--> 482 parser = TextFileReader(filepath_or_buffer, **kwds)
484 if chunksize or iterator:
485 return parser
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/parsers/readers.py:811, in TextFileReader.__init__(self, f, engine, **kwds)
808 if "has_index_names" in kwds:
809 self.options["has_index_names"] = kwds["has_index_names"]
--> 811 self._engine = self._make_engine(self.engine)
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/parsers/readers.py:1040, in TextFileReader._make_engine(self, engine)
1036 raise ValueError(
1037 f"Unknown engine: {engine} (valid options are {mapping.keys()})"
1038 )
1039 # error: Too many arguments for "ParserBase"
-> 1040 return mapping[engine](self.f, **self.options)
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/parsers/c_parser_wrapper.py:51, in CParserWrapper.__init__(self, src, **kwds)
48 kwds["usecols"] = self.usecols
50 # open handles
---> 51 self._open_handles(src, kwds)
52 assert self.handles is not None
54 # Have to pass int, would break tests using TextReader directly otherwise :(
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/parsers/base_parser.py:222, in ParserBase._open_handles(self, src, kwds)
218 def _open_handles(self, src: FilePathOrBuffer, kwds: dict[str, Any]) -> None:
219 """
220 Let the readers open IOHandles after they are done with their potential raises.
221 """
--> 222 self.handles = get_handle(
223 src,
224 "r",
225 encoding=kwds.get("encoding", None),
226 compression=kwds.get("compression", None),
227 memory_map=kwds.get("memory_map", False),
228 storage_options=kwds.get("storage_options", None),
229 errors=kwds.get("encoding_errors", "strict"),
230 )
File /opt/tljh/user/lib/python3.9/site-packages/pandas/io/common.py:642, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
640 if is_path:
641 assert isinstance(handle, str)
--> 642 handle = gzip.GzipFile(
643 filename=handle,
644 mode=ioargs.mode,
645 **compression_args,
646 )
647 else:
648 handle = gzip.GzipFile(
649 # error: Argument "fileobj" to "GzipFile" has incompatible type
650 # "Union[str, Union[IO[Any], RawIOBase, BufferedIOBase, TextIOBase,
(...)
654 **compression_args,
655 )
File /opt/tljh/user/lib/python3.9/gzip.py:173, in GzipFile.__init__(self, filename, mode, compresslevel, fileobj, mtime)
171 mode += 'b'
172 if fileobj is None:
--> 173 fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
174 if filename is None:
175 filename = getattr(fileobj, 'name', '')
FileNotFoundError: [Errno 2] No such file or directory: '/home/sunset0/htdocs/ges/ocn463-data/woce/drift/pacdrift.gz'
[ ]:
drifters
There’s a lot more data here than there was in the previous lecture. Let’s make a plot of every 100th track
[24]:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12,12))
ax.plot(drifters['lon'][::100], drifters['lat'][::100], '.')
ax.set_title('Drifter tracks')
ax.set_aspect('equal')

There are some gaps, but we can still see what the currents are like. Let’s make boxes of 1/2 degree latitude and all longitudes, and find the average U and V in each box as well as the standard deviation of them.
[25]:
cenlon, cenlat = np.meshgrid(np.arange(165.5, 267.6, 0.5), np.arange(-9.5, 9.6, 0.5))
Umean = np.nan * np.ones_like(cenlon)
Vmean = np.nan * np.ones_like(cenlon)
Ustd = np.nan * np.ones_like(cenlon)
Vstd = np.nan * np.ones_like(cenlon)
for ilat in range(cenlon.shape[0]):
for ilon in range(cenlon.shape[1]):
boxlat = np.where(np.absolute(drifters['lat']-cenlat[ilat, ilon]) < 0.5)[0]
boxlon = np.where(np.absolute(drifters['lon']-cenlon[ilat, ilon]) < 0.5)[0]
box = np.array(sorted(set(boxlat) & set(boxlon)))
Umean[ilat, ilon] = np.nanmean(drifters['u'][box])
Vmean[ilat, ilon] = np.nanmean(drifters['v'][box])
Ustd[ilat, ilon] = np.nanstd(drifters['u'][box])
Vstd[ilat, ilon] = np.nanstd(drifters['v'][box])
[5]:
cenlon
[5]:
array([[165.5, 166. , 166.5, ..., 266.5, 267. , 267.5],
[165.5, 166. , 166.5, ..., 266.5, 267. , 267.5],
[165.5, 166. , 166.5, ..., 266.5, 267. , 267.5],
...,
[165.5, 166. , 166.5, ..., 266.5, 267. , 267.5],
[165.5, 166. , 166.5, ..., 266.5, 267. , 267.5],
[165.5, 166. , 166.5, ..., 266.5, 267. , 267.5]])
[6]:
cenlat
[6]:
array([[-9.5, -9.5, -9.5, ..., -9.5, -9.5, -9.5],
[-9. , -9. , -9. , ..., -9. , -9. , -9. ],
[-8.5, -8.5, -8.5, ..., -8.5, -8.5, -8.5],
...,
[ 8.5, 8.5, 8.5, ..., 8.5, 8.5, 8.5],
[ 9. , 9. , 9. , ..., 9. , 9. , 9. ],
[ 9.5, 9.5, 9.5, ..., 9.5, 9.5, 9.5]])
And now let’s plot then
[26]:
fig, ax = plt.subplots(2,1,figsize=(15,8), sharex=True)
q = ax[0].quiver(cenlon, cenlat, Umean, Vmean)
ax[0].quiverkey(q, 178, 11, 0.25, '0.25 m/s', coordinates='data')
ax[0].set_ylabel('Latitude')
ax[0].set_xlabel('Longitude')
ax[0].set_title('Mean currents Equatorial Pacific from WOCE drifters')
ax[0].set_aspect('equal')
ax[0].set_ylim(-10, 12)
cs = ax[1].contourf(cenlon, cenlat, np.sqrt(Ustd**2 + Vstd**2), levels=50)
ax[1].set_aspect('equal')
cbaxes = fig.add_axes([0.11, 0.05, 0.8, 0.03])
cb = plt.colorbar(cs, cax = cbaxes, orientation='horizontal')

Now let’s try making zonal strips
[27]:
cenlat1 = np.arange(-9.5, 9.6, 0.5)
Umean1 = np.nan * np.ones_like(cenlat1)
Vmean1 = np.nan * np.ones_like(cenlat1)
Ustd1 = np.nan * np.ones_like(cenlat1)
Vstd1 = np.nan * np.ones_like(cenlat1)
for ilat in range(len(cenlat1)):
strip = np.where(np.absolute(drifters['lat']-cenlat1[ilat]) < 0.5)[0]
Umean1[ilat] = np.nanmedian(drifters['u'][strip])
Vmean1[ilat] = np.nanmedian(drifters['v'][strip])
Ustd1[ilat] = np.nanstd(drifters['u'][strip])
Vstd1[ilat] = np.nanstd(drifters['v'][strip])
And plot them
[28]:
fig, ax = plt.subplots(1,5,figsize=(15,12), sharey=True)
q = ax[0].quiver(np.zeros_like(cenlat1), cenlat1, Umean1, Vmean1)
ax[0].quiverkey(q, -178, 11, 0.25, '0.25 m/s', coordinates='data')
ax[0].set_ylabel('Latitude')
ax[0].set_title('Mean currents from drifters')
#ax[0].set_aspect('equal')
ax[0].set_ylim(-10, 12)
cs = ax[1].plot(Vmean1, cenlat1, 'k')
ax[1].set_title('meridional current')
cs = ax[2].plot(Umean1, cenlat1, 'k')
ax[2].set_title('zonal current')
cs = ax[3].plot(np.sqrt(Ustd1**2+Vstd1**2), cenlat1, 'k')
ax[3].set_title('standard deviation')
[28]:
Text(0.5, 1.0, 'standard deviation')

North Equatorial Counter Current: 5N to 8N EASTWARD North Equatorial Current: 9N to 20N (Hawaii) WESTWARD South Equatorial Current: -12S to 4N WESTWARD
Sea surface temperatures (SST)¶
Most of the drifting buoys have a surface temperature sensor attached to the flotation sphere. We will now use the surface temperature data to procuce maps of SST and compare with the velocity map.
First, we produce a gridded temperature array, using a similar loop as for estimating the gridded ‘’u’’ and ‘’v’’ arrays:
[34]:
Tmean = np.nan * np.ones_like(cenlon)
Tstd = np.nan * np.ones_like(cenlon)
for ilat in range(cenlon.shape[0]):
for ilon in range(cenlon.shape[1]):
boxlat = np.where(np.absolute(drifters['lat']-cenlat[ilat, ilon]) < 0.5)[0]
boxlon = np.where(np.absolute(drifters['lon']-cenlon[ilat, ilon]) < 0.5)[0]
box = np.array(sorted(set(boxlat) & set(boxlon)))
Tmean[ilat, ilon] = np.nanmean(drifters['sst'][box])
Tstd[ilat, ilon] = np.nanstd(drifters['sst'][box])
Just like we plotted the mean velocity vectors, we can plot the mean temperature field:
[40]:
fig, ax = plt.subplots(2,1,figsize=(15,8), sharex=True)
ctm = ax[0].contourf(cenlon, cenlat, Tmean, levels=50, cmap = plt.cm.jet)
ax[0].set_ylabel('Latitude')
ax[0].set_xlabel('Longitude')
ax[0].set_title('Mean temperature Equatorial Pacific from WOCE drifters')
ax[0].set_aspect('equal')
ax[0].set_ylim(-10, 12)
cts = ax[1].contourf(cenlon, cenlat, Tstd, levels=50)
ax[1].set_aspect('equal')
ax[1].set_ylim(-10, 12)
cbaxes = fig.add_axes([0.11, 0.05, 0.8, 0.03])
cb = plt.colorbar(ctm, cax = cbaxes, orientation='horizontal')

We can again compute medians over zonal strips:
[37]:
cenlat1 = np.arange(-9.5, 9.6, 0.5)
Tmean1 = np.nan * np.ones_like(cenlat1)
Tstd1 = np.nan * np.ones_like(cenlat1)
for ilat in range(len(cenlat1)):
strip = np.where(np.absolute(drifters['lat']-cenlat1[ilat]) < 0.5)[0]
Tmean1[ilat] = np.nanmedian(drifters['sst'][strip])
Tstd1[ilat] = np.nanstd(drifters['sst'][strip])
and plot the meridional profiles on the side of the meridional velocity plots:
[38]:
fig, ax = plt.subplots(1,5,figsize=(15,12), sharey=True)
q = ax[0].quiver(np.zeros_like(cenlat1), cenlat1, Umean1, Vmean1)
ax[0].quiverkey(q, -178, 11, 0.25, '0.25 m/s', coordinates='data')
ax[0].set_ylabel('Latitude')
ax[0].set_title('Mean currents from drifters')
#ax[0].set_aspect('equal')
ax[0].set_ylim(-10, 12)
cs = ax[1].plot(Vmean1, cenlat1, 'k')
ax[1].set_title('meridional current')
cs = ax[2].plot(Umean1, cenlat1, 'k')
ax[2].set_title('zonal current')
cs = ax[3].plot(np.sqrt(Ustd1**2+Vstd1**2), cenlat1, 'k')
ax[3].set_title('standard deviation')
cs = ax[4].plot(Tmean1, cenlat1, 'k')
ax[4].set_title('mean temperature')
[38]:
Text(0.5, 1.0, 'mean temperature')

[39]:
whos
Variable Type Data/Info
--------------------------------------
Tmean ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
Tmean1 ndarray 39: 39 elems, type `float64`, 312 bytes
Tstd ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
Tstd1 ndarray 39: 39 elems, type `float64`, 312 bytes
Umean ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
Umean1 ndarray 39: 39 elems, type `float64`, 312 bytes
Ustd ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
Ustd1 ndarray 39: 39 elems, type `float64`, 312 bytes
Vmean ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
Vmean1 ndarray 39: 39 elems, type `float64`, 312 bytes
Vstd ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
Vstd1 ndarray 39: 39 elems, type `float64`, 312 bytes
ax ndarray 5: 5 elems, type `object`, 40 bytes
box ndarray 314: 314 elems, type `int64`, 2512 bytes
boxlat ndarray 139149: 139149 elems, type `int64`, 1113192 bytes (1.0616226196289062 Mb)
boxlon ndarray 15254: 15254 elems, type `int64`, 122032 bytes (119.171875 kb)
cb Colorbar <matplotlib.colorbar.Colo<...>object at 0x7f4e9e34c128>
cbaxes Axes Axes(0.11,0.05;0.8x0.03)
cenlat ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
cenlat1 ndarray 39: 39 elems, type `float64`, 312 bytes
cenlon ndarray 39x205: 7995 elems, type `float64`, 63960 bytes
cs list n=1
ctm QuadContourSet <matplotlib.contour.QuadC<...>object at 0x7f4ea2c394a8>
cts QuadContourSet <matplotlib.contour.QuadC<...>object at 0x7f4e9e59a668>
drifters DataFrame id month <...>244756 rows x 13 columns]
fig Figure Figure(1080x864)
ilat int 38
ilon int 204
np module <module 'numpy' from '/op<...>kages/numpy/__init__.py'>
pd module <module 'pandas' from '/o<...>ages/pandas/__init__.py'>
plt module <module 'matplotlib.pyplo<...>es/matplotlib/pyplot.py'>
q Quiver <matplotlib.quiver.Quiver<...>object at 0x7f4ea28fe7b8>
strip ndarray 139149: 139149 elems, type `int64`, 1113192 bytes (1.0616226196289062 Mb)