Hi All,
I have been writing a lot of problems using the Currency context this semester. By default, that context has an absolute tolerance of 0.005 (so answers have to be within a penny). One issue that has come up is that since perl (and many other languages) uses "banker's rounding" an answer like 35.375 would be rounded to 35.37 rather than 35.38. Since most of my students have not been exposed to this type of rounding, I always set the tolerance to 0.01 (absolute) or more.
But I suppose an argument could be made for teaching students to use this type of rounding. Wikipedia claims banker's rounding is mandated by law in some contexts. Is that true? Do you know of any references on how intermediate calculations are "supposed" to be rounded in finance or accounting? Of course, this is probably relevant in other contexts, but it seems to me to come up more often when working with the absolute tolerance in the Currency context.
Thanks for any comments,
Jason
Jason:
Note that under the default settings for the Currency context, student's can't enter more than two decimal places, so they can't enter $35.375 as an answer, so I'm assuming that you mean the correct answer is listed as $35.375 and is obtained via something like Currency(35.375). In that case, the correct answer will be displayed as $35.37 and the student must enter $35.37, not $35.38 (as you point out). By setting the tolerance to .01, you do make both $35.37 and $35.38 be accepted as correct.
But this may have unwanted consequences in other situations; for example, if the correct answer is $35.37666667 then a tolerance of .01 will allow the incorrect answer of $35.37 to be marked correct. Indeed, a tolerance of .01 will ALWAYS allow two answers to be marked correct, except for the case where the correct answer is exact (i.e., has no more than two decimal values). But, of course, in that situation, .005 also works correctly. A tolerance of .005 makes a range of width .01 centered at the correct answer, and there should be only one monetary answer in that range; for a tolerance of .01 you get a range of width .02, and there are almost always two monetary values in that range.
Finally, the issue of rounding is more complicated than you may realize, because it is not the decimal representation that is being rounded, but rather the binary one, and while the decimal one may be exact, the binary one may not. For example, 1/10 (one tenth) can be correctly represented as .1 in decimal, but it is a repeating "decimal" in binary. When it is stored in a computer (as a floating point real number using only a finite number of binary digits) it can not be stored precisely, and so the internal representation is not quite accurate. Similarly, numbers like 1.235 and 1.225 both are repeating "decimals" in binary expansion, even though they have a finite number of decimal digits, and are represented "exactly" as I have typed them above.
When Perl has to round these, it is rounding an inexact value, and what happens with the "rounding of 5" case is not entirely clear. For example, 1.235 rounds to 1.23, but so does 1.225.
Here are some other values:
1.205 => 1.21
1.215 => 1.22
1.225 => 1.23
1.235 => 1.23
1.245 => 1.25
1.255 => 1.25
1.265 => 1.27
1.275 => 1.27
1.285 => 1.28
1.295 => 1.29
1.305 => 1.30
and so on. There is no readily apparent pattern, and I suspect you would have to carefully view the binary representations in order to make sense of this (and it would be dependent on the number of digits stored on the computer).
There is no obvious solution to this. If you want .5 always to round up, you could add a small amount of "fudge" to your correct answer (maybe something like .000001) that is well below the differences you are having to distinguish, and then round that by hand using spf($n,"%.2f") before creating your monetary value. Something like Compute(spf($n,'$%.2f')) might do.
I haven't really experimented to see what the consequences if this would be, but such fine control is notoriously difficult to achieve. I think your best bet is to guarantee that you give correct answers to exactly two decimal places and handle the rounding yourself. Perhaps the Currency context should provide a service routine that rounds the given value to exactly 2 places always rounding 5's up. There is also the "statisticians rounding" which rounds 5's depending on the digit before the 5 (evens go one way and odds the other) to avoid a bias in rounding all one way. It looks kind of like Perl is trying to do that, but not quite making it.
Davide
Note that under the default settings for the Currency context, student's can't enter more than two decimal places, so they can't enter $35.375 as an answer, so I'm assuming that you mean the correct answer is listed as $35.375 and is obtained via something like Currency(35.375). In that case, the correct answer will be displayed as $35.37 and the student must enter $35.37, not $35.38 (as you point out). By setting the tolerance to .01, you do make both $35.37 and $35.38 be accepted as correct.
But this may have unwanted consequences in other situations; for example, if the correct answer is $35.37666667 then a tolerance of .01 will allow the incorrect answer of $35.37 to be marked correct. Indeed, a tolerance of .01 will ALWAYS allow two answers to be marked correct, except for the case where the correct answer is exact (i.e., has no more than two decimal values). But, of course, in that situation, .005 also works correctly. A tolerance of .005 makes a range of width .01 centered at the correct answer, and there should be only one monetary answer in that range; for a tolerance of .01 you get a range of width .02, and there are almost always two monetary values in that range.
Finally, the issue of rounding is more complicated than you may realize, because it is not the decimal representation that is being rounded, but rather the binary one, and while the decimal one may be exact, the binary one may not. For example, 1/10 (one tenth) can be correctly represented as .1 in decimal, but it is a repeating "decimal" in binary. When it is stored in a computer (as a floating point real number using only a finite number of binary digits) it can not be stored precisely, and so the internal representation is not quite accurate. Similarly, numbers like 1.235 and 1.225 both are repeating "decimals" in binary expansion, even though they have a finite number of decimal digits, and are represented "exactly" as I have typed them above.
When Perl has to round these, it is rounding an inexact value, and what happens with the "rounding of 5" case is not entirely clear. For example, 1.235 rounds to 1.23, but so does 1.225.
Here are some other values:
1.205 => 1.21
1.215 => 1.22
1.225 => 1.23
1.235 => 1.23
1.245 => 1.25
1.255 => 1.25
1.265 => 1.27
1.275 => 1.27
1.285 => 1.28
1.295 => 1.29
1.305 => 1.30
and so on. There is no readily apparent pattern, and I suspect you would have to carefully view the binary representations in order to make sense of this (and it would be dependent on the number of digits stored on the computer).
There is no obvious solution to this. If you want .5 always to round up, you could add a small amount of "fudge" to your correct answer (maybe something like .000001) that is well below the differences you are having to distinguish, and then round that by hand using spf($n,"%.2f") before creating your monetary value. Something like Compute(spf($n,'$%.2f')) might do.
I haven't really experimented to see what the consequences if this would be, but such fine control is notoriously difficult to achieve. I think your best bet is to guarantee that you give correct answers to exactly two decimal places and handle the rounding yourself. Perhaps the Currency context should provide a service routine that rounds the given value to exactly 2 places always rounding 5's up. There is also the "statisticians rounding" which rounds 5's depending on the digit before the 5 (evens go one way and odds the other) to avoid a bias in rounding all one way. It looks kind of like Perl is trying to do that, but not quite making it.
Davide
Davide,
Thanks for your message. The "Banker's method" I think is the same as the statistician's rounding. This article claims that Perl uses that method for rounding floating point numbers with sprintf (and printf). But from your message it seems that's not quite true. I like your suggestion of a fudge factor, at least for getting it to always round as students expect it to in cases where the exact answer has more than two decimal places. (However, this does deepen the mystery as to what is done in the financial world -- or maybe it confirms the horrible truth :) )
See you,
Jason
Thanks for your message. The "Banker's method" I think is the same as the statistician's rounding. This article claims that Perl uses that method for rounding floating point numbers with sprintf (and printf). But from your message it seems that's not quite true. I like your suggestion of a fudge factor, at least for getting it to always round as students expect it to in cases where the exact answer has more than two decimal places. (However, this does deepen the mystery as to what is done in the financial world -- or maybe it confirms the horrible truth :) )
See you,
Jason
I agree that that's probably what Perl is trying to do (though it looks more like round-to-odd than round-to-even), but I think the problem really is that the binary representations are not actually equal to the decimal ones, so what you think of as 1.235 is not actually that but something slightly less than that (on the order of 1E-16) and so is not "really" 5 followed by 0's but actually more like 1.2349999999999999999999 and so rounds to 1.23.
In fact, I just did the following: 1.235 - 1.23 and got 0.00499999999999989, not .5, so that confirms it. Similarly, 1.225 - 1.22 yields 0.00500000000000012, so 1.225 rounds up to 1.23. These small errors at the tail of the decimal are due to the fact that the infinite binary expansion has been truncated (or more likely, rounded). For example, the binary expansion for .1 has repeating 1100 digits, giving ...110011001100110011..., so if the cut-off occurs before a zero, as in ..1100110|0110011..., then the resulting binary number is slightly too small, and we have the case like the one in 1.235 (it rounds down because it is slightly below 1.235). On the other hand, if it cuts off before a 1, as in ..11001100|110011..., then the number stored might be rounded up to ..11001101, which is slightly too big. This is like the 1.225, which rounds up, because the internal representation is just a little over 1.225 (by 1.2*10^(-16)).
Numbers stored as IEEE double-precision floating point reals (like the Perl I'm running uses) have 54 binary digits for the mantissa (the constant in front of the 10^(-16)) and 11 for the exponent (yes, that's 65, which I could also explain). So the error should be in the 54th binary "decimal" place, so the error should be on the order or 2^(-54). As a rough estimate of how big that is, we can use the fact that 2^10=1024 is about 1000=10^3, so
2^(-54) = 2^6*2^(-60) = 64*2^(10*-6) = 64*(2^10)^(-6) = 64*(10^3)^(-6) = 64*10^(-18) = 6.4*10^(-17)
which is just about exactly the sized error we see (and since 1024 is a little bigger than 1000, our estimate is a little smaller than the actual error).
Anyway, the reason the rounding doesn't work as expected is that the numbers actually being rounded are in binary not decimal, and so are not exactly what you think they are. The reason you don't see these error when you print the numbers is that the numbers are printed using one or two fewer (decimal) digits than the binary representation actually allows, and the result is rounded for printing, so 1.234999999999999999 becomes 1.235 even though internally it isn't exactly that.
The upshot is, you can't trust printed numbers to display the actual number stored, and the actual number stored may not be the one you entered.
Davide
PS, I don't even want to THINK about what banks do.
In fact, I just did the following: 1.235 - 1.23 and got 0.00499999999999989, not .5, so that confirms it. Similarly, 1.225 - 1.22 yields 0.00500000000000012, so 1.225 rounds up to 1.23. These small errors at the tail of the decimal are due to the fact that the infinite binary expansion has been truncated (or more likely, rounded). For example, the binary expansion for .1 has repeating 1100 digits, giving ...110011001100110011..., so if the cut-off occurs before a zero, as in ..1100110|0110011..., then the resulting binary number is slightly too small, and we have the case like the one in 1.235 (it rounds down because it is slightly below 1.235). On the other hand, if it cuts off before a 1, as in ..11001100|110011..., then the number stored might be rounded up to ..11001101, which is slightly too big. This is like the 1.225, which rounds up, because the internal representation is just a little over 1.225 (by 1.2*10^(-16)).
Numbers stored as IEEE double-precision floating point reals (like the Perl I'm running uses) have 54 binary digits for the mantissa (the constant in front of the 10^(-16)) and 11 for the exponent (yes, that's 65, which I could also explain). So the error should be in the 54th binary "decimal" place, so the error should be on the order or 2^(-54). As a rough estimate of how big that is, we can use the fact that 2^10=1024 is about 1000=10^3, so
2^(-54) = 2^6*2^(-60) = 64*2^(10*-6) = 64*(2^10)^(-6) = 64*(10^3)^(-6) = 64*10^(-18) = 6.4*10^(-17)
which is just about exactly the sized error we see (and since 1024 is a little bigger than 1000, our estimate is a little smaller than the actual error).
Anyway, the reason the rounding doesn't work as expected is that the numbers actually being rounded are in binary not decimal, and so are not exactly what you think they are. The reason you don't see these error when you print the numbers is that the numbers are printed using one or two fewer (decimal) digits than the binary representation actually allows, and the result is rounded for printing, so 1.234999999999999999 becomes 1.235 even though internally it isn't exactly that.
The upshot is, you can't trust printed numbers to display the actual number stored, and the actual number stored may not be the one you entered.
Davide
PS, I don't even want to THINK about what banks do.