Wie man gerundete Prozentsätze macht, summiert sich zu 100%


192

Betrachten Sie die folgenden vier Prozentsätze, dargestellt als floatZahlen:

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

Ich muss diese Prozentsätze als ganze Zahlen darstellen. Wenn ich einfach benutze Math.round(), erhalte ich insgesamt 101%.

14 + 48 + 10 + 29 = 101

Wenn ich benutze parseInt(), erhalte ich insgesamt 97%.

13 + 47 + 9 + 28 = 97

Was ist ein guter Algorithmus, um eine beliebige Anzahl von Prozentsätzen als ganze Zahlen darzustellen und dabei insgesamt 100% beizubehalten?


Bearbeiten : Nachdem Sie einige der Kommentare und Antworten gelesen haben, gibt es eindeutig viele Möglichkeiten, dies zu lösen.

Um den Zahlen treu zu bleiben, ist das "richtige" Ergebnis meines Erachtens dasjenige, das den Gesamtfehler minimiert, definiert durch die Menge an Fehlerrundungen im Verhältnis zum tatsächlichen Wert:

        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)

Bei Gleichstand (3.33, 3.33, 3.33) kann eine willkürliche Entscheidung getroffen werden (zB 3, 4, 3).


21
Angenommen, Sie haben 3.33, 3.33 und 3.33. Welches wirst du 4 machen?
RobG

3
Genau. Die Frage verkörpert einen Widerspruch.
Marquis von Lorne

4
Es ist ein sehr häufiges Szenario bei der Berichterstellung - wie eine "Summe" von Dezimalwerten angezeigt wird, die nicht immer mit der Summe der angezeigten Werte übereinstimmt.
D Stanley

1
Was ist das "richtige" Ergebnis in Ihrem Beispielfall? Das könnte die Meinungsverschiedenheiten darüber lösen, was die "beste" Lösung ist.
D Stanley

Antworten:


35

Da keine der Antworten hier es richtig zu lösen scheint, ist hier meine halbverschleierte Version mit Unterstrichen :

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
Korrigieren Sie mich, wenn ich falsch liege, aber ist dies nicht eine Implementierung des in meiner Antwort vorgeschlagenen Algorithmus? (Nicht auf Unterstrichen zu löschen)
vvohra87

@ VarunVohra Entschuldigung, ich habe das bis jetzt nicht bemerkt, ja, es sieht so aus, als ob dein Algorithmus der gleiche ist :) Ich bin mir nicht sicher, warum mein Beitrag die akzeptierte Antwort ist. Der verschleierte Code war nur für die Lolz ...
Yonilevy

@yonilevy Mein Kommentar wurde gelöscht. Ich wusste nur nicht, dass es eine sortierte Liste zurückgeben sollte. Ich entschuldige mich!
Zack Burt

2
Bei dieser Funktion tritt ein Problem auf, wenn das letzte Element 0 ist und die vorherigen zu 100 addiert werden. Beispiel: [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. Der letzte gibt logischerweise -1 zurück. Ich dachte sehr schnell an die folgende Lösung, aber es gibt wahrscheinlich etwas Besseres: jsfiddle.net/0o75bw43/1
Cruclax

1
@Cruclax zeigt alle 1, wenn alle Einträge im Eingabearray Null sind
tony.0919

158

Es gibt viele Möglichkeiten, dies zu tun, vorausgesetzt, Sie sind nicht besorgt über das Vertrauen in die ursprünglichen Dezimaldaten.

Die erste und vielleicht beliebteste Methode wäre die größte Restmethode

Welches ist im Grunde:

  1. Alles abrunden
  2. Den Unterschied in Summe und 100 bekommen
  3. Verteilen der Differenz durch Hinzufügen von 1 zu Elementen in absteigender Reihenfolge ihrer Dezimalstellen

In Ihrem Fall würde es so aussehen:

13.626332%
47.989636%
 9.596008%
28.788024%

Wenn Sie die ganzzahligen Teile nehmen, erhalten Sie

13
47
 9
28

Das ergibt 97, und Sie möchten drei weitere hinzufügen. Nun betrachten Sie die Dezimalstellen, die sind

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

und nehmen Sie die größten, bis die Summe 100 erreicht. Sie würden also erhalten:

14
48
 9
29

Alternativ können Sie einfach eine Dezimalstelle anstelle von ganzzahligen Werten anzeigen. Die Zahlen wären also 48,3 und 23,9 usw. Dies würde die Varianz von 100 um ein Vielfaches verringern.


5
Diese "Feature Column" auf der Website der American Mathematical Society - Aufteilung II: Aufteilungssysteme - beschreibt mehrere ähnliche Aufteilungsmethoden.
Kenny Evitt

1
Dies sieht fast aus wie ein Kopieren und Einfügen meiner Antwort hier stackoverflow.com/questions/5227215/… .
Sawa

Beachten Sie, dass im Gegensatz zu Ihrem Kommentar zur Antwort von @DStanley in Ihrer Antwort 9,596008% auf 9% gerundet wurden, was mehr als 0,5% Unterschied ist. Trotzdem eine gute Antwort.
Rolazaro Azeveires

32

Wahrscheinlich ist der "beste" Weg, dies zu tun (zitiert, da "bester" ein subjektiver Begriff ist), eine laufende (nicht integrale) Zählung Ihrer Position zu führen und diesen Wert zu runden .

Verwenden Sie dies dann zusammen mit dem Verlauf, um herauszufinden, welcher Wert verwendet werden soll. Verwenden Sie beispielsweise die von Ihnen angegebenen Werte:

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

In jeder Phase runden Sie die Zahl selbst nicht. Stattdessen runden Sie die angesammelten Wert und berechnen die beste Ganzzahl, die diesen Wert aus der vorherigen Basislinie erreicht - diese Basislinie ist der kumulative Wert (gerundet) der vorherigen Zeile.

Dies funktioniert, weil Sie nicht in jeder Phase Informationen verlieren, sondern die Informationen intelligenter nutzen. Die 'richtigen' gerundeten Werte befinden sich in der letzten Spalte und Sie können sehen, dass sie sich zu 100 summieren.

Sie können den Unterschied zwischen diesem und dem blinden Runden jedes Werts im dritten Wert oben sehen. Während 9.596008normalerweise auf 10aufgerundet 71.211976wird, rundet das akkumulierte korrekt auf ab 71- dies bedeutet, dass nur 9zum Hinzufügen zur vorherigen Grundlinie von benötigt wird 62.


Dies funktioniert auch für "problematische" Sequenzen wie drei Grobwerte , von denen einer aufgerundet werden sollte: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
Der zweite Ansatz behebt beide Probleme. Der erste gibt 26, 25, 26, 23, der zweite 1, 0, 1, 0, 1, 0, ....
Paxdiablo

Dieser Ansatz
eignet

18

Das Ziel der Rundung ist es, die geringste Fehlermenge zu erzeugen. Wenn Sie einen einzelnen Wert runden, ist dieser Prozess einfach und unkompliziert, und die meisten Menschen verstehen ihn leicht. Wenn Sie mehrere Zahlen gleichzeitig runden, wird der Prozess schwieriger - Sie müssen definieren, wie die Fehler kombiniert werden sollen, dh was minimiert werden muss.

Die gut gewählte Antwort von Varun Vohra minimiert die Summe der absoluten Fehler und ist sehr einfach zu implementieren. Es gibt jedoch Randfälle, die nicht behandelt werden - was sollte das Ergebnis einer Rundung sein?24.25, 23.25, 27.25, 25.25 ? Eine davon muss aufgerundet statt abgerundet werden. Sie würden wahrscheinlich nur willkürlich den ersten oder letzten in der Liste auswählen.

Vielleicht ist es besser, den relativen Fehler anstelle des absoluten zu verwenden Fehlers zu verwenden. Durch Runden von 23,25 auf 24 wird es um 3,2% geändert, während durch Runden von 27,25 auf 28 nur um 2,8% geändert wird. Jetzt gibt es einen klaren Gewinner.

Es ist möglich, dies noch weiter zu optimieren. Eine übliche Technik ist zu quadrieren jeden Fehler, so dass große Fehler zählen unverhältnismäßig mehr als kleine. Ich würde auch einen nichtlinearen Divisor verwenden, um den relativen Fehler zu erhalten - es scheint nicht richtig, dass ein Fehler bei 1% 99-mal wichtiger ist als ein Fehler bei 99%. Im folgenden Code habe ich die Quadratwurzel verwendet.

Der vollständige Algorithmus lautet wie folgt:

  1. Summieren Sie die Prozentsätze, nachdem Sie sie alle abgerundet haben, und subtrahieren Sie sie von 100. Hier erfahren Sie, wie viele dieser Prozentsätze stattdessen aufgerundet werden müssen.
  2. Generieren Sie zwei Fehlerwerte für jeden Prozentsatz, einen beim Abrunden und einen beim Aufrunden. Nehmen Sie den Unterschied zwischen den beiden.
  3. Sortieren Sie die oben erzeugten Fehlerunterschiede.
  4. Nehmen Sie für die Anzahl der Prozentsätze, die aufgerundet werden müssen, ein Element aus der sortierten Liste und erhöhen Sie den abgerundeten Prozentsatz um 1.

Sie können beispielsweise immer noch mehr als eine Kombination mit derselben Fehlersumme haben 33.3333333, 33.3333333, 33.3333333 . Dies ist unvermeidlich und das Ergebnis ist völlig willkürlich. Der Code, den ich unten gebe, rundet die Werte auf der linken Seite lieber auf.

So sieht es in Python aus.

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]

Wie Sie an diesem letzten Beispiel sehen können, kann dieser Algorithmus immer noch nicht intuitive Ergebnisse liefern. Obwohl 89.0 keinerlei Rundung benötigt, musste einer der Werte in dieser Liste aufgerundet werden. Der niedrigste relative Fehler ergibt sich aus der Aufrundung dieses großen Werts und nicht aus den viel kleineren Alternativen.

Diese Antwort befürwortete ursprünglich, jede mögliche Kombination von Aufrunden / Abrunden durchzugehen, aber wie in den Kommentaren ausgeführt, funktioniert eine einfachere Methode besser. Der Algorithmus und der Code spiegeln diese Vereinfachung wider.


1
Ich glaube nicht , dass Sie alle Kombinationen berücksichtigen müssen: Prozess um von aus gehen Rückgang der gewichteten Fehlern abnehmend Runde auf Null zu Runde bis ins Unendliche (ziemlich genau die Einführung mit einem Gewicht in Verun Vohras des und yonilevy der ( „identisch“) Antworten).
Graubart

@ Greybeard du hast recht, ich habe das überlegt. Ich konnte den Fehler nicht einfach sortieren, da es für jeden Wert zwei Fehler gibt, aber die Differenz zu nehmen, löste das Problem. Ich habe die Antwort aktualisiert.
Mark Ransom

Ich bevorzuge es immer 0% zu haben, wenn die tatsächliche Anzahl 0% ist. Das Hinzufügen if actual == 0: return 0zu error_genfunktioniert also großartig.
Nikolay Baluk

1
Was ist die iscloseMethode am Anfang von round_to_100?
toto_tico


7

Summieren Sie NICHT die gerundeten Zahlen. Sie werden ungenaue Ergebnisse haben. Die Summe könnte in Abhängigkeit von der Anzahl der Begriffe und der Verteilung der Bruchteile erheblich abweichen.

Zeigen Sie die gerundeten Zahlen an, aber summieren Sie die tatsächlichen Werte. Je nachdem, wie Sie die Zahlen präsentieren, variiert die tatsächliche Vorgehensweise. Auf diese Weise erhalten Sie

 14
 48
 10
 29
 __ __
100

Wie auch immer Sie gehen, Sie werden Diskrepanzen haben. In Ihrem Beispiel gibt es keine Möglichkeit, Zahlen anzuzeigen, die sich zu 100 addieren, ohne einen Wert falsch zu "runden" (der geringste Fehler wäre die Änderung von 9,596 auf 9).

BEARBEITEN

Sie müssen zwischen folgenden Optionen wählen:

  1. Genauigkeit der Artikel
  2. Genauigkeit der Summe (wenn Sie gerundete Werte summieren)
  3. Konsistenz zwischen den gerundeten Elementen und der gerundeten Summe)

Meistens ist der Umgang mit Prozentsätzen Nr. 3 die beste Option, da es offensichtlicher ist, wenn die Gesamtsumme 101% beträgt, als wenn die einzelnen Elemente nicht 100 ergeben, und Sie die einzelnen Elemente genau halten. "Rundung" 9.596 auf 9 ist meiner Meinung nach ungenau.

Um dies zu erklären, füge ich manchmal eine Fußnote hinzu, die erklärt, dass die einzelnen Werte gerundet sind und möglicherweise nicht 100% betragen. Jeder, der die Rundung versteht, sollte diese Erklärung verstehen können.


6
Das ist nicht sehr hilfreich, da sich die gedruckten Werte nicht zu 100 addieren. Der Zweck der Frage bestand darin, zu verhindern, dass Benutzer denken, dass die Werte falsch sind, was in diesem Fall die meisten Leute tun würden, wenn sie die Summe betrachten und mit ihr vergleichen .
Vvohra87

@VarunVohra lies meine Bearbeitung, du kannst deine Zahlen NICHT so anzeigen, dass sie 100 ergeben, ohne eine um mehr als 0,5 zu "runden".
D Stanley

1
@DStanley tatsächlich, abgesehen von einem Satz, bei dem alle Zahlen weniger als 0,5 sind, können Sie. Überprüfen Sie meine Antwort - LRM macht genau das.
Vvohra87

3
@VarunVohra Im ursprünglichen Beispiel LRM nachgeben 14, 48, 9 und 29 das wird „rund“ 9,596 bis 9. Wenn wir die Zuteilung auf Basis von ganzen Zahlen LRM wird die genaueste, aber es ändert sich immer noch ein Ergebnis von mehr als eine halbe Einheit.
D Stanley

7

Ich habe einen Rundungshelfer für die C # -Version geschrieben. Der Algorithmus entspricht der Antwort von Varun Vohra. Ich hoffe, er hilft.

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

Es besteht den folgenden 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 });
}

Nett! gab mir eine Basis, um mit zu beginnen .. Enumerable hat ForEach nicht, obwohl ich glaube
Jack0fshad0ws

4

Sie können versuchen, Ihren Fehler aufgrund von Rundungen zu verfolgen und dann gegen das Korn zu runden, wenn der akkumulierte Fehler größer als der Bruchteil der aktuellen Zahl ist.

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

Ich bin mir nicht sicher, ob dies im Allgemeinen funktionieren würde, aber es scheint ähnlich zu funktionieren, wenn die Reihenfolge umgekehrt ist:

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

Ich bin mir sicher, dass es Randfälle gibt, in denen dies möglicherweise nicht funktioniert, aber jeder Ansatz wird zumindest etwas willkürlich sein, da Sie Ihre Eingabedaten grundsätzlich ändern.


2
Buchhalter und Banker verwenden seit Hunderten von Jahren eine ähnliche Technik. "Tragen Sie den Rest" von einer Reihe zur nächsten. Beginnen Sie mit einem halben Cent im "Carry". Fügen Sie den "Übertrag" zum ersten Wert hinzu und schneiden Sie ihn ab. Geben Sie nun den Betrag, den Sie durch Abschneiden verloren haben, in das Feld "Tragen" ein. Wenn Sie dies ganz nach unten tun, addieren sich die gerundeten Zahlen jedes Mal genau zu der gewünschten Summe.
Jeff Grigg

Carolyn Kay schlug diese Implementierung in Access VB 2007 vor: <code> 'Runde Rückerstattungsdollar mit der Methode "Übertragen Sie den Rest" ref1 = rsQry! [Rückerstattung bezahlt $$$] * rsQry! [Eigenschaftswert] / propValTot ref2 = ref1 + ref5 'Addiere den übertragenen Rest, Null, um ref3 = ref2 * 100 zu starten.' Multipliziere mit 100 zu einer ganzen Zahl ref4 = ref3 / 100 'Dividiere durch 100 in eine Dezimalzahl rsTbl! [Rückerstattung bezahlt $$$] = ref4' Setze das " Rest "gerundete Zahl in der Tabelle ref5 = ref2 - ref4 'Tragen Sie den neuen Rest </ code>
Jeff Grigg

2

Ich habe einmal ein ungerundetes Werkzeug geschrieben, um die minimale Störung einer Reihe von Zahlen zu finden, die einem Ziel entsprechen. Es war ein anderes Problem, aber theoretisch könnte man hier eine ähnliche Idee verwenden. In diesem Fall haben wir eine Reihe von Möglichkeiten.

Für das erste Element können wir es also entweder auf 14 oder auf 13 abrunden. Die Kosten (im Sinne einer binären Ganzzahlprogrammierung) sind für die Aufrundung geringer als für die Abrundung, da die Abrundung dies erfordert Bewegen Sie diesen Wert um eine größere Strecke. Ebenso können wir jede Zahl auf- oder abrunden, sodass wir insgesamt 16 Auswahlmöglichkeiten haben.

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

Normalerweise würde ich das allgemeine Problem in MATLAB lösen, hier mit bintprog, einem Programmierwerkzeug für binäre Ganzzahlen, aber es müssen nur wenige Optionen getestet werden, sodass es mit einfachen Schleifen einfach genug ist, jede der 16 Alternativen zu testen. Angenommen, wir würden diesen Satz wie folgt abrunden:

 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

Der gesamte absolute Fehler beträgt 1,25266. Sie kann durch folgende alternative Rundung leicht reduziert werden:

 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

In der Tat wird dies die optimale Lösung in Bezug auf den absoluten Fehler sein. Wenn es 20 Begriffe gäbe, hätte der Suchraum natürlich die Größe 2 ^ 20 = 1048576. Für 30 oder 40 Begriffe hat dieser Raum eine signifikante Größe. In diesem Fall müssten Sie ein Tool verwenden, das den Raum effizient durchsuchen kann, möglicherweise mithilfe eines Verzweigungs- und gebundenen Schemas.


Nur zum späteren Nachschlagen: Der Algorithmus "größter Rest" muss den absoluten Gesamtfehler gemäß Ihrer Metrik minimieren (siehe Antwort von @ varunvohra). Der Beweis ist einfach: Angenommen, er minimiert den Fehler nicht. Dann muss es einen Satz von Werten geben, die abgerundet werden und aufgerundet werden sollen, und umgekehrt (die beiden Sätze sind gleich groß). Jeder abgerundete Wert ist jedoch weiter von der nächsten Ganzzahl entfernt als jeder aufgerundete Wert (und vv), sodass der neue Fehlerbetrag größer sein muss. QED. Es funktioniert jedoch nicht für alle Fehlermetriken. andere Algorithmen werden benötigt.
Rici

2

Ich denke, das Folgende wird das erreichen, wonach Sie suchen

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]

Als letztes habe ich die Funktion mit den ursprünglich in der Frage angegebenen Zahlen ausgeführt, um sie mit der gewünschten Ausgabe zu vergleichen

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

Dies war anders als die Frage wollte => [48, 29, 14, 9]. Ich konnte das nicht verstehen, bis ich mir die gesamte Fehlerquote angesehen hatte

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

Im Wesentlichen führt das Ergebnis meiner Funktion tatsächlich die geringste Fehlermenge ein.

Geige hier


Das ist ziemlich genau das, was ich mir vorgestellt habe, mit dem Unterschied, dass der Fehler relativ zum Wert gemessen werden sollte (Rundung 9,8 auf 10 ist ein größerer Fehler als Rundung von 19,8 auf 20). Dies könnte jedoch leicht erreicht werden, indem dies im Sortierrückruf berücksichtigt wird.
poezn

Dies ist falsch für [33.33, 33.33, 33.33, 0.1], es gibt eher [1, 33, 33, 33] als das genauere [34, 33, 33, 0] zurück
yonilevy

@yonilevy Danke dafür. Jetzt behoben.
Bruno

noch nicht, für [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] wird eher [15, 17, 17, 17, 17, 17] als [16, 16, 17, 17, 17, 17] zurückgegeben - siehe meine Antwort
Yonilevy

2

Ich bin mir nicht sicher, welche Genauigkeit Sie benötigen, aber ich würde einfach 1 die ersten nZahlen addieren, was ndie Obergrenze der Gesamtsumme der Dezimalstellen darstellt. In diesem Fall 3würde ich also 1 zu den ersten 3 Elementen hinzufügen und den Rest auf den Boden legen. Natürlich ist dies nicht sehr genau, einige Zahlen können aufgerundet oder abgerundet werden, wenn dies nicht der Fall sein sollte, aber es funktioniert in Ordnung und führt immer zu 100%.

So [ 13.626332, 47.989636, 9.596008, 28.788024 ]wäre [14, 48, 10, 28]dennMath.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

Sie können Benutzer jederzeit darüber informieren, dass die Zahlen gerundet sind und möglicherweise nicht sehr genau sind ...


1

Wenn Sie es runden, gibt es keine gute Möglichkeit, es in jedem Fall genau gleich zu bekommen.

Sie können den Dezimalteil der N Prozentsätze nehmen, die Sie haben (in dem Beispiel, das Sie angegeben haben, ist es 4).

Fügen Sie die Dezimalstellen hinzu. In Ihrem Beispiel haben Sie insgesamt Bruchteil = 3.

Decken Sie die 3 Zahlen mit den höchsten Anteilen ab und legen Sie den Rest auf den Boden.

(Entschuldigung für die Änderungen)


1
Während dies Zahlen liefern kann, die sich zu 100 addieren, können Sie am Ende 3,9 in 3 und 25,1 in 26
verwandeln

Nein. 3,9 wird 4 und 25,1 wird 25 sein. Ich sagte, die 3 Zahlen mit den höchsten Brüchen nicht den höchsten Wert zu begrenzen.
Arunlalam

2
Wenn es viel zu viele Brüche gibt, die mit 0,9 enden, sagen 9 Werte von 9,9% und ein Wert von 10,9, dann gibt es einen Wert, der als 9%, 8 als 10% und einen als 11% endet.
Arunlalam

1

Wenn Sie sie wirklich abrunden müssen, gibt es hier bereits sehr gute Vorschläge (größter Rest, geringster relativer Fehler usw.).

Es gibt auch schon einen guten Grund, nicht zu runden (Sie erhalten mindestens eine Zahl, die "besser aussieht", aber "falsch" ist), und wie Sie das lösen können (warnen Sie Ihre Leser), und das ist, was ich tue.

Lassen Sie mich den "falschen" Nummernteil hinzufügen.

Angenommen, Sie haben drei Ereignisse / Entitäten / ... mit einigen Prozentsätzen, die Sie als ungefähr annähern:

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

Später ändern sich die Werte geringfügig auf

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

Die erste Tabelle hat das bereits erwähnte Problem, eine "falsche" Zahl zu haben: 33,34 ist näher an 33 als an 34.

Aber jetzt hast du einen größeren Fehler. Im Vergleich von Tag 2 mit Tag 1 stieg der reale Prozentwert für A um 0,01%, aber die Annäherung zeigt eine Abnahme um 1%.

Das ist ein qualitativer Fehler, wahrscheinlich schlimmer als der anfängliche quantitative Fehler.

Man könnte eine Annäherung für den gesamten Satz entwickeln, aber möglicherweise müssen Sie Daten am ersten Tag veröffentlichen, sodass Sie nichts über den zweiten Tag wissen. Wenn Sie sich also nicht wirklich annähern müssen, sollten Sie es wahrscheinlich besser nicht tun.


Wer weiß, wie man bessere Tabellen erstellt, kann diese entweder bearbeiten oder mir sagen, wie / wo
Rolazaro Azeveires

0

Überprüfen Sie, ob dies für meine Testfälle gültig ist oder nicht. Ich kann dies zum Laufen bringen.

Nehmen wir an, die Zahl ist k.

  1. Sortierprozentsatz nach absteigend oder.
  2. Iterieren Sie über jeden Prozentsatz aus absteigender Reihenfolge.
  3. Berechnen Sie den Prozentsatz von k für den ersten Prozentsatz. Nehmen Sie Math.Ceil der Ausgabe.
  4. nächstes k = k-1
  5. iterieren Sie, bis der gesamte Prozentsatz verbraucht ist.

0

Ich habe die Methode aus Varun Vohras Antwort hier sowohl für Listen als auch für Diktate implementiert.

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

Hier ist eine einfachere Python-Implementierung der Antwort von @ 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

Sie müssen math, itertools, operator.


0

Für diejenigen, die die Prozentsätze in einer Pandas-Serie haben, ist hier meine Implementierung der Methode "Größter Rest" (wie in der Antwort von Varun Vohra ), bei der Sie sogar die Dezimalstellen auswählen können, auf die Sie runden möchten.

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

Dies ist ein Fall für die Rundung des Bankiers, auch bekannt als "Round Half-Even". Es wird von BigDecimal unterstützt. Damit soll sichergestellt werden, dass Rundungen ausgeglichen werden, dh weder die Bank noch der Kunde bevorzugt werden.


5
Es stellt NICHT sicher, dass die Rundung ausgeglichen wird - es reduziert lediglich die Fehlermenge, indem die Halbrundung zwischen geraden und ungeraden Zahlen verteilt wird. Es gibt immer noch Szenarien, in denen die Rundung von Bankern zu ungenauen Ergebnissen führt.
D Stanley

@ DStanley Einverstanden. Ich habe nichts anderes gesagt. Ich habe seinen Zweck angegeben . Sehr vorsichtig.
Marquis von Lorne

2
Fair genug - ich habe falsch interpretiert, was Sie sagen wollten. In beiden Fällen glaube ich nicht, dass dies das Problem löst, da die Verwendung von Banker-Rundungen die Ergebnisse im Beispiel nicht ändert.
D Stanley
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.