Как эффективно разделить столбцы файлов друг на друга, используя bash? - PullRequest
1 голос
/ 29 апреля 2020

У меня большой текстовый файл в формате:

cat 10 5 20
pig 20 5 25
dog 0 5 0
goat 10 0 10
sheep 0 0 0 

Как использовать awk для добавления двух новых столбцов, один из которых содержит столбец 2, разделенный на столбец 4, и один, содержащий столбец 3, разделенный на столбец 4? Если знаменатель равен 0, тогда я хотел бы вставить 0. Например:

cat 10 5 20 0.5 0.25 
pig 20 5 25 0.8 0.2
dog 0 5 0 0 0 0
goat 10 0 10 1 0 
sheep 0 0 0 0 0

Я пытался:

awk '{ print $1, $2, $3, $4, $2/$4, $3/$4 }' input_file > output_file

Однако, это дает следующую ошибку:

fatal: division by zero attempted

Файл очень большой, поэтому производительность важна. Любая помощь будет оценена!

Ответы [ 2 ]

3 голосов
/ 29 апреля 2020

Поскольку производительность важна, тестирование на 4 доллара один раз будет быстрее, чем тестирование дважды:

awk '$4{print $0, $2/$4, $3/$4; next} {print $0, 0, 0}' Input_file

Компромиссом для повышения производительности является дублирующий код (2 print $0 с), но это, очевидно, минимально в этом case и все решения имеют некоторое дублирование кода.

Мне было любопытно, и я решил определить время всех трех текущих ответов (мои, @ Ravinders и @ Inians ). Вот результаты 3-го запуска синхронизации с использованием GNU awk на MacOS с 10-миллионным строчным файлом, сгенерированным путем запуска awk '{for (i=1; i<=2000000; i++) print}' file > file10m на OP, предоставленных в качестве примера ввода.

$ time awk '$4{print $0, $2/$4, $3/$4; next} {print $0, 0, 0}' file10m >/dev/null

real    0m10.087s
user    0m10.009s
sys     0m0.054s

$ time awk '{print $0, ($4 ? $2/$4 : 0), ($4 ? $3/$4 : 0)}' file10m >/dev/null

real    0m10.329s
user    0m10.249s
sys     0m0.060s

$ time awk '{ $(NF+1) = ($4 ? $2/$4 :0); $(NF+1) = ($4 ? $3/$4 :0)  }1' file10m >/dev/null

real    0m11.293s
user    0m11.208s
sys     0m0.063s

и с использованием awk по умолчанию OSX:

$ time /usr/bin/awk '$4{print $0, $2/$4, $3/$4; next} {print $0, 0, 0}' file10m >/dev/null

real    0m13.383s
user    0m13.240s
sys     0m0.123s

$ time /usr/bin/awk '{print $0, ($4 ? $2/$4 : 0), ($4 ? $3/$4 : 0)}' file10m >/dev/null

real    0m14.293s
user    0m14.082s
sys     0m0.161s

$ time /usr/bin/awk '{ $(NF+1) = ($4 ? $2/$4 :0); $(NF+1) = ($4 ? $3/$4 :0)  }1' file10m >/dev/null

real    0m15.668s
user    0m15.516s
sys     0m0.130s

, поэтому подход Inians в этих тестах был примерно на 12-17% медленнее, чем у меня, а Ravinders примерно на 2-7% медленнее с различными улучшениями скорости в зависимости от версии awk.

Я также тестировал awk '{print $0, ($4 ? $2/$4 OFS $3/$4 : 0 OFS 0)}' но обнаружил, что он немного медленнее моего предложения в GNU awk и еще медленнее в OSX awk (аналогично Ravinders), вероятно, из-за конкатенации строк. Использование жесткого кода "0 0" для остальной части троичного во избежание конкатенации в этом отрезке дало небольшое улучшение в скорости выполнения. С gawk:

$ time awk '{print $0, ($4 ? $2/$4 OFS $3/$4 : "0 0")}' file10m >/dev/null

real    0m10.145s
user    0m10.058s
sys     0m0.064s

и с BSD awk:

$ time /usr/bin/awk '{print $0, ($4 ? $2/$4 OFS $3/$4 : "0 0")}' file10m >/dev/null

real    0m13.993s
user    0m13.818s
sys     0m0.139s

, но это все же не так быстро, как мое первоначальное предложение выше, и я не уверен, что нужно жестко кодировать это Строка, включающая то, на что, как вы надеетесь, будет иметь значение OFS, является компромиссом.

2 голосов
/ 29 апреля 2020

Не могли бы вы попробовать следующее.

awk '{print $0, ($4 ? $2/$4 : 0), ($4 ? $3/$4 : 0)}' Input_file

Объяснение выше: Печать текущей строки и проверка состояния, если 4-е поле НЕ равно нулю, а затем печатать $2/$4 или распечатать 0. Затем снова проверьте, что 4-е поле НЕ равно нулю, затем напечатайте $3/$4 или напечатайте 0.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...