Сортировать столбец по количеству одинаковых вхождений - используя awk, sort, tr или uniq?

Скажем, у меня есть данные, разделенные табуляцией:

Peter   5
Joe     8
Peter   7
Peter   8
Joe     4
Laura   3

И я хочу отсортировать его по количеству раз, когда имя встречается в первом столбце (от максимального к минимальному). Таким образом, у нас будет Питер (3 вхождения), Джо (2 вхождения) и Лаура (1 вхождение).

Peter   5
Peter   7
Peter   8
Joe     8
Joe     4
Laura   3

Его нужно отсортировать только по первому столбцу, а не по второму. Я читал документацию sort и не думаю, что у нее есть функциональность. У кого-нибудь есть простой метод?


person Chris J. Vargo    schedule 08.02.2013    source источник
comment
значит ли это, что 2-й столбец не имеет ничего общего с этим вопросом?   -  person Kent    schedule 08.02.2013
comment
правильный. Данные в этом столбце просто должны оставаться в порядке с заданным именем.   -  person Chris J. Vargo    schedule 08.02.2013
comment
Имеет ли значение, что в результирующем наборе записи Питера перечислены в порядке возрастания значений столбца 2, а записи Джо — в обратном порядке?   -  person Jonathan Leffler    schedule 08.02.2013
comment
@JonathanLeffler: Нет - OP хочет сортировать по убыванию при появлении первого столбца, сохраняя порядок второго столбца, как они появляются в файле. Предположительно, порядок повторяющихся вхождений (столбца один) не имеет значения, пока столбец два поддерживает порядок с заданным именем. См. комментарий ОП выше. ХТН.   -  person Steve    schedule 08.02.2013
comment
Для вашего комментария к решению Стива было бы полезно упомянуть размер файла в вопросе. Возможно, есть какое-то ограничение на количество данных, которые можно отсортировать в массиве a? Какой диапазон у вас на спичках? Максимум три? 50 макс? Какие?   -  person Bill Woodger    schedule 08.02.2013


Ответы (5)


не сексуально, но работает для вашего примера:

 awk  'NR==FNR{a[$1]++;next}{ print a[$1],$0}' file file|sort -nr|sed -r 's/[0-9]* //'

тест с вашими данными:

kent$  cat n.txt
Peter   5
Joe     8
Peter   7
Peter   8
Joe     4
Laura   3

kent$  awk  'NR==FNR{a[$1]++;next}{ print a[$1],$0}' n.txt n.txt|sort -nr|sed -r 's/[0-9]* //'
Peter   8
Peter   7
Peter   5
Joe     8
Joe     4
Laura   3
person Kent    schedule 08.02.2013
comment
Данные (поле 2) должны оставаться в порядке, как указано в комментариях под вопросом. - person Steve; 08.02.2013
comment
Опция -r не нужна; используемое регулярное выражение является базовым sed regex. - person Jonathan Leffler; 08.02.2013
comment
@ChrisJ.Vargo, да, вы можете просто удалить опцию '-r'. тоже должно работать. - person Kent; 08.02.2013
comment
Ух ты. Я только что запустил это на своем BSD-боксе, и это работает блестяще. Более того, очень эффективно для списка из 38 миллионов строк. Кент, спасибо большое! - person Chris J. Vargo; 08.02.2013
comment
@ChrisJ.Vargo рад помочь. он работал быстро, потому что у вас был мощный процессор. :) в моих кодах нет алгоритма взлома - person Kent; 08.02.2013

Это работает:

for person in $(awk '{print $1}' file.txt | sort | uniq -c | sort -dnr | awk '{print $2}');
do grep -e "^$person[[:space:]]" file.txt;
done
person flodel    schedule 08.02.2013
comment
Итак, чтобы упростить пример данных, я не упомянул, что имена из $1 иногда упоминаются в $2. Этого можно избежать в вашем коде, добавив /t после $person. Как вы думаете: grep '$person\| ' должно сработать? - person Chris J. Vargo; 08.02.2013

Вот один из способов использования GNU awk. Беги как:

awk -f script.awk file

Содержание script.awk:

BEGIN {
    FS="\t"
}

{
    c[$1]++
    r[$1] = (r[$1] ? r[$1] ORS : "") $0
}

END {

    for (i in c) {
        a[c[i],i] = i
    }

    n = asorti(a)

    for (i=1;i<=n;i++) {
        split(a[i], b, SUBSEP)
        x[++j] = b[2]
    }

    for (i=n;i>=1;i--) {
        print r[x[i]]
    }
}

Результаты:

Peter   5
Peter   7
Peter   8
Joe     8
Joe     4
Laura   3
person Steve    schedule 08.02.2013
comment
Ух ты. Стив, это второй мой вопрос, на который ты ответил за последние несколько дней. Какие ресурсы вы рекомендуете для изучения сценариев, подобных этому? К вашему сведению: я запускаю это в большом текстовом файле. Я получаю эту ошибку: awk: вызов неопределенной функции asorti номер входной записи 38823587, номер исходной строки файла input.txt 16 - person Chris J. Vargo; 08.02.2013
comment
@ChrisJ.Vargo: Похоже, мне следовало изучить некоторые из ваших предыдущих вопросов. Из того, что я вижу, похоже, что вы используете BSD/OSX awk, который, к сожалению, не поддерживает функции сортировки массива (asort и asorti), доступные при использовании GNU awk. Я настоятельно рекомендую установить и использовать утилиты GNU; конкретные GNU awk и GNU sed - вы только сэкономите себе много времени и хлопот в будущем. Ресурсы: SO, вероятно, лучшее место для изучения разработки алгоритмов с использованием awk. ТБХ, лучшего ресурса я не встречал. Учитесь, делая. ХТН. - person Steve; 08.02.2013
comment
Большое спасибо. Я сделаю это. - person Chris J. Vargo; 08.02.2013

Это удивительно сложный критерий сортировки. Этот код работает, но он довольно уродлив:

data=${1:-data}
awk '{ print $1 }' $data |
sort |
uniq -c |
sort -k2 |
join -1 2 -2 2 -o 1.1,2.1,2.2,2.3 - <(awk '{ print NR, $0 }' $data | sort -k2) |
sort -k1,1nr -k3,3 -k2n |
awk 'BEGIN{OFS="\t"} { print $3, $4 }'

Он предполагает bash 4.x для «подстановки процесса», но не использует встроенную в awk сортировку (это расширение GNU по сравнению с POSIX awk). С явным временным файлом его можно заставить работать в оболочках без замены процесса.

data=${1:-data}                  # File named on command line, or uses name 'data'
awk '{ print $1 }' $data |       # List of names
sort |                           # Sorted list of names
uniq -c |                        # Count occurrences of each name
sort -k2 |                       # Sort in name order
join -1 2 -2 2 -o 1.1,2.1,2.2,2.3 - <(awk '{ print NR, $0 }' $data | sort -k2) |
# The process substitution numbers each record in sequence and sorts in name order
# The join matches the names (column 2) and outputs the frequency, record number, name, value
sort -k1,1nr -k3,3 -k2n |        # Sort on frequency reversed, name, original line number
awk 'BEGIN{OFS="\t"} { print $3, $4 }'   # Print name and value

Использование GNU awk со встроенной сортировкой, Perl или Python, вероятно, лучше, чем это.

Для исходных данных вывод:

Peter   5
Peter   7
Peter   8
Joe     8
Joe     4
Laura   3

Учитывая эту расширенную версию данных:

Peter   5
Joe     8
Peter   7
Peter   8
Joe     4
Laura   3
Peter   50
Joe     80
Peter   70
Peter   80
Joe     40
Laura   30
Peter   700
Peter   800
Peter   7002
Peter   8002
Peter   7000
Peter   8000
Peter   7001
Peter   8001
Pater   50
Jae     80
Pater   70
Pater   80
Jae     40
Laura   30

Результат:

Peter   5
Peter   7
Peter   8
Peter   50
Peter   70
Peter   80
Peter   700
Peter   800
Peter   7002
Peter   8002
Peter   7000
Peter   8000
Peter   7001
Peter   8001
Joe     8
Joe     4
Joe     80
Joe     40
Laura   3
Laura   30
Laura   30
Pater   50
Pater   70
Pater   80
Jae     80
Jae     40

Для этого набора данных необходим термин сортировки -k3,3; он сортирует записи Лауры перед записями Патера (если он опущен, эти два списка чередуются).

person Jonathan Leffler    schedule 08.02.2013

Вот еще один, использующий awk:

awk '{a[$1, ++b[$1]]=$0; если(b[$1]›max) max=b[$1] }

   END{ for(x=max;x>=1;x--)
         for( k in b )
           if( a[k,x] )
              for(y=1;y<=x;y++) {
                    print a[k,y]
                    delete a[k,y]
               }
   }' filename

Он отлично работает с gawk и POSIX awk. Наличие трех циклов в операторе END может повлиять на производительность при работе с большими файлами.

person lind    schedule 08.02.2013