usdata

NOAA · CoastWatch Blended Sea Surface Temperature

Sea-surface temperature

NOAA geo-polar blended daily SST analysis (day and night) on a global 5 km grid, ERDDAP dataset noaacwBLENDEDsstDNDaily on the CoastWatch server, with server-side spatial, temporal, and variable subsetting.

CSV with units rowSince v0.5
The walkthrough's first look at sea-surface temperature

At a glance

Spatial
0.05 degree global grid, about 5 km
Time step
Daily
Updates
Daily
Files
CSV with units row
Selection
Grid centers and timestamps inside the requested bounds; optional stride
You provide
BBox or location, and both timestamps
Full description

NOAA geo-polar blended daily SST analysis (day and night) on a global 5 km grid, ERDDAP dataset noaacwBLENDEDsstDNDaily on the CoastWatch server, with server-side spatial, temporal, and variable subsetting. Raw CSV subsets retain grid coordinates and units without volatile NetCDF history.

Quick start

Terminal

python -m pip install "usdata[pandas]"
usdata fetch noaa:coastwatch-sst \
  --start 2024-05-06T12:00:00Z \
  --end 2024-05-06T12:00:00Z \
  --bbox -81.5,28.0,-74.0,32.0 \
  --vars analysed_sst

Python

from usdata import build_query, fetch, get

items = fetch(
    get("noaa:coastwatch-sst"),
    build_query(
        start="2024-05-06T12:00:00Z",
        end="2024-05-06T12:00:00Z",
        bbox=(-81.5, 28.0, -74.0, 32.0),
        variables=["analysed_sst"],
    ),
)
data = items[0].open()

Manifest

# dataset.yaml, then: usdata pull dataset.yaml
name: florida-gulf-stream-sst
sources:
  - dataset: noaa:coastwatch-sst
    start: 2024-05-06T12:00:00Z
    end: 2024-05-06T12:00:00Z
    bbox: {west: -81.5, south: 28.0, east: -74.0, north: 32.0}
    variables: [analysed_sst]

The same query the walkthrough below ran. The pandas extra opens the files.

Saved results from a run against the live service; the notebook records when it ran and the checksums of what it read. Run it yourself.

NOAA's Geo-polar Blended Sea Surface Temperature analysis merges infrared and microwave retrievals from polar-orbiting and geostationary satellites into one gap-free daily map of the global ocean on a 0.05 degree (about 5 km) grid. NOAA/NESDIS produces it, and the CoastWatch ERDDAP server serves it as noaacwBLENDEDsstDNDaily, with the day's analysis stamped 12:00 UTC. ERDDAP subsets on the server, so one request returns one CSV holding just the grid cells, times, and variables asked for.

This walkthrough pulls one day's analysis, 6 May 2024, for a 7.5 by 4 degree box off northeast Florida and Georgia that the Gulf Stream crosses. It needs usdata[pandas] and matplotlib, and downloads one 550 kB file in a few seconds.

from datetime import UTC, datetime
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.patches import Rectangle

import usdata
from usdata import cite_lockfile, pull, verify

# One figure style for every usdata notebook, so previews look alike.
plt.rcParams.update(
    {
        "figure.figsize": (8, 4.5),
        "figure.dpi": 120,
        "axes.spines.top": False,
        "axes.spines.right": False,
        "axes.grid": True,
        "grid.alpha": 0.3,
        "font.size": 10,
    }
)
manifest = Path("dataset.yaml")
print("Executed (UTC):", datetime.now(UTC).isoformat(timespec="seconds"))
print(f"usdata {usdata.__version__}; pandas {pd.__version__}")
Executed (UTC): 2026-09-24T06:15:38+00:00
usdata 0.26.0; pandas 3.0.6

Select

The manifest names a bounding box, a time window, and one variable. ERDDAP returns only grid centres that fall inside the box, so the edges here select centres from 28.025 to 31.975° N and 81.475 to 74.025° W: 80 rows by 150 columns. Both timestamps are 12:00 UTC because that is when each day's analysis is stamped. A bare date as the end runs through that day, so start: 2024-05-06 and end: 2024-05-06 would also catch it, but a window from midnight to midnight would select nothing. analysed_sst is the default variable; analysis_error, sea_ice_fraction, and mask can be added to the list. A query over 1,000,000 rows has to shrink or use params: {stride: n}.

print(manifest.read_text())
name: florida-gulf-stream-sst
sources:
  - dataset: noaa:coastwatch-sst
    start: 2024-05-06T12:00:00Z
    end: 2024-05-06T12:00:00Z
    bbox: {west: -81.5, south: 28.0, east: -74.0, north: 32.0}
    variables: [analysed_sst]

What arrives

The first pull downloads one CSV and writes dataset.lock.json beside the manifest, pinning its checksum. The source URL is the ERDDAP request, with the box already snapped to grid centres.

result = pull(manifest)
(item,) = result.fetched
print("File:", item.path.name)
print("Bytes:", item.provenance.size)
print("Source:", item.provenance.source_url)
print("Retrieved (UTC):", item.provenance.retrieved_at.isoformat(timespec="seconds"))
print("Checksum:", item.provenance.checksum)
File: sst_aabc57cc35c5f9276be9.csv
Bytes: 548837
Source: https://coastwatch.noaa.gov/erddap/griddap/noaacwBLENDEDsstDNDaily.csv?analysed_sst%5B(2024-05-06T12:00:00Z):1:(2024-05-06T12:00:00Z)%5D%5B(28.025):1:(31.975)%5D%5B(-81.475):1:(-74.025)%5D
Retrieved (UTC): 2026-09-24T06:15:45+00:00
Checksum: sha256:1bf285d1856013471071d063a6f5549cb44dda7c46c6f5af53a00529d1403b67

Open

ERDDAP writes a units row under the CSV header. item.open() picks the units-row reader, which moves that row into attrs["units"] instead of reading it as an observation. The table is long: one row per grid centre and time, with latitude and longitude the cell centre in degrees, time in UTC, and analysed_sst in degrees Celsius. Land cells are present with a missing value. Pivoting on latitude and longitude turns the table back into a grid.

frame = item.open_csv(parse_dates=["time"])
print("Units:", frame.attrs["units"])
print(f"{len(frame):,} rows; {frame['analysed_sst'].isna().sum()} land cells without a value")
grid = frame.pivot(index="latitude", columns="longitude", values="analysed_sst")
print(f"Grid: {grid.shape[0]} latitudes x {grid.shape[1]} longitudes")
frame.head()
Units: {'time': 'UTC', 'latitude': 'degrees_north', 'longitude': 'degrees_east', 'analysed_sst': 'degree_C'}
12,000 rows; 540 land cells without a value
Grid: 80 latitudes x 150 longitudes
time latitude longitude analysed_sst
0 2024-05-06 12:00:00+00:00 28.025 -81.475 NaN
1 2024-05-06 12:00:00+00:00 28.025 -81.425 NaN
2 2024-05-06 12:00:00+00:00 28.025 -81.375 NaN
3 2024-05-06 12:00:00+00:00 28.025 -81.325 NaN
4 2024-05-06 12:00:00+00:00 28.025 -81.275 NaN

A first look

The day's analysis as a map. Land is left blank, and the small square marks the four-cell sample an earlier version of this example pulled.

fig, ax = plt.subplots(layout="constrained")
ax.set_facecolor("#e6e1d6")
mesh = ax.pcolormesh(grid.columns, grid.index, grid.to_numpy(), cmap="RdYlBu_r", shading="nearest")
ax.add_patch(Rectangle((-80.1, 30.0), 0.1, 0.1, fill=False, edgecolor="black", linewidth=1.2))
ax.set_aspect(1 / np.cos(np.radians(30)))
ax.grid(False)
ax.set_xlabel("Longitude (degrees east)")
ax.set_ylabel("Latitude (degrees north)")
ax.set_title("CoastWatch blended SST off northeast Florida, 6 May 2024")
fig.colorbar(mesh, ax=ax, label="Sea-surface temperature (°C)")
plt.show()
Saved plot from Sea-surface temperature
row = grid.loc[30.025]
core = row.idxmax()
print(f"Along 30.025° N: warmest {row.max():.2f} °C at {-core}° W")
print(f"  shelf, west of the core: coolest {row[row.index < core].min():.2f} °C")
print(f"  offshore, east of the core: coolest {row[row.index > core].min():.2f} °C")
sample = frame[frame["latitude"].between(30.0, 30.1) & frame["longitude"].between(-80.1, -80.0)]
low, high = sample["analysed_sst"].agg(["min", "max"])
print(f"Four-cell sample: {low:.2f} to {high:.2f} °C")
Along 30.025° N: warmest 27.07 °C at 79.925° W
  shelf, west of the core: coolest 24.07 °C
  offshore, east of the core: coolest 23.63 °C
Four-cell sample: 26.78 to 26.99 °C

The Gulf Stream is the warm band running north a few tens of kilometres off the coast, about 27 °C at its core. Along 30° N the shelf water to its west is 3 °C cooler, and the open ocean to its east as much as 3.4 °C cooler. North of 30° N the stream bends away to the northeast, and a warm tongue reaches east from it to about 77° W. The four-cell sample sits in the core, so its average says little about the region around it. The warm cells on the coast near 28.5° N are shallow lagoon water behind Cape Canaveral, not ocean.

Pin and cite

verify checks the cached file against the lockfile's checksum. NOAA can revise the analysis, so keep the manifest and lockfile with your results; the citation below is what a methods section needs, and usdata cite dataset.yaml prints the same.

assert verify(manifest) == []
for citation in cite_lockfile(manifest):
    print(citation.as_text())
noaa:coastwatch-sst
  NOAA/NESDIS Office of Satellite Products and Operations, Geo-polar Blended Sea Surface Temperature Analysis (noaacwBLENDEDsstDNDaily), accessed via usdata
  homepage: https://coastwatch.noaa.gov/erddap/griddap/noaacwBLENDEDsstDNDaily.html
  license: GHRSST free and open data
  terms: https://coastwatch.noaa.gov/erddap/info/noaacwBLENDEDsstDNDaily/index.html
  retrieved: 2026-09-24; 1 checksummed asset (548,837 bytes) pinned by usdata 0.26.0
  sources: 1

What was awkward

  • Each day's analysis is stamped 12:00 UTC, not midnight, and the time axis has gaps. An exact timestamp has to say 12:00; a midnight-to-midnight window matches no analysis and returns no assets.
  • Box edges select grid centres inside them, so the first and last centres sit 0.025° inside the requested bounds; the source URL shows the snapped request.
  • The CSV is long, one row per cell, and carries land cells as missing values; a map needs a pivot first.
  • The ERDDAP CSV writes temperatures such as 25.349995, floating-point residue of the stored values, so printed values need rounding.
  • The grid is in plain degrees. Without a map projection or coastline, the figure's only coastline is where the land cells have no value.

Reference

Cite as NOAA/NESDIS Office of Satellite Products and Operations, Geo-polar Blended Sea Surface Temperature Analysis (noaacwBLENDEDsstDNDaily), accessed via usdata