【问题标题】:How to make rounded percentages add up to 100%如何使四舍五入的百分比加起来等于 100%
【发布时间】:2012-11-09 02:58:21
【问题描述】:

考虑以下四个百分比,表示为float 数字:

    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%?


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

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

        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)。

【问题讨论】:

  • 假设您有 3.33、3.33 和 3.33。你会做 4 个哪一个?
  • 没错。这个问题体现了一个矛盾的术语。
  • 这是报告中非常常见的场景 - 如何显示并不总是与显示值的总和匹配的十进制值的“总和”。
  • 在您的示例中,“正确的”结果是什么?这可能会解决关于什么是“最佳”解决方案的分歧。

标签: algorithm math rounding percentage


【解决方案1】:

可能做到这一点的“最佳”方式(由于“最佳”是一个主观术语而被引用)是保持对您所在位置的运行(非整体)计数,并舍入 那个值.

然后将其与历史记录一起使用以计算出应使用的值。例如,使用您提供的值:

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

在每个阶段,您都不会对数字本身进行四舍五入。取而代之的是,您将 accumulated 值四舍五入并计算出从前一个基线达到该值的最佳整数 - 该基线是前一行的累积值(四舍五入)。

这很有效,因为您不会在每个阶段丢失信息,而是更智能地使用信息。 “正确”的四舍五入值位于最后一列,您可以看到它们的总和为 100。

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


这也适用于“有问题”的序列,例如三个大致-<sup>1</sup>/<sub>3</sub> 值,其中 一个 应该四舍五入:

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

【讨论】:

  • 这种方法也适用于舍入小数字,因为它可以防止输出中出现负数
  • @paxdiablo 这是一个聪明的解决方案。 :) 你能分享一个简短而快速的纯 JavaScript 实现吗?
  • @paxdiablo 你到底指的是什么“这两个问题”?
  • @Ben,关于您对我的“解决这两个问题”评论的查询,这将是对 earlier 评论的回应,该评论现已被删除。我现在已经删除了它,希望以这样的方式回答 this 如果你删除你的评论仍然有意义:-)
  • @paxdiablo 感谢您的回答。太可惜了!我会对这两个结果的相关内容感兴趣。你真的看到我的第一条评论了吗?
【解决方案2】:

如果您对它进行四舍五入,则没有好的方法可以在所有情况下都完全相同。

您可以取 N 个百分比的小数部分(在您给出的示例中为 4)。

添加小数部分。在您的示例中,总小数部分 = 3。

将分数最高的 3 个数字设置为上限,并将其余部分设置为下限。

(抱歉修改)

【讨论】:

  • 虽然这可能会提供加到 100 的数字,但您最终可能会将 3.9 变成 3,将 25.1 变成 26。
  • 没有。 3.9 将是 4,25.1 将是 25。我说要限制分数最高的 3 个数字而不是最高值。
  • 如果有太多以 .9 结尾的分数,比如 9 个 9.9% 的值和一个 10.9 的值,那么一个值将最终为 9%,8 个为 10%,一个为 11% .
【解决方案3】:

您可以尝试跟踪由于四舍五入导致的错误,然后如果累积的错误大于当前数字的小数部分,则根据粒度进行四舍五入。

13.62 -> 14 (+.38)
47.98 -> 48 (+.02 (+.40 total))
 9.59 -> 10 (+.41 (+.81 total))
28.78 -> 28 (round down because .81 > .78)
------------
        100

不确定这是否可以正常工作,但如果顺序颠倒,它似乎也可以工作:

28.78 -> 29 (+.22)
 9.59 ->  9 (-.37; rounded down because .59 > .22)
47.98 -> 48 (-.35)
13.62 -> 14 (+.03)
------------
        100

我确信在某些极端情况下这可能会失败,但任何方法都至少会有些武断,因为您基本上是在修改输入数据。

【讨论】:

  • 数百年来,会计师和银行家一直在使用类似的技术。 “把剩余的”从一排移到下一排。从“进位”中的 1/2 美分开始。将“进位”添加到第一个值,然后截断。现在你通过截断损失的金额,把它放在“进位”中。一直这样做,四舍五入的数字每次都会精确地加起来。
  • Carolyn Kay 在 Access VB 2007 中建议了此实现: '使用“携带余数”方法循环退款 ref1 = rsQry![Refund Paid $$$] * rsQry![Property Value ] / propValTot ref2 = ref1 + ref5 '将进位的余数相加,零开始 ref3 = ref2 * 100 '乘以100为整数 ref4 = ref3 / 100 '除以100为十进制数 rsTbl![退款已支付$$ $] = ref4 '将“余数”四舍五入的数放入表中 ref5 = ref2 - ref4 '携带新的余数
【解决方案4】:

不要对四舍五入的数字求和。你会得到不准确的结果。根据术语的数量和小数部分的分布,总数可能会显着下降。

显示四舍五入的数字,但求和实际值。根据您呈现数字的方式,执行此操作的实际方式会有所不同。这样你就得到了

14 48 10 29 __ 100

无论你走哪条路,你都会有差异。在您的示例中,如果不以错误的方式“四舍五入”一个值,则无法显示加起来为 100 的数字(最小错误是将 9.596 更改为 9)

编辑

您需要在以下选项之一中进行选择:

  1. 项目的准确性
  2. 总和的准确性(如果您对四舍五入的值求和)
  3. 四舍五入的项目与四舍五入的和之间的一致性)

在大多数情况下,处理百分比 #3 是最佳选择,因为总和等于 101% 时比单个项目的总和不等于 100 时更明显,并且您保持单个项目准确。在我看来,“四舍五入”9.596 到 9 是不准确的。

为了解释这一点,我有时会添加一个脚注,说明各个值是四舍五入的,可能不是 100% - 任何了解四舍五入的人都应该能够理解该解释。

【讨论】:

  • 这不是很有帮助,因为打印的值加起来不会为 100。问题的目的是防止用户认为值不正确,在这种情况下,大多数人会在查看和比较总数时做。
  • @VarunVohra 阅读了我的编辑,你不能显示你的数字,这样它们加起来就是 100 而没有“四舍五入”超过 0.5。
  • @DStanley 实际上,除了所有数字都小于 0.5 的集合,你可以。检查我的答案 - LRM 正是这样做的。
  • @VarunVohra 在原始示例中,LRM 将产生 14、48、9 和 29,它们会将 9.596 “四舍五入”为 9。如果我们基于整数分配 LRM 将是最准确的,但它仍然会改变一个结果超过半个单位。
【解决方案5】:

只要您不担心对原始十进制数据的依赖,有很多方法可以做到这一点。

第一个也是最流行的方法是Largest Remainder Method

基本上是:

  1. 四舍五入
  2. 求和与 100 的差
  3. 按小数部分的递减顺序将项目加 1 来分配差值

在你的情况下,它会是这样的:

13.626332%
47.989636%
 9.596008%
28.788024%

如果你取整数部分,你会得到

13
47
 9
28

加起来是 97,你想再加三个。现在,你看看小数部分,它们是

.626332%
.989636%
.596008%
.788024%

并取最大的,直到总数达到 100。所以你会得到:

14
48
 9
29

或者,您可以简单地选择显示一位小数而不是整数值。所以数字将是 48.3 和 23.9 等。这将使方差从 100 下降很多。

【讨论】:

  • 美国数学会网站上的这个“专题专栏”——Apportionment II: Apportionment Systems——描述了几种类似的“分配”方法。
  • 这几乎看起来像是我在stackoverflow.com/questions/5227215/…的答案的复制和粘贴。
  • 请注意,与您对@DStanley 答案的评论相反,在您的答案中,9.596008% 被四舍五入为 9%,相差超过 0.5%。不过,这仍然是一个很好的答案。
【解决方案6】:

这是银行家四舍五入的情况,也就是“四舍五入”。 BigDecimal 支持它。其目的是确保四舍五入平衡,即不利于银行或客户。

【讨论】:

  • 它不能确保舍​​入平衡 - 它只是通过在偶数和奇数之间分配半舍入来减少错误量。在某些情况下,银行家四舍五入会产生不准确的结果。
  • @DStanley 同意。我没有说别的。我陈述了它的目的。非常小心。
  • 很公平 - 我误解了你想说的话。无论哪种情况,我都不认为它可以解决问题,因为使用银行家四舍五入不会改变示例中的结果。
【解决方案7】:

我不确定您需要什么级别的准确度,但我只需将第一个 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

您可以随时告知用户这些数字是四舍五入的,可能不是超级准确...

【讨论】:

    【解决方案8】:

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

    因此,对于第一个元素,我们可以将其向上舍入到 14,或向下舍入到 13。这样做的成本(在二进制整数编程意义上)向上舍入比向下舍入要少,因为down 要求我们将该值移动更大的距离。同样,我们可以将每个数字向上或向下取整,因此我们必须从总共 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 个词,该空间将非常大。在这种情况下,您需要使用可以有效搜索空间的工具,可能使用分支定界方案。

    【讨论】:

    • 仅供将来参考:“最大余数”算法必须根据您的指标最小化总绝对误差(请参阅@varunvohra 的答案)。证明很简单:假设它没有最小化错误。然后必须有一些向下舍入的值集,应该向上舍入,反之亦然(两组大小相同)。但是它向下舍入的每个值都比它向上舍入的任何值(和 v.v.)离下一个整数更远,因此新的错误量必须更大。 QED。但是,它不适用于所有错误度量;需要其他算法。
    【解决方案9】:

    我认为以下将实现你所追求的

    function func( orig, target ) {
    
        var i = orig.length, j = 0, total = 0, change, newVals = [], next, factor1, factor2, len = orig.length, marginOfErrors = [];
    
        // map original values to new array
        while( i-- ) {
            total += newVals[i] = Math.round( orig[i] );
        }
    
        change = total < target ? 1 : -1;
    
        while( total !== target ) {
    
            // Iterate through values and select the one that once changed will introduce
            // the least margin of error in terms of itself. e.g. Incrementing 10 by 1
            // would mean an error of 10% in relation to the value itself.
            for( i = 0; i < len; i++ ) {
    
                next = i === len - 1 ? 0 : i + 1;
    
                factor2 = errorFactor( orig[next], newVals[next] + change );
                factor1 = errorFactor( orig[i], newVals[i] + change );
    
                if(  factor1 > factor2 ) {
                    j = next; 
                }
            }
    
            newVals[j] += change;
            total += change;
        }
    
    
        for( i = 0; i < len; i++ ) { marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i]; }
    
        // Math.round() causes some problems as it is difficult to know at the beginning
        // whether numbers should have been rounded up or down to reduce total margin of error. 
        // This section of code increments and decrements values by 1 to find the number
        // combination with least margin of error.
        for( i = 0; i < len; i++ ) {
            for( j = 0; j < len; j++ ) {
                if( j === i ) continue;
    
                var roundUpFactor = errorFactor( orig[i], newVals[i] + 1)  + errorFactor( orig[j], newVals[j] - 1 );
                var roundDownFactor = errorFactor( orig[i], newVals[i] - 1) + errorFactor( orig[j], newVals[j] + 1 );
                var sumMargin = marginOfErrors[i] + marginOfErrors[j];
    
                if( roundUpFactor < sumMargin) { 
                    newVals[i] = newVals[i] + 1;
                    newVals[j] = newVals[j] - 1;
                    marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i];
                    marginOfErrors[j] = newVals[j] && Math.abs( orig[j] - newVals[j] ) / orig[j];
                }
    
                if( roundDownFactor < sumMargin ) { 
                    newVals[i] = newVals[i] - 1;
                    newVals[j] = newVals[j] + 1;
                    marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i];
                    marginOfErrors[j] = newVals[j] && Math.abs( orig[j] - newVals[j] ) / orig[j];
                }
    
            }
        }
    
        function errorFactor( oldNum, newNum ) {
            return Math.abs( oldNum - newNum ) / oldNum;
        }
    
        return newVals;
    }
    
    
    func([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100); // => [16, 16, 17, 17, 17, 17]
    func([33.333, 33.333, 33.333], 100); // => [34, 33, 33]
    func([33.3, 33.3, 33.3, 0.1], 100); // => [34, 33, 33, 0] 
    func([13.25, 47.25, 11.25, 28.25], 100 ); // => [13, 48, 11, 28]
    func( [25.5, 25.5, 25.5, 23.5], 100 ); // => [25, 25, 26, 24]
    

    最后一件事,我使用问题中最初给出的数字运行该函数,以与所需的输出进行比较

    func([13.626332, 47.989636, 9.596008, 28.788024], 100); // => [48, 29, 13, 10]
    

    这与问题想要的不同 => [ 48, 29, 14, 9]。在我查看总误差之前我无法理解这一点

    -------------------------------------------------
    | original  | question | % diff | mine | % diff |
    -------------------------------------------------
    | 13.626332 | 14       | 2.74%  | 13   | 4.5%   |
    | 47.989636 | 48       | 0.02%  | 48   | 0.02%  |
    | 9.596008  | 9        | 6.2%   | 10   | 4.2%   |
    | 28.788024 | 29       | 0.7%   | 29   | 0.7%   |
    -------------------------------------------------
    | Totals    | 100      | 9.66%  | 100  | 9.43%  |
    -------------------------------------------------
    

    基本上,我的函数的结果实际上引入了最少的错误。

    小提琴here

    【讨论】:

    • 这几乎就是我的想法,不同之处在于应该相对于值来测量误差(将 9.8 舍入到 10 比从 19.8 舍入到 20 误差更大)。不过,这可以通过在排序回调中反映它来轻松完成。
    • 这对于 [33.33, 33.33, 33.33, 0.1] 是错误的,它返回 [1, 33, 33, 33] 而不是更准确的 [34, 33, 33, 0]
    • @yonilevy 谢谢你。现已修复。
    • 还没有,对于 [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] 它返回 [15, 17, 17, 17, 17, 17] 而不是 [16, 16, 17, 17 , 17, 17] - 看我的回答
    【解决方案10】:

    由于这里的答案似乎都无法正确解决,这是我使用underscorejs 的半混淆版本:

    function foo(l, target) {
        var off = target - _.reduce(l, function(acc, x) { return acc + Math.round(x) }, 0);
        return _.chain(l).
                sortBy(function(x) { return Math.round(x) - x }).
                map(function(x, i) { return Math.round(x) + (off > i) - (i >= (l.length + off)) }).
                value();
    }
    
    foo([13.626332, 47.989636, 9.596008, 28.788024], 100) // => [48, 29, 14, 9]
    foo([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100) // => [17, 17, 17, 17, 16, 16]
    foo([33.333, 33.333, 33.333], 100) // => [34, 33, 33]
    foo([33.3, 33.3, 33.3, 0.1], 100) // => [34, 33, 33, 0]
    

    【讨论】:

    • 如果我错了,请纠正我,但这不是我的答案提出的算法的实现吗? (不清除 underscorejs)
    • @VarunVohra 抱歉,我直到现在才注意到这一点,是的,看起来你的算法是一样的 :) 不知道为什么我的帖子是被接受的答案,混淆的代码只是为了 lolz。 ..
    • @yonilevy 删除了我的评论;我只是没有意识到它应该返回一个排序列表。我道歉!
    • 当最后一个元素为0,前一个元素加100时,此函数有问题。 [52.6813880126183、5.941114616193481、24.55310199789695、8.780231335436383、8.04416403785489、0]。最后一个逻辑返回-1。我很快想到了以下解决方案,但可能有更好的方法:jsfiddle.net/0o75bw43/1
    • @Cruclax 当输入数组中的所有条目都为零时,它显示全部为 1
    【解决方案11】:

    我写了一个C#版本的rounding helper,算法和Varun Vohra's answer一样,希望对你有帮助。

    public static List<decimal> GetPerfectRounding(List<decimal> original,
        decimal forceSum, int decimals)
    {
        var rounded = original.Select(x => Math.Round(x, decimals)).ToList();
        Debug.Assert(Math.Round(forceSum, decimals) == forceSum);
        var delta = forceSum - rounded.Sum();
        if (delta == 0) return rounded;
        var deltaUnit = Convert.ToDecimal(Math.Pow(0.1, decimals)) * Math.Sign(delta);
    
        List<int> applyDeltaSequence; 
        if (delta < 0)
        {
            applyDeltaSequence = original
                .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
                .OrderBy(a => original[a.index] - rounded[a.index])
                .ThenByDescending(a => a.index)
                .Select(a => a.index).ToList();
        }
        else
        {
            applyDeltaSequence = original
                .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
                .OrderByDescending(a => original[a.index] - rounded[a.index])
                .Select(a => a.index).ToList();
        }
    
        Enumerable.Repeat(applyDeltaSequence, int.MaxValue)
            .SelectMany(x => x)
            .Take(Convert.ToInt32(delta/deltaUnit))
            .ForEach(index => rounded[index] += deltaUnit);
    
        return rounded;
    }
    

    它通过了以下单元测试:

    [TestMethod]
    public void TestPerfectRounding()
    {
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 2),
            new List<decimal> {3.33m, 3.34m, 3.33m});
    
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> {3.33m, 3.34m, 3.33m}, 10, 1),
            new List<decimal> {3.3m, 3.4m, 3.3m});
    
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 1),
            new List<decimal> {3.3m, 3.4m, 3.3m});
    
    
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> { 13.626332m, 47.989636m, 9.596008m, 28.788024m }, 100, 0),
            new List<decimal> {14, 48, 9, 29});
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> { 16.666m, 16.666m, 16.666m, 16.666m, 16.666m, 16.666m }, 100, 0),
            new List<decimal> { 17, 17, 17, 17, 16, 16 });
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> { 33.333m, 33.333m, 33.333m }, 100, 0),
            new List<decimal> { 34, 33, 33 });
        CollectionAssert.AreEqual(Utils.GetPerfectRounding(
            new List<decimal> { 33.3m, 33.3m, 33.3m, 0.1m }, 100, 0),
            new List<decimal> { 34, 33, 33, 0 });
    }
    

    【讨论】:

    • 不错!给了我一个基础。虽然我相信 Enumerable 没有 ForEach
    • 但它因某些数据而失败 - 它为 Convert.ToInt32(delta/deltaUnit) 创建了一个太大的数字,例如-80298.70329 -9899.774653 -1219.826237 -12668.67994 -6545.783201 -4406.79133 -8027.827479 -4578.333489 -242.7060883 -1271.779903 -635.8899513 -768.5692796 -1239.937831 -627.3605659 -566.8920777 66250 130000 -970.8243532 -62279.32034 跨度>
    【解决方案12】:

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

    well-voted answer by Varun Vohra 最小化了绝对误差的总和,而且实现起来非常简单。但是,它无法处理某些极端情况 - 舍入 24.25, 23.25, 27.25, 25.25 的结果应该是什么?其中之一需要向上取整而不是向下取整。您可能会随意选择列表中的第一个或最后一个。

    也许最好使用 relative 错误而不是 absolute 错误。将 23.25 向上舍入为 24 将其更改为 3.2%,而将 27.25 向上舍入为 28 仅将其更改为 2.8%。现在有一个明显的赢家。

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

    完整算法如下:

    1. 在四舍五入后将百分比相加,然后从 100 中减去。这会告诉您其中有多少百分比必须四舍五入。
    2. 为每个百分比生成两个错误分数,一个在向下舍入时,一个在向上舍入时。取两者之间的差异。
    3. 对上面产生的误差差异进行排序。
    4. 对于需要四舍五入的百分比数,从排序列表中取一项并将向下舍入的百分比增加 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 不需要四舍五入,但该列表中的一个值需要四舍五入;最小的相对误差来自于对大值进行四舍五入,而不是对更小的替代值进行四舍五入。

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

    【讨论】:

    • 我认为您不需要考虑所有组合:按加权误差从 round to zeroround to infinity 递减的顺序处理i>(几乎只是将 weighing 引入 Verun Vohras'syonilevy's(“相同”)答案)。
    • @greybeard 你说得对,我想多了。我不能只对错误进行排序,因为每个值都有 两个 错误,但取差值解决了这个问题。我已经更新了答案。
    • 我更喜欢在实际数字为 0% 时始终为 0%。所以将if actual == 0: return 0 添加到error_gen 效果很好。
    • round_to_100开头的isclose方法是什么?
    【解决方案13】:

    如果你真的必须对它们进行四舍五入,这里已经有很好的建议(最大的余数,最小的相对误差,等等)。

    还有一个很好的理由不四舍五入(你会得到至少一个“看起来更好”但“错误”的数字),以及如何解决这个问题(警告你的读者),这就是我所做的.

    让我补充一下“错误”的数字部分。

    假设您有三个事件/实体/...,其中一些百分比近似为:

    DAY 1
    who |  real | app
    ----|-------|------
      A | 33.34 |  34
      B | 33.33 |  33
      C | 33.33 |  33
    

    稍后数值会略有变化,到

    DAY 2
    who |  real | app
    ----|-------|------
      A | 33.35 |  33
      B | 33.36 |  34
      C | 33.29 |  33
    

    第一个表存在已经提到的“错误”数字的问题:33.34 比 34 更接近 33。

    但是现在你有一个更大的错误。比较第 2 天和第 1 天,A 的实际百分比值增加了 0.01%,但近似值显示减少了 1%。

    这是一个定性错误,可能比最初的定量错误更糟糕。

    您可以为整个集合设计一个近似值,但是您可能必须在第一天发布数据,因此您不知道第二天的情况。所以,除非你真的、真的、必须近似,否则最好不要。

    【讨论】:

    • 任何知道如何制作更好的表格的人请编辑或告诉我如何/在哪里
    【解决方案14】:

    根据我的测试用例检查这是否有效。

    假设数字是 k;

    1. 按降序排列百分比。
    2. 从降序遍历每个百分比。
    3. 计算第一个百分比的 k 百分比,取输出的 Math.Ceil。
    4. 下一个 k = k-1
    5. 迭代直到所有百分比都用完。

    【讨论】:

      【解决方案15】:

      我已经为列表和字典实现了 Varun Vohra 的答案中的方法。

      import math
      import numbers
      import operator
      import itertools
      
      
      def round_list_percentages(number_list):
          """
          Takes a list where all values are numbers that add up to 100,
          and rounds them off to integers while still retaining a sum of 100.
      
          A total value sum that rounds to 100.00 with two decimals is acceptable.
          This ensures that all input where the values are calculated with [fraction]/[total]
          and the sum of all fractions equal the total, should pass.
          """
          # Check input
          if not all(isinstance(i, numbers.Number) for i in number_list):
              raise ValueError('All values of the list must be a number')
      
          # Generate a key for each value
          key_generator = itertools.count()
          value_dict = {next(key_generator): value for value in number_list}
          return round_dictionary_percentages(value_dict).values()
      
      
      def round_dictionary_percentages(dictionary):
          """
          Takes a dictionary where all values are numbers that add up to 100,
          and rounds them off to integers while still retaining a sum of 100.
      
          A total value sum that rounds to 100.00 with two decimals is acceptable.
          This ensures that all input where the values are calculated with [fraction]/[total]
          and the sum of all fractions equal the total, should pass.
          """
          # Check input
          # Only allow numbers
          if not all(isinstance(i, numbers.Number) for i in dictionary.values()):
              raise ValueError('All values of the dictionary must be a number')
          # Make sure the sum is close enough to 100
          # Round value_sum to 2 decimals to avoid floating point representation errors
          value_sum = round(sum(dictionary.values()), 2)
          if not value_sum == 100:
              raise ValueError('The sum of the values must be 100')
      
          # Initial floored results
          # Does not add up to 100, so we need to add something
          result = {key: int(math.floor(value)) for key, value in dictionary.items()}
      
          # Remainders for each key
          result_remainders = {key: value % 1 for key, value in dictionary.items()}
          # Keys sorted by remainder (biggest first)
          sorted_keys = [key for key, value in sorted(result_remainders.items(), key=operator.itemgetter(1), reverse=True)]
      
          # Otherwise add missing values up to 100
          # One cycle is enough, since flooring removes a max value of < 1 per item,
          # i.e. this loop should always break before going through the whole list
          for key in sorted_keys:
              if sum(result.values()) == 100:
                  break
              result[key] += 1
      
          # Return
          return result
      

      【讨论】:

        【解决方案16】:

        这是@varun-vohra 答案的更简单的 Python 实现:

        def apportion_pcts(pcts, total):
            proportions = [total * (pct / 100) for pct in pcts]
            apportions = [math.floor(p) for p in proportions]
            remainder = total - sum(apportions)
            remainders = [(i, p - math.floor(p)) for (i, p) in enumerate(proportions)]
            remainders.sort(key=operator.itemgetter(1), reverse=True)
            for (i, _) in itertools.cycle(remainders):
                if remainder == 0:
                    break
                else:
                    apportions[i] += 1
                    remainder -= 1
            return apportions
        

        你需要mathitertoolsoperator

        【讨论】:

          【解决方案17】:

          对于那些在熊猫系列中有百分比的人,这是我对Largest remainder method(如Varun Vohra's answer)的实现,您甚至可以选择要四舍五入的小数。

          import numpy as np
          
          def largestRemainderMethod(pd_series, decimals=1):
          
              floor_series = ((10**decimals * pd_series).astype(np.int)).apply(np.floor)
              diff = 100 * (10**decimals) - floor_series.sum().astype(np.int)
              series_decimals = pd_series - floor_series / (10**decimals)
              series_sorted_by_decimals = series_decimals.sort_values(ascending=False)
          
              for i in range(0, len(series_sorted_by_decimals)):
                  if i < diff:
                      series_sorted_by_decimals.iloc[[i]] = 1
                  else:
                      series_sorted_by_decimals.iloc[[i]] = 0
          
              out_series = ((floor_series + series_sorted_by_decimals) / (10**decimals)).sort_values(ascending=False)
          
              return out_series
          

          【讨论】:

            【解决方案18】:

            这是一个实现最大余数方法的 Ruby gem: https://github.com/jethroo/lare_round

            使用方法:

            a =  Array.new(3){ BigDecimal('0.3334') }
            # => [#<BigDecimal:887b6c8,'0.3334E0',9(18)>, #<BigDecimal:887b600,'0.3334E0',9(18)>, #<BigDecimal:887b4c0,'0.3334E0',9(18)>]
            a = LareRound.round(a,2)
            # => [#<BigDecimal:8867330,'0.34E0',9(36)>, #<BigDecimal:8867290,'0.33E0',9(36)>, #<BigDecimal:88671f0,'0.33E0',9(36)>]
            a.reduce(:+).to_f
            # => 1.0
            

            【讨论】:

              【解决方案19】:

              注意:选择的答案是改变不首选的数组顺序,这里我提供了更多不同的变体,以达到相同的结果并保持数组的顺序

              讨论

              给定[98.88, .56, .56],你想如何舍入它?你有四个选择

              1- 四舍五入并从其余数字中减去相加的内容,因此结果变为[98, 1, 1]

              这可能是一个很好的答案,但如果我们有 [97.5, .5, .5, .5, .5, .5] 怎么办?那么你需要将它四舍五入到[95, 1, 1, 1, 1, 1]

              你知道它是怎么回事吗?如果您添加更多类似 0 的数字,您将从其余数字中失去更多价值。当您有一大堆类似零的数字(例如[40, .5, .5 , ... , .5])时,这可能会非常麻烦。当你四舍五入时,你可能会得到一个数组:[1, 1, .... , 1]

              所以总结不是一个好的选择。

              2- 你将数字四舍五入。所以[98.88, .56, .56]变成[98, 0, 0],那么你比100少2。你忽略任何已经是0的东西,然后把差值加到最大的数字上。所以更大的数字会得到更多。

              3- 和前面一样,向下取整,但是你根据小数降序排序,根据小数除以差异,所以最大的小数将得到差异。

              4- 你四舍五入,但你将你添加的内容添加到下一个数字。所以就像波浪一样,您添加的内容将被重定向到数组的末尾。所以[98.88, .56, .56] 变成了[99, 0, 1]

              这些都不是理想的,因此请注意您的数据会变形。

              在这里,我提供了案例 2 和 3 的代码(因为当您有很多类似零的数字时,案例 1 不实用)。它是现代 Js,不需要任何库来使用

              第二种情况

              const v1 = [13.626332, 47.989636, 9.596008, 28.788024];// => [ 14, 48, 9, 29 ]
              const v2 = [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] // => [ 17, 17, 17, 17, 16, 16 ] 
              const v3 = [33.333, 33.333, 33.333] // => [ 34, 33, 33 ]
              const v4 = [33.3, 33.3, 33.3, 0.1] // => [ 34, 33, 33, 0 ]
              const v5 = [98.88, .56, .56] // =>[ 100, 0, 0 ]
              const v6 = [97.5, .5, .5, .5, .5, .5] // => [ 100, 0, 0, 0, 0, 0 ]
              
              const normalizePercentageByNumber = (input) => {
                  const rounded: number[] = input.map(x => Math.floor(x));
                  const afterRoundSum = rounded.reduce((pre, curr) => pre + curr, 0);
                  const countMutableItems = rounded.filter(x => x >=1).length;
                  const errorRate = 100 - afterRoundSum;
                  
                  const deductPortion = Math.ceil(errorRate / countMutableItems);
                  
                  const biggest = [...rounded].sort((a, b) => b - a).slice(0, Math.min(Math.abs(errorRate), countMutableItems));
                  const result = rounded.map(x => {
                      const indexOfX = biggest.indexOf(x);
                      if (indexOfX >= 0) {
                          x += deductPortion;
                          console.log(biggest)
                          biggest.splice(indexOfX, 1);
                          return x;
                      }
                      return x;
                  });
                  return result;
              }
              

              第三种情况

              const normalizePercentageByDecimal = (input: number[]) => {
              
                  const rounded= input.map((x, i) => ({number: Math.floor(x), decimal: x%1, index: i }));
              
                  const decimalSorted= [...rounded].sort((a,b)=> b.decimal-a.decimal);
                  
                  const sum = rounded.reduce((pre, curr)=> pre + curr.number, 0) ;
                  const error= 100-sum;
                  
                  for (let i = 0; i < error; i++) {
                      const element = decimalSorted[i];
                      element.number++;
                  }
              
                  const result= [...decimalSorted].sort((a,b)=> a.index-b.index);
                  
                  return result.map(x=> x.number);
              }
              
              

              第四种情况

              您只需要计算在每次汇总中为您的数字添加或减少了多少额外空气,然后在下一项中再次添加或减去它。

              const v1 = [13.626332, 47.989636, 9.596008, 28.788024];// => [14, 48, 10, 28 ]
              const v2 = [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] // => [17, 16, 17, 16, 17, 17]
              const v3 = [33.333, 33.333, 33.333] // => [33, 34, 33]
              const v4 = [33.3, 33.3, 33.3, 0.1] // => [33, 34, 33, 0]
              
              const normalizePercentageByWave= v4.reduce((pre, curr, i, arr) => {
              
                  let number = Math.round(curr + pre.decimal);
                  let total = pre.total + number;
              
                  const decimal = curr - number;
              
                  if (i == arr.length - 1 && total < 100) {
                      const diff = 100 - total;
                      total += diff;
                      number += diff;
                  }
              
                  return { total, numbers: [...pre.numbers, number], decimal };
              
              }, { total: 0, numbers: [], decimal: 0 });
              

              【讨论】:

                【解决方案20】:

                如果您只有两个选项,您最好使用Math.round()。唯一有问题的值对是 X.5(例如 37.5 和 62.5),它会将这两个值向上取整,您最终会得到 101%,您可以在这里尝试:

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

                由于您需要始终显示 100%,您只需从其中删除一个百分比,例如在第一个上

                const correctedARounded = Number.isInteger(aRounded-0.5) ? a - 1 : a

                或者您也可以赞成票数更高的选项。

                对于 1-100 个值对之间的 10k 次除法,1% diff 的错误发生了 114 次。

                【讨论】:

                  【解决方案21】:

                  well-voted answer by Varun Vohra 的我的 JS 实现

                  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]}
                  

                  【讨论】:

                  • 如果我使用值 [22, 25, 14, 36, 2, 2] 我得到 101...不确定是否有其他解决方法?
                  • 此算法适用于以错误方式四舍五入的小数。 2 + 3 永远不会是 4。同样,50 + 60 永远不会是 100。不能取消数学 :)
                  • 是的,谢谢,这是我的深夜评论!从本质上讲,我想要实现的是在保持小数位固有值的完整性的同时对一系列值进行四舍五入。这是针对一家公司的股权百分比,0.4% 可能会产生很大的不同。例如,如果我有值 [46.33, 22.13, 3.10, 11.61, 12.08, 4.76],则使用您的代码将这些值四舍五入为 [46, 22, 3, 12, 12, 5]。这一切都很好,但 12.08 和 11.61 之间的差异是 0.47,所以在我的情况下,将这些数字都舍入到 12 并不理想。
                  【解决方案22】:

                  为了简洁起见,或者类似的东西,你只是累积错误......

                  const p = [13.626332, 47.989636, 9.596008, 28.788024];
                  const round = (a, e = 0) => a.map(x => (r = Math.round(x + e), e += x - r, r));
                  console.log(round(p));
                  

                  结果:[14, 48, 9, 29]

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2021-02-18
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-12-05
                    • 2013-11-19
                    • 1970-01-01
                    相关资源
                    最近更新 更多