考虑下面四个百分比,用浮点数表示:

    13.626332%
    47.989636%
     9.596008%
    28.788024%
   -----------
   100.000000%

我需要用整数表示这些百分比。如果我简单地使用Math.round(),我最终得到的总数是101%。

14 + 48 + 10 + 29 = 101

如果我使用parseInt(),我最终得到了97%。

13 + 47 + 9 + 28 = 97

有什么好的算法可以将任何百分比数表示为整数,同时还保持总数为100%?


编辑:在阅读了一些评论和回答后,显然有很多方法可以解决这个问题。

在我看来,为了保持数字的真实性,“正确”的结果是最小化总体误差的结果,定义为相对于实际值会引入多少误差舍入:

        value  rounded     error               decision
   ----------------------------------------------------
    13.626332       14      2.7%          round up (14)
    47.989636       48      0.0%          round up (48)
     9.596008       10      4.0%    don't round up  (9)
    28.788024       29      2.7%          round up (29)

在平局的情况下(3.33,3.33,3.33)可以做出任意的决定(例如3,4,3)。


当前回答

如果你只有两个选项,你可以使用Math.round()。唯一有问题的值对是X.5(例如;37.5和62.5)它会四舍五入两个值,你最终会得到101%,你可以试试这里:

https://jsfiddle.net/f8np1t0k/2/

因为你需要始终显示100%,你只需从它们中删除一个百分比,例如在第一个

const correctedar= Number.isInteger(around -0.5) ?A - 1: A

或者你可以选择有更多%选票的选项。

1% diff的错误在1-100对值的划分的10k例中发生114次。

其他回答

我的JS实现由Varun Vohra投票的答案

const set1 = [13.626332, 47.989636, 9.596008, 28.788024];
// const set2 = [24.25, 23.25, 27.25, 25.25];

const values = set1;

console.log('Total: ', values.reduce((accum, each) => accum + each));
console.log('Incorrectly Rounded: ', 
  values.reduce((accum, each) => accum + Math.round(each), 0));

const adjustValues = (values) => {
  // 1. Separate integer and decimal part
  // 2. Store both in a new array of objects sorted by decimal part descending
  // 3. Add in original position to "put back" at the end
  const flooredAndSortedByDecimal = values.map((value, position) => (
    {
        floored: Math.floor(value),
        decimal: value - Number.parseInt(value),
        position
    }
  )).sort(({decimal}, {decimal: otherDecimal}) => otherDecimal - decimal);

  const roundedTotal = values.reduce((total, value) => total + Math.floor(value), 0);
  let availableForDistribution = 100 - roundedTotal;

  // Add 1 to each value from what's available
  const adjustedValues = flooredAndSortedByDecimal.map(value => {
    const { floored, ...rest } = value;
    let finalPercentage = floored;
    if(availableForDistribution > 0){
        finalPercentage = floored + 1;
        availableForDistribution--;
    }

    return {
        finalPercentage,
        ...rest
    }
  });

  // Put back and return the new values
  return adjustedValues
    .sort(({position}, {position: otherPosition}) => position - otherPosition)
    .map(({finalPercentage}) => finalPercentage);
}

const finalPercentages = adjustValues(values);
console.log({finalPercentages})

// { finalPercentage: [14, 48, 9, 29]}

我曾经写过一个un舍入工具,来找到一组数字的最小扰动来匹配一个目标。这是一个不同的问题,但理论上可以在这里使用类似的想法。在这种情况下,我们有一系列的选择。

因此,对于第一个元素,我们可以四舍五入到14,也可以四舍五入到13。这样做的代价(在二进制整数编程的意义上)对于向上舍入比向下舍入要小,因为向下舍入需要我们将该值移动更大的距离。同样,我们可以把每个数字四舍五入,所以我们总共有16个选择。

  13.626332
  47.989636
   9.596008
+ 28.788024
-----------
 100.000000

我通常会在MATLAB中使用bintprog(一种二进制整数编程工具)解决一般问题,但这里只有几个选项需要测试,所以用简单的循环就可以很容易地测试出16个选项中的每一个。例如,假设我们将这个集合四舍五入为:

 Original      Rounded   Absolute error
   13.626           13          0.62633
    47.99           48          0.01036
    9.596           10          0.40399
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.25266

总绝对误差为1.25266。它可以通过以下替代舍入来略微减少:

 Original      Rounded   Absolute error
   13.626           14          0.37367
    47.99           48          0.01036
    9.596            9          0.59601
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.19202

事实上,这就是绝对误差的最优解。当然,如果有20项,搜索空间的大小将是2^20 = 1048576。对于30或40个术语,这个空间将是相当大的。在这种情况下,您将需要使用能够有效搜索空间的工具,可能使用分支和绑定方案。

我不确定你需要什么程度的精度,但我要做的就是简单地把前n个数字加1,n是小数总和的上界。在这种情况下,它是3,所以我将给前3项加1,然后将其余的取整。当然,这并不是非常准确,有些数字可能会四舍五入或在不应该的时候,但它工作得很好,总是会得到100%。

因此[13.626332,47.989636,9.596008,28.788024]将是[14,48,10,28],因为Math.ceil(.626332+.989636+.596008+.788024) == 3

function evenRound( arr ) {
  var decimal = -~arr.map(function( a ){ return a % 1 })
    .reduce(function( a,b ){ return a + b }); // Ceil of total sum of decimals
  for ( var i = 0; i < decimal; ++i ) {
    arr[ i ] = ++arr[ i ]; // compensate error by adding 1 the the first n items
  }
  return arr.map(function( a ){ return ~~a }); // floor all other numbers
}

var nums = evenRound( [ 13.626332, 47.989636, 9.596008, 28.788024 ] );
var total = nums.reduce(function( a,b ){ return a + b }); //=> 100

你总是可以告诉用户这些数字是四舍五入的,可能不是非常准确……

可能做到这一点的“最佳”方法(引用是因为“最佳”是一个主观术语)是保持你所处位置的连续(非积分)计数,并四舍五入该值。

然后将其与历史记录一起使用,以确定应该使用什么值。例如,使用您给出的值:

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
13.626332   13.626332            14             0    14 ( 14 -  0)
47.989636   61.615968            62            14    48 ( 62 - 14)
 9.596008   71.211976            71            62     9 ( 71 - 62)
28.788024  100.000000           100            71    29 (100 - 71)
                                                    ---
                                                    100

在每个阶段,都不需要四舍五入数字本身。相反,将累积值四舍五入,并计算出从上一个基线中达到该值的最佳整数——该基线是前一行的累积值(四舍五入)。

这是可行的,因为您不会在每个阶段都丢失信息,而是更聪明地使用信息。“正确的”四舍五入值在最后一列,你可以看到它们的和是100。

在上面的第三个值中,您可以看到这与盲目舍入每个值之间的区别。虽然9.596008通常会四舍五入到10,但累积的71.211976正确地四舍五入到71 -这意味着只需要9就可以加上之前的基线62。


这也适用于“有问题的”序列,比如三个大约1/3的值,其中一个应该四舍五入:

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
33.333333   33.333333            33             0    33 ( 33 -  0)
33.333333   66.666666            67            33    34 ( 67 - 33)
33.333333   99.999999           100            67    33 (100 - 67)
                                                    ---
                                                    100

舍入的目标是产生最少的错误。当您对单个值进行舍入时,这个过程简单而直接,大多数人都很容易理解。当你同时四舍五入多个数字时,这个过程变得更加棘手——你必须定义如何组合错误,即必须最小化的错误。

Varun Vohra的答案将绝对误差的总和最小化,而且实现起来非常简单。然而,有一些边缘情况它不能处理-舍入24.25,23.25,27.25,25.25的结果应该是什么?其中一个需要被围捕,而不是减少。你可能会任意选择列表中的第一个或最后一个。

也许用相对误差比绝对误差更好。将23.25四舍五入到24会使它变化3.2%,而将27.25四舍五入到28只会使它变化2.8%。现在有一个明显的赢家。

我们还可以做进一步的调整。一种常见的技术是对每个错误进行平方运算,这样大错误的计数就不成比例地多于小错误。我还会使用非线性除数来得到相对误差——1%的误差比99%的误差重要99倍,这似乎是不对的。在下面的代码中,我使用了平方根。

完整算法如下:

将这些百分比四舍五入后相加,再减去100。这将告诉您这些百分比中有多少必须四舍五入。 为每个百分比生成两个错误分数,一个是四舍五入,另一个是四舍五入。取两者之差。 对上面产生的误差差异进行排序。 对于需要四舍五入的百分比数,从已排序的列表中选取一项,并将四舍五入后的百分比增加1。

您仍然可能有多个具有相同错误和的组合,例如33.3333333,33.3333333,33.3333333。这是不可避免的,结果完全是任意的。下面给出的代码倾向于四舍五入左边的值。

在Python中把它们放在一起是这样的。

from math import isclose, sqrt

def error_gen(actual, rounded):
    divisor = sqrt(1.0 if actual < 1.0 else actual)
    return abs(rounded - actual) ** 2 / divisor

def round_to_100(percents):
    if not isclose(sum(percents), 100):
        raise ValueError
    n = len(percents)
    rounded = [int(x) for x in percents]
    up_count = 100 - sum(rounded)
    errors = [(error_gen(percents[i], rounded[i] + 1) - error_gen(percents[i], rounded[i]), i) for i in range(n)]
    rank = sorted(errors)
    for i in range(up_count):
        rounded[rank[i][1]] += 1
    return rounded

>>> round_to_100([13.626332, 47.989636, 9.596008, 28.788024])
[14, 48, 9, 29]
>>> round_to_100([33.3333333, 33.3333333, 33.3333333])
[34, 33, 33]
>>> round_to_100([24.25, 23.25, 27.25, 25.25])
[24, 23, 28, 25]
>>> round_to_100([1.25, 2.25, 3.25, 4.25, 89.0])
[1, 2, 3, 4, 90]

正如您在最后一个示例中看到的,该算法仍然能够提供非直观的结果。尽管89.0不需要四舍五入,但是列表中的一个值需要四舍五入;相对误差最小的结果是将较大的值舍入,而不是较小的可选值。

这个答案最初主张遍历所有可能的向上舍入/向下舍入组合,但正如评论中指出的那样,更简单的方法效果更好。算法和代码反映了这种简化。