web-dev-qa-db-fra.com

Choisissez des lignes au hasard dans un fichier sans le slurper avec Unix

J'ai un fichier de 10 ^ 7 lignes, dans lequel je veux choisir 1/100 de lignes au hasard dans le fichier. C'est le code AWK que j'ai, mais il absorbe tout le contenu du fichier avant la main. La mémoire de mon PC ne peut pas gérer de telles perturbations. Y a-t-il une autre approche pour le faire?

awk 'BEGIN{srand()}
!/^$/{ a[c++]=$0}
END {  
  for ( i=1;i<=c ;i++ )  { 
    num=int(Rand() * c)
    if ( a[num] ) {
        print a[num]
        delete a[num]
        d++
    }
    if ( d == c/100 ) break
  }
 }' file
51
neversaint

si vous avez autant de lignes, êtes-vous sûr de vouloir exactement 1% ou une estimation statistique serait suffisante?

Dans ce deuxième cas, il suffit de randomiser à 1% à chaque ligne ...

awk 'BEGIN {srand()} !/^$/ { if (Rand() <= .01) print $0}'

Si vous souhaitez la ligne d'en-tête plus un échantillon aléatoire de lignes après, utilisez:

awk 'BEGIN {srand()} !/^$/ { if (Rand() <= .01 || FNR==1) print $0}'
86
cadrian

Vous avez utilisé awk, mais je ne sais pas si c'est nécessaire. Si ce n'est pas le cas, voici une manière triviale de faire w/Perl (et sans charger le fichier entier en mémoire):

cat your_file.txt | Perl -n -e 'print if (Rand() < .01)'

(forme plus simple, à partir des commentaires):

Perl -ne 'print if (Rand() < .01)' your_file.txt 
53
Bill

J'ai écrit ce code exact dans Gawk - vous avez de la chance. C'est long en partie parce qu'il préserve l'ordre d'entrée. Il existe probablement des améliorations de performances qui peuvent être apportées.

Cet algorithme est correct sans connaître à l'avance la taille d'entrée. J'ai posté un pierre de rosette ici à ce sujet. (Je n'ai pas posté cette version car elle fait des comparaisons inutiles.)

Fil d'origine: Soumis pour votre avis - échantillonnage aléatoire dans awk.

# Waterman's Algorithm R for random sampling
# by way of Knuth's The Art of Computer Programming, volume 2

BEGIN {
    if (!n) {
        print "Usage: sample.awk -v n=[size]"
        exit
    }
    t = n
    srand()

}

NR <= n {
    pool[NR] = $0
    places[NR] = NR
    next

}

NR > n {
    t++
    M = int(Rand()*t) + 1
    if (M <= n) {
        READ_NEXT_RECORD(M)
    }

}

END {
    if (NR < n) {
        print "sample.awk: Not enough records for sample" \
            > "/dev/stderr"
        exit
    }
    # gawk needs a numeric sort function
    # since it doesn't have one, zero-pad and sort alphabetically
    pad = length(NR)
    for (i in pool) {
        new_index = sprintf("%0" pad "d", i)
        newpool[new_index] = pool[i]
    }
    x = asorti(newpool, ordered)
    for (i = 1; i <= x; i++)
        print newpool[ordered[i]]

}

function READ_NEXT_RECORD(idx) {
    rec = places[idx]
    delete pool[rec]
    pool[NR] = $0
    places[idx] = NR  
} 
19
Steven Huwig

Cela devrait fonctionner sur la plupart des machines GNU/Linux.

$ shuf -n $(( $(wc -l < $file) / 100)) $file

Je serais surpris si la gestion de la mémoire était mal effectuée par la commande GNU shuf.

16
ashawley

Le problème de l'échantillonnage uniforme de N éléments dans une grande population (de taille inconnue) est connu sous le nom de échantillonnage en réservoir . (Si vous aimez les problèmes d'algorithmes, passez quelques minutes à essayer de le résoudre sans lire l'algorithme sur Wikipedia.)

Une recherche sur le Web pour "échantillonnage de réservoir" trouvera beaucoup d'implémentations. Ici est Perl et Python code qui implémente ce que vous voulez, et ici est un autre thread Stack Overflow en discutant.

5
Tudor Bosman

Je ne sais pas awk , mais il existe une excellente technique pour résoudre une version plus générale du problème que vous avez décrit, et dans le cas général il est beaucoup plus rapide que l'approche pour la ligne de retour de fichier si Rand <0.01, donc cela pourrait être utile si vous avez l'intention de faire des tâches comme les nombreuses (milliers, millions) de fois. Il est connu sous le nom de échantillonnage du réservoir et cette page a une assez bonne explication d'une version de celui-ci qui est applicable à votre situation.

5
advait

Vous pouvez le faire en deux passes:

  • Parcourez le fichier une fois, juste pour compter le nombre de lignes
  • Sélectionnez au hasard les numéros de ligne des lignes que vous souhaitez imprimer, en les stockant dans une liste triée (ou un ensemble)
  • Parcourez à nouveau le fichier et sélectionnez les lignes aux positions sélectionnées

Exemple en python:

fn = '/usr/share/dict/words'

from random import randint
from sys import stdout

count = 0
with open(fn) as f:
   for line in f:
      count += 1

selected = set()
while len(selected) < count//100:
   selected.add(randint(0, count-1))

index = 0
with open(fn) as f:
   for line in f:
      if index in selected:
          stdout.write(line)
      index += 1
3
sth

Dans ce cas, l'échantillonnage du réservoir pour obtenir exactement les valeurs k est assez trivial avec awk que je suis surpris qu'aucune solution n'ait encore suggéré cela. J'ai dû résoudre le même problème et j'ai écrit le programme awk suivant pour l'échantillonnage:

NR < k {
    reservoir[NR] = $0;
}
NR >= k {
    i = int(NR * Rand());
    if (i < k) {
        reservoir[i] = $0;
    }
}
END {
    for (i in reservoir) {
        print reservoir[i];
    }
}

Ensuite, déterminer ce que k doit être fait séparément, par exemple en définissant awk -v 'k=int('$(dc -e "$(cat FILE | wc -l) 0.01 * n")')'

2
kqr

Au lieu d'attendre la fin pour choisir au hasard vos 1% de lignes, faites-le toutes les 100 lignes dans "/ ^ $ /". De cette façon, vous ne détenez que 100 lignes à la fois.

1
Travis Jensen

Si le but est juste d'éviter l'épuisement de la mémoire, et que le fichier est un fichier normal, pas besoin d'implémenter l'échantillonnage du réservoir. Le nombre de lignes dans le fichier peut être connu si vous effectuez deux passages dans le fichier, un pour obtenir le nombre de lignes (comme avec wc -l), un pour sélectionner l'échantillon:

file=/some/file
awk -v percent=0.01 -v n="$(wc -l < "$file")" '
  BEGIN {srand(); p = int(n * percent)}
  Rand() * n-- < p {p--; print}' < "$file"
1
Stephane Chazelas