Pourquoi ne pas faire la sum zéro mais plutôt un très petit nombre?

J'ai ce file et je veux résumer tout le nombre dans la première colonne. Facile:

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 

Vous voyez, le dernier devrait être 0. Je sais que e-17 est zéro, mais parfois la sortie est exactement 0. Si ce n'est pas 0, la sortie est dans la plage de e-15 à e-17 , négative ou positive signe. Pour résoudre ce problème, je dois utiliser la valeur absolue:

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

Savez-vous pourquoi cela arrive?

Votre question est «Pourquoi cela se produit-il?», Mais votre question implicite (que d'autres ont abordée) est «Comment puis-je résoudre ce problème?» Vous avez trouvé une approche que vous avez soulevée dans un commentaire:

Donc, si je le multiplie à 1000 pour éliminer le point, je peux get le résultat exact, n'est-ce pas?

Oui. Eh bien, 10000, puisque vous avez quatre décimales. Considère ceci:

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

Malheureusement, cela ne fonctionne pas, car la corruption a déjà eu lieu dès que nous interprétons le jeton (string) comme un nombre décimal. Par exemple, printf "%.20f\n" montre que datatables d'input 0.4157 sont en fait interprétées comme 0.41570000000000001394. Dans ce cas, multiplier par 10000 vous obtient ce que vous attendez: 4157. Mais, par exemple, 0.5973 = 0,59730000000000005311, et en multipliant cela par 10000 cède 5973.00000000000090949470.

Donc, nous essayons

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

pour convertir les nombres qui devraient être des nombres entiers (par exemple, 5973.00000000000090949470) en nombres entiers correspondants (5973). Mais cela échoue parce que parfois l'erreur de conversion est négative; par exemple, 0.7130 est 0,71299999999999996714. Et les fonctions int( expr ) awk tronquent (vers zéro) plutôt que d'arrondir, donc int(7129.99999999) est 7129.

Donc, quand la vie vous donne des citrons, vous faites de la limonade. Et quand un outil vous donne une fonction tronquée, vous arrondissez en ajoutant 0,5. 7129.99999999 + 0.5≈7130.49999999 et, bien sûr, int(7130.49999999) est 7130. Mais callbackez-vous: int() tronque vers zéro et votre input contient des nombres négatifs. Si vous voulez arrondir -7129.99999999 à -7130, vous devez soustraire 0.5 pour get -7130.49999999. Alors,

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

qui ajoute -0,5 à $1*10000 si $1 est ≤ 0.

Cela arrive parce qu'un ordinateur n'a qu'une précision limitée lorsqu'il s'agit de nombres. Et la précision disponible utilise un format binary pour représenter le nombre.

Cela rend les nombres qui semblent être sortingviaux à écrire dans notre système décimal, seulement représentables en tant qu'approximation (voir l'input Wikipedia à ce sujet): par exemple 0.1 (comme dans 1/10 ) est vraiment stocké comme 0.100000001490116119384765625 sur l'ordinateur.

Donc, tous vos nombres ne sont vraiment traités que par approximation (sauf si vous avez de la chance et que vous avez des nombres comme 0.5 qui peuvent être représentés exactement ).

Résumer tous ces nombres approximatifs peut éventuellement conduire à une erreur qui est != 0 .

Pour contourner ce problème, vous pouvez utiliser un programme spécialement conçu pour gérer les opérations arithmétiques telles que bc :

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

Si, comme cela semble être le cas, vous disposez d'un nombre fixe de décimales, vous pouvez simplement les supprimer pour travailler avec des entiers, puis les append à nouveau à la fin:

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

ou

 $ perl -lne 's/0\.//; $s+=$_; END{print $s/10000}' file 0 

La plupart des versions de awk ont une command printf . Au lieu de

 print $1,s 

utilisation

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

et les sorties seront arrondies à 4 décimales. De cette façon, vous ne verrez pas la plupart des erreurs d'arrondi.

Ce n'est pas un problème unique, c'est aussi un autre problème de langages de programmation. Exemple avec perl :

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

C'est le problème de représenter une série non-terminante pour la base 2 en utilisant un nombre fini de numbers binarys. Les nombres à floating point ne sont pas des nombres entiers. Cela peut prendre une quantité infinie de memory pour stocker des nombres à floating point.

Vous pouvez lire cet article pour en savoir plus.