在循环数据中查找异常值
Posted
技术标签:
【中文标题】在循环数据中查找异常值【英文标题】:Finding outliers in circular data 【发布时间】:2021-12-05 09:16:53 【问题描述】:我有一组具有圆形刻度(角度从 0 到 360°)的数据。我知道数据集中的大多数值彼此接近,但有些是异常值。我想确定哪些必须被淘汰。
圆形刻度的问题如下(使用示例): data = [350, 0, 10] 是一个包含角度的数组,以度为单位。这个数组的绝对平均值是 123.33。但是考虑到它们的单位,350°、0°和10°的平均值是0°。
我们在这里看到平均值存在问题。计算标准差时也存在这个问题。
我该怎么做?
【问题讨论】:
什么是异常值? 这能回答你的问题吗? Easy way to keeping angles between -179 and 180 degrees 取角度的符号或余符号,您将得到一个范围在 -1 和 1 之间的值 - 但至关重要的是,因为它是周期性的,355 的角度将具有接近的值角度为 5。使用 sin 或 cos 也应该适用于您想要使用负角的情况。 如果我没记错的话,这是个棘手的问题。你怎么定义意思?即,0°、0° 和 90° 的平均值是 30° 还是 26.5°(arctan(1/2))?你如何定义标准差? 为什么不使用(校正后的)样本标准差,使用角度之间的绝对差(请参阅下面我的答案中的函数absDiff_angle
)?
【参考方案1】:
因此,您将获得一个角度列表,并希望找到“平均”(平均)角度和异常值。一种简单的可能性是平均与角度对应的二维向量(cos(a),sin(a))
,并再次计算角度的标准偏差:
from math import degrees, radians, sin, cos, atan2
def absDiff_angle(a1, a2, fullAngle=360):
a1,a2 = a1%fullAngle,a2%fullAngle
if a1 >= a2: a1,a2 = a2,a1
return min(a2-a1, a1+fullAngle-a2)
# sample input of angles 350,351,...359,0,...,10, 90
angles_deg = list(range(350,360)) + list(range(11)) + [90]
# compute corresponding 2D vectors
angles_rad = [radians(a) for a in angles_deg]
xVals = [cos(a) for a in angles_rad]
yVals = [sin(a) for a in angles_rad]
# average of 2D vectors
N = len(angles_rad)
xMean = sum(xVals)/N
yMean = sum(yVals)/N
# go back to angle
angleMean_rad = atan2(yMean,xMean)
angleMean_deg = degrees(angleMean_rad)
# filter outliers
square = lambda v: v*v
stddev = sqrt(sum([square(absDiff_angle(a, angleMean_deg)) for a in angles_deg])/(N-1))
MIN_DIST_OUTLIER = 3*stddev
isOutlier = lambda a: absDiff_angle(a, angleMean_deg) >= MIN_DIST_OUTLIER
outliers = [a for a in angles_deg if isOutlier(a)]
print(angleMean_deg)
print(outliers)
请注意,异常值会扭曲平均值和标准偏差。为了对异常值不那么敏感,可以计算角度的直方图(例如,箱[0°, 10°[, [10°, 20°[, ..., [350°,360°[
)并从箱中选择具有大多数成员和邻居的角度来计算平均角度(和标准偏差)。
【讨论】:
【参考方案2】:循环平均值
您可以将单位半径圆上对应点的向量代入角度,然后将均值定义为向量和的角度。
但请注意,这给出了 [0°, 0°, 90°] 的 26.5° 平均值,因为 26.5° = arctan(1/2) 而 [0°, 180°] 没有平均值。
异常值
离群值是离均值越远的角度,即角度差的绝对值越大。
标准差
标准差可用于定义异常值。
@coproc 在其回答中给出了相应的代码。
四分位数
也可以使用四分位距值,它对异常值的依赖程度低于标准差,但在循环情况下它可能无关紧要。
无论如何:
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]
让我们试一试:
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)
输出:
(-1.1650923760388311e-14, 40.000000000000014, [-179, 179])
正如我们所料:
circular_mean
接近 0,因为列表对于 0° 轴是对称的;
circular_interquartiles_value
是 40°,因为第一个四分位数是 -20°,第三个四分位数是 20°;
异常值被正确检测到,350 和 720 被取为其归一化值。
【讨论】:
将平均值计算为向量和的角度非常好。和等于 0 的问题很容易处理。但是标准差才是真正要解决的问题。也许使用均值和标准来定义异常值不是正确的方法..也许 @jeandemeusy ,我在答案中添加了基于四分位数值的异常值检测。 很好的答案,谢谢! @jeandemeusy,警告:代码根本没有经过测试。由于平均值和增量计算了两次,因此有优化的空间。你可以“喜欢”它。【参考方案3】:如果您立即使用正弦或余弦函数转换角度 data
(0..360),您会将数据转换为 -1.0、1.0 范围。
这样做会丢失与角度所在象限相关的信息,因此您需要提取该信息。
quadrant = [n // 90 for n in data] # values: 0, 1, 2, 3
您可以将象限合二为一,结果的正弦或余弦变换将在 0.0、1.0 范围内。
single_quadrant = [n % 90 for n in data] # values: 0, 1, ..., 89
使用这两个想法,可以使用正弦或余弦函数将data
映射到 0.0 - 4.0 范围,如下所示:
import math
using_sine = [(n//90 + math.sin(math.radians(n % 90))) for n in data]
using_cosine = [(n//90 + math.cos(math.radians(n % 90))) for n in data]
【讨论】:
不错的映射,但在改变角度的分布时看起来像它。以上是关于在循环数据中查找异常值的主要内容,如果未能解决你的问题,请参考以下文章