
Methods
The definition of the index, the satellite data it is built from, the step-by-step recipe used for every monthly update, the EOF pattern itself, and a short script that reproduces it.
1 · Definition
The North Pacific Gyre Oscillation (NPGO) index is the principal component of the second EOF of monthly sea surface height anomalies (SSHa) over the Northeast Pacific, 180°–110°W and 25°–62°N. It is reported in standard-deviation units.
The spatial pattern (the EOF) was computed once, from a regional ocean model hindcast of 1950–2004 (Di Lorenzo et al. 2008). That pattern has been held fixed ever since. Every month, satellite altimetry SSHa is projected onto it to extend the index forward. Recomputing the EOF each month would shift the historical values that are cited in the literature, so it is never done.
The index therefore has three segments:
| Period | Source |
|---|---|
| Jan 1950 – Dec 2004 | Model hindcast principal component (Di Lorenzo et al. 2008) |
| Jan 2005 – Dec 2022 | Satellite SSHa projected onto the fixed pattern; frozen, never rewritten |
| Jan 2023 – present | Satellite SSHa projected onto the fixed pattern; recomputed at every monthly update |
2 · Data sources
Satellite sea level from the Copernicus Marine Service (CMEMS): gridded L4 multi-satellite products at
0.125°, variable sla (sea level anomaly).
| Stream | Product | Dataset | Cadence | Role |
|---|---|---|---|---|
| Delayed time (DT) | SEALEVEL_GLO_PHY_L4_MY_008_047 | cmems_obs-sl_glo_phy-ssh_my_allsat-l4-duacs-0.125deg_P1M-m | monthly, 1993– | The backbone. Lags the present by 8–12 months. |
| Near real time (NRT) | SEALEVEL_GLO_PHY_L4_NRT_008_046 | cmems_obs-sl_glo_phy-ssh_nrt_allsat-l4-duacs-0.125deg_P1D | daily | The most recent months. Provisional. |
Both are free with a Copernicus Marine account at data.marine.copernicus.eu.
The NPGO EOF pattern is provided as NPGO_EOF.nc. It holds
eof(lon, lat) on a 255 × 201 grid (about 0.25° × 0.22°,
174.7°–111.3°W, 27.1°–61.1°N) and an ocean mask.
3 · Procedure
sla fields and the NRT daily fields over the domain.pc(t) = Σxy eof(x,y) · ssha(x,y,t), summing over the points that are valid in both the pattern and the data for the whole record.4 · Code
The script below reproduces the satellite-era update with copernicusmarine, xarray,
numpy and pandas. It downloads only the Northeast Pacific box (a few hundred MB for the
full DT record). Download npgo_update.py.
# npgo_update.py -- project CMEMS sea level anomalies onto the NPGO pattern
#
# Minimal reproduction of the satellite-era NPGO update described at
# https://www.npgo.org/method.html. Needs: copernicusmarine, xarray, numpy,
# pandas, and a free Copernicus Marine account (run `copernicusmarine login`
# once). Place NPGO_EOF.nc and NPGO.txt (both from npgo.org) alongside.
#
# Cite Di Lorenzo et al. (2008), GRL, doi:10.1029/2007GL032838.
import copernicusmarine, numpy as np, pandas as pd, xarray as xr
BOX = dict(minimum_longitude=-180, maximum_longitude=-110,
minimum_latitude=25, maximum_latitude=62)
# 1. download DT monthly (1993-) and NRT daily (recent) SLA over the box
copernicusmarine.subset(
dataset_id="cmems_obs-sl_glo_phy-ssh_my_allsat-l4-duacs-0.125deg_P1M-m",
variables=["sla"], output_filename="sla_dt.nc", **BOX)
copernicusmarine.subset(
dataset_id="cmems_obs-sl_glo_phy-ssh_nrt_allsat-l4-duacs-0.125deg_P1D",
variables=["sla"], output_filename="sla_nrt.nc", **BOX)
# 2. monthly means; 3. splice NRT after the last DT month
dt = xr.open_dataset("sla_dt.nc")["sla"].resample(time="1MS").mean()
nrt = xr.open_dataset("sla_nrt.nc")["sla"].resample(time="1MS").mean(skipna=False)
sla = xr.concat([dt, nrt.sel(time=nrt.time > dt.time[-1])], dim="time")
# 4. anomalies relative to the 1993-2004 monthly climatology
clim = sla.sel(time=slice("1993", "2004")).groupby("time.month").mean()
ssha = sla.groupby("time.month") - clim
# 5. regrid onto the EOF grid; 6. project
eof = xr.open_dataset("NPGO_EOF.nc")
pattern = eof["eof"] * eof["mask"]
ssha_i = ssha.interp(longitude=eof.lon, latitude=eof.lat) # bilinear
valid = np.isfinite(pattern) & np.isfinite(ssha_i).all("time") # common points
pc = (ssha_i.where(valid) * pattern.where(valid)).sum(("lon", "lat"))
# 7. normalize to the published index over the overlap
ref = np.loadtxt("NPGO.txt", comments="#") # year month value
ref_s = pd.Series(ref[:, 2], index=pd.PeriodIndex(
[pd.Period(year=int(y), month=int(m), freq="M") for y, m in ref[:, :2]]))
pc_s = pd.Series(pc.values, index=pd.PeriodIndex(pc.time.values, freq="M"))
ov = pc_s.index.intersection(ref_s.index)
npgo_sat = (pc_s - pc_s[ov].mean()) / pc_s[ov].std() * ref_s[ov].std() + ref_s[ov].mean()
# 8. append the months beyond the published index
npgo = pd.concat([ref_s, npgo_sat[npgo_sat.index > ref_s.index[-1]]])
print(npgo.tail(6))
Run on the delayed-time archive (Jan 1993 – Dec 2025), this script reproduces the published index with a correlation of 0.997 over 2005–2025 and agrees with the newest months to three decimals. Over 1993–2004 the correlation is 0.98, because the published values for those years come from the model hindcast rather than from satellite data. Small residual differences come from the operational pipeline's exact land mask, regridding and normalization window.
The same projection in MATLAB, given ssha(x,y,t) already on the EOF grid:
load NPGO_EOF npgo % npgo.eof, npgo.mask, npgo.lon, npgo.lat
mask = mean(ssha,3) .* npgo.mask; mask(~isnan(mask)) = 1;
E = reshape(ssha .* mask, [], size(ssha,3)); E = E(~isnan(E(:,1)),:)';
E2 = reshape(npgo.eof .* mask, [], 1); E2 = E2(~isnan(E2));
pc = E * E2; % then normalize to NPGO.txt over the overlap
5 · Models & projections
The NPGO is an observational diagnostic, not a forecast. The recipe above, however, works on any gridded SSH field, which is how the index is estimated in climate-model simulations and projections:
zos) over the same domain.Seasonal-to-interannual predictability of the NPGO comes mostly through the atmosphere: the index is the oceanic expression of the North Pacific Oscillation and is coupled to central Pacific ENSO (Di Lorenzo et al. 2010; Joh and Di Lorenzo 2017). Di Lorenzo et al. (2023) reviews the mechanisms.
6 · References
Last revised September 2026. Questions about the method: e.dilorenzo@brown.edu.