2013-04-02 8 views
5

Ho una lista o una matrice di numeri decimali in Python. Ho bisogno di arrotondarli al 2 decimale più vicino in quanto si tratta di importi monetari. Ma ho bisogno che la somma complessiva sia mantenuta, cioè la somma dell'array originale arrotondato a 2 cifre decimali deve essere uguale alla somma degli elementi arrotondati dell'array.Arrotondare un elenco di numeri Python e mantenere la somma

Ecco il mio codice finora:

myOriginalList = [27226.94982, 193.0595233, 1764.3094, 12625.8607, 26714.67907, 18970.35388, 12725.41407, 23589.93271, 27948.40386, 23767.83261, 12449.81318] 
originalTotal = round(sum(myOriginalList), 2) 
# Answer = 187976.61 

# Using numpy 
myRoundedList = numpy.array(myOriginalList).round(2) 
# New Array = [ 27226.95 193.06 1764.31 12625.86 26714.68 18970.35 12725.41 23589.93 27948.4 23767.83 12449.81] 

newTotal = myRoundedList.sum() 
# Answer = 187976.59 

ho bisogno di un modo efficiente di modificare il mio nuovo array arrotondata tale che la somma è anche 187.976,61. La differenza di 2 pence deve essere applicata agli articoli 7 e 6 poiché presentano la maggiore differenza tra le voci arrotondate e le voci originali.

Qualche idea?

+2

Probabilmente non dovrebbe essere utilizzando i numeri in virgola mobile di rappresentare importi monetari. Ci sono molti numeri, come '0.10', che' float' non può rappresentare esattamente. – NPE

+0

Cosa c'è di sbagliato nella tua soluzione numpy? Se ho capito bene, questa è la risposta che stai cercando ... – mgilson

+0

@NPE: Non è questo il motivo? Voglio dire, qualsiasi incertezza nel numero 0.1 dovrebbe essere paragonabile alla precisione della macchina, giusto? – BenDundee

risposta

1

con tutte le riserve sull'utilizzo di numeri in virgola mobile:

delta_pence = int(np.rint((originalTotal - np.sum(myRoundedList))*100)) 
if delta_pence > 0: 
    idx = np.argsort(myOriginalList - myRoundedList)[-delta_pence:] 
    myRoundedList[idx] += 0.01 
elif delta_pence < 0: 
    idx = np.argsort(myOriginalList - myRoundedList)[:delta_pence] 
    myRoundedList[idx] -= 0.01 

>>> myRoundedList.sum() 
187976.60999999999 
+0

Nel tuo esempio, quando hai '8 * [0.001] + [0.009]', ottieni '8 * [0.0] + [0.2]' che è lo stesso tipo di errore, che hai nominato nella mia soluzione. L'arrotondamento è arrotondato. – palooh

+0

@palooh Quello era un bug, grazie per averlo scoperto! Se lo esegui ora, il risultato è '7 * [0.00] + 2 * [0.01]', in modo tale che vengano aggiunti i pence extra riducendo al minimo gli errori di arrotondamento individuali. – Jaime

1

Prima di tutto, non si dovrebbe usare galleggianti per la memorizzazione di denaro (uso decimali invece). Ma di seguito fornisco una soluzione abbastanza generica: è necessario archiviare, accumulare e utilizzare la somma delle differenze nell'arrotondamento. Alcuni verbose (e non molto divinatorio esempio ;-) con i numeri:

# define your accuracy 
decimal_positions = 2 

numbers = [27226.94982, 193.0595233, 1764.3094, 12625.8607, 26714.67907, 18970.35388, 12725.41407, 23589.93271, 27948.40386, 23767.83261, 12449.81318] 
print round(sum(numbers),decimal_positions) 
>>> 187976.61 

new_numbers = list() 
rest = 0.0 
for n in numbers: 
    new_n = round(n + rest,decimal_positions) 
    rest += n - new_n 
    new_numbers.append(new_n) 

print sum(new_numbers) 
>>> 187976.61 
+0

Se i primi due numeri sono 'x.0049' e' y.0001', il metodo di arrotondamento li convertirà in 'x.00' e' y.01', che non sembrano la cosa migliore da fare. – Jaime

+0

Bene, puoi sempre ordinare la lista per 'abs (n% 0.01)' discendente, ma considerando il _rounding_ forse non è così importante ... – palooh

4

Il primo passo è quello di calcolare l'errore tra il risultato desiderato e la somma effettiva:

>>> error = originalTotal - sum(myRoundedList) 
>>> error 
0.01999999996041879 

Questo può essere sia positivo o negativo. Poiché ogni articolo in myRoundedList è compreso tra 0,005 del valore effettivo, questo errore sarà inferiore a 0,01 per elemento dell'array originale. Si può semplicemente dividere per 0,01 e rotondo per ottenere il numero di elementi che deve essere regolato:

>>> n = int(round(error/0.01)) 
>>> n 
2 

Ora tutto quello che resta è quello di selezionare gli elementi che devono essere adeguati. I risultati ottimali derivano dall'aggiustare quei valori che erano più vicini al confine in primo luogo. È possibile trovare quelli ordinando per la differenza tra il valore originale e il valore arrotondato.

>>> myNewList = myRoundedList[:] 
>>> for _,i in sorted(((myOriginalList[i] - myRoundedList[i], i) for i in range(len(myOriginalList))), reverse=n>0)[:abs(n)]: 
    myNewList[i] += math.copysign(0.01, n) 

>>> myRoundedList 
[27226.95, 193.06, 1764.31, 12625.86, 26714.68, 18970.35, 12725.41, 23589.93, 27948.4, 23767.83, 12449.81] 
>>> myNewList 
[27226.95, 193.06, 1764.31, 12625.86, 26714.68, 18970.359999999997, 12725.42, 23589.93, 27948.4, 23767.83, 12449.81] 
>>> sum(myNewList) 
187976.61 
0

Se si dispone di una lunga lista, i metodi di cui sopra sono inefficienti perché sono O (n * log (n)) (ordinamento di n elementi). Se le probabilità sono alte, dovresti cambiare solo a pochi (o uno) di questi indici, potresti usare un heap (o un min/max se c'è un solo posto da cambiare).

Non sono molto codificatore di Python, ma ecco una soluzione considerando quanto sopra (ma non considerando l'inesattezza della rappresentazione in virgola mobile (già menzionata da altri)).

import math 
import heapq 

def roundtosum(l, r): 
    q = 10**(-r) 
    d = int((round(sum(l),r) - sum([ round(x, r) for x in l ])) * (10**r)) 
    if d == 0: 
     return l 
    elif d in [ -1, 1 ]: 
     c, _ = max(enumerate(l), key=lambda x: math.copysign(1,d) * math.fmod(x[1] - 0.5*q, q)) 
     return [ round(x, r) + q * math.copysign(1,d) if i == c else round(x, r) for (i, x) in enumerate(l) ] 
    else: 
     c = [ i for i, _ in heapq.nlargest(abs(d), enumerate(l), key=lambda x: math.copysign(1,d) * math.fmod(x[1] - 0.5*q, q)) ] 
     return [ round(x, r) + q * math.copysign(1,d) if i in c else round(x, r) for (i, x) in enumerate(l) ] 

d è la differenza numerica tra la somma arrotondata e la somma di giri, questo ci dice quanti posti dovremmo cambiare l'arrotondamento. Se lo d è zero, chiaramente non abbiamo nulla da fare. Se d è 1 o -1 il posto migliore può essere trovato facilmente con min o max. Per un numero arbitrario, possiamo usare heapq.nlargest per trovare i migliori posti D=abs(d).

Quindi perché c'è un max, se lo fosse nlargest ?!Perché min e max sono implementati in modo molto più efficiente di quello.

In questo modo, l'algoritmo è O (n + D * log (n)).

Nota: con un heap, è possibile creare anche un algoritmo O (n + D^2 * log (D)) perché gli elementi principali D devono trovarsi nei primi livelli D dell'heap ed è possibile ordinare quella lista in O (D^2 * log (D)) passi. Se n è enorme e D è molto piccolo questo può significare molto.

(diritti di riesame riservati (perché è dopo la mezzanotte).)