How to Calculate Wind Speed and Direction From U and V in Python
On this page
Problem statement
Weather models store wind as two components: u, the eastward part, and v, the northward part, both in metres per second. Stations, forecasts and people describe wind as a speed and a direction, and the direction follows a meteorological convention: the compass bearing the wind blows from, measured clockwise from north. A westerly wind blows from the west towards the east, so it has a positive u, and its direction is 270°. Getting from one form to the other is two lines of NumPy, and getting the convention wrong turns every arrow around.
This page converts GFS 10-metre winds at 00 UTC on 27 September 2024, as Hurricane Helene approached Florida, into speed and direction, interpolates them to eight airports in Florida and Georgia, and compares them with what the airports reported.
Quick answer
Speed is the length of the vector; the direction the wind comes from is the angle of the reversed vector, measured from north:
def speed_direction(u, v):
speed = np.hypot(u, v)
direction = np.degrees(np.arctan2(-u, -v)) % 360 # from which the wind blows, clockwise from north
return speed, np.where(speed > 0, direction, np.nan) # no direction when calm
for name, (u, v) in {"from the west": (5, 0), "from the south": (0, 5), "from the north-east": (-3.5, -3.5)}.items():
s, d = speed_direction(u, v)
print(f"u = {u:+.1f}, v = {v:+.1f} → {s:.1f} m/s from {d:.0f}° ({name})")
u = +5.0, v = +0.0 → 5.0 m/s from 270° (from the west)
u = +0.0, v = +5.0 → 5.0 m/s from 180° (from the south)
u = -3.5, v = -3.5 → 4.9 m/s from 45° (from the north-east)
Interpolate to stations, read observations, compare on the circle
1. Interpolate the model to the stations
GFS longitudes run from 0 to 360, so station longitudes need 360 added before interpolating:
pts = STATIONS.set_index("icao")
at = WIND.interp(latitude=xr.DataArray(pts.lat, dims="icao"), longitude=xr.DataArray(pts.lon % 360, dims="icao"))
pts["gfs_speed"], pts["gfs_dir"] = speed_direction(at.u10.values, at.v10.values)
print(pts[["name", "gfs_speed", "gfs_dir"]].round(1).to_string())
name gfs_speed gfs_dir
icao
KJAX Jacksonville International Airport 9.6 126.9
KTPA Tampa International Airport 20.9 170.3
KCTY Cross City Airport 10.3 110.9
KTLH Tallahassee Regional Airport 6.3 38.1
KVLD Valdosta Regional Airport 7.9 87.0
KAAF Apalachicola Airport 12.2 0.7
KGNV Gainesville Regional Airport 9.3 126.3
KMAI Marianna Municipal Airport 7.0 26.0
2. Read the station observations
The ISD WND field packs direction, a quality flag, a type code, speed in tenths of a metre per second, and another flag. Take the routine hourly report closest to the model time:
def parse_wnd(wnd):
d, dq, kind, s, sq = wnd.split(",")
direction = np.nan if d == "999" or kind == "C" else float(d)
speed = np.nan if s == "9999" else int(s) / 10
return direction, speed
near = OBS[(OBS.REPORT_TYPE.str.strip() == "FM-15") & (abs(OBS.time - MODEL_TIME) <= pd.Timedelta("10min"))]
near = near.assign(obs_dir=[parse_wnd(w)[0] for w in near.WND], obs_speed=[parse_wnd(w)[1] for w in near.WND])
pts = pts.join(near.set_index("icao")[["time", "obs_dir", "obs_speed"]])
print(pts[["obs_dir", "obs_speed", "time"]].to_string())
obs_dir obs_speed time
icao
KJAX 120.0 10.8 2024-09-26 23:56:00
KTPA 160.0 12.9 2024-09-26 23:53:00
KCTY NaN NaN NaT
KTLH 40.0 6.7 2024-09-26 23:53:00
KVLD 80.0 4.6 2024-09-26 23:53:00
KAAF 10.0 13.4 2024-09-26 23:53:00
KGNV 110.0 9.8 2024-09-26 23:53:00
KMAI 10.0 5.7 2024-09-26 23:53:00
Cross City, close to where Helene came ashore three hours later, has no routine report in that window; a comparison has to allow for stations that go quiet when the weather is worst.
3. Compare directions on the circle
A direction difference has to wrap: 350° and 10° are 20° apart, not 340°:
diff = (pts.gfs_dir - pts.obs_dir + 180) % 360 - 180
pts["dir_error"], pts["speed_error"] = diff, pts.gfs_speed - pts.obs_speed
print(pts[["gfs_dir", "obs_dir", "dir_error", "gfs_speed", "obs_speed", "speed_error"]].round(1).to_string())
print(f"median absolute direction error {pts.dir_error.abs().median():.0f}°; median speed error {pts.speed_error.median():+.1f} m/s")
gfs_dir obs_dir dir_error gfs_speed obs_speed speed_error
icao
KJAX 126.9 120.0 6.9 9.6 10.8 -1.2
KTPA 170.3 160.0 10.3 20.9 12.9 8.0
KCTY 110.9 NaN NaN 10.3 NaN NaN
KTLH 38.1 40.0 -1.9 6.3 6.7 -0.4
KVLD 87.0 80.0 7.0 7.9 4.6 3.3
KAAF 0.7 10.0 -9.3 12.2 13.4 -1.2
KGNV 126.3 110.0 16.3 9.3 9.8 -0.5
KMAI 26.0 10.0 16.0 7.0 5.7 1.3
median absolute direction error 9°; median speed error -0.4 m/s
The directions agree within 17° everywhere, and most speeds within about a metre per second. Tampa is the exception, 20.9 m/s in the model against 12.9 observed: the airport is on the shore of Tampa Bay, and a grid cell 27 km across averages the faster winds over water with the slower ones over land.
The same result from MetPy, and back again
The data used on this page
The GFS 0.25° analysis for 00 UTC on 27 September 2024 from the NOAA GFS bucket on AWS, 10 m UGRD and VGRD only, and NOAA's Integrated Surface Database (Global Hourly) for eight airports, located from the ISD station list.
from pathlib import Path
import numpy as np
import pandas as pd
import requests
import xarray as xr
Path("data").mkdir(exist_ok=True)
GFS = "https://noaa-gfs-bdp-pds.s3.amazonaws.com/gfs.20240927/00/atmos/gfs.t00z.pgrb2.0p25.f000"
G = Path("data/gfs_helene.grib2")
if not G.exists(): # the two 10 m wind messages, by byte range
lines = requests.get(GFS + ".idx", timeout=60).text.splitlines()
starts = [int(l.split(":")[1]) for l in lines]
with open(G, "wb") as f:
for i, l in enumerate(lines):
if ":UGRD:10 m above ground:" in l or ":VGRD:10 m above ground:" in l:
f.write(requests.get(GFS, headers={"Range": f"bytes={starts[i]}-{starts[i + 1] - 1}"}, timeout=120).content)
WIND = xr.open_dataset(G, engine="cfgrib", decode_timedelta=False, backend_kwargs={
"indexpath": "", "filter_by_keys": {"typeOfLevel": "heightAboveGround", "level": 10}}).load()
MODEL_TIME = pd.Timestamp(WIND.time.values)
IDS = {"KJAX": "72206013889", "KTPA": "72211012842", "KCTY": "72212012833", "KTLH": "72214093805",
"KVLD": "72216693845", "KAAF": "72220012832", "KGNV": "74756012816", "KMAI": "74776003818"}
frames = []
for icao, sid in IDS.items():
f = Path(f"data/{sid}.csv")
if not f.exists():
f.write_bytes(requests.get(f"https://noaa-global-hourly-pds.s3.amazonaws.com/2024/{sid}.csv", timeout=300).content)
d = pd.read_csv(f, usecols=["DATE", "REPORT_TYPE", "NAME", "LATITUDE", "LONGITUDE", "WND"], dtype=str)
frames.append(d.assign(icao=icao))
OBS = pd.concat(frames)
OBS["time"] = pd.to_datetime(OBS.DATE)
STATIONS = (OBS.drop_duplicates("icao")[["icao", "NAME", "LATITUDE", "LONGITUDE"]]
.rename(columns={"NAME": "name", "LATITUDE": "lat", "LONGITUDE": "lon"}).astype({"lat": float, "lon": float}))
STATIONS["name"] = STATIONS.name.str.split(",").str[0].str.title()
print(f"GFS analysis at {MODEL_TIME:%Y-%m-%d %H:%M} UTC; {len(STATIONS)} stations, {len(OBS):,} reports in 2024")
GFS analysis at 2024-09-27 00:00 UTC; 8 stations, 110,088 reports in 2024
Example 1: the same result from MetPy
MetPy's wind_direction uses the same convention and returns 0° only for calm, with northerly winds at 360°:
import metpy.calc as mpcalc
from metpy.units import units
d_mp = mpcalc.wind_direction(at.u10.values * units("m/s"), at.v10.values * units("m/s")).m
print(f"largest difference from our function: {np.abs((d_mp - pts.gfs_dir.values + 180) % 360 - 180).max():.1e}°")
for u, v in ((0.0, -5.0), (0.0, 0.0)):
print(f"u = {u}, v = {v}: MetPy {mpcalc.wind_direction(u * units('m/s'), v * units('m/s')).m:.0f}°, ours {speed_direction(u, v)[1]:.0f}°")
largest difference from our function: 0.0e+00°
u = 0.0, v = -5.0: MetPy 360°, ours 0°
u = 0.0, v = 0.0: MetPy 0°, ours nan°
Example 2: back from speed and direction to components
Station observations need the reverse to be averaged or compared as vectors:
obs = pts.dropna(subset=["obs_dir"])
rad = np.radians(obs.obs_dir)
u_obs, v_obs = -obs.obs_speed * np.sin(rad), -obs.obs_speed * np.cos(rad)
print(pd.DataFrame({"u": u_obs.round(2), "v": v_obs.round(2), "back to direction": speed_direction(u_obs, v_obs)[1].round(0)}).to_string())
u v back to direction
icao
KJAX -9.35 5.40 120.0
KTPA -4.41 12.12 160.0
KTLH -4.31 -5.13 40.0
KVLD -4.53 -0.80 80.0
KAAF -2.33 -13.20 10.0
KGNV -9.21 3.35 110.0
KMAI -0.99 -5.61 10.0
Components are what the arithmetic needs
The components are what the model's equations use, and they are the right form for any arithmetic: averaging, interpolating and differencing all work on u and v, never on directions. The direction convention exists for people. Meteorologists name winds after where they come from, because that is where the weather comes from; oceanographers name currents after where they go. The two conventions differ by exactly 180°, and the formula atan2(-u, -v) is atan2(u, v) turned around.
A model value at a point is not an observation. The GFS grid cell is about 27 km across, and a station measures one spot 10 m above an airfield, averaged over two minutes. Near a hurricane the gradient across one cell is steep, and the timing differs too: the METAR closest to 00 UTC was taken at 23:53.
Calm, north and grid-relative winds
- Calm: direction is undefined when speed is zero. Without the
np.where,arctan2of two negative zeros gives 180°, a southerly that is not there; ISD marks calm with typeC, and MetPy returns 0°. - North: report northerly winds as 360°, keeping 0° for calm, as aviation does;
% 360gives 0°. - Missing values: ISD uses 999 for direction and 9999 for speed.
- Grid-relative winds: on rotated or Lambert grids such as HRRR's,
uandvfollow the grid, not east and north. - Units: ISD speeds are in metres per second times ten; knots are m/s × 1.94384.
Internal links
- How to download GFS wind data from AWS — getting the components
- How to read GRIB weather data in Python — GRIB in general
FAQ
How do I calculate wind direction from u and v?
Use np.degrees(np.arctan2(-u, -v)) % 360, which gives the direction the wind blows from, clockwise from north.
How do I calculate wind speed from u and v?
np.hypot(u, v), the length of the wind vector.
What does a positive u wind mean?
Wind blowing towards the east, which is a westerly wind with a direction of 270°.
How do I convert wind speed and direction to u and v?
u = -speed × sin(direction) and v = -speed × cos(direction), with the direction in radians.