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
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}'
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
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
}
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.
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.
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.
Vous pouvez le faire en deux passes:
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
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")')'
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.
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"