Как сделать округленные проценты составляют до 100%

Рассмотрим четыре процента ниже, представленные как float numbers:

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

Ответ 1

Поскольку ни один из ответов здесь, похоже, не позволяет решить эту проблему, здесь моя полузапущенная версия с использованием 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]

Ответ 2

Есть много способов сделать это, если вы не обеспокоены использованием исходных десятичных данных.

Первым и, возможно, самым популярным методом будет метод наибольшего остатка

Что в основном:

  1. Округление всего вниз
  2. Получение разницы в сумме и 100
  3. Распределение разности путем добавления 1 к элементам в порядке убывания их десятичных частей

В вашем случае это будет выглядеть так:

13.626332%
47.989636%
 9.596008%
28.788024%

Если вы возьмете целые числа, вы получите

13
47
 9
28

который добавляет до 97, и вы хотите добавить еще три. Теперь вы смотрите на десятичные части, которые являются

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

и возьмите самые большие, пока общее число не достигнет 100. Таким образом, вы получите:

14
48
 9
29

В качестве альтернативы, вы можете просто выбрать показ одного десятичного знака вместо целых значений. Таким образом, числа будут 48,3 и 23,9 и т.д. Это значительно снизит дисперсию от 100.

Ответ 3

Вероятно, "лучший" способ сделать это состоит в том, чтобы сохранить текущую (неинтегральную) подсчеты, где вы находитесь, и округлить это значение, а затем использовать это вместе с историей для определения того, какое значение следует использовать. Например, используя значения, которые вы указали:

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.

Ответ 4

Цель округления - генерировать наименьшее количество ошибок. Когда вы округляете одно значение, этот процесс прост и прост, и большинство людей легко это понимают. Когда вы одновременно объединяете несколько номеров, процесс становится более сложным - вы должны определить, как будут собираться ошибки, т.е. Что должно быть сведено к минимуму.

хорошо проголосовавший ответ 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 выглядит следующим образом.

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 не нуждается в округлении, одно из значений в этом списке необходимо было округлить; самая низкая относительная ошибка является результатом округления этого большого значения, а не намного меньших альтернатив.

Этот ответ первоначально выступал за все возможные комбинации округления вверх/вниз, но, как указано в комментариях, более простой метод работает лучше. Алгоритм и код отражают это упрощение.

Ответ 5

НЕ суммируйте округленные числа. У вас будут неточные результаты. Сумма может значительно отличаться в зависимости от количества терминов и распределения дробных частей.

Отображение округленных чисел, но сумма фактических значений. В зависимости от того, как вы представляете числа, фактический способ сделать это будет отличаться. Таким образом вы получаете

 14
 48
 10
 29
 __
100

В любом случае, у вас будет несоответствие. В вашем примере нет способа показать числа, которые добавляют до 100 без "округления" одного значения неправильным способом (наименьшая ошибка будет меняться от 9.596 до 9)

ИЗМЕНИТЬ

Вам нужно выбрать один из следующих вариантов:

  • Точность элементов
  • Точность суммы (если вы суммируете округленные значения)
  • Согласованность между округленными элементами и округленной суммой)

Большую часть времени, когда имеет дело с процентами № 3, является лучшим вариантом, потому что это более очевидно, когда общая сумма равна 101%, чем когда отдельные элементы не достигают 100, и вы сохраняете отдельные элементы точными. "Округление" 9.596 - 9, по моему мнению, неточно.

Чтобы объяснить это, я иногда добавляю сноску, которая объясняет, что отдельные значения округлены и могут не составлять 100%. Любой, кто понимает округление, должен уметь понимать это объяснение.

Ответ 6

Я написал помощник округления версии С#, алгоритм такой же, как Ответ 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 test:

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

Ответ 7

Вы можете попытаться отслеживать свою ошибку из-за округления, а затем округлить против зерна, если накопленная ошибка больше, чем дробная часть текущего числа.

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

Я уверен, что есть крайние случаи, когда это может сломаться, но любой подход будет по меньшей мере несколько произвольным, поскольку вы в основном изменяете свои входные данные.

Ответ 8

Я как-то написал инструмент unround, чтобы найти минимальное возмущение для набора чисел для соответствия цели. Это была другая проблема, но теоретически можно использовать аналогичную идею. В этом случае у нас есть набор вариантов.

Таким образом, для первого элемента мы можем округлить его до 14 или до 13. Стоимость (в смысле двоичного целочисленного программирования) делает это меньше для раунда, чем округление, потому что раунд вниз, мы переместим это значение на большее расстояние. Точно так же мы можем округлять каждое число вверх или вниз, поэтому в общей сложности мы должны выбрать 16 вариантов.

  13.626332
  47.989636
   9.596008
+ 28.788024
-----------
 100.000000

Обычно я решаю общую проблему в MATLAB, здесь используя bintprog, двоичный целочисленный инструмент программирования, но есть только несколько вариантов, которые нужно проверить, поэтому достаточно просто с простыми циклами для тестирования каждого из 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 терминов это пространство будет иметь значительный размер. В этом случае вам нужно будет использовать инструмент, который может эффективно искать пространство, возможно, используя схему ветвей и границ.

Ответ 9

Я думаю, что следующее будет достигнуто, что вы после

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%  |
-------------------------------------------------

По сути, результат от моей функции фактически вводит наименьшее количество ошибок.

Fiddle здесь

Ответ 10

Если вы округлите его, нет никакого хорошего способа получить его точно так же во всех случаях.

Вы можете взять десятичную часть из N процентов, которые у вас есть (в примере, который вы дали, это 4).

Добавьте десятичные части. В вашем примере у вас есть дробная часть = 3.

Поверните 3 числа с наивысшими фракциями и оставьте остальные.

(Извините за изменения)

Ответ 11

Я не уверен, какой уровень точности вам нужен, но я бы просто добавил 1 первые числа n, n, являющиеся потолком общей суммы десятичных знаков. В этом случае это 3, поэтому я бы добавил 1 к первым 3 предметам и оставил все остальное. Конечно, это не очень точно, некоторые цифры могут быть округлены вверх или вниз, если это не так, но все работает нормально и всегда будет на 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

Вы всегда можете сообщить пользователям, что числа округлены и не могут быть суперточными...

Ответ 12

Если вы действительно должны их округлить, здесь есть очень хорошие предложения (самый большой остаток, наименьшая относительная ошибка и т.д.).

Существует также одна хорошая причина не округлять (вы получите хотя бы один номер, который "выглядит лучше", но "неправильный" ), и как его решить (предупредите своих читателей), и это то, что я делаю.

Позвольте мне добавить "неправильную" цифровую часть.

Предположим, что у вас есть три события/сущности/... с некоторыми процентами, которые вы оцениваете как:

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 ближе к 33, чем к 34.

Но теперь у вас большая ошибка. Сравнивая день 2 и день 1, реальное процентное значение для А увеличилось на 0,01%, но аппроксимация показывает уменьшение на 1%.

Это качественная ошибка, вероятно, хуже, чем исходная количественная ошибка.

Можно было бы придумать приближение для всего набора, но, возможно, вам придется публиковать данные в первый день, поэтому вы не узнаете о втором дне. Итак, если вы действительно, действительно, не должны приближаться, вы, вероятно, лучше не.

Ответ 13

Это случай для округления банкиров, иначе "круглый полу-четный". Он поддерживается BigDecimal. Его цель состоит в том, чтобы обеспечить сбалансирование округления, т.е. Не благоприятствует ни банку, ни клиенту.

Ответ 14

проверить, действительно ли это или нет, насколько я могу проверить это.

пусть число будет k;

  • процент сортировки по убыванию.
  • итерации по каждому проценту в порядке убывания.
  • вычисляет процентное отношение k для первого процента, принимает значение Math.Ceil.
  • next k = k-1
  • повторяется до тех пор, пока не будет потреблен весь процент.

Ответ 15

Я применил метод от Varun Vohra, который отвечает здесь как для списков, так и для dicts.

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

Ответ 16

Здесь более простая реализация Python для @varun-vohra:

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.