Averaging Compass Bearings Without Breaking Geometry
When you’re tracking a ptarmigan with a Yagi antenna, you log compass bearings: 354°, 8°, 2°. Three observations toward magnetic north. What’s the average?
Naive arithmetic gives (354 + 8 + 2) ÷ 3 = 121°, which points southeast. The actual answer is 1.3°—nearly due north. Angles wrap at 360°, so they live on a circle, not a line. The fix: convert each bearing to a unit vector, average the vectors, convert back to an angle.
This matters when you’re combining multiple direction-finding readings to estimate an animal’s position. Get it wrong and your triangulation puts the bird in the wrong valley.
Rust (using f64 for trig):
fn circular_mean(bearings: &[f64]) -> f64 {
let (sum_sin, sum_cos): (f64, f64) = bearings.iter()
.map(|&b| (b.to_radians().sin(), b.to_radians().cos()))
.fold((0.0, 0.0), |(s, c), (sin, cos)| (s + sin, c + cos));
sum_sin.atan2(sum_cos).to_degrees().rem_euclid(360.0)
}
fn main() {
let bearings = vec![354.0, 8.0, 2.0];
println!("Circular mean: {:.1}°", circular_mean(&bearings));
}
Haskell (concise with sum and pattern matching):
import Data.List (foldl')
import Text.Printf (printf)
circularMean :: [Double] -> Double
circularMean bearings = if degrees < 0 then degrees + 360 else degrees
where
degrees = atan2 sumSin sumCos * 180 / pi
toRad = (* pi) . (/ 180)
(sumSin, sumCos) = foldl' (\(s, c) b -> (s + sin (toRad b), c + cos (toRad b))) (0, 0) bearings
main :: IO ()
main = printf "Circular mean: %.1f°\n" (circularMean [354, 8, 2])
Both output 1.3°. The method generalizes: any time you’re working with periodic data—angles, times of day, compass headings—the arithmetic needs to respect the topology. Wildlife biologists in the 1970s implementing the first computerized telemetry systems had to learn this the hard way, after early software put radio-collared elk in impossible locations.