查找一组循环数据的中位数

时间:2017-09-27 15:11:18

标签: c++ algorithm median

我想编写一个C ++函数来查找循环数据数组的中位数。 例如,考虑从罗盘读取,其中假设读数在[0,360]。虽然1& 359看起来很远,由于阅读的循环性质,它们非常接近。

查找普通数据中N元素的中值如下。 1.排序N元素的数据(升序或降序) 2.如果N是奇数,则中值是排序数组中的第(N + 1)/第2个元素。 3.如果N是偶数,则中值是排序数组中第N / 2和N / 2 + 1个元素的平均值。

然而,循环数据中的环绕问题将问题带到了不同的维度,解决方案非常重要。

这里解释了从循环数据中找到意思的类似问题How do you calculate the average of a set of circular data? 上述链接中的建议是找到对应于每个角度的单位向量并找到平均值。然而,中位数需要对数据进行分类,并且在这种情况下对载体的分类没有任何意义。因此,我认为我们不能使用提议的方案来找到中位数!

5 个答案:

答案 0 :(得分: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)或{{ 1}},两者都是已知的。

修改

在此之后,我之前的回答需要进行一些调整。

从这里开始,很容易。获取上一步(atan(-inf))中获得的值,并将该值加180度。这是您的数据开始和结束位置的参考点。从这里开始,您可以将此参考点作为起点和终点对角度数据进行排序,并找到该数据的中位数。

答案 1 :(得分:2)

中位数的两个属性允许为中位数发现发明两种不同的算法。

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&lt; = N / 2,OR项[I]&gt;我+ 180
  • 如果不满足中位数的条件,则前进i或I。
  • 需要O(N * log N)进行排序,O(N)进行下一次扫描

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

答案 2 :(得分:2)

不可能将中位数概念扩展为循环数据。为简单起见,我们可以考虑[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...

任何一个都和另一个一样好。

声称无法在循环数据上定义中位数。我只是声称&#34;正常&#34;在不添加额外约束或做出任意选择的情况下,无法以有意义的方式将中位数扩展到该情况。

答案 3 :(得分:2)

实际上,我对这个话题的思考方式超出了健康状况的思考范围,因此在这里我将分享我的想法和发现。也许有人会遇到类似的问题,并发现这很有用。

我已经很多年没有使用C ++了,所以如果我用C#编写所有代码,请原谅我。我相信能说一口流利的C ++语言的人可以很轻松地翻译算法。

圆均值

首先,让我们定义circular mean。它是通过将点转换为弧度来计算的,其中您的周期(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即可。这样,您就可以找到圆点的marginal median,并以此作为角度。

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)selection algorithm,就像C ++中的nth_element)。但是,此数字是一个值,该值具有到数据元素的所有绝对(非平方!)距离的最小和。 (应该将此值称为位置的L1估算值。)

对循环数据进行排序无法帮助您找到中间值,因此通常的中位数思考方法行不通,但是您仍然可以找到使所有数据点的绝对距离之和最小的点。这是我想出的算法,它假设输入数据被归一化为> = 0和

通过遍历所有数据点并跟踪距离总和来工作。当您向右数据点移动距离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);

几何圆形中值

先前的算法存在不一致之处,即相对于圆均值定义中位数的方式。圆均值使圆上各点之间的欧几里德距离的平方和最小。换句话说,它看的是连接圆上的点的直线,穿过圆。

通过上面的计算得出的弧线中值是指弧距:通过在圆的周长上移动而不是在它们之间采用直线,这些点之间的距离是多少。

我已经考虑过如何解决这个问题,如果它困扰您,但是我还没有做任何实验,因此我不能声称以下方法有效。简而言之,我相信您可以对Iteratively reweighted least squares algorithm (IRLS)进行修改,这通常是用于计算geometric medians的内容。

想法是选择一个起始值(例如,上面给出的圆均值或圆弧中值),然后计算到每个点的欧几里得距离:Di = sqrt(dxi ^ 2 + dyi ^ 2)。圆均值将最小化这些距离的平方,因此每个点的权重应抵消该平方并重置为D:Wi = Di / Di ^ 2,即Wi = 1 / Di。

使用这些权重,计算加权的圆均值(与圆均值相同,但将每个正弦和余弦乘以该点的权重再求和),然后重复该过程。重复进行,直到经过足够的迭代,或者直到结果停止改变为止。

此算法的问题在于,如果当前解恰好落在数据点上,则该算法将被零除。即使距离不完全为零,如果您击中的距离足够近,解决方案也将停止移动,因为与其他所有对象相比,权重将变得巨大。可以通过除以该距离之前的距离增加一个小的固定偏移量来解决此问题。这将使解决方案不理想,但至少不会在错误的地方停下来。

除非偏移量相对较大,否则仍需要花费大量的迭代才能从错误的点中进行挖掘,并且偏移量越大,最终的解决方案就越糟糕。因此,最好的方法可能是从一个较大的偏移量开始,然后在每次下一次迭代时逐渐减小它。

答案 4 :(得分:1)

有关圆形中位数的定义和讨论,请参见

N.I。费希尔的“循环数据的统计分析”,剑桥大学。出版社1993

以及有关方程2.32和2.33的讨论。对于多峰或各向同性数据,可能不存在唯一的中位数。

找到一个将数据分成两个相等组的轴,然后选择角度较小的轴的末端。如果样本大小为奇数,则中位数将是一个数据点,否则将是2个数据点的中点。

还有其他语言(例如R,MatLab)的软件包,这些软件包将有助于为您编写的任何函数提供测试值。

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

特别请参阅median.circularmedianHL.circular

贝伦,菲利普。 “ CircStat:用于循环统计的MATLAB工具箱”。 Journal of Statistics Software(统计软件杂志)31,第。 1(2009年9月23日):1-21。 https://doi.org/10.18637/jss.v031.i10

并查看circ_median