在循环数据中查找异常值

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]

【讨论】:

不错的映射,但在改变角度的分布时看起来像它。

以上是关于在循环数据中查找异常值的主要内容,如果未能解决你的问题,请参考以下文章

在 PHP 中的数字数组中查找和删除异常值/异常

用于过滤和替换异常值的循环

R - 使用标准偏差查找时间序列数据集中的异常值

数学建模学习(74):隔离森林的异常值查找,简单的学会

如何检测业务数据中的异常

Python 条件循环异常处理