如何使舍入百分比加起来达到100%

poe*_*ezn 173 algorithm math rounding percentage

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

    13.626332%
    47.989636%
     9.596008%
    28.788024%
   -----------
   100.000000%
Run Code Online (Sandbox Code Playgroud)

我需要将这些百分比表示为整数.如果我只是使用Math.round(),我最终总共有101%.

14 + 48 + 10 + 29 = 101
Run Code Online (Sandbox Code Playgroud)

如果我使用parseInt(),我最终总共有97%.

13 + 47 + 9 + 28 = 97
Run Code Online (Sandbox Code Playgroud)

什么是一个很好的算法来表示任意数量的百分比作为整数,同时仍然保持总计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)
Run Code Online (Sandbox Code Playgroud)

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

Var*_*hra 148

如果您不担心依赖原始十进制数据,有很多方法可以做到这一点.

第一种也许是最流行的方法是最大剩余方法

基本上是:

  1. 把一切都搞定了
  2. 获得总和和100的差异
  3. 通过按小数部分的降序向项目添加1来分配差异

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

13.626332%
47.989636%
 9.596008%
28.788024%
Run Code Online (Sandbox Code Playgroud)

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

13
47
 9
28
Run Code Online (Sandbox Code Playgroud)

最多可添加97个,并且您想再添加三个.现在,你看一下小数部分

.626332%
.989636%
.596008%
.788024%
Run Code Online (Sandbox Code Playgroud)

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

14
48
 9
29
Run Code Online (Sandbox Code Playgroud)

或者,您可以简单地选择显示一个小数位而不是整数值.因此数字将是48.3和23.9等.这将使方差从100减少很多.

  • 美国数学学会网站上的这个"专题栏" - [分配II:分配系统](http://www.ams.org/samplings/feature-column/fcarc-apportionii1) - 描述了几种类似的"分配"方法. (5认同)
  • 这几乎看起来像是我的答案的复制和粘贴 /sf/ask/365905081/# 5229037。 (3认同)

yon*_*evy 30

由于这里的答案似乎都没有正确解决,这里是我使用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]
Run Code Online (Sandbox Code Playgroud)

  • 如果我错了,请纠正我,但这不是我的回答提出的算法的实现吗?(不要清楚下划线) (6认同)
  • 当最后一个元素为0且前一个元素加为100时,此功能存在问题。例如[52.6813880126183、5.941146616193481、24.55310199789695、8.78203135436383、8.04416403785489、0]。最后一个逻辑上返回-1。我很快想到了以下解决方案,但可能还有更好的方法:https://jsfiddle.net/0o75bw43/1/ (2认同)

pax*_*blo 27

实现这一目标的"最佳"方法可能是保持一个运行(非整数)的位置,并将值四舍五入,然后将其与历史一起使用以确定应使用的值.例如,使用您提供的值:

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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 这可行,但问题是舍入有点随意,并且取决于数字的顺序。使用@vvohra87的答案将使其独立于顺序,并确保具有最高余数的数字被四舍五入 (4认同)

Mar*_*som 14

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

Varun Vohra精心回答的答案最大限度地减少了绝对误差的总和,并且实现起来非常简单.然而,有一些它无法处理的边缘情况 - 舍入的结果应该是什么24.25, 23.25, 27.25, 25.25?其中一个需要四舍五入而不是下降.您可能只是随意选择列表中的第一个或最后一个.

也许最好使用相对误差而不是绝对误差.在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中将它们放在一起就像这样.

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]
Run Code Online (Sandbox Code Playgroud)

正如您在上一个示例中所看到的,此算法仍然能够提供非直观的结果.尽管89.0不需要四舍五入,但该列表中的一个值需要四舍五入; 最小的相对误差是由于将这个较大的值四舍五入而不是更小的替代值.

这个答案最初主张通过舍入/向下舍入的每个可能的组合,但正如评论中指出的,更简单的方法更好.算法和代码反映了这种简化.

  • 当实际数字为 0% 时,我更喜欢始终为 0%。因此,将“if实际== 0:返回0”添加到“error_gen”效果很好。 (2认同)
  • @toto_tico /sf/ask/391679781/ (2认同)

D S*_*ley 7

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

显示舍入的数字但总和实际值.根据您呈现数字的方式,实际的方式会有所不同.那样你就得到了

 14
 48
 10
 29
 __
100

你走的任何方式都会产生差异.你的例子中没有办法显示加起来为100的数字而没有以错误的方式"舍入"一个值(最小错误将是9.596变为9)

编辑

您需要选择以下之一:

  1. 物品的准确性
  2. 总和的准确性(如果你总结了舍入值)
  3. 舍入项和舍入项之间的一致性)

处理百分比#3的大部分时间是最佳选择,因为当总数等于101%时,比单个项目总数不到100时更明显,并且您保持单个项目的准确性.在我看来,"舍入"9.596到9是不准确的.

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

  • 这并不是很有帮助,因为打印的值不会达到100.问题的目的是防止用户认为值不正确,在这种情况下,大多数人在查看和比较总数时会这样做. (6认同)
  • @VarunVohra在最初的例子中,LRM将产生14,48,9和29,它们将"9.59"舍入到9.如果我们_allocating_基于整数LRM将是最准确的,但它仍然改变一个结果更多超过半个单位. (2认同)

Bru*_*uce 7

我写了一个C#版本的舍入助手,算法与Varun Vohra的答案相同,希望它有所帮助.

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;
}
Run Code Online (Sandbox Code Playgroud)

它通过以下单元测试:

[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 });
}
Run Code Online (Sandbox Code Playgroud)


atk*_*sch 6

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

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
Run Code Online (Sandbox Code Playgroud)

不确定这在一般情况下是否有效,但如果顺序颠倒,它似乎工作相似:

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

我确信在某些边缘情况下这可能会崩溃,但任何方法至少都会有些随意,因为您基本上是在修改输入数据。

  • 数百年来,会计师和银行家一直在使用类似的技术。“将余数”从一行带到下一行。从“进位”中的 1 美分的 1/2 开始。将“进位”添加到第一个值,然后截断。现在,您因截断而损失的金额,将其放入“进位”。一直这样做,四舍五入的数字每次都会精确地加起来为所需的总数。 (2认同)