如何使四舍五入的百分比加起来达到100%
考虑下面的四个百分比,用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
什么是一个很好的algorithm来表示任何百分比的整数,同时仍然保持100%?
编辑 :阅读了一些评论和答案后,显然有很多方法可以解决这个问题。
在我看来,为了保持数字的真实性,“正确的”结果是最小化整体误差的结果,即由相对于实际值的误差舍入的多less来定义:
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,4)。
由于这里没有任何答案似乎解决了这个问题,下面是我使用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]
如果你不关心依赖原始的十进制数据,有很多方法可以做到这一点。
第一种也许是最受欢迎的方法是最大剩余法
基本上是:
- 四舍五入
- 总计和100的差异
- 通过将小数部分递减的顺序加1来分配差异
在你的情况下,它会是这样的:
13.626332% 47.989636% 9.596008% 28.788024%
如果你拿整数部分,你会得到
13 47 9 28
最多可以添加97个,并且您想添加三个。 现在,你看小数部分,这是
.626332% .989636% .596008% .788024%
并采取最大的,直到总数达到100.所以你会得到:
14 48 9 29
或者,您可以简单地select显示一个小数位而不是整数值。 所以这个数字会是48.3和23.9等等。这会使得方差从100减less很多。
取决于你需要的“准确”。 一个select是简单地保留四舍五入的数字的总和,而不是四舍五入最后一个,简单地设置为100 - that sum
。 然后它保证加起来100。
这忽略了这样一个事实:对9.59..
数字(四舍五入,但是小数部分最小的数字)可能更有意义,但是如果开始考虑这个数字,则最终会得到五十单独的规则:-)
看起来更“准确”的另一种方法是保持你所在的位置和整个值的运行(非整数)。 例如,使用您给出的值:
Value CumulValue CumulRounded Baseline 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。
不要总和四舍五入的数字。 你会有不准确的结果。 根据术语的数量和小数部分的分布,总数可能会显着减less。
显示舍入的数字,但求和实际值。 根据你如何呈现数字,实际的做法会有所不同。 这样你就可以得到
14 48 10 29 __ 100
任何你走的路你都会有差异。 在你的例子中,没有办法显示加起来为100的数字,而没有将一个值“舍入”为错误的方式(最小错误将会改变9.596到9)
编辑
您需要select以下之一:
- 项目的准确性
- 总和的精确度(如果你总结了四舍五入的值)
- 四舍五入之间的一致性和四舍五入的总和)
大多数时候处理百分比#3是最好的select,因为当总数等于101%时比个别项目总数不到100时更明显,并且保持单个项目准确。 9.596到9的“舍入”在我看来是不准确的。
为了解释这个问题,我有时会添加一个脚注,说明个体价值观是四舍五入的,可能不会达到100% – 任何理解舍入的人都应该能理解这个解释。
舍入的目标是产生最less量的错误。 当你把一个单一的价值四舍五入时,这个过程简单而直接,大多数人很容易理解。 当你同时舍入多个数字时,这个过程变得更加棘手 – 你必须定义错误将如何组合,也就是说必须最小化。
Varun Vohra的投票回答得到了良好的答复 ,使绝对误差的总和最小化,实施起来非常简单。 但是有边缘情况下它不处理 – 应该是24.25, 23.25, 27.25, 25.25
四舍五入的结果? 其中一个需要四舍五入,而不是倒下。 你可能会随便select列表中的第一个或最后一个。
也许最好使用相对误差而不是绝对误差。 将23.25上调至24,将上涨3.2%,而27.25上调至28上涨2.8%。 现在有一个明显的赢家。
这是可能的,甚至进一步调整。 一种常见的技术是对每个错误进行平方 ,以使大错误比小错多。 我也会用一个非线性除数来得到相对误差 – 在99%的情况下,1%的误差比99%的误差要重要99倍似乎不太合适。 在下面的代码中,我使用了平方根。
完整的algorithm如下:
- 总结四舍五入后的百分比,并从100中减去。这告诉你有多less百分比必须四舍五入。
- 为每个百分比生成两个错误分数,向下舍入时生成一个,向上舍入时生成一个。 把两者区别开来
- sorting上面产生的错误差异。
- 对于需要向上取整的百分比数量,从sorting列表中取一个项目,并将向下取整的百分比增加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]
正如你可以看到的最后一个例子,这个algorithm仍然能够提供非直观的结果。 尽pipe89.0不需要四舍五入,但是该列表中的一个值需要四舍五入。 最小的相对误差是由于大的值而不是小得多的值。
这个答案最初主张通过所有可能的圆整/倒圆的组合,但正如在评论中指出的那样,更简单的方法效果更好。 algorithm和代码反映了这种简化。
我写了一个C#版本的四舍五入帮助器,algorithm和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; }
它通过以下unit testing:
[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 }); }
您可以尝试跟踪由于四舍五入而导致的错误,然后在积累的误差大于当前数字的小数部分的情况下对四舍五入进行四舍五入。
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
我敢肯定,在这种情况下可能会出现这种情况,但是任何方法都至less有点武断,因为你基本上是在修改你的input数据。
我曾经写过一个非圆形的工具,find一组数字来匹配一个目标的最小扰动。 这是一个不同的问题,但理论上可以在这里使用类似的想法。 在这种情况下,我们有一套select。
因此,对于第一个元素,我们可以将其舍入到14,或者减小到13,这样做的成本(在二进制整数编程意义上)是小于四舍五入,因为舍入要求我们将这个值移动一个更大的距离。 同样,我们可以将每个数字向上或向下舍入,所以总共有16个select,我们必须从中select。
13.626332 47.989636 9.596008 + 28.788024 ----------- 100.000000
我通常在MATLAB中解决一般问题,在这里使用bintprog,一个二进制整数编程工具,但只有几个select要testing,所以用简单的循环来testing16个备选scheme中的每一个是很容易的。 例如,假设我们将这个集合舍入为:
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。 可以通过下面的替代舍入稍微减less:
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
事实上,这将是绝对误差的最佳解决scheme。 当然,如果有20个术语,search空间的大小将是2 ^ 20 = 1048576。对于30或40个术语,这个空间将是相当大的。 在这种情况下,您需要使用一种可以高效search空间的工具,也许使用分支和绑定scheme。
如果你四舍五入,在所有情况下都没有什么好办法使它完全一样。
你可以把你有的百分比的小数部分(在你给它的例子是4)。
添加小数部分。 在你的例子中,总共有小数部分= 3。
将最高分数的3个数字和剩下的其他地方分开。
(对不起,编辑)
我认为以下将达到你以后的
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. eg 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% | -------------------------------------------------
本质上,我的函数的结果实际上引入了最less的错误。
在这里拨弄
我不确定你需要什么级别的准确性,但是我要做的只是将前n
数字加1, n
是小数总和的小数。 在这种情况下,是3
,所以我会加1前3项,其余的地板。 当然这并不是非常准确,有些数字可能会在不应该的情况下上下移动,但是它可以正常工作,并且总是可以达到100%。
所以[ 13.626332, 47.989636, 9.596008, 28.788024 ]
将是[14, 48, 10, 28]
[ 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
您可以随时通知用户,这些数字是四舍五入的,可能不是超精确的…
如果你真的必须围绕它们,这里已经有很好的build议(最大的余数,最小的相对误差等等)。
还有一个很好的理由不要轮回(至less你会得到一个“看起来更好”但是“错误”的数字),以及如何解决这个问题(警告你的读者),这就是我所做的。
让我加上“错误”的数字部分。
假设你有三个事件/实体/ …有一些百分比,你近似为:
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%,但近似值显示减less了1%。
这是一个质的错误,可能是最初的定量误差更糟。
人们可以为整个集合devise一个近似值,但是,您可能需要在第一天发布数据,因此您不会了解第二天。 所以,除非你真的,真的,必须近似,你最好不要。
这是银行家四舍五入的情况,也叫做“一半一半”。 它由BigDecimal支持。 其目的是确保四舍五入的余额,即不利于银行或客户。
检查这是否有效,只要我的testing案例我能够得到这个工作。
比方说数字是k;
- 按降序排列百分比。
- 从下降的顺序迭代每个百分比。
- 计算输出的Math.Ceil的第一个百分比。
- 接下来k = k-1
- 迭代直到所有百分比被消耗。
我已经在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, # ie 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