Як зробити закруглені відсотки до 100%


192

Розглянемо чотири відсотки нижче, представлені у вигляді 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).


21
Припустимо, у вас є 3,33, 3,33 та 3,33. Який з них ви зробите 4?
RobG

3
Саме так. Питання втілює суперечність у термінах.
Маркіз Лорн

4
Це дуже поширений сценарій звітування - як відобразити «загальну» десяткових значень, яка не завжди відповідає сумі відображених значень.
D Стенлі

1
Який "правильний" результат у вашому прикладі? Це може вирішити розбіжності щодо того, що найкраще рішення.
D Стенлі

Відповіді:


35

Оскільки жодна з відповідей тут, здається, не вирішує її належним чином, ось моя напівзабруднена версія з використанням підкреслення :

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]

6
Виправте мене, якщо я помиляюся, але чи це не реалізація алгоритму, запропонованого моєю відповіддю? (Не ясно для підкреслення)
vvohra87

@VarunVohra вибачте, що я цього не помічав до цього моменту, так, схоже, ваш алгоритм той самий :) не впевнений, чому мій пост - це прийнята відповідь, затуманений код був саме для lolz ...
yonilevy

@yonilevy Видалив мій коментар; Я просто не усвідомлював, що повинен повернути відсортований список. Я прошу вибачення!
Зак Берт

2
Існує aproblem з цією функцією, коли останній елемент дорівнює 0, а попередній додає до 100. Напр. [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. Останній логічно повертає -1. Я дуже швидко подумав про таке рішення, але, мабуть, є щось краще: jsfiddle.net/0o75bw43/1
Cruclax

1
@Cruclax він показує всі 1, коли всі вхідні масиви
нульові

159

Існує багато способів зробити саме це, якщо ви не переймаєтесь опорою на вихідні десяткові дані.

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

Що в основному:

  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 на багато.


5
Ця "стовпчик функцій" на веб-сайті Американського математичного товариства - Apportionment II: Apportionment Systems - описує кілька подібних методів "розподілу".
Кенні Евітт

1
Це майже схоже на копію та вставлення моєї відповіді тут stackoverflow.com/questions/5227215/… .
sawa

Зауважте, що, всупереч вашому коментарю до відповіді @DStanley, у вашій відповіді 9,596008% було округлено до 9%, що становить більше ніж 0,5% різниці. Але все ж гарна відповідь.
Rolazaro Azeveires

33

Напевно, "найкращий" спосіб це зробити (цитується, оскільки "найкраще" є суб'єктивним терміном) - тримати (неінтегральне) підрахунок того, де ви знаходитесь, і округлити це значення.

Потім використовуйте це разом з історією, щоб визначити, яке значення слід використовувати. Наприклад, використовуючи вказані вами значення:

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.

Ви можете побачити різницю між цим і сліпо округлити кожне значення у третьому значенні вище. Хоча 9.596008зазвичай округляємо до 10, накопичений 71.211976правильно округляє до 71- це означає, що 9потрібно лише додати до попереднього базового рівня 62.


Це також працює для "проблемної" послідовності, як три приблизно значення, де одне з них має бути округлене:1/3

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
33.333333   33.333333            33             0    33 ( 33 -  0)
33.333333   66.666666            67            33    34 ( 67 - 33)
33.333333   99.999999           100            67    33 (100 - 67)
                                                    ---
                                                    100

1
Другий підхід вирішує обидві ці проблеми. Перший дає 26, 25, 26, 23, другий 1, 0, 1, 0, 1, 0, ....
paxdiablo

Цей підхід також добре підходить для округлення невеликих чисел, оскільки запобігає негативному числу гріх виводу
Jonty5817

19

Мета округлення - генерувати найменшу кількість помилок. Коли ви округляєте одне значення, цей процес простий і простий, і більшість людей його легко розуміють. Коли ви округляєте декілька чисел одночасно, процес стає складнішим - ви повинні визначити, як помилки збиратимуться, тобто що потрібно мінімізувати.

Добре проголосували відповідь на Varun Вохра мінімізує суму абсолютних помилок, і це дуже просто реалізувати. Однак є крайові випадки, з якими він не вирішується - що має бути результатом округлення 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. Це неминуче, і результат буде абсолютно довільним. Код, поданий нижче, вважає за краще округляти значення зліва.

Зведення все це в 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
Я не думаю, що вам потрібно враховувати всі комбінації: обробляти в порядку зменшення падіння зваженої помилки, що переходить від раунду до нуля до круглого до нескінченності (майже просто введення зважування у відповіді Веруна Вохраса та Йонілеви ("однакові")).
сіра борода

@greybeard ти маєш рацію, я це передумував. Я не міг просто розібратися за помилкою, оскільки для кожного значення є дві помилки, але прийняття різниці вирішило цю проблему. Я оновив відповідь.
Марк Рансом

Я вважаю за краще завжди мати 0%, коли фактична кількість становить 0%. Тож додавання if actual == 0: return 0до error_genроботи чудово.
Микола Балюк

1
що таке iscloseметод на початку round_to_100?
toto_tico


7

НЕ сумуйте округлих чисел. Ви отримаєте неточні результати. Загальна сума може бути суттєво відхилена залежно від кількості членів та розподілу дробових частин.

Відображайте округлі числа, але підсумовуйте фактичні значення. Залежно від того, як ви представляєте числа, фактичний спосіб зробити це буде різним. Таким чином ви отримаєте

 14
 48
 10
 29
 __
100

У будь-якому випадку ви підете, у вас виникнуть розбіжності. У вашому прикладі немає способу показувати числа, що складають до 100 без "округлення" одного значення неправильним способом (найменша помилка - це зміна 9,596 на 9)

EDIT

Вам потрібно вибрати одне з наступного:

  1. Точність предметів
  2. Точність суми (якщо ви підсумовуєте округлі значення)
  3. Узгодженість між округленими елементами та округлою сумою)

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

Для пояснення цього я іноді додаю виноску, яка пояснює, що окремі значення округлені і не можуть становити 100% - кожен, хто розуміє округлення, повинен мати можливість зрозуміти це пояснення.


6
Це не дуже корисно, оскільки надруковані значення не дорівнюватимуть 100. Метою питання було не допустити, щоб користувачі думали, що значення невірні, що в цьому випадку більшість людей зробить при перегляді та порівнянні з загальними .
vvohra87

@VarunVohra прочитайте мою редагування, ви НЕ МОЖЕТЕ відображати свої номери таким чином, щоб вони додавали до 100, не "округлюючи" один на більше 0,5.
D Стенлі

1
@DStanley насправді, заборонивши набір, де всі числа сором’язливі 0,5, ви можете. Перевірте мою відповідь - LRM робить саме це.
vvohra87

3
@VarunVohra У вихідному прикладі LRM дасть 14, 48, 9 і 29 , які будуть «круглі» 9,596 до 9. Якщо ми розподілу на основі цілих чисел LRM буде найбільш точним, але він по - , як і раніше змінюється один результат більш ніж пів-одиниці.
D Стенлі

7

Я написав помічник із округлення версії C #, алгоритм такий самий, як відповідь Варуна Вогра , сподіваюся, що це допоможе.

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

Приємно! дав мені ґрунтову базу для початку .. У багатьох немає ForEach, хоча я вірю
Jack0fshad0ws

4

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

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

Я впевнений, що є випадки, коли це може вийти з ладу, але будь-який підхід буде хоча б дещо довільним, оскільки ви в основному змінюєте свої вхідні дані.


2
Бухгалтери та банкіри вже сотні років використовують подібну техніку. "Перенесіть залишок" з одного ряду в інший. Почніть з 1/2 від одного цента в "несуть". До першої величини додайте "нести" та обрізайте. Тепер суму, яку ви втратили, обрізаючи, покладіть цю суму в "перенос". Зробіть це до кінця вниз, і округлені числа додаватимуть до потрібної загальної кількості рівно кожного разу.
Джефф Грігг

Керолін Кей запропонувала цю реалізацію в Access VB 2007: <code> 'Круглі долари повернення коштів методом "перенести залишок" ref1 = rsQry! [Відшкодування виплачено $$$] * rsQry! [Значення властивості] / propValTot ref2 = ref1 + ref5 'Додайте перенесений залишок, нуль для початку ref3 = ref2 * 100' Помножте на 100 на ціле число ref4 = ref3 / 100 'Розділіть на 100 на десяткове число rsTbl! [Відшкодування виплачено $$$] = ref4' Помістіть " залишок "округлене число в таблиці ref5 = ref2 - ref4" Перенесіть новий залишок </code>
Джефф Грігг

2

Я колись написав необмежений інструмент, щоб знайти мінімальне збурення до набору чисел, щоб відповідати цілі. Це була інша проблема, але теоретично тут можна було використати подібну ідею. У цьому випадку ми маємо набір варіантів.

Таким чином, для першого елемента ми можемо або округлити його до 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 термінів цей простір матиме значні розміри. У цьому випадку вам знадобиться використовувати інструмент, який дозволяє ефективно шукати простір, можливо, використовуючи гілку та пов'язану схему.


Тільки для подальшої довідки: алгоритм "найбільшого залишку" повинен мінімізувати повну абсолютну помилку відповідно до вашої метрики (див. Відповідь @ varunvohra). Доказ простий: припустимо, це не мінімізує помилку. Тоді повинен бути якийсь набір значень, який він округляє, який слід округлювати вгору, і навпаки (два набори мають однаковий розмір). Але кожне значення, яке воно округляє вниз, знаходиться далі від наступного цілого числа, ніж будь-яке значення, яке воно округляє (і vv), тому нове значення помилки повинно бути більше QED. Однак він працює не для всіх показників помилок; потрібні інші алгоритми.
rici

2

Я думаю, що наступне допоможе досягти того, за чим ти прагнеш

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

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

Скрипаль тут


це майже все, що я мав на увазі, з тією різницею, що похибка повинна вимірюватися відносно значення (округлення 9,8 до 10 - більша помилка, ніж округлення від 19,8 до 20). Це можна легко зробити, відобразивши це в зворотному режимі виклику.
poezn

це неправильно для [33.33, 33.33, 33.33, 0.1], воно повертається [1, 33, 33, 33], а не точніше [34, 33, 33, 0]
йонілевий

@yonilevy Дякую за це. Виправлено зараз.
Бруно

ще немає, для [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] вона повертається [15, 17, 17, 17, 17, 17], а не [16, 16, 17, 17, 17, 17] - див. відповідь
yonilevy

2

Я не впевнений, який рівень точності вам потрібен, але що я би зробив, це просто додати 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

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


1

Якщо ви округляєте його, немає жодного хорошого способу отримати його точно у всіх випадках.

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

Додайте десяткові частини. У вашому прикладі ви маєте загальну частку дробів = 3.

Перекрийте 3 номери з найвищими частками і залиште решту.

(Вибачте за зміни)


1
Хоча це може забезпечити числа, що додають до 100, ви можете перетворити 3,9 на 3 і 25,1 на 26.
RobG

немає. 3.9 буде 4, а 25.1 - 25. Я сказав, що слід перекрити 3 числа з найвищими дробами, не найвищим значенням.
arunlalam

2
якщо є занадто багато дробів, що закінчуються на .9, кажуть, що 9 значень 9,9% і одне значення 10,9 є одне значення, яке в кінцевому підсумку складе як 9%, 8 як 10% і одна як 11%.
arunlalam

1

Якщо ви дійсно повинні їх округлити, тут вже є дуже хороші пропозиції (найбільша залишок, найменша відносна помилка тощо).

Також є вже одна вагома причина не крутити (ви отримаєте принаймні одне число, яке "виглядає краще", але є "неправильним"), і як це вирішити (попередити своїх читачів), і це я роблю.

Дозвольте додати «неправильну» числову частину.

Припустимо, у вас є три події / сутності / ... з деякими відсотками, які ви приблизно оцінюєте як:

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-й день, реальне відсоткове значення A збільшилось на 0,01%, але наближення показує зменшення на 1%.

Це якісна помилка, ймовірно, набагато гірша від початкової кількісної помилки.

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


будь-хто знає, як зробити кращі таблиці, будь ласка, відредагуйте або розкажіть, як / де
Rolazaro Azeveires

0

перевірте, чи це дійсно чи ні, наскільки я можу зробити тестові справи.

скажімо, число - k;

  1. відсоток сортування за спаданням коефіцієнта
  2. повторюйте кожен відсоток від порядку зменшення.
  3. обчислити відсоток k для першого відсотка взяти Math.Ceil виходу.
  4. наступний k = k-1
  5. повторіть, поки не буде витрачений весь відсоток.

0

Я реалізував тут метод з відповіді Варуна Вогра і для списків, і для диктів.

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

0

Ось простіша реалізація 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.


0

Для тих, хто має відсотки у серії панд, ось моя реалізація методу найбільшого залишку (як у відповіді Варуна Вогра ), де ви навіть можете вибрати десяткові знаки, до яких ви хочете округлити.

import numpy as np

def largestRemainderMethod(pd_series, decimals=1):

    floor_series = ((10**decimals * pd_series).astype(np.int)).apply(np.floor)
    diff = 100 * (10**decimals) - floor_series.sum().astype(np.int)
    series_decimals = pd_series - floor_series / (10**decimals)
    series_sorted_by_decimals = series_decimals.sort_values(ascending=False)

    for i in range(0, len(series_sorted_by_decimals)):
        if i < diff:
            series_sorted_by_decimals.iloc[[i]] = 1
        else:
            series_sorted_by_decimals.iloc[[i]] = 0

    out_series = ((floor_series + series_sorted_by_decimals) / (10**decimals)).sort_values(ascending=False)

    return out_series

-1

Це стосується округлення банкіра, так само "круглої половини". Він підтримується BigDecimal. Його мета - забезпечити вирівнювання врівноважених коштів, тобто не надавати переваги ні банку, ні клієнту.


5
Він НЕ гарантує, що округлення врівноважується - воно просто зменшує кількість помилок, розподіляючи напівкруглення між парними та непарними числами. Досі існують сценарії, коли округлення банкірів дає неточні результати.
D Стенлі

@DStanley Погодився. Я не сказав іншого. Я заявив про своє призначення . Дуже обережно.
Маркіз Лорн

2
Досить справедливо - я неправильно трактував те, що ви намагалися сказати. В будь-якому випадку я не думаю, що це вирішує проблему, оскільки використання округлення банкірів не змінить результатів у прикладі.
D Стенлі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.