Search code examples
algorithmmathgeometrymedian

How do you calculate the median of a set of angles?


I have a list of angles and want to get rid of outlier. My first idea is to calculate the median. Unfortunately there is the "wrap around" problem. I don't know of a "correct" way to define the median for a set of angles (or clock-positions).

My idea is to first calculate the mean, and use this to break the circle on the opposite side.

Example:
{6, 50, 52, 54, 60, 250} (in degree, 0-360)
average ~ 39
new range [-219, 219) -> new order 250, 6, 50, 52, 54, 60, 250
52 or 54 as median

Is this a good approach, or are there maybe better ones i don't know of?

Somewhat related: This Question showed ways to calculate the Mean of angles.


Solution

  • You can use the approach shown in the question you linkes: Calculate the average as the angle of accumulated unit vectors of your angles. In my opinion, this approach is not very suited to large sets of vectors.

    There's another approach that works with weighted interpolations. It doesn't require any trigonometric functions, which means that you can work with your data in degrees without converting them to radians.

    In this approach, all angles must be between 0° and 360°. If they lie outside, they must be brought into this range, e.g. -5° becomes 355°. Then you do a pairwise weighted average, where you adjust the angles when their difference is more than a semicircle, so that you always avarage over the shorter arc between the angles. After averaging, the resulting angle is brought into the range 0° to 360°.

    def angle_interpol(a1, w1, a2, w2):
        """Weighted avarage of two angles a1, a2 with weights w1, w2
    
        diff = a2 - a1        
        if diff > 180: a1 += 360
        elif diff < -180: a1 -= 360
    
        aa = (w1 * a1 + w2 * a2) / (w1 + w2)
    
        if aa > 360: aa -= 360
        elif aa < 0: aa += 360
    
        return aa
    
    def angle_mean(angle):    
        """Unweighted average of a list of angles"""
    
        if not angle: return 0
    
        aa = 0.0
        ww = 0.0
    
        for a in angle:
            aa = angle_interpol(aa, ww, a, 1)
            ww += 1
    
        return aa
    

    If you look at your example {6°, 50°, 52°, 54°, 60°, 250°}, you'll notice that all points lie on the same semicircle between 250° (or -110°) and 70°. With the proposed avarage method, the average angle is 18.67°. This is also the linear average of {6, 50, 52, 54, 60, -110}, which seems reasonable. The median would be between 50 and 52. The outlier is still the angle at 250°, but it is closer to the average if you come from -110° than if you come from 250°.

    Another example is {0°, 0°, 90°}. The vector approach calculates atan(0.5), i.e. approximately 26.6° as average. The proposed approach determines 30° as average.

    Calculating a circular average is only meaningful if your data is not evenly distributed in the feasible angle range. The arctan approach has a singularity if the angles cancel each other out; the approach proposed above just produces garbage.