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

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


当前回答

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

讨论

给定[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,....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,不需要任何库来使用

2例

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

3例

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);
}

4例

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

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 });

其他回答

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

这是一个银行家四舍五入的例子,又名“四舍五入半偶数”。BigDecimal支持。它的目的是确保四舍五入平衡,即不偏袒银行或客户。

下面是@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。

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

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不需要四舍五入,但是列表中的一个值需要四舍五入;相对误差最小的结果是将较大的值舍入,而不是较小的可选值。

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

对于那些在熊猫系列中有百分比的人,这里是我的最大余数方法的实现(就像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