Exploring the Dry Tortugas Sr/Ca data#

Authors#

Deborah Khider

Preamble#

A common exercise in paleoclimatology is the calibration of the measured proxy (in this case, coral Sr/Ca) to the environmental parameter that controls it (sea surface temperature). This notebook walks through data exploration of the Sr/Ca measurments made on various coral in Dry Tortugas compared to instrumental data. Some figures are reproduced from the original study by DeLong et al. 2011.

Let’s import the necessary packages:

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import math
import pyleoclim as pyleo

Data Description#

The data are available through the database maintained by NOAA NCEI in .txt and .xls format. Let’s import them into the workspace using pandas:

url = 'https://www.ncei.noaa.gov/pub/data/paleo/coral/atlantic/tortugas2011.xls'
df = pd.read_excel(url, sheet_name=1,header=[0,1,2])
df.head()
Coral Siderastrea sidera Montastraea faveolata Monthly mean SST from NDBC www.ndbc.noaa.gov
Core A1-A F1-A B3-A DRYF1 and PLSF1 SANF1
Year Mean Sr/Ca (mmol/mol) Number of samples St. error of mean Mean Sr/Ca (mmol/mol) Number of samples St. error of mean Mean Sr/Ca (mmol/mol) Number of samples St. error of mean DRTO SST (ºC) St. error of mean Sand Key SST (ºC) St. error of mean
0 2008.625000 8.737095 1 0.012000 NaN NaN NaN 9.025385 3 0.003027 30.193011 0.010330 NaN NaN
1 2008.541667 8.770223 2 0.003228 NaN NaN NaN 9.051444 3 0.010060 29.551197 0.010570 NaN NaN
2 2008.458333 8.841024 2 0.003956 NaN NaN NaN 9.111843 3 0.000554 28.539499 0.022829 NaN NaN
3 2008.375000 8.940933 2 0.001614 8.972382 1.0 0.01 9.190204 3 0.021459 26.642473 0.026493 NaN NaN
4 2008.291667 9.022715 2 0.003125 9.062088 1.0 0.01 9.232626 3 0.010278 24.905000 0.019899 NaN NaN

The file contains the following information:

  • Year: Time axis

  • Mean Sr/Ca (mmol/mol): the measured Sr/Ca values for Siderastrea sidera core A1-A (column 2), core F1-A (column 5), and for Montastraea faveolata core B3-A (column 8)

  • Number of samples: Number of measured samples for Siderastrea sidera core A1-A (column 3), core F1-A (column 6), and for Montastraea faveolata core B3-A (column 9)

  • St. error of mean (columns 4,7,10): The standard error associated with the measurements for for Siderastrea sidera core A1-A (column 4), core F1-A (column 7), and for Montastraea faveolata core B3-A (column 10)

  • DRTO SST (°C) column 11): Instrumental sea surface temperature (°C) from two stations in the National Data Buoy Center database close to the location of the coral records.

  • St. error of mean (column 12): Standard error on the DRTO instrumental record.

  • Sand Key SST (°C) (column 13): Instrumental sea surface temperature (°C) from a station in the National Data Buoy Center database located on a coral reef at Sand Key. The Sand Key record was used to augment the number of data points compared to the DRTO record.

  • St. error of mean (column 14): Standard error on the Sand Key instrumental record.

Coral Sr/Ca#

let’s plot the data, leveraging pandas indexing (iloc) to access what we want:

fig,ax1 = plt.subplots(figsize=(15,4))
ax1.plot(df.iloc[:,0], df.iloc[:,1], label = "Siderastrea sidera A1-A", color = sns.xkcd_rgb['red'])
ax1.plot(df.iloc[:,0], df.iloc[:,4], label = "Siderastrea sidera F1-A", color = sns.xkcd_rgb['wine red'])
ax1.plot(df.iloc[:,0], df.iloc[:,7], label = "Montastraea faveolata B3-A", color = sns.xkcd_rgb['purple'])
ax1.set_xlabel('Year')
ax1.set_ylabel('Sr/Ca (mmol/mol)')

fig.tight_layout()  # otherwise the right y-label is slightly clipped
fig.legend(bbox_to_anchor=(1.2, 1.05))
plt.show()
_images/350abd4564522ea1e77793151511d006d06d5e7a85539bc9119ec47eb721b43d.png

There seems to be an offset between the two species, which will require separate calibation. This offset was observed in the original study and attributed to sampling effects.

Instrumental Sea Surface Temperature#

fig,ax2 = plt.subplots(figsize=(15,4))

ax2.plot(df.iloc[:,0],df.iloc[:,10], label = 'DRTO SST', color = sns.xkcd_rgb['peach'])
ax2.plot(df.iloc[:,0],df.iloc[:,12], label = 'Sand Key SST', color = sns.xkcd_rgb['dark orange'])
ax2.set_ylabel('SST ($\circ$C)')
ax2.set_xlabel('Year')

fig.tight_layout()  # otherwise the right y-label is slightly clipped
fig.legend(bbox_to_anchor=(1.1, 1.05))
plt.show()
_images/66e270b77c63980e63df80aeea82221083f0869fd401047cac7110c2e0bc8c81.png

There is a remarkable agreement between the instrumental datasets from the two locations.

Sr/Ca vs Sea Surface Temperature#

A timeseries view: Siderastrea sidera#

Let’s have a look at each coral species vs temperature, inverting the Sr/Ca axis to highlight the negative relationship between the two. We will start with Siderastrea sidera:

fig,ax1 = plt.subplots(figsize=(15,4))
ax1.plot(df.iloc[:,0], df.iloc[:,1], label = "Siderastrea sidera A1-A", color = sns.xkcd_rgb['red'])
ax1.plot(df.iloc[:,0], df.iloc[:,4], label = "Siderastrea sidera F1-A", color = sns.xkcd_rgb['wine red'])
ax1.set_xlabel('Year')
ax1.set_ylabel('Sr/Ca (mmol/mol)')
ax1.invert_yaxis()

ax2=ax1.twinx()
ax2.plot(df.iloc[:,0],df.iloc[:,10], label = 'DRTO SST', color = sns.xkcd_rgb['peach'])
ax2.plot(df.iloc[:,0],df.iloc[:,12], label = 'Sand Key SST', color = sns.xkcd_rgb['dark orange'])
ax2.set_ylabel('SST ($\circ$C)')
ax2.spines['right']

fig.tight_layout()  # otherwise the right y-label is slightly clipped
fig.legend(bbox_to_anchor=(1.2, 1.05))
plt.show()
_images/47f2944ab6e344540bc177afecd266c2cdd02defa714dde5c0eed876391c1954.png

Let’s calculate the correlation between coral Sr/Ca and SST using the Pyleoclim package. First, let’s create Series objects for each of the coral and instrumental records:

A1A = pyleo.Series(time=df.iloc[:,0], value= df.iloc[:,1], time_name = 'Year', time_unit = 'CE', value_name = 'Sr/Ca', value_unit = 'mmmol/mol', label = 'Siderastrea sidera A1-A', verbose = False)
F1A = pyleo.Series(time=df.iloc[:,0], value= df.iloc[:,4], time_name = 'Year', time_unit = 'CE', value_name = 'Sr/Ca', value_unit = 'mmmol/mol', label = 'Siderastrea sidera F1-A', verbose = False)
DRTO = pyleo.Series(time=df.iloc[:,0], value= df.iloc[:,10], time_name = 'Year', time_unit = 'CE', value_name = 'SST', value_unit = '$\circ$C', label = 'DRTO', verbose = False)
SK = pyleo.Series(time=df.iloc[:,0], value= df.iloc[:,12], time_name = 'Year', time_unit = 'CE', value_name = 'SST', value_unit = '$\circ$C', label = 'Sand Key', verbose = False)

Let’s create a MultipleSeries object so we can take advantage of the time axis alignment capabilities.

ms = pyleo.MultipleSeries([A1A,F1A,DRTO,SK])

Let’s get all these Series on a common time axis so we can make meaningful comparisons among the correlations:

ms_interp = ms.common_time()

Correlation between Siderastrea sidera and SST#

Now we can compute the correlation (as expressed by Pearson’s product-moment correlation, or as more commonly known, Pearson’s R) between the coral records and instrumental datasets. Given the autocorrelation in the two timeseries introduced by the seasonal cycle, we need to be able to assess the significance of this correlation. Here we will use phase-randomized surrogates, the default method in Pyleoclim. Let’s start with core A1A and DRTO:

corr = ms_interp.series_list[0].standardize().correlation(ms_interp.series_list[2].standardize())
print('The correlation between Siderastrea sidera core A1A and DRTO instrumental temperature is:')
print(corr)
Evaluating association on surrogate pairs: 100%|█| 1000/1000 [00:00<00:00, 3296.
The correlation between Siderastrea sidera core A1A and DRTO instrumental temperature is:
  correlation  p-value    signif. (α: 0.05)
-------------  ---------  -------------------
    -0.710412  < 1e-6     True

Not surprinsgly, we find a strong and significant anti-correlation between the coral Sr/Ca and SST. Let’s check with the Sand Key instrumental record:

corr = ms_interp.series_list[0].standardize().correlation(ms_interp.series_list[3].standardize())
print('The correlation between Siderastrea sidera core A1A and Sand Key instrumental temperature is:')
print(corr)
Evaluating association on surrogate pairs: 100%|█| 1000/1000 [00:00<00:00, 3641.
The correlation between Siderastrea sidera core A1A and Sand Key instrumental temperature is:
  correlation  p-value    signif. (α: 0.05)
-------------  ---------  -------------------
     -0.96559  < 1e-6     True

The anticorrelation is even stronger between coral Sr/Ca and Sand Key, which is not surprising given the gaps in the DRTO data that were filled with a simple interpolation.

Let’s repeat the analysis for core F1A:

corr = ms_interp.series_list[1].standardize().correlation(ms_interp.series_list[2].standardize())
print('The correlation between Siderastrea sidera core F1A and DRTO instrumental temperature is:')
print(corr)

corr = ms_interp.series_list[1].standardize().correlation(ms_interp.series_list[3].standardize())
print('The correlation between Siderastrea sidera core F1A and Sand Key instrumental temperature is:')
print(corr)
Evaluating association on surrogate pairs: 100%|█| 1000/1000 [00:00<00:00, 3917.
The correlation between Siderastrea sidera core F1A and DRTO instrumental temperature is:
  correlation  p-value    signif. (α: 0.05)
-------------  ---------  -------------------
    -0.705579  < 1e-6     True
Evaluating association on surrogate pairs: 100%|█| 1000/1000 [00:00<00:00, 3818.
The correlation between Siderastrea sidera core F1A and Sand Key instrumental temperature is:
  correlation  p-value    signif. (α: 0.05)
-------------  ---------  -------------------
    -0.968178  < 1e-6     True

The results are very similar between the two cores.

A timeseries view: Montastraea faveolata#

Let’s do the same with Montastraea faveolata B3-A:

fig,ax1 = plt.subplots(figsize=(15,4))
ax1.plot(df.iloc[:,0], df.iloc[:,7], label = "Montastraea faveolata B3-A", color = sns.xkcd_rgb['purple'])
ax1.set_xlabel('Year')
ax1.set_ylabel('Sr/Ca (mmol/mol)')
ax1.invert_yaxis()
ax2=ax1.twinx()
ax2.plot(df.iloc[:,0],df.iloc[:,10], label = 'DRTO SST', color = sns.xkcd_rgb['peach'])
ax2.plot(df.iloc[:,0],df.iloc[:,12], label = 'Sand Key SST', color = sns.xkcd_rgb['dark orange'])
ax2.set_ylabel('SST ($\circ$C)')
plt.grid(False)

fig.tight_layout()  # otherwise the right y-label is slightly clipped
fig.legend(bbox_to_anchor=(1.2, 1.05))
plt.show()
_images/9f54d8908a9b412853e1d83bce15d37b5a5fe9a16aa370a258f124e5a520ac96.png

Correlation between Montastraea faveolata and SST#

B3A = pyleo.Series(time=df.iloc[:,0], value= df.iloc[:,7], time_name = 'Year', time_unit = 'CE', value_name = 'Sr/Ca', value_unit = 'mmmol/mol', label = 'Montastraea faveolata B3-A', verbose = False)

ms_mf = pyleo.MultipleSeries([B3A,DRTO,SK])

Let’s place these series on a common time axis:

ms_mf_interp = ms_mf.common_time()

And let’s take the correlations!

corr = ms_mf_interp.series_list[0].standardize().correlation(ms_mf_interp.series_list[1].standardize())
print('The correlation between Montastraea faveolata core B3A and DRTO instrumental temperature is:')
print(corr)

corr = ms_mf_interp.series_list[0].standardize().correlation(ms_mf_interp.series_list[2].standardize())
print('The correlation between Montastraea faveolata core B3A and Sand Key instrumental temperature is:')
print(corr)
Evaluating association on surrogate pairs: 100%|█| 1000/1000 [00:00<00:00, 3768.
The correlation between Montastraea faveolata core B3A and DRTO instrumental temperature is:
  correlation    p-value  signif. (α: 0.05)
-------------  ---------  -------------------
    -0.622494       0.03  True
Evaluating association on surrogate pairs: 100%|█| 1000/1000 [00:00<00:00, 3854.
The correlation between Montastraea faveolata core B3A and Sand Key instrumental temperature is:
  correlation  p-value    signif. (α: 0.05)
-------------  ---------  -------------------
    -0.923575  < 1e-6     True

We observe the same strength of the anticorrelation.

A scatter plot view#

A more straighforward way to visualize this dataset is to plot Sr/Ca vs SST as in Figure 7 of the original manuscript. The figure also contains a panel showing seasonal variations in Sr/Ca and SST. Let’s start by pulling the data for this part of the figure:

def round_to_significant(x, significant_digits=3):
    # Shift the number so the significant part is left of the decimal point
    shift = significant_digits - 1 - math.floor(math.log10(abs(x)))
    factor = 10 ** shift
        # Round the number at the shifted position and shift back
    return round(x * factor) / factor

df_strip = pd.read_excel(url, sheet_name=1,header=[2])
df_strip['Year_Dec'] = df_strip['Year'].apply(lambda x: round_to_significant(x % 1, 3) if isinstance(x, (int, float)) else x)
grouped_df = df_strip.groupby('Year_Dec').mean()
grouped_df
Year Mean Sr/Ca (mmol/mol) Number of samples St. error of mean Mean Sr/Ca (mmol/mol).1 Number of samples.1 St. error of mean.1 Mean Sr/Ca (mmol/mol).2 Number of samples.2 St. error of mean.2 DRTO SST (ºC) St. error of mean.3 Sand Key SST (ºC) St. error of mean.4
Year_Dec
0.0417 1999.541667 9.109034 2.111111 0.008934 9.150712 1.333333 0.009633 9.305624 2.500000 0.015127 22.451185 0.028995 23.062052 0.034643
0.1250 1999.625000 9.133615 2.111111 0.010182 9.180347 1.277778 0.009690 9.328770 2.444444 0.017359 21.739958 0.026452 22.692976 0.036881
0.2080 1999.708333 9.098765 2.166667 0.012220 9.140562 1.277778 0.009979 9.300653 2.611111 0.016524 22.517117 0.032490 23.528907 0.040111
0.2920 1999.791667 9.024714 2.111111 0.013141 9.059087 1.277778 0.010395 9.248827 2.722222 0.015633 24.438476 0.027952 24.931675 0.027108
0.3750 1999.875000 8.948943 2.111111 0.009085 8.974847 1.277778 0.011020 9.194245 2.722222 0.015871 26.341956 0.032119 26.821058 0.031313
0.4580 1999.958333 8.873235 2.111111 0.006919 8.900051 1.352941 0.010505 9.140231 2.722222 0.014527 28.597185 0.026130 28.740994 0.024248
0.5420 2000.041667 8.822578 2.111111 0.007958 8.850342 1.352941 0.010950 9.107287 2.722222 0.015620 29.877780 0.017445 29.708116 0.019679
0.6250 2000.125000 8.811292 2.055556 0.010360 8.826928 1.352941 0.011434 9.074950 2.666667 0.017475 30.208851 0.014917 30.115725 0.023875
0.7080 1999.708333 8.824181 2.117647 0.009765 8.858771 1.352941 0.013244 9.109323 2.588235 0.015410 29.727717 0.017995 29.419857 0.023371
0.7920 1999.791667 8.890490 2.117647 0.008968 8.923891 1.352941 0.011075 9.147143 2.588235 0.013281 27.830348 0.029223 28.051541 0.022184
0.8750 1999.875000 8.976746 2.117647 0.012209 9.001760 1.294118 0.009745 9.205009 2.588235 0.014158 25.605335 0.024531 26.238214 0.027356
0.9580 1999.958333 9.052643 2.117647 0.009397 9.078462 1.352941 0.010100 9.264479 2.470588 0.012400 23.877666 0.028641 24.375051 0.032753

Now that we have all the data, let’s create our plot!

fig, axs = plt.subplots(2, 2, figsize=(10, 10))

#Plotting A1 S. siderea
axs[0,0].errorbar(df.iloc[:,10],df.iloc[:,1], xerr = df.iloc[:,11], yerr = df.iloc[:,3], marker= 'o', color = sns.xkcd_rgb['peach'], linestyle='None', label = 'DRTO')
axs[0,0].errorbar(df.iloc[:,12],df.iloc[:,1], xerr = df.iloc[:,13], yerr = df.iloc[:,3], marker= 'o', color = sns.xkcd_rgb['dark orange'], linestyle='None', label = 'Sand Key')
axs[0,0].set_title('A1 S. siderea')
axs[0,0].set_xlabel('SST ($^\circ$C)')
axs[0,0].set_ylabel('Sr/Ca (mmol/mol)')
axs[0,0].legend(loc= 'lower left')

#Plotting F1 S. siderea
axs[0,1].errorbar(df.iloc[:,10],df.iloc[:,4], xerr = df.iloc[:,11], yerr = df.iloc[:,6], marker= 'o', color = sns.xkcd_rgb['peach'], linestyle='None', label = 'DRTO')
axs[0,1].errorbar(df.iloc[:,12],df.iloc[:,4], xerr = df.iloc[:,13], yerr = df.iloc[:,6], marker= 'o', color = sns.xkcd_rgb['dark orange'], linestyle='None', label = 'Sand Key')
axs[0,1].set_title('F1 S. siderea')
axs[0,1].set_xlabel('SST ($^\circ$C)')
axs[0,1].set_ylabel('Sr/Ca (mmol/mol)')
axs[0,1].legend(loc= 'lower left')

#Plotting B3 M. faveolata
axs[1,0].errorbar(df.iloc[:,10],df.iloc[:,7], xerr = df.iloc[:,11], yerr = df.iloc[:,9], marker= 'o', color = sns.xkcd_rgb['peach'], linestyle='None', label = 'DRTO')
axs[1,0].errorbar(df.iloc[:,12],df.iloc[:,7], xerr = df.iloc[:,13], yerr = df.iloc[:,9], marker= 'o', color = sns.xkcd_rgb['dark orange'], linestyle='None', label = 'Sand Key')
axs[1,0].set_title('B3 M. faveolata')
axs[1,0].set_xlabel('SST ($^\circ$C)')
axs[1,0].set_ylabel('Sr/Ca (mmol/mol)')
axs[1,0].legend(loc= 'lower left')

#Plotting seasonal cycle
axs[1,1].plot(grouped_df.iloc[:,1], color = sns.xkcd_rgb['red'], label='A1')
axs[1,1].plot(grouped_df.iloc[:,4], color = sns.xkcd_rgb['wine red'], label='F1')
axs[1,1].plot(grouped_df.iloc[:,7], color = sns.xkcd_rgb['purple'], label='B3')
axs[1,1].set_ylim(axs[1, 1].get_ylim()[::-1])
axs[1, 1].legend(loc='upper left')
axs[1,1].set_ylabel('Sr/Ca (mmol/mol)')
axs[1,1].set_xticks(ticks=grouped_df.index.to_list()[0::2], labels=['Jan','Mar','May','Jul','Sep','Nov'], rotation=45)

ax2 = axs[1, 1].twinx()
ax2.plot(grouped_df.iloc[:,10], color = sns.xkcd_rgb['peach'], label='DRTO')
ax2.legend(loc='upper left', bbox_to_anchor=(0, 0.8))
ax2.set_ylabel('SST ($^\circ$C)')

# Adjust layout to prevent overlap
plt.tight_layout()
_images/322d4914e53f23db322b84c97229e5afc6c20eb2dbd7f2d88cd2a4aaab20174d.png

Given the strong relationship between coral Sr/Ca and SST at this location, the next logical step is to calibrate this paleothermomter. First, we will use a frequentist approach.

References#

DeLong, K.L., J.A. Flannery, C.R. Maupin, R.Z. Poore, and T.M. Quinn. 2011. A coral Sr/Ca calibration and replication study of two massive corals from the Gulf of Mexico. Palaeogeography, Palaeoclimatology, Palaeoecology, 307(1-4), 117-128. doi: 10.1016/j.palaeo.2011.05.005