Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I have this file and I want to sum all the number in first column. Easy:

awk '{s+=$1;print $1,s}' file
-0.4157 -0.4157
0.0143 -0.4014
0.5973 0.1959
-0.5679 -0.372
-0.2041 -0.5761
0.7130 0.1369
-0.5931 -0.4562
-0.9191 -1.3753
0.1048 -1.2705
0.4196 -0.8509
0.4196 -0.4313
0.2719 -0.1594
0.0797 -0.0797
0.0797 -5.55112e-17

You see, the last one should be 0. I know that e-17 is zero, but sometimes the output is exactly 0. If it's not 0, the output is in range of e-15 to e-17, in negative or positive form. To fix this, I have to use the absolute value:

awk '{s+=$1;if (sqrt(s^2)<0.01) s=0;print $1,s}' file

Do you know why this happens?

share|improve this question

5 Answers 5

up vote 1 down vote accepted

Your question is “Why does this happen?”, but your implicit question (which others have addressed) is “How can I fix this?”  You figured out an approach, which you raised in a comment:

So if I multiply it to 1000 to eliminate the point, I can get the exact result, can’t I?

Yes.  Well, 10000, since you have four decimal places.  Consider this:

awk '{ s+=$1*10000; print $1, s/10000 }'

Unfortunately, this doesn’t work, because the corruption has already occurred as soon as we interpret the token (string) as a decimal number.  For example, printf "%.20f\n" shows that the input data 0.4157 is actually interpreted as 0.41570000000000001394.  In this case, multiplying by 10000 gets you what you would expect: 4157.  But, for example, 0.5973 = 0.59730000000000005311, and multiplying that by 10000 yields 5973.00000000000090949470.

So instead we try

awk '{ s+=int($1*10000); print $1, s/10000 }'

to convert the numbers that “should be” whole numbers (e.g., 5973.00000000000090949470) into the corresponding whole numbers (5973).  But that fails because sometimes the conversion error is negative; e.g., 0.7130 is 0.71299999999999996714.  And awk’s int(expr) functions truncates (toward zero) rather than rounding, so int(7129.99999999) is 7129.

So, when life gives you lemons, you make lemonade.  And when a tool gives you a truncate function, you round by adding 0.5.  7129.99999999+0.5≈7130.49999999, and, of course, int(7130.49999999) is 7130.  But remember: int() truncates toward zero, and your input includes negative numbers.  If you want to round –7129.99999999 to –7130, you need to subtract 0.5 to get –7130.49999999.  So,

awk '{ s+=int($1*10000+($1>0?0.5:-0.5)); print $1, s/10000 }'

which adds –0.5 to $1*10000 if $1 is ≤ 0.

share|improve this answer
    
Nice explanation and solution. But I don't understand what's wrong when you take 0.5973 times 10000. Can you give me more detail? Thank you. –  Ooker Jul 9 at 23:19
1  
I’m not sure what you’re asking. (1) What’s happening? I’m not sure, exactly. What nobody else has said is that people typically use decimal (base 10) to represent numbers, but computers use binary (base 2). So, when a computer reads a decimal number, it converts it to binary, and then converts back to decimal on output. For example, ½ is 0.5 in decimal, 0.1 in binary; ¾ is 0.75 in decimal, 0.11 in binary. And some simple fractions cannot be represented exactly in decimal; e.g., ⅓ is 0.33333333…. When you truncate to a finite number of digits, you get an approximation (i.e., rounding error). … –  Scott Jul 9 at 23:51
    
… Well, the only fractions that can be represented exactly in binary are fractions of the form (some integer) ∕ (a power of 2), so, while a simple fraction like ⅕ can be represented exactly in decimal (as 0.2), it cannot be represented exactly in binary. Now, it happens to work out that some decimal numbers of the form 0.nnnn, when converted to binary at a certain precision (number of bits) and multiplied by 10000 at that same precision, come out to the original nnnn. And some don’t. I can’t explain why. … –  Scott Jul 9 at 23:52
    
… But I guess you might also be asking (2) why does it matter? It’s because we’re trying to get the exact results that you want. So if we have the input 0.5973, -0.5971, and -0.0002, and we multiply each (converted to binary) number by 10000, we get 5973.00000000000090949470, -5971, and -2. So the sum is 0.00000000000090949470. And that’s why we need to round the numbers to the nearest integer after we’ve multiplied by 10000. –  Scott Jul 9 at 23:53
    
Why does only 0.5973 become 5973.000000 but not also -0.5971 and -0.0002 when we multiply them with 10000? Does that always happen or it's just an example? Also, I think we should round the numbers before we divide them back, not after they are multiplied by 10000 because adding 0.5 many times can be dangerous. –  Ooker Jul 10 at 0:27

It happens because a computer only has a limited precision when dealing with numbers. And the available precision uses a binary format to represent the number.

This makes numbers that appear to be trivial to write in our decimal system only representable as an approximation (see the Wikipedia entry on this): e.g. 0.1 (as in 1/10) is really stored as something like 0.100000001490116119384765625 on the computer.

So all your numbers are really only handled by an approximation (unless you are lucky and have numbers like 0.5 that can be represented exactly).

Summing up all those approximate numbers can eventually lead to an error that is != 0.

share|improve this answer
    
So you mean that awk (or unix system) can't handle four digits after the point? Is that normal? In the wiki entry, they can handle to the sixth digit. –  Ooker Jul 9 at 10:38
5  
i'm saying that no IEEE754 32bit floating point number can represent certain numbers exactly (regardless whether this is unix, or w32 or whatever). you will always get an error (and the error you have is about 10^-15, this is the 15th digit, not just the sixth). the reason why you don't often see this, is because many programs do some rounding in the output. –  umläute Jul 9 at 11:04
    
I see. But why when the result is 1 (which is exactly 1.00000xyz), the program do rounding it, but not do so with 0.000xyz? –  Ooker Jul 9 at 11:15
1  
because print also does some rounding in order to save characters :-) now if your number is 0.000000000000000000052 you can easily print this as 5.2e-20 (takes 7 characters, rather than 24), wheras 1.000000000000000000052 is rather complicated to print (you could do 1+5.2e-20 but that is not the usual scientific notation) –  umläute Jul 9 at 11:22
    
I got it. Thank you. –  Ooker Jul 9 at 11:24

As a way around this, you could use a program that is specifically designed to handle arithmetic operations like bc:

$ awk '{printf "%s + ",$1}' file | sed 's/\+ $/\n/' | bc
0

If, as seems to be the case, you have a fixed number of decimal places, you could simply remove them to work with integers and then add them again at the end:

$ awk '{sub("0.","",$1);s+=$1;}END{print s/10000}' file
0

or

$ perl -lne 's/0\.//; $s+=$_; END{print $s/10000}' file
0
share|improve this answer
    
If I have a complex awk with if, I think that using its built-in calculation is better. –  Ooker Jul 9 at 11:23
    
@Ooker depends on how important precision is. As you pointed out, you can have issues when calculating floating point numbers. –  terdon Jul 9 at 11:25
    
If you built an abs function, you won't need to pipe. –  Ooker Jul 9 at 11:30
    
@Ooker see updated answer for another approach. –  terdon Jul 9 at 11:59
    
Nice one :D. Just to make sure, will this approach be good when your number is not start with 0, but other digit? I think there will be no problem. –  Ooker Jul 9 at 14:13

Most versions of awk have a printf command. Instead of

print $1,s

use

printf "%.4f %.4f\n",$1,s

and the outputs will be rounded to 4 decimal places. That way you won't see most rounding errors.

share|improve this answer
    
Nice. But the problem only appears when the number is 0.xyz. Substitution by '0' is easier to understand and makes the print neater. –  Ooker Jul 9 at 14:44

This is not awk unique problem, it's also another programming languages problems. Example with perl:

$ perl -anle '$sum+=$F[0]}{print $sum' file 
-5.55111512312578e-17

It's the problem of representing a non-terminating series for base 2 using a finite number of binary digits. Floating point numbers are not integers. It can take an infinitive amount of memory to store floating point numbers.

You can read this article to understand more.

share|improve this answer
    
So if I multiply it to 1000 to eliminate the point, I can get the exact result, can't I? –  Ooker Jul 9 at 11:18
    
@terdon: It's not syntax error, just a quick typing for example :) –  Gnouc Jul 9 at 11:18
    
Wow, you're right! I had never seen that before. So the }{ acts like a {}END{}? –  terdon Jul 9 at 11:20
    
@terdon: yes, because -n cause you already in a while loop. So it's really while{...}{...} –  Gnouc Jul 9 at 11:21
    
That I understood, it was the lone }{ instead of {}{} that confused me. –  terdon Jul 9 at 11:25

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.