Buoy Sea Surface Temperature Anomalies

Andrew Edwards and Andrea Hilborn

Last rendered on 23 October, 2026

Daily average sea surface temperature from buoys – climatologies and anomalies

# load_all()    # for developers
library(pacea)  # for users
library(dplyr)
library(tibble)  # Else prints all of a tibble
library(ggplot2)

In pacea we include daily average sea surface temperature (SST) that we have calculated from two-minute measurements from 19 buoys in Canadian Pacific waters, yielding over 200,000 daily means. Data are from Environment and Climate Change Canada, and Fisheries and Oceans Canada. The earliest data are from September 1987, and 18 buoys were still providing data as of August 2026.

See the buoys_vignette and the help file ?buoy_sst for background concerning the calculated daily mean sea surface temperatures and associated simple plotting functions in pacea. Here we focus on constructing climatologies and subsequent anomalies, to plot in a standardised way.

The locations of the buoys are:

If you want to quickly get a sense of current temperatures (and also across the wider northeast Pacific Ocean), check out Andrea Hilborn et al.’s Pacific_SST_Monitoring website. This complements pacea, which is suited for doing specific plots and analyses defined by the user.

Calculations and plotting

Default plot

We can easily calculate a climatology (by month) and subsequent anomalies for each buoy, and plot results:

all_buoys_anomalies <- calculate_anomalies(buoy_sst)

plot(all_buoys_anomalies)

The plot’s title describes that this plot is for the anomalies in April (the default) for all buoys, and the climatology was 1991-2020 where available. Buoys are arranged in a roughly north-to-south order (see map). So each square gives the SST anomaly for that buoy for April of that year. Empty (white) squares indicate not enough data available for that particular calculation.

The figure clearly shows that April 2015 and April 2016 were warmer than usual for April, for every buoy (with those years having the warmest two Aprils for almost every buoy). The bottom three buoys clearly have limited data; in particular the anomalies in 2020 for both Entrance English Bay and South Georgia Strait are 0, because the climatology for 1991-2020 only includes the value for 2020.

Absolute SST’s

To show the actual monthly average SST’s (rather than anomalies), again for the default month of April:

plot(all_buoys_anomalies,
     sst_plot = "mean")

Here you can roughly see the water getting warmer from north to south, noting that the three “NOMAD” buoys are much further offshore than the others, and look slightly cooler.

Full results for a specific buoy

If you specify a buoy, you get the monthly anomalies across the years (again with an automated title), with months running from top to bottom:

plot(all_buoys_anomalies,
     stn_id = "Halibut Bank")   # In the Strait of Georgia

You can check that the April 2016 high anomaly matches that shown in the first anomaly plot shown above.

Anomalies for all buoys for different months

You can specify a single month (the default being “4” for April); here is December:

plot(all_buoys_anomalies,
     months = 12)

Interestingly, in 2023 the December anomalies are all positive, whereas in the first anomaly plot above the April anomalies are all negative (for the available buoys in each case). Discuss. (We may use this example in future vignettes for future plots; stay tuned).

If you specify multiple consecutive months (e.g. 4:6), then the plot shows the anomalies averaged over those months (and the title is automatically correct):

plot(all_buoys_anomalies,
     months = 4:6)

The eagle-eyed among you will have spotted that the S. Georgia Strait buoy does not appear on this plot (it was second from bottom in the previous plots). We will come back to this later (briefly, the plot requires every specified month in months to have an anomaly available, else it shows a blank).

And if you want a wintertime-ish (not strictly ‘winter’) index that includes December, that can be done:

plot(all_buoys_anomalies,
     months = c(12, 1, 2, 3))

As stated in the title, the year on the x-axis is the year of the January.

If you specify months and a single buoy, the anomalies for those months are all shown individually:

plot(all_buoys_anomalies,
     months = c(12, 1, 2, 3),
     stn_id = "Halibut Bank")  

This plot can be used as a check to see what anomalies are available (if you are only interested in a particular buoy). It also shows explicitly that Dec-Mar is missing at least one month in 1993 (i.e. Dec 1992 - Mar 1993) and 1994, but has all four months available for 1995. That is why in the plot above that one, “Halibut Bank” has no anomaly shown for 1993 or 1994, but does for 1995.

Returning the values shown in the plot

Setting return_results = TRUE returns a list containing the plot and then the results used for the plot (and also automatically shows the plot). Repeating the previous plot with this setting:

res <- plot(all_buoys_anomalies,
            months = c(12, 1, 2, 3),
            stn_id = "Halibut Bank",
            return_results = TRUE)  

res$results
#> # A tibble: 124 × 9
#>    stn_id  year month sst_mean sst_n sst_anomaly month_as_factor sst_plot_value
#>    <fct>  <dbl> <dbl>    <dbl> <int>       <dbl> <fct>                    <dbl>
#>  1 C46146  1992    12     7.56    26      0.152  Dec                     0.152 
#>  2 C46146  1993     2     6.82    27     -0.0682 Feb                    -0.0682
#>  3 C46146  1993     3     7.89    30      0.0577 Mar                     0.0577
#>  4 C46146  1993    12     7.65    31      0.244  Dec                     0.244 
#>  5 C46146  1994     1     7.61    24      0.797  Jan                     0.797 
#>  6 C46146  1994    12     7.24    20     -0.165  Dec                    -0.165 
#>  7 C46146  1995     1     6.56    17     -0.247  Jan                    -0.247 
#>  8 C46146  1995     2     7.03    18      0.142  Feb                     0.142 
#>  9 C46146  1995     3     7.91    26      0.0768 Mar                     0.0768
#> 10 C46146  1996     1     6.69    31     -0.118  Jan                    -0.118 
#> # ℹ 114 more rows
#> # ℹ 1 more variable: label <chr>

So res is a list (res$plot will show the above plot again), and res$results is a tibble that includes the values in the plot, as sst_plot_value and then label for the rounded values that are shown numerically. You can then use these for analyses if desired.

That tibble contains results that depend on options used in the plotting. But we will now examine the full results generated at the start in the all_buoys_anomalies object.

Examining the climatology and anomalies

At the beginning we ran

all_buoys_anomalies <- calculate_anomalies(buoy_sst)

which we have plotted using various options. We now examine all_buoys_anomalies in detail:

all_buoys_anomalies
#> $climatology
#> # A tibble: 228 × 5
#>    stn_id month clim_sst_mean clim_sst_sd clim_sst_n
#>    <fct>  <dbl>         <dbl>       <dbl>      <int>
#>  1 C46184     1          6.24       0.913        718
#>  2 C46184     2          5.82       0.864        623
#>  3 C46184     3          5.65       0.849        692
#>  4 C46184     4          6.13       0.821        696
#>  5 C46184     5          7.52       1.21         658
#>  6 C46184     6         10.0        1.41         758
#>  7 C46184     7         12.5        1.38         839
#>  8 C46184     8         14.0        1.16         753
#>  9 C46184     9         13.5        1.23         727
#> 10 C46184    10         11.3        1.23         730
#> # ℹ 218 more rows
#> 
#> $anomalies
#> # A tibble: 7,341 × 6
#>    stn_id  year month sst_mean sst_n sst_anomaly
#>    <fct>  <dbl> <dbl>    <dbl> <int>       <dbl>
#>  1 C46184  1987     9    NA        0       NA   
#>  2 C46184  1987    10    10.1     29       -1.16
#>  3 C46184  1987    11    NA        0       NA   
#>  4 C46184  1987    12    NA        0       NA   
#>  5 C46184  1988     1    NA        0       NA   
#>  6 C46184  1988     2     4.57    24       -1.25
#>  7 C46184  1988     3     4.33    27       -1.32
#>  8 C46184  1988     4    NA        0       NA   
#>  9 C46184  1988     5     6.18    31       -1.34
#> 10 C46184  1988     6     7.97    22       -2.04
#> # ℹ 7,331 more rows
#> 
#> $climatology_years
#>  [1] 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005
#> [16] 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020
#> 
#> $climatology_time
#> [1] "month"
#> 
#> attr(,"class")
#> [1] "pacea_buoy_anomalies_list" "list"

This is a list object, with $climatology giving, for each buoy and month, a mean SST (clim_sst_mean), standard deviation (clim_sst_sd), and sample size (clim_sst_n) of the values used for that buoy-month combination. The default for calculate_anomalies() creates the monthly climatology for each buoy based on the years 1991-2020 (details of other arguments will be investigated later).

The $anomalies tibble gives, for each buoy-year-month combination, the anomaly for that month compared to the climatology. The first buoy shown is “C46184” which is the North NOMAD buoy. For October 1987 the mean SST was 10.1 degC (sst_mean), which was calculated from 29 days (sst_n) in that month. The resulting anomaly from the climatology is -1.16 (sst_anomaly). This can be manually checked – the climatology for buoy C46184 in October is a mean SST of 11.3 (the top tibble), the mean SST in October 1987 was 10.1, which is -1.16 less (sst_anomaly). This should help you understand how calculations are done.

So the same calculations are done for all buoy-year-month combinations. There happen to be a few NA’s early on in the data for this buoy (more on that later).

The vector $climatology_years gives the years prescribed for the climatology in the calculate_anomalies() call, for which the default is 1991:2020.

The attribute shown (under attr(, "class")) given just tells R to use our customised plotting function on these results, which is automatically called with plot().

Further calculation and plotting options

The calculate_anomalies(buoy_sst) uses the function calculate_anomalies.pacea_buoy(buoy_sst), which has further arguments that users can tweak. See ?calculate_anomalies.pacea_buoy(buoy_sst) for details, but briefly these allow users to change:

We (Elise Keppel and Andy) pragmatically decided on requiring 15 days (roughly half) of daily SST within a month, and a maximum consecutive number of missing days of 6. This avoids having, say, 15 days of data but all at the beginning of the month – they need to be somewhat spread out. These are the defaults, but can be changed to any number.

The resulting plot(all_buoys_anomalies) uses plot.pacea_buoy_anomalies_list(all_buoys_anomalies), which also has arguments that users can tweak. See ?plot.pacea_buoy_anomalies_list for details, but briefly these allow users to change:

We suggest the following workflow if users want to tweak some of the defaults (explained in detail below):

The sst_plot = "counts" setting shows how many daily counts are available to calculate each anomaly over the defined period. Say we want to look at the North NOMAD buoy over Dec-Feb, we would plot the counts for those three months:

plot(all_buoys_anomalies,
     stn_id = "East Dellwood Knolls",
     sst_plot = "count",
     months = c(12, 1, 2))

So every month here has SST values for at least 16 days. Say we wanted to instead insist on having 25 days of values for each month, we could plot the same figure with a different breaks in the colour scale (this also changes the actual colour palette for some reason):

plot(all_buoys_anomalies,
     stn_id = "East Dellwood Knolls",
     sst_plot = "count",
     months = c(12, 1, 2),
     count_breaks = c(0, 25, 31))

which shows the months that would get dropped if we used the more stringent criterion of 25 days per month. We can apply this criterion by recalculating the climatology and anomalies:

all_buoys_anomalies_require_25_days <- calculate_anomalies(buoy_sst,
                                                           min_days_per_month = 25)
plot(all_buoys_anomalies_require_25_days,
     stn_id = "East Dellwood Knolls",
     months = c(12, 1, 2))

Plotting the same figure with the default criterion of 15 days for each month, we can see the extra months that get calculated (e.g. Dec 1989, Feb and Dec 1994):

plot(all_buoys_anomalies,
     stn_id = "East Dellwood Knolls",
     months = c(12, 1, 2))

The extreme case of using all available data would be min_days_per_months = 0 and max_consecutive_missing_days = 32 in calculate_anomalies(), though this is not recommended (but can be used to check the upper limit of how much data is available).

We can also change the number of required months for a plot that averages over multiple months. Above, we noted that the S. Georgia Strait buoy does not appear in this plot:

plot(all_buoys_anomalies,
     months = 4:6)

This is because it does not have a climatology for Jun therefore we cannot do a mean climatology for Apr-Jun:

all_buoys_anomalies$climatology %>%
  filter(stn_id == "C46303") %>%
  a()
#>    stn_id month clim_sst_mean clim_sst_sd clim_sst_n
#> 1  C46303     1      7.258448   0.6485600         28
#> 2  C46303     2      6.676682   0.5304471         29
#> 3  C46303     3      7.468842   0.5445882         31
#> 4  C46303     4      9.875364   1.0869259         30
#> 5  C46303     5     12.608609   1.3637622         31
#> 6  C46303     6            NA          NA          0
#> 7  C46303     7            NA          NA          0
#> 8  C46303     8     17.995327   0.9621684         30
#> 9  C46303     9     15.958602   1.6339430         30
#> 10 C46303    10     11.509418   1.5451591         59
#> 11 C46303    11      8.493295   0.8989438         60
#> 12 C46303    12      7.407808   0.6118972         62

and the anomalies for 2022 (as an example) are:

all_buoys_anomalies$anomalies %>%
  filter(year == 2022,
         stn_id == "C46303") %>%
  a()
#>    stn_id year month  sst_mean sst_n sst_anomaly
#> 1  C46303 2022     1  5.718514    25  -1.5399335
#> 2  C46303 2022     2  6.331062    26  -0.3456200
#> 3  C46303 2022     3  7.589869    31   0.1210265
#> 4  C46303 2022     4  9.117719    30  -0.7576448
#> 5  C46303 2022     5 11.449919    31  -1.1586901
#> 6  C46303 2022     6 14.067619    29          NA
#> 7  C46303 2022     7 17.680563    28          NA
#> 8  C46303 2022     8 19.550376    30   1.5550494
#> 9  C46303 2022     9 16.231981    29   0.2733790
#> 10 C46303 2022    10 13.307402    28   1.7979844
#> 11 C46303 2022    11  8.217504    28  -0.2757913
#> 12 C46303 2022    12  6.935964    20  -0.4718443

where you can see that Jun and Jul have SST values, but no anomaly because there is no climatology for those months for this buoy (due to limited data, and the default climatology years of 1991-2020).

However, if we specify that we only require require_requested_months = 2 months to be available to make a climatology and anomalies for Apr-Jun

plot(all_buoys_anomalies,
     months = 4:6,
     require_requested_months = 2)

there are enough values, and so anomalies for this buoy can be calculated, but beware that they are not truly anomalies that cover all of Apr-Jun.

Given the huge number of options that users may wish to explore (many buoys, many years, many possible combinations of months), users may wish to relax or tighten the defaults, and have the power to do so.