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
如果您不担心依赖原始十进制数据,有很多方法可以做到这一点.
第一种也许是最流行的方法是最大剩余方法
基本上是:
在你的情况下,它会像这样:
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减少很多.
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)
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.
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倍似乎不对.在下面的代码中,我使用了平方根.
完整的算法如下:
例如,您可能仍然有多个具有相同误差总和的组合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不需要四舍五入,但该列表中的一个值需要四舍五入; 最小的相对误差是由于将这个较大的值四舍五入而不是更小的替代值.
这个答案最初主张通过舍入/向下舍入的每个可能的组合,但正如评论中指出的,更简单的方法更好.算法和代码反映了这种简化.
不要对四舍五入的数字求和.你将得到不准确的结果.根据术语的数量和小数部分的分布,总数可能会显着偏离.
显示舍入的数字但总和实际值.根据您呈现数字的方式,实际的方式会有所不同.那样你就得到了
14 48 10 29 __ 100
你走的任何方式都会产生差异.你的例子中没有办法显示加起来为100的数字而没有以错误的方式"舍入"一个值(最小错误将是9.596变为9)
编辑
您需要选择以下之一:
处理百分比#3的大部分时间是最佳选择,因为当总数等于101%时,比单个项目总数不到100时更明显,并且您保持单个项目的准确性.在我看来,"舍入"9.596到9是不准确的.
为了解释这一点,我有时会添加一个脚注,说明单个值是四舍五入的,可能不是100% - 任何理解舍入的人都应该能够理解这个解释.
我写了一个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)
您可以尝试跟踪由于舍入而导致的错误,然后如果累积错误大于当前数字的小数部分,则根据粒度进行舍入。
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)
我确信在某些边缘情况下这可能会崩溃,但任何方法至少都会有些随意,因为您基本上是在修改输入数据。
| 归档时间: |
|
| 查看次数: |
82280 次 |
| 最近记录: |