Come aumentare le percentuali arrotondate fino al 100%


193

Considera le quattro percentuali seguenti, rappresentate come floatnumeri:

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

Devo rappresentare queste percentuali come numeri interi. Se lo uso semplicemente Math.round(), finisco con un totale del 101%.

14 + 48 + 10 + 29 = 101

Se lo uso parseInt(), finisco con un totale del 97%.

13 + 47 + 9 + 28 = 97

Qual è un buon algoritmo per rappresentare un numero qualsiasi di percentuali come numeri interi pur mantenendo un totale del 100%?


Modifica : dopo aver letto alcuni dei commenti e delle risposte, ci sono chiaramente molti modi per risolvere questo problema.

Nella mia mente, per rimanere fedele ai numeri, il risultato "giusto" è quello che minimizza l'errore complessivo, definito da quanto arrotondare l'errore introdurrebbe rispetto al valore reale:

        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)

In caso di pareggio (3.33, 3.33, 3.33) può essere presa una decisione arbitraria (ad es. 3, 4, 3).


21
Supponiamo di avere 3.33, 3.33 e 3.33. Quale ne farai 4?
RobG

3
Esattamente. La domanda incarna una contraddizione in termini.
Marchese di Lorne,

4
È uno scenario molto comune nei rapporti: come visualizzare un "totale" di valori decimali che non corrisponde sempre alla somma dei valori visualizzati.
D Stanley,

1
Qual è il "diritto" risultato nel tuo esempio caso? Ciò potrebbe risolvere i disaccordi su quale sia la soluzione "migliore".
D Stanley,

Risposte:


35

Poiché nessuna delle risposte qui sembra risolverlo correttamente, ecco la mia versione semi-offuscata usando underscorejs :

function foo(l, target) {
    var off = target - _.reduce(l, function(acc, x) { return acc + Math.round(x) }, 0);
    return _.chain(l).
            sortBy(function(x) { return Math.round(x) - x }).
            map(function(x, i) { return Math.round(x) + (off > i) - (i >= (l.length + off)) }).
            value();
}

foo([13.626332, 47.989636, 9.596008, 28.788024], 100) // => [48, 29, 14, 9]
foo([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100) // => [17, 17, 17, 17, 16, 16]
foo([33.333, 33.333, 33.333], 100) // => [34, 33, 33]
foo([33.3, 33.3, 33.3, 0.1], 100) // => [34, 33, 33, 0]

6
Correggimi se sbaglio, ma questa non è un'implementazione dell'algoritmo proposta dalla mia risposta? (Non cancellare su underscorejs)
vvohra87

@VarunVohra scusa non l'ho notato fino ad ora, sì, sembra che il tuo algoritmo sia lo stesso :) non so perché il mio post sia la risposta accettata, il codice offuscato era solo per il lolz ...
yonilevy

@yonilevy Cancella il mio commento; Non mi rendevo conto che avrebbe dovuto restituire un elenco ordinato. Chiedo scusa!
Zack Burt,

2
Esiste un problema con questa funzione quando l'ultimo elemento è 0 e quelli precedenti si aggiungono a 100. Ad esempio [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. L'ultimo restituisce logicamente -1. Ho pensato molto rapidamente alla seguente soluzione, ma probabilmente c'è qualcosa di meglio: jsfiddle.net/0o75bw43/1
Cruclax,

1
@Cruclax mostra tutti 1 quando tutte le voci sono zero nella matrice di input
tony.0919

159

Esistono molti modi per fare proprio questo, purché non ti preoccupi della dipendenza dai dati decimali originali.

Il primo e forse il metodo più popolare sarebbe il metodo di residuo più grande

Che è fondamentalmente:

  1. Arrotondando tutto
  2. Ottenere la differenza in somma e 100
  3. Distribuire la differenza aggiungendo 1 agli articoli in ordine decrescente delle loro parti decimali

Nel tuo caso, andrebbe così:

13.626332%
47.989636%
 9.596008%
28.788024%

Se prendi le parti intere, ottieni

13
47
 9
28

che aggiunge fino a 97 e si desidera aggiungere altri tre. Ora, guardi le parti decimali, che sono

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

e prendi quelli più grandi fino a quando il totale raggiunge 100. Quindi otterrai:

14
48
 9
29

In alternativa, puoi semplicemente scegliere di mostrare una cifra decimale anziché i valori interi. Quindi i numeri sarebbero 48.3 e 23.9 ecc. Ciò eliminerebbe molto la varianza da 100.


5
Questa "colonna caratteristica" sul sito web dell'American Mathematical Society - Apportionment II: Apportionment Systems - descrive diversi metodi simili di 'ripartizione'.
Kenny Evitt,

1
Sembra quasi una copia e incolla della mia risposta qui stackoverflow.com/questions/5227215/… .
Sawa,

Nota che, contrariamente al tuo commento sulla risposta di @DStanley, nella tua risposta il 9.596008% è stato arrotondato al 9%, che rappresenta una differenza superiore allo 0,5%. Comunque una buona risposta, comunque.
Rolazaro Azeveires,

33

Probabilmente il modo "migliore" per farlo (citato poiché "migliore" è un termine soggettivo) è quello di mantenere un riscontro (non integrale) di dove ti trovi e arrotondare quel valore.

Quindi usalo insieme alla cronologia per capire quale valore dovrebbe essere usato. Ad esempio, usando i valori che hai dato:

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 ogni fase, non arrotondare il numero stesso. Al contrario, arrotondate il valore accumulato e elaborate il numero intero migliore che raggiunge quel valore dalla linea di base precedente: quella linea di base è il valore cumulativo (arrotondato) della riga precedente.

Questo funziona perché si sta non perdere le informazioni in ogni fase, ma piuttosto utilizzando le informazioni in modo più intelligente. I valori arrotondati 'corretti' sono nella colonna finale e puoi vedere che si sommano a 100.

Puoi vedere la differenza tra questo e arrotondare alla cieca ogni valore, nel terzo valore sopra. Mentre 9.596008normalmente verrebbe arrotondato a 10, l'accumulato si 71.211976arrotonderà correttamente a 71- questo significa che 9è necessario solo aggiungere alla precedente base di 62.


Questo funziona anche per una sequenza "problematica" come tre valori approssimativi , in cui uno di questi dovrebbe essere arrotondato per eccesso :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
Il secondo approccio risolve entrambi questi problemi. Il primo dà 26, 25, 26, 23, il secondo 1, 0, 1, 0, 1, 0, ....
paxdiablo,

Questo approccio funziona anche per arrotondare piccoli numeri in quanto impedisce il numero negativo sin dall'output
Jonty5817

19

L'obiettivo dell'arrotondamento è generare la minima quantità di errore. Quando arrotondi un singolo valore, quel processo è semplice e diretto e la maggior parte delle persone lo capisce facilmente. Quando si arrotondano più numeri contemporaneamente, il processo diventa più complicato: è necessario definire la modalità di combinazione degli errori, ovvero ciò che deve essere ridotto al minimo.

La risposta ben votata di Varun Vohra minimizza la somma degli errori assoluti ed è molto semplice da implementare. Tuttavia, ci sono casi limite che non gestisce: quale dovrebbe essere il risultato dell'arrotondamento 24.25, 23.25, 27.25, 25.25? Uno di questi deve essere arrotondato per eccesso anziché per difetto. Probabilmente sceglieresti arbitrariamente il primo o l'ultimo nell'elenco.

Forse è meglio usare l' errore relativo invece dell'assoluto dell'errore . L'arrotondamento da 23,25 a 24 lo modifica del 3,2%, mentre l'arrotondamento da 27,25 a 28 lo modifica solo del 2,8%. Ora c'è un chiaro vincitore.

È possibile modificarlo ulteriormente. Una tecnica comune è quella di quadrare ogni errore, in modo che gli errori grandi contino in modo sproporzionato più di quelli piccoli. Userei anche un divisore non lineare per ottenere l'errore relativo - non sembra giusto che un errore all'1% sia 99 volte più importante di un errore al 99%. Nel codice qui sotto ho usato la radice quadrata.

L'algoritmo completo è il seguente:

  1. Sommare le percentuali dopo averle arrotondate per difetto e sottrarre da 100. Ciò indica invece quante di queste percentuali devono essere arrotondate per eccesso.
  2. Genera due punteggi di errore per ogni percentuale, uno quando arrotondato per difetto e uno quando arrotondato per eccesso. Prendi la differenza tra i due.
  3. Ordinare le differenze di errore prodotte sopra.
  4. Per il numero di percentuali che devono essere arrotondate per eccesso, prendere un elemento dall'elenco ordinato e incrementare la percentuale arrotondata per difetto di 1.

Ad esempio, potresti avere più di una combinazione con la stessa somma di errori 33.3333333, 33.3333333, 33.3333333. Questo è inevitabile e il risultato sarà completamente arbitrario. Il codice che fornisco di seguito preferisce arrotondare i valori a sinistra.

Mettere tutto insieme in Python è simile a questo.

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]

Come puoi vedere con l'ultimo esempio, questo algoritmo è ancora in grado di fornire risultati non intuitivi. Anche se 89.0 non ha bisogno di arrotondamenti, è necessario arrotondare uno dei valori in quella lista; l'errore relativo più basso risulta dall'arrotondamento di quel valore elevato anziché dalle alternative molto più piccole.

Questa risposta inizialmente suggeriva di esaminare ogni possibile combinazione di arrotondamento per eccesso / per difetto, ma come sottolineato nei commenti un metodo più semplice funziona meglio. L'algoritmo e il codice riflettono questa semplificazione.


1
Non penso che sia necessario prendere in considerazione tutte le combinazioni: processo in ordine decrescente di calo dell'errore ponderato che va dal giro allo zero al giro all'infinito (praticamente semplicemente introducendo la ponderazione nelle risposte di Verun Vohras e yonilevy ("identiche")).
Greybeard,

@greybeard hai ragione, stavo pensando troppo a questo. Non ho potuto semplicemente ordinare l'errore poiché ci sono due errori per ogni valore, ma prendere la differenza ha risolto il problema. Ho aggiornato la risposta.
Mark Ransom,

Preferisco avere sempre lo 0% quando il numero effettivo è lo 0%. Quindi aggiungendo funziona if actual == 0: return 0alla error_gengrande.
Nikolay Baluk,

1
qual è il isclosemetodo all'inizio di round_to_100?
toto_tico,


7

NON sommare i numeri arrotondati. Avrai risultati imprecisi. Il totale potrebbe essere significativamente diverso a seconda del numero di termini e della distribuzione delle parti frazionarie.

Visualizza i numeri arrotondati ma somma i valori effettivi. A seconda di come stai presentando i numeri, il modo effettivo per farlo potrebbe variare. In questo modo ottieni

 14
 48
 10
 29
 __
100

In qualunque modo tu abbia delle discrepanze. Nel tuo esempio non c'è modo di mostrare numeri che si sommano fino a 100 senza "arrotondare" un valore nel modo sbagliato (il minimo errore cambierebbe da 9.596 a 9)

MODIFICARE

Devi scegliere tra una delle seguenti opzioni:

  1. Precisione degli articoli
  2. Precisione della somma (se si sommano i valori arrotondati)
  3. Coerenza tra gli elementi arrotondati e la somma arrotondata)

La maggior parte delle volte quando si ha a che fare con le percentuali n. 3 è l'opzione migliore perché è più ovvio quando il totale è pari al 101% rispetto a quando i singoli articoli non totalizzano a 100 e si mantengono precisi i singoli articoli. "L'arrotondamento" da 9.596 a 9 non è preciso secondo me.

Per spiegare questo a volte aggiungo una nota a piè di pagina che spiega che i singoli valori sono arrotondati e potrebbero non essere del 100%: chiunque capisca l'arrotondamento dovrebbe essere in grado di comprendere quella spiegazione.


6
Ciò non è molto utile poiché i valori stampati non aggiungono fino a 100. Lo scopo della domanda era impedire agli utenti di pensare che i valori non fossero corretti, cosa che in questo caso la maggior parte delle persone farebbe guardando e confrontando con il totale .
vvohra87,

@VarunVohra leggi la mia modifica, NON PUOI visualizzare i tuoi numeri in modo tale che aggiungano fino a 100 senza "arrotondare" uno di più di 0,5.
D Stanley,

1
@DStanley in realtà, escludendo un set in cui tutti i numeri sono timidi di 0,5, puoi. Controlla la mia risposta - LRM fa esattamente questo.
vvohra87,

3
@VarunVohra Nell'esempio originale LRM produrrà 14, 48, 9 e 29 che "arrotonderanno" da 9.596 a 9. Se stiamo allocando in base a numeri interi, LRM sarà il più preciso, ma sta ancora cambiando un risultato di più di una mezza unità.
D Stanley,

7

Ho scritto un aiuto per arrotondare la versione C #, l'algoritmo è lo stesso della risposta di Varun Vohra , spero che sia d'aiuto.

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

Passa il seguente test unitario:

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

Bello! mi ha dato una base di partenza per iniziare .. Enumerable non ha ForEach anche se credo
Jack0fshad0ws

4

Potresti provare a tenere traccia del tuo errore a causa dell'arrotondamento e quindi arrotondare contro il grano se l'errore accumulato è maggiore della parte frazionaria del numero corrente.

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

Non sono sicuro che funzionerebbe in generale, ma sembra funzionare in modo simile se l'ordine è invertito:

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

Sono sicuro che ci sono casi limite in cui ciò potrebbe guastarsi, ma qualsiasi approccio sarà almeno in qualche modo arbitrario poiché stai sostanzialmente modificando i tuoi dati di input.


2
Ragionieri e banchieri hanno usato una tecnica simile per centinaia di anni. "Trasporta il resto" da una riga all'altra. Inizia con 1/2 di un centesimo nel "carry". Aggiungere "carry" al primo valore e troncare. Ora l'importo che hai perso troncando, inseriscilo nel "carry". Fallo fino in fondo e i numeri arrotondati si sommeranno ogni volta esattamente al totale desiderato.
Jeff Grigg,

Carolyn Kay ha suggerito questa implementazione in Access VB 2007: <code> 'Dollari di rimborso rotondi usando il metodo "carry the restinder" ref1 = rsQry! [Refund Paid $$$] * rsQry! [Valore proprietà] / propValTot ref2 = ref1 + ref5 'Aggiungi il resto trasportato, zero per iniziare ref3 = ref2 * 100' Moltiplica per 100 in un numero intero ref4 = ref3 / 100 'Dividi per 100 in un numero decimale rsTbl! [Rimborso pagamento a pagamento] = ref4' Inserisci il " resto "numero arrotondato nella tabella ref5 = ref2 - ref4" Porta il nuovo resto </code>
Jeff Grigg

2

Una volta ho scritto uno strumento non circoscritto, per trovare la minima perturbazione a un insieme di numeri per abbinare un obiettivo. Era un problema diverso, ma in teoria si potrebbe usare un'idea simile qui. In questo caso, abbiamo una serie di scelte.

Pertanto, per il primo elemento, possiamo arrotondare fino a 14 o fino a 13. Il costo (in un senso di programmazione di numeri interi binari) di farlo è inferiore per il arrotondamento rispetto al arrotondamento verso il basso, perché il arrotondamento richiede sposta quel valore a una distanza maggiore. Allo stesso modo, possiamo arrotondare ogni numero su o giù, quindi ci sono un totale di 16 scelte tra cui scegliere.

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

Normalmente risolverei il problema generale in MATLAB, qui usando bintprog, uno strumento di programmazione di numeri interi binari, ma ci sono solo alcune scelte da testare, quindi è abbastanza facile con semplici loop per testare ognuna delle 16 alternative. Ad esempio, supponiamo di dover arrotondare questo set come:

 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

L'errore assoluto totale commesso è 1.25266. Può essere leggermente ridotto con il seguente arrotondamento alternativo:

 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 effetti, questa sarà la soluzione ottimale in termini di errore assoluto. Naturalmente, se ci fossero 20 termini, lo spazio di ricerca avrà dimensioni 2 ^ 20 = 1048576. Per 30 o 40 termini, quello spazio avrà dimensioni significative. In tal caso, è necessario utilizzare uno strumento in grado di cercare in modo efficiente lo spazio, magari utilizzando uno schema ramificato e associato.


Solo per riferimento futuro: l'algoritmo "resto più grande" deve ridurre al minimo l'errore assoluto totale in base alla metrica (vedere la risposta di @ varunvohra). La prova è semplice: supponiamo che non minimizzi l'errore. Quindi ci deve essere un insieme di valori che arrotonda per difetto che dovrebbe essere arrotondato per eccesso e viceversa (i due insiemi hanno le stesse dimensioni). Ma ogni valore che arrotonda è più lontano dal numero intero successivo rispetto a qualsiasi valore che arrotonda (e vv), quindi la nuova quantità di errore deve essere maggiore. QED. Tuttavia, non funziona per tutte le metriche di errore; sono necessari altri algoritmi.
rici,

2

Penso che quanto segue raggiungerà ciò che stai cercando

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]

Un'ultima cosa, ho eseguito la funzione utilizzando i numeri originariamente indicati nella domanda per confrontare l'output desiderato

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

Questo era diverso da quello che la domanda voleva => [48, 29, 14, 9]. Non riuscivo a capirlo fino a quando non ho visto il margine totale di errore

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

In sostanza, il risultato della mia funzione introduce effettivamente la minima quantità di errore.

Violino qui


è praticamente quello che avevo in mente, con la differenza che l'errore dovrebbe essere misurato rispetto al valore (arrotondare da 9,8 a 10 è un errore più grande di arrotondare da 19,8 a 20). Questo potrebbe essere fatto facilmente riflettendolo nel callback dell'ordinamento, comunque.
Poezn,

questo è sbagliato per [33.33, 33.33, 33.33, 0.1], restituisce [1, 33, 33, 33] piuttosto che il più preciso [34, 33, 33, 0]
yonilevy

@yonilevy Grazie per quello. Riparato ora.
Bruno,

non ancora, per [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] restituisce [15, 17, 17, 17, 17, 17] anziché [16, 16, 17, 17, 17, 17] - vedi mio risposta
yonilevy

2

Non sono sicuro di quale livello di precisione hai bisogno, ma quello che vorrei fare è semplicemente aggiungere 1 i primi nnumeri, nessendo il ceil della somma totale dei decimali. In questo caso 3, quindi aggiungerei 1 ai primi 3 elementi e pianificherei il resto. Naturalmente questo non è molto preciso, alcuni numeri potrebbero essere arrotondati per eccesso o per difetto quando non dovrebbe, ma funziona bene e si tradurrà sempre in 100%.

Quindi [ 13.626332, 47.989636, 9.596008, 28.788024 ]sarebbe [14, 48, 10, 28]perché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

Puoi sempre informare gli utenti che i numeri sono arrotondati e potrebbero non essere super precisi ...


1

Se lo stai arrotondando non c'è un buon modo per farlo esattamente lo stesso in tutti i casi.

Puoi prendere la parte decimale delle N percentuali che hai (nell'esempio che hai dato è 4).

Aggiungi le parti decimali. Nel tuo esempio hai un totale di parte frazionaria = 3.

Cementa i 3 numeri con le frazioni più alte e pianifica il resto.

(Ci scusiamo per le modifiche)


1
Sebbene ciò possa fornire numeri che si sommano a 100, potresti finire per trasformare 3.9 in 3 e 25.1 in 26.
RobG

no. 3,9 saranno 4 e 25,1 saranno 25. Ho detto di cementare i 3 numeri con le frazioni più alte non il valore più alto.
Arunlalam,

2
se ci sono troppe frazioni che terminano con .9, diciamo 9 valori del 9,9% e un valore di 10,9, un valore finirà per il 9%, 8 come 10% e uno come 11%.
Arunlalam,

1

Se devi davvero arrotondarli, ci sono già ottimi suggerimenti qui (resto più grande, errore relativo minore e così via).

C'è anche una buona ragione per non arrotondare (otterrai almeno un numero che "sembra migliore" ma è "sbagliato") e come risolverlo (avverti i tuoi lettori) ed è quello che faccio.

Vorrei aggiungere la parte numerica "sbagliata".

Supponiamo di avere tre eventi / entità / ... con alcune percentuali che approssimate come:

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

Successivamente i valori cambiano leggermente, in

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

La prima tabella presenta il problema già menzionato di avere un numero "sbagliato": 33.34 è più vicino a 33 che a 34.

Ma ora hai un errore più grande. Confrontando il giorno 2 con il giorno 1, il valore percentuale reale per A è aumentato dello 0,01%, ma l'approssimazione mostra una diminuzione dell'1%.

Questo è un errore qualitativo, probabilmente molto peggio dell'errore quantitativo iniziale.

Si potrebbe escogitare un'approssimazione per l'intero set ma, potrebbe essere necessario pubblicare i dati il ​​primo giorno, quindi non si saprà del secondo giorno. Quindi, a meno che tu non debba davvero avvicinarti, probabilmente è meglio di no.


chiunque sappia come creare tabelle migliori, per favore modifica o dimmi come / dove
Rolazaro Azeveires,

0

controlla se questo è valido o meno per quanto riguarda i miei casi di test sono in grado di farlo funzionare.

diciamo che il numero è k;

  1. ordina la percentuale in ordine decrescente.
  2. scorrere su ciascuna percentuale dall'ordine decrescente.
  3. calcolare la percentuale di k per la prima percentuale prendere Math.Ceil di output.
  4. successivo k = k-1
  5. scorrere fino a quando non viene consumata tutta la percentuale.

0

Ho implementato il metodo dalla risposta di Varun Vohra qui sia per gli elenchi che per i dicts.

import math
import numbers
import operator
import itertools


def round_list_percentages(number_list):
    """
    Takes a list where all values are numbers that add up to 100,
    and rounds them off to integers while still retaining a sum of 100.

    A total value sum that rounds to 100.00 with two decimals is acceptable.
    This ensures that all input where the values are calculated with [fraction]/[total]
    and the sum of all fractions equal the total, should pass.
    """
    # Check input
    if not all(isinstance(i, numbers.Number) for i in number_list):
        raise ValueError('All values of the list must be a number')

    # Generate a key for each value
    key_generator = itertools.count()
    value_dict = {next(key_generator): value for value in number_list}
    return round_dictionary_percentages(value_dict).values()


def round_dictionary_percentages(dictionary):
    """
    Takes a dictionary where all values are numbers that add up to 100,
    and rounds them off to integers while still retaining a sum of 100.

    A total value sum that rounds to 100.00 with two decimals is acceptable.
    This ensures that all input where the values are calculated with [fraction]/[total]
    and the sum of all fractions equal the total, should pass.
    """
    # Check input
    # Only allow numbers
    if not all(isinstance(i, numbers.Number) for i in dictionary.values()):
        raise ValueError('All values of the dictionary must be a number')
    # Make sure the sum is close enough to 100
    # Round value_sum to 2 decimals to avoid floating point representation errors
    value_sum = round(sum(dictionary.values()), 2)
    if not value_sum == 100:
        raise ValueError('The sum of the values must be 100')

    # Initial floored results
    # Does not add up to 100, so we need to add something
    result = {key: int(math.floor(value)) for key, value in dictionary.items()}

    # Remainders for each key
    result_remainders = {key: value % 1 for key, value in dictionary.items()}
    # Keys sorted by remainder (biggest first)
    sorted_keys = [key for key, value in sorted(result_remainders.items(), key=operator.itemgetter(1), reverse=True)]

    # Otherwise add missing values up to 100
    # One cycle is enough, since flooring removes a max value of < 1 per item,
    # i.e. this loop should always break before going through the whole list
    for key in sorted_keys:
        if sum(result.values()) == 100:
            break
        result[key] += 1

    # Return
    return result

0

Ecco un'implementazione Python più semplice della risposta @ 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

È necessario math, itertools, operator.


0

Per coloro che hanno le percentuali in una serie di panda, ecco la mia implementazione del metodo di residuo più grande (come nella risposta di Varun Vohra ), dove puoi persino selezionare i decimali a cui vuoi arrotondare.

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

Questo è il caso dell'arrotondamento del banchiere, noto anche come 'round half-even'. È supportato da BigDecimal. Il suo scopo è garantire che l'arrotondamento si bilanci, cioè non favorisca né la banca né il cliente.


5
NON assicura che l'arrotondamento si bilanci - riduce semplicemente la quantità di errore distribuendo il mezzo arrotondamento tra numeri pari e dispari. Esistono ancora scenari in cui l'arrotondamento dei banchieri produce risultati imprecisi.
D Stanley,

@DStanley concordato. Non ho detto diversamente. Ho dichiarato il suo scopo . Molto attentamente.
Marchese di Lorne,

2
Abbastanza giusto - ho frainteso quello che stavi cercando di dire. In entrambi i casi, non penso che risolva il problema poiché l'uso dell'arrotondamento dei banchieri non cambierà i risultati nell'esempio.
D Stanley,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.