【问题标题】:Finding outliers in circular data在循环数据中查找异常值
【发布时间】:2021-12-05 09:16:53
【问题描述】:

我有一组具有圆形刻度(角度从 0 到 360°)的数据。我知道数据集中的大多数值彼此接近,但有些是异常值。我想确定哪些必须被淘汰。

圆形刻度的问题如下(使用示例): data = [350, 0, 10] 是一个包含角度的数组,以度为单位。这个数组的绝对平均值是 123.33。但是考虑到它们的单位,350°、0°和10°的平均值是0°。

我们在这里看到平均值存在问题。计算标准差时也存在这个问题。

我该怎么做?

【问题讨论】:

  • 什么是异常值
  • 取角度的符号或余符号,您将得到一个范围在 -1 和 1 之间的值 - 但至关重要的是,因为它是周期性的,355 的角度将具有接近的值角度为 5。使用 sin 或 cos 也应该适用于您想要使用负角的情况。
  • 如果我没记错的话,这是个棘手的问题。你怎么定义意思?即,0°、0° 和 90° 的平均值是 30° 还是 26.5°(arctan(1/2))?你如何定义标准差?
  • 为什么不使用(校正后的)样本标准差,使用角度之间的绝对差(请参阅下面我的答案中的函数absDiff_angle)?

标签: outliers anomaly-detection


【解决方案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]
    

    【讨论】:

    • 不错的映射,但在改变角度的分布时看起来像它。
    猜你喜欢
    • 2015-03-13
    • 2023-03-15
    • 1970-01-01
    • 1970-01-01
    • 2019-03-22
    • 2021-10-30
    • 2020-01-17
    • 2015-05-17
    • 2019-04-13
    相关资源
    最近更新 更多