How do you calculate the average of a set of circular data?

Viewed 107576

I want to calculate the average of a set of circular data. For example, I might have several samples from the reading of a compass. The problem of course is how to deal with the wraparound. The same algorithm might be useful for a clockface.

The actual question is more complicated - what do statistics mean on a sphere or in an algebraic space which "wraps around", e.g. the additive group mod n. The answer may not be unique, e.g. the average of 359 degrees and 1 degree could be 0 degrees or 180, but statistically 0 looks better.

This is a real programming problem for me and I'm trying to make it not look like just a Math problem.

31 Answers

Compute unit vectors from the angles and take the angle of their average.

This question is examined in detail in the book: "Statistics On Spheres", Geoffrey S. Watson, University of Arkansas Lecture Notes in the Mathematical Sciences, 1983 John Wiley & Sons, Inc. as mentioned at http://catless.ncl.ac.uk/Risks/7.44.html#subj4 by Bruce Karsh.

A good way to estimate an average angle, A, from a set of angle measurements a[i] 0<=i

                   sum_i_from_1_to_N sin(a[i])
a = arctangent ---------------------------
                   sum_i_from_1_to_N cos(a[i])

The method given by starblue is computationally equivalent, but his reasons are clearer and probably programmatically more efficient, and also work well in the zero case, so kudos to him.

The subject is now explored in more detail on Wikipedia, and with other uses, like fractional parts.

I see the problem - for example, if you have a 45' angle and a 315' angle, the "natural" average would be 180', but the value you want is actually 0'.

I think Starblue is onto something. Just calculate the (x, y) cartesian coordinates for each angle, and add those resulting vectors together. The angular offset of the final vector should be your required result.

x = y = 0
foreach angle {
    x += cos(angle)
    y += sin(angle)
}
average_angle = atan2(y, x)

I'm ignoring for now that a compass heading starts at north, and goes clockwise, whereas "normal" cartesian coordinates start with zero along the X axis, and then go anti-clockwise. The maths should work out the same way regardless.

FOR THE SPECIAL CASE OF TWO ANGLES:

The answer ( (a + b) mod 360 ) / 2 is WRONG. For angles 350 and 2, the closest point is 356, not 176.

The unit vector and trig solutions may be too expensive.

What I've got from a little tinkering is:

diff = ( ( a - b + 180 + 360 ) mod 360 ) - 180
angle = (360 + b + ( diff / 2 ) ) mod 360
  • 0, 180 -> 90 (two answers for this: this equation takes the clockwise answer from a)
  • 180, 0 -> 270 (see above)
  • 180, 1 -> 90.5
  • 1, 180 -> 90.5
  • 20, 350 -> 5
  • 350, 20 -> 5 (all following examples reverse properly too)
  • 10, 20 -> 15
  • 350, 2 -> 356
  • 359, 0 -> 359.5
  • 180, 180 -> 180

You have to define average more accurately. For the specific case of two angles, I can think of two different scenarios:

  1. The "true" average, i.e. (a + b) / 2 % 360.
  2. The angle that points "between" the two others while staying in the same semicircle, e.g. for 355 and 5, this would be 0, not 180. To do this, you need to check if the difference between the two angles is larger than 180 or not. If so, increment the smaller angle by 360 before using the above formula.

I don't see how the second alternative can be generalized for the case of more than two angles, though.

If anyone is looking for a JavaScript solution to this, I've translated the example given in the wikipedia page Mean of circular quantities (which was also referred to in Nick's answer) into JavaScript/NodeJS code, with help from the mathjs library.

If your angles are in degrees:

const maths = require('mathjs');

getAverageDegrees = (array) => {
    let arrayLength = array.length;

    let sinTotal = 0;
    let cosTotal = 0;

    for (let i = 0; i < arrayLength; i++) {
        sinTotal += maths.sin(array[i] * (maths.pi / 180));
        cosTotal += maths.cos(array[i] * (maths.pi / 180));
    }

    let averageDirection = maths.atan(sinTotal / cosTotal) * (180 / maths.pi);

    if (cosTotal < 0) {
        averageDirection += 180;
    } else if (sinTotal < 0) {
        averageDirection += 360;
    }

    return averageDirection;
}

This solution worked really well for me in order to find the average direction from a set of compass directions. I've tested this on a large range of directional data (0-360 degrees) and it seems very robust.

Alternatively, if your angles are in radians:

const maths = require('mathjs');
getAverageRadians = (array) => {
    let arrayLength = array.length;

    let sinTotal = 0;
    let cosTotal = 0;

    for (let i = 0; i < arrayLength; i++) {
        sinTotal += maths.sin(array[i]);
        cosTotal += maths.cos(array[i]);
    }

    let averageDirection = maths.atan(sinTotal / cosTotal);

    if (cosTotal < 0) {
        averageDirection += 180;
    } else if (sinTotal < 0) {
        averageDirection += 360;
    }

    return averageDirection;
}

Hopefully these solutions are helpful to someone facing a similar programming challenge to me.

Here's an idea: build the average iteratively by always calculating the average of the angles that are closest together, keeping a weight.

Another idea: find the largest gap between the given angles. Find the point that bisects it, and then pick the opposite point on the circle as the reference zero to calculate the average from.

Let's represent these angles with points on the circumference of the circle.

Can we assume that all these points fall on the same half of the circle? (Otherwise, there is no obvious way to define the "average angle". Think of two points on the diameter, e.g. 0 deg and 180 deg --- is the average 90 deg or 270 deg? What happens when we have 3 or more evenly spread out points?)

With this assumption, we pick an arbitrary point on that semicircle as the "origin", and measure the given set of angles with respect to this origin (call this the "relative angle"). Note that the relative angle has an absolute value strictly less than 180 deg. Finally, take the mean of these relative angles to get the desired average angle (relative to our origin of course).

Well I'm hugely late to the party but thought I'd add my 2 cents worth as I couldn't really find any definitive answer. In the end I implemented the following Java version of the Mitsuta method which, I hope, provides a simple and robust solution. Particularly as the Standard Deviation provides both a measure dispersion and, if sd == 90, indicates that the input angles result in an ambiguous mean.

EDIT: Actually I realised that my original implementation can be even further simplified, in fact worryingly simple considering all the conversation and trigonometry going on in the other answers.

/**
 * The Mitsuta method
 *
 * @param angles Angles from 0 - 360
 * @return double array containing
 * 0 - mean
 * 1 - sd: a measure of angular dispersion, in the range [0..360], similar to standard deviation.
 * Note if sd == 90 then the mean can also be its inverse, i.e. 360 == 0, 300 == 60.
 */
public static double[] getAngleStatsMitsuta(double... angles) {
    double sum = 0;
    double sumsq = 0;
    for (double angle : angles) {
        if (angle >= 180) {
            angle -= 360;
        }
        sum += angle;
        sumsq += angle * angle;
    }

    double mean = sum / angles.length;
    return new double[]{mean <= 0 ? 360 + mean: mean, Math.sqrt(sumsq / angles.length - (mean * mean))};
}

... and for all you (Java) geeks out there, you can use the above approach to get the mean angle in one line.

Arrays.stream(angles).map(angle -> angle<180 ? angle: (angle-360)).sum() / angles.length;

Alnitak has the right solution. Nick Fortescue's solution is functionally the same.

For the special case of where

( sum(x_component) = 0.0 && sum(y_component) = 0.0 ) // e.g. 2 angles of 10. and 190. degrees ea.

use 0.0 degrees as the sum

Computationally you have to test for this case since atan2(0. , 0.) is undefined and will generate an error.

The average angle phi_avg should have the property that sum_i|phi_avg-phi_i|^2 becomes minimal, where the difference has to be in [-Pi, Pi) (because it might be shorter to go the other way around!). This is easily achieved by normalizing all input values to [0, 2Pi), keeping a running average phi_run and choosing normalizing |phi_i-phi_run| to [-Pi,Pi) (by adding or subtractin 2Pi). Most suggestions above do something else that does not have that minimal property, i.e., they average something, but not angles.

The problem is extremely simple. 1. Make sure all angles are between -180 and 180 degrees. 2. a Add all non-negative angles, take their average, and COUNT how many 2. b.Add all negative angles, take their average and COUNT how many. 3. Take the difference of pos_average minus neg_average If difference is greater than 180 then change difference to 360 minus difference. Otherwise just change the sign of difference. Note that difference is always non-negative. The Average_Angle equals the pos_average plus difference times the "weight", negative count divided by the sum of negative and positive count

Related