How to Analyse a Sea-Ice Extent Time Series in Python

On this page

Problem statement

The National Snow and Ice Data Center publishes the daily extent of Arctic and Antarctic sea ice since 1978 as a small CSV file, and it is the most quoted climate record there is. It is also easy to analyse badly. The file is not a clean daily series: for its first nine years it has a value every other day, there is a six-week hole in the winter of 1987–88, the most recent weeks come from a different satellite product and are revised, and the file changes every day it is downloaded. A trend, a record low or a "lowest since" claim computed without handling those will be wrong in ways that are hard to spot.

This page reads the NSIDC Sea Ice Index (G02135, version 4) daily extent for the Arctic, cleans it into a series you can compute with, and measures the September minimum, its trend and its ranking — with every number tied to a fixed end date so it reproduces.

Quick answer

Read the CSV, skip its units row, fix the end date, put it on a daily index, and work from monthly means for trends:

d = pd.read_csv("data/N_seaice_extent_daily_v4.0.csv", skiprows=[1], skipinitialspace=True)
d.columns = d.columns.str.strip()
d["date"] = pd.to_datetime(dict(year=d.Year, month=d.Month, day=d.Day))
s = d[(d.date >= "1979-01-01") & (d.date <= END)].set_index("date")["Extent"]
sept = s[s.index.month == 9].groupby(s[s.index.month == 9].index.year).mean()
slope = np.polyfit(sept.index, sept.values, 1)[0] * 10
base = sept.loc[1981:2010].mean()
print(f"September mean extent: 1979 {sept.loc[1979]:.2f}, 2012 {sept.loc[2012]:.2f}, 2025 {sept.loc[2025]:.2f} million km²")
print(f"trend {slope:+.2f} million km² per decade = {slope / base:+.1%} of the 1981-2010 mean per decade")
September mean extent: 1979 7.05, 2012 3.57, 2025 4.75 million km²
trend -0.76 million km² per decade = -11.9% of the 1981-2010 mean per decade
Daily Arctic sea-ice extent by day of year for every year from 1979 to 2025 in grey, the 1981 to 2010 median and 10th to 90th percentile band in blue, 2012 in red reaching about 3.4 million km² in mid-September, and 2025 in dark blue below the band almost all year. Daily Arctic sea-ice extent by day of year for every year from 1979 to 2025 in grey, the 1981 to 2010 median and 10th to 90th percentile band in blue, 2012 in red reaching about 3.4 million km² in mid-September, and 2025 in dark blue below the band almost all year.
5-day means from the NSIDC Sea Ice Index v4, Northern Hemisphere, to 31 December 2025.

Read it as it is, find gaps, smooth as NSIDC does, rank minima

From the NSIDC daily extent CSV to a September trend that reproduces. from a moving file to a fixed numberread the CSVskip the units rowfix the enddate31 Dec 2025daily index1,617 missing days5-day meansNSIDC conventionmonthly trend−0.76 M km² /decadeThe file grows every day; the end date is what makes the result repeatable spatialworkflow.io
Five steps from a file that changes daily to a number that does not.

1. Read the file as it is

The first row is a header, the second a row of units, and the last column a list of source files, sometimes quoted, sometimes not. skiprows=[1] drops the units and skipinitialspace=True strips the padding the columns are aligned with:

print(d.columns.tolist())
print(d[["date", "Extent", "Missing"]].tail(3).to_string(index=False))
print("rows after", END, ":", int((d.date > END).sum()), "- excluded so the numbers reproduce")
['Year', 'Month', 'Day', 'Extent', 'Missing', 'Source Data', 'date']
      date  Extent  Missing
2026-09-27   5.228      0.0
2026-09-28   5.333      0.0
2026-09-29   5.337      0.0
rows after 2025-12-31 : 272 - excluded so the numbers reproduce

The file is regenerated every day with the newest values appended, and the last few weeks are near-real-time data from a different product. Fixing an end date — here the last day of 2025 — is what makes a result reproducible.

2. Find the gaps

gaps = s.index.to_series().diff().dt.days
print(gaps.value_counts().sort_index().to_string())
big = gaps[gaps > 2]
print("longest gap:", int(big.max()), "days, ending", big.idxmax().date())
date
1.0     13971
2.0      1576
42.0        1
longest gap: 42 days, ending 1988-01-13

Until August 1987 the record has a value every second day — the early satellite sensor observed on alternate days. Then there is a 42-day hole from December 1987 into January 1988 with no usable satellite data at all. Any statistic that assumes one value per day counts the 1980s at half weight.

3. Put it on a daily index and smooth as NSIDC does

daily = s.asfreq("D")
filled = daily.interpolate(limit=3)                     # bridge the alternate days, not the 1987-88 hole
five = filled.rolling(5).mean()                         # NSIDC quotes daily values as 5-day trailing means
print(f"missing days on the daily index: {daily.isna().sum():,}; still missing after filling gaps of up to 3 days: {filled.isna().sum()}")
missing days on the daily index: 1,617; still missing after filling gaps of up to 3 days: 38

4. Rank the minima

The annual minimum is usually in September. Rank years by their lowest 5-day mean:

minima = five.groupby(five.index.year).min()
dates = five.groupby(five.index.year).idxmin()
top = minima.loc[1979:].nsmallest(5)
for y, v in top.items():
    print(f"{y}: {v:.2f} million km² on {dates[y]:%d %B}")
2012: 3.39 million km² on 17 September
2020: 3.82 million km² on 16 September
2007: 4.15 million km² on 18 September
2016: 4.17 million km² on 10 September
2019: 4.19 million km² on 18 September

2012 remains the lowest in the record by a wide margin; the years that follow it are spread across the last two decades. A claim that a year was "the second lowest" depends on whether it is ranked by daily, 5-day or monthly values, so say which.

5. Measure the trend on monthly means

Daily values are autocorrelated and the early record is half-sampled; monthly means are neither. The September trend from the quick answer, and March for comparison:

mar = s[s.index.month == 3].groupby(s[s.index.month == 3].index.year).mean()
for label, series in (("March (maximum)", mar), ("September (minimum)", sept)):
    b = np.polyfit(series.index, series.values, 1)[0] * 10
    print(f"{label:20} {b:+.2f} million km² per decade ({b / series.loc[1981:2010].mean():+.1%} of 1981-2010 per decade)")
March (maximum)      -0.38 million km² per decade (-2.5% of 1981-2010 per decade)
September (minimum)  -0.76 million km² per decade (-11.9% of 1981-2010 per decade)
Bar chart of September mean Arctic sea-ice extent from 1979 to 2025, falling from about 7 to between 4 and 5 million km², 2012 in red as the lowest at 3.57, with a dashed trend line of minus 0.76 million km² per decade. Bar chart of September mean Arctic sea-ice extent from 1979 to 2025, falling from about 7 to between 4 and 5 million km², 2012 in red as the lowest at 3.57, with a dashed trend line of minus 0.76 million km² per decade.
September means from the daily NSIDC values, 1979–2025.

The summer minimum is shrinking twice as fast as the winter maximum in absolute terms, and almost five times as fast relative to its own average — which is why the September record is the one usually shown.

A climatology band like NSIDC's chart

The data used on this page

NSIDC Sea Ice Index, version 4 (G02135), daily extent for the Northern Hemisphere, from NSIDC / NOAA at noaadata.apps.nsidc.org. Fetterer, F., K. Knowles, W. N. Meier, M. Savoie, and A. K. Windnagel. Sea Ice Index, Version 4. Boulder, Colorado USA. NSIDC: National Snow and Ice Data Center. Values used here end on 31 December 2025.

from pathlib import Path

import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
UA = {"User-Agent": "spatialworkflow.io guide (https://www.spatialworkflow.io)"}
URL = "https://noaadata.apps.nsidc.org/NOAA/G02135/north/daily/data/N_seaice_extent_daily_v4.0.csv"
if not Path("data/N_seaice_extent_daily_v4.0.csv").exists():
    Path("data/N_seaice_extent_daily_v4.0.csv").write_bytes(requests.get(URL, headers=UA, timeout=120).content)
END = "2025-12-31"                                       # fixed, so the numbers on this page reproduce
print(Path("data/N_seaice_extent_daily_v4.0.csv").stat().st_size > 1_000_000)
True

Example 1: a climatology band, the way the NSIDC chart draws it

The familiar chart shows each year against the 1981–2010 median and interdecile range by day of year:

doy = five.index.dayofyear
clim = five[(five.index.year >= 1981) & (five.index.year <= 2010)]
band = clim.groupby(clim.index.dayofyear).quantile([0.1, 0.5, 0.9]).unstack()
y25 = five["2025"]
below = (y25.values < band.loc[y25.index.dayofyear, 0.1].values).mean()
print(f"2025 days below the 1981-2010 10th percentile: {below:.0%}")
print(band.loc[[60, 258]].round(2).rename(index={60: "1 March", 258: "15 September"}).to_string())
2025 days below the 1981-2010 10th percentile: 100%
                0.1    0.5    0.9
date                             
1 March       14.83  15.53  16.03
15 September   5.10   6.39   7.26

Example 2: how much the smoothing and the sampling change a record

raw_min = s.groupby(s.index.year).min().loc[1979:2025]
print("lowest raw daily value:", raw_min.idxmin(), round(raw_min.min(), 3))
print("lowest 5-day mean:     ", minima.loc[1979:].idxmin(), round(minima.loc[1979:].min(), 3))
early = minima.loc[1979:1986]
print(f"1979-86 minima from alternate-day data, interpolated: mean {early.mean():.2f} million km²")
lowest raw daily value: 2012 3.34
lowest 5-day mean:      2012 3.387
1979-86 minima from alternate-day data, interpolated: mean 6.98 million km²

Smoothing raises every minimum slightly and can reorder close years; quote the method with the number.

What extent counts

Sea-ice extent is the total area of grid cells with at least 15% ice concentration — a threshold count, not the area of ice itself (see sea-ice extent versus area). The daily series is computed from passive microwave satellites flown since late 1978, and the record stitches several sensors together; the alternate-day sampling and the 1987–88 gap are the seams that show.

The seams in the NSIDC daily sea-ice extent record and how to handle each. where the record is not a daily seriesperiodwhat the file holdshandle it byOct 1978 – Aug 1987every other dayinterpolate gaps ≤ 3 daysDec 1987 – Jan 198842 days missingleave missingmost of the recorddaily valuesuse as they arelast few weeksnear-real-timecut at a fixed dateCounting rows counts the 1980s at half spatialworkflow.io
Each seam needs its own handling.

Three habits keep an analysis honest. Fix the end date, because the file changes daily and its newest values are provisional. Handle the sampling explicitly — a daily index with short gaps filled, the long gap left empty — rather than letting a groupby silently average over unequal numbers of days. And compute trends on monthly means, which are the quantities the record's producers themselves trend, and report the trend both in absolute terms and relative to a stated baseline.

Daily updates, early alternate days and the 1987-88 hole

Checks before quoting a number from the sea-ice extent record. before quoting a sea-ice numberEnd date fixed and statedfile updates dailyGaps handled explicitlyalternate days, 1987–88Smoothing stateddaily, 5-day or monthlyTrend on monthly meanswith its baselineExtent, not areathe 15% thresholdEach item changes a record or a ranking somewhere spatialworkflow.io
Five checks before quoting a sea-ice number.
  • The file updates daily. Fix an end date and record the download date.
  • Every other day until August 1987. Interpolate short gaps; do not count days.
  • The 1987–88 hole is 42 days; leave it missing.
  • 5-day trailing means are the NSIDC convention for daily values.
  • Monthly means for trends; daily values are autocorrelated.
  • Antarctic ice behaves differently; the southern file is analysed the same way but tells a different story.

FAQ

Where can I download Arctic sea-ice extent data?

The NSIDC Sea Ice Index (G02135) publishes daily and monthly extent as CSV files for both hemispheres, free and without registration.

How fast is Arctic sea ice declining?

In this record, the September mean extent declined by about 0.76 million km² per decade from 1979 to 2025, about 12% of the 1981–2010 average per decade.

Why does the sea-ice record have missing days?

Until August 1987 the satellite instrument observed every other day, and from December 1987 to January 1988 there are 42 days with no usable data.

What was the lowest Arctic sea-ice extent?

September 2012 holds the record in this series, with a 5-day mean minimum of 3.39 million km².