How to Analyse SNOTEL Snow Water Equivalent in Python

On this page

Problem statement

In the western United States, most of the summer's water falls as winter snow, and the network that measures it is SNOTEL: around 900 automated stations run by the Natural Resources Conservation Service, each weighing the snowpack every day. Its key measurement is snow water equivalent (SWE) β€” the depth of water you would get by melting the snow β€” and the questions asked of it are always the same: how big was this year's peak, when did it come, when did the snow melt out, and is any of that changing?

The data are free through an open REST API, but they arrive in inches, on water years that begin in October, with some elements starting decades later than others. This page fetches 45 water years from the Niwot station in Colorado's Front Range, computes peak SWE, peak date and melt-out date, and tests whether they have changed.

Quick answer

Ask the AWDB API for daily WTEQ, convert to millimetres, index by water year, and take the maximum and the first snow-free day after it:

url = ("https://wcc.sc.egov.usda.gov/awdbRestApi/services/v1/data?stationTriplets=663:CO:SNTL"
       "&elements=WTEQ,SNWD&duration=DAILY&beginDate=1980-10-01&endDate=2025-09-30")
d = to_frame(requests.get(url, timeout=120).json())
d["swe_mm"] = d["WTEQ"] * 25.4
d["wy"] = (d.index + pd.DateOffset(months=3)).year              # 1 October starts the next water year
d = d[d.wy.between(1981, 2025)]
peak = d.groupby("wy").swe_mm.max()
peak_day = d.groupby("wy").swe_mm.idxmax()
print(f"{len(peak)} water years; median peak {peak.median():.0f} mm on day {peak_day.dt.dayofyear.median():.0f}; "
      f"lowest {peak.min():.0f} mm in {peak.idxmin()}, highest {peak.max():.0f} mm in {peak.idxmax()}")
45 water years; median peak 378 mm on day 103; lowest 178 mm in 2002, highest 536 mm in 1984
Snow water equivalent at Niwot SNOTEL through every water year from 1981 to 2025 in grey, the day-by-day median in blue peaking near 330 mm in mid-April, 2002 in red peaking at 178 mm and melting out by early May, 1984 in dark blue peaking at 536 mm in May, and 2025 in teal. Snow water equivalent at Niwot SNOTEL through every water year from 1981 to 2025 in grey, the day-by-day median in blue peaking near 330 mm in mid-April, 2002 in red peaking at 178 mm and melting out by early May, 1984 in dark blue peaking at 536 mm in May, and 2025 in teal.
Daily SWE from NRCS SNOTEL station 663:CO:SNTL, converted from inches.

Find the station, use water years, peak and melt-out

From the NRCS AWDB API to peak SWE, peak date and melt-out for a SNOTEL station. from a station to three numbers per winterAWDB request663:CO:SNTL, WTEQinches to mmΓ— 25.4water yearsOct–Sep, 1981–2025peak, date,melt-outmedian 378 mm, day103trend vsspreadnone at NiwotEvery step is a place where a unit or a calendar can slip spatialworkflow.io
From a station's daily record to three numbers per winter.

1. Find the station and what it measures

Every SNOTEL station has a triplet β€” station number, state, network. The station endpoint gives its metadata; elements started on different dates, which matters for any long-term analysis:

meta = requests.get("https://wcc.sc.egov.usda.gov/awdbRestApi/services/v1/stations"
                    "?stationTriplets=663:CO:SNTL", timeout=60).json()[0]
print({k: meta[k] for k in ("name", "elevation", "latitude", "longitude", "beginDate")})
print({c: str(d[c].first_valid_index().date()) for c in ("WTEQ", "SNWD")})
{'name': 'Niwot', 'elevation': 9940.0, 'latitude': 40.03581, 'longitude': -105.5452, 'beginDate': '1979-10-01 00:00'}
{'WTEQ': '1980-10-01', 'SNWD': '2005-07-28'}

Niwot sits at 9,940 ft (3,030 m) just below the Continental Divide. SWE has been recorded since 1980 from a snow pillow; snow depth from an ultrasonic sensor only since 2005.

2. Work in water years

A water year runs from 1 October to 30 September and is named for the year it ends in, so the winter of 2024–25 is water year 2025. Grouping by calendar year would split every winter in two. Adding three months to each date and taking the year does the conversion; keep only complete water years.

3. Peak, peak date and melt-out

Peak SWE is the most water the pack held. Melt-out is the first day after the peak with no snow left:

def melt_out(g):
    after = g.loc[g.swe_mm.idxmax():]
    zero = after[after.swe_mm == 0]
    return zero.index[0] if len(zero) else pd.NaT

out = d.groupby("wy").apply(melt_out)
print("melt-out, median:", f"day {out.dt.dayofyear.median():.0f}", "| 2025:", out.loc[2025].date(),
      "| earliest:", out.dt.dayofyear.idxmin(), out.loc[out.dt.dayofyear.idxmin()].date())
melt-out, median: day 152 | 2025: 2025-06-01 | earliest: 2002 2002-04-30

4. Test for change, and believe the answer

Fit linear trends to each of the three and look at their size against the year-to-year spread:

for label, series in (("peak SWE (mm)", peak), ("peak date (day of year)", peak_day.dt.dayofyear),
                      ("melt-out (day of year)", out.dt.dayofyear)):
    slope = np.polyfit(series.index, series.values, 1)[0] * 10
    r = stats.spearmanr(series.index, series.values)
    print(f"{label:24} trend {slope:+6.1f} per decade; year-to-year SD {series.std():5.1f}; "
          f"Spearman rho {r.statistic:+.2f}, p = {r.pvalue:.2f}")
peak SWE (mm)            trend   +2.5 per decade; year-to-year SD  81.6; Spearman rho +0.03, p = 0.86
peak date (day of year)  trend   -0.3 per decade; year-to-year SD  17.4; Spearman rho +0.01, p = 0.95
melt-out (day of year)   trend   +1.2 per decade; year-to-year SD  10.9; Spearman rho +0.19, p = 0.21

At Niwot, none of the three shows a trend distinguishable from its own year-to-year variation over 1981–2025. That is a result, not a failure: a high, cold site on the Divide is among the places where warming shows least in the snowpack, and 45 noisy years cannot resolve a small change. Regional studies built on hundreds of stations find declines that a single station cannot show.

5. Use snow depth to get density

Where both are measured, SWE divided by depth is the bulk density of the pack β€” fresh powder near 100 kg/mΒ³, spring corn snow over 400. It tells you what kind of snow is there:

m = d.SNWD >= 5
density = (d.WTEQ / d.SNWD)[m] * 1000
print(density.groupby(density.index.month).median().round().loc[[11, 1, 3, 5]].to_string())
date
11    221.0
1     238.0
3     280.0
5     400.0

Niwot against a neighbour and its own record

The data used on this page

NRCS SNOTEL station Niwot (663:CO:SNTL), daily snow water equivalent (WTEQ) and snow depth (SNWD), water years 1981–2025, from the NRCS Air and Water Database REST API. Values are as provisionally or finally edited by NRCS at download.

import numpy as np
import pandas as pd
import requests
from scipy import stats

def to_frame(payload):
    """AWDB data response -> DataFrame, one column per element, indexed by date."""
    cols = {}
    for el in payload[0]["data"]:
        v = pd.DataFrame(el["values"])
        cols[el["stationElement"]["elementCode"]] = v.set_index(pd.to_datetime(v["date"]))["value"]
    return pd.DataFrame(cols)

print(to_frame([{"data": [{"stationElement": {"elementCode": "WTEQ"}, "values": [{"date": "2025-04-06", "value": 13.3}]}]}]))
            WTEQ
date            
2025-04-06  13.3

Example 1: compare with a station nearby

University Camp (838:CO:SNTL) is 2.6 km west and 119 m higher. Two stations this close should agree on the shape of each winter, but not on the totals:

uc = to_frame(requests.get(url.replace("663:CO:SNTL", "838:CO:SNTL").replace("WTEQ,SNWD", "WTEQ"), timeout=120).json())
uc["wy"] = (uc.index + pd.DateOffset(months=3)).year
uc_peak = uc[uc.wy.between(1981, 2025)].groupby("wy").WTEQ.max() * 25.4
both = pd.concat({"Niwot": peak, "University Camp": uc_peak}, axis=1).dropna()
print(both.describe().loc[["mean", "min", "max"]].round().to_string())
print("correlation of yearly peaks:", round(both.corr().iloc[0, 1], 2))
      Niwot  University Camp
mean  368.0            515.0
min   178.0            231.0
max   536.0            884.0
correlation of yearly peaks: 0.89

Example 2: this year against the record

Rank a water year's peak among all of them β€” the kind of statement water managers make each spring:

pct = peak.rank(pct=True)
for y in (2002, 2011, 2024, 2025):
    print(f"water year {y}: peak {peak[y]:.0f} mm, {pct[y]:.0%} of years lower or equal, "
          f"{peak[y] / peak.loc[1991:2020].median():.0%} of the 1991-2020 median")
water year 2002: peak 178 mm, 2% of years lower or equal, 47% of the 1991-2020 median
water year 2011: peak 432 mm, 82% of years lower or equal, 113% of the 1991-2020 median
water year 2024: peak 381 mm, 54% of years lower or equal, 100% of the 1991-2020 median
water year 2025: peak 338 mm, 36% of years lower or equal, 88% of the 1991-2020 median

Why SWE matters for water

SWE is the quantity that matters for water supply because it is the water, not the snow: a metre of fresh powder and 30 cm of spring snow can hold the same amount. A SNOTEL snow pillow measures it directly, as the weight of the pack on a fluid-filled bladder, which is why SWE records are long and continuous while depth records, needing a separate sensor, often start later.

Three SNOTEL snowpack metrics at Niwot, Colorado, water years 1981-2025. three metrics, three questionsmetricanswersNiwot mediantrendpeak SWEhow much water stored378 mm+2.5 mm/decade, p 0.86peak datewhen melt beganday 103 (13 Apr)βˆ’0.3 d/decade, p 0.95melt-outwhen it was goneday 152 (1 Jun)+1.2 d/decade, p 0.21None distinguishable from year-to-year variation at this one station spatialworkflow.io
Three metrics, none with a detectable trend at this station.

The three standard metrics describe different things. Peak SWE is how much water the winter stored. Peak date is when accumulation gave way to melt. Melt-out date is when the stored water had all left the pack β€” the one most sensitive to spring warmth, and the one that decides how much is still available in early summer. A station can have an average peak and an early melt-out, and that combination matters more to a reservoir than the peak alone.

A single station is a point. Its year-to-year variation is large, its trends small and site-specific, and a 45-year record rarely resolves them. Results like Niwot's absence of trend are worth stating plainly, and worth not extending to the region.

Units, water years and cumulative precipitation

Checks before using SNOTEL data. before trusting a SNOTEL seriesUnits converted onceinches to mmWater years, not calendar1 October startElement start dates checkedSNWD from 2005PREC differencedcumulative, resetsTrend against spreadSD 82 mmOne station is a point, not a region spatialworkflow.io
Five checks before trusting a SNOTEL series.
  • Inches in, millimetres out. AWDB stores WTEQ and SNWD in inches; convert once, early.
  • Water years start on 1 October. Group by (date + 3 months).year.
  • Elements start at different dates; SNWD at Niwot only from 2005.
  • Precipitation (PREC) is cumulative and resets each 1 October; difference it for daily values.
  • Provisional data in the current water year may be revised.
  • Snow pillows bridge under ice layers and can under-read; a sudden step not seen in depth is worth checking.

FAQ

How do I download SNOTEL data in Python?

Use the NRCS AWDB REST API: /services/v1/data?stationTriplets=663:CO:SNTL&elements=WTEQ&duration=DAILY&beginDate=...&endDate=... returns JSON with daily values; no key is needed.

What is a SNOTEL station triplet?

The station number, state and network separated by colons, for example 663:CO:SNTL for Niwot in Colorado.

What units is SNOTEL SWE in?

Inches. Multiply by 25.4 for millimetres.

Is snowpack declining at Niwot?

Not detectably over water years 1981–2025: peak SWE, peak date and melt-out show no trend distinguishable from year-to-year variation at this single high-elevation station.