寻找一个圆形数据集的中位数

6
我想编写一个C ++函数,用于查找循环数据数组的中位数。例如,考虑从罗盘读取的数据,假定读数在[0,360)范围内。尽管1和359看起来相距很远,但由于读数的循环性质,它们非常接近。
对于普通数据中的N个元素,找到中位数的步骤如下: 1. 对N个元素的数据进行排序(按升序或降序) 2. 如果N为奇数,则中位数是排序后数组中的(N + 1)/ 2个元素。 3. 如果N为偶数,则中位数是排序后数组中的第N / 2个元素和第N / 2 + 1个元素的平均值。
然而,循环数据中的环绕问题使问题具有不同的维度,该解决方案变得非常复杂。 如何计算一组循环数据的平均值? 在上面链接中解释了从循环数据中找到平均值的类似问题。上述链接中的建议是找到与每个角度对应的单位向量并找到平均值。然而,中位数需要对数据进行排序,而向量排序在此情况下没有任何意义。因此,我不认为我们可以使用所提出的方案来找到中位数!

1
实际上,我认为中位数的概念在这种情况下没有自然的扩展。我认为您需要一个额外的条件,例如:中位数使得其周围的分布最小化。 - 463035818_is_not_a_number
2
考虑例如 [0 180],其中 90270 都同样适用... 或者对于 [0 60 120 180 270],存在 5 种可能的解决方案,即使是“最小间隔”条件也无法帮助唯一选择其中一个。 - 463035818_is_not_a_number
1
你能给出一个关于循环值的中位数的定义并解释其含义吗? - MBo
2
也许你在https://math.stackexchange.com/上会更有运气。 - 463035818_is_not_a_number
1
但是360和0之间没有区别,我认为@tobi303是正确的。 - aLoneStrider
显示剩余7条评论
5个回答

6
我其实对这个主题考虑得比健康的多,所以我会在这里分享我的想法和发现。也许有人会遇到类似的问题,并从中受益。
我很多年没有使用C++了,所以如果我用C#编写所有代码,请原谅我。我相信流利的C++使用者可以很容易地将算法转换过来。

平均角度

首先,让我们定义平均角度。它是通过将您的点转换为弧度计算得出的,其中您的周期(256、360或任何值 - 被解释为与零相同的值)被缩放为2*pi。然后,计算这些弧度值的正弦和余弦。这些是您在单位圆上的值的y和x坐标。然后您将所有的正弦和余弦相加,并计算atan2。这给出了平均角度,可以通过除以缩放因子轻松地转换回您的数据点。
var scalingFactor = 2 * Math.PI / period;

var sines = 0.0;
var cosines = 0.0;
foreach (var value in inputs)
{
    var radians = value * scalingFactor;
    sines += Math.Sin(radians);
    cosines += Math.Cos(radians);
}

var circularMean = Math.Atan2(sines, cosines) / scalingFactor;

if (circularMean >= 0)
    return circularMean;
else
    return circularMean + period;

边缘圆中位数

计算圆中位数的最简单方法是修改计算圆平均值的方式。

可以通过寻找正弦和余弦的中位数而不是总和,然后计算其atan2来计算圆中位数。这样,您就可以找到圆点的边缘中位数并将其角度作为结果。

var scalingFactor = 2 * Math.PI / period;

var sines = new List<double>();
var cosines = new List<double>();
foreach (var value in inputs)
{
    var radians = value * scalingFactor;
    sines.Add(Math.Sin(radians));
    cosines.Add(Math.Cos(radians));
}

var circularMedian = Math.Atan2(Median(sines), Median(cosines)) / scalingFactor;

if (circularMedian >= 0)
    return circularMedian;
else
    return circularMedian + period;

这种方法是O(n),鲁棒性强,实现非常简单。它可能足够适合您的目的,但它有一个问题:旋转输入点会给出不同的结果。根据您输入数据的分布,这可能是一个问题或不是一个问题。
圆弧中位数
为了理解这个其他方法,您需要停止以“这就是如何计算”为思考均值和中位数,而是从结果值实际代表的角度来思考。
对于非循环数据,通过将所有值相加并除以元素数量获得平均值。然而,这个数字所代表的是与数据元素的所有平方距离之和最小的值。(我听到统计学家称这个值为L2位置估计,但一个统计学家应该确认或否认这一点。)
同样地,对于中位数。通过找到在所有数据排序后处于中间位置的数据元素来获得它(理想情况下,使用O(n)的选择算法,如C++中的nth_element)。然而,这个数字是一个具有最小绝对距离和的值(非平方!)到数据元素。(据说,这个值被称为L1位置估计。)
对于循环数据进行排序无法帮助您找到中间位置,因此通常思考中位数的方法不起作用,但是您仍然可以找到最小化与所有数据点的绝对距离之和的点。以下是我想出的算法,假设输入数据已归一化为>= 0且<周期,然后排序,时间复杂度为O(n)。(如果您需要将此排序作为计算的一部分进行,则运行时间为O(n log n)。)
它通过遍历所有数据点并跟踪距离总和来工作。当你将右侧的数据点向右移动D距离时,所有左侧点到原点的距离总和增加了D*LeftCount,而所有右侧点到原点的距离总和减少了D*RightCount。然后,如果一些左侧点现在实际上是右侧点,因为它们的左侧距离大于period/2,则会减去它们的先前距离并添加新的正确距离。
为了将当前总和与最佳总和进行比较,我添加了一些容差以防止不精确的浮点运算。
可能存在多个或无限多个满足最小距离条件的点。对于具有偶数个值的非循环中位数,中位数可以是两个中心值之间的任何值。通常取这两个中心值的平均值,因此我采用了类似的方法来计算这个中位数算法。我找到所有最小化距离的数据点,然后只计算这些点的圆形平均值。
// Requires a sorted list with values normalized to [0,period).

// Doing an initialization pass:
//   * candidate is the lowest number
//   * finding the index where the circle with this candidate starts
//   * calculating the score for this candidate - the sum of absolute distances
//   * counting the number of values to the left of the candidate
int i;
var candidate = list[0];
var distanceSum = 0.0;
for (i = 1; i < list.Count; ++i)
{
    if (list[i] >= candidate + period / 2)
        break;
    distanceSum += list[i] - candidate;
}
var leftCount = list.Count - i;
var circleStart = i;
if (circleStart == list.Count)
    circleStart = 0;
else
    for (; i < list.Count; ++i)
        distanceSum += candidate + period - list[i];

var previousCandidate = candidate;
var bestCandidates = new List<double> { candidate };
var bestDistanceSum = distanceSum;
var equalityTolerance = period * 1e-10;

for (i = 1; i < list.Count; ++i)
{
    candidate = list[i];

    // A formula for correcting the distance given the movement to the right.
    // It doesn't take into account that some values may have wrapped to the other side of the circle.
    ++leftCount;
    distanceSum += (2 * leftCount - list.Count) * (candidate - previousCandidate);

    // Counting all the values that wrapped to the other side of the circle
    // and correcting the sum of distances from the candidate.
    if (i <= circleStart)
        while (list[circleStart] < candidate + period / 2)
        {
            --leftCount;
            distanceSum += 2 * (list[circleStart] - candidate) - period;
            ++circleStart;
            if (circleStart == list.Count)
            {
                circleStart = 0;
                break; // Letting the next loop continue.
            }
        }
    if (i > circleStart)
        while (list[circleStart] < candidate - period / 2)
        {
            --leftCount;
            distanceSum += 2 * (list[circleStart] - candidate) + period;
            ++circleStart;
        }

    // Comparing current sum to the best one, using the given tolerance.
    if (distanceSum <= bestDistanceSum + equalityTolerance)
    {
        if (distanceSum >= bestDistanceSum - equalityTolerance)
        {
            // The numbers are close, so using their average as the next best.
            bestDistanceSum = (bestCandidates.Count * bestDistanceSum + distanceSum) / (bestCandidates.Count + 1);
        }
        else
        {
            // The new number is significantly better, clearing.
            bestDistanceSum = distanceSum;
            bestCandidates.Clear();
        }
        bestCandidates.Add(candidate);
    }

    previousCandidate = candidate;
}

if (bestCandidates.Count == 1)
    return bestCandidates[0];
else
    return CircularMean(bestCandidates, period);

几何圆中位数

在先前的算法中存在一些不一致之处,即中位数与圆平均值之间的定义方式。圆平均值将欧几里德距离的平方和最小化,即它看着连接圆上点的直线,穿过圆。

如我上面所计算的弧中位数,它看着弧距离:通过在圆的周长上移动来确定点之间的距离,而不是通过它们之间的直线。

如果这个问题困扰你,我已经考虑了如何解决它,但我还没有进行任何实验,因此无法声称以下方法有效。简而言之,我相信您可以使用迭代重新加权最小二乘算法(IRLS)的修改版本,这通常用于计算几何中位数

这个算法的思路是选择一个起始值(例如上面提到的圆形平均值或弧中位数),并计算到每个点的欧几里德距离:Di = sqrt(dxi^2 + dyi^2)。圆形平均值将最小化这些距离的平方,因此每个点的权重应该抵消平方并重置为只有D:Wi = Di / Di^2,即Wi = 1 / Di。
使用这些权重,计算加权圆形平均值(与圆形平均值相同,但在对它们求和之前将每个正弦和余弦乘以该点的权重)并重复此过程。重复直到足够多的迭代已经过去或者结果变化不大为止。
这个算法的问题是,如果当前解恰好落在数据点上,则会出现除以零的情况。即使距离不是完全为零,如果你靠近点,解也会停止移动,因为权重将与所有其他权重相比变得巨大。这可以通过在除以距离之前添加一个小的固定偏移量来修复。这将使解不是最优的,但至少它不会停在错误的点上。
“除非偏差相对较大,否则仍需要进行若干次迭代才能使其摆脱错误点,并且最终解决方案的偏差越大,结果就越糟糕。因此,最好的方法可能是从一个相当大的偏差开始,然后逐步减小每个下一次迭代的偏差。”

在关于平均数的话题中,楼主提到了一种算法,可能很适合与我所介绍的弧中位数方法配对,用于最终平均多个候选者:https://dev59.com/3nRB5IYBdhLWcg3w26t3#3651941 与圆形平均数方法不同,该算法通过弧长而非直线距离来计算平均值。但我并没有真正研究这个算法,也不确定它是否保证从弧中位数给定的候选点产生一个唯一的点。 - relatively_random

4

中位数有两个性质,可以发明两种不同的中位数查找算法。

1) 中位数使得到所有其他元素的绝对距离之和最小 -- O(n^2) 算法:

for (i = 0; i < N; i++)
{
     sum = 0;
     for (j = 0; j < N; j++)
        sum += abs(item[i] - item[j]) % 360;
     if (sum < best_so_far) { best_so_far = sum; index = i; }
}

2) 中位数满足一半的项目小于它,另一半大于它

  • 对项目进行排序
  • 定位第一组项目(i = 0 ... I),满足以下条件之一: I <= N/2,或者 item[I] > i + 180
  • 如果中位数的条件不满足,则推进 i 或 I。
  • 需要 O(N*log N) 进行排序和 O(N) 进行下一个扫描

当然,在循环数据中,所有项目(以及数据点之间的所有项目)都可以成为中位数的合适候选项。


4

关于循环中位数的定义和讨论,请参见 N.I. Fisher 的《Circular Data 的统计分析》(剑桥大学出版社,1993年)以及围绕着方程式2.32和2.33的讨论。对于多峰或各向同性数据,可能不存在唯一的中位数。

找到将数据分为2组的轴,并选择角度较小的端点。如果样本量是奇数,则中位数将是一个数据点,否则它将是2个数据点的中点。

其他语言中也有一些包(如 R、MatLab),可以帮助为您编写的任何函数提供测试值。

例如:https://www.rdocumentation.org/packages/circular/versions/0.4-93

特别是请查看 median.circularmedianHL.circular

或者

Berens, Philipp. ‘CircStat: A MATLAB Toolbox for Circular Statistics’. Journal of Statistical Software 31, no. 1 (23 September 2009): 1–21. https://doi.org/10.18637/jss.v031.i10.

请查看 circ_median


3
使用你的角度数据点向量(即从0到259的数字向量),创建两个新向量,我称它们为xy。这两个新向量分别是您的角度数据点的正弦和余弦。
也就是说,x[n]=cos(data[n])y[n]=sin(data[n]),其中data是您的角度数据向量,n是数据点的数量。
接下来,将x向量中的所有值加起来,得到一个单独的值,称其为sum_x,将y向量中的所有值加起来,得到另一个单独的值,称其为sum_y
现在,您可以进行反正切运算(例如atan(sum_y/sum_x))以获得一个新值。而这个值非常有意义。这个值基本上告诉您数据“指向”的方向,即您的数据主要存在于哪个方向。注意:必须谨慎处理除以0的情况(当sum_x=0时)和不定形式的出现(当sum_x=0且sum_y=0时)。不定形式只是表示您的数据均匀分布,在这种情况下中位数是无意义的,而当sum_x=0但sum_y!= 0时,则实际上为atan(inf)atan(-inf),两者都是已知的。
编辑:
在此之后,很容易。取出您在前一步中得到的值(atan(sum_y/sum_x))并将180度加到该值。这是您的数据开始和结束的参考点。从这里开始,您可以使用此参考点对您的角度数据进行排序,并找到该数据的中位数。

1
聪明但可惜不是真的。你的arctan(sum_y / sum_x)是平均点,没有统计学上的理由证明你所说的中位数是最接近该点的值。 - Bathsheba
我理解你的观点,但我并不完全确定那是否正确。在循环数据中,中位数和平均数更为密切相关。 - ImaginaryHuman072889
2
对我来说,以平均值的相反数为原点,旋转变换数据使得该原点的值为零,取该集合的中位数,然后再进行反向变换是一个很好的定义。但我没有这种方法的引用。 - Bathsheba
但我认为现在这里有足够的材料可以获得点赞了。它来了。 - Bathsheba
这是个好主意。我现在会修改我的回答。 - ImaginaryHuman072889

1
无法将中位数的概念正式扩展到圆形数据。为了简单起见,让我们考虑在[0 10)内的数字,并以(已排序)集合{ 1 3 5 7 8 }为例。根据数组的旋转方式,中位数会有不同的值:
1 3 5 7 8    -> 5
3 5 7 8 1    -> 7
5 7 8 1 3    -> 8
...etc...

任何一个都和其他的一样好。

并不声称在循环数据上定义中位数是不可能的。我只是声称,如果没有添加额外的限制或进行任意选择,"正常的"中位数不能以有意义的方式扩展到该情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接