mboost-dp1

En klassiker omkring penge


Gå til bund
Gravatar #1 - arne_v
12. feb. 2024 17:49
Der var lige en på LinkedIn som ville bruge double til at gemme et beløb i.

Tsk tsk.

Her er en lang smøre omkring hvorfor det er en rigtig dårlig ide.

Eksemplet er i Java (og Groovy) men problemstillingen er relevant i alle sprog.

I C# er det float/double vs decimal. I Python er det "tal" vs Decimal. Etc..

For a very quick primer.

Let us say that you borrow 100001.00 dollars and pay back 1000.01 dollars every month for 100 months.

(we are not doing anything with interest here)

Any 7th grader with a hand calculator can tell that the loan is paid back after the 100 months.

Java and float:


public class M1 {
public static void main(String[] args) {
float loanamt = 100001.00f;
float repayment = 0.00f;
for(int i = 0; i < 100; i++) {
repayment += 1000.01f;
}
System.out.printf("You borrowed %.2f\n", loanamt);
System.out.printf("You repaid %.2f\n", repayment);
if(repayment < loanamt) {
System.out.println("You still owe " + (loanamt - repayment));
} else if(repayment > loanamt) {
System.out.println("You will get a refund of " + (repayment - loanamt));
} else {
System.out.println("All good");
}
}
}


You borrowed 100001.00
You repaid 100000.98
You still owe 0.0234375

Where did those 2/2.3 cent go????

Java and double:


public class M2 {
public static void main(String[] args) {
double loanamt = 100001.00;
double repayment = 0.00;
for(int i = 0; i < 100; i++) {
repayment += 1000.01;
}
System.out.printf("You borrowed %.2f\n", loanamt);
System.out.printf("You repaid %.2f\n", repayment);
if(repayment < loanamt) {
System.out.println("You still owe " + (loanamt - repayment));
} else if(repayment > loanamt) {
System.out.println("You will get a refund of " + (repayment - loanamt));
} else {
System.out.println("All good");
}
}
}


You borrowed 100001.00
You repaid 100001.00
You still owe 1.3096723705530167E-10

Where did that crazy rounding error come from????

Java and BigDecimal:


import java.math.BigDecimal;

public class M3 {
public static void main(String[] args) {
BigDecimal loanamt = new BigDecimal("100001.00");
BigDecimal repayment = new BigDecimal("0.00");
for(int i = 0; i < 100; i++) {
repayment = repayment.add(new BigDecimal("1000.01"));
}
System.out.printf("You borrowed %.2f\n", loanamt);
System.out.printf("You repaid %.2f\n", repayment);
if(repayment.compareTo(loanamt) < 0) {
System.out.println("You still owe " + (loanamt.subtract(repayment)));
} else if(repayment.compareTo(loanamt) > 0) {
System.out.println("You will get a refund of " + (repayment.subtract(loanamt)));
} else {
System.out.println("All good");
}
}
}


You borrowed 100001.00
You repaid 100001.00
All good

We finally got something as good as the hand calculator!

Groovy (which use BigDecimal under the hood):


loanamt = 100001.00
repayment = 0.00
for(int i = 0; i < 100; i++) {
repayment += 1000.01
}
println("You borrowed ${loanamt}")
println("You repaid ${repayment}")
if(repayment < loanamt) {
println("You still owe ${loanamt - repayment}")
} else if(repayment > loanamt) {
println("You will get a refund of ${repayment - loanamt}")
} else {
println("All good")
}


You borrowed 100001.00
You repaid 100001.00
All good

Gravatar #2 - larsp
13. feb. 2024 08:03
arne_v (1) skrev:
Der var lige en på LinkedIn som ville bruge double til at gemme et beløb i.

Tsk tsk.

Oops.

I Python er int iøvrigt arbitrary size, dvs. der bruges big-int hvis nødvendigt. Jeg ved ikke hvor dygtigt runtime miljøet er til at holde sig til native ints når bigints ikke er nødvendige, for a speede ting op.

Så i Python kan man håndtere beløb i ints ved at tælle øre/cents. Men decimal er nok bedre.

Med floats i Python:

>>> loanamt = 100001.00
>>> repayment = 0.00
>>> for i in range(100):
... repayment += 1000.01
...
>>> print(f"You borrowed ${loanamt}")
You borrowed $100001.0
>>> print(f"You repaid ${repayment}")
You repaid $100000.99999999987


Med ints

>>> loanamt = 10000100
>>> repayment = 0
>>> for i in range(100):
... repayment += 100001
...
>>> print(f"You borrowed ${loanamt/100.0}")
You borrowed $100001.0
>>> print(f"You repaid ${repayment/100.0}")
You repaid $100001.0



Gravatar #3 - arne_v
13. feb. 2024 15:47
#2

Der er 2 fundamentalt forskellige måder at implementere decimal på:

A) software implementering - i et objekt orienteret sprog vil det typisk være en klasse med en stor integer med værdi og en lille integer med antal decimaler

B) hardware implementering - enten IEEE 754 decimal32/decimal64/decimal128 (vistnok kun implementeret i IBM mainframe og AIX Power) eller BCD (implementeret i de fleste CPU arkitekturer tiltænkt business processing designet før 1980).

Hvis der er en decimal implementering så vil det være bedre at bruge den end selv at bruger en stor integer med værdi og en viden om antal decimaler, fordi gør det selv løsningen er jo reelt en ikke genbrugelig virtuel decimal klasse.
Gravatar #4 - arne_v
13. feb. 2024 16:08
#3

En Java BigDecimal i standard implementering er lige ud af landevejen - forsimplet:

public class BigDecimal {
private BigInteger intVal;
private int precision;
...
}

Men JVM implementeringer kan lave noget andet hvis det giver mening.

Det forlyder (jeg har ikke selv testet!) at en:

new BigDecimal("123.45", MathContext.DECIMAL64)

på IBM mainframe ikke gemmes som ovenfor men som en CPU understøttet IEEE 754 decimal64.

Smart.

Ikke helt så smart er at BigDecimal er noget bøvlede i Java p.g.a. den manglende operator overload mulighed.

Derfor Groovy eksemplet.

Og Groovy er ret cool med hensyn til det her problem.

0.01f er en float
0.01d er en double
0.01 er en BigDecimal !!

Jeg synes at det giver mening at folk der ikke eksplicit sætter f eller d på får en BigDecimal. Det er mest sandsynligt at de ikke-skrappe udviklere som ikke kender til f og d vil blive mindst overrasket med BigDecimal.
Gravatar #5 - arne_v
14. feb. 2024 02:23
#FP

Der er mange udviklere som fejlagtigt tror at floating point svarer til matematikkens reelle tal.

Det er meget forkert.

Der er masser af tilfælde hvor floating point opfører sig meget forskelligt fra reelle tal.

Et af de mest kendte eksempler er formler for bregning af varians.

Python kode:


def mean(a):
sum = 0.0
for v in a:
sum = sum + v
return sum / len(a)

def var1(a):
u = mean(a)
sum = 0.0
for v in a:
sum = sum + (v - u)**2
return sum / len(a)

def var2(a):
sum1 = 0.0
sum2 = 0.0
for v in a:
sum1 = sum1 + v**2
sum2 = sum2 + v
return sum1 / len(a) - (sum2 / len(a))**2


De matematisk formler bagved var1 og var2 funktionerne er helt ekvivalente og korrekte. Med reelle tal giver funktionerne var1 og var2 altid samme resultat.

Igennem tiden har mange begyndere (begyndere i numerisk analyse - ikke nødvendigvis begyndere i programmering) valgt at bruge var2 funktionen, fordi den kun løber array igennem 1 gang, mens var1 løber array igennem 2 gange - først for mean og så for selve var.

Men var2 er en katastrofe med floating point.


def mean(a):
sum = 0.0
for v in a:
sum = sum + v
return sum / len(a)

def var1(a):
u = mean(a)
sum = 0.0
for v in a:
sum = sum + (v - u)**2
return sum / len(a)

def var2(a):
sum1 = 0.0
sum2 = 0.0
for v in a:
sum1 = sum1 + v**2
sum2 = sum2 + v
return sum1 / len(a) - (sum2 / len(a))**2

n = 0
while True:
abase = 10**n
if (abase + 0.1) == abase:
break;
a = [ abase + 0.1, abase + 0.2, abase + 0.3 ]
v1 = var1(a)
v2 = var2(a)
print(f"{a} : var1={v1} var2={v2}")
n = n + 1


output:

[1.1, 1.2, 1.3] : var1=0.006666666666666664 var2=0.006666666666667043
[10.1, 10.2, 10.3] : var1=0.006666666666666737 var2=0.006666666666674814
[100.1, 100.2, 100.3] : var1=0.006666666666666856 var2=0.006666666666205856
[1000.1, 1000.2, 1000.3] : var1=0.0066666666666621195 var2=0.006666666246019304
[10000.1, 10000.2, 10000.3] : var1=0.006666666666593907 var2=0.006666645407676697
[100000.1, 100000.2, 100000.3] : var1=0.006666666666472642 var2=0.0066680908203125
[1000000.1, 1000000.2, 1000000.3] : var1=0.00666666667132328 var2=0.0068359375
[10000000.1, 10000000.2, 10000000.3] : var1=0.006666666741172473 var2=0.015625
[100000000.1, 100000000.2, 100000000.3] : var1=0.006666666865348854 var2=0.0
[1000000000.1, 1000000000.2, 1000000000.3] : var1=0.0066666618983219905 var2=-384.0
[10000000000.1, 10000000000.2, 10000000000.3] : var1=0.006666590373545962 var2=-16384.0
[100000000000.1, 100000000000.2, 100000000000.3] : var1=0.006666463256503145 var2=2097152.0
[1000000000000.1, 1000000000000.2, 1000000000000.3] : var1=0.00667157769203186 var2=268435456.0
[10000000000000.1, 10000000000000.2, 10000000000000.3] : var1=0.006745656331380208 var2=0.0
[100000000000000.1, 100000000000000.2, 100000000000000.3] : var1=0.006917317708333333 var2=-2199023255552.0
[1000000000000000.1, 1000000000000000.2, 1000000000000000.2] : var1=0.03125 var2=-422212465065984.0
Gravatar #6 - larsp
14. feb. 2024 07:20
arne_v (5) skrev:
... valgt at bruge var2 funktionen, fordi den kun løber array igennem 1 gang, mens var1 løber array igennem 2 gange - først for mean og så for selve var.

Heh, den er jeg skyldig i, for ganske nylig, i forbindelse med noget billedbehandling. Jeg ville vide hvor meget pixels varierer i et område og greb fat i en single pass beregning, uden at tænke nærmere over det. Jeg skal da lige se om min beregning faktisk går i skoven. Resultatet bliver dog kun brugt til noget så harmløst som at styre hvor kraftigt et watermark skal brændes ind i et billede.

gray_sum = 0.0
gray_sqr_sum = 0.0
num = 0

# Sum up all the RGB values in the cropped area
for pixel in cropped_area.getdata():
gray = int(0.21 * pixel[0] + 0.72 * pixel[1] + 0.07 * pixel[2])
gray_sum += gray
gray_sqr_sum += gray*gray
num += 1

mean = gray_sum / num
var = (gray_sqr_sum - gray_sum ** 2 / num) / num
return (round(mean), var)

Jeg valgte single pass fordi det gjorde ondt på mig at skulle beregne grå-værdien to gange kan jeg huske nu. Man kunne selvfølgelig have gemt alle de grå værdier i et midlertidigt array.
Gravatar #7 - larsp
14. feb. 2024 08:06
Fixet kode:

gray_pixels = [0.21 * pixel[0] + 0.72 * pixel[1] + 0.07 * pixel[2] for pixel in cropped_area.getdata()]

mean = sum(gray_pixels) / len(gray_pixels)
var = sum((x - mean) ** 2 for x in gray_pixels) / len(gray_pixels)
return (round(mean), var)

Der var IKKE forskel på resultatet i denne use case. Her er begge algoritmer, (single pass algoritmen rettet så den ikke tager int() om gray værdien)

Pixel count: 107822, mean: 5.4, single pass var: 1.451724, two pass var: 1.451724
Pixel count: 142202, mean: 62.6, single pass var: 723.687870, two pass var: 723.687870
Pixel count: 89628, mean: 43.8, single pass var: 3.684630, two pass var: 3.684630
Pixel count: 67670, mean: 10.7, single pass var: 74.563583, two pass var: 74.563583
Pixel count: 52746, mean: 28.9, single pass var: 628.958476, two pass var: 628.958476
Pixel count: 55176, mean: 17.1, single pass var: 56.189237, two pass var: 56.189237
Pixel count: 97566, mean: 82.9, single pass var: 14.028422, two pass var: 14.028422
Pixel count: 105043, mean: 111.3, single pass var: 5365.773062, two pass var: 5365.773062
Pixel count: 20646, mean: 107.3, single pass var: 1379.063676, two pass var: 1379.063676
Gravatar #8 - larsp
14. feb. 2024 08:20
PS. Og der tages sqrt() så det er std. afvigelsen der bruges til at styre opacity for watermark. Mean bruges også. Det virker ret godt. Gad vide om der er en velkendt algoritme til at sætte et watermark ind så det er diskret og stadig lige kan læses på forskellige baggrunde. Jeg droppede litteratursøgningen og lavede min egen ...
Gravatar #9 - arne_v
14. feb. 2024 14:18
#7

Om der er et problem eller ej afhænger af den relative forskel mellem værdi og værdis afvigelse fra mean i forhold til præcision i den brugte floating point.

Gravatar #10 - arne_v
14. feb. 2024 14:21
#9


import math

def mean(a):
sum = 0.0
for v in a:
sum = sum + v
return sum / len(a)

def var1(a):
u = mean(a)
sum = 0.0
for v in a:
sum = sum + (v - u)**2
return sum / len(a)

DOUBLE_DECIMAL_DIGITS = 15.95

def status2(a):
u = mean(a)
for v in a:
if (abs(v - u) > 0.0000000001) and (math.log10(abs(v) / abs(v - u)) > (DOUBLE_DECIMAL_DIGITS / 2)):
return "Problem"
return "OK"

def var2(a):
sum1 = 0.0
sum2 = 0.0
for v in a:
sum1 = sum1 + v**2
sum2 = sum2 + v
return sum1 / len(a) - (sum2 / len(a))**2

n = 0
while True:
abase = 10**n
if (abase + 0.1) == abase:
break;
a = [ abase + 0.1, abase + 0.2, abase + 0.3 ]
v1 = var1(a)
s2 = status2(a)
v2 = var2(a)
print(f"{a} : var1={v1} status2={s2} var2={v2}")
n = n + 1


[1.1, 1.2, 1.3] : var1=0.006666666666666664 status2=OK var2=0.006666666666667043
[10.1, 10.2, 10.3] : var1=0.006666666666666737 status2=OK var2=0.006666666666674814
[100.1, 100.2, 100.3] : var1=0.006666666666666856 status2=OK var2=0.006666666666205856
[1000.1, 1000.2, 1000.3] : var1=0.0066666666666621195 status2=OK var2=0.006666666246019304
[10000.1, 10000.2, 10000.3] : var1=0.006666666666593907 status2=OK var2=0.006666645407676697
[100000.1, 100000.2, 100000.3] : var1=0.006666666666472642 status2=OK var2=0.0066680908203125
[1000000.1, 1000000.2, 1000000.3] : var1=0.00666666667132328 status2=Problem var2=0.0068359375
[10000000.1, 10000000.2, 10000000.3] : var1=0.006666666741172473 status2=Problem var2=0.015625
[100000000.1, 100000000.2, 100000000.3] : var1=0.006666666865348854 status2=Problem var2=0.0
[1000000000.1, 1000000000.2, 1000000000.3] : var1=0.0066666618983219905 status2=Problem var2=-384.0
[10000000000.1, 10000000000.2, 10000000000.3] : var1=0.006666590373545962 status2=Problem var2=-16384.0
[100000000000.1, 100000000000.2, 100000000000.3] : var1=0.006666463256503145 status2=Problem var2=2097152.0
[1000000000000.1, 1000000000000.2, 1000000000000.3] : var1=0.00667157769203186 status2=Problem var2=268435456.0
[10000000000000.1, 10000000000000.2, 10000000000000.3] : var1=0.006745656331380208 status2=Problem var2=0.0
[100000000000000.1, 100000000000000.2, 100000000000000.3] : var1=0.006917317708333333 status2=Problem var2=-2199023255552.0
[1000000000000000.1, 1000000000000000.2, 1000000000000000.2] : var1=0.03125 status2=Problem var2=-422212465065984.0


Gå til top

Opret dig som bruger i dag

Det er gratis, og du binder dig ikke til noget.

Når du er oprettet som bruger, får du adgang til en lang række af sidens andre muligheder, såsom at udforme siden efter eget ønske og deltage i diskussionerne.

Opret Bruger Login