从(非)正态分布的数字数组中删除不相关的值(尾部)

Removing irrelevant values (end tail) from (non)normal distribution array of numbers

虽然我很欣赏这个问题 math-heavy,但这个问题的真正答案将对所有正在处理 MongoDB 的 $bucket 运算符(或其SQL 类比),并构建 cluster/heatmap 图表数据。

问题的详细描述:

我的数据库中有一组 unique/distinct 个价格值(它始终是一个 numbers 的数组,精度为 0.01)。

如您所见,它的大部分值都在 ~8 到 40 之间(在这种情况下)。

[
    7.9,  7.98,  7.99,  8.05,  8.15,  8.25,   8.3,  8.34,   8.35,  8.39,
    8.4,  8.49,   8.5,  8.66,   8.9,  8.97,  8.98,  8.99,      9,   9.1,
   9.15,   9.2,  9.28,   9.3,  9.31,  9.32,   9.4,  9.46,   9.49,   9.5,
   9.51,  9.69,   9.7,   9.9,  9.98,  9.99,    10,  10.2,  10.21, 10.22,
  10.23, 10.24, 10.25, 10.27, 10.29, 10.49, 10.51, 10.52,  10.53, 10.54,
  10.55, 10.77, 10.78, 10.98, 10.99,    11, 11.26, 11.27,  11.47, 11.48,
  11.49, 11.79, 11.85,  11.9, 11.99,    12, 12.49, 12.77,   12.8, 12.86,
  12.87, 12.88, 12.89,  12.9, 12.98,    13, 13.01, 13.49,  13.77, 13.91,
  13.98, 13.99,    14, 14.06, 14.16, 14.18, 14.19,  14.2,   14.5, 14.53,
  14.54, 14.55, 14.81, 14.88,  14.9, 14.98, 14.99,    15,  15.28, 15.78,
  15.79,  15.8, 15.81, 15.83, 15.84,  15.9, 15.92, 15.93,  15.96,    16,
   16.5,    17, 17.57, 17.58, 17.59,  17.6, 17.88, 17.89,   17.9, 17.93,
  17.94, 17.97, 17.99,    18, 18.76, 18.77, 18.78, 18.99,  19.29, 19.38,
  19.78,  19.9, 19.98, 19.99,    20, 20.15, 20.31, 20.35,  20.38, 20.39,
  20.44, 20.45, 20.49,  20.5, 20.69,  20.7, 20.77, 20.78,  20.79,  20.8,
   20.9, 20.91, 20.92, 20.93, 20.94, 20.95, 20.96, 20.99,     21, 21.01,
  21.75, 21.98, 21.99,    22, 22.45, 22.79, 22.96, 22.97,  22.98, 22.99,
     23, 23.49, 23.78, 23.79,  23.8, 23.81,  23.9, 23.94,  23.95, 23.96,
  23.97, 23.98, 23.99,    24, 24.49,  24.5, 24.63, 24.79,   24.8, 24.89,
   24.9, 24.96, 24.97, 24.98, 24.99,    25, 25.51, 25.55,  25.88, 25.89,
   25.9, 25.96, 25.97, 25.99,    26, 26.99,    27, 27.55,     28,  28.8,
  28.89,  28.9, 28.99,    29, 29.09,    30, 31.91, 31.92,  31.93,  33.4,
   33.5,  33.6,  34.6,  34.7, 34.79,  34.8,    35, 38.99,  39.57, 39.99,
     40,    49,    50, 50.55, 60.89, 99.99, 20000, 63000, 483000
]

问题本身或如何从 non-normal 元素中清除(非)正态分布尾部

我需要在这个数组中找到不相关的值,某种“脏尾巴”,并将它们删除。实际上,我什至不需要从数组中删除它,实际情况是找到 latest 相关的数字。将其定义为 cap 值,以找到 floor(最小相关)和 cap(最大相关)之间的范围,例如:

floor value => 8
cap value => 40

我在说什么?

例如,对于上面的数组:它将是 40(甚至可能是 60)之后的所有值,例如 49, 50, 50.55, 60.89, 99.99, 20000, 63000, 483000

他们都被我定义为non-normal。

什么会被算作回答?

  1. S级。 clear/optimal 代码(语言无关紧要,但首选 JavaScript)或公式(如果数学有的话)可以在一小段/non-resourceful 时间内解决问题。如果我什至不需要检查数组中的每个元素,或者可以跳过其中的一些元素,比如从 peak / 数组中最受欢迎的值开始,那将是完美的。

  2. 一层。您自己的经验或 code 尝试任何相关结果或改进当前公式以获得更好的性能。

  3. B 级。有用的东西。博客 article/google link。主要要求是有意义。 Non-obvious 欢迎提供解决方案。即使您的代码格式非常糟糕等等。

TL:DR 视觉澄清

我应该根据什么标准以及如何“定位尾部”/从数组中删除具有 x(急剧上升且很少出现)值的 non-relevant 元素?

这是代码的第 2 版,运行 目前正在生产中的确切版本。它涵盖了大约 80%+ 的问题,但仍然存在 bottle-neck.

/** priceRangeArray ALWAYS SORTED ASC */
let priceRangeArray = [1,2,3...]
/** Resulting array */
let priceArray = []
/** Control variable */
let prev_sV = 0
/** Array length is always more then 3 elements */
const L = priceRangeArray.length;
/** Sample Variance algorithm */
for (let i = 2; i < L-1; i++) {
    /**
     * We skip the first two value, because 1st sV could be too low
     * sV becomes previous sV
     */
     if (prev_SV === 0) {
       /** prev_sV of 2nd element */
       prev_sV = ( 1 / L * (Math.pow(priceRangeArray[1],2))) - (Math.pow((1 / L * priceRangeArray[1]),2));
     } else {
       prev_sV = sV 
     }
     /**
     * sample variance, right?
     * 1 / L * (el ^ 2) - ( 1 / L * el) ^ 2
     * @type {number}
     */
     sV = ( 1 / L * (Math.pow(priceRangeArray[i],2))) - (Math.pow((1 / L * priceRangeArray[i]),2));
     /** User-defined, 1.1 is a control constant */
     if (prev_sV * 1.1 < sV) {
        break;
     }
    /** Control passed values to new array */
    priceArray.push(priceRangeArray[i]);
}
console.log(priceArray)

它基于 Wikipedia's Variance article。逻辑很简单,只要我不能删除开始(前 2 个值,即使它们太低),我从数组的第 3 个元素开始 for of 循环并检查每个下一个,使用我的 control 公式(当前值和先前值的 sqrt(pow^2))。

此代码的第一个版本,具有线性逻辑,只需通过以下简单原则之一更改当前值的先前值,例如:

  • 如果当前值是前一个值的两倍 (xN),则 break
  • 如果当前值比前一个多 10%,则 break

真正的问题是,它对起始值或小值不起作用,例如数组:[ 1,2,3,4,13,14,16,22,100,500000].

如您所见,cap 值可以终止为 4 而不是 22,或 100.

给定的数据集有一些巨大的异常值,这使得使用标准统计方法进行分析有点困难(如果表现更好,我建议将几个候选分布拟合到它并找出最适合的 - 对数正态分布分布、beta 分布、gamma 分布等)。

确定要忽略哪些异常值的问题通常可以通过更简单但不那么严格的方法来解决; one method is to compare the values of the data at various percentiles and throw away the ones where the differences become "too high" (for a suitably chosen value of "too high").

例如,如果我们上升两个百分位,这里是最后几个条目; delta 列给出了前一个百分位数和这个百分位数之间的差异。

在这里,您可以看到一旦我们达到 87,与上一个条目的差异增加了近 2,并且从那里(大部分)上升。要使用“不错”的数字,让我们将 cut-off 设置为第 85 个百分位数,并忽略高于该值的所有值。

给定上面在名为数据的数组中的排序列表,我们忽略上面的任何索引

Math.floor(data.length*85/100)

如果上面的分析应该动态变化(或提醒注意 85 不是正确值的偏差),则可以在代码中重复上述分析,但我将其留作 reader 的练习。 =12=]

我还找到了另一个代码,对我的生产有帮助,至于现在,当前的工作版本结合了我之前的回答和 James McLead 的最佳实践:

  priceRange(
    quotes: number[],
    blocks: number,
  ): number[] {
    if (!quotes.length) return [];
    const length = quotes.length > 3 ? quotes.length - 3 : quotes.length;
    const start = length === 1 ? 0 : 1;

    const cap = Math.round(quotes[Math.floor(length * 0.9)]);
    const floor = Math.round(quotes[start]);
    const price_range = cap - floor;
    /** Step represent 2.5% for each cluster */
    const tick = price_range / blocks;
    return Array(Math.ceil((cap + tick - floor) / tick))
      .fill(floor)
      .map((x, y) => parseFloat((x + y * tick).toFixed(4)));
  }

对于这样的数组:

[1, 20, ..., 40, 432, 567, 345346]

底价将确定为:20, 上限为 ~40,步长为 ~0.5,结果为:

[20, 20.5, 21, ... 39.5, 40]