Por que o awk não faz a soma zero, mas um número muito pequeno?

5

Eu tenho este arquivo e quero somar todo o número na primeira coluna. Fácil:

awk '{s+=$1;print $1,s}' file
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   #Notice this line

Você vê, o último deve ser 0. Eu sei que e-17 é zero, mas às vezes a saída é exatamente 0. Se não for 0, a saída está no intervalo de e-15 a e-17 , em sinal negativo ou positivo. Para corrigir isso, eu tenho que usar o valor absoluto:

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

Você sabe por que isso acontece?

    
por Ooker 09.07.2014 / 11:58

5 respostas

1

Sua pergunta é "Por que isso acontece?", mas sua pergunta implícita (que outros abordaram) é "Como posso consertar isso?" Você descobriu uma abordagem, que você levantou em um comentário:

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

Sim. Bem, 10000, desde que você tenha quatro casas decimais. Considere isto:

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

Infelizmente, isso não funciona, porque a corrupção já ocorreu assim que interpretamos o token (string) como um número decimal. Por exemplo, printf "%.20f\n" mostra que os dados de entrada 0.4157 é realmente interpretado como 0,41570000000000001394. Neste caso, multiplicando por 10000 você obtém o que você esperaria: 4157. Mas, por exemplo, 0.5973 = 0,59730000000000005311, e multiplicando isso por 10000 produz 5973.00000000000090949470.

Então, em vez disso, tentamos

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

para converter os números que "devem ser" números inteiros (por exemplo, 5973.00000000000090949470) nos números inteiros correspondentes (5973). Mas isso falha porque às vezes o erro de conversão é negativo; por exemplo, 0.7130 é 0,71299999999999996714. E as funções awk int(expr) são truncadas (em direção a zero) em vez de arredondar, então int(7129.99999999) é 7129.

Então, quando a vida te dá limões, você faz limonada. E quando uma ferramenta lhe dá uma função truncada, você adiciona 0.5. 7129.99999999 + 0.5≈7130.49999999 e, claro, int(7130.49999999) é 7130. Mas lembre-se: int() trunca para zero e sua entrada inclui números negativos. Se pretender arredondar –7129.99999999 para –7130, você precisa subtrair 0,5 para obter –7130.49999999. Então,

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

que adiciona –0,5 a $1*10000 se $1 for ≤ 0.

    
por 09.07.2014 / 23:18
11

Isso acontece porque um computador só tem uma precisão limitada ao lidar com números. E a precisão disponível usa um formato binário para representar o número.

Isto faz com que os números que parecem triviais para escrever em nosso sistema decimal sejam apenas representáveis como uma aproximação (veja a Wikipedia entrada sobre isso): por exemplo 0.1 (como em 1/10 ) é realmente armazenado como algo como 0.100000001490116119384765625 no computador.

Assim, todos os seus números são realmente manipulados apenas por uma aproximação (a menos que você tenha sorte e tenha números como 0.5 que podem ser representados exatamente ).

Resumindo, todos esses números aproximados podem levar a um erro que é != 0 .

    
por 09.07.2014 / 12:15
4

Como forma de contornar isso, você poderia usar um programa especificamente projetado para lidar com operações aritméticas como bc :

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

Se, como parece ser o caso, você tiver um número fixo de casas decimais, basta removê-las para trabalhar com números inteiros e adicioná-las novamente no final:

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

ou

$ perl -lne 's/0\.//; $s+=$_; END{print $s/10000}' file
0
    
por 09.07.2014 / 13:19
3

A maioria das versões de awk tem um comando printf . Em vez de

print $1,s

use

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

e as saídas serão arredondadas para 4 casas decimais. Dessa forma, você não verá mais erros de arredondamento.

    
por 09.07.2014 / 16:23
2

Este não é awk problema único, é também outro problema de linguagens de programação. Exemplo com perl :

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

É o problema de representar uma série sem terminação para a base 2 usando um número finito de dígitos binários. Números de ponto flutuante não são inteiros. Pode levar uma quantidade infinita de memória para armazenar números de ponto flutuante.

Você pode ler este artigo para entender mais.

    
por 09.07.2014 / 13:10