unix сортировать группы по их максимальному значению?

Допустим, у меня есть этот входной файл 49142202.txt:

A   5
B   6
C   3
A   4
B   2
C   1

Можно ли отсортировать группы в столбце 1 по значению в столбце 2? Желаемый результат выглядит следующим образом:

B   6 <-- B group at the top, because 6 is larger than 5 and 3
B   2 <-- 2 less than 6
A   5 <-- A group in the middle, because 5 is smaller than 6 and larger than 3
A   4 <-- 4 less than 5
C   3 <-- C group at the bottom, because 3 is smaller than 6 and 5
C   1 <-- 1 less than 3

Вот мое решение:

join -t$'\t' -1 2 -2 1 \
 <(cat 49142202.txt | sort -k2nr,2 | sort --stable -k1,1 -u | sort -k2nr,2 \
  | cut -f1 | nl | tr -d " " | sort -k2,2) \
 <(cat 49142202.txt | sort -k1,1 -k2nr,2) \
| sort --stable -k2n,2 | cut -f1,3

Первый вход в join отсортировано по столбцу 2 это:

2   A
1   B
3   C

Второй вход в join отсортировано по столбцу 1 это:

A   5
A   4
B   6
B   2
C   3
C   1

Выход из join является:

A   2   5
A   2   4
B   1   6
B   1   2
C   3   3
C   3   1

Который затем сортируется по nl номер строки в столбце 2, а затем исходные столбцы ввода 1 и 3 сохраняются с cut,

Я знаю, что это может быть сделано намного проще, например, с groupby панд Python, но есть ли более элегантный способ сделать это, придерживаясь при этом использования GNU Coreutils, таких как sort, join, cut, tr а также nl? Желательно, чтобы избежать неэффективной памяти awk решение, но, пожалуйста, поделитесь ими. Спасибо!

3 ответа

Как поясняется в комментарии, мое решение пытается уменьшить количество pipesненужный cat Команды и особенно количество конвейеров sort Операции, поскольку сортировка является сложной / трудоемкой операцией:

Я пришел к следующему решению, где f_grp_sort это входной файл:

for elem in $(sort -k2nr f_grp_sort | awk '!seen[$1]++{print $1}')
do 
   grep $elem <(sort -k2nr f_grp_sort) 
done

ВЫХОД:

B       6
B       2
A       5
A       4
C       3
C       1

Пояснения:

sort -k2nr f_grp_sort сгенерирует следующий вывод:

B       6
A       5
A       4
C       3
B       2
C       1

а также sort -k2nr f_grp_sort | awk '!seen[$1]++{print $1}' сгенерирует вывод:

B
A
C

awk просто сгенерирует в том же порядке 1 уникальный элемент первого столбца временного вывода.

Тогда for elem in $(...)do grep $elem <(sort -k2nr f_grp_sort); doneбудут grep для строк, содержащих B затем A, затем C что обеспечит требуемый вывод.

Теперь в качестве улучшения вы можете использовать временный файл, чтобы избежать sort -k2nr f_grp_sort операция дважды:

$ sort -k2nr f_grp_sort > tmp_sorted_file && for elem in $(awk '!seen[$1]++{print $1}' tmp_sorted_file); do grep $elem tmp_sorted_file; done && rm tmp_sorted_file

Большое спасибо @JeffBreadner и @Allan! Я придумал еще одно решение, которое очень похоже на мое первое, но дает немного больше контроля, поскольку позволяет упростить вложение с циклами for:

for x in $(sort -k2nr,2 $file | sort --stable -k1,1 -u | sort -k2nr,2 | cut -f1); do
 awk -v x=$x '$1==x' $file | sort -k2nr,2
done

Вы не возражаете, если я не приму ни один из ваших ответов, пока у меня не будет времени оценить время и производительность памяти ваших решений? В противном случае я бы, вероятно, просто пойти на awk решение от @Allan.

Таким образом, это не будет работать для всех случаев, но если значения в вашем первом столбце можно превратить в переменные bash, мы можем использовать динамически именованные массивы, чтобы сделать это вместо набора соединений. Это должно быть довольно быстро.

Первый блок while читает содержимое файла, получает первые две строки, разделенные пробелом, и помещает их в col1 а также col2, Затем мы создаем серию массивов с именем как ARR_A а также ARR_B где A а также B значения из столбца 1 (но только если $col1 содержит только символы, которые можно использовать в именах переменных bash). Массив содержит значения столбца 2, связанные с этими значениями столбца 1.

Я использую вашу необычную цепочку сортировки, чтобы получить порядок, в котором мы хотим вывести значения столбца 1, мы просто перебираем их, затем для каждого массива столбца 1 сортируем значения и выводим столбец 1 и столбец 2.

За битами динамической переменной может быть сложно следить, но для правильных значений в столбце 1 это будет работать. Опять же, если в столбце 1 есть какие-либо символы, которые не могут быть частью имени переменной bash, это решение не будет работать.

file=./49142202.txt

while read col1 col2 extra
do
  if [[ "$col1" =~ ^[a-zA-Z0-9_]+$ ]]
  then
    eval 'ARR_'${col1}'+=("'${col2}'")'
  else
    echo "Bad character detected in Column 1:  '$col1'"
    exit 1
  fi
done < "$file"

sort -k2nr,2 "$file" | sort --stable -k1,1 -u | sort -k2nr,2 | while read col1 extra
do 
  for col2 in $(eval 'printf "%s\n" "${ARR_'${col1}'[@]}"' | sort -r)
  do
    echo $col1 $col2
  done
done 

Это был мой тест, немного более сложный, чем приведенный вами пример:

$ cat 49142202.txt
A 4
B 6
C 3
A 5
B 2
C 1
C 0

$ ./run
B 6
B 2
A 5
A 4
C 3
C 1
C 0
Другие вопросы по тегам