Circular mean
You can substitute the vectors to the corresponding points on the unit radius circle to the angles, then define the mean as the angle of the sum of the vectors.
But beware this gives a mean of 26.5° for [0°, 0°, 90°] as 26.5° = arctan(1/2) and there is no mean for [0°, 180°].
Outliers
Outliers are the angles the farther from the mean, which is the greater absolute value of the difference of angles.
Standard deviation
The standard deviation can be use to define outliers.
@coproc gives the corresponding code in its answer.
Interquartiles value
The interquartiles value can also be used, it is less dependable on outliers values than the standard deviation but in the circular case it could be irrelevant.
Anyway :
from functools import reduce
from math import degrees, radians, sin, cos, atan2, pi
def norm_angle(angle, degree_unit = True):
""" Normalize an angle return in a value between ]180, 180] or ]pi, pi]."""
mpi = 180 if degree_unit else pi
angle = angle % (2 * mpi)
return angle if abs(angle) <= mpi else angle - (1 if angle >= 0 else -1) * 2 * mpi
def circular_mean(angles, degree_unit = True):
""" Returns the circular mean from a collection of angles. """
angles = [radians(a) for a in angles] if degree_unit else angles
x_sum, y_sum = reduce(lambda tup, ang: (tup[0]+cos(ang), tup[1]+sin(ang)), angles, (0,0))
if x_sum == 0 and y_sum == 0: return None
return (degrees if degree_unit else lambda x:x)(atan2(y_sum, x_sum))
def circular_interquartiles_value(angles, degree_unit = True):
""" Returns the circular interquartiles value from a collection of angles."""
mean = circular_mean(angles, degree_unit=degree_unit)
deltas = tuple(sorted([norm_angle(a - mean, degree_unit=degree_unit) for a in angles]))
nb = len(deltas)
nq1, nq3, direct = nb // 4, nb - nb // 4, (nb % 4) // 2
q1 = deltas[nq1] if direct else (deltas[nq1-1] + deltas[nq1]) / 2
q3 = deltas[nq3-1] if direct else(deltas[nq3-1] + deltas[nq3]) / 2
return q3-q1
def circular_outliers(angles, coef = 1.5, values=True, degree_unit=True):
""" Returns outliers from a collection of angles. """
mean = circular_mean(angles, degree_unit=degree_unit)
maxdelta = coef * circular_interquartiles_value(angles, degree_unit=degree_unit)
deltas = [norm_angle(a - mean, degree_unit=degree_unit) for a in angles]
return [z[0] if values else i for i, z in enumerate(zip(angles, deltas)) if abs(z[1]) > maxdelta]
Lets give it a try:
angles = [-179, -20, 350, 720, 10, 20, 179] # identical to [-179, -20, -10, 0, 10, 20, 179]
circular_mean(angles), circular_interquartiles_value(angles), circular_outliers(angles)
output:
(-1.1650923760388311e-14, 40.000000000000014, [-179, 179])
As we might expect:
- the
circular_mean
is near 0 as the list is symetric for the 0° axis;
- the
circular_interquartiles_value
is 40° as the first quartile is -20° and the third quartile is 20°;
- the outliers are correctly detected, 350 and 720 been taken for their normalized values.