Daten schneller sortieren

11

Ich muss eine bedDatei 10000 Mal zufällig sortieren und jedes Mal die obersten 1000 Zeilen nehmen. Derzeit verwende ich den folgenden Code:

for i in {1..100}; do
    for j in {1..100}; do
        sort -R myfile.bed_sorted | tail -n 1000 > myfile.bed.$i.$j.bed
    done
done

Dies dauert für jede Datei fast 6 Stunden. Ich habe ungefähr 150 davon zu erarbeiten. Gibt es dafür eine schnellere Lösung?

Ein Beispiel der Daten (myfile.bed_sorted), die ich habe:

    chr1    111763899   111766405   peak1424    1000    .   3224.030    -1  -1
    chr1    144533459   144534584   peak1537    998 .   3219.260    -1  -1
    chr8    42149384    42151246    peak30658   998 .   3217.620    -1  -1
    chr2    70369299    70370655    peak16886   996 .   3211.600    -1  -1
    chr8    11348914    11352994    peak30334   990 .   3194.180    -1  -1
    chr21   26828820    26830352    peak19503   988 .   3187.820    -1  -1
    chr16   68789901    68791150    peak11894   988 .   3187.360    -1  -1
    chr6    11458964    11462245    peak26362   983 .   3169.750    -1  -1
    chr1    235113793   235117308   peak2894    982 .   3166.000    -1  -1
    chr6    16419968    16422194    peak26522   979 .   3158.520    -1  -1
    chr6    315344  321339  peak26159   978 .   3156.320    -1  -1
    chr1    111756584   111759633   peak1421    964 .   3110.520    -1  -1
    chrX    12995098    12997685    peak33121   961 .   3100.000    -1  -1
    chr9    37408601    37410262    peak32066   961 .   3100.000    -1  -1
    chr9    132648603   132651523   peak32810   961 .   3100.000    -1  -1
    chr8    146103178   146104943   peak31706   961 .   3100.000    -1  -1
    chr8    135611963   135614649   peak31592   961 .   3100.000    -1  -1
    chr8    128312253   128315935   peak31469   961 .   3100.000    -1  -1
    chr8    128221486   128223644   peak31465   961 .   3100.000    -1  -1
    chr8    101510621   101514237   peak31185   961 .   3100.000    -1  -1
    chr8    101504210   101508005   peak31184   961 .   3100.000    -1  -1
    chr7    8173062 8174642 peak28743   961 .   3100.000    -1  -1
    chr7    5563424 5570618 peak28669   961 .   3100.000    -1  -1
    chr7    55600455    55603724    peak29192   961 .   3100.000    -1  -1
    chr7    35767878    35770820    peak28976   961 .   3100.000    -1  -1
    chr7    28518260    28519837    peak28923   961 .   3100.000    -1  -1
    chr7    104652502   104654747   peak29684   961 .   3100.000    -1  -1
    chr6    6586316 6590136 peak26279   961 .   3100.000    -1  -1
    chr6    52362185    52364270    peak27366   961 .   3100.000    -1  -1
    chr6    407805  413348  peak26180   961 .   3100.000    -1  -1
    chr6    32936987    32941352    peak26978   961 .   3100.000    -1  -1
    chr6    226477  229964  peak26144   961 .   3100.000    -1  -1
    chr6    157017923   157020836   peak28371   961 .   3100.000    -1  -1
    chr6    137422769   137425128   peak28064   961 .   3100.000    -1  -1
    chr5    149789084   149793727   peak25705   961 .   3100.000    -1  -1
    chr5    149778033   149783125   peak25702   961 .   3100.000    -1  -1
    chr5    149183766   149185906   peak25695   961 .   3100.000    -1  -1
Biobudhan
quelle
1
Wie groß ist Ihre Datei und wie streng ist Ihre Vorstellung von "zufällig"? splitSie können eine Datei in Teile von jeweils 1000 Zeilen aufteilen, sodass Sie mit einem einzigen Aufruf von mehr Dateien erhalten sort. Haben Sie auch überprüft, ob heades etwas schneller ist als tailweil es nicht die gesamte Datei lesen muss?
Ulrich Schwarz
@UlrichSchwarz: Die Beispieldatei, die ich oben eingefügt habe, enthält ungefähr 33000 Zeilen. Im Allgemeinen haben alle meine Bettdateien mehr oder weniger die gleiche Anzahl von Zeilen. Zum Beispiel: Aus einer 33000-Zeilendatei möchte ich nicht 33 Teilmengen (jeweils 1000 Zeilen) in einem einzigen Lauf erhalten. Ich möchte nur die obersten 1000 Zeilen aus jedem Lauf nehmen. Ich werde auch einen Schwanz der gleichen Datei machen. Nur zum Beispiel habe ich headhier verwendet.
Biobudhan
Laut Manpage sort -Rwird ein "zufälliger Hash von Schlüsseln" verwendet. Das Erstellen des Hashs ist reine Zeitverschwendung und dauert wahrscheinlich länger als alles andere. Es wäre besser, die Zeilen in ein Array einzulesen und diese dann mithilfe von Indizes zu mischen. Persönlich würde ich dafür verwenden perl; Sie könnten es tun, bashaber Sie benötigen eine Funktion, um Zufallszahlen zu generieren.
Goldlöckchen
@ Goldlöckchen: Ich bin keine perlPerson! Könnten Sie mir bitte helfen?
Biobudhan
6
Versuchen Sie shufstattdessen sort -R, es ist erheblich schneller. Wenn Sie dies im Speicher tun (siehe Perl-Antwort), wird natürlich alles übertroffen, was ein erneutes Lesen der gesamten Datei in der Shell erfordert.
Frostschutz

Antworten:

14

Angenommen, Sie haben genügend Speicher, um die Datei zu schlürfen, können Sie es versuchen

perl -e 'use List::Util 'shuffle'; @k=shuffle(<>); print @k[0..999]' file.bed

Da Sie dies 10000 Mal tun möchten, würde ich empfehlen, die Wiederholung in das Skript zu integrieren und die Indizes anstelle des Arrays selbst zu mischen, um die Dinge zu beschleunigen:

$ time perl -e 'use List::Util 'shuffle'; 
            @l=<>; for $i (1..10000){
               open(my $fh, ">","file.$i.bed"); 
               @r=shuffle(0..$#l); 
               print $fh @l[@r[0..999]]
            }' file.bed

real    1m12.444s
user    1m8.536s
sys     0m3.244s

Die oben genannten Dateien erstellten 10000 Dateien mit jeweils 1000 Zeilen aus einer Datei, die 37000 Zeilen enthielt (Ihre Beispieldatei wurde 1000 Mal wiederholt). Wie Sie sehen, hat es auf meinem System etwas mehr als drei Minuten gedauert.

Erläuterung

  • use List::Util 'shuffle';: Dies importiert ein Perl-Modul, das die shuffle()Funktion zum Randomisieren eines Arrays bereitstellt .
  • @l=<>;: Laden Sie die Eingabedatei ( <>) in das Array @l.
  • for $i (1..10000){} : Führen Sie dies 10000 Mal aus.
  • @r=shuffle(0..$#l);: $#list die Anzahl der Elemente in, @lso @rist jetzt eine zufällige Liste der Indexnummern des Arrays @l(die Zeilen der Eingabedatei).
  • open(my $fh, ">","file.$i.bed");: Öffnen Sie eine Datei, die file.$i.bedzum Schreiben aufgerufen wird . $inimmt Werte von 1 bis 10000 an.
  • print $fh @l[@r[0..999]]: Nehmen Sie die ersten 1000 Indizes im gemischten Array und drucken Sie die entsprechenden Zeilen (Elemente von @l).

Ein anderer Ansatz ist zu verwenden shuf( danke @frostschutz ):

$ time for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.abed; done

real    1m9.743s
user    0m23.732s
sys     0m31.764s
terdon
quelle
Beeindruckend!! Das ist genial!! Es hat in 2 Minuten funktioniert :-) Ich habe nur noch eine Frage. Wie wäre es auch mit dem Abrufen der letzten 1000 Zeilen der Datei? Weil wir die Länge (Anzahl der Zeilen) in der Datei kennen müssen, um dies zu erreichen? Bitte helfen Sie!
Biobudhan
1
@biobudhan berücksichtigen, shufwie von Frostschutz vorgeschlagen : for i in {1..10000}; do shuf -n 1000 file.bed > file.$i.bed; done. Das hat auf meinem System ca. 1 Minute gedauert. Für die letzten 1000 Zeilen brauchen Sie nur tail -n 1000.
Terdon
1
@biobudhan sehen auch aktualisierte Antwort für eine 3x schnellere Perl-Version.
Terdon
Ja, ich habe es versucht und es funktioniert jetzt schneller !! Vielen Dank!!! :-)
Biobudhan
Haben Sie die Ausgabedateien der Perl-Version überprüft? Es scheint mir seltsam, dass es so wenig sysZeit hat, was Datei-E / A wäre - dies sollte nicht so völlig anders sein als das shuf, das ~ 30s hat sys. Also habe ich die Perl hier getestet (Ausschneiden und Einfügen) und O_O hat 1000 Dateien erstellt, aber alle Dateien waren leer ...
Goldlöckchen
9

Wenn Sie möchten, dass ein Benchmark sieht, wie schnell dies möglich ist, kopieren Sie ihn, fügen Sie ihn ein 10kshuffle.cppund kompilieren Sie ihn g++ 10kshuffle.cpp -o 10kshuffle. Sie können es dann ausführen:

10kshuffle filename < inputfile

Wo filenameist ein Basispfad für die Ausgabedateien zu verwenden? sie werden genannt werden filename.0, filename.1usw. , und jeder enthält die ersten 1000 Zeilen eines shuffle. Es schreibt den Namen jeder Datei, wie es geht.

#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <string>
#include <sstream>
#include <unistd.h>
#include <vector>

using namespace std;

unsigned int randomSeed () {
    int in = open("/dev/urandom", O_RDONLY);
    if (!in) {
        cerr << strerror(errno);
        exit(1);
    }
    unsigned int x;
    read(in, &x, sizeof(x));
    close(in);
    return x;
}

int main (int argc, const char *argv[]) {
    char basepath[1024];
    strcpy(basepath,argv[1]);
    char *pathend = &basepath[strlen(basepath)];
// Read in.
    vector<char*> data;
    data.reserve(1<<16);
    while (!cin.eof()) {
        char *buf = new char[1024];
        cin.getline(buf,1023);
        data.push_back(buf);
    }

    srand(randomSeed());
    for (int n = 0; n < 10000; n++) {
        vector<char*> copy(data);
    // Fisher-Yates shuffle.
        int last = copy.size() - 1;
        for (int i = last; i > 0; i--) {
            int r = rand() % i;
            if (r == i) continue;
            char *t = copy[i];
            copy[i] = copy[r];
            copy[r] = t;
        }
    // Write out.
        sprintf(pathend, ".%d", n);
        ofstream file(basepath);
        for (int j = 0; j < 1000; j++) file << copy[j] << endl;
        cout << basepath << endl;
        file.close();
    }

    return 0;
}  

Auf einem einzelnen 3,5-GHz-Kern läuft dies in ~ 20 Sekunden:

   time ./10kshuffle tmp/test < data.txt
   tmp/test.0
   [...]
   tmp/test.9999
   real 19.95, user 9.46, sys 9.86, RSS 39408

data.txtEs wurden 37000 Zeilen aus der Frage dupliziert. Wenn Sie anstelle der ersten 1000 Zeilen die gesamte Zufallswiedergabe in der Ausgabedatei wünschen, ändern Sie Zeile 54 in:

for (int j = 0; j < copy.size(); j++) file << copy[j] << endl; 
Goldlöckchen
quelle
3

Ihre Frage hat also einen Unix-Aspekt, aber es lohnt sich, zuerst Ihr grundlegendes Problem zu lösen und dann nach einem Unix-y-Weg zu suchen, um diese Lösung zu implementieren.

Sie müssen 10.000 Beispiele mit einer Größe von jeweils 1.000 aus einer Datei mit einer unbekannten, großen Anzahl von Zeilen erstellen. Dies ist in einem einzigen Durchgang der Datei möglich, wenn Sie 10.000 x 1.000 Zeilen im Speicher halten können. Wenn Sie nicht so viele Zeilen im Speicher halten können, können Sie dies trotzdem in einem einzigen Durchgang tun, wenn Sie wissen, wie viele Zeilen Ihre Datei enthält. Wenn Sie nicht wissen, wie viele Zeilen Ihre Datei enthält, benötigen Sie einen zusätzlichen Durchgang, um die Anzahl der Zeilen zu zählen.

In dem schwierigeren Fall, wenn Sie die Anzahl der Zeilen nicht kennen, führt der Algorithmus für jedes Sample Folgendes aus (parallel, wobei die Samples im Speicher bleiben):

  • Nehmen Sie die ersten 1.000 Zeilen in die Stichprobe auf
  • Geben Sie für die n-te Zeile (wo n > 1000) die Wahrscheinlichkeit an 1000 / nund verwerfen Sie eine zufällige Zeile aus den bereits ausgewählten Zeilen. (Aufgrund der Wahrscheinlichkeit, dass einige Zeilen verworfen werden, müssen wir das Sample bis zum Ende der Eingabe im Speicher halten.)

Eine elegante Möglichkeit, den zweiten Schritt zu implementieren, besteht darin, eine zufällige Ganzzahl kin zu generieren [1, n]. Wenn k <= 1000dann die Zeile einschließen und die vorhandene k-te Zeile durch diese ersetzen . Hier ist eine Standardbeschreibung des Algorithmus: http://en.wikipedia.org/wiki/Reservoir_sampling

Wenn Sie die Anzahl der Zeilen kennen R, dann:

  • Beginnen Sie mit einer Stichprobengröße svon 0
  • n-te Zeile mit Wahrscheinlichkeit einschließen (1000 - s) / (R - n + 1)und sofort ausgeben (und Stichprobengröße erhöhen s)

Wie geht das unter Unix? awkscheint die Antwort für diesen Beitrag im Internet zu sein (ich kann nicht für die Richtigkeit bürgen, aber der Code ist da) https://news.ycombinator.com/item?id=4840043

Nekromant
quelle