239

考虑下面的四个百分比,用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% 的好算法是什么?


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

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

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

4

22 回答 22

183

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

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

基本上是:

  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 下降很多。

于 2012-11-20T23:02:02.223 回答
52

可能执行此操作的“最佳”方法(引用,因为“最佳”是一个主观术语)是保持对您所在位置的运行(非整数)计数,然后将该值四舍五入。

然后将其与历史一起使用来确定应该使用什么值。例如,使用您提供的值:

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
于 2012-11-20T22:43:54.520 回答
37

由于这里的答案似乎都没有正确解决它,这是我使用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]
于 2012-11-21T03:27:55.510 回答
24

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

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 中看起来像这样。

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 不需要四舍五入,但该列表中的一个值需要四舍五入;最小的相对误差来自于对那个大值而不是更小的替代值进行四舍五入。

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

于 2016-01-23T05:30:01.850 回答
10

我写了一个 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;
}

它通过了以下单元测试:

[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 });
}
于 2016-01-18T00:55:47.967 回答
9

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

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

14
 48
 10
 29
 __
100

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

编辑

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

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

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

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

于 2012-11-20T22:53:26.047 回答
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

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

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

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

于 2012-11-20T22:50:22.153 回答
3

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

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

于 2012-11-20T23:38:46.453 回答
2

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

因此,对于第一个元素,我们可以将其向上舍入到 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 个词,该空间将非常大。在这种情况下,您将需要使用可以有效搜索空间的工具,可能使用分支定界方案。

于 2012-11-21T00:01:46.540 回答
2

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

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

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

在这里提琴

于 2012-11-21T00:11:47.453 回答
2

我对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]}
于 2021-10-01T18:42:58.843 回答
2

如果您只有两个选项,您可以使用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 次。

于 2021-09-26T14:26:11.050 回答
2

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

讨论

鉴于[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 });
于 2021-02-16T09:42:10.493 回答
1

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

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

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

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

(抱歉编辑)

于 2012-11-20T22:44:07.637 回答
1

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

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

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

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

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 更接近 33 而不是 34。

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

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

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

于 2016-12-28T13:13:58.410 回答
1

或者为了简洁起见,您只是在其中累积错误...

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]

于 2021-11-04T02:41:09.923 回答
0

这是一个实现最大余数方法的 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
于 2020-12-31T03:24:09.337 回答
0

这是@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

你需要math, itertools, operator.

于 2018-04-12T05:56:40.947 回答
0

就我的测试用例而言,检查这是否有效,我能够使其正常工作。

假设数字是k;

  1. 按降序排列百分比。
  2. 从降序遍历每个百分比。
  3. 计算第一个百分比的 k 百分比取输出的 Math.Ceil。
  4. 下一个 k = k-1
  5. 迭代直到消耗完所有百分比。
于 2017-07-31T06:50:50.903 回答
0

我已经为列表和字典实现了 Varun Vohra's answer here 中的方法。

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
于 2017-09-13T22:34:47.657 回答
0

对于那些在熊猫系列中有百分比的人,这是我对最大余数方法的实施(如Varun Vohra 的回答),您甚至可以选择要四舍五入的小数。

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
于 2020-01-14T16:16:56.070 回答
-1

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

于 2012-11-20T23:03:20.407 回答