arithmétique de haute précision awk

11

Je cherche un moyen de dire à awk de faire de l'arithmétique de haute précision dans une opération de substitution. Cela implique de lire un champ d'un fichier et de le remplacer par un incrément de 1% sur cette valeur. Cependant, je perd en précision là-bas. Voici une reproduction simplifiée du problème:

 $ echo 0.4970436865354813 | awk '{gsub($1, $1*1.1)}; {print}'
   0.546748

Ici, j'ai 16 chiffres après la précision décimale mais awk n'en donne que six. En utilisant printf, j'obtiens le même résultat:

$ echo 0.4970436865354813 | awk '{gsub($1, $1*1.1)}; {printf("%.16G\n", $1)}'
0.546748

Des suggestions sur la façon d'obtenir la précision souhaitée?

mkc
la source
Peut-être awk a une résolution plus élevée, mais c'est juste que le formatage de votre sortie est tronqué. Utilisez printf.
dubiousjim
Aucune modification de la valeur du résultat après utilisation de printf. Question modifiée en conséquence.
mkc
Comme l'a souligné @manatwork, cela gsubn'est pas nécessaire. Le problème est gsubsur les chaînes, pas sur les nombres, donc une conversion est effectuée en premier en utilisant CONVFMT, et la valeur par défaut pour cela est %.6g.
jw013
@ jw013, Comme je l'ai mentionné dans la question, mon problème d'origine nécessite gsub car je dois remplacer un nombre par un incrément de 1%. D'accord, dans l'exemple simplifié, ce n'est pas obligatoire.
mkc

Réponses:

12
$ echo 0.4970436865354813 | awk -v CONVFMT=%.17g '{gsub($1, $1*1.1)}; {print}'
0.54674805518902947

Ou plutôt ici:

$ echo 0.4970436865354813 | awk '{printf "%.17g\n", $1*1.1}'
0.54674805518902947

est probablement le meilleur que vous puissiez réaliser. Utilisez à la bcplace pour une précision arbitraire.

$ echo '0.4970436865354813 * 1.1' | bc -l
.54674805518902943
Stéphane Chazelas
la source
Si vous voulez une précision arbitraire, AWKvous pouvez utiliser l' -Mindicateur et définir la PRECvaleur sur un grand nombre
Robert Benson
3
@RobertBenson, uniquement avec GNU awk et uniquement avec les versions récentes (4.1 ou supérieures, donc pas au moment où cette réponse a été écrite) et uniquement lorsque MPFR a été activé au moment de la compilation.
Stéphane Chazelas
2

Pour une plus grande précision avec (GNU) awk (avec bignum compilé en) utilisez:

$ echo '0.4970436865354813' | awk -M -v PREC=100 '{printf("%.18f\n", $1)}'
0.497043686535481300

PREC = 100 signifie 100 bits au lieu des 53 bits par défaut.
Si cet awk n'est pas disponible, utilisez bc

$ echo '0.4970436865354813*1.1' | bc -l
.54674805518902943

Ou vous devrez apprendre à vivre avec l'imprécision inhérente des flotteurs.


Dans vos lignes d'origine, il y a plusieurs problèmes:

  • Un facteur de 1,1 correspond à une augmentation de 10% et non à 1% (devrait être un multiplicateur de 1,01). Je vais utiliser 10%.
  • Le format de conversion d'une chaîne en un nombre (flottant) est donné par CONVFMT. Sa valeur par défaut est %.6g. Cela limite les valeurs à 6 chiffres décimaux (après le point). Ceci est appliqué au résultat du changement gsub de $1.

    $ a='0.4970436865354813'
    $ echo "$a" | awk '{printf("%.16f\n", $1*1.1)}'
    0.5467480551890295
    
    $ echo "$a" | awk '{gsub($1, $1*1.1)}; {printf("%.16f\n", $1)}'
    0.5467480000000000
  • Le format printf gsupprime les zéros de fin:

    $ echo "$a" | awk '{gsub($1, $1*1.1)}; {printf("%.16g\n", $1)}'
    0.546748
    
    $ echo "$a" | awk '{gsub($1, $1*1.1)}; {printf("%.17g\n", $1)}'
    0.54674800000000001

    Les deux problèmes pourraient être résolus avec:

    $ echo "$a" | awk '{printf("%.17g\n", $1*1.1)}'
    0.54674805518902947

    Ou

    $ echo "$a" | awk -v CONVFMT=%.30g '{gsub($1, $1*1.1)}; {printf("%.17f\n", $1)}'
    0.54674805518902947 

Mais ne pensez pas que cela signifie une plus grande précision. La représentation numérique interne est toujours un flotteur en double taille. Cela signifie 53 bits de précision et avec cela, vous ne pouvez être sûr que de 15 chiffres décimaux corrects, même si plusieurs fois jusqu'à 17 chiffres semblent corrects. Voilà un mirage.

$ echo "$a" | awk -v CONVFMT=%.30g '{gsub($1, $1*1.1}; {printf("%.30f\n", $1)}'
0.546748055189029469325134868996

La valeur correcte est:

$ echo "scale=18; 0.4970436865354813 * 1.1" | bc
.54674805518902943

Ce qui pourrait également être calculé avec (GNU) awk si la bibliothèque bignum a été compilée dans:

$ echo "$a" | awk -M -v PREC=100 -v CONVFMT=%.30g '{printf("%.30f\n", $1)}'
0.497043686535481300000000000000
Isaac
la source