Jak zrobić, aby zaokrąglone wartości procentowe sumowały się do 100%


192

Rozważ cztery poniższe wartości procentowe przedstawione w postaci floatliczb:

    13.626332%
    47.989636%
     9.596008%
    28.788024%
   -----------
   100.000000%

Muszę przedstawić te wartości procentowe jako liczby całkowite. Jeśli po prostu użyję Math.round(), w sumie otrzymam 101%.

14 + 48 + 10 + 29 = 101

Jeśli użyję parseInt(), otrzymam w sumie 97%.

13 + 47 + 9 + 28 = 97

Jaki jest dobry algorytm do reprezentowania dowolnej liczby procentowej jako liczb całkowitych przy jednoczesnym zachowaniu w sumie 100%?


Edycja : po przeczytaniu niektórych komentarzy i odpowiedzi istnieje wyraźnie wiele sposobów rozwiązania tego problemu.

Moim zdaniem, aby pozostać wiernym liczbom, „właściwy” wynik to taki, który minimalizuje ogólny błąd, określony przez to, ile zaokrągleń błędu wprowadziłoby w stosunku do rzeczywistej wartości:

        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)

W przypadku remisu (3.33, 3.33, 3.33) można podjąć dowolną decyzję (np. 3, 4, 3).


21
Załóżmy, że masz 3,33, 3,33 i 3,33. Który zrobisz 4?
RobG

3
Dokładnie. Pytanie zawiera sprzeczność w kategoriach.
Markiz Lorne

4
Jest to bardzo częsty scenariusz w raportowaniu - jak wyświetlić „sumę” wartości dziesiętnych, która nie zawsze odpowiada sumie wyświetlanych wartości.
D Stanley,

1
Jaki jest „właściwy” wynik w twoim przykładzie? To może rozwiązać spory na temat „najlepszego” rozwiązania.
D Stanley,

Odpowiedzi:


35

Ponieważ żadna z odpowiedzi tutaj nie wydaje się poprawnie rozwiązać, oto moja częściowo zaciemniona wersja przy użyciu podkreślenia :

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
Popraw mnie, jeśli się mylę, ale czy nie jest to implementacja algorytmu zaproponowanego w mojej odpowiedzi? (Nie do wyczyszczenia na underscorejs)
vvohra87,

@VarunVohra przepraszam, nie zauważyłem tego do tej pory, tak, wygląda na to, że twój algorytm jest taki sam :) nie jestem pewien, dlaczego mój post jest akceptowaną odpowiedzią, zaciemniony kod był tylko dla lolz ...
yonilevy

@yonilevy Usunąłem mój komentarz; Po prostu nie zdawałem sobie sprawy, że to ma zwrócić posortowaną listę. Przepraszam!
Zack Burt

2
Istnieje problem z tą funkcją, gdy ostatnim elementem jest 0, a poprzednie dodają do 100. Np. [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. Ostatni logicznie zwraca -1. Pomyślałem o następującym rozwiązaniu bardzo szybko, ale prawdopodobnie jest coś lepszego: jsfiddle.net/0o75bw43/1
Cruclax

1
@Cruclax pokazuje wszystkie 1, gdy wszystkie wpisy są zerowe w tablicy wejściowej
tony.0919,

159

Jest na to wiele sposobów, pod warunkiem, że nie martwisz się o poleganie na oryginalnych danych dziesiętnych.

Pierwszą i być może najbardziej popularną metodą byłaby metoda największej pozostałej

Co jest w zasadzie:

  1. Zaokrąglając wszystko w dół
  2. Uzyskanie różnicy w sumie i 100
  3. Rozłóż różnicę, dodając 1 do pozycji w malejącej kolejności ich części dziesiętnych

W twoim przypadku wyglądałoby to tak:

13.626332%
47.989636%
 9.596008%
28.788024%

Jeśli weźmiesz części całkowite, otrzymasz

13
47
 9
28

co daje w sumie 97, a chcesz dodać jeszcze trzy. Teraz patrzysz na części dziesiętne, które są

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

i bierz największe, aż suma osiągnie 100. Otrzymasz:

14
48
 9
29

Alternatywnie możesz po prostu wybrać wyświetlanie jednego miejsca po przecinku zamiast wartości całkowitych. Tak więc liczby wynosiłyby 48,3 i 23,9 itd. To znacznie zmniejszyłoby wariancję ze 100.


5
Ta „kolumna funkcji” na stronie internetowej American Mathematical Society - Apportionment II: Apportionment Systems - opisuje kilka podobnych metod „podziału”.
Kenny Evitt,

1
To prawie wygląda jak kopia i wklej moją odpowiedź tutaj stackoverflow.com/questions/5227215/… .
sawa

Zauważ, że w przeciwieństwie do twojego komentarza do odpowiedzi @DStanley, w twojej odpowiedzi 9,596008% zostało zaokrąglone do 9%, co stanowi więcej niż 0,5% różnicy. Jednak wciąż dobra odpowiedź.
Rolazaro Azeveires

33

Prawdopodobnie „najlepszym” sposobem na to (cytowanym, ponieważ „najlepszy” jest terminem subiektywnym) jest prowadzenie bieżącej (niezintegrowanej) oceny tego, gdzie jesteś, i robienie tego wartości.

Następnie użyj tego wraz z historią, aby dowiedzieć się, jaką wartość należy zastosować. Na przykład używając podanych wartości:

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

Na każdym etapie nie zaokrąglasz samej liczby. Zamiast tego zaokrąglasz nagromadzone wartość i obliczasz najlepszą liczbę całkowitą, która osiąga tę wartość z poprzedniej linii bazowej - ta linia bazowa jest skumulowaną wartością (zaokrągloną) z poprzedniego wiersza.

To działa, ponieważ jesteś nie tracisz informacji na każdym etapie, ale bardziej inteligentnie je wykorzystujesz. „Prawidłowe” zaokrąglone wartości znajdują się w ostatniej kolumnie i widać, że sumują się do 100.

Możesz zobaczyć różnicę między tym a ślepym zaokrągleniem każdej wartości, w trzeciej wartości powyżej. Podczas gdy 9.596008normalnie zaokrąglaby w górę 10, zgromadzone 71.211976poprawnie zaokrągla w dół do 71- oznacza to, że wystarczy tylko 9dodać do poprzedniej linii bazowej 62.


Działa to również dla „problematycznej” sekwencji, takiej jak trzy z grubsza wartości, przy czym jedną z nich należy zaokrąglić w górę: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
Drugie podejście rozwiązuje oba te problemy. Pierwszy daje 26, 25, 26, 23, drugi 1, 0, 1, 0, 1, 0, ....
paxdiablo

Podejście to działa również dobrze w przypadku zaokrąglania małych liczb, ponieważ zapobiega wyświetlaniu liczb ujemnych
Jonty5817,

19

Celem zaokrąglania jest wygenerowanie najmniejszej ilości błędów. Gdy zaokrąglasz jedną wartość, proces ten jest prosty i bezpośredni, a większość ludzi łatwo to rozumie. Gdy zaokrąglasz wiele liczb w tym samym czasie, proces staje się trudniejszy - musisz zdefiniować sposób łączenia błędów, tj. Co należy zminimalizować.

Dobrze głosowało odpowiedź przez Varun Vohrą minimalizuje sumę błędów bezwzględnych, i to jest bardzo proste do wykonania. Są jednak przypadki brzegowe, których nie obsługuje - co powinno wynikać z zaokrąglania 24.25, 23.25, 27.25, 25.25? Jeden z nich należy zaokrąglić w górę zamiast w dół. Prawdopodobnie wybrałbyś pierwszy lub ostatni z listy.

Być może lepiej jest użyć błędu względnego zamiast bezwzględnego błędu . Zaokrąglenie do 23,25 do 24 powoduje zmianę o 3,2%, natomiast zaokrąglenie do 27,25 do 28 zmienia tylko o 2,8%. Teraz jest wyraźny zwycięzca.

Można to jeszcze bardziej ulepszyć. Jedną z powszechnych technik jest wyrównywanie każdego błędu, dzięki czemu duże błędy liczą się nieproporcjonalnie więcej niż małe. Użyłbym również nieliniowego dzielnika, aby uzyskać błąd względny - nie wydaje się właściwe, aby błąd przy 1% był 99 razy ważniejszy niż błąd przy 99%. W poniższym kodzie użyłem pierwiastka kwadratowego.

Kompletny algorytm wygląda następująco:

  1. Zsumuj wartości procentowe po zaokrągleniu ich wszystkich w dół i odejmij od 100. To pokazuje, ile z tych wartości procentowych należy zaokrąglić w górę.
  2. Wygeneruj dwa wyniki błędu dla każdego procentu, jeden po zaokrągleniu w dół i jeden po zaokrągleniu w górę. Weź różnicę między nimi.
  3. Posortuj różnice błędów powstałe powyżej.
  4. Aby uzyskać liczbę procentową, którą należy zaokrąglić w górę, weź element z posortowanej listy i zwiększ zaokrągloną wartość procentową o 1.

Na przykład nadal możesz mieć więcej niż jedną kombinację z tą samą sumą błędów 33.3333333, 33.3333333, 33.3333333 . Jest to nieuniknione, a wynik będzie całkowicie arbitralny. Podany poniżej kod woli zaokrąglać w górę wartości po lewej stronie.

Złożenie tego wszystkiego razem w Pythonie wygląda następująco.

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]

Jak widać na ostatnim przykładzie, algorytm ten nadal może dostarczać nieintuicyjne wyniki. Mimo że 89,0 nie wymaga zaokrąglania, jedną z wartości z tej listy trzeba zaokrąglić w górę; najniższy błąd względny wynika z zaokrąglenia w górę tej dużej wartości, a nie ze znacznie mniejszych alternatyw.

Ta odpowiedź początkowo opowiadała się za każdą możliwą kombinacją zaokrąglania w górę / zaokrąglania w dół, ale jak wskazano w komentarzach, prostsza metoda działa lepiej. Algorytm i kod odzwierciedlają to uproszczenie.


1
Nie sądzę, że trzeba brać pod uwagę wszystkie kombinacje: proces w kolejności malejącego spadku ważonego błędu przechodzącego od rundy do zera do rundy do nieskończoności (w zasadzie wprowadzenie ważenia do odpowiedzi Veruna Vohrasa i Yonilevy'ego („identycznych”)).
siwobrody

@ Greybeard masz rację, myślałem o tym. Nie mogłem po prostu posortować według błędu, ponieważ dla każdej wartości występują dwa błędy, ale biorąc pod uwagę różnicę, rozwiązałem ten problem. Zaktualizowałem odpowiedź.
Mark Ransom

Wolę zawsze mieć 0%, gdy faktyczna liczba wynosi 0%. Więc dodawanie if actual == 0: return 0do error_gendziała świetnie.
Nikolay Baluk

1
jaka jest isclosemetoda na początku round_to_100?
toto_tico


7

NIE sumuj zaokrąglonych liczb. Będziesz miał niedokładne wyniki. Suma może być znacznie mniejsza w zależności od liczby terminów i rozkładu części ułamkowych.

Wyświetl zaokrąglone liczby, ale zsumuj wartości rzeczywiste. W zależności od tego, jak prezentujesz liczby, rzeczywisty sposób na zrobienie tego może się różnić. W ten sposób dostajesz

 14
 48
 10
 29
 __
100

Jakkolwiek pójdziesz, będziesz mieć rozbieżności. W twoim przykładzie nie ma sposobu na pokazanie liczb, które sumują się do 100 bez „zaokrąglenia” jednej wartości w niewłaściwy sposób (najmniejszy błąd zmieniłby się z 9,596 na 9)

EDYTOWAĆ

Musisz wybrać jedną z następujących opcji:

  1. Dokładność przedmiotów
  2. Dokładność sumy (jeśli sumujesz zaokrąglone wartości)
  3. Spójność między zaokrąglonymi pozycjami a zaokrągloną sumą)

Przez większość czasu, gdy masz do czynienia z odsetkami # 3, najlepsza jest opcja, ponieważ bardziej oczywiste jest, gdy suma równa się 101% niż wtedy, gdy poszczególne pozycje nie sumują się do 100, a ty zachowujesz dokładność poszczególnych pozycji. „Zaokrąglanie” 9,596 do 9 jest moim zdaniem niedokładne.

Aby to wyjaśnić, czasami dodaję przypis wyjaśniający, że poszczególne wartości są zaokrąglone i mogą nie sumować 100% - każdy, kto rozumie zaokrąglanie, powinien być w stanie zrozumieć to wyjaśnienie.


6
Nie jest to bardzo pomocne, ponieważ drukowane wartości nie sumują się do 100. Celem tego pytania było uniemożliwienie użytkownikom myślenia, że ​​wartości są nieprawidłowe, co w tym przypadku większość osób zrobiłaby, patrząc i porównując z sumą .
vvohra87

@ VarunVohra przeczytaj moją edycję, NIE MOŻESZ wyświetlać swoich liczb, tak że sumują się one do 100 bez „zaokrąglania” jednego o więcej niż 0,5.
D Stanley,

1
@DStanley faktycznie, z wyjątkiem zestawu, w którym wszystkie liczby są nieśmiałe od 0,5, możesz. Sprawdź moją odpowiedź - LRM właśnie to robi.
vvohra87,

3
@VarunVohra W oryginalnym przykładzie LRM da 14, 48, 9 i 29, co „zaokrągli” 9,596 do 9. Jeśli przydzielamy na podstawie liczb całkowitych, LRM będzie najdokładniejszy, ale nadal zmienia jeden wynik o więcej niż pół jednostki.
D Stanley,

7

Napisałem pomocnika zaokrąglania wersji C #, algorytm jest taki sam jak odpowiedź Varun Vohra , mam nadzieję, że to pomoże.

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

Przeszedł następujący test jednostkowy:

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

Miły! dał mi podstawę na początek. Wyliczenie nie ma ForEach, choć wierzę
Jack0fshad0ws

4

Możesz spróbować śledzić swój błąd z powodu zaokrąglania, a następnie zaokrąglać względem ziarna, jeśli skumulowany błąd jest większy niż ułamkowa część bieżącej liczby.

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

Nie jestem pewien, czy to w ogóle zadziałałoby, ale wydaje się działać podobnie, jeśli kolejność zostanie odwrócona:

28.78 -> 29 (+.22)
 9.59 ->  9 (-.37; rounded down because .59 > .22)
47.98 -> 48 (-.35)
13.62 -> 14 (+.03)
------------
        100

Jestem pewien, że istnieją przypadki skrajne, w których może się to zepsuć, ale każde podejście będzie przynajmniej nieco arbitralne, ponieważ zasadniczo modyfikujesz swoje dane wejściowe.


2
Księgowi i bankierzy stosują podobną technikę od setek lat. „Noś resztę” z jednego rzędu do drugiego. Zacznij od 1/2 jednego centa w „carry”. Dodaj „carry” do pierwszej wartości i obetnij. Teraz kwotę, którą straciłeś przez obcięcie, umieść to w „carry”. Zrób to do samego końca, a zaokrąglone liczby będą za każdym razem sumować się do pożądanej sumy.
Jeff Grigg

Carolyn Kay zasugerowała tę implementację w programie Access VB 2007: <kod> „Okrągły zwrot pieniędzy za pomocą metody„ przenieś resztę ”ref1 = rsQry! [Refund Paid $$$] * rsQry! [Value Value] / propValTot ref2 = ref1 + ref5 „Dodaj przenoszoną resztę, zero, aby rozpocząć ref3 = ref2 * 100” Pomnóż przez 100 do liczby całkowitej ref4 = ref3 / 100 ”Podziel przez 100 na liczbę dziesiętną rsTbl! [Refund Paid $$$] = ref4” Wpisz „ pozostała ”zaokrąglona liczba w tabeli ref5 = ref2 - ref4 'Noś nową resztę </code>
Jeff Grigg

2

Kiedyś napisałem nieziemskie narzędzie, aby znaleźć minimalne zaburzenie dla zestawu liczb odpowiadających celowi. To był inny problem, ale teoretycznie można tu zastosować podobny pomysł. W tym przypadku mamy do wyboru.

Tak więc dla pierwszego elementu możemy go zaokrąglić w górę do 14 lub w dół do 13. Koszt (w binarnym programowaniu liczb całkowitych) zrobienia tego jest mniejszy w przypadku zaokrąglania w górę niż zaokrąglania w dół, ponieważ zaokrąglanie w dół wymaga przesuń tę wartość na większą odległość. Podobnie możemy zaokrąglać każdą liczbę w górę lub w dół, więc mamy do wyboru 16 opcji.

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

Zwykle rozwiązałbym ogólny problem w MATLAB, tutaj za pomocą bintprog, binarnego narzędzia do programowania liczb całkowitych, ale jest tylko kilka opcji do przetestowania, więc wystarczy proste pętle, aby przetestować każdą z 16 alternatyw. Załóżmy na przykład, że mamy zaokrąglić ten zestaw jako:

 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

Całkowity wykonany błąd bezwzględny wynosi 1,25266. Można go nieco zmniejszyć, stosując następujące alternatywne zaokrąglenie:

 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

W rzeczywistości będzie to optymalne rozwiązanie pod względem błędu bezwzględnego. Oczywiście, jeśli będzie 20 terminów, przestrzeń wyszukiwania będzie miała rozmiar 2 ^ 20 = 1048576. W przypadku 30 lub 40 haseł przestrzeń będzie miała znaczny rozmiar. W takim przypadku należy użyć narzędzia, które może skutecznie przeszukiwać przestrzeń, być może używając schematu rozgałęzienia i powiązania.


Na wszelki wypadek: algorytm „największej pozostałej” musi zminimalizować całkowity błąd bezwzględny zgodnie z Twoją metryką (patrz odpowiedź @ varunvohra). Dowód jest prosty: załóżmy, że nie minimalizuje to błędu. Następnie musi istnieć pewien zestaw wartości, które zaokrągla w dół, które należy zaokrąglić w górę i odwrotnie (oba zestawy są tego samego rozmiaru). Ale każda wartość, którą zaokrągla w dół, jest większa od następnej liczby całkowitej niż jakakolwiek wartość, którą zaokrągla w górę (i vv), więc nowa wartość błędu musi być większa. CO BYŁO DO OKAZANIA. Jednak nie działa dla wszystkich wskaźników błędów; potrzebne są inne algorytmy.
rici

2

Myślę, że następujące rzeczy osiągną to, czego szukasz

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]

I ostatnią rzeczą, uruchomiłem funkcję używając liczb podanych pierwotnie w pytaniu, aby porównać z pożądanym wyjściem

func([13.626332, 47.989636, 9.596008, 28.788024], 100); // => [48, 29, 13, 10]

Różniło się to od tego, czego chciało pytanie => [48, 29, 14, 9]. Nie mogłem tego zrozumieć, dopóki nie spojrzałem na całkowity margines błędu

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

Zasadniczo wynik mojej funkcji wprowadza najmniej błędu.

Fiddle tutaj


właśnie to miałem na myśli, z tą różnicą, że błąd powinien być mierzony w stosunku do wartości (zaokrąglenie 9,8 do 10 jest większym błędem niż zaokrąglenie z 19,8 do 20). Można to jednak łatwo zrobić, odzwierciedlając to w wywołaniu zwrotnym sortowania.
poezn

jest to niewłaściwe dla [33.33, 33.33, 33.33, 0.1], zwraca [1, 33, 33, 33], a nie bardziej dokładne [34, 33, 33, 0]
yonilevy

@yonilevy Dzięki za to. Naprawiono teraz.
Bruno,

jeszcze nie, dla [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] zwraca [15, 17, 17, 17, 17, 17] zamiast [16, 16, 17, 17, 17, 17] - patrz mój odpowiedź
yonilevy,

2

Nie jestem pewien, jakiego poziomu dokładności potrzebujesz, ale chciałbym po prostu dodać 1 pierwsze nliczby, co njest pułapem całkowitej sumy dziesiętnej. W takim przypadku 3dodam 1 do pierwszych 3 przedmiotów, a resztę wyłożę na podłogę. Oczywiście nie jest to zbyt dokładne, niektóre liczby mogą być zaokrąglane w górę lub w dół, gdy nie powinno, ale działa dobrze i zawsze daje 100%.

Tak [ 13.626332, 47.989636, 9.596008, 28.788024 ]byłoby, [14, 48, 10, 28]ponieważ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

Zawsze możesz poinformować użytkowników, że liczby są zaokrąglone i mogą nie być zbyt dokładne ...


1

Jeśli zaokrąglasz go, nie ma dobrego sposobu na uzyskanie tego samego we wszystkich przypadkach.

Możesz wziąć dziesiętną część N procentów, które masz (w podanym przykładzie jest to 4).

Dodaj części dziesiętne. W twoim przykładzie masz całkowitą część ułamkową = 3.

Sufituj 3 liczby z najwyższymi ułamkami, a resztę podłogę.

(Przepraszamy za zmiany)


1
Chociaż może to dać liczby zwiększające się do 100, możesz w końcu zmienić 3,9 na 3 i 25,1 na 26.
RobG

Nie. 3,9 będzie wynosić 4, a 25,1 będzie wynosić 25. Powiedziałem, żeby podciąć 3 liczby najwyższymi ułamkami, a nie najwyższą wartością.
arunlalam

2
jeśli jest zbyt wiele ułamków kończących się na .9, powiedzmy 9 wartości 9,9% i jedna wartość 10,9 tam jedna wartość, która skończy się jako 9%, 8 jako 10% i jedna jako 11%.
arunlalam

1

Jeśli naprawdę musisz je zaokrąglić, istnieją już bardzo dobre sugestie (największa reszta, najmniejszy błąd względny itd.).

Jest też jeden dobry powód, aby nie zaokrąglać (dostaniesz co najmniej jedną liczbę, która „wygląda lepiej”, ale jest „zła”), i jak to rozwiązać (ostrzeż swoich czytelników) i to właśnie robię.

Pozwól mi dodać „niewłaściwą” część liczbową.

Załóżmy, że masz trzy zdarzenia / byty / ... z pewnymi wartościami procentowymi, które przybliżasz jako:

DAY 1
who |  real | app
----|-------|------
  A | 33.34 |  34
  B | 33.33 |  33
  C | 33.33 |  33

Później wartości nieznacznie się zmieniają na

DAY 2
who |  real | app
----|-------|------
  A | 33.35 |  33
  B | 33.36 |  34
  C | 33.29 |  33

Pierwszy stół ma już wspomniany problem z „niewłaściwą” liczbą: 33,34 jest bliższy 33 niż 34.

Ale teraz masz większy błąd. Porównując dzień 2 z dniem 1, rzeczywista wartość procentowa dla A wzrosła o 0,01%, ale przybliżenie pokazuje spadek o 1%.

Jest to błąd jakościowy, prawdopodobnie znacznie gorszy niż początkowy błąd ilościowy.

Można opracować przybliżenie dla całego zestawu, ale być może będziesz musiał opublikować dane pierwszego dnia, więc nie będziesz wiedział o drugim dniu. Tak więc, chyba że naprawdę naprawdę musisz się zbliżyć, prawdopodobnie lepiej nie.


każdy, kto wie, jak tworzyć lepsze stoły, albo edytuj, albo powiedz mi, jak / gdzie
Rolazaro Azeveires

0

sprawdź, czy jest to prawidłowe, czy nie, o ile w moich testowych przypadkach mogę to uruchomić.

powiedzmy, że liczba to k;

  1. sortuj procent według malejącej wartości.
  2. powtarzaj każdy procent od malejącej kolejności.
  3. oblicz procent k dla pierwszego procentu weź Math.Ceil mocy wyjściowej.
  4. następny k = k-1
  5. powtarzaj, aż cały procent zostanie zużyty.

0

Wdrożyłem metodę z odpowiedzi Varun Vohra tutaj zarówno dla list, jak i nagrań.

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

Oto prostsza implementacja w języku Python odpowiedzi @ 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

Trzeba math, itertools, operator.


0

Dla tych, którzy mają wartości procentowe w serii pand, oto moja implementacja metody największej reszty (jak w odpowiedzi Varuna Vohry ), w której możesz nawet wybrać ułamki dziesiętne, do których chcesz zaokrąglić.

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

Jest to przypadek zaokrąglania przez bankiera, zwanego również „okrągłym pół-parzystym”. Jest obsługiwany przez BigDecimal. Jego celem jest zapewnienie, że zaokrąglanie się równoważy, tzn. Nie faworyzuje ani banku, ani klienta.


5
NIE zapewnia to, że zaokrąglanie się równoważy - to po prostu zmniejsza ilość błędów poprzez rozdzielenie półokrągłości między liczby parzyste i nieparzyste. Nadal istnieją scenariusze, w których zaokrąglanie przez bankierów daje niedokładne wyniki.
D Stanley,

@DStanley Zgoda. Nie powiedziałem inaczej. Podałem swój cel . Bardzo ostrożnie.
Markiz Lorne

2
W porządku - źle zinterpretowałem to, co próbujesz powiedzieć. W obu przypadkach nie sądzę, że to rozwiązuje problem, ponieważ użycie zaokrąglania przez bankierów nie zmieni wyników w przykładzie.
D Stanley,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.