float
の数値として表される、以下の4つの割合を考慮してください。
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など)。
ここの答えはどれも適切に解決しないようですので、ここに nderscorejs を使用した半難読化バージョンがあります:
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]
元の10進数データへの依存を心配しない限り、これを行う方法は多数あります。
最初の、おそらく最も一般的な方法は、 最大残余法
基本的には次のとおりです。
あなたの場合、次のようになります。
13.626332%
47.989636%
9.596008%
28.788024%
整数部分を取ると、
13
47
9
28
合計で97になり、さらに3つ追加したいとします。次に、小数部分を見てみましょう。
.626332%
.989636%
.596008%
.788024%
そして、合計が100に達するまで最大のものを取得します。
14
48
9
29
または、整数値の代わりに小数点以下1桁を表示するように選択することもできます。したがって、数値は48.3や23.9などになります。これにより、分散が100から大きく低下します。
おそらく、これを行うための「最良の」方法は、現在の(非整数の)集計を保持し、that値を丸め、それを履歴とともに使用して、どの値を計算するかです。利用される。たとえば、指定した値を使用します。
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
各段階で、数値自体を丸めません。代わりに、累積値を丸め、前のベースラインからその値に達する最適な整数を算出します。そのベースラインは前の行の累積値(丸められた)です。
not各段階で情報を失うのではなく、情報をよりインテリジェントに使用するため、これは機能します。 「正しい」丸められた値は最終列にあり、合計が100になっていることがわかります。
丸めの目的は、エラーを最小限に抑えることです。単一の値を丸める場合、そのプロセスは単純で簡単であり、ほとんどの人はそれを簡単に理解します。複数の数値を同時に丸めると、プロセスが複雑になります。エラーの結合方法、つまり最小化する必要があるものを定義する必要があります。
Varun Vohraによるよく投票された回答 は、絶対誤差の合計を最小化し、実装が非常に簡単です。ただし、Edgeで処理できない場合があります。24.25, 23.25, 27.25, 25.25
を丸めた結果はどうなりますか?それらの1つは、切り捨てるのではなく切り上げる必要があります。おそらく、リストの最初または最後のものを勝手に選ぶだけでしょう。
おそらく、absoluteエラーの代わりにrelativeエラーを使用する方が良いでしょう。 23.25を24に丸めると3.2%変化し、27.25を28に丸めると2.8%しか変化しません。今、明確な勝者がいます。
これをさらに調整することは可能です。一般的な手法の1つは、各エラーをsquareすることです。そのため、大きなエラーは小さなエラーよりも不均衡に多くカウントされます。また、相対誤差を得るために非線形除数を使用します-1%の誤差が99%の誤差より99倍重要であるとは思えません。以下のコードでは、平方根を使用しました。
完全なアルゴリズムは次のとおりです。
33.3333333, 33.3333333, 33.3333333
のように、同じエラー合計で複数の組み合わせがある場合があります。これは不可避であり、結果は完全にcompletely意的です。以下に示すコードは、左側の値を切り上げることを好みます。
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]
最後の例でわかるように、このアルゴリズムはまだ直感的でない結果を提供できます。 89.0では丸めはまったく必要ありませんが、そのリストの値の1つを切り上げる必要がありました。最も小さい相対誤差は、はるかに小さい代替値ではなく、その大きな値を切り上げた結果です。
この回答はもともと、切り上げ/切り捨てのあらゆる可能な組み合わせを通過することを推奨していましたが、コメントで指摘されているように、より単純な方法がより効果的です。アルゴリズムとコードはその単純化を反映しています。
丸められた数値を合計しないでください。結果が不正確になります。項の数と小数部の分布によっては、合計が大幅にオフになる可能性があります。
表示端数は切り捨てられますが、合計は実際の値です。数字の表示方法によって、実際の方法は異なります。そのようにして
14 48 10 29 __ 100
いずれにしても、矛盾が生じます。あなたの例では、1つの値を間違った方法で「四捨五入」せずに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;
}
次の単体テストに合格します。
[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
これが破綻する可能性のあるEdgeのケースは確かにありますが、基本的に入力データを変更しているため、どのアプローチも少なくともsomewhat意的になります。
私はかつて、丸みのあるツールを書いて、目標に一致する一連の数値に対する最小の摂動を見つけました。これは別の問題でしたが、理論的にはここで同様のアイデアを使用できます。この場合、選択肢があります。
したがって、最初の要素については、14に切り上げるか、13に切り捨てることができます。(バイナリ整数プログラミングの意味での)切り上げには、切り捨てよりも切り上げの方がコストがかかります。その値をより大きな距離に移動します。同様に、各数値を切り上げまたは切り捨てることができるため、合計16の選択肢から選択する必要があります。
13.626332
47.989636
9.596008
+ 28.788024
-----------
100.000000
通常、ここではバイナリ整数プログラミングツールであるbintprogを使用して、MATLABの一般的な問題を解決しますが、テストする選択肢はわずかしかないため、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個の用語の場合、そのスペースはかなりのサイズになります。その場合は、おそらく分岐限定スキームを使用して、スペースを効率的に検索できるツールを使用する必要があります。
私は次のことがあなたが望んでいることを達成すると思う
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% |
-------------------------------------------------
基本的に、私の関数からの結果は実際に最小のエラーをもたらします。
フィドル ここ
どのレベルの精度が必要かはわかりませんが、最初にn
の数字を1だけ追加するだけです。n
は小数の合計の上限です。この場合は3
なので、最初の3つのアイテムに1を追加し、残りをフロアします。もちろん、これは非常に正確ではありません。一部の数値は切り上げまたは切り捨てられる場合がありますが、正常に機能し、常に100%になります。
Math.ceil(.626332+.989636+.596008+.788024) == 3
であるため、[ 13.626332, 47.989636, 9.596008, 28.788024 ]
は[14, 48, 10, 28]
になります。
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
あなたはいつでも数字が四捨五入されており、超正確でないかもしれないことをユーザーに知らせることができます...
本当にそれらを丸める必要がある場合は、ここに既に非常に良い提案があります(最大の残余、最小の相対誤差など)。
また、丸めない1つの正当な理由(「見た目が良い」が「間違っている」を少なくとも1つ取得する)と、それを解決する方法(読者に警告する)があります。
「間違った」番号の部分を追加します。
3つのイベント/エンティティ/ ...があり、いくつかの割合を次のように概算するとします。
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%増加しましたが、近似では1%減少しています。
これは定性的なエラーであり、おそらく最初の定量的なエラーよりもかなりひどいものです。
セット全体の概算を考案することもできますが、1日目にデータを公開する必要がある場合があるため、2日目はわかりません。ですから、本当に、本当に、近似しなければならないのでなければ、おそらくそうではないでしょう。
あなたがそれを丸めている場合、それをすべての場合にまったく同じにする良い方法はありません。
Nパーセントの小数部分を取得できます(指定した例では4です)。
小数部分を追加します。あなたの例では、小数部の合計= 3です。
最も高い割合の3つの数字を天井に置き、残りを床にします。
(編集の申し訳ありません)
@ 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の回答からメソッドを実装しました。
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
これが有効であるかどうか、テストケースでこれが機能するかどうかを確認してください。
数をkとしましょう。
これは、銀行家の丸め、つまり「ラウンドハーフイーブン」の場合です。 BigDecimalでサポートされています。その目的は、四捨五入のバランスをとることです。つまり、銀行にも顧客にも有利になりません。