Why wind_degree Is Useless for Interpolation (And How to Build Direction-Aware Wind Logic That Doesn’t Break at North)

Wind direction is stored as a bearing in degrees: 0 to 359, where 0 and 360 both mean north. That seems fine until you try to average two wind_degree values — say, 350° and 10° — and get 180°, which is due south. Completely wrong. The real mean direction is 0°, straight north.

This is the circular mean problem, and it quietly breaks a surprising number of weather applications. Hourly interpolation, directional averaging over a forecast window, rose chart aggregation, wind-shift detection — all of them fail the same way if you treat wind_degree like a regular number.

Why the WeatherAPI Response Makes This Worse

WeatherAPI returns both wind_degree (numeric bearing) and wind_dir (compass string like “NNW”) in the same response. The compass string is discretized into 16 sectors of 22.5° each — accurate enough for display, but too coarse for any interpolation or aggregation logic. If you need to track wind shifts or compute a mean direction over several hours, you have to work with wind_degree. Which means you have to handle the wrap-around correctly.

The fix is a few lines, but it’s not obvious if you haven’t run into it before.

Circular Mean: The Correct Approach

Convert each bearing to a unit vector, average the components, then convert back. In pseudocode:

// degrees to radians, then to unit vector
sin_sum = sum(sin(deg * π / 180) for each deg)
cos_sum = sum(cos(deg * π / 180) for each deg)

mean_deg = atan2(sin_sum, cos_sum) * 180 / π
if mean_deg < 0:
    mean_deg += 360

atan2 handles the quadrant logic; the final check shifts any negative result back into the 0–360 range. The 350° and 10° case now correctly returns 0°.

In JavaScript:

function circularMeanDegrees(angles) {
  const rad = angles.map(a => a * Math.PI / 180);
  const sinMean = rad.reduce((s, r) => s + Math.sin(r), 0) / rad.length;
  const cosMean = rad.reduce((s, r) => s + Math.cos(r), 0) / rad.length;
  let mean = Math.atan2(sinMean, cosMean) * 180 / Math.PI;
  return mean < 0 ? mean + 360 : mean;
}

And in Python:

import math

def circular_mean_degrees(angles):
    rads = [a * math.pi / 180 for a in angles]
    sin_mean = sum(math.sin(r) for r in rads) / len(rads)
    cos_mean = sum(math.cos(r) for r in rads) / len(rads)
    mean = math.atan2(sin_mean, cos_mean) * 180 / math.pi
    return mean % 360

% 360 handles the negative case cleanly in Python.

Where This Shows Up in Real API Usage

Hourly interpolation for sub-hourly apps. If you're pulling hourly forecast data and want to estimate wind direction at a 15-minute mark between two hours, a linear lerp on degrees will give you garbage near north. Interpolate the sin and cos components separately, then convert back.

Prevailing wind over a forecast window. A common pattern is pulling 24 or 48 hours of forecast data and computing a prevailing direction for a summary card — outdoor events, construction scheduling, that kind of thing. Arithmetic mean of the bearings will produce nonsense if any significant portion of the hours straddle north. Circular mean is the only correct answer here.

Wind-shift detection. Detecting when the wind rotates more than, say, 45° between consecutive hours matters for sailing routing, fire weather, and agriculture. The naive approach — abs(hour2 - hour1) > 45 — breaks completely for a 350° to 20° shift, which is a 30° clockwise rotation but registers as 330° under simple subtraction. The correct approach:

function angularDifference(a, b) {
  let diff = ((b - a) + 360) % 360;
  return diff > 180 ? diff - 360 : diff;
}

This returns a signed difference in [-180, 180], where positive is clockwise and negative is counterclockwise. Much more useful than an unsigned magnitude that can't tell you which way the wind moved.

The Resultant Length Problem

There's a secondary output of the circular mean calculation that most people ignore: resultant length (R), the magnitude of the averaged unit vector.

R = sqrt(sinMean² + cosMean²)

R runs from 0 to 1. All directions identical: R = 1. Directions scattered uniformly around the compass: R ≈ 0. An R below roughly 0.3 means the mean direction you computed is statistically meaningless — the wind was variable enough that any single bearing is misleading. That's worth surfacing to users rather than confidently displaying a direction that's essentially noise.

Wind roses that skip this check end up displaying a dominant direction for locations where wind was genuinely directionless over the period. Honest reporting would show "variable" instead.

One Place We Got This Wrong in Our Own Pipeline

We process wind direction fields from GRIB2 output — HRRR, NAM, GFS — and for most downstream calculations this is fine because we're working with U and V wind components directly from the model files. Those are Cartesian (eastward and northward velocities) and have no wrap problem. The bearing is only derived at the very end, for display.

But when we aggregate across observation sources — METAR reports blended with model output, weighted by freshness and distance — we do have to circular-average derived bearings. Early versions of that logic used arithmetic mean, and it showed up as occasional garbage readings near north for stations reporting variable winds. Not dramatic, but consistently wrong in a way that took some digging to pin down. Switching to the vector approach fixed it.

If you're working purely with the API response rather than raw model output, you don't get U and V components — you get wind_degree. So you're in circular arithmetic territory any time you do aggregation at all.

Displaying the Result: The Compass-String Temptation

Once you have a circular mean bearing, it's tempting to round it to the nearest 22.5° sector and display "NNW" or "ENE". Fine for casual display. But if your application does anything directional — wind roses, sector-specific rules, orientation-dependent outputs — keep the bearing as a float until the last possible moment. Discretizing early throws away precision you've worked to preserve.

wind_dir in the API response is convenient for human reading. It's not a substitute for wind_degree in any calculation that actually depends on direction.

Before you ship anything with wind direction logic, test your aggregation explicitly with a list of bearings that straddles north — [340, 350, 0, 10, 20] — and verify the output is around 0°, not around 144°. If it's the latter, arithmetic mean is still in the chain somewhere.

Scroll to Top