How to Calculate a Wind Climatology in Python

On this page

Problem statement

A wind climatology is the typical wind of a place, averaged over decades. Averaging wind needs two different averages, and they answer different questions. The mean of the speed says how windy a place is. The mean of the vector, averaging u and v and then taking the length, says how much air is carried in one direction on average, and in a place where the wind blows from every quarter it can be close to zero while the mean speed is high. Their ratio, the steadiness, separates the trade winds, which blow from the same direction month after month, from the westerly belts, where storms bring wind from everywhere.

This page builds a 1991–2020 climatology from the NCEP/NCAR Reanalysis 1 monthly means, maps the mean vector and the steadiness, and compares the two averages at four places.

Quick answer

Average the components and the speed separately over the normal period, then compare:

period = slice("1991-01-01", "2020-12-31")
U = DS.uwnd.sel(time=period).mean("time")
V = DS.vwnd.sel(time=period).mean("time")
S = DS.wspd.sel(time=period).mean("time")
vector_mean = np.hypot(U, V)
steadiness = vector_mean / S
for name, (lat, lon) in PLACES.items():
    p = dict(lat=lat, lon=lon % 360, method="nearest")
    print(f"{name:>26}: mean speed {float(S.sel(**p)):4.1f} m/s, mean vector {float(vector_mean.sel(**p)):4.1f} m/s, "
          f"steadiness {float(steadiness.sel(**p)):.2f}")
   trade winds, 15°N 140°W: mean speed  8.2 m/s, mean vector  7.9 m/s, steadiness 0.96
 Southern Ocean, 50°S 90°E: mean speed 12.6 m/s, mean vector 10.5 m/s, steadiness 0.83
equatorial Atlantic, 0° 25°W: mean speed  4.8 m/s, mean vector  4.2 m/s, steadiness 0.87
        Chicago, 42°N 88°W: mean speed  5.2 m/s, mean vector  1.6 m/s, steadiness 0.30
World map of the 1991 to 2020 near-surface wind from the NCEP/NCAR Reanalysis: mean speed shaded, highest over the Southern Ocean and the North Atlantic and Pacific storm tracks and lowest over tropical land, with arrows for the mean vector showing the easterly trade winds either side of the equator and westerlies in the middle latitudes, with short arrows over the continents. World map of the 1991 to 2020 near-surface wind from the NCEP/NCAR Reanalysis: mean speed shaded, highest over the Southern Ocean and the North Atlantic and Pacific storm tracks and lowest over tropical land, with arrows for the mean vector showing the easterly trade winds either side of the equator and westerlies in the middle latitudes, with short arrows over the continents.
Mean speed and mean vector, 1991–2020.

A covered period, how the mean speed was made, steadiness

1. Choose the period and check the data cover it

Climate normals use 30-year periods; 1991–2020 is the current one. The reanalysis runs from 1948:

print(f"months available: {DS.sizes['time']:,}, {pd.Timestamp(DS.time.values[0]):%Y-%m} to {pd.Timestamp(DS.time.values[-1]):%Y-%m}")
print(f"months in 1991–2020: {DS.uwnd.sel(time=period).sizes['time']} (expect 360)")
months available: 938, 1948-01 to 2026-02
months in 1991–2020: 360 (expect 360)

2. Read how the mean speed was made

The wspd file is not the mean of every instantaneous speed: its metadata say it is computed from daily speeds, which are themselves from daily vector winds. Variation within each day is averaged out before the speed is taken, so this mean speed is lower than one from 6-hourly winds would be, and the steadiness correspondingly higher:

print(DS.wspd.attrs["long_name"])
print(f"grid: {DS.sizes['lat']} × {DS.sizes['lon']}, {float(abs(DS.lat[1] - DS.lat[0]))}° spacing; level {DS.wspd.attrs.get('level')}")
Monthly Mean Wind Speed at sigma level 0.995 from daily wind speed (from daily vector winds)
grid: 73 × 144, 2.5° spacing; level 0.995 sigma

3. Map steadiness

bands = steadiness.groupby_bins("lat", [-90, -60, -30, -10, 10, 30, 60, 90]).mean(...)
print(bands.to_series().round(2).to_string())
print(f"share of the globe with steadiness above 0.8: {float((steadiness > 0.8).weighted(np.cos(np.radians(steadiness.lat))).mean()):.0%}")
lat_bins
(-90, -60]    0.64
(-60, -30]    0.54
(-30, -10]    0.70
(-10, 10]     0.62
(10, 30]      0.59
(30, 60]      0.35
(60, 90]      0.27
share of the globe with steadiness above 0.8: 22%
World map of wind steadiness from 0 to 1: bright, near 1, in the trade-wind belts over the oceans, along the Antarctic coast and over parts of the Southern Ocean; dark, below 0.3, over the mid-latitude continents, the Arctic and parts of the tropics; four sample places are marked. World map of wind steadiness from 0 to 1: bright, near 1, in the trade-wind belts over the oceans, along the Antarctic coast and over parts of the Southern Ocean; dark, below 0.3, over the mid-latitude continents, the Arctic and parts of the tropics; four sample places are marked.
Where the wind keeps its direction.

Winter and summer at Chicago

The data used on this page

NCEP/NCAR Reanalysis 1 monthly means at the near-surface sigma level 0.995 from NOAA PSL: uwnd, vwnd and wspd, 2.5° grid, 1948 to the present.

import os
import subprocess
from pathlib import Path

import numpy as np
import pandas as pd
import xarray as xr

Path("data").mkdir(exist_ok=True)
BASE = "https://psl.noaa.gov/thredds/fileServer/Datasets/ncep.reanalysis/Monthlies/surface"
for v in ("uwnd", "vwnd", "wspd"):
    p = Path(f"data/{v}.mon.mean.nc")
    for _ in range(5):                                   # PSL drops connections: resume and retry
        if p.exists():
            break
        part = str(p) + ".part"
        if subprocess.run(["curl", "-sfL", "-C", "-", "--retry", "3", "-o", part, f"{BASE}/{v}.mon.mean.nc"]).returncode == 0:
            os.replace(part, p)
    if not p.exists():
        raise RuntimeError(f"could not download {v}.mon.mean.nc")
DS = xr.merge([xr.open_dataset(f"data/{v}.mon.mean.nc")[v] for v in ("uwnd", "vwnd", "wspd")]).load()
PLACES = {"trade winds, 15°N 140°W": (15, -140), "Southern Ocean, 50°S 90°E": (-50, 90),
          "equatorial Atlantic, 0° 25°W": (0, -25), "Chicago, 42°N 88°W": (42, -88)}
print(f"variables {list(DS.data_vars)}; {DS.sizes['time']} months")
variables ['uwnd', 'vwnd', 'wspd']; 938 months

Example 1: winter and summer at Chicago

The mean vector changes with the season even where the annual one is weak:

p = dict(lat=42, lon=272, method="nearest")
for label, months in (("Dec–Feb", [12, 1, 2]), ("Jun–Aug", [6, 7, 8])):
    sub = DS.sel(time=period).sel(time=DS.sel(time=period).time.dt.month.isin(months))
    u, v = float(sub.uwnd.sel(**p).mean()), float(sub.vwnd.sel(**p).mean())
    d = np.degrees(np.arctan2(-u, -v)) % 360
    print(f"{label}: mean vector {np.hypot(u, v):.1f} m/s from {d:.0f}°, mean speed {float(sub.wspd.sel(**p).mean()):.1f} m/s")
Dec–Feb: mean vector 2.7 m/s from 254°, mean speed 5.8 m/s
Jun–Aug: mean vector 1.2 m/s from 214°, mean speed 4.1 m/s

The mean of speeds is not the speed of the mean

Wind steadiness, the mean vector divided by the mean speed, at four places for 1991 to 2020. how steady the wind is, four placestrade winds, 15°N 140°W0.96: 7.9 of 8.2 m/sequatorial Atlantic0.87: 4.2 of 4.8Southern Ocean, 50°S0.83: 10.5 of 12.6Chicago0.30: 1.6 of 5.200.51NCEP/NCAR Reanalysis 1 monthly means, sigma 0.995 spatialworkflow.io
How steady the wind is at four places.

The two averages differ because speed is the length of a vector, and the average of lengths is never smaller than the length of the average. They are equal only if the wind always blows from the same direction. Steadiness, the ratio, therefore runs from 0, for winds that come equally from every direction, to 1, for a wind that never changes direction. The trade winds and the westerlies of the Southern Ocean are steady; the mid-latitude continents, where weather systems pass every few days, are not.

Which average to use depends on the question. Wind energy and wind loading depend on speed, and in fact on the full distribution of speed. Transport, of moisture, dust, pollution or seeds, depends on the vector mean. Mapping a climatological "wind direction" from averaged directions is meaningless where steadiness is low; average the components instead, and show the steadiness alongside.

Resolution, inhomogeneity and monthly vectors

Averaging wind over decades: problems and fixes. wind climatology trapsTrapInsteaddirections averaged in degreesaverage u and v, then convertspeed from mean u and vthat is the vector mean: 1.6, not5.2wspd read as instantaneousfrom daily vectors: a low estimatea mean direction where unsteadyshow steadiness alongsideChicago and the globe, 1991–2020 spatialworkflow.io
Wind climatology traps.
  • Resolution: a 2.5° reanalysis smooths coasts and mountains; use ERA5 or a regional reanalysis for local climatology.
  • Level: sigma 0.995 is about 40 m above the surface; it is close to, but not the same as, 10 m wind.
  • Inhomogeneity: the observing system changed over the decades, especially with satellites from 1979; trends need care.
  • Seasons: an annual steadiness can hide two steady seasons with opposite winds, as in monsoon regions.
  • Speed from monthly vectors: never compute mean speed from monthly mean u and v; that is the vector mean.

FAQ

How do I calculate average wind direction over many years?

Average the u and v components, then compute the direction from the averages; do not average directions in degrees.

What is wind steadiness?

The ratio of the mean vector wind's length to the mean wind speed, from 0 for winds from all directions to 1 for a constant direction.

Where can I get long-term wind data for free?

NOAA PSL publishes the NCEP/NCAR Reanalysis monthly means since 1948; ERA5 from Copernicus has finer resolution and needs an account.

Why is mean vector wind smaller than mean wind speed?

Winds from opposite directions cancel in the vector average but add in the speed average.