Вычесть соответствующие строки

У меня есть два файла, file1.csv

3 1009
7 1012
2 1013
8 1014

и файл2.csv

5 1009
3 1010
1 1013

В оболочке я хочу вычесть количество в первом столбце во втором файле из первого файла на основе идентификатора во втором столбце. Если идентификатор отсутствует во втором столбце, считается, что счетчик равен 0.

Результат будет

-2 1009
-3 1010 
7 1012
1 1013
8 1014

Файлы огромные (несколько ГБ). Вторые столбцы отсортированы.

Как бы я сделал это эффективно в оболочке?


person user32849    schedule 14.12.2016    source источник


Ответы (4)


Предполагая, что оба файла отсортированы по второму столбцу:

$ join -j2 -a1 -a2 -oauto -e0 file1 file2 | awk '{print $2 - $3, $1}'
-2 1009
-3 1010
7 1012
1 1013
8 1014

join соединит отсортированные файлы.
-j2 соединит один второй столбец.
-a1 напечатает записи из файла1, даже если в файле2 нет соответствующей строки.
-a2 То же, что и -a1, но применяется для файла2.
-oauto в этом случае совпадает с -o1.2,1.1,2.1, который напечатает объединенный столбец, а затем оставшиеся столбцы из файла1 и файла2.
-e0 вставит 0 вместо пустого столбца. Это работает с -a1 и -a2.

Выход из join - это три столбца, например:

1009 3 5
1010 0 3
1012 7 0
1013 2 1
1014 8 0

Который передается в awk, чтобы вычесть третий столбец из столбца 2, а затем переформатировать.

person andlrc    schedule 14.12.2016
comment
Элегантное решение, красиво! Интересно, есть ли существенная разница во времени или памяти между join и слиянием (sort -m) для таких больших файлов, как эти - @user... не могли бы вы проверить свои данные и сообщить нам? - person Ed Morton; 14.12.2016
comment
Я только что написал (независимо) ответ, который очень похож, но я выбрал dc, а не awk. Я думаю, что ваш ответ немного лучше моего, только с двумя процессами, а не с тремя. Я должен чаще обращаться к awk! - person Toby Speight; 14.12.2016
comment
Действительно очень приятно. - person James Brown; 14.12.2016

$ awk 'NR==FNR { a[$2]=$1; next }
               { a[$2]-=$1 }
           END { for(i in a) print a[i],i }' file1 file2
7 1012
1 1013
8 1014
-2 1009
-3 1010

Он читает первый файл в памяти, поэтому у вас должно быть достаточно памяти. Если у вас нет памяти, я бы, возможно, сначала sort -k2 файлы, затем sort -m (объединил) их и продолжил с этим выводом:

$ sort -m -k2 -k3 <(sed 's/$/ 1/' file1|sort -k2) <(sed 's/$/ 2/' file2|sort -k2) # | awk ...
3 1009 1
5 1009 2  # previous $2 = current $2 -> subtract
3 1010 2  # previous $2 =/= current and current $3=2 print -$3
7 1012 1
2 1013 1  # previous $2 =/= current and current $3=1 print prev $2
1 1013 2
8 1014 1

(у меня сейчас нет времени, может быть, я закончу это позже)

РЕДАКТИРОВАТЬ Эда Мортона Надеюсь, вы не возражаете, если я добавлю то, над чем я работал, а не опубликую свой собственный очень похожий ответ, не стесняйтесь изменять или удалять его:

$ cat tst.awk
{ split(prev,p) }
$2 == p[2] {
    print p[1] - $1, p[2]
    prev = ""
    next
}
p[2] != "" {
    print (p[3] == 1 ? p[1] : 0-p[1]), p[2]
}
{ prev = $0 }
END {
    split(prev,p)
    print (p[3] == 1 ? p[1] : 0-p[1]), p[2]
}

$ sort -m -k2 <(sed 's/$/ 1/' file1) <(sed 's/$/ 2/' file2) | awk -f tst.awk
-2 1009
-3 1010
7 1012
1 1013
8 1014
person James Brown    schedule 14.12.2016
comment
Опередили на 3 секунды! собирался тоже самое выложить! :) - person Inian; 14.12.2016
comment
@Inian Victory лучше подавать холодным или что-то в этом роде ...: D Сам был там много раз, теперь я думаю, если у него действительно есть концерты и концерты данных, должен быть какой-то более эффективный способ. Это решение дерьмовое в этом смысле. - person James Brown; 14.12.2016
comment
добавьте | sort -k2 для полного соответствия :-) - person NeronLeVelu; 14.12.2016
comment
Он по-прежнему хранит первую таблицу целиком в памяти. - person James Brown; 14.12.2016
comment
@JamesBrown в огромном файле данных, память часто является проблемой, если вам нужно запомнить информацию и не работать с SQL, например с инструментами/средой - person NeronLeVelu; 14.12.2016
comment
для оптимизации памяти нам нужно немного подготовить вход 2 и подать его как идеально упорядоченный поток. Сортировка или подготовка данных по-прежнему занимает много времени :-( - person NeronLeVelu; 14.12.2016
comment
Возможно, есть способ использовать «объединение», поскольку второй столбец отсортирован? - person user32849; 14.12.2016
comment
Я тоже шел по маршруту sort -m, но столкнулся со стеной, когда обнаружил, что не могу отличить значение только для файла1 от значения только для файла2 в выходных данных, и, конечно, решение для этого — это именно то, что вы делаете с добавлением номер дела. Вам не нужны |sort -k2, поскольку входные файлы уже отсортированы. - person Ed Morton; 14.12.2016
comment
Надеюсь, вы не возражаете, но я добавил свой awk-скрипт в конец вашего ответа вместо того, чтобы публиковать свой собственный ответ, который также был бы основан на sort -m, который является ключевой частью этого решения. Вам не нужен -k3, кстати, поскольку -k2 означает, что нужно начинать с ключа 2, а не использовать только ключ 2. - person Ed Morton; 14.12.2016
comment
Не за что, спасибо, сэр. Ненавижу, когда работа мешает моему хобби. :D - person James Brown; 14.12.2016

Поскольку файлы отсортированы¹, вы можете объединить их построчно с помощью утилиты join в coreutils:

$ join -j2 -o auto -e 0 -a 1 -a 2 41144043-a 41144043-b
1009 3 5
1010 0 3
1012 7 0
1013 2 1
1014 8 0

Все эти параметры необходимы:

  • -j2 предлагает присоединиться на основе второго столбца каждого файла
  • -o auto говорит, что каждая строка должна иметь одинаковый формат, начиная с ключа соединения
  • -e 0 говорит, что пропущенные значения должны быть заменены нулем
  • -a 1 и -a 2 включают строки, отсутствующие в том или ином файле
  • имена файлов (здесь я использовал имена на основе номера вопроса)

Теперь у нас есть поток вывода в этом формате, мы можем выполнять вычитание в каждой строке. Я использовал эту команду GNU sed для преобразования приведенного выше вывода в программу dc:

sed -re 's/.*/c&-n[ ]np/e'

Он берет три значения в каждой строке и преобразует их в команду dc для вычитания, а затем выполняет ее. Например, первая строка становится (с добавлением пробелов для ясности)

c 1009 3 5 -n [ ]n p

который вычитает 5 из 3, печатает его, затем печатает пробел, затем печатает 1009 и новую строку, давая

-2 1009

как требуется.

Затем мы можем передать все эти строки в dc, что даст нам выходной файл, который мы хотим:

$ join -o auto -j2 -e 0 -a 1 -a 2 41144043-a 41144043-b \
>   | sed -e 's/.*/c& -n[ ]np/' \
>   | dc
-2 1009
-3 1010
7 1012
1 1013
8 1014

¹ Сортировка должна соответствовать языковому стандарту LC_COLLATE. Это вряд ли будет проблемой, если поля всегда числовые.


TL;DR

Полная команда:

join -o auto -j2 -e 0 -a 1 -a 2 "$file1" "$file2" | sed -e 's/.*/c& -n[ ]np/' | dc

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

person Toby Speight    schedule 14.12.2016

Предполагая, что это csv с пробелами, если это ",", используйте аргумент -F ','

awk 'FNR==NR {Inits[$2]=$1; ids[$2]++; next}
             {Discounts[$2]=$1; ids[$2]++}
     END     { for (id in ids) print Inits[ id] - Discounts[ id] " " id}
    ' file1.csv file2.csv

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

awk 'FNR==NR{print;next}{print -1 * $1 " " $2}' file1 file2 \
 | sort -k2 \
 > file.tmp
awk 'Last != $2 { 
        if (NR != 1) print Result " "Last
        Last = $2; Result = $1
        }
    Last == $2 { Result+= $1; next}
    END { print Result " " $2}
    ' file.tmp
rm file.tmp
person NeronLeVelu    schedule 14.12.2016
comment
Большое спасибо! Кажется, это работает, но использует довольно значительный объем памяти (убил ее на 67 ГБ). Я предполагаю, что он использует какой-то ассоциативный массив для хранения значений? Я изменил вопрос, чтобы подчеркнуть, что второй столбец отсортирован. - person user32849; 14.12.2016