Obtener el recuento de valores únicos en una columna en bash

95

Tengo archivos delimitados por tabulaciones con varias columnas. Quiero contar la frecuencia de ocurrencia de los diferentes valores en una columna para todos los archivos en una carpeta y ordenarlos en orden decreciente de conteo (el conteo más alto primero). ¿Cómo podría lograr esto en un entorno de línea de comandos de Linux?

Puede usar cualquier lenguaje de línea de comandos común como awk, perl, python, etc.

factor
fuente

Respuestas:

152

Para ver un recuento de frecuencia para la columna dos (por ejemplo):

awk -F '\t' '{print $2}' * | sort | uniq -c | sort -nr

fileA.txt

z    z    a
a    b    c
w    d    e

fileB.txt

t    r    e
z    d    a
a    g    c

fileC.txt

z    r    a
v    d    c
a    m    c

Resultado:

  3 d
  2 r
  1 z
  1 m
  1 g
  1 b
Pausado hasta nuevo aviso.
fuente
68

Aquí hay una forma de hacerlo en el shell:

FIELD=2
cut -f $FIELD * | sort| uniq -c |sort -nr

Este es el tipo de cosas en las que bash es genial.

Thedward
fuente
22
El "tipo" de cosas ... ar ar ar! :)
John Rix
3
Una especie de cosita única. : P (por cierto, se usa -d,para delimitar campos por comas o cualquier otro delimitador).
cprn
4
Yo usé cut -f 1 -d ' '. Muchas gracias. :)
Alfonso Nishikawa
8

El sitio GNU sugiere este agradable script awk, que imprime tanto las palabras como su frecuencia.

Posibles cambios:

  • Puede canalizar sort -nr(e invertir wordy freq[word]) para ver el resultado en orden descendente.
  • Si desea una columna específica, puede omitir el bucle for y simplemente escribir freq[3]++: reemplace 3 con el número de columna.

Aquí va:

 # wordfreq.awk --- print list of word frequencies

 {
     $0 = tolower($0)    # remove case distinctions
     # remove punctuation
     gsub(/[^[:alnum:]_[:blank:]]/, "", $0)
     for (i = 1; i <= NF; i++)
         freq[$i]++
 }

 END {
     for (word in freq)
         printf "%s\t%d\n", word, freq[word]
 }
Adam Matan
fuente
2
Gran guión de ejemplo. Demuestra gran parte de la capacidad de awk.
David Mann
Esta secuencia de comandos fue útil para mí para determinar a qué filas en un libro de Excel realmente necesitaba prestar atención :) (copié el contenido de Excel en un archivo de texto, use awk y, ¡listo !, puedo hacer un archivo de patrón para grep -n) .
Jubbles
6

Perl

Este código calcula las ocurrencias de todas las columnas e imprime un informe ordenado para cada una de ellas:

# columnvalues.pl
while (<>) {
    @Fields = split /\s+/;
    for $i ( 0 .. $#Fields ) {
        $result[$i]{$Fields[$i]}++
    };
}
for $j ( 0 .. $#result ) {
    print "column $j:\n";
    @values = keys %{$result[$j]};
    @sorted = sort { $result[$j]{$b} <=> $result[$j]{$a}  ||  $a cmp $b } @values;
    for $k ( @sorted ) {
        print " $k $result[$j]{$k}\n"
    }
}

Guarde el texto como columnvalues.pl
Ejecútelo como: perl columnvalues.pl files*

Explicación

En el bucle while de nivel superior:
* Recorre cada línea de los archivos de entrada combinados
* Divide la línea en la matriz @Fields
* Para cada columna, incrementa la estructura de datos de matriz de hashes de resultado

En el bucle for de nivel superior:
* Recorrer la matriz de resultados
* Imprimir el número de columna
* Obtener los valores utilizados en esa columna
* Ordenar los valores por el número de ocurrencias
* Orden secundario basado en el valor (por ejemplo, b vs g vs m vs z)
* Itere a través del hash del resultado, usando la lista ordenada
* Imprima el valor y el número de cada ocurrencia

Resultados basados ​​en los archivos de entrada de muestra proporcionados por @Dennis

column 0:
 a 3
 z 3
 t 1
 v 1
 w 1
column 1:
 d 3
 r 2
 b 1
 g 1
 m 1
 z 1
column 2:
 c 4
 a 3
 e 2

.csv entrada

Si sus archivos de entrada son .csv, cambie /\s+/a/,/

Ofuscación

En un feo concurso, Perl está particularmente bien equipado.
Este one-liner hace lo mismo:

perl -lane 'for $i (0..$#F){$g[$i]{$F[$i]}++};END{for $j (0..$#g){print "$j:";for $k (sort{$g[$j]{$b}<=>$g[$j]{$a}||$a cmp $b} keys %{$g[$j]}){print " $k $g[$j]{$k}"}}}' files*
Chris Koknat
fuente
2

Rubí (1.9+)

#!/usr/bin/env ruby
Dir["*"].each do |file|
    h=Hash.new(0)
    open(file).each do |row|
        row.chomp.split("\t").each do |w|
            h[ w ] += 1
        end
    end
    h.sort{|a,b| b[1]<=>a[1] }.each{|x,y| print "#{x}:#{y}\n" }
end
Kurumi
fuente
5
Esto es muy interesante, tanto porque lo usé y funcionó, como también porque estoy asombrado de lo feo que es el rubí ... ¡Pensé que Perl era malo!
ryansstack
En defensa de Ruby, esto podría realmente arreglarse. Por ejemplo, usando each_with_object, entre otras cosas. En resumen, esto está escrito de manera algo grosera.
Rambatino