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.

5c3240fc00d14f1a883eacc85a7d5551

[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')
../_images/notebooks_0311_Drifters_pacific_5_0.png

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')

../_images/notebooks_0311_Drifters_pacific_11_0.png

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')
../_images/notebooks_0311_Drifters_pacific_15_1.png

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')
../_images/notebooks_0311_Drifters_pacific_20_0.png

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')
../_images/notebooks_0311_Drifters_pacific_24_1.png
[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)