Obtenir le nombre de valeurs uniques dans une colonne dans bash


95

J'ai des fichiers délimités par des tabulations avec plusieurs colonnes. Je veux compter la fréquence d'apparition des différentes valeurs dans une colonne pour tous les fichiers d'un dossier et les trier par ordre décroissant de comptage (le plus grand nombre en premier). Comment puis-je accomplir cela dans un environnement de ligne de commande Linux?

Il peut utiliser n'importe quel langage de ligne de commande courant comme awk, perl, python, etc.

Réponses:


152

Pour afficher un nombre de fréquences pour la deuxième colonne (par exemple):

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

Résultat:

  3 d
  2 r
  1 z
  1 m
  1 g
  1 b

68

Voici un moyen de le faire dans le shell:

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

C'est le genre de chose pour laquelle bash est génial.


22
Le "genre" de chose ... ar ar ar! :)
John Rix

3
Un peu un truc unique. : P (btw. Utiliser -d,pour délimiter les champs par une virgule ou tout autre délimiteur).
cprn le

4
J'ai utilisé cut -f 1 -d ' '. Merci beaucoup. :)
Alfonso Nishikawa

8

Le site GNU suggère ce joli script awk, qui imprime à la fois les mots et leur fréquence.

Changements possibles:

  • Vous pouvez passer par sort -nr(et inverser wordet freq[word]) pour voir le résultat dans l'ordre décroissant.
  • Si vous voulez une colonne spécifique, vous pouvez omettre la boucle for et simplement écrire freq[3]++- remplacer 3 par le numéro de colonne.

Voici:

 # 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]
 }

2
Excellent exemple de script. Cela démontre tellement la capacité de awk.
David Mann

Ce script m'a été utile pour déterminer les lignes d'un classeur Excel auxquelles je devais vraiment prêter attention :) (contenu Excel copié dans un fichier texte, utilisez awk et, voila !, je peux créer un fichier de modèle pour grep -n) .
Jubbles

6

Perl

Ce code calcule les occurrences de toutes les colonnes et imprime un rapport trié pour chacune d'elles:

# 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"
    }
}

Enregistrez le texte sous columnvalues.pl
Exécutez-le comme: perl columnvalues.pl files*

Explication

Dans la boucle while de niveau supérieur:
* Boucle sur chaque ligne des fichiers d'entrée combinés
* Fractionne la ligne dans le tableau @Fields
* Pour chaque colonne, incrémente la structure de données résultat du tableau de hachages

Dans la boucle for de niveau supérieur:
* Boucle sur le tableau de résultats
* Imprimer le numéro de colonne
* Obtenir les valeurs utilisées dans cette colonne
* Trier les valeurs par le nombre d'occurrences
* Tri secondaire basé sur la valeur (par exemple b vs g vs m vs z)
* Itérer à travers le hachage du résultat, en utilisant la liste triée
* Imprimer la valeur et le numéro de chaque occurrence

Résultats basés sur les exemples de fichiers d'entrée fournis par @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

entrée .csv

Si vos fichiers d'entrée sont .csv, passez /\s+/à/,/

Obfuscation

Dans un concours moche, Perl est particulièrement bien équipé.
Ce one-liner fait la même chose:

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*

2

Rubis (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

5
C'est très intéressant, à la fois parce que je l'ai utilisé et cela a fonctionné, et aussi parce que je suis juste étonné de voir à quel point le rubis est moche ... Je pensais que perl était mauvais!
ryansstack

Pour la défense de Ruby, cela pourrait être vraiment amélioré. Par exemple, en utilisant each_with_object, entre autres. En bref, c'est un peu grossièrement écrit.
Rambatino
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.