Почему awk не делает сумму ноль, а вместо этого очень маленьким?

У меня есть этот файл, и я хочу суммировать все числа в первом столбце. Легко:

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 

Понимаете, последнее должно быть 0. Я знаю, что e-17 равно нулю, но иногда вывод равен ровно 0. Если это не 0, выход находится в диапазоне от e-15 до e-17 , отрицательно или положительно знак. Чтобы исправить это, я должен использовать абсолютное значение:

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

Вы знаете, почему это происходит?

  • объединить файлы csv, сохранить запятую внутри кавычек
  • Чтение текстового файла; Копирование текстовой строки без отступов; Заменить начало отступов с указанным текстом
  • Регулярное выражение ERE для разделения () между разделителем и конечным словом
  • awk объединить два больших файла и удалить дублированные строки
  • Если значение попадает в диапазон, напечатайте сумму значений
  • Вставить строку после сопоставления строки в sshd внутри pam.d
  • Как напечатать n-ю строку после совпадения / пустые строки
  • Как awk уникальная строка и печать 2 строк выше
  • 5 Solutions collect form web for “Почему awk не делает сумму ноль, а вместо этого очень маленьким?”

    Ваш вопрос: «Почему это происходит?», Но ваш неявный вопрос (к которому другие обратились): «Как я могу это исправить?» Вы выяснили подход, который вы подняли в комментарии:

    Поэтому, если я умножу его на 1000, чтобы устранить эту точку, я могу получить точный результат, не так ли?

    Да. Ну, 10000, так как у вас есть четыре знака после запятой. Учти это:

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

    К сожалению, это не работает, потому что повреждение уже произошло, как только мы интерпретируем токен (строку) как десятичное число. Например, printf "%.20f\n" показывает, что входные данные 0.4157 фактически интерпретируются как 0.41570000000000001394. В этом случае умножение на 10000 дает вам то, что вы ожидаете: 4157. Но, например, 0.5973 = 0,59730000000000005311, и умножая его на 10000, получается 5973.00000000000090949470.

    Поэтому вместо этого мы пытаемся

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

    для преобразования чисел, которые должны быть «целыми» (например, 5973.00000000000090949470) в соответствующие целые числа (5973). Но это терпит неудачу, потому что иногда ошибка преобразования отрицательна; например, 0.7130 составляет 0,71299999999999996714. И функции int( expr ) awk усекаются (к нулю), а не округляются, поэтому int(7129.99999999) равен 7129.

    Итак, когда жизнь дает вам лимоны, вы делаете лимонад. И когда инструмент дает вам функцию усечения, вы округлите, добавив 0.5. 7129.99999999 + 0.5≈7130.49999999, и, конечно, int(7130.49999999) – 7130. Но помните: int() усекает к нулю , а ваш ввод содержит отрицательные числа. Если вы хотите округлить -7129.99999999 до -7130, вам нужно вычесть 0,5 для получения -7130.49999999. Так,

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

    который добавляет от -0,5 до $1*10000 если $1 равен ≤ 0.

    Это происходит потому, что компьютер имеет ограниченную точность при работе с числами. И доступная точность использует двоичный формат для представления числа.

    Это приводит к тому, что числа, которые кажутся тривиальными для записи в нашей десятичной системе, представлены только в качестве приближения (см. Запись в Википедии ): например, 0.1 (как в 1/10 ) действительно хранится как-то как 0.100000001490116119384765625 на компьютере.

    Таким образом, все ваши числа действительно обрабатываются только приближением (если вам не повезло и имеют цифры, равные 0.5 которые могут быть представлены точно ).

    Суммирование всех этих приблизительных чисел может в конечном итоге привести к ошибке, которая равна != 0 .

    Для этого вы можете использовать программу, специально предназначенную для обработки арифметических операций, таких как bc :

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

    Если, как кажется, у вас есть фиксированное число десятичных знаков, вы можете просто удалить их для работы с целыми числами и затем добавить их снова в конце:

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

    или

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

    Большинство версий awk имеют команду printf . Вместо

     print $1,s 

    использование

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

    и выходы будут округлены до 4 знаков после запятой. Таким образом, вы не увидите большинства ошибок округления.

    Это не уникальная проблема awk , это еще и проблемы с программированием. Пример с perl :

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

    Это проблема представления бесконечной серии для базы 2 с использованием конечного числа двоичных цифр. Числа с плавающей запятой не являются целыми числами. Для хранения чисел с плавающей запятой может потребоваться бесконечный объем памяти.

    Вы можете прочитать эту статью, чтобы понять больше.

    Linux и Unix - лучшая ОС в мире.