IT

반올림 백분율을 100 %까지 만드는 방법

lottoking 2020. 5. 26. 08:06
반응형

반올림 백분율을 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

여전히 총 ​​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).


여기에 대한 답변 중 어느 것도 올바르게 해결하지 못하는 것 같으므로 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]

원래 십진 데이터에 의존하는 것에 대해 걱정하지 않는다면이 작업을 수행하는 많은 방법이 있습니다.

가장 많이 사용되는 첫 번째 방법은 가장 큰 나머지 방법입니다.

기본적으로

  1. 모든 것을 반올림
  2. 합계와 100의 차이 얻기
  3. 소수 부분의 내림차순으로 항목에 1을 더하여 차이 분배

귀하의 경우 다음과 같이 진행됩니다.

13.626332%
47.989636%
 9.596008%
28.788024%

정수 부분을 취하면

13
47
 9
28

최대 97 개까지 추가하고 3 개를 더 추가하려고합니다. 자 이제 소수점 이하 부분을 봅니다.

.626332%
.989636%
.596008%
.788024%

그리고 합계가 100에 도달 할 때까지 가장 큰 것을 취하십시오.

14
48
 9
29

또는 정수 값 대신 소수점 이하 하나를 표시하도록 선택할 수 있습니다. 따라서 숫자는 48.3과 23.9 등이 될 것입니다. 이것은 분산을 100에서 많이 떨어 뜨립니다.


아마도이 작업을 수행하는 "가장 좋은"방법은 현재 위치와 값의 반올림 (비 적분) 집계를 유지 한 다음 기록과 함께 사용하여 어떤 값을 사용해야 하는지를 계산하는 것입니다. 예를 들어, 제공 한 값을 사용하십시오.

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임을 알 수 있습니다.


반올림의 목표는 가장 적은 양의 오류를 생성하는 것입니다. 단일 값을 반올림하는 경우 해당 프로세스는 간단하고 간단하며 대부분의 사람들이 쉽게 이해할 수 있습니다. 동시에 여러 숫자를 반올림하면 프로세스가 까다로워집니다. 오류가 어떻게 결합되는지, 즉 최소화해야하는 것을 정의해야합니다.

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. 이것은 피할 수 없으며 결과는 완전히 임의적입니다. 아래 코드는 왼쪽의 값을 반올림하는 것을 선호합니다.

파이썬에서 모든 것을 합치는 것은 다음과 같습니다.

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에서는 반올림이 필요하지 않지만 목록의 값 중 하나를 반올림해야합니다. 가장 작은 상대 오차는 훨씬 작은 대안이 아니라 큰 값을 반올림함으로써 발생합니다.

이 답변은 원래 모든 가능한 반올림 / 반올림 조합을 통과하는 것을 옹호했지만 의견에서 지적했듯이 간단한 방법이 더 효과적입니다. 알고리즘과 코드는 단순화를 반영합니다.


반올림 숫자를 합산하지 마십시오. 부정확 한 결과가 나타납니다. 항의 수와 분수 부분의 분포에 따라 총계가 크게 떨어질 수 있습니다.

반올림 된 숫자를 표시 하지만 실제 값을 합산 하십시오. 숫자를 표현하는 방법에 따라 실제 방법은 다를 수 있습니다. 그렇게하면

 14
 48
 10
 29
 __
100

어떤 방식 으로든 불일치가 발생합니다. 하나의 값을 잘못된 방식으로 "반올림"하지 않고 100을 더하는 숫자를 표시하는 방법은 없습니다 (최소 오류는 9.596에서 9로 변경됩니다)

편집하다

다음 중 하나를 선택해야합니다.

  1. 품목의 정확도
  2. 합계의 정확도 (반올림 된 값을 합한 경우)
  3. 반올림 항목과 반올림 합계 간의 일관성)

백분율 # 3을 다룰 때 대부분의 경우 가장 좋은 방법은 개별 항목이 총 100 개가 아닌 경우보다 총계가 101 % 일 때 더 명확하고 개별 항목을 정확하게 유지하기 때문입니다. 내 의견으로는 "라운딩"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

나는 이것이 고장날 수있는 엣지 케이스가 있다고 확신하지만, 기본적으로 입력 데이터를 수정하기 때문에 모든 접근 방식은 다소 임의적입니다.


한때 목표에 맞도록 일련의 숫자에 대한 최소한의 동요를 찾기 위해 둥근 도구를 썼습니다. 그것은 다른 문제 였지만 이론 상으로는 비슷한 아이디어를 사용할 수 있습니다. 이 경우 선택 사항이 있습니다.

따라서 첫 번째 요소의 경우 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 퍼센트의 소수 부분을 취할 수 있습니다 (예를 들어 당신이 준 예는 4입니다).

소수 부분을 추가하십시오. 귀하의 예에서는 총 분수 부분 = 3입니다.

분수가 가장 높은 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

당신은 항상 숫자가 반올림되고 정확하지 않을 수 있음을 사용자에게 알릴 수 있습니다 ...


실제로 반올림 해야하는 경우 여기에 이미 매우 좋은 제안이 있습니다 (가장 큰 나머지, 가장 적은 상대 오류 등).

또한 반올림하지 말아야 할 좋은 이유가 하나 있습니다 ( "더보기에 좋지만"잘못된 "최소한 하나의 숫자가 표시됨).이를 해결하는 방법 (독자에게 경고)) 그리고 이것이 제가하는 일입니다.

"잘못된"숫자 부분을 추가하겠습니다.

세 가지 이벤트 / 엔티티 / ...가 있고 다음과 같은 비율이 있다고 가정합니다.

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 % 감소했습니다.

이는 정 성적 오류 일 수 있으며 초기 정량적 오류보다 상당히 나쁩니다.

하나는 전체 집합에 대한 근사치를 만들 수 있지만 첫 번째 날에 데이터를 게시해야 할 수 있으므로 두 번째 날에 대해 알 수 없습니다. 따라서 실제로 실제로 근사 해야하는 경우가 아니라면 아마 더 나을 것입니다.


이것은 은행원의 반올림, 즉 '라운드 반일'의 경우입니다. BigDecimal에서 지원합니다. 그 목적은 반올림의 균형을 유지하는 것입니다. 즉, 은행이나 고객에게 유리하지 않습니다.


이것이 유효한지 아닌지 테스트 케이스까지이 작업을 수행 할 수 있습니다.

숫자가 k라고 가정 해 봅시다.

  1. 내림차순으로 백분율을 정렬합니다.
  2. 내림차순에서 각 백분율을 반복합니다.
  3. 첫 번째 백분율에 대한 k의 백분율을 계산하면 Math.Ceil이 출력됩니다.
  4. 다음 k = k-1
  5. 모든 백분율이 소비 될 때까지 반복하십시오.

나는 여기에 목록과 dicts에 대한 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

@ 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.

참고 URL : https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100

반응형